AIKP Actually Boots Now

A few days ago AIKP was still mostly an architecture on paper. In my previous post, What If the Kernel Couldn't Read Application Memory?, I basically asked one question: why does compromising the kernel automatically have to mean compromising every application on the machine?

Normally the kernel manages memory, processes, devices, files, networking and basically everything else, which is obviously useful. The slightly unfortunate side effect is that once somebody owns the kernel, they also get access to all the state the kernel manages. Process memory, page tables, register frames, debugging interfaces, DMA-capable devices. Once ring 0 is gone, the isolation between applications becomes a lot less interesting.

AIKP tries to change that by separating application execution from kernel services at the hardware boundary. And at this point it actually boots. Not Linux, not a production-ready processor and definitely not something I am about to send to TSMC, but there is now a real QEMU machine with two RISC-V CPUs, separate address spaces, separate memory, interrupts, a mailbox, temporary memory grants, DMA authorization, swap and a Kernel Processor that actively tries to violate the architecture.

The repository is available here. This post describes the state at commit 4cbc1f3.

The boundary is now an actual machine

One thing I wanted to avoid from the beginning was building something that only looks isolated in an architecture diagram. Putting the kernel on another CPU core doesn't solve much if both cores still see the same physical memory. If the KP can simply execute a load against AP RAM, congratulations, I invented an unnecessarily complicated SMP system.

The AIKP QEMU machine instead assigns each CPU its own AddressSpace root. The AP sees AP RAM, AP-private supervisor state and its own firmware. The KP sees KP RAM, its firmware and a grant aperture which normally contains nothing from the AP. There is no shared general-purpose RAM sitting between them where everything eventually gets dumped anyway.

The concrete machine is implemented in qemu-aikp/hw/riscv/aikp.c, based on a pinned QEMU 9.2.0 source tree.

┌──────────────────────── AP trust domain ────────────────────────┐
│                                                                 │
│             ┌──────────────────────────────┐                    │
│             │ Application memory and state │                    │
│             └──────────────┬───────────────┘                    │
│                            │ syscall or fault                   │
│                            ▼                                    │
│                  ┌───────────────────┐                          │
│                  │   AP supervisor   │                          │
│                  └─────────┬─────────┘                          │
└────────────────────────────┼────────────────────────────────────┘
                             │
              ┌──────────────┴──────────────┐
              │                             │
   create or revoke exact alias       messages and interrupts
              │                             ↕
              ▼                             │
┌──────────── Explicit cross-domain mechanisms ───────────────────┐
│                                                                 │
│   ┌──────────────────────────────┐    ┌─────────────────────┐   │
│   │ AP-controlled grant aperture │    │   Bounded mailbox   │   │
│   └──────────────┬───────────────┘    └──────────┬──────────┘   │
│                  │ bounded view                  ↕              │
└──────────────────┼───────────────────────────────┼──────────────┘
                   │                               │
                   └──────────────┬────────────────┘
                                  ▼
┌──────────────────────── KP trust domain ────────────────────────┐
│                                                                 │
│                       ┌─────────────────┐                       │
│                       │ Kernel services │                       │
│                       └────────┬────────┘                       │
│                                │                                │
│                                ▼                                │
│                      ┌───────────────────┐                      │
│                      │ KP-private memory │                      │
│                      └───────────────────┘                      │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

The grant aperture begins at 0x30000 in the KP address space. It isn't shared memory in the normal sense because there is no permanent AP backing behind it. The AP-controlled grant controller can temporarily install an exact alias into that aperture, and only then can the KP reach the selected bytes. AP RAM itself begins at 0x10000, KP RAM at 0x20000, and direct cross-domain probes against both regions are expected to fault.

The mailbox is mapped into both domains at 0xB000, but it only carries bounded descriptors and payloads. An address inside a mailbox message is just a number. It does not create a mapping and it does not grant authority. That distinction is pretty important because otherwise the design would basically become copy_from_user() with additional paperwork.

The mailbox device itself is implemented in aikp_mailbox.c. It has bounded payloads, sequence and correlation IDs, pending state, one outstanding descriptor per direction and real interrupts wired to the CPUs.

Temporary access instead of permanent authority

Say an application calls something equivalent to write() with a 200-byte buffer. The kernel eventually needs those bytes. That's literally the service being requested. But there is a fairly large difference between “the kernel needs these 200 bytes for this operation” and “the kernel therefore needs permanent access to the entire process address space.”

The AP supervisor first validates that the buffer belongs to the calling process and is currently readable. It then creates an exact read-only grant for that range. The KP receives an opaque grant ID, generation, length and aperture location through the mailbox, reads the bytes through its own address space, returns a correlated response and then the AP revokes the mapping before returning to the application.

     App              AP supervisor          Grant controller          Mailbox                 KP
      │                      │                        │                    │                     │
      │  write(buffer, 200)  │                        │                    │                     │
      ├─────────────────────►│                        │                    │                     │
      │                      │                        │                    │                     │
      │                      ├── Validate live ──┐    │                    │                     │
      │                      │   readable mapping│    │                    │                     │
      │                      │◄──────────────────┘    │                    │                     │
      │                      │                        │                    │                     │
      │                      │ Create exact           │                    │                     │
      │                      │ read-only grant        │                    │                     │
      │                      ├───────────────────────►│                    │                     │
      │                      │                        │                    │                     │
      │                      │ Send handle, length and request ID          │                     │
      │                      ├────────────────────────────────────────────►│                     │
      │                      │                        │                    │                     │
      │                      │                        │                    │      Interrupt      │
      │                      │                        │                    ├────────────────────►│
      │                      │                        │                    │                     │
      │                      │                        │  Read through      │                     │
      │                      │                        │  KP aperture       │                     │
      │                      │                        │◄─────────────────────────────────────────┤
      │                      │                        │                    │                     │
      │                      │                        │ Return only the    │                     │
      │                      │                        │ granted bytes      │                     │
      │                      │                        ├─────────────────────────────────────────►│
      │                      │                        │                    │                     │
      │                      │                        │                    │ Correlated response │
      │                      │                        │                    │◄────────────────────┤
      │                      │                        │                    │                     │
      │                      │       Interrupt        │                    │                     │
      │                      │◄────────────────────────────────────────────┤                     │
      │                      │                        │                    │                     │
      │                      │ Revoke grant           │                    │                     │
      │                      ├───────────────────────►│                    │                     │
      │                      │                        │                    │                     │
      │       Return         │                        │                    │                     │
      │◄─────────────────────┤                        │                    │                     │
      │                      │                        │                    │                     │

Every grant has a generation, so reusing an aperture later doesn't suddenly make an old handle valid again. Permissions are checked, ranges are bounded and grants can expire. The KP also cannot create or retarget grants because the controller at 0x7000 only exists in the AP address space.

This is the central idea behind the current implementation: the KP can request services and receive narrowly scoped capabilities, but it cannot manufacture authority over AP memory for itself.

Operating systems do slightly more than write()

This was where the project started becoming interesting. Saying “the kernel cannot access application memory” sounds nice until you remember that operating systems have fork(), exec(), page faults, mmap, shared libraries, signals, timers, IPC, swap, debugging, crash handling and a few hundred other places where the kernel casually assumes it can inspect application state whenever it feels like it.

So I've basically been going through those assumptions one by one and asking whether the KP genuinely needs the memory, or whether it only needs some operation to happen.

Scheduling and context switching happen on the AP side, so application register frames remain private. fork() uses AP-controlled copy-on-write pages. The KP can request process creation and learn that an opaque child exists, but it doesn't receive the contents of the parent's pages. exec() replaces the process image locally and increments an image epoch, invalidating handles tied to the previous image.

Page faults are classified by the AP supervisor as demand allocation, copy-on-write, swapped, file-backed, invalid or protection faults. There are also shared AP pages, file-backed mappings, shared executable frames, signals, timer preemption and bounded crash extraction. A process can provide a stack snapshot after crashing without turning “please give me useful crash information” into “please map the entire process into kernel memory.”

Most of that booted supervisor logic lives in qemu-aikp/firmware/supervisor.c, with the AP trap and execution paths in ap.S.

There is also a broader Python reference model, which is useful for testing state transitions and semantics without rebuilding QEMU every five minutes. But I think the distinction matters: the Python model can show that the rules are internally consistent, while the QEMU implementation shows whether CPU-originated transactions really hit different address spaces. Those aren't the same claim, and I don't want to quietly merge them just because both tests print green text.

Swap was particularly annoying

Swap is a good example of why simply moving responsibilities around isn't enough. Giving an untrusted KP plaintext pages and politely asking it not to read them would be a somewhat questionable security design.

In the current prototype, the AP encrypts and authenticates a complete 4,096-byte page before handing anything to KP-controlled storage. The KP only receives a 4,128-byte opaque blob: ciphertext followed by a 32-byte authentication tag. The key material is derived from a fresh per-boot seed delivered through an AP-only interface and never enters the KP domain.

The authentication covers more than the ciphertext. It also binds the process ID, virtual address, page version, storage slot and page length. That matters because otherwise the KP could take a perfectly valid encrypted page from process A and return it as process B's page, move it to another virtual address or replay an older version.

And naturally the hostile KP tries all of those. It modifies the stored ciphertext, replays an earlier version, substitutes a blob belonging to another process and relocates one to another virtual page. The AP verifies the complete tag before installing a PTE or exposing any plaintext, and all four attacks fail closed.

The implementation is split between the AP supervisor and firmware/crypto.c. The storage assumptions are also checked in test_qemu_storage_contract.py.

This is still prototype crypto. It uses an HMAC-SHA-256-derived stream plus authentication, not a reviewed production AEAD implementation. Replay state is trusted only for the current boot, the version counter is volatile and the host file is not supposed to be a persistent swap volume. The point isn't that I have solved encrypted swap better than existing crypto libraries. The point is showing where the trust boundary and associated metadata have to exist if the KP is treated as hostile.

So I made the Kernel Processor malicious

A security architecture where every component politely follows the architecture isn't testing very much, so the KP firmware is intentionally hostile.

It tries to read and write AP memory directly. It scans for secrets, guesses addresses, probes another application's page and attempts to read supervisor memory. It puts AP addresses into mailbox descriptors, reuses stale grants, writes through read-only grants, crosses grant boundaries, tries to create and modify grants, enables debugging, modifies page tables and saved contexts, performs unauthorized DMA, looks for plaintext swap data and accesses AP shared pages without permission.

The actual attack code is in qemu-aikp/firmware/kp.S. The hostile-KP matrix expects one explicit denial result for each of 19 named booted attacks. Missing evidence, duplicated evidence, an unexpected result or a failed boot makes the suite fail. It then performs four additional boots for the swap tamper, replay, substitution and relocation cases.

The important part is that the direct memory tests aren't just a Python method throwing AccessDenied. The KP CPU actually executes load and store instructions. QEMU routes those transactions through the KP's restricted address space, they hit unmapped memory and the firmware's trap handler observes the fault. The AP does the reverse against KP RAM, and the demo refuses to continue unless both cross-domain probes failed.

That's a much stronger result than a model returning false because I wrote an if statement somewhere.

DMA and debugging don't get magical exceptions

DMA would be a fairly obvious way to accidentally destroy the whole model. You could isolate the KP perfectly, attach a device capable of accessing all AP memory and then let the KP program it. Fantastic, ten out of ten.

So device access follows the same basic capability model. The AP authorizes an exact region and permission set, the device can perform the requested operation inside that region and the authorization can then be revoked. Unauthorized and post-revocation operations fail.

Internally, QEMU eventually calls address_space_read(&s->ap_as, ...) or the corresponding write operation. There is no magic here. The important part is that those calls happen only after an AP-created DMA grant has been checked. The KP can request an operation, but it cannot create that authorization itself.

This slightly changes how I describe AIKP. I wouldn't say that nothing outside the AP can ever address AP memory, because that's neither completely accurate nor particularly useful. Devices need data. Kernel services sometimes need data. Debuggers need data.

Nothing outside the AP trust domain gets arbitrary access to AP memory. Every cross-domain access requires an explicit AP-controlled capability.

Debugging follows the same logic. If the KP had a command that mapped arbitrary AP memory because LLDB asked nicely, I would have recreated unrestricted kernel access and renamed it debugging. The current machine therefore has a boot-latched debug state. If debugging is disabled when the machine starts, the KP cannot enable it later after being compromised. The policy is checked by test_qemu_debug_latch.py.

That isn't a complete production debug design yet. A real machine would still need an authenticated external mechanism, secure boot integration and a usable development workflow. But at least the architecture no longer has an invisible “except when debugging” hole large enough to drive the entire kernel through.

What this proves, and what it doesn't

I don't think the prototype proves that AIKP is ready for production hardware. It doesn't. But it does move the idea from “this seems theoretically possible” to something concrete enough to attack.

The prototype demonstrates It does not demonstrate yet
Two CPUs can execute with genuinely different QEMU address spaces A physical cache, interconnect and memory-controller design
A hostile KP cannot issue ordinary loads or stores into AP RAM Compatibility with Linux, XNU or another production kernel
Kernel services can use temporary AP-controlled capabilities Acceptable real-world syscall and scheduling performance
fork, exec, faults, mmap, signals, IPC and swap can be split across the boundary A complete general-purpose operating system
DMA and debugging can be made explicit capabilities instead of implicit kernel powers A finished device ecosystem or production debug workflow
Swap attacks can be detected without exposing plaintext to the KP Production cryptography or persistent rollback protection

The repository exposes the main checks through its Makefile:

make demo
make attacks
python3 -m unittest discover -v

With the QEMU build directory configured, make demo builds the firmware and boots both processors. make attacks runs the named booted denial matrix and the four hostile swap variants. The Python test suite currently discovers 53 tests; four QEMU integration tests skip cleanly when the QEMU binary hasn't been built, which is useful behaviour but obviously shouldn't be confused with those boot tests having passed.

The remaining questions are still large. Performance, cache coherence, physical implementation, device architecture, failure recovery, boot trust, compatibility and the amount of pain involved in porting an actual operating system are all very much unsolved. QEMU also isn't silicon, and a small deterministic supervisor is not Linux.

But quite a few things which initially looked like they required unrestricted kernel access turned out not to require it. The KP often needs a result, an opaque identity or a bounded piece of data. It does not automatically need every application page and register frame on the machine.

Until now AIKP was mostly an argument about what should be possible. Now there is a machine you can boot, give malicious kernel code to and watch the memory boundary survive. Which is honestly much more useful than another diagram with two rectangles and a very confident-looking line between them.