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);

Reply via email to