kit

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

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:
MMakefile | 94++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Ddriver/env.c | 1848-------------------------------------------------------------------------------
Adriver/env/common.c | 208+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adriver/env/env_internal.h | 130+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adriver/env/freebsd.c | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adriver/env/icache_arm.c | 11+++++++++++
Adriver/env/icache_riscv.c | 12++++++++++++
Adriver/env/icache_x86.c | 14++++++++++++++
Adriver/env/jit_tls_posix.c | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adriver/env/linux.c | 142+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adriver/env/linux_exec_hint_default.c | 14++++++++++++++
Adriver/env/linux_exec_hint_x86_64.c | 22++++++++++++++++++++++
Adriver/env/macos.c | 130+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adriver/env/posix.c | 791+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adriver/env/posix_dbg.c | 304+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adriver/env/uctx_aarch64_linux.c | 23+++++++++++++++++++++++
Adriver/env/uctx_aarch64_macos.c | 28++++++++++++++++++++++++++++
Adriver/env/uctx_rv64_linux.c | 25+++++++++++++++++++++++++
Adriver/env/uctx_x86_64_linux.c | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adriver/env/windows.c | 21+++++++++++++++++++++
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, &quoted_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, &quoted_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)"