Limit exec streams to max string length (#5898)

Some operations set `exec`'s `maxOutput` to a value larger than the max
string length. This change ensures we always cap output to the engine's
string limit (currently 512 MB in recent Node versions).

This also tweaks handling when reaching the string limit to ensure
adding the "truncated" message itself does not send us beyond the limit.
This commit is contained in:
J. Ryan Stinnett
2023-12-23 23:54:46 +00:00
committed by GitHub
parent 359c0f4a80
commit e7b22b41be
6 changed files with 33 additions and 7 deletions

View File

@@ -1428,7 +1428,15 @@ export class BaseCompiler implements ICompiler {
if (output.timedOut) {
return {
error: 'Clang invocation timed out',
error: 'Invocation timed out',
results: {},
compileTime: output.execTime || compileEnd - compileStart,
};
}
if (output.truncated) {
return {
error: 'Exceeded max output limit',
results: {},
compileTime: output.execTime || compileEnd - compileStart,
};

View File

@@ -203,7 +203,15 @@ export class RacketCompiler extends BaseCompiler {
if (output.timedOut) {
return {
error: 'Racket invocation timed out',
error: 'Invocation timed out',
results: {},
compileTime: output.execTime || compileEnd - compileStart,
};
}
if (output.truncated) {
return {
error: 'Exceeded max output limit',
results: {},
compileTime: output.execTime || compileEnd - compileStart,
};

View File

@@ -24,6 +24,7 @@
import child_process from 'child_process';
import path from 'path';
import buffer from 'buffer';
import fs from 'fs-extra';
import treeKill from 'tree-kill';
@@ -59,7 +60,7 @@ export function executeDirect(
filenameTransform?: FilenameTransformFunc,
): Promise<UnprocessedExecResult> {
options = options || {};
const maxOutput = options.maxOutput || 1024 * 1024;
const maxOutput = Math.min(options.maxOutput || 1024 * 1024, buffer.constants.MAX_STRING_LENGTH);
const timeoutMs = options.timeoutMs || 0;
const env = {...process.env, ...options.env};
@@ -123,8 +124,10 @@ export function executeDirect(
if (streams.truncated) return;
const newLength = streams[name].length + data.length;
if (maxOutput > 0 && newLength > maxOutput) {
streams[name] = streams[name] + data.slice(0, maxOutput - streams[name].length);
streams[name] += '\n[Truncated]';
const truncatedMsg = '\n[Truncated]';
const spaceLeft = Math.max(maxOutput - streams[name].length - truncatedMsg.length, 0);
streams[name] = streams[name] + data.slice(0, spaceLeft);
streams[name] += truncatedMsg.slice(0, maxOutput - streams[name].length);
streams.truncated = true;
kill();
return;
@@ -162,7 +165,11 @@ export function executeDirect(
truncated: streams.truncated,
execTime: ((endTime - startTime) / BigInt(1000000)).toString(),
};
logger.debug('Execution', {type: 'executed', command: command, args: args, result: result});
// Check debug level explicitly as result may be a very large string
// which we'd prefer to avoid preparing if it won't be used
if (logger.isDebugEnabled()) {
logger.debug('Execution', {type: 'executed', command: command, args: args, result: result});
}
resolve(result);
});
if (child.stdin) {

View File

@@ -118,7 +118,7 @@ describe('Execution tests', () => {
});
it('limits output', () => {
return exec
.execute('echo', ['A very very very very very long string'], {maxOutput: 10})
.execute('echo', ['A very very very very very long string'], {maxOutput: 22})
.then(testExecOutput)
.should.eventually.deep.equals({
code: 0,

View File

@@ -146,6 +146,7 @@ export type CompilationResult = {
devices?: Record<string, CompilationResult>;
stdout: ResultLine[];
stderr: ResultLine[];
truncated?: boolean;
didExecute?: boolean;
execResult?: {
stdout?: ResultLine[];

View File

@@ -29,6 +29,8 @@ export type BasicExecutionResult = {
execTime: string;
processExecutionResultTime?: number;
timedOut: boolean;
languageId?: string;
truncated?: boolean;
};
export enum RuntimeToolType {