Skip to content

[kernel/mutex] fix: reject deleted mutex waiters - #11730

Open
liulangrenaaa wants to merge 1 commit into
RT-Thread:masterfrom
liulangrenaaa:20260821-fix/lifetime/mutex-delete-waiter-uaf
Open

[kernel/mutex] fix: reject deleted mutex waiters#11730
liulangrenaaa wants to merge 1 commit into
RT-Thread:masterfrom
liulangrenaaa:20260821-fix/lifetime/mutex-delete-waiter-uaf

Conversation

@liulangrenaaa

@liulangrenaaa liulangrenaaa commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

A mutex waiter can race with timeout callbacks, mutex release, object
deletion/detachment, and thread exit. A waiter may already be READY while
its rt_mutex_take() path has not resumed yet, so accessing the mutex after
rt_schedule() without a lifetime guarantee can result in stale accesses,
incorrect wakeup errors, or corrupted priority-inheritance state.

The problematic cases include:

  • rt_mutex_delete() / rt_mutex_detach() waking a suspended waiter and
    releasing or reusing the mutex before the waiter resumes.
  • A waiter timing out and becoming READY while still retaining mutex-specific
    state, followed by the mutex being deleted before the waiter runs.
  • delete/detach or rt_mutex_release() racing with a timeout callback that
    already owns the waiter's timer wakeup.
  • A successful release handoff making a waiter the new mutex owner while the
    waiter is still READY and has not returned from rt_schedule().
  • Thread exit and priority propagation observing an in-flight handoff as if it
    were still a normal mutex wait dependency.

The fix makes the waiter lifetime and wakeup ownership explicit.

  • Add _mutex_detach_waiter_locked() to unlink a mutex waiter and clear its
    pending_object while the scheduler lock is held. This helper only performs
    waiter bookkeeping and does not recompute PI state.

  • Add rt_mutex_timeout_waiter() for the mutex timeout path. It detaches the
    waiter, updates the mutex priority, and updates the owner priority before the
    timed-out thread is inserted into the ready queue.

  • Use rt_sched_thread_ready() in delete/detach and release paths to arbitrate
    wakeup ownership between the IPC path and an in-flight timeout callback.

    If rt_sched_thread_ready() succeeds, the IPC path owns the wakeup.

    If it fails because the timeout path already owns the timer wakeup, only the
    mutex-specific waiter state is detached and the timeout callback remains
    responsible for making the thread READY and setting its timeout error.

  • Batch PI recomputation in delete/detach. Waiters are detached without
    repeatedly recalculating the old owner's inherited priority; the owner PI
    state is recomputed after the mutex is removed from the owner's taken list.

  • In rt_mutex_release(), skip timeout-owned head waiters and continue looking
    for the next valid waiter instead of treating the first failed wakeup as if
    there were no usable waiters.

  • Keep thread->pending_object pointing to the mutex after a successful release
    handoff. While the new owner has not completed its rt_mutex_take() wakeup
    path, this pointer acts as an in-flight handoff token and provides the mutex
    lifetime guarantee required by the resumed waiter.

  • Delete/detach clears this handoff token and sets RT_ERROR before the mutex
    object can become invalid.

  • After rt_schedule() returns, _rt_mutex_take() only dereferences the mutex
    when the handoff token is still valid. If another wakeup/delete path has
    already cleared the token, it returns the corresponding error without
    accessing the mutex again.

  • Thread exit recognizes pending_object == mutex && mutex->owner == thread as
    an in-flight handoff token rather than a normal mutex wait dependency.

The mutex structure and ABI are unchanged. The internal timeout/drop helpers
are exposed only to kernel/IPC source files.

Regression coverage includes:

  • dynamic mutex deletion followed by possible allocator reuse;
  • static mutex detachment followed by object-memory poisoning;
  • timeout -> READY -> mutex detach before the waiter runs;
  • delete/detach racing with timeout wakeup ownership;
  • release skipping a timeout-owned head waiter and waking the next valid
    waiter;
  • release -> ownership handoff -> waiter READY -> mutex delete before the
    waiter resumes;
  • release -> ownership handoff -> waiter READY -> static mutex detach and
    memory poisoning before the waiter resumes;
  • thread-exit handling of an in-flight mutex handoff.

Validation:

  • Parent UP QEMU reproduces the original stale mutex access/data abort.
  • Fixed core.mutex regression tests pass.
  • QEMU SMP with 2 CPUs passes.
  • QEMU UP with 1 CPU passes.
  • QEMU SMP with 1 CPU passes.
  • RT_DEBUGING_ASSERT and RT_DEBUGING_CRITICAL were enabled during
    validation.
  • scons -j$(nproc) --strict -C bsp/qemu-vexpress-a9 passes.

为什么提交这份 PR (why to submit this PR)

修复 mutex waiter 在 timeout、release handoff、delete/detach 和 thread exit
并发情况下可能在 mutex 生命周期结束后继续访问 mutex 的问题。

核心原则是:waiter 从 rt_schedule() 返回以后,只有仍持有有效的 mutex
handoff token 时才允许再次访问 mutex;如果 timeout、delete 或其他 wakeup
路径已经完成 cleanup,则直接返回对应错误,不再解引用可能已经失效的 mutex。

你的解决方案是什么 (what is your solution)

  1. 将 mutex waiter unlink 与 PI recompute 分离:
    _mutex_detach_waiter_locked() 仅维护 waiter 生命周期,
    rt_mutex_timeout_waiter() 和各调用路径负责必要的 priority inheritance 更新。

  2. timeout waiter 在进入 READY 前完成 mutex-specific cleanup。

  3. delete/detach 和 release 使用 rt_sched_thread_ready() 仲裁 wakeup ownership,
    避免 timeout callback 与 IPC 路径同时处理同一个 waiter。

  4. delete/detach 批量清理 waiter 后只重新计算一次 owner PI。

  5. release 跳过 timeout-owned 的队首 waiter并继续寻找下一个有效 waiter。

  6. release 成功 handoff 后保留 pending_object 作为 in-flight lifetime token,
    直到 _rt_mutex_take() 完成 handoff 校验。

  7. delete/detach 在 mutex 失效前清除此 token,并使 resumed waiter 返回
    -RT_ERROR

  8. _rt_mutex_take() 在 token 已失效时直接返回错误,不再通过旧 fallback
    路径重新访问 mutex。

  9. thread exit 区分普通 mutex waiter 和 handed-off mutex owner。

请提供验证的 bsp 和 config (provide the config and bsp)

  • BSP:

    • bsp/qemu-vexpress-a9
  • Config:

    • RT_USING_MUTEX
    • RT_USING_HEAP for dynamic mutex regression cases
    • mutex UTest enabled
    • validation additionally performed with
      RT_DEBUGING_ASSERT and RT_DEBUGING_CRITICAL
  • Validation:

    • QEMU SMP, 2 CPUs: core.mutex passed
    • QEMU UP, 1 CPU: core.mutex passed
    • QEMU SMP, 1 CPU: core.mutex passed
    • strict QEMU build passed

当前拉取/合并请求的状态 Intent for your PR

必须选择一项 Choose one (Mandatory):

  • 本拉取/合并请求是一个草稿版本 This PR is for a code-review and is intended to get feedback
  • 本拉取/合并请求是一个成熟版本 This PR is mature, and ready to be integrated into the repo

代码质量 Code Quality:

我在这个拉取/合并请求中已经考虑了 As part of this pull request, I've considered the following:

  • 已经仔细查看过代码改动的对比 Already check the difference between PR and old code
  • 代码风格正确,包括缩进空格,命名及其他风格 Style guide is adhered to, including spacing, naming and other styles
  • 没有垃圾代码,代码尽量精简,不包含#if 0代码,不包含已经被注释了的代码 All redundant code is removed and cleaned up
  • 所有变更均有原因及合理的,并且不会影响到其他软件组件代码或BSP All modifications are justified and not affect other components or BSP
  • 对难懂代码均提供对应的注释 I've commented appropriately where code is tricky
  • 代码是高质量的 Code in this PR is of high quality
  • 已经使用clang-format 源码格式化工具确保格式符合RT-Thread代码规范 This PR has been formatted with clang-format and complies with RT-Thread code specification
  • 如果是新增bsp, 已经添加ci检查到.github/ALL_BSP_COMPILE.json 详细请参考链接BSP自查

@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 分支 20260821-fix/lifetime/mutex-delete-waiter-uaf
    Enter PR branch 20260821-fix/lifetime/mutex-delete-waiter-uaf 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.

完成后,提交将自动更新至 20260821-fix/lifetime/mutex-delete-waiter-uaf 分支,关联的 Pull Request 也会同步更新。
Once completed, commits will be pushed to the 20260821-fix/lifetime/mutex-delete-waiter-uaf 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 github-actions Bot added the Kernel PR has src relate code label Aug 21, 2026
@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown

📌 Code Review Assignment

🏷️ Tag: kernel

Reviewers: @GorrayLi @ReviewSun @hamburger-os @lianux-mm @wdfk-prog @xu18838022837

Changed Files (Click to expand)
  • src/ipc.c
  • src/thread.c
  • src/utest/mutex_tc.c

📊 Current Review Status (Last Updated: 2026-08-28 18:27 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

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown

MemBrowse Memory Report

at32f415-start

  • ROM: .text +204 B (+0.3%, 65,572 B / 262,144 B, total: 25% used)

esp32-c3

  • dram0_0_seg: .dram0.data +52 B, .dram0.dummy +1,024 B, .stack +4 B (+0.5%, 221,808 B / 325,120 B, total: 68% used)
  • drom0_0_seg: .eh_frame +168 B, .eh_frame_hdr +32 B (+0.0%, 411,188 B / 8,388,576 B, total: 5% used)
  • iram0_0_seg: .iram0.text +1,098 B, .iram0.text_end -74 B (+1.4%, 75,776 B / 325,120 B, total: 23% used)

gd32105r-start

  • CODE: .text +204 B (+0.3%, 69,488 B / 262,144 B, total: 27% used)

hc32f334

  • FLASH: .text +204 B (+0.2%, 99,700 B / 131,072 B, total: 76% used)

hpmicro-hpm5301evklite

  • ILM: .fast +800 B (+2.1%, 39,008 B / 131,072 B, total: 30% used)
  • XPI0: .fast +800 B (+0.6%, 125,792 B / 1,048,576 B, total: 12% used)

infineon-psoc6

  • flash: .text +204 B (+0.2%, 116,344 B / 262,144 B, total: 44% used)

k230

  • SRAM: .eh_frame +136 B, .text +32 B (+0.0%, 1,123,235 B / 268,300,288 B, total: 0% used)

loongson-ls1cdev

  • Code: .text +940 B (+0.3%, 364,188 B)

nordic-nrf51822

  • FLASH: .text +240 B (+0.5%, 52,248 B / 262,144 B, total: 20% used)

nuvoton-m487

  • CODE: .text +748 B (+0.2%, 356,764 B / 524,288 B, total: 68% used)

qemu-virt64-aarch64

  • Code: .eh_frame +128 B, .text +1,152 B (+0.1%, 1,071,492 B)

raspberry-pico-rp2040

  • FLASH: .rodata -24 B, .text +264 B (+0.2%, 114,344 B / 2,097,152 B, total: 5% used)

renesas-ra2l1

  • FLASH: .text +188 B (+0.2%, 98,464 B / 262,144 B, total: 38% used)

simulator

  • Code: .eh_frame +144 B, .eh_frame_hdr +32 B, .rela.dyn +4,632 B, .rodata +864 B, .text +3,747 B (+0.4%, 2,682,555 B)
  • Data: .data +5,344 B (+0.3%, 1,614,948 B)

stm32f407-rt-spark

  • CODE: .text +204 B (+0.2%, 85,968 B / 1,048,576 B, total: 8% used)

stm32l475-atk-pandora-llvm

  • ROM: .text +216 B (+0.3%, 83,644 B / 524,288 B, total: 16% used)

wch-ch32v208w-r0

  • FLASH: .text +880 B (+0.6%, 160,800 B / 491,520 B, total: 33% used)

x86

  • Code: .rodata +32 B, .text +944 B (+0.5%, 199,853 B)

xuantie-e901plus

  • ISRAM: .rodata +16 B, .text +48 B (+0.1%, 46,424 B / 131,072 B, total: 35% used)
    No memory changes detected for:
  • nxp-lpc1114

@wdfk-prog

Copy link
Copy Markdown
Contributor

我看了一下这个修复,当前 delete/detach 直接唤醒仍挂在 mutex suspend list 上的 waiter 场景应该可以覆盖,不过这里可能还有两个和 timeout 相关的生命周期竞态需要考虑。

  1. waiter 已经 timeout,但还没有真正恢复执行

复现时序大致如下:

Thread A 持有 mutex
    |
Thread B rt_mutex_take(mutex, timeout)
    |
    +--> B suspend
         pending_object = mutex
    |
timer timeout
    |
_thread_timeout(B)
    +--> B->error = -RT_ETIMEOUT
    +--> 从 mutex suspend list 删除
    +--> B 进入 READY
    |
    |  B 此时还没有得到 CPU
    |
Thread A / Thread C
    +--> rt_mutex_delete(mutex)
    +--> mutex memory free/reuse
    |
B finally resumes from rt_schedule()
    |
    +--> 当前 _rt_mutex_take() 仍继续访问 mutex
         -> potential UAF

这里的问题是:timeout 已经把 B 从 mutex->parent.suspend_thread 中移除了,因此 rt_mutex_delete() 当前新增的遍历无法再找到 B,也就无法清理它的 pending_object

而 B 从 rt_schedule() 返回以后,目前还需要重新访问 mutex 做 priority / suspend-list 等处理,所以 mutex 如果在 READY → RUNNING 这段时间被释放,就仍然存在 UAF。

建议考虑把 mutex waiter 的 timeout cleanup 前移到 waiter 变为 READY 之前,即由“将 waiter 从 SUSPEND 转为 READY 的路径”负责完成 mutex-specific cleanup:

timeout
  -> mutex PI / priority cleanup
  -> remove waiter
  -> pending_object = NULL
  -> error = -RT_ETIMEOUT
  -> READY

最理想的结果是 _rt_mutex_take()rt_schedule() 返回后,在错误路径不再需要重新解引用 mutex。

  1. clear pending_objectresume waiter 之间仍存在 timeout IRQ 窗口

当前 patch 的 delete/detach 路径大致是:

rt_sched_lock()
    |
clear waiter->pending_object
    |
rt_sched_unlock()
    |
    | <--- timer IRQ may timeout waiter here
    |
rt_susp_list_resume_all(..., RT_ERROR)

如果 timeout IRQ 恰好发生在这里,timeout 路径可能先把 waiter 从 suspend list 移除,并设置:

pending_object = NULL
error = -RT_ETIMEOUT
state = READY

随后 rt_susp_list_resume_all() 已经无法再处理这个 waiter。

waiter 恢复后,当前新增判断:

if (thread->pending_object == RT_NULL &&
    thread->error == RT_ERROR)
{
    return -RT_ERROR;
}

也不会命中,因为此时 error == -RT_ETIMEOUT,之后仍可能继续访问已经删除的 mutex。

这里建议不要先单独遍历清 pending_object,再调用 rt_susp_list_resume_all(),而是让:

stop timeout timer
+ remove from suspend list
+ clear pending_object
+ set RT_ERROR
+ READY

成为同一个 scheduler-lock 临界区内的状态转换。

类似:

rt_sched_lock(&slvl);

ret = rt_sched_thread_ready(thread);
if (ret == RT_EOK)
{
    /* delete path wins the wakeup race */
    thread->pending_object = RT_NULL;
    thread->error = RT_ERROR;
}

rt_sched_unlock(slvl);

rt_sched_thread_ready() 本身已经包含 timer stop,并且明确考虑了 timeout ISR racing 的情况,因此我觉得这里可以利用它来决定到底是 delete 路径还是 timeout 路径取得 waiter 的 wakeup ownership,而不是先修改 pending_object 再尝试唤醒。

总体上,我认为当前 PR 对“delete/detach 直接唤醒 suspended waiter”的原始 crash 是有效的,但 mutex object lifetime 的边界可能还需要覆盖:

SUSPEND -> timeout -> READY -> 尚未运行 -> mutex delete

以及 delete 与 timeout 同时竞争 waiter wakeup 的情况。建议再补一个 deterministic testcase:让 waiter timeout 后保持 READY 但暂时不能运行,此时由高优先级线程 delete/free/reuse mutex,最后再让 waiter 恢复执行。

@liulangrenaaa
liulangrenaaa force-pushed the 20260821-fix/lifetime/mutex-delete-waiter-uaf branch from 11e636e to cbde064 Compare August 22, 2026 17:43
@liulangrenaaa

Copy link
Copy Markdown
Contributor Author

我看了一下这个修复,当前 delete/detach 直接唤醒仍挂在 mutex suspend list 上的 waiter 场景应该可以覆盖,不过这里可能还有两个和 timeout 相关的生命周期竞态需要考虑。

都改了, 也增加了新的 测试, 新版本 可以再review下

@wdfk-prog

Copy link
Copy Markdown
Contributor

我看了一下这个修复,当前 delete/detach 直接唤醒仍挂在 mutex suspend list 上的 waiter 场景应该可以覆盖,不过这里可能还有两个和 timeout 相关的生命周期竞态需要考虑。

都改了, 也增加了新的 测试, 新版本 可以再review下

这里有个结构上的优化建议。

目前 rt_mutex_cleanup_waiter() 同时承担了:

  • waiter 从 mutex suspend list 脱离;
  • 清理 pending_object
  • _mutex_update_priority()
  • _thread_get_mutex_priority()
  • owner PI priority propagation。

这样在 delete/detachrelease 连续处理多个 waiter 时,循环内部可能重复执行 mutex priority / owner PI 的遍历和重算。

我觉得可以考虑把 waiter detachPI recompute 拆成两层,例如:

/* 只负责 waiter 与 mutex 脱钩,不处理 PI */
static rt_mutex_t _mutex_detach_waiter_locked(rt_thread_t thread,
                                               rt_bool_t remove_from_list);

/* timeout 路径:解绑 waiter 后完成必要的 PI 更新 */
rt_bool_t rt_mutex_timeout_waiter(rt_thread_t thread);

这样三个路径的职责可以更清晰:

timeout
   |
   v
detach waiter
   |
   v
update mutex priority
   |
   v
update owner PI
        ^
        |
     只计算一次
delete / detach
      |
      v
+-----------------------------+
| for each waiter             |
|                             |
| rt_sched_thread_ready()     |
|          |                  |
|          v                  |
| detach waiter               |
| (这里只做脱钩,不计算 PI) |
+-----------------------------+
      |
      v
remove mutex from
owner->taken_object_list
      |
      v
update owner PI
      |
      v
整批 waiter 只重算一次
release
   |
   v
restore old owner PI
   |
   v
+-------------------------------+
| while head is timeout-owned   |
|                               |
| detach waiter                 |
| (不再重算 old owner PI)     |
+-------------------------------+
   |
   v
找到有效 waiter
   |
   v
transfer ownership
   |
   v
update mutex->priority
   |
   v
完成最终 priority 更新

感觉这样不仅多 waiter 场景下开销更小,接口职责也会更清晰:
waiter unlink 只维护 waiter 生命周期,PI update 单独负责 priority inheritance。

可以考虑一下这种拆法。

@wdfk-prog

Copy link
Copy Markdown
Contributor

补充考虑了一下,这个问题可能不完全是 mutex 特有的问题。

当前 PR 修复的直接问题确实是 mutex waiter 在被唤醒或 timeout 后,还会继续访问 mutex 来做 priority inheritance、wait-list cleanup 和 pending_object 清理,因此 mutex 的 路径比较明确。

不过从更底层看,这里还涉及一个通用问题:

timeout callback 和 IPC 唤醒方之间存在 wakeup ownership 竞争。

例如:

  1. waiter 正在等待某个 IPC;
  2. timeout timer 已经触发,并取得了该 waiter 的 timer wakeup ownership;
  3. 此时 IPC 的 release/delete/detach 路径调用 rt_sched_thread_ready()
  4. rt_sched_thread_ready() 因 timer 已经被 timeout path 获取而失败;
  5. waiter 此时可能仍然挂在原 IPC 的 suspend list 上,等待 timeout callback 完成后续处理。

mutex 因为还有 PI、owner priority、pending_object 等额外状态,所以这个问题表现得最明显,但其他 IPC 也可能存在类似竞态,建议后续一起检查:

  • semaphore
  • event
  • mailbox
  • message queue

目前看到几个值得关注的点。

1. rt_susp_list_dequeue() / rt_susp_list_resume_all() 的通用语义

rt_susp_list_dequeue() 当前如果:

rt_sched_thread_ready(thread)

失败,会直接返回 RT_NULL

rt_susp_list_resume_all() 的逻辑是:

thread = rt_susp_list_dequeue(...);
while (thread)
{
    thread = rt_susp_list_dequeue(...);
}

因此如果 suspend list 的队首 waiter 正好已经被 timeout path 取得 wakeup ownership,第一次 dequeue 就可能返回 RT_NULL

此时问题是:

list 后面可能还有其他有效 waiter,但 resume_all() 已经停止。

对于 delete/detach 路径,还需要进一步确认这种情况下 IPC object 是否可能在 suspend list 尚未真正清理完成时就被释放。

这个问题不是 mutex 特有的,因为 semaphore/event/mailbox/message queue 的 delete/detach/reset 都在使用这套通用 suspend-list API。

2. semaphore / mailbox / message queue 可能存在 lost wakeup

这些 IPC 的 release/send/recv 路径通常也是:

rt_susp_list_dequeue(...)

一次。

如果当前队首 waiter 正好已经 timeout-owned:

waiter A: timeout 已经触发
waiter B: RT_WAITING_FOREVER
producer: release/send

producer 尝试唤醒 A 时 rt_sched_thread_ready(A) 失败。

如果当前代码随后直接按“没有 waiter 被唤醒”处理,而没有继续检查 B,就可能出现:

资源已经可用
+
后面仍有合法 waiter
+
合法 waiter 没有被唤醒

也就是潜在的 lost wakeup。

这个场景我认为至少值得给:

  • semaphore release
  • mailbox send/recv
  • message queue send/recv

分别补一个 timeout-owned head waiter + second valid waiter 的回归测试。

3. event / mailbox / message queue 还需要检查 READY -> RUNNING 之间的对象生命周期

mutex 当前 PR 重点处理的是:

waiter 被唤醒
    ↓
READY
    ↓
对象被 delete/detach
    ↓
waiter 真正运行
    ↓
继续访问已经失效的 mutex

其他 IPC 也存在类似模式。

例如一些 recv/send 路径在:

rt_schedule();

返回后,如果 thread->error == RT_EOK,还会重新:

rt_spin_lock(&(ipc->spinlock));

并继续读取 IPC 内部状态。

因此还应该验证:

正常 IPC wakeup
→ waiter 已 READY 但尚未获得 CPU
→ 另一个线程 delete/detach IPC
→ waiter 恢复执行

这种情况下是否存在对象已经释放,但 waiter 仍继续解引用 IPC 的问题。

这部分我目前认为是需要进一步写 regression test 验证的风险点,还不建议在没有复现前直接认定为确定 bug。

4. 当前 mutex PR 本身也建议再补一个 lifetime case

现在已经覆盖了:

timeout -> READY -> mutex detach -> waiter resume

但还可以考虑增加:

mutex release
    ↓
成功 handoff 给 waiter
    ↓
waiter 已 READY,但还没有运行
    ↓
另一个线程 delete/detach mutex
    ↓
waiter 从 rt_schedule() 返回

因为 release 成功后已经:

next_thread->pending_object = RT_NULL;

但 waiter 从 rt_schedule() 返回时:

thread->error == RT_EOK

当前 _rt_mutex_take() 仍然会继续:

rt_spin_lock(&(mutex->spinlock));

然后检查:

mutex->owner == thread

所以这个 case 建议也验证一下,避免还有一条正常 handoff 后的 lifetime gap。


我的建议是:

当前 #11730 仍然保持 mutex scope,不要一次把所有 IPC 都改进来。

但可以:

  1. 先补一下 release -> READY -> delete/detach -> waiter resume 的 mutex regression test;
  2. 当前 PR 合并后,再单独开一个 generic IPC issue/PR;
  3. 系统检查 rt_susp_list_dequeue() / rt_susp_list_resume_all() 在 timeout wakeup ownership race 下的语义;
  4. 给 semaphore/event/mailbox/message queue 分别补 delete/detach、timeout-owned head waiter 和 READY-before-running 生命周期测试。

这样可以避免把当前 mutex 修复范围扩大太多,同时也不会漏掉同一套 scheduler/IPC wakeup 机制下可能存在的其他问题。

@liulangrenaaa
liulangrenaaa force-pushed the 20260821-fix/lifetime/mutex-delete-waiter-uaf branch from 3c34f48 to 29e39b8 Compare August 28, 2026 05:30
@liulangrenaaa

Copy link
Copy Markdown
Contributor Author

感谢建议,我后续再修改下

@liulangrenaaa
liulangrenaaa force-pushed the 20260821-fix/lifetime/mutex-delete-waiter-uaf branch 2 times, most recently from 9961da0 to 9169122 Compare August 28, 2026 09:53
@liulangrenaaa

Copy link
Copy Markdown
Contributor Author

@wdfk-prog
感谢之前的 review,按你的建议又整理了一版,主要改动如下:

  • 将 waiter detach 和 PI recompute 拆开:

    • _mutex_detach_waiter_locked() 只负责 waiter unlink 和清理 pending_object
    • rt_mutex_timeout_waiter() 负责 timeout 路径的 mutex priority / owner PI 更新
  • delete/detach 路径使用 rt_sched_thread_ready() 仲裁 timeout 与 delete 的 wakeup ownership,并在同一个 scheduler-lock 临界区内完成 waiter cleanup。

  • timeout waiter 在进入 READY 前完成 mutex-specific cleanup,因此 _rt_mutex_take() 的 timeout/error 路径不再需要重新访问已经可能失效的 mutex。

  • release 路径会跳过 timeout-owned 的队首 waiter,继续选择下一个有效 waiter。

  • release handoff 后保留 pending_object 作为 in-flight handoff token,直到 _rt_mutex_take() 完成 handoff 校验。

  • mutex delete/detach 会在对象失效前清除 handed-off owner 的 token,避免 READY/RUNNING 期间的 stale mutex access。

  • _rt_mutex_take() 在 handoff token 已经失效时直接返回错误,不再 fallback 解引用原 mutex。

  • thread exit 路径也区分普通 mutex waiter 和 handed-off owner token。

  • 补充了 release -> handoff -> READY -> delete/detach -> waiter resume 的 dynamic/static regression tests,以及 timeout READY、timeout/delete race、timeout-owned head waiter 等测试。

本地验证:

  • QEMU SMP 2 CPUs
  • UP 1 CPU
  • SMP 1 CPU
  • RT_DEBUGING_ASSERT / RT_DEBUGING_CRITICAL enabled

最新版本已经更新到这个 PR,麻烦有空再帮忙 review 一下,谢谢。

Mutex waiters can race with timeout callbacks, mutex release, object
deletions, and thread exit. A waiter may already be READY while its
take path has not resumed, so clearing or reusing the mutex object at
the wrong time can lead to stale accesses, incorrect error values, or
corrupted priority-inheritance state.

- Add rt_mutex_timeout_waiter() to detach a timed-out waiter while the
  scheduler is locked. Update the mutex priority and the owner priority
  before the waiter is inserted into the ready queue.
- Use rt_sched_thread_ready() to arbitrate timeout, release, and delete
  wakeups. This prevents two paths from claiming the same waiter and
  lets mutex release skip a waiter whose timeout callback owns the
  wakeup.
- Keep thread->pending_object pointing to the mutex after release hands
  ownership to a waiter. The pointer is used as an in-flight handoff
  token until _rt_mutex_take() completes its wakeup handling.
- Consume the handoff token under the scheduler lock before touching
  the mutex. Clear the token only after the handoff is validated, and
  return RT_EINTR for an unexpected resume that did not grant ownership.
- Clear the handoff token and set RT_ERROR before deleting or detaching
  the mutex. This prevents the resumed waiter from dereferencing an
  object whose storage has already been released or reused.
- Handle the handoff token during thread exit so a handed-off owner is
  not incorrectly removed from the mutex wait list.
- Add regression tests covering dynamic and static mutex deletion,
  timeout-to-READY races, timeout and release races, timeout-owned
  release heads, cross-thread owner cleanup, and post-handoff deletion.

The mutex structure and ABI remain unchanged. The internal timeout
waiter interface is exposed only to kernel and IPC sources.

Verified on QEMU with core.mutex using SMP with 2 CPUs, UP with 1 CPU,
and SMP with 1 CPU with RT_DEBUGING_ASSERT and
RT_DEBUGING_CRITICAL enabled.

Signed-off-by: Hui Su <3164683437@qq.com>
@liulangrenaaa
liulangrenaaa force-pushed the 20260821-fix/lifetime/mutex-delete-waiter-uaf branch from 9169122 to 98a9deb Compare August 28, 2026 10:27

@wdfk-prog wdfk-prog left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

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

Labels

Kernel PR has src relate code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants