diff --git a/.github/workflows/real-binary-layout.yml b/.github/workflows/real-binary-layout.yml index a8d84a2234..e312baa5af 100644 --- a/.github/workflows/real-binary-layout.yml +++ b/.github/workflows/real-binary-layout.yml @@ -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: | diff --git a/bridge/pwntools/layout_impl.py b/bridge/pwntools/layout_impl.py index 86ec3f623b..a9c112fb70 100644 --- a/bridge/pwntools/layout_impl.py +++ b/bridge/pwntools/layout_impl.py @@ -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): diff --git a/docs/binary-diagnostics.md b/docs/binary-diagnostics.md index 56427673af..cf074b10c3 100644 --- a/docs/binary-diagnostics.md +++ b/docs/binary-diagnostics.md @@ -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: diff --git a/docs/testing-lanes.md b/docs/testing-lanes.md index 5e54caf8a2..4aea0bebc0 100644 --- a/docs/testing-lanes.md +++ b/docs/testing-lanes.md @@ -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 diff --git a/scripts/verify-binary-layout.mjs b/scripts/verify-binary-layout.mjs index 64a1b430f4..428daf53f5 100644 --- a/scripts/verify-binary-layout.mjs +++ b/scripts/verify-binary-layout.mjs @@ -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); diff --git a/src/application/binaryDiagnostics/BinaryLayoutService.test.ts b/src/application/binaryDiagnostics/BinaryLayoutService.test.ts index f8a4c6b878..c087a83a64 100644 --- a/src/application/binaryDiagnostics/BinaryLayoutService.test.ts +++ b/src/application/binaryDiagnostics/BinaryLayoutService.test.ts @@ -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"], diff --git a/src/application/binaryDiagnostics/BinaryLayoutService.ts b/src/application/binaryDiagnostics/BinaryLayoutService.ts index 87dd9723b6..757ab7eb6b 100644 --- a/src/application/binaryDiagnostics/BinaryLayoutService.ts +++ b/src/application/binaryDiagnostics/BinaryLayoutService.ts @@ -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", diff --git a/src/contracts/native/binaryDiagnosticsToolContracts.ts b/src/contracts/native/binaryDiagnosticsToolContracts.ts index eba10d04fa..7bc97ecbc4 100644 --- a/src/contracts/native/binaryDiagnosticsToolContracts.ts +++ b/src/contracts/native/binaryDiagnosticsToolContracts.ts @@ -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, diff --git a/src/domain/native/binaryLayout.test.ts b/src/domain/native/binaryLayout.test.ts index 172c0c0e99..1d4e8a9410 100644 --- a/src/domain/native/binaryLayout.test.ts +++ b/src/domain/native/binaryLayout.test.ts @@ -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=="; diff --git a/src/domain/native/binaryLayout.ts b/src/domain/native/binaryLayout.ts index c60a728d51..d80c9ca4ca 100644 --- a/src/domain/native/binaryLayout.ts +++ b/src/domain/native/binaryLayout.ts @@ -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"), }), diff --git a/src/native/pwntools/PwntoolsDecoder.ts b/src/native/pwntools/PwntoolsDecoder.ts index 60b8bc967a..5bfbc1ea1f 100644 --- a/src/native/pwntools/PwntoolsDecoder.ts +++ b/src/native/pwntools/PwntoolsDecoder.ts @@ -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.", diff --git a/src/native/pwntools/PwntoolsFailures.test.ts b/src/native/pwntools/PwntoolsFailures.test.ts index bec9ac4be3..fb3840b066 100644 --- a/src/native/pwntools/PwntoolsFailures.test.ts +++ b/src/native/pwntools/PwntoolsFailures.test.ts @@ -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."); +}); diff --git a/tests/boundary/binaryDiagnostics/pwntoolsLayoutProvider.test.ts b/tests/boundary/binaryDiagnostics/pwntoolsLayoutProvider.test.ts index 1f2156ce19..b1e816758b 100644 --- a/tests/boundary/binaryDiagnostics/pwntoolsLayoutProvider.test.ts +++ b/tests/boundary/binaryDiagnostics/pwntoolsLayoutProvider.test.ts @@ -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."); }, );