VYPR
Medium severity6.2NVD Advisory· Published May 27, 2026

CVE-2026-23679

CVE-2026-23679

Description

libusb before version 1.0.30 contains a NULL pointer dereference vulnerability that allows attackers to crash applications by supplying a malformed USB configuration descriptor where an interface claims bNumEndpoints greater than zero but is followed by a class-specific descriptor whose bLength exceeds the remaining buffer size, causing parse_interface() to return early without allocating the endpoint array. Attackers can exploit this flaw through libusb_get_active_config_descriptor or libusb_get_config_descriptor by providing crafted descriptors via virtualized USB passthrough, file-based descriptor parsing, or network sources, causing any application iterating over endpoints to dereference a NULL endpoint pointer and crash.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

A NULL pointer dereference in libusb before 1.0.30 allows attackers to crash applications by supplying a malformed USB configuration descriptor via device or file input.

Vulnerability

A NULL pointer dereference vulnerability exists in libusb before version 1.0.30 in the parse_interface() function (file descriptor.c) [1]. When parsing a malformed USB configuration descriptor, if an interface descriptor claims bNumEndpoints > 0 but is followed by a class-specific descriptor whose bLength exceeds the remaining buffer size, the early return inside the inner loop occurs before the endpoint array is allocated. This leaves ifp->endpoint as NULL while bNumEndpoints remains non-zero [1][2]. The issue is reachable via libusb_get_active_config_descriptor() or libusb_get_config_descriptor() when supplied crafted descriptor data from an attached USB device, virtualized passthrough, file input, or network sources [1].

Exploitation

An attacker needs the ability to supply a malformed USB configuration descriptor to a target application that uses libusb to enumerate device endpoints [1]. This can be achieved via physical USB device connection, virtual USB device passthrough, or by providing a crafted descriptor file that the application parses [1]. No authentication or user interaction beyond the application processing the descriptor is required. The attack triggers the vulnerability when the application iterates over the endpoint array, dereferencing the NULL endpoint pointer, leading to an immediate crash (denial of service) [1][2].

Impact

Successful exploitation causes the application to dereference a NULL pointer and crash (denial of service) [1]. The impact is limited to availability; confidentiality and integrity are not affected. The crash occurs in user-space, so the system remains operational, but the target application terminates unexpectedly. No privilege escalation or code execution is reported [1][2].

Mitigation

The vulnerability is fixed in libusb version 1.0.30, released on 2026-05-27 [3][4]. The fix resets bNumEndpoints to 0 before the early return in parse_interface(), maintaining the invariant that endpoint is non-NULL only when bNumEndpoints > 0 [2]. Users should update to libusb 1.0.30 or later. No workaround is available for earlier versions; applications must not parse descriptors from untrusted sources without patching [1][4].

AI Insight generated on May 27, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

1

Patches

2
578ab76b4c43

tests/fuzz: Add descriptor parser fuzzer with regression seeds

https://github.com/libusb/libusbMarkLee131Apr 25, 2026via nvd-ref
5 files changed · +84 1
  • libusb/version_nano.h+1 1 modified
    @@ -1 +1 @@
    -#define LIBUSB_NANO 12028
    +#define LIBUSB_NANO 12029
    
  • tests/fuzz/corpus/descriptor_parsers/min_valid_config.bin+0 0 added
  • tests/fuzz/corpus/descriptor_parsers/regression_bug_a_endpoint_null.bin+0 0 added
  • tests/fuzz/corpus/descriptor_parsers/regression_bug_b_iad_oob.bin+0 0 added
  • tests/fuzz/fuzz_descriptor_parsers.c+83 0 added
    @@ -0,0 +1,83 @@
    +/* Fuzz the static descriptor parsers in libusb/descriptor.c.
    + *
    + * The relevant entry points (parse_configuration, parse_interface,
    + * parse_endpoint, parse_iad_array) all have file-local linkage, so we
    + * compile them into this fuzzer's translation unit by including the source
    + * file directly. The libusb_get_*() public APIs that wrap them require a
    + * libusb_device with a backend, which is more setup than a fuzz target
    + * needs. The unity-include keeps the fuzzer focused on parser logic only.
    + *
    + * Three small symbol stubs satisfy references that descriptor.c makes into
    + * other libusb translation units; none of those code paths are reachable
    + * from the parsers we exercise here.
    + *
    + * Built as part of the OSS-Fuzz integration; not exercised by the autotools
    + * test suite.
    + */
    +
    +#include <stdint.h>
    +#include <stddef.h>
    +#include <stdlib.h>
    +#include <string.h>
    +
    +/* ENABLE_LOGGING affects only printf-like macros we don't care about. */
    +#define ENABLE_LOGGING 0
    +
    +#include "config.h"
    +#include "libusbi.h"
    +#include "../../libusb/descriptor.c"
    +
    +/* Stubs for symbols that descriptor.c references but the parsers we fuzz
    + * never reach (logging sink, backend dispatch, control-transfer wrapper). */
    +const struct usbi_os_backend usbi_backend = {0};
    +void usbi_log(struct libusb_context *ctx, enum libusb_log_level level,
    +              const char *function, const char *format, ...) {
    +    (void)ctx; (void)level; (void)function; (void)format;
    +}
    +int libusb_control_transfer(libusb_device_handle *dev_handle,
    +                            uint8_t bmRequestType, uint8_t bRequest,
    +                            uint16_t wValue, uint16_t wIndex,
    +                            unsigned char *data, uint16_t wLength,
    +                            unsigned int timeout) {
    +    (void)dev_handle; (void)bmRequestType; (void)bRequest; (void)wValue;
    +    (void)wIndex; (void)data; (void)wLength; (void)timeout;
    +    return LIBUSB_ERROR_NOT_SUPPORTED;
    +}
    +
    +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    +    /* The limit of 8192 is comfortably above practical cases and
    +     * still keeping libFuzzer iterations fast and the corpus small */
    +    if (size < LIBUSB_DT_CONFIG_SIZE || size > 8192)
    +        return 0;
    +
    +    /* Every call gets a fresh exact-size copy so any byte read past the
    +     * end is caught by ASan. The fuzzer must not observe state between
    +     * iterations. */
    +    uint8_t *buf;
    +
    +    /* (1) parse_configuration -> parse_interface -> parse_endpoint */
    +    buf = malloc(size);
    +    if (!buf) return 0;
    +    memcpy(buf, data, size);
    +    {
    +        struct libusb_config_descriptor cfg;
    +        memset(&cfg, 0, sizeof(cfg));
    +        if (parse_configuration(NULL, &cfg, buf, (int)size) >= 0)
    +            clear_configuration(&cfg);
    +    }
    +    free(buf);
    +
    +    /* (2) parse_iad_array */
    +    buf = malloc(size);
    +    if (!buf) return 0;
    +    memcpy(buf, data, size);
    +    {
    +        struct libusb_interface_association_descriptor_array iad;
    +        memset(&iad, 0, sizeof(iad));
    +        if (parse_iad_array(NULL, &iad, buf, (int)size) == LIBUSB_SUCCESS)
    +            free((void *)iad.iad);
    +    }
    +    free(buf);
    +
    +    return 0;
    +}
    
440af4e7fc98

Merge 28f0c3d83440bc5ddd6c27f7d60f885a09f0eb4f into 9ffcb458abb311edec73bab59bda88c1aa4a1ea8

https://github.com/libusb/libusbMarkLee131Apr 28, 2026via nvd-ref
5 files changed · +88 3
  • libusb/descriptor.c+7 3 modified
    @@ -246,6 +246,10 @@ static int parse_interface(libusb_context *ctx,
     				usbi_warn(ctx,
     					  "short extra intf desc read %d/%u",
     					  size, header->bLength);
    +				/* Keep the invariant: bNumEndpoints > 0 implies
    +				 * endpoint != NULL. The endpoint array isn't
    +				 * allocated yet on this early return. */
    +				ifp->bNumEndpoints = 0;
     				return parsed;
     			}
     
    @@ -1376,17 +1380,17 @@ static int parse_iad_array(struct libusb_context *ctx,
     
     	/* First pass: Iterate through desc list, count number of IADs */
     	iad_array->length = 0;
    -	while (consumed < size) {
    +	while (size - consumed >= DESC_HEADER_LENGTH) {
     		header.bLength = buf[0];
     		header.bDescriptorType = buf[1];
     		if (header.bLength < DESC_HEADER_LENGTH) {
     			usbi_err(ctx, "invalid descriptor bLength %d",
     				 header.bLength);
     			return LIBUSB_ERROR_IO;
     		}
    -		else if (header.bLength > size) {
    +		else if (header.bLength > size - consumed) {
     			usbi_warn(ctx, "short config descriptor read %d/%u",
    -					  size, header.bLength);
    +					  size - consumed, header.bLength);
     			return LIBUSB_ERROR_IO;
     		}
     		if (header.bDescriptorType == LIBUSB_DT_INTERFACE_ASSOCIATION)
    
  • tests/fuzz/corpus/descriptor_parsers/min_valid_config.bin+0 0 added
  • tests/fuzz/corpus/descriptor_parsers/regression_bug_a_endpoint_null.bin+0 0 added
  • tests/fuzz/corpus/descriptor_parsers/regression_bug_b_iad_oob.bin+0 0 added
  • tests/fuzz/fuzz_descriptor_parsers.c+81 0 added
    @@ -0,0 +1,81 @@
    +/* Fuzz the static descriptor parsers in libusb/descriptor.c.
    + *
    + * The relevant entry points (parse_configuration, parse_interface,
    + * parse_endpoint, parse_iad_array) all have file-local linkage, so we
    + * compile them into this fuzzer's translation unit by including the source
    + * file directly. The libusb_get_*() public APIs that wrap them require a
    + * libusb_device with a backend, which is more setup than a fuzz target
    + * needs. The unity-include keeps the fuzzer focused on parser logic only.
    + *
    + * Three small symbol stubs satisfy references that descriptor.c makes into
    + * other libusb translation units; none of those code paths are reachable
    + * from the parsers we exercise here.
    + *
    + * Built as part of the OSS-Fuzz integration; not exercised by the autotools
    + * test suite.
    + */
    +
    +#include <stdint.h>
    +#include <stddef.h>
    +#include <stdlib.h>
    +#include <string.h>
    +
    +/* ENABLE_LOGGING affects only printf-like macros we don't care about. */
    +#define ENABLE_LOGGING 0
    +
    +#include "config.h"
    +#include "libusbi.h"
    +#include "../../libusb/descriptor.c"
    +
    +/* Stubs for symbols that descriptor.c references but the parsers we fuzz
    + * never reach (logging sink, backend dispatch, control-transfer wrapper). */
    +const struct usbi_os_backend usbi_backend = {0};
    +void usbi_log(struct libusb_context *ctx, enum libusb_log_level level,
    +              const char *function, const char *format, ...) {
    +    (void)ctx; (void)level; (void)function; (void)format;
    +}
    +int libusb_control_transfer(libusb_device_handle *dev_handle,
    +                            uint8_t bmRequestType, uint8_t bRequest,
    +                            uint16_t wValue, uint16_t wIndex,
    +                            unsigned char *data, uint16_t wLength,
    +                            unsigned int timeout) {
    +    (void)dev_handle; (void)bmRequestType; (void)bRequest; (void)wValue;
    +    (void)wIndex; (void)data; (void)wLength; (void)timeout;
    +    return LIBUSB_ERROR_NOT_SUPPORTED;
    +}
    +
    +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    +    if (size < LIBUSB_DT_CONFIG_SIZE || size > 8192)
    +        return 0;
    +
    +    /* Every call gets a fresh exact-size copy so any byte read past the
    +     * end is caught by ASan. The fuzzer must not observe state between
    +     * iterations. */
    +    uint8_t *buf;
    +
    +    /* (1) parse_configuration -> parse_interface -> parse_endpoint */
    +    buf = malloc(size);
    +    if (!buf) return 0;
    +    memcpy(buf, data, size);
    +    {
    +        struct libusb_config_descriptor cfg;
    +        memset(&cfg, 0, sizeof(cfg));
    +        if (parse_configuration(NULL, &cfg, buf, (int)size) >= 0)
    +            clear_configuration(&cfg);
    +    }
    +    free(buf);
    +
    +    /* (2) parse_iad_array */
    +    buf = malloc(size);
    +    if (!buf) return 0;
    +    memcpy(buf, data, size);
    +    {
    +        struct libusb_interface_association_descriptor_array iad;
    +        memset(&iad, 0, sizeof(iad));
    +        if (parse_iad_array(NULL, &iad, buf, (int)size) == LIBUSB_SUCCESS)
    +            free((void *)iad.iad);
    +    }
    +    free(buf);
    +
    +    return 0;
    +}
    

Vulnerability mechanics

Root cause

"In `parse_interface()`, when a class-specific descriptor's `bLength` exceeds the remaining buffer size, the function returns early without allocating the endpoint array, leaving `bNumEndpoints > 0` but `endpoint == NULL`."

Attack vector

An attacker supplies a malformed USB configuration descriptor where an interface descriptor claims `bNumEndpoints > 0` but is followed by a class-specific descriptor whose `bLength` exceeds the remaining buffer size. This causes `parse_interface()` to return early without allocating the endpoint array [ref_id=1]. The crafted descriptor can be delivered through a virtualized USB device (passthrough), file-based descriptor parsing, or any network source that feeds descriptors into `libusb_get_active_config_descriptor()` or `libusb_get_config_descriptor()` [CVE description]. Any application that iterates over the endpoint array (e.g., `examples/testlibusb.c`, `examples/xusb.c`) will dereference a NULL pointer and crash [ref_id=1].

Affected code

The vulnerability is in `libusb/descriptor.c` in the `parse_interface()` function, around line 245 [ref_id=1]. The early-return path inside the "skip extra/class descriptors" loop fails to reset `ifp->bNumEndpoints` to zero before returning, leaving the endpoint pointer NULL while `bNumEndpoints` remains non-zero [ref_id=1].

What the fix does

The patch [patch_id=2662161] adds a single line in `libusb/descriptor.c` inside the early-return path of `parse_interface()`: `ifp->bNumEndpoints = 0;` [ref_id=1]. This enforces the invariant that `bNumEndpoints > 0` implies `endpoint != NULL`, so callers that iterate over endpoints will see zero endpoints and safely skip the loop. The same commit also fixes an out-of-bounds read in `parse_iad_array()` by changing the bounds check from `header.bLength > size` to `header.bLength > size - consumed` [ref_id=1]. Additionally, a new fuzz harness (`fuzz_descriptor_parsers.c`) was added in [patch_id=2662160] to exercise these parsers directly via OSS-Fuzz.

Preconditions

  • inputAttacker must be able to supply a malformed USB configuration descriptor to the target application via virtualized USB passthrough, file-based descriptor parsing, or network sources.
  • configThe target application must call libusb_get_active_config_descriptor() or libusb_get_config_descriptor() and then iterate over the endpoint array.

Reproduction

Use the 20-byte reproducer from [ref_id=1]: `09 02 19 00 01 01 00 a0 00` (config descriptor), `09 04 00 00 01 ff 00 00 00` (interface descriptor with `bNumEndpoints=1`), `07 05` (truncated extra descriptor claiming `bLength=7`). Compile the `verify.c` unity-include harness from [ref_id=1] against libusb's own `descriptor.c`, run it, and observe `bNumEndpoints=1 endpoint=0x0` printed. A real application looping over endpoints would SEGV [ref_id=1].

Generated on May 27, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.