* test(cli): keep IPC trace input under the Linux spawn limit The listener-callsite CLI builds one trace argument from the full application Evidence document. On 4 KiB-page Linux that string exceeds MAX_ARG_STRLEN, so spawn throws E2BIG before the command starts. Keep the document in a file, require the spawned argv to fit the single-argument budget, and lock that budget through the test process registry. Co-authored-by: morluto <morluto@users.noreply.github.com> * test(process): size the spawn ceiling from ARG_MAX and MAX_ARG_STRLEN A payload at the single-string limit still fails execve with E2BIG when the stack shrinks ARG_MAX below the rest of the image. Accept only the bytes that fit both budgets, and skip that success spawn when the fixed argv and environment already fill ARG_MAX. Co-authored-by: morluto <morluto@users.noreply.github.com> * style(test): format files rejected by oxfmt in static checks check:ci:fast failed format:check on the CLI fixture import and, after merging main, on the admission test's new command-abort case. Co-authored-by: morluto <morluto@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: morluto <morluto@users.noreply.github.com>
67 lines
2.6 KiB
TypeScript
67 lines
2.6 KiB
TypeScript
import { Buffer } from "node:buffer";
|
|
|
|
/**
|
|
* Largest single argv or environment string Linux will pass to `execve` on a
|
|
* 4 KiB page host. The kernel's `MAX_ARG_STRLEN` is 32 pages and includes the
|
|
* trailing NUL, so the caller-visible budget is one byte smaller. GitHub
|
|
* Actions runners use 4 KiB pages. One argument past this budget fails with
|
|
* `E2BIG` before the process starts. The combined argv, environment, filename,
|
|
* and pointer table must also fit in `ARG_MAX`, which shrinks with the stack.
|
|
*/
|
|
export const LINUX_4K_SINGLE_ARGUMENT_BYTES = 32 * 4096 - 1;
|
|
|
|
/**
|
|
* Largest payload that fits this execve image.
|
|
*
|
|
* `fixedStringBytes` counts the filename, every argv string except the
|
|
* payload, and every `KEY=value` environment string, each including its NUL.
|
|
* `pointerCount` is `max(argc, 1) + envc`: Linux reserves that many native
|
|
* pointers inside `ARG_MAX` before it copies strings. The payload's own NUL
|
|
* is reserved here. The result is undefined when the fixed image already
|
|
* fills `ARG_MAX`.
|
|
*/
|
|
export const acceptedSpawnPayloadBytes = (input: {
|
|
readonly pageSize: number;
|
|
readonly argMax: number;
|
|
readonly pointerBytes: number;
|
|
readonly pointerCount: number;
|
|
readonly fixedStringBytes: number;
|
|
}): number | undefined => {
|
|
for (const [name, value] of [
|
|
["pageSize", input.pageSize],
|
|
["argMax", input.argMax],
|
|
["pointerBytes", input.pointerBytes],
|
|
["pointerCount", input.pointerCount],
|
|
["fixedStringBytes", input.fixedStringBytes],
|
|
] as const) {
|
|
if (!Number.isSafeInteger(value) || value < 0)
|
|
throw new Error(`${name} must be a non-negative safe integer`);
|
|
}
|
|
if (input.pageSize < 1 || input.pointerBytes < 1 || input.pointerCount < 1)
|
|
throw new Error(
|
|
"page size, pointer size, and pointer count must be positive",
|
|
);
|
|
const singleStringBytes = 32 * input.pageSize - 1;
|
|
const aggregatePayload =
|
|
input.argMax -
|
|
input.pointerCount * input.pointerBytes -
|
|
input.fixedStringBytes -
|
|
1;
|
|
if (aggregatePayload < 1) return undefined;
|
|
return Math.min(singleStringBytes, aggregatePayload);
|
|
};
|
|
|
|
/** Require every argv string to fit in the Linux single-argument budget. */
|
|
export const assertSpawnArgumentsWithinLimit = (
|
|
arguments_: readonly string[],
|
|
): void => {
|
|
for (const argument of arguments_) {
|
|
const bytes = Buffer.byteLength(argument);
|
|
if (bytes > LINUX_4K_SINGLE_ARGUMENT_BYTES) {
|
|
throw new Error(
|
|
`spawn argument is ${bytes} bytes; one Linux argument above ${LINUX_4K_SINGLE_ARGUMENT_BYTES} bytes fails with E2BIG before the process starts. Pass the payload through a file.`,
|
|
);
|
|
}
|
|
}
|
|
};
|