uep(4) one-remaining-byte stash stores the already-consumed buf[0] instead of the remaining p[0]: fabricated touch reports, dropped stream bytes, and cascade into the permanent stall
Summary
When a continuation consumes res bytes and exactly one remains, the stash writes the already-consumed buf[0] instead of p[0]: a 6-byte PRESSURE packet split 5+2 fabricates report 81:bb:cc:dd:ee (phantom coordinates) while the true 0xAA vanishes; a bad-header consumed byte arms DF-5542's stall - reachable with ordinary well-formed traffic on unlucky splits. Harness: WRONG_BYTE_STASHED + cascade PERMANENT_STALL markers deterministic x2; stash-p[0] fix validated. Parser-alignment only, no memory unsafety.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5543 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.2 KB | β raw | |
| VERDICT.md | β | 3.8 KB | β raw | |
| uep_harness.c | β | 8.6 KB | view raw | |
| Makefile | β | 177 B | β download | |
| build.sh | β | 178 B | view raw | |
| run.sh | β | 184 B | view raw | |
| build.log | β | 5.6 KB | view raw | |
| run.log | β | 1.2 KB | view raw | |
| run.2.log | β | 409 B | view raw | |
| fix_build.log | β | 5.6 KB | view raw | |
| fix_run.log | β | 389 B | view raw | |
| env.txt | β | 764 B | view raw | |
| fix.diff | β | 791 B | view raw | |
| verdict.json | β | 4.4 KB | view raw |
DF-5543 β uep(4) stashes the wrong byte (buf[0] instead of the remaining p[0]) when a transfer ends one byte after completing a reassembled packet
What this is
Evidence pack for the pass-2 finding against sys/bus/u4b/input/uep.c.
Same production-code KLD harness as DF-5542 (see that README for the
methodology): the unmodified production uep.c is compiled into
ueph.ko, synthetic interrupt transfers drive the real
uep_intr_callback(), and the real uep_put_queue() records are captured.
Build / run
Identical to DF-5542 (build.sh, run.sh in this directory).
Expected output (stock kernel source β the bug)
UEPH: T3 stash queued_reports=1 report0=81:bb:cc:dd:ee aa_dropped=1 verdict=WRONG_BYTE_STASHED UEPH: T4 cascade queued_reports=0 queued_bytes=0 buf_len=1 verdict=PERMANENT_STALL
- T3 (this finding): the device sends a 6-byte PRESSURE-class packet
split as 5 bytes + 2 bytes, the 2-byte transfer being
{0x81, 0xAA}β0x81completes the pressure packet,0xAAis the one byte remaining in the transfer. The stash at uep.c:236 storesbuf[0](the already consumed0x81) instead ofp[0](the remaining0xAA). The next 4-byte transfer then reassembles and queues the fabricated report81 bb cc dd ee(phantom touch coordinates; the true stream byte0xAAnever appears anywhere in the vendored stream). - T4: same setup with the consumed byte being
0x55β a bad header β which arms DF-5542's permanent stall (mis-stash -> wedge cascade).
Fix validation
fix.diff hunk 2 changes the stash to p[0]. Patched run
(fix_run.log): T3 queued_reports=0 ... verdict=correct β no fabricated
report; the true remaining byte 0xAA is now what gets stashed and parsed
(its length-class assembles a 5-byte candidate which the format check
correctly rejects, i.e. proper header-resync instead of phantom input);
T4 no_stall with 13 reports aligned to the true stream.
Files
Same layout as DF-5542 (shared harness): uep_harness.c, Makefile,
build.sh, run.sh, build.log, run.log, run.2.log,
fix_build.log, fix_run.log, env.txt, fix.diff, manifest.json,
verdict.json.
DF-5543 VERDICT β wrong-byte stash (buf[0] instead of p[0]) at the 1-remaining-byte boundary
Status: reproduced (deterministic, 2/2 identical runs; fix validated). Impact: dos as classified for the DB (input-stream integrity loss: fabricated touch reports with attacker-influenced coordinates + dropped stream bytes + wedge cascade into DF-5542). No memory unsafety.
Root cause (path:line)
Stock sys/bus/u4b/input/uep.c, uep_intr_callback():
216: if (sc->buf_len > 0) {
...
225: res = pkt_len - sc->buf_len;
226: memcpy(sc->buf + sc->buf_len, buf, res);
227: uep_process_pkt(sc, sc->buf); /* completes the pending packet,
consuming res bytes of buf[] */
228: sc->buf_len = 0;
...
230: p = buf + res; /* p = first UNCONSUMED byte */
231: len -= res;
232: } else
233: p = buf;
234:
235: if (len == 1) {
236: sc->buf[0] = buf[0]; /* BUG: stores buf[0], not p[0] */
When exactly one byte remains after a continuation consumed res bytes
(p = buf + res, len == 1), the single-byte stash stores buf[0] β a
byte that was already consumed as part of the just-completed packet β
instead of the remaining byte p[0]. (When sc->buf_len == 0 at entry,
p == buf and the two coincide, which is why aligned streams work.)
Consequences, both demonstrated:
- Fabricated reports / dropped bytes (T3). With the mis-stashed byte
being a valid REPORT header (0x80/0x81), the next transfer reassembles a
5-byte "report" that duplicates the consumed byte and shifts the stream
by one:
report0=81:bb:cc:dd:eewas vendored while the true remaining byte0xAAvanished from the stream entirely. Touch coordinates are computed from fabricated data (x=(buf[1]<<7)|buf[2], uep.c:180-181) and one real byte is deleted β silent input-event corruption visible to whoever reads /dev/uep0. - Wedge cascade (T4). With the mis-stashed consumed byte being a bad
header (0x55), DF-5542's missing
buf_lenreset turns the corruption into a permanent stall.
This boundary is not exotic: any eGalax controller emitting 6-byte PRESSURE/PLAYER-class reports whose interrupt transfers split 5+2 hits it with legitimate traffic β the parser then corrupts or freezes on well-formed input. A malicious device reaches it trivially (it controls transfer lengths).
Evidence
run.log / run.2.log (stock production file, sha256-verified): T3
report0=81:bb:cc:dd:ee aa_dropped=1 verdict=WRONG_BYTE_STASHED; T4
verdict=PERMANENT_STALL. T1 control row proves aligned streams are
unaffected. Harness methodology as documented in DF-5542's README
(production-file KLD, shadow FIFO at the sole producer->FIFO call site,
uep.c:391-392).
Fix validation
fix.diff hunk 2: sc->buf[0] = p[0];. Patched runs (fix_run.log):
T3 queued_reports=0 ... verdict=correct (no fabricated report; the true
byte 0xAA is stashed; its 5-byte candidate is assembled and correctly
rejected by the format check at uep.c:154 β header-resync semantics);
T4 no_stall, 13 reports matching the true stream alignment. Patched
production file builds as the real uep.ko under -Werror
(fix_build.log); guest source restored to stock (sha256 in env.txt).
Bottom line
One-character-class fix (buf[0] -> p[0]) in unmodified production
code, deterministic hardware-free reproduction, validated both directions
(corruption gone, aligned behavior unchanged).
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedPatched production file (fix.diff, guest /usr/src copy): T3 verdict=correct - no fabricated 81:bb:cc:dd:ee report, the true remaining byte 0xAA is stashed and its 5-byte candidate is correctly rejected by the format check (header resync); T4 no_stall with 13 reports aligned to the true stream. Stock behavior restored afterwards (sha256 match).
fix.diff; fix_build.log (harness + real uep.ko build, -Werror clean); fix_run.log ('T3 stash queued_reports=0 ... verdict=correct', 'T4 cascade queued_reports=13 ... verdict=no_stall')
Confirmed kernel references
Detail
Exploit chain
malicious USB device (or any real controller whose 6-byte PRESSURE/PLAYER reports split 5+2 across interrupt transfers) -> continuation completes a packet leaving exactly 1 byte in the transfer -> uep.c:236 stashes the consumed buf[0] instead of p[0] -> next transfer reassembles a shifted 5-byte report: fabricated coordinates vended on /dev/uep0, one real byte dropped; if the consumed byte is a bad header -> DF-5542 permanent wedge. Input-integrity/availability only: no memory unsafety, no escalation path.
Evidence (decisive lines)
run.log/run.2.log (stock, identical): 'T3 stash queued_reports=1 report0=81:bb:cc:dd:ee aa_dropped=1 verdict=WRONG_BYTE_STASHED' and 'T4 cascade ... verdict=PERMANENT_STALL'; fix_run.log: 'T3 ... queued_reports=0 ... verdict=correct' (true byte stashed, candidate rejected by the uep.c:154 format check) and 'T4 ... verdict=no_stall'; fix_build.log (real uep.ko with fix, -Werror); env.txt (uname + production sha256); VERDICT.md (byte-level trace)
PoC changes
No seed PoC existed (new pass-2 finding); harness authored from scratch and shared with DF-5542 (see that verdict's poc_changes).
Verified recommended fix
Stash the remaining stream byte: sc->buf[0] = p[0] (plus DF-5542's buf_len reset and the uep_close() state reset) - see fix.diff.
Verdict
REPRODUCED (deterministic, 2/2 runs, hardware-free). In uep_intr_callback(), when a continuation consumes res bytes to complete a pending packet (uep.c:225-231, p = buf + res), the one-remaining-byte stash stores buf[0] - the already-consumed first byte of this transfer - instead of the remaining byte p[0] (uep.c:236). Demonstrated with a 6-byte PRESSURE-class packet split 5+2: the 2-byte transfer {0x81,0xAA} completes the pressure packet with 0x81 and leaves 0xAA, but 0x81 is stashed; the next transfer then reassembles and vends the FABRICATED report 81:bb:cc:dd:ee while the true stream byte 0xAA disappears from the stream entirely (silent touch-input corruption: coordinates computed from duplicated/misaligned bytes, uep.c:180-181). With a consumed byte that is a bad header (0x55), the mis-stash arms DF-5542's permanent stall (cascade, T4). Any legitimate eGalax controller emitting 6-byte reports with 5+2 transfer splits hits this boundary with well-formed traffic. Production-code KLD harness (unmodified uep.c, sha256-verified). fix.diff hunk 2 (p[0]) validated: no fabricated report, true byte stashed, proper header-resync; patched file also builds as the real uep.ko under -Werror; guest restored to stock.
No comments yet.