Agent write-through-symlink can corrupt live runtime Git into a recursive exec loop; two AMFI kalloc panics followed

Open 💬 0 comments Opened Jul 10, 2026 by johnsilvavlogs

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, build 5059
  • 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 to dependencies/bin/fallback/git

Exact corruption sequence

The local Codex session record captures this sequence:

  1. The agent created /tmp/codex-git-bin/git as a symlink to the live managed wrapper at <HOME>/.cache/codex-runtimes/codex-primary-runtime/dependencies/bin/git.
  2. Invoking the symlinked wrapper failed because its relative SCRIPT_DIR resolved from /tmp/codex-git-bin, so ../native/git/bin/git did not exist there.
  3. To “fix” the temporary wrapper, the agent used shell redirection to /tmp/codex-git-bin/git.
  4. Redirection followed the symlink and replaced the live managed wrapper with an absolute wrapper that execs the same live path.
  5. The subsequent git --version remained 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

  1. Managed runtime dependencies should not be writable through agent-controlled temporary aliases.
  2. 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).
  3. Runtime dependencies should have a signed or hashed manifest and be verified before execution and at app/runtime startup.
  4. A mutated dependency should be quarantined and automatically restored from the known-good bundle, with a clear user-visible diagnostic.
  5. Wrapper validation should reject direct and normalized self-targets, symlink cycles, and targets that do not resolve to the distinct native executable.
  6. 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.
  7. 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/bash wrapper with one fixed absolute native-Git target, never resolves through PATH, forces the verified Git config/exec/template paths, and rejects literal, canonical, symlink, hard-link, missing, and non-executable self-targets before exec.
  • macOS UF_IMMUTABLE protects 452 declared runtime and control-plane paths. An event-only WatchPaths LaunchAgent 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/bash residue 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.1024 zone 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_refcnt overflow through AppleSystemPolicy, not AMFI data.kalloc.1024 zone 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.

View original on GitHub ↗