From 2c38becc4e9892424776a71c65743369bf9d8e92 Mon Sep 17 00:00:00 2001 From: Salman Almutairi <141588124+salmanalmutairi1@users.noreply.github.com> Date: Sun, 25 Oct 2026 09:18:44 +0300 Subject: [PATCH] feat(binary-layout): admit ELF32 i386, ARM and MIPS layout profiles (#7052) * feat(binary-layout): admit ELF32 i386, ARM and MIPS layout profiles inspect_binary_layout refused every ELF32 object, so router and IoT firmware (ARM32, MIPS32) got no layout facts at all. The pwntools adapter now admits EXEC/DYN/REL objects in these profiles: ELF64 x86-64, ELF32 i386, ELF32 little-endian ARM and ELF32 little- or big-endian MIPS. Header fields are read in the declared byte order, the ELF32 header is bounds-checked at 52 bytes, and pwntools' own reported architecture, class and byte order must match the admitted profile. Other machines, classes and byte orders keep an unsupported_target refusal that names the admitted profiles. The output schema admits exactly those architecture combinations, and the Evidence subject now names the reported machine instead of always claiming x86_64. verify:binary:layout cross-compiles i386, ARM, MIPS and MIPSel executables and relocatable objects (never executed) and checks every section name, address, offset and size against GNU readelf through CLI and MCP. The real-binary-layout workflow installs those toolchains. Refs #6934 Co-Authored-By: Claude Opus 5.5 * ci(binary-layout): install target libc headers for ELF32 fixtures --no-install-recommends skips the libc6-dev-*-cross packages the cross compilers only recommend, so the i686 fixture fell back to the host's /usr/include and failed to find bits/libc-header-start.h. Install the four target libc development packages explicitly. Refs #6934 Co-Authored-By: Claude Opus 5.5 * fix(binary-layout): size the minimum symbol entry by ELF class The relocation reference check required a 24-byte Elf64_Sym entry, so every positive symbol reference in an ELF32 report was refused as unreadable output. Elf32_Sym is 16 bytes; take the minimum from the reported class. A 64-bit table with 16-byte entries is still refused. Refs #6934 Co-Authored-By: Claude Opus 5.5 * test(binary-layout): compare ELF32 symbols and relocations with readelf --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> --- .github/workflows/real-binary-layout.yml | 6 + bridge/pwntools/layout_impl.py | 28 ++- docs/binary-diagnostics.md | 9 +- docs/testing.md | 8 +- scripts/verify-binary-layout.mjs | 177 +++++++++++++++++- .../BinaryLayoutService.test.ts | 22 +++ .../binaryDiagnostics/BinaryLayoutService.ts | 8 +- .../native/binaryDiagnosticsToolContracts.ts | 2 +- src/domain/native/binaryLayout.test.ts | 30 +++ src/domain/native/binaryLayout.ts | 26 ++- 10 files changed, 291 insertions(+), 25 deletions(-) diff --git a/.github/workflows/real-binary-layout.yml b/.github/workflows/real-binary-layout.yml index 24e9794beb..a8d84a2234 100644 --- a/.github/workflows/real-binary-layout.yml +++ b/.github/workflows/real-binary-layout.yml @@ -55,6 +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 + 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 - 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 99cc30d440..86ec3f623b 100644 --- a/bridge/pwntools/layout_impl.py +++ b/bridge/pwntools/layout_impl.py @@ -15,6 +15,11 @@ import sys 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.") class LayoutFailure(Exception): @@ -116,13 +121,16 @@ def inspect_elf(path, cache): raise LayoutFailure("format", "Selected object does not contain an ELF header.") if len(content) < 16: raise LayoutFailure("format", "Selected ELF identification is truncated.") - if content[4] != 2 or content[5] != 1: - raise LayoutFailure("unsupported-target", "Initial layout profile requires ELF64 little-endian objects.") - if len(content) < 64: - raise LayoutFailure("format", "Selected ELF64 header is truncated.") - if int.from_bytes(content[18:20], "little") != 62: - raise LayoutFailure("unsupported-target", "Initial layout profile requires x86-64 ELF objects.") - if int.from_bytes(content[16:18], "little") not in (1, 2, 3): + if content[4] not in (1, 2) or content[5] not in (1, 2): + raise LayoutFailure("unsupported-target", "Selected ELF declares an unknown class or byte order.") + bits, header_size = (32, 52) if content[4] == 1 else (64, 64) + order = "little" if content[5] == 1 else "big" + if len(content) < header_size: + raise LayoutFailure("format", f"Selected ELF{bits} header is truncated.") + profile = (int.from_bytes(content[18:20], order), bits, order) + if profile not in PROFILES: + raise LayoutFailure("unsupported-target", PROFILE_SCOPE) + if int.from_bytes(content[16:18], order) not in (1, 2, 3): raise LayoutFailure("unsupported-target", "Selected ELF type is outside the EXEC/DYN/REL layout profile; recorded core analysis is separate.") from pwnlib.context import context with context.local(cache_dir=str(cache), log_level="warning"): @@ -185,8 +193,8 @@ def inspect_elf(path, cache): if type is None or section.header.sh_type == type) try: image = LayoutELF(str(path), checksec=False) - if image.arch != "amd64" or image.bits != 64 or image.endian != "little": - raise LayoutFailure("unsupported-target", "Initial layout profile requires x86-64 ELF64 little-endian objects.") + if (image.arch, image.bits, image.endian) != (PROFILES[profile], bits, order): + raise LayoutFailure("unsupported-target", PROFILE_SCOPE) if image.header.e_type not in ("ET_EXEC", "ET_DYN", "ET_REL"): raise LayoutFailure("unsupported-target", "Selected ELF type is outside the EXEC/DYN/REL layout profile; recorded core analysis is separate.") length = len(content) @@ -208,7 +216,7 @@ def inspect_elf(path, cache): }) if h.sh_type in ("SHT_SYMTAB", "SHT_DYNSYM"): if h.sh_entsize < image.structs.Elf_Sym.sizeof(): - raise LayoutFailure("format", "ELF symbol entry is smaller than the decoded Elf64_Sym structure.") + raise LayoutFailure("format", f"ELF symbol entry is smaller than the decoded Elf{bits}_Sym structure.") if h.sh_link >= len(all_sections) or all_sections[h.sh_link].header.sh_type != "SHT_STRTAB": raise LayoutFailure("format", f"ELF symbol table {index} links to invalid string table {h.sh_link}.") strings = all_sections[h.sh_link] diff --git a/docs/binary-diagnostics.md b/docs/binary-diagnostics.md index d5cf4d19da..b58b8d04dc 100644 --- a/docs/binary-diagnostics.md +++ b/docs/binary-diagnostics.md @@ -8,9 +8,12 @@ ordinary binary inspection use the same modular capability. ## Bring your own engine -Initial real verification covers **Linux x64, ELF64 x86-64 little-endian -EXEC/DYN/REL**. Provide an absolute Python executable whose environment already -contains unchanged pwntools 4.15.0, pyelftools 0.33 and Unicorn 2.1.2: +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 +Python executable whose environment already contains unchanged pwntools 4.15.0, +pyelftools 0.33 and Unicorn 2.1.2: ```sh REA_PWNTOOLS_PYTHON=/absolute/isolated-env/bin/python \ diff --git a/docs/testing.md b/docs/testing.md index 8124da3f3d..25d0f3967d 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -2008,12 +2008,16 @@ coverage limits. ### Offline binary layout -`npm run verify:binary:layout` requires Linux x64, GCC/binutils, absolute +`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 `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. Exec syscall tracing must identify only the declared +released process ownership. Cross-compiled 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 separate verification lanes. Pass an installed package entrypoint as the script's first argument to verify packaging independently of the checkout. diff --git a/scripts/verify-binary-layout.mjs b/scripts/verify-binary-layout.mjs index ccbf568048..dd004a06d2 100644 --- a/scripts/verify-binary-layout.mjs +++ b/scripts/verify-binary-layout.mjs @@ -35,11 +35,27 @@ 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 and strip 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 i686/ARM/MIPS/MIPSel linux-gnu cross gcc and readelf must be available. No target is executed.", ); const run = createVerifierRun(); const execute = promisify(execFile); -for (const command of ["gcc", "ld", "strip", "/usr/bin/prlimit", strace]) { +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"], +]; +for (const command of [ + "gcc", + "ld", + "strip", + "/usr/bin/prlimit", + strace, + ...ELF32_TOOLCHAINS.flatMap(([prefix]) => [ + `${prefix}-gcc`, + `${prefix}-readelf`, + ]), +]) { try { await execute(command, [command === strace ? "-V" : "--version"], { timeout: 10_000, @@ -328,6 +344,163 @@ try { cases++; } } + // ELF32 profiles: cross-compiled, never executed, checked against GNU readelf. + for (const [ + prefix, + machine, + byteOrder, + kind, + flags, + ] of ELF32_TOOLCHAINS.flatMap((toolchain) => [ + [...toolchain, "executable", ["-fPIE", "-pie"]], + [...toolchain, "relocatable", ["-c", "-fPIC"]], + ])) { + const path = join(root.path, `${prefix}-${kind}`); + await execute(`${prefix}-gcc`, ["-O1", ...flags, source, "-o", path], { + timeout: 30_000, + maxBuffer: 1024 * 1024, + }); + const { stdout } = await execute(`${prefix}-readelf`, ["-SWsr", path], { + timeout: 10_000, + maxBuffer: 16 * 1024 * 1024, + env: { ...process.env, LC_ALL: "C" }, + }); + const expectedSections = [ + ...stdout.matchAll( + /^\s*\[\s*(\d+)\]\s+(\S+)\s+\S+\s+([0-9a-f]+)\s+([0-9a-f]+)\s+([0-9a-f]+)\s+[0-9a-f]+\s+[A-Za-z]*\s+(\d+)\s+\d+\s+\d+\s*$/gm, + ), + ] + .filter(([, index]) => index !== "0") + .map(([, index, name, address, offset, size, link]) => ({ + index: Number(index), + name, + address: BigInt(`0x${address}`), + offset: BigInt(`0x${offset}`), + size: BigInt(`0x${size}`), + link: Number(link), + })); + assert.ok( + expectedSections.length > 0, + `readelf listed no sections for ${path}`, + ); + const expectedSymbols = []; + for (const [, name, count, body] of stdout.matchAll( + /^Symbol table '([^']+)' contains (\d+) entr(?:y|ies):\n((?:[^\n]+\n?)*)/gm, + )) { + const table = expectedSections.find((section) => section.name === name); + assert.notEqual(table, undefined, `readelf symbol table absent: ${name}`); + const rows = [...body.matchAll(/^\s*(\d+):\s+([0-9a-f]+)\s+(\d+)\s/gm)]; + assert.equal(rows.length, Number(count), `readelf symbol count: ${name}`); + for (const [, index, value, size] of rows) + expectedSymbols.push({ + table_index: table.index, + entry_index: Number(index), + value: BigInt(`0x${value}`), + size: BigInt(size), + }); + } + assert.ok( + expectedSymbols.length > 0, + `readelf listed no symbols for ${path}`, + ); + const expectedRelocations = []; + for (const [, name, count, body] of stdout.matchAll( + /^Relocation section '([^']+)' at offset 0x[0-9a-f]+ contains (\d+) entr(?:y|ies):\n((?:[^\n]+\n?)*)/gm, + )) { + const section = expectedSections.find((section) => section.name === name); + assert.notEqual( + section, + undefined, + `readelf relocation section absent: ${name}`, + ); + const rows = [...body.matchAll(/^\s*([0-9a-f]+)\s+([0-9a-f]+)\s+\S+/gm)]; + assert.equal( + rows.length, + Number(count), + `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. + const decodedInfo = BigInt(`0x${info}`); + expectedRelocations.push({ + section_index: section.index, + entry_index: entryIndex, + reported_offset: BigInt(`0x${offset}`), + type: Number(decodedInfo & 0xffn), + symbol_table_index: section.link, + symbol_index: Number(decodedInfo >> 8n), + }); + } + } + assert.ok( + expectedRelocations.length > 0, + `readelf listed no relocations for ${path}`, + ); + const bytes = await readFile(path); + for (const mode of ["cli", "mcp"]) { + const value = await inspect(mode, path); + assert.deepEqual(value.architecture, { + machine, + bits: 32, + byte_order: byteOrder, + }); + assert.equal( + value.image_type, + kind === "executable" ? "ET_DYN" : "ET_REL", + ); + for (const section of expectedSections) { + const reported = value.sections[section.index]; + assert.equal(reported.name.display, section.name); + assert.equal(BigInt(reported.address), section.address); + assert.equal(BigInt(reported.offset), section.offset); + assert.equal(BigInt(reported.size), section.size); + } + assert.equal(value.sections.length, expectedSections.length + 1); + assert.deepEqual( + value.symbols.map(({ table_index, entry_index, value, size }) => ({ + table_index, + entry_index, + value: BigInt(value), + size: BigInt(size), + })), + expectedSymbols, + `${mode} symbol values differ from readelf for ${path}`, + ); + assert.deepEqual( + value.relocations.map( + ({ + section_index, + entry_index, + reported_offset, + type, + symbol_table_index, + symbol_index, + }) => ({ + section_index, + entry_index, + reported_offset: BigInt(reported_offset), + type, + symbol_table_index, + symbol_index, + }), + ), + expectedRelocations, + `${mode} relocations differ from readelf for ${path}`, + ); + if (kind === "executable") { + assert.ok(value.segments.length > 0); + assert.equal(value.linkage.interpreters.length, 1); + assert.ok( + value.linkage.needed_libraries.some( + (library) => library.display === "libc.so.6", + ), + ); + } + assert.deepEqual(await readFile(path), bytes); + cases++; + } + } const protectedReport = reports.get("protected"); // Ordinary data markers are not a request to discover/decompress a kernel config. const markerBytes = Buffer.from(await readFile(join(root.path, "protected"))); diff --git a/src/application/binaryDiagnostics/BinaryLayoutService.test.ts b/src/application/binaryDiagnostics/BinaryLayoutService.test.ts index bd4fd7fc52..96f563cfdc 100644 --- a/src/application/binaryDiagnostics/BinaryLayoutService.test.ts +++ b/src/application/binaryDiagnostics/BinaryLayoutService.test.ts @@ -56,6 +56,28 @@ it("records the decoded layout once in its Evidence", async () => { expect(evidence.raw_result).toBeNull(); }); +it.each([ + [{ machine: "EM_X86_64", bits: 64, byte_order: "little" }, "x86_64"], + [{ 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"], +] as const)( + "names the reported %j machine in its Evidence subject", + async (architecture, expected) => { + const layout = { ...binaryLayoutFixture(), architecture }; + const service = new BinaryLayoutService({ + inspectAndClose(input, options) { + return this.inspect(input, options); + }, + identity: BINARY_LAYOUT_TEST_PROVIDER, + inspect: () => Promise.resolve(ok(layout)), + }); + const result = await service.inspect({ path: layout.artifact.path }); + if (!result.ok) throw result.error; + expect(parseEvidence(result.value).subject?.architecture).toBe(expected); + }, +); + it("preserves a provider's actionable unsupported reason", async () => { const failure = new AnalysisCapabilityUnavailableError( "fixture-layout", diff --git a/src/application/binaryDiagnostics/BinaryLayoutService.ts b/src/application/binaryDiagnostics/BinaryLayoutService.ts index 903171394b..c35e4c28cf 100644 --- a/src/application/binaryDiagnostics/BinaryLayoutService.ts +++ b/src/application/binaryDiagnostics/BinaryLayoutService.ts @@ -15,6 +15,12 @@ import { err, ok, type Result } from "../../domain/result.js"; import { completeProviderEvidence } from "../ProviderOperationExecution.js"; const OPERATION = "inspect_binary_layout"; +const EVIDENCE_ARCHITECTURES = { + EM_X86_64: "x86_64", + EM_386: "x86", + EM_ARM: "arm", + EM_MIPS: "mips", +} as const; /** Shared CLI/MCP validation and Evidence composition for offline layout inspection. */ export class BinaryLayoutService { @@ -87,7 +93,7 @@ export class BinaryLayoutService { { path: value.artifact.path, format: "elf", - architecture: "x86_64", + architecture: EVIDENCE_ARCHITECTURES[value.architecture.machine], sha256: value.artifact.sha256, }, this.provider.identity, diff --git a/src/contracts/native/binaryDiagnosticsToolContracts.ts b/src/contracts/native/binaryDiagnosticsToolContracts.ts index 3533219923..3dbb820bad 100644 --- a/src/contracts/native/binaryDiagnosticsToolContracts.ts +++ b/src/contracts/native/binaryDiagnosticsToolContracts.ts @@ -250,7 +250,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. Initial profile: ELF64 x86-64 little-endian EXEC/DYN/REL 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, 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 49b26d4177..172c0c0e99 100644 --- a/src/domain/native/binaryLayout.test.ts +++ b/src/domain/native/binaryLayout.test.ts @@ -121,6 +121,21 @@ it("retains high linked values and NOBITS without inventing file backing", () => expect(value.runtime_load_base).toBeNull(); }); +it.each([ + [{ 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], + [{ machine: "EM_MIPS", bits: 32, byte_order: "big" }, true], + [{ machine: "EM_X86_64", bits: 32, byte_order: "little" }, false], + [{ machine: "EM_ARM", bits: 32, byte_order: "big" }, false], + [{ machine: "EM_386", bits: 64, byte_order: "little" }, false], + [{ machine: "EM_MIPS", bits: 64, byte_order: "big" }, false], +])("admits only profiled architecture %j: %s", (architecture, admitted) => { + expect( + binaryLayoutSchema.safeParse({ ...report(), architecture }).success, + ).toBe(admitted); +}); + it.each([ "file-backed-outside", "header-outside", @@ -276,6 +291,9 @@ it.each([ "wrong-unknown-target", "missing-string-table", "wrong-string-table-kind", + "elf32-symbol-entry", + "elf32-undersized-symbol-entry", + "elf64-elf32-sized-symbol-entry", ])("validates relocation symbol reference semantics: %s", (problem) => { const value = binaryLayoutSchema.parse(report()); value.artifact.bytes = 512; @@ -359,6 +377,17 @@ it.each([ if (problem === "owner-link-mismatch") owner.link = 0; if (problem === "wrong-meaning") relocation.symbol_reference_meaning = "zero-symbol-value"; + // Elf32_Sym is 16 bytes and Elf64_Sym 24; the minimum follows the ELF class. + if (problem.includes("elf32")) { + if (problem.startsWith("elf32")) + value.architecture = { + machine: "EM_386", + bits: 32, + byte_order: "little", + }; + table.size = "0x20"; + table.entry_size = problem.includes("undersized") ? "0xc" : "0x10"; + } if (problem.includes("target")) { value.image_type = "ET_REL"; value.entry_point.meaning = "not-applicable"; @@ -411,6 +440,7 @@ it.each([ "valid-section-target", "undefined-target", "inactive-target", + "elf32-symbol-entry", ].includes(problem), ); }); diff --git a/src/domain/native/binaryLayout.ts b/src/domain/native/binaryLayout.ts index cf34591b7d..c60a728d51 100644 --- a/src/domain/native/binaryLayout.ts +++ b/src/domain/native/binaryLayout.ts @@ -33,11 +33,23 @@ const binaryLayoutObjectSchema = z.strictObject({ bytes: index, }), format: z.literal("elf"), - architecture: z.strictObject({ - machine: z.literal("EM_X86_64"), - bits: z.literal(64), - byte_order: z.literal("little"), - }), + architecture: z.union([ + z.strictObject({ + machine: z.literal("EM_X86_64"), + bits: z.literal(64), + byte_order: z.literal("little"), + }), + z.strictObject({ + machine: z.enum(["EM_386", "EM_ARM"]), + bits: z.literal(32), + byte_order: z.literal("little"), + }), + z.strictObject({ + machine: z.literal("EM_MIPS"), + bits: z.literal(32), + byte_order: z.enum(["little", "big"]), + }), + ]), image_type: z.enum(["ET_EXEC", "ET_DYN", "ET_REL"]), entry_point: z.strictObject({ reported_value: unsignedHex, @@ -384,7 +396,9 @@ const validateRelocationReferences = ( (table?.type === "SHT_SYMTAB" || table?.type === "SHT_DYNSYM"); const positiveReferenceExists = tableExists && - BigInt(table.entry_size) >= 24n && + // Elf32_Sym is 16 bytes; Elf64_Sym is 24. + BigInt(table.entry_size) >= + (value.architecture.bits === 32 ? 16n : 24n) && BigInt(relocation.symbol_index) < BigInt(table.size) / BigInt(table.entry_size) && symbols.has(