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:
morluto authored and GitHub committed 2026-11-10 05:11:20 -05:00
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;
}
+68 -65
View File
@@ -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");
+54 -33
View File
@@ -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;
}
+69 -25
View File
@@ -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 }];
});
};