Repository navigation
#74 ABI half: HostCtx is a kernel-tagged handle verified before use (no host pointer crosses) - #1007
Open
MattJackson wants to merge 6 commits into
Open
MattJackson wants to merge 6 commits into
MattJackson wants to merge 6 commits into
Conversation
…ied before it names anything BUSBAR-1.6.0.md Part 2 #74 (l.2274): "the handle a plugin holds is kernel-MAC'd ... the kernel verifies it on every ABI call". The mechanism's HostCtx was the address of the instance's leaked InstanceWake, and host_wake, every host service (services.rs served/caller/credential_kinds/ destination), every connector slot (conn_services.rs armed/warn_unverified/ticket_waker) and every I/O slot (io_slots.rs caller) dereferenced whatever pointer the plugin handed back: a forged context was undefined behaviour inside the host, not a refused handle. busbar-contract abi/mechanism/ticket.rs: HostCtx { handle: u64 } (NONE = 0). The same 8 bytes and alignment as the pointer, so no struct that carries it moves: the layout golden changes one field name (MechHostCtx.ptr -> .handle) and the C header renders uint64_t handle. plugin-loader dispatch/handle.rs: handle = tag << 24 | id. id indexes the host's instance table; tag = the high 40 bits of SipHash-2-4(key, [domain, id]) with a 128-bit key from getrandom at the first bind (host memory only, never logged; the #75 channel-2 residual stands). resolve() recomputes the tag and compares it in constant time before the table is read; a handle the host did not mint names nothing, is counted (handle::forged), and the entry answers as it answers no context. Every former pointer dereference goes through resolve(). A bind past 2^24 instances is refused (LoadError::HandlesSpent) rather than a handle reused. RED: host_services_tests::a_forged_context_is_refused_before_it_names_an_instance (tag bit off, id moved, a raw host address: clock.now REFUSED, wake wakes nothing, both counted; control: the minted context serves and wakes). handle_tests: SipHash-2-4 reference vectors; only a minted handle resolves (six forgeries, each counted; NONE is not a forgery). Test doubles hand HostCtx::NONE where they handed a null pointer.
… = 1, one guess) The coordinator's bar for the 40-bit tag: a failed verify quarantines the calling instance, so no plugin gets a second guess. Every crossing now states its instance on its thread for the span of the op (plugin.rs CROSSING, set on the caller's thread and on the FFI worker for lifecycle ops, restored for nested crossings). handle::resolve's refusal counts the miss and calls plugin::quarantine_crossing: the crossing instance is faulted exactly as the watchdog faults a hung one (never called again on any worker, every later op FAULT, its connector streams purged) and a WARN names the plugin. A call from a plugin's own thread outside any crossing names no caller and is refused and counted. RED: dispatch_tests::a_forged_context_quarantines_the_instance_that_forged_it: the test plugin's new FORGE tick hands wake a context one tag bit off; the instance is faulted and its next op is FAULT; control: an honest instance on the same dispatcher is never faulted.
…ng op, key at boot, own-instance check, closure-only crossing mark Rulings on the review of e84c5dc, in one change: 1. LATCH: a forgery outside any crossing names no instance to quarantine, so it counts against a process-wide latch (handle::Latch); at LATCH_AT = 16 every resolve made outside a crossing is refused for the rest of the process, with one ERROR line; DispatchStats carries forged_handles and handles_latched. A revoked handle is no forgery (its tag verifies). 2. REVOCATION: InstanceWake.revoked, set by Instance::revoke (quarantine, watchdog fault), on close, and on drop; resolve refuses a revoked entry after the tag check. 3. The forging op itself answers FAULT: after the crossing returns, a faulted instance's answer is neither judged nor ingested. 4. KEY AT BOOT: drawn when the loader boots (Dispatcher::build -> handle::arm) and at the latest by the first bind; a failing OS random source refuses the bind with LoadError::Entropy. The RandomState fallback is deleted. 6. Inside a crossing a context must name the crossing's own instance; another instance's handle is a forgery (the interim bound before the CallId ticket). 7. The crossing mark is set only through with_crossing(inst, || ..) (and without_crossing for the inline HOT-lane plane and transport slots, plane.rs and transport.rs, which null it). REDs: handle_tests -- the raw address of the leaked entry itself, a revoked handle refused and not counted, another instance's handle refused inside a crossing, the latch tripping at K and refusing every off-crossing resolve (a crossing's own still resolves), a failing entropy source refused; dispatch_tests -- the forging tick (the FFI-worker lifecycle path) answers FAULT, the instance is quarantined and its own handle revoked, and a boot whose entropy fails is refused through the test seam; host_services_tests -- the raw-address case is the entry's own address. The HOT lane's HostCtx (abi/hot/host.rs, kernel plane_host) still carries a raw pointer; it goes with abi/hot (row 84).
…oot refusal, readiness on the latch 1. REDs added: a closed, a dropped and a watchdog-faulted instance's handle names nothing and is no forgery (dispatch_tests); a forgery inside an inline HOT-lane slot nested in a crossing blames no one and the outer mark is restored (without_crossing under with_crossing); FORGE through a NON-lifecycle op (a plane's arrive, crossed on the caller's thread, crates/busbar/tests/ plane_forged_context.rs) answers FAULT and quarantines. 2. No test asserts on the process-wide counter any more: Latch carries its own forged/unattributed counts and the tests judge a latch of their own; the in-crossing case runs under a real crossing (with_crossing on a loaded instance) and asserts the quarantine and an unmoved latch. 3. The boot refusal is real: Dispatcher::try_new / try_with_services draw the key first and answer LoadError::Entropy; new / with_services panic on it rather than run keyless; busbar's main draws the key before the dispatcher and exits 1 on a failing source. RED: a boot (try_new) and a bind through the seam, both refused. 4. Readiness: /healthz answers 503 with a reason line while the handle latch is tripped (no new metrics family); the ERROR line stays. 5. Inside a crossing another instance's handle is a forgery even if revoked (own-instance check before the revocation check). 7. A bind refused after mint revokes the minted entry (Unbound guard). 8. The latch is read again after counting, so no off-crossing resolve passes once it trips. 9. sdk/services.rs SAFETY comments describe the u64 handle.
MattJackson
added a commit
that referenced
this pull request
Oct 14, 2026
…e counter Per the #1007 re-review (no test reads the global forgery counter): the stale-wake RED wakes from inside a crossing of its own instance, so a misjudged forgery would quarantine it, and asserts it is not; the forged-generation RED asserts the quarantine and a revoked handle on a latch of its own.
…cket-less REDs, a neutral readiness line
1. handle_tests judges its own latch (latch.forged()), not the process-wide counter.
3. REDs: /healthz answers 503 while the handle latch is tripped (plugin_dispatch_halted); an instance
adopted by the dispatcher whose ticket-less call hangs past its budget is faulted by the watchdog's
ticket-less path (watchdog.rs ticketless -> Instance::revoke) and its handle names nothing, no
forgery counted.
4. The 503 reason line is neutral ("plugin dispatch halted; restart required"): the route is
unauthenticated, and the detail is the latch's ERROR line.
MattJackson
marked this pull request as ready for review
October 14, 2026 11:15
Collaborator
Author
|
Rule 20 note from #1030 (row 155): disjoint hunks in busbar_plugin.h, abi-layout.golden, layout_golden.rs (OnPieceOut gains |
…es in the root crate, no inline test - Latch::forged/unattributed are the tests' readings: cfg(test), not dead code in a no-test build. - plane_forged_context: the forged context comes from the plane-driver test plane's own /forge-context arrival, so the test declares no extern "C" door of its own. Each "C" was a hit on busbar x instance:secret (the census learns `c` from busbar-secret-c): the cell is back at its base 35, the ceiling untouched. - endpoints: the /healthz latch RED moves to tests/plugin_dispatch_halted_tests.rs.
MattJackson
added a commit
that referenced
this pull request
Oct 15, 2026
…ext a u64 handle); the streaming door's PlaneTail states no metric lanes (the tail grew metric_lanes beside its stream ceiling)
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
BUSBAR-1.6.0.md Part 2 #74 (l.2274): the half that covers the mechanism door's untrusted ABI. DRAFT: security-critical; a fresh reviewer re-checks before undraft.
#74 is NOT closed by this PR. The HOT lane's
HostCtx(crates/busbar-contract/src/abi/hot/host.rs:61-71, kernel plane_host) still sends a raw pointer. #74 closes only when abi/hot is deleted (row 84: #898, #899, #904 and #960's chain).Defect. The mechanism
HostCtxwas the address of the instance's leakedInstanceWake. wake, every host service, every connector slot and every I/O slot dereferenced whatever value the plugin handed back.Change.
HostCtx { handle: u64 }, where handle = tag<<24 | id. The tag is 40 bits of SipHash-2-4 under a 128-bit key from the OS random source.Dispatcher::try_new/try_with_servicesreturnLoadError::Entropyon failure, and busbar's main exits 1. There is no fallback key.handle::resolveruns these checks in order:DispatchStatsgainsforged_handlesandhandles_latched.with_crossing(inst, || ..). The inline HOT-lane plane and transport slots runwithout_crossing.Layout. HostCtx stays 8 bytes, align 8. The golden changes one field name and the header renders
uint64_t handle. No R9 re-seed.Operator-visible effects and trade-offs
/healthzanswers 503 with the neutral line "plugin dispatch halted; restart required". The detail is in the latch's ERROR log line only. No new /metrics family is added; the count will ride /metrics: the dispatcher's bb_* counters, one HELP/TYPE family each (stacked on #952) #971's quarantine family once it exists.resolvehas loaded it. That entry is still a leaked, validInstanceWake, so the call in flight finishes against an instance whose next op is refused.Follow-up (stacked draft #1018): the ticket half.
RED tests (each judged on a latch of its own; none reads the process-wide counter):