macOS: terminal sessions (Darwin session runtime, D335) — J146 #328

Open
opened 2026-11-03 16:17:28 +00:00 by kayg · 29 comments
Owner

J146: terminal sessions on macOS (Darwin session runtime for the shim). Linked to #265 (macOS mesh node, J80) and #320 (shim portability, J136).

Owner, D335: on macOS a shell runs inside the person's own login session while they are logged in, with their keychain, ssh-agent and privacy permissions. Otherwise it is a plain shell started by the root daemon as their user: "correct".

Scope:

  • PTY creation; per-user shims with privilege drop (setuid, initgroups, a clean environment).
  • D335 placement: inside the user's login session (launchctl asuser / bootstrap) when logged in, otherwise the plain shell.
  • Sessions survive infraternal agent restarts and upgrades (D13).
  • libghostty-vt terminal state in the shim (D94, D14), built on macOS with the pinned Zig (D213).
  • Reviewed macOS replacements for the Linux-only checks: peer credentials (getpeereid/LOCAL_PEERCRED), pid start time (proc_pidinfo), no core dumps and no debugger attach, mlock for secret pages (D244), process-exit watch (kqueue). A check with no safe macOS equivalent refuses its feature; nothing is weakened silently.

Proof on infraternal-macos-01-std under lockf -k /tmp/mac-bench.lock, without touching lume's login keychain or TCC. A security review follows; no merge or deploy in this job.

**J146: terminal sessions on macOS (Darwin session runtime for the shim).** Linked to #265 (macOS mesh node, J80) and #320 (shim portability, J136). Owner, D335: on macOS a shell runs inside the person's own login session while they are logged in, with their keychain, ssh-agent and privacy permissions. Otherwise it is a plain shell started by the root daemon as their user: "correct". Scope: - PTY creation; per-user shims with privilege drop (setuid, initgroups, a clean environment). - D335 placement: inside the user's login session (`launchctl asuser` / bootstrap) when logged in, otherwise the plain shell. - Sessions survive infraternal agent restarts and upgrades (D13). - libghostty-vt terminal state in the shim (D94, D14), built on macOS with the pinned Zig (D213). - Reviewed macOS replacements for the Linux-only checks: peer credentials (`getpeereid`/`LOCAL_PEERCRED`), pid start time (`proc_pidinfo`), no core dumps and no debugger attach, `mlock` for secret pages (D244), process-exit watch (kqueue). A check with no safe macOS equivalent refuses its feature; nothing is weakened silently. Proof on `infraternal-macos-01-std` under `lockf -k /tmp/mac-bench.lock`, without touching lume's login keychain or TCC. A security review follows; no merge or deploy in this job.
Author
Owner

J146 start (Claude Opus 5.5, 2026-11-03 ~21:40 IST). Branch macos-sessions-j146, base macos-build-j136 head 3e3c84384 (which sits on J80's macos-node-265-s1). J80's and J136's commits stay untouched; I rebase when J80d lands. Worktree on code: ~/Developer/Worktrees/infraternal-j146. The plan with an estimate follows within 15 minutes.

**J146 start** (Claude Opus 5.5, 2026-11-03 ~21:40 IST). Branch `macos-sessions-j146`, base `macos-build-j136` head `3e3c84384` (which sits on J80's `macos-node-265-s1`). J80's and J136's commits stay untouched; I rebase when J80d lands. Worktree on code: `~/Developer/Worktrees/infraternal-j146`. The plan with an estimate follows within 15 minutes.
Author
Owner

J146 plan, about 14 h of work plus Mac lock and build-slot queues. I'll report if I pass 28 h.

The design keeps the Linux shape (D13). The root infraternal agent starts one detached shim per terminal. The shim keeps the PTY and the libghostty-vt state, and listens on a root 0600 socket in /run/infraternal/sessions. After a restart or upgrade, the infraternal agent finds it again by scanning that directory and accepting only uid-0 peers. Only the shell child drops privileges.

  1. libghostty-vt on macOS (1.5 h). build.rs and scripts/build-vt build the pinned archive and our snapshot adapter for aarch64-macos with the single pinned Zig 0.15.2 (D94, D213). The Linux build path stays byte-identical.
  2. Darwin PTY and process boundary in the shim (4 h).
    • openpty, setsid and TIOCSCTTY; FIONREAD and TIOCSWINSZ; the signal reset loops to NSIG (macOS has no SIGRTMAX); no THP calls.
    • The pump's signalfd becomes a kqueue EVFILT_SIGNAL/EVFILT_PROC source.
    • Peer credentials use getpeereid and LOCAL_PEERCRED (xucred, version-checked), replacing SO_PEERCRED.
    • Unsafe code stays confined to the audited boundary module.
  3. Privilege drop and a clean environment (1 h). In the child: initgroups (falling back to getgrouplist/setgroups if the 16-group limit truncates), then setgid, then setuid, with a check that setuid(0) fails afterwards. Then chdir as the user and env_clear with a fixed allowlist, so no DYLD_* or inherited variables reach the shell (the lesson of J135's P1-1).
  4. D335 placement (2 h).
    • When the person is logged in, the shell starts in their login session: the child execs /bin/launchctl asuser <uid>, which execs a small fixed private entry of our binary. That entry drops privileges and execs the login shell, so it gets the keychain, ssh-agent and TCC context.
    • Otherwise it is the plain privilege-dropped shell.
    • Safe default for "logged in": the person has a GUI login session (their gui/<uid> launchd domain exists, checked when the terminal opens). An SSH-only session does not count. This is an owner question, asked below.
  5. Hardening (1.5 h).
    • No debugger attach: ptrace(PT_DENY_ATTACH) in the shim.
    • No core dumps: RLIMIT_CORE=0, hard and soft.
    • PID identity: start time from proc_pidinfo(PROC_PIDTBSDINFO) (pbi_start_tvsec/usec), replacing /proc/<pid>/stat field 22.
    • Process-exit watch: kqueue EVFILT_PROC NOTE_EXIT, re-checked against the start time to rule out PID reuse.
    • mlock for secret pages (D244): macOS has no MADV_DONTDUMP. I'll apply mlock together with RLIMIT_CORE=0 only where the session path actually holds secrets. The CA key heap stays refused off Linux as J136 left it (LockedBytes is uninhabited), because the dump-exclusion half has no reviewed macOS equivalent. Any other check that has no equivalent refuses its feature, and I'll list each one on this issue.
  6. infraternal agent side (2 h).
    • Darwin launch_configured: detached double fork, no systemd-run.
    • Socket readiness and session-directory events through kqueue EVFILT_VNODE instead of inotify.
    • process_exits on kqueue.
    • The D247 systemd/PAM host-login worker stays Linux-only. On macOS, D335 placement replaces it.
  7. Proof on the Mac VM under lockf -k /tmp/mac-bench.lock (2 h). lume's keychain and TCC are never touched; the only keychain step is the read-only security list-keychains the task asks for.
    • Open a shell through infraternal, type, and read the output back.
    • For lume (GUI logged in), security list-keychains shows login.keychain-db.
    • For a user with no GUI login, it is a plain shell; I'll record what list-keychains shows there.
    • A session survives an infraternal agent restart and an upgrade to a rebuilt binary.
  8. Linux (1 h plus queue). The x86_64/aarch64 static sizes before and after, and scripts/verify fast verbatim with the head SHA.

Owner question (kayg): D335 says "while they are logged in". Does that mean a GUI login (they are at the Mac or in Screen Sharing, gui/<uid> exists), or any login, including SSH-only? Safe default meanwhile: GUI login only. An SSH-only session has no unlocked login keychain or TCC context to join, so treating it as logged in would promise access it cannot give.

**J146 plan, about 14 h of work plus Mac lock and build-slot queues.** I'll report if I pass 28 h. The design keeps the Linux shape (D13). The root infraternal agent starts one detached shim per terminal. The shim keeps the PTY and the libghostty-vt state, and listens on a root `0600` socket in `/run/infraternal/sessions`. After a restart or upgrade, the infraternal agent finds it again by scanning that directory and accepting only uid-0 peers. Only the shell child drops privileges. 1. **libghostty-vt on macOS (1.5 h).** `build.rs` and `scripts/build-vt` build the pinned archive and our snapshot adapter for `aarch64-macos` with the single pinned Zig 0.15.2 (D94, D213). The Linux build path stays byte-identical. 2. **Darwin PTY and process boundary in the shim (4 h).** - `openpty`, `setsid` and `TIOCSCTTY`; `FIONREAD` and `TIOCSWINSZ`; the signal reset loops to `NSIG` (macOS has no `SIGRTMAX`); no THP calls. - The pump's `signalfd` becomes a kqueue `EVFILT_SIGNAL`/`EVFILT_PROC` source. - Peer credentials use `getpeereid` and `LOCAL_PEERCRED` (`xucred`, version-checked), replacing `SO_PEERCRED`. - Unsafe code stays confined to the audited boundary module. 3. **Privilege drop and a clean environment (1 h).** In the child: `initgroups` (falling back to `getgrouplist`/`setgroups` if the 16-group limit truncates), then `setgid`, then `setuid`, with a check that `setuid(0)` fails afterwards. Then `chdir` as the user and `env_clear` with a fixed allowlist, so no `DYLD_*` or inherited variables reach the shell (the lesson of J135's P1-1). 4. **D335 placement (2 h).** - When the person is logged in, the shell starts in their login session: the child execs `/bin/launchctl asuser <uid>`, which execs a small fixed private entry of our binary. That entry drops privileges and execs the login shell, so it gets the keychain, ssh-agent and TCC context. - Otherwise it is the plain privilege-dropped shell. - **Safe default for "logged in":** the person has a GUI login session (their `gui/<uid>` launchd domain exists, checked when the terminal opens). An SSH-only session does not count. This is an owner question, asked below. 5. **Hardening (1.5 h).** - No debugger attach: `ptrace(PT_DENY_ATTACH)` in the shim. - No core dumps: `RLIMIT_CORE=0`, hard and soft. - PID identity: start time from `proc_pidinfo(PROC_PIDTBSDINFO)` (`pbi_start_tvsec/usec`), replacing `/proc/<pid>/stat` field 22. - Process-exit watch: kqueue `EVFILT_PROC NOTE_EXIT`, re-checked against the start time to rule out PID reuse. - `mlock` for secret pages (D244): macOS has no `MADV_DONTDUMP`. I'll apply `mlock` together with `RLIMIT_CORE=0` only where the session path actually holds secrets. The CA key heap stays refused off Linux as J136 left it (`LockedBytes` is uninhabited), because the dump-exclusion half has no reviewed macOS equivalent. Any other check that has no equivalent refuses its feature, and I'll list each one on this issue. 6. **infraternal agent side (2 h).** - Darwin `launch_configured`: detached double fork, no `systemd-run`. - Socket readiness and session-directory events through kqueue `EVFILT_VNODE` instead of inotify. - `process_exits` on kqueue. - The D247 systemd/PAM host-login worker stays Linux-only. On macOS, D335 placement replaces it. 7. **Proof on the Mac VM under `lockf -k /tmp/mac-bench.lock` (2 h).** lume's keychain and TCC are never touched; the only keychain step is the read-only `security list-keychains` the task asks for. - Open a shell through infraternal, type, and read the output back. - For lume (GUI logged in), `security list-keychains` shows `login.keychain-db`. - For a user with no GUI login, it is a plain shell; I'll record what `list-keychains` shows there. - A session survives an infraternal agent restart and an upgrade to a rebuilt binary. 8. **Linux (1 h plus queue).** The x86_64/aarch64 static sizes before and after, and `scripts/verify fast` verbatim with the head SHA. **Owner question (kayg):** D335 says "while they are logged in". Does that mean a **GUI login** (they are at the Mac or in Screen Sharing, `gui/<uid>` exists), or **any login, including SSH-only**? Safe default meanwhile: GUI login only. An SSH-only session has no unlocked login keychain or TCC context to join, so treating it as logged in would promise access it cannot give.
Author
Owner

J146 finding 1: what tells a login-session shell from a plain one on macOS (Mac VM, macOS 26.5.2, read-only probes under lockf -k /tmp/mac-bench.lock; no keychain items were read and TCC was not touched).

gui/501=0  user/501=0          # lume: GUI login
gui/1500=125  user/1500=0      # claude: no GUI login
asuser keeps pid and fds:  outer=45918 inner=45918, fds 0 1 2 3 4 7
launchctl asuser 1500 /usr/bin/id   -> rc=0 (accepted silently with no GUI session)
sudo -u lume   security list-keychains -> /Users/lume/Library/Keychains/login.keychain-db, System
asuser 501 + sudo -u lume  list-keychains -> the same
sudo -u claude security list-keychains -> /Users/lume/...login.keychain-db (sudo kept HOME)
launchctl managername: plain drop = Background, asuser 501 = Aqua
asuser 501: launchctl getenv SSH_AUTH_SOCK -> /var/run/com.apple.launchd.*/Listeners

Consequences:

  • security list-keychains alone does not prove placement. The search list comes from $HOME preferences, so a plain shell lists the login keychain too. The Mac proof shows it as asked, plus the real signals: launchctl managername = Aqua, and SSH_AUTH_SOCK equal to the gui domain's agent socket. In the plain case they are Background and no agent socket.
  • "Logged in" is detected explicitly (gui/<uid> exists), because launchctl asuser accepts any uid.
  • Placement mechanism: after setsid/TIOCSCTTY, the shell child execs /bin/launchctl asuser <uid>, and that execs a fixed private entry of our root-owned binary. Pid and descriptors are kept. The entry drops privileges (initgroups, setgid, setuid, then verifies that root can't be regained), takes SSH_AUTH_SOCK from the gui domain only if it is a socket owned by that uid, and execs the login shell with the allowlisted environment.
**J146 finding 1: what tells a login-session shell from a plain one on macOS** (Mac VM, macOS 26.5.2, read-only probes under `lockf -k /tmp/mac-bench.lock`; no keychain items were read and TCC was not touched). ``` gui/501=0 user/501=0 # lume: GUI login gui/1500=125 user/1500=0 # claude: no GUI login asuser keeps pid and fds: outer=45918 inner=45918, fds 0 1 2 3 4 7 launchctl asuser 1500 /usr/bin/id -> rc=0 (accepted silently with no GUI session) sudo -u lume security list-keychains -> /Users/lume/Library/Keychains/login.keychain-db, System asuser 501 + sudo -u lume list-keychains -> the same sudo -u claude security list-keychains -> /Users/lume/...login.keychain-db (sudo kept HOME) launchctl managername: plain drop = Background, asuser 501 = Aqua asuser 501: launchctl getenv SSH_AUTH_SOCK -> /var/run/com.apple.launchd.*/Listeners ``` Consequences: - **`security list-keychains` alone does not prove placement.** The search list comes from `$HOME` preferences, so a plain shell lists the login keychain too. The Mac proof shows it as asked, plus the real signals: `launchctl managername` = `Aqua`, and `SSH_AUTH_SOCK` equal to the gui domain's agent socket. In the plain case they are `Background` and no agent socket. - **"Logged in" is detected explicitly** (`gui/<uid>` exists), because `launchctl asuser` accepts any uid. - **Placement mechanism:** after `setsid`/`TIOCSCTTY`, the shell child execs `/bin/launchctl asuser <uid>`, and that execs a fixed private entry of our root-owned binary. Pid and descriptors are kept. The entry drops privileges (initgroups, setgid, setuid, then verifies that root can't be regained), takes `SSH_AUTH_SOCK` from the gui domain only if it is a socket owned by that uid, and execs the login shell with the allowlisted environment.
Author
Owner

J146 finding 2: libghostty-vt builds for macOS; launchctl asuser joins the GUI audit session

  • scripts/build-vt with TARGET=aarch64-apple-darwin (pinned Zig 0.15.2 copied from the nix fixed-output derivation) gives BUILDVT_EXIT=0 and libghostty-vt.a in 2m25s cold. The only script change macOS needs is the flock(2) fallback through perl, because util-linux's flock(1) is Linux-only.
  • Audit session (asid) probe: a 10-line getaudit_addr program. No keychain access.
gui/501 security context: uid = 501, asid = 100003
plain drop (sudo -u lume):           uid=501 asid=100123 flags=0x0    login=root
asuser 501 then drop:                uid=501 asid=100003 flags=0x2030 login=root
launchctl print gui/501 x10: 0.090 s total (sudo alone x10: 0.074 s), so about 2 ms per check

So asuser places the process in the person's GUI audit session (keychain unlock state, 0x10 graphic access) as well as their launchd bootstrap (Aqua, ssh-agent). No separate audit_session_join is needed.

  • getlogin() reports root after a plain drop, so the session-shell entry calls setlogin(name) (it is the session leader) before it drops privileges.
  • Placement is checked, not assumed: after asuser, the entry reads its own getaudit_addr. If auid is not the uid or the graphic-access flag is missing (for example, the person logged out between the check and the exec), it starts the plain shell. That still matches D335.
**J146 finding 2: libghostty-vt builds for macOS; `launchctl asuser` joins the GUI audit session** - `scripts/build-vt` with `TARGET=aarch64-apple-darwin` (pinned Zig 0.15.2 copied from the nix fixed-output derivation) gives `BUILDVT_EXIT=0` and `libghostty-vt.a` in 2m25s cold. The only script change macOS needs is the `flock(2)` fallback through perl, because util-linux's `flock(1)` is Linux-only. - Audit session (asid) probe: a 10-line `getaudit_addr` program. No keychain access. ``` gui/501 security context: uid = 501, asid = 100003 plain drop (sudo -u lume): uid=501 asid=100123 flags=0x0 login=root asuser 501 then drop: uid=501 asid=100003 flags=0x2030 login=root launchctl print gui/501 x10: 0.090 s total (sudo alone x10: 0.074 s), so about 2 ms per check ``` So `asuser` places the process in the person's GUI **audit session** (keychain unlock state, `0x10` graphic access) as well as their launchd bootstrap (`Aqua`, ssh-agent). No separate `audit_session_join` is needed. - `getlogin()` reports `root` after a plain drop, so the session-shell entry calls `setlogin(name)` (it is the session leader) before it drops privileges. - **Placement is checked, not assumed:** after `asuser`, the entry reads its own `getaudit_addr`. If `auid` is not the uid or the graphic-access flag is missing (for example, the person logged out between the check and the exec), it starts the plain shell. That still matches D335.
Author
Owner

J146 progress (~22:55 IST). WIP is pushed on macos-sessions-j146 (93405fbd8 plus local fixes; the atomic split is next). The Darwin session runtime works end to end on the Mac VM (macOS 26.5.2, under lockf -k /tmp/mac-bench.lock).

The proof drives infraternal agent --fd 3 as root. That is the real leg-B link the server uses, driven with the repo's agent_fixture through the new scripts/test-macos-sessions.py:

--- lume shell (GUI login) ---
lume / Aqua / SOCK=/var/run/com.apple.launchd.3nPAot2s0k/Listeners / LOGIN=lume
    "/Users/lume/Library/Keychains/login.keychain-db"  "/Library/Keychains/System.keychain"
uid=501 euid=501 asid=100003 auid=501 flags=0x2030      # gui/501 audit session is 100003
--- claude shell (no GUI login) ---
claude / Background / SOCK= / LOGIN=claude
    "/Library/Keychains/System.keychain"
uid=1500 euid=1500 asid=100194 auid=501 flags=0x0
PASS: shells open, take input and return output; D335 placement holds
PASS: both shells survive an infraternal agent restart (pids 34744, 34763)
PASS: both shells survive an upgrade of the infraternal agent; placement unchanged
PROOF_EXIT=0

This run's upgrade step used a debug build as the "different binary". The recorded proof will use two commits of the branch.

Findings so far (details go in the commit messages):

  1. Every kqueue registration through readiness::Registration failed with EINVAL on macOS. It registered read and write interest, and a kqueue descriptor refuses EVFILT_WRITE. This also affected J80's existing process_exits_macos.rs, which never ran because the daemon was not compiled on macOS. Kqueue sources now register read-only.
  2. The lifecycle registry identified shims through /proc/<pid>/stat, so after a restart the census was empty on macOS. It now uses proc_pidinfo(PROC_PIDTBSDINFO) start time in µs, and refuses zombies.
  3. Embedded mode with plugin-mesh cannot start on macOS: J80 slice 1 initializes the Mesh node in every execution mode, and its key access refuses outside launchd. J80d is reworking that code, so I didn't touch it. The proof build is the default feature set minus plugin-mesh, and its session code is identical. A proof through J80's launchd service plus a server comes after J80d lands.
  4. PT_DENY_ATTACH on the shim also blocks DTrace (dtruss shows nothing). That is intended.

Next: an exit-watch step in the proof, a check that PT_DENY_ATTACH is not inherited by the shell, the commit split, Mac clippy for the touched crates, then Linux verify fast and static sizes. Mac target: ~/worktrees/j146/target on the Mac VM.

**J146 progress (~22:55 IST).** WIP is pushed on `macos-sessions-j146` (`93405fbd8` plus local fixes; the atomic split is next). The Darwin session runtime works end to end on the Mac VM (macOS 26.5.2, under `lockf -k /tmp/mac-bench.lock`). The proof drives `infraternal agent --fd 3` as root. That is the real leg-B link the server uses, driven with the repo's `agent_fixture` through the new `scripts/test-macos-sessions.py`: ``` --- lume shell (GUI login) --- lume / Aqua / SOCK=/var/run/com.apple.launchd.3nPAot2s0k/Listeners / LOGIN=lume "/Users/lume/Library/Keychains/login.keychain-db" "/Library/Keychains/System.keychain" uid=501 euid=501 asid=100003 auid=501 flags=0x2030 # gui/501 audit session is 100003 --- claude shell (no GUI login) --- claude / Background / SOCK= / LOGIN=claude "/Library/Keychains/System.keychain" uid=1500 euid=1500 asid=100194 auid=501 flags=0x0 PASS: shells open, take input and return output; D335 placement holds PASS: both shells survive an infraternal agent restart (pids 34744, 34763) PASS: both shells survive an upgrade of the infraternal agent; placement unchanged PROOF_EXIT=0 ``` This run's upgrade step used a debug build as the "different binary". The recorded proof will use two commits of the branch. Findings so far (details go in the commit messages): 1. **Every kqueue registration through `readiness::Registration` failed with EINVAL on macOS.** It registered read and write interest, and a kqueue descriptor refuses `EVFILT_WRITE`. This also affected J80's existing `process_exits_macos.rs`, which never ran because the daemon was not compiled on macOS. Kqueue sources now register read-only. 2. **The lifecycle registry identified shims through `/proc/<pid>/stat`,** so after a restart the census was empty on macOS. It now uses `proc_pidinfo(PROC_PIDTBSDINFO)` start time in µs, and refuses zombies. 3. **Embedded mode with `plugin-mesh` cannot start on macOS:** J80 slice 1 initializes the Mesh node in every execution mode, and its key access refuses outside launchd. J80d is reworking that code, so I didn't touch it. The proof build is the default feature set minus `plugin-mesh`, and its session code is identical. A proof through J80's launchd service plus a server comes after J80d lands. 4. `PT_DENY_ATTACH` on the shim also blocks DTrace (`dtruss` shows nothing). That is intended. Next: an exit-watch step in the proof, a check that `PT_DENY_ATTACH` is not inherited by the shell, the commit split, Mac clippy for the touched crates, then Linux `verify fast` and static sizes. Mac target: `~/worktrees/j146/target` on the Mac VM.
Author
Owner

J146 progress (~00:15 IST). Atomic history is pushed: macos-sessions-j146 head 6363a04c4, 7 commits on J136 3e3c84384.

  • The first Linux verify fast (at 7ed513224, dev-02) failed CLI compile-out, clippy and test on one error. A Linux unit test (tests/unit/sessions.rs:209) called the inotify helper I had wrapped in SocketReadiness. The test now reads the wrapper's inner registration and asserts the same events. I squashed that into the agent commit (fb1694a4e). Static builds passed. The requeued gate at 6363a04c4 is waiting for a dev-02 slot.
  • Linux static sizes, both built in one target directory on dev-02:
target base 3e3c84384 head delta
x86_64-unknown-linux-musl 22,478,192 B 22,479,024 B +832 B
aarch64-unknown-linux-musl 20,496,600 B 20,497,256 B +656 B
  • macOS: nix develop -c scripts/macos-build-check at 7ed513224 gives MACOS_CHECK_EXIT=0. The default-feature release binary is 21,397,280 B (J136 measured 20,036,512 B without the session engine). It links only Security, CoreFoundation, /usr/lib/libiconv.2.dylib and libSystem; libghostty-vt is linked statically.
  • Debugger denial is verified: root lldb -p <shim> gives attach failed: lost connection, and lldb -p <the person's shell> attaches, so PT_DENY_ATTACH is not inherited.
**J146 progress (~00:15 IST).** Atomic history is pushed: `macos-sessions-j146` head `6363a04c4`, 7 commits on J136 `3e3c84384`. - The first Linux `verify fast` (at `7ed513224`, dev-02) failed CLI compile-out, clippy and test on **one** error. A Linux unit test (`tests/unit/sessions.rs:209`) called the inotify helper I had wrapped in `SocketReadiness`. The test now reads the wrapper's inner registration and asserts the same events. I squashed that into the agent commit (`fb1694a4e`). Static builds passed. The requeued gate at `6363a04c4` is waiting for a dev-02 slot. - **Linux static sizes**, both built in one target directory on dev-02: | target | base `3e3c84384` | head | delta | |---|---|---|---| | x86_64-unknown-linux-musl | 22,478,192 B | 22,479,024 B | +832 B | | aarch64-unknown-linux-musl | 20,496,600 B | 20,497,256 B | +656 B | - **macOS:** `nix develop -c scripts/macos-build-check` at `7ed513224` gives `MACOS_CHECK_EXIT=0`. The default-feature release binary is 21,397,280 B (J136 measured 20,036,512 B without the session engine). It links only Security, CoreFoundation, `/usr/lib/libiconv.2.dylib` and libSystem; libghostty-vt is linked statically. - Debugger denial is verified: root `lldb -p <shim>` gives `attach failed: lost connection`, and `lldb -p <the person's shell>` attaches, so `PT_DENY_ATTACH` is not inherited.
Author
Owner

J146 finish. Branch macos-sessions-j146, head 6363a04c4270f9c77e496a3986e470a627640de3, stacked on J136 3e3c84384 (on J80 macos-node-265-s1). J80's and J136's commits are untouched. Not merged, not deployed. A security review is next.

Commits (3e3c84384..6363a04c4):

  • f35fd05db shim: register kqueue sources for read readiness only. Every macOS kqueue Registration failed with EINVAL, including J80's process_exits_macos.rs.
  • d57eddb1c build: libghostty-vt for aarch64-macos with the pinned Zig (D94, D213); flock(2) fallback through perl.
  • 37f481cb6 shim: the Linux PTY child setup moves into its own function (refactor, no behaviour change).
  • 393bc74e6 shim: the PTY session engine on macOS with D335 placement (__session-shell: setlogin, initgroups, setgid, setuid, verified drop, allowlisted environment, launchctl asuser with placement checked from the audit record), PT_DENY_ATTACH, getpeereid, kqueue SIGCHLD. host-login stays Linux-only (D247).
  • fb1694a4e agent: the terminal daemon and shim launcher on macOS (kqueue vnode readiness and retirement events, /var/run/infraternal).
  • 865aa1cec agent: surviving shims identified by proc_pidinfo start time (D81).
  • 6363a04c4 test: scripts/test-macos-sessions.py, plus macos-build-check runs the session-shell tests.

Mac proof (macOS 26.5.2, root via lume sudo, under lockf -k /tmp/mac-bench.lock, through the real leg B of infraternal agent --fd 3). The upgrade goes from a build of 17d280bd3 (the agent before the shim-identity commit, sha256 374c0bdb…) to a build of the head code (a60b0780…, built at 7ed513224; after the rebase, the head differs only in a Linux unit test). Build features: default minus plugin-mesh (reason under "Open").

--- lume shell (GUI login) ---
lume / Aqua / SOCK=/var/run/com.apple.launchd.3nPAot2s0k/Listeners / SHELLPID=87160 / LOGIN=lume
    "/Users/lume/Library/Keychains/login.keychain-db" "/Library/Keychains/System.keychain"
uid=501 euid=501 asid=100003 auid=501 flags=0x2030      (gui/501 audit session 100003)
--- claude shell (no GUI login) ---
claude / Background / SOCK= / SHELLPID=87180 / LOGIN=claude
    "/Library/Keychains/System.keychain"
uid=1500 euid=1500 asid=100228 auid=501 flags=0x0
PASS: shells open, take input and return output; D335 placement holds
PASS: both shells survive an infraternal agent restart (pids 87160, 87180)
installed upgrade: ... sha256 a60b07809d2be6f88741f16229c718973df91e3080ca15b7fed8e7c7ef707ad3
PASS: both shells survive an upgrade of the infraternal agent; placement unchanged
PASS: shell exits reach the infraternal agent (kqueue exit watches); the census drops both
PROOF_EXIT=0
  • Debugger denial: root lldb -p <shim> gives attach failed: lost connection, and lldb -p <shell> attaches, so the flag is not inherited.
  • nix develop -c scripts/macos-build-check gives MACOS_CHECK_EXIT=0 (log attached).
  • The default-feature release binary is 21,397,280 B. It links only Security, CoreFoundation, /usr/lib/libiconv.2.dylib and libSystem.
  • Mac clippy -D warnings is clean for infraternal-shim --all-targets and infraternal-agent --features remote. The only remaining binary-level lint is J136's ssh_internal.rs:62 (unnecessary_wraps); I left it.

Linux static sizes (dev-02, nix, one target directory):

target base 3e3c84384 head delta
x86_64-unknown-linux-musl 22,478,192 B 22,479,024 B +832 B
aarch64-unknown-linux-musl 20,496,600 B 20,497,256 B +656 B

scripts/verify fast on infraternal-dev-02-std at 6363a04c4 (verbatim, third run, log attached):

+---------------------------+--------------+
| Rust gate                 | Result       |
+---------------------------+--------------+
| fmt                       | PASS         |
| VT native fmt             | PASS         |
| CLI plugin compile-out    | PASS         |
| clippy                    | PASS         |
| SSH runtime test helper   | PASS         |
| real-binary fixture paths | PASS         |
| test                      | FAIL (101)   |
| SSH inode reuse fixture   | PASS         |
| shim process-per-user     | PASS         |
| cargo-deny                | PASS         |
| gate architecture         | PASS         |
| static builds             | PASS         |
+---------------------------+--------------+
VERIFY_EXIT=1

The gate is not green. Only the test row fails, on a different test from run to run:

  • Head run 1 and run 3: the same four tests in crates/infraternal/tests/agent_daemon.rs: real_binary_keeps_background_backend_probe_off_the_control_path, …_preserves_input_and_output_across_24_abrupt_stops, …_rootless_person_preparation_honors_explicit_state_without_runtime_environment and …_rootless_private_readiness_does_not_refuse_person_revocation. In each, rootless readiness terminal.open returns resource_unavailable or times out.
  • Head run 2: remote_client::supervisor::review_tests::durable_state::acknowledged_network_access_survives_superseded_worker_completion.
  • Unmodified base 3e3c84384, same host: also test FAIL (101), on synthetic_master_socket_is_private_at_creation. Its agent_daemon passed. Log attached.
  • Targeted reruns at head all pass: agent_daemon 2/2 with -p infraternal and 5/5 under --workspace feature unification; the supervisor test 5/5. Base failed 1 of 5 agent_daemon runs under --workspace. A bisect across every commit passes 11/11.

My Linux-side changes are cfg gates, a verbatim function move, an identical tokio interest and an identical readiness loop. Even so, the four agent_daemon failures repeated in 2 of 3 head gate runs and did not appear in the single base gate run, so a head-specific interaction inside the full gate is not ruled out. It needs a green gate run, or a reviewer's look, before merge.

Refused or not ported, not weakened:

  • D244 CA key heap: it stays refused on macOS, as J136 left it (LockedBytes is uninhabited). macOS has no per-mapping dump exclusion like MADV_DONTDUMP. The process-wide hard RLIMIT_CORE=0 is not equivalent, because a root process can raise it. The session path holds no secret pages, so no mlock was needed there.
  • Session automation (infraternal sessions): it stays Linux-only. It proves the caller sits inside a saved session by walking /proc ancestry, and there is no reviewed macOS process-tree check for it yet.
  • SSH session bridge and host-login workers (D247): they stay Linux-only.

Open / for kayg:

  • The owner question from the plan: does "logged in" mean a GUI login only (the current safe default) or does an SSH-only login count?
  • The plain shell keeps the starter's audit user (auid=501 in the proof, from sudo). Under launchd it would be unset. Should a plain shell get its own audit session with auid=<uid>? That is a question for the security review.
  • J80 slice 1's Mesh initialization runs in every macOS execution mode and refuses outside launchd, so the embedded agent can't start with plugin-mesh. The proof therefore used default features minus plugin-mesh; the session code is identical. After J80d lands: rebase, then a proof through J80's launchd service and a server.
  • PT_DENY_ATTACH also blocks DTrace on the shim. That is intended, but it makes field debugging harder.

Cleanup: the Mac test install, state, probes, trace build, worktrees and the 8.7 GB target are removed, and the Mac lock is released. The dev-02 target is removed after this comment; logs are kept in ~/j146-out on dev-02 and ~/j146-evidence on the Mac.

**J146 finish.** Branch `macos-sessions-j146`, **head `6363a04c4270f9c77e496a3986e470a627640de3`**, stacked on J136 `3e3c84384` (on J80 `macos-node-265-s1`). J80's and J136's commits are untouched. Not merged, not deployed. A security review is next. Commits (`3e3c84384..6363a04c4`): - `f35fd05db` shim: register kqueue sources for read readiness only. Every macOS kqueue `Registration` failed with EINVAL, including J80's `process_exits_macos.rs`. - `d57eddb1c` build: libghostty-vt for `aarch64-macos` with the pinned Zig (D94, D213); `flock(2)` fallback through perl. - `37f481cb6` shim: the Linux PTY child setup moves into its own function (refactor, no behaviour change). - `393bc74e6` shim: the PTY session engine on macOS with D335 placement (`__session-shell`: setlogin, initgroups, setgid, setuid, verified drop, allowlisted environment, `launchctl asuser` with placement checked from the audit record), `PT_DENY_ATTACH`, getpeereid, kqueue SIGCHLD. host-login stays Linux-only (D247). - `fb1694a4e` agent: the terminal daemon and shim launcher on macOS (kqueue vnode readiness and retirement events, `/var/run/infraternal`). - `865aa1cec` agent: surviving shims identified by `proc_pidinfo` start time (D81). - `6363a04c4` test: `scripts/test-macos-sessions.py`, plus macos-build-check runs the session-shell tests. **Mac proof** (macOS 26.5.2, root via lume sudo, under `lockf -k /tmp/mac-bench.lock`, through the real leg B of `infraternal agent --fd 3`). The upgrade goes from a build of `17d280bd3` (the agent before the shim-identity commit, sha256 `374c0bdb…`) to a build of the head code (`a60b0780…`, built at `7ed513224`; after the rebase, the head differs only in a Linux unit test). Build features: default minus `plugin-mesh` (reason under "Open"). ``` --- lume shell (GUI login) --- lume / Aqua / SOCK=/var/run/com.apple.launchd.3nPAot2s0k/Listeners / SHELLPID=87160 / LOGIN=lume "/Users/lume/Library/Keychains/login.keychain-db" "/Library/Keychains/System.keychain" uid=501 euid=501 asid=100003 auid=501 flags=0x2030 (gui/501 audit session 100003) --- claude shell (no GUI login) --- claude / Background / SOCK= / SHELLPID=87180 / LOGIN=claude "/Library/Keychains/System.keychain" uid=1500 euid=1500 asid=100228 auid=501 flags=0x0 PASS: shells open, take input and return output; D335 placement holds PASS: both shells survive an infraternal agent restart (pids 87160, 87180) installed upgrade: ... sha256 a60b07809d2be6f88741f16229c718973df91e3080ca15b7fed8e7c7ef707ad3 PASS: both shells survive an upgrade of the infraternal agent; placement unchanged PASS: shell exits reach the infraternal agent (kqueue exit watches); the census drops both PROOF_EXIT=0 ``` - Debugger denial: root `lldb -p <shim>` gives `attach failed: lost connection`, and `lldb -p <shell>` attaches, so the flag is not inherited. - `nix develop -c scripts/macos-build-check` gives `MACOS_CHECK_EXIT=0` (log attached). - The default-feature release binary is 21,397,280 B. It links only Security, CoreFoundation, `/usr/lib/libiconv.2.dylib` and libSystem. - Mac clippy `-D warnings` is clean for `infraternal-shim --all-targets` and `infraternal-agent --features remote`. The only remaining binary-level lint is J136's `ssh_internal.rs:62` (unnecessary_wraps); I left it. **Linux static sizes** (dev-02, nix, one target directory): | target | base `3e3c84384` | head | delta | |---|---|---|---| | x86_64-unknown-linux-musl | 22,478,192 B | 22,479,024 B | +832 B | | aarch64-unknown-linux-musl | 20,496,600 B | 20,497,256 B | +656 B | **`scripts/verify fast` on infraternal-dev-02-std at `6363a04c4`** (verbatim, third run, log attached): ``` +---------------------------+--------------+ | Rust gate | Result | +---------------------------+--------------+ | fmt | PASS | | VT native fmt | PASS | | CLI plugin compile-out | PASS | | clippy | PASS | | SSH runtime test helper | PASS | | real-binary fixture paths | PASS | | test | FAIL (101) | | SSH inode reuse fixture | PASS | | shim process-per-user | PASS | | cargo-deny | PASS | | gate architecture | PASS | | static builds | PASS | +---------------------------+--------------+ VERIFY_EXIT=1 ``` **The gate is not green.** Only the `test` row fails, on a different test from run to run: - Head run 1 and run 3: the same four tests in `crates/infraternal/tests/agent_daemon.rs`: `real_binary_keeps_background_backend_probe_off_the_control_path`, `…_preserves_input_and_output_across_24_abrupt_stops`, `…_rootless_person_preparation_honors_explicit_state_without_runtime_environment` and `…_rootless_private_readiness_does_not_refuse_person_revocation`. In each, rootless readiness `terminal.open` returns `resource_unavailable` or times out. - Head run 2: `remote_client::supervisor::review_tests::durable_state::acknowledged_network_access_survives_superseded_worker_completion`. - **Unmodified base `3e3c84384`, same host**: also `test` FAIL (101), on `synthetic_master_socket_is_private_at_creation`. Its `agent_daemon` passed. Log attached. - Targeted reruns at head all pass: `agent_daemon` 2/2 with `-p infraternal` and 5/5 under `--workspace` feature unification; the supervisor test 5/5. Base failed 1 of 5 `agent_daemon` runs under `--workspace`. A bisect across every commit passes 11/11. My Linux-side changes are cfg gates, a verbatim function move, an identical tokio interest and an identical readiness loop. Even so, the four `agent_daemon` failures repeated in 2 of 3 head gate runs and did not appear in the single base gate run, so **a head-specific interaction inside the full gate is not ruled out**. It needs a green gate run, or a reviewer's look, before merge. **Refused or not ported, not weakened:** - **D244 CA key heap:** it stays refused on macOS, as J136 left it (`LockedBytes` is uninhabited). macOS has no per-mapping dump exclusion like `MADV_DONTDUMP`. The process-wide hard `RLIMIT_CORE=0` is not equivalent, because a root process can raise it. The session path holds no secret pages, so no `mlock` was needed there. - **Session automation (`infraternal sessions`):** it stays Linux-only. It proves the caller sits inside a saved session by walking `/proc` ancestry, and there is no reviewed macOS process-tree check for it yet. - **SSH session bridge and host-login workers (D247):** they stay Linux-only. **Open / for kayg:** - The owner question from the plan: does "logged in" mean a **GUI login only** (the current safe default) or does an SSH-only login count? - **The plain shell keeps the starter's audit user** (`auid=501` in the proof, from sudo). Under launchd it would be unset. Should a plain shell get its own audit session with `auid=<uid>`? That is a question for the security review. - **J80 slice 1's Mesh initialization runs in every macOS execution mode and refuses outside launchd,** so the embedded agent can't start with `plugin-mesh`. The proof therefore used default features minus `plugin-mesh`; the session code is identical. After J80d lands: rebase, then a proof through J80's launchd service and a server. - `PT_DENY_ATTACH` also blocks DTrace on the shim. That is intended, but it makes field debugging harder. Cleanup: the Mac test install, state, probes, trace build, worktrees and the 8.7 GB target are removed, and the Mac lock is released. The dev-02 target is removed after this comment; logs are kept in `~/j146-out` on dev-02 and `~/j146-evidence` on the Mac.
Author
Owner

J180 start: security review of J146 (Claude Opus 5.5, 2026-11-04 ~10:00 IST). Review only. I don't change any branch.

Scope: 3e3c84384..6363a04c4 (J146) plus J136's build and pinning commits (2bbd212e9, 70a5376b2, 87adfee40). I'm reading D335, D337 and J146's report here.

Plan, about 3 h plus the Mac lock queue:

  1. Static review: the __session-shell entry and launchctl asuser placement, the plain-shell fallback, socket and file modes, the environment allowlist, the kqueue EINVAL fix, the restart path, and the Zig pins, nix FOD and release builder. (Done.)
  2. Mac proof under lockf -k /tmp/mac-bench.lock (queued behind another job). A build of 6363a04c4, then a probe run as a launchd system job (the production audit and bootstrap context, not sudo from an SSH login). It checks what each person's shell inherits: descriptors, DYLD_* and other variables, the audit user and session, and the launchd manager, before and after an infraternal agent restart. Plus direct escalation attempts as the SSH-only claude account.
  3. Verdict with P1/P2/P3 and evidence.

Already checked: scripts/zig.sha256 matches ziglang.org's index.json for 0.15.2 on all three hosts (x86_64-linux 02aa270f…, aarch64-linux 958ed7d1…, aarch64-macos 3cc2bab3…).

**J180 start: security review of J146** (Claude Opus 5.5, 2026-11-04 ~10:00 IST). Review only. I don't change any branch. Scope: `3e3c84384..6363a04c4` (J146) plus J136's build and pinning commits (`2bbd212e9`, `70a5376b2`, `87adfee40`). I'm reading D335, D337 and J146's report here. Plan, about 3 h plus the Mac lock queue: 1. Static review: the `__session-shell` entry and `launchctl asuser` placement, the plain-shell fallback, socket and file modes, the environment allowlist, the kqueue `EINVAL` fix, the restart path, and the Zig pins, nix FOD and release builder. (Done.) 2. Mac proof under `lockf -k /tmp/mac-bench.lock` (queued behind another job). A build of `6363a04c4`, then a probe run as a **launchd system job** (the production audit and bootstrap context, not `sudo` from an SSH login). It checks what each person's shell inherits: descriptors, `DYLD_*` and other variables, the audit user and session, and the launchd manager, before and after an infraternal agent restart. Plus direct escalation attempts as the SSH-only `claude` account. 3. Verdict with P1/P2/P3 and evidence. Already checked: `scripts/zig.sha256` matches ziglang.org's `index.json` for 0.15.2 on all three hosts (x86_64-linux `02aa270f…`, aarch64-linux `958ed7d1…`, aarch64-macos `3cc2bab3…`).
Author
Owner

J180 security review of J146 (3e3c84384..6363a04c4) and J136's build pins: REQUEST CHANGES (1 × P1, 1 × P2, 3 × P3)

Proof setup: Mac VM, macOS 26.5.2, under lockf -k /tmp/mac-bench.lock (taken 02:23 PST, released 02:48 PST). A dev build of 6363a04c4 (default features minus plugin-mesh, as J146 used; sha256 3cf70201…), installed root-owned. The probe ran as a launchd system job (launchctl bootstrap system <plist>, then bootout), so the infraternal agent had the audit and bootstrap context of a LaunchDaemon. J146's proof started it with sudo from an SSH login, and that changes the result (P2 below). The agent was started with descriptor 9 open on a root-only file (0600 root, contents ROOT-ONLY-J180-SECRET-MARKER) and with DYLD_LIBRARY_PATH, DYLD_FALLBACK_LIBRARY_PATH, BASH_ENV, ENV and a marker variable in its environment. Full log attached.

P1-1: no descriptor sweep between the root infraternal agent and the person's shell

Nothing in the agent → shim → launchctl asuser → __session-shell → shell chain closes descriptors ≥ 3 (there is no closefrom, /dev/fd walk or POSIX_SPAWN_CLOEXEC_DEFAULT anywhere in the tree). Any descriptor without CLOEXEC in the agent therefore ends up in every person's shell and in every root shim, and stays there for the shell's whole life:

--- GUI uid 501 ---            lume / Aqua
FD9=ROOT-ONLY-J180-SECRET-MARKER          <- lume read the root-only file
--- lsof shell GUI pid 46812 ---
zsh 46812 lume    9r REG /private/tmp/j180root/rootonly
--- lsof shell PLAIN pid 46859 ---
zsh 46859 claude  9r REG /private/tmp/j180root/rootonly
--- lsof shim (46810, 46855) ---  9r REG .../rootonly   (also in both root shims)
after the infraternal agent restart: still 0 1 2 9 10 in both shells

I planted the descriptor to make the gap deterministic. launchd itself starts daemons with only 0–2. The production path is a race: macOS has no SOCK_CLOEXEC or pipe2, so Rust std and mio sockets and pipes get CLOEXEC set by a second syscall. The agent is multi-threaded tokio, and it forks shims (Command with pre_exec, so fork+exec, not posix_spawn) while other threads connect to shim sockets, plugins.sock and leg B. A descriptor that slips through that window is a root-authenticated connection: peer credentials are recorded at connect time. Handed to a person's shell, it would let them talk to another person's shim, or to the plugin service, as root. Linux is not exposed to the race, because its CLOEXEC flags are atomic.
Fix: in session_shell::prepare, while still root and single-threaded, close every descriptor above 2 (walk /dev/fd, or use proc_pidinfo(PROC_PIDLISTFDS)) before the privilege drop. Also spawn the shim's child, and the agent's shim launch, with POSIX_SPAWN_CLOEXEC_DEFAULT (or a matching sweep in the double fork) so that root shims don't hold strays either. Add a proof row that plants a descriptor, as above.

P2-1: under a real LaunchDaemon, the plain shell runs in the root daemon's audit session and the System bootstrap

--- daemon context ---                      uid=0    asid=100001 auid=4294967295 flags=0x1  System
--- PLAIN uid 1500 (claude, no GUI login) --- uid=1500 asid=100001 auid=4294967295 flags=0x1  System
--- launchctl asuser 1500 from the daemon --- uid=0    asid=100062 auid=1500       flags=0x0  Background

The plain fallback (D335's "plain shell started by the root daemon as their user") inherits the daemon's system audit session and launchd bootstrap. Every person's plain shell then shares one security session with the root daemon and with each other, and their audit records have no audit user, so actions can't be attributed to the person. J146's proof showed Background and auid=501 only because it started the agent with sudo from lume's SSH login. check_plain's Background assertion would fail in production. This answers J146's open question: yes, the plain shell needs its own session.
Fix (measured above): run the plain shell through /bin/launchctl asuser <uid> too. From the daemon, that already gives a fresh audit session with auid=<uid> and the user's Background bootstrap. In the entry, require auid == uid for both outcomes; graphic access then only decides GUI versus plain. This also closes a smaller gap: today, if the daemon itself sits in a GUI audit session with auid == uid (a developer sudo from Terminal), a "plain" shell is classified as placed and takes SSH_AUTH_SOCK from that bootstrap. Run the proof as a launchd system job, as here.

P3-1: ZDOTDIR from the root agent's environment reaches every person's shell

darwin_command and session_shell::PASSED forward ZDOTDIR unchecked. In the proof, lume's login shell ran with ZDOTDIR=/Users/claude/worktrees/j180/crates/infraternal/tests/profile, a directory claude owns. That is a cross-user path for startup code. A production plist sets no ZDOTDIR, so this is P3. Forward it only in test builds, or only when the directory is owned by root or by the target uid and not group- or world-writable. (Linux linux_command has the same pass-through, which predates this slice.)

P3-2: INFRATERNAL_ZIG bypasses the checksum

scripts/auth-toolchain copies any directory named by INFRATERNAL_ZIG whose zig version prints 0.15.2, with no hash or store-path check. The pin holds only because the nix dev shell sets that variable from the FOD. Require a /nix/store/ path (or hash the copied tree) so that a stray variable in a CI or release environment can't swap the compiler.

P3-3: proof hygiene

test-macos-sessions.py should run the infraternal agent from launchd (see P2-1) and plant a descriptor (see P1-1). Otherwise it proves the sudo context rather than the production one.

Checked and fine

  • Escalation as the SSH-only claude: launchctl asuser 501 id → Operation not permitted. __session-shell --uid 501 from a fresh PTY session → permission denied. Without a PTY → not a terminal session leader. ls /var/run/j180-probe → Permission denied. A non-GUI user cannot reach the GUI user's keychain or session. Placement is decided by the root shim for the target uid only, and verified from the audit record (asid=100003 auid=501 flags=0x2030, the same as gui/501).
  • Privilege drop: setlogin, initgroups, setgid, setuid, a setuid(0) re-check, and chdir after the drop. launchctl getenv and the audit check also run after the drop. SSH_AUTH_SOCK is accepted only as a socket owned by the uid.
  • Environment: DYLD_*, BASH_ENV, ENV and the marker were stripped from both shells. The entry gets only TERM, COLORTERM, INFRATERNAL_SESSION, ZDOTDIR and locale, plus fixed PATH, HOME, USER, LOGNAME, SHELL and TMPDIR.
  • Sockets: state and sessions/ are drwx------ root, shim sockets are srw------- root, and the peer must be uid 0 or the shim's uid (getpeereid).
  • Restart: both shells answered with the same pids after an abrupt agent stop and restart, no new descriptors appeared in the shells, and the census dropped both on exit (PROBE_DONE). Shim descriptors were 0–2 on /dev/null, a kqueue, the socket and ptmx (plus the planted 9, see P1). PT_DENY_ATTACH was verified by J146.
  • kqueue EINVAL fix: confirmed on the Mac. Registering a kqueue descriptor in another kqueue gives READ ok and WRITE [Errno 22] Invalid argument. Only the Kqueue source gets read-only interest. OwnedFd, UnixStream, epoll and inotify are unchanged.
  • J136 pins: scripts/zig.sha256 matches ziglang.org index.json for 0.15.2 on all three hosts. nix/zig.nix and nix/package.nix fetch through fetchurl with that table (FOD). build-vt keeps its sha256 check for libghostty-vt and fails closed when sha256sum is missing. .cargo/apple-cc.sh only strips the nix SDK and flags.
  • Release builder: neither J136 nor J146 touches the release builder, signer or release_verify, and nothing in the stack produces or feeds a macOS release artifact. So this stack opens no path for unsigned artifacts into the release builder. A future macOS release lane needs its own review.

Not reviewed: J146's Linux verify fast is still test FAIL (101) by its own report. It needs a green gate before merge, independent of this review.

Cleanup: the launchd job is booted out, /var/run/j180-probe and the root install are removed, the Mac worktree and its target are deleted, and the lock is released. No branch was changed.

**J180 security review of J146 (`3e3c84384..6363a04c4`) and J136's build pins: REQUEST CHANGES** (1 × P1, 1 × P2, 3 × P3) Proof setup: Mac VM, macOS 26.5.2, under `lockf -k /tmp/mac-bench.lock` (taken 02:23 PST, released 02:48 PST). A dev build of `6363a04c4` (default features minus `plugin-mesh`, as J146 used; sha256 `3cf70201…`), installed root-owned. **The probe ran as a launchd system job** (`launchctl bootstrap system <plist>`, then `bootout`), so the infraternal agent had the audit and bootstrap context of a LaunchDaemon. J146's proof started it with `sudo` from an SSH login, and that changes the result (P2 below). The agent was started with descriptor 9 open on a root-only file (`0600 root`, contents `ROOT-ONLY-J180-SECRET-MARKER`) and with `DYLD_LIBRARY_PATH`, `DYLD_FALLBACK_LIBRARY_PATH`, `BASH_ENV`, `ENV` and a marker variable in its environment. Full log attached. ### P1-1: no descriptor sweep between the root infraternal agent and the person's shell Nothing in the agent → shim → `launchctl asuser` → `__session-shell` → shell chain closes descriptors ≥ 3 (there is no `closefrom`, `/dev/fd` walk or `POSIX_SPAWN_CLOEXEC_DEFAULT` anywhere in the tree). Any descriptor without CLOEXEC in the agent therefore ends up in **every** person's shell and in every root shim, and stays there for the shell's whole life: ``` --- GUI uid 501 --- lume / Aqua FD9=ROOT-ONLY-J180-SECRET-MARKER <- lume read the root-only file --- lsof shell GUI pid 46812 --- zsh 46812 lume 9r REG /private/tmp/j180root/rootonly --- lsof shell PLAIN pid 46859 --- zsh 46859 claude 9r REG /private/tmp/j180root/rootonly --- lsof shim (46810, 46855) --- 9r REG .../rootonly (also in both root shims) after the infraternal agent restart: still 0 1 2 9 10 in both shells ``` I planted the descriptor to make the gap deterministic. launchd itself starts daemons with only 0–2. The production path is a race: macOS has no `SOCK_CLOEXEC` or `pipe2`, so Rust std and mio sockets and pipes get CLOEXEC set by a second syscall. The agent is multi-threaded tokio, and it forks shims (`Command` with `pre_exec`, so fork+exec, not posix_spawn) while other threads connect to shim sockets, `plugins.sock` and leg B. A descriptor that slips through that window is a **root-authenticated** connection: peer credentials are recorded at connect time. Handed to a person's shell, it would let them talk to another person's shim, or to the plugin service, as root. Linux is not exposed to the race, because its CLOEXEC flags are atomic. **Fix:** in `session_shell::prepare`, while still root and single-threaded, close every descriptor above 2 (walk `/dev/fd`, or use `proc_pidinfo(PROC_PIDLISTFDS)`) before the privilege drop. Also spawn the shim's child, and the agent's shim launch, with `POSIX_SPAWN_CLOEXEC_DEFAULT` (or a matching sweep in the double fork) so that root shims don't hold strays either. Add a proof row that plants a descriptor, as above. ### P2-1: under a real LaunchDaemon, the plain shell runs in the root daemon's audit session and the System bootstrap ``` --- daemon context --- uid=0 asid=100001 auid=4294967295 flags=0x1 System --- PLAIN uid 1500 (claude, no GUI login) --- uid=1500 asid=100001 auid=4294967295 flags=0x1 System --- launchctl asuser 1500 from the daemon --- uid=0 asid=100062 auid=1500 flags=0x0 Background ``` The plain fallback (D335's "plain shell started by the root daemon as their user") inherits the daemon's **system** audit session and launchd bootstrap. Every person's plain shell then shares one security session with the root daemon and with each other, and their audit records have no audit user, so actions can't be attributed to the person. J146's proof showed `Background` and `auid=501` only because it started the agent with `sudo` from lume's SSH login. `check_plain`'s `Background` assertion would **fail** in production. This answers J146's open question: yes, the plain shell needs its own session. **Fix (measured above):** run the plain shell through `/bin/launchctl asuser <uid>` too. From the daemon, that already gives a fresh audit session with `auid=<uid>` and the user's `Background` bootstrap. In the entry, require `auid == uid` for both outcomes; graphic access then only decides GUI versus plain. This also closes a smaller gap: today, if the daemon itself sits in a GUI audit session with `auid == uid` (a developer `sudo` from Terminal), a "plain" shell is classified as placed and takes `SSH_AUTH_SOCK` from that bootstrap. Run the proof as a launchd system job, as here. ### P3-1: `ZDOTDIR` from the root agent's environment reaches every person's shell `darwin_command` and `session_shell::PASSED` forward `ZDOTDIR` unchecked. In the proof, **lume's** login shell ran with `ZDOTDIR=/Users/claude/worktrees/j180/crates/infraternal/tests/profile`, a directory `claude` owns. That is a cross-user path for startup code. A production plist sets no `ZDOTDIR`, so this is P3. Forward it only in test builds, or only when the directory is owned by root or by the target uid and not group- or world-writable. (Linux `linux_command` has the same pass-through, which predates this slice.) ### P3-2: `INFRATERNAL_ZIG` bypasses the checksum `scripts/auth-toolchain` copies any directory named by `INFRATERNAL_ZIG` whose `zig version` prints `0.15.2`, with no hash or store-path check. The pin holds only because the nix dev shell sets that variable from the FOD. Require a `/nix/store/` path (or hash the copied tree) so that a stray variable in a CI or release environment can't swap the compiler. ### P3-3: proof hygiene `test-macos-sessions.py` should run the infraternal agent from launchd (see P2-1) and plant a descriptor (see P1-1). Otherwise it proves the sudo context rather than the production one. ### Checked and fine - **Escalation as the SSH-only `claude`:** `launchctl asuser 501 id` → `Operation not permitted`. `__session-shell --uid 501` from a fresh PTY session → `permission denied`. Without a PTY → `not a terminal session leader`. `ls /var/run/j180-probe` → `Permission denied`. A non-GUI user cannot reach the GUI user's keychain or session. Placement is decided by the root shim for the target uid only, and verified from the audit record (`asid=100003 auid=501 flags=0x2030`, the same as `gui/501`). - **Privilege drop:** setlogin, initgroups, setgid, setuid, a `setuid(0)` re-check, and `chdir` after the drop. `launchctl getenv` and the audit check also run after the drop. `SSH_AUTH_SOCK` is accepted only as a socket owned by the uid. - **Environment:** `DYLD_*`, `BASH_ENV`, `ENV` and the marker were stripped from both shells. The entry gets only `TERM`, `COLORTERM`, `INFRATERNAL_SESSION`, `ZDOTDIR` and locale, plus fixed `PATH`, `HOME`, `USER`, `LOGNAME`, `SHELL` and `TMPDIR`. - **Sockets:** `state` and `sessions/` are `drwx------ root`, shim sockets are `srw------- root`, and the peer must be uid 0 or the shim's uid (`getpeereid`). - **Restart:** both shells answered with the same pids after an abrupt agent stop and restart, no new descriptors appeared in the shells, and the census dropped both on exit (`PROBE_DONE`). Shim descriptors were 0–2 on `/dev/null`, a kqueue, the socket and `ptmx` (plus the planted 9, see P1). `PT_DENY_ATTACH` was verified by J146. - **kqueue `EINVAL` fix:** confirmed on the Mac. Registering a kqueue descriptor in another kqueue gives `READ ok` and `WRITE [Errno 22] Invalid argument`. Only the `Kqueue` source gets read-only interest. `OwnedFd`, `UnixStream`, epoll and inotify are unchanged. - **J136 pins:** `scripts/zig.sha256` matches ziglang.org `index.json` for 0.15.2 on all three hosts. `nix/zig.nix` and `nix/package.nix` fetch through `fetchurl` with that table (FOD). `build-vt` keeps its sha256 check for libghostty-vt and fails closed when `sha256sum` is missing. `.cargo/apple-cc.sh` only strips the nix SDK and flags. - **Release builder:** neither J136 nor J146 touches the release builder, signer or `release_verify`, and nothing in the stack produces or feeds a macOS release artifact. So this stack opens no path for unsigned artifacts into the release builder. A future macOS release lane needs its own review. Not reviewed: J146's Linux `verify fast` is still `test` FAIL (101) by its own report. It needs a green gate before merge, independent of this review. Cleanup: the launchd job is booted out, `/var/run/j180-probe` and the root install are removed, the Mac worktree and its target are deleted, and the lock is released. No branch was changed.
Author
Owner

J146 round 2 progress (J180 fixes + rebase onto dev). macos-sessions-j146 is now rebased onto origin/dev (which has #265 merged as c4bc39984), via git rebase --onto origin/dev 70a5376b2: J136's 7 commits, my 7, then the new ones. Pushed head: 4b0df9f1a.

New commits (each with a test that failed first):

  • 26d7ffd62 P1: only the standard descriptors reach shims and shells. The allowlist hook is close_range(CLOSE_RANGE_CLOEXEC) on Linux and PROC_PIDLISTFDS on macOS. The macOS entry closes everything above 2 while still root. Linux had the same leak for descriptors inherited without CLOEXEC, and it is fixed too (shell builders plus the agent's shim launch). Linux red→green: with the hook removed from linux_command, a_pty_program_inherits_only_its_terminal prints DESCRIPTOR-LEAKED; with it, it passes 3/3.
  • e477fb516 P2: a plain macOS shell runs through launchctl asuser too, and its own audit record must name the person (classify).
  • 5de3947e0 P3-1: ZDOTDIR is never forwarded on macOS. On Linux it is forwarded only from a directory the person or root controls. The entry's environment allowlist is audited (TERM, COLORTERM, the marker and locale). Linux red→green for startup_directory_must_belong_to_the_person_or_root.
  • 6783afa7a P3-2: INFRATERNAL_ZIG must be the nix-store --verify-path-verified output of nix/zig.nix. scripts/test-auth-toolchain-pin fails against the old script ("accepted a look-alike") and passes now.
  • 7cb5a06ef P3-3: the proof runs the agent as a launchd system job and plants descriptor 9 (a root-only file without CLOEXEC) plus DYLD_LIBRARY_PATH, BASH_ENV, ENV, ZDOTDIR and a marker.
  • b3328e0bd docs (D345 ratchet), and 4b0df9f1a: the rebased agent died on EINTR from kqueue drains once dev's #324 launch wait handled SIGCHLD; readiness::kevent_now retries.

Rebase resolutions: main.rs takes dev's run_up plus J136's Linux gate for the SSH session. sessions.rs keeps dev's #324 await_launch/wait_for_hello, made portable through SocketReadiness, with LAUNCH unchanged on macOS; dev's new #324 tests use SocketReadiness::new. pty.rs adds the P1 helpers next to dev's proxy child; dev's pty::prepare_proxy_child is gated Linux-only like its public wrapper (macOS refuses earlier, and nix has no Darwin setgroups). build-vt now drops the shared libghostty-vt after the build: dev's new image check (J80e) caught a debug test binary loading @rpath/libghostty-vt.dylib.

Mac, as a launchd system job at 4b0df9f1a:

--- infraternal agent (launchd system job) ---   uid=0 asid=100001 auid=4294967295 flags=0x1  System
--- lume (GUI) ---  Aqua  SOCK=/var/run/com.apple.launchd.3nPAot2s0k/Listeners  FD9=closed  PLANTED=
                    uid=501 asid=100003 auid=501 flags=0x2030
--- claude (no GUI) --- Background  SOCK=  FD9=closed  PLANTED=
                    uid=1500 asid=100062 auid=1500 flags=0x0
PASS: shells open, take input and return output; D335 placement holds
PASS: no shell or shim holds the planted root descriptor; no planted variable reached a shell
PASS: both shells survive an infraternal agent restart (pids 95263, 95281)
PASS: both shells survive an upgrade of the infraternal agent; placement unchanged
PASS: shell exits reach the infraternal agent (kqueue exit watches); the census drops both
PROOF_EXIT=0

Red on the old head 6363a04c4, same harness: lume's shell has FD9=open and PLANTED=/private/tmp/j146-planted-zdotdir. At the rebased head: macos-build-check: PASS (MACOS_CHECK_EXIT=0), and Mac clippy -D warnings is clean for shim and agent.

Linux: the gate at 84cd599fc failed only its test row, on 3 server restart tests that timed out at 60 s (restart_tests.rs:167) on a loaded dev-02, where the test row took 1747 s against about 860 s before. I'm running those tests at origin/dev and at my head to tell a flake from a regression, then the final verify fast on the final head. The dev-02 queue is long (position 3); I keep polling.

**J146 round 2 progress (J180 fixes + rebase onto dev).** `macos-sessions-j146` is now **rebased onto origin/dev** (which has #265 merged as `c4bc39984`), via `git rebase --onto origin/dev 70a5376b2`: J136's 7 commits, my 7, then the new ones. Pushed head: `4b0df9f1a`. New commits (each with a test that failed first): - `26d7ffd62` **P1:** only the standard descriptors reach shims and shells. The allowlist hook is `close_range(CLOSE_RANGE_CLOEXEC)` on Linux and `PROC_PIDLISTFDS` on macOS. The macOS entry closes everything above 2 while still root. **Linux had the same leak** for descriptors inherited without CLOEXEC, and it is fixed too (shell builders plus the agent's shim launch). Linux red→green: with the hook removed from `linux_command`, `a_pty_program_inherits_only_its_terminal` prints `DESCRIPTOR-LEAKED`; with it, it passes 3/3. - `e477fb516` **P2:** a plain macOS shell runs through `launchctl asuser` too, and its own audit record must name the person (`classify`). - `5de3947e0` **P3-1:** ZDOTDIR is never forwarded on macOS. On Linux it is forwarded only from a directory the person or root controls. The entry's environment allowlist is audited (TERM, COLORTERM, the marker and locale). Linux red→green for `startup_directory_must_belong_to_the_person_or_root`. - `6783afa7a` **P3-2:** INFRATERNAL_ZIG must be the `nix-store --verify-path`-verified output of nix/zig.nix. `scripts/test-auth-toolchain-pin` fails against the old script ("accepted a look-alike") and passes now. - `7cb5a06ef` **P3-3:** the proof runs the agent as a **launchd system job** and plants descriptor 9 (a root-only file without CLOEXEC) plus DYLD_LIBRARY_PATH, BASH_ENV, ENV, ZDOTDIR and a marker. - `b3328e0bd` docs (D345 ratchet), and `4b0df9f1a`: the rebased agent died on EINTR from kqueue drains once dev's #324 launch wait handled SIGCHLD; `readiness::kevent_now` retries. Rebase resolutions: `main.rs` takes dev's `run_up` plus J136's Linux gate for the SSH session. `sessions.rs` keeps dev's #324 `await_launch`/`wait_for_hello`, made portable through `SocketReadiness`, with LAUNCH unchanged on macOS; dev's new #324 tests use `SocketReadiness::new`. `pty.rs` adds the P1 helpers next to dev's proxy child; dev's `pty::prepare_proxy_child` is gated Linux-only like its public wrapper (macOS refuses earlier, and nix has no Darwin `setgroups`). `build-vt` now drops the shared libghostty-vt after the build: dev's new image check (J80e) caught a debug test binary loading `@rpath/libghostty-vt.dylib`. **Mac, as a launchd system job at `4b0df9f1a`:** ``` --- infraternal agent (launchd system job) --- uid=0 asid=100001 auid=4294967295 flags=0x1 System --- lume (GUI) --- Aqua SOCK=/var/run/com.apple.launchd.3nPAot2s0k/Listeners FD9=closed PLANTED= uid=501 asid=100003 auid=501 flags=0x2030 --- claude (no GUI) --- Background SOCK= FD9=closed PLANTED= uid=1500 asid=100062 auid=1500 flags=0x0 PASS: shells open, take input and return output; D335 placement holds PASS: no shell or shim holds the planted root descriptor; no planted variable reached a shell PASS: both shells survive an infraternal agent restart (pids 95263, 95281) PASS: both shells survive an upgrade of the infraternal agent; placement unchanged PASS: shell exits reach the infraternal agent (kqueue exit watches); the census drops both PROOF_EXIT=0 ``` Red on the old head `6363a04c4`, same harness: lume's shell has `FD9=open` and `PLANTED=/private/tmp/j146-planted-zdotdir`. At the rebased head: `macos-build-check: PASS` (`MACOS_CHECK_EXIT=0`), and Mac clippy `-D warnings` is clean for shim and agent. **Linux:** the gate at `84cd599fc` failed only its `test` row, on 3 server **restart** tests that timed out at 60 s (`restart_tests.rs:167`) on a loaded dev-02, where the test row took 1747 s against about 860 s before. I'm running those tests at origin/dev and at my head to tell a flake from a regression, then the final `verify fast` on the final head. The dev-02 queue is long (position 3); I keep polling.
Author
Owner

J146 round 2: the Linux gate at the final head 4b0df9f1a. Every row is green except test, which hit a different load flake in each run.

scripts/verify fast on infraternal-dev-02-std at 4b0df9f1a4c4fcb23ebc08076c6d5e9437b89fc4 (run 2, verbatim):

+---------------------------+--------------+
| Rust gate                 | Result       |
+---------------------------+--------------+
| fmt                       | PASS         |
| VT native fmt             | PASS         |
| CLI plugin compile-out    | PASS         |
| clippy                    | PASS         |
| SSH runtime test helper   | PASS         |
| real-binary fixture paths | PASS         |
| Traefik component manifest | PASS         |
| release component publication | PASS         |
| test                      | FAIL (101)   |
| SSH inode reuse fixture   | PASS         |
| shim process-per-user     | PASS         |
| cargo-deny                | PASS         |
| gate architecture         | PASS         |
| doc coverage ratchet      | PASS         |
| static builds             | PASS         |
+---------------------------+--------------+
VERIFY_EXIT=1

The test failures are one test per run, a different one each time. None is in code this stack changes for Linux, and each passes when repeated alone:

run failing test standalone, same host
84cd599fc 3 × infraternal restart tests (60 s deadline, test row 1747 s) at 4b0df9f1a: run 1 FAILED 5/19 (load 16.9), run 2 ok 19/19; dev 070c9a0b6: 2/2 ok (load 8.6–8.8)
4b0df9f1a run 1 proxy::traefik_child_receives_only_credential_file_paths_and_redacts_its_logs (the test reads its capture file as soon as it exists, before the fake child has written it) dev 81f938165 5/5 ok, head 5/5 ok
4b0df9f1a run 2 release::pending_channel_writers_preserve_authority_read_capacity (SQLx PoolTimedOut) queued next (dev vs head, 5 runs each)

The restart tests passed inside the 4b0df9f1a gate runs. Linux static sizes at 4b0df9f1a: x86_64 23,366,832 B, aarch64 21,247,104 B (on dev, so not comparable with the old base). A third full gate at the same SHA is queued at position 7 on dev-02 (compute01 is no shorter); I keep polling in the foreground and will post its table verbatim.

**J146 round 2: the Linux gate at the final head `4b0df9f1a`. Every row is green except `test`, which hit a different load flake in each run.** `scripts/verify fast` on infraternal-dev-02-std at `4b0df9f1a4c4fcb23ebc08076c6d5e9437b89fc4` (run 2, verbatim): ``` +---------------------------+--------------+ | Rust gate | Result | +---------------------------+--------------+ | fmt | PASS | | VT native fmt | PASS | | CLI plugin compile-out | PASS | | clippy | PASS | | SSH runtime test helper | PASS | | real-binary fixture paths | PASS | | Traefik component manifest | PASS | | release component publication | PASS | | test | FAIL (101) | | SSH inode reuse fixture | PASS | | shim process-per-user | PASS | | cargo-deny | PASS | | gate architecture | PASS | | doc coverage ratchet | PASS | | static builds | PASS | +---------------------------+--------------+ VERIFY_EXIT=1 ``` The `test` failures are one test per run, a different one each time. None is in code this stack changes for Linux, and each passes when repeated alone: | run | failing test | standalone, same host | |---|---|---| | `84cd599fc` | 3 × `infraternal` restart tests (60 s deadline, test row 1747 s) | at `4b0df9f1a`: run 1 FAILED 5/19 (load 16.9), run 2 ok 19/19; dev `070c9a0b6`: 2/2 ok (load 8.6–8.8) | | `4b0df9f1a` run 1 | `proxy::traefik_child_receives_only_credential_file_paths_and_redacts_its_logs` (the test reads its capture file as soon as it exists, before the fake child has written it) | dev `81f938165` 5/5 ok, head 5/5 ok | | `4b0df9f1a` run 2 | `release::pending_channel_writers_preserve_authority_read_capacity` (SQLx `PoolTimedOut`) | queued next (dev vs head, 5 runs each) | The `restart` tests passed inside the `4b0df9f1a` gate runs. Linux static sizes at `4b0df9f1a`: x86_64 23,366,832 B, aarch64 21,247,104 B (on dev, so not comparable with the old base). A third full gate at the same SHA is queued at **position 7** on dev-02 (compute01 is no shorter); I keep polling in the foreground and will post its table verbatim.
Author
Owner

J146 round 2 finish: J180's P1, P2 and P3s fixed, rebased onto dev, Mac proof green as a launchd job. The Linux gate is not green: only test fails, on a different load-sensitive test each run.

Branch macos-sessions-j146, head 4b0df9f1a4c4fcb23ebc08076c6d5e9437b89fc4, rebased onto origin/dev 843ec7eec (which contains the #265 merge c4bc39984) with git rebase --onto origin/dev 70a5376b2. That is J136's 7 commits, my 7, and 7 new ones. Not merged, not deployed.

New commits:

  • 26d7ffd62 P1, descriptors. Shells and shims inherit only 0–2 (an allowlist: close_range(CLOSE_RANGE_CLOEXEC) on Linux, PROC_PIDLISTFDS on macOS, applied in all four PTY builders and in the agent's shim launch). The macOS entry closes everything above 2 while still root. Linux had the same leak for descriptors inherited without CLOEXEC, and it is fixed by the same hooks.
  • e477fb516 P2. Plain shells also go through launchctl asuser, and the entry requires its own audit user (classify).
  • 5de3947e0 P3-1. ZDOTDIR is never forwarded on macOS; on Linux only from a root- or person-owned directory. The entry's environment allowlist is audited.
  • 6783afa7a P3-2. INFRATERNAL_ZIG must be the nix-store --verify-path-verified nix/zig.nix output. New gate check: scripts/test-auth-toolchain-pin.
  • 7cb5a06ef P3-3. The proof runs the agent as a launchd system job and plants descriptor 9 plus five variables.
  • b3328e0bd D345 docs for every new item (the doc-coverage ratchet now passes).
  • 4b0df9f1a After the rebase, dev's #324 launch wait handles SIGCHLD, and macOS kevent returned EINTR, which ended the agent. readiness::kevent_now retries.
  • Folded in during the rebase: dev's pty::prepare_proxy_child gated Linux-only like its public wrapper; LAUNCH portable; build-vt drops the shared libghostty-vt (J80e's new image check caught a test binary loading @rpath/libghostty-vt.dylib).

Tests that failed first:

  • P1, Linux: with the hook removed, a_pty_program_inherits_only_its_terminal prints DESCRIPTOR-LEAKED; with it, it passes 3/3.
  • P3-1, Linux: startup_directory_must_belong_to_the_person_or_root is red with the check stubbed, green 3/3.
  • P3-2: "FAIL: auth-toolchain accepted a look-alike INFRATERNAL_ZIG" against the old script, then PASS.
  • P2, P3-1 environment, P3-3: the launchd proof below goes red on 6363a04c4.
  • New unit tests: asuser_shells_run_only_in_the_persons_own_session, rootless_shells_keep_the_callers_session, only_terminal_and_locale_variables_are_forwarded, only_standard_descriptors_reach_the_child.

Mac, as a launchd system job, under lockf -k /tmp/mac-bench.lock (proof3.log and proof2.log attached):

RED, old head 6363a04c4:  lume: FD9=open  PLANTED=/private/tmp/j146-planted-zdotdir  -> PROOF_EXIT=1
GREEN, 4b0df9f1a (sha256 8d6f9aa6…, upgrade to bc6369b7…):
--- infraternal agent (launchd system job) ---  uid=0 asid=100001 auid=4294967295 flags=0x1 System
--- lume (GUI) ---    Aqua  SOCK=/var/run/com.apple.launchd.3nPAot2s0k/Listeners  FD9=closed  PLANTED=
                      uid=501 euid=501 asid=100003 auid=501 flags=0x2030
--- claude (plain) --- Background  SOCK=  FD9=closed  PLANTED=
                      uid=1500 euid=1500 asid=100062 auid=1500 flags=0x0
PASS: shells open, take input and return output; D335 placement holds
PASS: no shell or shim holds the planted root descriptor; no planted variable reached a shell
PASS: both shells survive an infraternal agent restart (pids 95263, 95281)
PASS: both shells survive an upgrade of the infraternal agent; placement unchanged
PASS: shell exits reach the infraternal agent (kqueue exit watches); the census drops both
PROOF_EXIT=0

Also at 4b0df9f1a: macos-build-check: PASS (MACOS_CHECK_EXIT=0), and Mac clippy -D warnings is clean for shim and agent.

Linux scripts/verify fast on infraternal-dev-02-std at 4b0df9f1a (run 3, verbatim; runs 2 and 3 attached):

+---------------------------+--------------+
| Rust gate                 | Result       |
+---------------------------+--------------+
| fmt                       | PASS         |
| VT native fmt             | PASS         |
| CLI plugin compile-out    | PASS         |
| clippy                    | PASS         |
| SSH runtime test helper   | PASS         |
| real-binary fixture paths | PASS         |
| Traefik component manifest | PASS         |
| release component publication | PASS         |
| test                      | FAIL (101)   |
| SSH inode reuse fixture   | PASS         |
| shim process-per-user     | PASS         |
| cargo-deny                | PASS         |
| gate architecture         | PASS         |
| doc coverage ratchet      | PASS         |
| static builds             | PASS         |
+---------------------------+--------------+
VERIFY_EXIT=1

Not green. Across the four gate runs at 84cd599fc/4b0df9f1a, every other row passed every time. test failed each time on a different test, none of which this stack changes for Linux:

run failing test standalone on the same host
84cd599fc 3 infraternal restart tests, 60 s deadline (test row took 1747 s) head: 1 FAILED (load 16.9), 1 ok; dev: 2/2 ok
4b0df9f1a #1 proxy traefik_child_receives_only_credential_file_paths… (reads its capture file before the fake child writes it) dev 5/5, head 5/5 ok
4b0df9f1a #2 release pending_channel_writers_preserve_authority_read_capacity (SQLx PoolTimedOut) dev 5/5, head 5/5 ok
4b0df9f1a #3 restart::…::relative_recovery_after_atomic_install_keeps_live_install_path: ETXTBSY, Text file busy (an exec racing a concurrent fork) n/a

The restart tests passed inside gate runs 1 and 2 at 4b0df9f1a. A green verify fast needs either a quieter host or fixes to those four tests in their own lanes. I did not mark this green.

Linux static sizes, same host and target directory, against the rebase base 843ec7eec:

target base 843ec7eec head 4b0df9f1a delta
x86_64-unknown-linux-musl 23,364,272 B 23,366,832 B +2,560 B
aarch64-unknown-linux-musl 21,244,736 B 21,247,104 B +2,368 B

For the orchestrator / kayg:

  • The dev-02 test row is flaky under load across unrelated crates. The orchestrator's merge gate should rerun it.
  • Still open from round 1: does "logged in" mean a GUI login only (the current safe default) or any login, SSH included?

Cleanup: the Mac test install, launchd job, state, worktrees and target are removed and the lock is released. The dev-02 target is removed. Logs are kept in ~/j146-out (dev-02) and ~/j146-evidence (Mac).

**J146 round 2 finish: J180's P1, P2 and P3s fixed, rebased onto dev, Mac proof green as a launchd job. The Linux gate is not green: only `test` fails, on a different load-sensitive test each run.** Branch `macos-sessions-j146`, **head `4b0df9f1a4c4fcb23ebc08076c6d5e9437b89fc4`**, rebased onto origin/dev `843ec7eec` (which contains the #265 merge `c4bc39984`) with `git rebase --onto origin/dev 70a5376b2`. That is J136's 7 commits, my 7, and 7 new ones. Not merged, not deployed. New commits: - `26d7ffd62` **P1, descriptors.** Shells and shims inherit only 0–2 (an allowlist: `close_range(CLOSE_RANGE_CLOEXEC)` on Linux, `PROC_PIDLISTFDS` on macOS, applied in all four PTY builders and in the agent's shim launch). The macOS entry closes everything above 2 while still root. **Linux had the same leak** for descriptors inherited without CLOEXEC, and it is fixed by the same hooks. - `e477fb516` **P2.** Plain shells also go through `launchctl asuser`, and the entry requires its own audit user (`classify`). - `5de3947e0` **P3-1.** ZDOTDIR is never forwarded on macOS; on Linux only from a root- or person-owned directory. The entry's environment allowlist is audited. - `6783afa7a` **P3-2.** INFRATERNAL_ZIG must be the `nix-store --verify-path`-verified nix/zig.nix output. New gate check: `scripts/test-auth-toolchain-pin`. - `7cb5a06ef` **P3-3.** The proof runs the agent as a launchd system job and plants descriptor 9 plus five variables. - `b3328e0bd` **D345 docs** for every new item (the doc-coverage ratchet now passes). - `4b0df9f1a` After the rebase, dev's #324 launch wait handles SIGCHLD, and macOS `kevent` returned EINTR, which ended the agent. `readiness::kevent_now` retries. - Folded in during the rebase: dev's `pty::prepare_proxy_child` gated Linux-only like its public wrapper; LAUNCH portable; `build-vt` drops the shared libghostty-vt (J80e's new image check caught a test binary loading `@rpath/libghostty-vt.dylib`). **Tests that failed first:** - P1, Linux: with the hook removed, `a_pty_program_inherits_only_its_terminal` prints `DESCRIPTOR-LEAKED`; with it, it passes 3/3. - P3-1, Linux: `startup_directory_must_belong_to_the_person_or_root` is red with the check stubbed, green 3/3. - P3-2: "FAIL: auth-toolchain accepted a look-alike INFRATERNAL_ZIG" against the old script, then PASS. - P2, P3-1 environment, P3-3: the launchd proof below goes red on `6363a04c4`. - New unit tests: `asuser_shells_run_only_in_the_persons_own_session`, `rootless_shells_keep_the_callers_session`, `only_terminal_and_locale_variables_are_forwarded`, `only_standard_descriptors_reach_the_child`. **Mac, as a launchd system job, under `lockf -k /tmp/mac-bench.lock`** (`proof3.log` and `proof2.log` attached): ``` RED, old head 6363a04c4: lume: FD9=open PLANTED=/private/tmp/j146-planted-zdotdir -> PROOF_EXIT=1 GREEN, 4b0df9f1a (sha256 8d6f9aa6…, upgrade to bc6369b7…): --- infraternal agent (launchd system job) --- uid=0 asid=100001 auid=4294967295 flags=0x1 System --- lume (GUI) --- Aqua SOCK=/var/run/com.apple.launchd.3nPAot2s0k/Listeners FD9=closed PLANTED= uid=501 euid=501 asid=100003 auid=501 flags=0x2030 --- claude (plain) --- Background SOCK= FD9=closed PLANTED= uid=1500 euid=1500 asid=100062 auid=1500 flags=0x0 PASS: shells open, take input and return output; D335 placement holds PASS: no shell or shim holds the planted root descriptor; no planted variable reached a shell PASS: both shells survive an infraternal agent restart (pids 95263, 95281) PASS: both shells survive an upgrade of the infraternal agent; placement unchanged PASS: shell exits reach the infraternal agent (kqueue exit watches); the census drops both PROOF_EXIT=0 ``` Also at `4b0df9f1a`: `macos-build-check: PASS` (`MACOS_CHECK_EXIT=0`), and Mac clippy `-D warnings` is clean for shim and agent. **Linux `scripts/verify fast` on infraternal-dev-02-std at `4b0df9f1a`** (run 3, verbatim; runs 2 and 3 attached): ``` +---------------------------+--------------+ | Rust gate | Result | +---------------------------+--------------+ | fmt | PASS | | VT native fmt | PASS | | CLI plugin compile-out | PASS | | clippy | PASS | | SSH runtime test helper | PASS | | real-binary fixture paths | PASS | | Traefik component manifest | PASS | | release component publication | PASS | | test | FAIL (101) | | SSH inode reuse fixture | PASS | | shim process-per-user | PASS | | cargo-deny | PASS | | gate architecture | PASS | | doc coverage ratchet | PASS | | static builds | PASS | +---------------------------+--------------+ VERIFY_EXIT=1 ``` **Not green.** Across the four gate runs at `84cd599fc`/`4b0df9f1a`, every other row passed every time. `test` failed each time on a different test, none of which this stack changes for Linux: | run | failing test | standalone on the same host | |---|---|---| | `84cd599fc` | 3 `infraternal` restart tests, 60 s deadline (test row took 1747 s) | head: 1 FAILED (load 16.9), 1 ok; dev: 2/2 ok | | `4b0df9f1a` #1 | `proxy` `traefik_child_receives_only_credential_file_paths…` (reads its capture file before the fake child writes it) | dev 5/5, head 5/5 ok | | `4b0df9f1a` #2 | `release` `pending_channel_writers_preserve_authority_read_capacity` (SQLx `PoolTimedOut`) | dev 5/5, head 5/5 ok | | `4b0df9f1a` #3 | `restart::…::relative_recovery_after_atomic_install_keeps_live_install_path`: `ETXTBSY`, Text file busy (an exec racing a concurrent fork) | n/a | The restart tests passed inside gate runs 1 and 2 at `4b0df9f1a`. A green `verify fast` needs either a quieter host or fixes to those four tests in their own lanes. **I did not mark this green.** **Linux static sizes**, same host and target directory, against the rebase base `843ec7eec`: | target | base `843ec7eec` | head `4b0df9f1a` | delta | |---|---|---|---| | x86_64-unknown-linux-musl | 23,364,272 B | 23,366,832 B | +2,560 B | | aarch64-unknown-linux-musl | 21,244,736 B | 21,247,104 B | +2,368 B | **For the orchestrator / kayg:** - The dev-02 `test` row is flaky under load across unrelated crates. The orchestrator's merge gate should rerun it. - Still open from round 1: does "logged in" mean a GUI login only (the current safe default) or any login, SSH included? Cleanup: the Mac test install, launchd job, state, worktrees and target are removed and the lock is released. The dev-02 target is removed. Logs are kept in `~/j146-out` (dev-02) and `~/j146-evidence` (Mac).
Author
Owner

J215 starting the focused security re-review of #328 at 4b0df9f1a4 on branch macos-sessions-j146, based at e99097fa80 (origin/dev). I will read D335/D337 and comments 77770/78959, inspect the change, then run the requested checks on the Mac under its file lock and on compute01 as root. The branch remains read-only.

J215 starting the focused security re-review of #328 at 4b0df9f1a4c4fcb23ebc08076c6d5e9437b89fc4 on branch macos-sessions-j146, based at e99097fa8046d31c411c1d04253ddf6da95c6412 (origin/dev). I will read D335/D337 and comments 77770/78959, inspect the change, then run the requested checks on the Mac under its file lock and on compute01 as root. The branch remains read-only.
Author
Owner

J215 progress — 2026-11-04

Branch macos-sessions-j146, head 4b0df9f1a4c4fcb23ebc08076c6d5e9437b89fc4 remains unchanged.

The Mac debug/release builds completed under the Mac lock. The real launchd proof is waiting for /tmp/mac-bench.lock: a live lockf -k ... /bin/bash -li session still holds it, so I have left that session untouched. The root Linux proof is queued through build-slot on compute01; all four slots are busy. dev-02 also has both slots busy and four queued jobs, so there is no free alternate slot.

Source review has two findings to verify on the real systems:

  • Potential P1, shim environment: crates/infraternal-agent/src/sessions.rs::isolated builds the shim command with Command::new(program) and only installs signal/descriptor hooks; it does not clear the inherited environment. The launchd fixture's hostile environment can therefore reach the root shim. DYLD_* variables can affect its loading before the shim reaches the scrubbed shell-entry path. I will confirm the process environment in the Mac proof.
  • P2, EINTR retry: crates/infraternal-shim/src/readiness.rs::kevent_now retries EINTR in an unconditional loop with no retry bound or cancellation/deadline. A sustained signal stream can keep the agent in this loop. This is visible in the current head.

The Linux proof and Mac launchd proof are still pending; I will post the final verdict with their outputs after both run.

J215 progress — 2026-11-04 Branch `macos-sessions-j146`, head `4b0df9f1a4c4fcb23ebc08076c6d5e9437b89fc4` remains unchanged. The Mac debug/release builds completed under the Mac lock. The real launchd proof is waiting for `/tmp/mac-bench.lock`: a live `lockf -k ... /bin/bash -li` session still holds it, so I have left that session untouched. The root Linux proof is queued through `build-slot` on compute01; all four slots are busy. dev-02 also has both slots busy and four queued jobs, so there is no free alternate slot. Source review has two findings to verify on the real systems: - **Potential P1, shim environment:** `crates/infraternal-agent/src/sessions.rs::isolated` builds the shim command with `Command::new(program)` and only installs signal/descriptor hooks; it does not clear the inherited environment. The launchd fixture's hostile environment can therefore reach the root shim. `DYLD_*` variables can affect its loading before the shim reaches the scrubbed shell-entry path. I will confirm the process environment in the Mac proof. - **P2, EINTR retry:** `crates/infraternal-shim/src/readiness.rs::kevent_now` retries `EINTR` in an unconditional loop with no retry bound or cancellation/deadline. A sustained signal stream can keep the agent in this loop. This is visible in the current head. The Linux proof and Mac launchd proof are still pending; I will post the final verdict with their outputs after both run.
Author
Owner

J215 progress update — head remains 4b0df9f1a4c4fcb23ebc08076c6d5e9437b89fc4.

The first Mac runner attempt exited before printing harness output; I do not count it as a test. The corrected launchd runner is now waiting on /tmp/mac-bench.lock behind live job J207. It preserves the fixture's read-only security list-keychains check, adds PATH and shim-environment probes, and uses a temporary audit-session probe.

The root Linux proof is second in compute01's build-slot queue. All four slots are occupied; dev-02 was also full when checked, and this root-only proof must run on compute01. Its queued script now uses -j4 and includes the symlink-retarget probe that runs zsh as uid 1500. On this host GID 1500 belongs only to claude, with no supplementary group members.

The source findings still awaiting real-system confirmation are the shim's inherited environment (potential P1) and the unbounded macOS kevent_now EINTR loop (P2). No repository branch has been modified.

J215 progress update — head remains `4b0df9f1a4c4fcb23ebc08076c6d5e9437b89fc4`. The first Mac runner attempt exited before printing harness output; I do not count it as a test. The corrected launchd runner is now waiting on `/tmp/mac-bench.lock` behind live job J207. It preserves the fixture's read-only `security list-keychains` check, adds PATH and shim-environment probes, and uses a temporary audit-session probe. The root Linux proof is second in compute01's `build-slot` queue. All four slots are occupied; dev-02 was also full when checked, and this root-only proof must run on compute01. Its queued script now uses `-j4` and includes the symlink-retarget probe that runs zsh as uid 1500. On this host GID 1500 belongs only to `claude`, with no supplementary group members. The source findings still awaiting real-system confirmation are the shim's inherited environment (potential P1) and the unbounded macOS `kevent_now` EINTR loop (P2). No repository branch has been modified.
Author
Owner

J215 Mac real-system finding — branch macos-sessions-j146, head 4b0df9f1a4c4fcb23ebc08076c6d5e9437b89fc4.

Under lockf -k /tmp/mac-bench.lock, the launchd system-job fixture completed its shell, restart, upgrade and census checks. GUI shell audit data was uid=501 euid=501 asid=100003 auid=501 flags=0x2030; plain shell data was uid=1500 euid=1500 asid=100062 auid=1500 flags=0x0, distinct from daemon audit session 100001. Both shells reported FD9=closed, PLANTED=, and PATH_MARKER=absent; the root-only descriptor was absent from both shim and shell. The read-only keychain search-list check passed. otool -L showed only CoreFoundation, libiconv and libSystem as dynamic dependencies.

P3 — shim environment is not isolated. The launchd fixture planted DYLD_LIBRARY_PATH, DYLD_FALLBACK_LIBRARY_PATH, BASH_ENV, ENV, ZDOTDIR, J146_PLANTED and a hostile PATH. The two root shim processes reported all seven values in their environments:

SHIM_ENV uid=501 pid=15826 inherited=DYLD_LIBRARY_PATH,BASH_ENV,ENV,ZDOTDIR,J146_PLANTED,PATH,DYLD_FALLBACK_LIBRARY_PATH
SHIM_ENV uid=1500 pid=15846 inherited=DYLD_LIBRARY_PATH,BASH_ENV,ENV,ZDOTDIR,J146_PLANTED,PATH,DYLD_FALLBACK_LIBRARY_PATH

The shell environment was clean and the PATH canary was absent, but sessions::isolated does not clear the agent environment before launching the shim. The runner exits 1 on this explicit shim-environment check. I will post the final verdict after the Linux root proof completes.

The first Linux run reached the ZDOTDIR probe but stopped because std::env::temp_dir() resolved inside Nix's private shell directory, which the person could not traverse. The probe now uses /tmp; the descriptor test filter is also corrected to the real test name. The rerun is queued through build-slot on compute01.

J215 Mac real-system finding — branch `macos-sessions-j146`, head `4b0df9f1a4c4fcb23ebc08076c6d5e9437b89fc4`. Under `lockf -k /tmp/mac-bench.lock`, the launchd system-job fixture completed its shell, restart, upgrade and census checks. GUI shell audit data was `uid=501 euid=501 asid=100003 auid=501 flags=0x2030`; plain shell data was `uid=1500 euid=1500 asid=100062 auid=1500 flags=0x0`, distinct from daemon audit session 100001. Both shells reported `FD9=closed`, `PLANTED=`, and `PATH_MARKER=absent`; the root-only descriptor was absent from both shim and shell. The read-only keychain search-list check passed. `otool -L` showed only CoreFoundation, libiconv and libSystem as dynamic dependencies. **P3 — shim environment is not isolated.** The launchd fixture planted `DYLD_LIBRARY_PATH`, `DYLD_FALLBACK_LIBRARY_PATH`, `BASH_ENV`, `ENV`, `ZDOTDIR`, `J146_PLANTED` and a hostile PATH. The two root shim processes reported all seven values in their environments: ``` SHIM_ENV uid=501 pid=15826 inherited=DYLD_LIBRARY_PATH,BASH_ENV,ENV,ZDOTDIR,J146_PLANTED,PATH,DYLD_FALLBACK_LIBRARY_PATH SHIM_ENV uid=1500 pid=15846 inherited=DYLD_LIBRARY_PATH,BASH_ENV,ENV,ZDOTDIR,J146_PLANTED,PATH,DYLD_FALLBACK_LIBRARY_PATH ``` The shell environment was clean and the PATH canary was absent, but `sessions::isolated` does not clear the agent environment before launching the shim. The runner exits 1 on this explicit shim-environment check. I will post the final verdict after the Linux root proof completes. The first Linux run reached the ZDOTDIR probe but stopped because `std::env::temp_dir()` resolved inside Nix's private shell directory, which the person could not traverse. The probe now uses `/tmp`; the descriptor test filter is also corrected to the real test name. The rerun is queued through `build-slot` on compute01.
Author
Owner

J215 source-review finding on 4b0df9f1a4c4fcb23ebc08076c6d5e9437b89fc4:

P2 — kevent_now can retry forever on EINTR. In crates/infraternal-shim/src/readiness.rs:164-178, the call uses a zero timespec, then loops unconditionally when queue.kevent returns EINTR. There is no retry cap, cancellation check, deadline or yield. The function can therefore stay in this loop and prevent its caller from processing events under sustained signal interruptions. This is the EINTR fix introduced in the rebase; the branch remains unchanged.

J215 source-review finding on `4b0df9f1a4c4fcb23ebc08076c6d5e9437b89fc4`: **P2 — `kevent_now` can retry forever on EINTR.** In `crates/infraternal-shim/src/readiness.rs:164-178`, the call uses a zero `timespec`, then loops unconditionally when `queue.kevent` returns `EINTR`. There is no retry cap, cancellation check, deadline or yield. The function can therefore stay in this loop and prevent its caller from processing events under sustained signal interruptions. This is the EINTR fix introduced in the rebase; the branch remains unchanged.
Author
Owner

J215 Linux proof progress — head remains 4b0df9f1a4.

The root ZDOTDIR probe now passes on compute01: the symlink to a trusted target resolves, the retargeted path is rejected on recheck, and the fixture shows the shell would source the retargeted file as uid 1500. The fixture’s parent was owned by claude, so root performed the retarget; this is not evidence of another account exploiting the path on this host. GID 1500 currently has no other members.

The auth-toolchain pin test passes. The live root descriptor probe first hit a harness collision because build-slot itself owns fd 9; the probe now plants fd 200 and is queued again through build-slot on compute01, currently third in line with all slots occupied. No branch files were changed.

J215 Linux proof progress — head remains 4b0df9f1a4c4fcb23ebc08076c6d5e9437b89fc4. The root ZDOTDIR probe now passes on compute01: the symlink to a trusted target resolves, the retargeted path is rejected on recheck, and the fixture shows the shell would source the retargeted file as uid 1500. The fixture’s parent was owned by claude, so root performed the retarget; this is not evidence of another account exploiting the path on this host. GID 1500 currently has no other members. The auth-toolchain pin test passes. The live root descriptor probe first hit a harness collision because build-slot itself owns fd 9; the probe now plants fd 200 and is queued again through build-slot on compute01, currently third in line with all slots occupied. No branch files were changed.
Author
Owner

J215 progress — Linux proof re-queued with corrected fd-200 fixture.

I stopped the stale J215 queued attempt (compute01 PID 4159566; it used fd 9, which build-slot itself occupied). The root throwaway clone remains at 4b0df9f1a4c4fcb23ebc08076c6d5e9437b89fc4, with only temporary test additions and runner files; the named branch is unchanged. The corrected runner plants a non-CLOEXEC root-only descriptor at fd 200, checks the live shim and shell, runs the descriptor allowlist and ZDOTDIR symlink/retarget tests, and checks the toolchain pin.

At the latest status check, compute01 had four occupied slots and four jobs ahead; dev-02 had both slots occupied and six queued. This proof must run as root on compute01, so I will wait foreground-only for its build-slot turn. The Mac lock is held by J207; I will not touch it.

**J215 progress — Linux proof re-queued with corrected fd-200 fixture.** I stopped the stale J215 queued attempt (compute01 PID 4159566; it used fd 9, which build-slot itself occupied). The root throwaway clone remains at `4b0df9f1a4c4fcb23ebc08076c6d5e9437b89fc4`, with only temporary test additions and runner files; the named branch is unchanged. The corrected runner plants a non-CLOEXEC root-only descriptor at fd 200, checks the live shim and shell, runs the descriptor allowlist and ZDOTDIR symlink/retarget tests, and checks the toolchain pin. At the latest status check, compute01 had four occupied slots and four jobs ahead; dev-02 had both slots occupied and six queued. This proof must run as root on compute01, so I will wait foreground-only for its build-slot turn. The Mac lock is held by J207; I will not touch it.
Author
Owner

J215 finding — P3, Linux ZDOTDIR trust check accepts shared group/ACL write access.

In crates/infraternal-shim/src/shell_startup.rs:87-105, private_group means only that the primary group name matches the account name, and group write is then accepted when the directory GID matches. It does not check for additional group members or named ACL entries. A group peer with write access can plant/replace zsh startup files after canonicalization; the shell then runs them as the target account. Compute01 currently reports claude:x:1500: with no other members, so this conditional path was not exploitable by another local account in the live fixture. The prior symlink retarget fixture used root and is not evidence of an unprivileged exploit.

J215 finding — P3, Linux fallback can leave high descriptors inheritable.

In crates/infraternal-shim/src/pty.rs:1150-1189, when close_range(CLOSE_RANGE_CLOEXEC) returns ENOSYS or EINVAL, the fallback scans only through min(RLIMIT_NOFILE, 1 << 20) and returns success. On a kernel without the flag and a process limit above 1,048,576, an open non-CLOEXEC descriptor numbered at or above that cap remains inheritable. Compute01's Linux 6.12 kernel and hard fd limit 524288 do not exercise the fallback gap.

J215 finding — **P3, Linux ZDOTDIR trust check accepts shared group/ACL write access.** In `crates/infraternal-shim/src/shell_startup.rs:87-105`, `private_group` means only that the primary group name matches the account name, and group write is then accepted when the directory GID matches. It does not check for additional group members or named ACL entries. A group peer with write access can plant/replace zsh startup files after canonicalization; the shell then runs them as the target account. Compute01 currently reports `claude:x:1500:` with no other members, so this conditional path was not exploitable by another local account in the live fixture. The prior symlink retarget fixture used root and is not evidence of an unprivileged exploit. J215 finding — **P3, Linux fallback can leave high descriptors inheritable.** In `crates/infraternal-shim/src/pty.rs:1150-1189`, when `close_range(CLOSE_RANGE_CLOEXEC)` returns `ENOSYS` or `EINVAL`, the fallback scans only through `min(RLIMIT_NOFILE, 1 << 20)` and returns success. On a kernel without the flag and a process limit above 1,048,576, an open non-CLOEXEC descriptor numbered at or above that cap remains inheritable. Compute01's Linux 6.12 kernel and hard fd limit 524288 do not exercise the fallback gap.
Author
Owner

J215 Linux descriptor probe progress — on compute01 as root, the first live fixture confirmed AGENT_FD200=/root/j215-review/j215-root-only CLOEXEC=0, then reported SHIM_PID=802557 ROOT_ONLY_FD=absent FD_COUNT=8; the real shell output contained J215_ROOT_FD_CLEAN. The runner exited 1 because its substring check matched the PTY-echoed command, not a leak. I corrected the temporary harness to match a standalone result line; the rerun is queued at position 5 through build-slot. No branch files changed.

J215 Linux descriptor probe progress — on compute01 as root, the first live fixture confirmed `AGENT_FD200=/root/j215-review/j215-root-only CLOEXEC=0`, then reported `SHIM_PID=802557 ROOT_ONLY_FD=absent FD_COUNT=8`; the real shell output contained `J215_ROOT_FD_CLEAN`. The runner exited 1 because its substring check matched the PTY-echoed command, not a leak. I corrected the temporary harness to match a standalone result line; the rerun is queued at position 5 through build-slot. No branch files changed.
Author
Owner

J215 final review — REQUEST CHANGES

Target: macos-sessions-j146, 4b0df9f1a4c4fcb23ebc08076c6d5e9437b89fc4. The branch was not changed.

P1 — inherited descriptor leak: closed on the tested paths, with one Linux fallback gap below. The Mac launchd system-job fixture planted inheritable root-only fd 9. The J215 proof reported it absent from both shims and both shells. On Linux compute01, as root, the live fixture verified AGENT_FD200=/root/j215-review/j215-root-only CLOEXEC=0, then SHIM_PID=802557 ROOT_ONLY_FD=absent FD_COUNT=8; the PTY shell emitted J215_ROOT_FD_CLEAN. That run exited 1 because the harness searched for the leak marker as a substring and matched the echoed shell command before it checked the standalone result line. The corrected duplicate was stopped while queued; I am not treating that harness exit as a green test, but the process-level output demonstrates the descriptor was absent. Source review found all four PTY shell builders, the shim launcher, and the macOS private entry apply the intended boundary. Later child hooks do not open arbitrary descriptors; the Linux login handoff explicitly re-enables only its named descriptors.

P2 — the EINTR retry can spin indefinitely. crates/infraternal-shim/src/readiness.rs:164-178 retries kevent forever on EINTR with no bound, cancellation check or deadline. Sustained interruptions can starve the event loop.

P3 — root shims retain hostile environment variables. In the Mac launchd proof, both root shims reported all seven planted values: DYLD_LIBRARY_PATH, DYLD_FALLBACK_LIBRARY_PATH, BASH_ENV, ENV, ZDOTDIR, J146_PLANTED and PATH. Both shells had FD9=closed, PLANTED= and PATH_MARKER=absent. crates/infraternal-agent/src/sessions.rs::isolated does not clear the inherited environment before the shim exec. The Mac audit/session evidence itself is correct: GUI shell uid/euid=501 asid=100003 auid=501 flags=0x2030; plain shell uid/euid=1500 asid=100062 auid=1500 flags=0.

P3 — Linux ZDOTDIR accepts group write without verifying exclusivity. shell_startup.rs:91-105 treats a group as private when its name equals the account name and accepts group-writable directories with that GID. It does not check additional group members or named ACL entries. A peer with write access can plant zsh startup code that runs as the account. Compute01 currently has claude:x:1500:, so no other local group member was available for a live exploit. The symlink canonicalization/retarget check passed, but the earlier retarget actor was root and does not cover this case.

P3 — Linux close-range fallback can skip descriptors above 1 MiB. pty.rs:1183-1188 caps the fallback scan at 1 << 20 and returns success. On a kernel without CLOSE_RANGE_CLOEXEC, if RLIMIT_NOFILE exceeds that cap, an open descriptor numbered at or above 1,048,576 can remain inheritable. This was not reproduced on compute01 (Linux 6.12; hard fd limit 524288).

The targeted descriptor allowlist tests passed (2), the ZDOTDIR unit test passed (1), and scripts/test-auth-toolchain-pin passed. J146's final verify fast output is in comment 78959 and ended VERIFY_EXIT=1; I did not rerun the gate because this review changed no branch code.

Cleanup complete: the Mac lock is released, the J215 Mac worktree and test launchd job are absent, and the root/Linux and local review worktrees and temporary state are removed.

**J215 final review — REQUEST CHANGES** Target: `macos-sessions-j146`, `4b0df9f1a4c4fcb23ebc08076c6d5e9437b89fc4`. The branch was not changed. **P1 — inherited descriptor leak: closed on the tested paths, with one Linux fallback gap below.** The Mac launchd system-job fixture planted inheritable root-only fd 9. The J215 proof reported it absent from both shims and both shells. On Linux compute01, as root, the live fixture verified `AGENT_FD200=/root/j215-review/j215-root-only CLOEXEC=0`, then `SHIM_PID=802557 ROOT_ONLY_FD=absent FD_COUNT=8`; the PTY shell emitted `J215_ROOT_FD_CLEAN`. That run exited 1 because the harness searched for the leak marker as a substring and matched the echoed shell command before it checked the standalone result line. The corrected duplicate was stopped while queued; I am not treating that harness exit as a green test, but the process-level output demonstrates the descriptor was absent. Source review found all four PTY shell builders, the shim launcher, and the macOS private entry apply the intended boundary. Later child hooks do not open arbitrary descriptors; the Linux login handoff explicitly re-enables only its named descriptors. **P2 — the EINTR retry can spin indefinitely.** `crates/infraternal-shim/src/readiness.rs:164-178` retries `kevent` forever on `EINTR` with no bound, cancellation check or deadline. Sustained interruptions can starve the event loop. **P3 — root shims retain hostile environment variables.** In the Mac launchd proof, both root shims reported all seven planted values: `DYLD_LIBRARY_PATH`, `DYLD_FALLBACK_LIBRARY_PATH`, `BASH_ENV`, `ENV`, `ZDOTDIR`, `J146_PLANTED` and `PATH`. Both shells had `FD9=closed`, `PLANTED=` and `PATH_MARKER=absent`. `crates/infraternal-agent/src/sessions.rs::isolated` does not clear the inherited environment before the shim exec. The Mac audit/session evidence itself is correct: GUI shell `uid/euid=501 asid=100003 auid=501 flags=0x2030`; plain shell `uid/euid=1500 asid=100062 auid=1500 flags=0`. **P3 — Linux ZDOTDIR accepts group write without verifying exclusivity.** `shell_startup.rs:91-105` treats a group as private when its name equals the account name and accepts group-writable directories with that GID. It does not check additional group members or named ACL entries. A peer with write access can plant zsh startup code that runs as the account. Compute01 currently has `claude:x:1500:`, so no other local group member was available for a live exploit. The symlink canonicalization/retarget check passed, but the earlier retarget actor was root and does not cover this case. **P3 — Linux close-range fallback can skip descriptors above 1 MiB.** `pty.rs:1183-1188` caps the fallback scan at `1 << 20` and returns success. On a kernel without `CLOSE_RANGE_CLOEXEC`, if `RLIMIT_NOFILE` exceeds that cap, an open descriptor numbered at or above 1,048,576 can remain inheritable. This was not reproduced on compute01 (Linux 6.12; hard fd limit 524288). The targeted descriptor allowlist tests passed (2), the ZDOTDIR unit test passed (1), and `scripts/test-auth-toolchain-pin` passed. J146's final `verify fast` output is in comment 78959 and ended `VERIFY_EXIT=1`; I did not rerun the gate because this review changed no branch code. Cleanup complete: the Mac lock is released, the J215 Mac worktree and test launchd job are absent, and the root/Linux and local review worktrees and temporary state are removed.
Author
Owner

J146 round 3 (J215's review, comment 79191): every finding fixed, origin/dev merged in, Mac launchd proof green, Linux verify fast green.

Branch macos-sessions-j146, head c793d36f617c0f1d5f145f24d7e383c5aa71835b. Not merged, not deployed. origin/dev 54ca98177 (#305 part 2 and the remote test cfg fix) is merged in as 4cbf63a32.

Commits since 4b0df9f1a, each with a test that failed first:

  • 70af9e059 P2: EINTR retry bounded. retry_interrupted tries at most 8 times and then returns EINTR. The two async kqueue drains yield to the runtime and retry with readiness kept, so they never spin or fail. Test: a_permanently_interrupted_call_gives_up.
  • 6bb602091 P3-1: the shim launch clears its environment and passes an explicit allow-list (shim_environment): locale everywhere; on Linux also XDG_RUNTIME_DIR, DBUS_SESSION_BUS_ADDRESS (for systemd-run --user) and ZDOTDIR (re-checked by the shim). systemd-run is resolved through the agent's own PATH, so the shim gets no PATH. Test: only_allow_listed_variables_reach_the_shim, plus a Mac proof row that reads each root shim's environment with ps -E.
  • 84506c076 and 6849d090b P3-2: a Linux ZDOTDIR directory is refused if it is writable by group or others, whatever the group. Only a sticky ancestor such as /tmp is exempt. Test: startup_directory_must_belong_to_the_person_or_root (a 0775 directory the person owns is refused).
  • f7a7b1205 P3-3: the pre-5.11 Linux fallback lists /proc/self/fd exactly (raw open/getdents64 into stack storage, safe after fork) and fails closed. Test: the_listing_fallback_marks_descriptors_above_the_limit (descriptor 200 above a lowered soft limit of 64).
  • 982c560e5 the shim builds on its own on macOS (nix event feature); 3f8831f52 dev's macOS proxy-child stub gains the new cgroup argument, which this merge exposed on macOS (E0061).
  • 829a7f196 and c793d36f6 proof hygiene: the root-shim environment check has a positive control (the agent's own planted marker must be visible), and a detached session's output is drained before its mux stream is reused (see below).

Red/green on compute01 (four reversions applied at once, then restored):

== RED: the four fixes reverted
test shell_startup::tests::startup_directory_must_belong_to_the_person_or_root ... FAILED
test pty::descriptor_allowlist_tests::the_listing_fallback_marks_descriptors_above_the_limit ... FAILED
test readiness::interrupt_tests::a_permanently_interrupted_call_gives_up ... FAILED
test sessions::shim_environment_tests::only_allow_listed_variables_reach_the_shim ... FAILED
== GREEN x3 (at 6849d090b): 3 passed + 1 passed, three times

Mac, as a launchd system job at 3f8831f52 (head c793d36f6 differs only in Linux-only code and the proof script; macos-build-check: PASS and Mac clippy -D warnings clean at 3f8831f52):

--- infraternal agent (launchd system job) --- uid=0 asid=100001 auid=4294967295 flags=0x1 System
--- lume (GUI)    --- Aqua  FD9=closed PLANTED=  uid=501 asid=100003 auid=501 flags=0x2030
--- claude (plain) --- Background  FD9=closed PLANTED=  uid=1500 asid=100062 auid=1500 flags=0x0
PASS: shells open, take input and return output; D335 placement holds
PASS: no shell or shim holds the planted root descriptor; no planted variable reached a shell
PASS: the root shims run with the allow-listed environment only (no planted variable, no PATH)
PASS: both shells survive an infraternal agent restart
PASS: both shells survive an upgrade of the infraternal agent; placement unchanged
PASS: shell exits reach the infraternal agent (kqueue exit watches); the census drops both
PROOF_EXIT=0          (6 of 6 runs)

J215's own run at 4b0df9f1a is the red evidence for the root-shim environment row (all seven planted values present).

Harness race found and fixed (not a product change): every proof session shares mux stream 1, and output already sent for a detached session could land in the next session's cursor. The fixture's ordering assert then failed in either direction, (2463, 1562) and (2039, 2339). It failed in 2 of 6 runs at the pre-merge head 4b0df9f1a too, so the earlier green proofs were lucky. With Agent.settle (stream 1 quiet for 0.5 s before reuse), 6 of 6 runs are green.

Linux scripts/verify fast on infraternal-dev-01-compute01 at 6849d090b50e27e4b860edb1185dd4c8b240bbf1 (verbatim; the head adds only scripts/test-macos-sessions.py, which no gate row runs):

+---------------------------+--------------+
| Rust gate                 | Result       |
+---------------------------+--------------+
| fmt                       | PASS         |
| migration names           | PASS         |
| migration tooling tests   | PASS         |
| VT native fmt             | PASS         |
| CLI plugin compile-out    | PASS         |
| clippy                    | PASS         |
| SSH runtime test helper   | PASS         |
| real-binary fixture paths | PASS         |
| Traefik component manifest | PASS         |
| release component publication | PASS         |
| test                      | PASS         |
| SSH inode reuse fixture   | PASS         |
| shim process-per-user     | PASS         |
| cargo-deny                | PASS         |
| gate architecture         | PASS         |
| doc coverage ratchet      | PASS         |
| static builds             | PASS         |
+---------------------------+--------------+
...
 48 pass
 0 fail
Ran 48 tests across 12 files. [10.44s]
VERIFY_EXIT=0

Linux static sizes against the merged dev 54ca98177, same host and target directory: x86_64 23,601,648 → 23,608,528 B (+6,880), aarch64 21,449,056 → 21,455,600 B (+6,544).

Still open for kayg (unchanged): does "logged in" mean a GUI login only (the current safe default) or does an SSH-only login count?

Cleanup: the Mac test install, launchd job, worktrees and target are removed and the lock is released (no shim left). The compute01 target is removed. Logs are kept in ~/j146-out on compute01 and ~/j146-evidence on the Mac.

**J146 round 3 (J215's review, comment 79191): every finding fixed, origin/dev merged in, Mac launchd proof green, Linux `verify fast` green.** Branch `macos-sessions-j146`, **head `c793d36f617c0f1d5f145f24d7e383c5aa71835b`**. Not merged, not deployed. `origin/dev` `54ca98177` (#305 part 2 and the remote test cfg fix) is merged in as `4cbf63a32`. Commits since `4b0df9f1a`, each with a test that failed first: - `70af9e059` **P2:** EINTR retry bounded. `retry_interrupted` tries at most 8 times and then returns EINTR. The two async kqueue drains yield to the runtime and retry with readiness kept, so they never spin or fail. Test: `a_permanently_interrupted_call_gives_up`. - `6bb602091` **P3-1:** the shim launch clears its environment and passes an explicit allow-list (`shim_environment`): locale everywhere; on Linux also XDG_RUNTIME_DIR, DBUS_SESSION_BUS_ADDRESS (for `systemd-run --user`) and ZDOTDIR (re-checked by the shim). `systemd-run` is resolved through the agent's own PATH, so the shim gets no PATH. Test: `only_allow_listed_variables_reach_the_shim`, plus a Mac proof row that reads each root shim's environment with `ps -E`. - `84506c076` and `6849d090b` **P3-2:** a Linux ZDOTDIR directory is refused if it is writable by group or others, whatever the group. Only a sticky ancestor such as /tmp is exempt. Test: `startup_directory_must_belong_to_the_person_or_root` (a 0775 directory the person owns is refused). - `f7a7b1205` **P3-3:** the pre-5.11 Linux fallback lists `/proc/self/fd` exactly (raw `open`/`getdents64` into stack storage, safe after fork) and fails closed. Test: `the_listing_fallback_marks_descriptors_above_the_limit` (descriptor 200 above a lowered soft limit of 64). - `982c560e5` the shim builds on its own on macOS (nix `event` feature); `3f8831f52` dev's macOS proxy-child stub gains the new cgroup argument, which this merge exposed on macOS (E0061). - `829a7f196` and `c793d36f6` proof hygiene: the root-shim environment check has a positive control (the agent's own planted marker must be visible), and a detached session's output is drained before its mux stream is reused (see below). **Red/green on compute01** (four reversions applied at once, then restored): ``` == RED: the four fixes reverted test shell_startup::tests::startup_directory_must_belong_to_the_person_or_root ... FAILED test pty::descriptor_allowlist_tests::the_listing_fallback_marks_descriptors_above_the_limit ... FAILED test readiness::interrupt_tests::a_permanently_interrupted_call_gives_up ... FAILED test sessions::shim_environment_tests::only_allow_listed_variables_reach_the_shim ... FAILED == GREEN x3 (at 6849d090b): 3 passed + 1 passed, three times ``` **Mac, as a launchd system job at `3f8831f52`** (head `c793d36f6` differs only in Linux-only code and the proof script; `macos-build-check: PASS` and Mac clippy `-D warnings` clean at `3f8831f52`): ``` --- infraternal agent (launchd system job) --- uid=0 asid=100001 auid=4294967295 flags=0x1 System --- lume (GUI) --- Aqua FD9=closed PLANTED= uid=501 asid=100003 auid=501 flags=0x2030 --- claude (plain) --- Background FD9=closed PLANTED= uid=1500 asid=100062 auid=1500 flags=0x0 PASS: shells open, take input and return output; D335 placement holds PASS: no shell or shim holds the planted root descriptor; no planted variable reached a shell PASS: the root shims run with the allow-listed environment only (no planted variable, no PATH) PASS: both shells survive an infraternal agent restart PASS: both shells survive an upgrade of the infraternal agent; placement unchanged PASS: shell exits reach the infraternal agent (kqueue exit watches); the census drops both PROOF_EXIT=0 (6 of 6 runs) ``` J215's own run at `4b0df9f1a` is the red evidence for the root-shim environment row (all seven planted values present). **Harness race found and fixed (not a product change):** every proof session shares mux stream 1, and output already sent for a detached session could land in the next session's cursor. The fixture's ordering assert then failed in either direction, (2463, 1562) and (2039, 2339). It failed in 2 of 6 runs at the pre-merge head `4b0df9f1a` too, so the earlier green proofs were lucky. With `Agent.settle` (stream 1 quiet for 0.5 s before reuse), 6 of 6 runs are green. **Linux `scripts/verify fast` on infraternal-dev-01-compute01 at `6849d090b50e27e4b860edb1185dd4c8b240bbf1`** (verbatim; the head adds only `scripts/test-macos-sessions.py`, which no gate row runs): ``` +---------------------------+--------------+ | Rust gate | Result | +---------------------------+--------------+ | fmt | PASS | | migration names | PASS | | migration tooling tests | PASS | | VT native fmt | PASS | | CLI plugin compile-out | PASS | | clippy | PASS | | SSH runtime test helper | PASS | | real-binary fixture paths | PASS | | Traefik component manifest | PASS | | release component publication | PASS | | test | PASS | | SSH inode reuse fixture | PASS | | shim process-per-user | PASS | | cargo-deny | PASS | | gate architecture | PASS | | doc coverage ratchet | PASS | | static builds | PASS | +---------------------------+--------------+ ... 48 pass 0 fail Ran 48 tests across 12 files. [10.44s] VERIFY_EXIT=0 ``` **Linux static sizes** against the merged dev `54ca98177`, same host and target directory: x86_64 23,601,648 → 23,608,528 B (+6,880), aarch64 21,449,056 → 21,455,600 B (+6,544). Still open for kayg (unchanged): does "logged in" mean a GUI login only (the current safe default) or does an SSH-only login count? Cleanup: the Mac test install, launchd job, worktrees and target are removed and the lock is released (no shim left). The compute01 target is removed. Logs are kept in `~/j146-out` on compute01 and `~/j146-evidence` on the Mac.
Author
Owner

J224 focused security re-review started. Target macos-sessions-j146 at c793d36f617c0f1d5f145f24d7e383c5aa71835b, base reviewed by J215 4b0df9f1a4c4fcb23ebc08076c6d5e9437b89fc4; isolated review branch review-j224. Scope: the four J215 fixes, excluding the dev merge 4cbf63a32.

Plan (about 60 minutes): inspect EINTR/readiness, shim environment/PATH, ZDOTDIR trust and post-fork descriptor enumeration; run the four regressions on compute01 through build-slot; post evidence and APPROVE or REQUEST CHANGES. No push, merge, deploy or issue closure. No Mac lock unless a finding needs Mac evidence.

compute01 df -h /: /dev/sda1 591G 252G 316G 45% /. Four slots occupied and three jobs queued at first check. Waiting will remain foreground-only. scripts/fj is absent in this checkout, so using the installed Forgejo CLI described in BRIEF-COMMON.

J224 focused security re-review started. Target `macos-sessions-j146` at `c793d36f617c0f1d5f145f24d7e383c5aa71835b`, base reviewed by J215 `4b0df9f1a4c4fcb23ebc08076c6d5e9437b89fc4`; isolated review branch `review-j224`. Scope: the four J215 fixes, excluding the dev merge `4cbf63a32`. Plan (about 60 minutes): inspect EINTR/readiness, shim environment/PATH, ZDOTDIR trust and post-fork descriptor enumeration; run the four regressions on compute01 through build-slot; post evidence and APPROVE or REQUEST CHANGES. No push, merge, deploy or issue closure. No Mac lock unless a finding needs Mac evidence. compute01 `df -h /`: `/dev/sda1 591G 252G 316G 45% /`. Four slots occupied and three jobs queued at first check. Waiting will remain foreground-only. `scripts/fj` is absent in this checkout, so using the installed Forgejo CLI described in BRIEF-COMMON.
Author
Owner

J224 source review progress at target c793d36f617c0f1d5f145f24d7e383c5aa71835b: no new P1/P2/P3 finding established. Both drains preserve readiness on Interrupted and yield after the bounded syscall attempts. The fallback uses raw open/getdents64/fcntl/close with stack storage; the apparent io::Error::other conversion is on an unreachable integer-conversion error for a nonnegative syscall byte count fitting usize. ZDOTDIR rejects a writable leaf, including sticky leaves; sticky ancestors still require root/target ownership and each canonical component is checked with symlink_metadata. The launcher lookup depends on the infraternal agent launcher's PATH, not a per-person/session environment; installed system units do not import a person's PATH.

The four regression tests are queued on compute01, foreground SSH run; log /home/claude/j224-regressions.log, one target /home/claude/worktrees/j224/target. Four slots remain busy; two earlier queue entries have progressed since launch. I will collect the actual test output before posting a verdict. No Mac lock taken, no source changes.

J224 source review progress at target `c793d36f617c0f1d5f145f24d7e383c5aa71835b`: no new P1/P2/P3 finding established. Both drains preserve readiness on Interrupted and yield after the bounded syscall attempts. The fallback uses raw open/getdents64/fcntl/close with stack storage; the apparent `io::Error::other` conversion is on an unreachable integer-conversion error for a nonnegative syscall byte count fitting usize. ZDOTDIR rejects a writable leaf, including sticky leaves; sticky ancestors still require root/target ownership and each canonical component is checked with symlink_metadata. The launcher lookup depends on the infraternal agent launcher's PATH, not a per-person/session environment; installed system units do not import a person's PATH. The four regression tests are queued on compute01, foreground SSH run; log `/home/claude/j224-regressions.log`, one target `/home/claude/worktrees/j224/target`. Four slots remain busy; two earlier queue entries have progressed since launch. I will collect the actual test output before posting a verdict. No Mac lock taken, no source changes.
Author
Owner

J224 compute01 regression evidence at c793d36f617c0f1d5f145f24d7e383c5aa71835b, through build-slot, user claude. All four requested tests passed:

== a_permanently_interrupted_call_gives_up ==
test readiness::interrupt_tests::a_permanently_interrupted_call_gives_up ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 121 filtered out; finished in 0.00s
== startup_directory_must_belong_to_the_person_or_root ==
test shell_startup::tests::startup_directory_must_belong_to_the_person_or_root ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 121 filtered out; finished in 0.00s
== the_listing_fallback_marks_descriptors_above_the_limit ==
test pty::descriptor_allowlist_tests::the_listing_fallback_marks_descriptors_above_the_limit ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 121 filtered out; finished in 0.05s
== only_allow_listed_variables_reach_the_shim ==
test sessions::shim_environment_tests::only_allow_listed_variables_reach_the_shim ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 150 filtered out; finished in 0.00s
J224_REGRESSIONS_EXIT=0
== scripts/verify quick ==

Commands: cargo test --locked -j4 -p infraternal-shim --features async-fd --lib <test> -- --nocapture for each of the first three; cargo test --locked -j4 -p infraternal-agent --lib only_allow_listed_variables_reach_the_shim -- --nocapture for the fourth, inside nix develop and nice -n10. The same foreground run is now running quick and then fast to satisfy the repository gate requirement. Logs: /home/claude/j224-regressions.log, /home/claude/j224-quick.log, /home/claude/j224-fast.log (once started). No branch code changed.

J224 compute01 regression evidence at `c793d36f617c0f1d5f145f24d7e383c5aa71835b`, through build-slot, user claude. All four requested tests passed: ``` == a_permanently_interrupted_call_gives_up == test readiness::interrupt_tests::a_permanently_interrupted_call_gives_up ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 121 filtered out; finished in 0.00s == startup_directory_must_belong_to_the_person_or_root == test shell_startup::tests::startup_directory_must_belong_to_the_person_or_root ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 121 filtered out; finished in 0.00s == the_listing_fallback_marks_descriptors_above_the_limit == test pty::descriptor_allowlist_tests::the_listing_fallback_marks_descriptors_above_the_limit ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 121 filtered out; finished in 0.05s == only_allow_listed_variables_reach_the_shim == test sessions::shim_environment_tests::only_allow_listed_variables_reach_the_shim ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 150 filtered out; finished in 0.00s J224_REGRESSIONS_EXIT=0 == scripts/verify quick == ``` Commands: `cargo test --locked -j4 -p infraternal-shim --features async-fd --lib <test> -- --nocapture` for each of the first three; `cargo test --locked -j4 -p infraternal-agent --lib only_allow_listed_variables_reach_the_shim -- --nocapture` for the fourth, inside nix develop and nice -n10. The same foreground run is now running quick and then fast to satisfy the repository gate requirement. Logs: `/home/claude/j224-regressions.log`, `/home/claude/j224-quick.log`, `/home/claude/j224-fast.log` (once started). No branch code changed.
Author
Owner

J224 gate runner correction (no product finding): all four security regressions passed, and quick's clippy and library-unit rows passed. The bin-only row failed seven restart/signature fixtures with PermissionDenied because I set INFRATERNAL_TEST_STATE_ROOT=/tmp/j224-state. restart_binary::Opened::open_with refuses any world-writable ancestor, and /tmp is mode 1777. These executable fixtures require trusted home ancestry; this is also documented in restart_tests.rs:27.

I stopped my entire gate process group (confirmed no remaining member), preserving the first quick log as /home/claude/j224-quick-tmp-root.log and the aborted fast log as /home/claude/j224-fast-aborted-tmp-root.log. I corrected only the external runner to use /home/claude/j224-state (0700) and requeued the four tests, quick and fast against the unchanged target head, reusing the one existing target directory. The current foreground log remains /home/claude/j224-regressions.log. No Mac lock or source change.

J224 gate runner correction (no product finding): all four security regressions passed, and quick's clippy and library-unit rows passed. The bin-only row failed seven restart/signature fixtures with `PermissionDenied` because I set `INFRATERNAL_TEST_STATE_ROOT=/tmp/j224-state`. `restart_binary::Opened::open_with` refuses any world-writable ancestor, and `/tmp` is mode 1777. These executable fixtures require trusted home ancestry; this is also documented in `restart_tests.rs:27`. I stopped my entire gate process group (confirmed no remaining member), preserving the first quick log as `/home/claude/j224-quick-tmp-root.log` and the aborted fast log as `/home/claude/j224-fast-aborted-tmp-root.log`. I corrected only the external runner to use `/home/claude/j224-state` (0700) and requeued the four tests, quick and fast against the unchanged target head, reusing the one existing target directory. The current foreground log remains `/home/claude/j224-regressions.log`. No Mac lock or source change.
Author
Owner

J224 runner diagnosis confirmed on the same CLI unit-test binary, with no source or binary change: inside nix develop, INFRATERNAL_TEST_STATE_ROOT=/home/claude/j224-state target/debug/deps/infraternal-d95167d8fcded1fa --exact restart_binary::audit_tests::pathname_replacement_keeps_the_admitted_inode --nocapture now reports:

running 1 test
test restart_binary::audit_tests::pathname_replacement_keeps_the_admitted_inode ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 158 filtered out; finished in 7.09s

The corrected full runner is queued (position 8 at admission); waiting remains foreground-only. The previous four focused regression results are valid and green. No product finding added.

J224 runner diagnosis confirmed on the same CLI unit-test binary, with no source or binary change: inside nix develop, `INFRATERNAL_TEST_STATE_ROOT=/home/claude/j224-state target/debug/deps/infraternal-d95167d8fcded1fa --exact restart_binary::audit_tests::pathname_replacement_keeps_the_admitted_inode --nocapture` now reports: ``` running 1 test test restart_binary::audit_tests::pathname_replacement_keeps_the_admitted_inode ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 158 filtered out; finished in 7.09s ``` The corrected full runner is queued (position 8 at admission); waiting remains foreground-only. The previous four focused regression results are valid and green. No product finding added.
Author
Owner

J224 final focused security re-review: APPROVE

Target: macos-sessions-j146, c793d36f617c0f1d5f145f24d7e383c5aa71835b, against J215's 4b0df9f1a4c4fcb23ebc08076c6d5e9437b89fc4. Reviewed git diff 4b0df9f1a...c793d36f6 for the repairs, excluding changes brought by dev merge 4cbf63a32. No new P1/P2/P3 security finding established in this scope.

  • P2 EINTR closed. crates/infraternal-shim/src/readiness.rs:162 bounds each helper invocation to eight attempts. The drains at crates/infraternal-agent/src/sessions.rs:655 and crates/infraternal-agent/src/process_exits_macos.rs:170 yield and retry Interrupted. Ready::try_io (crates/infraternal-shim/src/readiness.rs:128) clears readiness only for WouldBlock; dropping/cancelling the guard on Interrupted does not clear it. A persistent signal storm can continue delaying the interrupted syscall, but cannot trap execution in the former unbounded synchronous retry loop or remove the runtime's cancellation opportunities. EINTR is not treated as successful queue consumption.
  • P3-1 shim environment closed. crates/infraternal-agent/src/sessions.rs:1039 starts from env_clear, then the allow-list at :685. No loader, shell startup hook, HOME or PATH is inherited. Locale variables do not carry an executable search path; Linux ZDOTDIR is revalidated before use. XDG_RUNTIME_DIR and DBUS_SESSION_BUS_ADDRESS are transport hints used for the same-UID user manager, not a person's route to a root user-manager launcher: Manager::detect selects System for root and User for non-root (sessions.rs:80). on_path (sessions.rs:699) does trust the infraternal agent's own PATH and its directories; it is not independently a privileged-path validator. Session input does not supply/change that PATH, and the installed system service does not import the enrolling person's environment (crates/infraternal/src/up.rs:1118). An administrator starting a root process with a user-writable/relative PATH still has that administrative trust obligation; no new lower-privileged input path was found.
  • P3-2 ZDOTDIR closed. crates/infraternal-shim/src/shell_startup.rs:91-108 canonicalizes, rejects symlink components encountered during validation, requires every canonical directory to belong to root or the target UID, and refuses group/other write on the leaf even with sticky set. Sticky applies only to ancestors. A peer can create its own /tmp entry but that entry fails ownership validation; it cannot replace the trusted root/target-owned entry in sticky /tmp, or create startup files inside a nonwritable leaf. POSIX named ACL write permission is bounded by the group-class mask reflected in st_mode, so the new 022 rejection also covers it. Returning the canonical path removes dependence on the original symlink; the accepted ownership/mode chain prevents another UID from retargeting it before shell exec. Root/the target UID remain trusted actors, rather than a TOCTOU security boundary.
  • P3-3 descriptor fallback closed. crates/infraternal-shim/src/pty.rs:1200 opens /proc/self/fd, reads all getdents64 batches into a 4096-byte stack buffer and marks each descriptor with fcntl. Marking CLOEXEC does not mutate the directory entries or skip later batches. There is no RLIMIT/capped scan. Every reachable post-fork operation is stack/scalar work or raw open/syscall/fcntl/close; no opendir/readdir, allocation or allocator lock. The usize::try_from(read).map_err(io::Error::other) allocation branch cannot execute for a nonnegative c_long byte count fitting Linux usize. An unreadable/unavailable listing returns Err from the pre_exec hook and refuses spawn, rather than proceeding with inheritable descriptors. The listing descriptor itself opens CLOEXEC and is explicitly closed.

No Mac lock taken: the questioned new boundaries were reviewable in source and the four requested Linux regressions; I did not independently repeat J146's launchd proof or claim a pre-5.11 live-kernel run.

Independent compute01 evidence (user claude, df -h / before building: 316G free; build-slot, nice -n10, nix develop, cargo --locked -j4):

== a_permanently_interrupted_call_gives_up ==
test readiness::interrupt_tests::a_permanently_interrupted_call_gives_up ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 121 filtered out; finished in 0.00s
== startup_directory_must_belong_to_the_person_or_root ==
test shell_startup::tests::startup_directory_must_belong_to_the_person_or_root ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 121 filtered out; finished in 0.00s
== the_listing_fallback_marks_descriptors_above_the_limit ==
test pty::descriptor_allowlist_tests::the_listing_fallback_marks_descriptors_above_the_limit ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 121 filtered out; finished in 0.05s
== only_allow_listed_variables_reach_the_shim ==
test sessions::shim_environment_tests::only_allow_listed_variables_reach_the_shim ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 150 filtered out; finished in 0.00s
J224_REGRESSIONS_EXIT=0

The first three used cargo test --locked -j4 -p infraternal-shim --features async-fd --lib <test> -- --nocapture; the fourth used cargo test --locked -j4 -p infraternal-agent --lib only_allow_listed_variables_reach_the_shim -- --nocapture.

Gate disclosure: no code changed, full gate not applicable under BRIEF-COMMON “Planning/research jobs and the gate” (explicitly covers jobs that change no repository code). I initially over-applied the standing gate rule. The extra quick run produced this verbatim summary:

+---------------------------+--------------+
| Rust gate                 | Result       |
+---------------------------+--------------+
| scope guard               | PASS         |
| migration names           | PASS         |
| fmt                       | PASS         |
| doc coverage ratchet      | PASS         |
| clippy (workspace)        | PASS         |
| unit tests (changed)      | PASS         |
| unit tests (changed, bin-only) | FAIL (101)   |
+---------------------------+--------------+

The seven bin-only failures were restart/signature fixtures with PermissionDenied: my runner put executable fixtures under /tmp/j224-state, so the existing replacement validator correctly refused their world-writable /tmp ancestor. No product repair was needed. With the same binary and nix shell, changing only the fixture root to mode-0700 /home/claude/j224-state made the directly checked failing fixture pass:

running 1 test
test restart_binary::audit_tests::pathname_replacement_keeps_the_admitted_inode ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 158 filtered out; finished in 7.09s

The mistakenly started scripts/verify fast was stopped during compilation (exit 143), and the corrected extra gate was cancelled while queued, rather than spend another scarce slot on a no-code review. I claim no fresh green full gate. J146’s prior full-gate evidence remains in comment 79422.

Cleanup: own active gate process group and queued runner stopped, no J224 test process remains; the one target directory and temporary fixture roots are removed. Logs are retained under /home/claude/j224-*.log on compute01 and attached here. The target branch and head remain unchanged. No push, merge, deployment or issue closure.

APPROVE

**J224 final focused security re-review: APPROVE** Target: `macos-sessions-j146`, `c793d36f617c0f1d5f145f24d7e383c5aa71835b`, against J215's `4b0df9f1a4c4fcb23ebc08076c6d5e9437b89fc4`. Reviewed `git diff 4b0df9f1a...c793d36f6` for the repairs, excluding changes brought by dev merge `4cbf63a32`. No new P1/P2/P3 security finding established in this scope. - **P2 EINTR closed.** `crates/infraternal-shim/src/readiness.rs:162` bounds each helper invocation to eight attempts. The drains at `crates/infraternal-agent/src/sessions.rs:655` and `crates/infraternal-agent/src/process_exits_macos.rs:170` yield and retry Interrupted. `Ready::try_io` (`crates/infraternal-shim/src/readiness.rs:128`) clears readiness only for WouldBlock; dropping/cancelling the guard on Interrupted does not clear it. A persistent signal storm can continue delaying the interrupted syscall, but cannot trap execution in the former unbounded synchronous retry loop or remove the runtime's cancellation opportunities. EINTR is not treated as successful queue consumption. - **P3-1 shim environment closed.** `crates/infraternal-agent/src/sessions.rs:1039` starts from `env_clear`, then the allow-list at `:685`. No loader, shell startup hook, HOME or PATH is inherited. Locale variables do not carry an executable search path; Linux ZDOTDIR is revalidated before use. XDG_RUNTIME_DIR and DBUS_SESSION_BUS_ADDRESS are transport hints used for the same-UID user manager, not a person's route to a root user-manager launcher: `Manager::detect` selects System for root and User for non-root (`sessions.rs:80`). `on_path` (`sessions.rs:699`) does trust the infraternal agent's own PATH and its directories; it is not independently a privileged-path validator. Session input does not supply/change that PATH, and the installed system service does not import the enrolling person's environment (`crates/infraternal/src/up.rs:1118`). An administrator starting a root process with a user-writable/relative PATH still has that administrative trust obligation; no new lower-privileged input path was found. - **P3-2 ZDOTDIR closed.** `crates/infraternal-shim/src/shell_startup.rs:91-108` canonicalizes, rejects symlink components encountered during validation, requires every canonical directory to belong to root or the target UID, and refuses group/other write on the leaf even with sticky set. Sticky applies only to ancestors. A peer can create its own `/tmp` entry but that entry fails ownership validation; it cannot replace the trusted root/target-owned entry in sticky `/tmp`, or create startup files inside a nonwritable leaf. POSIX named ACL write permission is bounded by the group-class mask reflected in st_mode, so the new 022 rejection also covers it. Returning the canonical path removes dependence on the original symlink; the accepted ownership/mode chain prevents another UID from retargeting it before shell exec. Root/the target UID remain trusted actors, rather than a TOCTOU security boundary. - **P3-3 descriptor fallback closed.** `crates/infraternal-shim/src/pty.rs:1200` opens `/proc/self/fd`, reads all getdents64 batches into a 4096-byte stack buffer and marks each descriptor with fcntl. Marking CLOEXEC does not mutate the directory entries or skip later batches. There is no RLIMIT/capped scan. Every reachable post-fork operation is stack/scalar work or raw open/syscall/fcntl/close; no opendir/readdir, allocation or allocator lock. The `usize::try_from(read).map_err(io::Error::other)` allocation branch cannot execute for a nonnegative c_long byte count fitting Linux usize. An unreadable/unavailable listing returns Err from the pre_exec hook and refuses spawn, rather than proceeding with inheritable descriptors. The listing descriptor itself opens CLOEXEC and is explicitly closed. No Mac lock taken: the questioned new boundaries were reviewable in source and the four requested Linux regressions; I did not independently repeat J146's launchd proof or claim a pre-5.11 live-kernel run. **Independent compute01 evidence** (user claude, `df -h /` before building: 316G free; build-slot, nice -n10, nix develop, cargo --locked -j4): ``` == a_permanently_interrupted_call_gives_up == test readiness::interrupt_tests::a_permanently_interrupted_call_gives_up ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 121 filtered out; finished in 0.00s == startup_directory_must_belong_to_the_person_or_root == test shell_startup::tests::startup_directory_must_belong_to_the_person_or_root ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 121 filtered out; finished in 0.00s == the_listing_fallback_marks_descriptors_above_the_limit == test pty::descriptor_allowlist_tests::the_listing_fallback_marks_descriptors_above_the_limit ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 121 filtered out; finished in 0.05s == only_allow_listed_variables_reach_the_shim == test sessions::shim_environment_tests::only_allow_listed_variables_reach_the_shim ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 150 filtered out; finished in 0.00s J224_REGRESSIONS_EXIT=0 ``` The first three used `cargo test --locked -j4 -p infraternal-shim --features async-fd --lib <test> -- --nocapture`; the fourth used `cargo test --locked -j4 -p infraternal-agent --lib only_allow_listed_variables_reach_the_shim -- --nocapture`. **Gate disclosure:** no code changed, full gate not applicable under BRIEF-COMMON “Planning/research jobs and the gate” (explicitly covers jobs that change no repository code). I initially over-applied the standing gate rule. The extra quick run produced this verbatim summary: ``` +---------------------------+--------------+ | Rust gate | Result | +---------------------------+--------------+ | scope guard | PASS | | migration names | PASS | | fmt | PASS | | doc coverage ratchet | PASS | | clippy (workspace) | PASS | | unit tests (changed) | PASS | | unit tests (changed, bin-only) | FAIL (101) | +---------------------------+--------------+ ``` The seven bin-only failures were restart/signature fixtures with `PermissionDenied`: my runner put executable fixtures under `/tmp/j224-state`, so the existing replacement validator correctly refused their world-writable `/tmp` ancestor. No product repair was needed. With the same binary and nix shell, changing only the fixture root to mode-0700 `/home/claude/j224-state` made the directly checked failing fixture pass: ``` running 1 test test restart_binary::audit_tests::pathname_replacement_keeps_the_admitted_inode ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 158 filtered out; finished in 7.09s ``` The mistakenly started `scripts/verify fast` was stopped during compilation (exit 143), and the corrected extra gate was cancelled while queued, rather than spend another scarce slot on a no-code review. I claim no fresh green full gate. J146’s prior full-gate evidence remains in comment 79422. Cleanup: own active gate process group and queued runner stopped, no J224 test process remains; the one target directory and temporary fixture roots are removed. Logs are retained under `/home/claude/j224-*.log` on compute01 and attached here. The target branch and head remain unchanged. No push, merge, deployment or issue closure. **APPROVE**
Sign in to join this conversation.
No labels
launch-qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/infraternal#328
No description provided.