66 lines
2.6 KiB
TypeScript
66 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.`,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|