Skip to content

fix(install): atomic mv to a fresh inode (real fix for macOS SIGKILL)#25

Merged
NJannasch merged 1 commit into
mainfrom
fix/install-fresh-inode
May 9, 2026
Merged

fix(install): atomic mv to a fresh inode (real fix for macOS SIGKILL)#25
NJannasch merged 1 commit into
mainfrom
fix/install-fresh-inode

Conversation

@NJannasch

Copy link
Copy Markdown
Owner

What was actually wrong

The earlier `xattr -d → xattr -c` change in PR #24 wasn't the real root cause — local repro showed xattrs were already empty. The actual issue:

  • `inode=8641475` (curl -o overwriting in place) → exit 137 every time
  • `inode=8757579` (fresh download to a new inode) → runs fine

System log on the kill:
```
proc 7939: load code signature error 2 for file "vibecockpit"
(AppleSystemPolicy) ASP: Security policy would not allow process
```

macOS' `syspolicyd` tracks Gatekeeper trust per inode. When `curl -o` overwrites an existing file in place, the inode is preserved and any prior "blocked" verdict for that inode sticks even after the bytes are replaced — so the new binary fails to load with EBADEXEC at the kernel level despite `codesign --verify` reporting it as valid.

Fix

`download()` now writes to a sibling temp file (`mktemp` in `$bin_dir`) then `mv -f` over the destination. `mv` on the same filesystem is a `rename(2)` — atomic, fresh inode, no inherited verdict.

Two bonuses:

  • If the download fails midway, the existing binary at `$dest` is untouched (curl -o would have already truncated it to 0 bytes).
  • No `rm` of the user's existing file at any point.

Test plan

  • Local: reproduced the SIGKILL on a poisoned inode (curl -o into existing path) — `exit 137` every time
  • Local: ran the new install.sh against the poisoned destination — new inode produced, binary launches with `exit 0`
  • CI green
  • Followup test from a clean machine: `curl -fsSL …/install.sh | bash` succeeds end-to-end

The earlier xattr-c change was NOT the actual root cause. Investigation
on a poisoned install:

  inode=8641475 (curl -o overwriting in place)  → exit 137 every time
  inode=8757579 (rm + fresh download, new inode) → runs fine

System log on the kill:
  proc 7939: load code signature error 2 for file "vibecockpit"
  (AppleSystemPolicy) ASP: Security policy would not allow process

macOS' syspolicyd tracks Gatekeeper trust *per inode*. When curl -o (or
wget -O) overwrites an existing file in place, the inode is preserved
and any prior "blocked" verdict for that inode sticks even after the
bytes are replaced — so the new binary fails to load with EBADEXEC at
the kernel level despite `codesign --verify` reporting it as valid.

Fix: download() now writes to a sibling temp file (mktemp in $bin_dir)
and `mv -f` over the destination. mv on the same filesystem is a
rename(2): atomic, fresh inode, no inherited verdict. Bonus: if the
download fails the existing binary at $dest is untouched (curl -o would
have already truncated it).

Verified locally: poisoned inode SIGKILL'd; new install.sh produced a
fresh inode and the binary launched cleanly.
@NJannasch NJannasch merged commit 61d79d6 into main May 9, 2026
6 checks passed
@NJannasch NJannasch deleted the fix/install-fresh-inode branch May 9, 2026 21:05
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.

1 participant