Repository navigation
Description
[Bug] timerfd: timerfd_settime()/timerfd_gettime() accept any fd -> type confusion -> kernel function-pointer call (PC control) from an unprivileged user process
RT-Thread Version: master (v5.4.0-dev, tested at 5d1fb66; affected since v5.0.2, introduced by 7c2f807 / PR #8067, 2023-09-23)
Affected area: components (libc/posix io/timerfd)
Hardware-BSP vendor: QEMU (qemu-vexpress-a9 verified full chain; qemu-virt64-aarch64 corroborated)
Architecture: ARM Cortex-A (32-bit main), AArch64 (64-bit corroboration)
Board and hardware details: qemu-vexpress-a9 BSP built with RT_USING_SMART (default RT-Thread Smart build; RT_USING_POSIX_TIMERFD and RT_USING_POSIX_SIGNALFD are default y if RT_USING_SMART in components/libc/posix/Kconfig, so every RT-Smart build ships this code)
Develop Toolchain: GCC (arm-gnu-toolchain-13.2 / RT-Thread musl toolchain)
Describe the bug
timerfd_do_settime() and timerfd_do_gettime() take any file descriptor from the process fd table and cast df->vnode->data to struct rt_timerfd * without checking that the file is actually a timerfd (components/libc/posix/io/timerfd/timerfd.c:384 and :511):
df = fd_get(fd);
if (!df)
return -EINVAL;
tfd = df->vnode->data; /* no fops / type validation */Linux validates the same operation with if (file->f_op != &timerfd_fops) return -EINVAL; (fs/timerfd.c). RT-Thread performs no equivalent check, so vnode->data of any user-reachable fd object (regular file, signalfd, eventfd, pipe, device, ...) is interpreted as a struct rt_timerfd: the kernel then takes/releases a bogus rt_mutex embedded in the foreign object, writes attacker-controlled itimerspec words into it, dereferences tfd->timer (rt_timer_stop/rt_timer_delete on whatever qword sits there — an arbitrary list-unlink + rt_free), and — with a negative it_value.tv_sec — invokes timerfd_timeout(tfd) synchronously, whose first statement rt_wqueue_wakeup(&tfd->timerfd_queue, POLLIN) walks a waitqueue list head read from the confused object and calls entry->wakeup(entry, key) (components/drivers/ipc/waitqueue.c:100) — an indirect call through a function pointer taken from attacker-chosen memory, with attacker-chosen r0.
On 32-bit RT-Thread (RT_NAME_MAX=16) a signalfd is a perfect confused object: the first 128 bytes of its ctx are the sigset_t sigmask, copied verbatim from a user pointer by signalfd() (NUL bytes allowed), and they fully cover the fake rt_timerfd header — including the fake rt_mutex (its type byte can be set to RT_Object_Class_Mutex to satisfy the compiled-in RT_ASSERT, owner left 0 so the take succeeds immediately) and timerfd_queue.waiting_list.next (the waitqueue walk start). Since the RT-Smart kernel shares the page table and dereferences user mappings directly, the fake waitqueue node can simply live in the attacker's own user memory.
Two unprivileged syscalls (signalfd() + timerfd_settime() on the signalfd fd, both official POSIX-style APIs) then yield kernel control-flow hijacking.
Reproduce (qemu-vexpress-a9 + RT-Smart, default build)
- Build the
qemu-vexpress-a9BSP withRT_USING_SMART(timerfd/signalfd are enabled by default under Smart). PoC is a freestanding user ELF (no libc) run as/sbin/init; source: see "PoC" below. - Control group:
timerfd_create(CLOCK_MONOTONIC, 0)+timerfd_settime(fd, {1s})on the real timerfd → returns 0, kernel stays alive (versionstill answers). Log:log32pc-control.txt. - Attack (PC control):
signalfd(-1, mask, 0)withmask[4..8] = &fake_node(user .bss) andmask[76] = RT_Object_Class_Mutex, rest zeros;- fake node in user memory with
*(node + 12) = 0x42424242(the 32-bitentry->wakeupslot); timerfd_settime(sfd, 0, {it_value.tv_sec = -1}, NULL).
Result — kernel prefetch abort with fully controlled PC (3/3 runs byte-identical):
Under GDB, breaking atprefetch abort: ... r00:0x100004bc r02:0x100004c0 r03:0x42424242 lr :0xc0091ebc pc :0x42424246 (rt_wqueue_wakeup+140 return addr) backtrace: c0091eb8 c00431ec c00439f4 c0043cc0 c0063804 c0045750 = rt_wqueue_wakeup <- timerfd_timeout(timerfd.c:312) <- timerfd_do_settime(timerfd.c:481) <- timerfd_settime <- sys_timerfd_settime(lwp_syscall.c:11050) <- vector_swirt_wqueue_wakeup+136showsblx r3withr3 = 0x42424242,r0 = 0x100004bc(attacker user address); a single step lands atpc = 0x42424242. - Attack (arbitrary kernel function execution): same, but
*(node+12) = &rt_kprintf(0xc00b7db8, no KASLR) and the attacker string atnode-4. The kernel itself prints the attacker text on the console:then dies at an attacker-steered address. Log:[S3] before settime [timerfd-pwn] ke.. rt_kprintf() executed via signalfd sigmask waitqueue confusionlog32pc-banner.txt. - Minimal form (all-zero mask): deterministic kernel data abort at
dfar=0xCinside the same walk. Log:log32pc-null.txt. - AArch64 corroboration (qemu-virt64-aarch64, RT-Smart): on 64-bit the same missing check makes
timerfd_settime(signalfd_fd, ...)crash in_rt_mutex_take's suspend path (bogus owner read from the real mutex header), and with a tmpfs file fd the confusion passes the take and arms a real kernel soft timer whose callback parameter is the tmpfs object (GDB shows thert_timerpointer written atvnode->data + 64), then dies in the release path — i.e. a foreign kernel object can be given an armed kernel timer with no reference held. Logs:log-pc-null.txt,log-uaf2-attack1.txt, GDB dump inlog-gdb-run.txt.
Expected behavior: timerfd_settime()/timerfd_gettime() must reject fds whose file operations are not timerfd_fops (return -EBADF/-EINVAL), as Linux does.
Actual behavior: any fd is accepted; its vnode->data is used as struct rt_timerfd, leading to type confusion with kernel control-flow hijacking (demonstrated PC control and attacker-directed execution of rt_kprintf from an unprivileged user process).
Suggested fix
In timerfd_do_settime() / timerfd_do_gettime() (and for symmetry eventfd/signalfd equivalents), validate the file type before use:
df = fd_get(fd);
if (!df)
return -EINVAL;
#ifdef RT_USING_DFS_V2
if (df->fops != &timerfd_fops)
return -EINVAL;
#else
if (df->vnode->fops != &timerfd_fops)
return -EINVAL;
#endifOther additional context
- Severity: CVSS 3.1 7.8 (AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H); CWE-822 (Untrusted Pointer Dereference), CWE-843 (Type Confusion).
- Attack preconditions (explicit): attacker can run an unprivileged user program on an RT-Thread Smart build (
RT_USING_SMART); timerfd and signalfd are compiled in by default in every Smart build (default y if RT_USING_SMART). No special privileges, no race, fully deterministic. - Impact summary: one
timerfd_settime()call on a non-timerfd fd gives kernel memory corruption and control-flow hijacking: demonstrated arbitrary kernel indirect call (attacker-controlled target and first argument), arbitrary-object writes (itimerspecwords),rt_timer_stop/rt_timer_deleteon an attacker-influenced pointer (list-unlink writes +rt_freeof arbitrary memory), and arming a kernel soft timer on a foreign object (use-after-free surface). - Affected versions: introduced by 7c2f807 (2023-09-23, PR Add the system call timerfd and update SConscript for signalfd #8067); first affected release v5.0.2; v5.0.2 / v5.1.0 / v5.2.x / v5.3.0 / master (v5.4.0-dev) all verified affected (per-tag file reads); no fix commit or PR exists as of today.
- Relationship to prior reports: none — no existing issue/PR/CVE mentions timerfd (searched issues, PRs, NVD). An earlier third-party-style survey note in a local audit archive had mis-filed the missing type check as low severity under the incorrect assumption that TIMERFD is off by default; the Kconfig default under
RT_USING_SMARTproves otherwise. - All evidence is full-chain on real RT-Thread QEMU targets (no host-side sanitizers): serial exception dumps, kernel console output, and GDB (gdb-multiarch) register/backtrace captures; attack runs repeated 3× with byte-identical results; control groups included for every step.
Activity
github-actions commentedon Nov 3, 2026
如需认领并处理此 Issue,请评论
/claim。To claim and work on this issue, comment
/claim.