What changed, and why it matters
This commit rewrites the USB virtual serial port (VCP) ring buffer code in Trezor's firmware. The old code tracked buffer space using simple read/write counters, which could become inconsistent when the USB data-out interrupt and the application read data at the same time. The new code uses a dedicated ring-buffer module that protects its state with interrupt locks and tracks how many bytes are actually used. It also stops requesting new USB packets when the buffer is nearly full, preventing overflow and lost data. The change is a hardening/bug-fix patch; the commit message does not call it a security fix, but race conditions in USB buffering can affect device reliability and, in the worst case, be abused to cause information leakage or denial of service.
Treat as a reliability and potential security hardening fix. Review the new `usb_rbuf.c` for any remaining integer/alignment issues, verify that `irq_lock` duration is short enough not to affect USB timing, and consider fuzzing the VCP path with rapid small/large packets to confirm the race and overflow are resolved. If this commit fixes a reported vulnerability, request the vendor publish a security advisory or CVE.
Security signals we found
Race condition in interrupt-vs-task ring-buffer access mitigated by irq_lock
Buffer overflow/underflow accounting changed from counter arithmetic to explicit used-byte count
Receive re-arming now gated on free space, preventing dropped USB packets
New dedicated ring-buffer module centralizes safety-critical USB VCP buffering
No changelog entry and no vendor security advisory linked in commit
Evidence from the diff
The patch replaces the inline ring-buffer logic in usb_class_vcp.c with a new usb_rbuf.c/usb_rbuf.h module. Key changes: (1) the buffer now stores used, rptr, and wptr instead of deriving occupancy from write - read, making concurrent interrupt/application access safer; (2) all read/write/reset/length operations run under irq_lock()/irq_unlock(), eliminating the previous unprotected counter updates in usb_vcp_class_data_out and on_read/on_write; (3) usb_vcp_class_data_out now only re-arms USBD_LL_PrepareReceive when usb_rbuf_unused_bytes() >= max_packet_len, and on_read re-arms reception after freeing enough space; (4) a recv_pending flag prevents duplicate receive preparation. Build files add the new source file for STM32F4 and STM32U5. The patch is defensive and fixes a real concurrency/overflow bug, but the commit does not disclose a CVE or credit an external reporter.
Changed components
core/embed/io/usb/stm32/usb_class_vcp.ccore/embed/io/usb/stm32/usb_rbuf.ccore/embed/io/usb/stm32/usb_rbuf.hcore/site_scons/models/stm32f4_common.pycore/site_scons/models/stm32u5_common.pyInspect captured patch +247 / −93
diff --git a/core/embed/io/usb/stm32/usb_class_vcp.c b/core/embed/io/usb/stm32/usb_class_vcp.c
index 0bcfab1c5..6728a50cd 100644
--- a/core/embed/io/usb/stm32/usb_class_vcp.c
+++ b/core/embed/io/usb/stm32/usb_class_vcp.c
@@ -22,6 +22,7 @@
#include <trezor_rtl.h>
#include <io/usb_vcp.h>
+#include <sys/irq.h>
#include <sys/sysevent_source.h>
#ifdef USE_SUSPEND
@@ -29,6 +30,7 @@
#endif
#include "usb_internal.h"
+#include "usb_rbuf.h"
// Communications Device Class Code (bFunctionClass, bInterfaceClass)
#define USB_CLASS_CDC 0x02
@@ -129,14 +131,6 @@ typedef enum {
USB_CDC_SPACE_PARITY = 4,
} usb_cdc_line_coding_bParityType_t;
-/* usb_rbuf_t is used internally for the RX/TX buffering. */
-typedef struct {
- size_t cap;
- volatile size_t read;
- volatile size_t write;
- uint8_t *buf;
-} usb_rbuf_t;
-
// Maximal length of packets on IN CMD EP
#define USB_CDC_MAX_CMD_PACKET_LEN 0x08
@@ -159,6 +153,7 @@ typedef struct {
uint8_t ep_out;
uint16_t max_packet_len;
uint8_t ep_in_is_idle; // Set to 1 after IN endpoint gets idle
+ bool recv_pending;
uint8_t cmd_buffer[USB_CDC_MAX_CMD_PACKET_LEN];
} usb_vcp_state_t;
@@ -308,15 +303,8 @@ secbool usb_vcp_add(const usb_vcp_info_t *info) {
state->handle = info->handle;
state->desc_block = d;
- state->rx_ring.buf = info->rx_buffer;
- state->rx_ring.cap = info->rx_buffer_len;
- state->rx_ring.read = 0;
- state->rx_ring.write = 0;
-
- state->tx_ring.buf = info->tx_buffer;
- state->tx_ring.cap = info->tx_buffer_len;
- state->tx_ring.read = 0;
- state->tx_ring.write = 0;
+ usb_rbuf_init(&state->rx_ring, info->rx_buffer, info->rx_buffer_len);
+ usb_rbuf_init(&state->tx_ring, info->tx_buffer, info->tx_buffer_len);
state->rx_packet = info->rx_packet;
state->tx_packet = info->tx_packet;
@@ -340,47 +328,6 @@ secbool usb_vcp_add(const usb_vcp_info_t *info) {
return sectrue;
}
-static inline size_t ring_length(usb_rbuf_t *b) { return (b->write - b->read); }
-
-static inline bool ring_empty(usb_rbuf_t *b) { return ring_length(b) == 0; }
-
-static inline bool ring_full(usb_rbuf_t *b) { return ring_length(b) == b->cap; }
-
-static bool usb_vcp_can_read(usb_vcp_state_t *state) {
- return !ring_empty(&state->rx_ring);
-}
-
-static bool usb_vcp_can_write(usb_vcp_state_t *state) {
- return !ring_full(&state->tx_ring);
-}
-
-static ssize_t usb_vcp_read(usb_vcp_state_t *state, uint8_t *buffer,
- uint32_t buffer_size) {
- // Read from the rx ring buffer
- usb_rbuf_t *b = &state->rx_ring;
- size_t mask = b->cap - 1;
- size_t i;
- for (i = 0; (i < buffer_size) && !ring_empty(b); i++) {
- buffer[i] = b->buf[b->read & mask];
- b->read++;
- }
- return i;
-}
-
-static ssize_t usb_vcp_write(usb_vcp_state_t *state, const uint8_t *data,
- size_t data_size) {
- // Write into the tx ring buffer
- usb_rbuf_t *b = &state->tx_ring;
- size_t mask = b->cap - 1;
- size_t i;
- for (i = 0; (i < data_size) && !ring_full(b); i++) {
- b->buf[b->write & mask] = data[i];
- b->write++;
- }
-
- return i;
-}
-
static uint8_t usb_vcp_class_init(USBD_HandleTypeDef *dev, uint8_t cfg_idx) {
usb_vcp_state_t *state = (usb_vcp_state_t *)dev->pUserData;
@@ -393,15 +340,15 @@ static uint8_t usb_vcp_class_init(USBD_HandleTypeDef *dev, uint8_t cfg_idx) {
USB_CDC_MAX_CMD_PACKET_LEN);
// Reset the state
- state->rx_ring.read = 0;
- state->rx_ring.write = 0;
- state->tx_ring.read = 0;
- state->tx_ring.write = 0;
+ usb_rbuf_reset(&state->rx_ring);
+ usb_rbuf_reset(&state->tx_ring);
+
state->ep_in_is_idle = 1;
// Prepare the OUT EP to receive next packet
USBD_LL_PrepareReceive(dev, state->ep_out, state->rx_packet,
state->max_packet_len);
+ state->recv_pending = true;
if (!syshandle_register(state->handle, &usb_vcp_handle_vmt, state)) {
return USBD_FAIL;
@@ -478,25 +425,22 @@ static uint8_t usb_vcp_class_data_out(USBD_HandleTypeDef *dev, uint8_t ep_num) {
if (ep_num == state->ep_out) {
uint32_t len = USBD_LL_GetRxDataSize(dev, ep_num);
- // Write into the rx ring buffer
- usb_rbuf_t *b = &state->rx_ring;
- size_t mask = b->cap - 1;
- size_t i;
- for (i = 0; i < len; i++) {
- if (state->rx_intr_fn != NULL) {
+ if (state->rx_intr_fn != NULL) {
+ for (size_t i = 0; i < len; i++) {
if (state->rx_packet[i] == state->rx_intr_byte) {
state->rx_intr_fn();
}
}
- if (!ring_full(b)) {
- b->buf[b->write & mask] = state->rx_packet[i];
- b->write++;
- }
}
- // Prepare the OUT EP to receive next packet
- USBD_LL_PrepareReceive(dev, state->ep_out, state->rx_packet,
- state->max_packet_len);
+ usb_rbuf_write(&state->rx_ring, state->rx_packet, len);
+
+ state->recv_pending = false;
+ if (usb_rbuf_unused_bytes(&state->rx_ring) >= state->max_packet_len) {
+ USBD_LL_PrepareReceive(dev, state->ep_out, state->rx_packet,
+ state->max_packet_len);
+ state->recv_pending = true;
+ }
#ifdef USE_SUSPEND
wakeup_flags_set(WAKEUP_FLAG_USB);
@@ -513,22 +457,15 @@ static uint8_t usb_vcp_class_sof(USBD_HandleTypeDef *dev) {
return USBD_OK;
}
- // Read from the tx ring buffer
- usb_rbuf_t *b = &state->tx_ring;
- uint8_t *buf = state->tx_packet;
// We avoid sending full packets as they stall the hosts pipeline, see:
// <http://www.cypress.com/?id=4&rID=92719>
- size_t len = state->max_packet_len - 1;
- size_t mask = b->cap - 1;
- size_t i;
- for (i = 0; (i < len) && !ring_empty(b); i++) {
- buf[i] = b->buf[b->read & mask];
- b->read++;
- }
+ uint16_t buf_size = state->max_packet_len - 1;
- if (i > 0) {
+ uint16_t to_send = usb_rbuf_read(&state->tx_ring, state->tx_packet, buf_size);
+
+ if (to_send > 0) {
state->ep_in_is_idle = 0;
- USBD_LL_Transmit(dev, state->ep_in, buf, (uint16_t)i);
+ USBD_LL_Transmit(dev, state->ep_in, state->tx_packet, to_send);
}
return USBD_OK;
@@ -562,11 +499,11 @@ static void on_event_poll(void *context, bool read_awaited,
// assume that only one task is waiting for events and keep the
// logic simple.
- if (read_awaited && usb_vcp_can_read(state)) {
+ if (read_awaited && !usb_rbuf_is_empty(&state->rx_ring)) {
syshandle_signal_read_ready(state->handle, NULL);
}
- if (write_awaited && usb_vcp_can_write(state)) {
+ if (write_awaited && !usb_rbuf_is_full(&state->tx_ring)) {
syshandle_signal_write_ready(state->handle, NULL);
}
}
@@ -578,7 +515,7 @@ static bool on_check_read_ready(void *context, systask_id_t task_id,
UNUSED(task_id);
UNUSED(param);
- return usb_vcp_can_read(state);
+ return !usb_rbuf_is_empty(&state->rx_ring);
}
static bool on_check_write_ready(void *context, systask_id_t task_id,
@@ -588,19 +525,33 @@ static bool on_check_write_ready(void *context, systask_id_t task_id,
UNUSED(task_id);
UNUSED(param);
- return usb_vcp_can_write(state);
+ return !usb_rbuf_is_full(&state->tx_ring);
}
static ssize_t on_read(void *context, void *buffer, size_t buffer_size) {
usb_vcp_state_t *state = (usb_vcp_state_t *)context;
- return usb_vcp_read(state, (uint8_t *)buffer, buffer_size);
+ ssize_t recved_size = usb_rbuf_read(&state->rx_ring, buffer, buffer_size);
+
+ irq_key_t irq_key = irq_lock();
+
+ if (!state->recv_pending &&
+ usb_rbuf_unused_bytes(&state->rx_ring) >= state->max_packet_len) {
+ // Restart receiving if there is enough space in the ring buffer
+ USBD_LL_PrepareReceive(state->dev_handle, state->ep_out, state->rx_packet,
+ state->max_packet_len);
+ state->recv_pending = true;
+ }
+
+ irq_unlock(irq_key);
+
+ return recved_size;
}
static ssize_t on_write(void *context, const void *data, size_t data_size) {
usb_vcp_state_t *state = (usb_vcp_state_t *)context;
- return usb_vcp_write(state, (const uint8_t *)data, data_size);
+ return usb_rbuf_write(&state->tx_ring, (const uint8_t *)data, data_size);
}
static const syshandle_vmt_t usb_vcp_handle_vmt = {
diff --git a/core/embed/io/usb/stm32/usb_rbuf.c b/core/embed/io/usb/stm32/usb_rbuf.c
new file mode 100644
index 000000000..d68d5ddf9
--- /dev/null
+++ b/core/embed/io/usb/stm32/usb_rbuf.c
@@ -0,0 +1,90 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <trezor_rtl.h>
+
+#include <sys/irq.h>
+
+#include "usb_rbuf.h"
+
+void usb_rbuf_init(usb_rbuf_t *b, uint8_t *buf, size_t buf_size) {
+ b->buf = buf;
+ b->cap = buf_size;
+ b->used = 0;
+ b->rptr = b->buf;
+ b->wptr = b->buf;
+}
+
+void usb_rbuf_reset(usb_rbuf_t *b) {
+ irq_key_t irq_key = irq_lock();
+ b->used = 0;
+ b->rptr = b->buf;
+ b->wptr = b->buf;
+ irq_unlock(irq_key);
+}
+
+size_t usb_rbuf_used_bytes(usb_rbuf_t *b) {
+ irq_key_t irq_key = irq_lock();
+ size_t size = b->used;
+ irq_unlock(irq_key);
+ return size;
+}
+
+size_t usb_rbuf_unused_bytes(usb_rbuf_t *b) {
+ irq_key_t irq_key = irq_lock();
+ size_t size = b->cap - b->used;
+ irq_unlock(irq_key);
+ return size;
+}
+
+bool usb_rbuf_is_empty(usb_rbuf_t *b) { return usb_rbuf_used_bytes(b) == 0; }
+
+bool usb_rbuf_is_full(usb_rbuf_t *b) { return usb_rbuf_unused_bytes(b) == 0; }
+
+size_t usb_rbuf_read(usb_rbuf_t *b, uint8_t *buf, size_t buf_size) {
+ irq_key_t irq_key = irq_lock();
+ size_t to_read = MIN(buf_size, b->used);
+ size_t first_part = MIN(to_read, b->cap - (b->rptr - b->buf));
+ memcpy(buf, b->rptr, first_part);
+ size_t second_part = to_read - first_part;
+ memcpy(buf + first_part, b->buf, second_part);
+ b->rptr += first_part;
+ if (b->rptr == b->buf + b->cap) {
+ b->rptr = b->buf + second_part;
+ }
+ b->used -= to_read;
+ irq_unlock(irq_key);
+ return to_read;
+}
+
+size_t usb_rbuf_write(usb_rbuf_t *b, const uint8_t *data, size_t data_size) {
+ irq_key_t irq_key = irq_lock();
+ size_t to_write = MIN(data_size, b->cap - b->used);
+ size_t first_part = MIN(to_write, b->cap - (b->wptr - b->buf));
+ memcpy(b->wptr, data, first_part);
+ size_t second_part = to_write - first_part;
+ memcpy(b->buf, data + first_part, second_part);
+ b->wptr += first_part;
+ if (b->wptr == b->buf + b->cap) {
+ b->wptr = b->buf + second_part;
+ }
+ b->used += to_write;
+ irq_unlock(irq_key);
+ return to_write;
+}
diff --git a/core/embed/io/usb/stm32/usb_rbuf.h b/core/embed/io/usb/stm32/usb_rbuf.h
new file mode 100644
index 000000000..080ae355a
--- /dev/null
+++ b/core/embed/io/usb/stm32/usb_rbuf.h
@@ -0,0 +1,111 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <trezor_types.h>
+
+/** Ring buffer structure */
+typedef struct {
+ uint8_t *buf;
+ size_t cap;
+ size_t used;
+ uint8_t *rptr;
+ uint8_t *wptr;
+} usb_rbuf_t;
+
+/**
+ * @brief Initialize the ring buffer.
+ *
+ * The buffer memory must be provided by the caller and must remain valid
+ * for the lifetime of the ring buffer.
+ *
+ * @param b Pointer to the ring buffer structure to initialize.
+ * @param buf Pointer to the buffer memory.
+ * @param buf_size Size of the buffer memory in bytes.
+ */
+void usb_rbuf_init(usb_rbuf_t *b, uint8_t *buf, size_t buf_size);
+
+/**
+ * @brief Reset the ring buffer to an empty state.
+ *
+ * This function clears the contents of the ring buffer and resets
+ * the read and write pointers to the beginning of the buffer.
+ *
+ * @param b Pointer to the ring buffer structure to reset.
+ */
+void usb_rbuf_reset(usb_rbuf_t *b);
+
+/**
+ * @brief Get the number of bytes used in the ring buffer.
+ *
+ * @param b Pointer to the ring buffer structure.
+ * @return Number of bytes used in the ring buffer.
+ */
+size_t usb_rbuf_used_bytes(usb_rbuf_t *b);
+
+/**
+ * @brief Get the number of unused bytes in the ring buffer.
+ *
+ * @param b Pointer to the ring buffer structure.
+ * @return Number of unused bytes in the ring buffer.
+ */
+size_t usb_rbuf_unused_bytes(usb_rbuf_t *b);
+
+/**
+ * @brief Check if the ring buffer is empty.
+ *
+ * @param b Pointer to the ring buffer structure.
+ * @return true if the ring buffer is empty, false otherwise.
+ */
+bool usb_rbuf_is_empty(usb_rbuf_t *b);
+
+/**
+ * @brief Check if the ring buffer is full.
+ *
+ * @param b Pointer to the ring buffer structure.
+ * @return true if the ring buffer is full, false otherwise.
+ */
+bool usb_rbuf_is_full(usb_rbuf_t *b);
+
+/**
+ * @brief Read data from the ring buffer.
+ *
+ * This function reads up to `buf_size` bytes from the ring buffer into
+ * the provided `buf`. The actual number of bytes read is returned.
+ *
+ * @param b Pointer to the ring buffer structure.
+ * @param buf Pointer to the buffer where read data will be stored.
+ * @param buf_size Size of the buffer in bytes.
+ * @return Number of bytes actually read from the ring buffer.
+ */
+size_t usb_rbuf_read(usb_rbuf_t *b, uint8_t *buf, size_t buf_size);
+
+/**
+ * @brief Write data to the ring buffer.
+ *
+ * This function writes up to `data_size` bytes from the provided `data`
+ * buffer into the ring buffer. The actual number of bytes written is returned.
+ *
+ * @param b Pointer to the ring buffer structure.
+ * @param data Pointer to the data to be written to the ring buffer.
+ * @param data_size Size of the data in bytes.
+ * @return Number of bytes actually written to the ring buffer.
+ */
+size_t usb_rbuf_write(usb_rbuf_t *b, const uint8_t *data, size_t data_size);
diff --git a/core/site_scons/models/stm32f4_common.py b/core/site_scons/models/stm32f4_common.py
index 3c6146241..ef49cec9c 100644
--- a/core/site_scons/models/stm32f4_common.py
+++ b/core/site_scons/models/stm32f4_common.py
@@ -130,6 +130,7 @@ def stm32f4_common_files(env, features_wanted, defines, sources, paths):
"embed/io/usb/stm32/usb_class_vcp.c",
"embed/io/usb/stm32/usb_class_webusb.c",
"embed/io/usb/stm32/usb.c",
+ "embed/io/usb/stm32/usb_rbuf.c",
"embed/io/usb/stm32/usbd_conf.c",
"embed/io/usb/stm32/usbd_core.c",
"embed/io/usb/stm32/usbd_ctlreq.c",
diff --git a/core/site_scons/models/stm32u5_common.py b/core/site_scons/models/stm32u5_common.py
index 485d826bb..19b2cf755 100644
--- a/core/site_scons/models/stm32u5_common.py
+++ b/core/site_scons/models/stm32u5_common.py
@@ -159,6 +159,7 @@ def stm32u5_common_files(env, features_wanted, defines, sources, paths):
"embed/io/usb/stm32/usb_class_vcp.c",
"embed/io/usb/stm32/usb_class_webusb.c",
"embed/io/usb/stm32/usb.c",
+ "embed/io/usb/stm32/usb_rbuf.c",
"embed/io/usb/stm32/usbd_conf.c",
"embed/io/usb/stm32/usbd_core.c",
"embed/io/usb/stm32/usbd_ctlreq.c",
Why this scored 48/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.