carlos / platform Public

Isolated exec spec: revision 10 after codex round 9

Round 9 findings committed unedited. Both accepted: ProtectControlGroups=yes
in the templates and the probe; /var/cache and /var/lib hidden from
instances; a same-uid audit rule and test over the unit set.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZ1ZvWj7naf73GjTX8MBv5
Paul Campbell pushed by paul@keymail.dev 251e4d3c9495f68eb4722f03d964238bbf1846a0 parent d76f536
2 files changed, +82 −3
  • docs/superpowers/specs/2026-09-11-isolated-exec-design.md +37 −3
  • docs/superpowers/specs/CODEX-FINDINGS-ISOLATED-EXEC-R9.md +45 −0

CI passed — run details

diff --git a/docs/superpowers/specs/2026-09-11-isolated-exec-design.md b/docs/superpowers/specs/2026-09-11-isolated-exec-design.md
index def5d1cc0..4c411ca73 100644
--- a/docs/superpowers/specs/2026-09-11-isolated-exec-design.md
+++ b/docs/superpowers/specs/2026-09-11-isolated-exec-design.md
@@ -1,7 +1,7 @@
# Isolated exec — every exec-backed instance in its own namespace
-**Date:** 2026-09-11 · **Status:** revision 9, after codex rounds 1 to 8
-(19 + 14 + 7 + 4 + 3 + 1 + 1 + 1 findings, all accepted; §13) · **Requested by:** discussion
+**Date:** 2026-09-11 · **Status:** revision 10, after codex rounds 1 to 9
+(19 + 14 + 7 + 4 + 3 + 1 + 1 + 1 + 2 findings, all accepted; §13) · **Requested by:** discussion
#126 (Tito, with box evidence), approved in direction by Paul on
2026-09-11
@@ -609,11 +609,20 @@ StandardError=inherit
ProtectSystem=strict
TemporaryFileSystem=/data:ro
TemporaryFileSystem=/run/carlos:ro
+# Every other tree a carlos-uid unit writes on the box is hidden the same
+# way (codex R9-2): the restore drills' cache under /var/cache, and
+# /var/lib as a class. An instance needs nothing from either.
+TemporaryFileSystem=/var/cache:ro
+TemporaryFileSystem=/var/lib:ro
BindPaths=/data/instances/%I /run/carlos/instances/%I
ReadWritePaths=/data/instances/%I /run/carlos/instances/%I
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
+# /sys/fs/cgroup read-only: the edge's delegated subtree is carlos-owned,
+# and its cgroup.kill / cgroup.freeze would otherwise be one write away
+# for a same-uid tenant (codex R9-1).
+ProtectControlGroups=yes
ProtectProc=invisible
ProcSubset=pid
PrivatePIDs=yes
@@ -687,6 +696,24 @@ Lines that are load-bearing and why:
the path is in `/run/carlos/logs`, which no tenant mount contains, and
that directory is on a `nosymfollow` mount, so a same-uid process
outside any namespace cannot turn it into a symlink either.
+- **`ProtectControlGroups=yes`** (codex R9-1). `ProtectSystem=strict`
+ leaves kernel API filesystems alone and `ProtectKernelTunables=` leaves
+ `/sys/fs/cgroup` writable; the edge unit's `Delegate=` subtree,
+ including the `supervise/` cgroup the agent moves itself into
+ (`internal/cgroup/cgroup.go:172`), is created by `carlos` and its
+ `cgroup.kill` and `cgroup.freeze` are owner-writable. A same-uid
+ tenant with a writable cgroupfs could kill or freeze the agent with
+ no pid and no socket. Read-only cgroupfs closes that; an instance has
+ no business in cgroupfs at all.
+- **`TemporaryFileSystem=/var/cache:ro` and `/var/lib:ro`** (codex
+ R9-2). The isolation property is "no same-uid path on the box but its
+ own", and two trees outside `/data` and `/run/carlos` are written by
+ `carlos`-uid units: `carlos-hibernate-verify.service` and
+ `carlos-restore-verify.service` set `CacheDirectory=carlos-verify` and
+ restore every host's database into `/var/cache/carlos-verify` during a
+ drill (`internal/ops/ops.go:274`, `internal/lease/check.go:79`), and
+ `/var/lib` holds unit tenants' state. Both are hidden. §9 carries the
+ audit rule that found them, so the next such unit is caught at review.
- **`/opt/carlos`** stays readable through `ProtectSystem=strict`; the
trampoline and the live symlink are all the instance needs from it.
@@ -1614,7 +1641,9 @@ instance-probe`. From inside it asserts, and reports each as a separate
verdict line:
- `/data` and `/run/carlos` list nothing but the probe's own bound
- directories;
+ directories, and `/var/cache` and `/var/lib` list nothing at all;
+- a write to `cgroup.kill` under any cgroup directory it can name
+ (its own included) fails with `EROFS`;
- `/proc` lists only itself, and its own pid is 1;
- a symlink it creates in its directory is refused with `ELOOP`, and
`/proc/self/mountinfo` shows `nosymfollow` on both bound directories;
@@ -1746,6 +1775,7 @@ instance as an enrolled non-`carlos` uid
- `internal/host/systemunits.go:34-83` (active check on the socket)
- `update-platform-binary.sh:52-70` (pinfra): enables the new units by name before restarting the edge
- `harden-host.sh` (pinfra) writes hardening drop-ins for `$TENANT_UNITS`; the instance templates carry their own and must not be listed there
+- **The same-uid audit** (codex R9-1, R9-2): every unit in `internal/host/units/` that runs as `User=carlos` or delegates a `carlos`-owned tree, and every path it writes, is a path an isolated instance must not see. Today: `carlos-edge.service` (`/data`, `/run/carlos`, its delegated cgroup subtree), `carlos-litestream.service` (`/data`), `carlos-hibernate-verify.service` and `carlos-restore-verify.service` (`/var/cache/carlos-verify`). A test over `unitsFS` lists every `User=carlos` unit's `ReadWritePaths=`, `CacheDirectory=`, `StateDirectory=`, `RuntimeDirectory=` and `Delegate=` and asserts each resulting tree is covered by a `TemporaryFileSystem=`, `InaccessiblePaths=` or `ProtectControlGroups=` line in the templates, so a future unit that writes somewhere new fails the build until the templates hide it
**Tests that pin the flat layout** (§10): `internal/activator/activator_test.go` (20 fixture sites), `backend_exec_test.go` (4), `admintoken_test.go` (8, path-agnostic), `internal/appconfig/syncexec_test.go` (50), `internal/ops/ops_test.go` (52), `internal/provision/reconcile_test.go` (20), `internal/host/movespass_test.go` (1), `schedule_wiring_test.go` (1), `schedule_token_fence_test.go` (2), root `e2e_test.go` (24), `instances_e2e_test.go`, `config_e2e_test.go`, `deploy_e2e_test.go`. For this release they keep passing on the flat path; each gains a nested twin.
@@ -2033,3 +2063,7 @@ rollback runs the old binary).
- **Round 8** (codex, `CODEX-FINDINGS-ISOLATED-EXEC-R8.md`, 1 new
finding, every prior item resolved, verdict request changes). Accepted:
R8-1 the wake re-reads the row before `Acquire` and runs on it; the layout pass reloads routing before releasing the fence (§6.8, §8.2).
+- **Round 9** (codex, `CODEX-FINDINGS-ISOLATED-EXEC-R9.md`, 2 new
+ findings, every prior item resolved, verdict request changes). Both accepted:
+ R9-1 `ProtectControlGroups=yes` in the templates and the probe; the probe writes `cgroup.kill` and expects `EROFS` (§4.1, §8.5);
+ R9-2 `/var/cache` and `/var/lib` hidden; the same-uid audit rule and its test added to §9 (§4.1, §8.5, §9).
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-ISOLATED-EXEC-R9.md b/docs/superpowers/specs/CODEX-FINDINGS-ISOLATED-EXEC-R9.md
new file mode 100644
index 000000000..f52a7e670
--- /dev/null
+++ b/docs/superpowers/specs/CODEX-FINDINGS-ISOLATED-EXEC-R9.md
@@ -0,0 +1,45 @@
+| Prior finding | Disposition at `d76f5364` |
+|---|---|
+| R8-1 | **Resolved at spec level.** Wake refreshes the committed row before `Acquire`; migration reloads routing before releasing admission (§§6.8, 8.2). |
+| R5-2 | **Resolved.** Downgrade checks every row’s paths regardless of flags; unflagging nested rows is refused (§8.2). |
+| R5-3 | **Resolved.** Unit and socket names use the bounded `InstanceID` (§§3.1, 5.3). |
+| R2-4 | **Resolved.** Blocking seed execution and `RemainAfterExit=yes` retain identity and results through capture (§§4.2, 5.3). |
+| R2-5 | **Resolved.** Completion requires terminal state and cgroup emptiness; uncertain stops retain replication and the lease (§§6.3, 6.5). |
+| R2-7 | **Resolved.** Recovery derives both endpoints from journal direction, completes that transaction, then converges to current intent (§8.2). |
+| R2-9 | **Resolved.** Flat minting, explicit reversal and downgrade guards cover the reported failures (§§8.1–8.2). |
+| R2-14 | **Resolved at spec level.** The IPv6 exception is explicit, bounded, rechecked and reported (§8.5). |
+| R1-4 | **Resolved.** Completion is independent of log EOF; blocking observations remain outside the activator mutex (§§6.4–6.7). |
+| R1-5 | **Resolved.** Preparation, execution budget, cancellation, result retention and termination-before-cleanup are specified (§§6.1–6.5). |
+| R1-8 | **Resolved under the recorded decision.** Shared tier limits remain; per-service OOM families are an explicit difference (§4.3). |
+| R1-12 | **Resolved.** Consumer fences and bidirectional recovery cover interrupted migration (§8.2). |
+| R1-13 | **Resolved.** Reverse recovery and completed-reversal downgrade guards address the reported defects (§8.2). |
+| R1-15 | **Resolved for the reported PID/IMDS defects.** No unsafe fallback remains; the behavioural gate is specified (§8.5). |
+| R1-18 | **Resolved under the recorded ruling.** Remote operations and reporting are specified; the `foreign` repair residual remains explicitly accepted (§§8.3–8.4). |
+
+**R9-1 — P1 — §§5.4, 4.1, 6.6: A tenant can stop the agent without using the authenticated channel.**
+
+Retain §5.4’s process-authenticated control service. The namespace leaves a separate process-control path available.
+
+The [template’s hardening block](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/isolated-exec-spec/docs/superpowers/specs/2026-09-11-isolated-exec-design.md:608) omits `ProtectControlGroups=`. `ProtectSystem=strict` excludes kernel API filesystems, and systemd explicitly leaves `/sys/fs/cgroup` writable under `ProtectKernelTunables=yes` unless control-group protection is enabled. [systemd namespace implementation](https://raw.githubusercontent.com/systemd/systemd/v257/src/core/namespace.c).
+
+Section 6.6 retains the delegation machinery for this release. That machinery [creates `supervise/` as `carlos` and moves the agent into it](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/isolated-exec-spec/internal/cgroup/cgroup.go:172). Linux creates its control files with the creator’s ownership and owner-write permission. Consequently, the specified tenant can write `1` to:
+
+```text
+/sys/fs/cgroup/carlos.slice/carlos-edge.service/supervise/cgroup.kill
+```
+
+That kills the agent; writing to the adjacent `cgroup.freeze` freezes it. These operations require no visible target PID and no connection to the control service. PID isolation therefore does not prevent them. [Linux ownership and kill implementation](https://raw.githubusercontent.com/torvalds/linux/v6.12/kernel/cgroup/cgroup.c), [cgroup control semantics](https://docs.kernel.org/6.12/admin-guide/cgroup-v2.html).
+
+**Correction:** add `ProtectControlGroups=yes` to all three tenant templates and the probe, preserving the agent’s delegation separately. Require a behavioural check that a tenant cannot write the agent’s delegated cgroup controls.
+
+**R9-2 — P1 — §§4.1, 8.5, 9: Restore drills expose sibling databases outside the hidden data tree.**
+
+Both [hibernate verification](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/isolated-exec-spec/internal/host/units/carlos-hibernate-verify.service:38) and [ordinary restore verification](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/isolated-exec-spec/internal/host/units/carlos-restore-verify.service:35) set `CacheDirectory=carlos-verify`, `TMPDIR=%C/carlos-verify` and `User=carlos`. This creates a host directory at `/var/cache/carlos-verify`, owned by the same uid as every isolated tenant. `PrivateTmp=yes` isolates `/tmp` and `/var/tmp`; it does not hide that cache directory. [systemd execution semantics](https://raw.githubusercontent.com/systemd/systemd/v257/man/systemd.exec.xml).
+
+The [hibernate drill creates its temporary directory there](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/isolated-exec-spec/internal/ops/ops.go:274), then [restores each sibling’s database into `verify-<host>.db`](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/isolated-exec-spec/internal/lease/check.go:79). A tenant can enumerate and read those files while verification runs. Mode 0700 does not separate identical uids, and the tenant’s read-only view still permits reading. The ordinary drill exposes its restored database set through the same directory.
+
+The §8.5 probe can pass throughout: it checks `/data` and `/run/carlos`, while these copies live under `/var/cache`.
+
+**Correction:** hide the verifier workspace from every tenant role, retaining disk-backed restores. Add both verification units to §9’s isolation audit and test sibling-database unreadability while a restore drill holds a restored copy.
+
+**Verdict: request changes.**
\ No newline at end of file