Files
rea/tests/support/process/spawnArgumentLimit.ts
T
c38f96fa69 test(cli): keep IPC trace input under the Linux spawn limit (#5673)
* 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>
2026-10-21 04:48:33 +08:00

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.`,
);
}
}
};