refactor: consolidate binary artifact interpretation and fixture ownership

Share exact ELF program-header facts across nine consumers while preserving their admission and precision policies. Centralize conformance path and digest binding, retain verifier-specific checks, and route fixture-owner changes to CI qualification.
This commit is contained in:
morluto committed 2026-10-26 16:21:11 +08:00
1 parent 23bc51d5c4
commit 9341b116ef
20 files changed
+484 -144

No files matched your search

+3
View File
@@ -22,6 +22,9 @@ const GUARDED_WORKFLOWS = new Set([
// Sources compiled by build-conformance-fixtures.mjs. The readiness journey
// analyzes the built C fixture as well as its own applications.
const FIXTURE_SOURCES = [
"scripts/build-conformance-fixtures.mjs",
"scripts/verify-conformance-fixtures.mjs",
"scripts/lib/conformance-fixtures.mjs",
"tests/conformance/c/**",
"tests/conformance/napi/**",
"tests/conformance/objc/**",
+9
View File
@@ -13,3 +13,12 @@ export function generateLargeFixture(): string;
export function sourceDigest(
sources: readonly { readonly path: string; readonly content: string }[],
): string;
export function resolveConformanceFixturePath(
root: string,
declaredPath: unknown,
): Promise<string>;
export function bindConformanceFixtureArtifact(
path: string,
fixture: unknown,
): Promise<{ readonly path: string; readonly sha256: string }>;
+34
View File
@@ -1,4 +1,6 @@
import { createHash } from "node:crypto";
import { readFile, realpath } from "node:fs/promises";
import { isAbsolute, relative, resolve, sep } from "node:path";
export const LARGE_FIXTURE_COUNT = 1_205;
@@ -36,3 +38,35 @@ export const sourceDigest = (sources) => {
.join("");
return sha256(canonical);
};
/** Resolve manifest-relative paths once, including their real symlink destination. */
export const resolveConformanceFixturePath = async (root, declaredPath) => {
if (typeof declaredPath !== "string")
throw new Error("Conformance manifest path is invalid");
const path = await realpath(resolve(root, declaredPath));
const relation = relative(root, path);
if (
relation === ".." ||
relation.startsWith(`..${sep}`) ||
isAbsolute(relation)
)
throw new Error(`Conformance path escapes its root: ${declaredPath}`);
return path;
};
/** Read a resolved artifact and bind its exact bytes to the manifest digest. */
export const bindConformanceFixtureArtifact = async (path, fixture) => {
if (
fixture === null ||
typeof fixture !== "object" ||
typeof fixture.artifactSha256 !== "string" ||
!/^[0-9a-f]{64}$/u.test(fixture.artifactSha256)
)
throw new Error("Invalid conformance artifact declaration");
const actual = sha256(await readFile(path));
if (actual !== fixture.artifactSha256)
throw new Error(
`Fixture ${fixture.name} digest did not match its manifest`,
);
return { path, sha256: actual };
};
+17 -36
View File
@@ -1,6 +1,9 @@
import { createHash } from "node:crypto";
import { readFile, realpath } from "node:fs/promises";
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
import { dirname } from "node:path";
import {
bindConformanceFixtureArtifact,
resolveConformanceFixturePath,
} from "./conformance-fixtures.mjs";
/** Bind the real-Hopper semantic verifier to source-owned manifest artifacts. */
export async function loadRealHopperFixtureTargets(manifestPath) {
@@ -15,19 +18,22 @@ export async function loadRealHopperFixtureTargets(manifestPath) {
const largeOracle = requireLargeOracle(large.expectations);
const root = dirname(canonicalManifest);
const [primaryArtifact, secondaryArtifact, largeArtifact] = await Promise.all(
[
bindFixtureArtifact(root, primary),
bindFixtureArtifact(root, secondary),
bindFixtureArtifact(root, large),
],
[primary, secondary, large].map(async (fixture) =>
bindConformanceFixtureArtifact(
await resolveConformanceFixturePath(root, fixture.artifact),
fixture,
),
),
);
const objc = manifest.fixtures.find((fixture) => fixture?.name === "objc");
const unicode =
objc === undefined ? undefined : fixtureEntry(manifest.fixtures, "objc");
const unicodeArtifact =
objc === undefined
unicode === undefined
? undefined
: await bindFixtureArtifact(
root,
fixtureEntry(manifest.fixtures, "objc"),
: await bindConformanceFixtureArtifact(
await resolveConformanceFixturePath(root, unicode.artifact),
unicode,
);
requireDistinctArtifacts([
primaryArtifact,
@@ -100,28 +106,3 @@ function fixtureEntry(fixtures, name) {
if (matches.length !== 1) throw new Error(`Missing fixture ${name}`);
return matches[0];
}
async function bindFixtureArtifact(root, fixture) {
if (
typeof fixture.artifact !== "string" ||
typeof fixture.artifactSha256 !== "string" ||
!/^[0-9a-f]{64}$/u.test(fixture.artifactSha256)
)
throw new Error(`Invalid artifact declaration for ${fixture.name}`);
const path = await realpath(resolve(root, fixture.artifact));
const relation = relative(root, path);
if (
relation === ".." ||
relation.startsWith(`..${sep}`) ||
isAbsolute(relation)
)
throw new Error(`Fixture ${fixture.name} escapes its manifest root`);
const actual = sha256(await readFile(path));
if (actual !== fixture.artifactSha256)
throw new Error(
`Fixture ${fixture.name} digest did not match its manifest`,
);
return { path, sha256: actual };
}
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
+11 -20
View File
@@ -1,9 +1,11 @@
import { execFile } from "node:child_process";
import { readFile, realpath } from "node:fs/promises";
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
import { dirname, resolve } from "node:path";
import { promisify } from "node:util";
import {
bindConformanceFixtureArtifact,
resolveConformanceFixturePath,
generateLargeFixture,
sha256,
sourceDigest,
@@ -63,7 +65,10 @@ async function resolveSourceRoot(manifestPath, declaredSourceRoot) {
}
async function verifyFixture(fixture) {
const artifactPath = await confinedPath(root, fixture.artifact);
const artifactPath = await resolveConformanceFixturePath(
root,
fixture.artifact,
);
const sources = [];
for (const source of fixture.sources ?? []) {
if (typeof source.path !== "string" || typeof source.sha256 !== "string")
@@ -71,7 +76,9 @@ async function verifyFixture(fixture) {
const content =
source.path === "generated/large.c"
? generateLargeFixture()
: await readFile(await confinedPath(sourceRoot, source.path));
: await readFile(
await resolveConformanceFixturePath(sourceRoot, source.path),
);
if (sha256(content) !== source.sha256)
throw new Error(
`${fixture.name}: source hash mismatch for ${source.path}`,
@@ -80,9 +87,7 @@ async function verifyFixture(fixture) {
}
if (sourceDigest(sources) !== fixture.sourceSha256)
throw new Error(`${fixture.name}: source manifest digest mismatch`);
const artifact = await readFile(artifactPath);
if (sha256(artifact) !== fixture.artifactSha256)
throw new Error(`${fixture.name}: artifact hash mismatch`);
await bindConformanceFixtureArtifact(artifactPath, fixture);
const fileOutput = (await exec("file", [artifactPath])).stdout;
if (!fileOutput.includes(fileMarker))
throw new Error(`${fixture.name}: not ${fileMarker}`);
@@ -115,20 +120,6 @@ async function verifyFixture(fixture) {
);
}
async function confinedPath(rootPath, declaredPath) {
if (typeof declaredPath !== "string")
throw new Error("Conformance manifest path is invalid");
const path = await realpath(resolve(rootPath, declaredPath));
const relation = relative(rootPath, path);
if (
relation === ".." ||
relation.startsWith(`..${sep}`) ||
isAbsolute(relation)
)
throw new Error(`Conformance path escapes its root: ${declaredPath}`);
return path;
}
function verifyCount(output, prefix, expected, fixture) {
if (prefix === undefined || expected === undefined) return;
const count = output
+20 -9
View File
@@ -1,3 +1,4 @@
import { readElfProgramHeader } from "../domain/elfProgramHeader.js";
import { readElfRawTableDeclarations } from "../domain/elfRawTableDeclarations.js";
import type { FileHandle } from "node:fs/promises";
import type {
@@ -102,9 +103,6 @@ export const readMipsElfMetadata = async (
count: programs.count,
stride: programs.width,
entrySize: 32,
typeOffset: 0,
dataOffset: 4,
sizeOffset: 16,
recordType: 0x70000003, // PT_MIPS_ABIFLAGS
optional: false,
},
@@ -114,9 +112,6 @@ export const readMipsElfMetadata = async (
count: sections.count,
stride: sections.width,
entrySize: 40,
typeOffset: 4,
dataOffset: 16,
sizeOffset: 20,
recordType: 0x7000002a, // SHT_MIPS_ABIFLAGS
optional: true,
},
@@ -163,7 +158,17 @@ export const readMipsElfMetadata = async (
for (let i = 0; i < count; i += 1) {
checkCancelled();
const position = i * table.entrySize;
const type = u32(contents.value, position + table.typeOffset);
const parsedProgram = !table.optional
? readElfProgramHeader(
contents.value.subarray(position, position + table.entrySize),
32,
little,
)
: undefined;
if (parsedProgram !== undefined && !parsedProgram.ok)
return err(parsedProgram.error.message);
const program = parsedProgram?.value;
const type = program?.type ?? u32(contents.value, position + 4);
if (table.optional && type === SHT_GNU_ATTRIBUTES) {
if (gnuAttributes !== null)
return err("duplicate MIPS GNU attribute sections");
@@ -197,8 +202,14 @@ export const readMipsElfMetadata = async (
if (type !== table.recordType) continue;
if (found) return err(`duplicate MIPS ABI records in ${table.label}`);
found = true;
const offset = u32(contents.value, position + table.dataOffset);
const size = u32(contents.value, position + table.sizeOffset);
const offset =
program !== undefined
? Number(program.offset)
: u32(contents.value, position + 16);
const size =
program !== undefined
? Number(program.fileSize)
: u32(contents.value, position + 20);
if (size !== 24)
return err("unsupported MIPS ABI record size; expected 24 bytes");
const candidate = await read(offset, 24, "ABI record");
+14 -6
View File
@@ -1,3 +1,4 @@
import { readElfProgramHeader } from "../domain/elfProgramHeader.js";
import type { MipsElfMetadata } from "../domain/binaryTargetTypes.js";
import { err, ok, type Result } from "../domain/result.js";
@@ -38,12 +39,19 @@ export const readMipsElfSharedLoad = async (
let dynamic: { offset: number; address: number; size: number } | undefined;
for (let position = 0; position < programHeaders.length; position += 32) {
checkCancelled();
const type = u32(programHeaders, position);
const offset = u32(programHeaders, position + 4);
const size = u32(programHeaders, position + 16);
const parsedProgram = readElfProgramHeader(
programHeaders.subarray(position, position + 32),
32,
little,
);
if (!parsedProgram.ok) return err(parsedProgram.error.message);
const program = parsedProgram.value;
const { type } = program;
const offset = Number(program.offset);
const size = Number(program.fileSize);
if (type === 1) {
const address = u32(programHeaders, position + 8);
const memorySize = u32(programHeaders, position + 20);
const address = Number(program.virtualAddress);
const memorySize = Number(program.memorySize);
if (
offset > fileSize - size ||
size > memorySize ||
@@ -54,7 +62,7 @@ export const readMipsElfSharedLoad = async (
}
if (type === 2) {
if (dynamic !== undefined) return err("duplicate MIPS PT_DYNAMIC tables");
dynamic = { offset, address: u32(programHeaders, position + 8), size };
dynamic = { offset, address: Number(program.virtualAddress), size };
}
}
if (dynamic === undefined)
+23 -12
View File
@@ -1,3 +1,4 @@
import { readElfProgramHeader } from "../../domain/elfProgramHeader.js";
import { readElfRawTableDeclarations } from "../../domain/elfRawTableDeclarations.js";
import { createHash } from "node:crypto";
import { parsePeResources } from "../../native/pe/PeResourceParser.js";
@@ -353,18 +354,28 @@ const locateElfNotes = async (
if (programs === undefined) opaque = phnum > 0;
else
for (const entry of programs) {
if (readU32(entry, 0, little) !== PT_NOTE) continue;
const note = elf64
? {
offset: readU64(entry, 8, little),
length: readU64(entry, 32, little),
align: readU64(entry, 48, little),
}
: {
offset: readU32(entry, 4, little),
length: readU32(entry, 16, little),
align: readU32(entry, 28, little),
};
const parsedProgram = readElfProgramHeader(
entry,
header.value.elfClass,
little,
);
if (!parsedProgram.ok) continue;
const program = parsedProgram.value;
if (program.type !== PT_NOTE) continue;
const note = {
offset:
program.offset <= BigInt(Number.MAX_SAFE_INTEGER)
? Number(program.offset)
: undefined,
length:
program.fileSize <= BigInt(Number.MAX_SAFE_INTEGER)
? Number(program.fileSize)
: undefined,
align:
program.alignment <= BigInt(Number.MAX_SAFE_INTEGER)
? Number(program.alignment)
: undefined,
};
const scanned = await scanElfNoteTable(
carrier,
note,
+15 -7
View File
@@ -1,3 +1,4 @@
import { readElfProgramHeader } from "./elfProgramHeader.js";
import { readElfRawTableDeclarations } from "./elfRawTableDeclarations.js";
import { readElfFixedHeader, type ElfFixedHeader } from "./elfFixedHeader.js";
import { err, ok, type Result } from "./result.js";
@@ -208,14 +209,21 @@ function* inspectTableRanges(
const r = reader(bytes, header);
for (let item = 0; item < count; item++) {
const at = item * table.width;
const type = r.u32(at + (kind === "program" ? 0 : 4));
const program =
kind === "program"
? readElfProgramHeader(
bytes.subarray(at, at + table.width),
header.elfClass,
header.byteOrder === "little",
)
: undefined;
if (program !== undefined && !program.ok)
return err(program.error.message);
const facts = program?.value;
const type = facts?.type ?? r.u32(at + 4);
if (type === 0 || (kind === "section" && type === 8)) continue;
const offset = r.word(
at + (kind === "program" ? (wide ? 8 : 4) : wide ? 24 : 16),
);
const length = r.word(
at + (kind === "program" ? (wide ? 32 : 16) : wide ? 32 : 20),
);
const offset = facts?.offset ?? r.word(at + (wide ? 24 : 16));
const length = facts?.fileSize ?? r.word(at + (wide ? 32 : 20));
const failure = rangeFailure(
`${kind} ${String(index + item)}`,
offset,
+87
View File
@@ -0,0 +1,87 @@
import { expect, it } from "vitest";
import { readElfProgramHeader } from "./elfProgramHeader.js";
const formats = [
{ bits: 32, littleEndian: false },
{ bits: 32, littleEndian: true },
{ bits: 64, littleEndian: false },
{ bits: 64, littleEndian: true },
] as const;
it.each(formats)(
"preserves exact ELF$bits raw program fields (littleEndian=$littleEndian)",
({ bits, littleEndian }) => {
const wide = bits === 64;
const length = wide ? 56 : 32;
const backing = Buffer.alloc(length + 19, 0xa5);
const bytes = backing.subarray(7, 7 + length);
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const values = wide
? [
0xffffffffffffffffn,
0xfedcba9876543210n,
0x8000000000000001n,
0x123456789abcdef0n,
0x8888888888888888n,
0x7777777777777777n,
]
: [
0xffffffffn,
0xfedcba98n,
0x80000001n,
0x12345678n,
0x88888888n,
0x77777777n,
];
view.setUint32(0, 0xfedcba98, littleEndian);
view.setUint32(wide ? 4 : 24, 0x89abcdef, littleEndian);
const offsets = wide ? [8, 16, 24, 32, 40, 48] : [4, 8, 12, 16, 20, 28];
values.forEach((value, index) => {
if (wide) view.setBigUint64(offsets[index]!, value, littleEndian);
else view.setUint32(offsets[index]!, Number(value), littleEndian);
});
const before = Buffer.from(backing);
const parsed = readElfProgramHeader(
backing.subarray(7),
bits,
littleEndian,
);
expect(backing.equals(before)).toBe(true);
backing.fill(0);
expect(parsed).toEqual({
ok: true,
value: {
type: 0xfedcba98,
flags: 0x89abcdef,
offset: values[0],
virtualAddress: values[1],
physicalAddress: values[2],
fileSize: values[3],
memorySize: values[4],
alignment: values[5],
},
});
},
);
it.each(formats)(
"refuses each incomplete ELF$bits program record (littleEndian=$littleEndian)",
({ bits, littleEndian }) => {
const length = bits === 64 ? 56 : 32;
const backing = Buffer.alloc(length + 7);
for (let observed = 0; observed < length; observed++)
expect(
readElfProgramHeader(
backing.subarray(7, 7 + observed),
bits,
littleEndian,
),
).toEqual({
ok: false,
error: {
kind: "malformed",
message: "ELF program header is truncated",
},
});
},
);
+30
View File
@@ -0,0 +1,30 @@
import { err, ok } from "./result.js";
/** Fixed ELF32/ELF64 program declarations; consumers own type, precision and mapping admission. */
export const readElfProgramHeader = (
bytes: Uint8Array,
bits: 32 | 64,
littleEndian: boolean,
) => {
const wide = bits === 64;
if (bytes.byteLength < (wide ? 56 : 32))
return err({
kind: "malformed" as const,
message: "ELF program header is truncated",
});
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const word = (at: number): bigint =>
wide
? view.getBigUint64(at, littleEndian)
: BigInt(view.getUint32(at, littleEndian));
return ok({
type: view.getUint32(0, littleEndian),
flags: view.getUint32(wide ? 4 : 24, littleEndian),
offset: word(wide ? 8 : 4),
virtualAddress: word(wide ? 16 : 8),
physicalAddress: word(wide ? 24 : 12),
fileSize: word(wide ? 32 : 16),
memorySize: word(wide ? 40 : 20),
alignment: word(wide ? 48 : 28),
});
};
+21
View File
@@ -545,3 +545,24 @@ it("checks raw symbol precision only after definition and requested-name selecti
],
});
});
it("keeps precision admission limited to consumed ELF program fields and types", async () => {
const original = buildElf64Image([{ name: "selected", value: 64, size: 4 }]);
const expected = await readElfSymbols(original, ["selected"]);
const bytes = appendElfRanges(original, "program", [
{ type: 0x70000001, offset: 0, size: 0 },
]);
const table = Number(bytes.readBigUInt64LE(32));
const width = bytes.readUInt16LE(54);
for (const relative of [8, 16, 24, 32, 40, 48])
bytes.writeBigUInt64LE(0xffffffffffffffffn, table + width + relative);
// The load mapping does not consume physical address, memory size or alignment.
for (const relative of [24, 40, 48])
bytes.writeBigUInt64LE(0xffffffffffffffffn, table + relative);
expect(await readElfSymbols(bytes, ["selected"])).toEqual(expected);
bytes.writeBigUInt64LE(0xffffffffffffffffn, table + 32);
expect(await readElfSymbols(bytes, ["selected"])).toEqual({
symbols: [],
failures: [{ reason: "ELF segment 0 is not file-backed" }],
});
});
+18 -7
View File
@@ -1,3 +1,4 @@
import { readElfProgramHeader } from "../domain/elfProgramHeader.js";
import { readElfSymbol } from "../domain/elfSymbol.js";
import { readElfRawTableDeclarations } from "../domain/elfRawTableDeclarations.js";
import { Buffer } from "node:buffer";
@@ -199,15 +200,25 @@ export const readElfSymbols = async (
signal?.throwIfAborted();
}
const base = e_phoff + index * e_phentsize;
const type = reader.u32(base);
const parsedProgram = readElfProgramHeader(
bytes.subarray(base, base + e_phentsize),
is64 ? 64 : 32,
true,
);
if (!parsedProgram.ok) return failure(parsedProgram.error.message);
const program = parsedProgram.value;
const { type } = program;
if (type !== PT_LOAD && type !== PT_DYNAMIC) continue;
const vaddr = is64 ? reader.u64(base + 0x10) : reader.u32(base + 8);
const offset = is64 ? reader.u64(base + 8) : reader.u32(base + 4);
const filesz = is64 ? reader.u64(base + 0x20) : reader.u32(base + 0x10);
if (
vaddr === null ||
offset === null ||
filesz === null ||
program.virtualAddress > BigInt(Number.MAX_SAFE_INTEGER) ||
program.offset > BigInt(Number.MAX_SAFE_INTEGER) ||
program.fileSize > BigInt(Number.MAX_SAFE_INTEGER)
)
return failure(`ELF segment ${String(index)} is not file-backed`);
const vaddr = Number(program.virtualAddress);
const offset = Number(program.offset);
const filesz = Number(program.fileSize);
if (
!Number.isSafeInteger(vaddr + filesz) ||
offset > bytes.length ||
filesz > bytes.length - offset
+28 -16
View File
@@ -1,3 +1,4 @@
import { readElfProgramHeader } from "../domain/elfProgramHeader.js";
import { MAX_FLUTTER_BUILD_ID_BYTES } from "../domain/flutter/flutterBuildAnalysis.js";
import type { FileHandle } from "node:fs/promises";
import { setImmediate as yieldToEventLoop } from "node:timers/promises";
@@ -97,21 +98,33 @@ export const readElfBuildIds = async (
signal?.throwIfAborted();
}
const base = index * table.width;
const parsedProgram = table.program
? readElfProgramHeader(
bytes.subarray(base, base + table.width),
h.is64 ? 64 : 32,
true,
)
: undefined;
if (parsedProgram !== undefined && !parsedProgram.ok)
return failure(parsedProgram.error.message);
const program = parsedProgram?.value;
if (
bytes.readUInt32LE(base + (table.program ? 0 : 4)) !==
(program?.type ?? bytes.readUInt32LE(base + 4)) !==
(table.program ? 4 : 7)
)
continue;
const offsetAt =
base + (table.program ? (h.is64 ? 8 : 4) : h.is64 ? 0x18 : 0x10);
const sizeAt =
base + (table.program ? (h.is64 ? 0x20 : 0x10) : h.is64 ? 0x20 : 0x14);
const offset = h.is64
? Number(bytes.readBigUInt64LE(offsetAt))
: bytes.readUInt32LE(offsetAt);
const size = h.is64
? Number(bytes.readBigUInt64LE(sizeAt))
: bytes.readUInt32LE(sizeAt);
const offset =
program !== undefined
? Number(program.offset)
: h.is64
? Number(bytes.readBigUInt64LE(base + 0x18))
: bytes.readUInt32LE(base + 0x10);
const size =
program !== undefined
? Number(program.fileSize)
: h.is64
? Number(bytes.readBigUInt64LE(base + 0x20))
: bytes.readUInt32LE(base + 0x14);
if (
!Number.isSafeInteger(offset) ||
!Number.isSafeInteger(size) ||
@@ -133,12 +146,11 @@ export const readElfBuildIds = async (
}
}
if (size === 0) continue;
const alignmentAt =
base + (table.program ? (h.is64 ? 48 : 28) : h.is64 ? 48 : 32);
const alignment = elfNoteAlignment(
h.is64
? bytes.readBigUInt64LE(alignmentAt)
: bytes.readUInt32LE(alignmentAt),
program?.alignment ??
(h.is64
? bytes.readBigUInt64LE(base + 48)
: bytes.readUInt32LE(base + 32)),
);
const key = `${offset}:${size}:${alignment}`;
if (ranges.has(key)) continue;
+12
View File
@@ -625,3 +625,15 @@ it("keeps unsafe table offsets ahead of unsupported extended numbering", () => {
"ELF offset or size exceeds a safe integer.",
);
});
it("does not apply note precision admission to unrelated program records", () => {
const bytes = elf64({ phoff: 64, phnum: 1 }, Buffer.alloc(56));
bytes.writeUInt32LE(0x70000001, 64);
for (const relative of [8, 16, 24, 32, 40, 48])
bytes.writeBigUInt64LE(0xffffffffffffffffn, 64 + relative);
expect(parse(bytes).tables).toEqual([]);
bytes.writeUInt32LE(4, 64);
expect(() => parse(bytes)).toThrow(
"ELF offset or size exceeds a safe integer.",
);
});
+18 -4
View File
@@ -1,3 +1,4 @@
import { readElfProgramHeader } from "../../domain/elfProgramHeader.js";
import { readElfRawTableDeclarations } from "../../domain/elfRawTableDeclarations.js";
import { createHash } from "node:crypto";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
@@ -412,10 +413,23 @@ export const parseElfNotes = (
return fail("ELF program header table overlaps the native header.");
for (let index = 0; index < phnum; index += 1) {
const at = phoff + index * phentsize;
if (reader.u32(at) !== PT_NOTE) continue;
const fileOffset = reader.word(at + (wide ? 8 : 4));
const fileSize = reader.word(at + (wide ? 32 : 16));
const alignment = noteAlignment(reader.word(at + (wide ? 48 : 28)));
const parsedProgram = readElfProgramHeader(
bytes.subarray(at, at + phentsize),
header.elfClass,
reader.little,
);
if (!parsedProgram.ok) return fail(parsedProgram.error.message);
const program = parsedProgram.value;
if (program.type !== PT_NOTE) continue;
if (
program.offset > BigInt(Number.MAX_SAFE_INTEGER) ||
program.fileSize > BigInt(Number.MAX_SAFE_INTEGER) ||
program.alignment > BigInt(Number.MAX_SAFE_INTEGER)
)
return fail("ELF offset or size exceeds a safe integer.");
const fileOffset = Number(program.offset);
const fileSize = Number(program.fileSize);
const alignment = noteAlignment(Number(program.alignment));
tables.push({
kind: "program",
index,
+20 -15
View File
@@ -1,3 +1,4 @@
import { readElfProgramHeader } from "../../domain/elfProgramHeader.js";
import { readPeCoffHeader } from "../../domain/peCoffHeader.js";
import { readPeSectionHeader } from "../../domain/peSectionHeader.js";
import { readElfTableDeclarations } from "../../domain/elfDeclaredRanges.js";
@@ -115,33 +116,37 @@ export function* readElfSectionDirectory(
} = sectionTable;
const programDeclarations: ElfProgramDeclaration[] = [];
if (program_count !== 0) {
const programs = new BinaryReader(
yield request(
fileSize,
program_offset,
BigInt(program_count) * BigInt(program_width),
),
little,
const programBytes = yield request(
fileSize,
program_offset,
BigInt(program_count) * BigInt(program_width),
);
// At most one record per table entry, bounded before allocation by the
// existing 16 MiB table allowance and minimum 32/56-byte ELF record width.
for (let index = 0; index < program_count; index++) {
const at = index * program_width;
const type = programs.u32(at);
const parsedProgram = readElfProgramHeader(
programBytes.subarray(at, at + program_width),
bits,
little,
);
if (!parsedProgram.ok) return reject(parsedProgram.error.message);
const program = parsedProgram.value;
const { type } = program;
if (type === 1)
programDeclarations.push({
type,
flags: programs.u32(at + (wide ? 4 : 24)),
file_offset: programs.word(at + (wide ? 8 : 4), bits),
virtual_address: programs.word(at + (wide ? 16 : 8), bits),
file_size: programs.word(at + (wide ? 32 : 16), bits),
memory_size: programs.word(at + (wide ? 40 : 20), bits),
flags: program.flags,
file_offset: program.offset,
virtual_address: program.virtualAddress,
file_size: program.fileSize,
memory_size: program.memorySize,
});
else if (type === 2)
programDeclarations.push({
type,
offset: programs.word(at + (wide ? 8 : 4), bits),
size: programs.word(at + (wide ? 32 : 16), bits),
offset: program.offset,
size: program.fileSize,
});
}
}
@@ -1,3 +1,4 @@
import { readElfProgramHeader } from "../../domain/elfProgramHeader.js";
import { readElfRawTableDeclarations } from "../../domain/elfRawTableDeclarations.js";
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
import {
@@ -50,25 +51,26 @@ const segmentMatches = (
raw: Buffer,
kinds: ReadonlyMap<number, string>,
): boolean => {
const type = raw.readUInt32LE(0);
const parsed = readElfProgramHeader(raw, 64, true);
if (!parsed.ok) return false;
const program = parsed.value;
const { type } = program;
return (
type === segment.type_id &&
matchesKind(type, segment.type, kinds) &&
BigInt(segment.flags) === BigInt(raw.readUInt32LE(4)) &&
BigInt(segment.flags) === BigInt(program.flags) &&
segment.file_backing ===
(type === 0 || raw.readBigUInt64LE(32) === 0n ? "none" : "file") &&
(type === 0 || program.fileSize === 0n ? "none" : "file") &&
(
[
[segment.offset, 8],
[segment.virtual_address, 16],
[segment.physical_address, 24],
[segment.file_size, 32],
[segment.memory_size, 40],
[segment.alignment, 48],
[segment.offset, program.offset],
[segment.virtual_address, program.virtualAddress],
[segment.physical_address, program.physicalAddress],
[segment.file_size, program.fileSize],
[segment.memory_size, program.memorySize],
[segment.alignment, program.alignment],
] as const
).every(
([field, relative]) => BigInt(field) === raw.readBigUInt64LE(relative),
)
).every(([field, value]) => BigInt(field) === value)
);
};
@@ -1257,6 +1257,18 @@ it.each([
"tests/conformance/c/fixture.c",
{ code: true, fixtures: true, readiness: true, package: false },
],
[
"scripts/lib/conformance-fixtures.mjs",
{ code: true, fixtures: true, readiness: true, package: false },
],
[
"scripts/build-conformance-fixtures.mjs",
{ code: true, fixtures: true, readiness: true, package: false },
],
[
"scripts/verify-conformance-fixtures.mjs",
{ code: true, fixtures: true, readiness: true, package: false },
],
[
"tests/conformance/swift/fixture.swift",
{ code: true, fixtures: true, readiness: true, package: false },
@@ -0,0 +1,78 @@
import { createHash } from "node:crypto";
import { realpath, symlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import {
bindConformanceFixtureArtifact,
resolveConformanceFixturePath,
} from "../../../scripts/lib/conformance-fixtures.mjs";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
it("binds canonical manifest-relative artifacts without changing declared names", async () => {
const root = await createTestTempDirectory("rea-conformance-binding-");
const name = "caller α source artifact";
const bytes = Buffer.from("inert fixture content");
await writeFile(join(root, name), bytes);
const sha256 = createHash("sha256").update(bytes).digest("hex");
const path = await realpath(join(root, name));
const declaration = {
name: "fixture",
artifact: name,
artifactSha256: sha256,
};
expect(await bindConformanceFixtureArtifact(path, declaration)).toEqual({
path,
sha256,
});
expect(declaration.artifact).toBe(name);
expect(await resolveConformanceFixturePath(root, name)).toBe(path);
await expect(
bindConformanceFixtureArtifact(path, {
...declaration,
artifactSha256: "0".repeat(64),
}),
).rejects.toThrow("digest did not match");
});
it.each([
null,
{ name: "fixture", artifact: "artifact", artifactSha256: null },
{ name: "fixture", artifact: "artifact", artifactSha256: "A".repeat(64) },
])(
"rejects malformed artifact declarations before reading files: %j",
async (declaration) => {
const root = await createTestTempDirectory("rea-conformance-declaration-");
await expect(
bindConformanceFixtureArtifact(join(root, "artifact"), declaration),
).rejects.toThrow("Invalid conformance artifact declaration");
},
);
it("rejects paths outside the canonical manifest root", async () => {
const root = await createTestTempDirectory("rea-conformance-root-");
const outside = await createTestTempDirectory("rea-conformance-outside-");
const path = join(outside, "artifact");
await writeFile(path, "outside");
await expect(resolveConformanceFixturePath(root, path)).rejects.toThrow(
"escapes its root",
);
await expect(resolveConformanceFixturePath(root, null)).rejects.toThrow(
"manifest path is invalid",
);
});
it.skipIf(process.platform === "win32")(
"rejects artifact symlinks escaping their manifest root",
async () => {
const root = await createTestTempDirectory("rea-conformance-symlink-");
const outside = await createTestTempDirectory(
"rea-conformance-symlink-outside-",
);
const bytes = Buffer.from("outside");
await writeFile(join(outside, "artifact"), bytes);
await symlink(join(outside, "artifact"), join(root, "artifact"));
await expect(
resolveConformanceFixturePath(root, "artifact"),
).rejects.toThrow("escapes its root");
},
);