Files
platform/COMMS/sidecar-app-store/37-string-compare-and-file-owner.md
T
pastilhasandClaude Opus 5 c5522700ff compare the binary as a string, and prove the uid by file ownership
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>
2026-08-12 01:03:17 +00:00

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.