diff --git a/scripts/lib/process.mjs b/scripts/lib/process.mjs index 0e91d78..448dbbf 100644 --- a/scripts/lib/process.mjs +++ b/scripts/lib/process.mjs @@ -155,10 +155,18 @@ export function getProcessIdentity(pid) { } } -export function validateProcessIdentity(pid, expectedIdentity) { +export function validateProcessIdentity(pid, expectedIdentity, options = {}) { + const identityImpl = options.identityImpl ?? getProcessIdentity; try { - return getProcessIdentity(pid) === expectedIdentity; - } catch { + return identityImpl(pid) === expectedIdentity; + } catch (error) { + // A denied probe (sandboxed `ps`, unreadable /proc entry) means the identity + // cannot be established, not that the process is gone. Fall back to the + // zero-signal liveness probe, which still reports ESRCH for a dead PID. + // PID-reuse detection is only given up when identity is unknowable at all. + if (error?.code === "EPERM" || error?.code === "EACCES") { + return isProcessAlive(pid, options); + } return false; } } diff --git a/tests/process.test.mjs b/tests/process.test.mjs index 07b1ecf..bf94afc 100644 --- a/tests/process.test.mjs +++ b/tests/process.test.mjs @@ -387,4 +387,42 @@ describe("validateProcessIdentity", () => { it("returns false for non-existent PID", () => { assert.equal(validateProcessIdentity(99999999, "any"), false); }); + + it("falls back to the liveness probe when the identity probe is denied", () => { + const deniedIdentity = () => { + throw Object.assign(new Error("spawnSync ps EPERM"), { code: "EPERM" }); + }; + + assert.equal( + validateProcessIdentity(12345, "recorded-identity", { + identityImpl: deniedIdentity, + killImpl: () => { + throw Object.assign(new Error("operation not permitted"), { code: "EPERM" }); + }, + }), + true, + ); + + assert.equal( + validateProcessIdentity(12345, "recorded-identity", { + identityImpl: deniedIdentity, + killImpl: () => { + throw Object.assign(new Error("no such process"), { code: "ESRCH" }); + }, + }), + false, + ); + }); + + it("keeps a failed identity probe that is not a denial classified as dead", () => { + assert.equal( + validateProcessIdentity(12345, "recorded-identity", { + identityImpl: () => { + throw new Error("ps: exit=1"); + }, + killImpl: () => true, + }), + false, + ); + }); });