feat(binary-layout): admit ELF64 AArch64 and name the refused host (#8711)

* feat(binary-layout): admit ELF64 AArch64 and name the refused host

inspect_binary_layout refused every AArch64 ELF, and on a non-x64 host the
refusal read "supports Linux x64 only", which looked like a target refusal
(#8709 ran on arm64 WSL and suspected an e_machine check).

The pwntools adapter now admits little-endian ELF64 AArch64 EXEC/DYN/REL
objects alongside the existing profiles, pwntools must report aarch64/64/
little, and the Evidence subject names arm64. The host refusal now names the
detected platform/arch and says the target was not read; the host check
itself is unchanged.

verify:binary:layout cross-compiles an AArch64 executable and relocatable
object (never executed) and checks sections, symbols and relocations
against GNU readelf, decoding ELF64 r_info as a 32-bit type under a 32-bit
symbol index. The real-binary-layout workflow installs the toolchain.

Refs #8709

* test(binary-layout): refuse RISC-V as the unsupported machine and assert the host refusal path

verify:binary:layout used e_machine 183 (AArch64) as its unsupported
target, which this branch now admits; use EM_RISCV (243) instead.

The unsupported-host provider test now asserts the refusal names this
host's platform/arch and says the target was not read, so reverting the
decoder to the old message fails it.
This commit is contained in:
JANG BYUNGKUN authored and GitHub committed 2026-11-03 09:37:41 +08:00
1 parent eef8f44003
commit 6442ea02cb
13 files changed
+75 -37

No files matched your search

+3 -3
View File
@@ -55,12 +55,12 @@ jobs:
- uses: ./.github/actions/portable-build-cache
with:
build_cache_artifact: ${{ inputs.build_cache_artifact }}
- name: Install ELF32 cross toolchains for source-owned fixtures
- name: Install AArch64 and ELF32 cross toolchains for source-owned fixtures
run: |
sudo apt-get update
sudo apt-get install --yes --no-install-recommends \
gcc-i686-linux-gnu gcc-arm-linux-gnueabi gcc-mips-linux-gnu gcc-mipsel-linux-gnu \
libc6-dev-i386-cross libc6-dev-armel-cross libc6-dev-mips-cross libc6-dev-mipsel-cross
gcc-aarch64-linux-gnu gcc-i686-linux-gnu gcc-arm-linux-gnueabi gcc-mips-linux-gnu gcc-mipsel-linux-gnu \
libc6-dev-arm64-cross libc6-dev-i386-cross libc6-dev-armel-cross libc6-dev-mips-cross libc6-dev-mipsel-cross
- name: Acquire exact upstream libraries in an isolated runner environment
shell: bash
run: |
+4 -4
View File
@@ -16,10 +16,10 @@ PROFILE = "pwntools@4.15.0;pyelftools@0.33;unicorn@2.1.2"
PACKAGES = {"pwntools": "4.15.0", "pyelftools": "0.33", "unicorn": "2.1.2"}
OUTPUT_BYTES = 64 * 1024 * 1024
# (e_machine, ELF class bits, byte order) -> architecture pwntools must also report.
PROFILES = {(62, 64, "little"): "amd64", (3, 32, "little"): "i386", (40, 32, "little"): "arm",
(8, 32, "little"): "mips", (8, 32, "big"): "mips"}
PROFILE_SCOPE = ("Layout profile requires ELF64 x86-64, ELF32 i386, ELF32 ARM little-endian "
"or ELF32 MIPS little- or big-endian objects.")
PROFILES = {(62, 64, "little"): "amd64", (183, 64, "little"): "aarch64", (3, 32, "little"): "i386",
(40, 32, "little"): "arm", (8, 32, "little"): "mips", (8, 32, "big"): "mips"}
PROFILE_SCOPE = ("Layout profile requires ELF64 x86-64, ELF64 AArch64 little-endian, ELF32 i386, "
"ELF32 ARM little-endian or ELF32 MIPS little- or big-endian objects.")
class LayoutFailure(Exception):
+5 -3
View File
@@ -9,9 +9,11 @@ ordinary binary inspection use the same modular capability.
## Bring your own engine
Real verification covers **Linux x64** hosts and **EXEC/DYN/REL** objects in
these profiles: ELF64 x86-64, ELF32 i386, ELF32 little-endian ARM and ELF32
little- or big-endian MIPS. Other machines, classes and byte orders, including
AArch64 and big-endian ARM, return `unsupported_target`. Provide an absolute
these profiles: ELF64 x86-64, ELF64 little-endian AArch64, ELF32 i386, ELF32
little-endian ARM and ELF32 little- or big-endian MIPS. Other machines, classes
and byte orders, including big-endian ARM, return `unsupported_target`. Other
hosts, including Linux arm64, return `capability_unavailable` naming the
detected host before any target is read. Provide an absolute
Python executable whose environment already contains unchanged pwntools 4.15.0,
pyelftools 0.33 and Unicorn 2.1.2:
+3 -3
View File
@@ -1108,13 +1108,13 @@ coverage limits.
### Offline binary layout
`npm run verify:binary:layout` requires Linux x64, GCC/binutils, the
`i686-linux-gnu`, `arm-linux-gnueabi`, `mips-linux-gnu` and `mipsel-linux-gnu`
cross `gcc`/`readelf` toolchains, absolute
`aarch64-linux-gnu`, `i686-linux-gnu`, `arm-linux-gnueabi`, `mips-linux-gnu` and
`mipsel-linux-gnu` cross `gcc`/`readelf` toolchains, absolute
`REA_PWNTOOLS_PYTHON` with pwntools 4.15.0/pyelftools 0.33/Unicorn 2.1.2 and
absolute `REA_VERIFY_STRACE_COMMAND`. It compiles ephemeral source-owned ELF
fixtures and checks public CLI/MCP, lossless addresses/names, file ranges,
mitigation inferences, malformed/unsupported input, original file hashes and
released process ownership. Cross-compiled ELF32 executables and relocatable
released process ownership. Cross-compiled ELF64 AArch64 and ELF32 executables and relocatable
objects check the reported class, machine and byte order, and every section
name, address, offset and size against GNU `readelf`. Exec syscall tracing must identify only the declared
Node/Python launchers; no target binary is executed. Core/debugger claims need
+21 -16
View File
@@ -38,15 +38,16 @@ if (
!isAbsolute(strace ?? "")
)
throw new Error(
"verify:binary:layout requires Linux x64, absolute REA_PWNTOOLS_PYTHON (pwntools 4.15.0/pyelftools 0.33/Unicorn 2.1.2) and REA_VERIFY_STRACE_COMMAND. gcc, ld, strip and the i686/ARM/MIPS/MIPSel linux-gnu cross gcc and readelf must be available. No target is executed.",
"verify:binary:layout requires Linux x64, absolute REA_PWNTOOLS_PYTHON (pwntools 4.15.0/pyelftools 0.33/Unicorn 2.1.2) and REA_VERIFY_STRACE_COMMAND. gcc, ld, strip and the AArch64/i686/ARM/MIPS/MIPSel linux-gnu cross gcc and readelf must be available. No target is executed.",
);
const run = createVerifierRun();
const execute = promisify(execFile);
const ELF32_TOOLCHAINS = [
["i686-linux-gnu", "EM_386", "little"],
["arm-linux-gnueabi", "EM_ARM", "little"],
["mips-linux-gnu", "EM_MIPS", "big"],
["mipsel-linux-gnu", "EM_MIPS", "little"],
const CROSS_TOOLCHAINS = [
["aarch64-linux-gnu", "EM_AARCH64", 64, "little"],
["i686-linux-gnu", "EM_386", 32, "little"],
["arm-linux-gnueabi", "EM_ARM", 32, "little"],
["mips-linux-gnu", "EM_MIPS", 32, "big"],
["mipsel-linux-gnu", "EM_MIPS", 32, "little"],
];
for (const command of [
"gcc",
@@ -54,7 +55,7 @@ for (const command of [
"strip",
"/usr/bin/prlimit",
strace,
...ELF32_TOOLCHAINS.flatMap(([prefix]) => [
...CROSS_TOOLCHAINS.flatMap(([prefix]) => [
`${prefix}-gcc`,
`${prefix}-readelf`,
]),
@@ -347,14 +348,15 @@ try {
cases++;
}
}
// ELF32 profiles: cross-compiled, never executed, checked against GNU readelf.
// Cross profiles: cross-compiled, never executed, checked against GNU readelf.
for (const [
prefix,
machine,
bits,
byteOrder,
kind,
flags,
] of ELF32_TOOLCHAINS.flatMap((toolchain) => [
] of CROSS_TOOLCHAINS.flatMap((toolchain) => [
[...toolchain, "executable", ["-fPIE", "-pie"]],
[...toolchain, "relocatable", ["-c", "-fPIC"]],
])) {
@@ -423,16 +425,18 @@ try {
`readelf relocation count: ${name}`,
);
for (const [entryIndex, [, offset, info]] of rows.entries()) {
// ELF32 r_info uses the low byte for type and the upper 24 bits for
// the symbol index. readelf has already decoded the file's byte order.
// ELF32 r_info packs an 8-bit type under a 24-bit symbol index;
// ELF64 packs a 32-bit type under a 32-bit index. readelf has already
// decoded the file's byte order.
const decodedInfo = BigInt(`0x${info}`);
const typeBits = bits === 32 ? 8n : 32n;
expectedRelocations.push({
section_index: section.index,
entry_index: entryIndex,
reported_offset: BigInt(`0x${offset}`),
type: Number(decodedInfo & 0xffn),
type: Number(decodedInfo & ((1n << typeBits) - 1n)),
symbol_table_index: section.link,
symbol_index: Number(decodedInfo >> 8n),
symbol_index: Number(decodedInfo >> typeBits),
});
}
}
@@ -445,7 +449,7 @@ try {
const value = await inspect(mode, path);
assert.deepEqual(value.architecture, {
machine,
bits: 32,
bits,
byte_order: byteOrder,
});
assert.equal(
@@ -1312,7 +1316,8 @@ try {
}
}
}
unsupported.writeUInt16LE(183, 18);
// EM_RISCV: AArch64 (183) is now an admitted profile.
unsupported.writeUInt16LE(243, 18);
const core = Buffer.from(object);
core.writeUInt16LE(4, 16);
for (const [name, bytes, category] of [
@@ -1337,7 +1342,7 @@ try {
"invalid_input",
],
["undersized-program-header", undersizedProgramHeader, "invalid_input"],
["arm64", unsupported, "unsupported_target"],
["riscv64", unsupported, "unsupported_target"],
["core", core, "unsupported_target"],
]) {
const path = join(root.path, name);
@@ -58,6 +58,7 @@ it("records the decoded layout once in its Evidence", async () => {
it.each([
[{ machine: "EM_X86_64", bits: 64, byte_order: "little" }, "x86_64"],
[{ machine: "EM_AARCH64", bits: 64, byte_order: "little" }, "arm64"],
[{ machine: "EM_386", bits: 32, byte_order: "little" }, "x86"],
[{ machine: "EM_ARM", bits: 32, byte_order: "little" }, "arm"],
[{ machine: "EM_MIPS", bits: 32, byte_order: "big" }, "mips"],
@@ -14,6 +14,7 @@ import { parseAnalysisInput } from "../AnalysisInput.js";
const OPERATION = "inspect_binary_layout";
const EVIDENCE_ARCHITECTURES = {
EM_X86_64: "x86_64",
EM_AARCH64: "arm64",
EM_386: "x86",
EM_ARM: "arm",
EM_MIPS: "mips",
@@ -245,7 +245,7 @@ export const BINARY_DIAGNOSTICS_TOOL_CONTRACTS = [
...toolContractMetadata("inspect_binary_layout"),
kind: "native-provider",
description:
"Inspect an explicit local binary without launching it as a host process. Returns file-backed section/segment ranges, linked addresses, original symbol/REL/RELA table identities, complete encoded RELR tables and upstream-derived offsets with unknown per-word locations/addends, raw name bytes, dependency/interpreter names and static mitigation inferences inline with artifact SHA-256 Evidence. Profile: EXEC/DYN/REL ELF64 x86-64, ELF32 i386, ELF32 little-endian ARM and ELF32 little- or big-endian MIPS on Linux x64 via caller-supplied Python with unchanged pwntools 4.15.0, pyelftools 0.33 and Unicorn 2.1.2. Runtime addresses/library paths, overall relocation inventory completeness and complete derived GOT/PLT coverage remain unknown. Zero executable entry values mean absence; relocatable entries are not applicable. Upstream can emulate PLT instructions with Unicorn for derived static maps. Uses bounded complete output and owned temporary files/processes; malformed, unsupported or oversized objects return no partial success. Select detail summary to retain the complete layout Evidence in this session and receive only its summary view, then inspect sections, symbols, mitigations or linkage with inspect_analysis_view. pwntools_timeout_ms is each owned pwntools command's deadline (omitted means 30000 ms, range 1..2147483647, next call). cpuSeconds 30 and the output, diagnostic, and address-space byte allowances have no runtime override.",
"Inspect an explicit local binary without launching it as a host process. Returns file-backed section/segment ranges, linked addresses, original symbol/REL/RELA table identities, complete encoded RELR tables and upstream-derived offsets with unknown per-word locations/addends, raw name bytes, dependency/interpreter names and static mitigation inferences inline with artifact SHA-256 Evidence. Profile: EXEC/DYN/REL ELF64 x86-64, ELF64 little-endian AArch64, ELF32 i386, ELF32 little-endian ARM and ELF32 little- or big-endian MIPS on Linux x64 via caller-supplied Python with unchanged pwntools 4.15.0, pyelftools 0.33 and Unicorn 2.1.2. Runtime addresses/library paths, overall relocation inventory completeness and complete derived GOT/PLT coverage remain unknown. Zero executable entry values mean absence; relocatable entries are not applicable. Upstream can emulate PLT instructions with Unicorn for derived static maps. Uses bounded complete output and owned temporary files/processes; malformed, unsupported or oversized objects return no partial success. Select detail summary to retain the complete layout Evidence in this session and receive only its summary view, then inspect sections, symbols, mitigations or linkage with inspect_analysis_view. pwntools_timeout_ms is each owned pwntools command's deadline (omitted means 30000 ms, range 1..2147483647, next call). cpuSeconds 30 and the output, diagnostic, and address-space byte allowances have no runtime override.",
inputSchema: inspectBinaryLayoutRequestSchema,
outputSchema: analysisDetailOutputSchemaOf(
binaryLayoutSchema,
+4 -1
View File
@@ -122,6 +122,9 @@ it("retains high linked values and NOBITS without inventing file backing", () =>
});
it.each([
[{ machine: "EM_AARCH64", bits: 64, byte_order: "little" }, true],
[{ machine: "EM_AARCH64", bits: 64, byte_order: "big" }, false],
[{ machine: "EM_AARCH64", bits: 32, byte_order: "little" }, false],
[{ machine: "EM_386", bits: 32, byte_order: "little" }, true],
[{ machine: "EM_ARM", bits: 32, byte_order: "little" }, true],
[{ machine: "EM_MIPS", bits: 32, byte_order: "little" }, true],
@@ -150,7 +153,7 @@ it.each([
if (problem === "file-backed-outside") section.file_backing = "file";
if (problem === "header-outside") section.header_location.offset = "0x80";
if (problem === "wrong-index") section.index = 1;
if (problem === "wrong-machine") value.architecture.machine = "EM_AARCH64";
if (problem === "wrong-machine") value.architecture.machine = "EM_RISCV";
if (problem === "outside-elf64")
value.entry_point.reported_value = "0x10000000000000000";
if (problem === "noncanonical-base64") section.name.bytes_base64 = "AR==";
+1 -1
View File
@@ -35,7 +35,7 @@ const binaryLayoutObjectSchema = z.strictObject({
format: z.literal("elf"),
architecture: z.union([
z.strictObject({
machine: z.literal("EM_X86_64"),
machine: z.enum(["EM_X86_64", "EM_AARCH64"]),
bits: z.literal(64),
byte_order: z.literal("little"),
}),
+14 -5
View File
@@ -69,8 +69,12 @@ import { PWNTOOLS_LIMITS } from "./PwntoolsRelease.js";
import { PWNTOOLS_PROVIDER_IDENTITY } from "./PwntoolsProviderIdentity.js";
import { pwntoolsInputBytes } from "./PwntoolsInputLimit.js";
const HOST_REQUIREMENT =
"The initial real-verified pwntools artifact profile supports Linux x64 only.";
/** Names the detected host so a host refusal is not read as a target refusal. */
export const pwntoolsHostRequirement = (
platform: NodeJS.Platform,
arch: string,
): string =>
`The real-verified pwntools artifact profile runs only on a Linux x64 host; this host is ${platform}/${arch}. The selected target was not read.`;
/** The pwntools profile runs only on Linux x64; discovery and execution share this check. */
export const pwntoolsHostSupported = (
@@ -290,13 +294,18 @@ export class PwntoolsDecoder<
throw new AnalysisCancelledError(OPERATION, {
cause: options.signal.reason,
});
if (!pwntoolsHostSupported(process.platform, process.arch))
if (!pwntoolsHostSupported(process.platform, process.arch)) {
const requirement = pwntoolsHostRequirement(
process.platform,
process.arch,
);
throw new AnalysisCapabilityUnavailableError(
this.identity.id,
OPERATION,
HOST_REQUIREMENT,
{ userMessage: HOST_REQUIREMENT },
requirement,
{ userMessage: requirement },
);
}
if (!isAbsolute(selectedPath))
throw pwntoolsUnavailable(
"Set REA_PWNTOOLS_PYTHON to an absolute caller-supplied Python executable with pwntools 4.15.0, pyelftools 0.33 and Unicorn 2.1.2; REA never installs Python or packages.",
@@ -2,6 +2,10 @@ import { expect, it } from "vitest";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
import { OwnedCommandFailure } from "../../process/OwnedCommand.js";
import {
pwntoolsHostRequirement,
pwntoolsHostSupported,
} from "./PwntoolsDecoder.js";
import { pwntoolsDecoderFailure } from "./PwntoolsFailures.js";
it.each([undefined, null, false, 0, "stop", { stop: true }, new Error("stop")])(
@@ -149,3 +153,11 @@ it("retains legacy owner causes without guessing cancellation from error text",
"ProviderAdapterError",
);
});
it("names the refused host instead of implying the target was refused", () => {
expect(pwntoolsHostSupported("linux", "arm64")).toBe(false);
const message = pwntoolsHostRequirement("linux", "arm64");
expect(message).toContain("Linux x64 host");
expect(message).toContain("this host is linux/arm64");
expect(message).toContain("The selected target was not read.");
});
@@ -677,6 +677,11 @@ it.runIf(unsupportedHost)(
const projected = projectAnalysisError(result.error);
expect(projected.message).toContain("Linux x64");
expect(projected.remediation.action).toContain("Linux x64");
// The refusal names this host, not the target, which was never read.
expect(projected.message).toContain(
`this host is ${process.platform}/${process.arch}`,
);
expect(projected.message).toContain("The selected target was not read.");
},
);