diff --git a/packages/adapter-utils/src/execution-target-stdin-race.test.ts b/packages/adapter-utils/src/execution-target-stdin-race.test.ts index 4b5dfdc027..9341358c38 100644 --- a/packages/adapter-utils/src/execution-target-stdin-race.test.ts +++ b/packages/adapter-utils/src/execution-target-stdin-race.test.ts @@ -1796,7 +1796,7 @@ describe("deterministic remote process-session wrapper shutdown (PAP-5316)", () await waitFor(async () => (await findLivePidsByArgvSubstring(wrapperScriptSubstring)).length === 0, 8_000); }, 15_000); - // ---- PAP-5338: fail closed on an unusable creation time, and on every + // ---- PAP-5338: reject a change-time creation-time substitute, and every // lstat error during verification ------------------------------------- async function waitForTrackedChildPid(pidFile: string): Promise { @@ -1822,18 +1822,17 @@ describe("deterministic remote process-session wrapper shutdown (PAP-5316)", () // ever starts, often within a few milliseconds of the child's own spawn() // call returning. A freshly spawned Node.js child needs real wall-clock // time just to boot before it can run its own code, so it can lose the - // race to write a pid file before terminate()'s SIGTERM reaches it. This - // is the correct, intended shape of a fail-fast capture: the child never - // gets a chance to become a live orphan. So these two tests prove death - // through the OS process table by the child's own script path (the same - // technique T15 above uses for the wrapper itself), which needs no - // cooperation from code inside the child. + // race to write a pid file before terminate()'s SIGTERM reaches it. The + // change-time fallback test below therefore proves death through the OS + // process table by the child's own script path (the same technique T15 + // above uses for the wrapper itself), which needs no cooperation from code + // inside the child. async function expectNoLiveProcessByArgvSubstring(substring: string): Promise { await waitFor(async () => (await findLivePidsByArgvSubstring(substring)).length === 0, 8_000); expect(await findLivePidsByArgvSubstring(substring)).toEqual([]); } - it("T16 fails closed at capture when the reported creation time is zero, so no orphan wrapper or child ever starts polling", async () => { + it("T16 accepts a zero creation time when the filesystem does not report birth time", async () => { const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-birthtime-zero-")); cleanupDirs.push(rootDir); const pidFile = path.join(rootDir, "t16-child.pid"); @@ -1847,14 +1846,21 @@ describe("deterministic remote process-session wrapper shutdown (PAP-5316)", () fakeBirthtime: { target: "sessionDir", mode: "zero" }, }); + const pid = await waitForTrackedChildPid(pidFile); + expect(isPidAlive(pid)).toBe(true); + await writeFile( + path.join(wrapper.stdinDir, "000000000001.json"), + `${JSON.stringify({ type: "stdinEnd" })}\n`, + "utf8", + ); await Promise.race([ wrapper.exited, delay(8_000).then(() => { throw new Error("The wrapper process did not exit."); }), ]); - expect(wrapper.stderrText()).toMatch(/not usable/); - await expectNoLiveProcessByArgvSubstring(childPath); + expect(wrapper.exitInfo().code).toBe(0); + expect(wrapper.stderrText()).not.toMatch(/not usable/); }, 15_000); it("T17 fails closed at capture when the reported creation time follows the change time, so a change-time copy never passes as a real creation time", async () => { diff --git a/packages/adapter-utils/src/execution-target.ts b/packages/adapter-utils/src/execution-target.ts index 2a083f8a01..c11463841a 100644 --- a/packages/adapter-utils/src/execution-target.ts +++ b/packages/adapter-utils/src/execution-target.ts @@ -2461,10 +2461,6 @@ async function latchAndTerminate() { await terminate(); } -function isUsableBirthtimeMs(value) { - return typeof value === "number" && Number.isFinite(value) && value !== 0; -} - let probeSeq = 0; // A probe file name that pollStdin() can never read as a stdin message: it @@ -2593,14 +2589,10 @@ async function refuseUnusableCreationTime(label, dirPath, reason) { // it terminates now instead of polling a control path it never verified. // // This wrapper cannot assume stats.birthtimeMs is a real creation time. Node -// reports it in one of two unusable shapes on a filesystem or kernel that -// cannot supply one: 0 (the Linux statx() path when the filesystem reports -// no STATX_BTIME), or a copy of the change time (the generic POSIX stat() -// path on a platform with no birthtime field). A 0 value fails open, so this -// wrapper rejects it outright. A change-time copy fails closed but far too -// aggressively (it would move on every stdin file this wrapper deletes), so -// this wrapper proves the value is not a copy with a probe before it trusts -// it, run once here, before either directory's identity is captured. +// can report a change-time copy as a creation time. That value fails closed +// far too aggressively (it would move on every stdin file this wrapper +// deletes), so this wrapper proves the value is not a copy with a probe before +// it trusts it, run once here, before either directory's identity is captured. async function captureSessionIdentity() { try { const sessionProbeFailure = await birthtimeSurvivesProbe(sessionDir); @@ -2615,14 +2607,6 @@ async function captureSessionIdentity() { } const session = await statPathIdentity(sessionDir); const stdin = await statPathIdentity(stdinDir); - if (!isUsableBirthtimeMs(session.birthtimeMs)) { - await refuseUnusableCreationTime("sessionDir", sessionDir, "its reported creation time (" + session.birthtimeMs + ") is not usable"); - return; - } - if (!isUsableBirthtimeMs(stdin.birthtimeMs)) { - await refuseUnusableCreationTime("stdinDir", stdinDir, "its reported creation time (" + stdin.birthtimeMs + ") is not usable"); - return; - } sessionDirIdentity = session; stdinDirIdentity = stdin; } catch (error) {