mirror of
https://github.com/morluto/rea
synced 2026-10-27 22:45:19 -04:00
3ca1cc5466
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.
456 lines
16 KiB
TypeScript
456 lines
16 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { writeFile, type FileHandle } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
|
|
|
const fault = vi.hoisted(() => ({
|
|
path: "",
|
|
maximum: Number.POSITIVE_INFINITY,
|
|
eof: Number.POSITIVE_INFINITY,
|
|
reads: 0,
|
|
onRead: undefined as (() => void) | undefined,
|
|
beforeRead: undefined as
|
|
| ((position: number, length: number) => void)
|
|
| undefined,
|
|
handles: [] as FileHandle[],
|
|
closeFailure: undefined as Error | undefined,
|
|
}));
|
|
vi.mock("node:fs/promises", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("node:fs/promises")>();
|
|
return {
|
|
...actual,
|
|
open: async (...args: Parameters<typeof actual.open>) => {
|
|
const handle = await actual.open(...args);
|
|
if (String(args[0]) !== fault.path) return handle;
|
|
fault.handles.push(handle);
|
|
const close = handle.close.bind(handle);
|
|
handle.close = async () => {
|
|
if (fault.closeFailure !== undefined) throw fault.closeFailure;
|
|
await close();
|
|
};
|
|
const read = handle.read.bind(handle);
|
|
handle.read = (async (...args: unknown[]) => {
|
|
const [buffer, offset, length, position] = args;
|
|
if (
|
|
typeof length !== "number" ||
|
|
(typeof position !== "number" && typeof position !== "bigint")
|
|
)
|
|
throw new Error("Expected a positional descriptor read");
|
|
fault.reads += 1;
|
|
fault.beforeRead?.(Number(position), length);
|
|
const result = await Reflect.apply(read, handle, [
|
|
buffer,
|
|
offset,
|
|
Math.max(
|
|
0,
|
|
Math.min(length, fault.maximum, fault.eof - Number(position)),
|
|
),
|
|
position,
|
|
]);
|
|
fault.onRead?.();
|
|
return result;
|
|
}) as FileHandle["read"];
|
|
return handle;
|
|
},
|
|
};
|
|
});
|
|
|
|
import { traceDylibResolution } from "../../src/artifacts/apple/DylibResolutionReader.js";
|
|
import { DylibFileAccess } from "../../src/artifacts/apple/DylibFileAccess.js";
|
|
import {
|
|
machoImage,
|
|
dylibCommand,
|
|
LC,
|
|
CPU,
|
|
fatImage,
|
|
} from "../../src/artifacts/apple/MachoImage.fixture.js";
|
|
import { ArtifactResourceScope } from "../../src/artifacts/ArtifactResourceScope.js";
|
|
import { readElectronFuseImage } from "../../src/electron/ElectronFuseReader.js";
|
|
import { ElectronFuseProvider } from "../../src/electron/ElectronFuseProvider.js";
|
|
import { RegularFileChangedError } from "../../src/filesystem/RegularFile.js";
|
|
import { AnalysisCancelledError } from "../../src/domain/analysisErrorCore.js";
|
|
import {
|
|
thinFuseImage,
|
|
OBSIDIAN_FUSE_BYTES,
|
|
} from "../fixtures/electron/fuseImage.js";
|
|
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
|
import { NativeMacOSProvider } from "../../src/native/NativeMacOSProvider.js";
|
|
import {
|
|
NativeFixtureRunner,
|
|
nativeMachoTarget,
|
|
} from "../fixtures/nativeCommands.js";
|
|
import { inspectPlistSchema } from "../../src/domain/native/nativeInspection.js";
|
|
import { plistSourceFormat } from "../../src/native/NativePlistSource.js";
|
|
|
|
const plistXml =
|
|
"<plist><dict><key>Count</key><integer>9007199254740993</integer><key>Data</key><data>AAE=</data></dict></plist>";
|
|
const nativePlistFixture = async (
|
|
bytes: Buffer,
|
|
description = "ASCII text",
|
|
) => {
|
|
const root = await createTestTempDirectory("rea-plist-short-reads-");
|
|
const path = join(root, "selected.plist");
|
|
await writeFile(path, bytes);
|
|
const commands: string[][] = [];
|
|
const runner = new NativeFixtureRunner({
|
|
file: description,
|
|
plutil: "valid",
|
|
});
|
|
const client = new NativeMacOSProvider(
|
|
{},
|
|
{
|
|
run: async (tool, args, options) => {
|
|
commands.push([tool, ...args]);
|
|
if (tool === "plutil" && args[0] !== "-lint")
|
|
throw new Error("Unexpected conversion of directly readable XML");
|
|
return runner.run(tool, args, options);
|
|
},
|
|
},
|
|
"darwin",
|
|
).createClient(nativeMachoTarget(join(root, "target")));
|
|
return { path, commands, client };
|
|
};
|
|
|
|
it.each([
|
|
["UTF8", Buffer.from(plistXml)],
|
|
["UTF16LE", Buffer.from("\ufeff" + plistXml, "utf16le")],
|
|
["UTF16BE", Buffer.from("\ufeff" + plistXml, "utf16le").swap16()],
|
|
] as const)(
|
|
"preserves %s plist source facts through short reads without conversion",
|
|
async (_name, bytes) => {
|
|
const { path, commands, client } = await nativePlistFixture(bytes);
|
|
fault.path = path;
|
|
fault.maximum = 1;
|
|
try {
|
|
const result = await client.execute("inspect_plist", { path });
|
|
if (!result.ok) throw result.error;
|
|
expect(inspectPlistSchema.parse(result.value.result)).toMatchObject({
|
|
format: "xml",
|
|
source_path: path,
|
|
value: {
|
|
Count: { $plist_type: "integer", decimal: "9007199254740993" },
|
|
Data: { $plist_type: "data", base64: "AAE=" },
|
|
},
|
|
});
|
|
expect(commands).toEqual([
|
|
["file", "-b", path],
|
|
["plutil", "-lint", "--", path],
|
|
]);
|
|
expect(fault.reads).toBeGreaterThan(bytes.length);
|
|
expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true);
|
|
} finally {
|
|
await client.close();
|
|
}
|
|
},
|
|
);
|
|
|
|
it("treats true EOF in an incomplete plist markup probe as unknown", async () => {
|
|
const { path, client } = await nativePlistFixture(Buffer.from("<"));
|
|
const resources = new ArtifactResourceScope();
|
|
fault.path = path;
|
|
fault.maximum = 1;
|
|
try {
|
|
expect(
|
|
await plistSourceFormat("ASCII text", {
|
|
selection: { path, origin: "selected" },
|
|
resources,
|
|
signal: undefined,
|
|
}),
|
|
).toEqual({ ok: true, value: "unknown" });
|
|
expect(fault.reads).toBe(2);
|
|
expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true);
|
|
} finally {
|
|
await resources.close();
|
|
await client.close();
|
|
}
|
|
});
|
|
|
|
it("preserves a native plist probe read denial over a simultaneous abort", async () => {
|
|
const { path, client } = await nativePlistFixture(Buffer.from(plistXml));
|
|
const controller = new AbortController();
|
|
const physical = Object.assign(new Error("controlled plist read denial"), {
|
|
code: "EACCES",
|
|
});
|
|
fault.path = path;
|
|
fault.maximum = 1;
|
|
fault.beforeRead = () => {
|
|
if (fault.reads !== 2) return;
|
|
controller.abort("simultaneous abort");
|
|
throw physical;
|
|
};
|
|
try {
|
|
const result = await client.execute(
|
|
"inspect_plist",
|
|
{ path },
|
|
{ signal: controller.signal },
|
|
);
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) throw new Error("Expected native plist read denial");
|
|
expect(result.error).not.toBeInstanceOf(AnalysisCancelledError);
|
|
expect(result.error.cause).toBe(physical);
|
|
expect(fault.reads).toBe(2);
|
|
expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true);
|
|
} finally {
|
|
await client.close();
|
|
}
|
|
});
|
|
|
|
it.each([false, true])(
|
|
"preserves plist probe cancellation and descriptor cleanup (closeFails=%s)",
|
|
async (closeFails) => {
|
|
const { path, client } = await nativePlistFixture(Buffer.from(plistXml));
|
|
const controller = new AbortController();
|
|
const reason = { requested: "continued plist probe" };
|
|
fault.path = path;
|
|
fault.maximum = 1;
|
|
fault.onRead = () => {
|
|
if (fault.reads !== 2) return;
|
|
controller.abort(reason);
|
|
if (closeFails)
|
|
fault.closeFailure = new Error("controlled plist close failure");
|
|
};
|
|
try {
|
|
const result = await client.execute(
|
|
"inspect_plist",
|
|
{ path },
|
|
{ signal: controller.signal },
|
|
);
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) throw new Error("Expected plist probe cancellation");
|
|
const primary = closeFails ? result.error.cleanupPrimary : result.error;
|
|
expect(primary).toBeInstanceOf(AnalysisCancelledError);
|
|
expect(primary?.cause).toBe(reason);
|
|
expect(fault.reads).toBe(2);
|
|
if (closeFails) {
|
|
expect(result.error.cleanupIncomplete).toBe(true);
|
|
expect(fault.handles.at(-1)?.fd).toBeGreaterThanOrEqual(0);
|
|
fault.closeFailure = undefined;
|
|
expect(await client.close()).toEqual({ ok: true, value: null });
|
|
expect(result.error.cleanupIncomplete).toBe(false);
|
|
}
|
|
expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true);
|
|
} finally {
|
|
fault.closeFailure = undefined;
|
|
await client.close();
|
|
}
|
|
},
|
|
);
|
|
import { parseBinaryTarget } from "../../src/application/BinaryTargetResolver.js";
|
|
import { elf, pe, dosMz } from "../../src/domain/binaryTarget.fixture.js";
|
|
|
|
const distantCommands = () => {
|
|
const bytes = Buffer.alloc(32 + 4080 + 16);
|
|
Buffer.from(machoImage({})).copy(bytes);
|
|
bytes.writeUInt32LE(2, 16);
|
|
bytes.writeUInt32LE(4096, 20);
|
|
bytes.writeUInt32LE(0x7fffffff, 32);
|
|
bytes.writeUInt32LE(4080, 36);
|
|
bytes.writeUInt32LE(0x7fffffff, 4112);
|
|
bytes.writeUInt32LE(16, 4116);
|
|
return bytes;
|
|
};
|
|
|
|
it.each([
|
|
["ELF", () => elf(2, 1, 62)],
|
|
["DOS MZ", () => dosMz()],
|
|
[
|
|
"PE CodeView",
|
|
() => pe(0x8664, 64, 2, false, { pdbPath: "short.pdb", age: 3 }),
|
|
],
|
|
["distant PE", () => pe(0x8664, 8192)],
|
|
["thin command windows", distantCommands],
|
|
[
|
|
"FAT slice command windows",
|
|
() => fatImage([{ cpu: CPU.arm64, bytes: distantCommands() }]),
|
|
],
|
|
] as const)(
|
|
"retains complete %s target admission under positive short reads",
|
|
async (_label, fixture) => {
|
|
const root = await createTestTempDirectory("rea-target-short-reads-");
|
|
const path = join(root, "selected");
|
|
await writeFile(path, fixture());
|
|
const expected = await parseBinaryTarget(path, {
|
|
cwd: root,
|
|
hostArchitecture: "arm64",
|
|
});
|
|
expect(expected.ok).toBe(true);
|
|
fault.path = path;
|
|
fault.maximum = 3;
|
|
expect(
|
|
await parseBinaryTarget(path, { cwd: root, hostArchitecture: "arm64" }),
|
|
).toEqual(expected);
|
|
expect(fault.reads).toBeGreaterThan(5);
|
|
expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true);
|
|
},
|
|
);
|
|
|
|
beforeEach(() => {
|
|
fault.path = "";
|
|
fault.maximum = Number.POSITIVE_INFINITY;
|
|
fault.eof = Number.POSITIVE_INFINITY;
|
|
fault.reads = 0;
|
|
fault.onRead = undefined;
|
|
fault.beforeRead = undefined;
|
|
fault.handles = [];
|
|
fault.closeFailure = undefined;
|
|
});
|
|
afterEach(async () => {
|
|
fault.closeFailure = undefined;
|
|
fault.onRead = undefined;
|
|
fault.beforeRead = undefined;
|
|
for (const handle of fault.handles)
|
|
if (handle.fd !== -1) await handle.close();
|
|
});
|
|
|
|
it("keeps dyld root discovery, commands and digests when positive reads are short", async () => {
|
|
const root = await createTestTempDirectory("rea-dyld-short-reads-");
|
|
const path = join(root, "Main");
|
|
const bytes = machoImage({
|
|
commands: [dylibCommand(LC.LOAD_DYLIB, "@rpath/Absent.dylib")],
|
|
});
|
|
await writeFile(path, bytes);
|
|
const trace = () =>
|
|
traceDylibResolution({
|
|
rootPath: root,
|
|
targetPath: path,
|
|
targetSha256: createHash("sha256").update(bytes).digest("hex"),
|
|
enumerateRoots: true,
|
|
parameters: {},
|
|
});
|
|
const expected = await trace();
|
|
expect(expected.roots).toHaveLength(1);
|
|
expect(expected.edges).toHaveLength(1);
|
|
fault.path = path;
|
|
fault.maximum = 2;
|
|
expect(await trace()).toEqual(expected);
|
|
expect(fault.reads).toBeGreaterThan(10);
|
|
expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true);
|
|
});
|
|
|
|
it("keeps fuse sentinel offsets, states and complete-file digest across short reads", async () => {
|
|
const root = await createTestTempDirectory("rea-fuse-short-reads-");
|
|
const path = join(root, "Electron");
|
|
const { bytes } = thinFuseImage(OBSIDIAN_FUSE_BYTES);
|
|
await writeFile(path, bytes);
|
|
const provider = new ElectronFuseProvider();
|
|
try {
|
|
const expected = await provider.inspect({ path });
|
|
expect(expected.ok).toBe(true);
|
|
fault.path = path;
|
|
fault.maximum = 2;
|
|
expect(await provider.inspect({ path })).toEqual(expected);
|
|
const scanned = await readElectronFuseImage(path, { chunkBytes: 7 });
|
|
expect(scanned.sha256).toBe(
|
|
createHash("sha256").update(bytes).digest("hex"),
|
|
);
|
|
expect(scanned.slices[0]?.sentinels[0]?.sentinel_offset).toBe(32);
|
|
expect(fault.reads).toBeGreaterThan(10);
|
|
expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true);
|
|
} finally {
|
|
await provider.close();
|
|
}
|
|
});
|
|
|
|
it("returns malformed dyld header facts when short positive data ends at true EOF", async () => {
|
|
const root = await createTestTempDirectory("rea-dyld-read-eof-");
|
|
const path = join(root, "Main");
|
|
await writeFile(path, machoImage({}));
|
|
fault.path = path;
|
|
fault.maximum = 2;
|
|
fault.eof = 6;
|
|
const resources = new ArtifactResourceScope();
|
|
try {
|
|
const image = await new DylibFileAccess(resources, root).image(path);
|
|
expect(image.facts).toMatchObject({ status: "malformed" });
|
|
expect(fault.reads).toBeGreaterThan(1);
|
|
expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true);
|
|
} finally {
|
|
await resources.close();
|
|
}
|
|
});
|
|
|
|
it("rejects true EOF before the admitted Electron extent instead of publishing a digest", async () => {
|
|
const root = await createTestTempDirectory("rea-fuse-read-eof-");
|
|
const path = join(root, "Electron");
|
|
await writeFile(path, thinFuseImage(OBSIDIAN_FUSE_BYTES).bytes);
|
|
fault.path = path;
|
|
fault.maximum = 2;
|
|
fault.eof = 6;
|
|
await expect(readElectronFuseImage(path)).rejects.toBeInstanceOf(
|
|
RegularFileChangedError,
|
|
);
|
|
expect(fault.reads).toBeGreaterThan(1);
|
|
expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true);
|
|
});
|
|
|
|
it("preserves cancellation during continued short reads and releases the descriptor", async () => {
|
|
const root = await createTestTempDirectory("rea-fuse-short-read-cancel-");
|
|
const path = join(root, "Electron");
|
|
await writeFile(path, thinFuseImage(OBSIDIAN_FUSE_BYTES).bytes);
|
|
const controller = new AbortController();
|
|
fault.path = path;
|
|
fault.maximum = 2;
|
|
fault.onRead = () => {
|
|
if (fault.reads === 2) controller.abort(new Error("cancel continued read"));
|
|
};
|
|
const provider = new ElectronFuseProvider();
|
|
try {
|
|
const result = await provider.inspect(
|
|
{ path },
|
|
{ signal: controller.signal },
|
|
);
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) throw new Error("Expected cancellation");
|
|
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
|
|
expect(fault.reads).toBe(2);
|
|
expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true);
|
|
} finally {
|
|
await provider.close();
|
|
}
|
|
});
|
|
|
|
it.each(["cancelled", "eof", "io"] as const)(
|
|
"preserves %s during short scan reads after container identification",
|
|
async (outcome) => {
|
|
const root = await createTestTempDirectory("rea-fuse-scan-short-read-");
|
|
const path = join(root, "Electron");
|
|
await writeFile(path, thinFuseImage(OBSIDIAN_FUSE_BYTES).bytes);
|
|
const controller = new AbortController();
|
|
const physicalFailure = Object.assign(new Error("scan read failed"), {
|
|
code: "EIO",
|
|
});
|
|
let scanning = false;
|
|
const scanPositions: number[] = [];
|
|
fault.path = path;
|
|
fault.beforeRead = (position, length) => {
|
|
// Header reads use their format windows. A seven-byte read at zero
|
|
// starts this invocation's explicitly selected scan window.
|
|
if (position === 0 && length === 7) {
|
|
scanning = true;
|
|
fault.maximum = 2;
|
|
}
|
|
if (!scanning) return;
|
|
scanPositions.push(position);
|
|
if (position !== 4) return;
|
|
if (outcome === "eof") fault.eof = 4;
|
|
fault.onRead = () => controller.abort(new Error("cancel scan read"));
|
|
if (outcome === "io") {
|
|
controller.abort(new Error("cancel failed scan read"));
|
|
throw physicalFailure;
|
|
}
|
|
};
|
|
const failure: unknown = await readElectronFuseImage(path, {
|
|
signal: controller.signal,
|
|
chunkBytes: 7,
|
|
}).catch((cause: unknown) => cause);
|
|
expect(scanning).toBe(true);
|
|
expect(scanPositions).toEqual([0, 2, 4]);
|
|
if (outcome === "io") expect(failure).toBe(physicalFailure);
|
|
else if (outcome === "eof")
|
|
expect(failure).toBeInstanceOf(RegularFileChangedError);
|
|
else expect(failure).toMatchObject({ reason: "cancelled" });
|
|
expect(controller.signal.aborted).toBe(true);
|
|
expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true);
|
|
},
|
|
);
|