VYPR
Medium severity4.0NVD Advisory· Published May 27, 2026

CVE-2026-47104

CVE-2026-47104

Description

libusb before version 1.0.30 contains a one-byte out-of-bounds read vulnerability in parse_iad_array() in descriptor.c that allows attackers to trigger a denial of service by supplying a malformed USB descriptor whose bLength equals size minus one, causing the bounds check to use the original buffer size instead of the remaining size. Attackers in virtualized environments with USB passthrough can supply crafted descriptors through libusb_get_active_interface_association_descriptors or libusb_get_interface_association_descriptors to read one byte past the end of the malloc allocation, resulting in a denial of service.

AI Insight

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

A one-byte out-of-bounds read in libusb's parse_iad_array() function allows denial of service via malformed USB descriptors in virtualized environments.

Vulnerability

In libusb versions before 1.0.30, the parse_iad_array() function in descriptor.c contains a one-byte out-of-bounds read vulnerability. The first-pass loop compares header.bLength against the original size argument instead of the remaining bytes, so a descriptor with bLength equal to size - 1 causes the next iteration to read one byte past the end of the malloc allocation [1][2]. This code path is reachable from the public APIs libusb_get_active_interface_association_descriptors and libusb_get_interface_association_descriptors [2].

Exploitation

An attacker in a virtualized environment with USB passthrough can supply a crafted USB descriptor to the host system. No authentication or user interaction is required; the malformed descriptor is parsed automatically when the device is enumerated. A minimal reproducer of 9 bytes of descriptor data triggers the out-of-bounds read [1][2].

Impact

Successful exploitation results in a denial of service (DoS) due to reading one byte beyond the allocated buffer. This can cause a crash or hang of the application using libusb, potentially affecting the entire virtualized environment if the host service is critical. No code execution or privilege escalation is indicated [1][2].

Mitigation

The vulnerability is fixed in libusb version 1.0.30, released on April 25, 2026 [2][4]. Users should upgrade to this version or later. No workaround is available for earlier versions. The issue was discovered via fuzzing and is not known to be exploited in the wild.

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

2
  • Libusb/Libusbreferences2 versions
    (expand)+ 1 more
    • (no CPE)
    • (no CPE)range: <1.0.30

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

"Missing remaining-size check in parse_iad_array() bounds validation allows one-byte out-of-bounds read."

Attack vector

An attacker in a virtualized environment with USB passthrough supplies a malformed USB descriptor whose `bLength` equals `size - 1`. The loop in `parse_iad_array()` consumes that descriptor, then re-enters the `while (consumed

Affected code

The vulnerability is in `parse_iad_array()` in `libusb/descriptor.c` around line 1381 [patch_id=2659673]. The bounds check `header.bLength > size` uses the original buffer size instead of the remaining bytes (`size - consumed`), which is the same convention already used by `parse_configuration` and `parse_interface` in the same file [ref_id=1].

What the fix does

The patch in `libusb/descriptor.c` changes the bounds check from `header.bLength > size` to `header.bLength > size - consumed` and adds a guard `while (size - consumed >= DESC_HEADER_LENGTH)` [patch_id=2659673]. This ensures that each iteration checks against the remaining buffer length rather than the original total, preventing the loop from reading past the allocation. The fix also corrects the warning message to report the remaining size instead of the original size [ref_id=1].

Preconditions

  • networkAttacker must be able to supply a malformed USB descriptor to the affected libusb APIs, e.g. via USB passthrough in a virtualized environment
  • inputThe malformed descriptor must have bLength equal to size - 1 to trigger the OOB read on the next loop iteration

Reproduction

Use the supplied 9-byte input `08 00 00 00 00 00 00 00 ff` with a unity-include harness that calls `parse_iad_array()` directly, as shown in the researcher's `verify.c` [ref_id=1]. Build with AddressSanitizer enabled; ASan will report a heap-buffer-overflow read of size 1 at one byte past the allocation.

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.