fix(rizin): keep protocol failures outside exit recovery (#5290)

Separate debugger exit-policy readback from process descriptor checks. Require the final native readback before considering inferior-end proof, and regress an ambiguous reply delivered as the real owned inferior exits. Preserve debugger ownership on failure.

Fixes #5282

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
This commit is contained in:
gensokyo-devandKaoru0822 authored and GitHub committed 2026-10-20 02:56:41 +08:00
1 parent e84e17e30f
commit 909b9f7b6b
2 files changed
+42 -20

No files matched your search

+12 -6
View File
@@ -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<void> {
async #verifyDebuggerExitPolicy(session: RizinDebugSession): Promise<void> {
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<void> {
for (const descriptor of ["1", "2"]) {
const descriptorPath = `/proc/${pid}/fd/${descriptor}`;
const destination = await readlink(descriptorPath);
@@ -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 {