diff --git a/docs/coding-standards.md b/docs/coding-standards.md index 935b89a..fe32764 100644 --- a/docs/coding-standards.md +++ b/docs/coding-standards.md @@ -182,6 +182,26 @@ test for "is this an abbreviation I must expand" is simply: *is there a longer w is a clipped form of?* If yes, write the word. If it's an initialism standing in for a phrase, leave it. +## Kernel code touches user memory only through `user-memory` + +A syscall argument is an attacker-controlled integer. Kernel code never +dereferences one: every read of a process's memory goes through +`copyFromUser` and every write through `copyToUser` +(`system/kernel/user-memory.zig`), which walk that address space's page tables +and move the bytes through the physmap, with the permissions ring 3 itself +would face. A bad pointer then fails the call instead of faulting the kernel, +and a struct pulled in once cannot change underneath the checks that follow it. + +This is not a review convention — CR4.SMAP enforces it in hardware +(`docs/os-development/smep-smap.md`), so a raw dereference of a user address is +a #PF with a kernel instruction pointer the first time the QEMU suite reaches +it. Which is also why **there is no `stac` in this tree, and never should be**: +`stac` suspends exactly that enforcement, the copy layer needs no such window +by construction, and a change that adds one has removed the guarantee rather +than worked around a limitation. The same goes for the boot-time `clac` patch +at the interrupt entry — it exists so that ring 3 cannot suspend SMAP either, +by taking an interrupt with `EFLAGS.AC` set. + ## Zen of Zig * Communicate intent precisely. diff --git a/docs/os-development/smep-smap.md b/docs/os-development/smep-smap.md index af0cee7..a71c3a1 100644 --- a/docs/os-development/smep-smap.md +++ b/docs/os-development/smep-smap.md @@ -1,7 +1,7 @@ # SMEP and SMAP — supervisor-mode hardening -*Design, 2026-07-31. H2 (SMEP) and HS (the SYSRET guard) have landed; H3 is the -remaining work. Companion to +*Design, 2026-07-31. H1 (the copy layer), H2 (SMEP), HS (the SYSRET guard) and +H3 (SMAP) have all landed; the track is complete. Companion to [protocol-namespace.md](protocol-namespace.md) on the security track — this is the hardware half; that is the namespace half.* diff --git a/docs/security-track-plan.md b/docs/security-track-plan.md index c2ce9f4..dce91ff 100644 --- a/docs/security-track-plan.md +++ b/docs/security-track-plan.md @@ -36,11 +36,11 @@ plain `main` checkout always tells the truth about where the work is.** | | | |---|---| -| Working on | **H3** — SMAP (the last phase) | -| Branch carrying it | `feat/security-group-4` (pushed to origin) | -| On `main` | everything through P4c — groups 1, 2 and 3 merged | -| Awaiting merge | H2, HS — land with the group 4 merge | -| Suite | 113 cases, all passing | +| Working on | nothing — the track is complete | +| Branch carrying it | — | +| On `main` | every phase — groups 1, 2, 3 and 4 merged | +| Awaiting merge | nothing | +| Suite | 114 cases, all passing | | Last updated | 2026-08-01 | A checkbox below means the phase met its definition of green and was @@ -95,8 +95,27 @@ group boundary. counter, not the outcome: measured with the guard's branch removed, TCG does not model Intel's ring-0 #GP and every outcome check still passed. Suite 113/113) -- [ ] **H3** — SMAP + boot-patched `clac`; `-cpu max` in the harness; negative tests -- [ ] **merge** group 4 → main, push +- [x] **H3** — SMAP on every core, and the boot-patched `clac` that makes it + hold. Hardware does not clear `EFLAGS.AC` on interrupt delivery and ring 3 + sets it with `popfq`, so without the patch a hostile process suspends SMAP + for the length of any handler it can provoke; `clac` is #UD without the + feature, so the image ships the 3-byte canonical NOP at the first byte of + `isr_common` — ahead of the CPL test, because a fault nested in ring 0 + inherits AC just as readily — and the boot processor overwrites it through + the physmap, the same door `smp.arm` and `process.run` already use to write + a page the executing mapping holds read-only. The two halves are wired + together rather than merely ordered: `initHardening` refuses CR4 bit 21 + until the patch has been written *and* read back through the text address it + will be fetched from, so no core can turn SMAP on ahead of it and a + translation that lied leaves the machine unhardened and saying so instead of + enforcing over an entry path that cannot clear AC. The `smp` case now + requires SMAP on every core it lands on, which is the ordering checked from + the far end. New `fault-smap` case reads a mapped user page from ring 0 and + requires the fault: error code `0x1` — present, and nothing else, since a + SMAP violation has no bit of its own and U/S reports the ring of the access. + The suite is now the standing enforcement test, and it found nothing left to + find: H1's conversion of the nine stragglers was complete. Suite 114/114 +- [x] **merge** group 4 → main, push --- @@ -547,6 +566,11 @@ review-level proof plus the existing fault cases regression. Suite 112. **Test:** new QEMU case `fault-smap`: ring-0 deliberate read of a mapped user page; expect vector 14 + `error code : 0x1` + kernel IP. And the whole suite becomes the tripwire — any missed straggler now fails loudly. +*(Landed: the patch site sits at the very first byte of `isr_common`, and +CR4.SMAP is refused on every core until the patch has been read back through +the text mapping it will execute from — so the ordering is enforced, not +merely documented. Error code observed: `0x1` exactly, present and nothing +else. The tripwire found no missed straggler: H1 had converted them all.)* Suite 114 (HS added one). --- diff --git a/system/kernel/architecture/x86_64/cpu.zig b/system/kernel/architecture/x86_64/cpu.zig index 80ca8b1..a68e70d 100644 --- a/system/kernel/architecture/x86_64/cpu.zig +++ b/system/kernel/architecture/x86_64/cpu.zig @@ -150,9 +150,16 @@ pub fn init() void { tss.init(); idt.init(); pcpu.initSystemCall(); + // The machine-wide half of SMAP, and it must precede every CR4 write: the + // interrupt entry has to be able to clear EFLAGS.AC before any core claims the + // bit. This is the boot processor and the application processors are still + // parked, so "before every core" is simply "here". + _ = pcpu.armSupervisorAccessPrevention(); // Safe this early, before the kernel is on its own page tables: the loader's // bootstrap tables (boot/efi.zig) map with present|writable and never set the - // U/S bit, so no page the BSP executes from is user-accessible. + // U/S bit, so no page the BSP executes from is user-accessible — and, for SMAP, + // no page it *reads* is either, so there is nothing for the new bit to refuse + // between here and the switch to the kernel's own tables. pcpu.initHardening(); } @@ -163,6 +170,14 @@ pub fn supervisorExecutePreventionEnabled() bool { return pcpu.supervisorExecutePreventionEnabled(); } +/// Whether ring 0 is barred from *reading or writing* user-mapped pages on this core +/// (CR4.SMAP on x86_64; the privileged-access-never behaviour elsewhere). False means +/// either the CPU doesn't offer it or the interrupt entry could not be armed to clear +/// AC — see per-cpu.zig; the machine boots either way, unhardened and saying so. +pub fn supervisorAccessPreventionEnabled() bool { + return pcpu.supervisorAccessPreventionEnabled(); +} + /// Build the kernel's own page tables (with real permissions) and switch onto /// them. Needs the frame allocator and the boot info (for the memory map and the /// kernel's segment layout). Call once the frame allocator is up. diff --git a/system/kernel/architecture/x86_64/isr.s b/system/kernel/architecture/x86_64/isr.s index 0bba73f..277fcb3 100644 --- a/system/kernel/architecture/x86_64/isr.s +++ b/system/kernel/architecture/x86_64/isr.s @@ -442,7 +442,27 @@ STUB_NOERR 128 # If the interrupt came from ring 3 the GS base holds the user's value, so swap # in the kernel's before anything reads per-CPU data (swapgs discipline; see # percpu.zig). CS sits at offset 24 here (vector@0, error@8, RIP@16, CS@24). +# +# The first three bytes are the SMAP guard, and they come before everything — +# before the CPL test, before the swapgs. Interrupt delivery does not clear +# EFLAGS.AC (SYSCALL does, through SFMASK; an IDT gate does not), and ring 3 sets +# AC freely with popfq, so without this a hostile process could take an interrupt +# with AC=1 and have the whole handler run with SMAP suspended. Ahead of the CPL +# test because ring 0 inherits AC just as readily: a fault or IRQ nested inside +# kernel code carries whatever AC the interrupted context had, and that context +# may itself be an entry that has not reached its own guard yet. Clearing first, +# unconditionally, means no path into the kernel is ever a path in with AC set. +# +# `clac` is #UD on a CPU without SMAP, so the image ships the 3-byte canonical NOP +# (`nopl (%rax)`) and per-cpu.zig overwrites it with `clac` (0f 01 ca) at boot, +# on the boot processor, only when CPUID says the instruction exists — a one-time +# patch rather than a branch in the hottest path in the kernel. Neither encoding +# touches the flags the `testb` below sets, so the guard is invisible to the code +# that follows it either way. +.global isr_smap_patch isr_common: +isr_smap_patch: + .byte 0x0f, 0x1f, 0x00 # nopl (%rax) -> patched to `clac` when the CPU has SMAP testb $3, 24(%rsp) jz 1f swapgs diff --git a/system/kernel/architecture/x86_64/per-cpu.zig b/system/kernel/architecture/x86_64/per-cpu.zig index 9ccd484..9cd2ace 100644 --- a/system/kernel/architecture/x86_64/per-cpu.zig +++ b/system/kernel/architecture/x86_64/per-cpu.zig @@ -17,10 +17,18 @@ //! hardening bits live here too, because each is state a core owns and must set //! for itself. Both bring-up paths — `cpu.init` on the boot processor and //! `smp.apEntry` on every application processor — call the same functions here. +//! +//! One deliberate exception to "per-core": `armSupervisorAccessPrevention` patches +//! a machine-wide instruction into the shared interrupt entry, once, on the boot +//! processor. It lives here anyway because it is the other half of CR4.SMAP — +//! same feature probe, same fail-open posture — and splitting a hardening measure +//! across two files is how the halves drift apart. const std = @import("std"); const io = @import("io.zig"); const cpuid = @import("cpuid.zig"); +const paging = @import("paging.zig"); +const boot_handoff = @import("boot-handoff"); const parameters = @import("parameters"); const ia32_gs_base = 0xC000_0101; @@ -112,6 +120,95 @@ fn smepSupported() bool { return cpuid.leaf(7).ebx & (1 << 7) != 0; } +/// CR4.SMAP: a ring-0 data *read or write* to a page whose U/S bit says *user* +/// raises #PF, unless EFLAGS.AC is set. It is the standing enforcement behind +/// system/kernel/user-memory.zig: that layer never dereferences a user virtual +/// address — it walks the process's tables and moves bytes through the physmap, +/// kernel mappings throughout — so nothing legitimate in this kernel is refused, +/// and any future code that reaches for a user pointer directly faults the first +/// time it runs. danos therefore opens no `stac` window anywhere; there is no +/// correct reason to have one, and adding one is how the guarantee is lost. +const cr4_smap: u64 = 1 << 21; + +/// SMAP's feature bit: CPUID leaf 7, sub-leaf 0, EBX bit 20. +fn smapSupported() bool { + if (!cpuid.supports(7)) return false; + return cpuid.leaf(7).ebx & (1 << 20) != 0; +} + +/// `clac` — the three bytes that replace the NOP at `isr_smap_patch` once the +/// interrupt entry is allowed to execute them. +const clac_opcode = [_]u8{ 0x0f, 0x01, 0xca }; + +/// Set only once the interrupt entry really clears AC, and read by every core +/// before it turns SMAP on. Nothing turns SMAP on until this is true, so there is +/// no window — not even on the boot processor, not even before ring 3 exists — +/// in which the bit is live while an interrupt could still be taken with AC set. +var interrupt_entry_clears_ac = false; + +fn readCr3() u64 { + return asm volatile ("mov %%cr3, %[out]" + : [out] "=r" (-> u64), + ); +} + +/// Patch the `clac` into the shared interrupt entry, and by doing so authorize +/// CR4.SMAP. **Boot processor only, once, before any core sets the bit and before +/// the application processors are woken** — `initHardening` refuses SMAP until +/// this has run, so the order is enforced rather than merely documented, and an AP +/// climbing the trampoline cannot get ahead of it. +/// +/// The write goes through the physmap, not through the kernel's own view of its +/// text: once `paging.init` has run, kernel `.text` is mapped read-only under W^X, +/// and a store to it would fault (or, worse, silently need CR0.WP cleared). The +/// physmap alias of the same frame is an ordinary supervisor RW mapping — the same +/// door `smp.arm` uses to write the AP trampoline and `process.run` uses to fill a +/// read-only user code frame. Going through it also makes this correct under +/// *either* set of tables: the loader's bootstrap tables map the image writable, +/// the kernel's own do not, and this runs before the switch. +/// +/// Three bytes, translated one at a time, so a patch site that straddles a page +/// boundary is not a special case. A translation that fails leaves the NOP in +/// place and returns false, and the machine then boots without SMAP rather than +/// with SMAP and an entry path that cannot clear AC. +pub fn armSupervisorAccessPrevention() bool { + if (!smapSupported()) return false; + const site = @intFromPtr(@extern([*]const u8, .{ .name = "isr_smap_patch" })); + const root = readCr3() & 0x000F_FFFF_FFFF_F000; + + // MUST run with interrupts masked, and does: this is reached from cpu.init, + // long before the kernel's `sti`, and before any application processor exists. + // The reason is that the three bytes go in one at a time, and the middle state + // — 0f 01 00, once the second byte lands — is `sgdt (%rax)`, a ten-byte write + // to wherever RAX points, sitting at the first instruction of every interrupt + // entry. Nothing can take that entry here, so nothing can execute it. A future + // change that moves this after interrupts are enabled has to close that window + // first (one 16-bit store covering both changed bytes is the shape, but it + // needs the two to be physically contiguous and 2-byte aligned — a naive + // version of exactly that triple-faulted this kernel). + for (clac_opcode, 0..) |byte, i| { + const physical = paging.translateIn(root, site + i) orelse return false; + const alias: *volatile u8 = @ptrFromInt(boot_handoff.physicalToVirtual(physical)); + alias.* = byte; + } + // The bytes were written through a different linear address than the one they + // will be fetched from, so serialize before anyone can execute them: CPUID is + // the architecturally sanctioned way to discard whatever the core prefetched or + // decoded of the old encoding. + _ = cpuid.leaf(0); + // Read back through the *text* address, not the alias just written: that is the + // view the CPU will fetch from, so this is what proves the two are the same + // physical page and the patch landed where it will actually execute. A mismatch + // means the translation lied, and SMAP stays off rather than being enabled over + // an entry path that cannot clear AC. + const installed: [*]const volatile u8 = @ptrFromInt(site); + for (clac_opcode, 0..) |byte, i| { + if (installed[i] != byte) return false; + } + interrupt_entry_clears_ac = true; + return true; +} + fn readCr4() u64 { return asm volatile ("mov %%cr4, %[out]" : [out] "=r" (-> u64), @@ -134,8 +231,14 @@ fn writeCr4(value: u64) void { /// posture either way (see the platform block in system/kernel/kernel.zig), so /// an unhardened boot is visible rather than assumed. pub fn initHardening() void { - if (!smepSupported()) return; - writeCr4(readCr4() | cr4_smep); + var bits: u64 = 0; + if (smepSupported()) bits |= cr4_smep; + // SMAP only after the boot processor has armed the interrupt entry: with the + // NOP still in place an interrupt inherits ring 3's AC and suspends SMAP for + // the length of the handler, which is worse than not claiming the bit at all. + if (smapSupported() and interrupt_entry_clears_ac) bits |= cr4_smap; + if (bits == 0) return; + writeCr4(readCr4() | bits); } /// Whether supervisor-mode execution prevention is live on *this* core. Read @@ -145,3 +248,11 @@ pub fn initHardening() void { pub fn supervisorExecutePreventionEnabled() bool { return readCr4() & cr4_smep != 0; } + +/// Whether supervisor-mode *access* prevention is live on *this* core. Read from +/// CR4 for the same reason as its neighbour: the question the boot log and the +/// `fault-smap` case are asking is what the hardware is doing, not what a probe +/// once concluded. +pub fn supervisorAccessPreventionEnabled() bool { + return readCr4() & cr4_smap != 0; +} diff --git a/system/kernel/architecture/x86_64/smp.zig b/system/kernel/architecture/x86_64/smp.zig index 91d5c18..2428069 100644 --- a/system/kernel/architecture/x86_64/smp.zig +++ b/system/kernel/architecture/x86_64/smp.zig @@ -182,7 +182,10 @@ fn apEntry(percpu: usize) callconv(.c) noreturn { // CR4 is per-core: this core starts from the trampoline's CR4 (PAE + SSE only), // so it sets its own hardening bits here rather than in the trampoline — one // Zig code path shared with the BSP (cpu.init), and the trampoline stays minimal. - pcpu.initHardening(); // CR4.SMEP on this core, if the CPU has it + // CR4.SMEP and CR4.SMAP on this core, if the CPU has them. The `clac` the SMAP + // bit depends on was patched into the shared interrupt entry by the BSP long + // before this core was woken, so an AP only ever finds it already armed. + pcpu.initHardening(); apic.initSecondary(); // software-enable this core's LAPIC apic.initTimer(apic.frequencyHz()); // arm its timer (still masked: interrupts off) diff --git a/system/kernel/kernel.zig b/system/kernel/kernel.zig index df087f3..c7eee06 100644 --- a/system/kernel/kernel.zig +++ b/system/kernel/kernel.zig @@ -288,6 +288,13 @@ fn kmain(boot_information: *const BootInformation) noreturn { log.write(" smep : enabled - ring 0 cannot execute user pages\n") else log.write(" smep : absent - ring-0 execution of user pages unprevented\n"); + // The supervisor-access posture, the same way. Its second half is a boot-time + // patch (the `clac` at the interrupt entry), so "absent" here covers both a + // CPU without the feature and a patch that could not be applied. + if (architecture.supervisorAccessPreventionEnabled()) + log.write(" smap : enabled - ring 0 reaches user memory only through the checked copy layer\n") + else + log.write(" smap : absent - ring-0 access to user pages unprevented\n"); // The DMA-isolation posture, stated plainly at every boot. When a unit exists, // iommu.init already logged its enable block above; here we only state the // fail-open case, so a boot without the line is a boot with translation on. diff --git a/system/kernel/tests.zig b/system/kernel/tests.zig index f763a23..d815c89 100644 --- a/system/kernel/tests.zig +++ b/system/kernel/tests.zig @@ -142,6 +142,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void { faultNull(); } else if (eql(case, "fault-smep")) { faultSmep(); + } else if (eql(case, "fault-smap")) { + faultSmap(); } else if (eql(case, "sysret-canonical")) { sysretCanonicalTest(); } else if (eql(case, "usermem")) { @@ -774,6 +776,10 @@ var seen_core = [_]bool{false} ** 8; /// nothing about the ones the trampoline brought up, and the whole hardening is /// only as wide as its narrowest core. var seen_core_smep = [_]bool{false} ** 8; +/// The same, for SMAP. A core that came up without it would still be able to read +/// and write user pages from ring 0 while every other core could not — a hole +/// whose only symptom is that the tripwire never trips there. +var seen_core_smap = [_]bool{false} ** 8; var smp_running: bool = true; /// A worker that, while running, records which core it's executing on. Spread across @@ -785,6 +791,7 @@ fn smpWorker() void { if (c < seen_core.len) { seen_core[c] = true; seen_core_smep[c] = architecture.supervisorExecutePreventionEnabled(); + seen_core_smap[c] = architecture.supervisorAccessPreventionEnabled(); } } scheduler.exit(); @@ -831,6 +838,20 @@ fn smpTest() void { check("every core that ran work had SMEP enabled", hardened == cores_seen); } + // And the same for SMAP, for the same reason and with one more of its own: the + // bit is refused until the boot processor has patched the `clac` into the shared + // interrupt entry, so a core reporting SMAP on is also a core confirming it came + // up after that patch — the ordering the whole scheme rests on, checked from the + // far end. Gated on the running core the same way, so a CPU without SMAP passes. + if (architecture.supervisorAccessPreventionEnabled()) { + var hardened: u32 = 0; + for (seen_core, seen_core_smap) |ran, smap| { + if (ran and smap) hardened += 1; + } + log("DANOS-SMP: {d} of {d} core(s) had supervisor access prevention\n", .{ hardened, cores_seen }); + check("every core that ran work had SMAP enabled", hardened == cores_seen); + } + // Bring-up is done, so the trampoline frame must be inert: zeroed (no stale code) // and non-executable (W^X restored). It's armed only while a core is climbing. const tramp = architecture.trampolinePage(); @@ -4462,6 +4483,60 @@ fn faultSmep() void { log("DANOS-TEST-RESULT: FAIL (SMEP not enforced)\n", .{}); } +/// Verify SMAP: a ring-0 data read from a *user*-mapped page must fault. +/// +/// The bug shape this case stands in for is the ordinary one — a syscall that takes +/// the pointer ring 3 handed it and dereferences it. So the probe does exactly that +/// and nothing more: an ordinary user data page (present, user-accessible, read-only, +/// no-execute) mapped into the address space this kernel task is already running on, +/// then read directly rather than through system/kernel/user-memory.zig. With SMAP on +/// and EFLAGS.AC clear the load is a #PF; without it the byte comes back and the FAIL +/// line below reports the value it should never have seen. +/// +/// The CPU reports it as error code **0x1**: present (bit 0) alone. A SMAP violation +/// has no error-code bit of its own — bit 1 stays clear because this is a read, and +/// bit 2 (U/S) describes the *access*, which was made in ring 0, not the page. So the +/// report is indistinguishable in its bits from any other supervisor read of a present +/// page; what identifies it is the pairing with `ring 0 (kernel)` and a user CR2. +/// +/// The seed write goes through the physmap, which is the point in miniature: that is +/// the route the checked copy layer takes for every legitimate access to this same +/// frame, and SMAP has no objection to it. Only the second access — the one through +/// the *user* virtual address — is refused. +/// +/// The enabled-check up front stops the case passing vacuously on a CPU without SMAP, +/// and fails rather than skips: a suite that quietly stops testing the tripwire is +/// exactly the outcome the tripwire exists to prevent. +fn faultSmap() void { + log("DANOS-TEST-BEGIN: fault-smap\n", .{}); + if (!architecture.supervisorAccessPreventionEnabled()) { + log("DANOS-TEST-RESULT: FAIL (SMAP not enabled in this boot)\n", .{}); + return; + } + const frame = pmm.alloc() orelse { + log("DANOS-TEST-RESULT: FAIL (no frame for the probe page)\n", .{}); + return; + }; + const seed: [*]u8 = @ptrFromInt(boot_handoff.physicalToVirtual(frame)); + @memset(seed[0..abi.page_size], 0); + seed[0] = 0x5A; // a sentinel, so an unenforced read is reported as a value, not a guess + // RO + NX *and user-accessible*, in the kernel's own tables: this task is a kernel + // task, so it runs on them (a process would have its own address space). + architecture.mapUserPage(process.code_virtual, frame, false, false); + + // Launder the address through empty asm for the same reason `fault-null` does: + // the backend must form a real load from a runtime address rather than reasoning + // about a constant it can see the provenance of. + var target: u64 = process.code_virtual; + target = asm ("" + : [ret] "=r" (-> u64), + : [in] "0" (target), + ); + const p: *const volatile u8 = @ptrFromInt(target); + const value = p.*; // ring-0 data read from a user page -> #PF + log("DANOS-TEST-RESULT: FAIL (SMAP not enforced; read 0x{x})\n", .{value}); +} + /// Verify the null guard: dereferencing address 0 (page 0 left unmapped) faults. fn faultNull() void { log("DANOS-TEST-BEGIN: fault-null\n", .{}); diff --git a/test/qemu_test.py b/test/qemu_test.py index c7402be..936fe92 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -355,6 +355,19 @@ CASES = [ r"(?=.*ring 0 \(kernel\))(?=.*error code : 0x11)" r"(?=.*fault addr : 0x0000700000000000)", "fail": r"SMEP not enforced|SMEP not enabled|no frame for the probe page"}, + # SMAP: ring 0 reads a page that is present and *user*-accessible, the way a syscall + # that dereferenced its caller's pointer would. CR4.SMAP (plus the `clac` patched + # into the interrupt entry, which keeps ring 3 from suspending it by taking an + # interrupt with AC set) must refuse the load: #PF with error code 0x1 — present, + # and nothing else. A SMAP violation has no bit of its own; bit 1 is clear because + # it is a read and bit 2 (U/S) reports the ring of the *access*, which was 0. Taken + # in ring 0, at the user address it read. The boot line is asserted too: without + # SMAP nothing would fault and the case would prove nothing. + {"name": "fault-smap", + "expect": r"(?s)(?=.*smap\s+: enabled)(?=.*page fault \(vector 14\))" + r"(?=.*ring 0 \(kernel\))(?=.*error code : 0x1(?![0-9a-f]))" + r"(?=.*fault addr : 0x0000700000000000)", + "fail": r"SMAP not enforced|SMAP not enabled|no frame for the probe page"}, # SYSRET canonical-RIP guard: a ring-3 probe whose `syscall` sits on the last two # bytes of executable address space, so the return RIP the kernel must hand back # is 0x0000800000000000 — non-canonical, and a ring-0 #GP if it reached SYSRETQ.