Agent write-through-symlink can corrupt live runtime Git into a recursive exec loop; two AMFI kalloc panics followed
Summary
Codex Desktop allowed an agent-created temporary Git path to remain a symlink to the live Codex runtime Git wrapper. A later shell redirection intended to replace the temporary wrapper followed that symlink and overwrote the live runtime dependency instead.
The live wrapper changed from the vendor 422-byte relative wrapper into a 112-byte absolute self-target:
#!/usr/bin/env bash
exec <HOME>/.cache/codex-runtimes/codex-primary-runtime/dependencies/bin/git "$@"
git --version then hung in recursive bash/env execution. The runtime did not detect or repair the mutated dependency over the following days. Two later macOS kernel panics on the same machine show the same high-rate same-UID exec-storm class exhausting data.kalloc.1024 in AppleMobileFileIntegrity (AMFI). The exact user-space corruption is proven; the kernel reports prove the matching exec-storm/AMFI failure class, although they do not retain enough argv ancestry to attribute every snapshotted process to the wrapper independently.
This report asks for both a Codex integrity boundary and a recursion circuit breaker. A user-space tool should not be able to mutate its own managed runtime through a temporary symlink and then continue executing an unverified dependency indefinitely.
Environment
- macOS 26.5.2, build
25F84, Apple Silicon - Codex Desktop bundle:
com.openai.codex - Desktop version:
26.707.31428, build5059 - Signing team: OpenAI
2DC432GLL2 - Bundled CLI:
0.144.0-alpha.4 - Global CLI:
0.144.1 - Codex primary runtime bundle:
26.630.12135 - Bundled/runtime Git:
2.53.0 - Current runtime after the incident:
26.709.11516, whose Git wrapper moved todependencies/bin/fallback/git
Exact corruption sequence
The local Codex session record captures this sequence:
- The agent created
/tmp/codex-git-bin/gitas a symlink to the live managed wrapper at<HOME>/.cache/codex-runtimes/codex-primary-runtime/dependencies/bin/git. - Invoking the symlinked wrapper failed because its relative
SCRIPT_DIRresolved from/tmp/codex-git-bin, so../native/git/bin/gitdid not exist there. - To “fix” the temporary wrapper, the agent used shell redirection to
/tmp/codex-git-bin/git. - Redirection followed the symlink and replaced the live managed wrapper with an absolute wrapper that execs the same live path.
- The subsequent
git --versionremained running/hung.
Preserved evidence was copied before repair and is never executed:
- corrupted bytes: 112
- mode after preservation:
0400 - SHA-256:
89b24472bbcbcb1a893553f202fe17411d03edc7ce1c14a5834c45817261c856 - content class: shell script with an absolute self-target
The restored vendor wrapper is:
- 422 bytes
- mode
0555 - SHA-256:
26c9c1e18ce7971fb917723049d64b1493711f185bea0f608c91ba5c8995f584 - relative final target:
../native/git/bin/git(a distinct native binary)
Safe non-live reproduction of the write-through class
This reproduces only the symlink-following overwrite in a fresh temporary directory. It must not point at a real runtime and the resulting wrapper must not be executed.
scratch="$(mktemp -d)"
live="$scratch/live-git"
alias="$scratch/temp-git"
printf '%s\n' '#!/bin/sh' 'exec /usr/bin/git "$@"' > "$live"
before="$(shasum -a 256 "$live")"
ln -s "$live" "$alias"
# This is the dangerous primitive: shell redirection follows the symlink.
printf '%s\n' '#!/bin/sh' "exec \"$live\" \"\$@\"" > "$alias"
after="$(shasum -a 256 "$live")"
printf 'alias -> %s\nbefore=%s\nafter=%s\n' "$(readlink "$alias")" "$before" "$after"
grep -F "exec \"$live\"" "$live"
# Do not chmod or execute either file.
Expected proof: before and after differ, and the real file contains the self-target even though the write named only the symlink alias.
Kernel-panic evidence
Two panics occurred after the corruption and before the live wrapper was manually restored.
2026-07-06 00:55:13 -0300
zalloc[3]: zone map exhausted while allocating from zone [data.kalloc.1024]
20G, 21,328,464 elements allocated
Panicked task: env
Kernel extension in backtrace: AppleMobileFileIntegrity 1.0.5
0 swapfiles; memoryPressure=false
The dominant UID-501 coalition contained 14 live bash/env processes with about 7.04 billion page faults and 987 million copy-on-write faults.
2026-07-09 16:02:26 -0300
zalloc[3]: zone map exhausted while allocating from zone [data.kalloc.1024]
20G, 21,285,168 elements allocated
Panicked task: bash
Kernel extension in backtrace: AppleMobileFileIntegrity 1.0.5
0 swapfiles; memoryPressure=false
One UID-501 coalition contained 25 live bash/env processes with about 1.55 billion page faults and 217 million copy-on-write faults.
Only these scrubbed facts are included here; full panic dumps and unrelated local session content are intentionally omitted.
Expected behavior / requested class-level protections
- Managed runtime dependencies should not be writable through agent-controlled temporary aliases.
- Temporary executable wrappers should be regular files owned by the runtime, not symlinks to live dependencies; write paths should reject symlinks and symlink swaps (
lstat,O_NOFOLLOW, atomic create/rename inside a controlled directory). - Runtime dependencies should have a signed or hashed manifest and be verified before execution and at app/runtime startup.
- A mutated dependency should be quarantined and automatically restored from the known-good bundle, with a clear user-visible diagnostic.
- Wrapper validation should reject direct and normalized self-targets, symlink cycles, and targets that do not resolve to the distinct native executable.
- The process launcher should detect a tight same-command exec recursion and trip a bounded circuit breaker before it can create sustained OS security-policy pressure.
- Regression coverage should include the observed write-through-symlink case plus a representative symlink-swap/path-normalization drift case.
Current mitigation and local regression proof
- The corrupted file is preserved read-only and never executed.
- The current official runtime archive (
26.709.11516) was independently verified by its published SHA-256. The live archive surface, runtime metadata, native Git signature/hash, seven wrappers, fourteen immediate delegate seams, and all 397 executable/symlink entries immediately reachable through Git's forced exec path matched that archive before activation. - A durable local guard now lives outside the replaceable runtime cache. It installs a
/bin/bashwrapper with one fixed absolute native-Git target, never resolves throughPATH, forces the verified Git config/exec/template paths, and rejects literal, canonical, symlink, hard-link, missing, and non-executable self-targets beforeexec. - macOS
UF_IMMUTABLEprotects 452 declared runtime and control-plane paths. An event-onlyWatchPathsLaunchAgent detects drift without polling; drifted wrappers are quarantined or replaced with immutable rc-126 sentinels before external evidence I/O. Automatic cache-root replacement is intentionally blocked; verified updates use a controlled validate-then-activate flow with authenticated crash recovery. - The repository regression suite has 55 cases covering the observed recursive wrapper, canonical/symlink/hard-link identity, PATH rediscovery, write-through and helper drift, watcher backup failure, refresh/repair/uninstall transactions, and hard process death at rename and torn control-generation boundaries. It passed five consecutive runs and the release gate.
- A separately authored bounded sandbox harness passed 5/5: safe control, literal self-recursion, symlink/canonical recursion, PATH-prepended rediscovery fail-closed, and a real remove-and-replace simulated refresh overwrite. No hostile fixture was executed before guard rendering and no sandbox
git/env/bashresidue remained. - Live post-install status is safe: all 452 paths protected, the loaded LaunchAgent configuration matches exactly, cached Git 2.53.0 works normally, and 160 guarded Git invocations across eight workers completed with no errors or returning recursive/orphan processes.
- The portable implementation and validation were merged into the local canonical policy repository; a focused patch can be supplied privately if maintainers need it.
This is an unprivileged mitigation. A same-user actor can deliberately clear uchg; stronger enforcement would require a root-owned or Endpoint Security boundary. The Apple AMFI/XNU kernel-exhaustion robustness defect also remains external.
Duplicate search (rechecked 2026-07-09)
This appears to warrant a new issue rather than a comment:
- #20843 is the same
data.kalloc.1024zone but a different reported trigger: TUI redraw/vfs.namei/terminal/WindowServer. It was closed as not planned. - #16866 and #20283 are related kernel-panic severity, but their signature is
os_refcntoverflow through AppleSystemPolicy, not AMFIdata.kalloc.1024zone exhaustion from an exec storm. - #27662 is adjacent SystemPolicy pressure involving temporary Codex exec wrappers, but it reports global
spctl“Too many open files”, not live runtime dependency corruption or this panic signature. - #30811 establishes bundled runtime Git context, but it is specifically about the missing macOS credential helper.
Related for context: #20843, #16866, #20283, #27662, #30811.