Research note: Hubris and Tock¶
Kind: Read-only source analysis, 2026-10-07, performed on shallow clones. File and line citations refer to Hubris commit 446dfcd5019a (oxidecomputer/hubris) and Tock commit 40b9e1378345 (tock/tock, sparse: kernel/, doc/). Line numbers drift; file paths and symbol names are the durable reference. Synthesized in R-001.
Part A: Hubris¶
A1 Task model¶
Fixed task set from
app.toml; no create or destroy at runtime (“you can’t write a fork-bomb without fork”, doc/tasks.adoc:68-97). The build emitskconfig.rs(sys/kern/src/startup.rs:133; schema in build/kconfig/src/lib.rs:9-27, 50-76) withHUBRIS_TASK_DESCS,HUBRIS_TASK_TABLE_SPACE, and perfect-hash IRQ tables (sys/kern/build.rs:200-280).Each task has
REGIONS_PER_TASK = 8region descriptors by reference; unused slots point at a shared no-access region (sys/kern/src/descs.rs:7, 35-42, 78-104). Alignment: Armv6-M and Armv7-M regions must be power-of-two, naturally aligned, at least 32 bytes (descs.rs:94-100; arm_m.rs:400-454 computes RBAR/RASR); Armv8-M needs 32-byte alignment and uses RBAR/RLAR plus MAIR (arm_m.rs:550-650). The MPU is disabled, all eight regions rewritten, and re-enabled on every switch (arm_m.rs:480-521, 598-650; “single-digit cycles”, 613).Priorities are a fixed
u8, 0 highest;Prioritydeliberately has noOrd(descs.rs:9-31); strict priority, no time slicing, cooperative within a level, up to 256 levels (doc/tasks.adoc:19-42).Scheduler:
priority_scanis a linear scan of the whole table fromprevious+1picking the most important runnable task, round-robin among equals (sys/kern/src/task.rs:858-909); the kernel panics if nothing is runnable (864), so an idle task is required (task/idle/src/main.rs:13-30,wfi).States:
TaskState::Healthy(SchedState) | Faulted{fault, original_state};SchedState::{Stopped, Runnable, InSend(TaskId), InReply(TaskId), InRecv(Option<TaskId>)}(sys/abi/src/lib.rs:239-304).TaskId(u16)is a 10-bit index plus a 6-bit generation; the generation bumps on every reinit; stale ids receive a “dead code”0xFFFF_FF00 | new_gen(abi/src/lib.rs:13-85, 216-234; checked in task.rs:838-856).TaskId::KERNEL = !0.
A2 IPC¶
Synchronous send, recv, reply modelled on L4; rationale: single copy, no kernel queues to size, limits fault amplification, callers are known to be parked (doc/ipc.adoc:53-98). Messages are capped at 256 bytes because copies are uninterruptible (ipc.adoc:205-210).
send: if the callee is in a matching recv,delivercopies viasafe_copyand switches directly to the callee (NextTask::Specific); otherwise the caller blocksInSend(sys/kern/src/syscalls.rs:162-210, 691-725).safe_copyvalidates both slices against each task’s region table (umem.rs:346-388;Task::can_accesstask.rs:209-225 forbids DEVICE, usually DMA).Leases: the sender passes a
ULeasetable (base, len, READ or WRITE) (abi/src/lib.rs:184-214); the server reads and writes viaBORROW_READ,BORROW_WRITE,BORROW_INFO;borrow_leasehonours a lease only while the lender isInReply(server), so leases are revoked atomically when the client resumes (syscalls.rs:588-660; ipc.adoc:215-249). Up to 255 leases.replyis fire-and-forget: a wrong or stale target is silently ignored (MINIX 3 rationale, ipc.adoc:375-409; syscalls.rs:367-392); an oversized reply faults the server (ReplyTooBig, 426-430).REPLY_FAULTlets a server fault a misbehaving client withReplyFaultReason::{UndefinedOperation, BadMessageSize, BadMessageContents, BadLeases, ReplyBufferTooSmall, AccessViolation}recorded asFaultInfo::FromServer(abi:424-452; syscalls.rs:829-874).Closed receive:
recvwith a specificTaskIdaccepts only that sender (bit 31 flags the argument, task.rs:502-509; syscalls.rs:262-310); used for mutual exclusion (ipc.adoc:435-449).Notifications: 32 bits per task, OR’d by
post(task.rs:236-271), consumed byrecvwith a mask; delivered as a message fromTaskId::KERNELwith the bits inoperation(ipc.adoc:493-535; syscalls.rs:242-254).postforces a reschedule only when the peer is more important (syscalls.rs:796-820).IRQs:
app.tomlmapsinterrupts = {"usart1.irq" = "usart-irq"}(app/gimletlet/app.toml:71-74); the kernelDefaultHandlerlooks upHUBRIS_IRQ_TASK_LOOKUP, masks the IRQ, and posts the notification (arm_m.rs:1247-1300). Tasks re-enable viairq_controlby notification mask rather than IRQ number, so no ownership check is needed (doc/interrupts.adoc:66-95; syscalls.rs:732-768). Example driver loop: drv/lpc55-spi-server/src/main.rs:70,129.Timers: one per task {deadline: Option
, notification set}; fires once then clears (task.rs:758-769, 812-830; doc/timers.adoc:189-214). The time base is a 64-bit tick count ( Timestamp(u64), time.rs:14-18) incremented by SysTick, which also runsprocess_timersand pends PendSV if anything woke (arm_m.rs:1088-1124). Multiple deadlines are multiplexed in userland (lib/multitimer, timers.adoc:247-255).
A3 Supervision¶
Fault record:
FaultInfo::{MemoryAccess{address,source}, StackOverflow, BusError, DivideByZero, IllegalText, IllegalInstruction, InvalidOperation(cfsr), SyscallUsage(UsageError), Panic, Injected(TaskId), FromServer(..)},FaultSource::{User, Kernel}(abi/src/lib.rs:312-422).force_faultsavesoriginal_stateand posts bit 0 to task 0 (task.rs:928-955;HUBRIS_FAULT_NOTIFICATION = 1, startup.rs:22). Only the task state changes; stack and registers are preserved for the debugger (arm_m.rs:1399-1480 saves r4-r12, lr, FP registers).Supervisor is task index 0 at priority 0; if it faults the system reboots (doc/guide/supervision.adoc:183-205). Jefe loop: on the FAULT notification it iterates
kipc::find_faulted_task, counts faults, optionally dumps the task, then reinits or holds (task/jefe/src/main.rs:450-540). Restart is rate-limited:MIN_RUN_TIME = 50ms andMIN_RESTART_DELAY = 5ms so an error report can be collected first (main.rs:57-80).reinit_task(kernel IPC, kipc.rs:118-180): bump generation, reset timer, notifications, state, re-scribble the stack with 0xbaddcafe, reset registers (task.rs:318-325; arm_m.rs:279-376), then unblock every taskInSend,InReply,InRecv(Some)on the old id with the dead code. The kernel does not re-initialize.data/.bssor MPU config; userlib_startdoes data init (doc/tasks.adoc:143-175). All task RAM state is lost; peers seeDEAD, typically callnext_generation(), and retry (abi:22-26).Hold-for-debug: Humility writes
JEFE_EXTERNAL_REQUEST,TASKINDEX,KICKstatics (Start, Hold, Release, Fault) polled by jefe every 100 ms; the comment explains why this bypasses IPC (priority inversion with the HIF task) (task/jefe/src/external.rs:7-44, 56-97).
A4 Kernel internals¶
No allocator: the kernel Cargo has no
alloc; tables arestatic mutMaybeUninitarrays sized by kconfig (sys/kern/Cargo.toml:6-20; startup.rs:50-96); the task table is guarded by a singleTASK_TABLE_IN_USEflag that panics on re-entry (startup.rs:20-23, 100-130).Syscall ABI:
SVCwith the immediate ignored; arguments in r4-r10, number in r11, returns in r4-r11, because r0-r3 land on the user stack (doc/syscalls.adoc:36-72). Numbers 0-13 inSysnum(abi:454-471); kernel IPC toTaskId::KERNELusesKipcnum1-10 (abi:507-519).SVCallassembly stores r4-r12, lr, and s16-s31 into theTask.savearea, which must be first in the struct (arm_m.rs:983-1045; task.rs:30-35).Interrupt design: SysTick and IRQs do a cheap entry and set PendSV when a switch is needed;
pendsv_entryrunsselect(arm_m.rs:16-70, 1218-1245). Faults go toconfigurable_faultthenhandle_fault, which classifies CFSR bits (MSTKERR as StackOverflow, IACCVIOL, DIVBYZERO), clears pended derived exceptions, and panics on kernel-mode faults (arm_m.rs:1685-1810). Armv8-M sets MSPLIM to the kernel stack base (arm_m.rs:790-792); the STKOF bit is decoded (1559-1560). No PSPLIM use was found; task stack overflow is caught by MPU gaps.Footprint (doc/tr1.adoc:28-47): kernel text 19,848 bytes plus 7,280 bytes rodata (9,824 bytes text without panic messages), 32 bytes static RAM, 408 bytes per TCB plus 32 bytes region tracking.
A5 Observability¶
ringbuf!static ring buffers with de-duplication counters andcounted_ringbuf!per-variant counters, read byhumility ringbufor GDB, not streamed (lib/ringbuf/src/lib.rs:5-100, 166-190, 271-295).counters!stores event counts inAtomicU32s (lib/counters/src/lib.rs:5-60).Debugger-centric: the kernel exports
CURRENT_TASK_PTR,CLOCK_FREQ_KHZ,HUBRIS_TASK_TABLE_SPACEasno_manglestatics (arm_m.rs:101-108; build.rs:425-428); Humility computes task addresses from DWARFsizeof(Task)(profiling.rs:26-45). Jefe exposes its whole server struct asJEFE_SERVER_IMPLfor dumps (task/jefe/src/main.rs:82-85). Task dumps viaGetTaskDumpRegionandReadTaskDumpRegion(kipc.rs:235-300); panic messages kept to 128 bytes (doc/kipc.adoc:451). Kernel profiling hooks toggle GPIOs for logic analyzers (profiling.rs:5-23). Stack scribble lets Humility measure peak stack usage (tasks.adoc:157-161). No kernel UART logging path exists.
A6 Priority inversion¶
Handled by convention, not mechanism: the “uphill send rule” (only send to higher-priority servers; “The kernel will enforce this, eventually”, doc/guide/servers.adoc:24-35; ipc.adoc:100; FAQ.mkdn:32). reply assumes it goes downhill and never reschedules (syscalls.rs:454-457). No OS-level timeouts, by principle (FAQ.mkdn:44-49). Capabilities and IPC filtering are TODOs (intro.adoc:133-138; syscalls.rs:166).
Hubris: ideas to borrow¶
Generation-tagged task ids with kernel-delivered dead codes so restarts are visible to peers (abi/src/lib.rs:13-26; kipc.rs:140-170).
Leases: borrowed memory validated on every access and implicitly revoked by the blocking state machine; no kernel copy of large buffers (syscalls.rs:588-660).
Syscall misuse is a fault, not an error code, and servers can
REPLY_FAULTclients (tasks.adoc:114-117; syscalls.rs:829-874).IRQs named by notification bits in the task’s own namespace; no ownership check needed (interrupts.adoc:75-82).
Build-time generated kernel tables plus perfect-hash IRQ maps; all RAM usage knowable at compile time (build.rs:200-280; tasks.adoc:85-86).
Stack scribble plus debugger-readable statics instead of a logging subsystem (arm_m.rs:300-350; external.rs).
Supervisor policy out of the kernel with restart rate limiting and a fault “mailing list” (jefe main.rs:57-80, 540-548).
Hubris: weaknesses¶
Priority inversion and deadlock avoided only by the unenforced uphill-send convention (servers.adoc:24-35); no inheritance.
Single synchronous send per task; async needs the pingback pattern and polling (ipc.adoc:536-579); the supervisor cannot safely
sendto anyone (jefe main.rs:18-24).One timer per task; 1 ms SysTick polling of all tasks every tick (arm_m.rs:1088-1124).
O(n) scans of the task table for scheduling, receive, and faults (task.rs:882-909; syscalls.rs:326-330; supervision.adoc:224-226).
Eight MPU regions; power-of-two alignment on Armv7-M wastes RAM; separate compilation duplicates library code in flash (tasks.adoc:44-66).
Rust-only toolchain and
app.toml; no runtime loading or partial update by design (intro.adoc:30-32).A supervisor crash reboots the system; IPC filtering and MAC still TODO (supervision.adoc:205; syscalls.rs:166).
Part B: Tock¶
B1 Architecture¶
Single kernel thread; capsules are event-driven Rust components trusted for liveness and correctness but written without
unsafe(the core kernel holds “most unsafe code”, kernel/src/lib.rs:10-11, 30-45; sensitive APIs gated by capability traits, capabilities.rs:5-50). Processes are MPU-isolated and untrusted (platform/mpu.rs:82-88).kernel_loopthenkernel_loop_operation: tickle the watchdog; ifscheduler.do_kernel_work_nowrun bottom halves and deferred calls (execute_kernel_work), elsescheduler.next()leads todo_processorTrySleepwith interrupts disabled (kernel.rs:374-451; scheduler.rs:48-61).do_process: start the scheduler timer with the timeslice; loop: check remaining quantum (MIN_QUANTA_THRESHOLD_US), askcontinue_process, then by state: Running leads tosetup_mpu,enable_app_mpu,arm,switch_to,disarm,disable_app_mpu, then handle Fault, Syscall, or Interrupted; Yielded dequeues aTask(FunctionCall pushes an upcall frame); YieldedFor returns the specific upcall’s arguments without invoking it (kernel.rs:484-750). Kernel time on behalf of the process is charged to its slice (470-474).Scheduler trait:
next() -> RunProcess((pid, Option<timeslice_us>)) | TrySleep,result(reason, time), overridableexecute_kernel_work,do_kernel_work_now,continue_process(scheduler.rs:15-97). Implementations live outside the kernel crate in capsules/system/src/scheduler/{round_robin,priority,cooperative,mlfq}.rs.
B2 System calls¶
Classes: Yield=0, Subscribe=1, Command=2, ReadWriteAllow=3, ReadOnlyAllow=4, Memop=5, Exit=6, UserspaceReadableAllow=7 (syscall.rs:85-95); yield variants NoWait, Wait, WaitFor (121-128). TRD104 ABI: four argument and four return registers r0-r3 or a0-a3, class in the
svcimmediate or a4 (doc/reference/trd104-syscalls.md:93-108); return variants encoded in r0 (207-270).Upcalls are delivered only on yield by pushing a callback frame whose LR returns after the
svc(trd104:308-337; kernel.rs:640-660). The queue per process is aRingBuffer<Task>;QueueFulldrops upcalls and bumps a counter (upcall.rs:60-71, 166-204; process_standard.rs:552, 598-615).Swap semantics: Subscribe returns the previous upcall (Null Upcall 0x0 the first time) and cancels pending invocations of the old one (trd104:555-620; kernel.rs:886-1000 via
grant::subscribe). Allow replaces and returns the previous (address, len), 0/0 the first time (trd104:690-720; kernel.rs:1082-1130grant::allow_rw). The kernel must validate that the upcall pointer is in process executable memory (kernel.rs:937-948).Buffers:
ReadOnlyProcessBufferandReadWriteProcessBuffercarry pointer, length, andProcessId; everyenterre-checks process liveness and yields aReadableProcessSliceofCell-like bytes, so no long-lived references into process memory exist (processbuffer.rs:5-25, 40-60, 239-262, 335-350). TRD104 4.4.1 warns that buffers can change between capsule invocations (trd104:757-790).allow_high_water_markstopsbrkfrom shrinking below allowed buffers (process_standard.rs:500, 982-995).Platform
SyscallFiltercan reject any non-Yield, non-Exit, non-Memop syscall per process (kernel.rs:778-816; platform/platform.rs:120-135). Memop 0-11: brk and sbrk, memory and flash bounds, grant start, writeable flash regions, stack and heap hints (memop.rs:12-45).
B3 Grants¶
A
Grant<T, Upcalls, AllowROs, AllowRWs>is created at boot with a unique id; nothing is allocated until a capsuleenters it for aProcessId, thenTplusGrantKernelData(saved upcalls, allow slots) are carved from the process’s grant region, which grows down from the top of process RAM towardkernel_memory_break(grant.rs:5-128; process_standard.rs:446-500). The grant pointer table lives at the top of process memory (process_standard.rs:1830-1850).Why: no kernel heap; per-process capsule state is accounted to and freed with the process (terminate nulls all grant pointers, process_standard.rs:821-833). Extra memory via
GrantRegionAllocatorandenter_with_allocator(grant.rs:1331-1370).Restrictions: access only inside the closure; re-entering an already entered grant panics (the pointer’s low bit marks “entered”, process_standard.rs:1336-1349; grant.rs:1240-1310 explains the iterate-while-entered bug);
T: Default, fixed size;Grant::enterfails if the process is dead or out of grant memory (NOMEM, trd104:717-721).
B4 Isolation and faults¶
unsafe trait MPUwithMpuConfigcached per process;allocate_region,allocate_app_memory_region(choose a block whose app part can grow up while the kernel part grows down),update_app_memory_region(app_break, kernel_memory_break),configure_mpu;disable_app_mpuexists because ePMP hides user memory from the kernel (platform/mpu.rs:89-303). Alignment is MPU-specific (Region docs 26-28); the Cortex-M implementation (arch/cortex-m/src/mpu.rs) is not in the clone. Layout: a flash region R-X, a RAM region RW with stack, data, and heap growing up toapp_break, grants above (process_standard.rs:446-480; create at 1783-1960,MpuInvalidFlashLength1800-1823).States
Running,Yielded,YieldedFor,Stopped,Faulted,Terminated(process.rs:945-1000). Faults:set_fault_stateasks the per-processProcessFaultPolicyforFaultAction::{Panic, Restart, Stop}(process_policies.rs:18-26; process.rs:1027-1045; process_standard.rs:752-775). Restart means terminate (empty the task queue, null grants, completion code) thenreset, which mints a newProcessIdso stale ids are invalidated, resets debug counters and MPU config, and incrementsrestart_count(789-840, 2379-2515). The platform hookProcessFault::process_fault_hookcan intercept first (kernel.rs:600-610).Loading: processes are TBF objects discovered sequentially in flash (
parse_tbf_header_lengths, padding entries skipped) and turned intoProcessBinarythenProcessStandard::create(process_loading.rs:122-140, 295-360, 501-530; process_binary.rs:5-50 lists KernelVersion and fixed-address checks).SequentialProcessLoaderMachineruns an asynchronousProcessCheckerMachineover TBF footer credentials:AppCredentialsPolicy::{require_credentials, check_credentials}yields Accept, Pass, or Reject, plusAppIdPolicy(uniqueness and ShortId compression) (process_checker.rs:5-35, 56-70, 121-208; TRD trd-appid.md).KERNEL_MAJOR_VERSIONandKERNEL_MINOR_VERSION2.4 are compiled in for compatibility (lib.rs:107-116).
B5 Time¶
Ticks(width, wrapping add and sub,within_range),Frequency(const Hz enums),Time{Frequency,Ticks; now()},ConvertTicks,CounterandOverflowClient,AlarmandAlarmClient(set_alarm(reference, dt)so already-passed times are distinguished from far-future ones, never fires early,minimum_dt),TimerandTimerClient(oneshot and repeating) (hil/time.rs:24-136, 142-160, 236-335, 348-400; TRD105 sections 2-5, doc/reference/trd105-time.md:49-335).Virtualization:
MuxAlarmin capsules::virtual_alarm multiplexes one hardware alarm, picking the next expiry withmin_by_keyonTicks: Ord(trd105:60-72, 361-371); TRD105 section 9 gives theset_alarmpseudocode for past and too-near deadlines (386-423). The capsule is not in the clone (capsules/core/src/virtualizers/virtual_alarm.rs).Process timeouts: the userspace alarm driver (capsules/core/src/alarm.rs, driver 0x0, doc/syscalls/00000_alarm.md) is a syscall capsule over a virtual alarm; expiry becomes an upcall delivered at the next yield. Timeslices use a separate
SchedulerTimer(start,arm,disarm,get_remaining_us), with the kernel polling expiry rather than taking an interrupt itself (platform/scheduler_timer.rs:12-57).
B6 Observability and quality¶
debug!,debug_verbose!,debug_expr!,debug_gpio!write into an internal buffer drained asynchronously via aDebugWriter(UART by default) (debug.rs:5-80, 518, 727-800).CONFIG.trace_syscallslogs every syscall and upcall (config.rs:40-45; kernel.rs:641-650).Panic path: flush the debug buffer, banner, CPU state, disable the app MPU, then
ProcessPrinter::print_overviewfor every process, then blink LEDs forever (debug.rs:161-262, 441; process_printer.rs:5-61).ProcessStandardDebugkeeps the last syscall, syscall count, dropped-upcall count, timeslice expirations, stack minimum pointer (process_standard.rs:124-200).KernelInfointrospection is capability-gated (introspection.rs:5-40). The process console capsule (capsules/core/src/process_console.rs, not in the clone) uses these.Process: TRDs with RFC 2119 keywords and Draft or Final status (doc/reference/trd1-trds.md); TRD104 and TRD105 are normative. Pull-request process, mandatory Safety comments on every
unsafe, per-subsystem review guides (doc/CodeReview.md:242-330, 439-500). Threat model documents are referenced from README.md:133 but are not present in this clone.Testing: a few
#[cfg(test)]units in the kernel (ring_buffer, processbuffer, time, leasable_buffer); CI via Makefileci-job-*(clippy, msrv, compilation, kernel and capsule and chip tests, QEMU) (Makefile:206-330, 365-524); hardware CI with Treadmill (doc/TockHardwareCI.md:1-40); board test directories such as boards/*/src/tests/multi_alarm.rs.
Tock: ideas to borrow¶
Grants: per-process kernel state carved from the process’s own RAM, freed on termination, with enter-closure discipline (grant.rs:5-128).
Swap semantics for subscribe and allow returning the previous value, so userspace regains ownership deterministically (trd104:555-620, 690-720).
ProcessBufferliveness-checked slices and the explicit “buffers can change” rule (processbuffer.rs:20-25; trd104:757-790).Capability traits to gate sensitive kernel APIs at compile time (capabilities.rs).
Pluggable scheduler, fault, syscall-filter, and storage policies via
KernelResources(platform.rs:20-75).TBF loading with credential checking and AppId policy; a kernel version compatibility header (process_checker.rs; process_binary.rs:28-40).
The
set_alarm(reference, dt)API and TRD105 wraparound guidance (hil/time.rs:302-309; trd105:386-423).The TRD specification practice and mandatory Safety comments (trd1-trds.md; CodeReview.md:308-330).
Tock: weaknesses¶
Single kernel thread: a long capsule callback or deferred call delays everything; kernel work always preempts processes, so process latency depends on capsule behaviour (scheduler.rs:48-61; kernel.rs:384-395).
Capsules are trusted for liveness; a buggy capsule can
panic!the whole system (grant.rs:1246-1248), andGrant::enterdouble entry is only caught at runtime.Upcalls only on yield; queue overflow drops upcalls silently except for a counter (upcall.rs:60-71); re-subscribe can lose events (trd104:572-592).
The IPC capsule is acknowledged as unsafe and incomplete (kernel.rs:664-676, issue 1993).
Process loading, checking, and dynamic binary storage add considerable kernel code (process_loading.rs 1,379 lines, process_standard.rs 2,753 lines); no footprint figures in the docs.
The MPU abstraction pushes alignment rules into chip crates; app memory must be a contiguous grow-both-ways block (platform/mpu.rs:201-248).
Timeslice accounting polls
get_remaining_usand charges kernel time to the process; no priority inheritance or deadline support in the base kernel (kernel.rs:520-530, 470-474).Rust-only kernel and capsules; threat-model and design docs now live outside the repo (doc/README.md:4).