From 28f44b1736b0aae252ff2366ab6cdcfffa58d626 Mon Sep 17 00:00:00 2001 From: zeroaltitude Date: Mon, 9 Mar 2026 23:56:33 -0700 Subject: [PATCH] fix: add TOCTOU ownership check to post-hook content overwrite path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sessionSaveContent replacement path now verifies the file still contains our writtenEntry before overwriting — same guard as the late-block retraction path. Prevents concurrent runs with the same filename from clobbering each other's valid saves. --- src/hooks/bundled/session-memory/handler.ts | 28 +++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/hooks/bundled/session-memory/handler.ts b/src/hooks/bundled/session-memory/handler.ts index b4b21a15df7..60043293e84 100644 --- a/src/hooks/bundled/session-memory/handler.ts +++ b/src/hooks/bundled/session-memory/handler.ts @@ -525,6 +525,34 @@ const saveSessionToMemory: HookHandler = async (event) => { // is null because blockPreSet was true) OR if the content changed. (writtenEntry === null || postContent !== writtenEntry) ) { + // Verify ownership before overwriting — if another concurrent run wrote + // to the same file since our inline write, do not clobber their content. + // Same TOCTOU guard as the late-block retraction path. + if (writtenEntry !== null) { + let currentContent: string | null = null; + try { + currentContent = await fs.readFile(memoryFilePath, "utf-8"); + } catch (err: unknown) { + if ( + err instanceof Error && + "code" in err && + (err as NodeJS.ErrnoException).code === "ENOENT" + ) { + // File was externally deleted — safe to recreate with new content. + currentContent = null; + } else { + throw err; + } + } + if (currentContent !== null && currentContent !== writtenEntry) { + log.warn( + "Session save content replacement skipped — file was modified by another " + + "session since our inline write (concurrent save detected)", + ); + return; + } + } + // Ensure memoryDir exists — the inline write may have been // skipped (e.g. blockSessionSave was true initially) so mkdir // might never have run.