diff --git a/src/rizin/RizinDebugSessionManager.ts b/src/rizin/RizinDebugSessionManager.ts index c3657fadc0..cadb6284df 100644 --- a/src/rizin/RizinDebugSessionManager.ts +++ b/src/rizin/RizinDebugSessionManager.ts @@ -1189,7 +1189,8 @@ export class RizinDebugSessionManager { identity.startTime, await readLinuxProcessStartTime(pid), ); - await this.#verifyInferiorOutputConfiguration(session, pid); + await this.#verifyDebuggerExitPolicy(session); + await this.#verifyInferiorOutputDescriptors(session, pid); await this.#runInternalCommand(session, `o- ${fd}`); const after = parseRizinPtraceRows( await this.#runInternalCommand(session, "olj"), @@ -1203,12 +1204,14 @@ export class RizinDebugSessionManager { throw new Error( "Rizin did not confirm that the selected inferior FD was detached.", ); + // Protocol readback must never be waived by proof that a process ended. + await this.#verifyDebuggerExitPolicy(session); try { assertRizinProcessStartTimeStable( identity.startTime, await readLinuxProcessStartTime(pid), ); - await this.#verifyInferiorOutputConfiguration(session, pid); + await this.#verifyInferiorOutputDescriptors(session, pid); } catch (cause) { // Detach resumes the inferior. Its identity or descriptors may disappear // before readback; only a previously identified process can prove its end. @@ -1219,15 +1222,18 @@ export class RizinDebugSessionManager { return { status: "detached", outputSink: "durable" }; } - async #verifyInferiorOutputConfiguration( - session: RizinDebugSession, - pid: string, - ): Promise { + async #verifyDebuggerExitPolicy(session: RizinDebugSession): Promise { const exitKills = ( await this.#runInternalCommand(session, "e dbg.exitkills") ).trim(); if (exitKills !== "false") throw new Error("Rizin dbg.exitkills readback did not confirm false."); + } + + async #verifyInferiorOutputDescriptors( + session: RizinDebugSession, + pid: string, + ): Promise { for (const descriptor of ["1", "2"]) { const descriptorPath = `/proc/${pid}/fd/${descriptor}`; const destination = await readlink(descriptorPath); diff --git a/tests/boundary/providers/RizinInferiorExit.test.ts b/tests/boundary/providers/RizinInferiorExit.test.ts index 8a73e010a6..b201b65153 100644 --- a/tests/boundary/providers/RizinInferiorExit.test.ts +++ b/tests/boundary/providers/RizinInferiorExit.test.ts @@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto"; import { mkdtemp, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { afterEach, expect, it } from "vitest"; +import { afterEach, expect, it, vi } from "vitest"; import { ProviderProcessSupervisor, @@ -33,7 +33,9 @@ afterEach(async () => { } }); -it.skipIf(process.platform !== "linux").each(["exit", "redirect"] as const)( +it + .skipIf(process.platform !== "linux") + .each(["exit", "redirect", "ambiguous-policy"] as const)( "verifies the original inferior after %s during detach", async (mode) => { const directory = await mkdtemp(join(tmpdir(), "rea-rizin-exit-")); @@ -98,17 +100,25 @@ process.stdin.resume();`, ); else if (line === "o- 3") { descriptorOpen = false; - launched.process.stdin!.write(`${mode}\n`); - if (mode === "exit") + if (mode !== "ambiguous-policy") { + launched.process.stdin!.write(`${mode}\n`); + if (mode === "exit") + expect(await supervisor.waitForExit(1_000)).toBe(true); + else + await expect + .poll(() => readFile(redirected, "utf8"), { + timeout: 1_000, + }) + .toBe("redirected"); + } + } else if (line === "e dbg.exitkills") { + if (mode === "ambiguous-policy" && !descriptorOpen) { + // End the identified process only during the final policy query. + launched.process.stdin!.write("exit\n"); expect(await supervisor.waitForExit(1_000)).toBe(true); - else - await expect - .poll(() => readFile(redirected, "utf8"), { - timeout: 1_000, - }) - .toBe("redirected"); - } else if (line === "e dbg.exitkills") control.output("false\0"); - else if (line.startsWith("!echo ")) + control.output("ambiguous\0"); + } else control.output("false\0"); + } else if (line.startsWith("!echo ")) control.output(`${line.slice(6)}\0`); } }) @@ -118,6 +128,7 @@ process.stdin.resume();`, }); }, }); + const debuggerExit = vi.spyOn(injected.control, "exit"); const manager = createRizinTestManager( {}, { @@ -146,10 +157,15 @@ process.stdin.resume();`, } else { expect(closed.ok).toBe(false); expect(manager.status(started.value.session_id)).toBeDefined(); - expect(supervisor.observedExit()).toBeUndefined(); + expect(debuggerExit).not.toHaveBeenCalled(); + if (mode === "redirect") + expect(supervisor.observedExit()).toBeUndefined(); + else expect(supervisor.observedExit()?.code).toBe(0); if (!closed.ok) expect(closed.error.message).toContain( - "not routed to the selected TTY", + mode === "redirect" + ? "not routed to the selected TTY" + : "dbg.exitkills readback did not confirm false", ); } } finally {