fix(pcap): validate selected resource limiter before capture (#7947)

* fix(pcap): validate selected resource limiter before capture

* fix(pcap): verify limiter capabilities and preserve probe output
This commit is contained in:
morluto authored and GitHub committed 2026-10-29 09:23:07 +08:00
1 parent 59a895a193
commit 78a1e1956e
5 files changed
+375 -25

No files matched your search

+177 -2
View File
@@ -1,8 +1,13 @@
import { chmod, writeFile } from "node:fs/promises";
import { chmod, symlink, 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 {
AnalysisCancelledError,
AnalysisArtifactChangedError,
AnalysisOutputError,
} from "../domain/analysisErrorCore.js";
import {
BOUNDED_PRODUCER_LIMITS,
prlimitCeilingArguments,
@@ -10,6 +15,12 @@ import {
} from "../process/LinuxResourceLimits.js";
import { preparePcapNativeBound } from "./PcapNativeBound.js";
const probeResult = (stdout: string, stderr = "") => ({
stdout,
capturedOutput: { stdout, stderr, truncated: false },
});
const help = " --as[=limit] --fsize[=limit] --cpu[=limit] --core[=limit]";
const resources = {
readLimits: (limits: LinuxResourceLimits) => Promise.resolve(limits),
} as ArtifactAnalysisResources;
@@ -35,7 +46,9 @@ const withPlatform = async (
it("uses the shared 1 GiB ceiling for the Windows job memory limit", async () => {
await withPlatform("win32", async () => {
await expect(preparePcapNativeBound({}, resources)).resolves.toMatchObject({
await expect(
preparePcapNativeBound({}, resources, async () => probeResult("")),
).resolves.toMatchObject({
nativeBound: {
memoryLimitBytes: BOUNDED_PRODUCER_LIMITS.address_space_bytes,
},
@@ -43,6 +56,64 @@ it("uses the shared 1 GiB ceiling for the Windows job memory limit", async () =>
});
});
it("accepts selected limiter symlinks with different and whitespace-bearing basenames", async () => {
const root = await createTestTempDirectory("rea-prlimit-alias-");
const executable = join(root, "prlimit");
await writeFile(executable, "#!/bin/sh\nexit 0\n", { mode: 0o700 });
for (const name of ["bounded-capture", " limiter", " limiter "]) {
const selected = join(root, name);
await symlink(executable, selected);
await withPlatform("linux", async () => {
const bound = await preparePcapNativeBound(
{ REA_TSHARK_PRLIMIT_COMMAND: selected },
resources,
async (selection, args) => {
expect(selection.invocationPath).toBe(selected);
return probeResult(
args[0] === "--help" ? help : `${name} from util-linux 2.38.1\n`,
);
},
);
expect(bound.nativeBound.limiter?.command.invocationPath).toBe(selected);
});
}
});
it("attributes malformed limiter version output while retaining producer evidence", async () => {
const root = await createTestTempDirectory("rea-prlimit-invalid-output-");
const selected = join(root, "prlimit");
await writeFile(selected, "#!/bin/sh\nexit 0\n", { mode: 0o700 });
const capturedOutput = { stdout: "\ufffd", stderr: "", truncated: false };
const failure = new AnalysisOutputError(
"inspect_pcap_capture",
"invalid UTF-8",
{
capturedOutput,
diagnostics: { invalid_utf8_streams: ["stdout"] },
},
);
await withPlatform("linux", async () => {
await expect(
preparePcapNativeBound(
{ REA_TSHARK_PRLIMIT_COMMAND: selected },
resources,
async () => {
throw failure;
},
),
).rejects.toMatchObject({
_tag: "AnalysisCapabilityUnavailableError",
capturedOutput,
diagnostics: {
invalid_utf8_streams: ["stdout"],
configuration_key: "REA_TSHARK_PRLIMIT_COMMAND",
configured: selected,
},
userMessage: expect.stringContaining("unreadable version output"),
});
});
});
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");
@@ -57,6 +128,10 @@ it("pins capture ceilings through the shared prlimit prefix and keeps 300 CPU se
const prepared = await preparePcapNativeBound(
{ REA_TSHARK_PRLIMIT_COMMAND: limiter },
resources,
async (_selection, args) =>
probeResult(
args[0] === "--help" ? help : "prlimit from util-linux 2.38.1\n",
),
);
expect(prepared.nativeBound.limiter?.arguments).toEqual(
prlimitCeilingArguments(limits),
@@ -64,3 +139,103 @@ it("pins capture ceilings through the shared prlimit prefix and keeps 300 CPU se
expect(prepared.limitations[0]).toContain(String(limits.cpu_seconds));
});
});
it("rejects a directory or a command that does not identify as util-linux prlimit", async () => {
const root = await createTestTempDirectory("rea-invalid-prlimit-");
const executable = join(root, "not-prlimit");
await writeFile(executable, "#!/bin/sh\nexit 0\n");
await chmod(executable, 0o700);
await withPlatform("linux", async () => {
for (const [path, output] of [
[root, "prlimit from util-linux 2.38"],
[executable, ""],
] as const) {
await expect(
preparePcapNativeBound(
{ REA_TSHARK_PRLIMIT_COMMAND: path },
resources,
async () => probeResult(output),
),
).rejects.toMatchObject({
_tag: "AnalysisCapabilityUnavailableError",
diagnostics: {
configuration_key: "REA_TSHARK_PRLIMIT_COMMAND",
configured: path,
},
});
}
});
});
it("preserves cancellation and selected-command identity failures during the probe", async () => {
const root = await createTestTempDirectory("rea-prlimit-probe-failure-");
const limiter = join(root, "prlimit");
await writeFile(limiter, "#!/bin/sh\nexit 0\n", { mode: 0o700 });
await withPlatform("linux", async () => {
for (const failure of [
new AnalysisCancelledError("inspect_pcap_capture"),
new AnalysisArtifactChangedError(
"inspect_pcap_capture",
limiter,
"changed",
),
new AnalysisOutputError("inspect_pcap_capture", "invalid output", {
cleanup: { reason: "stop failed", resources: ["owned-process"] },
}),
]) {
await expect(
preparePcapNativeBound(
{ REA_TSHARK_PRLIMIT_COMMAND: limiter },
resources,
async () => {
throw failure;
},
),
).rejects.toBe(failure);
}
});
});
it("rejects another util-linux command and retains the rejected help output", async () => {
const root = await createTestTempDirectory("rea-prlimit-impostor-");
const selected = join(root, "lsblk");
await writeFile(selected, "#!/bin/sh\nexit 0\n", { mode: 0o700 });
const capturedOutput = probeResult(
" --output columns",
"help diagnostic",
).capturedOutput;
await withPlatform("linux", async () => {
await expect(
preparePcapNativeBound(
{ REA_TSHARK_PRLIMIT_COMMAND: selected },
resources,
async (_selection, args) =>
args[0] === "--help"
? { stdout: capturedOutput.stdout, capturedOutput }
: probeResult("lsblk from util-linux 2.38\n"),
),
).rejects.toMatchObject({
_tag: "AnalysisCapabilityUnavailableError",
capturedOutput,
});
});
});
it("retains rejected version output", async () => {
const root = await createTestTempDirectory("rea-prlimit-rejected-version-");
const selected = join(root, "prlimit");
await writeFile(selected, "#!/bin/sh\nexit 0\n", { mode: 0o700 });
const output = probeResult("unexpected banner", "diagnostic");
await withPlatform("linux", async () => {
await expect(
preparePcapNativeBound(
{ REA_TSHARK_PRLIMIT_COMMAND: selected },
resources,
async () => output,
),
).rejects.toMatchObject({
_tag: "AnalysisCapabilityUnavailableError",
capturedOutput: output.capturedOutput,
});
});
});
+80 -19
View File
@@ -3,17 +3,25 @@ import {
pcapCaptureHostSupported,
PCAP_CAPTURE_UNSUPPORTED_HOST_REASON,
} from "../domain/pcap/pcapAnalysis.js";
import { access } from "node:fs/promises";
import { access, stat } from "node:fs/promises";
import { constants } from "node:fs";
import { isAbsolute } from "node:path";
import { AnalysisError } from "../domain/analysisErrorBase.js";
import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js";
import { basename, isAbsolute } from "node:path";
import {
AnalysisError,
type AnalysisCapturedOutput,
} from "../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisOutputError,
} 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";
import type { ArtifactCommandPath } from "../process/ArtifactCommandPath.js";
const PRLIMIT_COMMAND_ENV = "REA_TSHARK_PRLIMIT_COMMAND";
/** Capture CPU ceiling, in seconds. */
@@ -23,6 +31,17 @@ const PCAP_CPU_SECONDS = 300;
export const preparePcapNativeBound = async (
environment: Readonly<Record<string, string | undefined>>,
resources: ArtifactAnalysisResources,
probe: (
selected: ArtifactCommandPath,
arguments_: string[],
) => Promise<{
readonly stdout: string;
readonly capturedOutput: AnalysisCapturedOutput;
}>,
signal?: AbortSignal,
operation:
| "inspect_pcap_client"
| "inspect_pcap_capture" = "inspect_pcap_capture",
): Promise<{
nativeBound: NonNullable<Parameters<typeof runPcapCommand>[0]["nativeBound"]>;
limitations: readonly string[];
@@ -39,19 +58,56 @@ export const preparePcapNativeBound = async (
}
if (!pcapCaptureHostSupported(process.platform)) {
const reason = PCAP_CAPTURE_UNSUPPORTED_HOST_REASON;
throw new AnalysisCapabilityUnavailableError(
"pcap",
"inspect_pcap_capture",
reason,
{ userMessage: reason },
);
throw new AnalysisCapabilityUnavailableError("pcap", operation, reason, {
userMessage: reason,
});
}
const configured = environment[PRLIMIT_COMMAND_ENV] ?? "/usr/bin/prlimit";
let probeFailure: AnalysisError | undefined;
let capturedOutput: AnalysisCapturedOutput | undefined;
try {
if (!isAbsolute(configured))
throw new Error(`${PRLIMIT_COMMAND_ENV} must be absolute.`);
const selection = await selectArtifactCommandPath(configured);
const selection = await selectArtifactCommandPath(configured, signal);
if (!(await stat(selection.resolvedPath)).isFile())
throw new Error("The selected limiter is not a regular file.");
await access(selection.resolvedPath, constants.X_OK);
const readProbe = async (argument: "--version" | "--help") => {
try {
const output = await probe(selection, [argument]);
capturedOutput = output.capturedOutput;
return output.stdout;
} catch (cause) {
if (
(cause instanceof AnalysisCapabilityUnavailableError ||
cause instanceof AnalysisOutputError) &&
!cause.cleanupIncomplete
) {
probeFailure = cause;
throw new Error(
cause instanceof AnalysisOutputError
? `The selected limiter produced unreadable ${argument === "--version" ? "version" : "help"} output.`
: "The selected limiter could not be run.",
{ cause },
);
}
throw cause;
}
};
const version = await readProbe("--version");
const prefix = `${basename(selection.invocationPath)} from util-linux `;
if (
!version.startsWith(prefix) ||
version.slice(prefix.length).trim() === ""
)
throw new Error(
"The selected limiter did not report a util-linux prlimit version.",
);
const help = await readProbe("--help");
for (const flag of ["--as", "--fsize", "--cpu", "--core"]) {
if (!new RegExp(`(?:^|\\s)${flag}(?:[=\\[\\s,]|$)`, "u").test(help))
throw new Error(`The selected limiter does not advertise ${flag}.`);
}
const limits = await resources.readLimits({
address_space_bytes: BOUNDED_PRODUCER_LIMITS.address_space_bytes,
file_bytes: BOUNDED_PRODUCER_LIMITS.file_bytes,
@@ -68,17 +124,22 @@ export const preparePcapNativeBound = async (
];
return { nativeBound, limitations };
} catch (cause) {
if (signal?.aborted === true && !(cause instanceof AnalysisError))
throw new AnalysisCancelledError(operation, {
cause: signal.reason,
});
if (cause instanceof AnalysisError) throw cause;
const reason =
"Cannot establish the selected Linux resource limiter before processing capture bytes.";
throw new AnalysisCapabilityUnavailableError(
"pcap",
"inspect_pcap_capture",
reason,
{
cause,
userMessage: `${reason} (${cause instanceof Error ? cause.message : String(cause)}) Check that ${PRLIMIT_COMMAND_ENV} (default /usr/bin/prlimit) names an absolute, executable util-linux prlimit.`,
throw new AnalysisCapabilityUnavailableError("pcap", operation, reason, {
cause,
capturedOutput: probeFailure?.capturedOutput ?? capturedOutput,
diagnostics: {
...probeFailure?.diagnostics,
configuration_key: PRLIMIT_COMMAND_ENV,
configured,
},
);
userMessage: `${reason} (${cause instanceof Error ? cause.message : String(cause)}) Check that ${PRLIMIT_COMMAND_ENV} (default /usr/bin/prlimit) names an absolute, executable util-linux prlimit.`,
});
}
};
+21 -3
View File
@@ -414,10 +414,28 @@ export class PcapProvider {
private nativeBound: Parameters<typeof runPcapCommand>[0]["nativeBound"];
private nativeLimitations: readonly string[] = [];
private async prepareCaptureBound(): Promise<void> {
private async prepareCaptureBound(
operation: PcapOperation,
signal?: AbortSignal,
): Promise<void> {
const prepared = await preparePcapNativeBound(
this.environment,
this.#resources,
async (selected, arguments_) => {
await assertArtifactCommandPath(selected, operation, { signal });
const output = await this.run(
operation,
"version",
selected.invocationPath,
arguments_,
{ timeoutMs: 20_000, maxOutputBytes: 64 * 1024 },
signal,
);
await assertArtifactCommandPath(selected, operation, { signal });
return output;
},
signal,
operation,
);
this.nativeBound = prepared.nativeBound;
this.nativeLimitations = prepared.limitations;
@@ -543,7 +561,7 @@ export class PcapProvider {
"No enforceable native memory boundary is available for capture summaries on this host.",
diagnostics: client,
};
await this.prepareCaptureBound();
await this.prepareCaptureBound("inspect_pcap_client", signal);
return {
status: "available",
code: null,
@@ -795,7 +813,7 @@ export class PcapProvider {
),
);
else {
await this.prepareCaptureBound();
await this.prepareCaptureBound(request.operation, signal);
snapshot = await PcapSnapshot.create();
this.snapshots.add(snapshot);
const identity = await snapshot.capture(request.input.path, signal);
+87 -1
View File
@@ -1,6 +1,6 @@
import { createHash } from "node:crypto";
import { watch } from "node:fs";
import { readFile, writeFile } from "node:fs/promises";
import { readFile, symlink, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { expect, onTestFinished, test, vi } from "vitest";
@@ -540,6 +540,92 @@ it("names the prlimit selection when the Linux limiter is unusable", async () =>
expect(projected.message).toContain("Check that REA_TSHARK_PRLIMIT_COMMAND");
});
it("rejects directory and non-prlimit selections before opening the capture", async () => {
const root = await createTestTempDirectory("rea-pcap-invalid-limiter-");
const tools = await writeStubTools(root);
for (const configured of [root, "/usr/bin/true"]) {
const provider = new PcapProvider({
environment: {
REA_TSHARK_COMMAND: tools.tshark,
REA_CAPINFOS_COMMAND: tools.capinfos,
REA_TSHARK_PRLIMIT_COMMAND: configured,
},
});
try {
const result = await provider.execute({
operation: "inspect_pcap_capture",
input: { path: "/not-opened/capture.pcap" },
});
if (result.ok) throw new Error("expected an unusable limiter");
const error = projectAnalysisError(result.error);
expect(error).toMatchObject({
code: "capability_unavailable",
details: {
diagnostics: {
configuration_key: "REA_TSHARK_PRLIMIT_COMMAND",
configured,
},
},
});
expect(error.message).toContain("REA_TSHARK_PRLIMIT_COMMAND");
expect(error.message).not.toContain("Cannot run selected tool");
} finally {
await provider.close();
}
}
});
it("rejects a different util-linux command before opening capture bytes", async () => {
const root = await createTestTempDirectory("rea-pcap-other-util-linux-");
const tools = await writeStubTools(root);
const provider = new PcapProvider({
environment: {
REA_TSHARK_COMMAND: tools.tshark,
REA_CAPINFOS_COMMAND: tools.capinfos,
REA_TSHARK_PRLIMIT_COMMAND: "/usr/bin/lsblk",
},
});
try {
const result = await provider.execute({
operation: "inspect_pcap_capture",
input: { path: "/not-opened/capture.pcap" },
});
if (result.ok) throw new Error("expected an unusable limiter");
expect(result.error).toMatchObject({
_tag: "AnalysisCapabilityUnavailableError",
capturedOutput: { stdout: expect.stringContaining("--output") },
});
} finally {
await provider.close();
}
});
it("accepts real prlimit selected through differently named symlinks", async () => {
const root = await createTestTempDirectory("rea-pcap-prlimit-alias-");
const tools = await writeStubTools(root);
for (const name of ["bounded-capture", " limiter "]) {
const limiter = join(root, name);
await symlink("/usr/bin/prlimit", limiter);
const provider = new PcapProvider({
environment: {
REA_TSHARK_COMMAND: tools.tshark,
REA_CAPINFOS_COMMAND: tools.capinfos,
REA_TSHARK_PRLIMIT_COMMAND: limiter,
},
});
try {
const result = await provider.execute({
operation: "inspect_pcap_capture",
input: { path: await captureFixture(root) },
});
if (!result.ok) throw result.error;
expect(result.value.result).toMatchObject({ coverage: "complete" });
} finally {
await provider.close();
}
}
});
// Capture execution requires the Linux native resource bound used by providerWith.
it.each([
{ pass: "selected", invalidUtf8: false },
@@ -474,6 +474,16 @@ it.skipIf(process.platform === "win32")(
prepared = await preparePcapNativeBound(
{ REA_TSHARK_PRLIMIT_COMMAND: selected },
resources,
async (_selected, args) => {
const stdout =
args[0] === "--help"
? " --as[=limit] --fsize[=limit] --cpu[=limit] --core[=limit]"
: "prlimit from util-linux 2.38.1";
return {
stdout,
capturedOutput: { stdout, stderr: "", truncated: false },
};
},
);
} finally {
Object.defineProperty(process, "platform", platform);