Files

66 lines
2.6 KiB
TypeScript
Raw Permalink Normal View History

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