Skip to content

feat(cap,svc): IPC peer re-wiring on watchdog rebirth + dead-target unlink (ADR-0023) - #223

Merged
NickFlach merged 2 commits into
mainfrom
feat/adr-0023-ipc-peer-rewire
Jul 15, 2026
Merged

feat(cap,svc): IPC peer re-wiring on watchdog rebirth + dead-target unlink (ADR-0023)#223
NickFlach merged 2 commits into
mainfrom
feat/adr-0023-ipc-peer-rewire

Conversation

@NickFlach

Copy link
Copy Markdown
Collaborator

Implements ADR-0023 (merged in #222), flipping it to Accepted. Parts 1+2 ship together, per the ADR.

Part 1 — IPC caps die with their target

  • cap_revoke_spawn_channelscap_revoke_ipc_targets: on process death, revoke every CAP_RESOURCE_IPC cap naming the dead pid — the epic feat(kernel): spawn-time parent↔child IPC channels — the society self-assembles #175 origin-tag filter is dropped; the type + dead-target filters stay. No capability ever outlives the process it names (closes the recycled-pid authority leak for SYS_SEND_TO and the SYS_CAP_DERIVE targeted-peer check).
  • Self-test: the "untagged hand-minted pair survives" assert is inverted (it is the Part-1 gate); the live-target and colliding-resource-id survivors still defeat an over-broad revoker; the dead cap_revoke(hand) cleanup is dropped.

Part 2 — declarative ipc_peers[], re-minted on every start

  • service_definition_t.ipc_peers[]: peer service name + independent to_perms/from_perms (the ADR-0019 key cap is from-only — a symmetric field could not express it and a stray reverse cap would break fieldsyncd's first-match routing).
  • start_slot mints declared pairs inside the same cli window as the grant_* caps: Pass 1 (my running peers — first boot + my rebirth), then Pass 2 (running services declaring me — their-side declarations + boot-order gaps). Pass 1 strictly before Pass 2 (first-fit slot order is load-bearing for swarm-svc's untargeted discovery).
  • Every mint is guarded by process_is_valid + generation (state==RUNNING alone is stale up to ~2s vs the reaper — an unguarded mint would reintroduce the exact leak Part 1 closes).
  • Pair mints are transactional: a half-minted pair is rolled back, never left one-way (a one-way qsh pair would flake as a reply timeout instead of hard-failing).
  • Converted: paradoxd/fieldsyncd/swarm-svc/qsh ↔ ghostd + the swarm-svc→fieldsyncd key cap; the citizens.c hand mints are deleted. One-shot test citizens and the agentdemo pair stay hand-minted (the latter recorded honestly as an accepted regression).

Key re-forward (fieldsyncd rebirth)

The re-mint restores only the delivery path — a reborn fieldsyncd would be keyless-but-wired, emitting seq=0 zero-tag frames keyed peers silently reject. swarm_svc now watches (~2s pacing) for a fieldsyncd pid change and re-forwards its cached session key.

CI gate (anchored + delivery-correct)

  • ci-smoke delivers a delayed ghost (separate write, 15s) to the REBORN shell — a same-burst ghost dies in the old shell's 32-byte cons_read chunk with exit.
  • The gate greps the post-QSH: reborn slice (awk '/QSH: reborn/{f=1} f') — the session already prints ghost R= twice pre-rebirth, so a whole-log grep would pass with the feature entirely broken.
  • CAPUNLINK gate string updated for the generalized policy.

Verification (local WSL, full runs)

  • Forward: make ci-smoke PASSES end to end — including SUCCESS: reborn shell re-acquired its ghostd IPC pair (declarative re-mint, ADR-0023); timed boot shows rebirth at t≈6s and the reborn shell answering ghost at t≈13s.
  • Revert-confirm: dropping qsh's ipc_peers declaration → boot healthy, rebirth happens, 0 ghost R= answers, all three ghosts EPERM-denied, and the anchored gate fails — non-vacuous in both directions.

Design was adversarially attacked pre-implementation (4 lenses; 3 blockers + 5 majors folded into the ADR — see #222).

🤖 Generated with Claude Code

flaukowski and others added 2 commits July 14, 2026 20:26
…nlink; ADR-0023 Accepted

Part 1 — generalize the epic #175 unlink (cap_revoke_spawn_channels ->
cap_revoke_ipc_targets): on process death revoke EVERY CAP_RESOURCE_IPC
cap naming the dead pid, tagged or not. No capability outlives the
process it names — closes the recycled-pid authority leak for all IPC
caps. Self-test survivor assert inverted (the untagged hand-minted cap
must now die); the live-target and colliding-resource-id survivors keep
guarding against an over-broad revoker.

Part 2 — declarative ipc_peers[] on service_definition_t: start_slot
re-mints declared pairs on every start inside the grant cli window.
Two passes (my running peers; running services declaring me), each mint
guarded by process_is_valid + generation (state==RUNNING alone is stale
vs the reaper for ~2s — an unguarded mint would reintroduce the exact
leak Part 1 closes). Per-direction to/from perms (the ADR-0019 key cap
is from-only); pair mints are transactional (a half-minted pair is
revoked, never left one-way); Pass 1 strictly before Pass 2 (first-fit
slot order is load-bearing for swarm-svc's untargeted discovery).
Converted: paradoxd/fieldsyncd/swarm-svc/qsh <-> ghostd + the
swarm-svc->fieldsyncd key cap; citizens.c hand mints deleted.

swarm_svc now re-forwards its cached session key when it observes a
fieldsyncd pid change: the re-mint alone restores only the delivery
path — a reborn fieldsyncd would be keyless-but-wired, emitting
zero-tag frames keyed peers silently reject.

ci-smoke: the scripted session delivers a delayed 'ghost' to the REBORN
shell (separate write — cons_read batches a same-burst ghost into the
dying shell's chunk) and gates the answer on the post-'QSH: reborn' log
slice (the session prints 'ghost R=' twice before rebirth; a whole-log
grep would pass vacuously). CAPUNLINK gate string updated with the
inverted policy.

Verified locally (WSL): full make ci-smoke PASSES end to end, reborn
shell answers ghost at t+13s via the declarative re-mint.

ADR-0023 flipped to Accepted; ADR index updated (0020-0022 were already
Accepted in their files, index rows stale).

Co-Authored-By: Claude Fable 5 <[email protected]>
@NickFlach
NickFlach merged commit 6b90288 into main Jul 15, 2026
25 checks passed
@NickFlach
NickFlach deleted the feat/adr-0023-ipc-peer-rewire branch July 15, 2026 01:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants