CR0.WP is not new. Linux has used it to protect kernel text and rodata pages for over a decade — mark_rodata_ro() at boot, STRICT_KERNEL_RWX as a config option since 2016. Windows PatchGuard depends on it. macOS/XNU uses it for kernel code pages. OpenBSD is more aggressive about it than any of them.
We're not inventing the mechanism. What we're doing is applying it to a different problem: making a write-once audit ledger tamper-resistant even against the kernel that wrote it. This post is about what that application looks like in practice, what it actually achieves, and where it still falls short.
WHY THIS MATTERS MORE FOR AGENTS
When a human operator does something unexpected, there's a fallback: you can ask them. They have memory, intent, context. They can be questioned, and their answers add signal. When an AI agent takes an action — spending budget, calling an API, deploying a change, deleting data — none of that fallback exists. The audit log is not a backup record of what happened; it is the record. If it can be corrupted, post-hoc oversight becomes reconstruction from nothing.
The second difference is scale. A human engineer might make fifty significant decisions in a day. An agent might make fifty thousand. That volume means the log is the only tractable oversight mechanism — there's no practical way to reconstruct history from interviews or memory at agent execution rates. The audit record is how a principal knows what its agents did.
The third is timing. Humans have a real-time oversight loop during execution: they notice something is wrong and stop. Autonomous agents don't. The first place anyone looks after an unexpected outcome is the audit log. If the agent's own execution corrupted that log — through a bug, through a memory error, through anything — the only interpretable record of the event is gone.
In a multi-agent system like Noesis, there's a fourth issue: agent-to-agent trust. If Agent A can modify its own ledger, Agent B has no reliable basis to trust what Agent A claims to have done. Tamper-resistant audit is what makes signed, federated agent records meaningful — not just a claim, but a property the architecture enforces.
Noesis makes this concrete: every credential use is a ledger event written at the kernel level, at the moment of injection, before the agent ever sees the key. An agent that could erase that event could deny having used a credential — or having exceeded its authorization scope. Hardware-enforced audit isn't an audit framework bolted on for compliance. It's the property that makes agent authorization legible after the fact.
THE PROBLEM: AUDIT LOGS THAT CAN BE ERASED BY THE AUDITED SYSTEM
Every major OS audit framework — Linux auditd, Windows Event Log, macOS BSM — has the same structural problem: the kernel it's auditing is also the kernel it relies on to store and protect the audit records. A sufficiently privileged attacker who controls the kernel can modify or erase the logs. The audit system and the threat model share the same trust boundary.
The standard response is to ship logs out of the system as fast as possible — remote syslog, SIEM forwarding, write-once object storage. That works, but it's a race condition at boot and it depends on network availability. If an attacker moves fast enough, or the system is air-gapped, logs can be lost.
We want a different property for Noesis's ledger: once an event is written and sealed, it cannot be erased or modified by any code path — including kernel code — without that attempt itself being recorded. The question is how close we can get to that goal, and CR0.WP is one tool in that direction.
HOW OTHER OSES USE CR0.WP
The standard application is protecting static, always-immutable data: kernel code and read-only data sections.
At boot, Linux marks kernel text pages read-only by clearing the PTE Writable bits, then sets CR0.WP so the kernel itself cannot accidentally or deliberately overwrite its own instructions. The pages that get this treatment never need to be written again. The protection is permanent and unconditional from boot onward.
This is a well-understood use case and the hardware primitive fits it perfectly: immutable data, sealed once, enforced by the CPU from that point forward.
THE DIFFERENT APPLICATION: DATA THAT STARTS MUTABLE AND GETS SEALED
Audit log entries are not immutable from the start. An event has to be written somewhere first. A ledger segment starts writable, accumulates events as the kernel runs, and at some point is full and should never change again.
This is a different lifecycle: mutable during construction, permanently read-only once sealed. The seal is a one-way transition. After it happens, we want the same hardware guarantee Linux gets for kernel code — but for a data structure that was being written to minutes earlier.
In Noesis, each ledger segment is a #[repr(C, align(4096))] struct occupying complete 4 KiB pages. When a segment is full, seal_resident_ledger_segment_page() runs:
- Validates the segment hasn't already been sealed
- Copies every event into the page-aligned struct with sequence-number continuity validation
- Sets
page.sealed = 1as a logical flag - Calls
protect_resident_page_aligned_region()— walks the PTE for every page, clearsPTE_WRITABLE, then callsenable_write_protect() - Returns an error and refuses to complete if
write_protect_enabledcomes back false
Step 5 matters: if the hardware didn't take the CR0.WP write — because a hypervisor intercepted it, or something else went wrong — the segment is not considered sealed and the operation fails. We do not silently degrade to a software-only flag.
Here is enable_write_protect() from arch/x86_64.rs:
pub fn enable_write_protect() -> bool {
unsafe {
let mut cr0 = read_cr0();
cr0 |= 1 << 16; // CR0.WP: supervisor writes respect read-only PTEs.
write_cr0(cr0);
read_cr0() & (1 << 16) != 0
}
}
And clear_writable_for_page() from memory.rs, which does the PTE walk for each page of the sealed region:
fn clear_writable_for_page(
root_physical: u64,
direct_map: u64,
virtual_address: u64,
) -> Result<bool, PageTableProtectError> {
let pml4 = page_table_mut(root_physical, direct_map);
let pml4e = unsafe { &mut *pml4.add(page_table_index(virtual_address, 39)) };
if *pml4e & PTE_PRESENT == 0 {
return Err(PageTableProtectError::MissingMapping);
}
let pdpt = page_table_mut(*pml4e & PTE_ADDR_MASK, direct_map);
let pdpte = unsafe { &mut *pdpt.add(page_table_index(virtual_address, 30)) };
if *pdpte & PTE_PRESENT == 0 { return Err(PageTableProtectError::MissingMapping); }
if *pdpte & PTE_HUGE_PAGE != 0 {
return clear_large_page_writable(pdpte, virtual_address, 1 << 30);
}
let pd = page_table_mut(*pdpte & PTE_ADDR_MASK, direct_map);
let pde = unsafe { &mut *pd.add(page_table_index(virtual_address, 21)) };
if *pde & PTE_PRESENT == 0 { return Err(PageTableProtectError::MissingMapping); }
if *pde & PTE_HUGE_PAGE != 0 {
return clear_large_page_writable(pde, virtual_address, 1 << 21);
}
let pt = page_table_mut(*pde & PTE_ADDR_MASK, direct_map);
let pte = unsafe { &mut *pt.add(page_table_index(virtual_address, 12)) };
if *pte & PTE_PRESENT == 0 { return Err(PageTableProtectError::MissingMapping); }
let was_writable = *pte & PTE_WRITABLE != 0;
*pte &= !PTE_WRITABLE;
Ok(was_writable)
}
At boot, seal_resident_static_regions() does a bulk seal for the initial protected set — the ledger protection page, credential ingress buffer, and service manifest. All PTEs are cleared first across every region, then CR0.WP is set in a single call, avoiding any window where some pages are hardware-protected and others are not.
THE FAULT RESPONSE: AUDIT-SURVIVABILITY OVER RECOVERY
After sealing, any write to a protected page raises a hardware #PF. Most OS page fault handlers try to recover — map a new page, expand a stack, resume the faulting thread. The Noesis handler in interrupt.rs does something different:
extern "C" fn noesis_page_fault_interrupt_rust(error_code: u64) {
let address = crate::arch::read_page_fault_address();
let count = crate::memory::record_resident_memory_violation(address, error_code);
let ledger_recorded =
crate::os_boot::append_resident_memory_violation_ledger_event(
address, error_code, count
);
let _ = crate::boot_diagnostics::emit_stage_beacon(
"SERVICE_MEMORY_VIOLATION", address, error_code
);
crate::arch::halt();
}
Read the faulting address from CR2. Record it atomically. Then append_resident_memory_violation_ledger_event() does the heavier work — it writes a ledger event describing the violation, seals the segment containing that event, and submits the NVMe write. The handler doesn't do those steps directly; they happen inside that call before it returns. emit_stage_beacon() records a diagnostic trace. Then halt permanently.
The sealing and NVMe submission should happen synchronously before halt() is reached — though whether the NVMe controller completes the write before the CPU stops is the open question in the gaps section below.
The design choice is audit survivability over continued execution: an auditor reading the NVMe partition after a suspicious halt should find a signed ledger event describing the tamper attempt, not a corrupted log and a mystery.
WHAT THIS ACTUALLY IMPROVES
Against accidental writes — a kernel bug writing to the wrong address, a pointer error, uninitialized memory stomping into a sealed region — the protection should be effective. The fault fires, the event is recorded, the system halts. That said, the SMP TLB gap means another CPU core could write through a stale cached entry in the window immediately after sealing — so "complete" would be the wrong word until TLB shootdown is in place.
Against ordinary kernel code running normally without deliberately manipulating control registers: same story. Normal code respects CR0.WP. A normal write to a sealed page should fault.
What this means in practice: the sealed ledger should be robust against the entire class of accidental and unintentional corruption. Any kernel path that could inadvertently clobber a sealed page should instead produce a permanent, attributed fault record. It's the same class of improvement Linux gets from STRICT_KERNEL_RWX — not a security silver bullet, but a meaningful hardening of the normal execution path.
WHERE IT STILL FALLS SHORT
Ring-0 can disable CR0.WP in three instructions
write_cr0(read_cr0() & !(1 << 16)); // disable WP
*sealed_page_ptr = tampered_value; // write succeeds
write_cr0(read_cr0() | (1 << 16)); // re-enable WP
This is the standard technique used by Linux kernel rootkits to patch read-only kernel memory — the same memory Linux protects with the same mechanism. Against a deliberate attacker who already has kernel code execution, CR0.WP provides no protection. The analog in Linux is that STRICT_KERNEL_RWX doesn't stop a kernel-level exploit either — it raises the bar; it doesn't define the ceiling.
What we're investigating: whether a hypervisor trap on CR0 writes could provide the next layer, at the cost of unconditional hypervisor trust. AMD SEV-SNP and Intel TDX change the hypervisor trust model in ways that might be relevant. If you've worked through this, we'd like to hear what you found.
DMA bypasses the MMU entirely
CR0.WP controls CPU writes through the MMU. A DMA-capable device — our GVNIC NIC, the NVMe controller, any PCIe peripheral — writes directly to physical memory without going through the MMU at all. Sealed page PTE flags mean nothing to a device doing DMA.
Closing this gap requires IOMMU (Intel VT-d / AMD-Vi) with sealed pages' physical address ranges excluded from device-accessible memory. We have not implemented this. We're investigating what GCP bare metal exposes in terms of IOMMU control and what the implementation cost looks like.
SMP: invlpg only flushes the local CPU's TLB
After clear_writable_for_page() returns, the wrapper protect_resident_page_aligned_region() issues invlpg for that virtual address. On a multi-core system this only invalidates the TLB entry on the CPU running the invalidation. Other cores may retain a cached translation with the Writable bit still set and continue writing to the page without faulting until their TLBs are also invalidated.
This is a correctness gap on every multi-core machine we run on. Proper TLB shootdown — an IPI to all CPUs, each running invlpg, confirmation before proceeding — is a known fix and it's on our near-term list.
NVMe write completion before halt
The fault handler triggers an NVMe write before halting. NVMe is asynchronous — a write command submitted to the submission queue may not have been processed by the controller when halt() executes. If the CPU halts before the DMA transfer completes, the fault record may not reach the medium.
We need an explicit completion poll before halt. We're not certain the current implementation handles this correctly. If you've solved this pattern in a bare metal context — synchronous NVMe flush before halt, without a full async completion stack — we'd like to know how.
The hypervisor boundary
On GCP bare metal, a hypervisor controls the physical memory the guest kernel believes it owns. It can modify guest physical pages directly, bypassing everything in the guest. It can also intercept and no-op the CR0 write.
This is an unconditional trust boundary. Everything above assumes the hypervisor is honest. For a cloud deployment that assumption is load-bearing and we're not pretending otherwise. Confidential computing environments (AMD SEV-SNP, Intel TDX) address parts of this — but not the DMA picture, and not side-channel exposure. We're watching this space closely.
WHAT WE'VE VERIFIED
The build includes a fault injection path enabled by NOESIS_PAGE_PROTECTION_FAULT_INJECTION. After sealing, the kernel deliberately attempts a volatile write to a sealed page:
pub unsafe fn deliberate_resident_ledger_static_overwrite_for_validation() {
let base = if resident_ledger_segment_page_is_sealed(0) {
resident_ledger_sealed_segment_page_ptr(0) as *mut u8
} else {
core::ptr::addr_of_mut!(RESIDENT_LEDGER_PROTECTION_PAGE) as *mut u8
};
core::ptr::write_volatile(base, 0xa5);
}
When CR0.WP is active and PTEs are cleared, this raises a #PF, the handler runs, and the machine halts with a ledger record. This confirms the mechanism works against the class of normal writes. It says nothing about the gaps above.
WHERE WE'RE GOING
The near-term list: SMP TLB shootdown, synchronous NVMe completion before halt. The medium-term investigation: IOMMU protection for DMA, and whether a hypervisor trap on CR0 writes is worth the added trust dependency.
The larger question — whether hardware-enforced audit with meaningful adversarial guarantees is achievable at all in a cloud deployment without confidential computing primitives — is something we're still working through.
If you've built something in this space, seen a gap we've missed, or have a different approach to any of the open problems above, we'd like to hear it. Contact is below.