From 53c2750cf1f28e2650b1bc03ac5579bfb259c1b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=98=84=EC=84=B1?= Date: Tue, 7 Jul 2026 21:00:59 +0900 Subject: [PATCH] fix(browse): add random suffix to atomic-write temp filenames writeSecureFileAtomic embedded only the pid in the staging filename, so two writes from the same process racing the same destination (the periodic checkpoint firing while a debounced checkpoint runs, both serialized but sharing a pid) could collide on the temp path and clobber each other's in-flight staging file before rename. Append 4 random bytes so every write gets a unique staging path. The destination still needs its own lock; this only guarantees each individual write is atomic. Co-Authored-By: Claude Opus 4.8 --- browse/src/file-permissions.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/browse/src/file-permissions.ts b/browse/src/file-permissions.ts index e6e25691d..b7dc08bb5 100644 --- a/browse/src/file-permissions.ts +++ b/browse/src/file-permissions.ts @@ -37,6 +37,7 @@ */ import { execFileSync } from 'child_process'; +import { randomBytes } from 'crypto'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; @@ -139,16 +140,16 @@ export function writeSecureFile( * * Same-directory is load-bearing: a temp file on another filesystem would make * `rename` fall back to copy+unlink, which is NOT atomic. The temp name embeds - * the pid so two daemons racing the same destination don't clobber each other's - * temp file before their own rename (the destination itself still needs a lock - * — this only guarantees each write is individually atomic). + * the pid plus random bytes so racing writes never share a staging path (the + * destination itself still needs a lock — this only guarantees each write is + * individually atomic). */ export function writeSecureFileAtomic( filePath: string, data: string | NodeJS.ArrayBufferView, ): void { const dir = path.dirname(filePath); - const tmp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.tmp`); + const tmp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`); let fd: number | null = null; try { fd = fs.openSync(tmp, 'w', 0o600);