Hi, The Red Hat entry is still not public, but the CVE description is specific enough to find this in the source. Upstream's main branch has no fix: usbredirhost.c there is identical to 0.15.0, and its last commit is from March 2026. A patch is attached.
>From reading usbredirhost/usbredirhost.c: For an out stream, usbredirhost_start_stream_unlocked() does count /= 2 and submits that many transfers, then sets stream_started. With transfer_count 1 that submits nothing. usbredirhost_iso_packet() starts the stream when available == needed, where needed is pkts_per_transfer * transfer_count / 2. With pkts_per_transfer 1 and transfer_count 1, needed is 0, and available is 0 right after the first packet because i and j wrapped. So the stream is marked started while its only transfer is full and unsubmitted. Later packets see stream_started, but packet_idx has moved past pkts_per_transfer, so the equality check that submits a full transfer never matches. j keeps growing, and both the memcpy to libusb_get_iso_packet_buffer() and the write to iso_packet_desc[j].length go out of bounds, under the control of the usbredir peer. So by my reading the trigger is narrower than the CVE text: it also needs pkts_per_transfer 1. With transfer_count 1 and pkts_per_transfer 2 or more, the stream starts halfway through the transfer and the transfer is still submitted when full. usbredirhost_alloc_stream_unlocked() accepts both values of 1. Out endpoints only. The patch does two things. It moves the check that submits a full transfer so it also runs right after the stream is started, and it treats an out-of-range packet_idx as a queue overflow. A failed start clears stream_started, so the new check does not touch a freed transfer. The patch applies to 0.15.0-1 (trixie/forky/sid), 0.13.0-2 (bookworm) and 0.8.0-1 (bullseye); on 0.8.0-1 one hunk has an offset. None of these has a patch in debian/patches. What I checked: the patched file compiles without warnings on all three versions, against libusb 1.0.30 headers and an empty config. I then ran the real usbredirhost_iso_packet() before and after the patch, under ASan and UBSan, in a harness with a stubbed libusb_submit_transfer() and modelled transfer completion. Ten valid configurations gave identical transfer submission order on all three versions, with no sanitizer reports: packets-per-transfer/transfer-count of 8/4, 8/2, 1/2, 1/4, 3/3, 2/1, 4/1, 16/10, 32/5 and 5/1. What I did not test: I deliberately did not run the 1/1 configuration, so the out-of-bounds write is not reproduced and the fix is not demonstrated on it. I did not build the full package, run its tests, or use a real device or guest. The harness models completion, so it says nothing about real USB timing. I have not sent this upstream. Regards, Sumit Verma
Description: usbredirhost: submit a full iso out transfer when none was started usbredirhost_start_stream_unlocked() submits only half of the transfers of an out stream, which is none when the stream has a single transfer. With one packet per transfer as well, the stream is marked started with its only transfer full but unsubmitted. packet_idx then grows past the descriptor array, so usbredirhost_iso_packet() writes out of bounds on later packets. . Submit a full transfer after starting the stream, and treat an out of range packet_idx as a queue overflow. Author: Sumit Verma <[email protected]> Bug-Debian: 1148831 Forwarded: no --- --- a/usbredirhost/usbredirhost.c +++ b/usbredirhost/usbredirhost.c @@ -2393,7 +2393,8 @@ i = host->endpoint[EP2I(ep)].out_idx; transfer = host->endpoint[EP2I(ep)].transfer[i]; j = transfer->packet_idx; - if (j == SUBMITTED_IDX) { + if (j == SUBMITTED_IDX || + j >= host->endpoint[EP2I(ep)].pkts_per_transfer) { DEBUG("overflow of iso out queue on ep: %02X, dropping packet", ep); /* Since we're interupting the stream anyways, drop enough packets to get back to our target buffer size */ @@ -2421,12 +2422,7 @@ j = 0; } - if (host->endpoint[EP2I(ep)].stream_started) { - if (transfer->packet_idx == - host->endpoint[EP2I(ep)].pkts_per_transfer) { - usbredirhost_submit_stream_transfer_unlocked(host, transfer); - } - } else { + if (!host->endpoint[EP2I(ep)].stream_started) { /* We've not started the stream (submitted some transfers) yet, do so once we have half our buffers filled */ int available = i * host->endpoint[EP2I(ep)].pkts_per_transfer + j; @@ -2438,6 +2434,15 @@ } } + /* Starting the stream submits only half of the transfers. With a single + transfer that is none, so a full transfer must be submitted here too; + a transfer submitted by the start no longer has a matching packet_idx */ + if (host->endpoint[EP2I(ep)].stream_started && + transfer->packet_idx == + host->endpoint[EP2I(ep)].pkts_per_transfer) { + usbredirhost_submit_stream_transfer_unlocked(host, transfer); + } + leave: UNLOCK(host); usbredirparser_free_packet_data(host->parser, data);

