From 8a2d00f330f9150d0d08cacb6382406eebef692a Mon Sep 17 00:00:00 2001 From: Javid Khan Date: Thu, 2 Jul 2026 21:49:32 +0530 Subject: [PATCH 1/5] bound cdc-data endpoints against descriptor length in acm_open --- src/class/cdc/cdc_host.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/class/cdc/cdc_host.c b/src/class/cdc/cdc_host.c index 30afc2f5c8..902316029b 100644 --- a/src/class/cdc/cdc_host.c +++ b/src/class/cdc/cdc_host.c @@ -1031,6 +1031,7 @@ static uint16_t acm_open(uint8_t daddr, const tusb_desc_interface_t *itf_desc, u // Open notification endpoint of control interface if any if (itf_desc->bNumEndpoints == 1) { + TU_ASSERT(tu_desc_in_bounds(p_desc, desc_end), 0); TU_ASSERT(TUSB_DESC_ENDPOINT == tu_desc_type(p_desc), 0); const tusb_desc_endpoint_t *desc_ep = (const tusb_desc_endpoint_t *)p_desc; TU_ASSERT(tuh_edpt_open(daddr, desc_ep), 0); @@ -1040,12 +1041,13 @@ static uint16_t acm_open(uint8_t daddr, const tusb_desc_interface_t *itf_desc, u } //------------- Data Interface (if any) -------------// - if (TUSB_DESC_INTERFACE == tu_desc_type(p_desc)) { + if (tu_desc_in_bounds(p_desc, desc_end) && TUSB_DESC_INTERFACE == tu_desc_type(p_desc)) { const tusb_desc_interface_t *data_itf = (const tusb_desc_interface_t *)p_desc; if (data_itf->bInterfaceClass == TUSB_CLASS_CDC_DATA) { p_desc = tu_desc_next(p_desc); // next to endpoint descriptor - // data endpoints expected to be in pairs + // data endpoints expected to be in pairs, make sure both fit before reading them + TU_ASSERT((uint16_t)(desc_end - p_desc) >= 2 * sizeof(tusb_desc_endpoint_t), 0); TU_ASSERT(open_ep_stream_pair(p_cdc, (const tusb_desc_endpoint_t *)p_desc), 0); p_desc += data_itf->bNumEndpoints * sizeof(tusb_desc_endpoint_t); } From 28daf9eb87fe4aaa6d2208d565b4bf53ace25730 Mon Sep 17 00:00:00 2001 From: Javid Khan Date: Fri, 3 Jul 2026 22:53:06 +0530 Subject: [PATCH 2/5] guard acm_open endpoint reads against short bLength descriptors --- src/class/cdc/cdc_host.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/class/cdc/cdc_host.c b/src/class/cdc/cdc_host.c index 902316029b..4f02face02 100644 --- a/src/class/cdc/cdc_host.c +++ b/src/class/cdc/cdc_host.c @@ -716,7 +716,9 @@ bool cdch_xfer_cb(uint8_t daddr, uint8_t ep_addr, xfer_result_t event, uint32_t //--------------------------------------------------------------------+ static bool open_ep_stream_pair(cdch_interface_t *p_cdc, tusb_desc_endpoint_t const *desc_ep) { for (size_t i = 0; i < 2; i++) { - TU_ASSERT(TUSB_DESC_ENDPOINT == desc_ep->bDescriptorType && TUSB_XFER_BULK == desc_ep->bmAttributes.xfer, 0); + // pin bLength so tu_desc_next() below cannot walk the second endpoint past a caller-checked bound + TU_ASSERT(sizeof(tusb_desc_endpoint_t) == desc_ep->bLength && + TUSB_DESC_ENDPOINT == desc_ep->bDescriptorType && TUSB_XFER_BULK == desc_ep->bmAttributes.xfer, 0); TU_ASSERT(tuh_edpt_open(p_cdc->daddr, desc_ep)); const uint8_t ep_dir = tu_edpt_dir(desc_ep->bEndpointAddress); tu_edpt_stream_t *stream = (ep_dir == TUSB_DIR_IN) ? &p_cdc->stream.rx : &p_cdc->stream.tx; @@ -1031,7 +1033,8 @@ static uint16_t acm_open(uint8_t daddr, const tusb_desc_interface_t *itf_desc, u // Open notification endpoint of control interface if any if (itf_desc->bNumEndpoints == 1) { - TU_ASSERT(tu_desc_in_bounds(p_desc, desc_end), 0); + // whole endpoint descriptor must fit: tuh_edpt_open reads the full struct regardless of bLength + TU_ASSERT(p_desc + sizeof(tusb_desc_endpoint_t) <= desc_end, 0); TU_ASSERT(TUSB_DESC_ENDPOINT == tu_desc_type(p_desc), 0); const tusb_desc_endpoint_t *desc_ep = (const tusb_desc_endpoint_t *)p_desc; TU_ASSERT(tuh_edpt_open(daddr, desc_ep), 0); @@ -1041,13 +1044,13 @@ static uint16_t acm_open(uint8_t daddr, const tusb_desc_interface_t *itf_desc, u } //------------- Data Interface (if any) -------------// - if (tu_desc_in_bounds(p_desc, desc_end) && TUSB_DESC_INTERFACE == tu_desc_type(p_desc)) { + if (p_desc + sizeof(tusb_desc_interface_t) <= desc_end && TUSB_DESC_INTERFACE == tu_desc_type(p_desc)) { const tusb_desc_interface_t *data_itf = (const tusb_desc_interface_t *)p_desc; if (data_itf->bInterfaceClass == TUSB_CLASS_CDC_DATA) { p_desc = tu_desc_next(p_desc); // next to endpoint descriptor // data endpoints expected to be in pairs, make sure both fit before reading them - TU_ASSERT((uint16_t)(desc_end - p_desc) >= 2 * sizeof(tusb_desc_endpoint_t), 0); + TU_ASSERT(p_desc + 2 * sizeof(tusb_desc_endpoint_t) <= desc_end, 0); TU_ASSERT(open_ep_stream_pair(p_cdc, (const tusb_desc_endpoint_t *)p_desc), 0); p_desc += data_itf->bNumEndpoints * sizeof(tusb_desc_endpoint_t); } From ea39a8f4b12e2fc572931d7da4f3025589c536e7 Mon Sep 17 00:00:00 2001 From: Javid Khan Date: Sat, 4 Jul 2026 10:41:47 +0530 Subject: [PATCH 3/5] advance acm_open descriptor walk by fixed struct sizes --- src/class/cdc/cdc_host.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/class/cdc/cdc_host.c b/src/class/cdc/cdc_host.c index 4f02face02..aeaab1c212 100644 --- a/src/class/cdc/cdc_host.c +++ b/src/class/cdc/cdc_host.c @@ -1040,19 +1040,21 @@ static uint16_t acm_open(uint8_t daddr, const tusb_desc_interface_t *itf_desc, u TU_ASSERT(tuh_edpt_open(daddr, desc_ep), 0); p_cdc->ep_notif = desc_ep->bEndpointAddress; - p_desc = tu_desc_next(p_desc); + // advance by the fixed struct size, not device-supplied bLength, so p_desc stays inside the checked window + p_desc += sizeof(tusb_desc_endpoint_t); } //------------- Data Interface (if any) -------------// if (p_desc + sizeof(tusb_desc_interface_t) <= desc_end && TUSB_DESC_INTERFACE == tu_desc_type(p_desc)) { const tusb_desc_interface_t *data_itf = (const tusb_desc_interface_t *)p_desc; if (data_itf->bInterfaceClass == TUSB_CLASS_CDC_DATA) { - p_desc = tu_desc_next(p_desc); // next to endpoint descriptor + p_desc += sizeof(tusb_desc_interface_t); // fixed struct size to endpoint descriptor, not device bLength - // data endpoints expected to be in pairs, make sure both fit before reading them + // open_ep_stream_pair consumes exactly two endpoints; require that count and that both fit before reading them + TU_ASSERT(data_itf->bNumEndpoints == 2, 0); TU_ASSERT(p_desc + 2 * sizeof(tusb_desc_endpoint_t) <= desc_end, 0); TU_ASSERT(open_ep_stream_pair(p_cdc, (const tusb_desc_endpoint_t *)p_desc), 0); - p_desc += data_itf->bNumEndpoints * sizeof(tusb_desc_endpoint_t); + p_desc += 2 * sizeof(tusb_desc_endpoint_t); } } From eebd0c1c1c04d68a56eac09f56ed16e0ee570a0d Mon Sep 17 00:00:00 2001 From: Javid Khan Date: Sun, 5 Jul 2026 16:26:13 +0530 Subject: [PATCH 4/5] bound acm_open functional descriptor walk against desc_end --- src/class/cdc/cdc_host.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/class/cdc/cdc_host.c b/src/class/cdc/cdc_host.c index aeaab1c212..6045350a88 100644 --- a/src/class/cdc/cdc_host.c +++ b/src/class/cdc/cdc_host.c @@ -1022,8 +1022,11 @@ static uint16_t acm_open(uint8_t daddr, const tusb_desc_interface_t *itf_desc, u p_desc = tu_desc_next(p_desc); // Communication Functional Descriptors - while ((p_desc < desc_end) && (TUSB_DESC_CS_INTERFACE == tu_desc_type(p_desc))) { - if (CDC_FUNC_DESC_ABSTRACT_CONTROL_MANAGEMENT == cdc_functional_desc_typeof(p_desc)) { + // need the 3-byte header (bLength/bDescriptorType/bDescriptorSubType) in bounds before reading it, and a + // bLength >= 3 both keeps those reads valid and stops a zero-length descriptor from spinning the walk + while (p_desc + 3 <= desc_end && TUSB_DESC_CS_INTERFACE == tu_desc_type(p_desc) && tu_desc_len(p_desc) >= 3) { + if (CDC_FUNC_DESC_ABSTRACT_CONTROL_MANAGEMENT == cdc_functional_desc_typeof(p_desc) && + p_desc + sizeof(cdc_desc_func_acm_t) <= desc_end) { // save ACM bmCapabilities p_cdc->acm.capability = ((cdc_desc_func_acm_t const *) p_desc)->bmCapabilities; } From be8e6990cee5f7c2d5f09fc8494a1159c5c7c5a7 Mon Sep 17 00:00:00 2001 From: Javid Khan Date: Tue, 7 Jul 2026 05:26:03 +0530 Subject: [PATCH 5/5] gate acm_open descriptor bounds behind CFG_TUH_VALIDATION_LEVEL add a three-level TUSB_VALIDATION_NONE/BASIC/STRICT knob and default CFG_TUH_VALIDATION_LEVEL to BASIC, then make acm_open the first user so the enumeration bounds checks compile out at NONE for trusted-device setups and stay on by default. --- src/class/cdc/cdc_host.c | 28 ++++++++++++++++++++-------- src/tusb_option.h | 13 +++++++++++++ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/class/cdc/cdc_host.c b/src/class/cdc/cdc_host.c index 6045350a88..d1cbc2924b 100644 --- a/src/class/cdc/cdc_host.c +++ b/src/class/cdc/cdc_host.c @@ -714,11 +714,21 @@ bool cdch_xfer_cb(uint8_t daddr, uint8_t ep_addr, xfer_result_t event, uint32_t //--------------------------------------------------------------------+ // Enumeration //--------------------------------------------------------------------+ + + // Descriptor-walk hardening gated by CFG_TUH_VALIDATION_LEVEL: at NONE the guards collapse to a pass so + // trusted-device setups pay no code size; at BASIC (default) the walk stays inside the enumeration buffer. + #if CFG_TUH_VALIDATION_LEVEL >= TUSB_VALIDATION_BASIC + #define TU_DESC_VALIDATE(_cond) (_cond) + #else + #define TU_DESC_VALIDATE(_cond) (true) + #endif + static bool open_ep_stream_pair(cdch_interface_t *p_cdc, tusb_desc_endpoint_t const *desc_ep) { for (size_t i = 0; i < 2; i++) { // pin bLength so tu_desc_next() below cannot walk the second endpoint past a caller-checked bound - TU_ASSERT(sizeof(tusb_desc_endpoint_t) == desc_ep->bLength && - TUSB_DESC_ENDPOINT == desc_ep->bDescriptorType && TUSB_XFER_BULK == desc_ep->bmAttributes.xfer, 0); + TU_ASSERT(TU_DESC_VALIDATE(sizeof(tusb_desc_endpoint_t) == desc_ep->bLength) && + TUSB_DESC_ENDPOINT == desc_ep->bDescriptorType && TUSB_XFER_BULK == desc_ep->bmAttributes.xfer, + 0); TU_ASSERT(tuh_edpt_open(p_cdc->daddr, desc_ep)); const uint8_t ep_dir = tu_edpt_dir(desc_ep->bEndpointAddress); tu_edpt_stream_t *stream = (ep_dir == TUSB_DIR_IN) ? &p_cdc->stream.rx : &p_cdc->stream.tx; @@ -1024,9 +1034,10 @@ static uint16_t acm_open(uint8_t daddr, const tusb_desc_interface_t *itf_desc, u // Communication Functional Descriptors // need the 3-byte header (bLength/bDescriptorType/bDescriptorSubType) in bounds before reading it, and a // bLength >= 3 both keeps those reads valid and stops a zero-length descriptor from spinning the walk - while (p_desc + 3 <= desc_end && TUSB_DESC_CS_INTERFACE == tu_desc_type(p_desc) && tu_desc_len(p_desc) >= 3) { + while ((p_desc < desc_end) && TU_DESC_VALIDATE(p_desc + 3 <= desc_end) && + TUSB_DESC_CS_INTERFACE == tu_desc_type(p_desc) && TU_DESC_VALIDATE(tu_desc_len(p_desc) >= 3)) { if (CDC_FUNC_DESC_ABSTRACT_CONTROL_MANAGEMENT == cdc_functional_desc_typeof(p_desc) && - p_desc + sizeof(cdc_desc_func_acm_t) <= desc_end) { + TU_DESC_VALIDATE(p_desc + sizeof(cdc_desc_func_acm_t) <= desc_end)) { // save ACM bmCapabilities p_cdc->acm.capability = ((cdc_desc_func_acm_t const *) p_desc)->bmCapabilities; } @@ -1037,7 +1048,7 @@ static uint16_t acm_open(uint8_t daddr, const tusb_desc_interface_t *itf_desc, u // Open notification endpoint of control interface if any if (itf_desc->bNumEndpoints == 1) { // whole endpoint descriptor must fit: tuh_edpt_open reads the full struct regardless of bLength - TU_ASSERT(p_desc + sizeof(tusb_desc_endpoint_t) <= desc_end, 0); + TU_ASSERT(TU_DESC_VALIDATE(p_desc + sizeof(tusb_desc_endpoint_t) <= desc_end), 0); TU_ASSERT(TUSB_DESC_ENDPOINT == tu_desc_type(p_desc), 0); const tusb_desc_endpoint_t *desc_ep = (const tusb_desc_endpoint_t *)p_desc; TU_ASSERT(tuh_edpt_open(daddr, desc_ep), 0); @@ -1048,14 +1059,15 @@ static uint16_t acm_open(uint8_t daddr, const tusb_desc_interface_t *itf_desc, u } //------------- Data Interface (if any) -------------// - if (p_desc + sizeof(tusb_desc_interface_t) <= desc_end && TUSB_DESC_INTERFACE == tu_desc_type(p_desc)) { + if (TU_DESC_VALIDATE(p_desc + sizeof(tusb_desc_interface_t) <= desc_end) && + TUSB_DESC_INTERFACE == tu_desc_type(p_desc)) { const tusb_desc_interface_t *data_itf = (const tusb_desc_interface_t *)p_desc; if (data_itf->bInterfaceClass == TUSB_CLASS_CDC_DATA) { p_desc += sizeof(tusb_desc_interface_t); // fixed struct size to endpoint descriptor, not device bLength // open_ep_stream_pair consumes exactly two endpoints; require that count and that both fit before reading them - TU_ASSERT(data_itf->bNumEndpoints == 2, 0); - TU_ASSERT(p_desc + 2 * sizeof(tusb_desc_endpoint_t) <= desc_end, 0); + TU_ASSERT(TU_DESC_VALIDATE(data_itf->bNumEndpoints == 2), 0); + TU_ASSERT(TU_DESC_VALIDATE(p_desc + 2 * sizeof(tusb_desc_endpoint_t) <= desc_end), 0); TU_ASSERT(open_ep_stream_pair(p_cdc, (const tusb_desc_endpoint_t *)p_desc), 0); p_desc += 2 * sizeof(tusb_desc_endpoint_t); } diff --git a/src/tusb_option.h b/src/tusb_option.h index e19ee1629d..de8439cfba 100644 --- a/src/tusb_option.h +++ b/src/tusb_option.h @@ -239,6 +239,15 @@ #define OPT_MODE_HIGH_SPEED 0x0400u ///< High Speed #define OPT_MODE_SPEED_MASK 0xff00u +//--------------------------------------------------------------------+ +// Descriptor Validation Level +// How much the stack hardens itself against mal-configured or hostile devices, traded against code size. +// Higher levels add more checks; set CFG_TUD_VALIDATION_LEVEL / CFG_TUH_VALIDATION_LEVEL to pick one. +//--------------------------------------------------------------------+ +#define TUSB_VALIDATION_NONE 0 ///< trusted devices only, minimal code size +#define TUSB_VALIDATION_BASIC 1 ///< default: mal-configured devices, no OOB reads / zero-length loops +#define TUSB_VALIDATION_STRICT 2 ///< reject malformed/hostile descriptors, stricter class validation + //--------------------------------------------------------------------+ // Include tusb_config.h //--------------------------------------------------------------------+ @@ -694,6 +703,10 @@ #ifndef CFG_TUH_ENUMERATION_BUFSIZE #define CFG_TUH_ENUMERATION_BUFSIZE 256 #endif + + #ifndef CFG_TUH_VALIDATION_LEVEL + #define CFG_TUH_VALIDATION_LEVEL TUSB_VALIDATION_BASIC + #endif #endif // CFG_TUH_ENABLED // Attribute to place data in accessible RAM for host controller (default: CFG_TUSB_MEM_SECTION)