| Titre | RT-Thread v5.0.2 Out-of-bounds Write |
|---|
| Description | I have already reported this vulnerability in the project's GitHub issue tracker for review by the maintainers. The report is provided below.
# ls1c CAN receive path trusts raw DLC and writes past rt_can_msg data
## Describe the bug
The Loongson ls1cdev CAN receive path stores the raw hardware DLC value in a global receive message and later uses that value as the copy bound for `struct rt_can_msg.data[8]`.
The external input boundary is the CAN controller receive frame. When a frame arrives from the CAN bus, the controller exposes attacker-controlled frame metadata through the `CANx->IDE_RTR_DLC` register. The low 4 bits of this register are used as the DLC. That external DLC value is stored in the global `RxMessage.DLC` and later controls the copy loop in `recvmsg()`.
The concrete data flow is:
```text
external CAN frame
-> CAN controller RX register CANx->IDE_RTR_DLC[DLC]
-> CAN_Receive(): global RxMessage.DLC
-> ls1c_can0_irqhandler()/ls1c_can1_irqhandler()
-> rt_hw_can_isr()
-> recvmsg(): pmsg->len and loop bound
-> out-of-bounds read from RxMessage.Data[8]
-> out-of-bounds write to pmsg->data[8]
```
The RT-Thread classic CAN message type stores only 8 payload bytes when `RT_CAN_USING_CANFD` is not enabled:
```text
components/drivers/include/drivers/dev_can.h:510
components/drivers/include/drivers/dev_can.h:526
```
The ls1c low-level receive routine extracts the low 4 bits of the hardware `IDE_RTR_DLC` register:
```c
/* External input: DLC bits from the received CAN frame. */
RxMessage->DLC = (CANx->IDE_RTR_DLC & 0x0F);
```
`CanRxMsg.Data` is only 8 bytes:
```c
typedef struct
{
unsigned long StdId;
unsigned long ExtId;
unsigned char IDE;
unsigned char RTR;
unsigned char DLC;
unsigned char Data[8];
} CanRxMsg;
```
The interrupt handler stores the received frame in the global `RxMessage` and then calls the generic RT-Thread CAN ISR:
```c
/* CAN_Receive stores the external DLC in global RxMessage.DLC. */
CAN_Receive(CANx, &RxMessage);
CANx->CMR |= CAN_CMR_RRB;
CANx->CMR |= CAN_CMR_CDO;
rt_hw_can_isr(&bxcan0, RT_CAN_EVENT_RX_IND);
```
The registered `recvmsg` callback then uses the unchecked global `RxMessage.DLC` as the loop bound:
```c
/* RxMessage.DLC is derived from the external CAN DLC. */
pmsg->len = RxMessage.DLC;
/*
* Overflow trigger:
* RxMessage.Data has valid indexes [0..7].
* pmsg->data also has valid indexes [0..7].
* If the external DLC is 9..15, the iteration with i == 8 reads
* RxMessage.Data[8] and writes pmsg->data[8].
*/
for(i= 0;i< RxMessage.DLC; i++)
{
pmsg->data[i] = RxMessage.Data[i];
}
```
In the generic CAN ISR, `pmsg` points to a stack `struct rt_can_msg tmpmsg`:
```c
/* Stack object inside rt_hw_can_isr(). */
struct rt_can_msg tmpmsg;
/* recvmsg() receives &tmpmsg as pmsg. */
ch = can->ops->recvmsg(can, &tmpmsg, no);
...
rt_memcpy(&listmsg->data, &tmpmsg, sizeof(struct rt_can_msg));
```
Therefore, if the controller reports a raw DLC code 9..15, `recvmsg()` reads past `RxMessage.Data[8]` and writes past `tmpmsg.data[8]` before the corrupted stack object is copied into the RT-Thread RX FIFO.
Locations:
```text
bsp/loongson/ls1cdev/libraries/ls1c_can.h:137
bsp/loongson/ls1cdev/libraries/ls1c_can.h:171
bsp/loongson/ls1cdev/libraries/ls1c_can.h:182
bsp/loongson/ls1cdev/libraries/ls1c_can.h:183
bsp/loongson/ls1cdev/libraries/ls1c_can.c:419
bsp/loongson/ls1cdev/drivers/drv_can.c:28
bsp/loongson/ls1cdev/drivers/drv_can.c:374
bsp/loongson/ls1cdev/drivers/drv_can.c:378
bsp/loongson/ls1cdev/drivers/drv_can.c:388
bsp/loongson/ls1cdev/drivers/drv_can.c:391
bsp/loongson/ls1cdev/drivers/drv_can.c:393
bsp/loongson/ls1cdev/drivers/drv_can.c:420
bsp/loongson/ls1cdev/drivers/drv_can.c:423
bsp/loongson/ls1cdev/drivers/drv_can.c:460
bsp/loongson/ls1cdev/drivers/drv_can.c:463
components/drivers/can/dev_can.c:982
components/drivers/can/dev_can.c:998
components/drivers/can/dev_can.c:1039
components/drivers/include/drivers/dev_can.h:526
```
## Steps to reproduce
I have not reproduced this on physical ls1cdev hardware yet. I verified the source-level bug with a standalone reduced check that preserves the relevant `recvmsg()` copy semantics.
The source-level trigger is:
```text
CANx->IDE_RTR_DLC low nibble = 9..15
RT_CAN_USING_CANFD = not enabled
USING_BXCAN0 or USING_BXCAN1 = enabled
```
Reduced check:
```c
#include <stdint.h>
typedef unsigned char rt_uint8_t;
typedef unsigned int rt_uint32_t;
struct rt_can_msg {
rt_uint32_t id;
rt_uint8_t len;
rt_uint8_t data[8];
};
typedef struct {
unsigned long StdId;
unsigned long ExtId;
unsigned char IDE;
unsigned char RTR;
unsigned char DLC;
unsigned char Data[8];
} CanRxMsg;
static CanRxMsg RxMessage;
static int recvmsg(void *buf)
{
struct rt_can_msg *pmsg = (struct rt_can_msg *)buf;
int i;
/* External input: raw DLC from the received CAN frame. */
pmsg->len = RxMessage.DLC;
for (i = 0; i < RxMessage.DLC; i++) {
/* OOB when RxMessage.DLC > 8 and i reaches 8. */
pmsg->data[i] = RxMessage.Data[i];
}
return 0;
}
int main(void)
{
struct rt_can_msg msg = {0};
RxMessage.DLC = 15; /* external DLC value 15 */
recvmsg(&msg);
return 0;
}
```
Compile it with AddressSanitizer, for example:
```text
clang -x c -fsanitize=address -O0 -g repro.c -o repro && ./repro
```
Changing `recvmsg()` to clamp or reject `RxMessage.DLC > 8` avoids the ASan report.
## Relevant log output
AddressSanitizer reports a stack buffer overflow on the write into `struct rt_can_msg.data`:
```text
ERROR: AddressSanitizer: stack-buffer-overflow
WRITE of size 1
#0 recvmsg ... repro.c
#1 main ... repro.c
This frame has 1 object(s):
[..] 'msg' <== Memory access overflows this variable
SUMMARY: AddressSanitizer: stack-buffer-overflow in recvmsg
```
## Impact
Potential memory corruption from an externally supplied CAN frame on Loongson ls1cdev boards when a CAN channel is enabled.
The attacker-controlled value is the raw DLC field observed by the CAN controller. If the controller reports DLC values 9..15 to software, the driver reads beyond the global `RxMessage.Data[8]` object and writes beyond the generic RT-Thread ISR stack message buffer.
This path is reachable from the receive interrupt handlers for CAN0 and CAN1 when the corresponding board options are enabled.
## Environment
```text
Initial RT-Thread commit checked: c39e92f4c1
Checked tree description: v5.0.2-2360-gc39e92f4c1-dirty
Affected driver: bsp/loongson/ls1cdev/drivers/drv_can.c
Affected low-level CAN code: bsp/loongson/ls1cdev/libraries/ls1c_can.c
Affected board family: Loongson ls1cdev
Target hardware: not tested on board yet
Verification: host-side AddressSanitizer semantic check
```
The board Kconfig defines CAN0 and CAN1 channel options:
```text
bsp/loongson/ls1cdev/Kconfig:79 config USING_BXCAN0
bsp/loongson/ls1cdev/Kconfig:84 config USING_BXCAN1
```
The currently checked configuration has the CAN core enabled but disables the two channel options:
```text
bsp/loongson/ls1cdev/.config:277 CONFIG_RT_USING_CAN=y
bsp/loongson/ls1cdev/.config:279 # CONFIG_RT_CAN_USING_CANFD is not set
bsp/loongson/ls1cdev/.config:1524 # CONFIG_USING_BXCAN0 is not set
bsp/loongson/ls1cdev/.config:1525 # CONFIG_USING_BXCAN1 is not set
bsp/loongson/ls1cdev/rtconfig.h:169 #define RT_USING_CAN
```
## Additional context
A fix should validate the DLC before it is stored as the RT-Thread message length or used as a copy bound. For classic CAN, the code should either reject/drop raw DLC values above 8 or clamp the payload length to 8:
```c
rt_uint8_t len = RxMessage.DLC;
if (len > 8) {
len = 8;
}
pmsg->len = len;
for (i = 0; i < len; i++) {
pmsg->data[i] = RxMessage.Data[i];
}
```
The low-level receive code should preserve the same invariant if `DLC` is later exposed to other call sites. |
|---|
| La source | ⚠️ https://github.com/RT-Thread/rt-thread/issues/11424 |
|---|
| Utilisateur | Zephyr Saxon (UID 80853) |
|---|
| Soumission | 01/06/2026 08:38 (il y a 1 mois) |
|---|
| Modérer | 03/07/2026 15:51 (1 month later) |
|---|
| Statut | Accepté |
|---|
| Entrée VulDB | 376113 [RT-Thread jusqu’à 5.0.2 ls1c CAN ls1c_can.h recvmsg buffer overflow] |
|---|
| Points | 20 |
|---|