* [Bug report] x86-64 module loader: relocation r_offset and symbol index are used without bounds checks - OOB write on module load
@ 2026-11-13 4:00 Guikun Yang
0 siblings, 0 replies; only message in thread
From: Guikun Yang @ 2026-11-13 4:00 UTC (permalink / raw)
To: Luis Chamberlain
Cc: Petr Pavlu, Sami Tolvanen, Igor Solodrai, linux-modules, linux-kernel
From: Guikun Yang <yangguikun19@qq.com>
To: Luis Chamberlain <mcgrof@kernel.org>
Cc: Petr Pavlu <petr.pavlu@suse.com>, Sami Tolvanen <samitolvanen@google.com>,
Igor Solodrai <ihor.solodrai@linux.dev>, linux-modules@vger.kernel.org,
linux-kernel@vger.kernel.org
Subject: [Bug report] x86-64 module loader: relocation r_offset and symbol
index are used without bounds checks - OOB write on module load
Hi Luis, Petr, Igor,
Loading a crafted x86-64 module where one relocation entry's r_offset
(or its symbol index) points outside the target section ends in a wild
read and, for a target that is mapped and zero-filled, a controlled
out-of-bounds write. Both the ELF validity check and
__write_relocate_add() itself leave these two fields completely
unchecked. A/B reproducer below.
Oops: general protection fault, probably for non-canonical address
0xe00013fff8040000: 0000 [#1] SMP DEBUG_PAGEALLOC KASAN NOPTI
KASAN: maybe wild-memory-access in range [0x0000bfffc0200000-...]
RIP: memcmp+0x67/0x1d0
RDI: 0000bfffc0200000
Call Trace:
apply_relocate_add+0x20e
load_module+0x40f7
__do_sys_init_module
## The gap
__write_relocate_add() (arch/x86/kernel/module.c) takes the target
address and symbol pointer straight from the relocation entry:
loc = (void *)sechdrs[sechdrs[relsec].sh_info].sh_addr
+ rel[i].r_offset;
sym = (Elf64_Sym *)sechdrs[symindex].sh_addr
+ ELF64_R_SYM(rel[i].r_info);
...
if (memcmp(loc, &zero, size)) /* wild read */
...
write(loc, &val, size); /* val = sym->st_value + r_addend */
r_offset is a 64-bit value from the module file, with no check against
the target section size. r_info's symbol index is likewise unchecked
against the symbol table entry count. apply_relocations() in
kernel/module/main.c only verifies infosec < e_shnum and SHF_ALLOC.
The ELF validity layer does not cover these either:
elf_validity_cache_sechdrs() validates sh_offset/sh_size (file-side,
via validate_section_offset()), and the recent series added checks for
section types and the symbol string table - but nothing bounds the
per-entry r_offset or the symbol index of the relocation entries.
## This is the same pattern that f9d69d5e7b fixed for st_shndx
f9d69d5e7b ("module: Fix kernel panic when a symbol st_shndx is out of
bounds", December 2025) added a bounds check for st_shndx indexing
sechdrs[] in simplify_symbols():
if (sym[i].st_shndx >= info->hdr->e_shnum) { ... return -ENOEXEC; }
That check is in the current tree. The relocation entries in
__write_relocate_add() index memory the same way (an ELF-controlled
offset into an array base) and are still unchecked:
simplify_symbols(): st_shndx -> sechdrs[st_shndx] /* bounded */
__write_relocate_add(): r_offset -> base + r_offset /* unbounded */
391e982bfa (2022) already accepted that a crafted module file is a
realistic input ("It is trivial to craft a module to trigger OOB
access"), and the elf_validity_check() series exists precisely to
defend against malformed ELF. The relocation entries are the part of
that input that the validity layer does not reach.
## Impact: more than a crash
The memcmp(loc, &zero, size) / write(loc, &val, size) pair means the
loader reads the target first and only writes if it is zero. With a
non-canonical r_offset this is a deterministic #GP (reproduced). With
a r_offset aimed at a mapped, zero-filled kernel address, the write
lands - val is sym->st_value + r_addend, both attacker-controlled
within the module. Since init_module() can be retried with different
r_offset values, this is a usable probe-and-write primitive against
kernel memory, not just a crash.
## Reproduction
The carrier module is a real xt_mark.ko built from the same tree as
the test kernel (vermagic, this_module size, all 11 undefined symbols
matching), so the only variable is the eight modified bytes
(.rela.text[0].r_offset at file offset 0x17a0, changed to
0xc00000000000 so loc lands in the non-canonical range):
before: init_module -> 0, delete_module -> 0 (module loads, unloads)
after: init_module -> Oops (trace above)
Same-tree build, same boot, single field changed. The A/B pair and
the init_reloc.c reproducer are available on request.
## Suggested fix
In apply_relocations(), before dispatching to per-arch
apply_relocate_add(), verify each entry points inside its target
section and its symbol index inside the symbol table. This covers all
architectures (arm64, powerpc, riscv, s390, loongarch have the same
raw `base + rel[i].r_offset` form):
for (i = 1; i < info->hdr->e_shnum; i++) {
...
if (info->sechdrs[i].sh_type == SHT_REL ||
info->sechdrs[i].sh_type == SHT_RELA) {
for (j = 0; j < sechdrs[i].sh_size / sizeof(*rel); j++) {
/* bounds per entry, arch-dependent size */
...
}
}
}
or, minimally, a per-arch check at the top of each relocation loop:
loc_off = rel[i].r_offset;
if (loc_off > sechdrs[sechdrs[relsec].sh_info].sh_size - sizeof(u64))
return -ENOEXEC;
if (ELF_R_SYM(rel[i].r_info) >=
sechdrs[symindex].sh_size / sizeof(Elf_Sym))
return -ENOEXEC;
(The minimal per-arch form matches how f9d69d5e7b added the st_shndx
bound locally in simplify_symbols().)
Tested on 7.3.0; arch/x86/kernel/module.c is unchanged in mainline as
of 2026-11-13.
Thanks,
Guikun Yang
^ permalink raw reply [flat|nested] only message in thread
only message in thread, other threads:[~2026-11-13 14:09 UTC | newest]
Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-13 4:00 [Bug report] x86-64 module loader: relocation r_offset and symbol index are used without bounds checks - OOB write on module load Guikun Yang
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®