hashicorp/packer · error

scanner binary is not executable at %s after chmod; check th

Error message

scanner binary is not executable at %s after chmod; check that /tmp is not mounted noexec on the remote host

What it means

This static error is returned when the post-chmod verification (`test -x`) fails on the remote Unix host, meaning the scanner binary at /tmp/packer-sbom-runner is still not executable even though `chmod +x` itself reported success. It is thrown at provisioner/hcp-sbom/provisioner.go:551 in uploadScanner. It exists to give an actionable hint: the usual cause is /tmp mounted with the noexec option, so any binary there cannot be executed regardless of permission bits.

Source

Thrown at provisioner/hcp-sbom/provisioner.go:551

		}

		// Step 2: upload binary directly to remote.
		localFile := bytes.NewReader(binaryData)
		log.Printf("[INFO] Uploading Packer binary to %s...", remotePath)
		if err := comm.Upload(remotePath, localFile, nil); err != nil {
			return "", fmt.Errorf("failed to upload Packer binary: %s", err)
		}

		// Step 3: make it executable.
		chmodCmd := fmt.Sprintf(`chmod +x "%s"`, remotePath)
		if err := p.runRemoteCmd(ctx, comm, chmodCmd, "chmod scanner binary"); err != nil {
			return "", err
		}

		// Final verify: confirm binary is executable.
		verifyCmd := fmt.Sprintf(`test -x "%s"`, remotePath)
		if err := p.runRemoteCmd(ctx, comm, verifyCmd, "verify scanner is executable"); err != nil {
			return "", fmt.Errorf("scanner binary is not executable at %s after chmod; "+
				"check that /tmp is not mounted noexec on the remote host", remotePath)
		}
	}

	return remotePath, nil
}

func extractBinaryFromZip(zipPath, binaryName string) ([]byte, error) {
	zr, err := zip.OpenReader(zipPath)
	if err != nil {
		return nil, fmt.Errorf("failed to open zip: %w", err)
	}
	defer func() { _ = zr.Close() }()

	for _, f := range zr.File {
		if f.Name != binaryName {
			continue
		}

View on GitHub (pinned to eb36e3c3e4)

Solutions

  1. Check the remote mount flags: `findmnt -no OPTIONS /tmp` — if `noexec` is present, remount writable+exec (`sudo mount -o remount,exec /tmp`) or bake it into the image.
  2. Change the provisioner's remote install location to an exec-allowed path if /tmp must stay noexec (requires adjusting remotePath in the hcp-sbom provisioner config/code).
  3. Check for cleanup agents (systemd-tmpfiles-clean, security daemons) removing the binary, and re-run to see if timing is the issue.
  4. Verify file ownership/permissions on the guest: `ls -l /tmp/packer-sbom-runner` as the SSH user; ensure the user owns it.
  5. Run with PACKER_LOG=1 to see the raw output of the failing `test -x` command and distinguish permission vs missing-file causes.

Example fix

// before (host /etc/fstab, hardened image)
tmpfs /tmp tmpfs rw,nosuid,nodev,noexec 0 0
// after
sudo mount -o remount,exec /tmp
# or in /etc/fstab: tmpfs /tmp tmpfs rw,nosuid,nodev 0 0
Defensive patterns

Strategy: validation

Validate before calling

// pre-flight inside the provisioner, before uploading:
checkCmd := `findmnt -no OPTIONS /tmp || mount | grep ' on /tmp '`
out, err := runRemoteCmd(ctx, comm, checkCmd)
if err != nil {
	return fmt.Errorf("cannot inspect /tmp mount options: %w", err)
}
if strings.Contains(out, "noexec") {
	return fmt.Errorf("/tmp is mounted noexec; remount exec or install elsewhere")
}
// and verify write+exec capability early:
probeCmd := `f=$(mktemp); printf '#!/bin/sh\n' > $f; chmod +x $f; $f; rc=$?; rm -f $f; exit $rc`

Type guard

func isExecAllowed(mountOptions string) bool {
	return !strings.Contains(mountOptions, "noexec")
}

Try / catch

err := p.runRemoteCmd(ctx, comm, verifyCmd, "verify scanner is executable")
if err != nil {
	// inspect mount options to confirm the noexec hypothesis before remounting
	return fmt.Errorf("scanner not executable at %s: %w (check: findmnt -no OPTIONS /tmp)", remotePath, err)
}

Prevention

When it happens

Trigger: p.runRemoteCmd(ctx, comm, `test -x "/tmp/packer-sbom-runner"`, ...) returns non-zero after a successful chmod +x — the file exists but lacks the execute bit, or the filesystem enforces noexec so test -x still reflects unexecutable permissions, or the file was removed between chmod and verify.

Common situations: /tmp is mounted noexec on hardened VM images (very common on CIS-baselined hosts and some cloud images); a security agent or systemd-tmpfiles cleanup deletes files from /tmp between steps; a custom umask or ACL strips the x bit right after chmod; the SSH user lacks ownership of the file so test -x fails.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of hashicorp/packer@eb36e3c3e4 (2026-09-05). Data as JSON: /api/errors/2a7f28f6eda3d9a3. Report an issue: GitHub.