a checker for the deprovision spec, and the trap that makes it pass for free
scripts/assert-uid-free.sh is the verification half of docs/deprovision-os-account.md, written outside the implementation on purpose: a checker the function calls is a restatement of its own beliefs rather than an audit. Nine checks — passwd entry, uid reuse, both subid files, linger, runtime dir, live processes, files owned by the uid, and files owned anywhere in the freed subuid range. Two modes, because the range has to be captured BEFORE deletion. userdel removes the /etc/subuid entry along with the account, and after that there is no way to ask what range it held — so a checker that only runs afterwards silently drops the half most likely to be wrong. Exercised against green while fully provisioned: eight of nine checks fail, exit 1. A checker that has never been seen to fail is not evidence. And the trap worth knowing before anyone trusts a green result: the subuid check passes vacuously on most accounts. Files get a mapped owner only when a process inside a container runs as a NON-root user; an image whose files are root-owned maps to the member's own uid and leaves the range empty. Measured on green after a night of real use — claude installed, an image pulled, transcripts written — the range check found zero files and passed without testing anything. The spec now says how to build a specimen that actually exercises it, and to watch the check fail on that tree before trusting it to pass on a cleaned one. Docs and a script only; no behaviour change. On a branch, for whoever merges it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -146,8 +146,28 @@ All of these must hold for the freed uid *and* its freed subuid range:
|
||||
- `/run/user/<uid>` absent
|
||||
- no processes owned by the uid
|
||||
|
||||
Worth extracting as `assertUidFree(uid, subuidRange)` and reusing it as the post-condition of the function and
|
||||
as a test.
|
||||
`scripts/assert-uid-free.sh` implements exactly this, deliberately **outside** the function: a checker the
|
||||
implementation calls is a restatement of its own beliefs, not an audit. Two modes, and the split matters —
|
||||
|
||||
```
|
||||
./scripts/assert-uid-free.sh --capture green # BEFORE: prints "green 1001 165536 65536"
|
||||
sudo ./scripts/assert-uid-free.sh --check green 1001 165536 65536 # AFTER: exit 1 unless clean
|
||||
```
|
||||
|
||||
The range has to be captured **before** deletion, because `userdel` removes the `/etc/subuid` entry with the
|
||||
account. After that there is no way to ask what range it held — and a check that silently skips that half is
|
||||
the exact failure this section exists to prevent.
|
||||
|
||||
**The subuid check passes vacuously on most accounts, and that is a trap.** Container files are owned by a
|
||||
mapped id only when a process inside the container runs as a NON-root user; an image whose files are root-owned
|
||||
maps to the member's own uid and leaves nothing in the range. Measured on green after a night of real use —
|
||||
`claude` installed, images pulled, transcripts written — the range check found **zero** files and passed
|
||||
without testing anything.
|
||||
|
||||
To build a specimen that actually exercises it, run a container whose process writes as a non-root user. The
|
||||
`postgres:18-alpine` case from the same night is the natural one: its entrypoint drops to uid 70, and the data
|
||||
directory came out owned by `subuid_start + 70` on the host. Verify the range check *fails* on that tree before
|
||||
trusting it to pass on a cleaned one.
|
||||
|
||||
**Trap for the verifier:** do not use `sudo -u <user> …` to check anything after step 2. Creating a session
|
||||
starts a user manager and recreates `/run/user/<uid>`, so the check would undo the step it is verifying.
|
||||
|
||||
Reference in New Issue
Block a user