[Bug] libdl/dlmodule: no bounds validation on ELF relocation metadata - malformed .mo gives arbitrary kernel-mode memory write with controlled value during load
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 5/5
- Thời gian dự kiến
- Hơn một tuần
- Mức phù hợp với người mới
- 25/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- c
- Lĩnh vực
- operating-systems, security
Hướng nghiên cứu
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.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
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.
- Ngôn ngữ chính
- C
- Star
- 12.3k
- Fork
- 5.5k
- Merge trung bình
- 4 ngày 23 giờ
- Pull request đã merge (30 ngày)
- 27
Chuẩn bị môi trường
Khởi chạy dev container của dự án ngay trên trình duyệt, bằng tài khoản GitHub của bạn.
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của RT-Thread/rt-thread
-
bug Component component: net
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
RT-Thread/rt-thread#11913 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
[Bug] dfs_v2 tmpfs: ftruncate(fd, 0) frees the file buffer but reports success — use-after-free read/write and double free from a legal POSIX callCó thể đã có người làm @remukisama đã nhận 3 ngày trước. Đang mởin progress
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
RT-Thread/rt-thread#11902 · 4 bình luận · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
-
[bsp][stm32][bluepill] README「快速上手」缺少重新生成 MDK 工程这一步,按文档操作无法编译通过Có thể đã có người làm @moment-NEW đã nhận 20 ngày trước. Đang mởin progress
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 75/100
RT-Thread/rt-thread#11818 · 4 bình luận · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
-
BSP BSP: Loongson bug RT-Smart
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
RT-Thread/rt-thread#11717 · 2 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Arch: RISC-V BSP BSP: HPMicro bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
RT-Thread/rt-thread#11687 · 4 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của RT-Thread/rt-thread
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 66/100
Maintainer thường phản hồi trong vòng 1 ngày
-
GEODIFF_changesCount aborts the process on a truncated changeset (uncaught GeoDiffException)Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
MerginMaps/geodiff#272 ·
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 74/100
open-quantum-safe/liboqs#2644 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 70/100
espressif/esp-adf-libs#89 · 1 bình luận ·
-
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 88/100
Maintainer thường phản hồi trong vòng 2 ngày