From 70bcd65c21981552b4977465c0b53d8aac86c09b Mon Sep 17 00:00:00 2001 From: Nils Jannasch Date: Sat, 9 May 2026 23:01:43 +0200 Subject: [PATCH] fix(install): atomic mv to a fresh inode (real fix for SIGKILL on macOS) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- install.sh | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index 9364e3d..5935700 100755 --- a/install.sh +++ b/install.sh @@ -45,11 +45,25 @@ get_latest_version() { download() { local url="$1" dest="$2" + # Download to a sibling temp file then atomically `mv` over the + # destination. Two reasons: + # 1. Atomic — if curl fails midway, the existing binary at $dest is + # untouched. (curl -o would have already truncated it.) + # 2. Fresh inode. macOS' syspolicyd tracks Gatekeeper trust per + # inode; when curl/wget overwrites a file in place the inode is + # preserved and any prior "blocked" verdict sticks even after + # the bytes are replaced. Symptom: Killed: 9 on launch with + # `load code signature error 2` in the system log, despite + # `codesign --verify` reporting the new bytes as valid. mv from + # a sibling temp file replaces the old inode entirely. + local tmp + tmp="$(mktemp "${dest}.XXXXXX")" || err "Could not create temp file in $(dirname "$dest")" if command -v curl &>/dev/null; then - curl -fsSL -o "$dest" "$url" + curl -fsSL -o "$tmp" "$url" || { rm -f "$tmp"; err "Download failed: $url"; } elif command -v wget &>/dev/null; then - wget -qO "$dest" "$url" + wget -qO "$tmp" "$url" || { rm -f "$tmp"; err "Download failed: $url"; } fi + mv -f "$tmp" "$dest" } main() {