kernel: ring 0 reaches user memory only through the checked copy

SMAP makes the rule the copy layer has followed since it was written into a
rule the hardware keeps. A ring-0 read or write of a user page now faults,
so any code that reaches for a user pointer directly fails the first time it
runs rather than the first time someone attacks it — and the suite becomes
the enforcement test, because every case exercises the kernel with the bit
on. Nothing had to be fixed to turn it on, which is the retrospective proof
that the nine stragglers converted earlier were all of them.

The interrupt entry needed one instruction first. Hardware does not clear
the alignment-check flag on its way into a handler, and ring 3 sets that
flag freely, so a process could have taken an interrupt with SMAP suspended
for the duration. The system call path was already covered — its flag mask
clears it — but the interrupt path needed a `clac`, which cannot simply be
assembled in: it is an invalid instruction on a processor without SMAP, and
danos boots on those too. So the entry ships as a three-byte NOP and is
patched at boot, through the physmap, because the kernel maps its own text
read-only.

The ordering that makes that safe is enforced rather than described: the
patch sets a flag, and no core will set the SMAP bit until it is true. A
translation that fails, or bytes that read back wrong through the address
they will actually be fetched from, leave the machine unhardened and saying
so — which is the same posture the IOMMU takes, and better than enforcing
over an entry path that cannot comply. The patch runs before interrupts are
enabled and before any second core exists; a comment says so, because the
three bytes pass through an encoding that must never be executed and a
future change that moves this later has to deal with that first.

Suite 114/114, with a case that reads a user page from ring 0 and requires
the fault, and the multi-core case asserting every core that ran work had
the bit — the same shape SMEP got, for the same reason: CR4 is per-core, and
a hardening is only as wide as its narrowest core.
This commit is contained in:
Daniel Samson 2026-08-01 12:20:44 +01:00
parent cb30faf15f
commit 5a5146ec13
No known key found for this signature in database
GPG Key ID: A9EB2589C60F7268
10 changed files with 301 additions and 13 deletions

View File

@ -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.

View File

@ -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.*

View File

@ -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).
---

View File

@ -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.

View File

@ -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

View File

@ -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;
}

View File

@ -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)

View File

@ -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.

View File

@ -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", .{});

View File

@ -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.