ELF Validation

July 16, 2026 ยท View on GitHub

Before any byte of a capsule image is mapped, its ELF header and program-header table are validated. A malformed or unexpected image is rejected up front with a specific error, not partway through mapping. This page documents the header checks and the program-header bounds. The code is under src/elf/loader/core/parse_header/.

Parsing the header

parse_elf_header (src/elf/loader/core/parse_header/header.rs:18) refuses an image smaller than a header before reading anything:

  parse_elf_header(elf_data):
      if elf_data.len() < ElfHeader::SIZE:  FileTooSmall
      read_unaligned an ElfHeader from the front

The read is unaligned because the caller's buffer is arbitrary bytes; the size check ahead of it means the read never runs off the end.

The header checks

validate_elf (parse_header/validate.rs:17) runs ten checks in order and returns a distinct error for each, so a rejected image says exactly why:

  is_valid_magic()                    else InvalidMagic          (0x7F "ELF")
  is_64bit()                          else InvalidClass          (ELFCLASS64)
  is_little_endian()                  else InvalidEndian         (ELFDATA2LSB)
  version_is_current()                else InvalidVersion
  has_native_header_size()            else InvalidHeaderSize
  has_native_program_header_size()    else InvalidProgramHeaderSize   (56 bytes)
  has_native_section_header_size()    else InvalidSectionHeaderSize   (64 bytes)
  has_valid_section_name_table_index() else InvalidIndex
  e_machine == EM_X86_64              else InvalidMachine        (value 62)
  e_type in { ET_EXEC, ET_DYN }       else InvalidType

The image must be a little-endian 64-bit x86_64 object, and it must be either an executable (ET_EXEC) or a shared object (ET_DYN, the PIE form); a relocatable object or a core file is rejected with InvalidType. The entry-size checks pin the program- and section-header entry sizes to the native struct sizes, so the loader's later fixed-stride indexing into those tables is sound by construction.

Program-header bounds

The program-header table is where the loadable segments are described, and its extent is bounds-checked before it is walked (parse_header/bounds.rs:18):

  program_header_bounds(elf_data, header):
      ph_offset = header.e_phoff        (checked into usize, else ProgramHeadersOutOfBounds)
      ph_count  = header.e_phnum
      if ph_count == 0:  return (nothing to load)
      if ph_entsize != sizeof(ProgramHeader):  InvalidProgramHeaderSize
      table_bytes = ph_entsize * ph_count       (checked_mul)
      table_end   = ph_offset + table_bytes      (checked_add)
      if table_end > elf_data.len():  ProgramHeadersOutOfBounds

Every multiplication and addition is checked, and the table end is required to lie inside the image, so a header claiming a table that runs past the buffer is refused rather than read out of bounds. parse_program_header_at (parse_header/program_entry.rs:19) re-checks the index against the count and computes each entry's offset with checked arithmetic, so no individual program-header read can escape the image either.

The error type

Every rejection is one variant of ElfError (src/elf/errors/types/state.rs:17), a flat enum covering the header checks above, the segment and relocation failures the loading path can raise (SegmentDataOutOfBounds, MemoryAllocationFailed, MemoryMappingFailed, WXViolation, AlignmentError, AddressOverflow), and the dynamic-linking errors the userland path uses. The load path converts an ElfError into a spawn failure; the specific variant is logged so a rejected capsule is diagnosable.

Security analysis

The header parser is the loader's first contact with an untrusted image, and everything downstream assumes it did its job. Two properties hold, and one boundary is worth stating plainly.

Every read is size-checked before it happens. parse_elf_header (parse_header/header.rs:18) refuses an image smaller than ElfHeader::SIZE with FileTooSmall before it reads a single byte, and it reads unaligned because the caller's buffer is arbitrary bytes rather than a placed struct. The program-header table is bounds-checked the same way: program_header_bounds (parse_header/bounds.rs:18) computes ph_entsize * ph_count and ph_offset + table_bytes with checked_mul and checked_add, and requires the end to lie inside the image, so a header claiming a table that runs past the buffer is refused with ProgramHeadersOutOfBounds rather than read out of bounds. parse_program_header_at (parse_header/program_entry.rs:19) re-checks the index against the count and recomputes each entry offset with checked arithmetic, so no individual entry read can escape the image either. The image is treated as hostile input: the header claims a shape, and the loader confirms that shape fits the bytes it was actually given before trusting any field.

The entry-size pins make later fixed-stride indexing sound. has_native_program_header_size and has_native_section_header_size (parse_header/validate.rs:17) require the program- and section-header entry sizes to equal the native struct sizes (56 and 64 bytes). Because the loader later indexes those tables at a fixed stride, pinning the stride here is what makes that indexing correct by construction rather than by hope, and it also rejects an object built for a different ABI up front. Alongside them the class, endianness, machine, and type checks confine the loader to a little-endian 64-bit x86_64 ET_EXEC or ET_DYN object; a relocatable object or a core file is refused with InvalidType.

The boundary worth naming: these checks establish that the image is a well-formed x86_64 ELF whose tables fit inside the buffer. They do not establish that it is trustworthy. Authenticity, the publisher signature and the manifest, is a separate gate that runs before the loader is ever called, described on the integration page. Header validation is structural safety, not attestation, and the two are deliberately different layers.

Debugging ELF validation

Every rejection here is one variant of ElfError (src/elf/errors/types/state.rs:17) with a fixed string form (src/elf/errors/types/strings.rs:20), and on the spawn path that string is printed after the caller's debug tag when a load fails (.../install/load_elf_into_pid.rs:27). For a driver capsule the tag is the [DRIVER-*] load_elf_executable error: line the spawn site passes in (for example src/hardware/nvme_capsule/spawn.rs:58), so a failed load reads as a named stage:

  [DRIVER-NVME] load_elf_executable error:
  ELF file too small                 FileTooSmall                  the buffer is shorter than a header
  Invalid ELF magic number           InvalidMagic                  not 0x7F "ELF"
  Invalid ELF class (not 64-bit)     InvalidClass                  not ELFCLASS64
  Invalid ELF machine type ...       InvalidMachine                not EM_X86_64
  Invalid ELF type (not EXEC or DYN) InvalidType                   a relocatable object or core file
  Invalid ELF program header ...     InvalidProgramHeaderSize      e_phentsize is not the native size
  Program headers out of bounds      ProgramHeadersOutOfBounds     the table runs past the buffer

The distinction these give you is between a malformed image and an unexpected one. FileTooSmall, ProgramHeadersOutOfBounds, and the InvalidProgramHeaderSize from the bounds check mean the bytes do not describe a coherent object, which points at truncation or corruption in the artifact. InvalidMachine and InvalidType mean the object is well-formed but built for the wrong target or as the wrong kind of file, which points at a build or packaging mistake, not a damaged file. On the top-level spawn path the same failure also surfaces as reason=elf_load (.../from_vfs/load.rs:99), so a [RUNTIME-LOAD] FAILED ... reason=elf_load line tells you the image got as far as the loader, meaning it had already passed verification, and the specific ElfError string that precedes it says which structural check refused it.

Source map

  src/elf/loader/core/parse_header/header.rs         parse_elf_header, the size floor
  src/elf/loader/core/parse_header/validate.rs        the ten header checks
  src/elf/loader/core/parse_header/bounds.rs          program-header table bounds
  src/elf/loader/core/parse_header/program_entry.rs   per-entry bounds-checked read
  src/elf/errors/types/state.rs                       the ElfError enum
  src/elf/errors/types/strings.rs                     ElfError::as_str, the logged strings

Every reference above is verified against those trees. The segment W^X gate these checks hand off to is on the segment loading page, the load orchestration that calls them in order is on the layout page, and the verify-then-load gate that runs before any of this is on the integration page.