macOS: terminal sessions (Darwin session runtime, D335) — J146 #328
Open
opened 2026-11-03 16:17:28 +00:00 by kayg
·
29 comments
No Branch/Tag specified
main
j470b-server-mesh-rejoin
mesh-compat-382
j444b-d381-key-wins-flood
j461b-macos-utun-reader-thread
j475b-remove-host-motion
fix-reconnect-new-link
j481-d380
dev
j479-mesh-soak
fix/j476-host-facts
j475-motion-pill-split
j470-mac-node-join
root-upgrade-d376
j474-review-merge
latency-targets-120hz
nightly-suite-shards
gotatun-pin-j469
macos-utun-reader-thread
fix-flood-echo-stall
wip/j416
motion-fast-ease-out
mesh-addhost-j259
takeover-close-terminals-d368
kbd-instant-followups
fix-trust-key-double-click
nightly-suite-partial-report
appwin-icons-j207
rusqlite-slice1
swipe-content-first-d374
mesh-mtu-381
addhost-instant-open
glass-contrast-d365-mock
glass-contrast-d365
signout-followups-442
fix-startup-toast-red
research-gotatun-matrix-j317
themes-sync-j304
fix-load-flakes
fix-nix-zig-patch
fix-compute01-browser-baseline
ratchet-size-baseline-419
predict-auto-d130
revoke-coverage-d367
nightly-suite-queue-timeout
nightly-browser-suite
fix-rustsec-2026-0459
fonts-ioskeley-obel
log-retention-d375
fix-kbd-no-anim-427
fix-dev-red-specs-2
takeover-proof-305-j205
wip/j410-server-pump
ui-consistency-d372
fix-osc52-clipboard
libghostty-vt-1.0
forget-retired-host-d369
j404-merge-426
fix-setup-lock-race
fix-touch-select-426
revoke-access-report-ui
fix-predictive-echo-specs
fix-selinux-agent-exec
wip/j395-checkpoint2
wip/j395
j391-review-scratch
ui-consistency-j195-deferred-rebased
fix-rustc-paths-421
decisions-wording-341
fix-install-suite-red
fix-dev-rtt-specs
fix-pending-removal-align
fix-privileged-skip
fix-install-suite-404
review-j368
mesh-dns-j254
revoke-access-report-d356b
revoke-privilege-round5
fix-removal-confirm
review-j360
gotatun-rotation-367
deps-refresh-340
review-j354
review-J351
review-j350
addhost-takeover-d359
revoke-report-items-347
revoke-end-sessions-d362
review-j348
size-mesh-ssh-346
size-trim-337
revoke-access-diff-d356
j259-pre-rebase-b06c5a6
j305-ended-at-start-spec
addhost-sudo-consent
fix-layout-real-specs-411
fix-shell-open-411
fix-console-hosts-373
fix-shell-exit
fuzz-host-name-353
fix-host-details-410
macos-filter-j197
research-gotatun-tricks
revoke-p3-j216l
stale-state-pill
revoke-followups-j216k
revoke-progress-j216e
backup/macos01-20261106/lume/j197
backup/macos01-20261106/lume/appwin-icons-j207-stash0
backup/macos01-20261106/claude/stash-0
backup/macos01-20261106/claude/stash-1
backup/macos01-20261106/claude/J80-macos-s1
backup/macos01-20261106/claude/J80
backup/macos01-20261106/claude/J80-slice1-20261102
backup/macos01-20261106/claude/J16b-appwin-on-dev
backup/macos01-20261106/claude/appwin-launch-idle-146
backup/macos01-20261106/claude/J80-s1-run
wip/j259
trust-285-p3c
runtime-lock-j216f
mesh-ssh-p3-j175
tun-proof-p3b
trust-285-p3
terminal-polish-j271
tun-proof-p3
ssh-install-flakes-j216i
fuzz-fixes-j264
trust-285-a
trust-285-b
restart-oversized-fixture-j198
backup/macos01-20261106/lume/appwin-icons-j207
restart-bound-j216h
up-preflight-p3-j198
backup/macos01-20261106/lume/j251
up-preflight-j198
wip/up-preflight-j198-j245-p2
wip/up-preflight-j198-j245-p1
agent-fsync-offloop-j196
wip/up-preflight-j198-j245
settings-offscreen-j231
wip/up-preflight-j198-j226-inspection
wip/j103f
themes-j206
wip/up-preflight-j198-j226
flakes-j216c
backup/macos01-20261106/claude/j197
flakes-j216b
backup/macos01-20261106/claude/j197-red
wip/up-preflight-j198-review
flakes-j216
backup/macos01-20261106/claude/appwin-icons-j207
engineering-j208b
appwin-mock-j209
toast-stack-j165
engineering-j208
decisions-d351
decisions-d350
decisions-d349
appearance-presets-mock-j129
decisions-d348
docs-audit-spec
doc-coverage-baseline-raise
decisions-d347
decisions-d346b
decisions-d346
j88h-profile
ui-consistency-j195-deferred
ui-consistency-j195
engineering-practices-j194
preferred-login-ui-j166
signin-pools-263-j174
macos-node-265-s1
decisions-d345
decisions-d344
proxy-cgroup-j164
verify-quick-fuzz-329
proxy-start-io-j168
timing-flakes-j163
up-263-s1
mcp-events-j147
verify-quick-fuzz-j158
proxy-p3-j159
agent-resource-j157-detect
wip/j103d-final
remote-cancel-hang-j155
proxy-traefik-281-j62d
j149-verify-quick
proxy-traefik-281-j62b
j150-drop-unnamed
wip/j79d-red-r4
wip/j103d-d332
rootless-toast-j142
wip/j103d-head
wip/j103d
j125c-mcp
macos-build-j136
backup/macos01-20261106/claude/j136
wip/j79d-red-cli
wip/j79d-red-server
j121-link-liveness
j118c-tracer-failures
wip/j79c-head
release-desktop-j131
j124c-signed-macos
wip/j79c-red-p3
wip/j79c-red-p23
wip/j79c-red-p22
merge-j128-check
perf-deferred-j128
sidebar-toggle-260
j127-shortcut-changed-dot
j125-mcp
j118-tracer-failures
j126-account-summary
settings-sections-mock-j92
j120-pinch-lock
sidebar-292
backup/macos01-20261106/claude/j120
taste-2026-11-03
row-actions-242-215
backup/macos01-20261106/claude/stash-0-untracked
backup/macos01-20261106/claude/J80-265-s1-run
backup/macos01-20261106/claude/J80-macos-s1-20261102
j112-canvas-commands
ui-no-overshoot-scroll
fix-recovery-restart
backup/macos01-20261106/claude/stash-1-untracked
taste-2026-11-02
size-baseline-ratchet
machine-cards-structure-j108
j109-boot-copy-mockups
fix-dev-red
wip/up-263-s1
j95b-settings-rows
proxy-traefik-281
j95-option-rows
docs-human-j91
docs-keys-palette
fix-flood-1013-270-j38d
mesh-ssh-s1
mesh-ssh-s0
addhost-d291-tunnel
test-stability-load
mesh-relay-220
repro-296
shim-thp-off-24
mesh-pins-258-v2
mesh-dns
signin-d311-merged
docs-pages
ui-size-283
fix-ui-batch-specs
wip/j12b-edge-update
gate-on-arm
fix-rename-sync-293
trust-285-j60b
fix-focus-ring-clip
integ-ui-1101
edge-update-v2
signin-d311
taste-2026-11-01
layout-per-tab-250
ui-approved-splitopt-224
ui-approved-splitopt-224-on-dev
icon-text-centre-282
ui-approved-189-212-216
research-reverse-proxy
transfer-budget-320
review-mesh-pins-258-r2
sidebar-pill-259
review-mesh-relay-220
wip/j12-edge-update-v2
nix-package
status-crash-loop-237
fix-dev-test-failures
fix-fixture-socket-path
wip/mesh-relay-220-pre-dev-advance-rebase
fix-canvas-side-clip
research-head-to-head
frosted-real-256
review-mesh-pins-258
wip/mesh-relay-220-pre-final-rebase
review-layout-per-tab-250-r2
broad-suite-triage
nix-devshell
review-status-crash-loop-237-r3
wip/nix-devshell-pre-p3
taste-frosted-real
wip/code-serve-demo-2026-10-30
wip/code-serve-glass-2026-10-30
wip/code-serve-clusters-2026-10-30
wip/code-merge-release94-dev-2026-10-30
wip/rproxy-delta-scratch-2026-10-30
wip/stale-note-257-staged-2026-10-30
wip/d241-size-staged-2026-10-30
review-broad-suite-207
wip/mesh-pins-258-pre-split
wip/mesh-relay-220-pre-rebase
mesh-measure-220
plan-211-update
review-edge-autoupdate-r3
perf24-reconnect
review-stale-note-r2
release-prod-live
d241-shell-marker
review-mesh-pins-server-258
review-revocation-r3
review-setup-token-r2
review-sync-journal-r3
wip/review-mesh-pins-258-prune-review-1995fee46
frosted-scroll-256
sync-journal-r3
revocation-per-account-wip
review-tokio-asyncfd-0386
review-sync-p3s
wip/stash-8by64-stash0-2026-10-30
mesh-drag-r2
setup-token-data-wip
release-9-4
auth-busy-1013
mesh-view-overlap
sync-p3s
review-auth-busy-wake
review-readd-r8-wip
mesh-drag-bounds
audit-auth-93
app-windows-stream-start
review-authority-cap-r3
app-windows-r7-measure
readd-minimal-r6
readd-minimal-r5
app-windows-r7-soak
readd-minimal-r4
app-windows-r7
app-windows-r6
readd-minimal-r3
readd-minimal-r2
readd-minimal
merge-release8-dev
review-busy-page-liveness-r2
review-busy-page-liveness
mesh-view-trim
review-authority-read-retry-r2
review-remove-uninstall-r2
release-retry-reattach
release-retry
review-remove-uninstall
review-authority-read-retry
app-windows-ship
mesh-autoswitch-p2
mesh-ssh
mesh-relay
mesh-macos-node
screen-and-control
session-labels
review-real-logins-r5
review-real-logins-r4
review-real-logins-r3
shim-memory-r2
review-real-logins-r2
shim-memory
shadow-addhost-instant
shadow-real-logins
shadow-same-session-flood
shadow-prod-perf-fix
hosts-from-server
app-windows-real
app-windows-transport
release-prod-6w
mesh-policy-verify
quiet-close-toast
dev-merge-r4
review-mesh-agent-transport
review-app-windows-transport
settings-roundtrip
review-release-pipeline-r4
mesh-agent-transport
ended-strip-fix
review-audit-ca-update-d244
settings-rows
phone-width
signout-clear
fix-restart-tests
dev-merge-rp
signer-hardening
review-audit-ca-update
terminal-fidelity
review-audit-ca-update-wip
mesh-integrate
review-mesh-agent-transport-515d3712
review-mesh-dataplane-feec1988
mesh-dataplane
signout
research-canvas-node
wip/review-renderer-cost
wip/review-terminal-fidelity
padlock-glyph
review-release-pipeline-2e95b689
phone-polish
sidebar-polish
inbox-generic
review-mesh-dataplane-378d4055
prod-batch
ui-inbox
ui-ca-unlock
ui-machine-sections
review-mesh-dataplane-a5f744fb
tls-session-prod
session-bounds-prod
fix-dead-session
session-bounds
tls-session
palette-polish
shortcuts-d222
research-appmeta
phone-gutter
ui-centre-button
wip/ui-centre-button
wip/tls-session
review-addhost-breakit
ui-platform
renderer-cost
remote-agents
mesh-api
ssh-bootstrap
prod-hotfix2
prod-fixes
review-remote-agents
review-mesh-dataplane
session-life
research-browser
review-mesh-api
research-capture
review-mesh-api-round3
review-mesh-api-round1
gate-stability
review-gate-stability
review-relay-direct
review-mesh-api-findings
proto-sessions
font-picker
proto-addhost
proto-windows
mesh-core
latency-gate
research-bsd
research-discovery
keytray-mods
mock-core
ui-tweaks
fix-motion-sel
fix-motion
integrate2
fix-robust
t-web
t-server
vt-ghostty
fix-snap-echo
t-web-auth
review-vt2
t-agent
lane-protocol
lane-shim
lane-auth
s1-base
s1-contracts
s1-workspace
No results found.
No labels
launch-qa
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
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
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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:
launchctl asuser/ bootstrap) when logged in, otherwise the plain shell.getpeereid/LOCAL_PEERCRED), pid start time (proc_pidinfo), no core dumps and no debugger attach,mlockfor 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-stdunderlockf -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 start (Claude Opus 5.5, 2026-11-03 ~21:40 IST). Branch
macos-sessions-j146, basemacos-build-j136head3e3c84384(which sits on J80'smacos-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 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
0600socket 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.build.rsandscripts/build-vtbuild the pinned archive and our snapshot adapter foraarch64-macoswith the single pinned Zig 0.15.2 (D94, D213). The Linux build path stays byte-identical.openpty,setsidandTIOCSCTTY;FIONREADandTIOCSWINSZ; the signal reset loops toNSIG(macOS has noSIGRTMAX); no THP calls.signalfdbecomes a kqueueEVFILT_SIGNAL/EVFILT_PROCsource.getpeereidandLOCAL_PEERCRED(xucred, version-checked), replacingSO_PEERCRED.initgroups(falling back togetgrouplist/setgroupsif the 16-group limit truncates), thensetgid, thensetuid, with a check thatsetuid(0)fails afterwards. Thenchdiras the user andenv_clearwith a fixed allowlist, so noDYLD_*or inherited variables reach the shell (the lesson of J135's P1-1)./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.gui/<uid>launchd domain exists, checked when the terminal opens). An SSH-only session does not count. This is an owner question, asked below.ptrace(PT_DENY_ATTACH)in the shim.RLIMIT_CORE=0, hard and soft.proc_pidinfo(PROC_PIDTBSDINFO)(pbi_start_tvsec/usec), replacing/proc/<pid>/statfield 22.EVFILT_PROC NOTE_EXIT, re-checked against the start time to rule out PID reuse.mlockfor secret pages (D244): macOS has noMADV_DONTDUMP. I'll applymlocktogether withRLIMIT_CORE=0only where the session path actually holds secrets. The CA key heap stays refused off Linux as J136 left it (LockedBytesis 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.launch_configured: detached double fork, nosystemd-run.EVFILT_VNODEinstead of inotify.process_exitson kqueue.lockf -k /tmp/mac-bench.lock(2 h). lume's keychain and TCC are never touched; the only keychain step is the read-onlysecurity list-keychainsthe task asks for.security list-keychainsshowslogin.keychain-db.list-keychainsshows there.scripts/verify fastverbatim 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 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).Consequences:
security list-keychainsalone does not prove placement. The search list comes from$HOMEpreferences, so a plain shell lists the login keychain too. The Mac proof shows it as asked, plus the real signals:launchctl managername=Aqua, andSSH_AUTH_SOCKequal to the gui domain's agent socket. In the plain case they areBackgroundand no agent socket.gui/<uid>exists), becauselaunchctl asuseraccepts any uid.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), takesSSH_AUTH_SOCKfrom the gui domain only if it is a socket owned by that uid, and execs the login shell with the allowlisted environment.J146 finding 2: libghostty-vt builds for macOS;
launchctl asuserjoins the GUI audit sessionscripts/build-vtwithTARGET=aarch64-apple-darwin(pinned Zig 0.15.2 copied from the nix fixed-output derivation) givesBUILDVT_EXIT=0andlibghostty-vt.ain 2m25s cold. The only script change macOS needs is theflock(2)fallback through perl, because util-linux'sflock(1)is Linux-only.getaudit_addrprogram. No keychain access.So
asuserplaces the process in the person's GUI audit session (keychain unlock state,0x10graphic access) as well as their launchd bootstrap (Aqua, ssh-agent). No separateaudit_session_joinis needed.getlogin()reportsrootafter a plain drop, so the session-shell entry callssetlogin(name)(it is the session leader) before it drops privileges.asuser, the entry reads its owngetaudit_addr. Ifauidis 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 progress (~22:55 IST). WIP is pushed on
macos-sessions-j146(93405fbd8plus local fixes; the atomic split is next). The Darwin session runtime works end to end on the Mac VM (macOS 26.5.2, underlockf -k /tmp/mac-bench.lock).The proof drives
infraternal agent --fd 3as root. That is the real leg-B link the server uses, driven with the repo'sagent_fixturethrough the newscripts/test-macos-sessions.py: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):
readiness::Registrationfailed with EINVAL on macOS. It registered read and write interest, and a kqueue descriptor refusesEVFILT_WRITE. This also affected J80's existingprocess_exits_macos.rs, which never ran because the daemon was not compiled on macOS. Kqueue sources now register read-only./proc/<pid>/stat, so after a restart the census was empty on macOS. It now usesproc_pidinfo(PROC_PIDTBSDINFO)start time in µs, and refuses zombies.plugin-meshcannot 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 minusplugin-mesh, and its session code is identical. A proof through J80's launchd service plus a server comes after J80d lands.PT_DENY_ATTACHon the shim also blocks DTrace (dtrussshows nothing). That is intended.Next: an exit-watch step in the proof, a check that
PT_DENY_ATTACHis not inherited by the shell, the commit split, Mac clippy for the touched crates, then Linuxverify fastand static sizes. Mac target:~/worktrees/j146/targeton the Mac VM.J146 progress (~00:15 IST). Atomic history is pushed:
macos-sessions-j146head6363a04c4, 7 commits on J1363e3c84384.verify fast(at7ed513224, 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 inSocketReadiness. 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 at6363a04c4is waiting for a dev-02 slot.3e3c84384nix develop -c scripts/macos-build-checkat7ed513224givesMACOS_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.dyliband libSystem; libghostty-vt is linked statically.lldb -p <shim>givesattach failed: lost connection, andlldb -p <the person's shell>attaches, soPT_DENY_ATTACHis not inherited.J146 finish. Branch
macos-sessions-j146, head6363a04c4270f9c77e496a3986e470a627640de3, stacked on J1363e3c84384(on J80macos-node-265-s1). J80's and J136's commits are untouched. Not merged, not deployed. A security review is next.Commits (
3e3c84384..6363a04c4):f35fd05dbshim: register kqueue sources for read readiness only. Every macOS kqueueRegistrationfailed with EINVAL, including J80'sprocess_exits_macos.rs.d57eddb1cbuild: libghostty-vt foraarch64-macoswith the pinned Zig (D94, D213);flock(2)fallback through perl.37f481cb6shim: the Linux PTY child setup moves into its own function (refactor, no behaviour change).393bc74e6shim: the PTY session engine on macOS with D335 placement (__session-shell: setlogin, initgroups, setgid, setuid, verified drop, allowlisted environment,launchctl asuserwith placement checked from the audit record),PT_DENY_ATTACH, getpeereid, kqueue SIGCHLD. host-login stays Linux-only (D247).fb1694a4eagent: the terminal daemon and shim launcher on macOS (kqueue vnode readiness and retirement events,/var/run/infraternal).865aa1cecagent: surviving shims identified byproc_pidinfostart time (D81).6363a04c4test: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 ofinfraternal agent --fd 3). The upgrade goes from a build of17d280bd3(the agent before the shim-identity commit, sha256374c0bdb…) to a build of the head code (a60b0780…, built at7ed513224; after the rebase, the head differs only in a Linux unit test). Build features: default minusplugin-mesh(reason under "Open").lldb -p <shim>givesattach failed: lost connection, andlldb -p <shell>attaches, so the flag is not inherited.nix develop -c scripts/macos-build-checkgivesMACOS_CHECK_EXIT=0(log attached)./usr/lib/libiconv.2.dyliband libSystem.-D warningsis clean forinfraternal-shim --all-targetsandinfraternal-agent --features remote. The only remaining binary-level lint is J136'sssh_internal.rs:62(unnecessary_wraps); I left it.Linux static sizes (dev-02, nix, one target directory):
3e3c84384scripts/verify faston infraternal-dev-02-std at6363a04c4(verbatim, third run, log attached):The gate is not green. Only the
testrow fails, on a different test from run to run: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_environmentand…_rootless_private_readiness_does_not_refuse_person_revocation. In each, rootless readinessterminal.openreturnsresource_unavailableor times out.remote_client::supervisor::review_tests::durable_state::acknowledged_network_access_survives_superseded_worker_completion.3e3c84384, same host: alsotestFAIL (101), onsynthetic_master_socket_is_private_at_creation. Itsagent_daemonpassed. Log attached.agent_daemon2/2 with-p infraternaland 5/5 under--workspacefeature unification; the supervisor test 5/5. Base failed 1 of 5agent_daemonruns 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_daemonfailures 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:
LockedBytesis uninhabited). macOS has no per-mapping dump exclusion likeMADV_DONTDUMP. The process-wide hardRLIMIT_CORE=0is not equivalent, because a root process can raise it. The session path holds no secret pages, so nomlockwas needed there.infraternal sessions): it stays Linux-only. It proves the caller sits inside a saved session by walking/procancestry, and there is no reviewed macOS process-tree check for it yet.Open / for kayg:
auid=501in the proof, from sudo). Under launchd it would be unset. Should a plain shell get its own audit session withauid=<uid>? That is a question for the security review.plugin-mesh. The proof therefore used default features minusplugin-mesh; the session code is identical. After J80d lands: rebase, then a proof through J80's launchd service and a server.PT_DENY_ATTACHalso 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-outon dev-02 and~/j146-evidenceon the Mac.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:
__session-shellentry andlaunchctl asuserplacement, the plain-shell fallback, socket and file modes, the environment allowlist, the kqueueEINVALfix, the restart path, and the Zig pins, nix FOD and release builder. (Done.)lockf -k /tmp/mac-bench.lock(queued behind another job). A build of6363a04c4, then a probe run as a launchd system job (the production audit and bootstrap context, notsudofrom 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-onlyclaudeaccount.Already checked:
scripts/zig.sha256matches ziglang.org'sindex.jsonfor 0.15.2 on all three hosts (x86_64-linux02aa270f…, aarch64-linux958ed7d1…, aarch64-macos3cc2bab3…).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 of6363a04c4(default features minusplugin-mesh, as J146 used; sha2563cf70201…), installed root-owned. The probe ran as a launchd system job (launchctl bootstrap system <plist>, thenbootout), so the infraternal agent had the audit and bootstrap context of a LaunchDaemon. J146's proof started it withsudofrom 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, contentsROOT-ONLY-J180-SECRET-MARKER) and withDYLD_LIBRARY_PATH,DYLD_FALLBACK_LIBRARY_PATH,BASH_ENV,ENVand 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 noclosefrom,/dev/fdwalk orPOSIX_SPAWN_CLOEXEC_DEFAULTanywhere 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: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_CLOEXECorpipe2, 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 (Commandwithpre_exec, so fork+exec, not posix_spawn) while other threads connect to shim sockets,plugins.sockand 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 useproc_pidinfo(PROC_PIDLISTFDS)) before the privilege drop. Also spawn the shim's child, and the agent's shim launch, withPOSIX_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
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
Backgroundandauid=501only because it started the agent withsudofrom lume's SSH login.check_plain'sBackgroundassertion 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 withauid=<uid>and the user'sBackgroundbootstrap. In the entry, requireauid == uidfor 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 withauid == uid(a developersudofrom Terminal), a "plain" shell is classified as placed and takesSSH_AUTH_SOCKfrom that bootstrap. Run the proof as a launchd system job, as here.P3-1:
ZDOTDIRfrom the root agent's environment reaches every person's shelldarwin_commandandsession_shell::PASSEDforwardZDOTDIRunchecked. In the proof, lume's login shell ran withZDOTDIR=/Users/claude/worktrees/j180/crates/infraternal/tests/profile, a directoryclaudeowns. That is a cross-user path for startup code. A production plist sets noZDOTDIR, 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. (Linuxlinux_commandhas the same pass-through, which predates this slice.)P3-2:
INFRATERNAL_ZIGbypasses the checksumscripts/auth-toolchaincopies any directory named byINFRATERNAL_ZIGwhosezig versionprints0.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.pyshould 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
claude:launchctl asuser 501 id→Operation not permitted.__session-shell --uid 501from 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 asgui/501).setuid(0)re-check, andchdirafter the drop.launchctl getenvand the audit check also run after the drop.SSH_AUTH_SOCKis accepted only as a socket owned by the uid.DYLD_*,BASH_ENV,ENVand the marker were stripped from both shells. The entry gets onlyTERM,COLORTERM,INFRATERNAL_SESSION,ZDOTDIRand locale, plus fixedPATH,HOME,USER,LOGNAME,SHELLandTMPDIR.stateandsessions/aredrwx------ root, shim sockets aresrw------- root, and the peer must be uid 0 or the shim's uid (getpeereid).PROBE_DONE). Shim descriptors were 0–2 on/dev/null, a kqueue, the socket andptmx(plus the planted 9, see P1).PT_DENY_ATTACHwas verified by J146.EINVALfix: confirmed on the Mac. Registering a kqueue descriptor in another kqueue givesREAD okandWRITE [Errno 22] Invalid argument. Only theKqueuesource gets read-only interest.OwnedFd,UnixStream, epoll and inotify are unchanged.scripts/zig.sha256matches ziglang.orgindex.jsonfor 0.15.2 on all three hosts.nix/zig.nixandnix/package.nixfetch throughfetchurlwith that table (FOD).build-vtkeeps its sha256 check for libghostty-vt and fails closed whensha256sumis missing..cargo/apple-cc.shonly strips the nix SDK and flags.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 fastis stilltestFAIL (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-probeand the root install are removed, the Mac worktree and its target are deleted, and the lock is released. No branch was changed.J146 round 2 progress (J180 fixes + rebase onto dev).
macos-sessions-j146is now rebased onto origin/dev (which has #265 merged asc4bc39984), viagit 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):
26d7ffd62P1: only the standard descriptors reach shims and shells. The allowlist hook isclose_range(CLOSE_RANGE_CLOEXEC)on Linux andPROC_PIDLISTFDSon 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 fromlinux_command,a_pty_program_inherits_only_its_terminalprintsDESCRIPTOR-LEAKED; with it, it passes 3/3.e477fb516P2: a plain macOS shell runs throughlaunchctl asusertoo, and its own audit record must name the person (classify).5de3947e0P3-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 forstartup_directory_must_belong_to_the_person_or_root.6783afa7aP3-2: INFRATERNAL_ZIG must be thenix-store --verify-path-verified output of nix/zig.nix.scripts/test-auth-toolchain-pinfails against the old script ("accepted a look-alike") and passes now.7cb5a06efP3-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.b3328e0bddocs (D345 ratchet), and4b0df9f1a: the rebased agent died on EINTR from kqueue drains once dev's #324 launch wait handled SIGCHLD;readiness::kevent_nowretries.Rebase resolutions:
main.rstakes dev'srun_upplus J136's Linux gate for the SSH session.sessions.rskeeps dev's #324await_launch/wait_for_hello, made portable throughSocketReadiness, with LAUNCH unchanged on macOS; dev's new #324 tests useSocketReadiness::new.pty.rsadds the P1 helpers next to dev's proxy child; dev'spty::prepare_proxy_childis gated Linux-only like its public wrapper (macOS refuses earlier, and nix has no Darwinsetgroups).build-vtnow 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:Red on the old head
6363a04c4, same harness: lume's shell hasFD9=openandPLANTED=/private/tmp/j146-planted-zdotdir. At the rebased head:macos-build-check: PASS(MACOS_CHECK_EXIT=0), and Mac clippy-D warningsis clean for shim and agent.Linux: the gate at
84cd599fcfailed only itstestrow, 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 finalverify faston the final head. The dev-02 queue is long (position 3); I keep polling.J146 round 2: the Linux gate at the final head
4b0df9f1a. Every row is green excepttest, which hit a different load flake in each run.scripts/verify faston infraternal-dev-02-std at4b0df9f1a4c4fcb23ebc08076c6d5e9437b89fc4(run 2, verbatim):The
testfailures 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:84cd599fcinfraternalrestart tests (60 s deadline, test row 1747 s)4b0df9f1a: run 1 FAILED 5/19 (load 16.9), run 2 ok 19/19; dev070c9a0b6: 2/2 ok (load 8.6–8.8)4b0df9f1arun 1proxy::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)81f9381655/5 ok, head 5/5 ok4b0df9f1arun 2release::pending_channel_writers_preserve_authority_read_capacity(SQLxPoolTimedOut)The
restarttests passed inside the4b0df9f1agate runs. Linux static sizes at4b0df9f1a: 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 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
testfails, on a different load-sensitive test each run.Branch
macos-sessions-j146, head4b0df9f1a4c4fcb23ebc08076c6d5e9437b89fc4, rebased onto origin/dev843ec7eec(which contains the #265 mergec4bc39984) withgit rebase --onto origin/dev 70a5376b2. That is J136's 7 commits, my 7, and 7 new ones. Not merged, not deployed.New commits:
26d7ffd62P1, descriptors. Shells and shims inherit only 0–2 (an allowlist:close_range(CLOSE_RANGE_CLOEXEC)on Linux,PROC_PIDLISTFDSon 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.e477fb516P2. Plain shells also go throughlaunchctl asuser, and the entry requires its own audit user (classify).5de3947e0P3-1. ZDOTDIR is never forwarded on macOS; on Linux only from a root- or person-owned directory. The entry's environment allowlist is audited.6783afa7aP3-2. INFRATERNAL_ZIG must be thenix-store --verify-path-verified nix/zig.nix output. New gate check:scripts/test-auth-toolchain-pin.7cb5a06efP3-3. The proof runs the agent as a launchd system job and plants descriptor 9 plus five variables.b3328e0bdD345 docs for every new item (the doc-coverage ratchet now passes).4b0df9f1aAfter the rebase, dev's #324 launch wait handles SIGCHLD, and macOSkeventreturned EINTR, which ended the agent.readiness::kevent_nowretries.pty::prepare_proxy_childgated Linux-only like its public wrapper; LAUNCH portable;build-vtdrops the shared libghostty-vt (J80e's new image check caught a test binary loading@rpath/libghostty-vt.dylib).Tests that failed first:
a_pty_program_inherits_only_its_terminalprintsDESCRIPTOR-LEAKED; with it, it passes 3/3.startup_directory_must_belong_to_the_person_or_rootis red with the check stubbed, green 3/3.6363a04c4.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.logandproof2.logattached):Also at
4b0df9f1a:macos-build-check: PASS(MACOS_CHECK_EXIT=0), and Mac clippy-D warningsis clean for shim and agent.Linux
scripts/verify faston infraternal-dev-02-std at4b0df9f1a(run 3, verbatim; runs 2 and 3 attached):Not green. Across the four gate runs at
84cd599fc/4b0df9f1a, every other row passed every time.testfailed each time on a different test, none of which this stack changes for Linux:84cd599fcinfraternalrestart tests, 60 s deadline (test row took 1747 s)4b0df9f1a#1proxytraefik_child_receives_only_credential_file_paths…(reads its capture file before the fake child writes it)4b0df9f1a#2releasepending_channel_writers_preserve_authority_read_capacity(SQLxPoolTimedOut)4b0df9f1a#3restart::…::relative_recovery_after_atomic_install_keeps_live_install_path:ETXTBSY, Text file busy (an exec racing a concurrent fork)The restart tests passed inside gate runs 1 and 2 at
4b0df9f1a. A greenverify fastneeds 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:843ec7eec4b0df9f1aFor the orchestrator / kayg:
testrow is flaky under load across unrelated crates. The orchestrator's merge gate should rerun it.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).J215 starting the focused security re-review of #328 at
4b0df9f1a4on branch macos-sessions-j146, based ate99097fa80(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 progress — 2026-11-04
Branch
macos-sessions-j146, head4b0df9f1a4c4fcb23ebc08076c6d5e9437b89fc4remains unchanged.The Mac debug/release builds completed under the Mac lock. The real launchd proof is waiting for
/tmp/mac-bench.lock: a livelockf -k ... /bin/bash -lisession still holds it, so I have left that session untouched. The root Linux proof is queued throughbuild-sloton 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:
crates/infraternal-agent/src/sessions.rs::isolatedbuilds the shim command withCommand::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.crates/infraternal-shim/src/readiness.rs::kevent_nowretriesEINTRin 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 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.lockbehind live job J207. It preserves the fixture's read-onlysecurity list-keychainscheck, adds PATH and shim-environment probes, and uses a temporary audit-session probe.The root Linux proof is second in compute01's
build-slotqueue. 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-j4and includes the symlink-retarget probe that runs zsh as uid 1500. On this host GID 1500 belongs only toclaude, 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_nowEINTR loop (P2). No repository branch has been modified.J215 Mac real-system finding — branch
macos-sessions-j146, head4b0df9f1a4c4fcb23ebc08076c6d5e9437b89fc4.Under
lockf -k /tmp/mac-bench.lock, the launchd system-job fixture completed its shell, restart, upgrade and census checks. GUI shell audit data wasuid=501 euid=501 asid=100003 auid=501 flags=0x2030; plain shell data wasuid=1500 euid=1500 asid=100062 auid=1500 flags=0x0, distinct from daemon audit session 100001. Both shells reportedFD9=closed,PLANTED=, andPATH_MARKER=absent; the root-only descriptor was absent from both shim and shell. The read-only keychain search-list check passed.otool -Lshowed 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_PLANTEDand a hostile PATH. The two root shim processes reported all seven values in their environments:The shell environment was clean and the PATH canary was absent, but
sessions::isolateddoes 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 throughbuild-sloton compute01.J215 source-review finding on
4b0df9f1a4c4fcb23ebc08076c6d5e9437b89fc4:P2 —
kevent_nowcan retry forever on EINTR. Incrates/infraternal-shim/src/readiness.rs:164-178, the call uses a zerotimespec, then loops unconditionally whenqueue.keventreturnsEINTR. 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 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 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 finding — P3, Linux ZDOTDIR trust check accepts shared group/ACL write access.
In
crates/infraternal-shim/src/shell_startup.rs:87-105,private_groupmeans 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 reportsclaude: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, whenclose_range(CLOSE_RANGE_CLOEXEC)returnsENOSYSorEINVAL, the fallback scans only throughmin(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 Linux descriptor probe progress — on compute01 as root, the first live fixture confirmed
AGENT_FD200=/root/j215-review/j215-root-only CLOEXEC=0, then reportedSHIM_PID=802557 ROOT_ONLY_FD=absent FD_COUNT=8; the real shell output containedJ215_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 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, thenSHIM_PID=802557 ROOT_ONLY_FD=absent FD_COUNT=8; the PTY shell emittedJ215_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-178retrieskeventforever onEINTRwith 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_PLANTEDandPATH. Both shells hadFD9=closed,PLANTED=andPATH_MARKER=absent.crates/infraternal-agent/src/sessions.rs::isolateddoes not clear the inherited environment before the shim exec. The Mac audit/session evidence itself is correct: GUI shelluid/euid=501 asid=100003 auid=501 flags=0x2030; plain shelluid/euid=1500 asid=100062 auid=1500 flags=0.P3 — Linux ZDOTDIR accepts group write without verifying exclusivity.
shell_startup.rs:91-105treats 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 hasclaude: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-1188caps the fallback scan at1 << 20and returns success. On a kernel withoutCLOSE_RANGE_CLOEXEC, ifRLIMIT_NOFILEexceeds 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-pinpassed. J146's finalverify fastoutput is in comment 78959 and endedVERIFY_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.
J146 round 3 (J215's review, comment 79191): every finding fixed, origin/dev merged in, Mac launchd proof green, Linux
verify fastgreen.Branch
macos-sessions-j146, headc793d36f617c0f1d5f145f24d7e383c5aa71835b. Not merged, not deployed.origin/dev54ca98177(#305 part 2 and the remote test cfg fix) is merged in as4cbf63a32.Commits since
4b0df9f1a, each with a test that failed first:70af9e059P2: EINTR retry bounded.retry_interruptedtries 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.6bb602091P3-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 (forsystemd-run --user) and ZDOTDIR (re-checked by the shim).systemd-runis 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 withps -E.84506c076and6849d090bP3-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).f7a7b1205P3-3: the pre-5.11 Linux fallback lists/proc/self/fdexactly (rawopen/getdents64into 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).982c560e5the shim builds on its own on macOS (nixeventfeature);3f8831f52dev's macOS proxy-child stub gains the new cgroup argument, which this merge exposed on macOS (E0061).829a7f196andc793d36f6proof 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):
Mac, as a launchd system job at
3f8831f52(headc793d36f6differs only in Linux-only code and the proof script;macos-build-check: PASSand Mac clippy-D warningsclean at3f8831f52):J215's own run at
4b0df9f1ais 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
4b0df9f1atoo, so the earlier green proofs were lucky. WithAgent.settle(stream 1 quiet for 0.5 s before reuse), 6 of 6 runs are green.Linux
scripts/verify faston infraternal-dev-01-compute01 at6849d090b50e27e4b860edb1185dd4c8b240bbf1(verbatim; the head adds onlyscripts/test-macos-sessions.py, which no gate row runs):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-outon compute01 and~/j146-evidenceon the Mac.J224 focused security re-review started. Target
macos-sessions-j146atc793d36f617c0f1d5f145f24d7e383c5aa71835b, base reviewed by J2154b0df9f1a4c4fcb23ebc08076c6d5e9437b89fc4; isolated review branchreview-j224. Scope: the four J215 fixes, excluding the dev merge4cbf63a32.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/fjis absent in this checkout, so using the installed Forgejo CLI described in BRIEF-COMMON.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 apparentio::Error::otherconversion 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 compute01 regression evidence at
c793d36f617c0f1d5f145f24d7e383c5aa71835b, through build-slot, user claude. All four requested tests passed:Commands:
cargo test --locked -j4 -p infraternal-shim --features async-fd --lib <test> -- --nocapturefor each of the first three;cargo test --locked -j4 -p infraternal-agent --lib only_allow_listed_variables_reach_the_shim -- --nocapturefor 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 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
PermissionDeniedbecause I setINFRATERNAL_TEST_STATE_ROOT=/tmp/j224-state.restart_binary::Opened::open_withrefuses any world-writable ancestor, and/tmpis mode 1777. These executable fixtures require trusted home ancestry; this is also documented inrestart_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.logand 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 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 --nocapturenow reports: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 final focused security re-review: APPROVE
Target:
macos-sessions-j146,c793d36f617c0f1d5f145f24d7e383c5aa71835b, against J215's4b0df9f1a4c4fcb23ebc08076c6d5e9437b89fc4. Reviewedgit diff 4b0df9f1a...c793d36f6for the repairs, excluding changes brought by dev merge4cbf63a32. No new P1/P2/P3 security finding established in this scope.crates/infraternal-shim/src/readiness.rs:162bounds each helper invocation to eight attempts. The drains atcrates/infraternal-agent/src/sessions.rs:655andcrates/infraternal-agent/src/process_exits_macos.rs:170yield 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.crates/infraternal-agent/src/sessions.rs:1039starts fromenv_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::detectselects 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.crates/infraternal-shim/src/shell_startup.rs:91-108canonicalizes, 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/tmpentry 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.crates/infraternal-shim/src/pty.rs:1200opens/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. Theusize::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):The first three used
cargo test --locked -j4 -p infraternal-shim --features async-fd --lib <test> -- --nocapture; the fourth usedcargo 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:
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/tmpancestor. No product repair was needed. With the same binary and nix shell, changing only the fixture root to mode-0700/home/claude/j224-statemade the directly checked failing fixture pass:The mistakenly started
scripts/verify fastwas 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-*.logon compute01 and attached here. The target branch and head remain unchanged. No push, merge, deployment or issue closure.APPROVE