Skip to content

fix(fspy): read the whole shebang line - #788

Merged
wan9chi merged 4 commits into
voidzero-dev:mainfrom
lifeiscontent:fix/fspy-long-shebang
Oct 12, 2026
Merged

wan9chi merged 4 commits into
voidzero-dev:mainfrom
lifeiscontent:fix/fspy-long-shebang

Conversation

@lifeiscontent

Copy link
Copy Markdown
Contributor

Motivation

fspy reads only the first 128 bytes of a script to find its interpreter. A script whose interpreter path is longer than 126 bytes runs fine on its own, but under a cached task the path gets cut short. The exec then fails with ENOENT, or runs whatever lives at the shorter path.

Changes

  • Read as much of the line as the kernel does: 512 bytes on macOS, 256 on Linux. I checked both limits by running scripts on macOS and in a Linux container.
  • Where the kernel refuses a script, return ENOEXEC instead of running a truncated path. That covers macOS with no newline in the first 512 bytes, and Linux when the interpreter path itself doesn't fit.
  • Unit tests for the limits on both platforms, plus an fspy test that runs a script with a 200-byte interpreter path.

lifeiscontent added a commit to lifeiscontent/vite-task that referenced this pull request Oct 2, 2026
@wan9chi
wan9chi force-pushed the fix/fspy-long-shebang branch from 45f64e7 to c6d9ea6 Compare October 12, 2026 09:54
wan9chi pushed a commit to lifeiscontent/vite-task that referenced this pull request Oct 12, 2026
lifeiscontent and others added 3 commits October 12, 2026 17:56
fspy read only the first 128 bytes of a script to find its interpreter, so
an interpreter path longer than 126 bytes was cut short. The traced exec then
ran the truncated path, which fails or runs a different program, while the
same script runs fine without tracing.

Read as much as the kernel does instead: 512 bytes on macOS and 256 on
Linux. Where the kernel refuses a line that doesn't fit, return ENOEXEC
like it does rather than running a truncated path.
On macOS, `#` ends the shebang line like a newline does (`IS_EOL` in XNU's
`exec_shell_imgact`). Reading 512 bytes instead of 128 made a `#` past the
first 128 bytes part of the interpreter or its arguments, and a line without
a newline in its first 512 bytes failed with ENOEXEC even when a `#` ended it.

On Linux, when the 256-byte buffer fills up without a newline, the space or
tab after the interpreter path can be the buffer's last byte. The kernel
looks for it there before dropping that byte, but fspy dropped the byte
first and refused the script.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@wan9chi
wan9chi force-pushed the fix/fspy-long-shebang branch from c6d9ea6 to 469b59b Compare October 12, 2026 09:56
fspy only swaps the arm64e `/bin/sh` for its own shell when it's run by
that path. Through the test's symlink it injects the arm64 preload instead,
and dyld refuses to load it where SIP is off, as on the CI runners. The
script still runs on Linux; on macOS the test checks that the whole
200-byte interpreter path was read, which the 128-byte read didn't do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@wan9chi

wan9chi commented Oct 12, 2026

Copy link
Copy Markdown
Member

Thank you!

@wan9chi
wan9chi merged commit 9ba4cc9 into voidzero-dev:main Oct 12, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants