@@ -0,0 +1,444 @@
//! protocol-registry-test — the registrar's own test fixture
//! (docs/os-development/protocol-namespace.md). One binary, four roles picked by
//! argv, driven by the `protocol-registry` kernel case:
//!
//! - `protocol-registry-test provider <mark>` — binds `/protocol/test/registry`
//! and answers every request with its one-byte `<mark>`, so a client can tell
//! *which instance* it reached.
//! - `protocol-registry-test deputy` / `... launder` — the two halves of the
//! laundering-deputy probe (assertion 7 below).
//! - `protocol-registry-test run` — the driver, and the assertions:
//! 1. a bind of a name this binary is not granted is refused, `-EPERM`;
//! 2. the kernel's reserved prefix holds: nothing may mount at or under
//! `/protocol`, and nobody may unmount it;
//! 3. a **forged power event** posted straight into the registry endpoint
//! does not shut the machine down — the registrar answers privileged
//! control traffic on the sender's kernel-stamped identity, never on two
//! bytes of payload. The system surviving to the next line is the
//! assertion, and it is the strongest one available: a regression takes
//! the whole boot down;
//! 4. zero-length calls carrying a capability do not consume PID 1's handle
//! table — after sixty-four of them a legitimate bind still succeeds;
//! 5. a bind colliding with a **live** owner is refused, `-EBUSY`;
//! 6. killing the provider makes the cached channel fail (`-EPEER`), and a
//! client that re-resolves reaches the **restarted** instance — the
//! restart story falling out of the naming layer, with no reconnect verb
//! anywhere;
//! 7. the laundering deputy is refused: the *same binary* under the *same
//! supervisor name* binds when its supervisor is a task the kernel
//! started, and is refused one hop further down, where the supervisor is
//! a task nobody authorized. The pair is the point — one alone would pass
//! for reasons that have nothing to do with the rule under test;
//! 8. a **forged terminate signal** does not reach PID 1 either: the kernel
//! refuses to bind this process's signal delivery to an endpoint it does
//! not own, so "bind init's endpoint, then signal yourself" — which needs
//! no forgery at all, only two ungated syscalls — cannot happen;
//! 9. the same for the other kernel notifications: a timer and an exit
//! subscription may only be armed on one's own endpoint;
//! 10. the handle-table storm of assertion 4, aimed at a **harness-run**
//! service instead of PID 1 — the loop every other service in the system
//! runs — after which a capability-passing operation still works.
//!
//! Assertions 8– 10 each pair a refusal with a control (the same operation on an
//! endpoint this process created, which must succeed), because a refusal alone
//! would pass just as happily against a kernel that refused everything.
//!
//! Prints `protocol-registry: ok` on success, or a `protocol-registry: FAIL`
//! line naming the step. Spawned bare (the initial-ramdisk sweep starts every
//! bundled binary), it exits silently so it cannot derange other tests.
const std = @import ( " std " ) ;
const channel = @import ( " channel " ) ;
const envelope = @import ( " envelope " ) ;
const file_system = @import ( " file-system " ) ;
const ipc = @import ( " ipc " ) ;
const logging = @import ( " logging " ) ;
const power_protocol = @import ( " power-protocol " ) ;
const process = @import ( " process " ) ;
const service = @import ( " service " ) ;
const time = @import ( " time " ) ;
/// The contract this fixture's provider role claims — under `/protocol/test`,
/// the subtree every `/test/` binary is granted.
const contract = " test/registry " ;
/// Where the deputy and the laundered grandchild report their bind verdicts. The
/// driver binds it and listens; both children reach it by name like any client.
const verdict_contract = " test/verdict " ;
/// The name the deputy claims (it may — its supervisor is the kernel-started
/// driver) and the one its own child attempts (it may not — its supervisor is
/// the deputy, a task neither init nor the kernel started).
const deputy_contract = " test/deputy " ;
const laundered_contract = " test/laundered " ;
/// A contract this fixture is deliberately NOT granted. It belongs to the
/// display service, and no manifest row names this binary against it.
const forbidden = " display " ;
fn fail ( step : [ ] const u8 ) noreturn {
_ = logging . write ( " protocol-registry: FAIL " ) ;
_ = logging . write ( step ) ;
_ = logging . write ( " \n " ) ;
process . exit ( 1 ) ;
}
// --- the provider role ------------------------------------------------------
/// Which instance this is, echoed to every caller. The point of the whole
/// exercise: a client cannot tell instances apart by the name it opened, so the
/// provider says so itself.
var mark : u8 = '?' ;
/// The provider's one operation, plus `handover_operation` — the capability-passing
/// verb the harness-exhaustion probe needs. Claiming the arriving capability with
/// `take` and *using* it is the point: it proves the harness handed over a working
/// handle, which a service whose table is full could not have been given.
fn answer ( message : [ ] const u8 , reply : [ ] u8 , sender : u32 , arrived : * ipc . Arrival ) usize {
_ = sender ;
if ( message . len != 0 and message [ 0 ] == handover_operation ) {
if ( arrived . take ( ) ) | handed | {
_ = ipc . send ( handed , & [ _ ] u8 { mark } ) ; // speak on it, then it is ours to close
_ = ipc . close ( handed ) ;
}
}
if ( reply . len == 0 ) return 0 ;
reply [ 0 ] = mark ;
return 1 ;
}
/// The one request byte the provider treats as "here is an endpoint, use it".
const handover_operation : u8 = 's' ;
// --- the laundering-deputy roles --------------------------------------------
/// One child's bind verdict, posted to the driver over `test/verdict`. Sent
/// asynchronously (no reply owed) so a child can report and exit without the
/// driver having to be waiting at that instant.
const Verdict = extern struct {
/// `deputy` or `launder` — which of the two spoke.
role : u8 ,
_reserved : [ 3 ] u8 = . { 0 } ** 3 ,
/// The registry's answer: 0 bound, or a negative errno.
status : i32 ,
} ;
const role_deputy : u8 = 'd' ;
const role_launder : u8 = 'l' ;
/// Report a verdict to the driver, best-effort: the driver has a deadline, and a
/// child that cannot reach it must not hang the boot.
fn report ( role : u8 , status : i32 ) void {
const verdict = Verdict { . role = role , . status = status } ;
var attempts : u32 = 0 ;
while ( attempts < 200 ) : ( attempts += 1 ) {
if ( channel . openEndpoint ( verdict_contract ) ) | endpoint | {
defer _ = ipc . close ( endpoint ) ;
if ( ipc . send ( endpoint , std . mem . asBytes ( & verdict ) ) ) return ;
}
time . sleepMillis ( 20 ) ;
}
}
/// The middle of the laundered chain: a granted binary whose own supervisor is
/// the kernel-started driver. It binds — that is the control, and it is what
/// makes the refusal below mean something — then starts an instance of *itself*
/// and stays alive while that instance asks for a name, so the registry judges a
/// live, fully attestable chain rather than an orphan.
fn deputy ( ) void {
const spare = ipc . createIpcEndpoint ( ) orelse return ;
report ( role_deputy , verdictOf ( deputy_contract , spare ) ) ;
const exits = ipc . createIpcEndpoint ( ) orelse return ;
const child = process . spawnSupervised ( " protocol-registry-test " , & . { " launder " } , exits ) orelse return ;
awaitExit ( exits , child ) ;
}
/// The laundered grandchild. Its binary is granted, and its supervisor's binary
/// is granted as a supervisor — every *name* in the row matches. The one thing
/// that does not is which task its supervisor is: nobody authorized this deputy,
/// so nothing it spawns inherits a name.
fn launder ( ) void {
const spare = ipc . createIpcEndpoint ( ) orelse return ;
report ( role_launder , verdictOf ( laundered_contract , spare ) ) ;
}
// --- the driver role --------------------------------------------------------
/// Ask whoever is on the other end of `endpoint` which instance they are. Null
/// if the call failed — which, after a provider dies, is `-EPEER`.
fn instanceOf ( endpoint : ipc . Handle ) ? u8 {
var reply : [ 8 ] u8 = undefined ;
const n = ipc . call ( endpoint , " ? " , & reply ) catch return null ;
if ( n != 1 ) return null ;
return reply [ 0 ] ;
}
/// Open `/protocol/<contract>` until the instance marked `wanted` answers,
/// re-resolving each time — exactly the recovery a client performs when its
/// provider dies. Returns the live endpoint. Handles from a failed attempt are
/// closed: a 32-slot table does not survive four hundred leaks.
fn reach ( wanted : u8 ) ? ipc . Handle {
var attempts : u32 = 0 ;
while ( attempts < 400 ) : ( attempts += 1 ) {
if ( channel . openEndpoint ( contract ) ) | endpoint | {
if ( instanceOf ( endpoint ) ) | seen | {
if ( seen == wanted ) return endpoint ;
}
_ = ipc . close ( endpoint ) ;
}
time . sleepMillis ( 20 ) ;
}
return null ;
}
/// Block until child `id`'s exit notification lands on `endpoint`.
fn awaitExit ( endpoint : ipc . Handle , id : u32 ) void {
var scratch : [ 8 ] u8 = undefined ;
var guard : u32 = 0 ;
while ( guard < 1000 ) : ( guard += 1 ) {
const got = ipc . replyWait ( endpoint , scratch [ 0 .. 0 ] , & scratch , null ) ;
if ( got . isChildExit ( ) and got . childProcessId ( ) == id ) return ;
}
fail ( " the provider's exit notification never arrived " ) ;
}
/// One bind, retried only while the registry is *unreachable* — a registry that
/// answered has decided. Null means it never answered, which the caller reports
/// in its own words: "not mounted yet" and "stopped serving mid-test" are the
/// same silence and very different findings.
fn verdictWithin ( name : [ ] const u8 , endpoint : ipc . Handle ) ? i32 {
var attempts : u32 = 0 ;
while ( attempts < 200 ) : ( attempts += 1 ) {
if ( channel . bind ( name , endpoint ) ) | status | return status ;
time . sleepMillis ( 20 ) ;
}
return null ;
}
/// The registry answers, whatever it answers — the first bind also waits out a
/// registry that has not mounted `/protocol` yet.
fn verdictOf ( name : [ ] const u8 , endpoint : ipc . Handle ) i32 {
return verdictWithin ( name , endpoint ) orelse fail ( " the registry never came up " ) ;
}
/// The registry's endpoint, obtained the way every process obtains it: resolve
/// `/protocol`. `fs_resolve` installs the backend's capability in ANY caller's
/// table, and the registrar's backend endpoint is PID 1's *supervision* endpoint
/// — the same mailbox its signals, timers, child deaths and power events land in.
/// That is precisely why what arrives there may not be believed on its content.
fn registryEndpoint ( ) ? ipc . Handle {
var relative : [ channel . path_maximum ] u8 = undefined ;
const route = file_system . fsResolve ( channel . root , 0 , & relative ) orelse return null ;
return switch ( route ) {
. kernel = > null ,
. backend = > | backend | backend . handle ,
} ;
}
/// Wait for both children of the laundering probe to report. Taken together
/// rather than one at a time because the two are independent processes and the
/// order they reach the driver is not the driver's business.
fn awaitVerdicts ( endpoint : ipc . Handle ) struct { deputy : i32 , launder : i32 } {
var scratch : [ 64 ] u8 = undefined ;
var from_deputy : ? i32 = null ;
var from_launder : ? i32 = null ;
var guard : u32 = 0 ;
while ( guard < 2000 ) : ( guard += 1 ) {
const got = ipc . replyWait ( endpoint , scratch [ 0 .. 0 ] , & scratch , null ) ;
if ( got . isMessage ( ) and got . len >= @sizeOf ( Verdict ) ) {
const verdict = std . mem . bytesToValue ( Verdict , scratch [ 0 .. @sizeOf ( Verdict ) ] ) ;
if ( verdict . role == role_deputy ) from_deputy = verdict . status ;
if ( verdict . role == role_launder ) from_launder = verdict . status ;
}
if ( from_deputy ) | d | {
if ( from_launder ) | l | return . { . deputy = d , . launder = l } ;
}
}
fail ( " the deputy chain never reported its bind verdicts " ) ;
}
fn run ( ) void {
// A spare endpoint to offer the registry. Every bind below is meant to be
// refused, so it is never actually claimed by anyone.
const spare = ipc . createIpcEndpoint ( ) orelse fail ( " create an endpoint to offer " ) ;
// 1. A name this binary is not granted. Refusal is `-EPERM` at the bind, not
// silence: the caller *is* the provider, and telling it its manifest is
// wrong is not a leak.
const ungranted = verdictOf ( forbidden , spare ) ;
if ( ungranted != - envelope . EPERM ) fail ( " an ungranted bind was not refused with -EPERM " ) ;
_ = logging . write ( " protocol-registry: ungranted bind refused \n " ) ;
// 2. The kernel's residual rule: `/protocol` is claimed once, by PID 1, and
// the prefix is closed for the boot. Shadowing one contract by mounting
// under it is refused too, and so is deleting the namespace outright.
if ( file_system . mount ( " /protocol " , spare ) ) fail ( " a second mount at /protocol was allowed " ) ;
if ( file_system . mount ( " /protocol/display " , spare ) ) fail ( " a mount under /protocol was allowed " ) ;
if ( file_system . fsUnmount ( " /protocol " ) ) fail ( " unmounting /protocol was allowed " ) ;
_ = logging . write ( " protocol-registry: /protocol reserved \n " ) ;
// 3. A forged power event. Resolving `/protocol` hands this process — any
// process — a sendable handle to PID 1's mailbox, and the bytes below are
// byte-for-byte what the real power service publishes when the button is
// pressed. If the registrar believed payloads, this single `send` would
// run the whole stop sequence and park PID 1 in its final sleep: every
// service terminated, the registry gone for the rest of the boot, and no
// way back. So the assertion is that the machine is still here afterwards
// — the strongest one available, because a regression does not fail this
// line, it takes the entire boot down with it.
const registry = registryEndpoint ( ) orelse fail ( " resolve /protocol " ) ;
const forged = power_protocol . EventMessage { . event = @intFromEnum ( power_protocol . Event . power_button ) } ;
if ( ! ipc . send ( registry , std . mem . asBytes ( & forged ) ) ) fail ( " post a forged power event " ) ;
const still_serving = verdictWithin ( forbidden , spare ) orelse
fail ( " the registrar went silent after a forged power event — it acted on it " ) ;
if ( still_serving != - envelope . EPERM ) fail ( " the registry misanswered after a forged power event " ) ;
_ = logging . write ( " protocol-registry: forged power event ignored \n " ) ;
// 4. Sixty-four zero-length calls, each carrying a capability. The kernel
// installs a sent capability in the receiver's table whatever the message
// length, so every one of these hands PID 1 a handle — and PID 1 has
// thirty-two slots. A registrar whose ping path returns without closing
// runs out before this loop is half done, after which nothing can ever
// bind again and the machine cannot recover for the rest of the boot.
var storm : u32 = 0 ;
while ( storm < 64 ) : ( storm += 1 ) {
var scratch : [ 8 ] u8 = undefined ;
_ = ipc . callCap ( registry , " " , & scratch , spare ) catch { } ;
}
// The proof is a legitimate bind landing afterwards. This one doubles as
// the driver's listening post for step 7.
const verdicts = ipc . createIpcEndpoint ( ) orelse fail ( " create the verdict endpoint " ) ;
const after_storm = verdictWithin ( verdict_contract , verdicts ) orelse
fail ( " the registrar went silent after capability-carrying pings " ) ;
if ( after_storm != 0 ) fail ( " a legitimate bind failed after capability-carrying pings " ) ;
_ = logging . write ( " protocol-registry: capability-carrying pings did not exhaust the registrar \n " ) ;
// 5. A live owner's name is not for the taking. Collision is an error, never
// last-writer-wins — otherwise any process could impersonate any service.
const exits = ipc . createIpcEndpoint ( ) orelse fail ( " create the exit endpoint " ) ;
const first = process . spawnSupervised ( " protocol-registry-test " , & . { " provider " , " 1 " } , exits ) orelse
fail ( " spawn the first provider " ) ;
const one = reach ( '1' ) orelse fail ( " the first provider never bound its contract " ) ;
const collision = verdictOf ( contract , spare ) ;
if ( collision != - envelope . EBUSY ) fail ( " a collision with a live owner was not refused with -EBUSY " ) ;
_ = logging . write ( " protocol-registry: collision refused \n " ) ;
// 6. The restart story. Kill the provider: the cached channel dies with it
// (the kernel fails calls on a dead endpoint rather than blocking
// forever), and re-resolving the same name reaches whatever instance the
// registry now points at. No reconnect verb, no client-side repair.
if ( ! process . kill ( first ) ) fail ( " kill the first provider " ) ;
awaitExit ( exits , first ) ;
if ( instanceOf ( one ) != null ) fail ( " a call on a dead provider's channel succeeded " ) ;
_ = logging . write ( " protocol-registry: dead channel refused \n " ) ;
_ = process . spawnSupervised ( " protocol-registry-test " , & . { " provider " , " 2 " } , exits ) orelse
fail ( " spawn the replacement provider " ) ;
const two = reach ( '2' ) orelse fail ( " re-resolving never reached the restarted provider " ) ;
_ = logging . write ( " protocol-registry: restarted provider reached \n " ) ;
// 7. The laundering deputy. Two binds by the *same binary*, whose grant rows
// read identically — same claimant path, same supervisor path — differing
// only in which task the supervisor is:
//
// kernel -> this driver -> deputy the deputy binds (control)
// kernel -> this driver -> deputy -> ... the grandchild does not
//
// Nobody authorized the deputy to be a supervisor, so nothing it spawns
// inherits a name — which is the whole reason a supervisor is attested by
// task id and not by the string the kernel stamped on it. The control
// matters as much as the refusal: without it this step would pass just as
// happily against a registrar that refused everything.
const deputy_exits = ipc . createIpcEndpoint ( ) orelse fail ( " create the deputy's exit endpoint " ) ;
_ = process . spawnSupervised ( " protocol-registry-test " , & . { " deputy " } , deputy_exits ) orelse
fail ( " spawn the deputy " ) ;
const chain = awaitVerdicts ( verdicts ) ;
if ( chain . deputy != 0 ) fail ( " a granted binary spawned by an AUTHORIZED task was refused " ) ;
if ( chain . launder != - envelope . EPERM ) fail ( " a granted binary spawned by an UNAUTHORIZED task was not refused " ) ;
_ = logging . write ( " protocol-registry: laundering deputy refused \n " ) ;
// 8. A forged terminate SIGNAL. Step 3 proved a *payload* cannot reach PID 1's
// shutdown path; this is the same destination by the other road, and the
// interesting one, because nothing here is forged at all. `signal_bind`
// nominates where a process's own signals are delivered, and `process_signal`
// lets any task signal itself — both correct in isolation, and together a
// shutdown primitive the moment the delivery point may be somebody else's
// endpoint. `fs_resolve` hands every process a handle to PID 1's, so all it
// would take is: bind, then signal yourself. The badge the kernel stamps is
// genuine, which is exactly why init cannot filter it and the kernel has to.
//
// The control comes first and matters as much as the refusal: an endpoint we
// created is accepted, so what the second line refuses is *foreignness* and
// not signals-in-general. If the second line ever succeeds again it also
// steals our own binding, so the terminate below lands in PID 1 and takes the
// boot with it — the assertion is the machine still being here.
const signals = ipc . createIpcEndpoint ( ) orelse fail ( " create the signal endpoint " ) ;
if ( ! process . bindSignals ( signals ) ) fail ( " binding signals to an endpoint we created was refused " ) ;
if ( process . bindSignals ( registry ) ) fail ( " binding signals to the registry's endpoint was allowed " ) ;
if ( ! process . sendSignal ( process . taskId ( ) , . terminate ) ) fail ( " post ourselves a terminate signal " ) ;
const after_signal = verdictWithin ( forbidden , spare ) orelse
fail ( " the registrar went silent after a redirected terminate signal — it acted on it " ) ;
if ( after_signal != - envelope . EPERM ) fail ( " the registry misanswered after a redirected terminate signal " ) ;
_ = logging . write ( " protocol-registry: foreign signal binding refused \n " ) ;
// 9. The same rule for the other two kernel notifications. A timer landing
// carries no identity — no sender, no cookie — so a service cannot tell a
// timer it armed from one a stranger armed on its endpoint; init re-arms its
// heartbeat on every landing, so N smuggled timers leave N+1 beats running
// forever. Exit subscriptions are the same shape with a smaller blast radius
// (an eight-slot table, and a firehose of deaths aimed at a stranger).
// Control first again, and for the timer the control's *landing* is the proof
// the refusal is about ownership rather than a timer syscall that just fails.
const ticker = ipc . createIpcEndpoint ( ) orelse fail ( " create the timer endpoint " ) ;
if ( ! time . timerOnce ( ticker , 1 ) ) fail ( " arming a timer on an endpoint we created was refused " ) ;
if ( time . timerOnce ( registry , 1 ) ) fail ( " arming a timer on the registry's endpoint was allowed " ) ;
var tick : [ 8 ] u8 = undefined ;
if ( ! ipc . replyWait ( ticker , tick [ 0 .. 0 ] , & tick , null ) . isTimer ( ) ) fail ( " our own timer did not land " ) ;
if ( process . subscribeExits ( registry ) ) fail ( " subscribing the registry's endpoint to exit events was allowed " ) ;
if ( ! process . subscribeExits ( ticker ) ) fail ( " subscribing an endpoint we created to exit events was refused " ) ;
_ = logging . write ( " protocol-registry: foreign timer and exit binding refused \n " ) ;
// 10. The handle-table storm again, aimed at a **harness-run service** this
// time. Step 4 covers PID 1, which runs its own hand-written loop; every
// other service in the system — the VFS, the display compositor, the device
// manager, every driver — runs `library/kernel/service.zig`, and that loop
// had the identical hole: the zero-length ping answered without closing what
// the ping carried, and callbacks handed a bare handle they had no use for.
// The provider role below runs that same harness, so it stands in for all of
// them. Sixty-four capability-carrying pings is twice its thirty-two slots.
var storm2 : u32 = 0 ;
while ( storm2 < 64 ) : ( storm2 += 1 ) {
var scratch : [ 8 ] u8 = undefined ;
_ = ipc . callCap ( two , " " , & scratch , spare ) catch { } ;
}
// The proof is a *capability-passing* operation still working: the kernel
// installs the sent handle in the receiver's table before the service sees
// the request, so against an exhausted service the call fails outright. And
// the provider answers on the handed-over endpoint, so the handle it claimed
// is a working one and not merely a number.
const handover = ipc . createIpcEndpoint ( ) orelse fail ( " create the handover endpoint " ) ;
var handover_reply : [ 8 ] u8 = undefined ;
const served = ipc . callCap ( two , & [ _ ] u8 { handover_operation } , & handover_reply , handover ) catch
fail ( " a capability-passing call failed after capability-carrying pings — the harness leaked them " ) ;
if ( served . len != 1 or handover_reply [ 0 ] != '2' ) fail ( " the harness-run provider misanswered after capability-carrying pings " ) ;
var echoed : [ 8 ] u8 = undefined ;
const back = ipc . replyWait ( handover , echoed [ 0 .. 0 ] , & echoed , null ) ;
if ( ! back . isMessage ( ) or back . len != 1 or echoed [ 0 ] != '2' ) fail ( " the provider never spoke on the endpoint it was handed " ) ;
_ = logging . write ( " protocol-registry: capability-carrying pings did not exhaust the harness \n " ) ;
_ = logging . write ( " protocol-registry: ok \n " ) ;
}
pub fn main ( startup : process . Init ) void {
const role = startup . arguments . get ( 1 ) orelse return ; // bare (ramdisk sweep): stay silent
if ( std . mem . eql ( u8 , role , " provider " ) ) {
const label = startup . arguments . get ( 2 ) orelse " ? " ;
if ( label . len != 0 ) mark = label [ 0 ] ;
service . run ( 64 , . { . service = contract , . on_message = answer } ) ;
return ; // terminate arrived, or the bind was refused; either way we are done
}
if ( std . mem . eql ( u8 , role , " deputy " ) ) return deputy ( ) ;
if ( std . mem . eql ( u8 , role , " launder " ) ) return launder ( ) ;
if ( std . mem . eql ( u8 , role , " run " ) ) run ( ) ;
}