mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
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:
@@ -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/**",
|
||||
|
||||
@@ -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 }>;
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -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),
|
||||
});
|
||||
};
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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.",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user