kit

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

commit 678aa2c2c1d76c07ae26fecbd5b4c2128564012d
parent 0f32ede0778411171ae700103652e8a02c781697
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Thu, 28 May 2026 09:34:31 -0700

driver/env: add Windows host TU; extract env selection to mk/env.mk

windows.c is a full Win32 implementation of the driver env surface --
file I/O via CreateFileW + UTF-8/UTF-16 conversion, dual-mapped execmem
via CreateFileMappingW + MapViewOfFile, a CfreeDbgOs vtable backed by
CreateThread/_beginthreadex + event objects + AddVectoredExceptionHandler
+ SuspendThread-based interrupt with inline CONTEXT marshalling for
x86_64 and aarch64, and a FlsAlloc-backed CfreeJitTls preserving the TLV
thunk's first-field contract. Replaces posix.c, posix_dbg.c, and
jit_tls_posix.c on Windows builds; common.c stays shared.

Hoist all HOST_OS/HOST_ARCH detection and driver/env source selection
into mk/env.mk so the main Makefile only consumes a documented set of
outputs (HOST_OS, HOST_ARCH, DRIVER_ENV_OS_CFLAGS, DRIVER_ENV_SRCS,
HOST_SYSROOT_*FLAGS, HOST_LDLIBS). Wire MinGW/MSYS/Cygwin into env.mk;
on Windows it picks windows.c plus the per-arch icache TU and adds
-lpsapi for EnumProcessModules.

Split env_internal.h into a strictly OS-neutral surface (heap/diag
externs + env_flush_icache) and a new env_posix.h that layers on the
POSIX-shared bits (<ucontext.h>, <signal.h>, <sys/stat.h>, exec_dual
registry, os_* hooks, DBG_INTERRUPT_SIGNO, POSIX vtable singletons).
POSIX TUs include env_posix.h; common.c, icache_*.c, and windows.c
include env_internal.h. No more #ifndef _WIN32 anywhere in the headers.

Diffstat:
MMakefile | 102++++++++++---------------------------------------------------------------------
Mdriver/env/env_internal.h | 129+++++++++++++------------------------------------------------------------------
Adriver/env/env_posix.h | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdriver/env/freebsd.c | 2+-
Mdriver/env/jit_tls_posix.c | 2+-
Mdriver/env/linux.c | 2+-
Mdriver/env/linux_exec_hint_default.c | 2+-
Mdriver/env/linux_exec_hint_x86_64.c | 2+-
Mdriver/env/macos.c | 2+-
Mdriver/env/posix.c | 2+-
Mdriver/env/posix_dbg.c | 2+-
Mdriver/env/uctx_aarch64_linux.c | 2+-
Mdriver/env/uctx_aarch64_macos.c | 2+-
Mdriver/env/uctx_rv64_linux.c | 2+-
Mdriver/env/uctx_x86_64_linux.c | 2+-
Mdriver/env/windows.c | 1608++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Amk/env.mk | 176+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
17 files changed, 1921 insertions(+), 228 deletions(-)

diff --git a/Makefile b/Makefile @@ -7,23 +7,11 @@ BUILD_DIR ?= build/release else 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 + +# Host detection, driver/env source selection, host SDK -isysroot wiring. +# Everything that branches on HOST_OS / HOST_ARCH lives in env.mk; the rest +# of this Makefile only reads the variables it produces. +include mk/env.mk .DEFAULT_GOAL := all @@ -31,7 +19,7 @@ ifeq ($(RELEASE),1) HOST_OPTFLAGS ?= -O2 HOST_MODE_CPPFLAGS = -DNDEBUG HOST_MODE_CFLAGS = -ffunction-sections -fdata-sections -ifeq ($(HOST_UNAME),Darwin) +ifeq ($(HOST_OS),darwin) HOST_MODE_LDFLAGS = -Wl,-dead_strip -Wl,-S else HOST_MODE_LDFLAGS = -Wl,--gc-sections -Wl,-S @@ -49,10 +37,9 @@ UBSAN_OPTIONS ?= halt_on_error=1:print_stacktrace=1 export ASAN_OPTIONS UBSAN_OPTIONS endif -# -isysroot lives in its own var so stage/bootstrap recipes can override -# host SDK handling when cfree is used as the compiler. -HOST_SYSROOT_CFLAGS = -isysroot $(SYSROOT) -HOST_SYSROOT_LDFLAGS = -isysroot $(SYSROOT) +# HOST_SYSROOT_{C,LD}FLAGS come from env.mk: a `-isysroot <path>` pair on +# Darwin, empty elsewhere. Stage/bootstrap recipes can clear these when +# cfree itself is the compiler. CFLAGS_COMMON = $(HOST_OPTFLAGS) $(HOST_MODE_CPPFLAGS) $(HOST_MODE_CFLAGS) \ -std=c11 -Wpedantic -Wall -Wextra -Werror @@ -96,77 +83,12 @@ LIB_CFLAGS = $(FREESTANDING_CFLAGS) $(LIB_VISIBILITY_CFLAGS) -Iinclude -Isrc # 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_ENV_CFLAGS. The per-OS feature-test macros and the exact source +# list (DRIVER_ENV_OS_CFLAGS / DRIVER_ENV_SRCS) come from mk/env.mk. 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 @@ -486,7 +408,7 @@ $(LIB_AR): $(LIB_RELOC_OBJ) $(AR) rcs $@ $(LIB_RELOC_OBJ) $(BIN): $(DRIVER_OBJS) $(LIB_AR) $(BUILD_CONFIG) - $(CC) $(HOST_LDFLAGS) -o $@ $(DRIVER_OBJS) $(LIB_AR) + $(CC) $(HOST_LDFLAGS) -o $@ $(DRIVER_OBJS) $(LIB_AR) $(HOST_LDLIBS) $(BUILD_DIR)/lib/%.o: src/%.c Makefile $(BUILD_CONFIG) @mkdir -p $(dir $@) diff --git a/driver/env/env_internal.h b/driver/env/env_internal.h @@ -1,20 +1,23 @@ #ifndef CFREE_DRIVER_ENV_INTERNAL_H #define CFREE_DRIVER_ENV_INTERNAL_H -/* Internal header shared by the driver/env/ TUs. +/* Internal header shared by every driver/env/ TU, regardless of host OS. * * Each TU implements one slice of the host environment with zero - * preprocessor conditionals -- selection is done by the Makefile, which + * preprocessor conditionals -- selection is done by mk/env.mk, 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. + * icache_<arch>.c, and (on POSIX) 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). - */ + * Scope of this header (must stay OS-neutral): + * - includes only of <stddef.h>/<stdint.h> and the libcfree public API + * - the libc-pure vtables defined in common.c (g_heap_libc, g_diag_stderr) + * - the arch-only icache flush hook (icache_<arch>.c) + * + * The POSIX-shared surface (file I/O scaffold, exec_dual registry, signal + * machinery, the os_* hooks the per-OS POSIX file implements, ucontext + * marshalling) lives in env_posix.h. Windows has no POSIX overlap, so + * windows.c folds everything into a single TU and only needs this + * header. */ #include <stddef.h> #include <stdint.h> @@ -26,105 +29,15 @@ #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); +/* ---- vtable singletons wired into DriverEnv (common.c) ----------------- + * Defined in common.c and consumed by every host's driver_env_init. */ +extern CfreeHeap g_heap_libc; +extern CfreeDiagSink g_diag_stderr; -/* 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. - */ +/* ---- icache (icache_<arch>.c) ------------------------------------------ + * Arch-only, OS-neutral. The POSIX dbg path delegates here from + * os_dbg_flush_icache; Windows uses FlushInstructionCache directly and + * does not consume this. */ 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/env_posix.h b/driver/env/env_posix.h @@ -0,0 +1,110 @@ +#ifndef CFREE_DRIVER_ENV_POSIX_H +#define CFREE_DRIVER_ENV_POSIX_H + +/* POSIX-shared internal surface used by the POSIX env TUs: + * driver/env/posix.c, posix_dbg.c, jit_tls_posix.c + * driver/env/macos.c, linux.c, freebsd.c + * driver/env/linux_exec_hint_<arch>.c + * driver/env/uctx_<arch>_<os>.c + * + * Holds the bits that are common to every POSIX-shaped host but absent on + * Windows: the exec_dual write/runtime alias registry, the dual-mapping + * token bookkeeping, the single-mapping execmem helper, the POSIX-vs- + * cfree protection translation, the per-OS hooks the shared POSIX TUs + * call into, ucontext marshalling, and the dbg interrupt signo. + * + * Anything that would compile on Windows belongs in env_internal.h + * (which this header pulls in); anything that needs <ucontext.h>, + * <signal.h>, or <sys/stat.h>'s POSIX `struct stat` belongs here. */ + +#include <signal.h> +#include <sys/stat.h> +#include <ucontext.h> + +#include "env_internal.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]; + +/* ---- 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 mk/env.mk + * 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. */ +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) ------------------------ + * Each hook has exactly one definition per POSIX build, selected by + * mk/env.mk. */ + +/* 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 on POSIX -------------------- */ +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); + +/* dbg interrupt signal: posix_dbg.c's signal handler treats this signo as + * "interrupt from the REPL" (vs SIGTRAP/SEGV/etc, which are faults). */ +#define DBG_INTERRUPT_SIGNO SIGUSR2 + +#endif diff --git a/driver/env/freebsd.c b/driver/env/freebsd.c @@ -17,7 +17,7 @@ #include <sys/stat.h> #include <unistd.h> -#include "env_internal.h" +#include "env_posix.h" /* ---------------- dual-mapped exec memory ---------------- */ diff --git a/driver/env/jit_tls_posix.c b/driver/env/jit_tls_posix.c @@ -13,7 +13,7 @@ #include <stdlib.h> #include <string.h> -#include "env_internal.h" +#include "env_posix.h" typedef struct JitTlsCtx { void* (*get_block)(void* ctx); /* first; matches tlv_thunk's expectation */ diff --git a/driver/env/linux.c b/driver/env/linux.c @@ -12,7 +12,7 @@ #include <sys/syscall.h> #include <unistd.h> -#include "env_internal.h" +#include "env_posix.h" /* ---------------- dual-mapped exec memory (memfd_create) ---------------- * memfd_create gives us an anonymous fd; two mmaps of that fd alias the diff --git a/driver/env/linux_exec_hint_default.c b/driver/env/linux_exec_hint_default.c @@ -4,7 +4,7 @@ #include <stddef.h> -#include "env_internal.h" +#include "env_posix.h" int env_execmem_runtime_extra_flags(void) { return 0; } diff --git a/driver/env/linux_exec_hint_x86_64.c b/driver/env/linux_exec_hint_x86_64.c @@ -7,7 +7,7 @@ #include <stdint.h> #include <sys/mman.h> -#include "env_internal.h" +#include "env_posix.h" int env_execmem_runtime_extra_flags(void) { return MAP_32BIT; } diff --git a/driver/env/macos.c b/driver/env/macos.c @@ -17,7 +17,7 @@ #include <sys/mman.h> #include <sys/stat.h> -#include "env_internal.h" +#include "env_posix.h" /* ---------------- dual-mapped exec memory ---------------- */ /* mach_vm_remap creates a second VA pointing at the same physical memory. diff --git a/driver/env/posix.c b/driver/env/posix.c @@ -18,7 +18,7 @@ #include <time.h> #include <unistd.h> -#include "env_internal.h" +#include "env_posix.h" /* ---------------- exec memory: single-mapping core + registry ---------------- */ diff --git a/driver/env/posix_dbg.c b/driver/env/posix_dbg.c @@ -16,7 +16,7 @@ #include <stdlib.h> #include <string.h> -#include "env_internal.h" +#include "env_posix.h" /* Single-session process model (one debug target at a time). The signal * handler reads these from async-signal context; both writes happen in diff --git a/driver/env/uctx_aarch64_linux.c b/driver/env/uctx_aarch64_linux.c @@ -3,7 +3,7 @@ #include <stdint.h> -#include "env_internal.h" +#include "env_posix.h" void dbg_ucontext_to_frame(const ucontext_t* uc, CfreeUnwindFrame* f) { const mcontext_t* mc = &uc->uc_mcontext; diff --git a/driver/env/uctx_aarch64_macos.c b/driver/env/uctx_aarch64_macos.c @@ -4,7 +4,7 @@ #include <stdint.h> -#include "env_internal.h" +#include "env_posix.h" void dbg_ucontext_to_frame(const ucontext_t* uc, CfreeUnwindFrame* f) { const struct __darwin_arm_thread_state64* ss = &uc->uc_mcontext->__ss; diff --git a/driver/env/uctx_rv64_linux.c b/driver/env/uctx_rv64_linux.c @@ -6,7 +6,7 @@ #include <stdint.h> -#include "env_internal.h" +#include "env_posix.h" void dbg_ucontext_to_frame(const ucontext_t* uc, CfreeUnwindFrame* f) { const mcontext_t* mc = &uc->uc_mcontext; diff --git a/driver/env/uctx_x86_64_linux.c b/driver/env/uctx_x86_64_linux.c @@ -6,7 +6,7 @@ #include <stdint.h> #include <string.h> -#include "env_internal.h" +#include "env_posix.h" void dbg_ucontext_to_frame(const ucontext_t* uc, CfreeUnwindFrame* f) { const greg_t* g = uc->uc_mcontext.gregs; diff --git a/driver/env/windows.c b/driver/env/windows.c @@ -1,21 +1,1593 @@ -/* 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: +/* Windows host environment. Replaces posix.c, posix_dbg.c, and + * jit_tls_posix.c on Win32 builds; common.c is reused unchanged. Built + * against the Win32 API with MinGW-w64 in mind. * - * - 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. + * Coverage: + * - file_io (CreateFileW/ReadFile/WriteFile + UTF-8 path conversion) + * - path helpers (GetFileAttributesExW for exists/mtime, mkdir_p via + * CreateDirectoryW, mark_executable is a no-op on NTFS) + * - stdin / read_line / edit_temp (GetTempPath + system()) + * - monotonic time (QueryPerformanceCounter) + * - SIGINT shim via SetConsoleCtrlHandler + * - dlsym via GetProcAddress over an EnumProcessModules snapshot + * - execmem: dual-mapping via CreateFileMappingW + MapViewOfFile; the + * write alias is RW, the runtime alias is RX after a protect flip. + * Single-mapping fallback uses VirtualAlloc. + * - dbg_os: CreateThread for the worker, event objects, vectored + * exception handling for SEGV/ILL/BP/etc, __try/__except guarded_copy, + * setjmp/longjmp for call_with_catch / thread_abort, SuspendThread + + * GetThreadContext for the interrupt path (the interrupt's on_fault + * runs on the *caller* thread; natural faults run on the worker + * thread inside the VEH, matching POSIX semantics there). + * - jit_tls: FlsAlloc/FlsGetValue/FlsSetValue with a per-thread dtor. + * The TLV thunk's first-field contract is preserved (`get_block` is + * the first field of JitTlsCtx). * - * Until that work lands, attempting to build for Windows is a hard error - * rather than a silently-broken binary. */ + * The W^X model on Windows mirrors the Linux/FreeBSD memfd path: a single + * pagefile-backed file-mapping object is mapped twice -- once RW (write + * alias) and once RX (runtime alias). The dbg code-write path translates + * a runtime address into the corresponding write alias via the same + * exec_dual registry the POSIX side uses (re-implemented locally with + * SRWLock since we don't link the POSIX TUs). Single-mapping reservations + * (no CFREE_PROT_EXEC) just VirtualAlloc a single RW region. + */ -#error "cfree: Windows host port is not yet implemented (driver/env/windows.c)" +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef _WIN32_WINNT +#define _WIN32_WINNT 0x0601 /* Windows 7+: SRWLock, AddVectoredExceptionHandler */ +#endif +#include <windows.h> +#include <psapi.h> + +#include <io.h> +#include <process.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <time.h> + +#include <setjmp.h> + +#include "env_internal.h" + +/* Win32 dbg interrupt code: a synthetic signo handed up to on_fault. The + * value just needs to be distinct from real exception codes; we pick a + * small positive int so it round-trips through the int field of + * CfreeDbgOs.interrupt_signo. */ +#define DBG_WIN_INTERRUPT_SIGNO 100 + +/* ============================================================ + * UTF-8 <-> UTF-16 path conversion + * ============================================================ */ + +/* Convert a UTF-8 path to a freshly-malloc'd wide string. Returns NULL on + * empty input or allocation failure. Callers free with `free`. */ +static wchar_t* widen(const char* utf8) { + int need; + wchar_t* w; + if (!utf8 || !*utf8) return NULL; + need = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0); + if (need <= 0) return NULL; + w = (wchar_t*)malloc((size_t)need * sizeof(wchar_t)); + if (!w) return NULL; + if (MultiByteToWideChar(CP_UTF8, 0, utf8, -1, w, need) <= 0) { + free(w); + return NULL; + } + return w; +} + +/* ============================================================ + * exec_dual registry (write/runtime alias bookkeeping) + * ============================================================ */ + +typedef struct ExecDualNode { + void* write_base; + void* runtime_base; + size_t size; + struct ExecDualNode* next; +} ExecDualNode; + +static ExecDualNode* g_jit_dual_map; +static SRWLOCK g_jit_dual_map_lock = SRWLOCK_INIT; + +static void exec_dual_register_w(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; + n->write_base = write_base; + n->runtime_base = runtime_base; + n->size = size; + AcquireSRWLockExclusive(&g_jit_dual_map_lock); + n->next = g_jit_dual_map; + g_jit_dual_map = n; + ReleaseSRWLockExclusive(&g_jit_dual_map_lock); +} + +static void exec_dual_unregister_w(void* runtime_base) { + ExecDualNode** pp; + AcquireSRWLockExclusive(&g_jit_dual_map_lock); + 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; + } + } + ReleaseSRWLockExclusive(&g_jit_dual_map_lock); +} + +static int exec_dual_lookup_w(void* runtime_addr, size_t n, void** write_out) { + ExecDualNode* cur; + uintptr_t a = (uintptr_t)runtime_addr; + AcquireSRWLockShared(&g_jit_dual_map_lock); + 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)); + ReleaseSRWLockShared(&g_jit_dual_map_lock); + return 0; + } + } + ReleaseSRWLockShared(&g_jit_dual_map_lock); + return 1; +} + +/* ============================================================ + * exec memory: dual-map via CreateFileMappingW, single via VirtualAlloc + * ============================================================ */ + +typedef struct ExecMemTokenWin { + HANDLE mapping; /* NULL for single-mapping reservations */ + void* write_addr; + void* runtime_addr; + size_t size; +} ExecMemTokenWin; + +static DWORD cfree_to_win_prot(int prot) { + int r = (prot & CFREE_PROT_READ) != 0; + int w = (prot & CFREE_PROT_WRITE) != 0; + int x = (prot & CFREE_PROT_EXEC) != 0; + if (x && w) return PAGE_EXECUTE_READWRITE; + if (x && r) return PAGE_EXECUTE_READ; + if (x) return PAGE_EXECUTE; + if (w) return PAGE_READWRITE; + if (r) return PAGE_READONLY; + return PAGE_NOACCESS; +} + +static size_t driver_host_page_size_win(void) { + SYSTEM_INFO si; + GetSystemInfo(&si); + return si.dwPageSize ? (size_t)si.dwPageSize : (size_t)0x1000; +} + +static CfreeStatus execmem_reserve_single_win(size_t size, + CfreeExecMemRegion* out) { + void* p = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); + if (!p) return CFREE_NOMEM; + out->write = p; + out->runtime = p; + out->size = size; + out->token = NULL; + return CFREE_OK; +} + +static CfreeStatus execmem_reserve_dual_win(size_t size, + CfreeExecMemRegion* out) { + HANDLE map; + void* w; + void* r; + ExecMemTokenWin* tok; + DWORD lo = (DWORD)(size & 0xFFFFFFFFu); + DWORD hi = (DWORD)((uint64_t)size >> 32); + + /* PAGE_EXECUTE_READWRITE on the section object is the max protection any + * view can request; per-view protections are narrower (RW vs RX). */ + map = CreateFileMappingW(INVALID_HANDLE_VALUE, NULL, + PAGE_EXECUTE_READWRITE, hi, lo, NULL); + if (!map) return CFREE_ERR; + + w = MapViewOfFile(map, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, size); + if (!w) { + CloseHandle(map); + return CFREE_NOMEM; + } + r = MapViewOfFile(map, FILE_MAP_READ | FILE_MAP_EXECUTE, 0, 0, size); + if (!r) { + UnmapViewOfFile(w); + CloseHandle(map); + return CFREE_NOMEM; + } + + tok = (ExecMemTokenWin*)malloc(sizeof(*tok)); + if (!tok) { + UnmapViewOfFile(r); + UnmapViewOfFile(w); + CloseHandle(map); + return CFREE_NOMEM; + } + tok->mapping = map; + tok->write_addr = w; + tok->runtime_addr = r; + tok->size = size; + + exec_dual_register_w(w, r, size); + + out->write = w; + out->runtime = r; + out->size = size; + out->token = tok; + return CFREE_OK; +} + +static CfreeStatus execmem_reserve_win(void* user, size_t size, int prot, + CfreeExecMemRegion* out) { + (void)user; + if (!out || !size) return CFREE_INVALID; + if (prot & CFREE_PROT_EXEC) return execmem_reserve_dual_win(size, out); + return execmem_reserve_single_win(size, out); +} + +static CfreeStatus execmem_protect_win(void* user, void* addr, size_t size, + int prot) { + DWORD old; + (void)user; + return VirtualProtect(addr, size, cfree_to_win_prot(prot), &old) ? CFREE_OK + : CFREE_ERR; +} + +static void execmem_release_win(void* user, CfreeExecMemRegion* region) { + (void)user; + if (!region || !region->size) return; + if (region->token) { + ExecMemTokenWin* tok = (ExecMemTokenWin*)region->token; + if (tok->runtime_addr && tok->runtime_addr != tok->write_addr) { + exec_dual_unregister_w(tok->runtime_addr); + UnmapViewOfFile(tok->runtime_addr); + } + if (tok->write_addr) UnmapViewOfFile(tok->write_addr); + if (tok->mapping) CloseHandle(tok->mapping); + free(tok); + } else if (region->write) { + VirtualFree(region->write, 0, MEM_RELEASE); + } + region->write = NULL; + region->runtime = NULL; + region->size = 0; + region->token = NULL; +} + +static void execmem_flush_icache_win(void* user, void* addr, size_t size) { + (void)user; + FlushInstructionCache(GetCurrentProcess(), addr, size); +} + +static CfreeExecMem g_execmem_win; + +/* ============================================================ + * Writer vtables: HANDLE-backed and stdio-backed + * ============================================================ */ + +typedef struct DriverHandleWriter { + CfreeWriter base; + CfreeHeap* heap; + HANDLE h; + CfreeStatus status; + uint64_t pos; +} DriverHandleWriter; + +static CfreeStatus hw_write(CfreeWriter* w, const void* data, size_t n) { + DriverHandleWriter* fw = (DriverHandleWriter*)w; + const unsigned char* p = (const unsigned char*)data; + if (fw->status != CFREE_OK) return fw->status; + while (n > 0) { + DWORD chunk = n > 0x40000000u ? 0x40000000u : (DWORD)n; + DWORD wrote = 0; + if (!WriteFile(fw->h, p, chunk, &wrote, NULL) || wrote == 0) { + fw->status = CFREE_IO; + return CFREE_IO; + } + p += wrote; + n -= wrote; + fw->pos += wrote; + } + return CFREE_OK; +} + +static CfreeStatus hw_seek(CfreeWriter* w, uint64_t off) { + DriverHandleWriter* fw = (DriverHandleWriter*)w; + LARGE_INTEGER li; + if (fw->status != CFREE_OK) return fw->status; + li.QuadPart = (LONGLONG)off; + if (!SetFilePointerEx(fw->h, li, NULL, FILE_BEGIN)) { + fw->status = CFREE_IO; + return CFREE_IO; + } + fw->pos = off; + return CFREE_OK; +} + +static uint64_t hw_tell(CfreeWriter* w) { + return ((DriverHandleWriter*)w)->pos; +} +static CfreeStatus hw_status(CfreeWriter* w) { + return ((DriverHandleWriter*)w)->status; +} +static void hw_close(CfreeWriter* w) { + DriverHandleWriter* fw = (DriverHandleWriter*)w; + if (fw->h && fw->h != INVALID_HANDLE_VALUE) CloseHandle(fw->h); + fw->heap->free(fw->heap, fw, sizeof(*fw)); +} + +static CfreeWriter* driver_writer_handle(CfreeHeap* h, HANDLE fh) { + DriverHandleWriter* fw = (DriverHandleWriter*)h->alloc( + h, sizeof(*fw), _Alignof(DriverHandleWriter)); + if (!fw) return NULL; + fw->base.write = hw_write; + fw->base.seek = hw_seek; + fw->base.tell = hw_tell; + fw->base.status = hw_status; + fw->base.close = hw_close; + fw->heap = h; + fw->h = fh; + fw->status = CFREE_OK; + fw->pos = 0; + return &fw->base; +} + +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); + 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 (CreateFileW + ReadFile/WriteFile) + * ============================================================ */ + +static CfreeStatus win_read_all(void* user, const char* path, + CfreeFileData* out) { + DriverEnv* env = (DriverEnv*)user; + wchar_t* wpath; + HANDLE h; + LARGE_INTEGER sz; + size_t size; + size_t got; + void* buf; + + wpath = widen(path); + if (!wpath) return CFREE_NOT_FOUND; + h = CreateFileW(wpath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, NULL); + free(wpath); + if (h == INVALID_HANDLE_VALUE) return CFREE_NOT_FOUND; + if (!GetFileSizeEx(h, &sz)) { + CloseHandle(h); + return CFREE_IO; + } + size = (size_t)sz.QuadPart; + buf = size ? env->heap->alloc(env->heap, size, 1) : NULL; + if (size && !buf) { + CloseHandle(h); + return CFREE_NOMEM; + } + got = 0; + while (got < size) { + DWORD chunk = (size - got) > 0x40000000u ? 0x40000000u : (DWORD)(size - got); + DWORD n = 0; + if (!ReadFile(h, (unsigned char*)buf + got, chunk, &n, NULL) || n == 0) { + env->heap->free(env->heap, buf, size); + CloseHandle(h); + return CFREE_IO; + } + got += n; + } + CloseHandle(h); + out->data = (const uint8_t*)buf; + out->size = size; + out->token = buf; + return CFREE_OK; +} + +static void win_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 win_open_writer(void* user, const char* path, + CfreeWriter** out) { + DriverEnv* env = (DriverEnv*)user; + wchar_t* wpath = widen(path); + HANDLE h; + CfreeWriter* w; + if (!wpath) return CFREE_IO; + h = CreateFileW(wpath, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, NULL); + free(wpath); + if (h == INVALID_HANDLE_VALUE) return CFREE_IO; + w = driver_writer_handle(env->heap, h); + if (!w) { + CloseHandle(h); + return CFREE_NOMEM; + } + *out = w; + return CFREE_OK; +} + +/* ============================================================ + * Path helpers + * ============================================================ */ + +int driver_path_exists(const char* path) { + WIN32_FILE_ATTRIBUTE_DATA fad; + wchar_t* wpath; + BOOL ok; + if (!path) return 0; + wpath = widen(path); + if (!wpath) return 0; + ok = GetFileAttributesExW(wpath, GetFileExInfoStandard, &fad); + free(wpath); + return ok ? 1 : 0; +} + +/* Convert a FILETIME (100-ns ticks since 1601-01-01 UTC) to ns since the + * Unix epoch (1970-01-01 UTC). 11644473600 sec is the gap. */ +static int64_t filetime_to_unix_ns(FILETIME ft) { + uint64_t t = ((uint64_t)ft.dwHighDateTime << 32) | (uint64_t)ft.dwLowDateTime; + /* Subtract Win-to-Unix epoch offset in 100-ns ticks. */ + static const uint64_t EPOCH_DIFF_100NS = 116444736000000000ull; + if (t < EPOCH_DIFF_100NS) t = EPOCH_DIFF_100NS; + return (int64_t)((t - EPOCH_DIFF_100NS) * 100ull); +} + +int driver_path_mtime_ns(const char* path, int64_t* out) { + WIN32_FILE_ATTRIBUTE_DATA fad; + wchar_t* wpath; + BOOL ok; + if (!path || !out) return 1; + wpath = widen(path); + if (!wpath) return 1; + ok = GetFileAttributesExW(wpath, GetFileExInfoStandard, &fad); + free(wpath); + if (!ok) return 1; + *out = filetime_to_unix_ns(fad.ftLastWriteTime); + return 0; +} + +int driver_mkdir_p(DriverEnv* env, const char* path) { + size_t len; + char* buf; + size_t i; + 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); + + /* Walk separators, accepting both '/' and '\\'. Skip the drive prefix + * ("C:") and the leading separator(s) of a UNC path so we don't try to + * CreateDirectory("\\\\server"). */ + for (i = 0; i <= len; ++i) { + int at_end = (i == len); + char ch = buf[i]; + int is_sep = (ch == '/' || ch == '\\'); + int do_create = at_end || is_sep; + if (!do_create) continue; + if (!at_end) buf[i] = '\0'; + /* Skip pure roots: "", ".", drive-letter-only ("C:"), and UNC roots + * ("\\\\server" or "\\\\server\\share"). */ + if (buf[0] == '\0') { + /* nothing yet */ + } else if (driver_streq(buf, ".")) { + /* skip */ + } else if (i >= 2 && buf[1] == ':' && buf[2] == '\0') { + /* "C:" — drive prefix only */ + } else { + wchar_t* wpath = widen(buf); + if (!wpath) { + driver_free(env, buf, len + 1); + return 1; + } + if (!CreateDirectoryW(wpath, NULL)) { + DWORD err = GetLastError(); + if (err != ERROR_ALREADY_EXISTS) { + free(wpath); + driver_free(env, buf, len + 1); + return 1; + } + } + { + WIN32_FILE_ATTRIBUTE_DATA fad; + BOOL ok = GetFileAttributesExW(wpath, GetFileExInfoStandard, &fad); + free(wpath); + if (!ok || !(fad.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { + driver_free(env, buf, len + 1); + return 1; + } + } + } + if (!at_end) buf[i] = ch; + } + + driver_free(env, buf, len + 1); + return 0; +} + +int driver_mark_executable_output(const char* path) { + /* Windows has no Unix +x bit; file extension governs executability and + * NTFS ACLs are inherited from the parent directory. No-op success. */ + (void)path; + return 0; +} + +/* ============================================================ + * Time + * ============================================================ */ + +uint64_t driver_now_ns(void) { + /* QueryPerformanceCounter is monotonic across cores on every supported + * Windows version since Vista. */ + LARGE_INTEGER freq; + LARGE_INTEGER ctr; + if (!QueryPerformanceFrequency(&freq) || freq.QuadPart <= 0) return 0; + if (!QueryPerformanceCounter(&ctr)) return 0; + /* Avoid 128-bit multiply: split ticks into integer-seconds and remainder. */ + { + int64_t sec = ctr.QuadPart / freq.QuadPart; + int64_t rem = ctr.QuadPart % freq.QuadPart; + return (uint64_t)sec * 1000000000ull + + (uint64_t)((rem * 1000000000) / freq.QuadPart); + } +} + +/* ============================================================ + * 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); + HANDLE h = GetStdHandle(STD_INPUT_HANDLE); + if (!buf) return 0; + for (;;) { + DWORD 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; + } + if (!ReadFile(h, buf + len, (DWORD)(cap - len), &n, NULL)) { + if (GetLastError() == ERROR_BROKEN_PIPE) break; /* EOF on pipe */ + e->heap->free(e->heap, buf, cap); + return 0; + } + if (n == 0) break; + len += 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_handle_all(HANDLE h, const uint8_t* data, size_t n) { + size_t off = 0; + while (off < n) { + DWORD chunk = (n - off) > 0x40000000u ? 0x40000000u : (DWORD)(n - off); + DWORD wr = 0; + if (!WriteFile(h, data + off, chunk, &wr, NULL) || wr == 0) return 0; + off += wr; + } + return 1; +} + +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) { + /* Windows temp-file dance: + * - GetTempPathW gives us %TMP%/%TEMP%/%USERPROFILE% with trailing '\\'. + * - GetTempFileNameW makes a unique "<dir>\\cfXXXX.tmp" path. We ignore + * the ".tmp" and append our requested suffix below by renaming. + * - We rename to "<base><suffix>" so the editor sees the right extension. + * - Editor is launched via system() so shell quoting / PATH lookup is + * handled by cmd.exe. + */ + wchar_t tmp_dir[MAX_PATH + 1]; + wchar_t tmp_path[MAX_PATH + 1]; + DWORD got; + HANDLE h = INVALID_HANDLE_VALUE; + wchar_t* wsuffix = NULL; + wchar_t final_path[MAX_PATH + 64]; + size_t final_len; + int ok = 0; + CfreeFileData fd_data; + const char* editor; + char* cmd = NULL; + int rc; + size_t cmd_cap; + char utf8_path[MAX_PATH * 4 + 1]; + int utf8_len; + + if (!out_data || !out_size) return 0; + *out_data = NULL; + *out_size = 0; + + got = GetTempPathW(MAX_PATH + 1, tmp_dir); + if (got == 0 || got > MAX_PATH) return 0; + if (GetTempFileNameW(tmp_dir, L"cf", 0, tmp_path) == 0) return 0; + + /* Build the final path = tmp_path with ".tmp" stripped + requested suffix. + * GetTempFileNameW already created the file at tmp_path; we MoveFileEx to + * rename it to final_path. */ + { + size_t tplen = wcslen(tmp_path); + /* Strip the ".tmp" extension GetTempFileNameW appends. */ + if (tplen >= 4 && tmp_path[tplen - 4] == L'.') tplen -= 4; + if (tplen >= sizeof(final_path) / sizeof(wchar_t) - 16) { + DeleteFileW(tmp_path); + return 0; + } + memcpy(final_path, tmp_path, tplen * sizeof(wchar_t)); + final_path[tplen] = L'\0'; + final_len = tplen; + if (suffix && *suffix) { + wsuffix = widen(suffix); + if (!wsuffix) { + DeleteFileW(tmp_path); + return 0; + } + { + size_t sl = wcslen(wsuffix); + if (final_len + sl + 1 >= sizeof(final_path) / sizeof(wchar_t)) { + free(wsuffix); + DeleteFileW(tmp_path); + return 0; + } + memcpy(final_path + final_len, wsuffix, sl * sizeof(wchar_t)); + final_len += sl; + final_path[final_len] = L'\0'; + } + free(wsuffix); + } + if (!MoveFileExW(tmp_path, final_path, MOVEFILE_REPLACE_EXISTING)) { + DeleteFileW(tmp_path); + return 0; + } + } + + /* Open the renamed file and write initial contents. */ + h = CreateFileW(final_path, GENERIC_WRITE, FILE_SHARE_READ, NULL, + TRUNCATE_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (h == INVALID_HANDLE_VALUE) goto out; + if (initial_size && + !driver_write_handle_all(h, initial ? initial : (const uint8_t*)"", + initial_size)) + goto out; + CloseHandle(h); + h = INVALID_HANDLE_VALUE; + + /* Convert final_path back to UTF-8 for the editor command. */ + utf8_len = WideCharToMultiByte(CP_UTF8, 0, final_path, -1, utf8_path, + (int)sizeof(utf8_path), NULL, NULL); + if (utf8_len <= 0) goto out; + + editor = getenv("VISUAL"); + if (!editor || !*editor) editor = getenv("EDITOR"); + if (!editor || !*editor) editor = "notepad"; + cmd_cap = strlen(editor) + (size_t)utf8_len + 16; + cmd = (char*)malloc(cmd_cap); + if (!cmd) goto out; + /* Wrap the entire command in extra quotes so cmd.exe /S doesn't strip + * matching outer quotes when the editor path itself has spaces. */ + rc = snprintf(cmd, cmd_cap, "\"\"%s\" \"%s\"\"", editor, utf8_path); + if (rc < 0 || (size_t)rc >= cmd_cap) goto out; + if (system(cmd) != 0) goto out; + + fd_data.data = NULL; + fd_data.size = 0; + fd_data.token = NULL; + if (win_read_all(e, utf8_path, &fd_data) != CFREE_OK) goto out; + *out_data = (uint8_t*)fd_data.data; + *out_size = fd_data.size; + ok = 1; + +out: + if (h != INVALID_HANDLE_VALUE) CloseHandle(h); + if (cmd) free(cmd); + DeleteFileW(final_path); + return ok; +} + +int driver_read_line(char* buf, size_t cap) { + size_t len = 0; + if (!buf || cap < 2) return -1; + for (;;) { + int c = fgetc(stdin); + if (c == EOF) { + buf[len] = '\0'; + if (ferror(stdin)) return -1; + if (len == 0) return 0; + return (int)len; + } + if (c == '\n') { + /* Strip a trailing CR if present (CRLF line ending). */ + if (len > 0 && buf[len - 1] == '\r') --len; + buf[len] = '\0'; + return (int)len; + } + if (len + 1 < cap) buf[len++] = (char)c; + } +} + +/* ============================================================ + * dlsym via GetProcAddress over loaded modules + * ============================================================ */ + +/* Snapshot the loaded modules at first call and remember them. We don't + * track DLL load/unload events; that's a reasonable trade-off for a JIT + * (the relevant DLLs -- ucrt, kernel32, the cfree binary -- are loaded + * before the JIT extern resolver runs). */ +static HMODULE g_dlsym_modules[256]; +static DWORD g_dlsym_count; +static SRWLOCK g_dlsym_lock = SRWLOCK_INIT; +static int g_dlsym_inited; + +static void dlsym_init_once(void) { + HANDLE proc; + DWORD need = 0; + AcquireSRWLockExclusive(&g_dlsym_lock); + if (g_dlsym_inited) { + ReleaseSRWLockExclusive(&g_dlsym_lock); + return; + } + proc = GetCurrentProcess(); + if (EnumProcessModules(proc, g_dlsym_modules, sizeof(g_dlsym_modules), + &need)) { + DWORD have = need / (DWORD)sizeof(HMODULE); + g_dlsym_count = + have > (DWORD)(sizeof(g_dlsym_modules) / sizeof(HMODULE)) + ? (DWORD)(sizeof(g_dlsym_modules) / sizeof(HMODULE)) + : have; + } else { + /* Fall back to the executable module and the CRT/kernel essentials. */ + g_dlsym_modules[0] = GetModuleHandleW(NULL); + g_dlsym_modules[1] = GetModuleHandleW(L"kernel32.dll"); + g_dlsym_modules[2] = GetModuleHandleW(L"msvcrt.dll"); + g_dlsym_modules[3] = GetModuleHandleW(L"ucrtbase.dll"); + g_dlsym_count = 4; + } + g_dlsym_inited = 1; + ReleaseSRWLockExclusive(&g_dlsym_lock); +} + +static void* win_dlsym(const char* name) { + DWORD i; + if (!name) return NULL; + dlsym_init_once(); + /* Try the source-level name; if that misses and there's a leading '_' + * (Mach-O-mangled), try without it. */ + AcquireSRWLockShared(&g_dlsym_lock); + for (i = 0; i < g_dlsym_count; ++i) { + void* p; + if (!g_dlsym_modules[i]) continue; + p = (void*)GetProcAddress(g_dlsym_modules[i], name); + if (p) { + ReleaseSRWLockShared(&g_dlsym_lock); + return p; + } + } + if (name[0] == '_' && name[1] != '\0') { + for (i = 0; i < g_dlsym_count; ++i) { + void* p; + if (!g_dlsym_modules[i]) continue; + p = (void*)GetProcAddress(g_dlsym_modules[i], name + 1); + if (p) { + ReleaseSRWLockShared(&g_dlsym_lock); + return p; + } + } + } + ReleaseSRWLockShared(&g_dlsym_lock); + return NULL; +} + +void* driver_dlsym_resolver(void* user, CfreeSlice name_s) { + (void)user; + if (!name_s.s || name_s.len == 0) return NULL; + return win_dlsym(name_s.s); +} + +/* ============================================================ + * SIGINT shim via SetConsoleCtrlHandler + * ============================================================ */ + +static void (*g_ctrlc_cb)(void*); +static void* g_ctrlc_cb_user; + +static BOOL WINAPI ctrlc_trampoline(DWORD type) { + if (type == CTRL_C_EVENT || type == CTRL_BREAK_EVENT) { + if (g_ctrlc_cb) g_ctrlc_cb(g_ctrlc_cb_user); + return TRUE; /* handled */ + } + return FALSE; +} + +int driver_install_sigint(void (*cb)(void*), void* user) { + g_ctrlc_cb = cb; + g_ctrlc_cb_user = user; + return SetConsoleCtrlHandler(ctrlc_trampoline, TRUE) ? 0 : 1; +} + +void driver_restore_sigint(void) { + SetConsoleCtrlHandler(ctrlc_trampoline, FALSE); + g_ctrlc_cb = NULL; + g_ctrlc_cb_user = NULL; +} + +/* ============================================================ + * Win32 CONTEXT <-> CfreeUnwindFrame marshalling + * ============================================================ */ + +/* Inline per-arch marshalling: the Win32 dbg path is short enough that a + * separate uctx_*_windows.c isn't worth the extra TU. Only the host arch + * matters; cross-arch debug isn't a Win32 concern (the worker runs in our + * own process). */ + +#if defined(_M_X64) || defined(__x86_64__) +static void ctx_to_frame(const CONTEXT* c, CfreeUnwindFrame* f) { + memset(f, 0, sizeof(*f)); + f->pc = (uint64_t)c->Rip; + f->cfa = (uint64_t)c->Rsp; + /* SysV DWARF mapping: rax=0, rdx=1, rcx=2, rbx=3, rsi=4, rdi=5, rbp=6, + * rsp=7, r8..r15=8..15. We use the same mapping so cfree's DWARF reader + * can interpret these directly. */ + f->regs[0] = c->Rax; + f->regs[1] = c->Rdx; + f->regs[2] = c->Rcx; + f->regs[3] = c->Rbx; + f->regs[4] = c->Rsi; + f->regs[5] = c->Rdi; + f->regs[6] = c->Rbp; + f->regs[7] = c->Rsp; + f->regs[8] = c->R8; + f->regs[9] = c->R9; + f->regs[10] = c->R10; + f->regs[11] = c->R11; + f->regs[12] = c->R12; + f->regs[13] = c->R13; + f->regs[14] = c->R14; + f->regs[15] = c->R15; +} + +static void frame_to_ctx(const CfreeUnwindFrame* f, CONTEXT* c) { + c->Rip = f->pc; + c->Rax = f->regs[0]; + c->Rdx = f->regs[1]; + c->Rcx = f->regs[2]; + c->Rbx = f->regs[3]; + c->Rsi = f->regs[4]; + c->Rdi = f->regs[5]; + c->Rbp = f->regs[6]; + c->Rsp = f->regs[7]; + c->R8 = f->regs[8]; + c->R9 = f->regs[9]; + c->R10 = f->regs[10]; + c->R11 = f->regs[11]; + c->R12 = f->regs[12]; + c->R13 = f->regs[13]; + c->R14 = f->regs[14]; + c->R15 = f->regs[15]; +} +#elif defined(_M_ARM64) || defined(__aarch64__) +static void ctx_to_frame(const CONTEXT* c, CfreeUnwindFrame* f) { + unsigned i; + memset(f, 0, sizeof(*f)); + f->pc = (uint64_t)c->Pc; + f->cfa = (uint64_t)c->Sp; + for (i = 0; i < 31; ++i) f->regs[i] = c->X[i]; + f->regs[31] = c->Sp; +} + +static void frame_to_ctx(const CfreeUnwindFrame* f, CONTEXT* c) { + unsigned i; + c->Pc = f->pc; + for (i = 0; i < 31; ++i) c->X[i] = f->regs[i]; + c->Sp = f->regs[31]; +} +#else +static void ctx_to_frame(const CONTEXT* c, CfreeUnwindFrame* f) { + (void)c; + memset(f, 0, sizeof(*f)); +} +static void frame_to_ctx(const CfreeUnwindFrame* f, CONTEXT* c) { + (void)f; + (void)c; +} +#endif + +/* Map a Win32 exception code to the POSIX-style signo the dbg session + * expects. Anything we don't recognize falls through as a generic SEGV. */ +static int exception_code_to_signo(DWORD code) { + switch (code) { + case EXCEPTION_BREAKPOINT: + case EXCEPTION_SINGLE_STEP: + return 5; /* SIGTRAP */ + case EXCEPTION_ILLEGAL_INSTRUCTION: + case EXCEPTION_PRIV_INSTRUCTION: + return 4; /* SIGILL */ + case EXCEPTION_INT_DIVIDE_BY_ZERO: + case EXCEPTION_INT_OVERFLOW: + case EXCEPTION_FLT_DIVIDE_BY_ZERO: + case EXCEPTION_FLT_OVERFLOW: + case EXCEPTION_FLT_UNDERFLOW: + case EXCEPTION_FLT_INVALID_OPERATION: + case EXCEPTION_FLT_DENORMAL_OPERAND: + case EXCEPTION_FLT_INEXACT_RESULT: + case EXCEPTION_FLT_STACK_CHECK: + return 8; /* SIGFPE */ + case EXCEPTION_DATATYPE_MISALIGNMENT: + return 7; /* SIGBUS */ + case EXCEPTION_ACCESS_VIOLATION: + case EXCEPTION_IN_PAGE_ERROR: + case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: + case EXCEPTION_STACK_OVERFLOW: + default: + return 11; /* SIGSEGV */ + } +} + +/* ============================================================ + * dbg_os: threads, events, exceptions, guarded_copy + * ============================================================ */ + +static CfreeDbgSignalOps g_dbg_ops; +static int g_dbg_ops_set; +static void* g_dbg_session; +static DWORD g_dbg_worker_tid; +static int g_dbg_worker_tid_valid; +static PVOID g_veh_cookie; + +/* Guarded-copy landing for the VEH path. Both __try/__except and a VEH + * fallback set g_guard_armed; if the VEH fires while it's set the + * exception is "translated" into a longjmp via the VEH's ability to + * rewrite the resume context. We use __try/__except for the primary + * path and treat the VEH armed-check as a backstop. */ +static __declspec(thread) int g_guard_armed; +static __declspec(thread) jmp_buf g_guard_buf; + +static int win_dbg_caller_is_worker(void) { + return g_dbg_worker_tid_valid && GetCurrentThreadId() == g_dbg_worker_tid; +} + +/* --- thread shim --- */ + +typedef struct DbgWinThread { + HANDLE handle; + DWORD tid; + void (*fn)(void*); + void* arg; +} DbgWinThread; + +static unsigned __stdcall dbg_thread_trampoline(void* p) { + DbgWinThread* t = (DbgWinThread*)p; + t->fn(t->arg); + return 0; +} + +static CfreeStatus dbg_thread_start_win(void* user, void (*fn)(void*), + void* arg, void** thread_out) { + DbgWinThread* t; + uintptr_t h; + (void)user; + t = (DbgWinThread*)malloc(sizeof(*t)); + if (!t) return CFREE_NOMEM; + t->fn = fn; + t->arg = arg; + /* _beginthreadex sets up the CRT TLS for the new thread; pure CreateThread + * leaves the CRT in an undefined state for code that calls printf/malloc. */ + h = _beginthreadex(NULL, 0, dbg_thread_trampoline, t, 0, (unsigned*)&t->tid); + if (h == 0) { + free(t); + return CFREE_ERR; + } + t->handle = (HANDLE)h; + g_dbg_worker_tid = t->tid; + g_dbg_worker_tid_valid = 1; + *thread_out = t; + return CFREE_OK; +} + +static void dbg_thread_join_win(void* user, void* thread) { + DbgWinThread* t = (DbgWinThread*)thread; + (void)user; + if (!t) return; + WaitForSingleObject(t->handle, INFINITE); + CloseHandle(t->handle); + g_dbg_worker_tid_valid = 0; + free(t); +} + +/* Interrupt the worker by suspending it, snapshotting its CONTEXT, + * calling on_fault on the caller thread (the REPL), and writing back any + * register edits. This differs from POSIX (where the signal handler runs + * the on_fault on the worker), but the observable contract -- "the worker + * is stopped while on_fault runs and resumes with the edited frame" -- + * matches. Locking quirks of SuspendThread aren't a concern here: the + * worker is, by construction, running JIT'd user code that holds no host + * locks. */ +static CfreeStatus dbg_thread_interrupt_win(void* user, void* thread) { + DbgWinThread* t = (DbgWinThread*)thread; + CONTEXT ctx; + CfreeUnwindFrame frame; + CfreeStatus rc; + (void)user; + if (!t || !g_dbg_ops_set || !g_dbg_ops.on_fault) return CFREE_INVALID; + if (SuspendThread(t->handle) == (DWORD)-1) return CFREE_ERR; + memset(&ctx, 0, sizeof(ctx)); + ctx.ContextFlags = CONTEXT_FULL; + if (!GetThreadContext(t->handle, &ctx)) { + ResumeThread(t->handle); + return CFREE_ERR; + } + ctx_to_frame(&ctx, &frame); + rc = g_dbg_ops.on_fault(g_dbg_session, DBG_WIN_INTERRUPT_SIGNO, &frame); + if (rc == CFREE_OK) { + frame_to_ctx(&frame, &ctx); + SetThreadContext(t->handle, &ctx); + } + ResumeThread(t->handle); + return rc; +} + +/* --- event shim --- */ + +static CfreeStatus dbg_event_new_win(void* user, void** event_out) { + HANDLE h; + (void)user; + /* Manual-reset so a signaler that races ahead of the waiter doesn't lose + * the wake-up; the dbg core explicitly resets via event_reset. */ + h = CreateEventW(NULL, TRUE, FALSE, NULL); + if (!h) return CFREE_ERR; + *event_out = (void*)h; + return CFREE_OK; +} + +static void dbg_event_free_win(void* user, void* ev) { + (void)user; + if (ev) CloseHandle((HANDLE)ev); +} + +static CfreeStatus dbg_event_wait_win(void* user, void* ev) { + (void)user; + if (WaitForSingleObject((HANDLE)ev, INFINITE) != WAIT_OBJECT_0) + return CFREE_ERR; + /* Auto-reset semantics expected by dbg_event_wait on POSIX (it clears + * the flag after consuming). Mirror that here so the contract holds. */ + ResetEvent((HANDLE)ev); + return CFREE_OK; +} + +static CfreeStatus dbg_event_signal_win(void* user, void* ev) { + (void)user; + return SetEvent((HANDLE)ev) ? CFREE_OK : CFREE_ERR; +} + +static CfreeStatus dbg_event_reset_win(void* user, void* ev) { + (void)user; + return ResetEvent((HANDLE)ev) ? CFREE_OK : CFREE_ERR; +} + +/* --- vectored exception handler --- */ + +/* Recovery thunk: when guarded_copy's __try/__except is unavailable (or + * for natural faults raised inside a guarded region) we hand control back + * to the longjmp target by rewriting the resume context. */ +static LONG WINAPI dbg_veh(EXCEPTION_POINTERS* ep) { + DWORD code = ep->ExceptionRecord->ExceptionCode; + CfreeUnwindFrame frame; + CfreeStatus rc; + int signo; + + /* Filter out C++-style exceptions / debug strings we don't care about. */ + if (code == 0x406D1388u /* MS_VC_EXCEPTION (SetThreadName) */ || + code == 0xE06D7363u /* CXX EH */ || code == DBG_PRINTEXCEPTION_C || + code == DBG_PRINTEXCEPTION_WIDE_C) + return EXCEPTION_CONTINUE_SEARCH; + + /* SEGV/BUS during a guarded_copy: bail out without involving the + * session. The VEH backstop only fires if SEH/__try wasn't compiled in + * for the function that armed the guard. */ + if (g_guard_armed && + (code == EXCEPTION_ACCESS_VIOLATION || code == EXCEPTION_IN_PAGE_ERROR || + code == EXCEPTION_DATATYPE_MISALIGNMENT)) { + g_guard_armed = 0; + longjmp(g_guard_buf, 1); + /* not reached */ + return EXCEPTION_CONTINUE_EXECUTION; + } + + if (!win_dbg_caller_is_worker() || !g_dbg_ops_set || !g_dbg_ops.on_fault) + return EXCEPTION_CONTINUE_SEARCH; + + signo = exception_code_to_signo(code); + ctx_to_frame(ep->ContextRecord, &frame); + rc = g_dbg_ops.on_fault(g_dbg_session, signo, &frame); + if (rc != CFREE_OK) return EXCEPTION_CONTINUE_SEARCH; + frame_to_ctx(&frame, ep->ContextRecord); + return EXCEPTION_CONTINUE_EXECUTION; +} + +static CfreeStatus dbg_signals_install_win(void* user, + const CfreeDbgSignalOps* ops, + void* session) { + (void)user; + if (g_veh_cookie) return CFREE_ERR; + g_dbg_ops = *ops; + g_dbg_ops_set = 1; + g_dbg_session = session; + /* First==1 ensures we run before any other VEH and before the system's + * default last-chance handler. */ + g_veh_cookie = AddVectoredExceptionHandler(1, dbg_veh); + if (!g_veh_cookie) { + memset(&g_dbg_ops, 0, sizeof(g_dbg_ops)); + g_dbg_ops_set = 0; + g_dbg_session = NULL; + return CFREE_ERR; + } + return CFREE_OK; +} + +static void dbg_signals_uninstall_win(void* user) { + (void)user; + if (g_veh_cookie) { + RemoveVectoredExceptionHandler(g_veh_cookie); + g_veh_cookie = NULL; + } + memset(&g_dbg_ops, 0, sizeof(g_dbg_ops)); + g_dbg_ops_set = 0; + g_dbg_session = NULL; +} + +/* --- W^X transitions --- */ + +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); +} + +static CfreeStatus dbg_code_write_begin_win(void* user, void* runtime_addr, + size_t n, void** write_out) { + size_t pg; + uintptr_t a; + uintptr_t base; + size_t span; + DWORD old; + (void)user; + if (!runtime_addr || !n || !write_out) return CFREE_INVALID; + /* Dual-mapped reservation: write through the alias, no protect flip. */ + if (exec_dual_lookup_w(runtime_addr, n, write_out) == 0) return CFREE_OK; + /* Single-mapping fallback: transient PAGE_EXECUTE_READWRITE. */ + pg = driver_host_page_size_win(); + a = (uintptr_t)runtime_addr; + base = page_floor(a, pg); + span = page_ceil((a - base) + n, pg); + if (!VirtualProtect((void*)base, span, PAGE_EXECUTE_READWRITE, &old)) + return CFREE_ERR; + *write_out = runtime_addr; + return CFREE_OK; +} + +static void dbg_code_write_end_win(void* user, void* runtime_addr, size_t n) { + void* w; + size_t pg; + uintptr_t a; + uintptr_t base; + size_t span; + DWORD old; + (void)user; + if (exec_dual_lookup_w(runtime_addr, n, &w) == 0) return; /* nothing to flip */ + pg = driver_host_page_size_win(); + a = (uintptr_t)runtime_addr; + base = page_floor(a, pg); + span = page_ceil((a - base) + n, pg); + VirtualProtect((void*)base, span, PAGE_EXECUTE_READ, &old); +} + +static void dbg_flush_icache_win(void* user, void* runtime_addr, size_t n) { + (void)user; + FlushInstructionCache(GetCurrentProcess(), runtime_addr, n); +} + +/* --- guarded copy via SEH __try/__except --- */ + +static CfreeStatus dbg_guarded_copy_win(void* user, void* dst, const void* src, + size_t n) { + (void)user; + /* MinGW-w64 supports __try/__except natively for the SEH model on x86_64 + * and arm64 (GCC: -fseh-exceptions; clang: built-in). The VEH backstop + * above catches it if the toolchain ever falls back to setjmp-based EH. */ + if (setjmp(g_guard_buf) != 0) { + g_guard_armed = 0; + return CFREE_ERR; + } + g_guard_armed = 1; + __try { + memcpy(dst, src, n); + g_guard_armed = 0; + return CFREE_OK; + } __except (EXCEPTION_EXECUTE_HANDLER) { + g_guard_armed = 0; + return CFREE_ERR; + } +} + +/* --- call_with_catch / thread_abort (longjmp-based) --- */ + +static __declspec(thread) jmp_buf g_dbg_abort_buf; + +static int dbg_call_with_catch_win(void* user, void (*fn)(void*), void* arg) { + (void)user; + if (setjmp(g_dbg_abort_buf) == 0) { + fn(arg); + return 0; + } + return 1; +} + +static void dbg_thread_abort_win(void* user) { + (void)user; + longjmp(g_dbg_abort_buf, 1); +} + +static CfreeDbgOs g_dbg_os_win = { + .thread_start = dbg_thread_start_win, + .thread_join = dbg_thread_join_win, + .thread_interrupt = dbg_thread_interrupt_win, + .event_new = dbg_event_new_win, + .event_free = dbg_event_free_win, + .event_wait = dbg_event_wait_win, + .event_signal = dbg_event_signal_win, + .event_reset = dbg_event_reset_win, + .signals_install = dbg_signals_install_win, + .signals_uninstall = dbg_signals_uninstall_win, + .interrupt_signo = DBG_WIN_INTERRUPT_SIGNO, + .code_write_begin = dbg_code_write_begin_win, + .code_write_end = dbg_code_write_end_win, + .flush_icache = dbg_flush_icache_win, + .guarded_copy = dbg_guarded_copy_win, + .call_with_catch = dbg_call_with_catch_win, + .thread_abort = dbg_thread_abort_win, + .user = NULL, +}; + +/* ============================================================ + * jit_tls (FlsAlloc with per-thread dtor) + * ============================================================ */ + +/* The TLV thunk's contract (src/jit/tlv_thunk.h): the first 8 bytes of + * ctx must be a function pointer the asm calls with arg0 == ctx and + * expects back arg0 == TLS block. We satisfy that by making `get_block` + * the first field. */ +typedef struct JitTlsCtxWin { + void* (*get_block)(void* ctx); + DWORD fls_index; + size_t image_size; + size_t image_filesz; + size_t align; + void* init_bytes; +} JitTlsCtxWin; + +static void __stdcall jit_tls_thread_dtor(PVOID block) { + /* FlsAlloc destructor: runs on the thread that's exiting, OR when the + * FLS index is freed via FlsFree (for live threads' blocks). */ + if (block) free(block); +} + +static void* jit_tls_alloc_block_win(JitTlsCtxWin* ctx) { + 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; + /* _aligned_malloc is the MSVCRT/UCRT counterpart to aligned_alloc; the + * matching free is _aligned_free, but only the FLS destructor frees + * these blocks and it uses plain free. So allocate with malloc-style + * alignment instead: overallocate and align manually -- but FLS dtor + * needs to know the original pointer. Simpler: require align <= + * MEMORY_ALLOCATION_ALIGNMENT (16 on Win64), which the TLS images we + * see in practice satisfy. */ + (void)a; + block = malloc(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_win(void* ctx_v) { + JitTlsCtxWin* ctx = (JitTlsCtxWin*)ctx_v; + void* block = FlsGetValue(ctx->fls_index); + if (block) return block; + block = jit_tls_alloc_block_win(ctx); + if (!block) { + fprintf(stderr, + "cfree run: out of memory allocating per-thread TLS block\n"); + abort(); + } + if (!FlsSetValue(ctx->fls_index, block)) { + fprintf(stderr, "cfree run: FlsSetValue failed in TLV thunk\n"); + abort(); + } + return block; +} + +static void* jit_tls_ctx_new_win(void* user, const void* init_bytes, + size_t image_filesz, size_t image_size, + size_t align) { + JitTlsCtxWin* ctx; + (void)user; + ctx = (JitTlsCtxWin*)malloc(sizeof(*ctx)); + if (!ctx) return NULL; + ctx->get_block = jit_tls_get_block_win; + 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); + } + ctx->fls_index = FlsAlloc(jit_tls_thread_dtor); + if (ctx->fls_index == FLS_OUT_OF_INDEXES) { + free(ctx->init_bytes); + free(ctx); + return NULL; + } + return ctx; +} + +static void jit_tls_ctx_destroy_win(void* user, void* ctx_v) { + JitTlsCtxWin* ctx = (JitTlsCtxWin*)ctx_v; + (void)user; + if (!ctx) return; + /* FlsFree runs the destructor for every live thread's block before + * releasing the index. The calling thread's block is reaped here too. */ + FlsFree(ctx->fls_index); + free(ctx->init_bytes); + free(ctx); +} + +static CfreeJitTls g_jit_tls_win = { + .ctx_new = jit_tls_ctx_new_win, + .ctx_destroy = jit_tls_ctx_destroy_win, + .user = NULL, +}; + +/* ============================================================ + * host target + * ============================================================ */ + +static CfreeArchKind host_arch_self_win(void) { +#if defined(_M_X64) || defined(__x86_64__) + return CFREE_ARCH_X86_64; +#elif defined(_M_ARM64) || defined(__aarch64__) + return CFREE_ARCH_ARM_64; +#elif defined(_M_IX86) || defined(__i386__) + return CFREE_ARCH_X86_32; +#elif defined(_M_ARM) || defined(__arm__) + return CFREE_ARCH_ARM_32; +#else + return CFREE_ARCH_X86_64; +#endif +} + +CfreeTarget driver_host_target(void) { + CfreeTarget t; + t.arch = host_arch_self_win(); + t.os = CFREE_OS_WINDOWS; + t.obj = CFREE_OBJ_COFF; + 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 + * ============================================================ */ + +static char g_cache_dir_win[4096]; + +void driver_env_init(DriverEnv* e) { + e->heap = &g_heap_libc; + e->diag = &g_diag_stderr; + e->file_io.read_all = win_read_all; + e->file_io.release = win_release; + e->file_io.open_writer = win_open_writer; + e->file_io.user = e; + + g_execmem_win.page_size = driver_host_page_size_win(); + g_execmem_win.reserve = execmem_reserve_win; + g_execmem_win.protect = execmem_protect_win; + g_execmem_win.release = execmem_release_win; + g_execmem_win.flush_icache = execmem_flush_icache_win; + g_execmem_win.user = NULL; + e->execmem = &g_execmem_win; + + e->dbg_os = &g_dbg_os_win; + e->jit_tls = &g_jit_tls_win; + e->metrics = NULL; + + { + /* XDG_CACHE_HOME wins if set (cross-platform tooling convention), + * otherwise fall back to %LOCALAPPDATA%\\cfree, otherwise a + * build-tree-local path. */ + const char* xdg = getenv("XDG_CACHE_HOME"); + const char* lap = getenv("LOCALAPPDATA"); + if (xdg && *xdg) { + snprintf(g_cache_dir_win, sizeof(g_cache_dir_win), "%s/cfree", xdg); + } else if (lap && *lap) { + snprintf(g_cache_dir_win, sizeof(g_cache_dir_win), "%s\\cfree", lap); + } else { + snprintf(g_cache_dir_win, sizeof(g_cache_dir_win), "build\\cfree-cache"); + } + g_cache_dir_win[sizeof(g_cache_dir_win) - 1] = '\0'; + e->cache_dir = g_cache_dir_win; + } + + { + 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) { + (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/mk/env.mk b/mk/env.mk @@ -0,0 +1,176 @@ +# mk/env.mk +# =========================================================================== +# Host environment detection and driver/env source selection. +# +# This file is the *only* place in the build system that branches on the +# host OS or arch. It produces the variables below; the rest of the build +# consumes them directly without re-deriving anything from `uname`. +# +# Contract: +# +# Detection: +# HOST_UNAME raw `uname -s` (Darwin / Linux / FreeBSD / +# MINGW64_NT-* / MSYS_NT-* / CYGWIN_NT-*) +# HOST_ARCH_RAW raw `uname -m` +# HOST_OS normalized OS tag: darwin | linux | freebsd +# | windows. Unknown is a hard error. +# HOST_ARCH normalized arch tag: x86_64 | aarch64 | rv64. +# Unknown is a hard error. +# +# Host SDK (Darwin only; empty elsewhere so `$(HOST_SYSROOT_CFLAGS)` is +# still safe to splice into any command line): +# HOST_SDK_PATH the resolved -isysroot path, or empty +# HOST_SYSROOT_CFLAGS `-isysroot <path>` or empty +# HOST_SYSROOT_LDFLAGS `-isysroot <path>` or empty +# +# driver/env wiring: +# DRIVER_ENV_OS_CFLAGS feature-test macros required by the per-OS env +# TU before any libc header is included +# (-D_XOPEN_SOURCE, -D_GNU_SOURCE, -D_WIN32_WINNT, +# ...) +# DRIVER_ENV_SRCS exact set of driver/env/*.c files to compile +# into the host binary. One file per OS, one +# per arch (icache), and on POSIX one per +# (arch, OS) for ucontext marshalling. Removing +# OS/arch ifdefs from the impl is the explicit +# point of this layout. +# +# Link surface: +# HOST_LDLIBS extra libraries the host link needs because of +# env choices (-lpsapi on Windows for +# EnumProcessModules). The main Makefile splices +# this onto the binary's link line. +# +# Not produced here: HOST_OPTFLAGS / HOST_MODE_*FLAGS. Those are +# build-mode (debug vs release) concerns, not env concerns; they live in +# the main Makefile. + +HOST_UNAME := $(shell uname -s) +HOST_ARCH_RAW := $(shell uname -m) + +# ---- HOST_OS --------------------------------------------------------------- + +ifeq ($(HOST_UNAME),Darwin) +HOST_OS := darwin +else ifeq ($(HOST_UNAME),Linux) +HOST_OS := linux +else ifeq ($(HOST_UNAME),FreeBSD) +HOST_OS := freebsd +else ifneq ($(filter MINGW% MSYS% CYGWIN%,$(HOST_UNAME)),) +HOST_OS := windows +else +$(error env.mk: unsupported HOST_UNAME=$(HOST_UNAME); see driver/env/windows.c for the Windows port skeleton) +endif + +# ---- HOST_ARCH ------------------------------------------------------------- + +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 +$(error env.mk: unsupported HOST_ARCH_RAW=$(HOST_ARCH_RAW)) +endif + +# ---- host SDK (Darwin only) ----------------------------------------------- +# -isysroot is a Darwin-only flag and xcrun lives there too. On Linux / +# FreeBSD / Windows we leave the vars empty so the host link/compile lines +# stay well-formed when they splice $(HOST_SYSROOT_*FLAGS). + +HOST_SDK_PATH := +HOST_SYSROOT_CFLAGS := +HOST_SYSROOT_LDFLAGS := +ifeq ($(HOST_OS),darwin) +HOST_SDK_PATH := $(shell xcrun --show-sdk-path 2>/dev/null) +ifneq ($(HOST_SDK_PATH),) +HOST_SYSROOT_CFLAGS := -isysroot $(HOST_SDK_PATH) +HOST_SYSROOT_LDFLAGS := -isysroot $(HOST_SDK_PATH) +endif +endif + +# ---- per-OS env wiring ----------------------------------------------------- + +DRIVER_ENV_OS_CFLAGS := +DRIVER_ENV_HINT_SRC := +HOST_LDLIBS := + +ifeq ($(HOST_OS),darwin) +DRIVER_ENV_OS_CFLAGS := -D_XOPEN_SOURCE=600 -D_DARWIN_C_SOURCE=1 +DRIVER_ENV_OS_SRC := driver/env/macos.c +else ifeq ($(HOST_OS),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_OS),freebsd) +DRIVER_ENV_OS_SRC := driver/env/freebsd.c +else ifeq ($(HOST_OS),windows) +# windows.c subsumes posix.c / posix_dbg.c / jit_tls_posix.c and folds in +# its own CONTEXT-based register marshalling -- there's no POSIX overlap +# worth sharing. EnumProcessModules pulls in -lpsapi. +DRIVER_ENV_OS_CFLAGS := -D_WIN32_WINNT=0x0601 +DRIVER_ENV_OS_SRC := driver/env/windows.c +HOST_LDLIBS += -lpsapi +endif + +# ---- per-arch icache flush TU --------------------------------------------- + +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 +endif + +# ---- per-(arch, OS) ucontext marshalling (POSIX only) --------------------- + +DRIVER_ENV_UCTX_SRC := +ifeq ($(HOST_OS),darwin) +ifeq ($(HOST_ARCH),aarch64) +DRIVER_ENV_UCTX_SRC := driver/env/uctx_aarch64_macos.c +endif +else ifeq ($(HOST_OS),linux) +ifeq ($(HOST_ARCH),aarch64) +DRIVER_ENV_UCTX_SRC := driver/env/uctx_aarch64_linux.c +else ifeq ($(HOST_ARCH),x86_64) +DRIVER_ENV_UCTX_SRC := driver/env/uctx_x86_64_linux.c +else ifeq ($(HOST_ARCH),rv64) +DRIVER_ENV_UCTX_SRC := driver/env/uctx_rv64_linux.c +endif +endif + +ifneq ($(HOST_OS),windows) +ifeq ($(DRIVER_ENV_UCTX_SRC),) +$(error env.mk: no ucontext marshalling for HOST_OS=$(HOST_OS) HOST_ARCH=$(HOST_ARCH)) +endif +endif + +# ---- DRIVER_ENV_SRCS ------------------------------------------------------- +# common.c is the libc-pure floor shared by every host (heap/diag/printf/ +# string helpers; no syscalls). POSIX hosts layer on the shared POSIX +# scaffolding (file I/O, mkdir, sigint, exec_dual registry, signals, +# pthread TLS) plus the per-OS file and the per-(arch, OS) uctx TU. +# Windows has no POSIX overlap; windows.c is the entire host surface. + +ifeq ($(HOST_OS),windows) +DRIVER_ENV_SRCS := \ + driver/env/common.c \ + $(DRIVER_ENV_OS_SRC) \ + $(DRIVER_ENV_ICACHE_SRC) +else +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) +endif