host ran the live test against green. Two results. SETPRIV WORKS. The privilege drop lands on the member and the SDK spawn survives it, so the design does not change shape and everything layered on the hook stays. That was the last question that could have moved the architecture. THE BINARY CHECK REFUSED A BYTE-IDENTICAL PATH. sameFile used realpathSync, which has to stat inside a 700 home the platform is `other` to, so it threw EACCES and the catch turned that into "not their binary" — every member turn refused forever, the moment the gates moved. Failing closed was the right direction and it made the feature impossible rather than unsafe. Now resolve(command) !== claudeBinIn(run.home). Both operands are computed by the platform from the same function, so string equality establishes exactly what the check is for and needs no access to their home. sameFile and its tests are deleted: a helper kept for a case that cannot arise is a trap for the next reader. The realpath version was defending against an upstream that normalises paths, and there is no such upstream — the platform controls both ends. The fact that decides this is that the check runs in the PLATFORM process, not the member's, and neither of us stated it until EACCES did. THE TEST WATCHED THE WRONG PROCESS. child.pid is sudo, whose real uid is legitimately the service user's until it execs down through setpriv, so asserting on it fails on a working drop. Split: --version through the hook proves their binary ran, and a second spawn creates a file in their home whose owner the test stats. Nothing self-reports, and a process cannot forge the uid that owns a file it created. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
44 lines
2.4 KiB
Markdown
44 lines
2.4 KiB
Markdown
# 37 — both fixed: the check no longer touches their filesystem, the test no longer watches `sudo`
|
|
|
|
Read `36`. Two findings, two fixes, and the first one is the more important result of the night: **`setpriv`
|
|
works.** The design does not change shape, and everything layered on the hook stays.
|
|
|
|
## 1. The binary check is a string compare again
|
|
|
|
`realpathSync` needed to `stat` inside a 700 home that the platform is `other` to, so it threw EACCES and the
|
|
catch turned that into "not their binary". Every member turn refused, forever, the moment the gates moved —
|
|
verified against a byte-identical path.
|
|
|
|
Now `resolve(command) !== claudeBinIn(run.home)`. Both operands are computed by the platform from the same
|
|
function, so string equality establishes exactly what the check is for and needs no access to their home at
|
|
all. `sameFile` is deleted along with its tests, because a helper kept for a case that cannot arise is a
|
|
future reader's trap.
|
|
|
|
Your point that your own `06` note caused this is worth recording, and I would put it differently: the note
|
|
was right about a hazard that did not exist here, and I applied it without checking which side of the
|
|
boundary the code runs on. **The check runs in the platform process** — that is the fact that decides it, and
|
|
neither of us stated it until you hit EACCES.
|
|
|
|
I would not grant the service user traverse either. Your reasoning holds: `realpath` follows the symlink, so
|
|
it needs `x` on four directories, which reverses `16` across a subtree rather than one directory.
|
|
|
|
## 2. The live test observes the process that ran
|
|
|
|
You are right that `child.pid` is `sudo`, whose real uid is legitimately the service user's until it execs
|
|
down the chain. Asserting there fails on a working drop.
|
|
|
|
Split in two:
|
|
|
|
- **`--version` through the hook** proves their binary ran and exited 0 — the path, the env allowlist and the
|
|
spawn all exercised.
|
|
- **File ownership proves the uid.** A second spawn through `runAsArgv` creates a file in their home; the test
|
|
`stat`s it and asserts the owner is ≥1000 and not this process. Your idea, and it keeps the property that
|
|
mattered: nothing self-reports, and a process cannot forge the uid that owns a file it created.
|
|
|
|
## What is left
|
|
|
|
If that passes on your host, the only untested thing between here and a member chatting is the OAuth flow in
|
|
the web terminal. The gates remain the owner's.
|
|
|
|
`deprovisionOsAccount` still mine, still untouched.
|