commit 0f32ede0778411171ae700103652e8a02c781697
parent debe1032c9ec9285e04e7194e61cfc3411489f84
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Thu, 28 May 2026 08:57:49 -0700
driver/env: split into per-OS/arch TUs, drop ifdefs from impl
driver/env.c was a single 1848-line TU laced with __APPLE__/__linux__/__x86_64__/
__aarch64__/__riscv ifdefs. Replace it with a directory of focused TUs and
let the Makefile pick the right subset by HOST_UNAME and HOST_ARCH:
common.c libc-only (heap, diag, writers, printf/strneq/...)
posix.c mac/linux/freebsd-shared (file_io, mkdir, sigint,
exec_dual registry, single-mapping execmem, env_init)
posix_dbg.c pthreads + sigaction frame + sigsetjmp guarded_copy
jit_tls_posix.c pthread_key TLS for the Mach-O TLV thunk
macos.c mach_vm_remap dual-map, sys_icache_invalidate,
st_mtimespec, dlsym _-strip, OS=MACOS/OBJ=MACHO
linux.c memfd_create dual-map, st_mtim, OS=LINUX/OBJ=ELF
freebsd.c memfd_create dual-map (UNTESTED), OS=FREEBSD/OBJ=ELF
windows.c #error stub; comment sketches what a real port needs
icache_{x86,arm,riscv}.c arch-specific env_flush_icache
uctx_{aarch64_macos,aarch64_linux,
x86_64_linux,rv64_linux}.c ucontext <-> CfreeUnwindFrame
linux_exec_hint_{x86_64,default}.c MAP_32BIT runtime-alias hint axis,
kept out of linux.c so no arch ifdef
env_internal.h declares the os_*/env_* hooks each TU implements
Per-OS feature-test macros (_XOPEN_SOURCE, _DARWIN_C_SOURCE, _GNU_SOURCE)
move from the top of env.c into DRIVER_ENV_OS_CFLAGS so the impl files
themselves carry zero conditionals. Cross-TU coupling at the libcfree
boundary still goes through CfreeExecMem/CfreeDbgOs/CfreeJitTls vtables
because libcfree is freestanding; inside driver/env/ the per-OS hooks
are plain extern functions resolved at link time.
driver/env.h is unchanged (callers across the driver don't move).
Diffstat:
20 files changed, 2276 insertions(+), 1853 deletions(-)
diff --git a/Makefile b/Makefile
@@ -9,6 +9,22 @@ BUILD_DIR ?= build
endif
SYSROOT = $(shell xcrun --show-sdk-path)
HOST_UNAME := $(shell uname -s)
+
+# Normalize uname -m to one of {x86_64, aarch64, rv64, unknown}. Used by
+# driver/env to select per-arch TUs (icache flush, uctx marshalling,
+# Linux exec runtime-alias hint) -- see "driver/env source selection"
+# below.
+HOST_ARCH_RAW := $(shell uname -m)
+ifneq ($(filter $(HOST_ARCH_RAW),x86_64 amd64),)
+HOST_ARCH := x86_64
+else ifneq ($(filter $(HOST_ARCH_RAW),aarch64 arm64),)
+HOST_ARCH := aarch64
+else ifneq ($(filter $(HOST_ARCH_RAW),riscv64),)
+HOST_ARCH := rv64
+else
+HOST_ARCH := unknown
+endif
+
.DEFAULT_GOAL := all
ifeq ($(RELEASE),1)
@@ -77,12 +93,80 @@ LIB_CFLAGS = $(FREESTANDING_CFLAGS) $(LIB_VISIBILITY_CFLAGS) -Iinclude -Isrc
# that's what makes the driver the first consumer of libcfree. -Ilang lets `cc`
# reach the C frontend's public header ("c/c.h") for the JIT REPL; it
# deliberately does NOT get -Isrc, so internal headers ("core/...", "link/...")
-# are unreachable from the driver. driver/env.c is the sole hosted OS/libc
-# adapter and is compiled with DRIVER_ENV_CFLAGS below.
+# are unreachable from the driver.
+#
+# driver/env/ holds all hosted OS/libc adapter code and is compiled with
+# DRIVER_ENV_CFLAGS. The OS-specific feature-test macros that the env TUs
+# need before any libc header is included live in DRIVER_ENV_OS_CFLAGS,
+# set under "driver/env source selection" below.
DRIVER_CFLAGS = $(FREESTANDING_CFLAGS) -Iinclude -Ilang
DRIVER_ENV_CFLAGS = $(HOST_CFLAGS) -Iinclude -Ilang
TEST_HOST_CFLAGS = $(HOST_CFLAGS) -Iinclude -Ilang
+# driver/env source selection. One file per OS, one per arch (icache),
+# one per (arch, OS) (uctx ucontext marshalling). Removing OS/arch ifdefs
+# from the impl is the explicit point of this layout.
+ifeq ($(HOST_UNAME),Darwin)
+DRIVER_ENV_OS_CFLAGS := -D_XOPEN_SOURCE=600 -D_DARWIN_C_SOURCE=1
+DRIVER_ENV_OS_SRC := driver/env/macos.c
+DRIVER_ENV_HINT_SRC :=
+else ifeq ($(HOST_UNAME),Linux)
+DRIVER_ENV_OS_CFLAGS := -D_GNU_SOURCE=1
+DRIVER_ENV_OS_SRC := driver/env/linux.c
+ifeq ($(HOST_ARCH),x86_64)
+DRIVER_ENV_HINT_SRC := driver/env/linux_exec_hint_x86_64.c
+else
+DRIVER_ENV_HINT_SRC := driver/env/linux_exec_hint_default.c
+endif
+else ifeq ($(HOST_UNAME),FreeBSD)
+DRIVER_ENV_OS_CFLAGS :=
+DRIVER_ENV_OS_SRC := driver/env/freebsd.c
+DRIVER_ENV_HINT_SRC :=
+else
+$(error driver/env: unsupported HOST_UNAME=$(HOST_UNAME); see driver/env/windows.c)
+endif
+
+ifeq ($(HOST_ARCH),x86_64)
+DRIVER_ENV_ICACHE_SRC := driver/env/icache_x86.c
+else ifeq ($(HOST_ARCH),aarch64)
+DRIVER_ENV_ICACHE_SRC := driver/env/icache_arm.c
+else ifeq ($(HOST_ARCH),rv64)
+DRIVER_ENV_ICACHE_SRC := driver/env/icache_riscv.c
+else
+$(error driver/env: unsupported HOST_ARCH=$(HOST_ARCH))
+endif
+
+DRIVER_ENV_UCTX_SRC :=
+ifeq ($(HOST_UNAME),Darwin)
+ifeq ($(HOST_ARCH),aarch64)
+DRIVER_ENV_UCTX_SRC := driver/env/uctx_aarch64_macos.c
+endif
+endif
+ifeq ($(HOST_UNAME),Linux)
+ifeq ($(HOST_ARCH),aarch64)
+DRIVER_ENV_UCTX_SRC := driver/env/uctx_aarch64_linux.c
+endif
+ifeq ($(HOST_ARCH),x86_64)
+DRIVER_ENV_UCTX_SRC := driver/env/uctx_x86_64_linux.c
+endif
+ifeq ($(HOST_ARCH),rv64)
+DRIVER_ENV_UCTX_SRC := driver/env/uctx_rv64_linux.c
+endif
+endif
+ifeq ($(DRIVER_ENV_UCTX_SRC),)
+$(error driver/env: no ucontext marshalling for HOST_UNAME=$(HOST_UNAME) HOST_ARCH=$(HOST_ARCH))
+endif
+
+DRIVER_ENV_SRCS := \
+ driver/env/common.c \
+ driver/env/posix.c \
+ driver/env/posix_dbg.c \
+ driver/env/jit_tls_posix.c \
+ $(DRIVER_ENV_OS_SRC) \
+ $(DRIVER_ENV_HINT_SRC) \
+ $(DRIVER_ENV_ICACHE_SRC) \
+ $(DRIVER_ENV_UCTX_SRC)
+
include mk/config.mk
# Core lib sources. Optional subsystems, backend directories, object format
@@ -286,7 +370,7 @@ LIB_OBJS = $(patsubst src/%.c,$(BUILD_DIR)/lib/%.o,$(LIB_SRCS)) \
LIB_DEPS = $(LIB_OBJS:.o=.d)
LIB_RELOC_OBJ = $(BUILD_DIR)/libcfree.o
-DRIVER_SRCS = driver/main.c driver/env.c driver/target.c
+DRIVER_SRCS = driver/main.c driver/target.c $(DRIVER_ENV_SRCS)
DRIVER_TOOL_SRCS =
ifeq ($(CFREE_TOOL_CC_ENABLED),1)
DRIVER_TOOL_SRCS += driver/cc.c
@@ -437,9 +521,9 @@ $(BUILD_DIR)/driver/%.o: driver/%.c Makefile $(BUILD_CONFIG)
@mkdir -p $(dir $@)
$(CC) $(DRIVER_CFLAGS) $(DEPFLAGS) -c $< -o $@
-$(BUILD_DIR)/driver/env.o: driver/env.c Makefile $(BUILD_CONFIG)
+$(BUILD_DIR)/driver/env/%.o: driver/env/%.c Makefile $(BUILD_CONFIG)
@mkdir -p $(dir $@)
- $(CC) $(DRIVER_ENV_CFLAGS) $(DEPFLAGS) -c $< -o $@
+ $(CC) $(DRIVER_ENV_CFLAGS) $(DRIVER_ENV_OS_CFLAGS) $(DEPFLAGS) -c $< -o $@
$(BUILD_DIR)/lang/toy/%.o: lang/toy/%.c Makefile $(BUILD_CONFIG)
@mkdir -p $(dir $@)
diff --git a/driver/env.c b/driver/env.c
@@ -1,1848 +0,0 @@
-/* ucontext.h is technically deprecated by POSIX but signal handlers
- * cannot snapshot register state without it. macOS gates the header on
- * _XOPEN_SOURCE; pair it with _DARWIN_C_SOURCE so MAP_ANON / RTLD_DEFAULT
- * (which the SUS feature macros would otherwise strip) stay visible. */
-#if defined(__APPLE__)
-#ifndef _XOPEN_SOURCE
-#define _XOPEN_SOURCE 600
-#endif
-#ifndef _DARWIN_C_SOURCE
-#define _DARWIN_C_SOURCE 1
-#endif
-#endif
-#if defined(__linux__) && !defined(_GNU_SOURCE)
-#define _GNU_SOURCE 1
-#endif
-
-#include <dlfcn.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <pthread.h>
-#include <setjmp.h>
-#include <signal.h>
-#include <stdarg.h>
-#include <stdint.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/mman.h>
-#include <sys/stat.h>
-#include <sys/wait.h>
-#include <time.h>
-#include <ucontext.h>
-#include <unistd.h>
-
-#if defined(__APPLE__)
-#include <libkern/OSCacheControl.h>
-#endif
-
-#include "driver.h"
-#include "env.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
-#if defined(__x86_64__) && defined(MAP_32BIT)
-#define DRIVER_MAP_32BIT MAP_32BIT
-static uintptr_t g_execmem_low_runtime_hint = 0x40000000u;
-static void* execmem_low_runtime_hint(size_t size) {
- uintptr_t p = g_execmem_low_runtime_hint;
- uintptr_t step = (uintptr_t)((size + 0xffffu) & ~(size_t)0xffffu);
- if (step < 0x10000u) step = 0x10000u;
- g_execmem_low_runtime_hint = p + step + 0x10000u;
- if (g_execmem_low_runtime_hint > 0x78000000u)
- g_execmem_low_runtime_hint = 0x40000000u;
- return (void*)p;
-}
-#else
-#define DRIVER_MAP_32BIT 0
-static void* execmem_low_runtime_hint(size_t size) {
- (void)size;
- return NULL;
-}
-#endif
-#else
-#define DRIVER_DUAL_LINUX 0
-#endif
-
-/* Host-side implementations of the three vtable interfaces libcfree needs:
- *
- * - DriverHeapLibc — malloc/realloc/free
- * - DriverDiagStderr — vfprintf to stderr, with kind labels
- * - DriverFdWriter — write/lseek/close on a POSIX fd
- *
- * Plus the POSIX CfreeFileIO that opens paths and hands the resulting fd
- * to a freshly-allocated DriverFdWriter. None of this lives in libcfree:
- * the core never depends on stdio, malloc, or POSIX I/O directly. */
-
-/* ---------------- heap (libc-backed) ---------------- */
-
-static void* heap_libc_alloc(CfreeHeap* h, size_t size, size_t align) {
- (void)h;
- (void)align; /* malloc satisfies all max_align_t alignments */
- return size ? malloc(size) : NULL;
-}
-
-static void* heap_libc_realloc(CfreeHeap* h, void* p, size_t old_size,
- size_t new_size, size_t align) {
- (void)h;
- (void)old_size;
- (void)align;
- return realloc(p, new_size);
-}
-
-static void heap_libc_free(CfreeHeap* h, void* p, size_t size) {
- (void)h;
- (void)size;
- free(p);
-}
-
-static CfreeHeap g_heap_libc = {
- heap_libc_alloc,
- heap_libc_realloc,
- heap_libc_free,
- NULL,
-};
-
-/* ---------------- diag sink (stderr) ---------------- */
-
-static const char* diag_label(CfreeDiagKind k) {
- switch (k) {
- case CFREE_DIAG_NOTE:
- return "note";
- case CFREE_DIAG_WARN:
- return "warning";
- case CFREE_DIAG_ERROR:
- return "error";
- case CFREE_DIAG_FATAL:
- return "fatal";
- }
- return "diag";
-}
-
-/* Tracks the compiler currently driving libcfree calls so the stderr
- * diag sink can resolve loc.file_id to the source's spelling (path or
- * memory-input label). NULL falls back to the numeric `<file:%u>` form,
- * which is what callers see before they've registered a compiler. */
-static CfreeCompiler* g_diag_active_compiler;
-
-void driver_diag_set_compiler(CfreeCompiler* c) { g_diag_active_compiler = c; }
-
-/* driver_compiler_{new,free}: thin wrappers around the libcfree
- * lifecycle entries that register the active compiler with the diag sink
- * and clear it on free. Driver tools call these instead of the raw
- * cfree_* calls so a fatal diagnostic from libcfree prints the source
- * file name rather than a bare numeric file_id. */
-CfreeStatus driver_compiler_new(CfreeTarget t, const CfreeContext* ctx,
- CfreeCompiler** out) {
- CfreeCompiler* c = NULL;
- CfreeStatus st = cfree_compiler_new(t, ctx, &c);
- if (st != CFREE_OK) {
- if (out) *out = NULL;
- return st;
- }
- driver_diag_set_compiler(c);
- if (out) *out = c;
- return CFREE_OK;
-}
-
-void driver_compiler_free(CfreeCompiler* c) {
- if (!c) return;
- if (g_diag_active_compiler == c) driver_diag_set_compiler(NULL);
- cfree_compiler_free(c);
-}
-
-static void diag_stderr_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc,
- const char* fmt, va_list ap) {
- (void)s;
- if (loc.file_id || loc.line) {
- CfreeSlice name =
- cfree_compiler_file_name(g_diag_active_compiler, loc.file_id);
- if (name.len) {
- fprintf(stderr, "%.*s:%u:%u: %.*s: ", CFREE_SLICE_ARG(name), loc.line,
- loc.col, CFREE_SLICE_ARG(cfree_slice_cstr(diag_label(k))));
- } else {
- fprintf(stderr, "<file:%u>:%u:%u: %.*s: ", loc.file_id, loc.line, loc.col,
- CFREE_SLICE_ARG(cfree_slice_cstr(diag_label(k))));
- }
- } else {
- fprintf(stderr, "%.*s: ", CFREE_SLICE_ARG(cfree_slice_cstr(diag_label(k))));
- }
- vfprintf(stderr, fmt, ap);
- fputc('\n', stderr);
-}
-
-static CfreeDiagSink g_diag_stderr = {
- diag_stderr_emit,
- NULL,
- 0,
- 0,
-};
-
-/* ---------------- exec memory (mmap-backed) ---------------- */
-
-static int cfree_to_posix_prot(int prot) {
- int p = 0;
- if (prot & CFREE_PROT_READ) p |= PROT_READ;
- if (prot & CFREE_PROT_WRITE) p |= PROT_WRITE;
- if (prot & CFREE_PROT_EXEC) p |= PROT_EXEC;
- return p;
-}
-
-/* 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;
-
-/* Registry of EXEC reservations with distinct write/runtime aliases. The
- * dbg_os code_write_begin path uses this to translate a runtime address
- * into the corresponding write alias on dual-mapping hosts. Single-mapping
- * reservations (write == runtime) are not registered. JITs typically hold
- * 1-2 reservations live so a linked list keeps the lookup trivial. */
-typedef struct ExecDualNode {
- void* write_base;
- void* runtime_base;
- size_t size;
- struct ExecDualNode* next;
-} ExecDualNode;
-
-static ExecDualNode* g_jit_dual_map;
-static pthread_mutex_t g_jit_dual_map_mu = PTHREAD_MUTEX_INITIALIZER;
-
-static void exec_dual_register(void* write_base, void* runtime_base,
- size_t size) {
- ExecDualNode* n;
- if (write_base == runtime_base) return;
- n = (ExecDualNode*)malloc(sizeof(*n));
- if (!n) return; /* registry is best-effort; lookup will fail open */
- n->write_base = write_base;
- n->runtime_base = runtime_base;
- n->size = size;
- pthread_mutex_lock(&g_jit_dual_map_mu);
- n->next = g_jit_dual_map;
- g_jit_dual_map = n;
- pthread_mutex_unlock(&g_jit_dual_map_mu);
-}
-
-static void exec_dual_unregister(void* runtime_base) {
- ExecDualNode** pp;
- pthread_mutex_lock(&g_jit_dual_map_mu);
- for (pp = &g_jit_dual_map; *pp; pp = &(*pp)->next) {
- if ((*pp)->runtime_base == runtime_base) {
- ExecDualNode* dead = *pp;
- *pp = dead->next;
- free(dead);
- break;
- }
- }
- pthread_mutex_unlock(&g_jit_dual_map_mu);
-}
-
-static int exec_dual_lookup(void* runtime_addr, size_t n, void** write_out) {
- ExecDualNode* cur;
- uintptr_t a = (uintptr_t)runtime_addr;
- pthread_mutex_lock(&g_jit_dual_map_mu);
- for (cur = g_jit_dual_map; cur; cur = cur->next) {
- uintptr_t base = (uintptr_t)cur->runtime_base;
- if (a >= base && a + n <= base + cur->size) {
- *write_out = (void*)((uintptr_t)cur->write_base + (a - base));
- pthread_mutex_unlock(&g_jit_dual_map_mu);
- return 0;
- }
- }
- pthread_mutex_unlock(&g_jit_dual_map_mu);
- return 1;
-}
-
-static CfreeStatus 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 CFREE_NOMEM;
- out->write = p;
- out->runtime = p;
- out->size = size;
- out->token = NULL; /* munmap suffices on release */
- return CFREE_OK;
-}
-
-#if DRIVER_DUAL_APPLE
-static CfreeStatus 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 CFREE_NOMEM;
-
- /* 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 CFREE_ERR;
- }
-
- /* 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 CFREE_ERR;
- }
-
- tok = (ExecMemToken*)malloc(sizeof(*tok));
- if (!tok) {
- munmap((void*)(uintptr_t)r_addr, size);
- munmap(w, size);
- return CFREE_NOMEM;
- }
- tok->write_addr = w;
- tok->runtime_addr = (void*)(uintptr_t)r_addr;
- tok->size = size;
-
- exec_dual_register(w, (void*)(uintptr_t)r_addr, size);
-
- out->write = w;
- out->runtime = (void*)(uintptr_t)r_addr;
- out->size = size;
- out->token = tok;
- return CFREE_OK;
-}
-#endif
-
-#if DRIVER_DUAL_LINUX
-static CfreeStatus 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 CFREE_ERR;
- if (ftruncate(fd, (off_t)size) != 0) {
- close(fd);
- return CFREE_ERR;
- }
-
- w = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED | DRIVER_MAP_32BIT,
- fd, 0);
- if (w == MAP_FAILED) {
- close(fd);
- return CFREE_NOMEM;
- }
- r = mmap(execmem_low_runtime_hint(size), size, PROT_READ,
- MAP_SHARED | DRIVER_MAP_32BIT, fd, 0);
- if (r == MAP_FAILED) {
- munmap(w, size);
- close(fd);
- return CFREE_NOMEM;
- }
- /* 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 CFREE_NOMEM;
- }
- tok->write_addr = w;
- tok->runtime_addr = r;
- tok->size = size;
-
- exec_dual_register(w, r, size);
-
- out->write = w;
- out->runtime = r;
- out->size = size;
- out->token = tok;
- return CFREE_OK;
-}
-#endif
-
-static CfreeStatus execmem_reserve(void* user, size_t size, int prot,
- CfreeExecMemRegion* out) {
- (void)user;
- if (!out || !size) return CFREE_INVALID;
- 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 CfreeStatus execmem_protect(void* user, void* addr, size_t size,
- int prot) {
- (void)user;
- return mprotect(addr, size, cfree_to_posix_prot(prot)) == 0 ? CFREE_OK
- : CFREE_ERR;
-}
-
-static void execmem_release(void* user, CfreeExecMemRegion* region) {
- (void)user;
- if (!region || !region->size) return;
- if (region->token) {
- ExecMemToken* tok = (ExecMemToken*)region->token;
- if (tok->runtime_addr && tok->runtime_addr != tok->write_addr) {
- exec_dual_unregister(tok->runtime_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) {
- (void)user;
-#if defined(__aarch64__) || defined(__arm__) || defined(__riscv)
- /* __builtin___clear_cache lowers to the right thing per arch:
- * - aarch64 / arm: dc cvau + ic ivau + isb sequence
- * - riscv64 (Linux): __riscv_flush_icache syscall (cross-hart)
- * On rv64 we still emit an inline fence.i first so the current
- * hart sees freshly written bytes even before the syscall returns. */
-#if defined(__riscv)
- __asm__ __volatile__("fence.i" ::: "memory");
-#endif
- __builtin___clear_cache((char*)addr, (char*)addr + size);
-#else
- (void)addr;
- (void)size;
-#endif
-}
-
-static size_t driver_host_page_size(void) {
- long p = sysconf(_SC_PAGESIZE);
- return (p > 0) ? (size_t)p : (size_t)0x4000;
-}
-
-static CfreeExecMem g_execmem_posix; /* page_size set in driver_env_init */
-
-/* ---------------- dbg os (POSIX) ---------------- */
-/* Implements CfreeDbgOs for the `cfree dbg` JIT debugger. v1 supports
- * aarch64 on macOS (Apple silicon) and Linux only — the ucontext shape
- * and W^X dance are arch/OS specific and a non-aarch64 build would link
- * but fail at runtime. */
-
-#define DBG_INTERRUPT_SIGNO SIGUSR2
-
-/* Single-session process model (one debug target at a time). The signal
- * handler reads these from async-signal context; both writes happen in
- * signals_install before any signal can arrive, both clears happen in
- * signals_uninstall after restoring SIG_DFL. */
-static CfreeDbgSignalOps g_dbg_ops;
-static int g_dbg_ops_set;
-static void* g_dbg_session;
-static pthread_t g_dbg_worker_tid;
-static int g_dbg_worker_tid_valid;
-
-/* Previous dispositions, restored by signals_uninstall. */
-static const int g_dbg_signos[] = {SIGTRAP, SIGSEGV, SIGBUS,
- SIGILL, SIGFPE, DBG_INTERRUPT_SIGNO};
-#define DBG_NSIGS ((int)(sizeof(g_dbg_signos) / sizeof(g_dbg_signos[0])))
-static struct sigaction g_dbg_prev_sa[DBG_NSIGS];
-static int g_dbg_installed;
-
-/* TLS landing slot for guarded_copy. The SEGV/BUS handler checks
- * g_guard_armed first; if set it siglongjmps back into guarded_copy
- * without touching on_fault. */
-static __thread sigjmp_buf g_guard_buf;
-static __thread int g_guard_armed;
-
-/* --- thread shim --- */
-
-typedef struct DbgThread {
- pthread_t tid;
- void (*fn)(void*);
- void* arg;
-} DbgThread;
-
-static void* dbg_thread_trampoline(void* p) {
- DbgThread* t = (DbgThread*)p;
- t->fn(t->arg);
- return NULL;
-}
-
-static CfreeStatus dbg_thread_start(void* user, void (*fn)(void*), void* arg,
- void** thread_out) {
- DbgThread* t;
- (void)user;
- t = (DbgThread*)malloc(sizeof(*t));
- if (!t) return CFREE_NOMEM;
- t->fn = fn;
- t->arg = arg;
- if (pthread_create(&t->tid, NULL, dbg_thread_trampoline, t) != 0) {
- free(t);
- return CFREE_ERR;
- }
- g_dbg_worker_tid = t->tid;
- g_dbg_worker_tid_valid = 1;
- *thread_out = t;
- return CFREE_OK;
-}
-
-static void dbg_thread_join(void* user, void* thread) {
- DbgThread* t = (DbgThread*)thread;
- (void)user;
- if (!t) return;
- pthread_join(t->tid, NULL);
- g_dbg_worker_tid_valid = 0;
- free(t);
-}
-
-static CfreeStatus dbg_thread_interrupt(void* user, void* thread) {
- DbgThread* t = (DbgThread*)thread;
- (void)user;
- if (!t) return CFREE_INVALID;
- return pthread_kill(t->tid, DBG_INTERRUPT_SIGNO) == 0 ? CFREE_OK : CFREE_ERR;
-}
-
-/* --- event shim --- */
-
-typedef struct DbgEvent {
- pthread_mutex_t mu;
- pthread_cond_t cv;
- int signaled;
-} DbgEvent;
-
-static CfreeStatus dbg_event_new(void* user, void** event_out) {
- DbgEvent* e;
- (void)user;
- e = (DbgEvent*)malloc(sizeof(*e));
- if (!e) return CFREE_NOMEM;
- if (pthread_mutex_init(&e->mu, NULL) != 0) {
- free(e);
- return CFREE_ERR;
- }
- if (pthread_cond_init(&e->cv, NULL) != 0) {
- pthread_mutex_destroy(&e->mu);
- free(e);
- return CFREE_ERR;
- }
- e->signaled = 0;
- *event_out = e;
- return CFREE_OK;
-}
-
-static void dbg_event_free(void* user, void* ev) {
- DbgEvent* e = (DbgEvent*)ev;
- (void)user;
- if (!e) return;
- pthread_cond_destroy(&e->cv);
- pthread_mutex_destroy(&e->mu);
- free(e);
-}
-
-static CfreeStatus dbg_event_wait(void* user, void* ev) {
- DbgEvent* e = (DbgEvent*)ev;
- (void)user;
- pthread_mutex_lock(&e->mu);
- while (!e->signaled) pthread_cond_wait(&e->cv, &e->mu);
- e->signaled = 0;
- pthread_mutex_unlock(&e->mu);
- return CFREE_OK;
-}
-
-/* pthread_cond_signal is not formally async-signal-safe per POSIX, but
- * it is in practice on glibc and Apple libc when callers manage the
- * signal mask carefully. LLDB and rr rely on the same pattern. */
-static CfreeStatus dbg_event_signal(void* user, void* ev) {
- DbgEvent* e = (DbgEvent*)ev;
- (void)user;
- pthread_mutex_lock(&e->mu);
- e->signaled = 1;
- pthread_cond_broadcast(&e->cv);
- pthread_mutex_unlock(&e->mu);
- return CFREE_OK;
-}
-
-static CfreeStatus dbg_event_reset(void* user, void* ev) {
- DbgEvent* e = (DbgEvent*)ev;
- (void)user;
- pthread_mutex_lock(&e->mu);
- e->signaled = 0;
- pthread_mutex_unlock(&e->mu);
- return CFREE_OK;
-}
-
-/* --- signal install + ucontext marshalling --- */
-
-/* Marshal ucontext_t <-> CfreeUnwindFrame. Register slots use each
- * architecture's DWARF numbering. */
-#if defined(__aarch64__) && defined(__APPLE__)
-static void dbg_ucontext_to_frame(const ucontext_t* uc, CfreeUnwindFrame* f) {
- const struct __darwin_arm_thread_state64* ss = &uc->uc_mcontext->__ss;
- int i;
- for (i = 0; i < 29; ++i) f->regs[i] = ss->__x[i];
- f->regs[29] = (uint64_t)ss->__fp;
- f->regs[30] = (uint64_t)ss->__lr;
- f->regs[31] = (uint64_t)ss->__sp;
- f->pc = (uint64_t)ss->__pc;
- f->cfa = (uint64_t)ss->__fp; /* DWARF CFI refines this in the session */
-}
-static void dbg_frame_to_ucontext(const CfreeUnwindFrame* f, ucontext_t* uc) {
- struct __darwin_arm_thread_state64* ss = &uc->uc_mcontext->__ss;
- int i;
- for (i = 0; i < 29; ++i) ss->__x[i] = f->regs[i];
- ss->__fp = f->regs[29];
- ss->__lr = f->regs[30];
- ss->__sp = f->regs[31];
- ss->__pc = f->pc;
-}
-#elif defined(__aarch64__) && defined(__linux__)
-static void dbg_ucontext_to_frame(const ucontext_t* uc, CfreeUnwindFrame* f) {
- const mcontext_t* mc = &uc->uc_mcontext;
- int i;
- for (i = 0; i < 31; ++i) f->regs[i] = mc->regs[i];
- f->regs[31] = mc->sp;
- f->pc = mc->pc;
- f->cfa = mc->regs[29]; /* fp; CFI refines */
-}
-static void dbg_frame_to_ucontext(const CfreeUnwindFrame* f, ucontext_t* uc) {
- mcontext_t* mc = &uc->uc_mcontext;
- int i;
- for (i = 0; i < 31; ++i) mc->regs[i] = f->regs[i];
- mc->sp = f->regs[31];
- mc->pc = f->pc;
-}
-#elif defined(__riscv) && (__riscv_xlen == 64) && defined(__linux__)
-/* RISC-V 64 on Linux: glibc's mcontext_t exposes __gregs[0..31] where
- * __gregs[0] holds the PC and __gregs[1..31] hold x1..x31 (ra, sp, gp,
- * tp, t0..t2, s0/fp, s1, a0..a7, s2..s11, t3..t6). DWARF numbering
- * assigns 0..31 to x0..x31, so we marshal pc separately and fold x1..x31
- * into f->regs[1..31], leaving f->regs[0] as the constant zero. */
-static void dbg_ucontext_to_frame(const ucontext_t* uc, CfreeUnwindFrame* f) {
- const mcontext_t* mc = &uc->uc_mcontext;
- int i;
- f->regs[0] = 0;
- for (i = 1; i < 32; ++i) f->regs[i] = (uint64_t)mc->__gregs[i];
- f->pc = (uint64_t)mc->__gregs[0];
- f->cfa = (uint64_t)mc->__gregs[8]; /* s0/fp; CFI refines */
-}
-static void dbg_frame_to_ucontext(const CfreeUnwindFrame* f, ucontext_t* uc) {
- mcontext_t* mc = &uc->uc_mcontext;
- int i;
- for (i = 1; i < 32; ++i) mc->__gregs[i] = (unsigned long)f->regs[i];
- mc->__gregs[0] = (unsigned long)f->pc;
-}
-#elif defined(__x86_64__) && defined(__linux__)
-static void dbg_ucontext_to_frame(const ucontext_t* uc, CfreeUnwindFrame* f) {
- const greg_t* g = uc->uc_mcontext.gregs;
- memset(f, 0, sizeof(*f));
- f->regs[0] = (uint64_t)g[REG_RAX];
- f->regs[1] = (uint64_t)g[REG_RDX];
- f->regs[2] = (uint64_t)g[REG_RCX];
- f->regs[3] = (uint64_t)g[REG_RBX];
- f->regs[4] = (uint64_t)g[REG_RSI];
- f->regs[5] = (uint64_t)g[REG_RDI];
- f->regs[6] = (uint64_t)g[REG_RBP];
- f->regs[7] = (uint64_t)g[REG_RSP];
- f->regs[8] = (uint64_t)g[REG_R8];
- f->regs[9] = (uint64_t)g[REG_R9];
- f->regs[10] = (uint64_t)g[REG_R10];
- f->regs[11] = (uint64_t)g[REG_R11];
- f->regs[12] = (uint64_t)g[REG_R12];
- f->regs[13] = (uint64_t)g[REG_R13];
- f->regs[14] = (uint64_t)g[REG_R14];
- f->regs[15] = (uint64_t)g[REG_R15];
- f->regs[16] = (uint64_t)g[REG_RIP];
- f->pc = (uint64_t)g[REG_RIP];
- f->cfa = (uint64_t)g[REG_RSP];
-}
-static void dbg_frame_to_ucontext(const CfreeUnwindFrame* f, ucontext_t* uc) {
- greg_t* g = uc->uc_mcontext.gregs;
- g[REG_RAX] = (greg_t)f->regs[0];
- g[REG_RDX] = (greg_t)f->regs[1];
- g[REG_RCX] = (greg_t)f->regs[2];
- g[REG_RBX] = (greg_t)f->regs[3];
- g[REG_RSI] = (greg_t)f->regs[4];
- g[REG_RDI] = (greg_t)f->regs[5];
- g[REG_RBP] = (greg_t)f->regs[6];
- g[REG_RSP] = (greg_t)f->regs[7];
- g[REG_R8] = (greg_t)f->regs[8];
- g[REG_R9] = (greg_t)f->regs[9];
- g[REG_R10] = (greg_t)f->regs[10];
- g[REG_R11] = (greg_t)f->regs[11];
- g[REG_R12] = (greg_t)f->regs[12];
- g[REG_R13] = (greg_t)f->regs[13];
- g[REG_R14] = (greg_t)f->regs[14];
- g[REG_R15] = (greg_t)f->regs[15];
- g[REG_RIP] = (greg_t)f->pc;
-}
-#else
-#error \
- "cfree dbg v1 supports only aarch64 on macOS/Linux, riscv64 on Linux, or x86_64 on Linux"
-#endif
-
-static void dbg_signal_handler(int signo, siginfo_t* si, void* ucv) {
- ucontext_t* uc = (ucontext_t*)ucv;
- CfreeUnwindFrame frame;
- CfreeStatus rc;
- (void)si;
-
- /* SIGSEGV/SIGBUS during an armed guarded_copy: bail out to the
- * sigsetjmp landing slot before the session ever sees the fault. */
- if ((signo == SIGSEGV || signo == SIGBUS) && g_guard_armed) {
- g_guard_armed = 0;
- siglongjmp(g_guard_buf, 1);
- }
-
- /* Only the registered worker thread participates in stop-the-world.
- * Faults on other threads (e.g. the REPL) fall through to the default. */
- if (!g_dbg_worker_tid_valid ||
- !pthread_equal(pthread_self(), g_dbg_worker_tid) || !g_dbg_ops_set ||
- !g_dbg_ops.on_fault) {
- int i;
- for (i = 0; i < DBG_NSIGS; ++i) {
- if (g_dbg_signos[i] == signo) {
- sigaction(signo, &g_dbg_prev_sa[i], NULL);
- break;
- }
- }
- raise(signo);
- return;
- }
-
- dbg_ucontext_to_frame(uc, &frame);
- rc = g_dbg_ops.on_fault(g_dbg_session, signo, &frame);
- if (rc != CFREE_OK) {
- /* Session declined to handle: restore default and re-raise so the
- * host produces a core dump for the original cause. */
- int i;
- for (i = 0; i < DBG_NSIGS; ++i) {
- if (g_dbg_signos[i] == signo) {
- sigaction(signo, &g_dbg_prev_sa[i], NULL);
- break;
- }
- }
- raise(signo);
- return;
- }
- dbg_frame_to_ucontext(&frame, uc);
-}
-
-static CfreeStatus dbg_signals_install(void* user, const CfreeDbgSignalOps* ops,
- void* session) {
- struct sigaction sa;
- int i;
- (void)user;
- if (g_dbg_installed) return CFREE_ERR;
- g_dbg_ops = *ops;
- g_dbg_ops_set = 1;
- g_dbg_session = session;
-
- memset(&sa, 0, sizeof(sa));
- sa.sa_sigaction = dbg_signal_handler;
- sa.sa_flags = SA_SIGINFO | SA_RESTART;
- sigemptyset(&sa.sa_mask);
- /* Block our signal cohort during the handler so nested faults from
- * the cond-wait critical region don't recurse. */
- for (i = 0; i < DBG_NSIGS; ++i) sigaddset(&sa.sa_mask, g_dbg_signos[i]);
-
- for (i = 0; i < DBG_NSIGS; ++i) {
- if (sigaction(g_dbg_signos[i], &sa, &g_dbg_prev_sa[i]) != 0) {
- /* Roll back what we installed. */
- int j;
- for (j = 0; j < i; ++j)
- sigaction(g_dbg_signos[j], &g_dbg_prev_sa[j], NULL);
- memset(&g_dbg_ops, 0, sizeof(g_dbg_ops));
- g_dbg_ops_set = 0;
- g_dbg_session = NULL;
- return CFREE_ERR;
- }
- }
- g_dbg_installed = 1;
- return CFREE_OK;
-}
-
-static void dbg_signals_uninstall(void* user) {
- int i;
- (void)user;
- if (!g_dbg_installed) return;
- for (i = 0; i < DBG_NSIGS; ++i)
- sigaction(g_dbg_signos[i], &g_dbg_prev_sa[i], NULL);
- g_dbg_installed = 0;
- memset(&g_dbg_ops, 0, sizeof(g_dbg_ops));
- g_dbg_ops_set = 0;
- g_dbg_session = NULL;
-}
-
-/* --- code write window (W^X dance) --- */
-
-#if defined(__linux__)
-static size_t dbg_page_floor(size_t v, size_t pg) { return v & ~(pg - 1); }
-static size_t dbg_page_ceil(size_t v, size_t pg) {
- return (v + pg - 1) & ~(pg - 1);
-}
-#endif
-
-static CfreeStatus dbg_code_write_begin(void* user, void* runtime_addr,
- size_t n, void** write_out) {
- (void)user;
- if (!runtime_addr || !n || !write_out) return CFREE_INVALID;
-#if defined(__APPLE__)
- /* Dual-mapped reservation (mach_vm_remap): the write alias is a
- * separate VA already RW. Translate via the registry; no protect flip
- * is required, so code_write_end is a no-op. */
- return exec_dual_lookup(runtime_addr, n, write_out) == 0 ? CFREE_OK
- : CFREE_ERR;
-#elif defined(__linux__)
- {
- size_t pg = driver_host_page_size();
- uintptr_t a = (uintptr_t)runtime_addr;
- uintptr_t base = dbg_page_floor(a, pg);
- size_t span = dbg_page_ceil((a - base) + n, pg);
- /* Linux dual-mapping uses memfd: write alias and runtime alias have
- * distinct VAs. Prefer the alias lookup; fall back to a transient
- * mprotect of the runtime alias for single-mapping reservations. */
- if (exec_dual_lookup(runtime_addr, n, write_out) == 0) return CFREE_OK;
- if (mprotect((void*)base, span, PROT_READ | PROT_WRITE | PROT_EXEC) != 0)
- return CFREE_ERR;
- *write_out = runtime_addr;
- return CFREE_OK;
- }
-#else
-#error "cfree dbg v1 supports only macOS and Linux"
-#endif
-}
-
-static void dbg_code_write_end(void* user, void* runtime_addr, size_t n) {
- (void)user;
-#if defined(__APPLE__)
- (void)runtime_addr;
- (void)n;
-#elif defined(__linux__)
- {
- void* w;
- size_t pg = driver_host_page_size();
- uintptr_t a = (uintptr_t)runtime_addr;
- uintptr_t base = dbg_page_floor(a, pg);
- size_t span = dbg_page_ceil((a - base) + n, pg);
- if (exec_dual_lookup(runtime_addr, n, &w) == 0)
- return; /* dual: nothing to flip back */
- mprotect((void*)base, span, PROT_READ | PROT_EXEC);
- }
-#endif
-}
-
-static void dbg_flush_icache(void* user, void* runtime_addr, size_t n) {
- (void)user;
-#if defined(__APPLE__) && defined(__aarch64__)
- sys_icache_invalidate(runtime_addr, n);
-#else
- __builtin___clear_cache((char*)runtime_addr, (char*)runtime_addr + n);
-#endif
-}
-
-/* --- guarded copy --- */
-
-static CfreeStatus dbg_guarded_copy(void* user, void* dst, const void* src,
- size_t n) {
- (void)user;
- if (sigsetjmp(g_guard_buf, 1) != 0) {
- g_guard_armed = 0;
- return CFREE_ERR; /* SIGSEGV/SIGBUS during the copy */
- }
- g_guard_armed = 1;
- memcpy(dst, src, n);
- g_guard_armed = 0;
- return CFREE_OK;
-}
-
-static __thread sigjmp_buf g_dbg_abort_buf;
-
-static int dbg_call_with_catch(void* user, void (*fn)(void*), void* arg) {
- (void)user;
- if (sigsetjmp(g_dbg_abort_buf, 1) == 0) {
- fn(arg);
- return 0;
- }
- return 1;
-}
-
-static void dbg_thread_abort(void* user) {
- (void)user;
- siglongjmp(g_dbg_abort_buf, 1);
-}
-
-static CfreeDbgOs g_dbg_os_posix;
-
-/* ---------------- jit_tls (pthread-key backed) ---------------- */
-/* Backs CfreeJitTls for `cfree run` on Mach-O targets: every JIT image
- * with TLS gets one pthread_key, the per-thread block is allocated
- * lazily on first access, and freed via the key's destructor when the
- * thread exits.
- *
- * The ctx layout is fixed by the contract in src/jit/tlv_thunk.h: the
- * first 8 bytes MUST be a function pointer the asm thunk calls with
- * x0 = ctx and expects back an x0 = TLS block. We satisfy this by
- * making `get_block` the first field. */
-typedef struct JitTlsCtx {
- void* (*get_block)(void* ctx); /* first; matches tlv_thunk's expectation */
- pthread_key_t key;
- size_t image_size;
- size_t image_filesz;
- size_t align;
- void* init_bytes; /* heap-owned copy of init bytes, or NULL if all BSS */
-} JitTlsCtx;
-
-static void jit_tls_thread_dtor(void* block) {
- /* POSIX pthread_key destructor: called when a thread that touched the
- * TLV exits. `block` is the void* set by pthread_setspecific, never
- * NULL (POSIX skips the destructor if it was NULL). */
- free(block);
-}
-
-static void* jit_tls_alloc_block(JitTlsCtx* ctx) {
- /* macOS aligned_alloc requires alignment >= sizeof(void*); bump
- * smaller request alignments up. Size must be a multiple of
- * alignment too. */
- size_t a = ctx->align ? ctx->align : sizeof(void*);
- if (a < sizeof(void*)) a = sizeof(void*);
- size_t sz = (ctx->image_size + a - 1u) & ~(a - 1u);
- if (sz == 0) sz = a; /* zero-size TLS image still needs a non-NULL block */
- void* block = aligned_alloc(a, sz);
- if (!block) return NULL;
- if (ctx->image_filesz && ctx->init_bytes)
- memcpy(block, ctx->init_bytes, ctx->image_filesz);
- if (ctx->image_size > ctx->image_filesz)
- memset((char*)block + ctx->image_filesz, 0,
- ctx->image_size - ctx->image_filesz);
- return block;
-}
-
-/* The thunk-callable entry; the asm trampoline calls this with x0=ctx
- * and expects x0 back = TLS block base. */
-static void* jit_tls_get_block(void* ctx_v) {
- JitTlsCtx* ctx = (JitTlsCtx*)ctx_v;
- void* block = pthread_getspecific(ctx->key);
- if (block) return block;
- block = jit_tls_alloc_block(ctx);
- if (!block) {
- /* OOM inside a TLV access has no clean recovery: the thunk's caller
- * is mid-expression and can't observe failure. Abort, matching
- * how dyld treats failures inside _tlv_bootstrap. */
- fprintf(stderr,
- "cfree run: out of memory allocating per-thread TLS block\n");
- abort();
- }
- if (pthread_setspecific(ctx->key, block) != 0) {
- fprintf(stderr, "cfree run: pthread_setspecific failed in TLV thunk\n");
- abort();
- }
- return block;
-}
-
-static void* jit_tls_ctx_new(void* user, const void* init_bytes,
- size_t image_filesz, size_t image_size,
- size_t align) {
- (void)user;
- JitTlsCtx* ctx = (JitTlsCtx*)malloc(sizeof(*ctx));
- if (!ctx) return NULL;
- ctx->get_block = jit_tls_get_block;
- ctx->image_size = image_size;
- ctx->image_filesz = image_filesz;
- ctx->align = align ? align : sizeof(void*);
- ctx->init_bytes = NULL;
- if (image_filesz && init_bytes) {
- ctx->init_bytes = malloc(image_filesz);
- if (!ctx->init_bytes) {
- free(ctx);
- return NULL;
- }
- memcpy(ctx->init_bytes, init_bytes, image_filesz);
- }
- if (pthread_key_create(&ctx->key, jit_tls_thread_dtor) != 0) {
- free(ctx->init_bytes);
- free(ctx);
- return NULL;
- }
- return ctx;
-}
-
-static void jit_tls_ctx_destroy(void* user, void* ctx_v) {
- JitTlsCtx* ctx = (JitTlsCtx*)ctx_v;
- (void)user;
- if (!ctx) return;
- /* Free the calling thread's block (POSIX won't run our destructor for
- * it; pthread_key_delete also doesn't fire destructors for live
- * threads). Other threads' blocks are reaped when those threads
- * exit, since their stored pointers remain reachable via the key
- * value already snapshotted into TSD before delete. */
- void* my_block = pthread_getspecific(ctx->key);
- if (my_block) {
- pthread_setspecific(ctx->key, NULL);
- free(my_block);
- }
- pthread_key_delete(ctx->key);
- free(ctx->init_bytes);
- free(ctx);
-}
-
-static CfreeJitTls g_jit_tls_posix;
-
-static char g_cache_dir[4096];
-
-/* ---------------- writer (fd-backed) ---------------- */
-
-typedef struct DriverFdWriter {
- CfreeWriter base; /* must be first; libcfree reads via this */
- CfreeHeap* heap;
- int fd;
- CfreeStatus status;
- uint64_t pos;
-} DriverFdWriter;
-
-static CfreeStatus fdw_write(CfreeWriter* w, const void* data, size_t n) {
- DriverFdWriter* fw = (DriverFdWriter*)w;
- const unsigned char* p = (const unsigned char*)data;
- if (fw->status != CFREE_OK) return fw->status;
- while (n > 0) {
- ssize_t k = write(fw->fd, p, n);
- if (k < 0) {
- fw->status = CFREE_IO;
- return CFREE_IO;
- }
- p += (size_t)k;
- n -= (size_t)k;
- fw->pos += (uint64_t)k;
- }
- return CFREE_OK;
-}
-
-static CfreeStatus fdw_seek(CfreeWriter* w, uint64_t off) {
- DriverFdWriter* fw = (DriverFdWriter*)w;
- if (fw->status != CFREE_OK) return fw->status;
- if (lseek(fw->fd, (off_t)off, SEEK_SET) < 0) {
- fw->status = CFREE_IO;
- return CFREE_IO;
- }
- fw->pos = off;
- return CFREE_OK;
-}
-
-static uint64_t fdw_tell(CfreeWriter* w) { return ((DriverFdWriter*)w)->pos; }
-static CfreeStatus fdw_status(CfreeWriter* w) {
- return ((DriverFdWriter*)w)->status;
-}
-
-static void fdw_close(CfreeWriter* w) {
- DriverFdWriter* fw = (DriverFdWriter*)w;
- if (fw->fd >= 0) close(fw->fd);
- fw->heap->free(fw->heap, fw, sizeof(*fw));
-}
-
-static CfreeWriter* driver_writer_fd(CfreeHeap* h, int fd) {
- DriverFdWriter* fw =
- (DriverFdWriter*)h->alloc(h, sizeof(*fw), _Alignof(DriverFdWriter));
- if (!fw) return NULL;
- fw->base.write = fdw_write;
- fw->base.seek = fdw_seek;
- fw->base.tell = fdw_tell;
- fw->base.status = fdw_status;
- fw->base.close = fdw_close;
- fw->heap = h;
- fw->fd = fd;
- fw->status = CFREE_OK;
- fw->pos = 0;
- return &fw->base;
-}
-
-/* Stdout writer routes through stdio so it shares libc's buffer with
- * driver_printf — otherwise interleaved printf+writer output would land
- * in stdout-order surprises (printf is line/full-buffered; raw write(2)
- * to fd 1 bypasses that buffer entirely). */
-typedef struct DriverStdioWriter {
- CfreeWriter base;
- CfreeHeap* heap;
- FILE* fp;
- CfreeStatus status;
-} DriverStdioWriter;
-
-static CfreeStatus stdio_w_write(CfreeWriter* w, const void* data, size_t n) {
- DriverStdioWriter* sw = (DriverStdioWriter*)w;
- if (n) {
- size_t got = fwrite(data, 1, n, sw->fp);
- if (got != n) {
- sw->status = CFREE_IO;
- return CFREE_IO;
- }
- }
- return CFREE_OK;
-}
-static CfreeStatus stdio_w_seek(CfreeWriter* w, uint64_t off) {
- DriverStdioWriter* sw = (DriverStdioWriter*)w;
- return fseek(sw->fp, (long)off, SEEK_SET) == 0 ? CFREE_OK : CFREE_IO;
-}
-static uint64_t stdio_w_tell(CfreeWriter* w) {
- long t = ftell(((DriverStdioWriter*)w)->fp);
- return t < 0 ? 0u : (uint64_t)t;
-}
-static CfreeStatus stdio_w_status(CfreeWriter* w) {
- DriverStdioWriter* sw = (DriverStdioWriter*)w;
- if (sw->status != CFREE_OK) return sw->status;
- return ferror(sw->fp) ? CFREE_IO : CFREE_OK;
-}
-static void stdio_w_close(CfreeWriter* w) {
- DriverStdioWriter* sw = (DriverStdioWriter*)w;
- fflush(sw->fp); /* flush but do not close stdout */
- sw->heap->free(sw->heap, sw, sizeof(*sw));
-}
-
-CfreeWriter* driver_stdout_writer(DriverEnv* e) {
- DriverStdioWriter* sw = (DriverStdioWriter*)e->heap->alloc(
- e->heap, sizeof(*sw), _Alignof(DriverStdioWriter));
- if (!sw) return NULL;
- sw->base.write = stdio_w_write;
- sw->base.seek = stdio_w_seek;
- sw->base.tell = stdio_w_tell;
- sw->base.status = stdio_w_status;
- sw->base.close = stdio_w_close;
- sw->heap = e->heap;
- sw->fp = stdout;
- sw->status = CFREE_OK;
- return &sw->base;
-}
-
-/* ---------------- file_io (POSIX) ---------------- */
-
-static CfreeStatus posix_read_all(void* user, const char* path,
- CfreeFileData* out) {
- DriverEnv* env = (DriverEnv*)user;
- int fd;
- struct stat sb;
- size_t size;
- size_t got;
- void* buf;
-
- fd = open(path, O_RDONLY);
- if (fd < 0) return CFREE_NOT_FOUND;
- if (fstat(fd, &sb) < 0) {
- close(fd);
- return CFREE_IO;
- }
- size = (size_t)sb.st_size;
- buf = size ? env->heap->alloc(env->heap, size, 1) : NULL;
- if (size && !buf) {
- close(fd);
- return CFREE_NOMEM;
- }
-
- got = 0;
- while (got < size) {
- ssize_t n = read(fd, (unsigned char*)buf + got, size - got);
- if (n <= 0) {
- env->heap->free(env->heap, buf, size);
- close(fd);
- return CFREE_IO;
- }
- got += (size_t)n;
- }
- close(fd);
-
- out->data = (const uint8_t*)buf;
- out->size = size;
- out->token = buf;
- return CFREE_OK;
-}
-
-static void posix_release(void* user, CfreeFileData* d) {
- DriverEnv* env = (DriverEnv*)user;
- if (d->token) env->heap->free(env->heap, d->token, d->size);
- d->data = NULL;
- d->size = 0;
- d->token = NULL;
-}
-
-static CfreeStatus posix_open_writer(void* user, const char* path,
- CfreeWriter** out) {
- DriverEnv* env = (DriverEnv*)user;
- int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
- CfreeWriter* w;
- if (fd < 0) return CFREE_IO;
- w = driver_writer_fd(env->heap, fd);
- if (!w) {
- close(fd);
- return CFREE_NOMEM;
- }
- *out = w;
- return CFREE_OK;
-}
-
-/* ---------------- env wiring ---------------- */
-
-void driver_env_init(DriverEnv* e) {
- e->heap = &g_heap_libc;
- e->diag = &g_diag_stderr;
- e->file_io.read_all = posix_read_all;
- e->file_io.release = posix_release;
- e->file_io.open_writer = posix_open_writer;
- e->file_io.user = e;
-
- 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;
- g_execmem_posix.flush_icache = execmem_flush_icache;
- g_execmem_posix.user = NULL;
- e->execmem = &g_execmem_posix;
-
- g_dbg_os_posix.thread_start = dbg_thread_start;
- g_dbg_os_posix.thread_join = dbg_thread_join;
- g_dbg_os_posix.thread_interrupt = dbg_thread_interrupt;
- g_dbg_os_posix.event_new = dbg_event_new;
- g_dbg_os_posix.event_free = dbg_event_free;
- g_dbg_os_posix.event_wait = dbg_event_wait;
- g_dbg_os_posix.event_signal = dbg_event_signal;
- g_dbg_os_posix.event_reset = dbg_event_reset;
- g_dbg_os_posix.signals_install = dbg_signals_install;
- g_dbg_os_posix.signals_uninstall = dbg_signals_uninstall;
- g_dbg_os_posix.interrupt_signo = DBG_INTERRUPT_SIGNO;
- g_dbg_os_posix.code_write_begin = dbg_code_write_begin;
- g_dbg_os_posix.code_write_end = dbg_code_write_end;
- g_dbg_os_posix.flush_icache = dbg_flush_icache;
- g_dbg_os_posix.guarded_copy = dbg_guarded_copy;
- g_dbg_os_posix.call_with_catch = dbg_call_with_catch;
- g_dbg_os_posix.thread_abort = dbg_thread_abort;
- g_dbg_os_posix.user = NULL;
- e->dbg_os = &g_dbg_os_posix;
-
- g_jit_tls_posix.ctx_new = jit_tls_ctx_new;
- g_jit_tls_posix.ctx_destroy = jit_tls_ctx_destroy;
- g_jit_tls_posix.user = NULL;
- e->jit_tls = &g_jit_tls_posix;
-
- e->metrics = NULL;
-
- {
- const char* xdg = getenv("XDG_CACHE_HOME");
- const char* home = getenv("HOME");
- if (xdg && *xdg) {
- snprintf(g_cache_dir, sizeof(g_cache_dir), "%s/cfree", xdg);
- } else if (home && *home) {
- snprintf(g_cache_dir, sizeof(g_cache_dir), "%s/.cache/cfree", home);
- } else {
- snprintf(g_cache_dir, sizeof(g_cache_dir), "build/cfree-cache");
- }
- e->cache_dir = g_cache_dir;
- }
-
- /* Reproducible-build precedent: SOURCE_DATE_EPOCH wins over wall clock.
- * If neither is set or the env value doesn't parse, advertise -1 ("no
- * clock") and pp falls back to C11 placeholders. */
- {
- const char* sde = getenv("SOURCE_DATE_EPOCH");
- if (sde && *sde) {
- char* endp = NULL;
- long long v = strtoll(sde, &endp, 10);
- e->now = (endp != sde && v >= 0) ? (int64_t)v : (int64_t)-1;
- } else {
- time_t t = time(NULL);
- e->now = (t == (time_t)-1) ? (int64_t)-1 : (int64_t)t;
- }
- }
-}
-
-void driver_env_fini(DriverEnv* e) {
- /* Singletons; nothing to release. */
- (void)e;
-}
-
-CfreeContext driver_env_to_context(const DriverEnv* e) {
- CfreeContext c;
- c.heap = e->heap;
- c.file_io = &e->file_io;
- c.diag = e->diag;
- c.metrics = e->metrics;
- c.now = e->now;
- return c;
-}
-
-CfreeJitHost driver_env_to_jit_host(const DriverEnv* e) {
- CfreeJitHost h;
- h.execmem = e->execmem;
- h.tls = e->jit_tls;
- return h;
-}
-
-CfreeDbgHost driver_env_to_dbg_host(const DriverEnv* e) {
- CfreeDbgHost h;
- h.os = e->dbg_os;
- return h;
-}
-
-/* ---------------- host-shim helpers ---------------- */
-
-/* The driver's only NUL-terminated-string handling: thin boundary shims that
- * route every length scan through cfree_slice_cstr and otherwise use the
- * length-based mem* primitives. No libc str* is used. */
-
-int driver_streq(const char* a, const char* b) {
- return cfree_slice_eq(cfree_slice_cstr(a), cfree_slice_cstr(b));
-}
-
-int driver_strneq(const char* a, const char* b, size_t n) {
- size_t i;
- for (i = 0; i < n; ++i) {
- unsigned char ca = (unsigned char)a[i], cb = (unsigned char)b[i];
- if (ca != cb) return 0;
- if (ca == '\0') return 1;
- }
- return 1;
-}
-
-size_t driver_strlen(const char* s) { return cfree_slice_cstr(s).len; }
-
-const char* driver_strchr(const char* s, int c) {
- /* search includes the terminator so driver_strchr(s, 0) works like strchr */
- return (const char*)memchr(s, c, cfree_slice_cstr(s).len + 1u);
-}
-
-const char* driver_basename(const char* path) {
- size_t i = cfree_slice_cstr(path).len;
- while (i > 0) {
- if (path[i - 1] == '/') return path + i;
- --i;
- }
- return path;
-}
-
-int driver_has_suffix(const char* s, const char* suffix) {
- size_t ls = cfree_slice_cstr(s).len;
- size_t lf = cfree_slice_cstr(suffix).len;
- return ls >= lf && memcmp(s + ls - lf, suffix, lf) == 0;
-}
-
-int driver_path_exists(const char* path) {
- struct stat sb;
- if (!path) return 0;
- return stat(path, &sb) == 0;
-}
-
-int driver_path_mtime_ns(const char* path, int64_t* out) {
- struct stat sb;
- int64_t sec;
- int64_t nsec;
-
- if (!path || !out) return 1;
- if (stat(path, &sb) != 0) return 1;
-#if defined(__APPLE__)
- sec = (int64_t)sb.st_mtimespec.tv_sec;
- nsec = (int64_t)sb.st_mtimespec.tv_nsec;
-#elif defined(__linux__)
- sec = (int64_t)sb.st_mtim.tv_sec;
- nsec = (int64_t)sb.st_mtim.tv_nsec;
-#else
- sec = (int64_t)sb.st_mtime;
- nsec = 0;
-#endif
- *out = sec * 1000000000LL + nsec;
- return 0;
-}
-
-int driver_mkdir_p(DriverEnv* env, const char* path) {
- size_t len;
- char* buf;
- size_t i;
- struct stat sb;
-
- if (!path || !path[0]) return 1;
- len = cfree_slice_cstr(path).len;
- buf = (char*)driver_alloc(env, len + 1);
- if (!buf) return 1;
- memcpy(buf, path, len + 1);
-
- for (i = 1; i <= len; ++i) {
- int at_end = (i == len);
- if (!at_end && buf[i] != '/') continue;
- if (!at_end) buf[i] = '\0';
- if (buf[0] != '\0' && !driver_streq(buf, ".")) {
- if (mkdir(buf, 0755) != 0 && errno != EEXIST) {
- driver_free(env, buf, len + 1);
- return 1;
- }
- if (stat(buf, &sb) != 0 || !S_ISDIR(sb.st_mode)) {
- driver_free(env, buf, len + 1);
- return 1;
- }
- }
- if (!at_end) buf[i] = '/';
- }
-
- driver_free(env, buf, len + 1);
- return 0;
-}
-
-int driver_mark_executable_output(const char* path) {
- mode_t mask;
- mode_t mode;
-
- if (!path) return 1;
- mask = umask(0);
- (void)umask(mask);
- mode = (mode_t)(0777 & ~mask);
- return chmod(path, mode) == 0 ? 0 : 1;
-}
-
-void* driver_alloc(DriverEnv* e, size_t n) {
- return e->heap->alloc(e->heap, n, _Alignof(max_align_t));
-}
-
-void* driver_alloc_zeroed(DriverEnv* e, size_t n) {
- void* p = driver_alloc(e, n);
- if (p) memset(p, 0, n);
- return p;
-}
-
-void driver_free(DriverEnv* e, void* p, size_t n) {
- if (p) e->heap->free(e->heap, p, n);
-}
-
-void driver_memcpy(void* dst, const void* src, size_t n) {
- memcpy(dst, src, n);
-}
-
-void driver_errf(const char* tool, const char* fmt, ...) {
- va_list ap;
- fprintf(stderr, "%.*s: ", CFREE_SLICE_ARG(cfree_slice_cstr(tool)));
- va_start(ap, fmt);
- vfprintf(stderr, fmt, ap);
- va_end(ap);
- fputc('\n', stderr);
-}
-
-void driver_logf(const char* fmt, ...) {
- va_list ap;
- va_start(ap, fmt);
- vfprintf(stderr, fmt, ap);
- va_end(ap);
- fputc('\n', stderr);
-}
-
-void cfree_debug_printf(const char* fmt, ...) {
- va_list ap;
- va_start(ap, fmt);
- vfprintf(stderr, fmt, ap);
- va_end(ap);
-}
-
-void driver_printf(const char* fmt, ...) {
- va_list ap;
- va_start(ap, fmt);
- vprintf(fmt, ap);
- va_end(ap);
-}
-
-uint64_t driver_now_ns(void) {
-#if defined(CLOCK_MONOTONIC)
- struct timespec ts;
- if (clock_gettime(CLOCK_MONOTONIC, &ts) == 0)
- return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
-#endif
- return 0;
-}
-
-const char* driver_getenv(const char* name) { return getenv(name); }
-
-int driver_load_bytes(const CfreeFileIO* io, const char* tool, const char* path,
- DriverLoad* out, CfreeSlice* in) {
- out->loaded = 0;
- out->fd.data = NULL;
- out->fd.size = 0;
- out->fd.token = NULL;
- if (!io || !io->read_all) {
- driver_errf(tool, "host file I/O unavailable");
- return 1;
- }
- if (io->read_all(io->user, path, &out->fd) != CFREE_OK) {
- driver_errf(tool, "failed to read: %.*s",
- CFREE_SLICE_ARG(cfree_slice_cstr(path)));
- return 1;
- }
- out->loaded = 1;
- in->data = out->fd.data;
- in->len = out->fd.size;
- return 0;
-}
-
-void driver_release_bytes(const CfreeFileIO* io, DriverLoad* lf) {
- if (!lf || !lf->loaded) return;
- if (io && io->release) io->release(io->user, &lf->fd);
- lf->loaded = 0;
-}
-
-int driver_read_stdin(DriverEnv* e, uint8_t** out_data, size_t* out_size) {
- /* stdin is unseekable in the general case (pipes, ttys), so grow a buffer
- * geometrically as we read, then shrink to the exact byte count so
- * out_size is a valid argument to driver_free. */
- size_t cap = 4096;
- size_t len = 0;
- uint8_t* buf = e->heap->alloc(e->heap, cap, 1);
- if (!buf) return 0;
- for (;;) {
- ssize_t n;
- if (len == cap) {
- size_t newcap = cap * 2;
- uint8_t* nb = e->heap->realloc(e->heap, buf, cap, newcap, 1);
- if (!nb) {
- e->heap->free(e->heap, buf, cap);
- return 0;
- }
- buf = nb;
- cap = newcap;
- }
- n = read(STDIN_FILENO, buf + len, cap - len);
- if (n == 0) break;
- if (n < 0) {
- e->heap->free(e->heap, buf, cap);
- return 0;
- }
- len += (size_t)n;
- }
- if (len < cap) {
- uint8_t* shrunk = len ? e->heap->realloc(e->heap, buf, cap, len, 1) : NULL;
- if (len && !shrunk) {
- /* Shrink failed: keep the larger buffer. The release size below
- * tracks `cap`, so this remains free-correct. */
- *out_data = buf;
- *out_size = cap;
- return 1;
- }
- if (!len) {
- e->heap->free(e->heap, buf, cap);
- buf = NULL;
- } else {
- buf = shrunk;
- }
- }
- *out_data = buf;
- *out_size = len;
- return 1;
-}
-
-static int driver_write_fd_all(int fd, const uint8_t* data, size_t n) {
- size_t off = 0;
- while (off < n) {
- ssize_t wr = write(fd, data + off, n - off);
- if (wr < 0) {
- if (errno == EINTR) continue;
- return 0;
- }
- if (wr == 0) return 0;
- off += (size_t)wr;
- }
- return 1;
-}
-
-static char* driver_shell_quote_path(DriverEnv* e, const char* path,
- size_t path_len, size_t* quoted_len_out) {
- size_t i;
- size_t quoted_len = 2u;
- char* out;
- char* q;
- for (i = 0; i < path_len; ++i) quoted_len += path[i] == '\'' ? 4u : 1u;
- out = e->heap->alloc(e->heap, quoted_len + 1u, 1);
- if (!out) return NULL;
- q = out;
- *q++ = '\'';
- for (i = 0; i < path_len; ++i) {
- if (path[i] == '\'') {
- *q++ = '\'';
- *q++ = '\\';
- *q++ = '\'';
- *q++ = '\'';
- } else {
- *q++ = path[i];
- }
- }
- *q++ = '\'';
- *q = '\0';
- if (quoted_len_out) *quoted_len_out = quoted_len;
- return out;
-}
-
-int driver_edit_temp(DriverEnv* e, const char* suffix, const uint8_t* initial,
- size_t initial_size, uint8_t** out_data,
- size_t* out_size) {
- const char* editor;
- const char* tmpdir;
- const char* base = "/cfree-dbg-XXXXXX";
- size_t tmpdir_len;
- size_t base_len;
- size_t suffix_len;
- size_t path_len;
- char* path;
- int fd = -1;
- int ok = 0;
- CfreeFileData fd_data;
-
- if (!out_data || !out_size) return 0;
- *out_data = NULL;
- *out_size = 0;
- suffix_len = suffix ? cfree_slice_cstr(suffix).len : 0u;
- tmpdir = getenv("TMPDIR");
- if (!tmpdir || !*tmpdir) tmpdir = "/tmp";
- tmpdir_len = cfree_slice_cstr(tmpdir).len;
- base_len = cfree_slice_cstr(base).len;
- path_len = tmpdir_len + base_len + suffix_len;
- path = e->heap->alloc(e->heap, path_len + 1u, 1);
- if (!path) return 0;
- memcpy(path, tmpdir, tmpdir_len);
- memcpy(path + tmpdir_len, base, base_len);
- if (suffix_len) memcpy(path + tmpdir_len + base_len, suffix, suffix_len);
- path[path_len] = '\0';
-
- fd = mkstemps(path, (int)suffix_len);
- if (fd < 0) goto out;
- if (initial_size &&
- !driver_write_fd_all(fd, initial ? initial : (const uint8_t*)"",
- initial_size))
- goto out;
- if (close(fd) != 0) {
- fd = -1;
- goto out;
- }
- fd = -1;
-
- editor = getenv("VISUAL");
- if (!editor || !*editor) editor = getenv("EDITOR");
- if (!editor || !*editor) editor = "vi";
- {
- size_t editor_len = cfree_slice_cstr(editor).len;
- size_t quoted_len = 0;
- char* quoted = driver_shell_quote_path(e, path, path_len, "ed_len);
- char* cmd;
- int status;
- pid_t pid;
- if (!quoted) goto out;
- cmd = e->heap->alloc(e->heap, editor_len + 1u + quoted_len + 1u, 1);
- if (!cmd) {
- e->heap->free(e->heap, quoted, quoted_len + 1u);
- goto out;
- }
- memcpy(cmd, editor, editor_len);
- cmd[editor_len] = ' ';
- memcpy(cmd + editor_len + 1u, quoted, quoted_len + 1u);
- e->heap->free(e->heap, quoted, quoted_len + 1u);
- pid = fork();
- if (pid == 0) {
- execl("/bin/sh", "sh", "-c", cmd, (char*)NULL);
- _exit(127);
- }
- e->heap->free(e->heap, cmd, editor_len + 1u + quoted_len + 1u);
- if (pid < 0) goto out;
- do {
- if (waitpid(pid, &status, 0) < 0) {
- if (errno == EINTR) continue;
- goto out;
- }
- break;
- } while (1);
- if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) goto out;
- }
-
- fd_data.data = NULL;
- fd_data.size = 0;
- fd_data.token = NULL;
- if (posix_read_all(e, path, &fd_data) != CFREE_OK) goto out;
- *out_data = (uint8_t*)fd_data.data;
- *out_size = fd_data.size;
- ok = 1;
-
-out:
- if (fd >= 0) close(fd);
- if (path) {
- unlink(path);
- e->heap->free(e->heap, path, path_len + 1u);
- }
- return ok;
-}
-
-void* driver_dlsym_resolver(void* user, CfreeSlice name_s) {
- /* The linker hands us interned/pool slices that are NUL-terminated, so
- * we can pass .s straight to dlsym (a host libc boundary). */
- const char* name = name_s.s;
- void* p;
- (void)user;
- if (!name || name_s.len == 0) return NULL;
- /* On Mach-O hosts the linker hands us C names with a leading underscore
- * (obj_format_c_mangle), but dlsym(RTLD_DEFAULT) expects the
- * source-level name. Try the stripped form first to avoid a wasted
- * dlsym call per libc symbol. */
-#if defined(__APPLE__)
- if (name[0] == '_' && name[1] != '\0') {
- p = dlsym(RTLD_DEFAULT, name + 1);
- if (p) return p;
- }
- return dlsym(RTLD_DEFAULT, name);
-#else
- p = dlsym(RTLD_DEFAULT, name);
- if (!p && name[0] == '_' && name[1] != '\0')
- p = dlsym(RTLD_DEFAULT, name + 1);
- return p;
-#endif
-}
-
-int driver_read_line(char* buf, size_t cap) {
- /* Reads one line from stdin into `buf` (cap >= 2 required). The trailing
- * newline is stripped; the result is NUL-terminated. Returns the number
- * of characters in the line on success, 0 at EOF (with buf[0]='\0'),
- * -1 on read error, or -2 when the read was interrupted by a signal
- * (errno == EINTR). Lines longer than cap-1 bytes are truncated to
- * cap-1 and the remainder up to the next newline is consumed. */
- size_t len = 0;
- if (!buf || cap < 2) return -1;
- for (;;) {
- int c;
- errno = 0;
- c = fgetc(stdin);
- if (c == EOF) {
- buf[len] = '\0';
- if (errno == EINTR) {
- clearerr(stdin);
- return -2;
- }
- if (ferror(stdin)) return -1;
- if (len == 0) return 0;
- return (int)len;
- }
- if (c == '\n') {
- buf[len] = '\0';
- return (int)len;
- }
- if (len + 1 < cap) buf[len++] = (char)c;
- /* else: drop overflow; keep consuming until the newline. */
- }
-}
-
-void driver_flush_stdout(void) { fflush(stdout); }
-
-/* SIGINT handling for the dbg REPL. The driver installs a handler that
- * just calls `cb(user)` so the dbg TU can decide what to do (asynchronously
- * call cfree_jit_session_interrupt). The handler is short and async-signal
- * safe by construction; the cb is the caller's responsibility. */
-
-static void (*s_sigint_cb)(void*);
-static void* s_sigint_cb_user;
-
-static void sigint_trampoline(int sig) {
- (void)sig;
- if (s_sigint_cb) s_sigint_cb(s_sigint_cb_user);
-}
-
-int driver_install_sigint(void (*cb)(void*), void* user) {
- struct sigaction sa;
- s_sigint_cb = cb;
- s_sigint_cb_user = user;
- sa.sa_handler = sigint_trampoline;
- sigemptyset(&sa.sa_mask);
- sa.sa_flags = 0; /* no SA_RESTART: fgetc returns EINTR */
- return sigaction(SIGINT, &sa, NULL) == 0 ? 0 : 1;
-}
-
-void driver_restore_sigint(void) {
- struct sigaction sa;
- s_sigint_cb = NULL;
- s_sigint_cb_user = NULL;
- sa.sa_handler = SIG_DFL;
- sigemptyset(&sa.sa_mask);
- sa.sa_flags = 0;
- sigaction(SIGINT, &sa, NULL);
-}
-
-CfreeTarget driver_host_target(void) {
- CfreeTarget t;
-#if defined(__x86_64__)
- t.arch = CFREE_ARCH_X86_64;
-#elif defined(__aarch64__)
- t.arch = CFREE_ARCH_ARM_64;
-#elif defined(__arm__)
- t.arch = CFREE_ARCH_ARM_32;
-#elif defined(__i386__)
- t.arch = CFREE_ARCH_X86_32;
-#elif defined(__riscv) && (__riscv_xlen == 64)
- t.arch = CFREE_ARCH_RV64;
-#elif defined(__riscv) && (__riscv_xlen == 32)
- t.arch = CFREE_ARCH_RV32;
-#elif defined(__wasm__)
- t.arch = CFREE_ARCH_WASM;
-#else
- t.arch = CFREE_ARCH_X86_64;
-#endif
-
-#if defined(__APPLE__)
- t.os = CFREE_OS_MACOS;
- t.obj = CFREE_OBJ_MACHO;
-#elif defined(__linux__)
- t.os = CFREE_OS_LINUX;
- t.obj = CFREE_OBJ_ELF;
-#elif defined(_WIN32)
- t.os = CFREE_OS_WINDOWS;
- t.obj = CFREE_OBJ_COFF;
-#elif defined(__wasi__)
- t.os = CFREE_OS_WASI;
- t.obj = CFREE_OBJ_WASM;
-#else
- t.os = CFREE_OS_FREESTANDING;
- t.obj = CFREE_OBJ_ELF;
-#endif
-
- t.ptr_size = (uint8_t)sizeof(void*);
- t.ptr_align = (uint8_t)sizeof(void*);
- t.big_endian = 0;
- t.pic = driver_default_pic(t.obj, t.os);
- t.code_model = CFREE_CM_DEFAULT;
- return t;
-}
diff --git a/driver/env/common.c b/driver/env/common.c
@@ -0,0 +1,208 @@
+/* Pure libc bits with no OS-specific behavior: the heap vtable, the
+ * stderr diag sink, stdout/fd writers, and the small printf/errf/alloc
+ * helpers that route through stdio + malloc. Compiled on every host. */
+
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "env_internal.h"
+
+/* ---------------- heap (libc-backed) ---------------- */
+
+static void* heap_libc_alloc(CfreeHeap* h, size_t size, size_t align) {
+ (void)h;
+ (void)align; /* malloc satisfies all max_align_t alignments */
+ return size ? malloc(size) : NULL;
+}
+
+static void* heap_libc_realloc(CfreeHeap* h, void* p, size_t old_size,
+ size_t new_size, size_t align) {
+ (void)h;
+ (void)old_size;
+ (void)align;
+ return realloc(p, new_size);
+}
+
+static void heap_libc_free(CfreeHeap* h, void* p, size_t size) {
+ (void)h;
+ (void)size;
+ free(p);
+}
+
+CfreeHeap g_heap_libc = {
+ heap_libc_alloc,
+ heap_libc_realloc,
+ heap_libc_free,
+ NULL,
+};
+
+/* ---------------- diag sink (stderr) ---------------- */
+
+static const char* diag_label(CfreeDiagKind k) {
+ switch (k) {
+ case CFREE_DIAG_NOTE:
+ return "note";
+ case CFREE_DIAG_WARN:
+ return "warning";
+ case CFREE_DIAG_ERROR:
+ return "error";
+ case CFREE_DIAG_FATAL:
+ return "fatal";
+ }
+ return "diag";
+}
+
+/* Tracks the compiler currently driving libcfree calls so the stderr
+ * diag sink can resolve loc.file_id to the source's spelling (path or
+ * memory-input label). NULL falls back to the numeric `<file:%u>` form. */
+static CfreeCompiler* g_diag_active_compiler;
+
+void driver_diag_set_compiler(CfreeCompiler* c) { g_diag_active_compiler = c; }
+
+CfreeStatus driver_compiler_new(CfreeTarget t, const CfreeContext* ctx,
+ CfreeCompiler** out) {
+ CfreeCompiler* c = NULL;
+ CfreeStatus st = cfree_compiler_new(t, ctx, &c);
+ if (st != CFREE_OK) {
+ if (out) *out = NULL;
+ return st;
+ }
+ driver_diag_set_compiler(c);
+ if (out) *out = c;
+ return CFREE_OK;
+}
+
+void driver_compiler_free(CfreeCompiler* c) {
+ if (!c) return;
+ if (g_diag_active_compiler == c) driver_diag_set_compiler(NULL);
+ cfree_compiler_free(c);
+}
+
+static void diag_stderr_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc,
+ const char* fmt, va_list ap) {
+ (void)s;
+ if (loc.file_id || loc.line) {
+ CfreeSlice name =
+ cfree_compiler_file_name(g_diag_active_compiler, loc.file_id);
+ if (name.len) {
+ fprintf(stderr, "%.*s:%u:%u: %.*s: ", CFREE_SLICE_ARG(name), loc.line,
+ loc.col, CFREE_SLICE_ARG(cfree_slice_cstr(diag_label(k))));
+ } else {
+ fprintf(stderr, "<file:%u>:%u:%u: %.*s: ", loc.file_id, loc.line, loc.col,
+ CFREE_SLICE_ARG(cfree_slice_cstr(diag_label(k))));
+ }
+ } else {
+ fprintf(stderr, "%.*s: ", CFREE_SLICE_ARG(cfree_slice_cstr(diag_label(k))));
+ }
+ vfprintf(stderr, fmt, ap);
+ fputc('\n', stderr);
+}
+
+CfreeDiagSink g_diag_stderr = {
+ diag_stderr_emit,
+ NULL,
+ 0,
+ 0,
+};
+
+/* ---------------- alloc helpers ---------------- */
+
+void* driver_alloc(DriverEnv* e, size_t n) {
+ return e->heap->alloc(e->heap, n, _Alignof(max_align_t));
+}
+
+void* driver_alloc_zeroed(DriverEnv* e, size_t n) {
+ void* p = driver_alloc(e, n);
+ if (p) memset(p, 0, n);
+ return p;
+}
+
+void driver_free(DriverEnv* e, void* p, size_t n) {
+ if (p) e->heap->free(e->heap, p, n);
+}
+
+void driver_memcpy(void* dst, const void* src, size_t n) {
+ memcpy(dst, src, n);
+}
+
+/* ---------------- string predicates ---------------- */
+
+/* The driver's only NUL-terminated-string handling: thin boundary shims
+ * that route every length scan through cfree_slice_cstr and otherwise use
+ * the length-based mem* primitives. No libc str* is used. */
+
+int driver_streq(const char* a, const char* b) {
+ return cfree_slice_eq(cfree_slice_cstr(a), cfree_slice_cstr(b));
+}
+
+int driver_strneq(const char* a, const char* b, size_t n) {
+ size_t i;
+ for (i = 0; i < n; ++i) {
+ unsigned char ca = (unsigned char)a[i], cb = (unsigned char)b[i];
+ if (ca != cb) return 0;
+ if (ca == '\0') return 1;
+ }
+ return 1;
+}
+
+size_t driver_strlen(const char* s) { return cfree_slice_cstr(s).len; }
+
+const char* driver_strchr(const char* s, int c) {
+ /* search includes the terminator so driver_strchr(s, 0) works like strchr */
+ return (const char*)memchr(s, c, cfree_slice_cstr(s).len + 1u);
+}
+
+const char* driver_basename(const char* path) {
+ size_t i = cfree_slice_cstr(path).len;
+ while (i > 0) {
+ if (path[i - 1] == '/') return path + i;
+ --i;
+ }
+ return path;
+}
+
+int driver_has_suffix(const char* s, const char* suffix) {
+ size_t ls = cfree_slice_cstr(s).len;
+ size_t lf = cfree_slice_cstr(suffix).len;
+ return ls >= lf && memcmp(s + ls - lf, suffix, lf) == 0;
+}
+
+/* ---------------- printf/errf/logf ---------------- */
+
+void driver_errf(const char* tool, const char* fmt, ...) {
+ va_list ap;
+ fprintf(stderr, "%.*s: ", CFREE_SLICE_ARG(cfree_slice_cstr(tool)));
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+ fputc('\n', stderr);
+}
+
+void driver_logf(const char* fmt, ...) {
+ va_list ap;
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+ fputc('\n', stderr);
+}
+
+void cfree_debug_printf(const char* fmt, ...) {
+ va_list ap;
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+}
+
+void driver_printf(const char* fmt, ...) {
+ va_list ap;
+ va_start(ap, fmt);
+ vprintf(fmt, ap);
+ va_end(ap);
+}
+
+void driver_flush_stdout(void) { fflush(stdout); }
+
+const char* driver_getenv(const char* name) { return getenv(name); }
diff --git a/driver/env/env_internal.h b/driver/env/env_internal.h
@@ -0,0 +1,130 @@
+#ifndef CFREE_DRIVER_ENV_INTERNAL_H
+#define CFREE_DRIVER_ENV_INTERNAL_H
+
+/* Internal header shared by the driver/env/ TUs.
+ *
+ * Each TU implements one slice of the host environment with zero
+ * preprocessor conditionals -- selection is done by the Makefile, which
+ * picks at most one of {macos,linux,freebsd,windows}.c, one of
+ * icache_<arch>.c, and one of uctx_<arch>_<os>.c per build.
+ *
+ * The shared scaffolding lives in common.c (libc-pure), posix.c
+ * (mac/linux/freebsd I/O), posix_dbg.c (pthreads + signals + sigsetjmp),
+ * and jit_tls_posix.c (pthread_key TLS). The per-OS file fills in the
+ * three hooks that genuinely differ: dual-mapped exec memory, the
+ * dbg W^X dance, and a few small bits (mtime ns flavor, dlsym mangling,
+ * host_target os/obj).
+ */
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include <cfree/compile.h>
+#include <cfree/core.h>
+#include <cfree/dbg.h>
+
+#include "../driver.h"
+#include "../env.h"
+
+/* ---- exec_dual registry (defined in posix.c) -----------------------------
+ * Apple/Linux/FreeBSD dual-mapping execmem produces a write alias and a
+ * runtime alias at distinct virtual addresses. The dbg code_write_begin
+ * path needs to translate a runtime address into the corresponding write
+ * alias. Single-mapping reservations (write == runtime) are not registered.
+ */
+void exec_dual_register(void* write_base, void* runtime_base, size_t size);
+void exec_dual_unregister(void* runtime_base);
+int exec_dual_lookup(void* runtime_addr, size_t n, void** write_out);
+
+/* Per-region bookkeeping carried on CfreeExecMemRegion.token.
+ * EXEC reservations carry a token; single-mapping ones leave token NULL. */
+typedef struct ExecMemToken {
+ void* write_addr;
+ void* runtime_addr;
+ size_t size;
+} ExecMemToken;
+
+/* Page size & single-mapping helpers (posix.c). */
+size_t driver_host_page_size(void);
+CfreeStatus execmem_reserve_single(size_t size, CfreeExecMemRegion* out);
+int cfree_to_posix_prot(int prot);
+
+/* Cache directory storage (posix.c). */
+extern char g_cache_dir[4096];
+
+/* ---- icache (icache_<arch>.c) --------------------------------------------
+ * Single arch-specific entry used by execmem_flush_icache and by per-OS
+ * dbg_flush_icache impls that don't have a better OS-native primitive.
+ */
+void env_flush_icache(void* addr, size_t n);
+
+/* ---- Linux x86_64 runtime-alias hint (linux_exec_hint_<arch>.c) ---------
+ * On x86_64 Linux the runtime alias is placed in the low 2 GiB so that
+ * direct call/jmp displacements from text reach without thunks. Other
+ * arches return 0/NULL and treat this as a no-op. Selected by Makefile
+ * via HOST_ARCH on Linux builds. */
+int env_execmem_runtime_extra_flags(void);
+void* env_execmem_low_runtime_hint(size_t size);
+
+/* ---- ucontext marshalling (uctx_<arch>_<os>.c) ---------------------------
+ * Provided per (arch, OS) combo. The signal handler in posix_dbg.c calls
+ * these to snapshot register state into a CfreeUnwindFrame and write
+ * (potentially session-edited) state back.
+ */
+#include <ucontext.h>
+void dbg_ucontext_to_frame(const ucontext_t* uc, CfreeUnwindFrame* f);
+void dbg_frame_to_ucontext(const CfreeUnwindFrame* f, ucontext_t* uc);
+
+/* ---- per-OS hooks (macos.c / linux.c / freebsd.c / windows.c) ------------
+ * Each hook has exactly one definition per build, selected by Makefile.
+ */
+
+#include <sys/stat.h>
+
+/* Reserve `size` bytes of W^X execute memory. May install a dual mapping
+ * (Apple mach_vm_remap, Linux memfd, FreeBSD SHM_ANON) or fall back to the
+ * single-mapping path on hosts without a dual primitive. */
+CfreeStatus os_execmem_reserve_exec(size_t size, CfreeExecMemRegion* out);
+
+/* dbg W^X transitions. begin yields a writable address (the alias on
+ * dual-mapped, runtime_addr with mprotect on single-mapped). end pairs
+ * with begin and may be a no-op (Apple) or mprotect-back-to-RX (Linux).
+ * Signatures match the CfreeDbgOs vtable shape for direct wiring. */
+CfreeStatus os_dbg_code_write_begin(void* user, void* runtime_addr, size_t n,
+ void** write_out);
+void os_dbg_code_write_end(void* user, void* runtime_addr, size_t n);
+
+/* Instruction-cache flush after dbg writes. Apple aarch64 calls
+ * sys_icache_invalidate; others delegate to env_flush_icache. */
+void os_dbg_flush_icache(void* user, void* runtime_addr, size_t n);
+
+/* Read st_mtim/st_mtimespec into nanoseconds. Returns 0 on success. */
+int os_stat_mtime_ns(const struct stat* sb, int64_t* out);
+
+/* Symbol resolver with platform name-mangling rules (Apple strips leading
+ * underscore for dlsym; ELF passes through). */
+void* os_dlsym(const char* name);
+
+/* Fill the OS/object-format slots of CfreeTarget for the host. */
+void os_host_target_fill(CfreeTarget* t);
+
+/* ---- vtable singletons wired into DriverEnv ----------------------------
+ * Each is defined in exactly one TU; driver_env_init pulls them together.
+ */
+extern CfreeHeap g_heap_libc; /* common.c */
+extern CfreeDiagSink g_diag_stderr; /* common.c */
+extern CfreeExecMem g_execmem_posix; /* posix.c — page_size set in init */
+extern CfreeDbgOs g_dbg_os_posix; /* posix_dbg.c */
+extern CfreeJitTls g_jit_tls_posix; /* jit_tls_posix.c */
+
+/* posix_dbg.c exposes a worker-thread check for the signal handler. */
+int posix_dbg_caller_is_worker(void);
+
+/* ---- POSIX dbg state shared with the per-OS code_write paths ------------
+ * Used only when the OS impl needs to introspect dbg state. Defined in
+ * posix_dbg.c.
+ */
+#include <signal.h>
+#define DBG_INTERRUPT_SIGNO SIGUSR2
+
+#endif
diff --git a/driver/env/freebsd.c b/driver/env/freebsd.c
@@ -0,0 +1,137 @@
+/* FreeBSD-specific env bits. UNTESTED -- written from the FreeBSD 13+ man
+ * pages and matched against the Linux/macOS impls; needs real-host
+ * verification before being trusted in production. Uses:
+ * - memfd_create(3) (FreeBSD 13+) for the dual-map fd
+ * - st_mtim (POSIX.1-2008) for mtime
+ * - dlsym(RTLD_DEFAULT) for the resolver
+ *
+ * If this file ever needs to support pre-13 FreeBSD, switch the fd source
+ * to shm_open(SHM_ANON, O_RDWR, 0) + shm_unlink(); the rest of the dance
+ * (ftruncate + two mmaps of the fd at distinct VAs) is identical. */
+
+#include <dlfcn.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "env_internal.h"
+
+/* ---------------- dual-mapped exec memory ---------------- */
+
+CfreeStatus os_execmem_reserve_exec(size_t size, CfreeExecMemRegion* out) {
+ int fd = memfd_create("cfree-jit", 0);
+ void* w;
+ void* r;
+ ExecMemToken* tok;
+ if (fd < 0) return CFREE_ERR;
+ if (ftruncate(fd, (off_t)size) != 0) {
+ close(fd);
+ return CFREE_ERR;
+ }
+
+ w = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+ if (w == MAP_FAILED) {
+ close(fd);
+ return CFREE_NOMEM;
+ }
+ r = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
+ if (r == MAP_FAILED) {
+ munmap(w, size);
+ close(fd);
+ return CFREE_NOMEM;
+ }
+ close(fd);
+
+ tok = (ExecMemToken*)malloc(sizeof(*tok));
+ if (!tok) {
+ munmap(r, size);
+ munmap(w, size);
+ return CFREE_NOMEM;
+ }
+ tok->write_addr = w;
+ tok->runtime_addr = r;
+ tok->size = size;
+
+ exec_dual_register(w, r, size);
+
+ out->write = w;
+ out->runtime = r;
+ out->size = size;
+ out->token = tok;
+ return CFREE_OK;
+}
+
+/* ---------------- dbg W^X dance ---------------- */
+/* Same shape as Linux: prefer alias lookup, fall back to transient mprotect
+ * of the runtime alias for single-mapping reservations. */
+
+static size_t page_floor(size_t v, size_t pg) { return v & ~(pg - 1); }
+static size_t page_ceil(size_t v, size_t pg) {
+ return (v + pg - 1) & ~(pg - 1);
+}
+
+CfreeStatus os_dbg_code_write_begin(void* user, void* runtime_addr, size_t n,
+ void** write_out) {
+ size_t pg;
+ uintptr_t a;
+ uintptr_t base;
+ size_t span;
+ (void)user;
+ if (!runtime_addr || !n || !write_out) return CFREE_INVALID;
+ if (exec_dual_lookup(runtime_addr, n, write_out) == 0) return CFREE_OK;
+ pg = driver_host_page_size();
+ a = (uintptr_t)runtime_addr;
+ base = page_floor(a, pg);
+ span = page_ceil((a - base) + n, pg);
+ if (mprotect((void*)base, span, PROT_READ | PROT_WRITE | PROT_EXEC) != 0)
+ return CFREE_ERR;
+ *write_out = runtime_addr;
+ return CFREE_OK;
+}
+
+void os_dbg_code_write_end(void* user, void* runtime_addr, size_t n) {
+ void* w;
+ size_t pg;
+ uintptr_t a;
+ uintptr_t base;
+ size_t span;
+ (void)user;
+ if (exec_dual_lookup(runtime_addr, n, &w) == 0) return;
+ pg = driver_host_page_size();
+ a = (uintptr_t)runtime_addr;
+ base = page_floor(a, pg);
+ span = page_ceil((a - base) + n, pg);
+ mprotect((void*)base, span, PROT_READ | PROT_EXEC);
+}
+
+void os_dbg_flush_icache(void* user, void* runtime_addr, size_t n) {
+ (void)user;
+ env_flush_icache(runtime_addr, n);
+}
+
+/* ---------------- st_mtim ---------------- */
+
+int os_stat_mtime_ns(const struct stat* sb, int64_t* out) {
+ *out = (int64_t)sb->st_mtim.tv_sec * 1000000000LL +
+ (int64_t)sb->st_mtim.tv_nsec;
+ return 0;
+}
+
+/* ---------------- dlsym ---------------- */
+
+void* os_dlsym(const char* name) {
+ void* p = dlsym(RTLD_DEFAULT, name);
+ if (!p && name[0] == '_' && name[1] != '\0')
+ p = dlsym(RTLD_DEFAULT, name + 1);
+ return p;
+}
+
+/* ---------------- host_target os/obj ---------------- */
+
+void os_host_target_fill(CfreeTarget* t) {
+ t->os = CFREE_OS_FREEBSD;
+ t->obj = CFREE_OBJ_ELF;
+}
diff --git a/driver/env/icache_arm.c b/driver/env/icache_arm.c
@@ -0,0 +1,11 @@
+/* aarch64 / arm32 icache flush. __builtin___clear_cache lowers to the
+ * arch-correct DC CVAU + IC IVAU + ISB sequence (or the libgcc helper on
+ * arm32). */
+
+#include <stddef.h>
+
+#include "env_internal.h"
+
+void env_flush_icache(void* addr, size_t n) {
+ __builtin___clear_cache((char*)addr, (char*)addr + n);
+}
diff --git a/driver/env/icache_riscv.c b/driver/env/icache_riscv.c
@@ -0,0 +1,12 @@
+/* riscv32 / riscv64 icache flush. Emit fence.i first so the current hart
+ * sees freshly written bytes immediately, then __builtin___clear_cache
+ * lowers to __riscv_flush_icache for cross-hart visibility (Linux). */
+
+#include <stddef.h>
+
+#include "env_internal.h"
+
+void env_flush_icache(void* addr, size_t n) {
+ __asm__ __volatile__("fence.i" ::: "memory");
+ __builtin___clear_cache((char*)addr, (char*)addr + n);
+}
diff --git a/driver/env/icache_x86.c b/driver/env/icache_x86.c
@@ -0,0 +1,14 @@
+/* x86 / x86_64 icache: a no-op. x86 instruction fetches are coherent with
+ * stores from the same core (the architecture spec mandates self-modifying
+ * code visibility on the next branch / serializing instruction). The JIT
+ * dispatch back to generated code is always a return / call from a
+ * different function, which is a sufficient serialization point. */
+
+#include <stddef.h>
+
+#include "env_internal.h"
+
+void env_flush_icache(void* addr, size_t n) {
+ (void)addr;
+ (void)n;
+}
diff --git a/driver/env/jit_tls_posix.c b/driver/env/jit_tls_posix.c
@@ -0,0 +1,121 @@
+/* pthread_key-backed CfreeJitTls. Backs `cfree run` on Mach-O targets:
+ * every JIT image with TLS gets one pthread_key, the per-thread block is
+ * allocated lazily on first access, and freed via the key's destructor
+ * when the thread exits.
+ *
+ * The ctx layout is fixed by the contract in src/jit/tlv_thunk.h: the
+ * first 8 bytes MUST be a function pointer the asm thunk calls with
+ * x0 = ctx and expects back an x0 = TLS block. We satisfy this by making
+ * `get_block` the first field. */
+
+#include <pthread.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "env_internal.h"
+
+typedef struct JitTlsCtx {
+ void* (*get_block)(void* ctx); /* first; matches tlv_thunk's expectation */
+ pthread_key_t key;
+ size_t image_size;
+ size_t image_filesz;
+ size_t align;
+ void* init_bytes; /* heap-owned copy of init bytes, or NULL if all BSS */
+} JitTlsCtx;
+
+static void jit_tls_thread_dtor(void* block) {
+ /* POSIX pthread_key destructor: called when a thread that touched the
+ * TLV exits. `block` is the void* set by pthread_setspecific, never
+ * NULL (POSIX skips the destructor if it was NULL). */
+ free(block);
+}
+
+static void* jit_tls_alloc_block(JitTlsCtx* ctx) {
+ /* macOS aligned_alloc requires alignment >= sizeof(void*); bump
+ * smaller request alignments up. Size must be a multiple of
+ * alignment too. */
+ size_t a = ctx->align ? ctx->align : sizeof(void*);
+ size_t sz;
+ void* block;
+ if (a < sizeof(void*)) a = sizeof(void*);
+ sz = (ctx->image_size + a - 1u) & ~(a - 1u);
+ if (sz == 0) sz = a; /* zero-size TLS image still needs a non-NULL block */
+ block = aligned_alloc(a, sz);
+ if (!block) return NULL;
+ if (ctx->image_filesz && ctx->init_bytes)
+ memcpy(block, ctx->init_bytes, ctx->image_filesz);
+ if (ctx->image_size > ctx->image_filesz)
+ memset((char*)block + ctx->image_filesz, 0,
+ ctx->image_size - ctx->image_filesz);
+ return block;
+}
+
+static void* jit_tls_get_block(void* ctx_v) {
+ JitTlsCtx* ctx = (JitTlsCtx*)ctx_v;
+ void* block = pthread_getspecific(ctx->key);
+ if (block) return block;
+ block = jit_tls_alloc_block(ctx);
+ if (!block) {
+ fprintf(stderr,
+ "cfree run: out of memory allocating per-thread TLS block\n");
+ abort();
+ }
+ if (pthread_setspecific(ctx->key, block) != 0) {
+ fprintf(stderr, "cfree run: pthread_setspecific failed in TLV thunk\n");
+ abort();
+ }
+ return block;
+}
+
+static void* jit_tls_ctx_new(void* user, const void* init_bytes,
+ size_t image_filesz, size_t image_size,
+ size_t align) {
+ JitTlsCtx* ctx;
+ (void)user;
+ ctx = (JitTlsCtx*)malloc(sizeof(*ctx));
+ if (!ctx) return NULL;
+ ctx->get_block = jit_tls_get_block;
+ ctx->image_size = image_size;
+ ctx->image_filesz = image_filesz;
+ ctx->align = align ? align : sizeof(void*);
+ ctx->init_bytes = NULL;
+ if (image_filesz && init_bytes) {
+ ctx->init_bytes = malloc(image_filesz);
+ if (!ctx->init_bytes) {
+ free(ctx);
+ return NULL;
+ }
+ memcpy(ctx->init_bytes, init_bytes, image_filesz);
+ }
+ if (pthread_key_create(&ctx->key, jit_tls_thread_dtor) != 0) {
+ free(ctx->init_bytes);
+ free(ctx);
+ return NULL;
+ }
+ return ctx;
+}
+
+static void jit_tls_ctx_destroy(void* user, void* ctx_v) {
+ JitTlsCtx* ctx = (JitTlsCtx*)ctx_v;
+ void* my_block;
+ (void)user;
+ if (!ctx) return;
+ /* Free the calling thread's block (POSIX won't run our destructor for
+ * it; pthread_key_delete also doesn't fire destructors for live
+ * threads). Other threads' blocks are reaped when those threads exit. */
+ my_block = pthread_getspecific(ctx->key);
+ if (my_block) {
+ pthread_setspecific(ctx->key, NULL);
+ free(my_block);
+ }
+ pthread_key_delete(ctx->key);
+ free(ctx->init_bytes);
+ free(ctx);
+}
+
+CfreeJitTls g_jit_tls_posix = {
+ .ctx_new = jit_tls_ctx_new,
+ .ctx_destroy = jit_tls_ctx_destroy,
+ .user = NULL,
+};
diff --git a/driver/env/linux.c b/driver/env/linux.c
@@ -0,0 +1,142 @@
+/* Linux-specific env bits: memfd_create dual-mapped exec memory (+ optional
+ * MAP_32BIT runtime-alias hint on x86_64 to keep direct call/jmp
+ * displacements in range from text), st_mtim mtime, dlsym pass-through
+ * with optional `_` retry, host_target OS=LINUX/OBJ=ELF. */
+
+#include <dlfcn.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <unistd.h>
+
+#include "env_internal.h"
+
+/* ---------------- dual-mapped exec memory (memfd_create) ----------------
+ * memfd_create gives us an anonymous fd; two mmaps of that fd alias the
+ * same physical pages at distinct VAs. Both aliases live across the
+ * close(fd) below. The runtime-alias hint and any extra mmap flags are
+ * arch-specific (x86_64 uses MAP_32BIT to keep direct displacements in
+ * range from text); see linux_exec_hint_<arch>.c. */
+CfreeStatus os_execmem_reserve_exec(size_t size, CfreeExecMemRegion* out) {
+ int fd = (int)syscall(SYS_memfd_create, "cfree-jit", 0u);
+ int extra = env_execmem_runtime_extra_flags();
+ void* w;
+ void* r;
+ ExecMemToken* tok;
+ if (fd < 0) return CFREE_ERR;
+ if (ftruncate(fd, (off_t)size) != 0) {
+ close(fd);
+ return CFREE_ERR;
+ }
+
+ w = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED | extra, fd, 0);
+ if (w == MAP_FAILED) {
+ close(fd);
+ return CFREE_NOMEM;
+ }
+ r = mmap(env_execmem_low_runtime_hint(size), size, PROT_READ,
+ MAP_SHARED | extra, fd, 0);
+ if (r == MAP_FAILED) {
+ munmap(w, size);
+ close(fd);
+ return CFREE_NOMEM;
+ }
+ close(fd);
+
+ tok = (ExecMemToken*)malloc(sizeof(*tok));
+ if (!tok) {
+ munmap(r, size);
+ munmap(w, size);
+ return CFREE_NOMEM;
+ }
+ tok->write_addr = w;
+ tok->runtime_addr = r;
+ tok->size = size;
+
+ exec_dual_register(w, r, size);
+
+ out->write = w;
+ out->runtime = r;
+ out->size = size;
+ out->token = tok;
+ return CFREE_OK;
+}
+
+/* ---------------- dbg W^X dance ---------------- */
+/* Linux dual-mapping uses memfd: write alias and runtime alias have
+ * distinct VAs. Prefer the alias lookup; fall back to a transient
+ * mprotect of the runtime alias for single-mapping reservations. */
+
+static size_t page_floor(size_t v, size_t pg) { return v & ~(pg - 1); }
+static size_t page_ceil(size_t v, size_t pg) {
+ return (v + pg - 1) & ~(pg - 1);
+}
+
+CfreeStatus os_dbg_code_write_begin(void* user, void* runtime_addr, size_t n,
+ void** write_out) {
+ size_t pg;
+ uintptr_t a;
+ uintptr_t base;
+ size_t span;
+ (void)user;
+ if (!runtime_addr || !n || !write_out) return CFREE_INVALID;
+ if (exec_dual_lookup(runtime_addr, n, write_out) == 0) return CFREE_OK;
+ pg = driver_host_page_size();
+ a = (uintptr_t)runtime_addr;
+ base = page_floor(a, pg);
+ span = page_ceil((a - base) + n, pg);
+ if (mprotect((void*)base, span, PROT_READ | PROT_WRITE | PROT_EXEC) != 0)
+ return CFREE_ERR;
+ *write_out = runtime_addr;
+ return CFREE_OK;
+}
+
+void os_dbg_code_write_end(void* user, void* runtime_addr, size_t n) {
+ void* w;
+ size_t pg;
+ uintptr_t a;
+ uintptr_t base;
+ size_t span;
+ (void)user;
+ if (exec_dual_lookup(runtime_addr, n, &w) == 0)
+ return; /* dual: nothing to flip back */
+ pg = driver_host_page_size();
+ a = (uintptr_t)runtime_addr;
+ base = page_floor(a, pg);
+ span = page_ceil((a - base) + n, pg);
+ mprotect((void*)base, span, PROT_READ | PROT_EXEC);
+}
+
+void os_dbg_flush_icache(void* user, void* runtime_addr, size_t n) {
+ (void)user;
+ env_flush_icache(runtime_addr, n);
+}
+
+/* ---------------- st_mtim ---------------- */
+
+int os_stat_mtime_ns(const struct stat* sb, int64_t* out) {
+ *out = (int64_t)sb->st_mtim.tv_sec * 1000000000LL +
+ (int64_t)sb->st_mtim.tv_nsec;
+ return 0;
+}
+
+/* ---------------- dlsym ---------------- */
+/* ELF has no underscore mangling; pass through. The retry with a stripped
+ * leading underscore matches the old shim's behavior for users that hand
+ * us Mach-O-mangled names. */
+void* os_dlsym(const char* name) {
+ void* p = dlsym(RTLD_DEFAULT, name);
+ if (!p && name[0] == '_' && name[1] != '\0')
+ p = dlsym(RTLD_DEFAULT, name + 1);
+ return p;
+}
+
+/* ---------------- host_target os/obj ---------------- */
+
+void os_host_target_fill(CfreeTarget* t) {
+ t->os = CFREE_OS_LINUX;
+ t->obj = CFREE_OBJ_ELF;
+}
diff --git a/driver/env/linux_exec_hint_default.c b/driver/env/linux_exec_hint_default.c
@@ -0,0 +1,14 @@
+/* Default Linux runtime-alias hint for arches without MAP_32BIT-style
+ * displacement constraints (aarch64, rv64, arm32, ...): no extra mmap
+ * flags and no address preference; let the kernel pick. */
+
+#include <stddef.h>
+
+#include "env_internal.h"
+
+int env_execmem_runtime_extra_flags(void) { return 0; }
+
+void* env_execmem_low_runtime_hint(size_t size) {
+ (void)size;
+ return NULL;
+}
diff --git a/driver/env/linux_exec_hint_x86_64.c b/driver/env/linux_exec_hint_x86_64.c
@@ -0,0 +1,22 @@
+/* x86_64 Linux runtime-alias hint: place the runtime alias in the low 2 GiB
+ * (MAP_32BIT) so that direct call/jmp displacements from text (typically
+ * loaded around 0x400000 under PIE) reach without large-code-model thunks.
+ * The write alias has no PC-relative usage and can live anywhere. */
+
+#include <stddef.h>
+#include <stdint.h>
+#include <sys/mman.h>
+
+#include "env_internal.h"
+
+int env_execmem_runtime_extra_flags(void) { return MAP_32BIT; }
+
+void* env_execmem_low_runtime_hint(size_t size) {
+ static uintptr_t g = 0x40000000u;
+ uintptr_t p = g;
+ uintptr_t step = (uintptr_t)((size + 0xffffu) & ~(size_t)0xffffu);
+ if (step < 0x10000u) step = 0x10000u;
+ g = p + step + 0x10000u;
+ if (g > 0x78000000u) g = 0x40000000u;
+ return (void*)p;
+}
diff --git a/driver/env/macos.c b/driver/env/macos.c
@@ -0,0 +1,130 @@
+/* macOS-specific env bits: mach_vm_remap dual-mapped exec memory,
+ * sys_icache_invalidate for the dbg flush path, st_mtimespec mtime,
+ * dlsym leading-underscore handling, host_target OS=MACOS/OBJ=MACHO.
+ *
+ * Compiled only on Darwin; the Makefile selects this file via
+ * HOST_UNAME=Darwin. No #ifdef inside.
+ */
+
+#include <dlfcn.h>
+#include <libkern/OSCacheControl.h>
+#include <mach/mach.h>
+#include <mach/mach_vm.h>
+#include <mach/vm_map.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+
+#include "env_internal.h"
+
+/* ---------------- dual-mapped exec memory ---------------- */
+/* 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 com.apple.security.cs.allow-unsigned-
+ * executable-memory). */
+CfreeStatus os_execmem_reserve_exec(size_t size, CfreeExecMemRegion* out) {
+ void* w;
+ mach_vm_address_t r_addr = 0;
+ vm_prot_t cur = 0, max = 0;
+ kern_return_t kr;
+ ExecMemToken* tok;
+
+ w = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
+ if (w == MAP_FAILED) return CFREE_NOMEM;
+
+ /* copy=FALSE: the new VA shares the same physical pages.
+ * VM_INHERIT_NONE: child processes don't observe the executable alias. */
+ 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 CFREE_ERR;
+ }
+
+ /* Drop the runtime alias to PROT_READ until protect() flips it. No
+ * window of writable+executable: write alias has W (no X), 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 CFREE_ERR;
+ }
+
+ tok = (ExecMemToken*)malloc(sizeof(*tok));
+ if (!tok) {
+ munmap((void*)(uintptr_t)r_addr, size);
+ munmap(w, size);
+ return CFREE_NOMEM;
+ }
+ tok->write_addr = w;
+ tok->runtime_addr = (void*)(uintptr_t)r_addr;
+ tok->size = size;
+
+ exec_dual_register(w, (void*)(uintptr_t)r_addr, size);
+
+ out->write = w;
+ out->runtime = (void*)(uintptr_t)r_addr;
+ out->size = size;
+ out->token = tok;
+ return CFREE_OK;
+}
+
+/* ---------------- dbg W^X dance ---------------- */
+/* Dual-mapped reservation: the write alias is a separate VA already RW.
+ * Translate via the registry; no protect flip required, so end is no-op. */
+CfreeStatus os_dbg_code_write_begin(void* user, void* runtime_addr, size_t n,
+ void** write_out) {
+ (void)user;
+ if (!runtime_addr || !n || !write_out) return CFREE_INVALID;
+ return exec_dual_lookup(runtime_addr, n, write_out) == 0 ? CFREE_OK
+ : CFREE_ERR;
+}
+
+void os_dbg_code_write_end(void* user, void* runtime_addr, size_t n) {
+ (void)user;
+ (void)runtime_addr;
+ (void)n;
+}
+
+/* sys_icache_invalidate is the documented Apple primitive on aarch64 and
+ * is a no-op on x86_64 (correctly modeling that x86 fetches are coherent
+ * with stores). Use it across arches for symmetry. */
+void os_dbg_flush_icache(void* user, void* runtime_addr, size_t n) {
+ (void)user;
+ sys_icache_invalidate(runtime_addr, n);
+}
+
+/* ---------------- st_mtim/mtimespec ---------------- */
+
+int os_stat_mtime_ns(const struct stat* sb, int64_t* out) {
+ *out = (int64_t)sb->st_mtimespec.tv_sec * 1000000000LL +
+ (int64_t)sb->st_mtimespec.tv_nsec;
+ return 0;
+}
+
+/* ---------------- dlsym name mangling ---------------- */
+/* On Mach-O the linker hands us C names with a leading underscore
+ * (obj_format_c_mangle), but dlsym(RTLD_DEFAULT) expects the source-level
+ * name. Try the stripped form first to avoid a wasted dlsym call per
+ * libc symbol. */
+void* os_dlsym(const char* name) {
+ void* p;
+ if (name[0] == '_' && name[1] != '\0') {
+ p = dlsym(RTLD_DEFAULT, name + 1);
+ if (p) return p;
+ }
+ return dlsym(RTLD_DEFAULT, name);
+}
+
+/* ---------------- host_target os/obj ---------------- */
+
+void os_host_target_fill(CfreeTarget* t) {
+ t->os = CFREE_OS_MACOS;
+ t->obj = CFREE_OBJ_MACHO;
+}
diff --git a/driver/env/posix.c b/driver/env/posix.c
@@ -0,0 +1,791 @@
+/* POSIX-shared environment: file I/O, mkdir, sigint, exec_dual registry,
+ * single-mapping execmem, monotonic clock, stdin/edit, dlsym resolver,
+ * driver_env_init for hosts where the POSIX TUs are compiled (Mac, Linux,
+ * FreeBSD). Each function here behaves identically on those three; per-OS
+ * specifics are isolated in macos.c / linux.c / freebsd.c. */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "env_internal.h"
+
+/* ---------------- exec memory: single-mapping core + registry ---------------- */
+
+int cfree_to_posix_prot(int prot) {
+ int p = 0;
+ if (prot & CFREE_PROT_READ) p |= PROT_READ;
+ if (prot & CFREE_PROT_WRITE) p |= PROT_WRITE;
+ if (prot & CFREE_PROT_EXEC) p |= PROT_EXEC;
+ return p;
+}
+
+/* Registry of EXEC reservations with distinct write/runtime aliases. The
+ * dbg_os code_write_begin path uses this to translate a runtime address
+ * into the corresponding write alias on dual-mapping hosts. Single-mapping
+ * reservations (write == runtime) are not registered. JITs typically hold
+ * 1-2 reservations live so a linked list keeps the lookup trivial. */
+typedef struct ExecDualNode {
+ void* write_base;
+ void* runtime_base;
+ size_t size;
+ struct ExecDualNode* next;
+} ExecDualNode;
+
+static ExecDualNode* g_jit_dual_map;
+static pthread_mutex_t g_jit_dual_map_mu = PTHREAD_MUTEX_INITIALIZER;
+
+void exec_dual_register(void* write_base, void* runtime_base, size_t size) {
+ ExecDualNode* n;
+ if (write_base == runtime_base) return;
+ n = (ExecDualNode*)malloc(sizeof(*n));
+ if (!n) return; /* registry is best-effort; lookup will fail open */
+ n->write_base = write_base;
+ n->runtime_base = runtime_base;
+ n->size = size;
+ pthread_mutex_lock(&g_jit_dual_map_mu);
+ n->next = g_jit_dual_map;
+ g_jit_dual_map = n;
+ pthread_mutex_unlock(&g_jit_dual_map_mu);
+}
+
+void exec_dual_unregister(void* runtime_base) {
+ ExecDualNode** pp;
+ pthread_mutex_lock(&g_jit_dual_map_mu);
+ for (pp = &g_jit_dual_map; *pp; pp = &(*pp)->next) {
+ if ((*pp)->runtime_base == runtime_base) {
+ ExecDualNode* dead = *pp;
+ *pp = dead->next;
+ free(dead);
+ break;
+ }
+ }
+ pthread_mutex_unlock(&g_jit_dual_map_mu);
+}
+
+int exec_dual_lookup(void* runtime_addr, size_t n, void** write_out) {
+ ExecDualNode* cur;
+ uintptr_t a = (uintptr_t)runtime_addr;
+ pthread_mutex_lock(&g_jit_dual_map_mu);
+ for (cur = g_jit_dual_map; cur; cur = cur->next) {
+ uintptr_t base = (uintptr_t)cur->runtime_base;
+ if (a >= base && a + n <= base + cur->size) {
+ *write_out = (void*)((uintptr_t)cur->write_base + (a - base));
+ pthread_mutex_unlock(&g_jit_dual_map_mu);
+ return 0;
+ }
+ }
+ pthread_mutex_unlock(&g_jit_dual_map_mu);
+ return 1;
+}
+
+CfreeStatus 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 CFREE_NOMEM;
+ out->write = p;
+ out->runtime = p;
+ out->size = size;
+ out->token = NULL; /* munmap suffices on release */
+ return CFREE_OK;
+}
+
+static CfreeStatus execmem_reserve(void* user, size_t size, int prot,
+ CfreeExecMemRegion* out) {
+ (void)user;
+ if (!out || !size) return CFREE_INVALID;
+ if (prot & CFREE_PROT_EXEC) return os_execmem_reserve_exec(size, out);
+ return execmem_reserve_single(size, out);
+}
+
+static CfreeStatus execmem_protect(void* user, void* addr, size_t size,
+ int prot) {
+ (void)user;
+ return mprotect(addr, size, cfree_to_posix_prot(prot)) == 0 ? CFREE_OK
+ : CFREE_ERR;
+}
+
+static void execmem_release(void* user, CfreeExecMemRegion* region) {
+ (void)user;
+ if (!region || !region->size) return;
+ if (region->token) {
+ ExecMemToken* tok = (ExecMemToken*)region->token;
+ if (tok->runtime_addr && tok->runtime_addr != tok->write_addr) {
+ exec_dual_unregister(tok->runtime_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) {
+ (void)user;
+ env_flush_icache(addr, size);
+}
+
+size_t driver_host_page_size(void) {
+ long p = sysconf(_SC_PAGESIZE);
+ return (p > 0) ? (size_t)p : (size_t)0x4000;
+}
+
+CfreeExecMem g_execmem_posix; /* page_size set in driver_env_init */
+
+/* ---------------- fd writer ---------------- */
+
+typedef struct DriverFdWriter {
+ CfreeWriter base; /* must be first; libcfree reads via this */
+ CfreeHeap* heap;
+ int fd;
+ CfreeStatus status;
+ uint64_t pos;
+} DriverFdWriter;
+
+static CfreeStatus fdw_write(CfreeWriter* w, const void* data, size_t n) {
+ DriverFdWriter* fw = (DriverFdWriter*)w;
+ const unsigned char* p = (const unsigned char*)data;
+ if (fw->status != CFREE_OK) return fw->status;
+ while (n > 0) {
+ ssize_t k = write(fw->fd, p, n);
+ if (k < 0) {
+ fw->status = CFREE_IO;
+ return CFREE_IO;
+ }
+ p += (size_t)k;
+ n -= (size_t)k;
+ fw->pos += (uint64_t)k;
+ }
+ return CFREE_OK;
+}
+
+static CfreeStatus fdw_seek(CfreeWriter* w, uint64_t off) {
+ DriverFdWriter* fw = (DriverFdWriter*)w;
+ if (fw->status != CFREE_OK) return fw->status;
+ if (lseek(fw->fd, (off_t)off, SEEK_SET) < 0) {
+ fw->status = CFREE_IO;
+ return CFREE_IO;
+ }
+ fw->pos = off;
+ return CFREE_OK;
+}
+
+static uint64_t fdw_tell(CfreeWriter* w) { return ((DriverFdWriter*)w)->pos; }
+static CfreeStatus fdw_status(CfreeWriter* w) {
+ return ((DriverFdWriter*)w)->status;
+}
+
+static void fdw_close(CfreeWriter* w) {
+ DriverFdWriter* fw = (DriverFdWriter*)w;
+ if (fw->fd >= 0) close(fw->fd);
+ fw->heap->free(fw->heap, fw, sizeof(*fw));
+}
+
+static CfreeWriter* driver_writer_fd(CfreeHeap* h, int fd) {
+ DriverFdWriter* fw =
+ (DriverFdWriter*)h->alloc(h, sizeof(*fw), _Alignof(DriverFdWriter));
+ if (!fw) return NULL;
+ fw->base.write = fdw_write;
+ fw->base.seek = fdw_seek;
+ fw->base.tell = fdw_tell;
+ fw->base.status = fdw_status;
+ fw->base.close = fdw_close;
+ fw->heap = h;
+ fw->fd = fd;
+ fw->status = CFREE_OK;
+ fw->pos = 0;
+ return &fw->base;
+}
+
+/* Stdout writer routes through stdio so it shares libc's buffer with
+ * driver_printf. */
+typedef struct DriverStdioWriter {
+ CfreeWriter base;
+ CfreeHeap* heap;
+ FILE* fp;
+ CfreeStatus status;
+} DriverStdioWriter;
+
+static CfreeStatus stdio_w_write(CfreeWriter* w, const void* data, size_t n) {
+ DriverStdioWriter* sw = (DriverStdioWriter*)w;
+ if (n) {
+ size_t got = fwrite(data, 1, n, sw->fp);
+ if (got != n) {
+ sw->status = CFREE_IO;
+ return CFREE_IO;
+ }
+ }
+ return CFREE_OK;
+}
+static CfreeStatus stdio_w_seek(CfreeWriter* w, uint64_t off) {
+ DriverStdioWriter* sw = (DriverStdioWriter*)w;
+ return fseek(sw->fp, (long)off, SEEK_SET) == 0 ? CFREE_OK : CFREE_IO;
+}
+static uint64_t stdio_w_tell(CfreeWriter* w) {
+ long t = ftell(((DriverStdioWriter*)w)->fp);
+ return t < 0 ? 0u : (uint64_t)t;
+}
+static CfreeStatus stdio_w_status(CfreeWriter* w) {
+ DriverStdioWriter* sw = (DriverStdioWriter*)w;
+ if (sw->status != CFREE_OK) return sw->status;
+ return ferror(sw->fp) ? CFREE_IO : CFREE_OK;
+}
+static void stdio_w_close(CfreeWriter* w) {
+ DriverStdioWriter* sw = (DriverStdioWriter*)w;
+ fflush(sw->fp); /* flush but do not close stdout */
+ sw->heap->free(sw->heap, sw, sizeof(*sw));
+}
+
+CfreeWriter* driver_stdout_writer(DriverEnv* e) {
+ DriverStdioWriter* sw = (DriverStdioWriter*)e->heap->alloc(
+ e->heap, sizeof(*sw), _Alignof(DriverStdioWriter));
+ if (!sw) return NULL;
+ sw->base.write = stdio_w_write;
+ sw->base.seek = stdio_w_seek;
+ sw->base.tell = stdio_w_tell;
+ sw->base.status = stdio_w_status;
+ sw->base.close = stdio_w_close;
+ sw->heap = e->heap;
+ sw->fp = stdout;
+ sw->status = CFREE_OK;
+ return &sw->base;
+}
+
+/* ---------------- file_io (POSIX open/read/write/stat) ---------------- */
+
+static CfreeStatus posix_read_all(void* user, const char* path,
+ CfreeFileData* out) {
+ DriverEnv* env = (DriverEnv*)user;
+ int fd;
+ struct stat sb;
+ size_t size;
+ size_t got;
+ void* buf;
+
+ fd = open(path, O_RDONLY);
+ if (fd < 0) return CFREE_NOT_FOUND;
+ if (fstat(fd, &sb) < 0) {
+ close(fd);
+ return CFREE_IO;
+ }
+ size = (size_t)sb.st_size;
+ buf = size ? env->heap->alloc(env->heap, size, 1) : NULL;
+ if (size && !buf) {
+ close(fd);
+ return CFREE_NOMEM;
+ }
+
+ got = 0;
+ while (got < size) {
+ ssize_t n = read(fd, (unsigned char*)buf + got, size - got);
+ if (n <= 0) {
+ env->heap->free(env->heap, buf, size);
+ close(fd);
+ return CFREE_IO;
+ }
+ got += (size_t)n;
+ }
+ close(fd);
+
+ out->data = (const uint8_t*)buf;
+ out->size = size;
+ out->token = buf;
+ return CFREE_OK;
+}
+
+static void posix_release(void* user, CfreeFileData* d) {
+ DriverEnv* env = (DriverEnv*)user;
+ if (d->token) env->heap->free(env->heap, d->token, d->size);
+ d->data = NULL;
+ d->size = 0;
+ d->token = NULL;
+}
+
+static CfreeStatus posix_open_writer(void* user, const char* path,
+ CfreeWriter** out) {
+ DriverEnv* env = (DriverEnv*)user;
+ int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+ CfreeWriter* w;
+ if (fd < 0) return CFREE_IO;
+ w = driver_writer_fd(env->heap, fd);
+ if (!w) {
+ close(fd);
+ return CFREE_NOMEM;
+ }
+ *out = w;
+ return CFREE_OK;
+}
+
+/* ---------------- path helpers ---------------- */
+
+int driver_path_exists(const char* path) {
+ struct stat sb;
+ if (!path) return 0;
+ return stat(path, &sb) == 0;
+}
+
+int driver_path_mtime_ns(const char* path, int64_t* out) {
+ struct stat sb;
+ if (!path || !out) return 1;
+ if (stat(path, &sb) != 0) return 1;
+ return os_stat_mtime_ns(&sb, out);
+}
+
+int driver_mkdir_p(DriverEnv* env, const char* path) {
+ size_t len;
+ char* buf;
+ size_t i;
+ struct stat sb;
+
+ if (!path || !path[0]) return 1;
+ len = cfree_slice_cstr(path).len;
+ buf = (char*)driver_alloc(env, len + 1);
+ if (!buf) return 1;
+ memcpy(buf, path, len + 1);
+
+ for (i = 1; i <= len; ++i) {
+ int at_end = (i == len);
+ if (!at_end && buf[i] != '/') continue;
+ if (!at_end) buf[i] = '\0';
+ if (buf[0] != '\0' && !driver_streq(buf, ".")) {
+ if (mkdir(buf, 0755) != 0 && errno != EEXIST) {
+ driver_free(env, buf, len + 1);
+ return 1;
+ }
+ if (stat(buf, &sb) != 0 || !S_ISDIR(sb.st_mode)) {
+ driver_free(env, buf, len + 1);
+ return 1;
+ }
+ }
+ if (!at_end) buf[i] = '/';
+ }
+
+ driver_free(env, buf, len + 1);
+ return 0;
+}
+
+int driver_mark_executable_output(const char* path) {
+ mode_t mask;
+ mode_t mode;
+ if (!path) return 1;
+ mask = umask(0);
+ (void)umask(mask);
+ mode = (mode_t)(0777 & ~mask);
+ return chmod(path, mode) == 0 ? 0 : 1;
+}
+
+/* ---------------- time ---------------- */
+
+uint64_t driver_now_ns(void) {
+ struct timespec ts;
+ if (clock_gettime(CLOCK_MONOTONIC, &ts) == 0)
+ return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
+ return 0;
+}
+
+/* ---------------- load helpers ---------------- */
+
+int driver_load_bytes(const CfreeFileIO* io, const char* tool, const char* path,
+ DriverLoad* out, CfreeSlice* in) {
+ out->loaded = 0;
+ out->fd.data = NULL;
+ out->fd.size = 0;
+ out->fd.token = NULL;
+ if (!io || !io->read_all) {
+ driver_errf(tool, "host file I/O unavailable");
+ return 1;
+ }
+ if (io->read_all(io->user, path, &out->fd) != CFREE_OK) {
+ driver_errf(tool, "failed to read: %.*s",
+ CFREE_SLICE_ARG(cfree_slice_cstr(path)));
+ return 1;
+ }
+ out->loaded = 1;
+ in->data = out->fd.data;
+ in->len = out->fd.size;
+ return 0;
+}
+
+void driver_release_bytes(const CfreeFileIO* io, DriverLoad* lf) {
+ if (!lf || !lf->loaded) return;
+ if (io && io->release) io->release(io->user, &lf->fd);
+ lf->loaded = 0;
+}
+
+/* ---------------- stdin / edit_temp / read_line ---------------- */
+
+int driver_read_stdin(DriverEnv* e, uint8_t** out_data, size_t* out_size) {
+ size_t cap = 4096;
+ size_t len = 0;
+ uint8_t* buf = e->heap->alloc(e->heap, cap, 1);
+ if (!buf) return 0;
+ for (;;) {
+ ssize_t n;
+ if (len == cap) {
+ size_t newcap = cap * 2;
+ uint8_t* nb = e->heap->realloc(e->heap, buf, cap, newcap, 1);
+ if (!nb) {
+ e->heap->free(e->heap, buf, cap);
+ return 0;
+ }
+ buf = nb;
+ cap = newcap;
+ }
+ n = read(STDIN_FILENO, buf + len, cap - len);
+ if (n == 0) break;
+ if (n < 0) {
+ e->heap->free(e->heap, buf, cap);
+ return 0;
+ }
+ len += (size_t)n;
+ }
+ if (len < cap) {
+ uint8_t* shrunk = len ? e->heap->realloc(e->heap, buf, cap, len, 1) : NULL;
+ if (len && !shrunk) {
+ *out_data = buf;
+ *out_size = cap;
+ return 1;
+ }
+ if (!len) {
+ e->heap->free(e->heap, buf, cap);
+ buf = NULL;
+ } else {
+ buf = shrunk;
+ }
+ }
+ *out_data = buf;
+ *out_size = len;
+ return 1;
+}
+
+static int driver_write_fd_all(int fd, const uint8_t* data, size_t n) {
+ size_t off = 0;
+ while (off < n) {
+ ssize_t wr = write(fd, data + off, n - off);
+ if (wr < 0) {
+ if (errno == EINTR) continue;
+ return 0;
+ }
+ if (wr == 0) return 0;
+ off += (size_t)wr;
+ }
+ return 1;
+}
+
+static char* driver_shell_quote_path(DriverEnv* e, const char* path,
+ size_t path_len, size_t* quoted_len_out) {
+ size_t i;
+ size_t quoted_len = 2u;
+ char* out;
+ char* q;
+ for (i = 0; i < path_len; ++i) quoted_len += path[i] == '\'' ? 4u : 1u;
+ out = e->heap->alloc(e->heap, quoted_len + 1u, 1);
+ if (!out) return NULL;
+ q = out;
+ *q++ = '\'';
+ for (i = 0; i < path_len; ++i) {
+ if (path[i] == '\'') {
+ *q++ = '\'';
+ *q++ = '\\';
+ *q++ = '\'';
+ *q++ = '\'';
+ } else {
+ *q++ = path[i];
+ }
+ }
+ *q++ = '\'';
+ *q = '\0';
+ if (quoted_len_out) *quoted_len_out = quoted_len;
+ return out;
+}
+
+int driver_edit_temp(DriverEnv* e, const char* suffix, const uint8_t* initial,
+ size_t initial_size, uint8_t** out_data,
+ size_t* out_size) {
+ const char* editor;
+ const char* tmpdir;
+ const char* base = "/cfree-dbg-XXXXXX";
+ size_t tmpdir_len;
+ size_t base_len;
+ size_t suffix_len;
+ size_t path_len;
+ char* path;
+ int fd = -1;
+ int ok = 0;
+ CfreeFileData fd_data;
+
+ if (!out_data || !out_size) return 0;
+ *out_data = NULL;
+ *out_size = 0;
+ suffix_len = suffix ? cfree_slice_cstr(suffix).len : 0u;
+ tmpdir = getenv("TMPDIR");
+ if (!tmpdir || !*tmpdir) tmpdir = "/tmp";
+ tmpdir_len = cfree_slice_cstr(tmpdir).len;
+ base_len = cfree_slice_cstr(base).len;
+ path_len = tmpdir_len + base_len + suffix_len;
+ path = e->heap->alloc(e->heap, path_len + 1u, 1);
+ if (!path) return 0;
+ memcpy(path, tmpdir, tmpdir_len);
+ memcpy(path + tmpdir_len, base, base_len);
+ if (suffix_len) memcpy(path + tmpdir_len + base_len, suffix, suffix_len);
+ path[path_len] = '\0';
+
+ fd = mkstemps(path, (int)suffix_len);
+ if (fd < 0) goto out;
+ if (initial_size &&
+ !driver_write_fd_all(fd, initial ? initial : (const uint8_t*)"",
+ initial_size))
+ goto out;
+ if (close(fd) != 0) {
+ fd = -1;
+ goto out;
+ }
+ fd = -1;
+
+ editor = getenv("VISUAL");
+ if (!editor || !*editor) editor = getenv("EDITOR");
+ if (!editor || !*editor) editor = "vi";
+ {
+ size_t editor_len = cfree_slice_cstr(editor).len;
+ size_t quoted_len = 0;
+ char* quoted = driver_shell_quote_path(e, path, path_len, "ed_len);
+ char* cmd;
+ int status;
+ pid_t pid;
+ if (!quoted) goto out;
+ cmd = e->heap->alloc(e->heap, editor_len + 1u + quoted_len + 1u, 1);
+ if (!cmd) {
+ e->heap->free(e->heap, quoted, quoted_len + 1u);
+ goto out;
+ }
+ memcpy(cmd, editor, editor_len);
+ cmd[editor_len] = ' ';
+ memcpy(cmd + editor_len + 1u, quoted, quoted_len + 1u);
+ e->heap->free(e->heap, quoted, quoted_len + 1u);
+ pid = fork();
+ if (pid == 0) {
+ execl("/bin/sh", "sh", "-c", cmd, (char*)NULL);
+ _exit(127);
+ }
+ e->heap->free(e->heap, cmd, editor_len + 1u + quoted_len + 1u);
+ if (pid < 0) goto out;
+ do {
+ if (waitpid(pid, &status, 0) < 0) {
+ if (errno == EINTR) continue;
+ goto out;
+ }
+ break;
+ } while (1);
+ if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) goto out;
+ }
+
+ fd_data.data = NULL;
+ fd_data.size = 0;
+ fd_data.token = NULL;
+ if (posix_read_all(e, path, &fd_data) != CFREE_OK) goto out;
+ *out_data = (uint8_t*)fd_data.data;
+ *out_size = fd_data.size;
+ ok = 1;
+
+out:
+ if (fd >= 0) close(fd);
+ if (path) {
+ unlink(path);
+ e->heap->free(e->heap, path, path_len + 1u);
+ }
+ return ok;
+}
+
+int driver_read_line(char* buf, size_t cap) {
+ size_t len = 0;
+ if (!buf || cap < 2) return -1;
+ for (;;) {
+ int c;
+ errno = 0;
+ c = fgetc(stdin);
+ if (c == EOF) {
+ buf[len] = '\0';
+ if (errno == EINTR) {
+ clearerr(stdin);
+ return -2;
+ }
+ if (ferror(stdin)) return -1;
+ if (len == 0) return 0;
+ return (int)len;
+ }
+ if (c == '\n') {
+ buf[len] = '\0';
+ return (int)len;
+ }
+ if (len + 1 < cap) buf[len++] = (char)c;
+ }
+}
+
+/* ---------------- dlsym resolver ---------------- */
+
+void* driver_dlsym_resolver(void* user, CfreeSlice name_s) {
+ /* The linker hands us interned/pool slices that are NUL-terminated, so
+ * we can pass .s straight through to the OS-specific os_dlsym. */
+ (void)user;
+ if (!name_s.s || name_s.len == 0) return NULL;
+ return os_dlsym(name_s.s);
+}
+
+/* ---------------- SIGINT handler for the dbg REPL ---------------- */
+
+static void (*s_sigint_cb)(void*);
+static void* s_sigint_cb_user;
+
+static void sigint_trampoline(int sig) {
+ (void)sig;
+ if (s_sigint_cb) s_sigint_cb(s_sigint_cb_user);
+}
+
+int driver_install_sigint(void (*cb)(void*), void* user) {
+ struct sigaction sa;
+ s_sigint_cb = cb;
+ s_sigint_cb_user = user;
+ sa.sa_handler = sigint_trampoline;
+ sigemptyset(&sa.sa_mask);
+ sa.sa_flags = 0; /* no SA_RESTART: fgetc returns EINTR */
+ return sigaction(SIGINT, &sa, NULL) == 0 ? 0 : 1;
+}
+
+void driver_restore_sigint(void) {
+ struct sigaction sa;
+ s_sigint_cb = NULL;
+ s_sigint_cb_user = NULL;
+ sa.sa_handler = SIG_DFL;
+ sigemptyset(&sa.sa_mask);
+ sa.sa_flags = 0;
+ sigaction(SIGINT, &sa, NULL);
+}
+
+/* ---------------- host target ---------------- */
+
+static CfreeArchKind host_arch_self(void) {
+#if defined(__x86_64__)
+ return CFREE_ARCH_X86_64;
+#elif defined(__aarch64__)
+ return CFREE_ARCH_ARM_64;
+#elif defined(__arm__)
+ return CFREE_ARCH_ARM_32;
+#elif defined(__i386__)
+ return CFREE_ARCH_X86_32;
+#elif defined(__riscv) && (__riscv_xlen == 64)
+ return CFREE_ARCH_RV64;
+#elif defined(__riscv) && (__riscv_xlen == 32)
+ return CFREE_ARCH_RV32;
+#elif defined(__wasm__)
+ return CFREE_ARCH_WASM;
+#else
+ return CFREE_ARCH_X86_64;
+#endif
+}
+
+CfreeTarget driver_host_target(void) {
+ CfreeTarget t;
+ t.arch = host_arch_self();
+ os_host_target_fill(&t);
+ t.ptr_size = (uint8_t)sizeof(void*);
+ t.ptr_align = (uint8_t)sizeof(void*);
+ t.big_endian = 0;
+ t.pic = driver_default_pic(t.obj, t.os);
+ t.code_model = CFREE_CM_DEFAULT;
+ return t;
+}
+
+/* ---------------- env wiring (POSIX) ---------------- */
+
+char g_cache_dir[4096];
+
+void driver_env_init(DriverEnv* e) {
+ e->heap = &g_heap_libc;
+ e->diag = &g_diag_stderr;
+ e->file_io.read_all = posix_read_all;
+ e->file_io.release = posix_release;
+ e->file_io.open_writer = posix_open_writer;
+ e->file_io.user = e;
+
+ 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;
+ g_execmem_posix.flush_icache = execmem_flush_icache;
+ g_execmem_posix.user = NULL;
+ e->execmem = &g_execmem_posix;
+
+ e->dbg_os = &g_dbg_os_posix;
+ e->jit_tls = &g_jit_tls_posix;
+ e->metrics = NULL;
+
+ {
+ const char* xdg = getenv("XDG_CACHE_HOME");
+ const char* home = getenv("HOME");
+ if (xdg && *xdg) {
+ snprintf(g_cache_dir, sizeof(g_cache_dir), "%s/cfree", xdg);
+ } else if (home && *home) {
+ snprintf(g_cache_dir, sizeof(g_cache_dir), "%s/.cache/cfree", home);
+ } else {
+ snprintf(g_cache_dir, sizeof(g_cache_dir), "build/cfree-cache");
+ }
+ e->cache_dir = g_cache_dir;
+ }
+
+ /* Reproducible-build precedent: SOURCE_DATE_EPOCH wins over wall clock. */
+ {
+ const char* sde = getenv("SOURCE_DATE_EPOCH");
+ if (sde && *sde) {
+ char* endp = NULL;
+ long long v = strtoll(sde, &endp, 10);
+ e->now = (endp != sde && v >= 0) ? (int64_t)v : (int64_t)-1;
+ } else {
+ time_t t = time(NULL);
+ e->now = (t == (time_t)-1) ? (int64_t)-1 : (int64_t)t;
+ }
+ }
+}
+
+void driver_env_fini(DriverEnv* e) {
+ /* Singletons; nothing to release. */
+ (void)e;
+}
+
+CfreeContext driver_env_to_context(const DriverEnv* e) {
+ CfreeContext c;
+ c.heap = e->heap;
+ c.file_io = &e->file_io;
+ c.diag = e->diag;
+ c.metrics = e->metrics;
+ c.now = e->now;
+ return c;
+}
+
+CfreeJitHost driver_env_to_jit_host(const DriverEnv* e) {
+ CfreeJitHost h;
+ h.execmem = e->execmem;
+ h.tls = e->jit_tls;
+ return h;
+}
+
+CfreeDbgHost driver_env_to_dbg_host(const DriverEnv* e) {
+ CfreeDbgHost h;
+ h.os = e->dbg_os;
+ return h;
+}
+
diff --git a/driver/env/posix_dbg.c b/driver/env/posix_dbg.c
@@ -0,0 +1,304 @@
+/* POSIX dbg_os scaffolding shared across mac/linux/freebsd: pthreads for
+ * worker thread and event objects, sigaction-based signal handling that
+ * delegates per-(arch,OS) ucontext marshalling to dbg_ucontext_to_frame /
+ * dbg_frame_to_ucontext, and a sigsetjmp-guarded memory copy.
+ *
+ * The W^X transitions (code_write_begin/end) and icache flush are wired
+ * directly from the per-OS file, since they're the genuine divergence
+ * point between Apple's alias-only model and the Linux/FreeBSD memfd
+ * model.
+ */
+
+#include <pthread.h>
+#include <setjmp.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "env_internal.h"
+
+/* Single-session process model (one debug target at a time). The signal
+ * handler reads these from async-signal context; both writes happen in
+ * signals_install before any signal can arrive, both clears happen in
+ * signals_uninstall after restoring SIG_DFL. */
+static CfreeDbgSignalOps g_dbg_ops;
+static int g_dbg_ops_set;
+static void* g_dbg_session;
+static pthread_t g_dbg_worker_tid;
+static int g_dbg_worker_tid_valid;
+
+static const int g_dbg_signos[6] = {SIGTRAP, SIGSEGV, SIGBUS,
+ SIGILL, SIGFPE, DBG_INTERRUPT_SIGNO};
+#define DBG_NSIGS ((int)(sizeof(g_dbg_signos) / sizeof(g_dbg_signos[0])))
+static struct sigaction g_dbg_prev_sa[DBG_NSIGS];
+static int g_dbg_installed;
+
+/* TLS landing slot for guarded_copy. The SEGV/BUS handler checks
+ * g_guard_armed first; if set it siglongjmps back into guarded_copy
+ * without touching on_fault. */
+static __thread sigjmp_buf g_guard_buf;
+static __thread int g_guard_armed;
+
+/* --- thread shim --- */
+
+typedef struct DbgThread {
+ pthread_t tid;
+ void (*fn)(void*);
+ void* arg;
+} DbgThread;
+
+static void* dbg_thread_trampoline(void* p) {
+ DbgThread* t = (DbgThread*)p;
+ t->fn(t->arg);
+ return NULL;
+}
+
+static CfreeStatus dbg_thread_start(void* user, void (*fn)(void*), void* arg,
+ void** thread_out) {
+ DbgThread* t;
+ (void)user;
+ t = (DbgThread*)malloc(sizeof(*t));
+ if (!t) return CFREE_NOMEM;
+ t->fn = fn;
+ t->arg = arg;
+ if (pthread_create(&t->tid, NULL, dbg_thread_trampoline, t) != 0) {
+ free(t);
+ return CFREE_ERR;
+ }
+ g_dbg_worker_tid = t->tid;
+ g_dbg_worker_tid_valid = 1;
+ *thread_out = t;
+ return CFREE_OK;
+}
+
+static void dbg_thread_join(void* user, void* thread) {
+ DbgThread* t = (DbgThread*)thread;
+ (void)user;
+ if (!t) return;
+ pthread_join(t->tid, NULL);
+ g_dbg_worker_tid_valid = 0;
+ free(t);
+}
+
+static CfreeStatus dbg_thread_interrupt(void* user, void* thread) {
+ DbgThread* t = (DbgThread*)thread;
+ (void)user;
+ if (!t) return CFREE_INVALID;
+ return pthread_kill(t->tid, DBG_INTERRUPT_SIGNO) == 0 ? CFREE_OK : CFREE_ERR;
+}
+
+int posix_dbg_caller_is_worker(void) {
+ return g_dbg_worker_tid_valid &&
+ pthread_equal(pthread_self(), g_dbg_worker_tid);
+}
+
+/* --- event shim --- */
+
+typedef struct DbgEvent {
+ pthread_mutex_t mu;
+ pthread_cond_t cv;
+ int signaled;
+} DbgEvent;
+
+static CfreeStatus dbg_event_new(void* user, void** event_out) {
+ DbgEvent* e;
+ (void)user;
+ e = (DbgEvent*)malloc(sizeof(*e));
+ if (!e) return CFREE_NOMEM;
+ if (pthread_mutex_init(&e->mu, NULL) != 0) {
+ free(e);
+ return CFREE_ERR;
+ }
+ if (pthread_cond_init(&e->cv, NULL) != 0) {
+ pthread_mutex_destroy(&e->mu);
+ free(e);
+ return CFREE_ERR;
+ }
+ e->signaled = 0;
+ *event_out = e;
+ return CFREE_OK;
+}
+
+static void dbg_event_free(void* user, void* ev) {
+ DbgEvent* e = (DbgEvent*)ev;
+ (void)user;
+ if (!e) return;
+ pthread_cond_destroy(&e->cv);
+ pthread_mutex_destroy(&e->mu);
+ free(e);
+}
+
+static CfreeStatus dbg_event_wait(void* user, void* ev) {
+ DbgEvent* e = (DbgEvent*)ev;
+ (void)user;
+ pthread_mutex_lock(&e->mu);
+ while (!e->signaled) pthread_cond_wait(&e->cv, &e->mu);
+ e->signaled = 0;
+ pthread_mutex_unlock(&e->mu);
+ return CFREE_OK;
+}
+
+/* pthread_cond_signal is not formally async-signal-safe per POSIX, but
+ * it is in practice on glibc and Apple libc when callers manage the
+ * signal mask carefully. LLDB and rr rely on the same pattern. */
+static CfreeStatus dbg_event_signal(void* user, void* ev) {
+ DbgEvent* e = (DbgEvent*)ev;
+ (void)user;
+ pthread_mutex_lock(&e->mu);
+ e->signaled = 1;
+ pthread_cond_broadcast(&e->cv);
+ pthread_mutex_unlock(&e->mu);
+ return CFREE_OK;
+}
+
+static CfreeStatus dbg_event_reset(void* user, void* ev) {
+ DbgEvent* e = (DbgEvent*)ev;
+ (void)user;
+ pthread_mutex_lock(&e->mu);
+ e->signaled = 0;
+ pthread_mutex_unlock(&e->mu);
+ return CFREE_OK;
+}
+
+/* --- signal install + ucontext dispatch --- */
+
+static void dbg_signal_handler(int signo, siginfo_t* si, void* ucv) {
+ ucontext_t* uc = (ucontext_t*)ucv;
+ CfreeUnwindFrame frame;
+ CfreeStatus rc;
+ (void)si;
+
+ /* SIGSEGV/SIGBUS during an armed guarded_copy: bail out to the
+ * sigsetjmp landing slot before the session ever sees the fault. */
+ if ((signo == SIGSEGV || signo == SIGBUS) && g_guard_armed) {
+ g_guard_armed = 0;
+ siglongjmp(g_guard_buf, 1);
+ }
+
+ /* Only the registered worker thread participates in stop-the-world.
+ * Faults on other threads (e.g. the REPL) fall through to the default. */
+ if (!posix_dbg_caller_is_worker() || !g_dbg_ops_set || !g_dbg_ops.on_fault) {
+ int i;
+ for (i = 0; i < DBG_NSIGS; ++i) {
+ if (g_dbg_signos[i] == signo) {
+ sigaction(signo, &g_dbg_prev_sa[i], NULL);
+ break;
+ }
+ }
+ raise(signo);
+ return;
+ }
+
+ dbg_ucontext_to_frame(uc, &frame);
+ rc = g_dbg_ops.on_fault(g_dbg_session, signo, &frame);
+ if (rc != CFREE_OK) {
+ int i;
+ for (i = 0; i < DBG_NSIGS; ++i) {
+ if (g_dbg_signos[i] == signo) {
+ sigaction(signo, &g_dbg_prev_sa[i], NULL);
+ break;
+ }
+ }
+ raise(signo);
+ return;
+ }
+ dbg_frame_to_ucontext(&frame, uc);
+}
+
+static CfreeStatus dbg_signals_install(void* user, const CfreeDbgSignalOps* ops,
+ void* session) {
+ struct sigaction sa;
+ int i;
+ (void)user;
+ if (g_dbg_installed) return CFREE_ERR;
+ g_dbg_ops = *ops;
+ g_dbg_ops_set = 1;
+ g_dbg_session = session;
+
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_sigaction = dbg_signal_handler;
+ sa.sa_flags = SA_SIGINFO | SA_RESTART;
+ sigemptyset(&sa.sa_mask);
+ for (i = 0; i < DBG_NSIGS; ++i) sigaddset(&sa.sa_mask, g_dbg_signos[i]);
+
+ for (i = 0; i < DBG_NSIGS; ++i) {
+ if (sigaction(g_dbg_signos[i], &sa, &g_dbg_prev_sa[i]) != 0) {
+ int j;
+ for (j = 0; j < i; ++j)
+ sigaction(g_dbg_signos[j], &g_dbg_prev_sa[j], NULL);
+ memset(&g_dbg_ops, 0, sizeof(g_dbg_ops));
+ g_dbg_ops_set = 0;
+ g_dbg_session = NULL;
+ return CFREE_ERR;
+ }
+ }
+ g_dbg_installed = 1;
+ return CFREE_OK;
+}
+
+static void dbg_signals_uninstall(void* user) {
+ int i;
+ (void)user;
+ if (!g_dbg_installed) return;
+ for (i = 0; i < DBG_NSIGS; ++i)
+ sigaction(g_dbg_signos[i], &g_dbg_prev_sa[i], NULL);
+ g_dbg_installed = 0;
+ memset(&g_dbg_ops, 0, sizeof(g_dbg_ops));
+ g_dbg_ops_set = 0;
+ g_dbg_session = NULL;
+}
+
+/* --- guarded copy --- */
+
+static CfreeStatus dbg_guarded_copy(void* user, void* dst, const void* src,
+ size_t n) {
+ (void)user;
+ if (sigsetjmp(g_guard_buf, 1) != 0) {
+ g_guard_armed = 0;
+ return CFREE_ERR; /* SIGSEGV/SIGBUS during the copy */
+ }
+ g_guard_armed = 1;
+ memcpy(dst, src, n);
+ g_guard_armed = 0;
+ return CFREE_OK;
+}
+
+static __thread sigjmp_buf g_dbg_abort_buf;
+
+static int dbg_call_with_catch(void* user, void (*fn)(void*), void* arg) {
+ (void)user;
+ if (sigsetjmp(g_dbg_abort_buf, 1) == 0) {
+ fn(arg);
+ return 0;
+ }
+ return 1;
+}
+
+static void dbg_thread_abort(void* user) {
+ (void)user;
+ siglongjmp(g_dbg_abort_buf, 1);
+}
+
+/* --- vtable --- */
+
+CfreeDbgOs g_dbg_os_posix = {
+ .thread_start = dbg_thread_start,
+ .thread_join = dbg_thread_join,
+ .thread_interrupt = dbg_thread_interrupt,
+ .event_new = dbg_event_new,
+ .event_free = dbg_event_free,
+ .event_wait = dbg_event_wait,
+ .event_signal = dbg_event_signal,
+ .event_reset = dbg_event_reset,
+ .signals_install = dbg_signals_install,
+ .signals_uninstall = dbg_signals_uninstall,
+ .interrupt_signo = DBG_INTERRUPT_SIGNO,
+ .code_write_begin = os_dbg_code_write_begin,
+ .code_write_end = os_dbg_code_write_end,
+ .flush_icache = os_dbg_flush_icache,
+ .guarded_copy = dbg_guarded_copy,
+ .call_with_catch = dbg_call_with_catch,
+ .thread_abort = dbg_thread_abort,
+ .user = NULL,
+};
diff --git a/driver/env/uctx_aarch64_linux.c b/driver/env/uctx_aarch64_linux.c
@@ -0,0 +1,23 @@
+/* ucontext_t <-> CfreeUnwindFrame marshalling for aarch64 on Linux.
+ * mcontext_t exposes regs[0..30] (x0..x30) and a separate `sp` and `pc`. */
+
+#include <stdint.h>
+
+#include "env_internal.h"
+
+void dbg_ucontext_to_frame(const ucontext_t* uc, CfreeUnwindFrame* f) {
+ const mcontext_t* mc = &uc->uc_mcontext;
+ int i;
+ for (i = 0; i < 31; ++i) f->regs[i] = mc->regs[i];
+ f->regs[31] = mc->sp;
+ f->pc = mc->pc;
+ f->cfa = mc->regs[29]; /* fp; CFI refines */
+}
+
+void dbg_frame_to_ucontext(const CfreeUnwindFrame* f, ucontext_t* uc) {
+ mcontext_t* mc = &uc->uc_mcontext;
+ int i;
+ for (i = 0; i < 31; ++i) mc->regs[i] = f->regs[i];
+ mc->sp = f->regs[31];
+ mc->pc = f->pc;
+}
diff --git a/driver/env/uctx_aarch64_macos.c b/driver/env/uctx_aarch64_macos.c
@@ -0,0 +1,28 @@
+/* ucontext_t <-> CfreeUnwindFrame marshalling for aarch64 on macOS.
+ * Register slots use DWARF numbering (x0..x30, sp at index 31). The
+ * mcontext_t exposes Darwin's __darwin_arm_thread_state64. */
+
+#include <stdint.h>
+
+#include "env_internal.h"
+
+void dbg_ucontext_to_frame(const ucontext_t* uc, CfreeUnwindFrame* f) {
+ const struct __darwin_arm_thread_state64* ss = &uc->uc_mcontext->__ss;
+ int i;
+ for (i = 0; i < 29; ++i) f->regs[i] = ss->__x[i];
+ f->regs[29] = (uint64_t)ss->__fp;
+ f->regs[30] = (uint64_t)ss->__lr;
+ f->regs[31] = (uint64_t)ss->__sp;
+ f->pc = (uint64_t)ss->__pc;
+ f->cfa = (uint64_t)ss->__fp; /* DWARF CFI refines this in the session */
+}
+
+void dbg_frame_to_ucontext(const CfreeUnwindFrame* f, ucontext_t* uc) {
+ struct __darwin_arm_thread_state64* ss = &uc->uc_mcontext->__ss;
+ int i;
+ for (i = 0; i < 29; ++i) ss->__x[i] = f->regs[i];
+ ss->__fp = f->regs[29];
+ ss->__lr = f->regs[30];
+ ss->__sp = f->regs[31];
+ ss->__pc = f->pc;
+}
diff --git a/driver/env/uctx_rv64_linux.c b/driver/env/uctx_rv64_linux.c
@@ -0,0 +1,25 @@
+/* ucontext_t <-> CfreeUnwindFrame marshalling for riscv64 on Linux.
+ * glibc's mcontext_t exposes __gregs[0..31] where __gregs[0] holds the PC
+ * and __gregs[1..31] hold x1..x31. DWARF numbering assigns 0..31 to
+ * x0..x31, so we marshal pc separately and fold x1..x31 into f->regs[1..31],
+ * leaving f->regs[0] as the constant zero. */
+
+#include <stdint.h>
+
+#include "env_internal.h"
+
+void dbg_ucontext_to_frame(const ucontext_t* uc, CfreeUnwindFrame* f) {
+ const mcontext_t* mc = &uc->uc_mcontext;
+ int i;
+ f->regs[0] = 0;
+ for (i = 1; i < 32; ++i) f->regs[i] = (uint64_t)mc->__gregs[i];
+ f->pc = (uint64_t)mc->__gregs[0];
+ f->cfa = (uint64_t)mc->__gregs[8]; /* s0/fp; CFI refines */
+}
+
+void dbg_frame_to_ucontext(const CfreeUnwindFrame* f, ucontext_t* uc) {
+ mcontext_t* mc = &uc->uc_mcontext;
+ int i;
+ for (i = 1; i < 32; ++i) mc->__gregs[i] = (unsigned long)f->regs[i];
+ mc->__gregs[0] = (unsigned long)f->pc;
+}
diff --git a/driver/env/uctx_x86_64_linux.c b/driver/env/uctx_x86_64_linux.c
@@ -0,0 +1,54 @@
+/* ucontext_t <-> CfreeUnwindFrame marshalling for x86_64 on Linux.
+ * mcontext_t.gregs[] is indexed by glibc's REG_* enum (RAX, RDX, RCX, ...);
+ * the frame array uses DWARF numbering, which is the System V x86-64 order
+ * (RAX, RDX, RCX, RBX, RSI, RDI, RBP, RSP, R8..R15, RIP). */
+
+#include <stdint.h>
+#include <string.h>
+
+#include "env_internal.h"
+
+void dbg_ucontext_to_frame(const ucontext_t* uc, CfreeUnwindFrame* f) {
+ const greg_t* g = uc->uc_mcontext.gregs;
+ memset(f, 0, sizeof(*f));
+ f->regs[0] = (uint64_t)g[REG_RAX];
+ f->regs[1] = (uint64_t)g[REG_RDX];
+ f->regs[2] = (uint64_t)g[REG_RCX];
+ f->regs[3] = (uint64_t)g[REG_RBX];
+ f->regs[4] = (uint64_t)g[REG_RSI];
+ f->regs[5] = (uint64_t)g[REG_RDI];
+ f->regs[6] = (uint64_t)g[REG_RBP];
+ f->regs[7] = (uint64_t)g[REG_RSP];
+ f->regs[8] = (uint64_t)g[REG_R8];
+ f->regs[9] = (uint64_t)g[REG_R9];
+ f->regs[10] = (uint64_t)g[REG_R10];
+ f->regs[11] = (uint64_t)g[REG_R11];
+ f->regs[12] = (uint64_t)g[REG_R12];
+ f->regs[13] = (uint64_t)g[REG_R13];
+ f->regs[14] = (uint64_t)g[REG_R14];
+ f->regs[15] = (uint64_t)g[REG_R15];
+ f->regs[16] = (uint64_t)g[REG_RIP];
+ f->pc = (uint64_t)g[REG_RIP];
+ f->cfa = (uint64_t)g[REG_RSP];
+}
+
+void dbg_frame_to_ucontext(const CfreeUnwindFrame* f, ucontext_t* uc) {
+ greg_t* g = uc->uc_mcontext.gregs;
+ g[REG_RAX] = (greg_t)f->regs[0];
+ g[REG_RDX] = (greg_t)f->regs[1];
+ g[REG_RCX] = (greg_t)f->regs[2];
+ g[REG_RBX] = (greg_t)f->regs[3];
+ g[REG_RSI] = (greg_t)f->regs[4];
+ g[REG_RDI] = (greg_t)f->regs[5];
+ g[REG_RBP] = (greg_t)f->regs[6];
+ g[REG_RSP] = (greg_t)f->regs[7];
+ g[REG_R8] = (greg_t)f->regs[8];
+ g[REG_R9] = (greg_t)f->regs[9];
+ g[REG_R10] = (greg_t)f->regs[10];
+ g[REG_R11] = (greg_t)f->regs[11];
+ g[REG_R12] = (greg_t)f->regs[12];
+ g[REG_R13] = (greg_t)f->regs[13];
+ g[REG_R14] = (greg_t)f->regs[14];
+ g[REG_R15] = (greg_t)f->regs[15];
+ g[REG_RIP] = (greg_t)f->pc;
+}
diff --git a/driver/env/windows.c b/driver/env/windows.c
@@ -0,0 +1,21 @@
+/* Windows host support is wired through the OS axis but not yet
+ * implemented. A real Windows port needs to replace nearly every TU in
+ * driver/env/ -- there's no POSIX overlap to share. Sketch:
+ *
+ * - common.c is reusable as-is (malloc/free/printf are universal).
+ * - posix.c equivalent (windows_io.c): VirtualAlloc + VirtualProtect for
+ * execmem, ReadFile / WriteFile / CreateFile for file_io, MoveFileEx
+ * for atomic replace, _stat64 for mtime, SetConsoleCtrlHandler in
+ * place of sigaction(SIGINT).
+ * - posix_dbg.c equivalent (windows_dbg.c): CreateThread / Events
+ * (CreateEvent + Set/Wait) instead of pthreads, structured exception
+ * handling (__try/__except) instead of sigaction + sigsetjmp,
+ * AddVectoredExceptionHandler for the W^X fault path.
+ * - jit_tls equivalent (windows_jit_tls.c): TlsAlloc / TlsGetValue /
+ * TlsSetValue (note the TLV thunk's first-field contract still holds).
+ * - dlsym equivalent: GetProcAddress over a snapshot of loaded modules.
+ *
+ * Until that work lands, attempting to build for Windows is a hard error
+ * rather than a silently-broken binary. */
+
+#error "cfree: Windows host port is not yet implemented (driver/env/windows.c)"