kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit a35b5d50f67122dfadc832705faea4bead9f617c
parent e9bbba2b0ac78b989cca2d0bf6ffd66d12a5221a
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat,  9 May 2026 11:43:15 -0700

execmem: dual-mapping JIT regions for strict W^X

Code regions now come back as a write alias (RW, never X) and a runtime
alias (R, flipped to RX by protect, never W) backing the same physical
pages — mach_vm_remap on Apple, memfd_create+dual-mmap on Linux. No VA
ever holds W and X simultaneously. link_jit reserves per-segment so data
and rodata stay single-aliased and JITed code can still write to globals
and BSS through their own RW pages.

Diffstat:
Mdriver/env.c | 180+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Minclude/cfree.h | 49++++++++++++++++++++++++++++++++++++-------------
Msrc/emu/runtime.c | 50++++++++++++++++++++++++++++----------------------
Msrc/link/link_jit.c | 158++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mtest/cg/harness/cg_runner.c | 93++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mtest/link/harness/jit_runner.c | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
6 files changed, 525 insertions(+), 95 deletions(-)

diff --git a/driver/env.c b/driver/env.c @@ -14,6 +14,38 @@ #include <time.h> #include <unistd.h> +/* Dual-mapping back-ends for strict W^X. Picks per-platform: + * + * - Apple (any arch): mach_vm_remap creates a second VA pointing at the + * same physical memory. The write alias is RW; the runtime alias is + * left at PROT_READ until protect() flips it to PROT_READ|PROT_EXEC. + * No MAP_JIT, no per-thread mode bit, no entitlement coupling for + * unsigned dev binaries (signed/hardened-runtime binaries need the + * com.apple.security.cs.allow-unsigned-executable-memory entitlement). + * + * - Linux: memfd_create + two mmaps of the same fd backs both aliases + * with the same physical pages. + * + * - Other POSIX: fall back to a single mapping (no dual aliasing + * available). Still W^X — reserve returns RW, protect transitions + * to RX with no overlap, no RWX state. + */ +#if defined(__APPLE__) +# include <mach/mach.h> +# include <mach/mach_vm.h> +# include <mach/vm_map.h> +# define DRIVER_DUAL_APPLE 1 +#else +# define DRIVER_DUAL_APPLE 0 +#endif + +#if defined(__linux__) +# include <sys/syscall.h> +# define DRIVER_DUAL_LINUX 1 +#else +# define DRIVER_DUAL_LINUX 0 +#endif + /* Host-side implementations of the three vtable interfaces libcfree needs: * * - DriverHeapLibc — malloc/realloc/free @@ -91,13 +123,129 @@ static int cfree_to_posix_prot(int prot) return p; } -static void* execmem_reserve(void* user, size_t size, int prot) +/* Per-region bookkeeping used by reserve/release. Only EXEC regions + * actually carry a token (single-mapping reservations leave it NULL). + * Apple stores nothing extra; Linux stores the memfd-backed write alias + * size so munmap can release the runtime alias separately. */ +typedef struct ExecMemToken { + void* write_addr; + void* runtime_addr; + size_t size; +} ExecMemToken; + +static int execmem_reserve_single(size_t size, CfreeExecMemRegion* out) +{ + void* p = mmap(NULL, size, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANON, -1, 0); + if (p == MAP_FAILED) return 1; + out->write = p; + out->runtime = p; + out->size = size; + out->token = NULL; /* munmap suffices on release */ + return 0; +} + +#if DRIVER_DUAL_APPLE +static int execmem_reserve_dual_apple(size_t size, CfreeExecMemRegion* out) +{ + /* 1) Allocate the writable backing via mmap. */ + void* w = mmap(NULL, size, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANON, -1, 0); + mach_vm_address_t r_addr = 0; + vm_prot_t cur = 0, max = 0; + kern_return_t kr; + ExecMemToken* tok; + if (w == MAP_FAILED) return 1; + + /* 2) Create an alias mapping. copy=FALSE makes the new VA share the + * same physical pages. We request VM_INHERIT_NONE so child + * processes don't observe the executable alias accidentally. */ + kr = mach_vm_remap(mach_task_self(), &r_addr, (mach_vm_size_t)size, + /*mask=*/0, + VM_FLAGS_ANYWHERE, + mach_task_self(), (mach_vm_address_t)(uintptr_t)w, + /*copy=*/FALSE, + &cur, &max, + VM_INHERIT_NONE); + if (kr != KERN_SUCCESS) { munmap(w, size); return 1; } + + /* 3) Drop the runtime alias to PROT_READ until protect() flips it. + * No window of writable+executable: the write alias has W (no X), + * the runtime alias has R only (no W, no X yet). */ + if (mprotect((void*)(uintptr_t)r_addr, size, PROT_READ) != 0) { + munmap((void*)(uintptr_t)r_addr, size); + munmap(w, size); + return 1; + } + + tok = (ExecMemToken*)malloc(sizeof(*tok)); + if (!tok) { + munmap((void*)(uintptr_t)r_addr, size); + munmap(w, size); + return 1; + } + tok->write_addr = w; + tok->runtime_addr = (void*)(uintptr_t)r_addr; + tok->size = size; + + out->write = w; + out->runtime = (void*)(uintptr_t)r_addr; + out->size = size; + out->token = tok; + return 0; +} +#endif + +#if DRIVER_DUAL_LINUX +static int execmem_reserve_dual_linux(size_t size, CfreeExecMemRegion* out) +{ + /* memfd_create gives us an anonymous fd; two mmaps of that fd alias + * the same physical pages at distinct VAs. */ + int fd = (int)syscall(SYS_memfd_create, "cfree-jit", 0u); + void* w; + void* r; + ExecMemToken* tok; + if (fd < 0) return 1; + if (ftruncate(fd, (off_t)size) != 0) { close(fd); return 1; } + + w = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (w == MAP_FAILED) { close(fd); return 1; } + r = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0); + if (r == MAP_FAILED) { munmap(w, size); close(fd); return 1; } + /* The fd is no longer needed once both aliases are mapped. */ + close(fd); + + tok = (ExecMemToken*)malloc(sizeof(*tok)); + if (!tok) { munmap(r, size); munmap(w, size); return 1; } + tok->write_addr = w; + tok->runtime_addr = r; + tok->size = size; + + out->write = w; + out->runtime = r; + out->size = size; + out->token = tok; + return 0; +} +#endif + +static int execmem_reserve(void* user, size_t size, int prot, + CfreeExecMemRegion* out) { - void* p; (void)user; - p = mmap(NULL, size, cfree_to_posix_prot(prot), - MAP_PRIVATE | MAP_ANON, -1, 0); - return (p == MAP_FAILED) ? NULL : p; + if (!out || !size) return 1; + if (prot & CFREE_PROT_EXEC) { +#if DRIVER_DUAL_APPLE + return execmem_reserve_dual_apple(size, out); +#elif DRIVER_DUAL_LINUX + return execmem_reserve_dual_linux(size, out); +#else + /* No dual-mapping primitive available: fall back to single + * mapping. Still W^X by mprotect transition (RW → RX). */ + return execmem_reserve_single(size, out); +#endif + } + return execmem_reserve_single(size, out); } static int execmem_protect(void* user, void* addr, size_t size, int prot) @@ -106,10 +254,24 @@ static int execmem_protect(void* user, void* addr, size_t size, int prot) return mprotect(addr, size, cfree_to_posix_prot(prot)); } -static void execmem_release(void* user, void* addr, size_t size) +static void execmem_release(void* user, CfreeExecMemRegion* region) { (void)user; - munmap(addr, size); + if (!region || !region->size) return; + if (region->token) { + ExecMemToken* tok = (ExecMemToken*)region->token; + if (tok->runtime_addr && tok->runtime_addr != tok->write_addr) + munmap(tok->runtime_addr, tok->size); + if (tok->write_addr) + munmap(tok->write_addr, tok->size); + free(tok); + } else if (region->write) { + munmap(region->write, region->size); + } + region->write = NULL; + region->runtime = NULL; + region->size = 0; + region->token = NULL; } static void execmem_flush_icache(void* user, void* addr, size_t size) @@ -122,7 +284,7 @@ static void execmem_flush_icache(void* user, void* addr, size_t size) #endif } -static size_t host_page_size(void) +static size_t driver_host_page_size(void) { long p = sysconf(_SC_PAGESIZE); return (p > 0) ? (size_t)p : (size_t)0x4000; @@ -301,7 +463,7 @@ void driver_env_init(DriverEnv* e) e->file_io.open_writer = posix_open_writer; e->file_io.user = e; - g_execmem_posix.page_size = host_page_size(); + g_execmem_posix.page_size = driver_host_page_size(); g_execmem_posix.reserve = execmem_reserve; g_execmem_posix.protect = execmem_protect; g_execmem_posix.release = execmem_release; diff --git a/include/cfree.h b/include/cfree.h @@ -221,18 +221,32 @@ typedef struct CfreeFileIO { /* Executable-memory vtable. Required by the JIT mapper (cfree_jit_from_image) * and the emu runtime; consulted by the linker for page-aligned segment * layout. May be NULL for hosts that never JIT and never run the emu — link - * layout falls back to a 16 KiB page in that case. Semantics mirror - * mmap/mprotect/munmap closely so a posix host needs only a thin shim. + * layout falls back to a 16 KiB page in that case. * - * reserve — allocate `size` bytes (page_size-aligned) with initial - * perms `prot`. Returns NULL on failure. - * protect — change perms of [addr, addr+size) (page_size-aligned) - * to `prot`. Returns 0 on success, nonzero on failure. - * release — free a prior reservation; addr/size match the reserve - * call. - * flush_icache — make freshly written instructions in [addr, addr+size) - * visible to the CPU. May be a no-op on x86; required on - * aarch64 before transferring control to JITed code. + * The vtable enforces strict W^X: no virtual page is ever simultaneously + * writable and executable. For regions that will eventually hold code + * (CFREE_PROT_EXEC in the requested perms) the host returns a dual mapping — + * two virtual addresses that alias the same physical memory, where the + * `write` alias has WRITE but never EXEC, and the `runtime` alias has + * EXEC after a corresponding protect() call but never WRITE. Callers + * populate code via the `write` alias and execute / take addresses against + * the `runtime` alias. For non-EXEC regions a single mapping suffices and + * write == runtime. + * + * reserve — allocate `size` bytes (page_size-aligned) whose final + * perms will be `prot`. On success returns 0 and fills + * *out (write/runtime/size/token); returns non-zero on + * failure. The returned `write` alias is always RW; + * `runtime` starts read-only and is flipped to final + * perms by protect(). + * protect — apply final perms via the runtime alias for [addr, + * addr+size) (page_size-aligned, lying inside the + * reservation's runtime alias). Returns 0 on success. + * release — free a prior reservation, including both aliases. + * flush_icache — make freshly written instructions visible to the CPU + * at [addr, addr+size) on the runtime alias. May be a + * no-op on x86; required on aarch64 before transferring + * control to JITed code. * * `prot` is a bitmask of CFREE_PROT_*. */ enum { @@ -242,11 +256,20 @@ enum { CFREE_PROT_EXEC = 1 << 2, }; +typedef struct CfreeExecMemRegion { + void* write; /* RW alias for population; never has EXEC */ + void* runtime; /* runtime/execution alias; never has WRITE. + For non-EXEC reservations equals `write`. */ + size_t size; /* page-aligned bytes */ + void* token; /* opaque host handle for release() */ +} CfreeExecMemRegion; + typedef struct CfreeExecMem { size_t page_size; - void* (*reserve) (void* user, size_t size, int prot); + int (*reserve) (void* user, size_t size, int prot, + CfreeExecMemRegion* out); int (*protect) (void* user, void* addr, size_t size, int prot); - void (*release) (void* user, void* addr, size_t size); + void (*release) (void* user, CfreeExecMemRegion* region); void (*flush_icache)(void* user, void* addr, size_t size); void* user; } CfreeExecMem; diff --git a/src/emu/runtime.c b/src/emu/runtime.c @@ -49,10 +49,10 @@ static u64 align_up_u64(u64 v, u64 a) } struct EmuCodeRegion { - Compiler* c; - void* base; - size_t size; - uintptr_t rx_end; /* high-water of pages currently RX */ + Compiler* c; + CfreeExecMemRegion region; /* dual-aliased on hosts that support it */ + uintptr_t rx_end; /* high-water of runtime-alias pages + currently flipped to RX */ }; EmuCodeRegion* emu_code_region_new(Compiler* c, size_t reserve_size) @@ -60,23 +60,28 @@ EmuCodeRegion* emu_code_region_new(Compiler* c, size_t reserve_size) Heap* h; const CfreeExecMem* mem; EmuCodeRegion* r; - void* p; size_t aligned; + CfreeExecMemRegion region; if (!c) return NULL; h = (Heap*)c->env->heap; mem = require_execmem(c); aligned = (size_t)align_up_u64((u64)reserve_size, page_size_bytes(mem)); - p = mem->reserve(mem->user, aligned, CFREE_PROT_NONE); - if (!p) return NULL; + /* Reserve as a code region. The host returns dual-mapped memory + * (writable alias / runtime alias) so the linker can write through + * the write alias while the runtime alias starts read-only and is + * flipped to RX page-by-page as cold blocks are committed. */ + if (mem->reserve(mem->user, aligned, + CFREE_PROT_READ | CFREE_PROT_EXEC, &region) != 0) { + return NULL; + } r = (EmuCodeRegion*)h->alloc(h, sizeof(*r), _Alignof(EmuCodeRegion)); - if (!r) { mem->release(mem->user, p, aligned); return NULL; } + if (!r) { mem->release(mem->user, &region); return NULL; } r->c = c; - r->base = p; - r->size = aligned; - r->rx_end = (uintptr_t)p; + r->region = region; + r->rx_end = (uintptr_t)region.runtime; return r; } @@ -87,20 +92,21 @@ void emu_code_region_free(EmuCodeRegion* r) if (!r) return; h = (Heap*)r->c->env->heap; mem = r->c->env->execmem; - if (r->base && r->size && mem && mem->release) { - mem->release(mem->user, r->base, r->size); + if (r->region.size && mem && mem->release) { + mem->release(mem->user, &r->region); } h->free(h, r, sizeof(*r)); } uintptr_t emu_code_region_base(const EmuCodeRegion* r) { - return r ? (uintptr_t)r->base : 0; + /* Runtime alias — what JIT'd block addresses must use. */ + return r ? (uintptr_t)r->region.runtime : 0; } size_t emu_code_region_size(const EmuCodeRegion* r) { - return r ? r->size : 0; + return r ? r->region.size : 0; } void emu_code_region_commit_rx_to(EmuCodeRegion* r, uintptr_t end) @@ -110,20 +116,20 @@ void emu_code_region_commit_rx_to(EmuCodeRegion* r, uintptr_t end) size_t len; if (!r) return; mem = require_execmem(r->c); - base = (uintptr_t)r->base; + base = (uintptr_t)r->region.runtime; page_end = (uintptr_t)align_up_u64((u64)end, page_size_bytes(mem)); /* Monotonic: never lower the high-water; chaining patches * already-committed code and depends on it staying RX. */ if (page_end <= r->rx_end) return; - if (page_end > base + r->size) page_end = base + r->size; + if (page_end > base + r->region.size) page_end = base + r->region.size; if (page_end <= r->rx_end) return; len = (size_t)(page_end - r->rx_end); - /* Linker has already written + relocated the section bytes via - * the original PROT_NONE mapping (which is technically a fault - * unless the mapping was promoted to RW). The actual write path - * is owned by link_resolve_extend; in v1 we expect the linker - * to flip to RW prior to writing. RX flip happens here. */ + /* Bytes are written through the WRITE alias by link_resolve_extend + * (a stub today). The runtime alias starts at PROT_READ and we flip + * it to PROT_READ|PROT_EXEC here; W^X is preserved because the two + * aliases are distinct VAs and neither holds W and X at the same + * time. */ if (mem->protect(mem->user, (void*)r->rx_end, len, CFREE_PROT_READ | CFREE_PROT_EXEC) == 0) { if (mem->flush_icache) { diff --git a/src/link/link_jit.c b/src/link/link_jit.c @@ -38,11 +38,18 @@ static u64 jit_page_size(Compiler* c) return m->page_size ? (u64)m->page_size : 0x4000u; } +/* Per-segment runtime placement. Each segment gets its own host + * reservation. Code segments come back as a dual mapping (write alias / + * runtime alias backing the same physical pages); data and rodata are + * single-aliased. We populate via the write alias, encode runtime + * addresses against the runtime alias, then protect the runtime alias to + * its final perms. No virtual page is ever simultaneously writable and + * executable. */ struct CfreeJit { - Compiler* c; - LinkImage* image; - void* base; - size_t map_size; + Compiler* c; + LinkImage* image; + CfreeExecMemRegion* segs; /* one per image->nsegments */ + u32 nsegs; }; static int perms_for(u32 secflags) @@ -53,14 +60,50 @@ static int perms_for(u32 secflags) return p; } +/* Find the segment that contains image-relative `vaddr` and return its + * runtime address (the runtime alias, not the write alias). Up to 3 + * segments after layout, so a linear scan is fine. */ +static uintptr_t vaddr_to_runtime(const LinkImage* img, + const CfreeExecMemRegion* segs, + u64 vaddr) +{ + u32 i; + for (i = 0; i < img->nsegments; ++i) { + const LinkSegment* s = &img->segments[i]; + u64 lo = s->vaddr; + u64 hi = lo + s->mem_size; + if (vaddr >= lo && vaddr < hi) + return (uintptr_t)segs[i].runtime + (uintptr_t)(vaddr - lo); + } + return 0; +} + +/* Same as above but returns the WRITE-alias address — used to determine + * the byte position at which to apply a relocation. The PC-relative + * arithmetic in link_reloc_apply uses the runtime address; the bytes + * themselves get patched at the matching offset of the write alias. */ +static uintptr_t vaddr_to_write(const LinkImage* img, + const CfreeExecMemRegion* segs, + u64 vaddr) +{ + u32 i; + for (i = 0; i < img->nsegments; ++i) { + const LinkSegment* s = &img->segments[i]; + u64 lo = s->vaddr; + u64 hi = lo + s->mem_size; + if (vaddr >= lo && vaddr < hi) + return (uintptr_t)segs[i].write + (uintptr_t)(vaddr - lo); + } + return 0; +} + CfreeJit* cfree_jit_from_image(LinkImage* img) { Compiler* c; Heap* heap; const CfreeExecMem* mem; CfreeJit* jit; - void* base; - size_t map_size = 0; + CfreeExecMemRegion* segs; u64 page; u32 i; @@ -70,37 +113,48 @@ CfreeJit* cfree_jit_from_image(LinkImage* img) mem = require_execmem(c); page = jit_page_size(c); - /* Total mapping size = top of last segment, page-aligned. */ if (img->nsegments == 0) { compiler_panic(c, no_loc(), "cfree_jit_from_image: image has no segments"); } - for (i = 0; i < img->nsegments; ++i) { - const LinkSegment* seg = &img->segments[i]; - u64 end = seg->vaddr + align_up_u64(seg->mem_size, page); - if (end > map_size) map_size = (size_t)end; - } - map_size = (size_t)align_up_u64((u64)map_size, page); - base = mem->reserve(mem->user, map_size, - CFREE_PROT_READ | CFREE_PROT_WRITE); - if (!base) { + segs = (CfreeExecMemRegion*)heap->alloc( + heap, sizeof(*segs) * img->nsegments, + _Alignof(CfreeExecMemRegion)); + if (!segs) { compiler_panic(c, no_loc(), - "cfree_jit_from_image: execmem.reserve failed"); + "cfree_jit_from_image: oom on segment table"); } - /* Reservation is required to be zeroed — matches mmap MAP_ANON - * semantics; BSS is naturally zero. */ + memset(segs, 0, sizeof(*segs) * img->nsegments); - /* Copy each segment's file bytes to (base + vaddr). */ + /* Reserve each segment with its FINAL perms. For EXEC segments the + * host returns a dual mapping (write alias / runtime alias); for + * data/rodata the two aliases coincide. */ + for (i = 0; i < img->nsegments; ++i) { + const LinkSegment* seg = &img->segments[i]; + size_t mlen = (size_t)align_up_u64(seg->mem_size, page); + if (mem->reserve(mem->user, mlen, perms_for(seg->flags), + &segs[i]) != 0) { + u32 j; + for (j = 0; j < i; ++j) mem->release(mem->user, &segs[j]); + heap->free(heap, segs, sizeof(*segs) * img->nsegments); + compiler_panic(c, no_loc(), + "cfree_jit_from_image: execmem.reserve failed"); + } + } + /* Reservations are zeroed; BSS is naturally zero. */ + + /* Copy each segment's file bytes to its write alias. */ for (i = 0; i < img->nsegments; ++i) { const LinkSegment* seg = &img->segments[i]; if (seg->file_size == 0) continue; - memcpy((u8*)base + seg->vaddr, + memcpy(segs[i].write, img->segment_bytes[i], (size_t)seg->file_size); } - /* Apply relocations with runtime base. */ + /* Apply relocations. The patch site bytes go through the write + * alias; PC-relative arithmetic uses the runtime alias address. */ for (i = 0; i < img->nrelocs; ++i) { const LinkRelocApply* r = &img->relocs[i]; const LinkSymbol* tgt = &img->syms[r->target - 1]; @@ -111,40 +165,53 @@ CfreeJit* cfree_jit_from_image(LinkImage* img) * already holds the runtime address. */ S = tgt->vaddr; } else { - S = tgt->vaddr + (u64)(uintptr_t)base; + S = (u64)vaddr_to_runtime(img, segs, tgt->vaddr); } - P = r->write_vaddr + (u64)(uintptr_t)base; - P_bytes = (u8*)base + r->write_vaddr; + P = (u64)vaddr_to_runtime(img, segs, r->write_vaddr); + P_bytes = (u8*)vaddr_to_write(img, segs, r->write_vaddr); link_reloc_apply(c, r->kind, P_bytes, S, r->addend, P); } - /* Flush the data caches we just wrote and invalidate the icache - * so the CPU sees the new instructions before the protect flip. - * Host decides whether this is a no-op (typical on x86). */ - if (mem->flush_icache) mem->flush_icache(mem->user, base, map_size); - - /* Flip permissions per segment. */ + /* Flip the runtime alias of each segment to its final perms. The + * write alias is unaffected (still RW for any segment we'd want to + * write to from JITed code; for EXEC segments the write alias is + * orphaned after this point — JITed code is not expected to write + * to its own code). */ for (i = 0; i < img->nsegments; ++i) { const LinkSegment* seg = &img->segments[i]; - size_t mlen = (size_t)align_up_u64(seg->mem_size, page); - if (mem->protect(mem->user, (u8*)base + seg->vaddr, mlen, + if (mem->protect(mem->user, segs[i].runtime, segs[i].size, perms_for(seg->flags)) != 0) { - mem->release(mem->user, base, map_size); + u32 j; + for (j = 0; j < img->nsegments; ++j) + mem->release(mem->user, &segs[j]); + heap->free(heap, segs, sizeof(*segs) * img->nsegments); compiler_panic(c, no_loc(), "cfree_jit_from_image: execmem.protect failed"); } } + /* Flush only the segments that will be executed, against the + * runtime alias (the address from which the CPU will fetch). */ + if (mem->flush_icache) { + for (i = 0; i < img->nsegments; ++i) { + const LinkSegment* seg = &img->segments[i]; + if (seg->flags & SF_EXEC) + mem->flush_icache(mem->user, segs[i].runtime, segs[i].size); + } + } + jit = (CfreeJit*)heap->alloc(heap, sizeof(*jit), _Alignof(CfreeJit)); if (!jit) { - mem->release(mem->user, base, map_size); + for (i = 0; i < img->nsegments; ++i) + mem->release(mem->user, &segs[i]); + heap->free(heap, segs, sizeof(*segs) * img->nsegments); compiler_panic(c, no_loc(), "cfree_jit_from_image: oom on jit handle"); } - jit->c = c; - jit->image = img; - jit->base = base; - jit->map_size = map_size; + jit->c = c; + jit->image = img; + jit->segs = segs; + jit->nsegs = img->nsegments; /* Take ownership of the image: undefer it from the compiler so a * future panic doesn't reap something we still hold. */ @@ -173,11 +240,18 @@ void cfree_jit_free(CfreeJit* jit) { Heap* heap; const CfreeExecMem* mem; + u32 i; if (!jit) return; heap = (Heap*)jit->c->env->heap; mem = jit->c->env->execmem; - if (jit->base && jit->map_size && mem && mem->release) { - mem->release(mem->user, jit->base, jit->map_size); + if (jit->segs && mem && mem->release) { + for (i = 0; i < jit->nsegs; ++i) { + if (jit->segs[i].size) + mem->release(mem->user, &jit->segs[i]); + } + } + if (jit->segs) { + heap->free(heap, jit->segs, sizeof(*jit->segs) * jit->nsegs); } if (jit->image) { /* link_image_free unfederes (no-op now) and releases storage. */ @@ -198,7 +272,7 @@ void* cfree_jit_lookup(CfreeJit* jit, const char* name) s = &jit->image->syms[id - 1]; if (!s->defined) return NULL; if (s->kind == SK_ABS) return (void*)(uintptr_t)s->vaddr; - return (u8*)jit->base + s->vaddr; + return (void*)vaddr_to_runtime(jit->image, jit->segs, s->vaddr); } /* ---- inspector entries (stubs; out of scope for this cut) ---- */ diff --git a/test/cg/harness/cg_runner.c b/test/cg/harness/cg_runner.c @@ -54,7 +54,23 @@ static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc, } static CfreeDiagSink g_diag = { diag_emit, NULL, 0, 0 }; -/* posix-backed CfreeExecMem for the JIT path. */ +/* posix-backed CfreeExecMem for the JIT path. Mirrors driver/env.c — see + * that file for the strict-W^X dual-mapping rationale. Apple uses + * mach_vm_remap; Linux uses memfd_create + dual mmap; other POSIX falls + * back to a single mapping with mprotect transitions. */ +#if defined(__APPLE__) +# include <mach/mach.h> +# include <mach/mach_vm.h> +# define XM_DUAL_APPLE 1 +#else +# define XM_DUAL_APPLE 0 +#endif +#if defined(__linux__) +# include <sys/syscall.h> +# define XM_DUAL_LINUX 1 +#else +# define XM_DUAL_LINUX 0 +#endif static int xm_to_posix(int p) { int q = 0; @@ -63,15 +79,82 @@ static int xm_to_posix(int p) if (p & CFREE_PROT_EXEC) q |= PROT_EXEC; return q; } -static void* xm_reserve(void* u, size_t n, int p) +typedef struct XmTok { void* w; void* r; size_t n; } XmTok; +static int xm_reserve_single(size_t n, CfreeExecMemRegion* out) +{ + void* p = mmap(NULL, n, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANON, -1, 0); + if (p == MAP_FAILED) return 1; + out->write = out->runtime = p; out->size = n; out->token = NULL; + return 0; +} +static int xm_reserve(void* u, size_t n, int p, CfreeExecMemRegion* out) { (void)u; - void* a = mmap(NULL, n, xm_to_posix(p), MAP_PRIVATE | MAP_ANON, -1, 0); - return a == MAP_FAILED ? NULL : a; + if (!out || !n) return 1; + if (!(p & CFREE_PROT_EXEC)) return xm_reserve_single(n, out); +#if XM_DUAL_APPLE + { + void* w = mmap(NULL, n, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANON, -1, 0); + mach_vm_address_t r = 0; + vm_prot_t cur = 0, max = 0; + XmTok* tok; + if (w == MAP_FAILED) return 1; + if (mach_vm_remap(mach_task_self(), &r, (mach_vm_size_t)n, 0, + VM_FLAGS_ANYWHERE, + mach_task_self(), (mach_vm_address_t)(uintptr_t)w, + FALSE, &cur, &max, VM_INHERIT_NONE) + != KERN_SUCCESS) { munmap(w, n); return 1; } + if (mprotect((void*)(uintptr_t)r, n, PROT_READ) != 0) { + munmap((void*)(uintptr_t)r, n); munmap(w, n); return 1; + } + tok = (XmTok*)malloc(sizeof(*tok)); + if (!tok) { munmap((void*)(uintptr_t)r, n); munmap(w, n); return 1; } + tok->w = w; tok->r = (void*)(uintptr_t)r; tok->n = n; + out->write = w; out->runtime = (void*)(uintptr_t)r; + out->size = n; out->token = tok; + return 0; + } +#elif XM_DUAL_LINUX + { + int fd = (int)syscall(SYS_memfd_create, "cfree-jit-test", 0u); + void *w, *r; XmTok* tok; + if (fd < 0) return 1; + if (ftruncate(fd, (off_t)n) != 0) { close(fd); return 1; } + w = mmap(NULL, n, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (w == MAP_FAILED) { close(fd); return 1; } + r = mmap(NULL, n, PROT_READ, MAP_SHARED, fd, 0); + close(fd); + if (r == MAP_FAILED) { munmap(w, n); return 1; } + tok = (XmTok*)malloc(sizeof(*tok)); + if (!tok) { munmap(r, n); munmap(w, n); return 1; } + tok->w = w; tok->r = r; tok->n = n; + out->write = w; out->runtime = r; out->size = n; out->token = tok; + return 0; + } +#else + return xm_reserve_single(n, out); +#endif } static int xm_protect(void* u, void* a, size_t n, int p) { (void)u; return mprotect(a, n, xm_to_posix(p)); } -static void xm_release(void* u, void* a, size_t n) { (void)u; munmap(a, n); } +static void xm_release(void* u, CfreeExecMemRegion* region) +{ + (void)u; + if (!region || !region->size) return; + if (region->token) { + XmTok* tok = (XmTok*)region->token; + if (tok->r && tok->r != tok->w) munmap(tok->r, tok->n); + if (tok->w) munmap(tok->w, tok->n); + free(tok); + } else if (region->write) { + munmap(region->write, region->size); + } + region->write = region->runtime = NULL; + region->size = 0; + region->token = NULL; +} static void xm_flush(void* u, void* a, size_t n) { (void)u; diff --git a/test/link/harness/jit_runner.c b/test/link/harness/jit_runner.c @@ -45,6 +45,21 @@ static void diag_fn(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc, } static CfreeDiagSink g_diag = { diag_fn, NULL, 0, 0 }; +/* Mirrors driver/env.c — see that file for the strict-W^X dual-mapping + * rationale. */ +#if defined(__APPLE__) +# include <mach/mach.h> +# include <mach/mach_vm.h> +# define XM_DUAL_APPLE 1 +#else +# define XM_DUAL_APPLE 0 +#endif +#if defined(__linux__) +# include <sys/syscall.h> +# define XM_DUAL_LINUX 1 +#else +# define XM_DUAL_LINUX 0 +#endif static int xm_to_posix(int p) { int q = 0; @@ -53,15 +68,82 @@ static int xm_to_posix(int p) if (p & CFREE_PROT_EXEC) q |= PROT_EXEC; return q; } -static void* xm_reserve(void* u, size_t n, int p) +typedef struct XmTok { void* w; void* r; size_t n; } XmTok; +static int xm_reserve_single(size_t n, CfreeExecMemRegion* out) +{ + void* p = mmap(NULL, n, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANON, -1, 0); + if (p == MAP_FAILED) return 1; + out->write = out->runtime = p; out->size = n; out->token = NULL; + return 0; +} +static int xm_reserve(void* u, size_t n, int p, CfreeExecMemRegion* out) { (void)u; - void* a = mmap(NULL, n, xm_to_posix(p), MAP_PRIVATE | MAP_ANON, -1, 0); - return a == MAP_FAILED ? NULL : a; + if (!out || !n) return 1; + if (!(p & CFREE_PROT_EXEC)) return xm_reserve_single(n, out); +#if XM_DUAL_APPLE + { + void* w = mmap(NULL, n, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANON, -1, 0); + mach_vm_address_t r = 0; + vm_prot_t cur = 0, max = 0; + XmTok* tok; + if (w == MAP_FAILED) return 1; + if (mach_vm_remap(mach_task_self(), &r, (mach_vm_size_t)n, 0, + VM_FLAGS_ANYWHERE, + mach_task_self(), (mach_vm_address_t)(uintptr_t)w, + FALSE, &cur, &max, VM_INHERIT_NONE) + != KERN_SUCCESS) { munmap(w, n); return 1; } + if (mprotect((void*)(uintptr_t)r, n, PROT_READ) != 0) { + munmap((void*)(uintptr_t)r, n); munmap(w, n); return 1; + } + tok = (XmTok*)malloc(sizeof(*tok)); + if (!tok) { munmap((void*)(uintptr_t)r, n); munmap(w, n); return 1; } + tok->w = w; tok->r = (void*)(uintptr_t)r; tok->n = n; + out->write = w; out->runtime = (void*)(uintptr_t)r; + out->size = n; out->token = tok; + return 0; + } +#elif XM_DUAL_LINUX + { + int fd = (int)syscall(SYS_memfd_create, "cfree-jit-test", 0u); + void *w, *r; XmTok* tok; + if (fd < 0) return 1; + if (ftruncate(fd, (off_t)n) != 0) { close(fd); return 1; } + w = mmap(NULL, n, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (w == MAP_FAILED) { close(fd); return 1; } + r = mmap(NULL, n, PROT_READ, MAP_SHARED, fd, 0); + close(fd); + if (r == MAP_FAILED) { munmap(w, n); return 1; } + tok = (XmTok*)malloc(sizeof(*tok)); + if (!tok) { munmap(r, n); munmap(w, n); return 1; } + tok->w = w; tok->r = r; tok->n = n; + out->write = w; out->runtime = r; out->size = n; out->token = tok; + return 0; + } +#else + return xm_reserve_single(n, out); +#endif } static int xm_protect(void* u, void* a, size_t n, int p) { (void)u; return mprotect(a, n, xm_to_posix(p)); } -static void xm_release(void* u, void* a, size_t n) { (void)u; munmap(a, n); } +static void xm_release(void* u, CfreeExecMemRegion* region) +{ + (void)u; + if (!region || !region->size) return; + if (region->token) { + XmTok* tok = (XmTok*)region->token; + if (tok->r && tok->r != tok->w) munmap(tok->r, tok->n); + if (tok->w) munmap(tok->w, tok->n); + free(tok); + } else if (region->write) { + munmap(region->write, region->size); + } + region->write = region->runtime = NULL; + region->size = 0; + region->token = NULL; +} static void xm_flush(void* u, void* a, size_t n) { (void)u;