β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5543

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5543 Β· 14 files
FileTypeDescriptionSize
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} β€” 0x81 completes the pressure packet, 0xAA is the one byte remaining in the transfer. The stash at uep.c:236 stores buf[0] (the already consumed 0x81) instead of p[0] (the remaining 0xAA). The next 4-byte transfer then reassembles and queues the fabricated report 81 bb cc dd ee (phantom touch coordinates; the true stream byte 0xAA never 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.

VERDICT.md
↓ download raw

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:

  1. 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:ee was vendored while the true remaining byte 0xAA vanished 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.
  2. Wedge cascade (T4). With the mis-stashed consumed byte being a bad header (0x55), DF-5542's missing buf_len reset 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched 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')
↓ fix.diffper-fix-DF-5543

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.