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:
| M | driver/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;
}