RT-Thread Version
v5.0.2
Hardware Type/Architectures
ls1c CAN
Develop Toolchain
Other
Describe the bug
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:
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:
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:
/* External input: DLC bits from the received CAN frame. */
RxMessage->DLC = (CANx->IDE_RTR_DLC & 0x0F);
CanRxMsg.Data is only 8 bytes:
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:
/* 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:
/* 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:
/* 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:
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:
CANx->IDE_RTR_DLC low nibble = 9..15
RT_CAN_USING_CANFD = not enabled
USING_BXCAN0 or USING_BXCAN1 = enabled
Reduced check:
#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:
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:
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
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:
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:
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:
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.
Other additional context
No response
RT-Thread Version
v5.0.2
Hardware Type/Architectures
ls1c CAN
Develop Toolchain
Other
Describe the bug
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_DLCregister. The low 4 bits of this register are used as the DLC. That external DLC value is stored in the globalRxMessage.DLCand later controls the copy loop inrecvmsg().The concrete data flow is:
The RT-Thread classic CAN message type stores only 8 payload bytes when
RT_CAN_USING_CANFDis not enabled:The ls1c low-level receive routine extracts the low 4 bits of the hardware
IDE_RTR_DLCregister:CanRxMsg.Datais only 8 bytes:The interrupt handler stores the received frame in the global
RxMessageand then calls the generic RT-Thread CAN ISR:The registered
recvmsgcallback then uses the unchecked globalRxMessage.DLCas the loop bound:In the generic CAN ISR,
pmsgpoints to a stackstruct rt_can_msg tmpmsg:Therefore, if the controller reports a raw DLC code 9..15,
recvmsg()reads pastRxMessage.Data[8]and writes pasttmpmsg.data[8]before the corrupted stack object is copied into the RT-Thread RX FIFO.Locations:
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:
Reduced check:
Compile it with AddressSanitizer, for example:
Changing
recvmsg()to clamp or rejectRxMessage.DLC > 8avoids the ASan report.Relevant log output
AddressSanitizer reports a stack buffer overflow on the write into
struct rt_can_msg.data: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
The board Kconfig defines CAN0 and CAN1 channel options:
The currently checked configuration has the CAN core enabled but disables the two channel options:
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:
The low-level receive code should preserve the same invariant if
DLCis later exposed to other call sites.Other additional context
No response