mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 18:52:48 +00:00
feat(analysis): make resource budgets recoverable across CLI and MCP (#9706)
* refactor(analysis): centralize resource budgets and recovery facts * feat(analysis): expose ASAR and Ghidra runtime admission controls * feat(artifacts): expose reader admission and preserve limited recognition * feat(javascript): expose recoverable semantic precision budgets * feat(web): add recoverable source-map budgets and compact retained views * feat(web): make historical capture decoding budgets recoverable * fix(desktop): enforce selected view policies across capture and export * feat(runtime): make Frida and LLDB retention budgets selectable * feat(native): expose diagnostic budgets and preserve exhaustion causes * feat(managed): expose recoverable snapshot and representation budgets * feat(metadata): make Go and Rust inspection budgets recoverable * feat(wasm): expose representation and producer admissions * feat(evm): expose per-call worker resource budgets * feat(android): make resource and string-pool omissions recoverable * feat(producers): expose command and recorded-capture budgets * docs(analysis): record recovery contracts and integrated validation * fix(docs): link calibration oracle from published verification pages * fix(docs): restore desktop task anchors and receipt links * test(analysis): align fixtures with selectable budget contracts * fix(analysis): remove source-location projector dependency cycle * test(analysis): refresh budget recovery and verifier regressions * test(analysis): preserve format and publication recovery expectations * test(windows): align conformance with owned runtime admission * fix(desktop): preserve selected alias kinds in exported state * test(analysis): align provider fixtures with recoverable budgets * test(desktop): validate emitted alias metadata structurally * fix(ghidra): classify bounded Windows endpoint refusals * test(cli): assert selectable artifact admission recovery * fix(cli): preserve capture provenance in environment builds
This commit is contained in:
1 parent
55b6d2ca20
commit
cb4e5cfbff
515 files changed
+21066
-3637
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import type { StableRegularFileDescriptor } from "../../filesystem/RegularFile.js";
|
||||
import type { DecodedNodeSeaBlob } from "./NodeSeaBlob.js";
|
||||
import type { NodeSeaLocation } from "./NodeSeaLocation.js";
|
||||
import type { NodeSeaReaderLimits } from "../../domain/artifactReaderLimits.js";
|
||||
|
||||
/** Validated blob facts borrowed with the exact descriptor that produced them. */
|
||||
export interface NodeSeaAdmission {
|
||||
@@ -8,4 +9,6 @@ export interface NodeSeaAdmission {
|
||||
readonly source: StableRegularFileDescriptor;
|
||||
readonly location: NodeSeaLocation;
|
||||
readonly decoded: DecodedNodeSeaBlob;
|
||||
/** Accepted producing policy; older admissions require fresh admission. */
|
||||
readonly limits?: NodeSeaReaderLimits;
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { setImmediate } from "node:timers/promises";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { ArtifactReaderFailure } from "../ArtifactReader.js";
|
||||
import { DEFAULT_NODE_SEA_READER_LIMITS } from "../../domain/artifactReaderLimits.js";
|
||||
import {
|
||||
NODE_SEA_MAGIC,
|
||||
decodeNodeSeaBlob,
|
||||
@@ -134,80 +134,83 @@ it.each([
|
||||
},
|
||||
);
|
||||
|
||||
const limitOf = async (bytes: Buffer): Promise<ArtifactReaderFailure> => {
|
||||
try {
|
||||
await decodeNodeSeaBlob(bytes);
|
||||
} catch (cause) {
|
||||
if (cause instanceof ArtifactReaderFailure) return cause;
|
||||
throw cause;
|
||||
}
|
||||
throw new Error("expected a limit failure");
|
||||
};
|
||||
|
||||
it("refuses an asset table over its count bound before creating records", async () => {
|
||||
const assets = Array.from({ length: 100_001 }, (_, index) => ({
|
||||
name: `asset-${String(index)}`,
|
||||
body: "",
|
||||
}));
|
||||
const failure = await limitOf(buildNodeSeaBlob({ assets }));
|
||||
expect(failure).toMatchObject({
|
||||
reason: "limit",
|
||||
message: expect.stringMatching(
|
||||
/asset count.*100000.*no runtime override/iu,
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
it("refuses asset names over the decoded name budget", async () => {
|
||||
const name = "n".repeat(4 * 1024 * 1024 + 1);
|
||||
const failure = await limitOf(
|
||||
buildNodeSeaBlob({
|
||||
assets: [
|
||||
{ name: `a${name}`, body: "" },
|
||||
{ name: `b${name}`, body: "" },
|
||||
],
|
||||
it("rejects impossible asset rows before applying resource admission", async () => {
|
||||
const prefix = buildNodeSeaBlob({ assets: [{ name: "a", body: "" }] });
|
||||
const count = Buffer.alloc(8);
|
||||
count.writeBigUInt64LE(2n);
|
||||
const bytes = Buffer.concat([prefix.subarray(0, -25), count]);
|
||||
await expect(
|
||||
decodeNodeSeaBlob(bytes, undefined, {
|
||||
...DEFAULT_NODE_SEA_READER_LIMITS,
|
||||
asset_count: 1,
|
||||
}),
|
||||
);
|
||||
expect(failure).toMatchObject({
|
||||
reason: "limit",
|
||||
message: expect.stringMatching(
|
||||
/asset names.*8388608.*no runtime override/iu,
|
||||
),
|
||||
).rejects.toMatchObject({
|
||||
reason: "format",
|
||||
message: expect.stringMatching(/Node SEA.*truncated/iu),
|
||||
readerLimit: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
label: "count",
|
||||
accepted: () => Array.from({ length: 4096 }, () => "x"),
|
||||
args: () => Array.from({ length: 4097 }, () => "x"),
|
||||
boundary: "capacity",
|
||||
warning: /exec argv count.*4096.*no runtime override/iu,
|
||||
},
|
||||
{
|
||||
label: "text",
|
||||
accepted: () => ["x".repeat(1024 * 1024)],
|
||||
args: () => ["x".repeat(1024 * 1024 + 1)],
|
||||
boundary: "memory",
|
||||
warning: /exec argv text.*1048576.*no runtime override/iu,
|
||||
},
|
||||
{ field: "asset_count" as const, limit: 1, raised: 2 },
|
||||
{ field: "asset_name_bytes" as const, limit: 2, raised: 3 },
|
||||
{ field: "argument_count" as const, limit: 1, raised: 2 },
|
||||
{ field: "argument_bytes" as const, limit: 2, raised: 3 },
|
||||
])(
|
||||
"admits exec argument $label at its bound and refuses more",
|
||||
async ({ accepted, args, boundary, warning }) => {
|
||||
const atLimit = accepted();
|
||||
expect(
|
||||
(await decodeNodeSeaBlob(buildNodeSeaBlob({ execArgv: atLimit })))
|
||||
.execArgv,
|
||||
).toEqual(atLimit);
|
||||
const failure = await limitOf(buildNodeSeaBlob({ execArgv: args() }));
|
||||
expect(failure).toMatchObject({
|
||||
reason: "limit",
|
||||
boundary,
|
||||
message: expect.stringMatching(warning),
|
||||
"recovers the same blob after raising $field",
|
||||
async ({ field, limit, raised }) => {
|
||||
const bytes = buildNodeSeaBlob({
|
||||
assets: [
|
||||
{ name: "a", body: "one" },
|
||||
{ name: "bb", body: "two" },
|
||||
],
|
||||
execArgv: ["x", "yz"],
|
||||
});
|
||||
await expect(
|
||||
decodeNodeSeaBlob(bytes, undefined, {
|
||||
...DEFAULT_NODE_SEA_READER_LIMITS,
|
||||
[field]: limit,
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
reason: "limit",
|
||||
readerLimit: {
|
||||
parameter: `artifact_limits.node_sea.${field}`,
|
||||
effective_value: limit,
|
||||
},
|
||||
});
|
||||
const recovered = await decodeNodeSeaBlob(bytes, undefined, {
|
||||
...DEFAULT_NODE_SEA_READER_LIMITS,
|
||||
[field]: raised,
|
||||
});
|
||||
expect(recovered.assets.map((asset) => asset.name)).toEqual(["a", "bb"]);
|
||||
expect(recovered.execArgv).toEqual(["x", "yz"]);
|
||||
},
|
||||
);
|
||||
|
||||
it("applies blob byte admission even when called without a carrier reader", async () => {
|
||||
const bytes = buildNodeSeaBlob({ main: "hello" });
|
||||
await expect(
|
||||
decodeNodeSeaBlob(bytes, undefined, {
|
||||
...DEFAULT_NODE_SEA_READER_LIMITS,
|
||||
blob_bytes: bytes.length - 1,
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
reason: "limit",
|
||||
readerLimit: {
|
||||
parameter: "artifact_limits.node_sea.blob_bytes",
|
||||
required_value: bytes.length,
|
||||
},
|
||||
});
|
||||
expect(
|
||||
(
|
||||
await decodeNodeSeaBlob(bytes, undefined, {
|
||||
...DEFAULT_NODE_SEA_READER_LIMITS,
|
||||
blob_bytes: bytes.length,
|
||||
})
|
||||
).mainScript?.length,
|
||||
).toBe(5);
|
||||
});
|
||||
|
||||
it("rejects non-zero trailing bytes and keeps zero padding", async () => {
|
||||
const padded = buildNodeSeaBlob({ trailing: Buffer.alloc(8) });
|
||||
expect((await decodeNodeSeaBlob(padded)).codePath).toBe("sea.js");
|
||||
|
||||
@@ -2,13 +2,15 @@ import { setImmediate } from "node:timers/promises";
|
||||
|
||||
import { assertArtifactReadActive } from "../ArtifactFileOperation.js";
|
||||
import { ArtifactReaderFailure } from "../ArtifactReader.js";
|
||||
import {
|
||||
DEFAULT_NODE_SEA_READER_LIMITS,
|
||||
type NodeSeaReaderLimits,
|
||||
} from "../../domain/artifactReaderLimits.js";
|
||||
import { artifactReaderLimitFailure } from "../ArtifactReaderLimits.js";
|
||||
|
||||
/** Node `SeaBlob::kMagic` from `src/node_sea.h` (v24). */
|
||||
export const NODE_SEA_MAGIC = 0x143da20;
|
||||
|
||||
/** In-memory cap for one injected blob. Larger declarations fail closed. */
|
||||
export const NODE_SEA_MAX_BLOB_BYTES = 256 * 1024 * 1024;
|
||||
|
||||
const FLAG_DISABLE_EXPERIMENTAL_SEA_WARNING = 1 << 0;
|
||||
const FLAG_USE_SNAPSHOT = 1 << 1;
|
||||
const FLAG_USE_CODE_CACHE = 1 << 2;
|
||||
@@ -21,15 +23,6 @@ const KNOWN_FLAGS =
|
||||
FLAG_INCLUDE_ASSETS |
|
||||
FLAG_INCLUDE_EXEC_ARGV;
|
||||
|
||||
// Resource bounds checked before records are created, not Node format limits.
|
||||
// Asset names are budgeted separately because empty bodies make each record
|
||||
// cost far more retained memory than its blob bytes.
|
||||
const MAX_ASSET_COUNT = 100_000;
|
||||
const MAX_ASSET_NAME_BYTES = 8 * 1024 * 1024;
|
||||
const MAX_EXEC_ARGV = 4096;
|
||||
const MAX_EXEC_ARGV_TEXT_BYTES = 1024 * 1024;
|
||||
const NO_OVERRIDE = "This release has no runtime override.";
|
||||
|
||||
const NODE_SEA_BAD_MAGIC = "Node SEA blob magic is not 0x143da20";
|
||||
export const NODE_SEA_TRUNCATED = "Node SEA blob is truncated";
|
||||
const NODE_SEA_UNKNOWN_FLAGS = "Node SEA blob flags contain unknown bits";
|
||||
@@ -54,11 +47,6 @@ const NODE_SEA_FUSE_UNSET =
|
||||
const NODE_SEA_FUSE_CONFLICT = "Node SEA fuse sentinels disagree";
|
||||
const NODE_SEA_EMPTY_CODE_PATH = "Node SEA code path is empty";
|
||||
const NODE_SEA_INVALID_UTF8 = "Node SEA string is not valid UTF-8";
|
||||
const NODE_SEA_ASSET_COUNT = `Node SEA asset count exceeds the ${String(MAX_ASSET_COUNT)}-asset bound. ${NO_OVERRIDE}`;
|
||||
const NODE_SEA_ASSET_NAME_BYTES = `Node SEA asset names exceed the ${String(MAX_ASSET_NAME_BYTES)}-byte name budget. ${NO_OVERRIDE}`;
|
||||
const NODE_SEA_EXEC_ARGV_COUNT = `Node SEA exec argv count exceeds the ${String(MAX_EXEC_ARGV)}-argument bound. ${NO_OVERRIDE}`;
|
||||
const NODE_SEA_EXEC_ARGV_TEXT = `Node SEA exec argv text exceeds the ${String(MAX_EXEC_ARGV_TEXT_BYTES)}-byte bound. ${NO_OVERRIDE}`;
|
||||
export const NODE_SEA_BLOB_TOO_LARGE = `Node SEA blob exceeds the ${String(NODE_SEA_MAX_BLOB_BYTES)}-byte in-memory bound`;
|
||||
|
||||
const FUSE_PREFIX = "NODE_SEA_FUSE_";
|
||||
|
||||
@@ -98,12 +86,6 @@ export interface NodeSeaFuseObservation {
|
||||
readonly fuseContradiction: string | null;
|
||||
}
|
||||
|
||||
const limitFailure = (
|
||||
message: string,
|
||||
boundary: "capacity" | "memory",
|
||||
): ArtifactReaderFailure =>
|
||||
new ArtifactReaderFailure("limit", message, { boundary });
|
||||
|
||||
const formatFailure = (
|
||||
message: string,
|
||||
cause?: unknown,
|
||||
@@ -123,9 +105,18 @@ const formatFailure = (
|
||||
export const decodeNodeSeaBlob = async (
|
||||
bytes: Buffer,
|
||||
signal?: AbortSignal,
|
||||
limits: NodeSeaReaderLimits = DEFAULT_NODE_SEA_READER_LIMITS,
|
||||
): Promise<DecodedNodeSeaBlob> => {
|
||||
assertArtifactReadActive(signal);
|
||||
const cursor = new BlobCursor(bytes);
|
||||
if (bytes.length > limits.blob_bytes)
|
||||
throw artifactReaderLimitFailure(
|
||||
"artifact_limits.node_sea.blob_bytes",
|
||||
limits.blob_bytes,
|
||||
"bytes",
|
||||
bytes.length,
|
||||
"memory",
|
||||
);
|
||||
const cursor = new BlobCursor(bytes, limits);
|
||||
if (cursor.remaining < 4) throw formatFailure(NODE_SEA_TRUNCATED);
|
||||
const magic = cursor.u32();
|
||||
if (magic !== NODE_SEA_MAGIC) throw formatFailure(NODE_SEA_BAD_MAGIC);
|
||||
@@ -212,7 +203,10 @@ const extensionName = (value: number): NodeSeaExecArgvExtension => {
|
||||
class BlobCursor {
|
||||
#offset = 0;
|
||||
|
||||
constructor(private readonly bytes: Buffer) {}
|
||||
constructor(
|
||||
private readonly bytes: Buffer,
|
||||
private readonly limits: NodeSeaReaderLimits,
|
||||
) {}
|
||||
|
||||
get remaining(): number {
|
||||
return this.bytes.length - this.#offset;
|
||||
@@ -241,6 +235,12 @@ class BlobCursor {
|
||||
raw,
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
if (cause instanceof RangeError)
|
||||
throw new ArtifactReaderFailure(
|
||||
"limit",
|
||||
"Node SEA UTF-8 text exceeds this runtime's string or memory capacity. Admission budgets do not raise runtime representation limits or reserve host memory.",
|
||||
{ boundary: "memory", cause },
|
||||
);
|
||||
throw formatFailure(NODE_SEA_INVALID_UTF8, cause);
|
||||
}
|
||||
if (!allowEmpty && text.length === 0) throw formatFailure(emptyMessage);
|
||||
@@ -255,14 +255,20 @@ class BlobCursor {
|
||||
}
|
||||
|
||||
async assets(signal?: AbortSignal): Promise<NodeSeaAsset[]> {
|
||||
const count = this.#count(MAX_ASSET_COUNT, 16, NODE_SEA_ASSET_COUNT);
|
||||
const count = this.#count("asset_count", 16);
|
||||
const seen = new Set<string>();
|
||||
const assets: NodeSeaAsset[] = [];
|
||||
let nameBytes = 0;
|
||||
const charge = (bytes: number): void => {
|
||||
nameBytes += bytes;
|
||||
if (nameBytes > MAX_ASSET_NAME_BYTES)
|
||||
throw limitFailure(NODE_SEA_ASSET_NAME_BYTES, "memory");
|
||||
if (nameBytes > this.limits.asset_name_bytes)
|
||||
throw artifactReaderLimitFailure(
|
||||
"artifact_limits.node_sea.asset_name_bytes",
|
||||
this.limits.asset_name_bytes,
|
||||
"bytes",
|
||||
nameBytes,
|
||||
"memory",
|
||||
);
|
||||
};
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
if (index % 256 === 0) {
|
||||
@@ -279,13 +285,19 @@ class BlobCursor {
|
||||
}
|
||||
|
||||
async execArgv(signal?: AbortSignal): Promise<string[]> {
|
||||
const count = this.#count(MAX_EXEC_ARGV, 8, NODE_SEA_EXEC_ARGV_COUNT);
|
||||
const count = this.#count("argument_count", 8);
|
||||
const values: string[] = [];
|
||||
let textBytes = 0;
|
||||
const charge = (bytes: number): void => {
|
||||
textBytes += bytes;
|
||||
if (textBytes > MAX_EXEC_ARGV_TEXT_BYTES)
|
||||
throw limitFailure(NODE_SEA_EXEC_ARGV_TEXT, "memory");
|
||||
if (textBytes > this.limits.argument_bytes)
|
||||
throw artifactReaderLimitFailure(
|
||||
"artifact_limits.node_sea.argument_bytes",
|
||||
this.limits.argument_bytes,
|
||||
"bytes",
|
||||
textBytes,
|
||||
"memory",
|
||||
);
|
||||
};
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
if (index % 256 === 0) {
|
||||
@@ -324,13 +336,22 @@ class BlobCursor {
|
||||
return Number(value);
|
||||
}
|
||||
|
||||
#count(limit: number, minimumRowBytes: number, message: string): number {
|
||||
#count(
|
||||
field: "asset_count" | "argument_count",
|
||||
minimumRowBytes: number,
|
||||
): number {
|
||||
if (this.remaining < 8) throw formatFailure(NODE_SEA_TRUNCATED);
|
||||
const value = this.#demand(8).readBigUInt64LE(0);
|
||||
if (value > BigInt(Math.floor(this.remaining / minimumRowBytes)))
|
||||
throw formatFailure(NODE_SEA_TRUNCATED);
|
||||
const count = Number(value);
|
||||
if (count > limit) throw limitFailure(message, "capacity");
|
||||
if (count > this.limits[field])
|
||||
throw artifactReaderLimitFailure(
|
||||
`artifact_limits.node_sea.${field}`,
|
||||
this.limits[field],
|
||||
"count",
|
||||
count,
|
||||
);
|
||||
return count;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { readElfSectionHeader } from "../../domain/elfSectionHeader.js";
|
||||
import { readElfProgramHeader } from "../../domain/elfProgramHeader.js";
|
||||
import { readElfRawTableDeclarations } from "../../domain/elfRawTableDeclarations.js";
|
||||
import { createHash } from "node:crypto";
|
||||
import { constants as bufferConstants } from "node:buffer";
|
||||
import { parsePeResources } from "../../native/pe/PeResourceParser.js";
|
||||
import {
|
||||
hasPeResourceName,
|
||||
@@ -14,9 +15,15 @@ import {
|
||||
MAX_INSPECT_PE_DIRECTORY_ENTRIES,
|
||||
} from "../../domain/native/peResources.js";
|
||||
import type { FileHandle } from "node:fs/promises";
|
||||
import type { StableRegularFileDescriptor } from "../../filesystem/RegularFile.js";
|
||||
|
||||
import { abortIfNeeded } from "../ArtifactHash.js";
|
||||
import { ArtifactReaderFailure } from "../ArtifactReader.js";
|
||||
import {
|
||||
DEFAULT_NODE_SEA_READER_LIMITS,
|
||||
type NodeSeaReaderLimits,
|
||||
} from "../../domain/artifactReaderLimits.js";
|
||||
import { artifactReaderLimitFailure } from "../ArtifactReaderLimits.js";
|
||||
import {
|
||||
readFatContainerHeader,
|
||||
readFatHeaderPrefix,
|
||||
@@ -30,14 +37,12 @@ import {
|
||||
readElfNoteHeader,
|
||||
} from "../../domain/native/elfNoteLayout.js";
|
||||
import {
|
||||
NODE_SEA_BLOB_TOO_LARGE,
|
||||
NODE_SEA_DUPLICATE_BLOB,
|
||||
NODE_SEA_ELF_DESCRIPTOR_TRUNCATED,
|
||||
NODE_SEA_ELF_NOTES_REPEATED,
|
||||
NODE_SEA_ELF_TABLE_TRUNCATED,
|
||||
NODE_SEA_MACHO_COMMANDS_TRUNCATED,
|
||||
NODE_SEA_MACHO_SEGMENT_TRUNCATED,
|
||||
NODE_SEA_MAX_BLOB_BYTES,
|
||||
NODE_SEA_TRUNCATED,
|
||||
observeNodeSeaFuseMatches,
|
||||
type NodeSeaFuseObservation,
|
||||
@@ -50,9 +55,6 @@ const SHT_NOTE = 7;
|
||||
const RT_RCDATA = 10;
|
||||
const RESOURCE_NAME = "NODE_SEA_BLOB";
|
||||
const SEA_SECTION = "__NODE_SEA_BLOB";
|
||||
const MAX_COMMAND_BYTES = 8 * 1024 * 1024;
|
||||
const MAX_ELF_HEADERS = 4096;
|
||||
const MAX_NOTE_NAME_BYTES = 4096;
|
||||
const FUSE_LENGTH = "NODE_SEA_FUSE_".length + 32 + 2;
|
||||
// PE header structures must fit this prefix for the bounded name probe.
|
||||
const PE_HEADER_PROBE_BYTES = 64 * 1024;
|
||||
@@ -73,6 +75,7 @@ interface Carrier {
|
||||
readonly handle: FileHandle;
|
||||
readonly fileSize: number;
|
||||
readonly signal: AbortSignal | undefined;
|
||||
readonly limits?: NodeSeaReaderLimits;
|
||||
}
|
||||
|
||||
const formatFailure = (message: string): ArtifactReaderFailure =>
|
||||
@@ -81,6 +84,22 @@ const formatFailure = (message: string): ArtifactReaderFailure =>
|
||||
const limitFailure = (message: string): ArtifactReaderFailure =>
|
||||
new ArtifactReaderFailure("limit", message, { boundary: "memory" });
|
||||
|
||||
const admitBlobBytes = (carrier: Carrier, required: number): void => {
|
||||
const limits = carrier.limits ?? DEFAULT_NODE_SEA_READER_LIMITS;
|
||||
if (required > limits.blob_bytes)
|
||||
throw artifactReaderLimitFailure(
|
||||
"artifact_limits.node_sea.blob_bytes",
|
||||
limits.blob_bytes,
|
||||
"bytes",
|
||||
required,
|
||||
"memory",
|
||||
);
|
||||
if (required > bufferConstants.MAX_LENGTH)
|
||||
throw limitFailure(
|
||||
`Node SEA blob requires ${String(required)} bytes, exceeding this runtime's ${String(bufferConstants.MAX_LENGTH)}-byte Buffer representation limit; increasing artifact_limits.node_sea.blob_bytes cannot raise that runtime limit`,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Find a postject `NODE_SEA_BLOB` slot. Mach-O accepts segment `NODE_SEA`
|
||||
* (the postject name) and `__NODE_SEA`. ELF matches the full note owner
|
||||
@@ -93,9 +112,10 @@ export const locateNodeSea = async (
|
||||
handle: FileHandle,
|
||||
fileSize: number,
|
||||
signal?: AbortSignal,
|
||||
limits: NodeSeaReaderLimits = DEFAULT_NODE_SEA_READER_LIMITS,
|
||||
): Promise<NodeSeaLocation | undefined> => {
|
||||
if (!Number.isSafeInteger(fileSize) || fileSize < 4) return undefined;
|
||||
const carrier: Carrier = { handle, fileSize, signal };
|
||||
const carrier: Carrier = { handle, fileSize, signal, limits };
|
||||
const prefix = await readExact(carrier, 0, Math.min(fileSize, 64));
|
||||
if (prefix === undefined || prefix.length < 4) return undefined;
|
||||
const thin = readMachoHeaderPrefixFacts(prefix).format;
|
||||
@@ -120,13 +140,16 @@ export const locateNodeSea = async (
|
||||
|
||||
/** Read a located blob, refusing declarations above the in-memory cap. */
|
||||
export const readNodeSeaBlob = async (
|
||||
handle: FileHandle,
|
||||
source: StableRegularFileDescriptor,
|
||||
location: NodeSeaLocation,
|
||||
fileSize: number,
|
||||
signal?: AbortSignal,
|
||||
limits: NodeSeaReaderLimits = DEFAULT_NODE_SEA_READER_LIMITS,
|
||||
): Promise<Buffer> => {
|
||||
if (location.blobLength > NODE_SEA_MAX_BLOB_BYTES)
|
||||
throw limitFailure(NODE_SEA_BLOB_TOO_LARGE);
|
||||
const {
|
||||
handle,
|
||||
initial: { size: fileSize },
|
||||
} = source;
|
||||
admitBlobBytes({ handle, fileSize, signal, limits }, location.blobLength);
|
||||
const bytes = await readExact(
|
||||
{ handle, fileSize, signal },
|
||||
location.blobOffset,
|
||||
@@ -256,7 +279,15 @@ const locateThinMachO = async (
|
||||
commandCount: ncmds,
|
||||
commandBytes: sizeofcmds,
|
||||
} = framing.value;
|
||||
if (sizeofcmds > MAX_COMMAND_BYTES) return [];
|
||||
const limits = carrier.limits ?? DEFAULT_NODE_SEA_READER_LIMITS;
|
||||
if (sizeofcmds > limits.recognition_load_command_bytes)
|
||||
throw artifactReaderLimitFailure(
|
||||
"artifact_limits.node_sea.recognition_load_command_bytes",
|
||||
limits.recognition_load_command_bytes,
|
||||
"bytes",
|
||||
sizeofcmds,
|
||||
"memory",
|
||||
);
|
||||
const commands = await readExact(carrier, base + headerSize, sizeofcmds);
|
||||
if (commands === undefined) return [];
|
||||
const found: BlobRange[] = [];
|
||||
@@ -309,8 +340,11 @@ const locateThinMachO = async (
|
||||
if (cString(record.nameBytes, 0, 16) !== SEA_SECTION) continue;
|
||||
const length = Number(record.size);
|
||||
const fileOffset = record.offset;
|
||||
if (!Number.isSafeInteger(length) || length > NODE_SEA_MAX_BLOB_BYTES)
|
||||
throw limitFailure(NODE_SEA_BLOB_TOO_LARGE);
|
||||
if (!Number.isSafeInteger(length))
|
||||
throw limitFailure(
|
||||
"Node SEA blob length exceeds exact-number representation",
|
||||
);
|
||||
admitBlobBytes(carrier, length);
|
||||
if (fileOffset > sliceSize || length > sliceSize - fileOffset)
|
||||
throw formatFailure(NODE_SEA_TRUNCATED);
|
||||
found.push({ offset: base + fileOffset, length });
|
||||
@@ -468,22 +502,30 @@ const readElfTables = async (
|
||||
if (table.count === 0) return [];
|
||||
if (
|
||||
table.offset === undefined ||
|
||||
table.count > MAX_ELF_HEADERS ||
|
||||
table.entrySize < table.minimumEntry ||
|
||||
table.entrySize > 1024 ||
|
||||
table.offset > carrier.fileSize
|
||||
table.offset > carrier.fileSize ||
|
||||
table.count >
|
||||
Math.floor((carrier.fileSize - table.offset) / table.entrySize)
|
||||
)
|
||||
return undefined;
|
||||
const limits = carrier.limits ?? DEFAULT_NODE_SEA_READER_LIMITS;
|
||||
if (table.count > limits.recognition_header_count)
|
||||
throw artifactReaderLimitFailure(
|
||||
"artifact_limits.node_sea.recognition_header_count",
|
||||
limits.recognition_header_count,
|
||||
"count",
|
||||
table.count,
|
||||
);
|
||||
const entries: Buffer[] = [];
|
||||
for (let index = 0; index < table.count; index += 1) {
|
||||
if (index % 128 === 0) abortIfNeeded(carrier.signal);
|
||||
const offset = table.offset + index * table.entrySize;
|
||||
if (
|
||||
offset > carrier.fileSize ||
|
||||
table.entrySize > carrier.fileSize - offset
|
||||
table.minimumEntry > carrier.fileSize - offset
|
||||
)
|
||||
return undefined;
|
||||
const entry = await readExact(carrier, offset, table.entrySize);
|
||||
const entry = await readExact(carrier, offset, table.minimumEntry);
|
||||
if (entry === undefined) return undefined;
|
||||
entries.push(entry);
|
||||
}
|
||||
@@ -533,8 +575,6 @@ const scanElfNoteTable = async (
|
||||
if (!parsedHeader.ok)
|
||||
return failNoteTable(alreadyFound || found.length > 0);
|
||||
const { namesz, descsz } = parsedHeader.value;
|
||||
if (namesz > MAX_NOTE_NAME_BYTES)
|
||||
return failNoteTable(alreadyFound || found.length > 0);
|
||||
if (cursor > note.length - 12 || namesz > note.length - cursor - 12)
|
||||
return failNoteTable(alreadyFound || found.length > 0);
|
||||
let layout: ReturnType<typeof elfNoteLayout>;
|
||||
@@ -549,7 +589,13 @@ const scanElfNoteTable = async (
|
||||
layout.next < 12
|
||||
)
|
||||
return failNoteTable(alreadyFound || found.length > 0);
|
||||
const name = await readExact(carrier, note.offset + cursor + 12, namesz);
|
||||
// Only the exact SEA owner matters here. A bounded prefix distinguishes
|
||||
// it without retaining an unrelated vendor's potentially long name.
|
||||
const name = await readExact(
|
||||
carrier,
|
||||
note.offset + cursor + 12,
|
||||
Math.min(namesz, RESOURCE_NAME.length + 1),
|
||||
);
|
||||
if (name === undefined)
|
||||
return failNoteTable(alreadyFound || found.length > 0);
|
||||
const owner = cString(name, 0, name.length);
|
||||
@@ -559,8 +605,7 @@ const scanElfNoteTable = async (
|
||||
if (owner === RESOURCE_NAME) {
|
||||
if (!descriptorFits)
|
||||
throw formatFailure(NODE_SEA_ELF_DESCRIPTOR_TRUNCATED);
|
||||
if (descsz > NODE_SEA_MAX_BLOB_BYTES)
|
||||
throw limitFailure(NODE_SEA_BLOB_TOO_LARGE);
|
||||
admitBlobBytes(carrier, descsz);
|
||||
const blobOffset = note.offset + cursor + layout.descriptor;
|
||||
if (
|
||||
blobOffset > carrier.fileSize ||
|
||||
@@ -644,8 +689,7 @@ const locatePeResources = async (carrier: Carrier): Promise<BlobRange[]> => {
|
||||
)
|
||||
return [];
|
||||
const range = resource.payload.location;
|
||||
if (range.bytes > NODE_SEA_MAX_BLOB_BYTES)
|
||||
throw limitFailure(NODE_SEA_BLOB_TOO_LARGE);
|
||||
admitBlobBytes(carrier, range.bytes);
|
||||
return [{ offset: range.offset, length: range.bytes }];
|
||||
});
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user