Skip to content

[libc][libdl] reject out-of-bounds ELF relocation targets - #12016

Merged
Rbb666 merged 1 commit into
RT-Thread:masterfrom
cms19859230182-lang:fix/dlmodule-reloc-bounds
Nov 12, 2026
Merged

Rbb666 merged 1 commit into
RT-Thread:masterfrom
cms19859230182-lang:fix/dlmodule-reloc-bounds

Conversation

@cms19859230182-lang

@cms19859230182-lang cms19859230182-lang commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

中文

问题

dlmodule 重定位写地址按 mem_space + r_offset - vstart 计算,未做范围校验;畸形 ELF 可写出模块内存(#11898)。

改动

  • dlelf.c / dlmodule.h:重定位目标越界则失败
  • arch/arm.c、riscv.c、x86.c:同策略防护

验证

静态审阅越界路径;未板级、未跑恶意模块。

English

Problem

dlmodule relocation writes mem_space + r_offset - vstart with no range check; a bad ELF can write outside the module (#11898).

Changes

  • dlelf.c / dlmodule.h: reject out-of-range reloc targets
  • arch/arm.c, riscv.c, x86.c: same guard

Test plan

Static review of OOB paths; no board / malicious module run.

Fixes #11898

- Validate relocation target range against module memory bounds to prevent out-of-bounds writes
- Add dlmodule_check_rel_target helper and apply checks to ARM, RISC-V, and x86 arch relocators
- Add bounds checks for ELF section header indices and symbol table indices
- Abort module load and ensure proper cleanup (freeing section_load_addr) on relocation failure
- Fix RT-Thread#11898
@github-actions

Copy link
Copy Markdown

👋 感谢您对 RT-Thread 的贡献!Thank you for your contribution to RT-Thread!

为确保代码符合 RT-Thread 的编码规范,请在你的仓库中执行以下步骤运行代码格式化工作流(如果格式化CI运行失败)。
To ensure your code complies with RT-Thread's coding style, please run the code formatting workflow by following the steps below (If the formatting of CI fails to run).


🛠 操作步骤 | Steps

  1. 前往 Actions 页面 | Go to the Actions page
    点击进入工作流 → | Click to open workflow →

  2. 点击 Run workflow | Click Run workflow

  • Use workflow from 保持默认分支(通常为 master)
    Keep the default branch (usually master) in Use workflow from
  • 在 branch 输入框填写 PR 分支 fix/dlmodule-reloc-bounds
    Enter PR branch fix/dlmodule-reloc-bounds in the branch field
  • 设置需排除的文件/目录(目录请以"/"结尾)
    Set files/directories to exclude (directories should end with "/")
  1. 等待工作流完成 | Wait for the workflow to complete
    格式化后的代码将作为独立提交推送至你的分支。
    The formatting changes will be pushed to your branch as a separate commit.

完成后,提交将自动更新至 fix/dlmodule-reloc-bounds 分支,关联的 Pull Request 也会同步更新。
Once completed, commits will be pushed to the fix/dlmodule-reloc-bounds branch automatically, and the related Pull Request will be updated.

如有问题欢迎联系我们,再次感谢您的贡献!💐
If you have any questions, feel free to reach out. Thanks again for your contribution!

@github-actions

Copy link
Copy Markdown

📌 Code Review Assignment

🏷️ Tag: components

Reviewers: @Maihuanyi

Changed Files (Click to expand)
  • components/libc/posix/libdl/arch/arm.c
  • components/libc/posix/libdl/arch/riscv.c
  • components/libc/posix/libdl/arch/x86.c
  • components/libc/posix/libdl/dlelf.c
  • components/libc/posix/libdl/dlmodule.h

🏷️ Tag: components_libc

Reviewers: @GorrayLi @mysterywolf

Changed Files (Click to expand)
  • components/libc/posix/libdl/arch/arm.c
  • components/libc/posix/libdl/arch/riscv.c
  • components/libc/posix/libdl/arch/x86.c
  • components/libc/posix/libdl/dlelf.c
  • components/libc/posix/libdl/dlmodule.h

📊 Current Review Status (Last Updated: 2026-11-10 10:02 CST)


📝 Review Instructions

  1. 维护者可以通过单击此处来刷新审查状态: 🔄 刷新状态
    Maintainers can refresh the review status by clicking here: 🔄 Refresh Status

  2. 确认审核通过后评论 LGTM/lgtm
    Comment LGTM/lgtm after confirming approval

  3. PR合并前需至少一位维护者确认
    PR must be confirmed by at least one maintainer before merging

ℹ️ 刷新CI状态操作需要具备仓库写入权限。
ℹ️ Refresh CI status operation requires repository Write permission.

@github-actions

Copy link
Copy Markdown

@cms19859230182-lang
cms19859230182-lang marked this pull request as draft November 10, 2026 15:41
@cms19859230182-lang
cms19859230182-lang marked this pull request as ready for review November 11, 2026 06:21
@Yunlk

Yunlk commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

补上 PR 描述里缺的那一半证据:真的喂了一个坏模块,在 QEMU 里跑通了。结论是补丁有效,而且 dlmodule_check_rel_target() 是承重的——不是冗余检查。

环境

bsp/qemu-vexpress-a9,QEMU 11.0.0,arm-none-eabi-gcc 15.2.1,RT_USING_MODULE + RT_USING_CUSTOM_DLMODULE。用 dlmodule_load_custom() + 内存 ops 加载,不依赖文件系统。

构建模块的方式参考了 #11989 的 fixtures:

arm-none-eabi-gcc -marm -mcpu=arm926ej-s -O0 -fcommon -fno-pic ... -c
arm-none-eabi-gcc -nostdlib -r obj -o x.mo          # ET_REL
arm-none-eabi-gcc -nostdlib -shared obj -o x.so     # ET_DYN

注入方式:只改 .rel.* 段里那一条的 r_offset(4 字节),不动段大小、不动节头,ELF 结构保持自洽。readelf -r 可以看到唯一差别就是这一列:

-  00000044  00000d02 R_ARM_ABS32  payload
+  fffffff0  00000d02 R_ARM_ABS32  payload

master(未打补丁)

文件 类型 r_offset 结果
good.mo ET_REL 0x44 LOADED ok,destroy 正常
bad_54 ET_REL 0x54(= mem_size 84) LOADED ok,静默写越 4 字节
bad_ff ET_REL 0xFFFFFFF0 LOADED ok → data abort
good.so ET_DYN 0x1238 LOADED ok,destroy 正常
bad.so ET_DYN 0xFFFFFFF0 LOADED ok → assertion 失败

两个坏样本的崩溃都发生在 dlmodule_destroy() → rt_free(mem_space) → rt_smem_free() 里:

[ET_REL] data abort
  rt_smem_free      src/mem.c:538
  rt_free           src/kservice.c:1283
  dlmodule_destroy  dlmodule.c:449

[ET_DYN] (MEM_ISUSED(mem)) assertion failed at function:rt_smem_free, line number:537
  rt_smem_free      src/mem.c:537
  rt_free           src/kservice.c:1283
  dlmodule_destroy  dlmodule.c:449

(backtrace 用 addr2line -e rtthread.elf 还原。)

一个值得注意的细节

写入本身没有立即 fault。 mem_space=0x600dec30,r_offset=0xFFFFFFF0,mem_space + r_offset 在 32 位下回绕到 0x600dec20,正好是分配块前面 16 字节——那是小内存管理的块头,在已映射的堆里,写能成功。破坏的后果延后到 dlmodule_destroy 释放该块时才爆。

所以:

  • 这类缺陷不会在加载时立刻崩,中间可以正常工作一段时间,实际危害比"立刻 data abort"更难察觉
  • bad_54(r_offset = mem_size)连崩溃都没有,只是把块后 4 字节写花,属于纯静默破坏

打上本 PR

文件 结果 拦住它的检查
good.mo LOADED ok —
bad_54 REJECTED 节级:rel->r_offset >= shdr[shdr[index].sh_info].sh_size
bad_ff REJECTED 节级
good.so LOADED ok —
bad.so REJECTED dlmodule_check_rel_target()

关键日志:

[DLMD] Module: relocation offset out of section bounds                    ← ET_REL 两个坏样本
[DLMD] Module: relocation target out of bounds (offset: 0xfffffff0, width: 4, mem_size: 0x1244)
[DLMD] Module: relocate failed                                            ← ET_DYN 坏样本

为什么这条证据重要:两种 ELF 类型走的是不同函数——

  • ET_REL → dlmodule_load_relocated_object(),被节级检查拦住(.text 只有 0x48,所以 r_offset=0x50 就已经超了)
  • ET_DYN → dlmodule_load_shared_object(),没有节级检查,只有 dlmodule_check_rel_target() 兜底

如果只测 .mo,就证明不了新加的那个 helper 有任何作用——它一次都不会被调用到。.so 的用例才把它逼出来。

一个设计上的观察(不是缺陷)

节级检查用的是 shdr[shdr[index].sh_info].sh_size。对 .rel.dyn 来说 sh_info 是 0,shdr[0].sh_size 也是 0,于是 r_offset >= 0 恒真。目前不影响任何东西,因为 ET_DYN 根本不走那段代码。但这意味着:如果将来有人把 ET_DYN 也接到 dlmodule_load_relocated_object(),所有 .so 会被全部拒掉。 要不要在那行加一个 sh_info != 0 的前提,可以顺带考虑。

未覆盖

  • 没有真机,全部在 QEMU 里做的
  • 没有做 r_offset 落在节内、但跨节指向其它段的构造(比如指向 .text 但实际写 .data)
  • 构建时需要给 src/defunct.c 补 #include <dlmodule.h>,那是 [dlmodule][utest] add QEMU regression coverage #11989 里的改动,本 PR 不含;我是本地加上的,没有混进这次的验证结论

我没有把 PoC 代码提交到任何分支——按日报的规矩,这类带攻击性的样本不该进仓库。需要的话可以把生成脚本(不含二进制)整理成一个宿主侧工具,但那要维护者先表态。

@BernardXiong BernardXiong left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

结论:未发现阻塞问题,可以合并。

已验证:

  • 新增 dlmodule_check_rel_target() 同时检查 r_offset 相对 vstart_addr 的下溢和 mem_size 上界,覆盖 arm/riscv/x86 三个现有 arch 实现;
  • dlmodule_load_shared_object() / dlmodule_load_relocated_object() 补了 sh_link、sh_info、sym_index、e_shstrndx 的边界校验,并检查 dlmodule_relocate() 返回值;
  • section_load_addr[sh_info] == 0 的段直接跳过,避免对未加载段做重定位。

非阻塞:sh_offset / sh_size 仍未按 ELF 文件实际大小校验(如 dlelf.c:203、dlelf.c:299),畸形 ELF 可在加载阶段越界读;不属于本次重定位写路径,建议后续补上。

CI:107 pass / 3 skip,全部通过。

@Rbb666
Rbb666 merged commit df2e0bf into RT-Thread:master Nov 12, 2026
113 checks passed

@Rbb666 Rbb666 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

补充一处重定位写入范围问题和两处加载兼容性回归,详见三条行内评论。

return 0;
}

if (dlmodule_check_rel_target(module, (rt_ubase_t)rel->r_offset, sizeof(Elf32_Addr)) != RT_EOK)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Thumb 重定位的写入范围仍超出此处检查

R_ARM_THM_CALL / R_ARM_THM_JUMP24 分支用 *(rt_uint16_t *)(where + 2) 写第二个半字。where 的类型是 Elf32_Addr *,所以该写入位于目标地址后 8 字节,而此处只校验 4 字节。目标位于模块尾部时仍可能越界写入。建议将第二个半字的写入改为 *((rt_uint16_t *)where + 1),使实际写入范围与校验一致。

对应写入位于 arm.c 第 123 行。此问题通过指针运算检查确认。

strtab + sym->st_name);
strtab + sym->st_name);
rt_free(section_load_addr);
return -RT_ERROR;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 此处会拒绝合法的空操作重定位和未定义弱符号

ET_REL 中 R_ARM_NONE 使用符号索引 0 时,加载器会先查找空符号名,并在这里返回错误,无法到达 ARM 实现中新增的空操作分支。引用未定义 STB_WEAK 对象的 R_ARM_ABS32 也会走到此处并失败。建议在解析符号前跳过空操作,并按 ABI 处理未定义弱符号。

用基线和本 PR 源码做宿主侧对照测试,上述两个用例在基线返回 RT_EOK,本 PR 均返回 -RT_ERROR。

unsolved = RT_FALSE;
return -RT_ERROR;
}
if (elf_module->e_shstrndx >= elf_module->e_shnum)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 此检查会拒绝无节头表的共享对象

ELF 允许共享对象省略节头表,此时 e_shoff、e_shnum 和 e_shstrndx 均为 0。对于无需重定位、可直接通过 PT_LOAD 加载的 ET_DYN,基线能够完成加载,此处却因 0 >= 0 返回错误。建议先处理节头表缺省的情况,再校验节名表索引。

用基线和本 PR 源码做宿主侧对照测试,该用例在基线返回 RT_EOK,本 PR 返回 -RT_ERROR。节头表缺省的字段约定见 ELF 头规范。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

4 participants