Parse an ELF PRX Safely

Status: COURSE CHAPTER 2 · Evidence class: DERIVED TEACHING MODEL / SECURITY · Chapter ID: CH-ELF-PRX-001 · Last reviewed: 2026-08-11

ELF and PRX files are structured, untrusted input. A parser for a recompiler must prove every span before reading it, preserve source offsets for later diagnostics, and fail closed when a format or relocation is unknown. Parsing is not execution.

Read in layers

  1. Container header: check the magic, class, endianness, machine, version, and fixed-width header size before trusting offsets.
  2. Program headers: validate the complete header table with checked multiplication and addition, then validate each load segment’s file span and memory extent. Reject overlapping or contradictory ranges unless the fixture format explicitly permits them.
  3. Section and BSS metadata: treat section names as metadata, not authority. Zero-fill BSS only after the memory extent is proven and bounded.
  4. PRX module information: locate module name, GP/load-base metadata, entry points, and relocation records through validated offsets. Keep the original file offset beside every derived address.
  5. Imports and exports: read library names, version fields, NID arrays, and stub/function tables only when their entire spans are inside the file. Preserve positional correspondence; a matching name alone is not proof of a matching NID or ABI.
  6. Relocations: decode only documented relocation classes, check the target and addend spans, and stop with a visible error on malformed or unknown records. Never “repair” an invalid relocation by guessing.

Fail-closed pseudocode

bytes = read_bounded_fixture()
header = parse_fixed_header(bytes)
require header.magic and supported_class(header)
require checked_span(header.phoff, header.phnum * header.phentsize, len(bytes))

for ph in program_headers(bytes, header):
    require checked_span(ph.file_offset, ph.file_size, len(bytes))
    require checked_extent(ph.vaddr, ph.mem_size, guest_image_limit)
    record_source_offsets(ph)

module = parse_prx_module_info(bytes, proven_offsets)
imports = parse_stub_tables(bytes, module, proven_spans)
relocs = parse_relocations(bytes, module, known_types_only)
apply_relocations(relocs, guest_image, load_base)
return image_with_provenance

Whole-span and resource checks

Use checked arithmetic before validating an extent: count × record-size and base + length must not wrap. Cap the number of headers, imports, relocations, and allocated bytes for the fixture profile. A zero-length span may be valid without touching memory; a non-zero span needs a complete readable range. These rules are useful even when the public fixture is tiny.

Public implementation anchors

The current public repository exposes tools/prxload.py for PRX loading, tools/imports.py for import analysis, and tools/elf_bounds.py plus its tests for span reasoning. Treat those links as a moving implementation reference and keep the parser contract in your own fixture tests.

Exercise: make malformed inputs first-class

Create a tiny synthetic ELF/PRX fixture and four mutations: truncated program-header table, wrapped count × size, segment past the file end, and unknown relocation type. For each mutation, assert the parser rejects before any guest image or host allocation is partially committed. Record the byte offset and error class, not the original file or any private path.

Continue with the first chapter and the course landing page. The next chapter, guest-memory construction, remains deliberately deferred until this fixture is reviewed.