//! /system/services/init — the first user-space program, PID 1. Built as its own //! freestanding binary (see build.zig), shipped on the boot volume at /system/services/init, //! loaded by the bootloader, and started in ring 3 as a scheduled process by the //! kernel (system/kernel/process.zig). It links against the shared user runtime //! library `runtime` and talks to the kernel only through `runtime`'s system_call wrappers. //! //! It proves the C-convention heap works, then — as PID 1 — acts as the system's //! **service supervisor**: it spawns the user-space services danos brings up at boot //! (the VFS server, the device manager), and settles into an event loop as the root //! of user space. Drivers are *not* its job: the device manager discovers the //! hardware and spawns those. This is the service half of the service/driver spawn //! split (docs/driver-model.md). //! //! M21: init also owns **orderly shutdown**. It supervises its children (keeping //! their ids and an exit endpoint), subscribes to the power service, and on a //! power-button event runs the stop sequence over its children in reverse order //! before asking the power service to enter S5 — lifecycle (M17) and events (M21) //! composing into a clean poweroff. //! //! P2: init is also the **registrar** — it serves `/protocol`, the namespace where //! a program finds everything it talks to //! (docs/os-development/protocol-namespace.md). It is the natural home: it already //! spawns the services and already holds the supervision link to each, so it is the //! process that *knows* which binary is which. Registry traffic rides the same //! endpoint as supervision, because one thread can only wait in one place — the //! loop below answers vfs `open`/`readdir`/`bind` alongside signals, timers, power //! events, and children's deaths. //! //! Which sets the security posture of this file. `fs_resolve` installs a mounted //! backend's endpoint capability in *any* caller's handle table, so sharing the //! mailbox means **every ring-3 process can send into PID 1**. Two rules follow, //! and both are structural here rather than remembered per branch: //! //! - **Privileged action requires an attested sender.** What arrives is a //! stranger's bytes; the only identity on it is the task id the kernel stamps. //! Content never authorizes (`onPowerEvent`), and neither does a name — the //! registrar attests a caller's supervision by task id (`supervisorSatisfies`). //! - **Absence is the enforcement.** P3: `open` consults the manifest with the //! same attested identity a `bind` does, and a caller with no grant is told //! exactly what a caller asking for a name nobody bound is told — `-ENOENT`, //! and no capability (`onOpen`). Restriction stage one of //! docs/os-development/protocol-namespace.md: what a process cannot open does //! not exist for it, so there is no "permission denied" to distinguish. //! - **A capability that arrives is closed unless it is claimed** (`Arrival`), //! because PID 1's thirty-two handle slots are a resource an unauthenticated //! caller would otherwise be able to spend. const std = @import("std"); const ipc = @import("ipc"); const process = @import("process"); const time = @import("time"); const memory = @import("memory"); const logging = @import("logging"); const power_protocol = @import("power-protocol"); const vfs_protocol = @import("vfs-protocol"); const envelope = @import("envelope"); const build_options = @import("build_options"); const fs = @import("file-system"); const csv = @import("csv"); /// The system services init brings up at boot are init's policy, not the kernel's — /// and that policy is now data: `/system/configuration/init.csv` (see `loadServices`), read at /// startup instead of a hardcoded list. Drivers are absent on purpose: the device /// manager owns those. /// /// The most services `/system/configuration/init.csv` can list, and the most argv entries (beyond the /// path) each may carry. Fixed caps because init parses the list into static storage — /// the freestanding, no-allocator counterpart to the device manager's registry table. const max_services = 16; const max_service_args = 4; /// One service init starts, parsed from a row of `/system/configuration/init.csv`: its binary path /// and argv, both slices into `init_csv` (held for the life of the process). const Service = struct { path: []const u8 = "", arg_buffer: [max_service_args][]const u8 = undefined, arg_count: usize = 0, fn arguments(self: *const Service) []const []const u8 { return self.arg_buffer[0..self.arg_count]; } }; /// The `/system/configuration/init.csv` bytes, held because the parsed services slice into them. var init_csv: [4096]u8 = undefined; var services: [max_services]Service = .{Service{}} ** max_services; var service_count: usize = 0; /// The live process id of each service (0 = not running) and its restart count, /// indexed by position in `services`. init supervises these: it spawns them against /// `supervision_endpoint` and, on a child's death, restarts it (up to /// `maximum_restarts`) — the reincarnation half of resilience (docs/resilience.md), /// the service-level counterpart to the device manager's driver restarts. var child_ids: [max_services]u32 = .{0} ** max_services; var restart_counts: [max_services]u32 = .{0} ** max_services; var shutting_down = false; var supervision_endpoint: ipc.Handle = 0; /// Parse `/system/configuration/init.csv` into `services`, in file order (startup order; shutdown is /// the reverse). Each row is a binary path followed by its argv, comma-separated; /// `#` comments and blank lines are ignored. The file lives in the initial ramdisk, /// which the kernel serves directly, so init — PID 1, running before any filesystem /// service — reads it with a plain fs.open, the same mechanism the device manager /// uses for /system/configuration/devices.csv. A missing file means no services (the no-ramdisk /// isolation test): loud, but not fatal. fn loadServices() void { const used = readConfiguration("/system/configuration/init.csv", &init_csv) orelse { _ = logging.write("/system/services/init: /system/configuration/init.csv missing — no services started\n"); return; }; var lines = std.mem.splitScalar(u8, init_csv[0..used], '\n'); while (lines.next()) |line| { const body = csv.stripComment(line); if (body.len == 0) continue; if (service_count >= services.len) { _ = logging.write("/system/services/init: /system/configuration/init.csv has more services than the table holds\n"); break; } var it = csv.fields(body); const path = it.next() orelse continue; if (path.len == 0) continue; var service: Service = .{ .path = path }; while (it.next()) |argument| { if (argument.len == 0) continue; // padding, or a trailing comma if (service.arg_count >= max_service_args) break; service.arg_buffer[service.arg_count] = argument; service.arg_count += 1; } services[service_count] = service; service_count += 1; } } /// Read a whole configuration file into `into`, returning the byte count. Both of /// init's manifests live in the initial ramdisk the kernel serves directly, so this /// works before any filesystem service exists. A file that fills the buffer exactly /// is reported: a manifest silently losing its last rows is a policy change nobody /// asked for, and the symptom (one service refused a name) points nowhere near it. fn readConfiguration(path: []const u8, into: []u8) ?usize { var file = fs.open(path, .{}) orelse return null; defer file.close(); var used: usize = 0; while (used < into.len) { const n = file.read(into[used..]) orelse break; if (n == 0) break; used += n; } if (used == into.len) std.log.info("{s} filled the read buffer — rows past {d} bytes are lost", .{ path, used }); return used; } /// Give up restarting a service after this many crashes — a crash-loop cap, so a service /// that faults immediately on every spawn doesn't respawn forever. const maximum_restarts = 3; // --- the registry: /protocol ------------------------------------------------ /// Longest contract name the namespace admits (`display`, `test/shared-memory`) /// and the most that may be bound at once. Both static, like everything else /// init holds. const maximum_name = 64; const maximum_bindings = 16; const maximum_grants = 64; /// One bound contract: the name, the provider's endpoint (a capability init /// holds and hands to whoever opens the name), and the provenance a diagnostic /// listing answers "who serves this?" with. const Binding = struct { used: bool = false, name: [maximum_name]u8 = undefined, name_len: usize = 0, endpoint: ipc.Handle = 0, task: u32 = 0, binary: [64]u8 = undefined, binary_len: usize = 0, fn nameSlice(self: *const Binding) []const u8 { return self.name[0..self.name_len]; } fn binarySlice(self: *const Binding) []const u8 { return self.binary[0..self.binary_len]; } }; var bindings: [maximum_bindings]Binding = .{Binding{}} ** maximum_bindings; /// What a grant row permits. /// /// - `bind` — claim the name, i.e. provide the contract. /// - `open` — reach the name, i.e. speak the contract to whoever provides it. /// - `supervise` — stand in a third task's supervision chain: a task running this /// binary, under this supervisor, may be the supervising task named by an /// `open` row for this contract. It grants the *delegate* nothing itself. /// /// `supervise` exists because attestation is deliberately one hop deep /// (`supervisorSatisfies`): init vouches only for tasks it or the kernel started. /// The driver tree is deeper than that — the device manager starts the PS/2 bus, /// and the bus starts the keyboard and mouse drivers — so without a way to say /// "this task is an authorized supervisor", a legitimate grandchild would be /// indistinguishable from a laundering deputy. Naming the delegate in the /// manifest is what tells them apart, and it is the same shape as every other /// row: a binary, the supervisor it must have, and the contract it concerns. /// Deliberately `open`-only — a delegate may vouch for what its children may /// *reach*, never for what they may *claim* — so the bind path's attestation is /// exactly what P2 shipped and every refusal it makes still holds. const Permission = enum { bind, open, supervise }; /// One row of `/system/configuration/protocol.csv`. Every field may end in `*`, /// which matches any tail — the subtree scoping the design doc describes, and /// what lets one row grant the whole `/test/` family its `test/...` names. const Grant = struct { binary: []const u8 = "", supervisor: []const u8 = "", permission: Permission = .bind, name: []const u8 = "", }; /// Roomier than init.csv's: this manifest carries a row per provider per spawn /// path, a row per client per contract it reaches, and its own format /// documentation — which is most of the bytes, and is the point of the file. var protocol_csv: [16384]u8 = undefined; var grants: [maximum_grants]Grant = .{Grant{}} ** maximum_grants; var grant_count: usize = 0; /// Parse `/system/configuration/protocol.csv` — the grant manifest. Separate from /// init.csv because every field there after the path is argv, and overloading that /// would be ambiguous; separate *files* also means a grant exists for binaries init /// never spawns (the drivers, which the device manager owns). fn loadGrants() void { const used = readConfiguration("/system/configuration/protocol.csv", &protocol_csv) orelse { _ = logging.write("/system/services/init: /system/configuration/protocol.csv missing — no protocol may be bound\n"); return; }; var lines = std.mem.splitScalar(u8, protocol_csv[0..used], '\n'); while (lines.next()) |line| { const body = csv.stripComment(line); if (body.len == 0) continue; if (grant_count >= grants.len) { _ = logging.write("/system/services/init: /system/configuration/protocol.csv has more rows than the table holds\n"); break; } var it = csv.fields(body); const binary = it.next() orelse continue; const supervisor = it.next() orelse continue; const permission = it.next() orelse continue; const name = it.next() orelse continue; if (binary.len == 0 or supervisor.len == 0 or name.len == 0) continue; const kind: Permission = if (std.mem.eql(u8, permission, "bind")) .bind else if (std.mem.eql(u8, permission, "open")) .open else if (std.mem.eql(u8, permission, "supervise")) .supervise else continue; // an unreadable row grants nothing rather than something wrong grants[grant_count] = .{ .binary = binary, .supervisor = supervisor, .permission = kind, .name = name }; grant_count += 1; } } /// Match a manifest field against a value: exact, or a trailing `*` matching any /// tail. The wildcard is how a subtree is granted whole (`/test/*` for every test /// fixture, `test/*` for every name they may claim). fn matches(pattern: []const u8, value: []const u8) bool { if (pattern.len != 0 and pattern[pattern.len - 1] == '*') { const prefix = pattern[0 .. pattern.len - 1]; return value.len >= prefix.len and std.mem.eql(u8, value[0..prefix.len], prefix); } return std.mem.eql(u8, pattern, value); } /// A snapshot of the kernel's process records — the only identity in the system /// that cannot be forged, because the kernel stamps it at spawn. Refreshed per /// authorization; binds are rare, so the copy costs nothing that matters. var process_table: [64]process.ProcessDescriptor = undefined; var process_count: usize = 0; var process_truncated = false; fn refreshProcessTable() void { const total = process.processes(&process_table); process_count = @min(total, process_table.len); process_truncated = total > process_table.len; } /// Whether task `id` is still alive, as the last snapshot saw it. A snapshot that /// did not fit answers "alive" for anything it did not list: refusing a bind is /// recoverable, stealing a live provider's name is not. fn taskAlive(id: u32) bool { return descriptorOf(id) != null or process_truncated; } fn descriptorOf(id: u32) ?*const process.ProcessDescriptor { for (process_table[0..process_count]) |*descriptor| { if (descriptor.id == id) return descriptor; } return null; } fn nameOf(descriptor: *const process.ProcessDescriptor) []const u8 { const length = @min(@as(usize, descriptor.name_length), descriptor.name.len); return descriptor.name[0..length]; } /// The name a kernel task answers to in a grant row. Kernel tasks carry no /// binary, so the manifest spells the harness's parentage `kernel`. const kernel_supervisor = "kernel"; /// init's own task id, read once at startup. Ids are monotonic and never reused /// (system/kernel/process.zig), so an id comparison is an *identity* test where a /// name comparison is only a resemblance test — the whole basis of the /// attestation below. var own_task: u32 = 0; /// Whether `id` is a process THIS init spawned: a lookup in its own child table, /// which is the one record of "I started that one" nobody else can write. fn spawnedByUs(id: u32) bool { if (id == 0) return false; for (child_ids[0..service_count]) |child| { if (child == id) return true; } return false; } /// Who is asking, attested by the kernel: the caller's binary, and the **task** /// that spawned it — an id, not a name. /// /// A name alone is not identity: `spawn` is ungated, so a hostile process can /// start a granted binary itself and would inherit its grants. Neither is the /// supervisor's *name* enough, and this is the trap the first cut fell into — /// init and the device manager are ordinary bundled binaries, so an attacker /// spawns its own `/system/services/init` and lets that instance spawn /// `/system/services/input`. Both kernel-stamped names then match the grant row /// exactly, and walking to the root of the chain does not help either: the /// laundered chain still roots at the real PID 1. What refuses it is asking /// *which task* the supervisor is, and only accepting one init can vouch for. const Identity = struct { /// The caller's binary path, exactly as the kernel stamped it at spawn. binary: []const u8, /// The supervising task's id. 0 means the kernel spawned the caller, which /// no ring-3 process can arrange: every `system_spawn` stamps the caller as /// the child's supervisor (system/kernel/process.zig `systemSpawn`). supervisor_task: u32, /// The supervising task's kernel-stamped binary — the grant row's supervisor /// column is matched against this, and the refusal log prints it. `kernel` /// when there is no supervising task. supervisor_binary: []const u8, /// Whether init can vouch for how the supervising task came to exist: it is /// this init, a process this init spawned, or a process the KERNEL spawned. /// A supervisor init cannot vouch for satisfies no row, however well its /// name reads — that is the laundering deputy's refusal. supervisor_vouched: bool, }; /// The process a task belongs to. A thread resolves to its leader: threads share /// a binary (a thread's own record is named `thread`), and the supervision link /// that matters is the process's. fn leaderOf(descriptor: *const process.ProcessDescriptor) *const process.ProcessDescriptor { if (descriptor.leader == descriptor.id) return descriptor; return descriptorOf(descriptor.leader) orelse descriptor; } /// Resolve the badge on a request into an identity, one hop up the supervision /// chain in the kernel's records — one hop is enough because the hop is attested /// by id (see `supervisorSatisfies`), and every id in the chain init accepts is /// one init or the kernel created. fn identify(task: u32) ?Identity { const caller = descriptorOf(task) orelse return null; const leader = leaderOf(caller); if (leader.supervisor == 0) return .{ .binary = nameOf(leader), .supervisor_task = 0, .supervisor_binary = kernel_supervisor, .supervisor_vouched = true, // the kernel is the root of trust, not a claimant }; // The supervising *task* may be a worker thread of the supervising process; // its process is what the manifest names and what init recorded at spawn. const supervisor = leaderOf(descriptorOf(leader.supervisor) orelse return null); // unattestable: refuse return .{ .binary = nameOf(leader), .supervisor_task = supervisor.id, .supervisor_binary = nameOf(supervisor), .supervisor_vouched = supervisor.id == own_task or spawnedByUs(supervisor.id) or supervisor.supervisor == 0, }; } /// Whether the caller's supervising task satisfies a grant row's supervisor /// column. The column names *the authorized supervising task*, matched by /// identity — the binary it must be, plus proof that this instance of that /// binary is the authorized one: /// /// - `kernel` is satisfied only by a genuinely kernel-spawned caller /// (supervisor id 0). A ring-3 process cannot manufacture that: user /// `system_spawn` always stamps the caller (system/kernel/process.zig). /// - init's own binary is satisfied only when the supervising task IS this /// init (`own_task`). /// - any other binary — the device manager, a test fixture spawning another — /// is satisfied only when the supervising task is one init spawned itself /// (its own child table) or one the kernel spawned. Everything init and the /// kernel start is therefore reachable; a chain that passes through a /// process *neither* of them started is not. fn supervisorSatisfies(column: []const u8, identity: Identity) bool { if (std.mem.eql(u8, column, kernel_supervisor)) return identity.supervisor_task == 0; if (identity.supervisor_task == 0) return false; // a kernel task answers to no binary column if (!matches(column, identity.supervisor_binary)) return false; return identity.supervisor_vouched; } /// Whether `identity` is granted `permission` on `name`. fn granted(identity: Identity, permission: Permission, name: []const u8) bool { for (grants[0..grant_count]) |grant| { if (grant.permission != permission) continue; if (!matches(grant.binary, identity.binary)) continue; if (!supervisorSatisfies(grant.supervisor, identity)) continue; if (!matches(grant.name, name)) continue; return true; } return false; } /// Whether `identity` may reach `name` — `granted(.open, …)`, plus the one hop /// `open` takes that `bind` does not (`Permission.supervise`). /// /// The hop is needed because the driver tree is three deep and attestation is /// one: the PS/2 keyboard driver's supervising task is the PS/2 bus driver, /// which the device manager started, which init started. Init cannot vouch for /// the bus by acquaintance — it never met it — so the manifest says so instead, /// and says it per contract: `ps2-bus` may be the supervisor named in an `open` /// grant for `ps2-bus` and for `input`, and for nothing else. fn mayOpen(identity: Identity, name: []const u8) bool { if (granted(identity, .open, name)) return true; return delegatedOpen(identity, name); } /// The delegated `open`: the row's supervisor column names the caller's actual /// supervising task by binary, that task is one init cannot vouch for directly, /// and a `supervise` row authorizes it for exactly this contract. /// /// The delegate itself is attested the ordinary way (`granted` → strict /// `supervisorSatisfies`), so the chain is still anchored one hop above it in /// init or the kernel and the recursion stops there. Two hops of manifest, never /// an unbounded walk — a laundering deputy is refused at the first hop nobody /// wrote a row for. fn delegatedOpen(identity: Identity, name: []const u8) bool { if (identity.supervisor_task == 0) return false; // a kernel-spawned caller needs no delegate if (identity.supervisor_vouched) return false; // already answered by `granted` above const delegate = identify(identity.supervisor_task) orelse return false; if (!granted(delegate, .supervise, name)) return false; for (grants[0..grant_count]) |grant| { if (grant.permission != .open) continue; if (!matches(grant.binary, identity.binary)) continue; if (!matches(grant.supervisor, identity.supervisor_binary)) continue; if (!matches(grant.name, name)) continue; return true; } return false; } fn findBinding(name: []const u8) ?*Binding { for (&bindings) |*binding| { if (binding.used and std.mem.eql(u8, binding.nameSlice(), name)) return binding; } return null; } /// Release a binding: the provider's endpoint capability goes back to the handle /// table, and the name is free for the next claimant. init's own cached power /// channel goes with it — a closed handle number is reused by the next capability /// that arrives, and a stale copy would quietly aim the shutdown call at a /// stranger. So does the authorized power *task*: nothing may speak for a /// contract nobody holds. fn releaseBinding(binding: *Binding) void { if (std.mem.eql(u8, binding.nameSlice(), power_contract)) { power_endpoint = null; power_task = null; power_pending = false; } _ = ipc.close(binding.endpoint); binding.* = .{}; } /// Drop every name a dead process held. Called when a supervised child dies (so /// the restarted instance can bind again) and whenever a bind finds the current /// owner gone — providers init does not supervise need the second path. fn unbindTask(task: u32) void { for (&bindings) |*binding| { if (binding.used and binding.task == task) { std.log.info("/protocol/{s} released ({s} is gone)", .{ binding.nameSlice(), binding.binarySlice() }); releaseBinding(binding); } } } /// A contract name as the namespace spells it: the mount-relative path a resolve /// hands us ("/display") and the name a bind sends ("display") are the same thing /// with and without a leading slash, so one normaliser serves both. Empty or /// longer than the namespace admits is not a name. fn contractName(raw: []const u8) ?[]const u8 { const name = if (raw.len != 0 and raw[0] == '/') raw[1..] else raw; if (name.len == 0 or name.len > maximum_name) return null; return name; } /// The provider's endpoint that will ride the *next* reply, when the request was /// an `open` that found its contract. var pending_capability: ?ipc.Handle = null; /// The ownership rule for a capability that arrives with a turn of the loop — /// **the turn owns it until a handler takes it, and closes whatever is left** — /// lives in `ipc.Arrival`, next to `replyWait`, because it is not PID 1's rule: /// the service harness every other service runs (library/kernel/service.zig) had /// the identical hole and now states the identical contract. const Arrival = ipc.Arrival; /// The one contract init is itself a client of. It never resolves the name — it /// *is* the registry, so it reads its own table; the binding is what hands it the /// channel. const power_contract = "power"; /// Set when `power` is bound: init subscribes to it on the next turn of the loop, /// never inside the bind — the provider is blocked on our reply until then, so /// calling it here would deadlock the pair. var power_pending = false; var power_endpoint: ?ipc.Handle = null; /// The one task authorized to deliver power events: whoever holds the `power` /// binding. Recorded at the bind and cleared with the binding, so a provider that /// dies and rebinds re-derives it with no further ceremony. /// /// This is the *authentication* for the shutdown path. init's registry endpoint /// is its supervision endpoint, and `fs_resolve("/protocol")` installs a sendable /// handle to it in any caller's table — so after P2 every ring-3 process can post /// into PID 1's mailbox. A power event may therefore never be believed on the /// strength of its payload; it is believed because the kernel stamped the /// sender's task id on it and that id is the provider's. var power_task: ?u32 = null; /// Whether the heartbeat's re-arming timer is running. A timer landing carries no /// identity, so the loop cannot tell one timer from another — which means exactly /// one may ever be in flight, or every landing re-arms and the beat doubles. (It /// did: two beats a second is enough extra chatter to cut a driver's echoed line /// in half on the shared serial stream.) So the deferred power subscribe borrows /// the heartbeat's tick when there is one, and arms its own only when there is not. var heartbeat_running = false; /// Answer one registry request. Writes a vfs-protocol reply into `reply` and /// returns its length; a capability the reply must carry lands in /// `pending_capability`. `arrived` is the capability the *request* carried, owned /// by the turn — nothing here has to close it, only `bind` has to claim it. fn serveRegistry(request_bytes: []const u8, reply: []u8, sender: u32, arrived: *Arrival) usize { if (request_bytes.len < envelope.prefix_size) return answer(reply, -envelope.EPROTO, 0); // The header is read field by field rather than reinterpreted whole, and the // verb is compared as a number rather than decoded into the generated // `Operation`: the bytes come from anyone at all, so a value outside the enum // must be a refusal, never an `@enumFromInt`. This is deliberately NOT // `Protocol.Provider.dispatch` for the same reason — PID 1 reads a stranger's // packet, and it reads it by hand. const operation = std.mem.readInt(u32, request_bytes[0..4], .little); const body = request_bytes[envelope.prefix_size..]; if (operation == @intFromEnum(vfs_protocol.Operation.bind)) return answer(reply, onBind(sender, body, arrived), 0); // Only `bind` claims a capability; one attached to anything else is closed by // the turn's `defer` in the loop, along with the ones sent to a request that // was too short to name a verb at all. if (operation == @intFromEnum(vfs_protocol.Operation.open)) { // `open`'s fixed part is the flags word, which means nothing to a // namespace; the name follows it as the packet's tail. if (body.len < @sizeOf(vfs_protocol.Open)) return answer(reply, -envelope.EPROTO, 0); return onOpen(reply, sender, body[@sizeOf(vfs_protocol.Open)..]); } if (operation == @intFromEnum(vfs_protocol.Operation.readdir)) { if (body.len < @sizeOf(vfs_protocol.Readdir)) return answer(reply, -envelope.EPROTO, 0); return onReaddir(reply, std.mem.readInt(u64, body[0..8], .little)); } // Everything else a filesystem answers is meaningless here: `/protocol` holds // contracts, not bytes. return answer(reply, -envelope.ENOSYS, 0); } /// Lay down the envelope's reply prefix (and say how many payload bytes the /// caller has already written after it). fn answer(reply: []u8, status: i32, payload_len: usize) usize { const header = envelope.Status{ .status = status, .len = @intCast(payload_len) }; @memcpy(reply[0..envelope.prefix_size], std.mem.asBytes(&header)); return envelope.prefix_size + payload_len; } /// `bind(name, capability = the provider's endpoint)`. The capability is the /// point of the call, so a bind without one is malformed. Every refusal below /// simply returns: the endpoint stays the turn's, and the turn closes it — which /// is why there is not one `ipc.close` on the way out of any of the six of them. /// The success path is the only one that says anything about ownership, because /// it is the only one that keeps the capability. fn onBind(sender: u32, raw_name: []const u8, arrived: *Arrival) i32 { if (arrived.peek() == null) return -envelope.EPROTO; const name = contractName(raw_name) orelse return -envelope.ENOENT; refreshProcessTable(); const identity = identify(sender) orelse return -envelope.EPERM; if (!granted(identity, .bind, name)) { std.log.info("refused bind of /protocol/{s} by {s} (pid {d}, supervisor {s} pid {d})", .{ name, identity.binary, sender, identity.supervisor_binary, identity.supervisor_task, }); return -envelope.EPERM; } if (findBinding(name)) |existing| { // Collision is an error — never last-writer-wins — unless the incumbent // is dead, which is how a restarted provider retakes its own name. if (taskAlive(existing.task)) { std.log.info("refused bind of /protocol/{s}: held by {s} (pid {d})", .{ name, existing.binarySlice(), existing.task }); return -envelope.EBUSY; } releaseBinding(existing); } const slot = for (&bindings) |*binding| { if (!binding.used) break binding; } else return -envelope.ENOSPC; // Claimed: the binding owns the endpoint from here, and `releaseBinding` is // what closes it. const endpoint = arrived.take().?; slot.* = .{ .used = true, .endpoint = endpoint, .task = sender }; @memcpy(slot.name[0..name.len], name); slot.name_len = name.len; const binary_len = @min(identity.binary.len, slot.binary.len); @memcpy(slot.binary[0..binary_len], identity.binary[0..binary_len]); slot.binary_len = binary_len; // Provenance, at the moment it becomes true: name -> pid -> binary path. std.log.info("/protocol/{s} -> pid {d} {s}", .{ name, sender, slot.binarySlice() }); if (std.mem.eql(u8, name, power_contract)) { power_endpoint = endpoint; // The bind is also the authentication: whoever holds `power` is the one // task whose power events init will act on (see `onPowerEvent`). power_task = sender; power_pending = true; // Wake ourselves once the reply has gone out; the subscribe call cannot // happen while the power service is still blocked on it. The heartbeat's // tick is that wake when it is running — see `heartbeat_running`. if (!heartbeat_running) _ = time.timerOnce(supervision_endpoint, 1); } return 0; } /// `open(name)` -> the provider's endpoint, delivered as the reply's capability. /// /// **A refusal and an absence are the same answer, and that is the whole point.** /// The namespace is the restriction (docs/os-development/protocol-namespace.md): /// what a process may open is what exists for it, so "you may not have this" and /// "there is no such thing" collapse into one reply — `-ENOENT`, no payload, no /// capability. A caller therefore has no oracle: it cannot use `open` to learn /// that a contract it lacks is bound, and — the reason this matters beyond /// tidiness — stage two's supervisor can refuse, stall for a human, or substitute /// a fake without the child being able to tell which happened. /// /// Indistinguishable is a claim about *work done*, not only about the bytes, so /// both questions are asked on every open whatever the first one answers: the /// process table is refreshed, the caller identified, the grants scanned and the /// bindings scanned, and only then is the single verdict formed. Nothing here /// logs, either — `klog_read` is ungated (system/kernel/process.zig), so a line /// written on one branch is a line the refused caller can read, and a serial line /// costs milliseconds it could time. The operator's diagnosis is the pair the /// namespace already publishes on purpose: `readdir` over `/protocol` says what is /// bound, `/system/configuration/protocol.csv` says who may reach it, and the /// client's own retry loop says which one it wanted. /// /// (Not constant-time in the cryptographic sense, and not claimed to be: the two /// scans stop at the row they match, and the optimiser is free to sink a pure /// table walk past a branch that discards it. What is removed is the difference a /// caller could actually measure or read — a syscall on one branch and not the /// other, a line in a world-readable log ring, or a serial write costing /// milliseconds.) fn onOpen(reply: []u8, sender: u32, raw_name: []const u8) usize { const name = contractName(raw_name) orelse return answer(reply, -envelope.ENOENT, 0); refreshProcessTable(); const identity = identify(sender); const permitted = if (identity) |who| mayOpen(who, name) else false; const binding = findBinding(name); if (!permitted) return answer(reply, -envelope.ENOENT, 0); const found = binding orelse return answer(reply, -envelope.ENOENT, 0); pending_capability = found.endpoint; // A contract node has no node id — the capability is the whole answer — but // the protocol says an `open` reply carries one, so it carries a zero. const opened = vfs_protocol.Opened{ .node = 0 }; @memcpy(reply[envelope.prefix_size..][0..@sizeOf(vfs_protocol.Opened)], std.mem.asBytes(&opened)); return answer(reply, 0, @sizeOf(vfs_protocol.Opened)); } /// `readdir(cursor)` — the namespace, browsable. One entry per turn, as the vfs /// protocol lists any directory: kind `protocol`, the contract's name, and the /// provider's task id in `size`, so a plain listing answers "who serves this?". fn onReaddir(reply: []u8, cursor: u64) usize { var index: u64 = 0; for (&bindings) |*binding| { if (!binding.used) continue; if (index != cursor) { index += 1; continue; } const name = binding.nameSlice(); return writeEntry(reply, .{ .kind = @intFromEnum(vfs_protocol.NodeKind.protocol), .name_len = @intCast(name.len), .size = binding.task, }, name); } // End of directory, which the envelope spells as an entry with no name: the // reply's own length cannot say it any more, because the fixed reply part // always travels. return writeEntry(reply, .{}, &.{}); } /// One `readdir` reply: the entry, then its name inline. fn writeEntry(reply: []u8, entry: vfs_protocol.DirectoryEntry, name: []const u8) usize { const total = vfs_protocol.directory_entry_size + name.len; if (envelope.prefix_size + total > reply.len) return answer(reply, -envelope.EPROTO, 0); @memcpy(reply[envelope.prefix_size..][0..vfs_protocol.directory_entry_size], std.mem.asBytes(&entry)); @memcpy(reply[envelope.prefix_size + vfs_protocol.directory_entry_size ..][0..name.len], name); return answer(reply, 0, total); } pub fn main(startup: process.Init) void { // `registry` is the scenario mode: serve /protocol and nothing else. The // kernel test harness spawns its own providers directly, so it wants the // naming layer up without init's whole service list underneath it // (docs/security-track-plan.md, decision 9). const registry_only = if (startup.arguments.get(1)) |role| std.mem.eql(u8, role, "registry") else false; // Prove the heap end to end: allocate through the runtime allocator (which // mmaps pages from the kernel and carves them with the free list), write into // that heap buffer (exercising the widened debug_write bounds check), and // free it. A fault here would kill init before it heartbeats — so the init // test doubles as the heap regression test. (C code links the same heap via // the extern malloc/free symbols; Zig code uses this allocator.) const gpa = memory.allocator(); if (gpa.alloc(u8, 64)) |buffer| { const message = "/system/services/init: heap ok\n"; @memcpy(buffer[0..message.len], message); _ = logging.write(buffer[0..message.len]); gpa.free(buffer); } else |_| {} // One endpoint carries everything init waits on: children's exit // notifications (they are spawned supervised against it), init's own // signals, power events it subscribes to, and — since PID 1 is the registrar // — every /protocol request. One thread can wait in one place, so they share // a mailbox and the loop below tells them apart. supervision_endpoint = ipc.createIpcEndpoint() orelse { _ = logging.write("/system/services/init: no endpoint\n"); return; }; _ = process.bindSignals(supervision_endpoint); // Our own id, before anything can ask us a question. It is half of the // registrar's authority: a grant row naming init as the supervisor is // satisfied by *this* task and no other instance of this binary // (`supervisorSatisfies`). own_task = process.taskId(); // The namespace goes up BEFORE anything is spawned, so a service's first // bind lands rather than retrying. The kernel reserves the prefix: this // mount is the only one it will ever hold. loadGrants(); if (!fs.mount("/protocol", supervision_endpoint)) { _ = logging.write("/system/services/init: /protocol already mounted — not the registrar\n"); } // Load the service list, then bring each up supervised so init can stop them // cleanly. Best-effort and silent: each service announces its own readiness, // and with no /system/configuration/init.csv (an isolation test) the loop starts nothing. if (!registry_only) { loadServices(); for (services[0..service_count], 0..) |*service, i| { if (process.spawnSupervised(service.path, service.arguments(), supervision_endpoint)) |id| child_ids[i] = id; } } // A re-arming timer drives the liveness heartbeat — proof PID 1 is alive (the // init test's marker) and a -Dserial diagnostic. It is a serial/test-build-only // concern: a flashable (serial-off) image runs a purely event-driven PID 1 that // wakes only for real work (signals, power events, children's exits), never for a // periodic beat. `build_options.serial` is comptime, so the heartbeat — its timer // and the handler below — folds away entirely when serial is off. heartbeat_running = build_options.serial and !registry_only; if (heartbeat_running) _ = time.timerOnce(supervision_endpoint, 1000); var receive: [vfs_protocol.message_maximum]u8 = undefined; var reply_buffer: [vfs_protocol.message_maximum]u8 = undefined; var reply_len: usize = 0; var reply_capability: ?ipc.Handle = null; while (true) { const got = ipc.replyWait(supervision_endpoint, reply_buffer[0..reply_len], &receive, reply_capability); reply_len = 0; // nothing owed until this turn's request says otherwise reply_capability = null; // Whatever capability came with this turn is the turn's, and the turn // closes it unless a handler claims it. Structural on purpose — see // `Arrival`; it is what keeps a zero-length call from spending a handle // slot of PID 1's per call. var arrived: Arrival = .{ .handle = got.cap }; defer arrived.release(); if (got.isNotification()) { // Every badge on this branch is stamped by the KERNEL, and a stranger // cannot stamp one: `ipc_send` — the only way a ring-3 process puts // something in this mailbox with no reply owed — sets exactly // `notify_badge_bit | notify_message_bit` and fills the low bits with // the sender's own task id (system/kernel/ipc-synchronous.zig, // `sendLocked`). // // That is a statement about `ipc_send`, and on its own it proved far // too little: an attacker does not use `ipc_send` to forge a signal, // it asks the kernel to deliver a real one *here*. `fs_resolve` // hands any process a sendable handle to this endpoint, and // `signal_bind`/`timer_bind`/`process_subscribe`/spawn's exit // endpoint all used to accept any handle the caller held — so a // stranger could point its own signal delivery at PID 1 and signal // itself, and the terminate badge landing here was genuine in every // bit. What makes these branches trustworthy is therefore in the // KERNEL, not in this comment: binding a kernel notification to an // endpoint now requires *owning* that endpoint (`ipc.ownedBy`), so a // signal here comes only from our supervisor or our own group, a // timer landing only from a timer we armed, and a child-exit notice // only from a child we spawned. The buffered-message branch below is // the one still carrying a stranger's bytes, and it is the one that // authenticates its sender. if (process.signalsFrom(got.badge)) |signals| { if (signals.has(.terminate)) shutDown(); continue; } if (got.isTimer()) { // The pending power subscription rides any timer landing: by the // time one arrives, the bind's reply has left and the power // service is serving again. if (power_pending) { power_pending = false; subscribePower(); } if (heartbeat_running) { _ = logging.write("/system/services/init: heartbeat\n"); _ = time.timerOnce(supervision_endpoint, 1000); } continue; } if (got.isMessage()) { // A buffered message: the only thing here an anonymous stranger // can put in front of PID 1. Authenticated by sender, never by // payload — see `onPowerEvent`. onPowerEvent(got.senderTaskId(), receive[0..got.len]); continue; } if (got.isChildExit()) { restartChild(got.childProcessId()); continue; } continue; // anything else: keep waiting } // The universal ping, answered by the empty reply. A ping may still carry // a capability — the kernel installs one regardless of length — and this // `continue` disposes of it through the turn's `defer`, which is exactly // what it failed to do when the close lived in the branches. if (got.len == 0) continue; reply_len = serveRegistry(receive[0..got.len], &reply_buffer, got.senderTaskId(), &arrived); reply_capability = pending_capability; pending_capability = null; } } /// A buffered message claiming to be a power event. /// /// **Privileged control traffic is authenticated by sender, never by content.** /// init's registry endpoint is its supervision endpoint, and `fs_resolve` installs /// a sendable handle to any mount's backend in *any* caller's table /// (system/kernel/process.zig), so after P2 every ring-3 process holds a handle it /// can `ipc_send` into. Two payload bytes were once enough to reach `shutDown()` /// from here — which stops every service and parks PID 1 in its final sleep, /// destroying the registry for the rest of the boot, and does it for any process /// that cares to ask. /// /// The sender's task id is the fix, because it is not the sender's to choose: the /// kernel stamps it into the badge's low bits as it copies the message into the /// ring. Init is the registry, so it knows exactly which task holds `power`, and /// that task alone is believed. A provider that dies and rebinds moves the /// authorization with the binding; a name nothing holds authorizes nobody. Task /// ids are never reused, so even a dead provider's id cannot be inherited. /// /// (One task, not one process: the ACPI service is single-threaded and publishes /// from the same task that bound the name. A threaded provider would want its /// leader compared instead — which is a change to make when one appears, not a /// looser rule to leave lying around for it.) fn onPowerEvent(sender: u32, payload: []const u8) void { const authorized = power_task orelse { std.log.info("ignored a power event from pid {d}: nothing holds /protocol/power", .{sender}); return; }; if (sender != authorized) { std.log.info("ignored a power event from pid {d}: /protocol/power is pid {d}", .{ sender, authorized }); return; } if (payload.len < 2) return; if (payload[0] != @intFromEnum(power_protocol.Operation.event)) return; if (payload[1] == @intFromEnum(power_protocol.Event.power_button)) shutDown(); } /// A supervised boot service died. Find which one and restart it — unless it exited /// cleanly (it chose to stop, e.g. a driver with no hardware) or has hit the crash-loop /// cap. Reclaiming the dead process is already the kernel's job (docs/process-lifecycle.md /// iron rule 1); init only decides whether to bring it back. fn restartChild(id: u32) void { // Whatever it served, it serves no longer: the name goes back before the // replacement asks for it, so the restarted instance binds rather than // colliding with its own corpse. unbindTask(id); if (shutting_down) return; // deaths during the stop sequence are expected, not crashes for (services[0..service_count], 0..) |*service, i| { if (child_ids[i] != id) continue; child_ids[i] = 0; // An unknown reason (the record aged out) is treated as a crash worth restarting. const reason = process.exitReason(id) orelse .fault; if (reason == .exited) { std.log.info("{s} exited cleanly; not restarting", .{service.path}); return; } restart_counts[i] += 1; if (restart_counts[i] > maximum_restarts) { std.log.info("{s} keeps crashing; giving up after {d} restarts", .{ service.path, maximum_restarts }); return; } std.log.info("{s} died ({s}); restarting ({d}/{d})", .{ service.path, @tagName(reason), restart_counts[i], maximum_restarts }); if (process.spawnSupervised(service.path, service.arguments(), supervision_endpoint)) |new_id| child_ids[i] = new_id; return; } // An untracked child (e.g. the log-flush one-shot): nothing to restart. } /// Subscribe our endpoint (handed over as the call's capability) to the power /// service, so events arrive as buffered messages here. init is the registry, so /// it never resolves `/protocol/power` — it reads its own table, which is also /// what makes this reachable at all: the subscription is armed by the bind that /// put the endpoint there. /// /// **This is the only place PID 1 blocks on another process, and it is the one /// hazard the registrar has.** One thread serves both the namespace and this /// call, so while it is outstanding init answers nobody: if the callee were /// itself blocked asking init to resolve a name, the pair would never move. Two /// things keep that from happening — the call is deferred to the next turn of /// the loop (so the provider has its bind reply and is on its way to /// `replyWait`), and the power provider resolves every name it needs *before* it /// binds (system/services/acpi/acpi.zig, `manager_channel`). Any future service /// init calls owes the same discipline. fn subscribePower() void { const handle = power_endpoint orelse return; const request = power_protocol.Subscribe{}; var reply: [power_protocol.message_maximum]u8 = undefined; _ = ipc.callCap(handle, std.mem.asBytes(&request), &reply, supervision_endpoint) catch {}; } /// The stop sequence: persist the log while storage is still up, then terminate /// each child in reverse spawn order (vfs last — other services may flush through /// it), waiting up to a deadline for each to exit before killing it, then ask the /// power service to enter S5. fn shutDown() void { shutting_down = true; // the stop loop below kills children — those deaths aren't crashes _ = logging.write("/system/services/init: shutting down\n"); // Log persistence is the logger service's job: it is the LAST boot service, // so the reverse-order stop below terminates it first and its final drain // runs while the whole storage chain is still alive. var i = service_count; while (i > 0) { i -= 1; if (child_ids[i] != 0) process.stop(child_ids[i], 2000, supervision_endpoint); } if (power_endpoint) |h| { const request = power_protocol.Shutdown{}; var reply: [power_protocol.message_maximum]u8 = undefined; _ = ipc.call(h, std.mem.asBytes(&request), &reply) catch {}; } // If S5 did not take, init has nothing left to do but idle. while (true) time.sleepMillis(1000); }