Track: the kernel mitigates what Linux mitigates on the T14, and matches its hardening defaults - #594
Conversation
… the T14 Owner ruling: ToyOS needs to be at least as secure as Linux on the same hardware. Today the kernel carries no Spectre/MDS/GDS/ITS mitigation, no KASLR or user ASLR, and no compiler stack protector, verified by grep against main. Stages S0-S9 the orchestrator commissioned, with S7 (microcode loading) parked for the owner because it is the one case CLAUDE.md's vendor-firmware rule does not carve out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review, round 1, at 4f790a4CI: BLOCKER
NOTE
REMOVE
SEND BACK |
…logic The review's blockers, answered against torvalds/linux v6.16 and QEMU v11.1.1 source: - The decision is a pure host-tested function carrying v6.16's cpu_vuln_whitelist/blacklist, intel-ucode-defs.h and bugs.c selection, fed S0's T14 facts and the TCG model's; its expected output is the captured Linux lines. QEMU exercises only the absent branches and the software sequences the TCG model selects (CPUID_7_0_EDX_KERNEL_FEATURES is 0 in system mode). - CPUID.(7,0):EDX bit 29 is ARCH_CAPABILITIES and 31 SSBD; 0x10A is read only when bit 29 is set. - GDS clears GDS_MITG_DIS and never writes GDS_MITG_LOCKED (update_gds_msr). - IBPB is conditional per-process opt-in (switch_mm_cond_ibpb), decided from the per-CPU last-user state, host-tested on A->idle->B and A->idle->A. - BHB clearing is gated on BHI_CTRL (CPUID.(7,2):EDX[4]), not eIBRS. - Spectre v1 names the usercopy barrier and pointer masking in user_ptr's window/object; SMAP becomes required; the lint and the nonexistent fuzzing suite are gone. - ITS thunks are asserted to end their branch at addr & 63 >= 32. - ASLR covers image, stack and mmap bases with entropy stated against mmap_rnd_bits and a per-bit frequency test that reds on BASE + n*2MiB. - The stack protector defines and seeds __stack_chk_guard; a zero guard reds kernel_stack_canary; the per-task canary is a recorded gap. - The exit cites only main; kernel KASLR (S10) and kernel IBT (S11) are added, and S8-S11 sit inside the exit. - Probes use boot-actuators and the syscall_entry byte gate; the one ABI change is a flag in SpawnArgs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review, round 2, at c0cce22CI: Round-1 blockers
BLOCKER
NOTE
REMOVE
SEND BACK |
… names what breaks it The oracle moves from torvalds/linux v6.16 to Ubuntu's 6.8.0-142-generic, the kernel the T14 boots (59c096b on wt/toyos-llvmbar). Its source is tag Ubuntu-6.8.0-142.142 of the noble kernel tree: `git ls-remote` lists it as the only tag with ABI 142 (tag object e230fb4a, commit 53e5d07a), and its debian.master/changelog heads with 6.8.0-142.142. Its config is /boot/config-6.8.0-142-generic from linux-modules-6.8.0-142-generic_6.8.0-142.142_amd64.deb (package sha256 ee4feb47..., config sha256 3b8533dd...). Every Linux path:line was re-read at that tag. What the re-pin changed: - The config leaves CONFIG_X86_KERNEL_IBT unset, so kernel IBT is not on the scoreboard and S11 is deleted. - CONFIG_SLS=y is the one CPU_MITIGATIONS menu entry no vulnerabilities line reports; S5 now owes int3 after every ret and indirect jmp. The rustc fork has no option for it. - CONFIG_ARCH_MMAP_RND_BITS=32 is S8's figure. - NO_MMIO is read at this tag (common.c:1495-1499). - The microcode table S1 carries is intel.c's spectre_bad_microcodes; intel-ucode-defs.h does not exist at this tag. - X86_USER_SHADOW_STACK=y. S0 measures whether Ubuntu's processes use it. S0 names its exact commands, verifies the kernel, package and config hashes, and ends only once the capture is committed, before Ubuntu is wiped. The TCG capture runs on the development host's QEMU, the version .github/qemu-version pins. KVM: the nightly's guest shards run -cpu host, so a QEMU test asserts wiring against S1 over the facts the guest reports. Independence comes from the T14 alone. S1's negative control deletes GDS from the TIGERLAKE_L blacklist row. Each other exit names the mutation that breaks it and the value it breaks: - the T14 prints S0's strings, with a per-line not-worse rule; - the GDS_MITG_DIS arm; - the BHB counter at 1000 per 1000 syscalls; - RSB fills computed from the reported facts; - 2 IBPB writes over the scripted sequence B, C, idle, B, A, idle, A, B; - S4's gadget at <= 16 of 1000 trials per site (false red 7.6e-7), plus a standing no-SMAP boot; - the thunk-body read-back; - S6 inheritance across spawn; - the first spawn's bases and the canary, bit-tested across S10's 64 weekly boots. Binomial tails were computed with awk: 1.01e-7 at n=64 [12,52], 3.31e-7 at n=256 [88,168], and 7.61e-7 for P(X>16) with X ~ Bin(1000, 1/256). S10 is Tier::Weekly and times one boot first. The loader draws both kernel bases, and PHYS_OFFSET becomes one handed-over value. Every entropy draw refuses on None. Filed issues/kernel/the-kernel-stack-canary-is-one-global-not-per-task.md. Removed: "QEMU exercises the absent branches ...", "The QEMU boot prints the TCG model's lines ...", and "the one ABI change is S6's bit". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review, round 3, at 8df7bc1CI: Round-2 blockers
BLOCKER
NOTE
REMOVE
SEND BACK |
…isposition The track now states its rule: it matches every hardening default the pinned Ubuntu config sets, and no more. A table gives each of 27 options exactly one disposition, verified at Ubuntu-6.8.0-142.142 and at this tree: 6 to a stage, 10 to one of seven defects filed here, 4 to a named ToyOS mechanism with file:line, and 7 not applicable with the reason. Filed: - every-syscall-runs-at-one-kernel-stack-offset (RANDOMIZE_KSTACK_OFFSET_DEFAULT) - kernel-functions-return-with-their-used-registers-intact (ZERO_CALL_USED_REGS) - a-threads-kernel-stack-has-no-guard-page (VMAP_STACK) - kernel-text-is-writable-and-every-kernel-page-executable (STRICT_KERNEL_RWX, DEBUG_WX) - the-kernel-heap-has-none-of-slubs-hardening (SLAB_FREELIST_RANDOM, SLAB_FREELIST_HARDENED, RANDOM_KMALLOC_CACHES) - tsx-stays-as-firmware-left-it (X86_INTEL_TSX_MODE_OFF) - a-device-without-a-domain-of-its-own-reaches-all-memory (INTEL_IOMMU_DEFAULT_ON) S5: no compiler option acts on SLS, because thunk-extern and the external retpoline thunks leave compiled code no raw ret or indirect jmp. The int3 is owed in the kernel's own assembly, as Linux's RET does (linkage.h:46-47,58-59), and the mutation is deleting the int3 after one thunk's ret. S3: a kernel.elf gate compares the BHB clear and the RSB fill with clear_bhb_loop (entry_64.S:1534-1569) and __FILL_RETURN_BUFFER with RSB_CLEAR_LOOPS (nospec-branch.h:132,137-162); the IBPB probe counts only writes of PRED_CMD_IBPB. The three mutations the review named are the ones that must go red. S0 builds the ToyOS facts line, captures CPUID 0x80000000, 0x80000008 and 0x80000021 and MSR 0x10F, and says the capture is one-time: only its text outputs are committed. S1 takes the two AMD leaves the TCG model's AuthenticAMD path reads (common.c:1072-1084). S2's base is S1's whole x86_spec_ctrl_base, RRSBA_DIS_S included. S4 names the per-test -cpu override. The citation of an unmerged branch's commit is removed. Linux was read from the source package linux_6.8.0-142.142 (orig tarball plus diff, both matching the .dsc's md5), whose lines agree with the review's reads of the tag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review, round 4, at 53da60cCI: How this round was checked:
Round-3 blockers
BLOCKER
NOTE
REMOVE
SEND BACK |
S3's gate compared loop counts, the fill's call count and the trailing
lfence with Linux's clear_bhb_loop and __FILL_RETURN_BUFFER; that let a
gutted sequence keep every count and stay green. The Exit now decodes
each instruction, opcode, immediate and normalised relative target,
against both functions at the pinned tag, and names three mutations
that must red it: dropping the inner loop's jmp/nop, dropping the
int3 __FILL_RETURN_SLOT places after each fill call, and flattening
the call/RET nesting into direct jmps.
RESET_ATTACK_MITIGATION and X86_USER_SHADOW_STACK now each carry a
filed defect instead of a disposition ("S0") that closed nothing: S0
was only ever going to file a track, and the Exit waits on defects,
not a track filed later. The MOR write is the loader's, before
ExitBootServices, and points at the loader-slimming track it belongs
with.
Fixed on the way: the hardening definition now covers process
protections, not just the kernel; BPF_JIT_ALWAYS_ON, MODULE_SIG and
KEXEC_SIG get not-applicable rows; the per-task-canary defect is
deleted because its own body said the gap it describes does not exist
until S9 lands, and S9 now says the gap is filed then, not before.
Removed, not rewritten: the false claim that Linux's RET/ASM_RET are
int3'd under CONFIG_SLS (the pinned config selects the return-thunk
instead), and the IOMMU defect's claim that another track's refusal is
this defect's fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review, round 5, at 956301fCI: Round-4 blockers
BLOCKER
NOTE
REMOVE
SEND BACK |
…ion, S9's canary is per thread, and a narrower S3 comparison - issues/boot-media/the-loader-never-sets-the-firmwares-memory-overwrite-request.md: the Exit now matches libstub/tpm.c:36-40 — the loader writes MemoryOverwriteRequestControl only where firmware already defines it and ignores the write's result — with a guest test on OVMF (which defines none) asserting no variable is created. - the-kernel-mitigates-what-linux-mitigates-on-the-t14.md: S9's Exit now requires a per-thread canary (a per-CPU %gs slot the scheduler overwrites on every switch, matching switch_to's fixed_percpu_data.stack_canary), with a cross-thread distinctness assertion that a shared global canary must red. S3's instruction comparison excludes clear_bhb_loop's placement-dependent .align padding and CALL_DEPTH_TRACKING's ASM_CREDIT_CALL_DEPTH, each with its reason. The dead x86_Thread_features capture is deleted. - user-programs-run-without-a-shadow-stack.md: the Exit states the behaviour (a process can have a shadow stack of its own) instead of prescribing the spawn-time syscall shape. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review, round 6, at 18af9f8CI: Round-5 blockers
BLOCKER
NOTE
REMOVE
SEND BACK |
…st reads the guard a frame compares, and the MOR test plants the variable S9. The kernel builds for x86_64-unknown-none. On that triple, X86's getIRStackGuard takes the segment slot only for glibc, musl, Fuchsia and Android (X86ISelLoweringCall.cpp:548-551,564 at a79bc52c). Every other triple falls through to the global __stack_chk_guard, and the guard-reg and guard-offset flags do nothing there. The CI LLVM's llc confirms it: with tls/gs/40 module flags under the kernel code model, x86_64-unknown-none-elf emits __stack_chk_guard(%rip) and x86_64-unknown-linux-gnu emits %gs:40. No ToyOS-owned mechanism reaches the slot. S9 is therefore four steps: - an LLVM change that honours an explicit tls guard on any x86 triple, as RISC-V does; - rustc options that set the module flags; - the kernel's slot, which is written only at entry and on context switch; - the guest test. The guest test reads %gs:N from inside a protected frame on two threads. Overwriting a frame's guard with its own thread's value returns. Overwriting it with the other thread's value panics. Deleting the switch-time write reds both. MOR. The guest test plants MemoryOverwriteRequestControl=0 under e20939be-32d4-41be-a150-897f85d49829 and asserts it reads 1 after boot. Deleting the loader's write reds that. The arm that plants nothing stays. Removed: the process_64.c citation, the sysreg/arch/x86/Makefile claim, and the unmeasured claim that OVMF defines no MOR variable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Root CLAUDE.md's dependency rule governs a fork's changes generally, not just target arms: a general cross-platform option written to upstream quality is admitted when ToyOS needs it and upstream lacks it. S9's LLVM guard-slot fix and its rustc stack-protector-guard options are such changes, so they proceed as named steps rather than waiting on a ruling; the stage that lands them amends src/forkcheck.rs's module header to admit exactly this class of change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review, round 8, at d73f123CI: Round-6 blockers
BLOCKERNone. NOTE
REMOVE
LAND |
This PR files a track and nine defects. It changes no production code.
issues/kernel/the-kernel-mitigates-what-linux-mitigates-on-the-t14.mdis akind: track. The T14 is an i5-1135G7 (Tiger Lake, 06_8C). The track makes ToyOS mitigate there what the T14's own Linux mitigates, and match every hardening default that Linux's config sets.issues/kernel/every-syscall-runs-at-one-kernel-stack-offset.mdissues/kernel/kernel-functions-return-with-their-used-registers-intact.mdissues/kernel/a-threads-kernel-stack-has-no-guard-page.mdissues/kernel/kernel-text-is-writable-and-every-kernel-page-executable.mdissues/kernel/the-kernel-heap-has-none-of-slubs-hardening.mdissues/kernel/tsx-stays-as-firmware-left-it.mdissues/kernel/a-device-without-a-domain-of-its-own-reaches-all-memory.mdissues/kernel/user-programs-run-without-a-shadow-stack.mdissues/boot-media/the-loader-never-sets-the-firmwares-memory-overwrite-request.mdThe oracle
The oracle is the kernel the T14 runs, Ubuntu's 6.8.0-142-generic.
Ubuntu-6.8.0-142.142ofgit.launchpad.net/~ubuntu-kernel/ubuntu/+source/linux/+git/noble, commit 53e5d07aac028a1523ab0b115f079d6d1bc831ef (git ls-remote).HEADis 53e5d07a, and from the source packagelinux_6.8.0-142.142(linux_6.8.0.orig.tar.gzpluslinux_6.8.0-142.142.diff.gz, both matching the md5 sums in the archive's.dsc). One diff hunk did not apply, innet/netfilter/xt_RATEEST.c, because macOS folds that name's case; no citation is innet/.rust/src/llvm-projectat a79bc52c1d5e, the gitlinkrust/pins, read withgit show a79bc52c:<path>./boot/config-6.8.0-142-generic, extracted fromlinux-modules-6.8.0-142-generic_6.8.0-142.142_amd64.debon the Ubuntu archive. Its sha256 is 3b8533dd9d235ca634ac58f82c5ce1ee35f12ef620693e17033184d2c9ca5890. The hardening table's numbers are that file's lines.qemu64isCPUID_VENDOR_AMD, family 15 (target/i386/cpu.c:3545-3549).Decisions
*_RESTRICT,STRICT_DEVMEM) belongs to the capability model and is not in the table.IOMMUhas a row because ToyOS binds every function to one identity domain over all memory (vtd/mod.rs:148,456-490), where Linux's default translates every device's DMA.SHUFFLE_PAGE_ALLOCATORis not applicable, because at this tag onlypage_alloc.shuffle=1enables it (mm/shuffle.c:12-30). The Kconfig help's memory-side-cache detection has no caller.CONFIG_X86_KERNEL_IBTunset.int3in the kernel's own assembly.thunk-externand the external retpoline thunks, compiled code keeps no rawretor indirectjmp, so no compiler option acts here.int3after everyretand indirectjmpin the thunks and the entry code.int3after one thunk'sret.s0.cpioand busybox are never committed, and no build or test boots them. Only the captured text outputs are committed.boot-actuatorsfacts line.common.c:1072-1084) and folds their bits into IBRS, IBPB, STIBP and SSBD (common.c:994-1011).x86_spec_ctrl_base,RRSBA_DIS_Sincluded (bugs.c:1736,1800,1934,2224).-cpuoverride.Arch::cpuis one string per accelerator. The no-SMAP test drops+smapunder TCG and useshost,-smapunder KVM.libstub/tpm.c:36-40writesMemoryOverwriteRequestControlonly where the firmware already defines it, and:42-45ignores the write's result. The guest test plants it as 0 undere20939be-32d4-41be-a150-897f85d49829(tpm.c:20-21) withvars::plantand assertsvars::livereads 1 after boot; deleting the loader'sSetVariablereds it. A second boot plants nothing and asserts nothing exists under that GUID afterward; making the write unconditional reds it.vars::plantandvars::livegain the vendor as an argument.x86_64-unknown-none, X86 lowers the guard to the segment slot only whenhasStackGuardSlotTLSholds (X86ISelLoweringCall.cpp:548-551,564); otherwise it falls through (:604,:638-640) to the global__stack_chk_guard.rust/build/aarch64-apple-darwin/ci-llvm/bin/llc(LLVM 22.1.8-rust-1.99.0-nightly, the CI LLVM, not the fork's build) on onesspstrongfunction with the module flagstls,gs, 40 under-code-model=kernel:x86_64-unknown-none-elfemitsmovq __stack_chk_guard(%rip), andx86_64-unknown-linux-gnuemitsmovq %gs:40. Both exit 0.LOAD_STACK_GUARDis 64-bit Mach-O only (X86ISelLowering.cpp:2770-2772), and a glibc or muslllvm-targetfor the kernel is a false environment claim.stack-protector-guard=tlson any triple, as RISC-V does (RISCVISelLowering.cpp:25703-25707). Clang accepts the flag on every x86 triple (Clang.cpp:3479-3491,3561-3570), so the change is an upstream fix. Its test is RUN lines instack-protector-3.ll.-Zstack-protector-guard*options, set as module flags the wayCodeGenModule.cpp:1543-1553sets them. Its test is an assembly test forx86_64-unknown-none.CLAUDE.md's dependency rule governs generally, not just atsrc/forkcheck.rs's existing target-arm dispatch site — "a fork carries a change written to upstream quality and goes when upstream has it" — so a general cross-platform option written to upstream quality is admitted when ToyOS needs it and upstream lacks it. Steps 1 and 2 are named steps of S9 with no more waiting; the stage that lands them amendssrc/forkcheck.rs's module header to admit exactly this class of change, in that stage's own PR.smp: 1. Two threads each read%gs:Ntwice from inside a protectedSYS_DEBUGframe, handing off to each other between the reads. The test asserts that each thread's reads agree and that the two threads' reads differ.%gs:N. Overwriting it with the other thread's read panics as the boot's last event.context_switch's write of the incoming thread's guard makes both threads' reads agree and lets the overflow with the other thread's guard return, so both assertions red.__switch_to_asm(entry_64.S:193-196).Gates
cargo run -- --ci host: EXIT=0 ("Host: 54 step(s), all green"), on the tree committed as d73f123.Unsure
/initin an initramfs, with direct kernel boot on q35 and-nodefaults -serial stdio. The first S0 run shows whether it works.INIT_STACK_ALL_ZEROandINIT_ON_ALLOC(slab) rest on Rust reading no local or allocation before writing it.copy_out'sUserSafeno-padding contract is checked by hand (user_ptr.rs:30). An unsafe read of uninitialised memory is undefined behaviour and is outside what those rows cover.🤖 Generated with Claude Code