(() => Buffer.alloc(32));
+ const result = signature(
+ Buffer.from([0x06, 0x12, value]),
+ undefined,
+ name,
+ );
+
+ expect(result.parse_status).toBe("decoded");
+ expect(result.resolved_sha256).toMatch(/^[0-9a-f]{64}$/u);
+ expect(name).toHaveBeenCalledTimes(1);
+ expect(name).toHaveBeenCalledWith(
+ expect.objectContaining({ value, coordinate: { table, row } }),
+ );
+ },
+ );
+});
+
describe("resolved member signature digests", () => {
it("names modifier, class, and generic type operands instead of their rows", () => {
// modreq(M) G on the left and the same types at other rows on the right.
@@ -114,6 +142,43 @@ describe("resolved member signature digests", () => {
expect(field?.signature.parse_status).toBe("decoded");
expect(field?.signature.resolved_sha256).toBeNull();
});
+});
+
+describe("metadata type-name digest ownership", () => {
+ it("shares declaration and operand identity while preserving invalid TypeDef diagnostics", () => {
+ const bytes = buildManagedPeFixture();
+ const layout = metadataLayout(bytes);
+ const issues: ManagedParseIssue[] = [];
+ const names = managedTypeNameDigests(
+ layout,
+ issues,
+ { types: [], nesting: { enclosing: new Map(), complete: true } },
+ managedTypeReferences(bytes, layout, issues),
+ );
+
+ for (const row of [0, 9]) {
+ expect(names.declaration(2, row)).toBeNull();
+ const value = row * 4;
+ expect(
+ names.operand({
+ value,
+ coordinate: metadataCodedIndexCoordinate(value, 2, [2, 1, 27]),
+ }),
+ ).toBeNull();
+ }
+ expect(issues).toMatchObject([
+ {
+ code: "invalid-row",
+ scope: "metadata.signature.type:2:0",
+ detail: "Signature references an unavailable TypeDef row 0",
+ },
+ {
+ code: "invalid-row",
+ scope: "metadata.signature.type:2:9",
+ detail: "Signature references an unavailable TypeDef row 9",
+ },
+ ]);
+ });
it("names each type operand once, so repeated operands are not charged again", () => {
const bytes = buildManagedPeFixture({
@@ -122,7 +187,7 @@ describe("resolved member signature digests", () => {
const layout = metadataLayout(bytes);
const budget = new ManagedDecodeBudget();
attachManagedDecodeBudget(layout, budget);
- const typeName = managedTypeNameDigests(
+ const typeNames = managedTypeNameDigests(
layout,
[],
{
@@ -133,9 +198,14 @@ describe("resolved member signature digests", () => {
);
const uri = (2 << 2) | 1;
- expect(typeName(uri)).toEqual(nameDigest("System.Uri"));
+ const operand = {
+ value: uri,
+ coordinate: metadataCodedIndexCoordinate(uri, 2, [2, 1, 27]),
+ };
+ expect(typeNames.operand(operand)).toEqual(nameDigest("System.Uri"));
const reserve = vi.spyOn(budget, "reserve");
- expect(typeName(uri)).toEqual(nameDigest("System.Uri"));
+ expect(typeNames.operand(operand)).toEqual(nameDigest("System.Uri"));
+ expect(typeNames.declaration(1, 2)).toEqual(nameDigest("System.Uri"));
expect(reserve).not.toHaveBeenCalled();
});
@@ -182,8 +252,8 @@ describe("resolved member signature digests", () => {
parsedTypes,
managedTypeReferences(bytes, layout, issues),
);
- expect(() => resolve((2 << 2) | 1)).not.toThrow();
- expect(resolve((2 << 2) | 1)).toBeNull();
+ expect(() => resolve.declaration(1, 2)).not.toThrow();
+ expect(resolve.declaration(1, 2)).toBeNull();
expect(issues).toEqual([]);
const inspection = inspectManagedMembersBytes(
bytes,
diff --git a/src/dotnet/ManagedTypeNameDigests.ts b/src/dotnet/ManagedTypeNameDigests.ts
index dff47b5f4d..c470a5a86f 100644
--- a/src/dotnet/ManagedTypeNameDigests.ts
+++ b/src/dotnet/ManagedTypeNameDigests.ts
@@ -5,6 +5,7 @@ import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js";
import type {
parseTypes,
SignatureTypeName,
+ DeclarationTypeName,
} from "./ManagedMemberInspectorCore.js";
import { metadataToken } from "./ManagedMetadataHeaps.js";
import type { ManagedTypeReferenceLookup } from "./ManagedTypeReferences.js";
@@ -146,7 +147,10 @@ export const managedTypeNameDigests = (
issues: ManagedParseIssue[],
parsedTypes: Pick, "types" | "nesting">,
reference: ManagedTypeReferenceLookup,
-): SignatureTypeName => {
+): {
+ readonly operand: SignatureTypeName;
+ readonly declaration: DeclarationTypeName;
+} => {
const retain = () => managedDecodeBudget(layout).reserve(128, null);
managedDecodeBudget(layout).reserve(parsedTypes.types.length * 128, null);
const types = new Map();
@@ -196,16 +200,28 @@ export const managedTypeNameDigests = (
return value?.scopeValid === true ? value : null;
}, retain);
const digests = new Map();
- return (codedIndex) => {
- let digest = digests.get(codedIndex);
+ const digestFor = (
+ key: number,
+ table: number | undefined,
+ row: number,
+ ): Buffer | null => {
+ let digest = digests.get(key);
if (digest === undefined) {
managedDecodeBudget(layout).reserve(256, null);
- const row = codedIndex >>> 2;
- const table = codedIndex & 3;
digest =
- table === 0 ? definitions(row) : table === 1 ? references(row) : null;
- digests.set(codedIndex, digest);
+ table === TYPE_DEF
+ ? definitions(row)
+ : table === TYPE_REF
+ ? references(row)
+ : null;
+ digests.set(key, digest);
}
return digest;
};
+ return {
+ operand: ({ value, coordinate }) =>
+ digestFor(value, coordinate.table, coordinate.row),
+ declaration: (table, row) =>
+ digestFor(row * 4 + Number(table === TYPE_REF), table, row),
+ };
};
diff --git a/src/dotnet/ManagedTypeNames.ts b/src/dotnet/ManagedTypeNames.ts
index 845ad145e8..bf52cea625 100644
--- a/src/dotnet/ManagedTypeNames.ts
+++ b/src/dotnet/ManagedTypeNames.ts
@@ -2,7 +2,6 @@ import type { ManagedTypeReferenceLookup } from "./ManagedTypeReferences.js";
import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js";
import {
fullName,
- readCompressed,
specSignatureFacts,
type TypeRange,
} from "./ManagedMemberInspectorCore.js";
@@ -15,7 +14,7 @@ import {
metadataToken,
readMetadataBlob,
} from "./ManagedMetadataHeaps.js";
-import { managedFailure, readManagedValue } from "./ManagedReaderFailure.js";
+import { readManagedValue } from "./ManagedReaderFailure.js";
/**
* Name the type a TypeDef, TypeRef, or generic-instantiation TypeSpec token
@@ -55,20 +54,10 @@ export const typeNameLookup = (
scope: `metadata.TypeSpec:${metadataToken(27, row)}`,
offset: cursor.start,
} as const;
- specSignatureFacts(layout, spec, owner);
- if (spec[0] !== 0x15) return null;
- // GENERICINST (CLASS | VALUETYPE) TypeDefOrRefEncoded: never a TypeSpec,
- // so naming never recurses.
- const encoded = readCompressed(spec, 2).value;
- const table = ([2, 1] as const)[encoded & 3];
- if (table === undefined)
- throw managedFailure(
- "invalid-blob",
- owner.scope,
- "Generic instantiation does not name a TypeDef or TypeRef",
- owner.offset,
- );
- return definitionName(table, Math.floor(encoded / 4));
+ const { nominalGenericType } = specSignatureFacts(layout, spec, owner);
+ return nominalGenericType === null
+ ? null
+ : definitionName(nominalGenericType.table, nominalGenericType.row);
};
return (token) => {
if (token === null) return null;
diff --git a/src/dotnet/ManagedTypeReferences.ts b/src/dotnet/ManagedTypeReferences.ts
index 6b436a410c..340a0bc681 100644
--- a/src/dotnet/ManagedTypeReferences.ts
+++ b/src/dotnet/ManagedTypeReferences.ts
@@ -1,7 +1,7 @@
import { managedDecodeBudget } from "./ManagedDecodeBudget.js";
import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
import {
- metadataCodedTokenInvalidReason,
+ decodeMetadataCodedIndex,
metadataRowCursor,
metadataToken,
readMetadataString,
@@ -46,25 +46,25 @@ export const managedTypeReferences = (
readManagedValue(() => {
const cursor = metadataRowCursor(bytes, layout, 1, row);
const raw = cursor.readIndex(layout.codedIndexSize("ResolutionScope"));
- const reason = metadataCodedTokenInvalidReason(
+ const scope = decodeMetadataCodedIndex(
raw,
2,
[0, 26, 35, 1],
layout.rowCounts,
);
- if (reason !== null)
+ if (scope.kind === "invalid")
issues.push({
code: "invalid-row",
scope: `metadata.TypeRef:${metadataToken(1, row)}`,
offset: cursor.start,
- detail: `TypeRef ResolutionScope coded index 0x${raw.toString(16)} is invalid: ${reason}`,
+ detail: `TypeRef ResolutionScope coded index 0x${raw.toString(16)} is invalid: ${scope.reason}`,
});
return {
nameIndex: cursor.readIndex(layout.stringIndexSize),
namespaceIndex: cursor.readIndex(layout.stringIndexSize),
enclosingRow:
- raw !== 0 && (raw & 3) === 3 ? Math.floor(raw / 4) : null,
- scopeValid: reason === null,
+ scope.kind !== "null" && scope.table === 1 ? scope.row : null,
+ scopeValid: scope.kind !== "invalid",
};
}, issues) ?? null;
rows.set(row, value);
diff --git a/src/go/GoBinaryImage.ts b/src/go/GoBinaryImage.ts
index 55bc6e4ec8..781c43c69b 100644
--- a/src/go/GoBinaryImage.ts
+++ b/src/go/GoBinaryImage.ts
@@ -3,10 +3,11 @@ import {
BinaryResourceFailure,
BinaryReader,
mappedFileOffset,
- fileBackedMappings,
+ imageFileMappings,
isZeroFilledSpan,
type BinaryContainer,
type FileMapping,
+ type ZeroFillMapping,
} from "../native/metadata/BinaryContainer.js";
import { readBinaryContainer } from "../native/metadata/readBinaryContainer.js";
import {
@@ -89,15 +90,11 @@ export const readGoBinaryImage = (bytes: Buffer): GoBinaryImage => {
: { section_names_limitation: sectionNamesLimitation }),
};
const { search, header } = selected;
+ const mappings = imageFileMappings(container);
const verifyMapping = (size: number): void => {
const address = search.address + BigInt(header - search.offset);
if (
- mappedFileOffset(
- container.mappings,
- address,
- size,
- container.zero_fills,
- ) !== header
+ mappedFileOffset(mappings, address, size, container.zero_fills) !== header
)
throw new BinaryFormatFailure(
"malformed",
@@ -160,14 +157,16 @@ export const readGoBinaryImage = (bytes: Buffer): GoBinaryImage => {
const pointerBits = ptrSize === 4 ? 32 : 64;
version = readPointerString(
reader,
- container,
+ mappings,
+ container.zero_fills,
reader.word(header + 16, pointerBits),
pointerBits,
);
const moduleField = header + 16 + ptrSize;
module = readOptionalModuleString(
reader,
- container,
+ mappings,
+ container.zero_fills,
reader.word(moduleField, pointerBits),
moduleField,
pointerBits,
@@ -347,12 +346,11 @@ const readInlineExtent = (
const readPointerString = (
reader: BinaryReader,
- container: BinaryContainer,
+ mappings: readonly FileMapping[],
+ zeroFills: readonly ZeroFillMapping[],
address: bigint,
pointerBits: 32 | 64,
): LocatedString => {
- const mappings = fileBackedMappings(container);
- const zeroFills = container.zero_fills;
const width = pointerBits / 8;
const header = mappedFileOffset(mappings, address, width * 2, zeroFills);
const valueAddress = reader.word(header, pointerBits);
@@ -377,19 +375,15 @@ const readPointerString = (
const readOptionalModuleString = (
reader: BinaryReader,
- container: BinaryContainer,
+ mappings: readonly FileMapping[],
+ zeroFills: readonly ZeroFillMapping[],
address: bigint,
pointerField: number,
pointerBits: 32 | 64,
): LocatedString =>
- isZeroFilledSpan(
- fileBackedMappings(container),
- container.zero_fills,
- address,
- (pointerBits / 8) * 2,
- )
+ isZeroFilledSpan(mappings, zeroFills, address, (pointerBits / 8) * 2)
? { bytes: Buffer.alloc(0), location: { offset: pointerField, bytes: 0 } }
- : readPointerString(reader, container, address, pointerBits);
+ : readPointerString(reader, mappings, zeroFills, address, pointerBits);
/** Decode metadata text without replacing invalid bytes or stripping an initial BOM. */
export const observedUtf8 = (bytes: Buffer, label: string): string => {
diff --git a/src/gpu/AmdGpuTarget.ts b/src/gpu/AmdGpuTarget.ts
index 1b289aa3cd..903e7afdd0 100644
--- a/src/gpu/AmdGpuTarget.ts
+++ b/src/gpu/AmdGpuTarget.ts
@@ -1,12 +1,12 @@
const processor = String.raw`gfx[0-9a-z]+(?:-[0-9a-z]+)*`;
const targetFeaturesSource = String.raw`(?:(?::[a-z0-9_-]+[+-])|(?:\+[a-z0-9_-]+))*`;
const targetIdSource = `${processor}${targetFeaturesSource}`;
-const targetId = new RegExp(`^${targetIdSource}$`, "iu");
+const targetId = new RegExp(`^(${processor})${targetFeaturesSource}$`, "iu");
const targetTripleSource = String.raw`(?:amdgcn|amdgpu(?:[0-9]+(?:\.[0-9a-f]+)?)?)-amd-amdhsa`;
const bundleTriple = new RegExp(`^(?:hip|hipv4)-${targetTripleSource}-`, "iu");
const bundleTarget = new RegExp(`(?:--|-)(${targetIdSource})$`, "iu");
const noteTarget = new RegExp(
- `^${targetTripleSource}(?:--|-unknown-)((${processor})${targetFeaturesSource})$`,
+ `^${targetTripleSource}(?:--|-unknown-)(${targetIdSource})$`,
"iu",
);
@@ -78,9 +78,15 @@ const ELF_PROCESSORS: Readonly> = {
0xeb: "gfx1250-strict",
};
-/** Target IDs retain the processor spelling and any declared feature modifiers. */
-export const parseAmdGpuTargetId = (value: string): string | null =>
- targetId.test(value) ? value : null;
+/** Retain exact target spelling separately from its canonical processor identity. */
+export const parseAmdGpuTargetId = (
+ value: string,
+): { readonly key: string; readonly processor: string } | null => {
+ const match = targetId.exec(value);
+ return match?.[1] === undefined
+ ? null
+ : { key: value, processor: match[1].toLowerCase() };
+};
/** Clang HIP bundle IDs carry an AMD triple followed by an optional target ID. */
export const isAmdGpuBundleId = (name: string): boolean =>
@@ -89,17 +95,17 @@ export const isAmdGpuBundleId = (name: string): boolean =>
export const readAmdGpuBundleTarget = (name: string): string | null => {
if (!isAmdGpuBundleId(name)) return null;
const target = bundleTarget.exec(name)?.[1];
- return target === undefined ? null : parseAmdGpuTargetId(target);
+ return target === undefined
+ ? null
+ : (parseAmdGpuTargetId(target)?.key ?? null);
};
/** Preserve the full target ID separately from its processor identity. */
export const readAmdGpuNoteTarget = (
target: string,
): { readonly key: string; readonly processor: string } | null => {
- const match = noteTarget.exec(target);
- return match?.[1] === undefined || match[2] === undefined
- ? null
- : { key: match[1], processor: match[2] };
+ const value = noteTarget.exec(target)?.[1];
+ return value === undefined ? null : parseAmdGpuTargetId(value);
};
/** Decode the processor field from AMDGPU ELF e_flags (EF_AMDGPU_MACH). */
diff --git a/src/gpu/GpuElfKernels.ts b/src/gpu/GpuElfKernels.ts
index c944a08f0d..09f6107ae2 100644
--- a/src/gpu/GpuElfKernels.ts
+++ b/src/gpu/GpuElfKernels.ts
@@ -392,7 +392,7 @@ export const readAmdElfMetadata = async (
const processorConflict =
processor !== null &&
noteIdentity !== null &&
- processor !== noteIdentity.processor.toLowerCase();
+ processor !== noteIdentity.processor;
const conflict = noteConflict || processorConflict;
const limitation = noteConflict
? "AMDGPU notes declare conflicting processor targets; processor identity is unknown"
diff --git a/src/gpu/GpuInventory.ts b/src/gpu/GpuInventory.ts
index 3b1c310459..db40b49a0c 100644
--- a/src/gpu/GpuInventory.ts
+++ b/src/gpu/GpuInventory.ts
@@ -52,6 +52,7 @@ import {
readPtxKernels,
readPtxTarget,
} from "./PtxKernels.js";
+import { parseAmdGpuTargetId } from "./AmdGpuTarget.js";
/** File declarations only. Every child range stays in its named parent coordinate space. */
export class GpuInventory {
@@ -428,12 +429,16 @@ export class GpuInventory {
state = base === inner ? "base-only" : "conflict";
} else if (ptxTargetIdentity(outer) === ptxTargetIdentity(payload.key))
state = "consistent";
- else if (
- row.kind === "amdgpu" &&
- outer.split(":")[0] === payload.key.split(":")[0]
- )
- state = "unknown";
- else state = "conflict";
+ else if (row.kind === "amdgpu") {
+ const outerTarget = parseAmdGpuTargetId(outer);
+ const payloadTarget = parseAmdGpuTargetId(payload.key);
+ state =
+ outerTarget !== null &&
+ payloadTarget !== null &&
+ outerTarget.processor === payloadTarget.processor
+ ? "unknown"
+ : "conflict";
+ } else state = "conflict";
}
this.#retain({
payload_target: payload.value,
diff --git a/src/hopper/HopperFileOffset.ts b/src/hopper/HopperFileOffset.ts
index fbb6cf7f2d..99cc014974 100644
--- a/src/hopper/HopperFileOffset.ts
+++ b/src/hopper/HopperFileOffset.ts
@@ -26,6 +26,7 @@ import type { JsonValue } from "../domain/jsonValue.js";
import type { FileOffsetPartialObservation } from "../domain/native/fileOffsetPartialObservation.js";
import {
readFatSliceDeclarations,
+ readMachoHeaderPrefixFacts,
type FatSliceDeclaration,
validateFatSlice,
} from "../domain/apple/machoContainer.js";
@@ -428,18 +429,12 @@ function originalOffset(
observation.provider_image_header_hex === null
? null
: Buffer.from(observation.provider_image_header_hex, "hex");
- const loadedMagic = header?.readUInt32BE(0);
- if (
- header === null ||
- loadedMagic === undefined ||
- ![0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe].includes(loadedMagic)
- )
+ const loadedIdentity =
+ header === null ? null : readMachoHeaderPrefixFacts(header).identity;
+ if (header === null || loadedIdentity === null)
return err(
"Hopper did not expose the loaded Mach-O header needed to identify its FAT slice",
);
- const little = loadedMagic === 0xcefaedfe || loadedMagic === 0xcffaedfe;
- const cpu = little ? header.readUInt32LE(4) : header.readUInt32BE(4);
- const subtype = little ? header.readUInt32LE(8) : header.readUInt32BE(8);
const matches = [];
const ranges = [...layout.value.slices].sort((a, b) => a.offset - b.offset);
for (let index = 1; index < ranges.length; index++) {
@@ -460,25 +455,22 @@ function originalOffset(
return err(
`FAT architecture ${slice.index} has a truncated Mach-O header`,
);
- const embeddedMagic = embedded.readUInt32BE(0);
- if (
- ![0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe].includes(embeddedMagic)
- )
+ const embeddedIdentity = readMachoHeaderPrefixFacts(embedded).identity;
+ if (embeddedIdentity === null)
return err(
`FAT architecture ${slice.index} does not contain a Mach-O image`,
);
- const embeddedLittle =
- embeddedMagic === 0xcefaedfe || embeddedMagic === 0xcffaedfe;
- const identityError = validateFatSlice(slice, size, layout.value.tableEnd, {
- cpuType: embeddedLittle
- ? embedded.readUInt32LE(4)
- : embedded.readUInt32BE(4),
- cpuSubtype: embeddedLittle
- ? embedded.readUInt32LE(8)
- : embedded.readUInt32BE(8),
- });
+ const identityError = validateFatSlice(
+ slice,
+ size,
+ layout.value.tableEnd,
+ embeddedIdentity,
+ );
if (identityError !== null) return err(identityError);
- if (slice.cpuType === cpu && slice.cpuSubtype === subtype)
+ if (
+ slice.cpuType === loadedIdentity.cpuType &&
+ slice.cpuSubtype === loadedIdentity.cpuSubtype
+ )
matches.push(slice);
}
const selected = matches.length === 1 ? matches[0] : undefined;
diff --git a/src/hopper/HopperMachOImage.ts b/src/hopper/HopperMachOImage.ts
index ab2f948802..ad2c9e3bf4 100644
--- a/src/hopper/HopperMachOImage.ts
+++ b/src/hopper/HopperMachOImage.ts
@@ -15,6 +15,7 @@ import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import {
readFatSliceDeclarations,
+ readMachoHeaderPrefixFacts,
validateFatSlice,
type FatSliceDeclaration,
} from "../domain/apple/machoContainer.js";
@@ -321,15 +322,14 @@ const inspectHopperMachOImage = async (
return invalid(
`Thin Mach-O source changed after target admission: expected SHA-256 ${target.sha256}, observed ${observedSha256}`,
);
- const little = magic === 0xcefaedfe || magic === 0xcffaedfe;
- const headerMagic = prefix.readUInt32BE(0);
- if (headerMagic !== magic)
+ const facts = readMachoHeaderPrefixFacts(prefix);
+ if (facts.magic !== magic || facts.identity === null)
return invalid("Thin Mach-O header changed during profile resolution");
return ok({
method: "thin_verified_copy",
byte_length: size,
- cpu_type: little ? prefix.readUInt32LE(4) : prefix.readUInt32BE(4),
- cpu_subtype: little ? prefix.readUInt32LE(8) : prefix.readUInt32BE(8),
+ cpu_type: facts.identity.cpuType,
+ cpu_subtype: facts.identity.cpuSubtype,
});
}
const isFatMagic = [0xcafebabe, 0xcafebabf, 0xbebafeca, 0xbfbafeca].includes(
@@ -390,16 +390,11 @@ const inspectHopperMachOImage = async (
const embedded = await read(slice.offset, 12);
if (embedded.length !== 12)
return invalid(`FAT architecture ${slice.index} became truncated`);
- const thinMagic = embedded.readUInt32BE(0);
- if (![0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe].includes(thinMagic))
+ const identity = readMachoHeaderPrefixFacts(embedded).identity;
+ if (identity === null)
return invalid(
`FAT architecture ${slice.index} does not contain a Mach-O image`,
);
- const little = thinMagic === 0xcefaedfe || thinMagic === 0xcffaedfe;
- const identity = {
- cpuType: little ? embedded.readUInt32LE(4) : embedded.readUInt32BE(4),
- cpuSubtype: little ? embedded.readUInt32LE(8) : embedded.readUInt32BE(8),
- };
const identityError = validateFatSlice(slice, size, tableEnd, identity);
if (identityError !== null) return invalid(identityError);
ranges.push(slice);
@@ -519,15 +514,12 @@ export const prepareHopperMachOImage = async (
if (!copiedResult.ok) return copiedResult;
const observed = hash.digest("hex");
const header = Buffer.concat(thinHeader);
- const magic = header.length === 12 ? header.readUInt32BE(0) : 0;
- const little = magic === 0xcefaedfe || magic === 0xcffaedfe;
+ const identity = readMachoHeaderPrefixFacts(header).identity;
const headerMatches =
header.length === 12 &&
- [0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe].includes(magic) &&
- (little ? header.readUInt32LE(4) : header.readUInt32BE(4)) ===
- image.cpu_type &&
- (little ? header.readUInt32LE(8) : header.readUInt32BE(8)) ===
- image.cpu_subtype;
+ identity !== null &&
+ identity.cpuType === image.cpu_type &&
+ identity.cpuSubtype === image.cpu_subtype;
if (
tableHash.digest("hex") !== image.container_table_sha256 ||
!headerMatches
@@ -591,15 +583,14 @@ const prepareThinHopperMachOImage = async (
}),
);
const observed = hash.digest("hex");
- const magic = prefixLength === prefix.length ? prefix.readUInt32BE(0) : 0;
- const little = magic === 0xcefaedfe || magic === 0xcffaedfe;
+ const identity = readMachoHeaderPrefixFacts(
+ prefix.subarray(0, prefixLength),
+ ).identity;
const headerMatches =
prefixLength === prefix.length &&
- [0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe].includes(magic) &&
- (little ? prefix.readUInt32LE(4) : prefix.readUInt32BE(4)) ===
- image.cpu_type &&
- (little ? prefix.readUInt32LE(8) : prefix.readUInt32BE(8)) ===
- image.cpu_subtype;
+ identity !== null &&
+ identity.cpuType === image.cpu_type &&
+ identity.cpuSubtype === image.cpu_subtype;
if (
observed !== sourceSha256 ||
copied !== image.byte_length ||
diff --git a/src/il2cpp/ElfRelocations.ts b/src/il2cpp/ElfRelocations.ts
index a3ea8465a4..041d403ef5 100644
--- a/src/il2cpp/ElfRelocations.ts
+++ b/src/il2cpp/ElfRelocations.ts
@@ -1,5 +1,5 @@
import {
- BinaryFormatFailure,
+ BinaryMappingFailure,
mappedFileOffset,
isZeroFilledSpan,
type FileMapping,
@@ -124,14 +124,15 @@ function* dynamicTags(image: Buffer): Generator> {
}
const fileOffset = (
- mappings: readonly FileMapping[],
+ source: RelocationSource,
address: bigint,
size: number,
): number | null => {
try {
- return mappedFileOffset(mappings, address, size);
+ return mappedFileOffset(source.mappings, address, size, source.zeroFills);
} catch (cause) {
- if (cause instanceof BinaryFormatFailure) return null;
+ if (cause instanceof BinaryMappingFailure && cause.reason === "unmapped")
+ return null;
throw cause;
}
};
@@ -163,7 +164,7 @@ function* applyRelaTable(
size > BigInt(source.image.length)
)
throw malformed("invalid DT_RELASZ or DT_RELAENT");
- const start = fileOffset(source.mappings, address, Number(size));
+ const start = fileOffset(source, address, Number(size));
if (start === null) throw malformed("RELA table is not file-backed");
for (let at = start; at < start + Number(size); at += 24) {
if ((at - start) % (24 * 4096) === 0) yield;
@@ -180,11 +181,11 @@ function* applyRelaTable(
output.unresolved.set(destination, Math.max(priorWidth ?? 0, width));
// Registration scans read this private image directly. Clear unknown
// fields so their stored bytes cannot masquerade as resolved pointers.
- const target = fileOffset(source.mappings, destination, width);
+ const target = fileOffset(source, destination, width);
if (target !== null) output.bytes.fill(0, target, target + width);
continue;
}
- const target = fileOffset(source.mappings, destination, 8);
+ const target = fileOffset(source, destination, 8);
const value = source.image.readBigInt64LE(at + 16) & 0xffffffffffffffffn;
if (target !== null) output.bytes.writeBigUInt64LE(value, target);
else if (
diff --git a/src/il2cpp/Il2cppBinaryImage.ts b/src/il2cpp/Il2cppBinaryImage.ts
index 6ad4bedb9f..d443e4503a 100644
--- a/src/il2cpp/Il2cppBinaryImage.ts
+++ b/src/il2cpp/Il2cppBinaryImage.ts
@@ -234,7 +234,12 @@ export const readUInt32 = (
export const isMapped = (image: NativeImage, address: bigint, size: number) => {
try {
- mappedFileOffset(image.container.mappings, address, size);
+ mappedFileOffset(
+ image.container.mappings,
+ address,
+ size,
+ image.container.zero_fills,
+ );
return true;
} catch (cause) {
if (cause instanceof BinaryFormatFailure) return false;
diff --git a/src/native/NativePlistSource.ts b/src/native/NativePlistSource.ts
index 368bd05447..dd112a6272 100644
--- a/src/native/NativePlistSource.ts
+++ b/src/native/NativePlistSource.ts
@@ -24,6 +24,7 @@ import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { fixedAllowanceOptions } from "../domain/fixedAllowanceRemediation.js";
import { err, ok, type Result } from "../domain/result.js";
+import { sameRegularFileState } from "../filesystem/RegularFile.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import {
ArtifactFileCleanupFailure,
@@ -345,12 +346,7 @@ export const readNativePlistBytes = (
{ boundary: "file-size" },
);
if (
- !after.isFile() ||
- before.dev !== after.dev ||
- before.ino !== after.ino ||
- before.size !== after.size ||
- before.mtimeNs !== after.mtimeNs ||
- before.ctimeNs !== after.ctimeNs ||
+ !sameRegularFileState(before, after) ||
BigInt(position) !== before.size
)
throw new ArtifactReaderFailure(
diff --git a/src/native/exports/NativeExportParser.test.ts b/src/native/exports/NativeExportParser.test.ts
index 838241cd2e..79f189c037 100644
--- a/src/native/exports/NativeExportParser.test.ts
+++ b/src/native/exports/NativeExportParser.test.ts
@@ -395,34 +395,26 @@ describe("ELF export version and name coverage", () => {
exports: [],
coverage: { status: "complete" },
});
+ expect(
+ parse(elfExportImage({ symbols: [], dynamicSymbols: false })),
+ ).toMatchObject({ exports: [], coverage: { status: "complete" } });
for (const bytes of [
- elfExportImage({ symbols: [], dynamicSymbols: false }),
elfExportImage({ symbols: [], sectionHeaders: false }),
+ elfExportImage({
+ symbols: [],
+ dynamicSymbols: false,
+ dynamicSegment: true,
+ }),
])
expect(parse(bytes)).toMatchObject({
exports: [],
coverage: {
status: "unsupported",
- reason: expect.stringContaining("No SHT_DYNSYM"),
+ reason: expect.stringContaining("dynamic segment"),
examined_entries: 0,
},
});
});
-
- it("does not turn a null-only section directory into exhaustive export absence", () => {
- const bytes = elfExportImage({ symbols: [{ name: "exported_answer" }] });
- expect(parse(bytes)).toMatchObject({
- exports: [{ name: "exported_answer" }],
- coverage: { status: "complete" },
- });
- // Keep every symbol byte and the existing inactive section-zero header.
- bytes.writeUInt16LE(1, 60);
- bytes.writeUInt16LE(0, 62);
- expect(parse(bytes)).toMatchObject({
- exports: [],
- coverage: { status: "unsupported", examined_entries: 0 },
- });
- });
});
describe("ELF version requirements", () => {
diff --git a/src/native/exports/NativeExportParser.ts b/src/native/exports/NativeExportParser.ts
index ea19806c9a..e32eb74f4e 100644
--- a/src/native/exports/NativeExportParser.ts
+++ b/src/native/exports/NativeExportParser.ts
@@ -3,7 +3,7 @@ import {
BinaryReader,
BinaryResourceFailure,
mappedFileOffset,
- fileBackedMappings,
+ imageFileMappings,
readMappedCString,
} from "../metadata/BinaryContainer.js";
import { architectureForElfMachine } from "../metadata/ElfImage.js";
@@ -111,7 +111,7 @@ const parsePeExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
],
};
const reader = new BinaryReader(bytes, true);
- const mappings = fileBackedMappings(image);
+ const mappings = imageFileMappings(image);
const map = (rva: number, size: number): number =>
mappedFileOffset(mappings, base + BigInt(rva), size, image.zero_fills);
const cString = (rva: number): string => {
@@ -460,20 +460,36 @@ const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
};
const { sections } = directory;
const dynsym = sections.find(({ type }) => type === SHT_DYNSYM);
- if (dynsym === undefined)
+ if (
+ sections.length === 0 ||
+ (dynsym === undefined && directory.has_dynamic_segment)
+ )
return {
...result,
exports: [],
coverage: {
status: "unsupported",
reason:
- "No SHT_DYNSYM section is available; enumerating dynamic symbols from the dynamic segment is not implemented",
+ sections.length === 0
+ ? "The ELF has no section headers; enumerating dynamic symbols from the dynamic segment is not implemented"
+ : "The ELF declares a dynamic segment but has no dynamic symbol section; enumerating dynamic symbols from the dynamic segment is not implemented",
examined_entries: 0,
exports: 0,
},
issues: [],
limitations: ELF_LIMITATIONS,
};
+ if (dynsym === undefined)
+ return {
+ ...result,
+ exports: [],
+ coverage: coverage([], 0, 0),
+ issues: [],
+ limitations: [
+ "The ELF has no dynamic symbol table, so it exports no dynamic symbols.",
+ ...ELF_LIMITATIONS,
+ ],
+ };
const width = directory.bits === 64 ? 24 : 16;
const strings = sections[dynsym.link];
if (
diff --git a/src/native/metadata/BinaryContainer.ts b/src/native/metadata/BinaryContainer.ts
index 7f993c9df0..0e52965016 100644
--- a/src/native/metadata/BinaryContainer.ts
+++ b/src/native/metadata/BinaryContainer.ts
@@ -9,6 +9,16 @@ export class BinaryFormatFailure extends Error {
}
}
+/** A missing file span is a candidate miss; conflicting owners are malformed. */
+export class BinaryMappingFailure extends BinaryFormatFailure {
+ constructor(
+ readonly reason: "unmapped" | "ambiguous",
+ message: string,
+ ) {
+ super("malformed", message);
+ }
+}
+
/** Resource boundary exceeded while decoding metadata or native container structure. */
export class BinaryResourceFailure extends Error {
constructor(
@@ -121,6 +131,14 @@ export interface BinaryContainer {
readonly sections: readonly ImageSection[];
}
+/** All declared file-backed image memory, including PE headers at the image base. */
+export const imageFileMappings = (
+ container: BinaryContainer,
+): readonly FileMapping[] =>
+ container.pe_header_mapping === undefined
+ ? container.mappings
+ : [container.pe_header_mapping, ...container.mappings];
+
/** Reader that checks every structural range before decoding numeric fields. */
export class BinaryReader {
constructor(
@@ -212,14 +230,6 @@ export const declaredFileRange = (
};
};
-/** All file-backed image ranges, including the separately observed PE header. */
-export const fileBackedMappings = (
- image: BinaryContainer,
-): readonly FileMapping[] =>
- image.pe_header_mapping === undefined
- ? image.mappings
- : [image.pe_header_mapping, ...image.mappings];
-
/** Map a physically contiguous virtual span across consistent adjacent ranges. */
export const mappedFileOffset = (
mappings: readonly FileMapping[],
@@ -245,8 +255,8 @@ export const mappedFileOffset = (
continue;
const origin = mapping.offset + Number(address - mapping.address);
if (found !== undefined && found !== origin)
- throw new BinaryFormatFailure(
- "malformed",
+ throw new BinaryMappingFailure(
+ "ambiguous",
"Virtual span has ambiguous or noncontiguous file mappings",
);
found = origin;
@@ -254,8 +264,8 @@ export const mappedFileOffset = (
else if (!whollyCovered) covered.push({ start, end: overlapEnd });
}
if (found === undefined)
- throw new BinaryFormatFailure(
- "malformed",
+ throw new BinaryMappingFailure(
+ "unmapped",
"Virtual address or string range is not file mapped",
);
if (!whollyCovered) {
@@ -265,22 +275,22 @@ export const mappedFileOffset = (
let cursor = address;
for (const span of covered) {
if (span.start > cursor)
- throw new BinaryFormatFailure(
- "malformed",
+ throw new BinaryMappingFailure(
+ "unmapped",
"Virtual address or string range is not file mapped",
);
if (span.end > cursor) cursor = span.end;
}
if (cursor < end)
- throw new BinaryFormatFailure(
- "malformed",
+ throw new BinaryMappingFailure(
+ "unmapped",
"Virtual address or string range is not file mapped",
);
}
for (const zeroFill of zeroFills) {
if (spansOverlap(address, BigInt(size), zeroFill.address, zeroFill.size))
- throw new BinaryFormatFailure(
- "malformed",
+ throw new BinaryMappingFailure(
+ "ambiguous",
"Virtual address has ambiguous file and zero-fill mappings",
);
}
@@ -294,7 +304,7 @@ export const readMappedCString = (
address: bigint,
maximumBytes: number,
): Buffer => {
- const mappings = fileBackedMappings(image);
+ const mappings = imageFileMappings(image);
const chunks: Buffer[] = [];
let length = 0;
while (length <= maximumBytes) {
@@ -305,8 +315,8 @@ export const readMappedCString = (
);
if (mapping === undefined) {
mappedFileOffset(mappings, cursor, 1, image.zero_fills);
- throw new BinaryFormatFailure(
- "malformed",
+ throw new BinaryMappingFailure(
+ "unmapped",
"String range is not file mapped",
);
}
@@ -352,8 +362,8 @@ export const isZeroFilledSpan = (
return false;
for (const mapping of mappings) {
if (spansOverlap(address, bytes, mapping.address, BigInt(mapping.size)))
- throw new BinaryFormatFailure(
- "malformed",
+ throw new BinaryMappingFailure(
+ "ambiguous",
"Virtual address has ambiguous file and zero-fill mappings",
);
}
diff --git a/src/native/metadata/NativeSectionDirectory.ts b/src/native/metadata/NativeSectionDirectory.ts
index 538689e32b..323b79f444 100644
--- a/src/native/metadata/NativeSectionDirectory.ts
+++ b/src/native/metadata/NativeSectionDirectory.ts
@@ -38,6 +38,7 @@ export interface NativeSectionDirectory {
readonly program_offset: bigint;
readonly program_count: number;
readonly program_width: number;
+ readonly has_dynamic_segment: boolean;
readonly sections: readonly NativeSection[];
}
const STRUCTURE_BYTES = 16 * 1024 * 1024;
@@ -130,14 +131,24 @@ export function* readElfSectionDirectory(
reject("ELF extended program count is below its reserved range");
}
}
+ let has_dynamic_segment = false;
if (program_count !== 0) {
if (program_width < (wide ? 56 : 32))
reject("ELF program header width is invalid");
- request(
- fileSize,
- program_offset,
- BigInt(program_count) * BigInt(program_width),
+ const programs = new BinaryReader(
+ yield request(
+ fileSize,
+ program_offset,
+ BigInt(program_count) * BigInt(program_width),
+ ),
+ little,
);
+ for (let index = 0; index < program_count; index++) {
+ if (programs.u32(index * program_width) === 2) {
+ has_dynamic_segment = true;
+ break;
+ }
+ }
}
const sections: NativeSection[] = [];
let section_names_limitation: string | undefined;
@@ -259,6 +270,7 @@ export function* readElfSectionDirectory(
program_offset,
program_count,
program_width,
+ has_dynamic_segment,
sections,
};
}
@@ -346,6 +358,7 @@ export function* readPeSectionDirectory(
program_offset: 0n,
program_count: 0,
program_width: 0,
+ has_dynamic_segment: false,
sections,
};
}
diff --git a/src/native/parsers/otool.ts b/src/native/parsers/otool.ts
index 7b5b755da7..4923a01d64 100644
--- a/src/native/parsers/otool.ts
+++ b/src/native/parsers/otool.ts
@@ -507,10 +507,19 @@ const TEXT_FIELDS = /^(?:segname|sectname|string #\d+)$/u;
*/
const OFFSET_STRING_FIELD = /^(name|path) (.* \(offset \d+\))$/u;
+/**
+ * otool prints segment and section names as ` %.16s`: one separator
+ * space, then the fixed-width name, whose own leading and trailing whitespace
+ * belongs to the Mach-O value.
+ */
+const MACHO_NAME_FIELD = /^(segname|sectname)(?: (.*))?$/u;
+
const parseField = (rawLine: string): readonly [string, string] | undefined => {
const offsetString = OFFSET_STRING_FIELD.exec(rawLine.trimStart());
if (offsetString?.[1] !== undefined && offsetString[2] !== undefined)
return [offsetString[1], offsetString[2]];
+ const name = MACHO_NAME_FIELD.exec(rawLine.trimStart());
+ if (name?.[1] !== undefined) return [name[1], name[2] ?? ""];
// otool prints each linker option as `string #n