Skip to content

HRX: crafted ELF header with small e_shoff still crashes inside the AMDGPU HAL executable loader #420

Closed

Description

@bong-water-water-bong

Summary

Follow-up found while fixing #409. #409's fix bounds the fat-binary/ELF parsing path, but a crafted 64-byte ELF header whose e_shoff points at a small, mapped offset is still accepted as a plausible extent, and the crash then happens inside the AMDGPU HAL's raw-hsaco loader, which trusts offsets inside the span it is handed.

This is a different defect from #409 (which was data_length == 0 disabling the bounds check in libhrx/src/binding/common/fat_binary.c). Filed separately at the request of the reviewer; not fixed by the #409 PR.

Reproducer

Prereqs: a build of 1bit/main + the #409 patch, an AMDGPU device visible to HRX, and a 64-byte file h-0x100.bin that is a valid-looking AMDGPU ELF header pointing its section table at 0x100:

00000000: 7f45 4c46 0201 0140 0100 0000 0000 0000  .ELF...@........
00000010: 0200 e000 0100 0000 0000 0000 0000 0000  ................
00000020: 4000 0000 0000 0000 0001 0000 0000 0000  @...............
00000030: 4e00 0000 4000 3800 0000 4000 0100 0000  N...@.8...@.....

i.e. e_machine = EM_AMDGPU (224), e_flags = 0x4e (gfx1201), e_phoff = 64, e_phnum = 0, e_shentsize = 64, e_shnum = 1, e_shoff = 0x100.

#include <hip/hip_runtime_api.h>
int main(void) { hipInit(0); hipModule_t m = NULL;
  unsigned char image[64] = { /* the header above */ };
  hipError_t e = hipModuleLoadData(&m, image);   /* SIGSEGV */
  return 0; }

Result: SIGSEGV, exit 139. (e_shoff values beyond the mapping, e.g. 0x30000000/0x7fffffff0000, and a truncated real hsaco header now return a HIP error thanks to #409; only in-buffer offsets are still fatal.)

Call chain

#0 iree_hal_amdgpu_executable_create_from_raw_hsaco
#1 iree_hal_amdgpu_executable_create
#2 iree_hal_amdgpu_logical_device_load_executable
#3 iree_hal_executable_load
#4 iree_hal_streaming_module_create_from_memory_impl
#5 iree_hal_streaming_module_create_from_memory
#6 iree_hip_module_load_data_span
#7 hipModuleLoadDataEx
#8 hipModuleLoadData

Why

iree_hal_streaming_fat_binary_extract_for_targets() forwards
iree_make_const_byte_span(data.data, elf_size) where elf_size is derived from the
(garbage) section/segment fields. iree_hal_executable_load() and the AMDGPU HAL
receive that span with an explicit length, but
iree_hal_amdgpu_executable_create_from_raw_hsaco() dereferences offsets read out of
the ELF content without checking them against that length, so arbitrary in-span
values become wild pointers.

Suggested fix

Validate section/segment/kernel-descriptor offsets in
iree_hal_amdgpu_executable_create_from_raw_hsaco() against
executable_data.data_length before dereferencing, the same way the fat-binary layer
now does (reject out-of-range instead of trusting the header). A defensive check in the
HAL also protects the length-carrying entry points (hipLibraryLoadData,
hipModuleLoad), which can be handed the same crafted content.

Evidence

Raw captures (backtrace, sweep, before/after, repro source, build command) are in the
campaign workspace under team/evidence/409-before-after.txt,
409-sweep.txt, 409-hal-residual-backtrace.txt, 409-repro.c, 409-build.sh.

Activity

  1. bong-water-water-bong commented on Oct 18, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    Fix is up as 1bit-MONSTER/hrx-system#7, on the new pin line from #441.

    The diagnosis is a little different from the description. On the pinned runtime, the AMDGPU HAL parser (hsaco_metadata.c) already bounds-checks every read against data_length. The surviving defect is in the #409 path itself: for a length-less hipModuleLoadData, fat_binary.c checked the derived span with mincore(). That function only reports that memory is mapped, not that it is readable, so a header whose table offset points into a PROT_NONE page passed the check. The fault was in iree_hal_streaming_fat_binary_describe_amdgpu_elf.

    The fix probes readability with process_vm_readv() instead. The crafted cases now return a HIP error with exit 0 instead of crashing with exit 139, real gfx1151 code objects still load, and fat_binary_test passes 16/16 with a new regression test.

    This issue closes once the engine pin includes the fix.

  2. bong-water-water-bong commented on Oct 18, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    Fixed in 1bit-MONSTER/hrx-system#7, and carried into the engine by #443 (pin third_party/hrx-system d3c731e1 -> 2ab311496).

    The crash is not inside the AMDGPU HAL on this line: hsaco_metadata.c already bounds-checks its reads against data_length, so the HAL is not what trusts the header. The surviving defect is the length-less path in libhrx/src/binding/common/fat_binary.c: with no image length, the extent is derived from the ELF header's table geometry and was then proved with mincore(), which reports that a mapping covers a page, not that the page is readable. A PROT_NONE guard page therefore passed the proof, and the table read faulted in iree_hal_streaming_fat_binary_describe_amdgpu_elf - the same frame the call chain in the report ends at.

    The proof is now process_vm_readv() over one byte per spanned page, which hits the same access checks a real read does and returns EFAULT on the first unreadable page, so the span is rejected with INVALID_ARGUMENT before any table is dereferenced. Only the length-less branch changes; loads that carry a length (what the HRX ggml backend does) never reach it.

    Verification:

    • The crafted 64-byte header from the report, backed by a PROT_NONE tail page with e_shoff at 0x100/0x1000 and e_phoff inside the header: SIGSEGV, exit 139 before the fix; HIP error 200, exit 0, in 3/3 runs after. Crafted in-buffer headers (large e_shnum/e_phnum, far e_shoff) keep returning non-fatal errors.
    • A real gfx1151 code object still loads through hipModuleLoadData.
    • fat_binary_test 16/16 including the new RejectsLengthlessElfSpanningUnreadablePage (5/5 on repeat) in the fork PR.
    • At the new pin, the engine built with -DONEBIT_HRX=ON and tests/hrx_long_prompt.py passed twice on Strix Halo: PASS 7082-token prompt: expected 7341, got '7341'; tools/check_pins.py origin/main reports ok third_party/hrx-system: d3c731e1d -> 2ab311496 is ahead.

    Merge order: hrx-system#7, then engine#443. This closes once the pin is in main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions