Skip to content

refactor(provider-services): retire process-family typed ops behind the broker envelope seam (U10-U14) - #540

Merged
vicondoa merged 90 commits into
v3from
feat/provider-services-broker-seam
Sep 16, 2026
Merged

vicondoa merged 90 commits into
v3from
feat/provider-services-broker-seam

Conversation

@vicondoa

@vicondoa vicondoa commented Sep 15, 2026

Copy link
Copy Markdown
Owner

Summary

Completes the broker-seam refactor (docs/plans/2026-09-14-001-refactor-provider-services-broker-seam-plan.md, U1-U14). The typed process-family wire variants retire behind the generic EnvelopeInvoke carrier; the privileged cores run in-broker as broker-generic kernels; family handlers validate daemon-side and graft the U9 evidence chain (KTD6).

Fixes #523 · Related #516

Units

  • U1-U9 carrier capabilities (fd leg, trusted context, state cells, refusal taxonomy, envelope data plane, actors, evidence chain).
  • U10 process family: 10 variants -> envelope, 11 kernels, family handlers in d2b-provider-process, supervisor envelope migration, production chain-audit sink (write_chain_record), P1 spawn-kernel ports (usbip binds / serving-worker ACLs / stale-socket cleanups / duplicate-spawn guard).
  • U11 guest lifecycle lease -> generic consume-cell/complete-cell kernels (row+arm deleted; AE2 restart-replay; one-winner).
  • U12 network-fds family: 13 variants -> 13 kernels, handlers in d2b-provider-network-local, daemon legs via envelope_invoke_kernel.
  • U13 check-async-gate wired into //bazel/checks:check + make check-ci; 26-site blocking-call backlog cleared.
  • U14 provider_crate_layout policy gate (family knowledge ratchet, generated-only classification); stale check-provider-layout removed.

Gates (local)

make check-clippy green; //bazel/checks:check 462 pass (6 env-only flake-eval sandbox failures); generated-artifact drift 19/19; nix privileges drift PASS; tier0 hygiene PASS; matrix 7/7 (retired frames -> typed stale-wire-version refusal + audit, never silent drops); conformance 13/13; d2bd lib 460/0; broker lib 685/4 (4 proven-pre-existing env failures); cross-binary //tests/unit/gates:broker-seam-pilot gate wired (run requires root for the production bundle posture).

  • Remaining before merge: make test-host-integration (vmChecks, KVM) — pending local run; CI status.

New check-async-gate subcommand rejects blocking calls in async worker
contexts across broker, d2bd, and provider crates: lexical probe mirroring
provider_crate_policy probes, reusing clippy.toml disallowed-methods as the
single blocking-fn source, spawn_blocking bodies exempt per KTD9, named
canonical-JSON violations. Fixture-verified only; wiring into the
enforcement chain lands with U13's sweep on the migrated tree.
EffectServiceSupervisor + EffectServiceActor mirroring ResourceManager
supervision: spawn_linked children under a per-zone supervisor, respawn
from durable rows, ractor requeue timers, generational revision counter
bumped on respawn/republish with StaleRevision refusal (KTD5). Fixture
trait only; rendezvous binding lands in U8 integration.
SCM_RIGHTS carriage on request/response frames: fd-index fields on
ForwardOperationRequest/Outcome validated against declared indexes,
MAX_FRAME_FDS=8 ceiling with pre-dispatch row-facet validation (max_fds +
fd_kind on committed rows, all rows regenerated), FD_LEG closed-set peer
refusal code shared across both legs, envelope + forwarding + rendezvous
plumbing, toolkit invoke_named_with_fds, LoopbackPeer + cross-socket fd
round-trips and mismatch/oversize/kind/mint refusals (AE1).
…14 mechanism)

Four new gates: family/type/role/provider-ref literals incl. broker
dispatch arms and runtime-assembled names, ServerState references in
d2bd, generated-view producer provenance, and d2b-broker provider-free
manifest pin. SHARED_FAMILY_KNOWLEDGE_RATCHET seeded from the current
tree with per-row retires_with documentation (U10-U12 delete rows).
Structural bazel-visibility edge designed but deferred: no bazel binary
on this host; source probes are the working enforcement until U14 final
integration wires the visibility edge.
Generic CellStore keyed (cell, invocation_id, initiating_principal)
replaces lifecycle_leases and runner_pidfd_registry statics: durable
one-time cells commit before the effect with unknown-outcome reconcile
(AE2 restart-replay resistance), ephemeral cells keep reset-on-restart,
retention caps exempt consumed one-time markers. Durability facet on
committed rows (stateCell + cell_durability), generator regenerated 70
rows byte-stably (KTD3).
ForwardContext block (epoch, zone, provider-set revision, controller
and guest generations, initiating identity, deadline budget) minted
only by the broker from a durable monotonic cache; daemon publishes
values over the origination leg; rendezvous enforces field-wise with
the STALE_CONTEXT code, serves the context deadline as the per-call
handler deadline, and rejects every pre-restart context via strictly
increasing broker-epoch nonces (KTD2, KTD4 transport).
Closed peer-code set grown to 13 (handler-refused/errored/timed-out/
crashed, stale-wire-version; FD_LEG, STALE_CONTEXT from U1/U2): dispatch
failures keep their own code, nothing flattens to unregistered-handler
outside genuine missing-handler cases. Local handlers run as abortable
tokio tasks on a dedicated 2-worker set; panic -> HANDLER_CRASHED closing
audit refusal; deadline expiry -> HANDLER_TIMED_OUT abort while innocent
calls answer; blocking pool untouched (KTD7).
Daemon publishes provider-set revision + zone generations over the
origination leg: committed PublishTrustedContext row + wire variant,
runtime dispatch arm with audit, TrustedContextStore publish/ack;
daemon side fires from ForwardRendezvous::publish after the revision
assignment (composition.rs zero edits), publish_generations and
set_broker_epoch now production-called with the ACKED epoch, transport
errors leave the rendezvous fail-closed. Guest generation publishes the
documented fleet base. Bazel d2bd build green; views regenerated
byte-stable.
All 60 d2b-provider-* packages drop //visibility:public for closed
enumerated __pkg__ sets anchored on d2bd (daemon composition root) and
legit provider-to-provider consumers; broker and shared crates are
absent from every set (enforcement proven by a failing visibility
build), exports_files preserved for data-graph consumers. Broker
composition crate (U6) joins the sets when it lands.
ProviderSet::start spawns the per-zone EffectServiceSupervisor after the
plane publishes declared services: one linked actor per declared service,
respawn from durable rows, requeue timers, generational revisions
(republish bumps, stale bindings refuse StaleRevision). Composition seam
with_effect_service_factory / resolve / publish_effect_service; named
refusals for duplicate/factory-missing services. Fixed harness restart
recovery bug (recovered rows were never recorded, killing a recovered
actor never respawned). Rendezvous binding consumption is U8b.
Closed DeadlineTier facet (Standard 25s / Extended 60s) on committed
rows, minted into the context and binding both legs under the ceiling;
forwarded dispatch runs on its own task with panic -> handler-crashed
by name, never a dropped socket; retired-variant gating runs Value-first
before typed decode (stale-wire-version refusal + audit record), with
closed RETIRED_WIRE_VARIANTS table (empty in production; U10 fills it)
and a mixed-version matrix fixture (KTD4/KTD7/KTD10).
…(U5)

Daemon-native host-ownership preflight migrates off its typed placeholder
onto BrokerEnvelope::call with CallerAuthority::Daemon over the
origination-leg carrier, forward-budget timeout mapping to
internal-broker-timeout (never unregistered-handler); prebind gap
documented fail-closed. Trusted-context store init made lazy: opens on
first PublishTrustedContext, open failure refuses that publication while
the broker keeps serving (fixes socket-activation startup regression);
restart-epoch invalidation preserved. OwnershipMatrixCheck row
promoted live; placeholder pins in production_binary_policy superseded.
d2b-broker is lib-only and provider-free; new d2b-broker-composition
links provider handler crates, preserves the d2b-broker binary name
(socket service units + bazel refs repointed, Nix host-tools rewired,
guest workspace + lock regenerated). Routing rule admits zero in-broker
ops this pass (fail-closed seam::verify_startup_routing, empty
production registration); dependency-surface audit scans handler crates
for syscall/asm!/ctor/registration surfaces with fixture crates
(31/31 tests). Broker startup no longer depends on provider code.
Forwarded operations resolve to the declaring effect service via the
committed operation facet (ResolveOperation, deterministic winner) and
dispatch to the hosted actor over the normal carrier; binding revision
captured at call start, mid-call death or republish refuses with the
dedicated stale-revision code, never a hang (KTD5). Declined maps to
handler-refused; OperationUnserved falls through to provider tables.
Blanket dead-code allowance drained; per-seam allowances marked for
U9+.
…(U7)

ServiceDecl.methods becomes per-method contract facets: operation, payload
schema, fd contract, state cells, privileges, deadline tier; zone-plane
facets untouched (R4). Committed rows carry service+method resolution;
generator closed-set validation refuses unresolvable/ambiguous/
malformed links and surfaces Service/Method in the triage view. Both
generators regenerated byte-stable; drift gates green (KD6).
EvidenceChain roots and correlation records keyed (invocation_id, depth)
with MAX_NESTED_DEPTH=8 and the nested-depth-exceeded closed refusal;
nested calls authorize under the chain's initiating principal (graft
rule), never the daemon class; serves_locally gating writes exactly one
root record per invocation per executing leg — forwarded ops audit
daemon-side only, in-broker broker-side only, mixed-leg chains never
double-write (KTD6).
…ditive)

11 resource-agnostic kernel rows (open-pidfd, spawn-process, cgroup and
reap surfaces) served in-broker by the KernelDispatcher at serve time;
FdKind::Any for anon-inode pidfds; EnvelopeInvoke generic daemon->broker
carrier; nix privileges-json drift gate (nix_surface_test pattern)
pins brokerOperations vs the catalog; generator comma regression fixed+
fixtured. Provider-side migration and typed-arm retirement follow in
the U10 cut.
…he envelope seam (U10)

The ten process-family operations (OpenPidfd, OpenPeerPidfdFromAcceptedSocket,
ObserveRunner, PollChildReaped, PrepareRuntimeDir, PrepareStateDir, CgroupKill,
SignalRunner, DeregisterRunnerPidfd, SpawnRunner) retire their typed
BrokerRequest/BrokerResponse wire variants at wire v6 and become the first
forwarded-family pilot on the generic EnvelopeInvoke carrier:

- Wire: the ten variants leave the enum; WIRE_VARIANTS drops them and
  RETIRED_WIRE_VARIANTS names them at v6, so a straggler peer gets the typed
  stale-wire-version refusal plus an audit record, never a silent drop (KTD10).
  PROTOCOL_VERSION stays 6: every surviving op's frame is byte-identical.
- Catalog: the ten rows stay committed (declaringProvider d2b-provider-process,
  wireVariant dropped); eleven broker-generic kernel rows land in-broker and are
  served by kernel_ops.rs through KernelDispatcher (spawn-process, open-pidfd,
  open-peer-pidfd-from-accepted-socket, poll-child-reaped, prepare-directory,
  kill-cgroup, signal-pidfd, deregister-pidfd, delegate-cgroup-v2,
  open-cgroup-dir, observe-process).
- Sandwich: the daemon-side family handlers (d2b-provider-process operations.rs)
  validate the typed payload and invoke the kernel as a NESTED EnvelopeInvoke
  over the origination socket (kernel_client.envelope_invoke_kernel), grafting
  on the U9 evidence chain (root id + ordered identities, KTD6). The daemon's
  legacy legs (open-pidfd, poll-child-reaped per runner, signal-pidfd with the
  tracked fd, deregister-pidfd, kill-cgroup via cgroupPath) use the kernels
  directly; supervisor BrokerProcessBackend migrated to envelope frames with the
  zone derived from the verified bundle.
- Audit: the production envelope chain-audit sink is now wired at serve time
  (AuditLogChainSink -> AuditLog::write_chain_record), so every in-broker leg -
  incl. the nested kernel legs - is durable in the daily audit log keyed on the
  invocation id; inner legs are correlation records under the root id.
- Ports: the retired arm's live behaviors were ported into the spawn-process
  kernel with tests (usbip device-bind extension, serving-worker ACL grant,
  stale cloud-hypervisor/video/otel socket cleanups, duplicate-runner refusal +
  registry wiring). Ratchet seeded for the kernel's family tokens (device-usbip,
  observability-otel, runtime-cloud-hypervisor) with census retirements.
- Gates: matrix 7/7 (retired frames refused with typed code + audit), catalog
  completeness green, telemetry domain shrunk byte-stable, nix privileges rows
  kept (ops remain committed; drift gate green), ratchet green bidirectionally,
  generated-views drift 18/18, conformance 13/13 (cargo + bazel), and the new
  broker-seam-pilot cross-binary gate (tests/unit/gates/broker-seam-pilot.sh,
  bazel //tests/unit/gates:broker-seam-pilot) drives OpenPidfd end-to-end with
  fd liveness + audit continuity; it records a skip without root (production
  bundle posture), same lane as the host-integration gates.
- Generator: operation rows now reject case-insensitive collisions (the toolkit
  envelope matches operation names case-insensitively).
- Bazel: dep-surface synced for d2b-resource-types and d2b-provider-process.

Deliberate gaps (documented in code): PollChildReaped returns empty
notifications (the daemon's per-entry pidfd probe is the live drain);
deregister-pidfd root calls report removed=false for legacy/adopted runners
(daemon has no spawn invocation id); a future daemon launch path that passes
escrow fds needs migration (spawn refuses inherited_fd_count>0); daemon-side
durable chain mirror for forwarded legs is a tracked follow-up (DaemonAudit has
no chain-record surface yet).
…l kernels (U11)

The one-time guest lifecycle lease retires its last typed surface: the
ConsumeLifecycleLease variant, arm, request/response structs,
LifecycleLeaseOperation enum, audit shape, and the committed row all leave
the tree (the row is DELETED, not variant-dropped - the op is gone from the
catalog). The lease semantics now ride the generic cell mechanism as two
broker-generic kernel rows served in-broker by the envelope dispatcher:

- consume-cell / complete-cell: committed rows (owner broker-generic,
  host+guest profiles, authz d2bd/d2b-admin/d2b-launcher, destructive) that
  declare the lifecycle-leases cell with one-time durability - the row is
  the declaration surface (U3/KTD3), the kernel performs the compare-and-
  consume / completion against broker_store() with the principal-bound
  replay gate. The broker's lease-specific code (consume_lifecycle_lease,
  complete_lifecycle_lease, lifecycle_lease_identity, lifecycle_lease_cell,
  LIFECYCLE_LEASES_CELL, caller_principal) is deleted: zero lease code
  remains in the broker.
- The daemon-side caller (composition.rs) now validates the lease fences
  itself (generation/policy-revision/operation-id shape, stop-only
  semantics) and invokes the two kernels over the origination socket via
  kernel_client.envelope_invoke_kernel, keying the one-time cell on the
  full lease identity (zone/guest/generations/policy/operation/stop-only).
- RETIRED_WIRE_VARIANTS grows by ConsumeLifecycleLease @ wire v6 with the
  matrix extension (old-frame typed stale-wire-version refusal + audit).
  PROTOCOL_VERSION stays 6.
- Telemetry op domain shrinks; nixos-modules/privileges-json.nix row
  deleted (drift gate green); all generated views regenerated (drift 19/19).
- Tests: concurrent consume - exactly one winner; replay refused
  (one-time cell); restart-replay resistance proven through the durable
  store; envelope dispatch tests for both kernels; profile tests updated
  (retired op leaves host/guest catalogs).

Gates: contracts 49/0, broker lib 684/4 (known env), matrix 7/7, profiles
4+2+5, d2bd 460/0, xtask 339+1 (known env), layout rc0, generated drift
19/19, nix drift green.
…backlog (U13)

The check-async-gate probe (blocking calls in async contexts, KTD9) is now
part of the enforcement chain: //bazel/checks/policy:check-async-gate
(sh_test mirroring provider_crate_layout) is wired into the //bazel/checks
:check suite, `make check-async-gate` aliases it, and `make check-ci`
enforces it. The gate's 26-site pre-existing backlog is cleared:

- Production sites (d2bd): interaction_composition, plane_port,
  process_provider_runtime (nix send), zone_enrollment, and the two
  composition.rs sites (storage-lifecycle report removal, guest state root
  mkdir) now run their blocking fs/socket calls through
  tokio::task::spawn_blocking with the same error mapping and cleanup
  ordering.
- Test sites (zone_provider_acceptance, relay_service): async test fns
  that only performed blocking setup/teardown became sync tests; where a
  test genuinely awaits, the calls are wrapped in spawn_blocking.

Gate result: `check-async-gate` scans 755 files, zero blocking calls in
async contexts. d2bd lib 460/0, device-security-key tests green.
…first census family)

Thirteen network-family operations (CreateTapFd, CreatePersistentTap,
DeletePersistentTap, SetBridgePortFlags, CreateBridge, DeleteBridge,
ApplyRoute, ApplyNftables, ApplyNftablesProjection, ApplyNmUnmanaged,
ApplySysctl, UpdateHostsFile, SeedDnsmasqLease) retire their typed
BrokerRequest/BrokerResponse wire variants at wire v6, mirroring the U10
process-family template:

- Wire: variants + arms gone (13 dispatch arms, 660 lines); RETIRED_WIRE_VARIANTS
  grows to 24 entries @ v6; matrix extended (24 retired frames refused with the
  typed stale-wire-version refusal + audit; current-only frames are envelope
  invocations). PROTOCOL_VERSION stays 6.
- Catalog: the 13 family rows stay committed (declaringProvider
  d2b-provider-network-local) with wireVariant dropped and provenance request;
  13 broker-generic kernel rows (apply-nftables, apply-nftables-projection,
  apply-nm-unmanaged, apply-route, apply-sysctl, create-bridge, delete-bridge,
  create-persistent-tap, delete-persistent-tap, create-tap-fd, set-bridge-port-
  flags, update-hosts-file, seed-dnsmasq-lease) served in-broker by the kernel
  dispatcher, wrapping the existing ops/ modules + live helpers (97 rows total).
  CreateTapFd carries its fd through the envelope response (fd_indexes/kinds).
- Sandwich: d2b-provider-network-local/src/operations.rs hosts the 13 family
  handlers (typed payload validation + nested kernel invocation through the
  KernelCaller seam with the KTD6 evidence chain via spawn_blocking);
  d2bd's NetworkEffectPort and the host-prep/destroy/reconcile DAG legs now
  call the kernels directly over the origination socket (dispatch_broker_kernel_ack
  + host_nft_kernel_payload), leaving zero typed network refs in d2bd.
- Ratchet: kernel_ops.rs network tokens seeded (nftables/dnsmasq -> network-local,
  sysctl -> activation-nixos, retires_with U12 network-fds census step); stale
  runtime.rs dnsmasq row removed.
- Tests: kernel dispatch suite for all 13 kernels (admit/refuse surfaces, field
  contracts, typed-payload refusals, create-tap-fd missing-intent path), 8
  network provider handler tests (nested frames, refusal preservation, fd
  round-trip), profile tests migrated, matrix current-only frames rewritten.

Also in this change (review-fix follow-ups on the committed U10/U11 surface):
- LaunchPosture::validate_request_fds refuses the ControllerEscrow posture
  outright (the retired arm refused a controller spawn without its bootstrap
  escrow fd; the fd-less family row can never transport it - fail-closed
  restored).
- The spawn-process kernel result no longer advertises extraFdIndexes (the
  kernel path mints no extra response fds; the handler keeps fd 0 only).
- The lifecycle-leases cell key space bumped to lifecycle-leases-v2: the U11
  caller keys the one-time cell on the camelCase payload while the pre-upgrade
  arm keyed the same cell name on a snake_case/Debug identity, which would
  make pre-upgrade consumed leases replayable across an upgrade; the renamed
  cell separates the key spaces by construction.
- The three host-prep/destroy broker-unreachable tests carry real fixture
  bundle artifacts (U12 legs resolve trusted intents before dialing) and the
  destroy fake broker answers envelope kernel frames.

Gates: contracts 49/0, broker lib 685/4 (known env), matrix 7/7, profiles
4+2+5, d2bd 460/0, network-local 44+8, provider-process 45/0, toolkit 16
(+1 known env), xtask 339+2 (+1 known env), layout rc0, async gate 757
files clean, generated drift 19/19, nix drift green.
The legacy `check-provider-layout` command (four provider-crate paths) was
superseded by the U14 canonical `check-provider-crate-layout` gate
(provider_crate_policy: crate layout + family-knowledge ratchet, wired
into //bazel/checks:check as //bazel/checks/policy:provider_crate_layout).
Remove the command, its implementation, and its fixture tests; the usage
string now names only the canonical gate. The plan's stale
`check-provider-layout` reference is thereby unwired repo-wide.
The first full //bazel/checks:check run on the post-U12 tree surfaced a
cluster of gate issues, all fixed here:

- broker_protocol_compatibility bazel target: srcs now include
  tests/common/mod.rs (the test declares `mod common;`) and the target
  deps the broker crate itself (the matrix reads
  RetiredWireVariant/retired_wire_variant) - the target never built under
  bazel on this branch until the full suite exercised it.
- Source hygiene (tier0_first_pass): non-ASCII dashes normalized to ASCII
  across 11 source/script files (incl. the plan doc; formatting only).
- Telemetry domain pins: telemetry_frame + redaction_guard fixtures named
  the retired SpawnRunner op label; re-pinned to the live EnvelopeInvoke
  row.
- spawn_process stale-socket test: the bind path inherited the sandbox
  execroot and exceeded SUN_LEN; the stale socket now binds a short
  /tmp path.
- d2bd bazel lib tests excluded from //bazel/checks:check (rust-main-
  packages) with a recorded follow-up: the U10 seam exposed a pre-existing
  dual-rlib test-support graph (d2b_core vs d2b_core_test_support through
  the volume/session/provider test-support chains) that makes
  d2bd_lib_test_support unbuildable; cargo covers the d2bd lib tests
  (460/0) and the test-support variant-graph unification is tracked.
- d2b-provider-process bazel test-support target gains the
  d2b-contracts-broker + d2b-core deps its seam code needs.

Remaining gate failures are environmental: the 6 flake-eval nix targets
cannot fetch the bazel execroot as a git tree (the execroot contains
symlinks; they need a real checkout) - unrelated to this branch's code.
Layer-1 suite: 462 pass / 6 env-fail.
The standing `make check-clippy` gate was never exercised during the U10-U14
unit work (workers ran focused tests only), so the workspace's denied-lint
backlog accumulated and blocked it; this clears it to GREEN (with the
CARGO_BIN_EXE_d2b-broker env the env!()-based broker tests require):

- d2b-contracts-broker: doc/attribute blank-line hygiene, iter().cloned()
  -> to_vec(), and the wire-contract constant pins carry a justified
  #[allow(clippy::assertions_on_constants)] (the literals are the shared
  WIRE codes, not tautology).
- d2b-broker: doc-gap hygiene, collapsible ifs (let-chains), Option::map,
  contains over iter().any(), needless Option::as_deref, String conversion,
  elided lifetime, needless cast, return-let; the spawn-process stale-socket
  test binds a short /tmp path (SUN_LEN under the bazel sandbox).
- d2b-provider-toolkit: too_many_arguments allows on the kernel-carrier
  surface (documented contract), needless Option::as_deref.
- d2b-provider-process: too_many_arguments allow on the control-identity
  helper.
- d2bd: doc-gap + doc-list hygiene, panic_message via match, Error::other,
  identity-map removal, and the effect-service poll timers are now
  tokio::spawn(ractor::time::send_after(...)) - a bare `let _` previously
  DROPPED the reschedule future (the poll loop would stop after the first
  interval).
- xtask: collapsible ifs, async-gate scanner repeat_n/vec-fill, dependency-
  surface sync-tool gets a file-level disallowed_methods allow (the U13
  deny list targets runtime workers, not this build-time audit).
- d2b-broker-composition, d2b-provider-supervisor, d2b-provider-network-local
  tests: hygiene + small lints.

Regression-checks: d2bd lib 460/0, broker lib 685/4 (known env), network-local
44+8, contracts/telemetry/toolkit/providers green, async gate still 0
violations (the timer spawn uses tokio, not the blocked calls list).
The live plan still named the removed `check-provider-layout` command in its
Verification Contract and U14 bullet; re-point both to the canonical
`check-provider-crate-layout`. Also dedent the async-gate scanner's for-loop
body left over-indented by the let-chain collapse in 6176b28.
The retired family rows declare closed payload schemas (additionalProperties
false) that do not carry tracingSpanId, but the typed request structs
serialized tracing_span_id as `null` when None - so every supervisor/daemon
envelope call carried an undeclared key and the real broker's envelope
validation refused it with invalid-payload. Unit tests used fake brokers and
never hit the real validation; the host-integration vmChecks (virtiofsd
volume runtime, device-worker launch, guest preflight, state posture,
resource-operator activation) caught it: the virtiofsd spawn failed with
"kernel refused: invalid-payload".

All 51 request structs now skip serializing an absent tracing_span_id
(#[serde(default, skip_serializing_if = "Option::is_none")]); a present span
still serializes normally. Also regenerate docs/reference/daemon-api.md
(stale after the post-U12 commits - the gen_daemon_api_drift CI check).
The committed rows name family operations in PascalCase (ObserveRunner,
ApplyNftables) while the family ResourceRef labels are lowercase and
dash-separated (Operation/observe-runner, Operation/apply-nftables). The
U12-era case-insensitive match never reconciled the dash, so the daemon's
rendezvous refused every forwarded family call with uncommitted-operation
(the host-integration vmChecks proved it: the supervisor's ObserveRunner
forward was refused). declares() and the bare-name invoke lookup now
canonicalize both spellings (case + dashes) before comparing.
…pe seam

The host-integration vmChecks exposed a second U10 integration gap: the
provider-controller launch path pre-arms a seqpacket bootstrap pair, but the
family rows declared no fd facet, so the supervisor refused every inherited
descriptor and the controllers could never launch (the known "daemon launch
path passing escrow fds needs later migration" item).

This restores the retired escrow contract through the seam:
- The SpawnRunner family row declares the request-fd facet (maxFds 3,
  fdKind Any), so the envelope's U1 fd validation admits the escrow leg.
- The supervisor's launch passes the ticket's inherited fds through the
  envelope request leg (envelope_call_with_fds) and reports the inherited
  count; the deep refusal is removed.
- The spawn-process kernel pops the controller bootstrap fd (the daemon
  end), inserts it into the broker-side controller-bootstrap registry under
  the runner id, and returns a duplicate as the extra response fd
  (controllerBootstrapFdIndex + extraFdIndexes).
- The family handler validates per posture again: ControllerEscrow requires
  its escrow descriptor (the retired (0,0) fail-closed), other postures
  refuse any descriptors; the forwarded request fds ride into the nested
  kernel call and the response's bootstrap index is forwarded.
- The wire-opacity storage tests aligned with the span omission from
  9464800 (an absent tracing span is no longer serialized).

Broker lib 685/4 (known env), supervisor 8, process 45, d2bd 460.
The resolver-side inherited-fd fence (a U10-era placeholder for the fd-less
family rows) refused every controller ticket before the launch; with the
SpawnRunner row now declaring the request-fd facet, the fence is removed and
the per-posture shape is enforced fail-closed in the family handler.
An outer forwarded invocation parks its dispatch worker on block_on until
the nested leg's reply crosses back. One shared round-robin pool could
land the nested leg on a pinned worker's queue and deadlock the pair
until the io budgets expire - the SignalRunner/open-pidfd 10s stalls.
Nested legs now dispatch on a dedicated pool, making the starvation
impossible by construction.
…entity

Two VolumeBinding workers share the template-scoped role id, so the
daemon's (vm, role) pidfd table kept only the first registration and
every later ObserveRunner resolved to the wrong pid - the readiness
probe then quarantined the row and its retries livelocked against the
broker's duplicate-spawn guard. Roles that admit several runners per VM
(virtiofsd: one per microvm.shares row) now register and are looked up
under '<role-id>@<resource-uid>'; single-runner roles keep the bare
role id the vm-stop signal paths key on. SignalRunner carries the
closed role on the wire so its lookup composes the same key.
Two VolumeBinding workers share the template-scoped role id, so the
daemon's (vm, role) pidfd table kept only the first registration: the
second worker's ObserveRunner resolved to the first worker's pid, its
readiness probe failed the cgroup check, the row quarantined, and every
retry livelocked against the broker's duplicate-spawn guard.

Serving-worker launches (the binding-owned virtiofsd worker class) now
register and are looked up under '<role-id>@<resource-uid>' - a key the
supervisor derives from the resolved intent's serving-worker class and
the family handlers derive from the same predicate
(resolved_intent_is_serving_worker). Single-runner roles keep the bare
role id the vm-stop signal paths key on. SignalRunner carries the
bundle runner intent ref so its lookup resolves the same class.
… zone

prepare_state_dir resolved the kernel zone from the Device's Guest target
VM, but a nested Guest is never registered with the host daemon's zone
coordinator - every reconcile failed with zone-coordinator-vm-not-registered
and the swtpm worker could never launch. The Device row's own zone (the
same zone the declared-children surface keys on) is the correct anchor.
…tory parent

The host unit provisions /var/lib/d2b/tpm-state owned by the d2bd service
account (the declared daemon-owned anchor the Device state subdirectories
are created inside); the broker's production root-parent rule refused it
and every TPM reconcile died on the prepare-directory kernel. The
prepare-directory kernel now carries the broker's daemon uid and admits a
parent owned by that trusted account; the vm-start prerequisite path
keeps its exact prior posture.
…state dir is absent

The declared swtpm Process row's driver launches as soon as the row
exists; a launch racing the backing state Volume's layout spawned a child
that died on its first log write and burned the row's restart budget
before the layout ever created the directory. The launch now refuses
retryably until the trusted state directory exists.
…ption

The swtpm_ioctl -i flush is a one-shot: its clean exit is the success
path. The launch's adoption read treated a child that already exited as
a failed launch (pidfd-unavailable) and the non-retryable ephemeral row
died before the reaper log could publish the exit outcome. A vanished
EphemeralProcess spawn now reports the launch; the exit outcome itself
decides the row.
The observation logs the broker-seam chase added (invocation timing,
fd-count and cgroup probes, pidfd-table markers, executable-mismatch
diffs) have served their purpose; the fixes they carried stand on their
own. Error-path diagnostics on code introduced by the fixes stay.
The U6 composition split left d2b-broker's integration tests compiling
against a CARGO_BIN_EXE env var cargo never sets for the package (the
bin moved to d2b-broker-composition); the harnesses now resolve the
binary at runtime from the cargo var or the workspace build output. The
test constructors gained the fields the nested-dispatch pool, the TPM
zone anchor, and the prepare-directory daemon-uid threading added.
# Conflicts:
#	packages/d2b-broker/BUILD.bazel
#	packages/d2b-core/BUILD.bazel
The v3 merge (#539) folds a per-crate clippy aspect into every suite member
and the workspace `-Dwarnings` flag carries into the clippy action, so the
warnings the merge introduced are gate failures now:

* provider-supervisor: a needless borrow on the resolved intent, a
  collapsible `if let` chain over the launched-runner snapshot, and a
  `map_err` that only logs before returning the same error (`inspect_err`).
* provider-process: the launched-runner snapshot tuple trips
  `clippy::type_complexity`; the tuple is the trait's wire shape, so the
  allowance names that instead of aliasing it.
* d2b-broker kernel ops registrations pass the kernels as closures; the
  context now takes the function paths directly.
* d2b-broker's BUILD keeps the U6 split (no binaries) but must still export
  the `:clippy` aggregate `//bazel/checks/rust:portable_rust_broker` names.
* `maybe_harden_swtpm_dir` matched `BrokerRequest::EnvelopeInvoke` on
  `envelope.request`, which is a `BootstrapCall` under `layer1-bootstrap`:
  no bootstrap leg can present a chain root, so that variant pins the
  nested-pool decision to false.
* the broker test harness resolved the binary through `env!(
  "CARGO_MANIFEST_DIR")`, which embeds the absolute sandbox path the bazel
  rustc wrapper refuses; the fallback resolves the workspace target dir from
  the test's runtime cwd instead.
…emon

The zone-native fallback restored in fe6e17f composed the Device state
Volume directory (`device-<32hex>-tpm-state`) locally, which the shared-crate
family-knowledge probes read as per-resource naming knowledge in `d2bd` and
which the L1 policy gate refuses (the ratchet only shrinks).

The broker's `PrepareStateDir` resolves that same directory from the same
trusted artifacts - the request's `baseDir` is not read on either arm - so the
daemon now sends the trusted `path:swtpm-state:<guest>` row's root and leaves
the narrowing where the owning artifacts live. `state_volume_name`,
`unique_tpm_state_dir`, `tpm_devices_of_guest` and `deterministic_resource_uid`
had no other caller and are gone with it.
… flush

db6ff2d put the presence check inside `derive_resource_backed_paths`, which
made the derivation a filesystem probe: the identity fence then lost its
precedence (a foreign state dir reported "not provisioned" instead of the
identity mismatch) and the pure path-derivation fixtures could no longer
accept a well-formed plan, while the check read the state *root* rather than
the Volume directory the child opens.

Presence now lives in the spawn hook that actually proceeds with the launch,
over `trusted_state_dir` - the state root joined with the trusted Volume name
the argv fence already pins - and only for the long-lived worker, the launch
whose argv names `--tpmstate dir=`: `swtpm` opens its log and pid file there
the moment it starts, so a launch racing the Volume's layout must fail
retryably instead of burning the row's restart budget on a child that dies on
its first write.

The one-shot `swtpm-init-flush` is admitted instead: its argv names
`--unix <dir>/ctrl.sock`, a control socket the worker binds inside that same
directory, so the flush waits for the socket and succeeds once the worker is
up - refusing it would spend the row's single launch attempt (one-shots never
restart) on a race it can win. The device-worker-launch vmCheck failed
exactly this way: the daemon admits the declared flush row at startup, before
the Volume layout lands.

Also pins the delete-TAP payload to the six identity/generation keys it
carries: 9464800 omits the absent tracing span from envelope payloads, which
the count assertion had not followed.
`runtime::tests::a_current_wire_variant_passes_the_retired_wire_gate` sends a
current Hello over a socketpair, so the peer-credential gate reads the kernel
peer uid of the test process and compares it with the fixture's
`config.d2bd_uid`, which was pinned to 1000. That only holds on a host whose
runner is uid 1000: a GitHub runner is 1001, where the gate refuses the
request (`Broker.PeerCredentialRefused`) and the three broker lib-test targets
fail in CI while passing locally.

The fixture now names the test process's own uid - the same `Uid::current()`
the production config falls back to - so the gate compares like with like on
any runner.
@vicondoa
vicondoa merged commit f29f202 into v3 Sep 16, 2026
16 checks passed
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.

1 participant