Person logins fail with 203/EXEC on SELinux-enforcing hosts (login binary copy labelled var_t) #401
Open
opened 2026-11-06 16:11:13 +00:00 by kayg
·
26 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#401
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?
Found by J301 (#393). On ssh.kayg.org (RHEL family, SELinux enforcing) every person login unit for kayg fails with
203/EXEC: SELinux refuses to exec the copied login binary in/var/cache/infraternal-login-bin, which is labelledvar_t. Only root can open a shell there.J311 will:
ls -Z), and record why the copy exists (D240/D247);Branch:
fix-selinux-login-execfrom origin/dev. No merge, no deploy.J311 start. Branch
fix-selinux-login-execfrom origin/dev26fbb02e3fcc83f00dc6524ee061041329da8e8b.Finding 1: confirmed read-only on production (ssh.kayg.org, AlmaLinux 10.2,
Enforcing, targeted policy)journalctl -u 'infraternal-login-*' --since -6h | grep -c 203/EXEC→9/var/log/audit/audit.log(ausearch -ts todayreturned nothing, so I read the raw log):ls -Z /var/cache/infraternal-login-bin:system_u:object_r:var_t:s0 ... infraternal-804-84a809-.../opt/infraternal/74b9c4d80/isusr_tand runs asunconfined_service_t. Soinit_tmay execusr_t/bin_tentrypoints, but notvar_t.matchpathcon):/usr/libexec/infraternal→bin_t,/usr/local/libexec/infraternal→usr_t,/opt/infraternal→usr_t,/var/lib/infraternal→var_lib_t.Why the copy exists (D240/D247)
A root infraternal agent starts each person login as
systemd-run -p User=<account> -p PAMName=login <image> __shim --login-worker. systemd execs that image after switching to the account. The installed binary sits in a root-private path that the account can't traverse, sologin_binary::publicationpublishes an account-readable, immutable (0555, root-owned, identity-named) copy. Running workers keep their image through self-updates (D251), andprunereclaims old images. The copy is needed, so it stays.Plan (about 2 h)
var_t(it runs only where SELinux is enabled).bin_t(the policy's generic executable type, keeping the user, role and level of the file's default label) throughfsetxattron the private staging fd, before the hard-link publish. A reused image gets its label checked and fixed again, so a relabel (restorecon/autorelabel) heals on the next login. No policy module, nosemanage, no permissive mode. Without SELinux nothing changes. Uses thexattrcrate the proxy plugin already ships, so there is no new dependency and nounsafe.Finding 2: reproduced on a throwaway enforcing guest, with a stock binary
Guest: AlmaLinux 10.2 GenericCloud
20261012.0(sha256cbee4726…8eca, the same release as prod), KVM on compute01 under/root/j311-vm,getenforce=Enforcing, systemd 257. Probe: copy/usr/bin/trueinto each location, then runsystemd-run --wait -p User=kayg -p PAMName=login <copy>(the same unit shape as a login worker):Finding 3 (item 4, other exec paths): one more affected surface, outside this fix
sessions.rs,systemd-run --scope … <exe> __shim): a scope is exec'd by the infraternal agent itself, not byinit_t, so the agent's own domain applies. Not affected.restart_image.rs): exec'd by the agent itself. Not affected.up(crates/infraternal/src/up.rs:40) and the SSH install (crates/plugins/ssh/src/install.rs) put the binary at/var/lib/infraternal/bin/infraternal, and the unit runsExecStart=/var/lib/infraternal/bin/infraternal agent run. That file isvar_lib_t, and the probe above showsinit_tis deniedexecuteonvar_lib_t. So a root infraternal agent installed this way on an enforcing host should not start at all. Prod doesn't hit this, because its server runs from/opt(usr_t). Self-updates that replace that file would need the same label.Recommendation: a follow-up job applies the same
bin_tstaging-label step inup_install::binary, the SSH installer and the updater, with anuprun on this guest as proof. I am not widening this branch (scope leash). It needs an orchestrator call.Progress (J311). Fix pushed:
793c1887donfix-selinux-login-exec. It labels the staging inodebin_tvia fsetxattr before the hard-link publish, and re-checks reused images. Red/green artifacts are queued on compute01 build-slot (12 in the queue):~claude/.cache/codex-scratch/j311/build.sh, logbuild.log, one target dir~claude/.cache/codex-scratch/j311/target. The enforcing guest is running (/root/j311-vm, ssh 127.0.0.1:2231 from compute01 root, key/root/j311-vm/id). Next: run the red/green unit test and the e2e proof on the guest, thenverify quick.Evidence: red/green on the enforcing guest (AlmaLinux 10.2,
getenforce=Enforcing)Artifacts are on compute01 under
~claude/.cache/codex-scratch/j311/out/. Static x86_64 musl release, built through build-slot (build.log):infraternal-base(origin/dev26fbb02e3): 27,782,064 B, sha256367c9156…306binfraternal-fix(793c1887d): 27,786,160 B, sha256c363b32c…9ee4. D243 x86 static delta: +4,096 BE2E, red (origin/dev). Root infraternal agent, person login as
kayg(uid 1001), via theagent_fixturecontrol protocol (the attachedj311-proof.py):E2E, green (this branch). Opens, types (
id -unin the shell →kayg), revokes (shell pidfd fires), and refuses a reopen. No new AVCs:The old
var_timage was reclaimed byprune. The shell runs asunconfined_u:unconfined_r:unconfined_t, so pam_selinux inPAMName=logingives the person their normal user context.Relabel heal.
restoreconresets the image tovar_t, and the next login labels itbin_tagain:Unit test red/green (
published_image_carries_an_executable_selinux_type, run as kayg on the guest). Red is the same test binary withexecutable_labelshort-circuited, which is origin/dev's publication behaviour:J311 finished. Branch
fix-selinux-login-exec, headc73263988acaea7b7dacaa451ee47713c0012def(one commit, rebased onto origin/dev37c93637e). Not merged, not deployed.Root cause: the root infraternal agent publishes its login image under
/var/cache/infraternal-login-bin, where new files inheritvar_t. SELinux refusesinit_texecute onvar_t, so everysystemd-run -p User=… -p PAMName=loginperson login dies with203/EXEC.Fix: the image is still copied (D240/D247 need an account-executable, upgrade-stable image). The private staging inode is labelled
bin_twith fsetxattr before the hard-link publish, keeping user, role and level. Reused images are re-checked, sorestoreconheals on the next login. Hosts without SELinux labels are untouched. No policy change.The guest proof (comment above) ran on
793c1887d. Later amends are fmt, D345 doc comments and clippy doc-markdown backticks, plus the rebase, with no logic change.scripts/verify quickon compute01 (log~claude/.cache/codex-scratch/j311/quick.log):verify fastwas not run: this job's brief asked forverify quick, and the orchestrator runs the full gate at merge. D243 x86 static delta: +4,096 B (27,782,064 → 27,786,160).Cleanup: the guest is stopped and
/root/j311-vmremoved. The scratch target and VM worktrees are deleted. Logs are kept in~claude/.cache/codex-scratch/j311/.Open for the orchestrator: Finding 3. A root infraternal agent installed by
upor by the SSH install lives at/var/lib/infraternal/bin/infraternal(var_lib_t). The guest probe showsinit_tis denied execute on it, so on enforcing hosts that unit likely can't start. I recommend a follow-up job that applies the same staging label inup_install, the SSH installer and the updater.J311 follow-up start: branch
fix-selinux-agent-exec, stacked onfix-selinux-login-execc73263988.Scope from the code audit:
ACTIVATE(crates/plugins/ssh/src/setup_scripts.rs) moves$base/.bootstrap-lock/payloadto$base/bin/live, and the unit runsExecStart="$base/bin/current" agent …(→bin/live).mvkeeps the staging label, so on a root install it isvar_lib_t.up(up_install::binary) publishes/var/lib/infraternal/bin/infraternal, which systemd runs directly.<agent exe> __shim, exec'd by the infraternal agent itself (its own domain), or by the login worker (already fixed). It is not run by systemd from the state directory, so no separate image needs labelling.Plan (about 3 h):
var_lib_t) and rootless (~/.local/share/infraternal/bin, user systemd) exec.root@guest.bin_tlabelling helper (moved out oflogin_binary) used byup_install, a start-time self-heal of the running agent's own install image, and a labelling step inACTIVATEbeforemv payload → bin/live.echo okand a re-add or update. This needs the branch on the test server; I'll ask here when it's ready.J326 security review start. Reviewing
fix-selinux-login-exec@c73263988(base origin/dev37c93637e). Not merging.Plan: code read of
login_binary.rs(write/label/check/publish order, repair path, failure modes), then a fresh AlmaLinux 10.2 GenericCloud20261012.0guest (sha256cbee4726…8eca, same as J311) on compute01 under/root/j326-vm, enforcing. Base and fix static x86 binaries are queued on compute01 build-slot (~claude/.cache/codex-scratch/j326/build.log). On the guest: J311's proof (red/green, revoke, restorecon heal), plus account-side tamper attempts, permissive mode and a read-only cache.J311 follow-up, progress. WIP pushed:
1140ea342onfix-selinux-agent-exec.Guest: fresh AlmaLinux 10.2 (
20261012.0, sha256cbee4726…8eca),Enforcing,/root/j311b-vmon compute01.Rootless check (task item 3), stock binary. Systemd runs the SSH-install layout (
.bootstrap-lock/payload→mv→bin/live, withbin/current→live):The user manager runs as
unconfined_u:unconfined_r:unconfined_t, and it may execdata_home_t. The rootless path is not affected. The root path fails withavc: denied { execute } … scontext=system_u:system_r:init_t:s0 tcontext=…:var_lib_t:s0. So the labelling is root-only, which also avoids refusing an install for a confined user.ACTIVATE snippet (from the branch, run verbatim on the guest against a staged payload, then
systemd-run bin/current):Real Add host environment. test.ssh.kayg.org can't reach compute01: its host's TCP to 10.69.69.218:22 and 10.70.3.22:22 times out, so it can't SSH to the guest. Instead I run a throwaway local server per binary on compute01 (the J207 pattern: own origin, own software passkey under
~claude/.cache/codex-scratch/j311b/sw-admin, never the shared admin file) and run the real Add host toroot@10.69.69.218:2242(guest). Red usesfix-selinux-login-execand green uses this branch. Both builds are queued (compute01 queue 24).J311 follow-up: compute01 maintenance pause (orchestrator notice, 02:15–02:45 IST). My queued build (
~claude/.cache/codex-scratch/j311b/build.sh, queue position 17) and the enforcing guest's qemu process will be cut by the restart. That is not a test failure. After "compute01 back" on #402 I will resubmit the build once in the foreground and reboot the guest from its kept disk (/root/j311b-vm/disk.qcow2).Done meanwhile:
52cca5c6erefactor (moveexecutable_labelintoexec_label, no behaviour change) ande43b83042fix (upstaging label, SSHACTIVATEchcon -t bin_tand check before the service stop with exit 78 and its own message, start-time self-heal of the root install image).root@guest, then "Open a terminal" andecho ok | tee /tmp/j311b-echo, checked on the guest over SSH).J311 follow-up: the earlier queued build (
build-cut-by-restart.log) was cut by the compute01 restart (02:43 IST) and never started. It was resubmitted once at 02:46 IST on heade43b83042and is building in slot 4. The guest was rebooted from its kept disk.J326 security review of
fix-selinux-login-exec@c73263988: REQUEST CHANGES (one small item)The mechanism is sound and I'd approve it as designed. One item fails the brief's "fail closed with a clear message" bar, and the fix is a few lines. Everything else below passed.
1. Does
bin_twiden anything? No./var/cache/infraternal-login-binis checked root-owned with no group/other write (private_directory), and so is its parent. The image is root:root 0555.create_newstaging (root, 0555) → copy → source identity check →fsetxattron the write fd →sync_all→ close →hard_link(never replaces) →validate_image. The label lands on the private inode before any name exists, and no path is involved, so there is no TOCTOU window.unconfined_u:unconfined_r:unconfined_t).xattr1.6.1 is MIT/Apache-2.0, already in the tree, in the Linux-only section, and#![forbid(unsafe_code)]is kept.kayg, enforcing,fs.protected_hardlinks=1): write,setfattr security.selinux,chcon, rename, unlink, planting a file in the directory, and hard-linking it into/tmpwere all refused. The image's sha256 was unchanged afterwards (ad03b43c…f078). Seeevidence/tamper.log.2. Repair path: verified by fd.
validate_imageopens withO_NOFOLLOW, checks regular file, uid, 0555 and length on the fd, then reads and writes the label on that same fd, under the exclusive publisher flock. systemd later execs by path. That is safe for the same reason the existing code is: the root-only directory fixes the name→inode binding.accessible()runs on every terminal open (logins.rs:255), so arestoreconheals on the next terminal. A relabel that lands between the check and the exec costs one 203/EXEC, and the next attempt heals it. Acceptable.3. Failure modes
ENODATA/EOPNOTSUPP→ untouched (code). J311'squickon compute01 (no SELinux) passed with the test's skip branchvar_timage relabelledbin_t, login opens, typing reaches the shell, revoke ends it (evidence/modes.logB)evidence/ro-base.logC3), becausefs::set_permissions(root, 0755)hits EROFS before the label step. Not a regressionbin_thard_link(Temporaryremoves the staging file), and on a reused image before the launch (code)Finding (P2, the requested change): a labelling failure leaves no trace anywhere.
spawn_open(daemon.rs:371-373) maps everyopen_requestederror throughopen_refusal(&error), which keeps only theErrorKind, and nothing on that path logs.logins.rshas notracing::calls. So "cannot label login image bin_t for SELinux: …" and "SELinux kept a non-executable label …" never reach the journal or the person:resource_unavailable. The agent's stderr was empty and the journal had 0 lines matching label/selinux/login image (evidence/ro-stderr.log).InvalidDataandPermissionDenied, whichopen_refusalturns intoInvalid("malformed request").203/EXEC" doesn't hold. Before this change the operator at least had 203/EXEC plus an AVC in the journal. If labelling fails after it (a confined agent domain, or a policy withoutbin_t), they get nothing.This is pre-existing for every publication error (noexec, untrusted image, EROFS), but this commit is the one that claims the clear message.
Ask:
warnwherelogin_binary::accessiblefails inlogins.rs. That is one line, it's not on the hot path, and it adds no latency to a successful open.Invalidkind (for exampleUnsupported/other), or reword the doc comment so it doesn't promise a clear error.No new test infrastructure is needed. A unit assertion on the kind is enough.
4. Reproduced on a fresh enforcing guest
AlmaLinux 10.2 GenericCloud
20261012.0(sha256cbee4726…8eca, same as J311), KVM on compute01,getenforce= Enforcing, targeted policy. I built both binaries myself (static x86_64 musl, build-slot,~claude/.cache/codex-scratch/j326/build.log):37c93637e: 27,782,800 B,fc9eba65…2701c73263988: 27,786,832 B,ad03b43c…f078Red (base),
evidence/red.log:Green (fix), then
restoreconheal,evidence/green.log: the login opens, typing reaches the shell, the revoke ends the login (shell pidfd fired) and a reopen is refused. No new AVCs in either run:5. Size
x86 static delta +4,032 B on my build (J311: +4,096). Acceptable for the fix.
Not this branch
J311's Finding 3 (
/var/lib/infraternal/bin/infraternalisvar_lib_t, so a root infraternal agent installed byupor the SSH installer likely can't start on enforcing hosts) still needs its own follow-up. I didn't re-test it.Notes
~claude/.cache/codex-scratch/j326/evidence/(red.log,green.log,tamper.log,modes.log,ro-base.log,ro-stderr.log,proof2.py= J311's proof with permissive mode allowed and stderr capture).verify(reviewer rule)./root/j326-vmremoved. The target dir and VM worktrees are deleted. The binaries and logs are kept (54 MB).Verdict: REQUEST CHANGES. Only the logging and refusal-kind item above. Not merged.
J311: J326 review item done on
fix-selinux-login-exec. One added commit,398b57762(on top ofc73263988):logins::ensurelogs any login-image publication failure with its text:<4>infraternal agent: login image in /var/cache/infraternal-login-bin unavailable (<kind>): <error>. The agent crate has no tracing subscriber on remote hosts, so stderr is what reaches the journal (embedded agent: the server's stderr; remote agent: its unit's).<4>is journald's warning priority. No person, account or other path is logged.label_failurewith kindUnsupported, the kind thenoexeccache refusal next to it already uses for a host condition.open_refusalmaps it toResourceUnavailable, no longerInvalid. NoOpenRefusalvariant was added, because that would change the plugin API. The doc comment and the commit message now say what really happens instead of "clear error".daemon::refusal_tests::selinux_label_failure_is_not_an_invalid_request.verify quickis queued on compute01 (position 20). The verbatim result follows in the next comment.fix-selinux-agent-execrebased onto it:8dfb8d458(refactor: move the helper, now withlabel_failure, intoexec_label) ande154a0b76(fix). The start-time self-heal also warns via<4>stderr instead oftracing::warn!, which a remoteagent runwould drop.Finding 3, status
var_lib_tfails, rootlessdata_home_tworks (the user manager isunconfined_t), and the exactACTIVATEsnippet makesvar_lib_texec-FAIL intobin_texec-ok (comments above).c73263988, own cert and software passkey, Add host →root@guestwith a throwaway key) gets through root consent, passkey, host-key trust and sign-in, then the bootstrap fails before any binary lands. First it failed asstep="connect" failure_kind="setup", then (with a temporary sshd command logger on the guest) asstep="upload" failure_kind="setup" exit_status=unavailable. Rootlesskayg@fails identically, so this is the rig, not the root path. Every remote command exited 0 on the guest side, and the server gives up in the chunkedddtransfer. The browser stream hides the captured setup stderr (it is exposed only to API-key streams), so I can't see the cause without more rig work: an API key via the device sign-in, or strace on the server.J311:
fix-selinux-login-exechead398b57762217f080205c4831aabd38ed843d71eb(J326's item: one commit onc73263988, description in the comment above).scripts/verify quickon compute01, run in the foreground (log~claude/.cache/codex-scratch/j311/quick-r2.log):The one red row is an unrelated flake.
infraternal-plugin-proxysupervisor_tests::the_public_gate_is_closed_before_a_child_starts_and_cleared_after_it_stopsfailed once with "proxy child descriptor mapping is outside policy", a check on the descriptors the test process holds while sibling tests open their own. The proxy crate is byte-identical to origin/dev on this branch (git diff origin/dev --stat -- crates/plugins/proxyis empty), and the previous quick onc73263988passed the same row. The same test binary from this run (infraternal_plugin_proxy-95927755d08b1130), with no rebuild:--test-threads=3: 6/6 pass, 123/123 (/tmp/j311-proxy-flake.txton code, below):All 290
infraternal-agenttests pass, including the newselinux_label_failure_is_not_an_invalid_request. I did not queue a second quick only to re-roll a flake: compute01 had 20+ queued. The orchestrator's merge gate reruns it.J311:
fix-selinux-agent-exechead9c37ce626f6cd30ddd6cd7769fc893f5b9c024bb, on398b57762. Two commits:e14e87a0frefactor (move the helper, now withlabel_failure, intoexec_label) and9c37ce626fix. Both are fmt-clean per commit. The fix commit now backticksSELinuxin one test doc (clippydoc_markdown).scripts/verify quick, launched per the BRIEF (build-slot nice -n10 nix develop ~/worktrees/j311b -c scripts/verify quick, launcher checked first:cargo 1.99.0). Log~claude/.cache/codex-scratch/j311b/quick-3.log:quick-1.logfailed clippy on that doc comment.quick-2.logfailed 34 agent tests withNotFound: my VM worktree'stargetsymlink had been lost during the VM-side rebase (the BRIEF's J155 gotcha), so restoring it fixed that. Neither was a code problem.the_parent_death_signal_survives_the_credential_dropwithspawn probe: NotADirectory(the child's working-directory descriptor). That fits the same descriptor-collision family as #414, so I added it there. The same proxy binary afterwards passed 4/4 whole-binary runs (123/123).infraternal-plugin-ssh138/138 (includingselinux_label_refusal_names_selinux). Agent 291/291 andinfraternal216/216 are in the quick log above. Output:/tmp/j311b-extra-tests.txton code:The guest (
/root/j311b-vm, stopped, disk kept) and the binaries (~claude/.cache/codex-scratch/j311b/out) are kept for the real Add host once 10.69.69.89 → 10.69.69.218:2242 is open and this branch is on the test server.J381 start: end-to-end enforcing proof for the agent-binary half (successor to J311b).
fix-selinux-agent-exec, previous head9c37ce626. Merged origin/dev098f2c445cleanly: new head03997394e(pushed)./root/j311b-vm, kept disk) runs on compute01 itself, with ssh forwarded to compute01127.0.0.1:2242.Plan (estimate ~3 h, mostly build-slot queue):
098f2c445.verify quick, thenverify faston compute01 (verbatim).getenforce.root@127.0.0.1:2242, then open a terminal and run a command.getenforce,ls -Zof the installed binary,systemctl is-activeof the agent unit, journal; red must show 203/EXEC.kayg@): the branch touches the SSHACTIVATEpath, which runs for both, so run Add host rootless on green too (the snippet is root-only, rootless must be untouched).step="upload"for both root and rootless. If that recurs I debug the rig first (captured setup stderr via the server log).J381 finding 1: GREEN, root Add host works under Enforcing (branch
03997394e, real Add host flow, throwaway local server on compute01).Rig: the kept AlmaLinux 10.2 guest (
/root/j311b-vm/disk.qcow2, never written; each run boots a fresh qcow2 overlay so red and green start from the same clean guest), ssh127.0.0.1:2242. Server binary built from the branch on127.0.0.1:47381,https://j381.infraternal.testwith a throwaway name-constrained CA trusted only on the guest (guest maps the name to10.0.2.2, qemu user net). Own software passkey dir~claude/.cache/codex-scratch/j381/sw-green-root; the shared admin file was not touched. Server sha25680431985171a86af….Why J311b's rig stalled at
step="upload": not fully pinned, but this run, which differs by listening on loopback instead of wt0, no sshd command wrapper on the guest, andINFRATERNAL_BOOTSTRAP_X86_64_BINARYset to a host payload, went through first time. The rig is fine now.Flow (
ev/green-root/flow-add.log): root consent → passkey → host key trusted → signed in with throwaway key →detect/connect/install/enrol/session: done→ Ready, then Open a terminal, typedecho <nonce> $(id -un) | tee /tmp/j381-echo.Guest (
ev/green-root/guest.txt,run.txt):Screenshot of the terminal attached.
J381 finding 2 (P2, needs a design call): a relabel still breaks the root agent, and the start-time self-heal cannot repair it under Enforcing. Same guest, right after finding 1 (
ev/green-root/relabel.txt):heal_running_imageruns inside the agent, so it only fires once systemd has already exec'd the binary. Afterrestorecon(orrestorecon -R /var/lib, or an autorelabel) under Enforcing, systemd refuses that exec, so the heal never runs and the unit loops on 203/EXEC until someone runschconby hand.ACTIVATEandup) is correct.(a)
ExecStartPre=+/usr/bin/chcon -t bin_t <live>in the root unit. Root runs it before every start, so a relabel heals itself on the next start. That touches the unit text, which install/uninstall validation compare exactly.(b) A persistent file-context rule (
semanage fcontext -a -t bin_t '/var/lib/infraternal/bin(/.*)?') at install.restoreconthen keepsbin_t. But that's a policy change, which J311 avoided.(c) Accept it: document that a relabel needs
chcon -t bin_t /var/lib/infraternal/bin/live, and keep the self-heal for the permissive case.Recommendation: (a). It's the smallest change that covers every relabel path and needs no policy tooling. I can do it as a follow-up if you approve it.
J381 finding 3: RED reproduced. Unmodified origin/dev
098f2c445fails the root Add host on the same guest with 203/EXEC.Same rig, same fresh overlay baseline, server built from origin/dev (sha256
0a1919b2668cbcd9…), port 47383. Evidence~claude/.cache/codex-scratch/j381/ev/red-root/(run.txt, guest.txt, label-watch.txt, flow-add.log, screenshots):A first red run (18:38 UTC) showed the same six 203/EXEC restarts and then the rollback. I reran it only to capture the
var_lib_tlabel before the rollback.So, same guest and same flow: origin/dev →
var_lib_t→ 203/EXEC; branch →bin_t→ agent active, terminal works (finding 1).J381 finding 4: rootless. The branch changes no rootless behaviour: the
ACTIVATEblock andheal_running_imageare both gated on uid 0. Rootless installs staydata_home_t, which the account's user manager may exec. Real Add hostkayg@127.0.0.1:2242, each run on a fresh overlay:echoas kaygj381-green-kayg-4e9f2ffe kayg, …)j381-red-kayg-38abfd2a kayg, …)Green try3 guest:
-rwx------. kayg kayg unconfined_u:object_r:data_home_t:s0 … /home/kayg/.local/share/infraternal/bin/live.The single exit 73 (
ev/green-kayg-try1/, server logstep="install" failure_kind="setup-running" exit_status=73 mode="rootless") comes from the setup lock/lease scripts (setup_lock.sh/setup_guard.sh), which this branch doesn't touch. In that run the user agent had started and its link had connected twice before the refusal, and the account was left with a.bootstrap-rollback-*record. With 1 in 4 green and 0 in 2 red, I can't call it branch-caused or prove it isn't from counts alone. The code path rules the branch out, so I think it's a pre-existing intermittent rootless lock race. Recommendation: its own issue (evidence kept inev/green-kayg-try1/). I'm not chasing it here (scope leash).Correction to finding 1: before I switched to per-run overlays, I wrote the rig's setup into the kept
disk.qcow2. That's the throwaway CA in the trust anchors, an/etc/hostsline forj381.infraternal.test, the throwaway key in root's and kayg'sauthorized_keys, andloginctl enable-linger kayg(it may already have been on). Every run booted an overlay on top of that baseline.J381 progress: gates on
03997394e, compute01 (build-slot nice -n10 nix develop ~/worktrees/j311b -c scripts/verify …,CARGO_TARGET_DIR=~/.cache/codex-scratch/j381/target, worktreetargetsymlinked per J155).scripts/verify quick(pre-check only, log~claude/.cache/codex-scratch/j381/logs/quick.log):scripts/verify fast(log~claude/.cache/codex-scratch/j381/logs/fast.log):test: twoinfraternal-plugin-proxysupervisor tests timed out on the proxy private health ping. Every other one of the 140 test binaries passed. The proxy crate is byte-identical to origin/dev on this branch (git diff origin/dev -- crates/plugins/proxyis empty), and this is the same proxy-supervisor family as #414. A same-slot comparison is queued (compute01 queue position 10): proxy lib tests ×3 on unmodified098f2c445, and ×3 with this gate's own branch test binary (logs/proxy-check.log). The result follows in the finish comment.verifystops after a failed Rust table. The branch changes no TypeScript (quick:tracer=0)./root/j311b-vm/disk.qcow2and J311b's files are kept.J381 finish:
fix-selinux-agent-exechead03997394eaefc57470873f1e390c67bcf4db2157(=9c37ce626+ a clean merge of origin/dev098f2c445; no code change in this job).Enforcing proof, real Add host, AlmaLinux 10.2 guest on compute01 (findings 1–4 above; evidence in
~claude/.cache/codex-scratch/j381/ev/):getenforce= Enforcing;/var/lib/infraternal/bin/live=unconfined_u:object_r:bin_t:s0;infraternal-agent.serviceactive (ExecMainStatus=0, Result=success); terminal ranecho <nonce> $(id -un)→j381-green-root-992169a2 root; 0 AVCs.live=unconfined_u:object_r:var_lib_t:s0→status=203/EXEC×6 ("Failed at step EXEC spawning /var/lib/infraternal/bin/current: Permission denied"), the add fails at the terminal step, then rolls back.data_home_t). Green 3/4 Ready with a working terminal; red 2/2 Ready, plus 1 inconclusive driver stall. One green rootless run hit exit 73 in the setup lock scripts, which this branch doesn't touch; I recommend its own issue.restoreconor relabel resetslivetovar_lib_t, and the next start is 203/EXEC again. The start-time self-heal can't run then, because systemd refuses the exec first. Recommendation:ExecStartPre=+chcon -t bin_tin the root unit (option a), as a follow-up.Gates (verbatim in the progress comment above):
verify quickPASS (pre-check).verify fast: every row PASS excepttest, where 2infraternal-plugin-proxysupervisor tests timed out on the private health ping under a full compute01. The proxy crate is identical to dev. Same-slot recheck, 3× unmodified dev098f2c445and 3× the gate's own branch test binary (logs/proxy-check.log):So it didn't reproduce on either side: a load flake (#414 family), not this branch. I didn't rerun the whole
fastonly to re-roll it (compute01 queue 8+). The merge gate reruns it. Static sizes (report-only): x86_64 28,176,912 B, aarch64 25,306,336 B.Cleanup: throwaway servers stopped by PID; 2 orphaned chromium processes killed by PID; guest stopped, per-run overlays deleted; 54G target deleted. Kept:
/root/j311b-vm/disk.qcow2(now with the rig baseline: throwaway CA, hosts line, throwaway key, kayg linger),/root/j311b-vm/j381-{start,stop}.sh, and the binaries, logs and evidence (98M) in~claude/.cache/codex-scratch/j381. Production and the test server were not touched. Nothing merged or deployed.J381: relabel step (follow-up to finding 2), decided by the orchestrator. Starting on
fix-selinux-agent-exec, base03997394e.Change, as one commit:
ExecStartPre=-+/bin/sh -c "[ ! -e /sys/fs/selinux/enforce ] || chcon -t bin_t /var/lib/infraternal/bin/current", placed beforeExecStart. Root is chosen by the prelude's$manager, which comes from the install mode. Rootless units are unchanged.up's unit: the same line for/var/lib/infraternal/bin/infraternal.+runs the step as root, and-means it can never fail the unit. The selinuxfs check makes it a no-op where SELinux is absent or disabled. It has no$(systemd expands it) and no;. No file-context policy is added.service_unit!()macro spliced intoACTIVATE, so the tests run exactly the lines that ship.Plan:
setup_scripts::tests::*,up::tests::the_unit_relabels_its_binary_before_every_start). Queued on compute01, log~claude/.cache/codex-scratch/j381/logs/redgreen.log.restorecon -v→var_lib_t→ restart → active,bin_t, 0 AVCs. Then the same restart without theExecStartPre→ 203/EXEC.verify quick.J381 finding 5: relabel step proven. Commit
f69c81325onfix-selinux-agent-exec(pushed). It is one commit on03997394e. I amended it once, force-with-lease on my own branch: the first push4a5160043failed clippydoc_markdown(unquoted "SELinux" in 4 doc lines), so the amend touches those 4 lines only.Red → green (compute01 build-slot; logs
~claude/.cache/codex-scratch/j381/logs/redgreen.log,green-quick-4a5160043.log). Red = the branch plus only the new tests; green = the pushed commit:(The first GREEN pass in
redgreen.logreused red's test binaries: both worktrees shared one target dir, and the green sources had older mtimes. The rerun touched the sources first.)Enforcing guest, AlmaLinux 10.2, fresh per-run overlay, build of
4a5160043(server sha256e2f338422a61e2ca…; it differs fromf69c81325only in doc comments):ev/green2-root/): Ready, then the terminal ranecho→j381-green2-root-729d8b8e root.liveisbin_t, the agent and login units are active (ExecMainStatus=0),avc_count=0. On this build the wizard ends by opening the terminal by itself; the driver now treats that as done.ev/green2-root-relabel/relabel-proof.txt; that run's install was the same real Add host, only its browser view stalled):The last block shows something extra: the shipped unit also recovers a binary already stuck at
var_lib_ton the next restart.Non-SELinux host: compute01, Debian 13, systemd 257, no selinuxfs. Throwaway units using the exact line, removed afterwards (
/tmp/j381/nonselinux.txton code):The exact line is skipped (status 0). Even a
chconthat runs and fails (status 1), or is missing (status 203), leaves the unit active, because of the-prefix.Cleanup: guest stopped, relabel overlays deleted, red worktree removed, servers and an orphaned driver/browser stopped by PID.
verify quickonf69c81325is queued on compute01 (position 9); the result follows in the finish comment.J381 finish (relabel step):
fix-selinux-agent-execheadf69c81325f9fe23a477e541cc2b6f3d654055564(one commit on03997394e, pushed).verify fastwas not rerun, per the orchestrator: the reviewer runs it.scripts/verify quickon compute01 (log~claude/.cache/codex-scratch/j381/logs/green-quick.log):Two earlier quick attempts on this change were not about the code. On
4a5160043: clippydoc_markdown, fixed by the amend. Then a link error: the shared target still pointed theinfraternalbuild script at the deleted red worktree'shandoff-note.ld. Logs:green-quick-4a5160043.log,green-quick-f69c81325-linkfail.log.f69c81325)echo→j381-green2-root-729d8b8e root;bin_t; agent active; 0 AVCsrestorecon -vbin_t→var_lib_tExecStartPrebin_t, 0 new AVCsExecStartPrestatus=203/EXEC, stillvar_lib_t, 2 new AVCs-)Cleanup: guest stopped (overlays deleted,
disk.qcow2kept), probe units removed, red worktree removed, target deleted. Nothing merged or deployed. Production and the test server were not touched.J397 review start (correctness + security) of
fix-selinux-agent-exec@f69c81325f9fe23a477e541cc2b6f3d654055564, read against origin/dev0a5432d2c. Scratch worktree has origin/dev merged in. I'm not merging or deploying anything.Plan: read the
exec_label, ACTIVATE,up/up_installand unit text code. Check the rootExecStartPre=-+line for path quoting and whether the account can reach the path,bin_tscope, the-prefix, permissive/disabled/container cases, upgrades and rootless. Judge whether the tests are meaningful. Runverify quickandverify faston compute01 through build-slot. If slots allow, rerun J381's guest steps 1–4 on a fresh overlay.J397 evidence: J381's guest proof steps 1–4 rerun on a fresh per-run overlay, plus extra probes. AlmaLinux 10.2,
/root/j311b-vm/disk.qcow2as backing (not written), overlayj381-j397a.qcow2(now deleted). I used J381's runner, copied so that its output, data and passkey dirs go to~claude/.cache/codex-scratch/j397/(J381's evidence was not touched). The binary is J381'sout/green2(4a5160043).git diff 4a5160043 f69c81325touches 4 lines, all in//comments, so it is the reviewed logic. Evidence:~claude/.cache/codex-scratch/j397/ev/{green2-root,relabel}/.Step 1, root Add host (
ev/green2-root/run.txt,guest.txt): Enforcing →HOST_BOOTSTRAP_RESULT done,ECHO_TYPED;/tmp/j381-echo=j397-green2-root-01d234d3 root;live=unconfined_u:object_r:bin_t:s0; agent and login units active, ExecMainStatus=0;avc_count=0.Steps 2–4 (
ev/relabel/relabel-proof.txt, J381'srelabel-proof.shunmodified):Extra probes (
ev/relabel/probes.txt,selinux0.txt,config-disabled.txt,autorelabel-domain.txt):/var,/var/libroot 755;/var/lib/infraternalandbin/root 700;current→/var/lib/infraternal/bin/liveroot;liveroot 700; unit file root 600. (up's layout: base 0711,bin/0755, both root,up_install.rs:225-227.) No account can write or swap anything on the pathargv[]=/bin/sh -c [ ! -e /sys/fs/selinux/enforce ] || chcon -t bin_t /var/lib/infraternal/bin/current: quoting is intactrestorecon, restartbin_tchattr +i live)sh[3007]: chcon: failed to change context of '/var/lib/infraternal/bin/current' … Operation not permitted, then 203/EXEC from ExecStart. Afterchattr -i, the next restart is active withbin_t.-hides nothing from the journalselinux=0kernel arg/sys/fs/selinuxabsent, ExecStartPre status=0 (skipped), agent activeSELINUX=disabledin config, no kernel arg (EL10)enforceabsent, so both the unit step and ACTIVATE skip; agent active/.autorelabelboot back to Enforcingbin_t, active, 0 × 203/EXEC, 0 AVCssystem_u:system_r:unconfined_service_t:s0, the same as prod's/opt(usr_t) server.bin_tgrants nothing a narrower stock type would not, and the file is root 0700 anywayGuest stopped, overlay removed;
disk.qcow2and J381's files are untouched.J397 findings on
f69c81325. No P1 and no P2. Four P3s, none blocking:P3-1 (tests): the labelling tests pass vacuously on every gate host.
exec_label::tests::healing_relabels_only_our_own_install_image(crates/infraternal-agent/src/exec_label.rs) andlogin_binary::tests::published_image_carries_an_executable_selinux_type(login_binary.rs:556)eprintln!andreturnwhen the filesystem has no SELinux labels. That is true on compute01 and dev-02 (Debian/Ubuntu, no selinuxfs), so the gate reports themokwithout checking anything. Only the manual guest runs (J311, J326, J381, and the one above) ever exercise them. The red/green that J311 showed ran on the guest by hand. Suggestion: make the skip visible (#[ignore = "needs an SELinux-labelled filesystem"]plus an opt-in env var for the guest), so a green gate doesn't read as coverage. The two new unit-text tests are fine.setup_scripts::tests::*runs the real shippedservice_unit!()shell lines, so it catches printf/quoting and the$managergate. It went red before the fix (J381'sredgreen.log). Theuptest is close to a string echo, but it pins the exact line and the order beforeExecStart.P3-2 (diagnostics): the two install-time label failures drop their reason. ACTIVATE does
chcon -t bin_t "$stage/payload" 2>/dev/null || exit 78, andup_install::binarydoesexecutable_label(&output).map_err(|_| refused(...)). Both turn the actual cause (EACCES from policy, EOPNOTSUPP, EPERM on an immutable file) into one generic sentence. That runs against J326's ask on the login half ("log the publication error with its text"). Suggestion: keep chcon's stderr in the setup output, and put{error}inup's message.P3-3 (portability, plausible, not reproduced): fail-closed where selinuxfs is visible but the installer's domain may not relabel. ACTIVATE exits 78, and
uprefuses, whenever/sys/fs/selinux/enforceexists (inup's case, whenever the file has asecurity.selinuxxattr) and the relabel is denied. Docker and podman don't mount selinuxfs in containers, so I expect them to skip. A system container that does see selinuxfs read-only and runs in a confined domain (nspawn or LXC with a container SELinux context) would now refuse an install that used to work, sincecontainer_tmay exec its owncontainer_file_t. The guest has no container runtime, so this is reasoning only. The fix isn't needed for this branch: one line in the install docs or the exit-78 message ("inside a confined container, …") would do.P3-4 (nit):
chconis looked up on systemd's default PATH in a+(root) step. That PATH has/usr/local/sbin:/usr/local/binbefore/usr/bin. Only root can write those, so this is not an escalation, but/usr/bin/chconwould be deterministic. The-prefix already covers distros without it.Answers to the brief's questions (no finding):
$basefor root is the constant/var/lib/infraternal(install.rs:1004). The prelude refuses a base that is a link, not owned by root, or not mode 700/711. Every component down toliveis root-owned and not writable by group or other (table in the previous comment). chcon dereferencingcurrent → livestays inside root-only directories. The account has no input to the unit text.$or;in the path. systemd's own argv parse is shown above.bin_t: the agent runs asunconfined_service_t, as prod's/optserver does withusr_t. A narrower type needs a policy module, which J311 ruled out and which is a policy change for the owner. No widening.-prefix: correct. A chcon failure lands in the unit's journal with its text, and if the label really matters, ExecStart then fails visibly with 203/EXEC. Without-, a host whose filesystem can't take the label would lose a working agent.daemon-reload(setup_scripts.rs:140), so existing SSH root installs pick up the line on their next update.infraternal-statusmatches onlyExecStart=(crates/infraternal-status/src/lib.rs:473), so the extraExecStartPre=doesn't change classification.uphas no updater, so an oldupunit keeps its text until the host rejoins. Under Enforcing such a unit could never have started, so nothing working regresses.$manager = systemctl, ACTIVATE on uid 0, andheal_running_imageon euid 0; covered byrootless_unit_has_no_relabel_stepand J381's rootless runs.exec_labelis code-identical. The only change is the error prefix,login image:→infraternal executable:, and nothing matches on that string.heal_running_imageis now mostly redundant with ExecStartPre (it only helps when the agent was started some other way). It's harmless; keep it or drop it, your call.J397 finish: APPROVE
fix-selinux-agent-exec@f69c81325f9fe23a477e541cc2b6f3d654055564No P1, no P2. Four P3s (comment above): P3-1, the SELinux labelling unit tests skip silently on every gate host; P3-2, ACTIVATE and
updrop the label failure's reason; P3-3, plausible fail-closed in a confined system container that can see selinuxfs; P3-4,chconis looked up on PATH in the root step. None blocks the merge. They can go in a follow-up or be accepted.Real-environment proof (comment above): J381's steps 1–4 reproduced on a fresh overlay. Also checked: permissive,
selinux=0,SELINUX=disabled, full autorelabel, a failing chcon (logged), and ownership along the path.Gates on the scratch merge of origin/dev
0a5432d2cintof69c81325(merge commitd65e2324b, local only, not pushed). compute01,build-slot nice -n10 nix develop ~/worktrees/j397 -c scripts/verify …,CARGO_TARGET_DIR=~/.cache/codex-scratch/j397/target.scripts/verify quick(pre-check; log~claude/.cache/codex-scratch/j397/logs/quick.log):scripts/verify fast(log~claude/.cache/codex-scratch/j397/logs/fast.log):The TypeScript stages ran and passed too (the TS protocol tests are the last block). The proxy-supervisor flake that hit J381's
fast(#414 family) did not recur.Note: the brief's reviewer rule says a review job doesn't run
verify. This job's task asked for quick and fast explicitly, so I ran them, one slot run each.Cleanup: the guest is stopped and my overlay is deleted. My compute01 worktree and its target dir are deleted. Logs and evidence are kept in
~claude/.cache/codex-scratch/j397/{logs,ev}. Nothing merged, pushed or deployed. Production and the test server were not touched.