← Back to AILP Home

The wildx lifecycle (JIT view)

wildx is the same allocator the memory chapter covers, but JIT pulls on every stage of its W^X state machine. This page walks the lifecycle from the JIT angle: what each step is for, what fails if you skip it, and what the runtime guarantees in return.

States

UNINITIALIZED ──wildx_alloc──▶ WRITABLE ──wildx_seal──▶ EXECUTABLE ──wildx_free──▶ FREED
                                  │                          │
                                  │                          └── any write here
                                  │                              traps with SIGSEGV
                                  │                              (PROT_EXEC only)
                                  │
                                  └── safe to memcpy machine bytes;
                                      no PROT_EXEC bit set yet

A page is keyed by its writable address in a process-wide registry. The transitions are one-way: you cannot un-seal, you cannot re-arm a freed page, and a foreign pointer (one that did not come from wildx_alloc) is rejected by every operation.

Step 1 — wildx_alloc(size)

wildx int8->:page = wildx_alloc(64);

mmaps an anonymous, page-aligned region with PROT_READ | PROT_WRITE (no PROT_EXEC). Registers the writable address in the W^X registry along with an ASLR jitter base, an FNV-1a code hash slot (filled in at seal time), and the quota accounting.

Step 2 — write machine bytes

// In Nitpick, you'll usually call an extern helper:
fixed int32:write_rc = npk_jit_install_add_i32(page);

This is where the JIT meets the metal. In v0.28.x we hand off to npk_jit_install_add_i32, which memcpys 5 bytes of x86-64 SysV-AMD64 ABI machine code:

89 f8     mov  eax, edi      ; ret = a
01 f0     add  eax, esi      ; ret += b
c3        ret

The writable mapping makes this a plain memcpy. No syscalls, no privilege boundary; just bytes into a writable buffer. Writes that overrun the page allocation are caught by the OS — pages are page-granular, and the next page is unmapped.

Step 3 — wildx_seal(page)

fixed int32:seal_rc = wildx_seal(page);

Transitions the page from WRITABLE to EXECUTABLE. The runtime:

  1. Computes an FNV-1a hash of the page contents and stores it in the guard for later integrity checks.
  2. Calls mprotect(..., PROT_READ | PROT_EXEC) — the page is no longer writable. The W is now gone, only the X is set.
  3. Records the state transition.

  4. Returns 0 on success, -1 if the page is not in the WRITABLE state. Calling wildx_seal twice fails the second call (regression: bug246).

  5. Hardware enforcement: any subsequent write to the page traps with SIGSEGV from the MMU, not from a runtime check. This is the entire point of W^X: even a compromised JIT cannot modify already-sealed code.

Step 4 — call through the page

fixed int32:result = npk_jit_call_i32_i32(page, 7i32, 35i32);
// result == 42

The call helper memcpys the page pointer into a typed function pointer and invokes it. The CPU executes the bytes you wrote.

Step 5 — wildx_free(page)

fixed int32:free_rc = wildx_free(page);

munmaps the page and removes the registry entry. The pointer is now invalid for every operation:

The borrow checker rejects most of these statically (ARIA-022 double-free, ARIA-015 use-after-release; regression bug247). The runtime -1 return is a backstop for pointers that get smuggled through function boundaries the borrow checker cannot see across.

Pitfalls specific to JIT

Validation