fix(gdb): keep inferior-tty verification for read-only queries (#5539)

* fix(gdb): keep TTY verification across read-only inferior-tty inspection

The console command path cleared `inferiorTtyVerified` before dispatching
any command whose text merely mentioned `inferior-tty`, so the read-only
`show inferior-tty` query permanently revoked a persistent session's
verified output sink; no in-session path ever restored the flag, and a
later close of a live session failed detach verification with
capability_unavailable.

Exempt the exact read-only query forms (`show inferior-tty` and the MI
`-inferior-tty-show`) from the output-mutation predicate; every other
command mentioning the TTY setting keeps revoking, so mutating commands
still fail closed and detach keeps re-reading the live TTY before
detaching. Cover the query forms, composite commands, and the
inspect-then-close detach path with regression tests.

Fixes #5099

* test(gdb): classify show inferior-tty in the one-shot table and rename the TTY classification table

- Add a 'show inferior-tty' row to the 'classifies one-shot GDB
  commands' table: the read-only query no longer invalidates the detach
  proof, a user-visible one-shot behavior change.
- Rename the 'classifies read-only inferior TTY queries as non-mutating'
  table to 'classifies inferior TTY commands' since two of its rows are
  mutation cases.
- Assert via the existing recorded-commands array that the inspected
  'show inferior-tty' text is actually dispatched, and add an
  execute-path assertion that the MI '-inferior-tty-show' form leaves
  the verified-sink flag intact (defensive exemption: REA's own MI calls
  do not route through this gate).

---------

Co-authored-by: akushonkamen <akushonkamen@users.noreply.github.com>
This commit is contained in:
akushonkamenandakushonkamen authored and GitHub committed 2026-11-05 18:28:22 +08:00
1 parent fbfb1b768c
commit 0714eba505
2 files changed
+82 -3

No files matched your search

+9 -3
View File
@@ -2429,9 +2429,15 @@ export const gdbOneShotCommandMayInvalidateDetachProof = (
/** @internal Classify commands that can invalidate the configured output sink. */
export const commandMayChangeInferiorOutput = (command: string): boolean => {
// These complete built-in inspections cannot change output routing. Keep
// compound commands and abbreviations under the conservative rules below.
if (/^[ \t]*(?:show|help)[ \t]+inferior-tty[ \t]*$/iu.test(command))
// These exact read-only TTY inspections ("show inferior-tty", "help
// inferior-tty", MI "-inferior-tty-show") cannot reconfigure the output
// sink, so they must not revoke the startup verification. Keep compound
// commands and abbreviations under the conservative rules below
// (fail-closed).
if (
/^[ \t]*(?:show|help)[ \t]+inferior-tty[ \t]*$/iu.test(command) ||
/^[ \t]*-inferior-tty-show[ \t]*$/iu.test(command)
)
return false;
const setting = /^\s*(?:set|with)\s+(\S+)/iu.exec(command)?.[1];
const maySetInferiorTty =
@@ -177,10 +177,22 @@ it.each([
["so commands.gdb", true],
["de", true],
["p 1; python print(2)", true],
["show inferior-tty", false],
] as const)("classifies one-shot GDB commands: %s", (command, expected) => {
expect(gdbOneShotCommandMayInvalidateDetachProof(command)).toBe(expected);
});
it.each([
["show inferior-tty", false],
[" show inferior-tty ", false],
["-inferior-tty-show", false],
["set inferior-tty /tmp/output", true],
["show inferior-t; set inferior-tty /tmp/output", true],
["show inferior-tty; define redirect = run > /tmp/output", true],
] as const)("classifies inferior TTY commands: %s", (command, expected) => {
expect(commandMayChangeInferiorOutput(command)).toBe(expected);
});
it.each([
[new Error("provider failure"), "provider failure"],
["non-error provider failure", "non-error provider failure"],
@@ -2784,6 +2796,67 @@ it.each([
}
});
it("keeps the verified sink across the read-only show inferior-tty inspection", async () => {
const tty = "/dev/pts/42";
const commands: string[] = [];
let detached = false;
const fake = createFakeSpawner((_child, token, command) => {
commands.push(command);
if (command.includes("inferior-tty-show"))
fake.children[0]?.stdout.write(
`${token}^done,inferior_tty_terminal=${JSON.stringify(tty)}\n`,
);
else if (command.includes("list-thread-groups"))
fake.children[0]?.stdout.write(
detached
? `${token}^done,groups=[]\n`
: `${token}^done,groups=[{id="i1",pid="123"}]\n`,
);
else {
if (command.includes("target-detach")) detached = true;
fake.children[0]?.stdout.write(`${token}^done\n`);
}
});
const manager = managerWithFakeSpawner(fake.spawnProcess);
const started = await expectGdbSessionStarted(manager, undefined, {
inferiorTty: tty,
});
const inspected = await manager.execute(
started.value.session_id,
"show inferior-tty",
);
expect(inspected.ok).toBe(true);
expect(manager.status(started.value.session_id)).toMatchObject({
inferior_tty: tty,
inferior_tty_verified: true,
});
expect(commands).toContain('-interpreter-exec console "show inferior-tty"');
// The MI form is a defensive exemption: REA's own MI calls do not route
// through this user-command gate, and the console interpreter may reject
// the text outright. The gate must still leave the verified flag alone.
const inspectedMi = await manager.execute(
started.value.session_id,
"-inferior-tty-show",
);
expect(inspectedMi.ok).toBe(true);
expect(manager.status(started.value.session_id)?.inferior_tty_verified).toBe(
true,
);
const closed = await manager.close(started.value.session_id);
expect(closed).toMatchObject({
ok: true,
value: { target_state: "unknown" },
});
expect(commands).toContain("-target-detach");
expect(fake.children[0]?.killSignals).toEqual(["SIGTERM"]);
});
it("revokes the verified sink after an abbreviated with inferior-tty run command", async () => {
const tty = "/dev/pts/42";
const commands: string[] = [];