mirror of
https://github.com/morluto/rea.git
synced 2026-10-22 13:52:32 -05:00
* feat(native): decode Swift nominal type declarations from __swift5_types Fill swift_decls from the nominal type context descriptors that survive stripping: kind (class, struct, enum, actor), own and qualified name, module, generic flag, anonymous-context flag, metadata accessor and descriptor location, with evidence citing the type record. Unresolved parent contexts keep qualified_name null and the entry partial. One shared reader walks each context once; class vtable recovery now consumes its descriptors instead of re-reading the records, so a type record that cannot be read keeps both facets partial. Remove the never-populated swift_decls fields that descriptors do not carry. Fixes #5341 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(native): bound Swift context walks and decode vtables in record order Address review of the nominal-type reader: - Cache intrinsic context depth (distance to the module) so a cached parent cannot bypass the 32-context bound, and never cache a stop that depends on where the walk began. Bound qualified names to 4096 characters so nesting cannot multiply name storage. - Visit each descriptor as soon as its kind and name are read, so a class vtable spends the record budget in record order instead of after every later declaration. - Report an accessor outside executable mapped code as null and keep the declaration partial. - Forward a name failure to vtable coverage only when the record is a class or its kind is unknown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
354 lines
12 KiB
JavaScript
354 lines
12 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { execFile } from "node:child_process";
|
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { promisify } from "node:util";
|
|
import { parseBinaryTarget } from "../../../dist/application/BinaryTargetResolver.js";
|
|
import { ArtifactResourceScope } from "../../../dist/artifacts/ArtifactResourceScope.js";
|
|
import { inspectAppleDispatchMetadata } from "../../../dist/native/AppleDispatchMetadata.js";
|
|
import {
|
|
artifactCliEvidence,
|
|
artifactMcpResult,
|
|
withArtifactMcp,
|
|
} from "../../lib/artifact-e2e.mjs";
|
|
import { nativeDispatchMetadataResultSchema } from "../../../dist/domain/native/objcSwiftMetadata.js";
|
|
|
|
if (process.platform !== "darwin")
|
|
throw new Error(
|
|
"Apple dispatch verification requires macOS with a host Objective-C compiler and Foundation SDK",
|
|
);
|
|
const exec = promisify(execFile);
|
|
const source = (name) =>
|
|
fileURLToPath(
|
|
new URL(`../../../tests/conformance/native/${name}`, import.meta.url),
|
|
);
|
|
|
|
/** Both link modes: legacy LC_DYLD_INFO binds and the default chained fixups. */
|
|
const LINK_MODES = [
|
|
{
|
|
mode: "dyld-info",
|
|
flags: ["-Wl,-no_fixup_chains"],
|
|
fixups: "LC_DYLD_INFO rebase opcodes",
|
|
swift: true,
|
|
},
|
|
{ mode: "chained", flags: [], fixups: "chained fixups:", swift: true },
|
|
// arm64e uses authenticated chained pointers (DYLD_CHAINED_PTR_ARM64E_*).
|
|
...(process.arch === "arm64"
|
|
? [
|
|
{
|
|
mode: "chained-arm64e",
|
|
flags: ["-arch", "arm64e"],
|
|
fixups: "chained fixups: DYLD_CHAINED_PTR_ARM64E",
|
|
swift: false,
|
|
},
|
|
]
|
|
: []),
|
|
];
|
|
|
|
const fixupCoverage = (metadata) =>
|
|
metadata.coverage.find(({ facet }) => facet === "pointer_fixups");
|
|
|
|
const inspect = async (path) => {
|
|
const target = await parseBinaryTarget(path);
|
|
if (!target.ok) throw target.error;
|
|
const resources = new ArtifactResourceScope();
|
|
try {
|
|
return {
|
|
target: target.value,
|
|
metadata: (
|
|
await resources.run(() =>
|
|
inspectAppleDispatchMetadata(target.value, 5000, resources),
|
|
)
|
|
).result,
|
|
};
|
|
} finally {
|
|
// Retry any descriptor whose close failed, surfacing unresolved cleanup.
|
|
await resources.close();
|
|
}
|
|
};
|
|
|
|
const checkObjc = (metadata, mode) => {
|
|
assert.equal(fixupCoverage(metadata)?.status, "complete");
|
|
const fixture = metadata.objc_classes.find(
|
|
({ name, is_meta_class: meta }) => name === "ReaDispatchFixture" && !meta,
|
|
);
|
|
const category = metadata.objc_categories.find(
|
|
({ name }) => name === "ReaDispatchAdditions",
|
|
);
|
|
const objcCoverage = metadata.coverage.find(
|
|
({ facet }) => facet === "objc_class_method_ivar_metadata",
|
|
);
|
|
assert.equal(fixture?.is_meta_class, false);
|
|
assert.equal(fixture?.is_root_class, false);
|
|
assert.equal(
|
|
fixture?.properties.find(({ name }) => name === "fixtureName")?.atomicity,
|
|
"nonatomic",
|
|
);
|
|
assert.equal(
|
|
fixture?.properties.find(({ name }) => name === "atomicValue")?.atomicity,
|
|
"atomic",
|
|
);
|
|
for (const method of fixture.methods) {
|
|
assert.equal(method.is_required, null);
|
|
assert.equal(method.is_optional, null);
|
|
}
|
|
for (const property of fixture.properties)
|
|
for (const attribute of property.attributes)
|
|
assert.deepEqual(Object.keys(attribute).sort(), ["name", "value"]);
|
|
const protocol = metadata.objc_protocols.find(
|
|
({ name }) => name === "ReaDispatchProtocol",
|
|
);
|
|
assert.equal(protocol?.methods[0]?.is_required, true);
|
|
assert.equal(protocol?.methods[0]?.is_optional, false);
|
|
assert.equal(protocol?.optional_methods[0]?.is_required, false);
|
|
assert.equal(protocol?.optional_methods[0]?.is_optional, true);
|
|
if (
|
|
!metadata.objc_dispatch_implementations.some(
|
|
(item) =>
|
|
item.class_name === "ReaDispatchFixture" &&
|
|
item.selector === "performAction:" &&
|
|
item.implementation_address !== null,
|
|
) ||
|
|
!metadata.objc_dispatch_implementations.some(
|
|
(item) =>
|
|
item.selector === "fixtureVersion" && item.method_type === "class",
|
|
) ||
|
|
!metadata.objc_ivars.some(
|
|
(item) => item.name === "state" && item.offset !== null,
|
|
) ||
|
|
!metadata.objc_protocol_records.some(
|
|
(protocol) =>
|
|
protocol.name === "ReaDispatchProtocol" &&
|
|
protocol.methods.some(
|
|
(method) => method.selector === "performAction:",
|
|
) &&
|
|
protocol.optional_methods.some(
|
|
(method) => method.selector === "optionalFixtureValue",
|
|
),
|
|
) ||
|
|
// The superclass lives in libobjc, so it comes from a bind fixup.
|
|
fixture?.super_class !== "NSObject" ||
|
|
fixture.decode.status !== "decoded" ||
|
|
!fixture.properties.some(
|
|
(property) =>
|
|
property.name === "fixtureName" &&
|
|
property.type_encoding === '@"NSString"' &&
|
|
property.attributes.some(({ name }) => name === "C"),
|
|
) ||
|
|
category?.class_name !== "NSString" ||
|
|
category.class_source !== "external" ||
|
|
!category.instance_methods.includes("reaFixtureLength") ||
|
|
!metadata.objc_dispatch_implementations.some(
|
|
(item) =>
|
|
item.class_name === "NSString" &&
|
|
item.category === "ReaDispatchAdditions" &&
|
|
item.selector === "reaFixtureLength",
|
|
) ||
|
|
objcCoverage?.status !== "complete" ||
|
|
fixupCoverage(metadata)?.reason?.startsWith(mode.fixups) !== true ||
|
|
(mode.mode === "dyld-info") !==
|
|
(fixupCoverage(metadata)?.reason?.includes(
|
|
"LC_DYLD_INFO bind opcodes",
|
|
) ===
|
|
true)
|
|
)
|
|
throw new Error(
|
|
`Apple metadata fixture drifted (${mode.mode}): ${JSON.stringify(metadata)}`,
|
|
);
|
|
};
|
|
|
|
const checkSwift = (metadata, mode) => {
|
|
assert.equal(fixupCoverage(metadata)?.status, "complete");
|
|
if (
|
|
!metadata.swift_dispatch_slots.some(
|
|
(slot) =>
|
|
slot.table_kind === "class_vtable" &&
|
|
slot.owner === "ReaVtableFixture" &&
|
|
slot.decode.status === "decoded",
|
|
)
|
|
)
|
|
throw new Error(
|
|
`Swift class vtable fixture unavailable (${mode.mode}): ${JSON.stringify(metadata.coverage)}`,
|
|
);
|
|
if (
|
|
!metadata.swift_conformances.some(
|
|
(item) =>
|
|
item.type_name === "ReaWitnessFixture" &&
|
|
item.protocol_name === "ReaWitnessFixtureProtocol",
|
|
) ||
|
|
!metadata.swift_dispatch_slots.some(
|
|
(item) =>
|
|
item.owner === "ReaWitnessFixture: ReaWitnessFixtureProtocol" &&
|
|
item.implementation_address !== null,
|
|
) ||
|
|
fixupCoverage(metadata)?.reason?.startsWith(mode.fixups) !== true ||
|
|
(mode.mode === "dyld-info") !==
|
|
(fixupCoverage(metadata)?.reason?.includes(
|
|
"LC_DYLD_INFO bind opcodes",
|
|
) ===
|
|
true)
|
|
)
|
|
throw new Error(
|
|
`Swift metadata fixture drifted (${mode.mode}): ${JSON.stringify(metadata)}`,
|
|
);
|
|
};
|
|
|
|
/** Declarations from dispatch.swift that survive stripping unchanged. */
|
|
const EXPECTED_SWIFT_DECLS = [
|
|
["struct", "ReaWitnessFixture", false],
|
|
["class", "ReaVtableFixture", false],
|
|
["enum", "ReaDeclFixtureEnum", false],
|
|
["struct", "ReaDeclFixtureOuter", false],
|
|
["struct", "ReaDeclFixtureOuter.Inner", false],
|
|
["struct", "ReaDeclFixtureGeneric", true],
|
|
["actor", "ReaDeclFixtureActor", false],
|
|
];
|
|
|
|
/** Stable declaration facts compared across the symbols and stripped builds. */
|
|
const swiftDeclarations = (metadata) =>
|
|
metadata.swift_decls.map((item) => ({
|
|
kind: item.kind,
|
|
qualified_name: item.qualified_name,
|
|
module: item.module,
|
|
generic: item.generic,
|
|
descriptor: item.location.address,
|
|
accessor: item.accessor_address,
|
|
status: item.decode.status,
|
|
}));
|
|
|
|
const checkSwiftDeclarations = (metadata, mode) => {
|
|
const declarations = swiftDeclarations(metadata);
|
|
for (const [kind, qualifiedName, generic] of EXPECTED_SWIFT_DECLS)
|
|
assert.ok(
|
|
declarations.some(
|
|
(item) =>
|
|
item.kind === kind &&
|
|
item.qualified_name === qualifiedName &&
|
|
item.module === "ReaWitnessFixture" &&
|
|
item.generic === generic &&
|
|
item.accessor !== null &&
|
|
item.status === "decoded",
|
|
),
|
|
`Swift declaration ${qualifiedName} missing (${mode.mode}): ${JSON.stringify(declarations)}`,
|
|
);
|
|
assert.equal(
|
|
metadata.coverage.find(({ facet }) => facet === "swift_declarations")
|
|
?.status,
|
|
"complete",
|
|
);
|
|
return declarations;
|
|
};
|
|
|
|
/** Descriptor and accessor addresses must be the compiler's Mn/Ma symbols. */
|
|
const checkSwiftSymbolAddresses = async (path, declarations) => {
|
|
const { stdout } = await exec("/usr/bin/nm", ["--defined-only", path]);
|
|
const symbols = (suffix) =>
|
|
new Set(
|
|
stdout.split("\n").flatMap((line) => {
|
|
const [address, , name] = line.trim().split(/\s+/u);
|
|
return name?.endsWith(suffix) === true
|
|
? [`0x${BigInt(`0x${address}`).toString(16)}`]
|
|
: [];
|
|
}),
|
|
);
|
|
const descriptors = symbols("Mn");
|
|
const accessors = symbols("Ma");
|
|
for (const item of declarations) {
|
|
assert.ok(descriptors.has(item.descriptor), JSON.stringify(item));
|
|
assert.ok(accessors.has(item.accessor), JSON.stringify(item));
|
|
}
|
|
};
|
|
|
|
const root = await mkdtemp(join(tmpdir(), "rea-dispatch-"));
|
|
let report;
|
|
try {
|
|
const results = [];
|
|
for (const mode of LINK_MODES) {
|
|
const objcPath = join(root, `fixture-${mode.mode}`);
|
|
await exec("/usr/bin/xcrun", [
|
|
"clang",
|
|
"-O2",
|
|
"-framework",
|
|
"Foundation",
|
|
...mode.flags,
|
|
source("dispatch.m"),
|
|
"-o",
|
|
objcPath,
|
|
]);
|
|
for (const variant of ["symbols", "stripped"]) {
|
|
if (variant === "stripped")
|
|
await exec("/usr/bin/strip", ["-x", objcPath]);
|
|
const { target, metadata } = await inspect(objcPath);
|
|
checkObjc(metadata, mode);
|
|
if (mode.mode === "chained" && variant === "stripped") {
|
|
const cli = await artifactCliEvidence(
|
|
"inspect-native-dispatch-metadata",
|
|
objcPath,
|
|
);
|
|
const cliResult = nativeDispatchMetadataResultSchema.parse(
|
|
cli.normalized_result,
|
|
);
|
|
assert.equal(cliResult.target_sha256, target.sha256);
|
|
checkObjc(cliResult.result, mode);
|
|
await withArtifactMcp(objcPath, async (client) => {
|
|
const mcpResult = nativeDispatchMetadataResultSchema.parse(
|
|
await artifactMcpResult(client, "inspect_native_dispatch_metadata"),
|
|
);
|
|
assert.equal(mcpResult.target_sha256, target.sha256);
|
|
checkObjc(mcpResult.result, mode);
|
|
assert.deepEqual(mcpResult.result, cliResult.result);
|
|
});
|
|
}
|
|
results.push({
|
|
variant: `objc-${mode.mode}-${variant}`,
|
|
target_sha256: target.sha256,
|
|
classes: metadata.objc_classes.length,
|
|
categories: metadata.objc_categories.length,
|
|
implementations: metadata.objc_dispatch_implementations.length,
|
|
fixups: fixupCoverage(metadata)?.reason,
|
|
});
|
|
}
|
|
if (!mode.swift) continue;
|
|
const swiftPath = join(root, `libReaWitnessFixture-${mode.mode}.dylib`);
|
|
await exec("/usr/bin/xcrun", [
|
|
"swiftc",
|
|
"-emit-library",
|
|
"-module-name",
|
|
"ReaWitnessFixture",
|
|
"-O",
|
|
...mode.flags
|
|
.map((flag) => flag.replace(/^-Wl,/u, "-Xlinker\0"))
|
|
.flatMap((flag) => flag.split("\0")),
|
|
source("dispatch.swift"),
|
|
"-o",
|
|
swiftPath,
|
|
]);
|
|
let unstrippedDeclarations;
|
|
for (const variant of ["symbols", "stripped"]) {
|
|
if (variant === "stripped")
|
|
await exec("/usr/bin/strip", ["-x", swiftPath]);
|
|
const { target, metadata } = await inspect(swiftPath);
|
|
checkSwift(metadata, mode);
|
|
const declarations = checkSwiftDeclarations(metadata, mode);
|
|
if (variant === "symbols") {
|
|
await checkSwiftSymbolAddresses(swiftPath, declarations);
|
|
unstrippedDeclarations = declarations;
|
|
} else assert.deepEqual(declarations, unstrippedDeclarations);
|
|
results.push({
|
|
variant: `swift-${mode.mode}-${variant}`,
|
|
target_sha256: target.sha256,
|
|
declarations: declarations.length,
|
|
conformances: metadata.swift_conformances.length,
|
|
slots: metadata.swift_dispatch_slots.length,
|
|
fixups: fixupCoverage(metadata)?.reason,
|
|
});
|
|
}
|
|
}
|
|
report = { ok: true, target_executed: false, fixtures: results };
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
process.stdout.write(`${JSON.stringify(report)}\n`);
|