kit

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

windows.c (63784B)


      1 /* Windows host environment. Replaces posix.c and posix_dbg.c on Win32
      2  * builds; common.c is reused unchanged. Built against the Win32 API with
      3  * MinGW-w64 in mind.
      4  *
      5  * Coverage:
      6  *   - file_io (CreateFileW/ReadFile/WriteFile + UTF-8 path conversion)
      7  *   - path helpers (GetFileAttributesExW for exists/mtime, mkdir_p via
      8  *     CreateDirectoryW, mark_executable is a no-op on NTFS)
      9  *   - stdin / read_line / edit_temp (GetTempPath + system())
     10  *   - monotonic time (QueryPerformanceCounter)
     11  *   - SIGINT shim via SetConsoleCtrlHandler
     12  *   - dlsym via GetProcAddress over an EnumProcessModules snapshot
     13  *   - execmem: dual-mapping via CreateFileMappingW + MapViewOfFile; the
     14  *     write alias is RW, the runtime alias is RX after a protect flip.
     15  *     Single-mapping fallback uses VirtualAlloc.
     16  *   - dbg_os: CreateThread for the worker, event objects, vectored
     17  *     exception handling for SEGV/ILL/BP/etc, __try/__except guarded_copy,
     18  *     setjmp/longjmp for call_with_catch / thread_abort, SuspendThread +
     19  *     GetThreadContext for the interrupt path (the interrupt's on_fault
     20  *     runs on the *caller* thread; natural faults run on the worker
     21  *     thread inside the VEH, matching POSIX semantics there).
     22  *
     23  * The W^X model on Windows mirrors the Linux/FreeBSD memfd path: a single
     24  * pagefile-backed file-mapping object is mapped twice -- once RW (write
     25  * alias) and once RX (runtime alias). The dbg code-write path translates
     26  * a runtime address into the corresponding write alias via the same
     27  * exec_dual registry the POSIX side uses (re-implemented locally with
     28  * SRWLock since we don't link the POSIX TUs). Single-mapping reservations
     29  * (no KIT_PROT_EXEC) just VirtualAlloc a single RW region.
     30  */
     31 
     32 #ifndef WIN32_LEAN_AND_MEAN
     33 #define WIN32_LEAN_AND_MEAN
     34 #endif
     35 #ifndef _WIN32_WINNT
     36 #define _WIN32_WINNT \
     37   0x0601 /* Windows 7+: SRWLock, AddVectoredExceptionHandler */
     38 #endif
     39 /* windows.h must precede psapi.h/process.h: the mingw SDK headers use
     40  * windows.h's WINBOOL/DWORD/LPVOID etc. and do not self-include it. Kept in
     41  * its own include group so a formatter's alphabetic sort can't reorder it
     42  * after psapi.h (which breaks the mingw cross-build). */
     43 #include <io.h>
     44 #include <process.h>
     45 #include <psapi.h>
     46 #include <stdint.h>
     47 #include <stdio.h>
     48 #include <windows.h>
     49 #define _CRT_RAND_S /* enable rand_s (OS CSPRNG) */
     50 #include <setjmp.h>
     51 #include <stdlib.h>
     52 #include <string.h>
     53 #include <sys/stat.h>
     54 #include <time.h>
     55 
     56 #include "env_internal.h"
     57 
     58 extern char** _environ;
     59 
     60 /* Win32 dbg interrupt code: a synthetic signo handed up to on_fault. The
     61  * value just needs to be distinct from real exception codes; we pick a
     62  * small positive int so it round-trips through the int field of
     63  * KitDbgOs.interrupt_signo. */
     64 #define DBG_WIN_INTERRUPT_SIGNO 100
     65 
     66 /* ============================================================
     67  *   UTF-8 <-> UTF-16 path conversion
     68  * ============================================================ */
     69 
     70 /* Convert a UTF-8 path to a freshly-malloc'd wide string. Returns NULL on
     71  * empty input or allocation failure. Callers free with `free`. */
     72 static wchar_t* widen(const char* utf8) {
     73   int need;
     74   wchar_t* w;
     75   if (!utf8 || !*utf8) return NULL;
     76   need = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0);
     77   if (need <= 0) return NULL;
     78   w = (wchar_t*)malloc((size_t)need * sizeof(wchar_t));
     79   if (!w) return NULL;
     80   if (MultiByteToWideChar(CP_UTF8, 0, utf8, -1, w, need) <= 0) {
     81     free(w);
     82     return NULL;
     83   }
     84   return w;
     85 }
     86 
     87 static char* narrow(const wchar_t* wide) {
     88   int need;
     89   char* out;
     90   if (!wide) return NULL;
     91   need = WideCharToMultiByte(CP_UTF8, 0, wide, -1, NULL, 0, NULL, NULL);
     92   if (need <= 0) return NULL;
     93   out = (char*)malloc((size_t)need);
     94   if (!out) return NULL;
     95   if (WideCharToMultiByte(CP_UTF8, 0, wide, -1, out, need, NULL, NULL) <= 0) {
     96     free(out);
     97     return NULL;
     98   }
     99   return out;
    100 }
    101 
    102 /* ============================================================
    103  *   exec_dual registry (write/runtime alias bookkeeping)
    104  * ============================================================ */
    105 
    106 typedef struct ExecDualNode {
    107   void* write_base;
    108   void* runtime_base;
    109   size_t size;
    110   struct ExecDualNode* next;
    111 } ExecDualNode;
    112 
    113 static ExecDualNode* g_jit_dual_map;
    114 static SRWLOCK g_jit_dual_map_lock = SRWLOCK_INIT;
    115 
    116 static void exec_dual_register_w(void* write_base, void* runtime_base,
    117                                  size_t size) {
    118   ExecDualNode* n;
    119   if (write_base == runtime_base) return;
    120   n = (ExecDualNode*)malloc(sizeof(*n));
    121   if (!n) return;
    122   n->write_base = write_base;
    123   n->runtime_base = runtime_base;
    124   n->size = size;
    125   AcquireSRWLockExclusive(&g_jit_dual_map_lock);
    126   n->next = g_jit_dual_map;
    127   g_jit_dual_map = n;
    128   ReleaseSRWLockExclusive(&g_jit_dual_map_lock);
    129 }
    130 
    131 static void exec_dual_unregister_w(void* runtime_base) {
    132   ExecDualNode** pp;
    133   AcquireSRWLockExclusive(&g_jit_dual_map_lock);
    134   for (pp = &g_jit_dual_map; *pp; pp = &(*pp)->next) {
    135     if ((*pp)->runtime_base == runtime_base) {
    136       ExecDualNode* dead = *pp;
    137       *pp = dead->next;
    138       free(dead);
    139       break;
    140     }
    141   }
    142   ReleaseSRWLockExclusive(&g_jit_dual_map_lock);
    143 }
    144 
    145 static int exec_dual_lookup_w(void* runtime_addr, size_t n, void** write_out) {
    146   ExecDualNode* cur;
    147   uintptr_t a = (uintptr_t)runtime_addr;
    148   AcquireSRWLockShared(&g_jit_dual_map_lock);
    149   for (cur = g_jit_dual_map; cur; cur = cur->next) {
    150     uintptr_t base = (uintptr_t)cur->runtime_base;
    151     if (a >= base && a + n <= base + cur->size) {
    152       *write_out = (void*)((uintptr_t)cur->write_base + (a - base));
    153       ReleaseSRWLockShared(&g_jit_dual_map_lock);
    154       return 0;
    155     }
    156   }
    157   ReleaseSRWLockShared(&g_jit_dual_map_lock);
    158   return 1;
    159 }
    160 
    161 /* ============================================================
    162  *   exec memory: dual-map via CreateFileMappingW, single via VirtualAlloc
    163  * ============================================================ */
    164 
    165 typedef struct ExecMemTokenWin {
    166   HANDLE mapping; /* NULL for single-mapping reservations */
    167   void* write_addr;
    168   void* runtime_addr;
    169   size_t size;
    170 } ExecMemTokenWin;
    171 
    172 static DWORD kit_to_win_prot(int prot) {
    173   int r = (prot & KIT_PROT_READ) != 0;
    174   int w = (prot & KIT_PROT_WRITE) != 0;
    175   int x = (prot & KIT_PROT_EXEC) != 0;
    176   if (x && w) return PAGE_EXECUTE_READWRITE;
    177   if (x && r) return PAGE_EXECUTE_READ;
    178   if (x) return PAGE_EXECUTE;
    179   if (w) return PAGE_READWRITE;
    180   if (r) return PAGE_READONLY;
    181   return PAGE_NOACCESS;
    182 }
    183 
    184 static size_t driver_host_page_size_win(void) {
    185   SYSTEM_INFO si;
    186   GetSystemInfo(&si);
    187   return si.dwPageSize ? (size_t)si.dwPageSize : (size_t)0x1000;
    188 }
    189 
    190 static KitStatus execmem_reserve_single_win(size_t size,
    191                                             KitExecMemRegion* out) {
    192   void* p = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    193   if (!p) return KIT_NOMEM;
    194   out->write = p;
    195   out->runtime = p;
    196   out->size = size;
    197   out->token = NULL;
    198   return KIT_OK;
    199 }
    200 
    201 static KitStatus execmem_reserve_dual_win(size_t size, KitExecMemRegion* out) {
    202   HANDLE map;
    203   void* w;
    204   void* r;
    205   ExecMemTokenWin* tok;
    206   DWORD lo = (DWORD)(size & 0xFFFFFFFFu);
    207   DWORD hi = (DWORD)((uint64_t)size >> 32);
    208 
    209   /* PAGE_EXECUTE_READWRITE on the section object is the max protection any
    210    * view can request; per-view protections are narrower (RW vs RX). */
    211   map = CreateFileMappingW(INVALID_HANDLE_VALUE, NULL, PAGE_EXECUTE_READWRITE,
    212                            hi, lo, NULL);
    213   if (!map) return KIT_ERR;
    214 
    215   w = MapViewOfFile(map, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, size);
    216   if (!w) {
    217     CloseHandle(map);
    218     return KIT_NOMEM;
    219   }
    220   /* Include FILE_MAP_WRITE in the runtime/exec view's mapping access. A view's
    221    * VirtualProtect ceiling is bounded by its map-access flags, not just the
    222    * section's max protection: a view mapped READ|EXECUTE can be reprotected to
    223    * R/RX/RO but NOT to PAGE_READWRITE (err 87). JIT images carry writable
    224    * runtime segments (an import GOT, .data/.bss) whose final perms are RW, so
    225    * the exec view must permit write to be VirtualProtect'd RW. Per-segment page
    226    * protection still enforces W^X (code stays RX, never writable). */
    227   r = MapViewOfFile(map, FILE_MAP_READ | FILE_MAP_WRITE | FILE_MAP_EXECUTE, 0,
    228                     0, size);
    229   if (!r) {
    230     UnmapViewOfFile(w);
    231     CloseHandle(map);
    232     return KIT_NOMEM;
    233   }
    234 
    235   tok = (ExecMemTokenWin*)malloc(sizeof(*tok));
    236   if (!tok) {
    237     UnmapViewOfFile(r);
    238     UnmapViewOfFile(w);
    239     CloseHandle(map);
    240     return KIT_NOMEM;
    241   }
    242   tok->mapping = map;
    243   tok->write_addr = w;
    244   tok->runtime_addr = r;
    245   tok->size = size;
    246 
    247   exec_dual_register_w(w, r, size);
    248 
    249   out->write = w;
    250   out->runtime = r;
    251   out->size = size;
    252   out->token = tok;
    253   return KIT_OK;
    254 }
    255 
    256 static KitStatus execmem_reserve_win(void* user, size_t size, int prot,
    257                                      KitExecMemRegion* out) {
    258   (void)user;
    259   if (!out || !size) return KIT_INVALID;
    260   if (prot & KIT_PROT_EXEC) return execmem_reserve_dual_win(size, out);
    261   return execmem_reserve_single_win(size, out);
    262 }
    263 
    264 static KitStatus execmem_protect_win(void* user, void* addr, size_t size,
    265                                      int prot) {
    266   DWORD old;
    267   (void)user;
    268   return VirtualProtect(addr, size, kit_to_win_prot(prot), &old) ? KIT_OK
    269                                                                  : KIT_ERR;
    270 }
    271 
    272 static void execmem_release_win(void* user, KitExecMemRegion* region) {
    273   (void)user;
    274   if (!region || !region->size) return;
    275   if (region->token) {
    276     ExecMemTokenWin* tok = (ExecMemTokenWin*)region->token;
    277     if (tok->runtime_addr && tok->runtime_addr != tok->write_addr) {
    278       exec_dual_unregister_w(tok->runtime_addr);
    279       UnmapViewOfFile(tok->runtime_addr);
    280     }
    281     if (tok->write_addr) UnmapViewOfFile(tok->write_addr);
    282     if (tok->mapping) CloseHandle(tok->mapping);
    283     free(tok);
    284   } else if (region->write) {
    285     VirtualFree(region->write, 0, MEM_RELEASE);
    286   }
    287   region->write = NULL;
    288   region->runtime = NULL;
    289   region->size = 0;
    290   region->token = NULL;
    291 }
    292 
    293 static void execmem_flush_icache_win(void* user, void* addr, size_t size) {
    294   (void)user;
    295   FlushInstructionCache(GetCurrentProcess(), addr, size);
    296 }
    297 
    298 static KitExecMem g_execmem_win;
    299 
    300 /* ============================================================
    301  *   Writer vtables: HANDLE-backed and stdio-backed
    302  * ============================================================ */
    303 
    304 typedef struct DriverHandleWriter {
    305   KitWriter base;
    306   KitHeap* heap;
    307   HANDLE h;
    308   KitStatus status;
    309   uint64_t pos;
    310 } DriverHandleWriter;
    311 
    312 static KitStatus hw_write(KitWriter* w, const void* data, size_t n) {
    313   DriverHandleWriter* fw = (DriverHandleWriter*)w;
    314   const unsigned char* p = (const unsigned char*)data;
    315   if (fw->status != KIT_OK) return fw->status;
    316   while (n > 0) {
    317     DWORD chunk = n > 0x40000000u ? 0x40000000u : (DWORD)n;
    318     DWORD wrote = 0;
    319     if (!WriteFile(fw->h, p, chunk, &wrote, NULL) || wrote == 0) {
    320       fw->status = KIT_IO;
    321       return KIT_IO;
    322     }
    323     p += wrote;
    324     n -= wrote;
    325     fw->pos += wrote;
    326   }
    327   return KIT_OK;
    328 }
    329 
    330 static KitStatus hw_seek(KitWriter* w, uint64_t off) {
    331   DriverHandleWriter* fw = (DriverHandleWriter*)w;
    332   LARGE_INTEGER li;
    333   if (fw->status != KIT_OK) return fw->status;
    334   li.QuadPart = (LONGLONG)off;
    335   if (!SetFilePointerEx(fw->h, li, NULL, FILE_BEGIN)) {
    336     fw->status = KIT_IO;
    337     return KIT_IO;
    338   }
    339   fw->pos = off;
    340   return KIT_OK;
    341 }
    342 
    343 static uint64_t hw_tell(KitWriter* w) { return ((DriverHandleWriter*)w)->pos; }
    344 static KitStatus hw_status(KitWriter* w) {
    345   return ((DriverHandleWriter*)w)->status;
    346 }
    347 static void hw_close(KitWriter* w) {
    348   DriverHandleWriter* fw = (DriverHandleWriter*)w;
    349   if (fw->h && fw->h != INVALID_HANDLE_VALUE) CloseHandle(fw->h);
    350   fw->heap->free(fw->heap, fw, sizeof(*fw));
    351 }
    352 
    353 static KitWriter* driver_writer_handle(KitHeap* h, HANDLE fh) {
    354   DriverHandleWriter* fw = (DriverHandleWriter*)h->alloc(
    355       h, sizeof(*fw), _Alignof(DriverHandleWriter));
    356   if (!fw) return NULL;
    357   fw->base.write = hw_write;
    358   fw->base.seek = hw_seek;
    359   fw->base.tell = hw_tell;
    360   fw->base.status = hw_status;
    361   fw->base.close = hw_close;
    362   fw->heap = h;
    363   fw->h = fh;
    364   fw->status = KIT_OK;
    365   fw->pos = 0;
    366   return &fw->base;
    367 }
    368 
    369 typedef struct DriverStdioWriter {
    370   KitWriter base;
    371   KitHeap* heap;
    372   FILE* fp;
    373   KitStatus status;
    374 } DriverStdioWriter;
    375 
    376 static KitStatus stdio_w_write(KitWriter* w, const void* data, size_t n) {
    377   DriverStdioWriter* sw = (DriverStdioWriter*)w;
    378   if (n) {
    379     size_t got = fwrite(data, 1, n, sw->fp);
    380     if (got != n) {
    381       sw->status = KIT_IO;
    382       return KIT_IO;
    383     }
    384   }
    385   return KIT_OK;
    386 }
    387 static KitStatus stdio_w_seek(KitWriter* w, uint64_t off) {
    388   DriverStdioWriter* sw = (DriverStdioWriter*)w;
    389   return fseek(sw->fp, (long)off, SEEK_SET) == 0 ? KIT_OK : KIT_IO;
    390 }
    391 static uint64_t stdio_w_tell(KitWriter* w) {
    392   long t = ftell(((DriverStdioWriter*)w)->fp);
    393   return t < 0 ? 0u : (uint64_t)t;
    394 }
    395 static KitStatus stdio_w_status(KitWriter* w) {
    396   DriverStdioWriter* sw = (DriverStdioWriter*)w;
    397   if (sw->status != KIT_OK) return sw->status;
    398   return ferror(sw->fp) ? KIT_IO : KIT_OK;
    399 }
    400 static void stdio_w_close(KitWriter* w) {
    401   DriverStdioWriter* sw = (DriverStdioWriter*)w;
    402   fflush(sw->fp);
    403   sw->heap->free(sw->heap, sw, sizeof(*sw));
    404 }
    405 
    406 static KitWriter* driver_stdio_writer(DriverEnv* e, FILE* fp) {
    407   DriverStdioWriter* sw = (DriverStdioWriter*)e->heap->alloc(
    408       e->heap, sizeof(*sw), _Alignof(DriverStdioWriter));
    409   if (!sw) return NULL;
    410   sw->base.write = stdio_w_write;
    411   sw->base.seek = stdio_w_seek;
    412   sw->base.tell = stdio_w_tell;
    413   sw->base.status = stdio_w_status;
    414   sw->base.close = stdio_w_close;
    415   sw->heap = e->heap;
    416   sw->fp = fp;
    417   sw->status = KIT_OK;
    418   return &sw->base;
    419 }
    420 
    421 KitWriter* driver_stdout_writer(DriverEnv* e) {
    422   return driver_stdio_writer(e, stdout);
    423 }
    424 
    425 KitWriter* driver_stderr_writer(DriverEnv* e) {
    426   return driver_stdio_writer(e, stderr);
    427 }
    428 
    429 const char* const* driver_environ(void) { return (const char* const*)_environ; }
    430 
    431 /* ============================================================
    432  *   file_io (CreateFileW + ReadFile/WriteFile)
    433  * ============================================================ */
    434 
    435 static KitStatus win_read_all(void* user, const char* path, KitFileData* out) {
    436   DriverEnv* env = (DriverEnv*)user;
    437   wchar_t* wpath;
    438   HANDLE h;
    439   LARGE_INTEGER sz;
    440   size_t size;
    441   size_t got;
    442   void* buf;
    443 
    444   wpath = widen(path);
    445   if (!wpath) return KIT_NOT_FOUND;
    446   h = CreateFileW(wpath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING,
    447                   FILE_ATTRIBUTE_NORMAL, NULL);
    448   free(wpath);
    449   if (h == INVALID_HANDLE_VALUE) return KIT_NOT_FOUND;
    450   if (!GetFileSizeEx(h, &sz)) {
    451     CloseHandle(h);
    452     return KIT_IO;
    453   }
    454   size = (size_t)sz.QuadPart;
    455   buf = size ? env->heap->alloc(env->heap, size, 1) : NULL;
    456   if (size && !buf) {
    457     CloseHandle(h);
    458     return KIT_NOMEM;
    459   }
    460   got = 0;
    461   while (got < size) {
    462     DWORD chunk =
    463         (size - got) > 0x40000000u ? 0x40000000u : (DWORD)(size - got);
    464     DWORD n = 0;
    465     if (!ReadFile(h, (unsigned char*)buf + got, chunk, &n, NULL) || n == 0) {
    466       env->heap->free(env->heap, buf, size);
    467       CloseHandle(h);
    468       return KIT_IO;
    469     }
    470     got += n;
    471   }
    472   CloseHandle(h);
    473   out->data = (const uint8_t*)buf;
    474   out->size = size;
    475   out->token = buf;
    476   return KIT_OK;
    477 }
    478 
    479 static void win_release(void* user, KitFileData* d) {
    480   DriverEnv* env = (DriverEnv*)user;
    481   if (d->token) env->heap->free(env->heap, d->token, d->size);
    482   d->data = NULL;
    483   d->size = 0;
    484   d->token = NULL;
    485 }
    486 
    487 static KitStatus win_open_writer(void* user, const char* path,
    488                                  KitWriter** out) {
    489   DriverEnv* env = (DriverEnv*)user;
    490   wchar_t* wpath = widen(path);
    491   HANDLE h;
    492   KitWriter* w;
    493   if (!wpath) return KIT_IO;
    494   h = CreateFileW(wpath, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS,
    495                   FILE_ATTRIBUTE_NORMAL, NULL);
    496   free(wpath);
    497   if (h == INVALID_HANDLE_VALUE) return KIT_IO;
    498   w = driver_writer_handle(env->heap, h);
    499   if (!w) {
    500     CloseHandle(h);
    501     return KIT_NOMEM;
    502   }
    503   *out = w;
    504   return KIT_OK;
    505 }
    506 
    507 /* ============================================================
    508  *   Path helpers
    509  * ============================================================ */
    510 
    511 int driver_path_exists(const char* path) {
    512   WIN32_FILE_ATTRIBUTE_DATA fad;
    513   wchar_t* wpath;
    514   BOOL ok;
    515   if (!path) return 0;
    516   wpath = widen(path);
    517   if (!wpath) return 0;
    518   ok = GetFileAttributesExW(wpath, GetFileExInfoStandard, &fad);
    519   free(wpath);
    520   return ok ? 1 : 0;
    521 }
    522 
    523 /* Convert a FILETIME (100-ns ticks since 1601-01-01 UTC) to ns since the
    524  * Unix epoch (1970-01-01 UTC). 11644473600 sec is the gap. */
    525 static int64_t filetime_to_unix_ns(FILETIME ft) {
    526   uint64_t t = ((uint64_t)ft.dwHighDateTime << 32) | (uint64_t)ft.dwLowDateTime;
    527   /* Subtract Win-to-Unix epoch offset in 100-ns ticks. */
    528   static const uint64_t EPOCH_DIFF_100NS = 116444736000000000ull;
    529   if (t < EPOCH_DIFF_100NS) t = EPOCH_DIFF_100NS;
    530   return (int64_t)((t - EPOCH_DIFF_100NS) * 100ull);
    531 }
    532 
    533 int driver_path_mtime_ns(const char* path, int64_t* out) {
    534   WIN32_FILE_ATTRIBUTE_DATA fad;
    535   wchar_t* wpath;
    536   BOOL ok;
    537   if (!path || !out) return 1;
    538   wpath = widen(path);
    539   if (!wpath) return 1;
    540   ok = GetFileAttributesExW(wpath, GetFileExInfoStandard, &fad);
    541   free(wpath);
    542   if (!ok) return 1;
    543   *out = filetime_to_unix_ns(fad.ftLastWriteTime);
    544   return 0;
    545 }
    546 
    547 int driver_path_stat(const char* path, uint64_t* out_size,
    548                      uint64_t* out_mtime_ns, uint8_t* out_filetype) {
    549   WIN32_FILE_ATTRIBUTE_DATA fad;
    550   wchar_t* wpath;
    551   BOOL ok;
    552   DWORD err;
    553   if (!path) return 2;
    554   wpath = widen(path);
    555   if (!wpath) return 2;
    556   ok = GetFileAttributesExW(wpath, GetFileExInfoStandard, &fad);
    557   err = ok ? 0 : GetLastError();
    558   free(wpath);
    559   if (!ok) {
    560     return (err == ERROR_FILE_NOT_FOUND || err == ERROR_PATH_NOT_FOUND) ? 1 : 2;
    561   }
    562   *out_mtime_ns = (uint64_t)filetime_to_unix_ns(fad.ftLastWriteTime);
    563   if (fad.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
    564     *out_size = 0u;
    565     *out_filetype = 3; /* DIRECTORY */
    566   } else if (fad.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {
    567     *out_size = ((uint64_t)fad.nFileSizeHigh << 32) | fad.nFileSizeLow;
    568     *out_filetype = 7; /* SYMBOLIC_LINK */
    569   } else {
    570     *out_size = ((uint64_t)fad.nFileSizeHigh << 32) | fad.nFileSizeLow;
    571     *out_filetype = 4; /* REGULAR_FILE */
    572   }
    573   return 0;
    574 }
    575 
    576 typedef struct DriverDirEntryRec {
    577   char* name;
    578   size_t name_alloc;
    579   uint32_t name_len;
    580   uint64_t ino;
    581   uint64_t size;
    582   uint64_t mtime_ns;
    583   uint8_t filetype;
    584 } DriverDirEntryRec;
    585 
    586 struct DriverDirHandle {
    587   DriverEnv* env;
    588   DriverDirEntryRec* entries;
    589   size_t entries_alloc;
    590   uint64_t count;
    591 };
    592 
    593 static char* driver_join_path(DriverEnv* env, const char* a, const char* b);
    594 
    595 DriverDirHandle* driver_open_dir(DriverEnv* env, const char* path) {
    596   char* pattern;
    597   wchar_t* wpattern;
    598   WIN32_FIND_DATAW fd;
    599   HANDLE h;
    600   DWORD last;
    601   DriverDirHandle* dh;
    602   uint64_t cap = 0;
    603   uint64_t count = 0;
    604 
    605   if (!env || !path) return NULL;
    606   pattern = driver_join_path(env, path, "*");
    607   if (!pattern) return NULL;
    608   wpattern = widen(pattern);
    609   driver_free(env, pattern, kit_slice_cstr(pattern).len + 1u);
    610   if (!wpattern) return NULL;
    611 
    612   h = FindFirstFileW(wpattern, &fd);
    613   free(wpattern);
    614   if (h == INVALID_HANDLE_VALUE) return NULL;
    615 
    616   dh = (DriverDirHandle*)env->heap->alloc(env->heap, sizeof(*dh),
    617                                           _Alignof(DriverDirHandle));
    618   if (!dh) {
    619     FindClose(h);
    620     return NULL;
    621   }
    622   memset(dh, 0, sizeof(*dh));
    623   dh->env = env;
    624 
    625   for (;;) {
    626     char* name;
    627     size_t name_len;
    628     DriverDirEntryRec* e;
    629 
    630     name = narrow(fd.cFileName);
    631     if (!name) goto fail;
    632     if (driver_streq(name, ".") || driver_streq(name, "..")) {
    633       free(name);
    634       goto loop_next;
    635     }
    636 
    637     name_len = kit_slice_cstr(name).len;
    638 
    639     /* grow entry array */
    640     if (count >= cap) {
    641       uint64_t new_cap = cap ? cap * 2u : 8u;
    642       size_t new_alloc = (size_t)new_cap * sizeof(DriverDirEntryRec);
    643       DriverDirEntryRec* nv = (DriverDirEntryRec*)env->heap->alloc(
    644           env->heap, new_alloc, _Alignof(DriverDirEntryRec));
    645       if (!nv) {
    646         free(name);
    647         goto fail;
    648       }
    649       if (dh->entries) {
    650         memcpy(nv, dh->entries, (size_t)count * sizeof(DriverDirEntryRec));
    651         env->heap->free(env->heap, dh->entries, dh->entries_alloc);
    652       }
    653       dh->entries = nv;
    654       dh->entries_alloc = new_alloc;
    655       cap = new_cap;
    656     }
    657 
    658     e = &dh->entries[count];
    659     memset(e, 0, sizeof(*e));
    660     e->name_alloc = name_len + 1u;
    661     e->name = (char*)env->heap->alloc(env->heap, e->name_alloc, 1u);
    662     if (!e->name) {
    663       free(name);
    664       goto fail;
    665     }
    666     memcpy(e->name, name, name_len + 1u);
    667     e->name_len = (uint32_t)name_len;
    668     free(name);
    669 
    670     e->size = ((uint64_t)fd.nFileSizeHigh << 32) | fd.nFileSizeLow;
    671     e->mtime_ns = (uint64_t)filetime_to_unix_ns(fd.ftLastWriteTime);
    672     if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
    673       e->filetype = 3;
    674     else if (fd.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
    675       e->filetype = 7;
    676     else
    677       e->filetype = 4;
    678     ++count;
    679 
    680   loop_next:
    681     if (!FindNextFileW(h, &fd)) {
    682       last = GetLastError();
    683       if (last == ERROR_NO_MORE_FILES) break;
    684       goto fail;
    685     }
    686   }
    687 
    688   FindClose(h);
    689   dh->count = count;
    690   return dh;
    691 
    692 fail:
    693   FindClose(h);
    694   driver_close_dir(env, dh);
    695   return NULL;
    696 }
    697 
    698 int driver_read_dir_entry(DriverDirHandle* h, uint64_t index,
    699                           const char** out_name, uint32_t* out_name_len,
    700                           uint64_t* out_ino, uint64_t* out_size,
    701                           uint64_t* out_mtime_ns, uint8_t* out_filetype) {
    702   DriverDirEntryRec* e;
    703   if (!h || index >= h->count) return 1;
    704   e = &h->entries[index];
    705   *out_name = e->name;
    706   *out_name_len = e->name_len;
    707   *out_ino = e->ino;
    708   *out_size = e->size;
    709   *out_mtime_ns = e->mtime_ns;
    710   *out_filetype = e->filetype;
    711   return 0;
    712 }
    713 
    714 void driver_close_dir(DriverEnv* env, DriverDirHandle* h) {
    715   uint64_t i;
    716   if (!h) return;
    717   if (!env) env = h->env;
    718   for (i = 0; i < h->count; ++i) {
    719     DriverDirEntryRec* e = &h->entries[i];
    720     if (e->name) env->heap->free(env->heap, e->name, e->name_alloc);
    721   }
    722   if (h->entries) env->heap->free(env->heap, h->entries, h->entries_alloc);
    723   env->heap->free(env->heap, h, sizeof(*h));
    724 }
    725 
    726 int driver_mkdir_p(DriverEnv* env, const char* path) {
    727   size_t len;
    728   char* buf;
    729   size_t i;
    730   if (!path || !path[0]) return 1;
    731   len = kit_slice_cstr(path).len;
    732   buf = (char*)driver_alloc(env, len + 1);
    733   if (!buf) return 1;
    734   memcpy(buf, path, len + 1);
    735 
    736   /* Walk separators, accepting both '/' and '\\'. Skip the drive prefix
    737    * ("C:") and the leading separator(s) of a UNC path so we don't try to
    738    * CreateDirectory("\\\\server"). */
    739   for (i = 0; i <= len; ++i) {
    740     int at_end = (i == len);
    741     char ch = buf[i];
    742     int is_sep = (ch == '/' || ch == '\\');
    743     int do_create = at_end || is_sep;
    744     if (!do_create) continue;
    745     if (!at_end) buf[i] = '\0';
    746     /* Skip pure roots: "", ".", drive-letter-only ("C:"), and UNC roots
    747      * ("\\\\server" or "\\\\server\\share"). */
    748     if (buf[0] == '\0') {
    749       /* nothing yet */
    750     } else if (driver_streq(buf, ".")) {
    751       /* skip */
    752     } else if (i >= 2 && buf[1] == ':' && buf[2] == '\0') {
    753       /* "C:" — drive prefix only */
    754     } else {
    755       wchar_t* wpath = widen(buf);
    756       if (!wpath) {
    757         driver_free(env, buf, len + 1);
    758         return 1;
    759       }
    760       if (!CreateDirectoryW(wpath, NULL)) {
    761         DWORD err = GetLastError();
    762         if (err != ERROR_ALREADY_EXISTS) {
    763           free(wpath);
    764           driver_free(env, buf, len + 1);
    765           return 1;
    766         }
    767       }
    768       {
    769         WIN32_FILE_ATTRIBUTE_DATA fad;
    770         BOOL ok = GetFileAttributesExW(wpath, GetFileExInfoStandard, &fad);
    771         free(wpath);
    772         if (!ok || !(fad.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {
    773           driver_free(env, buf, len + 1);
    774           return 1;
    775         }
    776       }
    777     }
    778     if (!at_end) buf[i] = ch;
    779   }
    780 
    781   driver_free(env, buf, len + 1);
    782   return 0;
    783 }
    784 
    785 int driver_mark_executable_output(const char* path) {
    786   /* Windows has no Unix +x bit; file extension governs executability and
    787    * NTFS ACLs are inherited from the parent directory. No-op success. */
    788   (void)path;
    789   return 0;
    790 }
    791 
    792 /* ---------------- self executable path ---------------- */
    793 /* GetModuleFileNameW(NULL) reports the path of the running image. A return
    794  * equal to the buffer size means truncation (older Windows doesn't fail), so
    795  * grow until the result fits, then narrow to UTF-8. */
    796 int driver_self_exe_path(DriverEnv* env, char** out, size_t* out_size) {
    797   DWORD cap = 256;
    798   wchar_t* wbuf = NULL;
    799   char* narrowed;
    800   size_t size;
    801   if (!env || !out || !out_size) return 1;
    802   for (;;) {
    803     wchar_t* nb = (wchar_t*)realloc(wbuf, (size_t)cap * sizeof(wchar_t));
    804     DWORD n;
    805     if (!nb) {
    806       free(wbuf);
    807       return 1;
    808     }
    809     wbuf = nb;
    810     n = GetModuleFileNameW(NULL, wbuf, cap);
    811     if (n == 0) {
    812       free(wbuf);
    813       return 1;
    814     }
    815     if (n < cap) break; /* fit: n excludes the terminator on success */
    816     if (cap >= (1u << 20)) {
    817       free(wbuf);
    818       return 1;
    819     }
    820     cap *= 2;
    821   }
    822   narrowed = narrow(wbuf); /* malloc'd UTF-8 */
    823   free(wbuf);
    824   if (!narrowed) return 1;
    825   size = driver_strlen(narrowed) + 1u;
    826   *out = (char*)driver_alloc(env, size);
    827   if (!*out) {
    828     free(narrowed);
    829     return 1;
    830   }
    831   driver_memcpy(*out, narrowed, size);
    832   *out_size = size;
    833   free(narrowed);
    834   return 0;
    835 }
    836 
    837 /* ---------------- link helpers (install) ---------------- */
    838 
    839 #ifndef SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE
    840 #define SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE 0x2
    841 #endif
    842 
    843 int driver_create_symlink(const char* target, const char* link_path) {
    844   wchar_t* wtarget;
    845   wchar_t* wlink;
    846   BOOLEAN ok;
    847   if (!target || !link_path) return 1;
    848   wtarget = widen(target);
    849   wlink = widen(link_path);
    850   if (!wtarget || !wlink) {
    851     free(wtarget);
    852     free(wlink);
    853     return 1;
    854   }
    855   /* Prefer the unprivileged (Developer Mode) flag; fall back without it for
    856    * older hosts that reject the unknown flag. */
    857   ok = CreateSymbolicLinkW(wlink, wtarget,
    858                            SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE);
    859   if (!ok) ok = CreateSymbolicLinkW(wlink, wtarget, 0);
    860   free(wtarget);
    861   free(wlink);
    862   return ok ? 0 : 1;
    863 }
    864 
    865 int driver_create_hardlink(const char* target, const char* link_path) {
    866   wchar_t* wtarget;
    867   wchar_t* wlink;
    868   BOOL ok;
    869   if (!target || !link_path) return 1;
    870   wtarget = widen(target);
    871   wlink = widen(link_path);
    872   if (!wtarget || !wlink) {
    873     free(wtarget);
    874     free(wlink);
    875     return 1;
    876   }
    877   ok = CreateHardLinkW(wlink, wtarget, NULL);
    878   free(wtarget);
    879   free(wlink);
    880   return ok ? 0 : 1;
    881 }
    882 
    883 int driver_remove_file(const char* path) {
    884   wchar_t* wpath;
    885   BOOL ok;
    886   if (!path) return 1;
    887   wpath = widen(path);
    888   if (!wpath) return 1;
    889   ok = DeleteFileW(wpath);
    890   if (!ok) {
    891     DWORD err = GetLastError();
    892     if (err == ERROR_FILE_NOT_FOUND || err == ERROR_PATH_NOT_FOUND) ok = TRUE;
    893   }
    894   free(wpath);
    895   return ok ? 0 : 1;
    896 }
    897 
    898 int driver_path_lexists(const char* path) {
    899   wchar_t* wpath;
    900   DWORD attr;
    901   if (!path) return 0;
    902   wpath = widen(path);
    903   if (!wpath) return 0;
    904   /* GetFileAttributesW does not traverse reparse points, so a dangling
    905    * symlink reports its own attributes rather than failing. */
    906   attr = GetFileAttributesW(wpath);
    907   free(wpath);
    908   return attr != INVALID_FILE_ATTRIBUTES;
    909 }
    910 
    911 static char* driver_join_path(DriverEnv* env, const char* a, const char* b) {
    912   size_t al = kit_slice_cstr(a).len;
    913   size_t bl = kit_slice_cstr(b).len;
    914   int slash = al > 0 && a[al - 1u] != '/' && a[al - 1u] != '\\';
    915   char* out = (char*)driver_alloc(env, al + (slash ? 1u : 0u) + bl + 1u);
    916   if (!out) return NULL;
    917   memcpy(out, a, al);
    918   if (slash) out[al++] = '/';
    919   memcpy(out + al, b, bl);
    920   out[al + bl] = '\0';
    921   return out;
    922 }
    923 
    924 static int driver_walk_regular_files_at(DriverEnv* env, const char* dir,
    925                                         const char* rel, DriverWalkFileFn cb,
    926                                         void* user) {
    927   char* pattern;
    928   wchar_t* wpattern;
    929   WIN32_FIND_DATAW fd;
    930   HANDLE h;
    931   DWORD last;
    932   int rc = 1;
    933 
    934   pattern = driver_join_path(env, dir, "*");
    935   if (!pattern) return 1;
    936   wpattern = widen(pattern);
    937   driver_free(env, pattern, kit_slice_cstr(pattern).len + 1u);
    938   if (!wpattern) return 1;
    939 
    940   h = FindFirstFileW(wpattern, &fd);
    941   free(wpattern);
    942   if (h == INVALID_HANDLE_VALUE) {
    943     last = GetLastError();
    944     return last == ERROR_FILE_NOT_FOUND ? 0 : 1;
    945   }
    946 
    947   for (;;) {
    948     char* name = narrow(fd.cFileName);
    949     char* child = NULL;
    950     char* child_rel = NULL;
    951     int child_rc = 0;
    952     if (!name) goto loop_fail;
    953     if (driver_streq(name, ".") || driver_streq(name, "..")) {
    954       free(name);
    955       goto loop_next;
    956     }
    957     child = driver_join_path(env, dir, name);
    958     child_rel = rel && rel[0] ? driver_join_path(env, rel, name)
    959                               : driver_join_path(env, "", name);
    960     if (!child || !child_rel) goto loop_fail;
    961     if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
    962       if ((fd.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) {
    963         child_rc = 1;
    964       } else {
    965         child_rc =
    966             driver_walk_regular_files_at(env, child, child_rel, cb, user);
    967       }
    968     } else {
    969       child_rc = cb(user, child, child_rel, 0);
    970     }
    971     if (child_rel)
    972       driver_free(env, child_rel, kit_slice_cstr(child_rel).len + 1u);
    973     if (child) driver_free(env, child, kit_slice_cstr(child).len + 1u);
    974     free(name);
    975     if (child_rc) goto out;
    976 
    977   loop_next:
    978     if (!FindNextFileW(h, &fd)) break;
    979     continue;
    980 
    981   loop_fail:
    982     if (child_rel)
    983       driver_free(env, child_rel, kit_slice_cstr(child_rel).len + 1u);
    984     if (child) driver_free(env, child, kit_slice_cstr(child).len + 1u);
    985     if (name) free(name);
    986     goto out;
    987   }
    988 
    989   last = GetLastError();
    990   rc = last == ERROR_NO_MORE_FILES ? 0 : 1;
    991 
    992 out:
    993   FindClose(h);
    994   return rc;
    995 }
    996 
    997 int driver_walk_regular_files(DriverEnv* env, const char* root,
    998                               DriverWalkFileFn cb, void* user) {
    999   if (!env || !root || !root[0] || !cb) return 1;
   1000   return driver_walk_regular_files_at(env, root, "", cb, user);
   1001 }
   1002 
   1003 /* ============================================================
   1004  *   Time
   1005  * ============================================================ */
   1006 
   1007 uint64_t driver_now_ns(void) {
   1008   /* QueryPerformanceCounter is monotonic across cores on every supported
   1009    * Windows version since Vista. */
   1010   LARGE_INTEGER freq;
   1011   LARGE_INTEGER ctr;
   1012   if (!QueryPerformanceFrequency(&freq) || freq.QuadPart <= 0) return 0;
   1013   if (!QueryPerformanceCounter(&ctr)) return 0;
   1014   /* Avoid 128-bit multiply: split ticks into integer-seconds and remainder. */
   1015   {
   1016     int64_t sec = ctr.QuadPart / freq.QuadPart;
   1017     int64_t rem = ctr.QuadPart % freq.QuadPart;
   1018     return (uint64_t)sec * 1000000000ull +
   1019            (uint64_t)((rem * 1000000000) / freq.QuadPart);
   1020   }
   1021 }
   1022 
   1023 int driver_random_bytes(uint8_t* out, size_t n) {
   1024   /* rand_s draws from the OS CSPRNG (RtlGenRandom) without linking bcrypt. */
   1025   size_t off = 0;
   1026   if (!out) return 1;
   1027   while (off < n) {
   1028     unsigned int v;
   1029     size_t chunk;
   1030     if (rand_s(&v) != 0) return 1;
   1031     chunk = n - off < sizeof v ? n - off : sizeof v;
   1032     memcpy(out + off, &v, chunk);
   1033     off += chunk;
   1034   }
   1035   return 0;
   1036 }
   1037 
   1038 /* ============================================================
   1039  *   load helpers
   1040  * ============================================================ */
   1041 
   1042 int driver_load_bytes(const KitFileIO* io, const char* tool, const char* path,
   1043                       DriverLoad* out, KitSlice* in) {
   1044   out->loaded = 0;
   1045   out->fd.data = NULL;
   1046   out->fd.size = 0;
   1047   out->fd.token = NULL;
   1048   if (!io || !io->read_all) {
   1049     driver_errf(tool, "host file I/O unavailable");
   1050     return 1;
   1051   }
   1052   if (io->read_all(io->user, path, &out->fd) != KIT_OK) {
   1053     driver_errf(tool, "failed to read: %.*s",
   1054                 KIT_SLICE_ARG(kit_slice_cstr(path)));
   1055     return 1;
   1056   }
   1057   out->loaded = 1;
   1058   in->data = out->fd.data;
   1059   in->len = out->fd.size;
   1060   return 0;
   1061 }
   1062 
   1063 void driver_release_bytes(const KitFileIO* io, DriverLoad* lf) {
   1064   if (!lf || !lf->loaded) return;
   1065   if (io && io->release) io->release(io->user, &lf->fd);
   1066   lf->loaded = 0;
   1067 }
   1068 
   1069 /* ============================================================
   1070  *   stdin / edit_temp / read_line
   1071  * ============================================================ */
   1072 
   1073 int driver_read_stdin(DriverEnv* e, uint8_t** out_data, size_t* out_size) {
   1074   size_t cap = 4096;
   1075   size_t len = 0;
   1076   uint8_t* buf = e->heap->alloc(e->heap, cap, 1);
   1077   HANDLE h = GetStdHandle(STD_INPUT_HANDLE);
   1078   if (!buf) return 0;
   1079   for (;;) {
   1080     DWORD n;
   1081     if (len == cap) {
   1082       size_t newcap = cap * 2;
   1083       uint8_t* nb = e->heap->realloc(e->heap, buf, cap, newcap, 1);
   1084       if (!nb) {
   1085         e->heap->free(e->heap, buf, cap);
   1086         return 0;
   1087       }
   1088       buf = nb;
   1089       cap = newcap;
   1090     }
   1091     if (!ReadFile(h, buf + len, (DWORD)(cap - len), &n, NULL)) {
   1092       if (GetLastError() == ERROR_BROKEN_PIPE) break; /* EOF on pipe */
   1093       e->heap->free(e->heap, buf, cap);
   1094       return 0;
   1095     }
   1096     if (n == 0) break;
   1097     len += n;
   1098   }
   1099   if (len < cap) {
   1100     uint8_t* shrunk = len ? e->heap->realloc(e->heap, buf, cap, len, 1) : NULL;
   1101     if (len && !shrunk) {
   1102       *out_data = buf;
   1103       *out_size = cap;
   1104       return 1;
   1105     }
   1106     if (!len) {
   1107       e->heap->free(e->heap, buf, cap);
   1108       buf = NULL;
   1109     } else {
   1110       buf = shrunk;
   1111     }
   1112   }
   1113   *out_data = buf;
   1114   *out_size = len;
   1115   return 1;
   1116 }
   1117 
   1118 static int driver_write_handle_all(HANDLE h, const uint8_t* data, size_t n) {
   1119   size_t off = 0;
   1120   while (off < n) {
   1121     DWORD chunk = (n - off) > 0x40000000u ? 0x40000000u : (DWORD)(n - off);
   1122     DWORD wr = 0;
   1123     if (!WriteFile(h, data + off, chunk, &wr, NULL) || wr == 0) return 0;
   1124     off += wr;
   1125   }
   1126   return 1;
   1127 }
   1128 
   1129 int driver_edit_temp(DriverEnv* e, const char* suffix, const uint8_t* initial,
   1130                      size_t initial_size, uint8_t** out_data,
   1131                      size_t* out_size) {
   1132   /* Windows temp-file dance:
   1133    *   - GetTempPathW gives us %TMP%/%TEMP%/%USERPROFILE% with trailing '\\'.
   1134    *   - GetTempFileNameW makes a unique "<dir>\\cfXXXX.tmp" path. We ignore
   1135    *     the ".tmp" and append our requested suffix below by renaming.
   1136    *   - We rename to "<base><suffix>" so the editor sees the right extension.
   1137    *   - Editor is launched via system() so shell quoting / PATH lookup is
   1138    *     handled by cmd.exe.
   1139    */
   1140   wchar_t tmp_dir[MAX_PATH + 1];
   1141   wchar_t tmp_path[MAX_PATH + 1];
   1142   DWORD got;
   1143   HANDLE h = INVALID_HANDLE_VALUE;
   1144   wchar_t* wsuffix = NULL;
   1145   wchar_t final_path[MAX_PATH + 64];
   1146   size_t final_len;
   1147   int ok = 0;
   1148   KitFileData fd_data;
   1149   const char* editor;
   1150   char* cmd = NULL;
   1151   int rc;
   1152   size_t cmd_cap;
   1153   char utf8_path[MAX_PATH * 4 + 1];
   1154   int utf8_len;
   1155 
   1156   if (!out_data || !out_size) return 0;
   1157   *out_data = NULL;
   1158   *out_size = 0;
   1159 
   1160   got = GetTempPathW(MAX_PATH + 1, tmp_dir);
   1161   if (got == 0 || got > MAX_PATH) return 0;
   1162   if (GetTempFileNameW(tmp_dir, L"cf", 0, tmp_path) == 0) return 0;
   1163 
   1164   /* Build the final path = tmp_path with ".tmp" stripped + requested suffix.
   1165    * GetTempFileNameW already created the file at tmp_path; we MoveFileEx to
   1166    * rename it to final_path. */
   1167   {
   1168     size_t tplen = wcslen(tmp_path);
   1169     /* Strip the ".tmp" extension GetTempFileNameW appends. */
   1170     if (tplen >= 4 && tmp_path[tplen - 4] == L'.') tplen -= 4;
   1171     if (tplen >= sizeof(final_path) / sizeof(wchar_t) - 16) {
   1172       DeleteFileW(tmp_path);
   1173       return 0;
   1174     }
   1175     memcpy(final_path, tmp_path, tplen * sizeof(wchar_t));
   1176     final_path[tplen] = L'\0';
   1177     final_len = tplen;
   1178     if (suffix && *suffix) {
   1179       wsuffix = widen(suffix);
   1180       if (!wsuffix) {
   1181         DeleteFileW(tmp_path);
   1182         return 0;
   1183       }
   1184       {
   1185         size_t sl = wcslen(wsuffix);
   1186         if (final_len + sl + 1 >= sizeof(final_path) / sizeof(wchar_t)) {
   1187           free(wsuffix);
   1188           DeleteFileW(tmp_path);
   1189           return 0;
   1190         }
   1191         memcpy(final_path + final_len, wsuffix, sl * sizeof(wchar_t));
   1192         final_len += sl;
   1193         final_path[final_len] = L'\0';
   1194       }
   1195       free(wsuffix);
   1196     }
   1197     if (!MoveFileExW(tmp_path, final_path, MOVEFILE_REPLACE_EXISTING)) {
   1198       DeleteFileW(tmp_path);
   1199       return 0;
   1200     }
   1201   }
   1202 
   1203   /* Open the renamed file and write initial contents. */
   1204   h = CreateFileW(final_path, GENERIC_WRITE, FILE_SHARE_READ, NULL,
   1205                   TRUNCATE_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
   1206   if (h == INVALID_HANDLE_VALUE) goto out;
   1207   if (initial_size &&
   1208       !driver_write_handle_all(h, initial ? initial : (const uint8_t*)"",
   1209                                initial_size))
   1210     goto out;
   1211   CloseHandle(h);
   1212   h = INVALID_HANDLE_VALUE;
   1213 
   1214   /* Convert final_path back to UTF-8 for the editor command. */
   1215   utf8_len = WideCharToMultiByte(CP_UTF8, 0, final_path, -1, utf8_path,
   1216                                  (int)sizeof(utf8_path), NULL, NULL);
   1217   if (utf8_len <= 0) goto out;
   1218 
   1219   editor = getenv("VISUAL");
   1220   if (!editor || !*editor) editor = getenv("EDITOR");
   1221   if (!editor || !*editor) editor = "notepad";
   1222   cmd_cap = strlen(editor) + (size_t)utf8_len + 16;
   1223   cmd = (char*)malloc(cmd_cap);
   1224   if (!cmd) goto out;
   1225   /* Wrap the entire command in extra quotes so cmd.exe /S doesn't strip
   1226    * matching outer quotes when the editor path itself has spaces. */
   1227   rc = snprintf(cmd, cmd_cap, "\"\"%s\" \"%s\"\"", editor, utf8_path);
   1228   if (rc < 0 || (size_t)rc >= cmd_cap) goto out;
   1229   if (system(cmd) != 0) goto out;
   1230 
   1231   fd_data.data = NULL;
   1232   fd_data.size = 0;
   1233   fd_data.token = NULL;
   1234   if (win_read_all(e, utf8_path, &fd_data) != KIT_OK) goto out;
   1235   *out_data = (uint8_t*)fd_data.data;
   1236   *out_size = fd_data.size;
   1237   ok = 1;
   1238 
   1239 out:
   1240   if (h != INVALID_HANDLE_VALUE) CloseHandle(h);
   1241   if (cmd) free(cmd);
   1242   DeleteFileW(final_path);
   1243   return ok;
   1244 }
   1245 
   1246 int driver_read_line(char* buf, size_t cap) {
   1247   size_t len = 0;
   1248   if (!buf || cap < 2) return -1;
   1249   for (;;) {
   1250     int c = fgetc(stdin);
   1251     if (c == EOF) {
   1252       buf[len] = '\0';
   1253       if (ferror(stdin)) return -1;
   1254       if (len == 0) return 0;
   1255       return (int)len;
   1256     }
   1257     if (c == '\n') {
   1258       /* Strip a trailing CR if present (CRLF line ending). */
   1259       if (len > 0 && buf[len - 1] == '\r') --len;
   1260       buf[len] = '\0';
   1261       return (int)len;
   1262     }
   1263     if (len + 1 < cap) buf[len++] = (char)c;
   1264   }
   1265 }
   1266 
   1267 static int win_line_history_add(DriverEnv* env, DriverLineHistory* h,
   1268                                 const char* line, size_t len) {
   1269   char** ni;
   1270   size_t* ns;
   1271   char* copy;
   1272   uint32_t nc;
   1273   if (!env || !h || !line || len == 0) return 0;
   1274   if (h->count == h->cap) {
   1275     size_t old_items = (size_t)h->cap * sizeof(*h->items);
   1276     size_t new_items;
   1277     size_t old_sizes = (size_t)h->cap * sizeof(*h->sizes);
   1278     size_t new_sizes;
   1279     nc = h->cap ? h->cap * 2u : 32u;
   1280     new_items = (size_t)nc * sizeof(*h->items);
   1281     new_sizes = (size_t)nc * sizeof(*h->sizes);
   1282     ni = (char**)env->heap->realloc(env->heap, h->items, old_items, new_items,
   1283                                     _Alignof(char*));
   1284     if (!ni) return 1;
   1285     h->items = ni;
   1286     ns = (size_t*)env->heap->realloc(env->heap, h->sizes, old_sizes, new_sizes,
   1287                                      _Alignof(size_t));
   1288     if (!ns) return 1;
   1289     h->sizes = ns;
   1290     h->cap = nc;
   1291   }
   1292   copy = (char*)driver_alloc(env, len + 1u);
   1293   if (!copy) return 1;
   1294   memcpy(copy, line, len);
   1295   copy[len] = '\0';
   1296   h->items[h->count] = copy;
   1297   h->sizes[h->count] = len + 1u;
   1298   h->count++;
   1299   return 0;
   1300 }
   1301 
   1302 int driver_read_line_edit(DriverEnv* env, const char* prompt, char* buf,
   1303                           size_t cap, DriverLineHistory* hist,
   1304                           DriverLineCompleteFn complete, void* complete_user) {
   1305   int n;
   1306   (void)complete;
   1307   (void)complete_user;
   1308   if (prompt && *prompt) {
   1309     fputs(prompt, stdout);
   1310     fflush(stdout);
   1311   }
   1312   n = driver_read_line(buf, cap);
   1313   if (n > 0 && hist) (void)win_line_history_add(env, hist, buf, (size_t)n);
   1314   return n;
   1315 }
   1316 
   1317 /* ============================================================
   1318  *   dlsym via GetProcAddress over loaded modules
   1319  * ============================================================ */
   1320 
   1321 /* Snapshot the loaded modules at first call and remember them. We don't
   1322  * track DLL load/unload events; that's a reasonable trade-off for a JIT
   1323  * (the relevant DLLs -- ucrt, kernel32, the kit binary -- are loaded
   1324  * before the JIT extern resolver runs). */
   1325 static HMODULE g_dlsym_modules[256];
   1326 static DWORD g_dlsym_count;
   1327 static SRWLOCK g_dlsym_lock = SRWLOCK_INIT;
   1328 static int g_dlsym_inited;
   1329 
   1330 static void dlsym_init_once(void) {
   1331   HANDLE proc;
   1332   DWORD need = 0;
   1333   AcquireSRWLockExclusive(&g_dlsym_lock);
   1334   if (g_dlsym_inited) {
   1335     ReleaseSRWLockExclusive(&g_dlsym_lock);
   1336     return;
   1337   }
   1338   proc = GetCurrentProcess();
   1339   if (EnumProcessModules(proc, g_dlsym_modules, sizeof(g_dlsym_modules),
   1340                          &need)) {
   1341     DWORD have = need / (DWORD)sizeof(HMODULE);
   1342     g_dlsym_count = have > (DWORD)(sizeof(g_dlsym_modules) / sizeof(HMODULE))
   1343                         ? (DWORD)(sizeof(g_dlsym_modules) / sizeof(HMODULE))
   1344                         : have;
   1345   } else {
   1346     /* Fall back to the executable module and the CRT/kernel essentials. */
   1347     g_dlsym_modules[0] = GetModuleHandleW(NULL);
   1348     g_dlsym_modules[1] = GetModuleHandleW(L"kernel32.dll");
   1349     g_dlsym_modules[2] = GetModuleHandleW(L"msvcrt.dll");
   1350     g_dlsym_modules[3] = GetModuleHandleW(L"ucrtbase.dll");
   1351     g_dlsym_count = 4;
   1352   }
   1353   g_dlsym_inited = 1;
   1354   ReleaseSRWLockExclusive(&g_dlsym_lock);
   1355 }
   1356 
   1357 static void* win_dlsym(const char* name) {
   1358   DWORD i;
   1359   if (!name) return NULL;
   1360   dlsym_init_once();
   1361   /* Try the source-level name; if that misses and there's a leading '_'
   1362    * (Mach-O-mangled), try without it. */
   1363   AcquireSRWLockShared(&g_dlsym_lock);
   1364   for (i = 0; i < g_dlsym_count; ++i) {
   1365     void* p;
   1366     if (!g_dlsym_modules[i]) continue;
   1367     p = (void*)GetProcAddress(g_dlsym_modules[i], name);
   1368     if (p) {
   1369       ReleaseSRWLockShared(&g_dlsym_lock);
   1370       return p;
   1371     }
   1372   }
   1373   if (name[0] == '_' && name[1] != '\0') {
   1374     for (i = 0; i < g_dlsym_count; ++i) {
   1375       void* p;
   1376       if (!g_dlsym_modules[i]) continue;
   1377       p = (void*)GetProcAddress(g_dlsym_modules[i], name + 1);
   1378       if (p) {
   1379         ReleaseSRWLockShared(&g_dlsym_lock);
   1380         return p;
   1381       }
   1382     }
   1383   }
   1384   ReleaseSRWLockShared(&g_dlsym_lock);
   1385   return NULL;
   1386 }
   1387 
   1388 static int win_name_eq(KitSlice s, const char* lit, size_t litlen) {
   1389   return s.len == litlen && memcmp(s.s, lit, litlen) == 0;
   1390 }
   1391 
   1392 /* libucrt-static stdio entry points handed to JIT'd code.
   1393  *
   1394  * With kit's UCRT profile (__USE_MINGW_ANSI_STDIO=0), mingw's <stdio.h>
   1395  * resolves the printf/scanf family to out-of-line wrappers that live ONLY in
   1396  * libucrt.a — ucrtbase.dll exports just the __stdio_common_v* cores (and
   1397  * __local_stdio_* helpers the wrappers call). An AOT `kit cc` link pulls the
   1398  * wrappers from libucrt.a; the in-process JIT (`kit run`) resolves externs via
   1399  * dlsym over loaded DLLs (win_dlsym), which can't see them, so a printf-using
   1400  * JIT program fails with an undefined `printf`. kit.exe statically links
   1401  * libucrt.a, so taking each wrapper's address here pulls its definition into
   1402  * our own image and lets us hand the JIT our copy. Functions that ARE
   1403  * ucrtbase.dll exports (puts, fputs, fflush, putchar, __stdio_common_v*,
   1404  * __acrt_iob_func, …) resolve through win_dlsym above and never reach this
   1405  * table. The __local_stdio_*_options helpers' storage only ever holds the
   1406  * default (0) flags, so sharing one instance between the host and the JIT'd
   1407  * program is benign. */
   1408 void* driver_dlsym_resolver(void* user, KitSlice name_s) {
   1409   void* p;
   1410   (void)user;
   1411   if (!name_s.s || name_s.len == 0) return NULL;
   1412   p = win_dlsym(name_s.s);
   1413   if (p) return p;
   1414   /* Match by source name; #fn stringizes it and sizeof-1 is its length. */
   1415 #define KIT_STATIC_STDIO(fn) \
   1416   if (win_name_eq(name_s, #fn, sizeof(#fn) - 1u)) return (void*)(uintptr_t)&fn
   1417   KIT_STATIC_STDIO(__local_stdio_printf_options);
   1418   KIT_STATIC_STDIO(__local_stdio_scanf_options);
   1419   KIT_STATIC_STDIO(printf);
   1420   KIT_STATIC_STDIO(fprintf);
   1421   KIT_STATIC_STDIO(sprintf);
   1422   KIT_STATIC_STDIO(snprintf);
   1423   KIT_STATIC_STDIO(_snprintf);
   1424   KIT_STATIC_STDIO(vprintf);
   1425   KIT_STATIC_STDIO(vfprintf);
   1426   KIT_STATIC_STDIO(vsprintf);
   1427   KIT_STATIC_STDIO(vsnprintf);
   1428   KIT_STATIC_STDIO(_vsnprintf);
   1429   KIT_STATIC_STDIO(scanf);
   1430   KIT_STATIC_STDIO(fscanf);
   1431   KIT_STATIC_STDIO(sscanf);
   1432   KIT_STATIC_STDIO(_snscanf);
   1433   KIT_STATIC_STDIO(vscanf);
   1434   KIT_STATIC_STDIO(vfscanf);
   1435   KIT_STATIC_STDIO(vsscanf);
   1436   KIT_STATIC_STDIO(_scprintf);
   1437   KIT_STATIC_STDIO(_vscprintf);
   1438 #undef KIT_STATIC_STDIO
   1439   return NULL;
   1440 }
   1441 
   1442 /* ============================================================
   1443  *   SIGINT shim via SetConsoleCtrlHandler
   1444  * ============================================================ */
   1445 
   1446 static void (*g_ctrlc_cb)(void*);
   1447 static void* g_ctrlc_cb_user;
   1448 
   1449 static BOOL WINAPI ctrlc_trampoline(DWORD type) {
   1450   if (type == CTRL_C_EVENT || type == CTRL_BREAK_EVENT) {
   1451     if (g_ctrlc_cb) g_ctrlc_cb(g_ctrlc_cb_user);
   1452     return TRUE; /* handled */
   1453   }
   1454   return FALSE;
   1455 }
   1456 
   1457 int driver_install_sigint(void (*cb)(void*), void* user) {
   1458   g_ctrlc_cb = cb;
   1459   g_ctrlc_cb_user = user;
   1460   return SetConsoleCtrlHandler(ctrlc_trampoline, TRUE) ? 0 : 1;
   1461 }
   1462 
   1463 void driver_restore_sigint(void) {
   1464   SetConsoleCtrlHandler(ctrlc_trampoline, FALSE);
   1465   g_ctrlc_cb = NULL;
   1466   g_ctrlc_cb_user = NULL;
   1467 }
   1468 
   1469 /* No fault-guard on Windows yet (the POSIX path uses sigaction + sigsetjmp; a
   1470  * vectored-exception-handler port is a follow-up). Run the entry directly so
   1471  * `kit run` still executes the program; on_crash never fires. */
   1472 int driver_run_with_crash_guard(DriverEnv* env, KitArchKind arch,
   1473                                 DriverRunEntryFn entry, int argc, char** argv,
   1474                                 int* ret_out, DriverRunCrashFn on_crash,
   1475                                 void* user) {
   1476   (void)env;
   1477   (void)arch;
   1478   (void)on_crash;
   1479   (void)user;
   1480   *ret_out = entry(argc, argv);
   1481   return 0;
   1482 }
   1483 
   1484 /* ============================================================
   1485  *   Win32 CONTEXT <-> KitUnwindFrame marshalling
   1486  * ============================================================ */
   1487 
   1488 /* Inline per-arch marshalling: the Win32 dbg path is short enough that a
   1489  * separate uctx_*_windows.c isn't worth the extra TU. Only the host arch
   1490  * matters; cross-arch debug isn't a Win32 concern (the worker runs in our
   1491  * own process). */
   1492 
   1493 #if defined(_M_X64) || defined(__x86_64__)
   1494 static void ctx_to_frame(const CONTEXT* c, KitUnwindFrame* f) {
   1495   memset(f, 0, sizeof(*f));
   1496   f->pc = (uint64_t)c->Rip;
   1497   f->cfa = (uint64_t)c->Rsp;
   1498   /* SysV DWARF mapping: rax=0, rdx=1, rcx=2, rbx=3, rsi=4, rdi=5, rbp=6,
   1499    * rsp=7, r8..r15=8..15. We use the same mapping so kit's DWARF reader
   1500    * can interpret these directly. */
   1501   f->regs[0] = c->Rax;
   1502   f->regs[1] = c->Rdx;
   1503   f->regs[2] = c->Rcx;
   1504   f->regs[3] = c->Rbx;
   1505   f->regs[4] = c->Rsi;
   1506   f->regs[5] = c->Rdi;
   1507   f->regs[6] = c->Rbp;
   1508   f->regs[7] = c->Rsp;
   1509   f->regs[8] = c->R8;
   1510   f->regs[9] = c->R9;
   1511   f->regs[10] = c->R10;
   1512   f->regs[11] = c->R11;
   1513   f->regs[12] = c->R12;
   1514   f->regs[13] = c->R13;
   1515   f->regs[14] = c->R14;
   1516   f->regs[15] = c->R15;
   1517 }
   1518 
   1519 static void frame_to_ctx(const KitUnwindFrame* f, CONTEXT* c) {
   1520   c->Rip = f->pc;
   1521   c->Rax = f->regs[0];
   1522   c->Rdx = f->regs[1];
   1523   c->Rcx = f->regs[2];
   1524   c->Rbx = f->regs[3];
   1525   c->Rsi = f->regs[4];
   1526   c->Rdi = f->regs[5];
   1527   c->Rbp = f->regs[6];
   1528   c->Rsp = f->regs[7];
   1529   c->R8 = f->regs[8];
   1530   c->R9 = f->regs[9];
   1531   c->R10 = f->regs[10];
   1532   c->R11 = f->regs[11];
   1533   c->R12 = f->regs[12];
   1534   c->R13 = f->regs[13];
   1535   c->R14 = f->regs[14];
   1536   c->R15 = f->regs[15];
   1537 }
   1538 #elif defined(_M_ARM64) || defined(__aarch64__)
   1539 static void ctx_to_frame(const CONTEXT* c, KitUnwindFrame* f) {
   1540   unsigned i;
   1541   memset(f, 0, sizeof(*f));
   1542   f->pc = (uint64_t)c->Pc;
   1543   f->cfa = (uint64_t)c->Sp;
   1544   for (i = 0; i < 31; ++i) f->regs[i] = c->X[i];
   1545   f->regs[31] = c->Sp;
   1546 }
   1547 
   1548 static void frame_to_ctx(const KitUnwindFrame* f, CONTEXT* c) {
   1549   unsigned i;
   1550   c->Pc = f->pc;
   1551   for (i = 0; i < 31; ++i) c->X[i] = f->regs[i];
   1552   c->Sp = f->regs[31];
   1553 }
   1554 #else
   1555 static void ctx_to_frame(const CONTEXT* c, KitUnwindFrame* f) {
   1556   (void)c;
   1557   memset(f, 0, sizeof(*f));
   1558 }
   1559 static void frame_to_ctx(const KitUnwindFrame* f, CONTEXT* c) {
   1560   (void)f;
   1561   (void)c;
   1562 }
   1563 #endif
   1564 
   1565 /* Map a Win32 exception code to the POSIX-style signo the dbg session
   1566  * expects. Anything we don't recognize falls through as a generic SEGV. */
   1567 static int exception_code_to_signo(DWORD code) {
   1568   switch (code) {
   1569     case EXCEPTION_BREAKPOINT:
   1570     case EXCEPTION_SINGLE_STEP:
   1571       return 5; /* SIGTRAP */
   1572     case EXCEPTION_ILLEGAL_INSTRUCTION:
   1573     case EXCEPTION_PRIV_INSTRUCTION:
   1574       return 4; /* SIGILL */
   1575     case EXCEPTION_INT_DIVIDE_BY_ZERO:
   1576     case EXCEPTION_INT_OVERFLOW:
   1577     case EXCEPTION_FLT_DIVIDE_BY_ZERO:
   1578     case EXCEPTION_FLT_OVERFLOW:
   1579     case EXCEPTION_FLT_UNDERFLOW:
   1580     case EXCEPTION_FLT_INVALID_OPERATION:
   1581     case EXCEPTION_FLT_DENORMAL_OPERAND:
   1582     case EXCEPTION_FLT_INEXACT_RESULT:
   1583     case EXCEPTION_FLT_STACK_CHECK:
   1584       return 8; /* SIGFPE */
   1585     case EXCEPTION_DATATYPE_MISALIGNMENT:
   1586       return 7; /* SIGBUS */
   1587     case EXCEPTION_ACCESS_VIOLATION:
   1588     case EXCEPTION_IN_PAGE_ERROR:
   1589     case EXCEPTION_ARRAY_BOUNDS_EXCEEDED:
   1590     case EXCEPTION_STACK_OVERFLOW:
   1591     default:
   1592       return 11; /* SIGSEGV */
   1593   }
   1594 }
   1595 
   1596 /* ============================================================
   1597  *   dbg_os: threads, events, exceptions, guarded_copy
   1598  * ============================================================ */
   1599 
   1600 static KitDbgSignalOps g_dbg_ops;
   1601 static int g_dbg_ops_set;
   1602 static void* g_dbg_session;
   1603 static DWORD g_dbg_worker_tid;
   1604 static int g_dbg_worker_tid_valid;
   1605 static PVOID g_veh_cookie;
   1606 
   1607 /* Guarded-copy landing for the VEH path. Both __try/__except and a VEH
   1608  * fallback set g_guard_armed; if the VEH fires while it's set the
   1609  * exception is "translated" into a longjmp via the VEH's ability to
   1610  * rewrite the resume context. We use __try/__except for the primary
   1611  * path and treat the VEH armed-check as a backstop. */
   1612 static __declspec(thread) int g_guard_armed;
   1613 static __declspec(thread) jmp_buf g_guard_buf;
   1614 
   1615 static int win_dbg_caller_is_worker(void) {
   1616   return g_dbg_worker_tid_valid && GetCurrentThreadId() == g_dbg_worker_tid;
   1617 }
   1618 
   1619 /* --- thread shim --- */
   1620 
   1621 typedef struct DbgWinThread {
   1622   HANDLE handle;
   1623   DWORD tid;
   1624   void (*fn)(void*);
   1625   void* arg;
   1626 } DbgWinThread;
   1627 
   1628 static unsigned __stdcall dbg_thread_trampoline(void* p) {
   1629   DbgWinThread* t = (DbgWinThread*)p;
   1630   t->fn(t->arg);
   1631   return 0;
   1632 }
   1633 
   1634 static KitStatus dbg_thread_start_win(void* user, void (*fn)(void*), void* arg,
   1635                                       void** thread_out) {
   1636   DbgWinThread* t;
   1637   uintptr_t h;
   1638   (void)user;
   1639   t = (DbgWinThread*)malloc(sizeof(*t));
   1640   if (!t) return KIT_NOMEM;
   1641   t->fn = fn;
   1642   t->arg = arg;
   1643   /* _beginthreadex sets up the CRT TLS for the new thread; pure CreateThread
   1644    * leaves the CRT in an undefined state for code that calls printf/malloc. */
   1645   h = _beginthreadex(NULL, 0, dbg_thread_trampoline, t, 0, (unsigned*)&t->tid);
   1646   if (h == 0) {
   1647     free(t);
   1648     return KIT_ERR;
   1649   }
   1650   t->handle = (HANDLE)h;
   1651   g_dbg_worker_tid = t->tid;
   1652   g_dbg_worker_tid_valid = 1;
   1653   *thread_out = t;
   1654   return KIT_OK;
   1655 }
   1656 
   1657 static void dbg_thread_join_win(void* user, void* thread) {
   1658   DbgWinThread* t = (DbgWinThread*)thread;
   1659   (void)user;
   1660   if (!t) return;
   1661   WaitForSingleObject(t->handle, INFINITE);
   1662   CloseHandle(t->handle);
   1663   g_dbg_worker_tid_valid = 0;
   1664   free(t);
   1665 }
   1666 
   1667 /* Interrupt the worker by suspending it, snapshotting its CONTEXT,
   1668  * calling on_fault on the caller thread (the REPL), and writing back any
   1669  * register edits. This differs from POSIX (where the signal handler runs
   1670  * the on_fault on the worker), but the observable contract -- "the worker
   1671  * is stopped while on_fault runs and resumes with the edited frame" --
   1672  * matches. Locking quirks of SuspendThread aren't a concern here: the
   1673  * worker is, by construction, running JIT'd user code that holds no host
   1674  * locks. */
   1675 static KitStatus dbg_thread_interrupt_win(void* user, void* thread) {
   1676   DbgWinThread* t = (DbgWinThread*)thread;
   1677   CONTEXT ctx;
   1678   KitUnwindFrame frame;
   1679   KitStatus rc;
   1680   (void)user;
   1681   if (!t || !g_dbg_ops_set || !g_dbg_ops.on_fault) return KIT_INVALID;
   1682   if (SuspendThread(t->handle) == (DWORD)-1) return KIT_ERR;
   1683   memset(&ctx, 0, sizeof(ctx));
   1684   ctx.ContextFlags = CONTEXT_FULL;
   1685   if (!GetThreadContext(t->handle, &ctx)) {
   1686     ResumeThread(t->handle);
   1687     return KIT_ERR;
   1688   }
   1689   ctx_to_frame(&ctx, &frame);
   1690   rc = g_dbg_ops.on_fault(g_dbg_session, DBG_WIN_INTERRUPT_SIGNO, &frame);
   1691   if (rc == KIT_OK) {
   1692     frame_to_ctx(&frame, &ctx);
   1693     SetThreadContext(t->handle, &ctx);
   1694   }
   1695   ResumeThread(t->handle);
   1696   return rc;
   1697 }
   1698 
   1699 /* --- event shim --- */
   1700 
   1701 static KitStatus dbg_event_new_win(void* user, void** event_out) {
   1702   HANDLE h;
   1703   (void)user;
   1704   /* Manual-reset so a signaler that races ahead of the waiter doesn't lose
   1705    * the wake-up; the dbg core explicitly resets via event_reset. */
   1706   h = CreateEventW(NULL, TRUE, FALSE, NULL);
   1707   if (!h) return KIT_ERR;
   1708   *event_out = (void*)h;
   1709   return KIT_OK;
   1710 }
   1711 
   1712 static void dbg_event_free_win(void* user, void* ev) {
   1713   (void)user;
   1714   if (ev) CloseHandle((HANDLE)ev);
   1715 }
   1716 
   1717 static KitStatus dbg_event_wait_win(void* user, void* ev) {
   1718   (void)user;
   1719   if (WaitForSingleObject((HANDLE)ev, INFINITE) != WAIT_OBJECT_0)
   1720     return KIT_ERR;
   1721   /* Auto-reset semantics expected by dbg_event_wait on POSIX (it clears
   1722    * the flag after consuming). Mirror that here so the contract holds. */
   1723   ResetEvent((HANDLE)ev);
   1724   return KIT_OK;
   1725 }
   1726 
   1727 static KitStatus dbg_event_signal_win(void* user, void* ev) {
   1728   (void)user;
   1729   return SetEvent((HANDLE)ev) ? KIT_OK : KIT_ERR;
   1730 }
   1731 
   1732 static KitStatus dbg_event_reset_win(void* user, void* ev) {
   1733   (void)user;
   1734   return ResetEvent((HANDLE)ev) ? KIT_OK : KIT_ERR;
   1735 }
   1736 
   1737 /* --- vectored exception handler --- */
   1738 
   1739 /* Recovery thunk: when guarded_copy's __try/__except is unavailable (or
   1740  * for natural faults raised inside a guarded region) we hand control back
   1741  * to the longjmp target by rewriting the resume context. */
   1742 static LONG WINAPI dbg_veh(EXCEPTION_POINTERS* ep) {
   1743   DWORD code = ep->ExceptionRecord->ExceptionCode;
   1744   KitUnwindFrame frame;
   1745   KitStatus rc;
   1746   int signo;
   1747 
   1748   /* Filter out C++-style exceptions / debug strings we don't care about. */
   1749   if (code == 0x406D1388u /* MS_VC_EXCEPTION (SetThreadName) */ ||
   1750       code == 0xE06D7363u /* CXX EH */ || code == DBG_PRINTEXCEPTION_C ||
   1751       code == DBG_PRINTEXCEPTION_WIDE_C)
   1752     return EXCEPTION_CONTINUE_SEARCH;
   1753 
   1754   /* SEGV/BUS during a guarded_copy: bail out without involving the
   1755    * session. The VEH backstop only fires if SEH/__try wasn't compiled in
   1756    * for the function that armed the guard. */
   1757   if (g_guard_armed &&
   1758       (code == EXCEPTION_ACCESS_VIOLATION || code == EXCEPTION_IN_PAGE_ERROR ||
   1759        code == EXCEPTION_DATATYPE_MISALIGNMENT)) {
   1760     g_guard_armed = 0;
   1761     longjmp(g_guard_buf, 1);
   1762     /* not reached */
   1763     return EXCEPTION_CONTINUE_EXECUTION;
   1764   }
   1765 
   1766   if (!win_dbg_caller_is_worker() || !g_dbg_ops_set || !g_dbg_ops.on_fault)
   1767     return EXCEPTION_CONTINUE_SEARCH;
   1768 
   1769   signo = exception_code_to_signo(code);
   1770   ctx_to_frame(ep->ContextRecord, &frame);
   1771   rc = g_dbg_ops.on_fault(g_dbg_session, signo, &frame);
   1772   if (rc != KIT_OK) return EXCEPTION_CONTINUE_SEARCH;
   1773   frame_to_ctx(&frame, ep->ContextRecord);
   1774   return EXCEPTION_CONTINUE_EXECUTION;
   1775 }
   1776 
   1777 static KitStatus dbg_signals_install_win(void* user, const KitDbgSignalOps* ops,
   1778                                          void* session) {
   1779   (void)user;
   1780   if (g_veh_cookie) return KIT_ERR;
   1781   g_dbg_ops = *ops;
   1782   g_dbg_ops_set = 1;
   1783   g_dbg_session = session;
   1784   /* First==1 ensures we run before any other VEH and before the system's
   1785    * default last-chance handler. */
   1786   g_veh_cookie = AddVectoredExceptionHandler(1, dbg_veh);
   1787   if (!g_veh_cookie) {
   1788     memset(&g_dbg_ops, 0, sizeof(g_dbg_ops));
   1789     g_dbg_ops_set = 0;
   1790     g_dbg_session = NULL;
   1791     return KIT_ERR;
   1792   }
   1793   return KIT_OK;
   1794 }
   1795 
   1796 static void dbg_signals_uninstall_win(void* user) {
   1797   (void)user;
   1798   if (g_veh_cookie) {
   1799     RemoveVectoredExceptionHandler(g_veh_cookie);
   1800     g_veh_cookie = NULL;
   1801   }
   1802   memset(&g_dbg_ops, 0, sizeof(g_dbg_ops));
   1803   g_dbg_ops_set = 0;
   1804   g_dbg_session = NULL;
   1805 }
   1806 
   1807 /* --- W^X transitions --- */
   1808 
   1809 static size_t page_floor(size_t v, size_t pg) { return v & ~(pg - 1); }
   1810 static size_t page_ceil(size_t v, size_t pg) {
   1811   return (v + pg - 1) & ~(pg - 1);
   1812 }
   1813 
   1814 static KitStatus dbg_code_write_begin_win(void* user, void* runtime_addr,
   1815                                           size_t n, void** write_out) {
   1816   size_t pg;
   1817   uintptr_t a;
   1818   uintptr_t base;
   1819   size_t span;
   1820   DWORD old;
   1821   (void)user;
   1822   if (!runtime_addr || !n || !write_out) return KIT_INVALID;
   1823   /* Dual-mapped reservation: write through the alias, no protect flip. */
   1824   if (exec_dual_lookup_w(runtime_addr, n, write_out) == 0) return KIT_OK;
   1825   /* Single-mapping fallback: transient PAGE_EXECUTE_READWRITE. */
   1826   pg = driver_host_page_size_win();
   1827   a = (uintptr_t)runtime_addr;
   1828   base = page_floor(a, pg);
   1829   span = page_ceil((a - base) + n, pg);
   1830   if (!VirtualProtect((void*)base, span, PAGE_EXECUTE_READWRITE, &old))
   1831     return KIT_ERR;
   1832   *write_out = runtime_addr;
   1833   return KIT_OK;
   1834 }
   1835 
   1836 static void dbg_code_write_end_win(void* user, void* runtime_addr, size_t n) {
   1837   void* w;
   1838   size_t pg;
   1839   uintptr_t a;
   1840   uintptr_t base;
   1841   size_t span;
   1842   DWORD old;
   1843   (void)user;
   1844   if (exec_dual_lookup_w(runtime_addr, n, &w) == 0)
   1845     return; /* nothing to flip */
   1846   pg = driver_host_page_size_win();
   1847   a = (uintptr_t)runtime_addr;
   1848   base = page_floor(a, pg);
   1849   span = page_ceil((a - base) + n, pg);
   1850   VirtualProtect((void*)base, span, PAGE_EXECUTE_READ, &old);
   1851 }
   1852 
   1853 static void dbg_flush_icache_win(void* user, void* runtime_addr, size_t n) {
   1854   (void)user;
   1855   FlushInstructionCache(GetCurrentProcess(), runtime_addr, n);
   1856 }
   1857 
   1858 /* --- guarded copy via SEH __try/__except --- */
   1859 
   1860 static KitStatus dbg_guarded_copy_win(void* user, void* dst, const void* src,
   1861                                       size_t n) {
   1862   (void)user;
   1863   /* MinGW-w64 supports __try/__except natively for the SEH model on x86_64
   1864    * and arm64 (GCC: -fseh-exceptions; clang: built-in). The VEH backstop
   1865    * above catches it if the toolchain ever falls back to setjmp-based EH. */
   1866   if (setjmp(g_guard_buf) != 0) {
   1867     g_guard_armed = 0;
   1868     return KIT_ERR;
   1869   }
   1870   g_guard_armed = 1;
   1871 #if defined(__kit__)
   1872   /* kit's C frontend implements no SEH (__try/__except). Rely on the VEH
   1873    * backstop (dbg_veh) installed by dbg_signals_install_win: it sees the
   1874    * thread-local g_guard_armed and longjmps back through g_guard_buf on an
   1875    * access fault, which is the exact fallback the comment above anticipates. */
   1876   memcpy(dst, src, n);
   1877   g_guard_armed = 0;
   1878   return KIT_OK;
   1879 #else
   1880   __try {
   1881     memcpy(dst, src, n);
   1882     g_guard_armed = 0;
   1883     return KIT_OK;
   1884   } __except (EXCEPTION_EXECUTE_HANDLER) {
   1885     g_guard_armed = 0;
   1886     return KIT_ERR;
   1887   }
   1888 #endif
   1889 }
   1890 
   1891 /* --- call_with_catch / thread_abort (longjmp-based) --- */
   1892 
   1893 static __declspec(thread) jmp_buf g_dbg_abort_buf;
   1894 
   1895 static int dbg_call_with_catch_win(void* user, void (*fn)(void*), void* arg) {
   1896   (void)user;
   1897   if (setjmp(g_dbg_abort_buf) == 0) {
   1898     fn(arg);
   1899     return 0;
   1900   }
   1901   return 1;
   1902 }
   1903 
   1904 static void dbg_thread_abort_win(void* user) {
   1905   (void)user;
   1906   longjmp(g_dbg_abort_buf, 1);
   1907 }
   1908 
   1909 static KitDbgOs g_dbg_os_win = {
   1910     .thread_start = dbg_thread_start_win,
   1911     .thread_join = dbg_thread_join_win,
   1912     .thread_interrupt = dbg_thread_interrupt_win,
   1913     .event_new = dbg_event_new_win,
   1914     .event_free = dbg_event_free_win,
   1915     .event_wait = dbg_event_wait_win,
   1916     .event_signal = dbg_event_signal_win,
   1917     .event_reset = dbg_event_reset_win,
   1918     .signals_install = dbg_signals_install_win,
   1919     .signals_uninstall = dbg_signals_uninstall_win,
   1920     .interrupt_signo = DBG_WIN_INTERRUPT_SIGNO,
   1921     .trap_signo = 5,
   1922     .code_write_begin = dbg_code_write_begin_win,
   1923     .code_write_end = dbg_code_write_end_win,
   1924     .flush_icache = dbg_flush_icache_win,
   1925     .guarded_copy = dbg_guarded_copy_win,
   1926     .call_with_catch = dbg_call_with_catch_win,
   1927     .thread_abort = dbg_thread_abort_win,
   1928     .user = NULL,
   1929 };
   1930 
   1931 /* JIT thread-local access resolves to in-image storage (single-threaded JIT:
   1932  * the in-image TLS data is the single instance — see src/link/link_jit.c), so
   1933  * no host-provided per-thread TLS vtable is needed. */
   1934 
   1935 /* ============================================================
   1936  *   host target
   1937  * ============================================================ */
   1938 
   1939 static KitArchKind host_arch_self_win(void) {
   1940 #if defined(_M_X64) || defined(__x86_64__)
   1941   return KIT_ARCH_X86_64;
   1942 #elif defined(_M_ARM64) || defined(__aarch64__)
   1943   return KIT_ARCH_ARM_64;
   1944 #elif defined(_M_IX86) || defined(__i386__)
   1945   return KIT_ARCH_X86_32;
   1946 #elif defined(_M_ARM) || defined(__arm__)
   1947   return KIT_ARCH_ARM_32;
   1948 #else
   1949   return KIT_ARCH_X86_64;
   1950 #endif
   1951 }
   1952 
   1953 KitTargetSpec driver_host_target(void) {
   1954   KitTargetSpec t;
   1955   t.arch = host_arch_self_win();
   1956   t.os = KIT_OS_WINDOWS;
   1957   t.obj = KIT_OBJ_COFF;
   1958   t.ptr_size = (uint8_t)sizeof(void*);
   1959   t.ptr_align = (uint8_t)sizeof(void*);
   1960   t.big_endian = 0;
   1961   t.pic = driver_default_pic(t.obj, t.os);
   1962   t.code_model = KIT_CM_DEFAULT;
   1963   return t;
   1964 }
   1965 
   1966 /* No host probe on Windows: the MinGW sysroot is supplied via --sysroot or the
   1967  * KIT_SYSROOT env var (handled in the cc driver), not auto-discovered from a
   1968  * fixed install path. */
   1969 int driver_default_hosted_dirs(DriverEnv* env, KitTargetSpec target,
   1970                                DriverHostedDirs* out) {
   1971   (void)env;
   1972   (void)target;
   1973   (void)out;
   1974   return 1;
   1975 }
   1976 
   1977 /* ============================================================
   1978  *   env wiring
   1979  * ============================================================ */
   1980 
   1981 static char g_cache_dir_win[4096];
   1982 
   1983 void driver_env_init(DriverEnv* e) {
   1984   e->heap = &g_heap_libc;
   1985   e->diag = &g_diag_stderr;
   1986   e->file_io.read_all = win_read_all;
   1987   e->file_io.release = win_release;
   1988   e->file_io.open_writer = win_open_writer;
   1989   e->file_io.user = e;
   1990 
   1991   g_execmem_win.page_size = driver_host_page_size_win();
   1992   g_execmem_win.reserve = execmem_reserve_win;
   1993   g_execmem_win.protect = execmem_protect_win;
   1994   g_execmem_win.release = execmem_release_win;
   1995   g_execmem_win.flush_icache = execmem_flush_icache_win;
   1996   g_execmem_win.user = NULL;
   1997   e->execmem = &g_execmem_win;
   1998 
   1999   e->dbg_os = &g_dbg_os_win;
   2000   e->metrics = NULL;
   2001 
   2002   {
   2003     /* XDG_CACHE_HOME wins if set (cross-platform tooling convention),
   2004      * otherwise fall back to %LOCALAPPDATA%\\kit, otherwise a
   2005      * build-tree-local path. */
   2006     const char* xdg = getenv("XDG_CACHE_HOME");
   2007     const char* lap = getenv("LOCALAPPDATA");
   2008     if (xdg && *xdg) {
   2009       snprintf(g_cache_dir_win, sizeof(g_cache_dir_win), "%s/kit", xdg);
   2010     } else if (lap && *lap) {
   2011       snprintf(g_cache_dir_win, sizeof(g_cache_dir_win), "%s\\kit", lap);
   2012     } else {
   2013       snprintf(g_cache_dir_win, sizeof(g_cache_dir_win), "build\\kit-cache");
   2014     }
   2015     g_cache_dir_win[sizeof(g_cache_dir_win) - 1] = '\0';
   2016     e->cache_dir = g_cache_dir_win;
   2017   }
   2018 
   2019   {
   2020     const char* sde = getenv("SOURCE_DATE_EPOCH");
   2021     if (sde && *sde) {
   2022       char* endp = NULL;
   2023       long long v = strtoll(sde, &endp, 10);
   2024       e->now = (endp != sde && v >= 0) ? (int64_t)v : (int64_t)-1;
   2025     } else {
   2026       time_t t = time(NULL);
   2027       e->now = (t == (time_t)-1) ? (int64_t)-1 : (int64_t)t;
   2028     }
   2029   }
   2030 }
   2031 
   2032 void driver_env_fini(DriverEnv* e) { (void)e; }
   2033 
   2034 KitContext driver_env_to_context(const DriverEnv* e) {
   2035   KitContext c;
   2036   c.heap = e->heap;
   2037   c.file_io = &e->file_io;
   2038   c.diag = e->diag;
   2039   c.metrics = e->metrics;
   2040   c.now = e->now;
   2041   return c;
   2042 }
   2043 
   2044 KitJitHost driver_env_to_jit_host(const DriverEnv* e) {
   2045   KitJitHost h;
   2046   h.execmem = e->execmem;
   2047   return h;
   2048 }
   2049 
   2050 KitDbgHost driver_env_to_dbg_host(const DriverEnv* e) {
   2051   KitDbgHost h;
   2052   h.os = e->dbg_os;
   2053   return h;
   2054 }