Summary
Zephyr's Linkable Loadable Extensions (LLEXT) subsystem computes relocation write targets as section_base + r_offset and dispatches them to architecture-specific relocation backends without validating that each relocation offset still falls within the referenced target section. A crafted LLEXT ELF can therefore trigger a kernel out-of-bounds write during extension load/link time, before any extension entrypoint is executed.
Because the write happens inside the privileged loader, the likely consequences are kernel memory corruption, hard faults, loader-state corruption, and possible control-flow hijack depending on allocator/layout and target architecture.
Details
The root cause is the missing per-relocation bounds validation for r_offset.
In current Zephyr, the relocation write target is derived by:
include/zephyr/llext/llext_internal.h
llext_get_reloc_instruction_location()
which is currently equivalent to:
return (uintptr_t)llext_loaded_sect_ptr(ldr, ext, shndx) + rela->r_offset;
There is no check here that rela->r_offset is still within the target section's sh_size.
The generic relocation dispatcher in:
subsys/llext/llext_link.c
llext_link()
does perform some section-level checks before dispatching relocations:
- relocation section type (
SHT_REL / SHT_RELA)
sh_entsize
sh_size % sh_entsize == 0
sh_info < ext->sect_cnt
However, it does not validate that each individual relocation record stays within the section it claims to patch.
After those section-level checks, llext_link() reads each relocation and hands it to arch_elf_relocate():
ret = llext_read(ldr, &rel, shdr->sh_entsize);
...
ret = arch_elf_relocate(ldr, ext, &rel, shdr);
At that point, the computed loc is already attacker-controlled through r_offset.
This is not just a theoretical concern. The architecture backends perform direct writes to the computed relocation address:
arch/arm/core/elf.c
arch/arm64/core/elf.c
arch/riscv/core/elf.c
arch/x86/core/elf.c
arch/arc/core/elf.c
Typical patterns include:
*(uint32_t *)loc = ...
*(uint64_t *)loc = ...
UNALIGNED_PUT(..., (uint32_t *)loc)
So once loc points outside the target section, the loader has a real kernel write primitive.
I also checked the history to confirm version scope. This is not a recent regression from the file split. The same root cause already existed in the original monolithic LLEXT implementation shipped in v3.5.0, where the relocation location was computed as:
op_loc = loc + rel.r_offset;
without a per-relocation bounds check.
History references:
- initial LLEXT introduction:
41e0a4a37141bad1c95cdd81653463032e4f05d7
- llext: Linkable loadable extensions
- later refactor moving load/link code:
1a2f6ae381421cf3fdd079187d8d60dfec7f41fe
- explicitly states "No functional changes are introduced"
Therefore the vulnerable logic should be considered present since the first release containing LLEXT, i.e. v3.5.0.
Affected configurations:
- systems with
CONFIG_LLEXT=y
- impact is strongest when the system loads attacker-controlled or untrusted LLEXT ELF files
- source inspection confirms the generic dispatcher issue is relevant to the currently supported LLEXT relocation backends on:
PoC
I reproduced this with a two-stage PoC:
- a generator that creates a minimal ELF64 file with:
- a
.text section of 16 bytes
- a
.rela.text relocation section targeting .text
- a single relocation record whose
r_offset = 0x40, i.e. 64 bytes, clearly outside the 16-byte target section
- a host-side ASan harness that models the current Zephyr LLEXT relocation logic:
section_base + r_offset
- no per-relocation bounds check
- then performs the backend-style write
The input generator (gen_bad_reloc.py) writes bad_reloc.elf with:
.text size = 16
- relocation target section index = 1 (
.text)
r_offset = 0x40
The host-side reproducer (repro_llext_reloc_oob.c) then:
- parses the crafted ELF
- performs the same section-level sanity checks Zephyr currently does
- allocates a heap buffer exactly equal to the target section size
- computes
loc = section_base + r_offset
- performs a 4-byte write at
loc
This produces a deterministic ASan heap-buffer-overflow.
Representative output from the reproducer:
Zephyr HEAD: 4f2e63556a0 2026-04-15 15:39:01 +0200
Wrote bad_reloc.elf (.text size=16, r_offset=0x40)
ELF ok: shnum=4 shoff=136
Found relocation section idx=2 type=4 info=1 entsize=24 size=24
Target section idx=1 size=16 flags=0x6
Relocation count: 1
rel[0]: r_offset=64 => loc=section_base+0x40
=================================================================
==455068==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x502000000050
WRITE of size 4 at 0x502000000050
...
0x502000000050 is located 48 bytes to the right of 16-byte region [0x502000000010,0x502000000020)
...
SUMMARY: AddressSanitizer: heap-buffer-overflow repro_llext_reloc_oob.c:164 in main
This directly demonstrates the missing invariant:
- Zephyr checks relocation section structure,
- but does not check relocation entry target bounds.
A practical on-target PoC should also be possible by loading a crafted extension through the normal LLEXT load path (for example via filesystem or shell loader), but the host-side ASan model already demonstrates the exact unsafe primitive in a deterministic and architecture-independent way.
Impact
This is a kernel-memory-corruption vulnerability in the LLEXT loader.
Impact includes:
- kernel out-of-bounds write during extension load
- hard fault / crash / denial of service
- corruption of adjacent extension memory regions or loader metadata
- corruption of exported symbol tables, init/fini arrays, or other nearby structures
- possible control-flow hijack depending on target architecture and layout
Who is impacted:
- Zephyr systems that enable
CONFIG_LLEXT
- especially systems that support loading extensions from attacker-influenced storage, shell input, or any other untrusted source
This is stronger than a simple "extension can execute arbitrary code after loading" argument, because the corruption happens inside the privileged loader before normal extension execution begins.
Fix suggestion
The minimum safe fix is to reject any relocation whose r_offset points outside the target section before calling arch_elf_relocate().
At a minimum, the generic dispatcher should enforce:
rela->r_offset < ext->sect_hdrs[shdr->sh_info].sh_size
before deriving loc.
A stronger hardening fix should also ensure that the mapped section still fits within its region and optionally add per-backend write-width checks for fixed-width relocation types.
Concretely, I suggest:
- add a checked helper in
include/zephyr/llext/llext_internal.h
- call it from
subsys/llext/llext_link.c before arch_elf_relocate()
- apply analogous bounds validation to the Xtensa PLT-style path in
llext_link_plt() as hardening
Suggested helper:
static inline int llext_validate_reloc_instruction_location(const struct llext_loader *ldr,
const struct llext *ext,
unsigned int shndx,
const elf_rela_t *rela)
{
enum llext_mem mem_idx;
size_t sect_off;
size_t sect_size;
size_t region_size;
const void *base;
if (shndx >= ext->sect_cnt || ext->sect_hdrs == NULL || ldr->sect_map == NULL) {
return -ENOEXEC;
}
mem_idx = ldr->sect_map[shndx].mem_idx;
if (mem_idx >= LLEXT_MEM_COUNT) {
return -ENOEXEC;
}
base = llext_loaded_sect_ptr((struct llext_loader *)ldr, (struct llext *)ext, shndx);
if (base == NULL) {
return -ENOEXEC;
}
sect_off = ldr->sect_map[shndx].offset;
sect_size = ext->sect_hdrs[shndx].sh_size;
region_size = ext->mem_size[mem_idx];
if (sect_off > region_size) {
return -ENOEXEC;
}
if (sect_size > (region_size - sect_off)) {
return -ENOEXEC;
}
if ((size_t)rela->r_offset >= sect_size) {
return -ENOEXEC;
}
return 0;
}
And then in subsys/llext/llext_link.c, before debug logging and before arch_elf_relocate():
ret = llext_validate_reloc_instruction_location(ldr, ext, shdr->sh_info, &rel);
if (ret != 0) {
LOG_ERR("Relocation %d:%d offset %#zx escapes target section %d (size %zu)",
i, j, (size_t)rel.r_offset, shdr->sh_info,
(size_t)ext->sect_hdrs[shdr->sh_info].sh_size);
return ret;
}
This generic check closes the primary OOB write primitive described in this report.
Kindly let me know if you intend to request a CVE ID upon confirmation of the vulnerability.
./run.sh
Zephyr HEAD: 4f2e63556a0 2026-04-15 15:39:01 +0200
Wrote bad_reloc.elf (.text size=16, r_offset=0x40)
ELF ok: shnum=4 shoff=136
Found relocation section idx=2 type=4 info=1 entsize=24 size=24
Target section idx=1 size=16 flags=0x6
Relocation count: 1
rel[0]: r_offset=64 => loc=section_base+0x40
=================================================================
==455068==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x502000000050 at pc 0x55ac87fc31b7 bp 0x7ffe2ca830f0 sp 0x7ffe2ca830e0
WRITE of size 4 at 0x502000000050 thread T0
#0 0x55ac87fc31b6 in main repro_llext_reloc_oob.c:164
#1 0x7f8873d75d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#2 0x7f8873d75e3f in __libc_start_main_impl ../csu/libc-start.c:392
#3 0x55ac87fc1484 in _start (/home/zyf/zafl/reshow/zephyr-vuln-repro/llext-reloc-oob/repro_llext_reloc_oob+0x4484)
0x502000000050 is located 48 bytes to the right of 16-byte region [0x502000000010,0x502000000020)
allocated by thread T0 here:
#0 0x7f887465e887 in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
#1 0x55ac87fc2b16 in main repro_llext_reloc_oob.c:139
#2 0x7f8873d75d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
SUMMARY: AddressSanitizer: heap-buffer-overflow repro_llext_reloc_oob.c:164 in main
Shadow bytes around the buggy address:
0x0a047fff7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0a047fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0a047fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0a047fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0a047fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0a047fff8000: fa fa 00 00 fa fa fa fa fa fa[fa]fa fa fa fa fa
0x0a047fff8010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0a047fff8020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0a047fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0a047fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0a047fff8050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
Shadow gap: cc
==455068==ABORTING
exit status: 1
Patches
For more information
If you have any questions or comments about this advisory:
embargo: 2026-08-11
Summary
Zephyr's Linkable Loadable Extensions (LLEXT) subsystem computes relocation write targets as
section_base + r_offsetand dispatches them to architecture-specific relocation backends without validating that each relocation offset still falls within the referenced target section. A crafted LLEXT ELF can therefore trigger a kernel out-of-bounds write during extension load/link time, before any extension entrypoint is executed.Because the write happens inside the privileged loader, the likely consequences are kernel memory corruption, hard faults, loader-state corruption, and possible control-flow hijack depending on allocator/layout and target architecture.
Details
The root cause is the missing per-relocation bounds validation for
r_offset.In current Zephyr, the relocation write target is derived by:
include/zephyr/llext/llext_internal.hllext_get_reloc_instruction_location()which is currently equivalent to:
There is no check here that
rela->r_offsetis still within the target section'ssh_size.The generic relocation dispatcher in:
subsys/llext/llext_link.cllext_link()does perform some section-level checks before dispatching relocations:
SHT_REL/SHT_RELA)sh_entsizesh_size % sh_entsize == 0sh_info < ext->sect_cntHowever, it does not validate that each individual relocation record stays within the section it claims to patch.
After those section-level checks,
llext_link()reads each relocation and hands it toarch_elf_relocate():At that point, the computed
locis already attacker-controlled throughr_offset.This is not just a theoretical concern. The architecture backends perform direct writes to the computed relocation address:
arch/arm/core/elf.carch/arm64/core/elf.carch/riscv/core/elf.carch/x86/core/elf.carch/arc/core/elf.cTypical patterns include:
*(uint32_t *)loc = ...*(uint64_t *)loc = ...UNALIGNED_PUT(..., (uint32_t *)loc)So once
locpoints outside the target section, the loader has a real kernel write primitive.I also checked the history to confirm version scope. This is not a recent regression from the file split. The same root cause already existed in the original monolithic LLEXT implementation shipped in
v3.5.0, where the relocation location was computed as:without a per-relocation bounds check.
History references:
41e0a4a37141bad1c95cdd81653463032e4f05d71a2f6ae381421cf3fdd079187d8d60dfec7f41feTherefore the vulnerable logic should be considered present since the first release containing LLEXT, i.e.
v3.5.0.Affected configurations:
CONFIG_LLEXT=yPoC
I reproduced this with a two-stage PoC:
.textsection of 16 bytes.rela.textrelocation section targeting.textr_offset = 0x40, i.e. 64 bytes, clearly outside the 16-byte target sectionsection_base + r_offsetThe input generator (
gen_bad_reloc.py) writesbad_reloc.elfwith:.textsize = 16.text)r_offset = 0x40The host-side reproducer (
repro_llext_reloc_oob.c) then:loc = section_base + r_offsetlocThis produces a deterministic ASan heap-buffer-overflow.
Representative output from the reproducer:
This directly demonstrates the missing invariant:
A practical on-target PoC should also be possible by loading a crafted extension through the normal LLEXT load path (for example via filesystem or shell loader), but the host-side ASan model already demonstrates the exact unsafe primitive in a deterministic and architecture-independent way.
Impact
This is a kernel-memory-corruption vulnerability in the LLEXT loader.
Impact includes:
Who is impacted:
CONFIG_LLEXTThis is stronger than a simple "extension can execute arbitrary code after loading" argument, because the corruption happens inside the privileged loader before normal extension execution begins.
Fix suggestion
The minimum safe fix is to reject any relocation whose
r_offsetpoints outside the target section before callingarch_elf_relocate().At a minimum, the generic dispatcher should enforce:
rela->r_offset < ext->sect_hdrs[shdr->sh_info].sh_sizebefore deriving
loc.A stronger hardening fix should also ensure that the mapped section still fits within its region and optionally add per-backend write-width checks for fixed-width relocation types.
Concretely, I suggest:
include/zephyr/llext/llext_internal.hsubsys/llext/llext_link.cbeforearch_elf_relocate()llext_link_plt()as hardeningSuggested helper:
And then in
subsys/llext/llext_link.c, before debug logging and beforearch_elf_relocate():This generic check closes the primary OOB write primitive described in this report.
Kindly let me know if you intend to request a CVE ID upon confirmation of the vulnerability.
Patches
main106540afbd22v4.4-branchv4.3-branchv3.7-branchFor more information
If you have any questions or comments about this advisory:
embargo: 2026-08-11