Skip to content

[Bug] timerfd: timerfd_settime()/timerfd_gettime() accept any fd -> type confusion -> kernel PC control from an unprivileged user process #11984

Open

Description

@XueDugu

[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)

  1. Build the qemu-vexpress-a9 BSP with RT_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.
  2. Control group: timerfd_create(CLOCK_MONOTONIC, 0) + timerfd_settime(fd, {1s}) on the real timerfd → returns 0, kernel stays alive (version still answers). Log: log32pc-control.txt.
  3. Attack (PC control):
    • signalfd(-1, mask, 0) with mask[4..8] = &fake_node (user .bss) and mask[76] = RT_Object_Class_Mutex, rest zeros;
    • fake node in user memory with *(node + 12) = 0x42424242 (the 32-bit entry->wakeup slot);
    • timerfd_settime(sfd, 0, {it_value.tv_sec = -1}, NULL).
      Result — kernel prefetch abort with fully controlled PC (3/3 runs byte-identical):
    prefetch 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_swi
    
    Under GDB, breaking at rt_wqueue_wakeup+136 shows blx r3 with r3 = 0x42424242, r0 = 0x100004bc (attacker user address); a single step lands at pc = 0x42424242.
  4. Attack (arbitrary kernel function execution): same, but *(node+12) = &rt_kprintf (0xc00b7db8, no KASLR) and the attacker string at node-4. The kernel itself prints the attacker text on the console:
    [S3] before settime
    [timerfd-pwn] ke.. rt_kprintf() executed via signalfd sigmask waitqueue confusion
    
    then dies at an attacker-steered address. Log: log32pc-banner.txt.
  5. Minimal form (all-zero mask): deterministic kernel data abort at dfar=0xC inside the same walk. Log: log32pc-null.txt.
  6. 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 the rt_timer pointer written at vnode->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 in log-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;
#endif

Other 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 (itimerspec words), rt_timer_stop/rt_timer_delete on an attacker-influenced pointer (list-unlink writes + rt_free of 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_SMART proves 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

  1. github-actions commented on Nov 3, 2026

    @github-actions

    如需认领并处理此 Issue,请评论 /claim。

    To claim and work on this issue, comment /claim.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions