carlos / platform Public

spec: scanner host draft 6, closes Astra review 5 (socket unit boot-ordering cycle)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 29934da1311bff1290fdc356da2970986aca20e5 parent edee53d
2 files changed, +57 −4
  • docs/superpowers/specs/2026-10-04-scanner-host-rollout-design.md +22 −4
  • docs/superpowers/specs/CODEX-FINDINGS-SCANNER-HOST-R5.md +35 −0
diff --git a/docs/superpowers/specs/2026-10-04-scanner-host-rollout-design.md b/docs/superpowers/specs/2026-10-04-scanner-host-rollout-design.md
index 86dad3ec9..5a2df4cf9 100644
--- a/docs/superpowers/specs/2026-10-04-scanner-host-rollout-design.md
+++ b/docs/superpowers/specs/2026-10-04-scanner-host-rollout-design.md
@@ -1,12 +1,15 @@
# Scanner host: getting object-storage scanning running on Carloku and Tito
-**Status:** draft 5. Closes Astra review 1 (`CODEX-FINDINGS-SCANNER-HOST.md`,
+**Status:** draft 6. Closes Astra review 1 (`CODEX-FINDINGS-SCANNER-HOST.md`,
findings 1–5, all accepted), review 2 (`CODEX-FINDINGS-SCANNER-HOST-R2.md`,
findings 6–8: 7 and 8 accepted, 6 declined with the reason in §4.3) and
review 3 (`CODEX-FINDINGS-SCANNER-HOST-R3.md`, finding 9, accepted and tested
against a real engine, §3.4) and review 4 (`CODEX-FINDINGS-SCANNER-HOST-R4.md`,
-findings 10 and 11, both accepted; 10 reshaped how the box listens, §3.2).
-Accepted findings are marked **(codex N)**.
+findings 10 and 11, both accepted; 10 reshaped how the box listens, §3.2) and
+review 5 (`CODEX-FINDINGS-SCANNER-HOST-R5.md`, finding 12, accepted). Review 5
+found the gating design sound at spec level once 12 is fixed, and left restart
+timing, connection cut-off and EICAR transport to the real-box acceptance
+checks in §3.3. Accepted findings are marked **(codex N)**.
**Builds on:** `2026-09-01-object-storage-virus-scanning-design.md` (the sweep,
verdict tags, enforcement) and `2026-09-02-scan-on-demand-design.md` §9 (clamd
off the console box, reachable over TCP).
@@ -110,6 +113,16 @@ So clamd itself never listens on TCP. Three units:
the port cannot open before the test passes, and it closes, cutting
connections in flight, when the test's unit goes inactive.
+ The socket unit sets **`DefaultDependencies=no`**. A socket unit is ordered
+ `Before=sockets.target` by default, `sockets.target` comes before
+ `basic.target`, and every service (the self-test included) comes after
+ `basic.target`; a socket that must also come *after* the self-test closes
+ that loop, and systemd breaks an ordering cycle by dropping a job, which
+ could leave the port shut after a reboot. So the unit opts out, restores what
+ it still needs by hand (`After=sysinit.target`, `Conflicts=shutdown.target`,
+ `Before=shutdown.target`), and is started by `WantedBy=multi-user.target` and
+ clamd's `Wants=`, never by `sockets.target`. **(codex 12)**
+
`clamd.conf` keys, each rewritten idempotently (clamd has no include
directory):
@@ -182,7 +195,12 @@ console is wired (§6.2), and recorded in the README:
for this check): clamd comes up, 3310 **stays closed**, the unit shows
failed; remove the override and restart: it opens;
- an INSTREAM scan of EICAR through `<private ip>:3310` from the console box
- answers `Eicar-Test-Signature FOUND`, so the proxy carries the protocol.
+ answers `Eicar-Test-Signature FOUND`, so the proxy carries the protocol;
+- **reboot the box**: it comes back with the self-test passed and 3310 open,
+ `systemd-analyze verify` on the three units is clean, and `journalctl -b`
+ has no `ordering cycle` line. First boot cannot show this, because
+ `basic.target` and `sockets.target` are already reached by the time
+ `user_data` runs. **(codex 12)**
### 3.4 What the limits do, measured
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-SCANNER-HOST-R5.md b/docs/superpowers/specs/CODEX-FINDINGS-SCANNER-HOST-R5.md
new file mode 100644
index 000000000..6417cdc74
--- /dev/null
+++ b/docs/superpowers/specs/CODEX-FINDINGS-SCANNER-HOST-R5.md
@@ -0,0 +1,35 @@
+| Finding | Disposition | Reason |
+|---|---|---|
+| 10 | **Closed for the original bypass** | The listener now requires a successful self-test, and both listener and proxy are bound to that test’s lifetime. §6.2 also requires scanner-side readiness before wiring. A separate boot-ordering defect remains below. |
+| 11 | **Closed** | Both scan limits are fixed at 100 MiB; §3.5 bounds the configurable stream limit so the specified fixtures fit. |
+
+Reviewed draft 5 at `edee53d4`. No files changed or live-box tests performed.
+
+12. **P2 — §§3.2–3.3: The gated socket creates a boot-ordering cycle with systemd’s default dependencies.**
+
+ The [specified socket dependency](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/scanner-host/docs/superpowers/specs/2026-10-04-scanner-host-rollout-design.md:109) orders `scanner-tcp.socket` after `scanner-selftest.service`, but neither unit disables default dependencies. On reboot, this produces the following cycle, where each arrow means “must start before”:
+
+ ```text
+ scanner-tcp.socket
+ → sockets.target
+ → basic.target
+ → scanner-selftest.service
+ → scanner-tcp.socket
+ ```
+
+ **Code evidence:** systemd adds `Before=sockets.target` to sockets in [`src/core/socket.c:262`](https://github.com/systemd/systemd/blob/v257/src/core/socket.c#L262), adds `After=basic.target` to services in [`src/core/service.c:712`](https://github.com/systemd/systemd/blob/v257/src/core/service.c#L712), and orders `basic.target` after `sockets.target` in [`units/basic.target:13`](https://github.com/systemd/systemd/blob/v257/units/basic.target#L13).
+
+ Systemd resolves an ordering cycle by deleting a dispensable job or rejecting the transaction; it cannot execute the specified startup sequence intact. See [`src/core/transaction.c:351–395`](https://github.com/systemd/systemd/blob/v257/src/core/transaction.c#L351). Consequently, the scanner can remain unavailable after reboot. Initial cloud-init startup and §3.3’s kill/restart checks can miss this because `basic.target` and `sockets.target` have already completed.
+
+ **Fix:** Specify `DefaultDependencies=no` for `scanner-tcp.socket`, restore its initialization and shutdown dependencies explicitly, and arrange activation through clamd or `multi-user.target` without ordering it before `sockets.target`. Retain the self-test dependencies. Add a reboot acceptance check and verify the complete boot transaction for ordering cycles.
+
+The other requested mechanisms do not produce additional findings:
+
+- **Self-test state:** `Type=oneshot` delays ordered dependents until successful completion. `RemainAfterExit=yes` retains that success as `active (exited)`; it neither overrides `BindsTo=` nor preserves success across reboot. A failed test cannot authorize the listener. [Service semantics](https://manpages.debian.org/trixie/systemd/systemd.service.5.en.html).
+- **Clamd restart and package restart:** `BindsTo=` plus `After=` stops dependents when clamd becomes inactive or fails; explicit restarts also propagate. Applying this to **both** TCP units removes the listener and terminates the proxy holding established connections. `Requires=` alone does not cover unexpected deactivation. [Dependency semantics](https://manpages.debian.org/trixie/systemd/systemd.unit.5.en.html).
+- **Automatic recovery:** With the default `RestartMode=normal`, automatic restart creates a new start transaction with dependency restart propagation. That transaction processes `Wants=scanner-tcp.socket`, which pulls in the self-test again. This is supported by [`service.c:2405–2418`](https://github.com/systemd/systemd/blob/v257/src/core/service.c#L2405) and [`transaction.c:937–947`](https://github.com/systemd/systemd/blob/v257/src/core/transaction.c#L937).
+- **INSTREAM:** `systemd-socket-proxyd` forwards bidirectional stream bytes between TCP and Unix sockets. The client’s [INSTREAM implementation](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/scanner-host/internal/clamd/clamd.go:195) uses byte framing, without descriptor passing, so the proxy supports its protocol. [Proxy semantics](https://manpages.debian.org/trixie/systemd/systemd-socket-proxyd.8.en.html).
+
+After fixing the boot cycle, the gating design is sound at spec level. Actual restart timing, connection termination and EICAR transport belong to the real-box acceptance checks.
+
+**Verdict: accept with fixes.**
\ No newline at end of file