fix(gdb): preserve MI framing and reject lossy result facts (#9982)

This commit is contained in:
morluto authored and GitHub committed 2026-11-11 17:18:18 +08:00
1 parent e55ce72d9d
commit 4812843e5b
2 files changed
+107 -5

No files matched your search

+12 -5
View File
@@ -1685,6 +1685,7 @@ export class GdbSessionManager {
const lines: { readonly value: string; readonly invalidUtf8: boolean }[] =
[];
let offset = 0;
let framingFailure: string | undefined;
while (offset < data.byteLength) {
if (session.discardingOversizedStreamRecord) {
const newline = data.indexOf(0x0a, offset);
@@ -1721,11 +1722,8 @@ export class GdbSessionManager {
offset = newline + 1;
continue;
}
this.#failProtocol(
session,
`GDB emitted an MI line exceeding the ${String(MAX_MI_LINE_BYTES)} byte (16 MiB) protocol limit. This release has no runtime override.`,
);
return;
framingFailure = `GDB emitted an MI line exceeding the ${String(MAX_MI_LINE_BYTES)} byte (16 MiB) protocol limit. This release has no runtime override.`;
break;
}
this.#appendLineBytes(session, piece);
if (newline < 0) break;
@@ -1801,6 +1799,13 @@ export class GdbSessionManager {
line,
);
if (match === null) continue;
if (invalidUtf8) {
this.#failProtocol(
session,
"GDB emitted an MI result record containing invalid UTF-8.",
);
return;
}
const token = Number(match[1]);
const pending =
session.pending.get(token) ??
@@ -1847,6 +1852,8 @@ export class GdbSessionManager {
completion_status: "complete",
});
}
if (framingFailure !== undefined)
this.#failProtocol(session, framingFailure);
}
#appendLineBytes(session: GdbSession, piece: Buffer): void {
@@ -3606,6 +3606,101 @@ process.stdin.on('data', (chunk) => {
}
});
it.each([false, true])(
"retains completed MI observations before a later framing failure (coalesced=%s)",
async (coalesced) => {
const prefix = '~"completed observation"\n';
const oversized = "x".repeat(16 * 1024 * 1024 + 1) + "\n";
const fake = createFakeSpawner((child, token, command) => {
if (command === "-gdb-set auto-load off") {
child.stdout.write(`${token}^done\n`);
return;
}
if (coalesced) child.stdout.write(prefix + oversized);
else {
child.stdout.write(prefix);
child.stdout.write(oversized);
}
});
const manager = managerWithFakeSpawner(fake.spawnProcess, {
stopProcess: async () => {
for (const child of fake.children) child.exit(0, null);
return { status: "stopped" };
},
});
const started = await expectGdbSessionStarted(manager);
const failed = await manager.execute(
started.value.session_id,
"fixture-only",
);
expect(failed).toMatchObject({
ok: false,
error: {
_tag: "AnalysisCapabilityUnavailableError",
partialObservation: {
normalized_result: {
records: ['~"completed observation"'],
console: '~"completed observation"',
completion_status: "unknown",
},
},
},
});
},
);
it.each(["literal", "octal", "invalid-result", "invalid-stream"] as const)(
"requires valid UTF-8 for TTY facts while retaining opaque stream data (%s)",
async (encoding) => {
const tty = "/fixture/terminal-\uFFFD";
const fake = createFakeSpawner((child, token, command) => {
if (command === "-inferior-tty-show") {
if (encoding === "invalid-stream")
child.stdout.write(Buffer.from([0x7e, 0x22, 0xff, 0x22, 0x0a]));
const suffix =
encoding === "invalid-result"
? Buffer.from([0xff])
: encoding === "octal"
? Buffer.from(String.raw`\357\277\275`)
: Buffer.from("\uFFFD");
child.stdout.write(
Buffer.concat([
Buffer.from(
`${token}^done,inferior_tty_terminal="/fixture/terminal-`,
),
suffix,
Buffer.from('"\n'),
]),
);
return;
}
child.stdout.write(
command.includes("list-thread-groups")
? `${token}^done,groups=[]\n`
: `${token}^done\n`,
);
});
const manager = managerWithFakeSpawner(fake.spawnProcess, {
stopProcess: async () => {
for (const child of fake.children) child.exit(0, null);
return { status: "stopped" };
},
});
const started = await manager.start(undefined, { inferiorTty: tty });
expect(started.ok).toBe(encoding !== "invalid-result");
if (started.ok)
expect(manager.status(started.value.session_id)).toMatchObject({
inferior_tty: tty,
inferior_tty_verified: true,
mi_invalid_utf8: encoding === "invalid-stream",
});
else
expect(started.error.partialObservation).toMatchObject({
normalized_result: { output_invalid_utf8: true },
});
},
);
it("closes the GDB session when a complete MI record exceeds the protocol limit", async () => {
const manager = await managerFor(`
process.stdout.write('(gdb)\\n');