mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
fix(core): remove shell outputChunks buffer caching to prevent memory bloat and sanitize prompt input (#23751)
This commit is contained in:
@@ -45,20 +45,18 @@ function addShellCommandToGeminiHistory(
|
|||||||
? resultText.substring(0, MAX_OUTPUT_LENGTH) + '\n... (truncated)'
|
? resultText.substring(0, MAX_OUTPUT_LENGTH) + '\n... (truncated)'
|
||||||
: resultText;
|
: resultText;
|
||||||
|
|
||||||
|
// Escape backticks to prevent prompt injection breakouts
|
||||||
|
const safeQuery = rawQuery.replace(/\\/g, '\\\\').replace(/\x60/g, '\\\x60');
|
||||||
|
const safeModelContent = modelContent
|
||||||
|
.replace(/\\/g, '\\\\')
|
||||||
|
.replace(/\x60/g, '\\\x60');
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
geminiClient.addHistory({
|
geminiClient.addHistory({
|
||||||
role: 'user',
|
role: 'user',
|
||||||
parts: [
|
parts: [
|
||||||
{
|
{
|
||||||
text: `I ran the following shell command:
|
text: `I ran the following shell command:\n\`\`\`sh\n${safeQuery}\n\`\`\`\n\nThis produced the following result:\n\`\`\`\n${safeModelContent}\n\`\`\``,
|
||||||
\`\`\`sh
|
|
||||||
${rawQuery}
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
This produced the following result:
|
|
||||||
\`\`\`
|
|
||||||
${modelContent}
|
|
||||||
\`\`\``,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -444,7 +442,7 @@ export const useShellCommandProcessor = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mainContent: string;
|
let mainContent: string;
|
||||||
if (isBinary(result.rawOutput)) {
|
if (isBinaryStream || isBinary(result.rawOutput)) {
|
||||||
mainContent =
|
mainContent =
|
||||||
'[Command produced binary output, which is not shown.]';
|
'[Command produced binary output, which is not shown.]';
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export type ExecutionMethod =
|
|||||||
| 'none';
|
| 'none';
|
||||||
|
|
||||||
export interface ExecutionResult {
|
export interface ExecutionResult {
|
||||||
rawOutput: Buffer;
|
rawOutput?: Buffer;
|
||||||
output: string;
|
output: string;
|
||||||
exitCode: number | null;
|
exitCode: number | null;
|
||||||
signal: number | null;
|
signal: number | null;
|
||||||
|
|||||||
@@ -880,15 +880,12 @@ describe('ShellExecutionService', () => {
|
|||||||
const binaryChunk1 = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
|
const binaryChunk1 = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
|
||||||
const binaryChunk2 = Buffer.from([0x0d, 0x0a, 0x1a, 0x0a]);
|
const binaryChunk2 = Buffer.from([0x0d, 0x0a, 0x1a, 0x0a]);
|
||||||
|
|
||||||
const { result } = await simulateExecution('cat image.png', (pty) => {
|
await simulateExecution('cat image.png', (pty) => {
|
||||||
pty.onData.mock.calls[0][0](binaryChunk1);
|
pty.onData.mock.calls[0][0](binaryChunk1);
|
||||||
pty.onData.mock.calls[0][0](binaryChunk2);
|
pty.onData.mock.calls[0][0](binaryChunk2);
|
||||||
pty.onExit.mock.calls[0][0]({ exitCode: 0, signal: null });
|
pty.onExit.mock.calls[0][0]({ exitCode: 0, signal: null });
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.rawOutput).toEqual(
|
|
||||||
Buffer.concat([binaryChunk1, binaryChunk2]),
|
|
||||||
);
|
|
||||||
expect(onOutputEventMock).toHaveBeenCalledTimes(4);
|
expect(onOutputEventMock).toHaveBeenCalledTimes(4);
|
||||||
expect(onOutputEventMock.mock.calls[0][0]).toEqual({
|
expect(onOutputEventMock.mock.calls[0][0]).toEqual({
|
||||||
type: 'binary_detected',
|
type: 'binary_detected',
|
||||||
@@ -1464,15 +1461,12 @@ describe('ShellExecutionService child_process fallback', () => {
|
|||||||
const binaryChunk1 = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
|
const binaryChunk1 = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
|
||||||
const binaryChunk2 = Buffer.from([0x0d, 0x0a, 0x1a, 0x0a]);
|
const binaryChunk2 = Buffer.from([0x0d, 0x0a, 0x1a, 0x0a]);
|
||||||
|
|
||||||
const { result } = await simulateExecution('cat image.png', (cp) => {
|
await simulateExecution('cat image.png', (cp) => {
|
||||||
cp.stdout?.emit('data', binaryChunk1);
|
cp.stdout?.emit('data', binaryChunk1);
|
||||||
cp.stdout?.emit('data', binaryChunk2);
|
cp.stdout?.emit('data', binaryChunk2);
|
||||||
cp.emit('exit', 0, null);
|
cp.emit('exit', 0, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.rawOutput).toEqual(
|
|
||||||
Buffer.concat([binaryChunk1, binaryChunk2]),
|
|
||||||
);
|
|
||||||
expect(onOutputEventMock).toHaveBeenCalledTimes(4);
|
expect(onOutputEventMock).toHaveBeenCalledTimes(4);
|
||||||
expect(onOutputEventMock.mock.calls[0][0]).toEqual({
|
expect(onOutputEventMock.mock.calls[0][0]).toEqual({
|
||||||
type: 'binary_detected',
|
type: 'binary_detected',
|
||||||
|
|||||||
@@ -120,7 +120,8 @@ interface ActiveChildProcess {
|
|||||||
state: {
|
state: {
|
||||||
output: string;
|
output: string;
|
||||||
truncated: boolean;
|
truncated: boolean;
|
||||||
outputChunks: Buffer[];
|
sniffChunks: Buffer[];
|
||||||
|
binaryBytesReceived: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,7 +494,8 @@ export class ShellExecutionService {
|
|||||||
const state = {
|
const state = {
|
||||||
output: '',
|
output: '',
|
||||||
truncated: false,
|
truncated: false,
|
||||||
outputChunks: [] as Buffer[],
|
sniffChunks: [] as Buffer[],
|
||||||
|
binaryBytesReceived: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (child.pid) {
|
if (child.pid) {
|
||||||
@@ -563,14 +565,19 @@ export class ShellExecutionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.outputChunks.push(data);
|
if (isStreamingRawContent && sniffedBytes < MAX_SNIFF_SIZE) {
|
||||||
|
state.sniffChunks.push(data);
|
||||||
|
} else if (!isStreamingRawContent) {
|
||||||
|
state.binaryBytesReceived += data.length;
|
||||||
|
}
|
||||||
|
|
||||||
if (isStreamingRawContent && sniffedBytes < MAX_SNIFF_SIZE) {
|
if (isStreamingRawContent && sniffedBytes < MAX_SNIFF_SIZE) {
|
||||||
const sniffBuffer = Buffer.concat(state.outputChunks.slice(0, 20));
|
const sniffBuffer = Buffer.concat(state.sniffChunks.slice(0, 20));
|
||||||
sniffedBytes = sniffBuffer.length;
|
sniffedBytes = sniffBuffer.length;
|
||||||
|
|
||||||
if (isBinary(sniffBuffer)) {
|
if (isBinary(sniffBuffer)) {
|
||||||
isStreamingRawContent = false;
|
isStreamingRawContent = false;
|
||||||
|
state.binaryBytesReceived = sniffBuffer.length;
|
||||||
const event: ShellOutputEvent = { type: 'binary_detected' };
|
const event: ShellOutputEvent = { type: 'binary_detected' };
|
||||||
onOutputEvent(event);
|
onOutputEvent(event);
|
||||||
if (child.pid) {
|
if (child.pid) {
|
||||||
@@ -610,10 +617,7 @@ export class ShellExecutionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const totalBytes = state.outputChunks.reduce(
|
const totalBytes = state.binaryBytesReceived;
|
||||||
(sum, chunk) => sum + chunk.length,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
const event: ShellOutputEvent = {
|
const event: ShellOutputEvent = {
|
||||||
type: 'binary_progress',
|
type: 'binary_progress',
|
||||||
bytesReceived: totalBytes,
|
bytesReceived: totalBytes,
|
||||||
@@ -629,7 +633,7 @@ export class ShellExecutionService {
|
|||||||
code: number | null,
|
code: number | null,
|
||||||
signal: NodeJS.Signals | null,
|
signal: NodeJS.Signals | null,
|
||||||
) => {
|
) => {
|
||||||
const { finalBuffer } = cleanup();
|
cleanup();
|
||||||
|
|
||||||
let combinedOutput = state.output;
|
let combinedOutput = state.output;
|
||||||
if (state.truncated) {
|
if (state.truncated) {
|
||||||
@@ -644,7 +648,7 @@ export class ShellExecutionService {
|
|||||||
const exitSignal = signal ? os.constants.signals[signal] : null;
|
const exitSignal = signal ? os.constants.signals[signal] : null;
|
||||||
|
|
||||||
const resultPayload: ShellExecutionResult = {
|
const resultPayload: ShellExecutionResult = {
|
||||||
rawOutput: finalBuffer,
|
rawOutput: Buffer.from(''),
|
||||||
output: finalStrippedOutput,
|
output: finalStrippedOutput,
|
||||||
exitCode,
|
exitCode,
|
||||||
signal: exitSignal,
|
signal: exitSignal,
|
||||||
@@ -733,8 +737,7 @@ export class ShellExecutionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalBuffer = Buffer.concat(state.outputChunks);
|
return;
|
||||||
return { finalBuffer };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { pid: child.pid, result };
|
return { pid: child.pid, result };
|
||||||
@@ -864,7 +867,8 @@ export class ShellExecutionService {
|
|||||||
let processingChain = Promise.resolve();
|
let processingChain = Promise.resolve();
|
||||||
let decoder: TextDecoder | null = null;
|
let decoder: TextDecoder | null = null;
|
||||||
let output: string | AnsiOutput | null = null;
|
let output: string | AnsiOutput | null = null;
|
||||||
const outputChunks: Buffer[] = [];
|
const sniffChunks: Buffer[] = [];
|
||||||
|
let binaryBytesReceived = 0;
|
||||||
const error: Error | null = null;
|
const error: Error | null = null;
|
||||||
let exited = false;
|
let exited = false;
|
||||||
|
|
||||||
@@ -995,14 +999,19 @@ export class ShellExecutionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
outputChunks.push(data);
|
if (isStreamingRawContent && sniffedBytes < MAX_SNIFF_SIZE) {
|
||||||
|
sniffChunks.push(data);
|
||||||
|
} else if (!isStreamingRawContent) {
|
||||||
|
binaryBytesReceived += data.length;
|
||||||
|
}
|
||||||
|
|
||||||
if (isStreamingRawContent && sniffedBytes < MAX_SNIFF_SIZE) {
|
if (isStreamingRawContent && sniffedBytes < MAX_SNIFF_SIZE) {
|
||||||
const sniffBuffer = Buffer.concat(outputChunks.slice(0, 20));
|
const sniffBuffer = Buffer.concat(sniffChunks.slice(0, 20));
|
||||||
sniffedBytes = sniffBuffer.length;
|
sniffedBytes = sniffBuffer.length;
|
||||||
|
|
||||||
if (isBinary(sniffBuffer)) {
|
if (isBinary(sniffBuffer)) {
|
||||||
isStreamingRawContent = false;
|
isStreamingRawContent = false;
|
||||||
|
binaryBytesReceived = sniffBuffer.length;
|
||||||
const event: ShellOutputEvent = { type: 'binary_detected' };
|
const event: ShellOutputEvent = { type: 'binary_detected' };
|
||||||
onOutputEvent(event);
|
onOutputEvent(event);
|
||||||
ExecutionLifecycleService.emitEvent(ptyPid, event);
|
ExecutionLifecycleService.emitEvent(ptyPid, event);
|
||||||
@@ -1027,10 +1036,7 @@ export class ShellExecutionService {
|
|||||||
resolveChunk();
|
resolveChunk();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const totalBytes = outputChunks.reduce(
|
const totalBytes = binaryBytesReceived;
|
||||||
(sum, chunk) => sum + chunk.length,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
const event: ShellOutputEvent = {
|
const event: ShellOutputEvent = {
|
||||||
type: 'binary_progress',
|
type: 'binary_progress',
|
||||||
bytesReceived: totalBytes,
|
bytesReceived: totalBytes,
|
||||||
@@ -1076,7 +1082,7 @@ export class ShellExecutionService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ExecutionLifecycleService.completeWithResult(ptyPid, {
|
ExecutionLifecycleService.completeWithResult(ptyPid, {
|
||||||
rawOutput: Buffer.concat(outputChunks),
|
rawOutput: Buffer.from(''),
|
||||||
output: getFullBufferText(headlessTerminal),
|
output: getFullBufferText(headlessTerminal),
|
||||||
exitCode,
|
exitCode,
|
||||||
signal: signal ?? null,
|
signal: signal ?? null,
|
||||||
|
|||||||
Reference in New Issue
Block a user