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 * fix(verify): report every nested JEB cleanup failure reason * test(verify): give the isolated ADB verifier runs the fixture owner it now imports
636 lines
20 KiB
JavaScript
636 lines
20 KiB
JavaScript
/** Compare binary hardening rows with host toolchains when they exist. */
|
|
import assert from "node:assert/strict";
|
|
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",
|
|
import.meta.url,
|
|
);
|
|
const { readMachoHardening } = await importVerifierSource(
|
|
"../src/native/hardening/MachoHardening.ts",
|
|
import.meta.url,
|
|
);
|
|
const { readPeHardening } = await importVerifierSource(
|
|
"../src/native/hardening/PeHardening.ts",
|
|
import.meta.url,
|
|
);
|
|
const { peHardeningImage } = await importVerifierSource(
|
|
"../src/native/hardening/BinaryHardening.fixture.ts",
|
|
import.meta.url,
|
|
);
|
|
const { hasMachoMagic } = await importVerifierSource(
|
|
"../src/domain/apple/machoContainer.ts",
|
|
import.meta.url,
|
|
);
|
|
|
|
/**
|
|
* Every probe and compiler driver runs as an owned process tree, so a timeout
|
|
* or close also stops the children a driver starts. Close it before removing
|
|
* the compiled outputs.
|
|
*/
|
|
const commands = await createVerifierCommands();
|
|
|
|
/**
|
|
* Ctrl-C or SIGTERM aborts the running command instead of ending Node, so the
|
|
* owned process trees and the compiled outputs are still cleaned up below.
|
|
*/
|
|
const interruption = new AbortController();
|
|
const interrupt = (signal) =>
|
|
interruption.abort(
|
|
new Error(`Binary hardening verifier interrupted by ${signal}`),
|
|
);
|
|
process.on("SIGINT", interrupt);
|
|
process.on("SIGTERM", interrupt);
|
|
|
|
/** Report an exit status the way the probes read it; other failures propagate. */
|
|
const run = async (command, args) => {
|
|
try {
|
|
const { stdout, stderr } = await commands.exec(command, args, {
|
|
maxBuffer: 16 * 1024 * 1024,
|
|
timeout: 120_000,
|
|
signal: interruption.signal,
|
|
});
|
|
return { status: 0, stdout, stderr };
|
|
} catch (failure) {
|
|
if (typeof failure?.code === "number" && failure.code !== 0)
|
|
return {
|
|
status: failure.code,
|
|
stdout: failure.snapshot.stdout.text,
|
|
stderr: failure.snapshot.stderr.text,
|
|
};
|
|
// A probe for an absent tool, such as xcrun off macOS, is not a failure.
|
|
if (failure?.code === "ENOENT")
|
|
return { status: null, stdout: "", stderr: String(failure.message) };
|
|
throw failure;
|
|
}
|
|
};
|
|
|
|
const which = async (command) => {
|
|
const result = await run("which", [command]);
|
|
return result.status === 0 ? result.stdout.trim() : null;
|
|
};
|
|
|
|
const xcrunFind = async (name) => {
|
|
const result = await run("xcrun", ["--find", name]);
|
|
return {
|
|
name,
|
|
status: result.status,
|
|
path: result.status === 0 ? result.stdout.trim() : null,
|
|
stderr: (result.stderr ?? "").trim().slice(0, 240),
|
|
};
|
|
};
|
|
|
|
const findTool = async (name) =>
|
|
(await which(name)) ?? (await xcrunFind(name)).path;
|
|
|
|
const versionLine = async (command, args = ["--version"]) => {
|
|
const result = await run(command, args);
|
|
return `${result.stdout ?? ""}\n${result.stderr ?? ""}`
|
|
.trim()
|
|
.split("\n")[0]
|
|
?.slice(0, 240);
|
|
};
|
|
|
|
const statesOf = (rows) =>
|
|
Object.fromEntries(rows.map((row) => [row.property, row.state]));
|
|
|
|
// 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;
|
|
await withVerifierCleanup(async () => {
|
|
const report = {
|
|
host: `${process.platform} ${process.arch}`,
|
|
node: process.version,
|
|
npm: await versionLine("npm", ["-v"]),
|
|
xcrun: await Promise.all(
|
|
["readelf", "llvm-readelf", "clang", "gcc", "cc", "ld"].map(xcrunFind),
|
|
),
|
|
unavailable: [],
|
|
checks: [],
|
|
};
|
|
|
|
const synthetic = peHardeningImage();
|
|
const peImage = await readPeHardening(synthetic, {
|
|
path: "/artifacts/synthetic.exe",
|
|
sha256: "a".repeat(64),
|
|
bytes: synthetic.length,
|
|
});
|
|
const peStates = statesOf(peImage.rows);
|
|
assert.equal(peStates.dynamic_base, "enabled");
|
|
assert.equal(peStates.high_entropy_va, "enabled");
|
|
assert.equal(peStates.nx_compat, "enabled");
|
|
assert.equal(peStates.force_integrity, "disabled");
|
|
assert.equal(peStates.no_seh, "disabled");
|
|
assert.equal(peStates.guard_cf, "enabled");
|
|
assert.equal(peStates.cfg_instrumented, "enabled");
|
|
assert.equal(peStates.authenticode_present, "disabled");
|
|
assert.equal(peStates.debug_record_present, "disabled");
|
|
report.checks.push({
|
|
kind: "synthetic-pe",
|
|
bits: peImage.bits,
|
|
states: peStates,
|
|
});
|
|
|
|
const source = `int main(void) {
|
|
volatile char buf[64];
|
|
buf[0] = 1;
|
|
return (int)buf[0];
|
|
}
|
|
`;
|
|
|
|
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 rootFixture?.release();
|
|
},
|
|
async () => {
|
|
process.off("SIGINT", interrupt);
|
|
process.off("SIGTERM", interrupt);
|
|
},
|
|
]);
|
|
// A signal that lands outside a command, such as during the final rm, is still
|
|
// a failed run. An earlier failure already left through the rejection above.
|
|
if (interruption.signal.aborted) throw interruption.signal.reason;
|
|
|
|
async function compilerCandidate(
|
|
name,
|
|
command,
|
|
prefix,
|
|
versionArgs = ["--version"],
|
|
) {
|
|
const located = await findTool(command);
|
|
if (located === null) return null;
|
|
return { name, command: located, prefix, versionArgs };
|
|
}
|
|
|
|
async function verifyElf(directory, cFile, output) {
|
|
const readelf =
|
|
(await findTool("readelf")) ?? (await findTool("llvm-readelf"));
|
|
const candidates = await Promise.all([
|
|
compilerCandidate("x86_64-linux-gnu-gcc", "x86_64-linux-gnu-gcc", []),
|
|
compilerCandidate("aarch64-linux-gnu-gcc", "aarch64-linux-gnu-gcc", []),
|
|
compilerCandidate("musl-gcc", "musl-gcc", []),
|
|
compilerCandidate("gcc", "gcc", []),
|
|
compilerCandidate("clang-linux-gnu", "clang", [
|
|
"--target=x86_64-linux-gnu",
|
|
]),
|
|
compilerCandidate("clang-linux-musl", "clang", [
|
|
"--target=x86_64-linux-musl",
|
|
]),
|
|
compilerCandidate(
|
|
"zig-linux-gnu",
|
|
"zig",
|
|
["cc", "-target", "x86_64-linux-gnu"],
|
|
["version"],
|
|
),
|
|
compilerCandidate(
|
|
"zig-linux-musl",
|
|
"zig",
|
|
["cc", "-target", "x86_64-linux-musl"],
|
|
["version"],
|
|
),
|
|
]);
|
|
const compilers = candidates.filter((candidate) => candidate !== null);
|
|
output.tools = {
|
|
...output.tools,
|
|
readelf,
|
|
readelfVersion:
|
|
readelf === null ? null : await versionLine(readelf, ["--version"]),
|
|
compilers: await Promise.all(
|
|
compilers.map(async (compiler) => ({
|
|
name: compiler.name,
|
|
command: compiler.command,
|
|
version: await versionLine(compiler.command, compiler.versionArgs),
|
|
})),
|
|
),
|
|
};
|
|
if (compilers.length === 0) {
|
|
output.unavailable.push({
|
|
kind: "elf",
|
|
reason:
|
|
"No x86_64-linux-gnu-gcc, aarch64-linux-gnu-gcc, musl-gcc, gcc, clang, or zig was found.",
|
|
compilers: [],
|
|
readelf,
|
|
});
|
|
return;
|
|
}
|
|
if (readelf === null)
|
|
output.unavailable.push({
|
|
kind: "elf-readelf",
|
|
reason:
|
|
"No readelf or llvm-readelf was found. Compiled ELF images are still parsed.",
|
|
readelf: null,
|
|
});
|
|
const variants = [
|
|
{ name: "pie", flags: ["-fPIE", "-pie"] },
|
|
{ name: "no-pie", flags: ["-fno-pie", "-no-pie"] },
|
|
{ name: "relro-now", flags: ["-fPIE", "-pie", "-Wl,-z,relro,-z,now"] },
|
|
{ name: "relro", flags: ["-fPIE", "-pie", "-Wl,-z,relro,-z,lazy"] },
|
|
{ name: "no-relro", flags: ["-fPIE", "-pie", "-Wl,-z,norelro"] },
|
|
{
|
|
name: "stack-protector",
|
|
flags: ["-fPIE", "-pie", "-fstack-protector-strong"],
|
|
},
|
|
{ name: "execstack", flags: ["-fPIE", "-pie", "-Wl,-z,execstack"] },
|
|
{ name: "build-id", flags: ["-fPIE", "-pie", "-Wl,--build-id=sha1"] },
|
|
{ name: "dynamic", flags: ["-fPIE", "-pie", "-dynamic"] },
|
|
{ name: "two-needed", twoNeeded: true },
|
|
];
|
|
for (const variant of variants) {
|
|
let met = false;
|
|
for (const compiler of compilers) {
|
|
const binary = join(directory, `${compiler.name}-${variant.name}`);
|
|
const compiled = variant.twoNeeded
|
|
? await compileTwoNeeded(compiler, directory, binary)
|
|
: await run(compiler.command, [
|
|
...compiler.prefix,
|
|
...variant.flags,
|
|
cFile,
|
|
"-o",
|
|
binary,
|
|
]);
|
|
if (compiled.status !== 0) {
|
|
output.checks.push({
|
|
kind: "elf-compile-skipped",
|
|
compiler: compiler.name,
|
|
variant: variant.name,
|
|
status: compiled.status,
|
|
stderr: (compiled.stderr ?? "").slice(0, 400),
|
|
});
|
|
continue;
|
|
}
|
|
const header = await readFile(binary);
|
|
if (header.subarray(0, 4).toString("latin1") !== "\u007fELF") {
|
|
output.checks.push({
|
|
kind: "elf-compile-skipped",
|
|
compiler: compiler.name,
|
|
variant: variant.name,
|
|
reason: "compiler did not emit ELF",
|
|
});
|
|
continue;
|
|
}
|
|
const observed = observeElfTags(header);
|
|
let expected = null;
|
|
let needed = observed.needed;
|
|
let buildId = observed.buildId;
|
|
if (readelf !== null) {
|
|
const dumped = await run(readelf, [
|
|
"--wide",
|
|
"-h",
|
|
"-l",
|
|
"-d",
|
|
"-S",
|
|
"-s",
|
|
"-n",
|
|
binary,
|
|
]);
|
|
if (dumped.status !== 0)
|
|
throw new Error(
|
|
`${readelf} failed for ${variant.name}: ${dumped.stderr}`,
|
|
);
|
|
expected = factsFromReadelf(dumped.stdout);
|
|
needed = dumped.stdout.match(/\(NEEDED\)/g)?.length ?? 0;
|
|
buildId = /Build ID|NT_GNU_BUILD_ID|GNU_BUILD_ID/i.test(dumped.stdout);
|
|
}
|
|
const image = readElfHardening(header);
|
|
const actual = statesOf(image.rows);
|
|
if (expected !== null)
|
|
assertElf(actual, expected, `${compiler.name} ${variant.name}`);
|
|
const satisfied = featureMet(variant.name, actual);
|
|
output.checks.push({
|
|
kind: "elf",
|
|
compiler: compiler.name,
|
|
variant: variant.name,
|
|
bytes: header.length,
|
|
readelf,
|
|
expected,
|
|
actual,
|
|
needed,
|
|
buildId,
|
|
noteAlign: observed.noteAlign,
|
|
featureMet: satisfied,
|
|
});
|
|
if (satisfied) {
|
|
met = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!met)
|
|
output.unavailable.push({
|
|
kind: "elf-feature",
|
|
variant: variant.name,
|
|
reason: unmetReason(variant.name),
|
|
});
|
|
}
|
|
}
|
|
|
|
async function compileTwoNeeded(compiler, directory, binary) {
|
|
const libaSource = join(directory, "liba.c");
|
|
const libbSource = join(directory, "libb.c");
|
|
const mainSource = join(directory, "needed-main.c");
|
|
await writeFile(libaSource, "int a_fn(void) { return 1; }\n");
|
|
await writeFile(libbSource, "int b_fn(void) { return 2; }\n");
|
|
await writeFile(
|
|
mainSource,
|
|
"int a_fn(void);\nint b_fn(void);\nint main(void) { return a_fn() + b_fn(); }\n",
|
|
);
|
|
const liba = join(directory, "liba.so");
|
|
const libb = join(directory, "libb.so");
|
|
const first = await run(compiler.command, [
|
|
...compiler.prefix,
|
|
"-shared",
|
|
"-fPIC",
|
|
libaSource,
|
|
"-o",
|
|
liba,
|
|
]);
|
|
if (first.status !== 0) return first;
|
|
const second = await run(compiler.command, [
|
|
...compiler.prefix,
|
|
"-shared",
|
|
"-fPIC",
|
|
libbSource,
|
|
"-o",
|
|
libb,
|
|
]);
|
|
if (second.status !== 0) return second;
|
|
return await run(compiler.command, [
|
|
...compiler.prefix,
|
|
"-fPIE",
|
|
"-pie",
|
|
"-dynamic",
|
|
"-L",
|
|
directory,
|
|
mainSource,
|
|
"-la",
|
|
"-lb",
|
|
"-o",
|
|
binary,
|
|
]);
|
|
}
|
|
|
|
/** Count DT_NEEDED and a 36-byte GNU build-id note in a little-endian ELF. */
|
|
function observeElfTags(header) {
|
|
const empty = { needed: null, buildId: false, noteAlign: [] };
|
|
if (header.length < 52 || header[0] !== 0x7f || header[5] !== 1) return empty;
|
|
const bits = header[4] === 2 ? 64 : header[4] === 1 ? 32 : 0;
|
|
if (bits === 0) return empty;
|
|
const u16 = (offset) => header.readUInt16LE(offset);
|
|
const u32 = (offset) => header.readUInt32LE(offset);
|
|
const u64 = (offset) => header.readBigUInt64LE(offset);
|
|
const word = (offset) => (bits === 64 ? u64(offset) : BigInt(u32(offset)));
|
|
const phoff = Number(bits === 64 ? u64(32) : u32(28));
|
|
const shoff = Number(bits === 64 ? u64(40) : u32(32));
|
|
const phentsize = u16(bits === 64 ? 54 : 42);
|
|
const phnum = u16(bits === 64 ? 56 : 44);
|
|
const shentsize = u16(bits === 64 ? 58 : 46);
|
|
const shnum = u16(bits === 64 ? 60 : 48);
|
|
let needed = null;
|
|
if (
|
|
phentsize > 0 &&
|
|
phnum > 0 &&
|
|
phoff >= 0 &&
|
|
phoff + phnum * phentsize <= header.length
|
|
) {
|
|
for (let index = 0; index < phnum; index += 1) {
|
|
const at = phoff + index * phentsize;
|
|
if (u32(at) !== 2) continue;
|
|
needed ??= 0;
|
|
const off = Number(bits === 64 ? u64(at + 8) : u32(at + 4));
|
|
const size = Number(bits === 64 ? u64(at + 32) : u32(at + 16));
|
|
const width = bits === 64 ? 16 : 8;
|
|
if (off < 0 || size < 0 || off + size > header.length) continue;
|
|
for (let cursor = 0; cursor + width <= size; cursor += width) {
|
|
const tag = bits === 64 ? u64(off + cursor) : BigInt(u32(off + cursor));
|
|
if (tag === 0n) break;
|
|
if (tag === 1n) needed += 1;
|
|
}
|
|
}
|
|
}
|
|
const noteAlign = [];
|
|
let buildId = false;
|
|
if (
|
|
shentsize > 0 &&
|
|
shnum > 0 &&
|
|
shoff >= 0 &&
|
|
shoff + shnum * shentsize <= header.length
|
|
) {
|
|
for (let index = 0; index < shnum; index += 1) {
|
|
const at = shoff + index * shentsize;
|
|
if (u32(at + 4) !== 7) continue;
|
|
const off = Number(bits === 64 ? u64(at + 24) : u32(at + 16));
|
|
const size = Number(word(bits === 64 ? at + 32 : at + 20));
|
|
const align = word(bits === 64 ? at + 48 : at + 32);
|
|
noteAlign.push(`${align.toString()}:${String(size)}`);
|
|
if (off >= 0 && size >= 16 && off + 16 <= header.length) {
|
|
const owner = header.subarray(off + 12, off + 16).toString("latin1");
|
|
if (
|
|
u32(off) === 4 &&
|
|
u32(off + 4) === 20 &&
|
|
u32(off + 8) === 3 &&
|
|
owner.startsWith("GNU")
|
|
)
|
|
buildId = true;
|
|
}
|
|
}
|
|
}
|
|
return { needed, buildId, noteAlign };
|
|
}
|
|
|
|
function dynsymText(text) {
|
|
const marker = "Symbol table '.dynsym'";
|
|
const start = text.indexOf(marker);
|
|
if (start < 0) return "";
|
|
const rest = text.slice(start);
|
|
const next = rest.indexOf("\nSymbol table ", marker.length);
|
|
return next < 0 ? rest : rest.slice(0, next);
|
|
}
|
|
|
|
function featureMet(variant, actual) {
|
|
if (variant === "execstack") return actual.nx === "disabled";
|
|
if (variant === "stack-protector")
|
|
return actual.stack_protector === "enabled";
|
|
return true;
|
|
}
|
|
|
|
function unmetReason(variant) {
|
|
if (variant === "execstack")
|
|
return "No compiler emitted an ELF whose PT_GNU_STACK is executable, so NX disabled was not observed.";
|
|
if (variant === "stack-protector")
|
|
return "No compiler emitted an ELF with an undefined dynamic __stack_chk_fail, so stack_protector enabled was not observed.";
|
|
return "No compiler emitted a readable ELF image for this variant.";
|
|
}
|
|
|
|
function factsFromReadelf(text) {
|
|
const type = /Type:\s+(\S+)/.exec(text)?.[1] ?? "";
|
|
const hasInterp = /\bINTERP\b/.test(text);
|
|
const flags1 = /FLAGS_1\b[^\n]*/.exec(text)?.[0] ?? "";
|
|
const stack = /GNU_STACK\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+([RWE]+)/i.exec(
|
|
text,
|
|
);
|
|
const dynsym = dynsymText(text);
|
|
return {
|
|
type,
|
|
pie: type === "DYN" && (hasInterp || /\bPIE\b/.test(flags1)),
|
|
executable: type === "EXEC",
|
|
nx: stack === null ? null : !stack[1].includes("E"),
|
|
relro: /GNU_RELRO/.test(text),
|
|
bindNow: /BIND_NOW/.test(text) || /\bNOW\b/.test(flags1),
|
|
symtab: /\.symtab\b/.test(text),
|
|
sectionHeaders: /Section Headers:/.test(text) && !/no sections/i.test(text),
|
|
canary: dynsym
|
|
.split("\n")
|
|
.some(
|
|
(line) => line.includes("__stack_chk_fail") && /\bUND\b/.test(line),
|
|
),
|
|
};
|
|
}
|
|
|
|
function assertElf(actual, expected, label) {
|
|
if (expected.executable) assert.equal(actual.pie, "disabled", label);
|
|
else if (expected.pie) assert.equal(actual.pie, "enabled", label);
|
|
else if (expected.type === "DYN")
|
|
assert.equal(actual.pie, "not_applicable", label);
|
|
if (expected.nx === null) assert.equal(actual.nx, "unknown", label);
|
|
else assert.equal(actual.nx, expected.nx ? "enabled" : "disabled", label);
|
|
if (!expected.relro) assert.equal(actual.relro, "disabled", label);
|
|
else
|
|
assert.equal(actual.relro, expected.bindNow ? "enabled" : "partial", label);
|
|
assert.equal(
|
|
actual.stack_protector,
|
|
expected.canary ? "enabled" : "unknown",
|
|
label,
|
|
);
|
|
if (!expected.sectionHeaders) assert.equal(actual.stripped, "unknown", label);
|
|
else
|
|
assert.equal(
|
|
actual.stripped,
|
|
expected.symtab ? "disabled" : "enabled",
|
|
label,
|
|
);
|
|
}
|
|
|
|
async function verifyMacho(directory, cFile, output) {
|
|
const cc = (await findTool("cc")) ?? (await findTool("clang"));
|
|
const otool = await findTool("otool");
|
|
if (cc === null) {
|
|
output.unavailable.push({
|
|
kind: "mach-o",
|
|
reason: "No cc or clang was found.",
|
|
});
|
|
return;
|
|
}
|
|
const binary = join(directory, "macho");
|
|
const compiled = await run(cc, [
|
|
"-arch",
|
|
process.arch === "arm64" ? "arm64" : "x86_64",
|
|
cFile,
|
|
"-o",
|
|
binary,
|
|
]);
|
|
if (compiled.status !== 0) {
|
|
output.unavailable.push({
|
|
kind: "mach-o",
|
|
reason: "cc did not emit a Mach-O image.",
|
|
stderr: (compiled.stderr ?? "").slice(0, 400),
|
|
});
|
|
return;
|
|
}
|
|
const bytes = await readFile(binary);
|
|
// A compiler that ignores -arch can succeed while emitting another format.
|
|
if (!hasMachoMagic(bytes)) {
|
|
output.unavailable.push({
|
|
kind: "mach-o",
|
|
reason: "cc did not emit a Mach-O image.",
|
|
});
|
|
return;
|
|
}
|
|
const images = await readMachoHardening(bytes);
|
|
assert.ok(images.length >= 1);
|
|
const pie = statesOf(images[0].rows).pie;
|
|
let otoolPie = null;
|
|
if (otool !== null) {
|
|
const dumped = await run(otool, ["-hv", binary]);
|
|
if (dumped.status === 0) {
|
|
otoolPie = /\bPIE\b/.test(dumped.stdout);
|
|
if (otoolPie) assert.equal(pie, "enabled", "otool PIE");
|
|
}
|
|
}
|
|
const otoolVersion = otool === null ? null : await run(otool, ["-version"]);
|
|
output.checks.push({
|
|
kind: "mach-o",
|
|
compiler: cc,
|
|
ccVersion: await versionLine(cc, ["--version"]),
|
|
otool,
|
|
otoolVersionStatus: otoolVersion?.status ?? null,
|
|
otoolVersion:
|
|
otoolVersion === null
|
|
? null
|
|
: `${otoolVersion.stdout ?? ""}\n${otoolVersion.stderr ?? ""}`
|
|
.trim()
|
|
.slice(0, 400),
|
|
images: images.length,
|
|
pie,
|
|
otoolPie,
|
|
hardened_runtime: statesOf(images[0].rows).hardened_runtime,
|
|
});
|
|
const fat = join(directory, "fat");
|
|
const universal = await run(cc, [
|
|
"-arch",
|
|
"x86_64",
|
|
"-arch",
|
|
"arm64",
|
|
cFile,
|
|
"-o",
|
|
fat,
|
|
]);
|
|
if (universal.status !== 0) {
|
|
output.unavailable.push({
|
|
kind: "mach-o-universal",
|
|
reason: "cc could not emit a universal Mach-O image.",
|
|
stderr: (universal.stderr ?? "").slice(0, 400),
|
|
});
|
|
return;
|
|
}
|
|
const fatBytes = await readFile(fat);
|
|
if (!hasMachoMagic(fatBytes)) {
|
|
output.unavailable.push({
|
|
kind: "mach-o-universal",
|
|
reason: "cc did not emit a universal Mach-O image.",
|
|
});
|
|
return;
|
|
}
|
|
const fatImages = await readMachoHardening(fatBytes);
|
|
assert.equal(fatImages.length, 2);
|
|
assert.ok(
|
|
fatImages.every(
|
|
(image) => image.status === "parsed" && image.slice !== null,
|
|
),
|
|
);
|
|
output.checks.push({
|
|
kind: "mach-o-universal",
|
|
slices: fatImages.map((image) => ({
|
|
architecture: image.architecture,
|
|
pie: statesOf(image.rows).pie,
|
|
})),
|
|
});
|
|
}
|