kit

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

commit 25d4039b4512c3bcc2ba654f684d7e3eb4e56519
parent 7a56910715c61ee826a4c8edeeeaa24ad90ba9bd
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Fri, 12 Jun 2026 09:43:28 -0700

fix(driver): write compiler/linker output atomically via temp + rename

posix_open_writer opened the target in place (O_TRUNC), keeping its inode. On
macOS the kernel caches its code-signature verdict per vnode, so rebuilding a
binary in place could serve a stale verdict for the new bytes (and a partial
write on failure clobbered the previous output). Write to a sibling mkstemp temp
and rename it over the target on a clean close, so every build lands a fresh
inode; fsync before the rename, unlink the temp on failure, and fall back to an
in-place open for special files (e.g. -o /dev/null).

Diffstat:
Mdriver/env/posix.c | 104++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 100 insertions(+), 4 deletions(-)

diff --git a/driver/env/posix.c b/driver/env/posix.c @@ -166,8 +166,16 @@ typedef struct DriverFdWriter { KitHeap* heap; int fd; KitStatus status; - uint64_t pos; /* logical position of the next byte (incl. buffered) */ - size_t buf_len; /* bytes buffered but not yet written to fd */ + uint64_t pos; /* logical position of the next byte (incl. buffered) */ + size_t buf_len; /* bytes buffered but not yet written to fd */ + /* Atomic output: when set, bytes go to tmp_path and a successful close + * renames it over final_path, so every build lands a *fresh inode*. This + * avoids clobbering a still-referenced file and — crucially on macOS — + * sidesteps the kernel's per-vnode code-signature cache, which otherwise + * serves a stale "invalid signature" verdict for a re-signed executable + * rewritten in place. NULL for plain fd writers (stdout, fallback). */ + char* tmp_path; + char* final_path; unsigned char buf[FDW_BUF_CAP]; } DriverFdWriter; @@ -233,8 +241,27 @@ static KitStatus fdw_status(KitWriter* w) { static void fdw_close(KitWriter* w) { DriverFdWriter* fw = (DriverFdWriter*)w; + KitStatus st; fdw_flush(fw); - if (fw->fd >= 0) close(fw->fd); + st = fw->status; + if (fw->fd >= 0) { + /* fsync the data before the rename so a crash can't leave the final + * path pointing at a renamed-but-unflushed (truncated) file. */ + if (st == KIT_OK && fw->tmp_path && fsync(fw->fd) != 0) st = KIT_IO; + if (close(fw->fd) != 0 && st == KIT_OK) st = KIT_IO; + } + if (fw->tmp_path) { + if (st == KIT_OK && rename(fw->tmp_path, fw->final_path) != 0) { + st = KIT_IO; + fw->status = KIT_IO; + } + /* On any failure the temp never became the target: remove it so a failed + * build leaves the previous output (if any) untouched and no litter. */ + if (st != KIT_OK) unlink(fw->tmp_path); + fw->heap->free(fw->heap, fw->tmp_path, strlen(fw->tmp_path) + 1u); + if (fw->final_path) + fw->heap->free(fw->heap, fw->final_path, strlen(fw->final_path) + 1u); + } fw->heap->free(fw->heap, fw, sizeof(*fw)); } @@ -252,9 +279,19 @@ static KitWriter* driver_writer_fd(KitHeap* h, int fd) { fw->status = KIT_OK; fw->pos = 0; fw->buf_len = 0; + fw->tmp_path = NULL; + fw->final_path = NULL; return &fw->base; } +/* Duplicate a NUL-terminated string into the heap (NULL on OOM). */ +static char* posix_strdup(KitHeap* h, const char* s) { + size_t n = strlen(s) + 1u; + char* p = (char*)h->alloc(h, n, 1); + if (p) memcpy(p, s, n); + return p; +} + /* Stdout writer routes through stdio so it shares libc's buffer with * driver_printf. */ typedef struct DriverStdioWriter { @@ -369,17 +406,76 @@ static void posix_release(void* user, KitFileData* d) { d->token = NULL; } +/* Open `path` for output, atomically: write to a sibling temp file and rename + * it over `path` on a clean close (see DriverFdWriter.tmp_path). The temp lives + * in `path`'s own directory so the rename stays within one filesystem (a + * cross-device rename would fail). Falls back to a plain in-place open when a + * temp can't be made — e.g. the target is a device/FIFO like /dev/null, or the + * directory rejects the temp create — preserving the old behavior there. */ static KitStatus posix_open_writer(void* user, const char* path, KitWriter** out) { DriverEnv* env = (DriverEnv*)user; - int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); KitWriter* w; + DriverFdWriter* fw; + char tmpl[4096]; + const char* slash; + size_t dlen; + int fd; + int via_temp = 0; + struct stat sb; + + /* Only regular files (or not-yet-existing targets) get the temp+rename + * treatment; special files must be written in place. */ + int special = (stat(path, &sb) == 0 && !S_ISREG(sb.st_mode)); + + slash = strrchr(path, '/'); + dlen = slash ? (size_t)(slash - path) : 0u; /* dir part, "" => cwd */ + /* "<dir>/.kit-tmp-XXXXXX" (or ".kit-tmp-XXXXXX" in cwd). */ + if (!special && dlen + sizeof(".kit-tmp-XXXXXX") + 1u <= sizeof(tmpl)) { + if (dlen) { + memcpy(tmpl, path, dlen); + tmpl[dlen] = '/'; + memcpy(tmpl + dlen + 1u, ".kit-tmp-XXXXXX", sizeof(".kit-tmp-XXXXXX")); + } else { + memcpy(tmpl, ".kit-tmp-XXXXXX", sizeof(".kit-tmp-XXXXXX")); + } + fd = mkstemp(tmpl); + if (fd >= 0) { + /* mkstemp creates 0600; match the in-place 0644 (the +x bit, when an + * executable, is applied to the final path after close). */ + (void)fchmod(fd, 0644); + via_temp = 1; + } else { + fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + } + } else { + fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + } if (fd < 0) return KIT_IO; + w = driver_writer_fd(env->heap, fd); if (!w) { close(fd); + if (via_temp) unlink(tmpl); return KIT_NOMEM; } + if (via_temp) { + fw = (DriverFdWriter*)w; + fw->tmp_path = posix_strdup(env->heap, tmpl); + fw->final_path = posix_strdup(env->heap, path); + if (!fw->tmp_path || !fw->final_path) { + /* Without both paths we can't rename; degrade to leaving the temp in + * place would be wrong, so fail cleanly. */ + if (fw->tmp_path) env->heap->free(env->heap, fw->tmp_path, strlen(tmpl) + 1u); + if (fw->final_path) + env->heap->free(env->heap, fw->final_path, strlen(path) + 1u); + fw->tmp_path = NULL; + fw->final_path = NULL; + fdw_close(w); + unlink(tmpl); + return KIT_NOMEM; + } + } *out = w; return KIT_OK; }