Repository navigation
HRX: crafted ELF header with small e_shoff still crashes inside the AMDGPU HAL executable loader #420
Description
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
bong-water-water-bong commentedon Oct 18, 2026
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 againstdata_length. The surviving defect is in the #409 path itself: for a length-lesshipModuleLoadData,fat_binary.cchecked the derived span withmincore(). 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 iniree_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, andfat_binary_testpasses 16/16 with a new regression test.This issue closes once the engine pin includes the fix.
bong-water-water-bong commentedon Oct 18, 2026
Fixed in 1bit-MONSTER/hrx-system#7, and carried into the engine by #443 (pin
third_party/hrx-systemd3c731e1->2ab311496).The crash is not inside the AMDGPU HAL on this line:
hsaco_metadata.calready bounds-checks its reads againstdata_length, so the HAL is not what trusts the header. The surviving defect is the length-less path inlibhrx/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 withmincore(), which reports that a mapping covers a page, not that the page is readable. APROT_NONEguard page therefore passed the proof, and the table read faulted iniree_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 returnsEFAULTon the first unreadable page, so the span is rejected withINVALID_ARGUMENTbefore 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:
PROT_NONEtail page withe_shoffat0x100/0x1000ande_phoffinside the header: SIGSEGV, exit 139 before the fix; HIP error 200, exit 0, in 3/3 runs after. Crafted in-buffer headers (largee_shnum/e_phnum, fare_shoff) keep returning non-fatal errors.hipModuleLoadData.fat_binary_test16/16 including the newRejectsLengthlessElfSpanningUnreadablePage(5/5 on repeat) in the fork PR.-DONEBIT_HRX=ONandtests/hrx_long_prompt.pypassed twice on Strix Halo:PASS 7082-token prompt: expected 7341, got '7341';tools/check_pins.py origin/mainreportsok third_party/hrx-system: d3c731e1d -> 2ab311496 is ahead.Merge order: hrx-system#7, then engine#443. This closes once the pin is in
main.