Repository navigation
fix: close the gaps the post-acceptance review found - #19
Conversation
Retirement trusted pgrep, so a missing or failing pgrep read as "no process", and it removed the unit, then the root, then the account, so a userdel failure left a profile no run could finish. - read every uid in /proc/<pid>/status; an unreadable table refuses - remove the account first, then the root with its marker last, then the unit - read userdel's outcome back; remove only the account's own unused group - report a leftover home or group by name, and document each recovery
Preflight accepts systemd 249 and later, but only 255 has run the acceptance. Accepting a version read as supporting it. - preflight warns on any accepted systemd other than 255 - the documents separate supported (255) from accepted but unverified
O_NOFOLLOW covers only the last path component, so a symlinked bin/ redirected root's read of the dependency manifest. Parser errors such as deep nesting escaped as exit 1 instead of "cannot tell". - walk down from the runner directory with O_NOFOLLOW on every component - any exception, and a manifest of the wrong shape, exits 2
The printed commands exported the token into the operator's shell and relied on a later unset, and a failing gh still ran config.sh. - print registration and deregistration as one ( ... ) block - a gh failure ends the block before config.sh runs
tar gave the directory the archive's own mode for "." (0775) while it unpacked, so the comment that it stayed closed to others was not true. - unpack with --no-overwrite-dir, so the directory stays 0700 until chmod
Both guards existed with no apply-level case, so removing either passed every drill. - a person's account of the runner's name creates and downloads nothing - root's temporary directory ignores an inherited TMPDIR
The retirement and reader changes were run on the acceptance VM with a never-registered test profile, and the record must say what that covered and what stays synthetic. - add the follow-up table to the arm64 acceptance record - state in verification.md which #18 cases reached RUNTIME VERIFIED
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
…much An independent review of the retirement rework found that removing the root's children one by one let rm --one-file-system empty a filesystem mounted at work/ or runner/, and that some failures still read as success. - refuse, removing nothing, while anything is mounted in the runtime root or the mount table cannot be read; rm also runs with --preserve-root=all - write the root's marker back when the final rmdir fails - tell a failed account lookup from an absent account, keep a group when accounts also come from a directory service, report an undeletable unit
The re-review of the retirement fixes found three ways the new proofs could still read a host as safe when it was not. - parse "passwd:files sss" and trailing comments, and treat compat with "+" or "-" lines in /etc/passwd as a directory service - compare mounts against the root's canonical path, so a symlinked /var still finds a mount below it - cover escaped mount paths, prefix siblings and an empty table in drills
|
Independent review, and what changed after it The review of e68c8a1 found one High, which it reproduced. Removing the runtime root's children one at a time let e2e58f8 fixes it, together with the Medium and Low findings:
The re-review of e2e58f8 found no Critical or High. 90f7ec8 closes its three Lows:
Evidence for 90f7ec8 (tree 84a0cec):
|
Why
The independent review after the ARM64 acceptance (#18) found gaps in #15–#17. One is medium: support for systemd below 255 was claimed without verification. The rest are low or informational:
Closes #18.
Stacked on #17 (
fix/vm-findings). Do not merge before #17, #16 and #15; merge order stays top-down.What changed
A. The process check fails closed (f7670f1)
pgrep -uis gone. A missing or failingpgrepprinted nothing, read as "no process", and let retirement proceed.host_account_processesreads/proc/<pid>/statusand matches the account's uid against the real, effective, saved and filesystem uids.Uid:line, or no status file at all is "cannot tell" (2), never "none" (1). Retirement refuses on 0 or 2.B. Partial retirement and recovery (f7670f1)
Removal order: the account first, then the runtime root, then the unit. While the account exists, the unit and the marked root still identify the runner, so a retry continues from where it stopped.
The root marker is removed last. A drill found the old
rm -rfremoved the marker first, so a failing removal left a root that no retry would ever prove again.userdel's outcome is read back, not inferred from its exit code. For example, exit 12 means the account is gone but the home is not.A group is removed only when it is provably the account's own:
userdel;This covers
USERGROUPS_ENAB no. A group found on a later run, without its account, is reported and never removed, because the proof is gone.Recovery: every residual message has a row in the recovery table in
docs/operations.md, and there is a procedure for a runtime root without its marker.C. systemd range (887d873)
RestrictNamespacesprinting thatcheckdepends on was read in the v249, v252 and v255 sources (src/shared/bus-print-properties.c) and is identical. That is a source review, not verification, and the documentation says so. Narrowing to 255 would refuse hosts nothing shows are broken.D. Other items
safe_loadopens the runner directory, which sits in a root-owned runtime root, then walks each component withO_NOFOLLOW, so a symlinkedbin/is refused. Before, only the last component was protected.RecursionError, exits 2.librariesis not an object is refused. The old code returned a version from a list.( ... )block. The token lives only in that subshell, and a failingghends it beforeconfig.shruns.tar --no-overwrite-dirkeeps the runner directory 0700 for the whole unpack. The comment is corrected..runnerand manifest that are symlinks, FIFOs, oversized, deeply nested, the wrong shape, or carry control characters;--replaceassertion, now checked against the whole printed block.How it was verified
shellcheck 0.10.0: 20 scripts clean, on the final tree e68c8a1 (tree 201295e).
Drills, final tree: 514 cases, 0 failed.
Each commit on its own: every intermediate commit passes shellcheck and the whole suite. The release drill skipped in those runs, because they ran outside a git checkout.
Failing before: the final tests against the code of 01ca427 give 41 failures (apply 22, host 19); after the fix, 0.
Mutation check: 30 mutations of the new guards, each run against its drills. 29 were caught. One survived and is equivalent: removing the
st_sizefast path changes nothing, because the read loop enforces the same limit, and removing the limit itself is caught.ARM64 VM (
rs-arm64, Ubuntu 24.04, systemd 255), commit 7f84c01:probewas used and never registered.docs/acceptance/2026-10-arm64.md(follow-up section). Summary:pgrep -ufound the saved-uid one: nothing;userdelfailing under a held/etc/passwdlock removed nothing else;USERGROUPS_ENAB no: the account's own group was removed;bin/was refused;nobody: 504 cases, 0 failed./etc/passwd,/etc/groupand/etc/login.defsmatched their checksums from before, byte for byte.Hosted CI: BLOCKED. The organisation has used 2000/2000 Linux minutes this month, with a $0 budget and
prevent_further_usage. No merge exception is requested.Evidence level
bin/./proc;ghandsudo);Limitations
Risks
userdelsucceeds but whose root cannot be removed now keeps the unit until a retry. That is intended and documented.Compatibility impact
Remaining work
runner-slice statuswith a realgh; a real misregistration; unknown job state on a real host; self-update; x86_64.