refactor(process): share paired prlimit ceiling ownership (#6352)

* refactor(process): share paired prlimit ceiling ownership

Route firmware, Wakaru, and pcap paired ceilings through
LinuxResourceLimits without changing their byte or CPU budgets.
Keep EVMole's soft-only limits as a dated architecture-guard exception.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* test(pcap): admit capture limits without host prlimit

The ceiling test only checks the shared argument prefix, so it uses a
temporary executable instead of /usr/bin/prlimit.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* style(pcap): format the capture ceiling test

oxfmt rejected the object literal wrapping in PcapNativeBound.test.ts.

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>
This commit is contained in:
authored and GitHub committed 2026-10-23 01:46:35 +08:00
1 parent 44a4c1ba87
commit 6dc0f60faa
12 files changed
+222 -46

No files matched your search

+37 -11
View File
@@ -89,23 +89,49 @@ check(
["src/domain/javascript/javascriptSemanticTraversal.ts"],
);
// A producer that pins both the soft and hard ceiling of every resource must take
// the vector from one owner. Audited 2026-10-13 against the other prlimit lanes:
// src/pcap/PcapNativeBound.ts pairs soft and hard but budgets 300 CPU seconds and
// has no resident-input ceiling; src/evm/EvmoleInterfaceProvider.ts,
// src/firmware/FirmwareCommand.ts and src/javascript/recovery/WakaruCommand.ts
// spell soft-only forms (`--core=0:`, `--core=0`). Those four stay as they are;
// a new paired producer calls prlimitArgumentVector instead of spelling options.
// A producer that pins both the soft and hard ceiling of every resource takes
// the vector from one owner. prlimitCeilingArguments is the command-less prefix
// of prlimitArgumentVector; both live in src/process/LinuxResourceLimits.ts.
// Re-audited 2026-10-22 with util-linux 2.39.3: a bare value sets both soft and
// hard, the same as soft:hard when the numbers are equal. `soft:` leaves the
// inherited hard ceiling unchanged.
// FirmwareCommand and WakaruCommand call prlimitArgumentVector and keep their
// own file-size and CPU budgets (120 CPU seconds; firmware file size is the
// call output budget or the 8 MiB report allowance; Wakaru file size is the
// 128 MiB output allowance). Their 1 GiB address space is
// BOUNDED_PRODUCER_LIMITS.address_space_bytes. PcapNativeBound calls
// prlimitCeilingArguments because PcapCommand appends the producer command.
// Its CPU ceiling stays 300 seconds and it has no resident-input budget;
// address space and file size use those shared fields.
// selectEvmWorkerLimits stays a separate selector: Result failures, unit
// checks, and a refusal when soft exceeds hard. It must not call
// clampLinuxResourceLimits.
check(
"inline paired prlimit ceilings (use prlimitArgumentVector from src/process/LinuxResourceLimits)",
files,
/--core=0:0/,
["src/process/LinuxResourceLimits.ts", "src/pcap/PcapNativeBound.ts"],
["src/process/LinuxResourceLimits.ts"],
);
// Census is spelling-shaped, like the rules above: a lane that expresses the same
// ceilings differently (`1024 ** 3`, camelCase fields) is not caught here, which is
// why the four lanes above are listed rather than left to a future reader.
check(
"bare prlimit value (a bare value sets both ceilings; use prlimitArgumentVector)",
files,
/--core=0(?!:)/,
);
// 2026-10-22: soft-only `resource:` options retain the inherited hard ceiling.
// EVMole budgets differ (3 GiB address space, 16 MiB file, 30 CPU seconds), so
// this lane must not call prlimitArgumentVector.
check(
"soft-only prlimit ceilings (paired producers use prlimitArgumentVector)",
files,
/--core=0:(?!0)/,
["src/evm/EvmoleInterfaceProvider.ts"],
);
// Resident input is the input_bytes field of BOUNDED_PRODUCER_LIMITS. CamelCase
// inputBytes values with a different policy (an overridable pwntools admission,
// historical capture intake) are not this budget.
check(
"inline resident-input budget (use BOUNDED_PRODUCER_LIMITS from src/process/LinuxResourceLimits)",
files,
+11 -7
View File
@@ -23,6 +23,8 @@ import {
firmwareProcessLimitRecovery,
firmwareSignalLimitError,
} from "./FirmwareFailures.js";
import { prlimitArgumentVector } from "../process/LinuxResourceLimits.js";
import { cleanupOwnedProcessGroup } from "../process/ProcessOwnership.js";
import {
spawnOwnedProviderProcess,
ProviderProcessSupervisor,
@@ -30,7 +32,6 @@ import {
type ProviderProcessSnapshot,
type SpawnedOwnedProviderProcess,
} from "../process/ProviderProcess.js";
import { cleanupOwnedProcessGroup } from "../process/ProcessOwnership.js";
import { FIRMWARE_LIMITS } from "./FirmwareRelease.js";
import {
FirmwareFileAccess,
@@ -159,12 +160,15 @@ export const runFirmwareCommand = async (context: {
const spawned = await (context.launcher ?? spawnOwnedProviderProcess)({
command: context.limiter,
arguments: [
`--as=${FIRMWARE_LIMITS.addressSpaceBytes}`,
`--fsize=${context.outputBudget?.bytes ?? FIRMWARE_LIMITS.reportBytes}`,
"--core=0",
`--cpu=${FIRMWARE_LIMITS.cpuSeconds}`,
"--",
context.command,
...prlimitArgumentVector(
{
address_space_bytes: FIRMWARE_LIMITS.addressSpaceBytes,
file_bytes:
context.outputBudget?.bytes ?? FIRMWARE_LIMITS.reportBytes,
cpu_seconds: FIRMWARE_LIMITS.cpuSeconds,
},
context.command,
),
...context.args,
],
expectedCommand: null,
+3 -1
View File
@@ -1,3 +1,5 @@
import { BOUNDED_PRODUCER_LIMITS } from "../process/LinuxResourceLimits.js";
/** Audited upstream sources; installed tool bytes are fingerprinted separately. */
export const FIRMWARE_RELEASES = {
binwalk: {
@@ -30,6 +32,6 @@ export const FIRMWARE_LIMITS = {
inputBytes: 128 * 1024 * 1024,
reportBytes: 8 * 1024 * 1024,
diagnosticBytes: 8 * 1024 * 1024,
addressSpaceBytes: 1024 * 1024 * 1024,
addressSpaceBytes: BOUNDED_PRODUCER_LIMITS.address_space_bytes,
cpuSeconds: 120,
} as const;
@@ -11,6 +11,7 @@ import {
runWakaruCommand,
type WakaruLauncher,
} from "./WakaruCommand.js";
import { prlimitArgumentVector } from "../../process/LinuxResourceLimits.js";
import { RECOVERY_LIMITS } from "./WakaruRelease.js";
it("names the Wakaru configuration requirement when it is not configured", async () => {
@@ -182,6 +183,52 @@ it("enforces the shared deadline while exited output streams remain open", async
}
});
it("pins both Wakaru ceilings through the shared prlimit vector", async () => {
let observed: readonly string[] | undefined;
const producer = createProducer();
const command = "/selected/wakaru";
await expect(
runWakaruCommand({
command,
limiter: "/usr/bin/prlimit",
arguments: ["--version"],
cwd: "/tmp/unused",
environment: {},
deadline: performance.now() + 5_000,
launcher: async (options) => {
observed = options.arguments;
setImmediate(() => {
producer.stdout.end();
producer.stderr.end();
producer.emit("exit", 0, null);
producer.emit("close", 0, null);
});
return {
process: producer,
ownership: {
runId: options.runId,
leaderPid: 12345,
processGroupId: 12345,
expectedParentPid: process.pid,
},
cleanup: async () => ({ cleaned: true, signaled: false }),
};
},
}),
).resolves.toMatchObject({ exit_code: 0 });
expect(observed).toEqual([
...prlimitArgumentVector(
{
address_space_bytes: RECOVERY_LIMITS.addressSpaceBytes,
file_bytes: RECOVERY_LIMITS.outputBytes,
cpu_seconds: RECOVERY_LIMITS.cpuSeconds,
},
command,
),
"--version",
]);
});
it("honors cancellation while waiting for output after exit", async () => {
const controller = new AbortController();
const { context, producer } = commandFixture((process) => {
+9 -6
View File
@@ -9,6 +9,7 @@ import {
} from "../../domain/analysisErrorCore.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
import { prlimitArgumentVector } from "../../process/LinuxResourceLimits.js";
import { cleanupOwnedProcessGroup } from "../../process/ProcessOwnership.js";
import {
ProviderProcessSupervisor,
@@ -118,12 +119,14 @@ export const runWakaruCommand = async (context: WakaruCommandContext) => {
const spawned = await (context.launcher ?? spawnOwnedProviderProcess)({
command: context.limiter,
arguments: [
`--as=${String(RECOVERY_LIMITS.addressSpaceBytes)}`,
`--fsize=${String(RECOVERY_LIMITS.outputBytes)}`,
"--core=0",
"--cpu=120",
"--",
context.command,
...prlimitArgumentVector(
{
address_space_bytes: RECOVERY_LIMITS.addressSpaceBytes,
file_bytes: RECOVERY_LIMITS.outputBytes,
cpu_seconds: RECOVERY_LIMITS.cpuSeconds,
},
context.command,
),
...context.arguments,
],
expectedCommand: null,
+4 -1
View File
@@ -1,3 +1,5 @@
import { BOUNDED_PRODUCER_LIMITS } from "../../process/LinuxResourceLimits.js";
/** Unchanged upstream source pin; executed bytes are identified separately. */
export const WAKARU_RELEASE = {
version: "1.14.0",
@@ -34,7 +36,8 @@ export const RECOVERY_LIMITS = {
outputBytes: 128 * 1024 * 1024,
outputEntries: 10000,
reportBytes: 8 * 1024 * 1024,
addressSpaceBytes: 1024 * 1024 * 1024,
addressSpaceBytes: BOUNDED_PRODUCER_LIMITS.address_space_bytes,
cpuSeconds: 120,
timeoutMs: 120000,
} as const;
+66
View File
@@ -0,0 +1,66 @@
import { chmod, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
import type { ArtifactAnalysisResources } from "../process/ArtifactAnalysisResources.js";
import {
BOUNDED_PRODUCER_LIMITS,
prlimitCeilingArguments,
type LinuxResourceLimits,
} from "../process/LinuxResourceLimits.js";
import { preparePcapNativeBound } from "./PcapNativeBound.js";
const resources = {
readLimits: (limits: LinuxResourceLimits) => Promise.resolve(limits),
} as ArtifactAnalysisResources;
const withPlatform = async (
platform: NodeJS.Platform,
run: () => Promise<void>,
) => {
const previousPlatform = process.platform;
Object.defineProperty(process, "platform", {
configurable: true,
value: platform,
});
try {
await run();
} finally {
Object.defineProperty(process, "platform", {
configurable: true,
value: previousPlatform,
});
}
};
it("uses the shared 1 GiB ceiling for the Windows job memory limit", async () => {
await withPlatform("win32", async () => {
await expect(preparePcapNativeBound({}, resources)).resolves.toMatchObject({
nativeBound: {
memoryLimitBytes: BOUNDED_PRODUCER_LIMITS.address_space_bytes,
},
});
});
});
it("pins capture ceilings through the shared prlimit prefix and keeps 300 CPU seconds", async () => {
const root = await createTestTempDirectory("rea-pcap-prlimit-");
const limiter = join(root, "prlimit");
await writeFile(limiter, "#!/bin/sh\n");
await chmod(limiter, 0o700);
const limits = {
address_space_bytes: BOUNDED_PRODUCER_LIMITS.address_space_bytes,
file_bytes: BOUNDED_PRODUCER_LIMITS.file_bytes,
cpu_seconds: 300,
};
await withPlatform("linux", async () => {
const prepared = await preparePcapNativeBound(
{ REA_TSHARK_PRLIMIT_COMMAND: limiter },
resources,
);
expect(prepared.nativeBound.limiter?.arguments).toEqual(
prlimitCeilingArguments(limits),
);
expect(prepared.limitations[0]).toContain(String(limits.cpu_seconds));
});
});
+13 -11
View File
@@ -4,9 +4,15 @@ import { isAbsolute } from "node:path";
import { AnalysisError } from "../domain/analysisErrorBase.js";
import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js";
import type { ArtifactAnalysisResources } from "../process/ArtifactAnalysisResources.js";
import {
BOUNDED_PRODUCER_LIMITS,
prlimitCeilingArguments,
} from "../process/LinuxResourceLimits.js";
import type { runPcapCommand } from "./PcapCommand.js";
const PRLIMIT_COMMAND_ENV = "REA_TSHARK_PRLIMIT_COMMAND";
/** Capture CPU ceiling, in seconds. */
const PCAP_CPU_SECONDS = 300;
/** Own native capture admission and the exact enforced resource profile. */
export const preparePcapNativeBound = async (
@@ -18,7 +24,9 @@ export const preparePcapNativeBound = async (
}> => {
if (process.platform === "win32") {
return {
nativeBound: { memoryLimitBytes: 1024 ** 3 },
nativeBound: {
memoryLimitBytes: BOUNDED_PRODUCER_LIMITS.address_space_bytes,
},
limitations: [
"Each owned Windows tool tree has a 1 GiB Job Object committed-memory limit; this is not a machine-wide quota.",
],
@@ -41,20 +49,14 @@ export const preparePcapNativeBound = async (
const limiter = await realpath(configured);
await access(limiter, constants.X_OK);
const limits = await resources.readLimits({
address_space_bytes: 1024 ** 3,
file_bytes: 64 * 1024 ** 2,
cpu_seconds: 300,
address_space_bytes: BOUNDED_PRODUCER_LIMITS.address_space_bytes,
file_bytes: BOUNDED_PRODUCER_LIMITS.file_bytes,
cpu_seconds: PCAP_CPU_SECONDS,
});
const nativeBound = {
limiter: {
command: limiter,
arguments: [
`--as=${limits.address_space_bytes}:${limits.address_space_bytes}`,
`--fsize=${limits.file_bytes}:${limits.file_bytes}`,
`--cpu=${limits.cpu_seconds}:${limits.cpu_seconds}`,
"--core=0:0",
"--",
],
arguments: prlimitCeilingArguments(limits),
},
};
const limitations = [
+9 -3
View File
@@ -11,6 +11,7 @@ import {
BOUNDED_PRODUCER_LIMITS,
clampLinuxResourceLimits,
prlimitArgumentVector,
prlimitCeilingArguments,
readLinuxResourceLimits,
} from "./LinuxResourceLimits.js";
@@ -87,9 +88,11 @@ it("pins the shared producer budget in one place", () => {
});
});
it("pairs each effective ceiling with itself before the producer argv", () => {
expect(
prlimitArgumentVector(BOUNDED_PRODUCER_LIMITS, "/bin/producer"),
).toEqual([
const vector = prlimitArgumentVector(
BOUNDED_PRODUCER_LIMITS,
"/bin/producer",
);
expect(vector).toEqual([
"--as=1073741824:1073741824",
"--fsize=67108864:67108864",
"--cpu=30:30",
@@ -97,6 +100,9 @@ it("pairs each effective ceiling with itself before the producer argv", () => {
"--",
"/bin/producer",
]);
expect(prlimitCeilingArguments(BOUNDED_PRODUCER_LIMITS)).toEqual(
vector.slice(0, -1),
);
expect(
prlimitArgumentVector(
clampLinuxResourceLimits(BOUNDED_PRODUCER_LIMITS, report("10 20")),
+11 -4
View File
@@ -24,19 +24,26 @@ export const BOUNDED_PRODUCER_LIMITS = {
cpu_seconds: 30,
} as const;
/** Pin soft and hard ceilings together, then hand the producer argv to the limiter. */
export const prlimitArgumentVector = (
/**
* Pin soft and hard ceilings together, ending at the prlimit option terminator.
* Callers that append the producer command themselves use this prefix.
*/
export const prlimitCeilingArguments = (
limits: LinuxResourceLimits,
command: string,
): string[] => [
`--as=${limits.address_space_bytes}:${limits.address_space_bytes}`,
`--fsize=${limits.file_bytes}:${limits.file_bytes}`,
`--cpu=${limits.cpu_seconds}:${limits.cpu_seconds}`,
"--core=0:0",
"--",
command,
];
/** Pin soft and hard ceilings together, then hand the producer argv to the limiter. */
export const prlimitArgumentVector = (
limits: LinuxResourceLimits,
command: string,
): string[] => [...prlimitCeilingArguments(limits), command];
/** Never raise either inherited soft policy or hard ceiling. */
export const clampLinuxResourceLimits = (
configured: LinuxResourceLimits,
@@ -7,6 +7,7 @@ import {
} from "../../fixtures/firmware/provider.js";
import { firmwareResultSchemas } from "../../../src/domain/firmware/firmwareAnalysis.js";
import { FirmwareAnalysisService } from "../../../src/application/firmware/FirmwareAnalysisService.js";
import { FIRMWARE_LIMITS } from "../../../src/firmware/FirmwareRelease.js";
import { FirmwareProvider } from "../../../src/firmware/FirmwareProvider.js";
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
import { AnalysisError } from "../../../src/domain/analysisErrorBase.js";
@@ -109,7 +110,11 @@ it("binds region observations to exact bytes and preserves provider-reported unc
report: [{ Analysis: { file_map: [{ id: "hit" }] } }],
});
expect(fixture.launches[1]?.args).toEqual(
expect.arrayContaining(["--as=1073741824", "--threads", "1"]),
expect.arrayContaining([
`--as=${String(FIRMWARE_LIMITS.addressSpaceBytes)}:${String(FIRMWARE_LIMITS.addressSpaceBytes)}`,
"--threads",
"1",
]),
);
await assertFirmwareCleanup(fixture.launches);
});
@@ -9,6 +9,7 @@ import { projectAnalysisError } from "../../src/domain/analysisErrorProjection.j
import { ProviderProcessSupervisor } from "../../src/process/ProviderProcess.js";
import { finishAnalysisCommand } from "../../src/cli/finishAnalysisCommand.js";
import { AnalysisOutputError } from "../../src/domain/analysisErrorCore.js";
import { FIRMWARE_LIMITS } from "../../src/firmware/FirmwareRelease.js";
import {
firmwareFixture,
assertFirmwareCleanup,
@@ -279,7 +280,11 @@ test("uses the selected phase deadline inside an owned command beyond the former
});
expect(fixture.launches).toHaveLength(2);
expect(
fixture.launches.every((launch) => launch.args.includes("--cpu=120")),
fixture.launches.every((launch) =>
launch.args.includes(
`--cpu=${String(FIRMWARE_LIMITS.cpuSeconds)}:${String(FIRMWARE_LIMITS.cpuSeconds)}`,
),
),
).toBe(true);
await fixture.close();
await assertFirmwareCleanup(fixture.launches);