fix(filesystem): complete plist and limits probes

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.
This commit is contained in:
morluto committed 2026-10-27 03:18:56 +08:00
1 parent 615fbbbdd7
commit 3ca1cc5466
4 files changed
+281 -14

No files matched your search

+99 -2
View File
@@ -1,5 +1,5 @@
import { expect, it, onTestFinished, vi } from "vitest";
import { writeFile } from "node:fs/promises";
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";
@@ -15,7 +15,16 @@ import {
readLinuxResourceLimits,
} from "./LinuxResourceLimits.js";
const kernel = vi.hoisted(() => ({ path: "", denied: false, opens: 0 }));
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 {
@@ -25,6 +34,21 @@ vi.mock("node:fs/promises", async (importOriginal) => {
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);
@@ -206,3 +230,76 @@ it("readmits analysis and resolves the original cleanup error after kernel descr
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);
});
+5 -7
View File
@@ -1,4 +1,5 @@
import { open } from "node:fs/promises";
import { readFileHandleRange } from "../filesystem/readFileHandleRange.js";
import { withArtifactFileHandle } from "../artifacts/ArtifactFileOperation.js";
import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
@@ -82,14 +83,11 @@ export const readLinuxResourceLimits = async (
resourceScope,
undefined,
async (handle) => {
const buffer = Buffer.alloc(16 * 1024);
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
if (bytesRead === buffer.length)
const reportBudget = 16 * 1024;
const buffer = await readFileHandleRange(handle, 0, reportBudget);
if (buffer.length === reportBudget)
throw new Error("Inherited Linux limits exceed the report budget.");
return clampLinuxResourceLimits(
configured,
buffer.subarray(0, bytesRead).toString("utf8"),
);
return clampLinuxResourceLimits(configured, buffer.toString("utf8"));
},
);
};