mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 19:23:24 +00:00
refactor(verify): give the native verifiers' roots to VerifierFixture (#11180)
* refactor(verify): give the native verifiers' roots to VerifierFixture * fix(verify): report every nested JEB cleanup failure reason * test(verify): give the isolated ADB verifier runs the fixture owner it now imports
This commit is contained in:
1 parent
0f53849160
commit
fdf856ab84
13 files changed
+305
-277
No files matched your search
+26
-17
@@ -1,13 +1,16 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdtemp, readFile, rm, stat } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { readFile, stat } from "node:fs/promises";
|
||||
import { join, posix } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { AdbDeviceAnalysisService } from "../dist/application/adb/AdbDeviceAnalysisService.js";
|
||||
import { createAdbDeviceAnalysisProvider } from "../dist/composition/adb.js";
|
||||
import { closeVerifierResources } from "./lib/verifier-cleanup.mjs";
|
||||
import {
|
||||
VerifierFixture,
|
||||
withVerifierFixture,
|
||||
} from "./lib/verifier-fixture.mjs";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const options = parseArguments(process.argv.slice(2));
|
||||
@@ -279,8 +282,12 @@ try {
|
||||
}
|
||||
|
||||
if (pullCandidate !== null) {
|
||||
const directory = await mkdtemp(join(tmpdir(), "rea-adb-verify-"));
|
||||
try {
|
||||
const directoryFixture = await VerifierFixture.open({
|
||||
prefix: "rea-adb-verify-",
|
||||
private: false,
|
||||
});
|
||||
const directory = directoryFixture.path;
|
||||
await withVerifierFixture(directoryFixture, async () => {
|
||||
const pull = await run("pull_adb_package", {
|
||||
serial,
|
||||
package: pullCandidate,
|
||||
@@ -309,15 +316,17 @@ try {
|
||||
);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// File transfer: push one lane-owned file, verify both digests, pull it
|
||||
// back, then clean up the device copy through the same fixed argv surface.
|
||||
const transferRoot = await mkdtemp(join(tmpdir(), "rea-adb-transfer-"));
|
||||
try {
|
||||
const transferRootFixture = await VerifierFixture.open({
|
||||
prefix: "rea-adb-transfer-",
|
||||
private: false,
|
||||
});
|
||||
const transferRoot = transferRootFixture.path;
|
||||
await withVerifierFixture(transferRootFixture, async () => {
|
||||
const payload = join(transferRoot, "rea-verify.txt");
|
||||
const payloadBytes = "rea adb transfer verification\n";
|
||||
await import("node:fs/promises").then((fs) =>
|
||||
@@ -331,8 +340,12 @@ try {
|
||||
devicePath: deviceTarget,
|
||||
outputDirectory: transferRoot,
|
||||
});
|
||||
const screenRoot = await mkdtemp(join(tmpdir(), "rea-adb-screen-"));
|
||||
try {
|
||||
const screenRootFixture = await VerifierFixture.open({
|
||||
prefix: "rea-adb-screen-",
|
||||
private: false,
|
||||
});
|
||||
const screenRoot = screenRootFixture.path;
|
||||
await withVerifierFixture(screenRootFixture, async () => {
|
||||
const screen = await run("capture_adb_screen", {
|
||||
serial,
|
||||
output_directory: screenRoot,
|
||||
@@ -343,9 +356,7 @@ try {
|
||||
screen.width !== null && screen.height !== null && screen.bytes > 0,
|
||||
`${screen.width}x${screen.height}, ${screen.bytes} bytes`,
|
||||
);
|
||||
} finally {
|
||||
await rm(screenRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
if (options.pull !== null) {
|
||||
const details = await run("inspect_adb_package", {
|
||||
serial,
|
||||
@@ -358,9 +369,7 @@ try {
|
||||
`versionCode=${String(details.version_code)}`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
await rm(transferRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (cause) {
|
||||
verifierFailed = true;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/** Compare binary hardening rows with host toolchains when they exist. */
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
|
||||
import { importVerifierSource } from "./lib/verifier-source-loader.mjs";
|
||||
import { createVerifierCommands } from "./lib/verify-commands.mjs";
|
||||
import { VerifierFixture, onceConfirmed } from "./lib/verifier-fixture.mjs";
|
||||
|
||||
const { readElfHardening } = await importVerifierSource(
|
||||
"../src/native/hardening/ElfHardening.ts",
|
||||
@@ -99,10 +99,11 @@ const versionLine = async (command, args = ["--version"]) => {
|
||||
const statesOf = (rows) =>
|
||||
Object.fromEntries(rows.map((row) => [row.property, row.state]));
|
||||
|
||||
// The root holds every compiled image. Remove it only after the owned
|
||||
// commands have settled, and keep it when their cleanup failed.
|
||||
// The root holds every compiled image. The fixture removes it only after the
|
||||
// owned commands have settled, and keeps it when their cleanup failed.
|
||||
const closeCommands = onceConfirmed(() => commands.close());
|
||||
let rootFixture;
|
||||
let root;
|
||||
let commandsClosed = false;
|
||||
await withVerifierCleanup(async () => {
|
||||
const report = {
|
||||
host: `${process.platform} ${process.arch}`,
|
||||
@@ -144,20 +145,21 @@ await withVerifierCleanup(async () => {
|
||||
}
|
||||
`;
|
||||
|
||||
root = await mkdtemp(join(tmpdir(), "rea-binary-hardening-"));
|
||||
rootFixture = await VerifierFixture.open({
|
||||
prefix: "rea-binary-hardening-",
|
||||
private: false,
|
||||
});
|
||||
rootFixture.releaseAfter(closeCommands);
|
||||
root = rootFixture.path;
|
||||
const cFile = join(root, "sample.c");
|
||||
await writeFile(cFile, source);
|
||||
await verifyElf(root, cFile, report);
|
||||
await verifyMacho(root, cFile, report);
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
}, [
|
||||
closeCommands,
|
||||
async () => {
|
||||
await commands.close();
|
||||
commandsClosed = true;
|
||||
},
|
||||
async () => {
|
||||
if (commandsClosed && root !== undefined)
|
||||
await rm(root, { recursive: true, force: true });
|
||||
await rootFixture?.release();
|
||||
},
|
||||
async () => {
|
||||
process.off("SIGINT", interrupt);
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
* C++ compiler or no readelf.
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join, resolve } from "node:path";
|
||||
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
|
||||
import { createVerifierCommands } from "./lib/verify-commands.mjs";
|
||||
import { VerifierFixture, onceConfirmed } from "./lib/verifier-fixture.mjs";
|
||||
|
||||
/**
|
||||
* Every probe, compiler driver and inspection runs as an owned process tree,
|
||||
@@ -178,10 +178,11 @@ const inspectedStarts = async (path) => {
|
||||
}
|
||||
};
|
||||
|
||||
// The root holds every compiled image. Remove it only after the owned
|
||||
// commands have settled, and keep it when their cleanup failed.
|
||||
// The root holds every compiled image. The fixture removes it only after the
|
||||
// owned commands have settled, and keeps it when their cleanup failed.
|
||||
const closeCommands = onceConfirmed(() => commands.close());
|
||||
let rootFixture;
|
||||
let root;
|
||||
let commandsClosed = false;
|
||||
let unavailable = false;
|
||||
await withVerifierCleanup(async () => {
|
||||
const compiler = await firstAvailable(["zig", "clang++", "g++"]);
|
||||
@@ -203,7 +204,12 @@ await withVerifierCleanup(async () => {
|
||||
unavailable = true;
|
||||
return;
|
||||
}
|
||||
root = await mkdtemp(join(tmpdir(), "rea-elf-unwind-"));
|
||||
rootFixture = await VerifierFixture.open({
|
||||
prefix: "rea-elf-unwind-",
|
||||
private: false,
|
||||
});
|
||||
rootFixture.releaseAfter(closeCommands);
|
||||
root = rootFixture.path;
|
||||
const source = join(root, "ranges.cpp");
|
||||
const unstripped = join(root, "ranges.unstripped");
|
||||
const stripped = join(root, "ranges.stripped");
|
||||
@@ -227,13 +233,9 @@ await withVerifierCleanup(async () => {
|
||||
}),
|
||||
);
|
||||
}, [
|
||||
closeCommands,
|
||||
async () => {
|
||||
await commands.close();
|
||||
commandsClosed = true;
|
||||
},
|
||||
async () => {
|
||||
if (commandsClosed && root !== undefined)
|
||||
await rm(root, { recursive: true, force: true });
|
||||
await rootFixture?.release();
|
||||
},
|
||||
async () => {
|
||||
process.off("SIGINT", interrupt);
|
||||
|
||||
+161
-161
@@ -1,19 +1,19 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { readdir, readFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { toolContract } from "../dist/contracts/toolContracts.js";
|
||||
import { ArtifactResourceScope } from "../dist/artifacts/ArtifactResourceScope.js";
|
||||
import { requireMcpOutput } from "./lib/mcp-verifier-results.mjs";
|
||||
import {
|
||||
closeVerifierResources,
|
||||
withVerifierCleanup,
|
||||
} from "./lib/verifier-cleanup.mjs";
|
||||
import { closeVerifierResources } from "./lib/verifier-cleanup.mjs";
|
||||
import { hashVerifierFile } from "./lib/verifier-file-resources.mjs";
|
||||
import {
|
||||
VerifierFixture,
|
||||
withVerifierFixture,
|
||||
} from "./lib/verifier-fixture.mjs";
|
||||
|
||||
const options = parseArguments(process.argv.slice(2));
|
||||
if (options.help) {
|
||||
@@ -33,7 +33,11 @@ const expectations =
|
||||
? null
|
||||
: JSON.parse(await readFile(options.expectations, "utf8"));
|
||||
// This is a shared test parent, not a leased provider-owned Windows runtime.
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-flutter-verifier-"));
|
||||
const rootFixture = await VerifierFixture.open({
|
||||
prefix: "rea-flutter-verifier-",
|
||||
private: false,
|
||||
});
|
||||
const root = rootFixture.path;
|
||||
const resources = new ArtifactResourceScope();
|
||||
const entrypoint = resolve("scripts/rea.mjs");
|
||||
const environment = Object.fromEntries(
|
||||
@@ -52,169 +56,165 @@ const transport = new StdioClientTransport({
|
||||
stderr: "pipe",
|
||||
});
|
||||
const execute = promisify(execFile);
|
||||
await withVerifierCleanup(
|
||||
async () => {
|
||||
await client.connect(transport);
|
||||
const apk = resolve(options.apk);
|
||||
const identity = await hashVerifierFile(apk, resources);
|
||||
const identified = await run("identify_flutter_build", { path: apk });
|
||||
assert.deepEqual(identified.target, { path: apk, ...identity });
|
||||
assert.equal(identified.flutter_detected, true);
|
||||
assert.equal(identified.coverage, "complete");
|
||||
const abis = identified.abis.filter((abi) => abi.libapp.present);
|
||||
assert.ok(abis.length > 0);
|
||||
const hashes = new Set();
|
||||
// Server shutdown retries retained provider cleanup and settles before the
|
||||
// fixture removes the root; snapshots left under it fail the run and keep it.
|
||||
rootFixture.releaseAfter(async () => {
|
||||
await closeVerifierResources([
|
||||
() => client.close(),
|
||||
() => transport.close(),
|
||||
() => resources.close(),
|
||||
]);
|
||||
assert.deepEqual(
|
||||
await readdir(root),
|
||||
[],
|
||||
`Provider snapshots remain under ${root}`,
|
||||
);
|
||||
});
|
||||
await withVerifierFixture(rootFixture, async () => {
|
||||
await client.connect(transport);
|
||||
const apk = resolve(options.apk);
|
||||
const identity = await hashVerifierFile(apk, resources);
|
||||
const identified = await run("identify_flutter_build", { path: apk });
|
||||
assert.deepEqual(identified.target, { path: apk, ...identity });
|
||||
assert.equal(identified.flutter_detected, true);
|
||||
assert.equal(identified.coverage, "complete");
|
||||
const abis = identified.abis.filter((abi) => abi.libapp.present);
|
||||
assert.ok(abis.length > 0);
|
||||
const hashes = new Set();
|
||||
for (const abi of abis) {
|
||||
assert.equal(abi.libapp.elf_header_valid, true);
|
||||
assert.equal(abi.libapp.snapshot_hash_sources, 2);
|
||||
assert.ok(abi.libapp.snapshot_hash);
|
||||
hashes.add(abi.libapp.snapshot_hash);
|
||||
assert.equal(abi.libflutter.present, true);
|
||||
assert.match(abi.libflutter.build_id ?? "", /^(?:[0-9a-f]{2}){4,64}$/u);
|
||||
assert.deepEqual(abi.libflutter.build_id_candidates, [
|
||||
abi.libflutter.build_id,
|
||||
]);
|
||||
}
|
||||
assert.equal(hashes.size, 1, "Every ABI must agree on the snapshot hash");
|
||||
if (expectations !== null) {
|
||||
assert.equal(identity.sha256, expectations.target_sha256);
|
||||
assert.deepEqual([...hashes], [expectations.snapshot_hash]);
|
||||
assert.deepEqual(
|
||||
abis.map((abi) => abi.abi).sort(),
|
||||
Object.keys(expectations.engines).sort(),
|
||||
);
|
||||
for (const abi of abis) {
|
||||
assert.equal(abi.libapp.elf_header_valid, true);
|
||||
assert.equal(abi.libapp.snapshot_hash_sources, 2);
|
||||
assert.ok(abi.libapp.snapshot_hash);
|
||||
hashes.add(abi.libapp.snapshot_hash);
|
||||
assert.equal(abi.libflutter.present, true);
|
||||
assert.match(abi.libflutter.build_id ?? "", /^(?:[0-9a-f]{2}){4,64}$/u);
|
||||
assert.deepEqual(abi.libflutter.build_id_candidates, [
|
||||
abi.libflutter.build_id,
|
||||
]);
|
||||
}
|
||||
assert.equal(hashes.size, 1, "Every ABI must agree on the snapshot hash");
|
||||
if (expectations !== null) {
|
||||
assert.equal(identity.sha256, expectations.target_sha256);
|
||||
assert.deepEqual([...hashes], [expectations.snapshot_hash]);
|
||||
assert.deepEqual(
|
||||
abis.map((abi) => abi.abi).sort(),
|
||||
Object.keys(expectations.engines).sort(),
|
||||
{
|
||||
dart_version: abi.libflutter.dart_version,
|
||||
toolchain_lines: abi.libflutter.toolchain_lines,
|
||||
},
|
||||
expectations.engines[abi.abi],
|
||||
);
|
||||
for (const abi of abis) {
|
||||
assert.deepEqual(
|
||||
{
|
||||
dart_version: abi.libflutter.dart_version,
|
||||
toolchain_lines: abi.libflutter.toolchain_lines,
|
||||
},
|
||||
expectations.engines[abi.abi],
|
||||
);
|
||||
assert.equal(abi.libflutter.dart_version_capped, false);
|
||||
assert.equal(abi.libflutter.toolchain_capped, false);
|
||||
}
|
||||
assert.equal(abi.libflutter.dart_version_capped, false);
|
||||
assert.equal(abi.libflutter.toolchain_capped, false);
|
||||
}
|
||||
process.stdout.write(
|
||||
"[ok] Exact per-ABI engine labels match digest-bound expectations\n",
|
||||
);
|
||||
} else {
|
||||
process.stdout.write(
|
||||
"[unverified] Exact engine labels and their absence need --expectations JSON\n",
|
||||
);
|
||||
}
|
||||
// The same checks apply to the default selection and each remaining ABI.
|
||||
const preferred = await run("inspect_dart_aot", { path: apk });
|
||||
assert.ok(
|
||||
abis.some((abi) => abi.abi === preferred.abi),
|
||||
"Default AOT selection must be one of the identified ABIs",
|
||||
);
|
||||
for (const abi of abis) {
|
||||
const aot =
|
||||
abi.abi === preferred.abi
|
||||
? preferred
|
||||
: await run("inspect_dart_aot", { path: apk, abi: abi.abi });
|
||||
assert.equal(aot.abi, abi.abi);
|
||||
assert.equal(
|
||||
aot.libapp.coverage,
|
||||
"complete",
|
||||
`Snapshot decoding must be complete on ${abi.abi}`,
|
||||
);
|
||||
assert.equal(aot.coverage, aot.string_pool.coverage);
|
||||
if (aot.coverage !== "complete") {
|
||||
process.stdout.write(
|
||||
"[ok] Exact per-ABI engine labels match digest-bound expectations\n",
|
||||
`[unverified] ${abi.abi}: full string-pool projection (returned pool is bounded and partial)\n`,
|
||||
);
|
||||
}
|
||||
assert.deepEqual(aot.target, identified.target);
|
||||
assert.equal(aot.libapp.sha256, abi.libapp.sha256);
|
||||
assert.equal(aot.libapp.bytes, abi.libapp.bytes);
|
||||
assert.equal(aot.libapp.snapshot_hash_sources, 2);
|
||||
assert.equal(aot.libapp.snapshot_hash, abi.libapp.snapshot_hash);
|
||||
assert.deepEqual(
|
||||
aot.libapp.sections.map((section) => section.name).sort(),
|
||||
[
|
||||
"_kDartVmSnapshotData",
|
||||
"_kDartVmSnapshotInstructions",
|
||||
"_kDartIsolateSnapshotData",
|
||||
"_kDartIsolateSnapshotInstructions",
|
||||
].sort(),
|
||||
);
|
||||
for (const section of aot.libapp.sections) {
|
||||
assert.ok(section.offset + section.size <= aot.libapp.bytes);
|
||||
if (!section.name.endsWith("Data")) continue;
|
||||
assert.equal(section.magic_valid, true);
|
||||
assert.equal(
|
||||
section.kind,
|
||||
3,
|
||||
"This verifier requires an AOT app snapshot",
|
||||
);
|
||||
assert.ok(
|
||||
section.header_length >= 48 &&
|
||||
section.header_length + 4 <= section.size,
|
||||
`Snapshot extent must fit ${section.name} on ${abi.abi}`,
|
||||
);
|
||||
}
|
||||
assert.ok(aot.string_pool.package_uri_count_at_least > 0);
|
||||
assert.ok(aot.string_pool.dart_uri_count_at_least > 0);
|
||||
if (expectations?.string_pools !== undefined) {
|
||||
const expected = expectations.string_pools[abi.abi];
|
||||
assert.ok(
|
||||
expected,
|
||||
`Pinned string-pool counts are required on ${abi.abi}`,
|
||||
);
|
||||
assert.equal(
|
||||
aot.string_pool.package_uri_count_at_least,
|
||||
expected.package_uri_count_at_least,
|
||||
);
|
||||
assert.equal(
|
||||
aot.string_pool.dart_uri_count_at_least,
|
||||
expected.dart_uri_count_at_least,
|
||||
);
|
||||
process.stdout.write(
|
||||
`[ok] ${abi.abi}: exact pinned package and Dart URI counts\n`,
|
||||
);
|
||||
} else {
|
||||
process.stdout.write(
|
||||
"[unverified] Exact engine labels and their absence need --expectations JSON\n",
|
||||
`[unverified] ${abi.abi}: exact string-pool counts need --expectations JSON\n`,
|
||||
);
|
||||
}
|
||||
// The same checks apply to the default selection and each remaining ABI.
|
||||
const preferred = await run("inspect_dart_aot", { path: apk });
|
||||
assert.ok(
|
||||
abis.some((abi) => abi.abi === preferred.abi),
|
||||
"Default AOT selection must be one of the identified ABIs",
|
||||
process.stdout.write(
|
||||
`[ok] ${abi.abi}: four symbols, bounded AOT headers, agreeing hashes and string pool\n`,
|
||||
);
|
||||
for (const abi of abis) {
|
||||
const aot =
|
||||
abi.abi === preferred.abi
|
||||
? preferred
|
||||
: await run("inspect_dart_aot", { path: apk, abi: abi.abi });
|
||||
assert.equal(aot.abi, abi.abi);
|
||||
assert.equal(
|
||||
aot.libapp.coverage,
|
||||
"complete",
|
||||
`Snapshot decoding must be complete on ${abi.abi}`,
|
||||
);
|
||||
assert.equal(aot.coverage, aot.string_pool.coverage);
|
||||
if (aot.coverage !== "complete") {
|
||||
process.stdout.write(
|
||||
`[unverified] ${abi.abi}: full string-pool projection (returned pool is bounded and partial)\n`,
|
||||
);
|
||||
}
|
||||
assert.deepEqual(aot.target, identified.target);
|
||||
assert.equal(aot.libapp.sha256, abi.libapp.sha256);
|
||||
assert.equal(aot.libapp.bytes, abi.libapp.bytes);
|
||||
assert.equal(aot.libapp.snapshot_hash_sources, 2);
|
||||
assert.equal(aot.libapp.snapshot_hash, abi.libapp.snapshot_hash);
|
||||
assert.deepEqual(
|
||||
aot.libapp.sections.map((section) => section.name).sort(),
|
||||
[
|
||||
"_kDartVmSnapshotData",
|
||||
"_kDartVmSnapshotInstructions",
|
||||
"_kDartIsolateSnapshotData",
|
||||
"_kDartIsolateSnapshotInstructions",
|
||||
].sort(),
|
||||
);
|
||||
for (const section of aot.libapp.sections) {
|
||||
assert.ok(section.offset + section.size <= aot.libapp.bytes);
|
||||
if (!section.name.endsWith("Data")) continue;
|
||||
assert.equal(section.magic_valid, true);
|
||||
assert.equal(
|
||||
section.kind,
|
||||
3,
|
||||
"This verifier requires an AOT app snapshot",
|
||||
);
|
||||
assert.ok(
|
||||
section.header_length >= 48 &&
|
||||
section.header_length + 4 <= section.size,
|
||||
`Snapshot extent must fit ${section.name} on ${abi.abi}`,
|
||||
);
|
||||
}
|
||||
assert.ok(aot.string_pool.package_uri_count_at_least > 0);
|
||||
assert.ok(aot.string_pool.dart_uri_count_at_least > 0);
|
||||
if (expectations?.string_pools !== undefined) {
|
||||
const expected = expectations.string_pools[abi.abi];
|
||||
assert.ok(
|
||||
expected,
|
||||
`Pinned string-pool counts are required on ${abi.abi}`,
|
||||
);
|
||||
assert.equal(
|
||||
aot.string_pool.package_uri_count_at_least,
|
||||
expected.package_uri_count_at_least,
|
||||
);
|
||||
assert.equal(
|
||||
aot.string_pool.dart_uri_count_at_least,
|
||||
expected.dart_uri_count_at_least,
|
||||
);
|
||||
process.stdout.write(
|
||||
`[ok] ${abi.abi}: exact pinned package and Dart URI counts\n`,
|
||||
);
|
||||
} else {
|
||||
process.stdout.write(
|
||||
`[unverified] ${abi.abi}: exact string-pool counts need --expectations JSON\n`,
|
||||
);
|
||||
}
|
||||
process.stdout.write(
|
||||
`[ok] ${abi.abi}: four symbols, bounded AOT headers, agreeing hashes and string pool\n`,
|
||||
);
|
||||
}
|
||||
if (options.plainApk !== null) {
|
||||
const plain = resolve(options.plainApk);
|
||||
const negative = await run("identify_flutter_build", { path: plain });
|
||||
assert.deepEqual(negative.target, {
|
||||
path: plain,
|
||||
...(await hashVerifierFile(plain, resources)),
|
||||
});
|
||||
assert.equal(negative.flutter_detected, false);
|
||||
assert.equal(negative.coverage, "complete");
|
||||
}
|
||||
assert.deepEqual(
|
||||
await hashVerifierFile(apk, resources),
|
||||
identity,
|
||||
"Verification must preserve its input",
|
||||
);
|
||||
},
|
||||
// Server shutdown retries retained provider cleanup and settles before success.
|
||||
[
|
||||
async () => {
|
||||
await closeVerifierResources([
|
||||
() => client.close(),
|
||||
() => transport.close(),
|
||||
() => resources.close(),
|
||||
]);
|
||||
assert.deepEqual(
|
||||
await readdir(root),
|
||||
[],
|
||||
`Provider snapshots remain under ${root}`,
|
||||
);
|
||||
await rm(root, { recursive: true, force: true });
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
if (options.plainApk !== null) {
|
||||
const plain = resolve(options.plainApk);
|
||||
const negative = await run("identify_flutter_build", { path: plain });
|
||||
assert.deepEqual(negative.target, {
|
||||
path: plain,
|
||||
...(await hashVerifierFile(plain, resources)),
|
||||
});
|
||||
assert.equal(negative.flutter_detected, false);
|
||||
assert.equal(negative.coverage, "complete");
|
||||
}
|
||||
assert.deepEqual(
|
||||
await hashVerifierFile(apk, resources),
|
||||
identity,
|
||||
"Verification must preserve its input",
|
||||
);
|
||||
});
|
||||
process.stdout.write(
|
||||
"all asserted Flutter CLI/MCP checks passed; unverified facets are reported above\n",
|
||||
);
|
||||
|
||||
+15
-9
@@ -1,12 +1,12 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { JebAnalysisService } from "../dist/application/jeb/JebAnalysisService.js";
|
||||
import { createJebAnalysisProvider } from "../dist/composition/jeb.js";
|
||||
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
|
||||
import { VerifierFixture } from "./lib/verifier-fixture.mjs";
|
||||
|
||||
const run = promisify(execFile);
|
||||
|
||||
@@ -26,10 +26,15 @@ if (options.help) {
|
||||
|
||||
const provider = createJebAnalysisProvider(process.env);
|
||||
const service = new JebAnalysisService(provider);
|
||||
let fixture;
|
||||
let fixtureDirectory;
|
||||
try {
|
||||
const report = await withVerifierCleanup(async () => {
|
||||
fixtureDirectory = await mkdtemp(join(tmpdir(), "rea-jeb-"));
|
||||
fixture = await VerifierFixture.open({
|
||||
prefix: "rea-jeb-",
|
||||
private: false,
|
||||
});
|
||||
fixtureDirectory = fixture.path;
|
||||
const client = await service.execute("inspect_jeb_client", {});
|
||||
if (!client.ok) throw new Error(client.error.message);
|
||||
const engine = client.value.normalized_result.engine;
|
||||
@@ -89,18 +94,19 @@ try {
|
||||
};
|
||||
}, [
|
||||
async () => {
|
||||
// The fixture is removed only after the provider confirms it closed.
|
||||
await provider.close();
|
||||
if (fixtureDirectory !== undefined) {
|
||||
await rm(fixtureDirectory, { recursive: true, force: true });
|
||||
fixtureDirectory = undefined;
|
||||
}
|
||||
await fixture?.release();
|
||||
},
|
||||
]);
|
||||
process.stdout.write(`${JSON.stringify(report)}\n`);
|
||||
} catch (cause) {
|
||||
if (fixtureDirectory !== undefined)
|
||||
if (fixture !== undefined && !fixture.released)
|
||||
process.stderr.write(`JEB fixture retained at ${fixtureDirectory}\n`);
|
||||
const causes = cause instanceof AggregateError ? cause.errors : [cause];
|
||||
// Fixture and cleanup failures nest AggregateErrors; report each leaf reason.
|
||||
const leaves = (reason) =>
|
||||
reason instanceof AggregateError ? reason.errors.flatMap(leaves) : [reason];
|
||||
const causes = leaves(cause);
|
||||
process.stderr.write(
|
||||
`JEB real-provider verification failed: ${causes.map((reason) => (reason instanceof Error ? reason.message : String(reason))).join("; ")}\n`,
|
||||
);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { requireMcpToolError } from "./lib/mcp-verifier-results.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { basename, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
@@ -14,6 +13,10 @@ import {
|
||||
artifactMcpCall,
|
||||
withArtifactMcp,
|
||||
} from "./lib/artifact-e2e.mjs";
|
||||
import {
|
||||
VerifierFixture,
|
||||
withVerifierFixture,
|
||||
} from "./lib/verifier-fixture.mjs";
|
||||
|
||||
const exec = promisify(execFile);
|
||||
|
||||
@@ -264,8 +267,12 @@ const checkHardenedRuntime = async (directory, binary) => {
|
||||
assert.equal(traced.process.exit_status, 0);
|
||||
};
|
||||
|
||||
const directory = await mkdtemp(join(tmpdir(), "rea-native-calls-"));
|
||||
try {
|
||||
const directoryFixture = await VerifierFixture.open({
|
||||
prefix: "rea-native-calls-",
|
||||
private: false,
|
||||
});
|
||||
const directory = directoryFixture.path;
|
||||
await withVerifierFixture(directoryFixture, async () => {
|
||||
const binary = join(directory, "ReaCalls");
|
||||
await exec("/usr/bin/xcrun", [
|
||||
"clang",
|
||||
@@ -346,6 +353,4 @@ try {
|
||||
},
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile, rm } from "node:fs/promises";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { isAbsolute, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createPackageVerifierCommands } from "./lib/verify-package-commands.mjs";
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
artifactMcpResult,
|
||||
withArtifactMcp,
|
||||
} from "./lib/artifact-e2e.mjs";
|
||||
import { createCanonicalTemporaryDirectory } from "./lib/canonical-temporary-directory.mjs";
|
||||
import { VerifierFixture } from "./lib/verifier-fixture.mjs";
|
||||
|
||||
const cancellation = new AbortController();
|
||||
let shutdownSignal;
|
||||
@@ -47,15 +47,14 @@ const callerLines = new Set(
|
||||
return matches[0];
|
||||
}),
|
||||
);
|
||||
const allocatedDirectory =
|
||||
await createCanonicalTemporaryDirectory("rea-pascal-calls-");
|
||||
let directory = allocatedDirectory.path;
|
||||
const directoryFixture = await VerifierFixture.open({
|
||||
prefix: "rea-pascal-calls-",
|
||||
private: false,
|
||||
});
|
||||
let directory = directoryFixture.path;
|
||||
// The package command owner releases the fixture after its commands close.
|
||||
const commands = await createPackageVerifierCommands({
|
||||
workspace: {
|
||||
path: allocatedDirectory.allocatedPath,
|
||||
close: () =>
|
||||
rm(allocatedDirectory.allocatedPath, { recursive: true, force: true }),
|
||||
},
|
||||
workspace: { path: directory, close: () => directoryFixture.release() },
|
||||
});
|
||||
const execute = (command, arguments_, { timeout }) =>
|
||||
commands.exec(command, arguments_, {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { execFile, spawn } from "node:child_process";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, mkdir, writeFile, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { mkdir, writeFile, readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
@@ -10,12 +9,17 @@ import { observeNativeUi } from "../dist/native/NativeUiObservation.js";
|
||||
import { NATIVE_UI_HELPER_MAX_BUFFER } from "../dist/native/NativeUiOutputBudget.js";
|
||||
import { artifactMcpResult, withArtifactMcp } from "./lib/artifact-e2e.mjs";
|
||||
import { observeProcessStartIdentity } from "../dist/process/ProcessOwnershipObservation.js";
|
||||
import { VerifierFixture } from "./lib/verifier-fixture.mjs";
|
||||
|
||||
if (process.platform !== "darwin")
|
||||
throw new Error(
|
||||
"Native UI verification requires macOS, an interactive desktop and Xcode command-line tools",
|
||||
);
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-ui-fixture-"));
|
||||
const rootFixture = await VerifierFixture.open({
|
||||
prefix: "rea-ui-fixture-",
|
||||
private: false,
|
||||
});
|
||||
const root = rootFixture.path;
|
||||
let child;
|
||||
let childExit;
|
||||
let fixturePid;
|
||||
@@ -294,7 +298,7 @@ try {
|
||||
}
|
||||
const fixtureExited =
|
||||
child === undefined || (await openExitedWithFixture(child, childExit));
|
||||
if (fixtureExited) await rm(root, { recursive: true, force: true });
|
||||
if (fixtureExited) await rootFixture.release();
|
||||
else {
|
||||
// The app may have been reparented away from `open`; stopping `open`
|
||||
// does not prove the fixture exited, so its root stays in place.
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { join, resolve } from "node:path";
|
||||
import {
|
||||
VerifierFixture,
|
||||
withVerifierFixture,
|
||||
} from "./lib/verifier-fixture.mjs";
|
||||
|
||||
// Requires a running Docker daemon, network access for the pinned fixture
|
||||
// producer, and `npm run build:cached`. Never executes the injected application.
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-node-sea-real-"));
|
||||
const rootFixture = await VerifierFixture.open({
|
||||
prefix: "rea-node-sea-real-",
|
||||
private: false,
|
||||
});
|
||||
const root = rootFixture.path;
|
||||
const image = "node:24.10.0-bookworm";
|
||||
const script =
|
||||
'throw new Error("SEA verification must not execute this application");\n';
|
||||
try {
|
||||
await withVerifierFixture(rootFixture, async () => {
|
||||
await writeFile(join(root, "main.js"), script);
|
||||
await writeFile(join(root, "asset.txt"), "real SEA asset\n");
|
||||
await writeFile(
|
||||
@@ -76,6 +83,4 @@ try {
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ producer: image, metadata, verified: ["ELF inventory", "code cache", "fuse", "script extraction", "asset extraction"] }, null, 2)}\n`,
|
||||
);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -1,13 +1,20 @@
|
||||
/** Build real PE TLS samples with Zig, then inspect through the public CLI. */
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join, resolve } from "node:path";
|
||||
import {
|
||||
VerifierFixture,
|
||||
withVerifierFixture,
|
||||
} from "./lib/verifier-fixture.mjs";
|
||||
|
||||
const zig = process.env.REA_ZIG ?? "zig";
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-pe-load-config-"));
|
||||
try {
|
||||
const rootFixture = await VerifierFixture.open({
|
||||
prefix: "rea-pe-load-config-",
|
||||
private: false,
|
||||
});
|
||||
const root = rootFixture.path;
|
||||
await withVerifierFixture(rootFixture, async () => {
|
||||
const source = join(root, "tls.c");
|
||||
await writeFile(
|
||||
source,
|
||||
@@ -77,6 +84,4 @@ void entry(void) {}
|
||||
}),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -8,18 +8,11 @@ import assert from "node:assert/strict";
|
||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import { createPackageVerifierCommands } from "./lib/verify-package-commands.mjs";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import {
|
||||
access,
|
||||
constants,
|
||||
mkdtemp,
|
||||
readFile,
|
||||
rm,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { access, constants, readFile, writeFile } from "node:fs/promises";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
import { createServer } from "../dist/server/createServer.js";
|
||||
import { VerifierFixture } from "./lib/verifier-fixture.mjs";
|
||||
|
||||
const cli = resolve("scripts/rea.mjs");
|
||||
const runCli = async (...arguments_) => {
|
||||
@@ -58,12 +51,14 @@ if (process.env.EXPECTED_PLATFORM !== undefined) {
|
||||
"Cutter runner did not match its declared platform.",
|
||||
);
|
||||
}
|
||||
const projectRoot = await mkdtemp(join(tmpdir(), "rea-cutter-smoke-"));
|
||||
const projectFixture = await VerifierFixture.open({
|
||||
prefix: "rea-cutter-smoke-",
|
||||
private: false,
|
||||
});
|
||||
const projectRoot = projectFixture.path;
|
||||
// The package command owner releases the fixture after its commands close.
|
||||
const commands = await createPackageVerifierCommands({
|
||||
workspace: {
|
||||
path: projectRoot,
|
||||
close: () => rm(projectRoot, { recursive: true, force: true }),
|
||||
},
|
||||
workspace: { path: projectRoot, close: () => projectFixture.release() },
|
||||
});
|
||||
const projectPath = join(projectRoot, "smoke.rzdb");
|
||||
await withVerifierCleanup(async () => {
|
||||
|
||||
@@ -375,17 +375,6 @@
|
||||
"src/sqlite/SqliteDatabaseProvider.ts": 1
|
||||
},
|
||||
"verifier-fixture-root": {
|
||||
"scripts/verify-adb.mjs": 3,
|
||||
"scripts/verify-binary-hardening.mjs": 1,
|
||||
"scripts/verify-elf-unwind-ranges.mjs": 1,
|
||||
"scripts/verify-flutter.mjs": 1,
|
||||
"scripts/verify-jeb.mjs": 1,
|
||||
"scripts/verify-native-calls.mjs": 1,
|
||||
"scripts/verify-native-pascal-calls.mjs": 1,
|
||||
"scripts/verify-native-ui.mjs": 1,
|
||||
"scripts/verify-node-sea.mjs": 1,
|
||||
"scripts/verify-pe-load-config.mjs": 1,
|
||||
"scripts/verify-real-cutter.mjs": 1,
|
||||
"scripts/verify-test-temp-hygiene.mjs": 1,
|
||||
"scripts/verify-windows-native.mjs": 1
|
||||
},
|
||||
|
||||
@@ -37,12 +37,19 @@ const runVerifier = async (
|
||||
join(root, "scripts", "verify-adb.mjs"),
|
||||
await readFile(new URL("../../../scripts/verify-adb.mjs", import.meta.url)),
|
||||
);
|
||||
await writeFile(
|
||||
join(root, "scripts", "lib", "verifier-cleanup.mjs"),
|
||||
await readFile(
|
||||
new URL("../../../scripts/lib/verifier-cleanup.mjs", import.meta.url),
|
||||
),
|
||||
);
|
||||
// The verifier's cleanup and fixture owners, with the fixture's imports.
|
||||
for (const helper of [
|
||||
"verifier-cleanup.mjs",
|
||||
"verifier-fixture.mjs",
|
||||
"canonical-temporary-directory.mjs",
|
||||
"verifier-source-loader.mjs",
|
||||
])
|
||||
await writeFile(
|
||||
join(root, "scripts", "lib", helper),
|
||||
await readFile(
|
||||
new URL(`../../../scripts/lib/${helper}`, import.meta.url),
|
||||
),
|
||||
);
|
||||
// The simulated device directory stands in for /data/local/tmp; the fake
|
||||
// adb client records its argv and removes only the named probe.
|
||||
const device = join(root, "device");
|
||||
|
||||
Reference in new issue
Block a user