[Bug] libdl/dlmodule: no bounds validation on ELF relocation metadata - malformed .mo gives arbitrary kernel-mode memory write with controlled value during load
Mantenedores costumam responder em até 1 dia
Ninguém assumiu esta issue ainda.
Avaliação
- Dificuldade
- 5/5
- Tempo estimado
- Mais de uma semana
- Facilidade para iniciantes
- 25/100
- Tipo de issue
- Bug
- Clareza
- Claramente especificada
- Status de atividade
- Ativa
- Stack de tecnologia
- c
- Domínio
- operating-systems, security
Direção de pesquisa
Start with components/libc/posix/libdl/dlelf.c and the architecture relocation handlers in components/libc/posix/libdl/arch/arm.c, arch/riscv.c, and arch/x86.c. Trace the ELF metadata and relocation offsets from dlmodule_load() into dlmodule_relocate(); the issue reports a crafted .mo causing an out-of-bounds kernel write. Done means malformed ELF ranges and indices are rejected safely across the listed metadata paths and architectures.
Escrita pelo modelo de indexação a partir do texto da issue.
Descrição
RT-Thread Version
master @ 5d1fb66d (v5.4.0-dev; components/libc/posix/libdl/dlelf.c verified byte-identical to origin/master as of 2026-10-19). Affected since v3.1.0; introduced by commit efe45231 ("[Kernel] move module to libc/libdl", 2018-08-30) which added dlelf.c/arch/arm.c with the unchecked relocation write. Verified present in every release tag since (checked by reading arch/arm.c/dlelf.c at v3.1.0, v4.0.0, v5.3.0 and master).
Affected area
Components / libc (libdl dynamic module loader, RT_USING_MODULE)
Hardware/BSP vendor
Not applicable / Other
Architecture
ARM Cortex-A/R/M 32-bit (reproduced), RISC-V 32, x86 — every architecture with a dlmodule_relocate port (arch/arm.c, arch/riscv.c, arch/x86.c share the same pattern). Not applicable to aarch64 (no port).
Board and hardware details
QEMU vexpress-a9 (Cortex-A9), stock bsp/qemu-vexpress-a9 defconfig + RT_USING_MODULE=y, FAT16 SD image holding the module file. Any board with the dynamic-module feature enabled is affected.
Develop Toolchain
arm-none-eabi-gcc 10.3.1 (gcc / scons)
Describe the bug
The dynamic module loader (dlopen()/msh auto-exec of .mo files) performs no bounds validation whatsoever on the ELF relocation metadata it parses. The write target of every relocation is computed directly from the attacker-controlled 32-bit Elf32_Rel.r_offset in the module file:
// components/libc/posix/libdl/arch/arm.c:33-37 (arch/riscv.c & arch/x86.c: same pattern)
where = (Elf32_Addr *)((rt_uint8_t *)module->mem_space
+ rel->r_offset // <-- file-supplied, never checked
- module->vstart_addr);
...
case R_ARM_GLOB_DAT:
case R_ARM_JUMP_SLOT:
*where = (Elf32_Addr)sym_val; // arm.c:65-66
and the value sym_val is the address of any symbol the module names from the kernel export table (dlmodule_symbol_find(strtab + sym->st_name), dlelf.c:193). On 32-bit targets mem_space + r_offset wraps modulo 2^32, so a malformed .mo obtains an arbitrary-address, attacker-valued 4-byte kernel-mode write, executed during dlmodule_load() — before any module code runs, so this is not "modules are trusted code": the corruption comes purely from the loader parsing malformed input.
The whole metadata chain feeding that write is likewise unchecked (same root cause, independent read primitives): r_info symbol index (24-bit, symtab[ELF32_R_SYM()] OOB), sh_link/sh_link->sh_link (unbounded shdr[] indexing → wild rel/symtab/strtab pointers), e_shoff/e_phoff/e_shstrndx (never compared against the file length; dlmodule_load() only checks magic + ELF class), p_offset+p_filesz / sh_offset+sh_size (heap over-read as memcpy source), and st_value (wild read feeding module->priority/stack_size). e_machine is not validated either, so cross-architecture ELF files are also interpreted.
Reproduction (full chain: FAT file → dfs → msh evil → dlmodule_exec → loader; QEMU + gdb-multiarch):
- Build
bsp/qemu-vexpress-a9withRT_USING_MODULE=y; attach a FAT16 SD image containing a craftedevil.mo(minimal ET_DYN ARM ELF, one.rel.textentry:R_ARM_GLOB_DATon symbolrt_kprintf,r_offset = 0xfffe4074). - Type
evilin the msh shell (msh auto-triesevil.mo, msh.c:312dlmodule_exec). - Result:
*(_ptr_t)(mem_space + r_offset - vstart) = &rt_kprintflands at kernel.bss0x600ec26c (_rt_module_symtab_end, 0x1bf8c bytes before the module allocation) — hardware watchpoint capturesOld = 0x600c1a40 → New = 0x600a7870 (&rt_kprintf)with backtracedlmodule_relocate ← dlmodule_load_shared_object (dlelf.c:201) ← dlmodule_load ← dlmodule_exec ← msh_exec_module (msh.c:312) ← msh_exec ← finsh_thread_entry,rel->r_offset = 0xfffe4074vsmodule->mem_size = 0x2000. The kernel keeps running — silent, persistent corruption. - Crash variant (
r_offset = 0x9fef7e48, write to the null page):data abort,pc=0x600163f0(strindlmodule_relocate),r3=0x40(computed target),r2=0x600a7870(value), same 6-frame backtrace, kernel shuts down. In the same session a well-formed module built with the stock toolchain (hello.mo, singleR_ARM_JUMP_SLOTat a legitimate in-module offset0x10338 < 0x1033c) loads, runs and exits cleanly — the only difference between the benign and malicious files isr_offset. - Value control: pointing the same primitive at another address with symbol
rt_strlenwrites0x6005e984there (watchpointNew = &rt_strlen).
Generator, control module, GDB scripts and full serial/GDB logs: see attached (gen_mo.py produces the malformed module; hello.c is the control).
Expected behavior
The loader must validate, against module->mem_size and the file buffer length: every relocation target (vstart_addr <= r_offset within the mapped image), the symbol index in r_info, all section-header indices (sh_link, sh_info, e_shstrndx), the table base offsets (e_shoff/e_phoff + count*sizeof within the file), copy sources (p_offset+p_filesz, sh_offset+sh_size within the file), and e_machine against the running target. Anything outside bounds must fail the load with an error, not write.
Other additional context
- Attack preconditions (explicit): attacker can place a file on a filesystem the system loads modules from, and the module gets loaded via the documented entry points — msh executing
<name>.mo//bin/<name>.mo(components/finsh/msh.c:255-322) or an application callingdlopen(). Typical exposure: module/plugin hot-updates from SD cards or network downloads, field-deployed kiosks/devices with a shell. The corruption happens inside the loader, before any module code executes — a robustness defect in trusted kernel code parsing untrusted input, not a "malicious module runs code" scenario. - Impact: arbitrary kernel-mode memory write with attacker-chosen value (any
RTM_EXPORTed symbol address), plusR_ARM_ABS32/R_ARM_RELATIVEadd-write and read-modify-write variants → kernel control-flow hijack (overwrite any function pointer/vtable in kernel .data/.bss/heap) or deterministic kernel crash (DoS). CVSS 3.1: 7.1 (AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H), CWE-122/CWE-129 (read-side variants CWE-125). - Relationship to prior reports: none — the only recent libdl changes are #11247 (2026-03, unrelated
static unsolvedreset) and #11062 (2025-12, loop cleanup); neither touches bounds validation. Not related to #11882 (lwp sigaction) — different component, different root cause. - Fix suggestion (one systematic pass, mirrors the checks Zephyr added for LLEXT): centralize a
dlmodule_check_range()and apply it at every file-derived index/offset listed above.
- Linguagem predominante
- C
- Estrelas
- 12.3k
- Forks
- 5.5k
- Merge médio
- 7d 22h
- PRs com merge (30d)
- 30
Preparar o ambiente
Inicia o contêiner de desenvolvimento do projeto no navegador, com a sua própria conta do GitHub.
- Sem Dockerfile nem arquivo Docker Compose
- Tem um modelo de pull request
- Ler o guia de contribuição
Primeiros passos
- Leia a issue inteira e depois o guia de contribuição do projeto.
- Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
- Faça um fork do repositório e trabalhe em uma branch.
- Abra um pull request que referencie o número da issue.
Mais de RT-Thread/rt-thread
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 78/100
RT-Thread/rt-thread#11902 · 1 comentário ·
Mantenedores costumam responder em até 1 dia
-
[bsp][stm32][bluepill] README「快速上手」缺少重新生成 MDK 工程这一步,按文档操作无法编译通过Talvez já em andamento @moment-NEW assumiu há 17 dias. Abertain progress
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 75/100
RT-Thread/rt-thread#11818 · 4 comentários · 1 responsável ·
Mantenedores costumam responder em até 1 dia
-
BSP BSP: Loongson bug RT-Smart
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 72/100
RT-Thread/rt-thread#11717 · 2 comentários ·
Mantenedores costumam responder em até 1 dia
-
Arch: RISC-V BSP BSP: HPMicro bug
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 72/100
RT-Thread/rt-thread#11687 · 3 comentários ·
Mantenedores costumam responder em até 1 dia
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 68/100
RT-Thread/rt-thread#11472 · 2 comentários ·
Mantenedores costumam responder em até 1 dia
Todas as issues de RT-Thread/rt-thread
Issues semelhantes
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 88/100
quickjs-ng/quickjs#1836 ·
Mantenedores costumam responder em até 2 dias
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 65/100
open-quantum-safe/liboqs#2637 ·
Mantenedores costumam responder em até 1 dia
-
Go device SDK: Whisper transcriber keeps buffering and transcribing audio appended after Stop()Talvez já em andamento @apoorvdarshan assumiu hoje. Aberta
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 84/100
BasedHardware/omi#22770 ·
Mantenedores costumam responder em até 1 dia
-
fido_do_ecdh() reports any getKeyAgreement failure as FIDO_ERR_INTERNALTalvez já em andamento @brlumen assumiu hoje. Aberta
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 74/100
Mantenedores costumam responder em até 1 dia
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 78/100
joncampbell123/dosbox-x#6698 ·
Mantenedores costumam responder em até 1 dia