ucom_get_data TX path passes unclamped request length to clist_qtob β latent heap OOB write past USB page-cache segment (consumer-side defect found in tty_subr.c pass-2 API sweep)
| Field | Value |
|---|---|
| ID | DF-2959 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-787 / CWE-805 |
| File | sys/bus/u4b/serial/usb_serial.c |
| Lines | 1898-1906 (sink: tty_subr.c:127-149) |
| Area | bus/u4b + kern/tty |
| Confidence | likely |
| Discovered | 2026-09-02 |
| Pass | 2 (GLM 5.3 second pass β tty_subr.c row) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
ucom_get_data clamps the contiguous DMA segment returned by usbd_get_page to res.length (:1901) and then ignores the clamp, calling clist_qtob(&tp->t_outq, res.buffer, len) with the full driver buffer size (len, e.g. UPLCOM_BULK_BUF_SIZE). clist_qtob copies min(len, t_outq.c_cc) bytes β up to len bytes into a segment that usbd_get_page may size as low as USB_PAGE_SIZE β offset == 1 byte (multiseg cache), or that may be a standalone per-page buffer. Sibling RX path uses res.length correctly (buf=res.buffer; cnt=res.length), proving the TX line is a defect. Local user with write access to a USB serial /dev/ttyU* on a system where the TX usb_page_cache is multi-segment or non-KVA: heap OOB write of attacker-supplied tty data (content fully chosen via prior write()), length up to ~1KB per TX callback, repeatable β a classic heap-corruption primitive with privesc potential. Masked in the common x86 single-segment configuration (res.length=(usb_size_t)-1 so clamp==len) and hardware-gated (needs a USB serial adapter attached); the audit guest has no USB devices β documented honestly as latent/untested. Fix: pass res.length (row diff, mirrors the RX path).
Timeline
- 2026-09-02 Discovered during pass-2 audit of tty_subr.c (GLM 5.3); API-sweep consumer defect, hardware-gated.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2959 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.0 KB | β raw | |
| VERDICT.md | β | 4.9 KB | β raw | |
| verdict.json | β | 2.9 KB | view raw | |
| fix.diff | β | 551 B | view raw |
DF-2959 β clist_qtob called with unclamped len in ucom_get_data TX path (heap OOB write, latent)
What
sys/bus/u4b/serial/usb_serial.c ucom_get_data() (the ttyβUSB data pump):
1898 usbd_get_page(pc, offset, &res);
1899
1900 /* Buffer bigger than max requested data */
1901 if (res.length > len) {
1902 res.length = len; /* segment clamped to request */
1903 }
1904 /* copy data directly into USB buffer */
1905 SET(tp->t_state, TS_BUSY);
1906 cnt = clist_qtob(&tp->t_outq, res.buffer, len); /* BUG: len, not res.length */
usbd_get_page() (sys/bus/u4b/usb_busdma.c:80-125) returns the contiguous
segment starting at offset:
- single-segment DMA buffer (
ismultiseg == 0):res.length = (usb_size_t)-1β after the clamp it equalslenβ harmless; - multi-segment / page-scattered cache (
ismultiseg != 0):res.length = USB_PAGE_SIZE - (offset % USB_PAGE_SIZE)β as short as 1 byte; - Case 1b (
pc->buffer == NULL):res.buffer = page->buffer + offsetβ a per-USB-page buffer allocation, NOT part of one large KVA mapping.
clist_qtob() (sys/kern/tty_subr.c:127-149) copies n = min(len, c_cc)
bytes of tty output data to res.buffer and returns the count. Passing the
uncapped len (the driver's whole bulk-out buffer size, e.g.
UPLCOM_BULK_BUF_SIZE at uplcom.c:838) instead of the clamped
res.length makes the copy run past the contiguous segment whenever
t_outq.c_cc > res.length β writing attacker-supplied tty bytes (whatever
the user write()d to /dev/ttyU*) past the end of a USB page buffer
(heap OOB write, length up to len - res.length, content and timing
attacker-controlled). The RX twin in the same file (usb_serial.c:2004-2032)
does it correctly: buf = res.buffer; cnt = res.length; clist_btoq(buf, cnt, β¦),
confirming the TX line is a defect, not a design choice.
Reachability (honest)
- Requires a USB serial adapter attached and its /dev/ttyU* open (driver runs ucom_get_data from the bulk-out TX callback). No privileged ioctl is involved β any user able to write the device node triggers the copy.
- In the common x86 configuration the serial drivers allocate a
single-segment (β€1 page) bulk buffer β
ismultiseg == 0βres.length = -1β clamp makesres.length == lenβ bug masked. The overflow needs a multi-segment (page-scattered / multi-page) TX page cache or a Case-1b (non-KVA) buffer, which the stock serial drivers do not normally produce. Treated as a latent heap-overflow / correctness defect (the clamp exists and is then ignored), not a demonstrated exploit path. - QEMU guest:
ucom.ko/uplcom.ko/uftdi.koare present but no USB hardware is attached (ls /dev/ttyU*β No such file or directory), so the path cannot be exercised on the audit guest.
Fix
Pass the clamped segment length (see fix.diff β mirrors the RX path).
Reproduce
Static-analysis finding; no runnable trigger was possible on the audit guest (no USB device). See VERDICT.md.
VERDICT β DF-2959
Status: untested (out of Phase-V scope: Low severity, latent reachability,
and the audit QEMU guest has no USB serial hardware). This pack documents a
static-analysis-confirmed defect found during the pass-2 consumer sweep of
sys/kern/tty_subr.c (the clist API): a caller passes an unclamped length
to clist_qtob, whose documented contract (tty_subr.c:127-149) trusts n
as the destination bound.
Why this is a real defect (code-level certainty)
sys/bus/u4b/serial/usb_serial.c:1898obtains the contiguous DMA segment(res.buffer, res.length)viausbd_get_page.usb_serial.c:1900-1903clampsres.lengthto the request β proving the author's intent that at mostres.lengthbytes may touchres.buffer.usb_serial.c:1906then ignores the clamp and callsclist_qtob(&tp->t_outq, res.buffer, len)with the full request lengthlen(e.g.UPLCOM_BULK_BUF_SIZE, uplcom.c:838).clist_qtobwrites exactlymin(len, t_outq.c_cc)bytes tores.buffer(tty_subr.c:138-144) β the only bound it enforces is the queue count.usbd_get_page(usb_busdma.c:96-108) yieldsres.length = USB_PAGE_SIZE - (offset % USB_PAGE_SIZE)for multi-segment caches (as low as 1), and for Case 1b returnsres.buffer = page->buffer + offset, a standalone per-page allocation β so a segment overrun is a heap OOB write into foreign memory, not merely into slack of one big mapping.- The sibling RX path in the same function family
(
ucom_put_data, usb_serial.c:2004-2032) usesres.lengthcorrectly (buf = res.buffer; cnt = res.length; β¦ clist_btoq((char *)buf, cnt, β¦)), so the TX line is an asymmetry/bug, not a subtlety.
Overflow preconditions: multi-segment (or non-KVA) TX page cache AND
t_outq.c_cc > res.length. Both are satisfiable by the API contract but are
not produced by the stock serial drivers on x86 (single-segment β€1-page bulk
buffers β res.length = (usb_size_t)-1 β clamp == len). Hence "latent".
Impact ceiling (if preconditions met)
Kernel heap out-of-bounds write: contents = bytes the user previously
write()d to the /dev/ttyU* device (fully attacker-chosen), length =
min(len, queued bytes) - res.length (up to ~1KB), repeatable per TX
callback. That is a classic heap-corruption primitive β potential local
privilege escalation on machines with USB serial adapters; not demonstrated
here because no such hardware exists on the guest.
Why not reproduced on the guest
vm.sh run_user 'ls /dev/ttyU*'βNo such file or directory;ucom.ko,uplcom.ko,uftdi.ko,u3g.koexist in /boot but there is no USB device for the drivers to bind, soucom_get_datanever executes.- Severity Low (hardware-gated, masked in common config) β per contract, Phase V is conditional and was not warranted for this finding.
Negative results recorded during the same pass-2 sweep
(everything hunted in tty_subr.c itself and its consumers, all killed)
- Ring arithmetic
clist_getc/putc/btoq/qtob/ndflush/unputc: single subtractioni -= c_ccmaxis sufficient under the invariants0 <= c_cchead < c_ccmax,0 <= c_cc <= c_ccmax, which every function preserves for non-negative inputs β tty_subr.c:113-116,140-141,161-163, 179-181,205-207,282-284. clist_alloc_cblocksshrink/grow copy math (tty_subr.c:64-76): proven in-bounds (count <= ccmax,n <= c_ccmax-c_cchead,count-n <= c_cchead).- Blocking-
kmalloc(M_INTWAIT) TOCTOU (tty_subr.c:61-80): killed β LWKT tokens are re-acquired before the blocked thread resumes, so the post-kmalloc field reads (lines 64-80) run under the caller's tty token; the "NOTE: cl fields may now be different" comment is handled by the fresh re-read. - sio interrupt vs tty-softcall concurrency: killed β sio touches clists
only via
comstart/comparam/comstop, each reached withtp->t_tokenheld (ttstarttty.c:1549-1556,ttyinputtty.c:404,ttyflushtty.c:1433,ttioctltty.c:1056);sioinputβttyinputtakes the token itself (tty.c:404). - Huge
clist_alloc_cblockssize via usertermios.c_ispeed(tty.c:1082 β ttsetwater:2439,2456): killed for reachable devices β sio'scomparamrejectsc_ispeed != c_ospeedand invalid divisors (sio.c:2275-2283); ptys havet_ispeedwat == 0β clamped legacy path (tty.c:2444-2449). - Quoted-character sentinel collision (
clist_getcreturning TTY_QUOTE-flagged shorts): killed βTTY_QUOTE == 0x0100(sys/sys/tty.h:208), stored values β€ 0x1ff are always positive as int, so>= 0loops (e.g. ttypend, tty.c:1665) and!= -1loops are equivalent. ccmax * sizeof(short)signed overflow forccmax β (2^30, 2^31): same unvalidated-ccmaxroot cause and same fix as known DF-0211 β folded into that finding's remediation, not re-filed.- Info leak from
clist_qtob/clist_getc: killed β every readable element is written by putc/btoq/alloc-copy; new buffers areM_ZERO(tty_subr.c:61).
Fix verification
not_testableConfirmed kernel references
Detail
Evidence (decisive lines)
['sys/bus/u4b/serial/usb_serial.c:1898-1906 β clamp of res.length ignored by clist_qtob(..., len)', 'sys/bus/u4b/serial/usb_serial.c:2004-2032 β RX path uses res.length correctly (asymmetry proof)', 'sys/bus/u4b/usb_busdma.c:80-125 β usbd_get_page segment semantics (multiseg: USB_PAGE_SIZE - offset; Case 1b: per-page buffer)', 'sys/bus/u4b/serial/uplcom.c:838 β caller passes len = UPLCOM_BULK_BUF_SIZE', 'sys/kern/tty_subr.c:127-149 β clist_qtob enforces only min(n, c_cc) as the copy count']
PoC changes
No PoC exists to fix: the defect is reachable only with USB serial hardware; the audit guest has none (/dev/ttyU* absent, ucom.ko present but unbound).
Verified recommended fix
usb_serial.c:1906: pass res.length instead of len to clist_qtob (mirrors the RX path at 2004-2032)
Verdict
Static-analysis-confirmed consumer-side defect found during the pass-2 consumer sweep of sys/kern/tty_subr.c: ucom_get_data (sys/bus/u4b/serial/usb_serial.c:1898-1906) clamps the contiguous DMA segment to res.length but then calls clist_qtob with the unclamped request length len, so the copy can run up to len bytes past a segment as short as one USB page byte when the TX page cache is multi-segment (usbd_get_page, sys/bus/u4b/usb_busdma.c:96-108) β a latent kernel heap OOB write with attacker-supplied tty data. The sibling RX path (usb_serial.c:2004-2032) uses res.length correctly. Masked in the common single-segment x86 configuration (res.length = -1 -> clamp == len) and hardware-gated (needs a USB serial adapter); not runnable on the audit guest which has no USB devices (/dev/ttyU* absent), so left untested per the conditional Phase-V policy for Low findings.
No comments yet.