mirror of
https://github.com/morluto/rea.git
synced 2026-11-05 06:15:59 +00:00
Read bounded plist format probes and Linux resource-limit reports through positive short reads before classifying their contents. Preserve explicit EOF, report capacity, physical failures and descriptor cleanup ownership. Validated 66 focused tests and check:fast; nine new fault-injected cases fail against the previous implementation. Linux procfs was not verified on a real Linux host.
306 lines
11 KiB
TypeScript
306 lines
11 KiB
TypeScript
import { expect, it, onTestFinished, vi } from "vitest";
|
|
import { writeFile, type FileHandle } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
|
import { ArtifactFileCleanupFailure } from "../artifacts/ArtifactFileOperation.js";
|
|
import { ArtifactAnalysisResources } from "./ArtifactAnalysisResources.js";
|
|
import { readStableArtifact } from "../artifacts/readStableArtifact.js";
|
|
import { ok } from "../domain/result.js";
|
|
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
|
import {
|
|
BOUNDED_PRODUCER_LIMITS,
|
|
clampLinuxResourceLimits,
|
|
prlimitArgumentVector,
|
|
prlimitCeilingArguments,
|
|
readLinuxResourceLimits,
|
|
} from "./LinuxResourceLimits.js";
|
|
|
|
const kernel = vi.hoisted(() => ({
|
|
path: "",
|
|
denied: false,
|
|
opens: 0,
|
|
maximumRead: Number.POSITIVE_INFINITY,
|
|
readFailure: undefined as Error | undefined,
|
|
failAt: Number.POSITIVE_INFINITY,
|
|
reads: [] as { position: number; length: number }[],
|
|
handles: [] as FileHandle[],
|
|
}));
|
|
vi.mock("node:fs/promises", async (importOriginal) => {
|
|
const filesystem = await importOriginal<typeof import("node:fs/promises")>();
|
|
return {
|
|
...filesystem,
|
|
open: async (...args: Parameters<typeof filesystem.open>) => {
|
|
if (args[0] !== "/proc/self/limits" || kernel.path === "")
|
|
return filesystem.open(...args);
|
|
const file = await filesystem.open(kernel.path, args[1], args[2]);
|
|
kernel.opens += 1;
|
|
kernel.handles.push(file);
|
|
const read = file.read.bind(file);
|
|
file.read = (async (...readArgs: unknown[]) => {
|
|
const [buffer, offset, length, position] = readArgs;
|
|
if (typeof length !== "number" || typeof position !== "number")
|
|
throw new Error("Expected positional limits report read");
|
|
kernel.reads.push({ position, length });
|
|
if (position >= kernel.failAt) throw kernel.readFailure;
|
|
return Reflect.apply(read, file, [
|
|
buffer,
|
|
offset,
|
|
Math.min(length, kernel.maximumRead),
|
|
position,
|
|
]);
|
|
}) as FileHandle["read"];
|
|
const close = file.close.bind(file);
|
|
// Keep fixture ownership even when an assertion interrupts retry checks.
|
|
onTestFinished(close);
|
|
file.close = async () => {
|
|
if (kernel.denied) throw new Error("Kernel descriptor close denied");
|
|
await close();
|
|
};
|
|
return file;
|
|
},
|
|
};
|
|
});
|
|
const configured = {
|
|
address_space_bytes: 1073741824,
|
|
file_bytes: 67108864,
|
|
cpu_seconds: 30,
|
|
};
|
|
const report = (
|
|
memory: string,
|
|
file = "unlimited unlimited",
|
|
cpu = "unlimited unlimited",
|
|
) =>
|
|
`Limit Soft Limit Hard Limit Units\nMax address space ${memory} bytes\nMax file size ${file} bytes\nMax cpu time ${cpu} seconds\n`;
|
|
it("keeps configured ceilings below unlimited or larger inherited limits", () => {
|
|
expect(
|
|
clampLinuxResourceLimits(configured, report("unlimited unlimited")),
|
|
).toEqual(configured);
|
|
expect(
|
|
clampLinuxResourceLimits(
|
|
configured,
|
|
report("18446744073709551615 18446744073709551615"),
|
|
),
|
|
).toEqual(configured);
|
|
});
|
|
it("preserves stricter soft limits and lower hard ceilings, including zero", () => {
|
|
expect(
|
|
clampLinuxResourceLimits(
|
|
configured,
|
|
report("268435456 2147483648", "0 1048576", "10 20"),
|
|
),
|
|
).toEqual({ address_space_bytes: 268435456, file_bytes: 0, cpu_seconds: 10 });
|
|
expect(
|
|
clampLinuxResourceLimits(configured, report("268435456 536870912")),
|
|
).toMatchObject({ address_space_bytes: 268435456 });
|
|
});
|
|
it("refuses missing, ambiguous or malformed kernel limits before launch", () => {
|
|
for (const value of [
|
|
"",
|
|
report("invalid unlimited"),
|
|
report("unlimited unlimited") + "Max address space 1 2 bytes\n",
|
|
])
|
|
expect(() => clampLinuxResourceLimits(configured, value)).toThrow();
|
|
});
|
|
it("pins the shared producer budget in one place", () => {
|
|
expect(BOUNDED_PRODUCER_LIMITS).toEqual({
|
|
input_bytes: 33554432,
|
|
output_bytes: 4194304,
|
|
command_timeout_ms: 30000,
|
|
address_space_bytes: 1073741824,
|
|
file_bytes: 67108864,
|
|
cpu_seconds: 30,
|
|
});
|
|
});
|
|
it("pairs each effective ceiling with itself before the producer argv", () => {
|
|
const vector = prlimitArgumentVector(
|
|
BOUNDED_PRODUCER_LIMITS,
|
|
"/bin/producer",
|
|
);
|
|
expect(vector).toEqual([
|
|
"--as=1073741824:1073741824",
|
|
"--fsize=67108864:67108864",
|
|
"--cpu=30:30",
|
|
"--core=0:0",
|
|
"--",
|
|
"/bin/producer",
|
|
]);
|
|
expect(prlimitCeilingArguments(BOUNDED_PRODUCER_LIMITS)).toEqual(
|
|
vector.slice(0, -1),
|
|
);
|
|
expect(
|
|
prlimitArgumentVector(
|
|
clampLinuxResourceLimits(BOUNDED_PRODUCER_LIMITS, report("10 20")),
|
|
"/bin/producer",
|
|
),
|
|
).toEqual([
|
|
"--as=10:10",
|
|
"--fsize=67108864:67108864",
|
|
"--cpu=30:30",
|
|
"--core=0:0",
|
|
"--",
|
|
"/bin/producer",
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
["completed", true],
|
|
["failed", true],
|
|
["completed", false],
|
|
["failed", false],
|
|
] as const)(
|
|
"retains exact cleanup ownership after a %s read (scoped=%s)",
|
|
async (outcome, scoped) => {
|
|
const directory = await createTestTempDirectory("rea-kernel-limits-");
|
|
kernel.path = join(directory, "limits");
|
|
kernel.denied = true;
|
|
const scope = new ArtifactResourceScope();
|
|
onTestFinished(async () => {
|
|
kernel.denied = false;
|
|
await scope.close();
|
|
kernel.path = "";
|
|
});
|
|
await writeFile(
|
|
kernel.path,
|
|
outcome === "completed" ? report("unlimited unlimited") : "invalid",
|
|
);
|
|
const failure: unknown = await readLinuxResourceLimits(
|
|
configured,
|
|
scoped ? scope : undefined,
|
|
).catch((cause: unknown) => cause);
|
|
expect(failure).toBeInstanceOf(ArtifactFileCleanupFailure);
|
|
if (!(failure instanceof ArtifactFileCleanupFailure))
|
|
throw new Error("Expected exact descriptor cleanup ownership");
|
|
const physical = failure.fileCleanupFailure;
|
|
expect(physical.outcome.kind).toBe(outcome);
|
|
if (physical.outcome.kind === "completed")
|
|
expect(physical.outcome.value).toEqual(configured);
|
|
else
|
|
expect(physical.outcome.cause).toMatchObject({
|
|
message: "Cannot establish inherited Linux limit: Max address space",
|
|
});
|
|
expect(physical.owner.handle.fd).toBeGreaterThanOrEqual(0);
|
|
if (scoped)
|
|
await expect(scope.retryCleanup()).rejects.toMatchObject({
|
|
cleanupLeases: [failure.cleanupLease],
|
|
});
|
|
else
|
|
await expect(failure.cleanupLease.release()).rejects.toThrow(
|
|
"Kernel descriptor close denied",
|
|
);
|
|
kernel.denied = false;
|
|
if (scoped) await scope.retryCleanup();
|
|
else await failure.cleanupLease.release();
|
|
expect(failure.cleanupLease.released).toBe(true);
|
|
expect(physical.owner.handle.fd).toBe(-1);
|
|
expect(scope.hasPending).toBe(false);
|
|
expect(failure.fileCleanupFailure).toBe(physical);
|
|
},
|
|
);
|
|
|
|
it("readmits analysis and resolves the original cleanup error after kernel descriptor recovery", async () => {
|
|
const directory = await createTestTempDirectory("rea-kernel-admission-");
|
|
kernel.path = join(directory, "limits");
|
|
kernel.denied = true;
|
|
const resources = new ArtifactAnalysisResources(
|
|
"kernel-test",
|
|
"read-limits",
|
|
readStableArtifact,
|
|
readLinuxResourceLimits,
|
|
{ message: "busy", details: {} },
|
|
);
|
|
onTestFinished(async () => {
|
|
kernel.denied = false;
|
|
await resources.close();
|
|
kernel.path = "";
|
|
});
|
|
await writeFile(kernel.path, report("unlimited unlimited"));
|
|
const read = () =>
|
|
resources.run(async () => ok(await resources.readLimits(configured)));
|
|
const first = await read();
|
|
if (first.ok) throw new Error("Expected denied descriptor cleanup");
|
|
expect(first.error.cleanupIncomplete).toBe(true);
|
|
const opened = kernel.opens;
|
|
const blocked = await read();
|
|
expect(blocked.ok).toBe(false);
|
|
expect(kernel.opens).toBe(opened);
|
|
kernel.denied = false;
|
|
const recovered = await read();
|
|
expect(recovered).toEqual(ok(configured));
|
|
expect(kernel.opens).toBe(opened + 1);
|
|
expect(first.error.cleanupIncomplete).toBe(false);
|
|
if (!blocked.ok) expect(blocked.error.cleanupIncomplete).toBe(false);
|
|
});
|
|
|
|
const kernelReportFixture = async (text: string): Promise<void> => {
|
|
const directory = await createTestTempDirectory("rea-kernel-short-read-");
|
|
kernel.path = join(directory, "limits");
|
|
kernel.reads = [];
|
|
kernel.handles = [];
|
|
onTestFinished(() => {
|
|
kernel.path = "";
|
|
kernel.maximumRead = Number.POSITIVE_INFINITY;
|
|
kernel.readFailure = undefined;
|
|
kernel.failAt = Number.POSITIVE_INFINITY;
|
|
kernel.reads = [];
|
|
kernel.handles = [];
|
|
});
|
|
await writeFile(kernel.path, text);
|
|
};
|
|
|
|
it("clamps all inherited ceilings after positive short report reads", async () => {
|
|
const text = report("268435456 536870912", "0 1048576", "10 20");
|
|
await kernelReportFixture(text);
|
|
kernel.maximumRead = 7;
|
|
expect(await readLinuxResourceLimits(configured)).toEqual({
|
|
address_space_bytes: 268435456,
|
|
file_bytes: 0,
|
|
cpu_seconds: 10,
|
|
});
|
|
expect(kernel.reads.at(-1)?.position).toBe(Buffer.byteLength(text));
|
|
expect(kernel.reads.length).toBeGreaterThan(2);
|
|
expect(kernel.handles.every((handle) => handle.fd === -1)).toBe(true);
|
|
});
|
|
|
|
it.each([16 * 1024, 16 * 1024 + 1])(
|
|
"rejects a %i-byte report even when its initial short prefix contains all required limits",
|
|
async (bytes) => {
|
|
const prefix = report("unlimited unlimited");
|
|
await kernelReportFixture(prefix.padEnd(bytes, " "));
|
|
kernel.maximumRead = Buffer.byteLength(prefix);
|
|
await expect(readLinuxResourceLimits(configured)).rejects.toThrow(
|
|
"Inherited Linux limits exceed the report budget.",
|
|
);
|
|
expect(kernel.reads.length).toBeGreaterThan(1);
|
|
expect(
|
|
kernel.reads.every(
|
|
({ position, length }) => position + length <= 16 * 1024,
|
|
),
|
|
).toBe(true);
|
|
expect(kernel.handles.every((handle) => handle.fd === -1)).toBe(true);
|
|
},
|
|
);
|
|
|
|
it("accepts a report below the fixed budget after reading through explicit EOF", async () => {
|
|
await kernelReportFixture(
|
|
report("unlimited unlimited").padEnd(16 * 1024 - 1, " "),
|
|
);
|
|
kernel.maximumRead = 127;
|
|
expect(await readLinuxResourceLimits(configured)).toEqual(configured);
|
|
expect(kernel.reads.at(-1)).toEqual({ position: 16 * 1024 - 1, length: 1 });
|
|
expect(kernel.handles.every((handle) => handle.fd === -1)).toBe(true);
|
|
});
|
|
|
|
it("preserves a later I/O failure instead of accepting a complete-looking short prefix", async () => {
|
|
const prefix = report("unlimited unlimited");
|
|
await kernelReportFixture(prefix + "unexamined suffix");
|
|
kernel.maximumRead = Buffer.byteLength(prefix);
|
|
kernel.failAt = Buffer.byteLength(prefix);
|
|
const cause = Object.assign(new Error("kernel report read failed"), {
|
|
code: "EIO",
|
|
});
|
|
kernel.readFailure = cause;
|
|
await expect(readLinuxResourceLimits(configured)).rejects.toBe(cause);
|
|
expect(kernel.reads).toHaveLength(2);
|
|
expect(kernel.handles.every((handle) => handle.fd === -1)).toBe(true);
|
|
});
|