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

Unchecked register read uses uninitialized stack for port-count decision in attach

Summary

In umcs7840_attach return value of umcs7840_get_reg_sync(sc MCS7840_DEV_REG_GPIO &data) at :324 is discarded. On USB control-transfer failure local uint8_t data declared at :301 remains uninitialized and read at :325 to choose 2- vs 4-port configuration. Next register read at :339 correctly checks return value - only GPIO read omits check. No memory-safety impact: decision only sets sc_numports 2 or 4 portno hardcoded within UMCS7840_MAX_PORTS. Worst: wrong port count causes attach failure. [VERIFIED pass2: reproduced on stock-codegen harness - groomed stack byte decides sc_numports 4-of-4 deterministically with read stalled, device value decides when read succeeds; config-only impact; return-check fix-validated - ENXIO on failure, good device attaches 4 ports]

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2344 Β· 12 files
FileTypeDescriptionSize
umcs_harness.c β€” 18.0 KB view raw
umcsh.h β€” 1.5 KB view raw
umcshctl.c β€” 1.3 KB view raw
tioc0.c β€” 2.9 KB view raw
prep.sh β€” 2.2 KB view raw
build.sh β€” 146 B view raw
build.log β€” 5.6 MB ↓ download
run.log β€” 3.6 KB view raw
fix_2344.diff β€” 466 B view raw
env.txt β€” 1.1 KB view raw
VERDICT.md β€” 4.8 KB ↓ raw
README.md β€” 3.7 KB ↓ raw

DF-2344 evidence pack

umcs7840_attach() discards the return value of the GPIO register read at sys/bus/u4b/serial/umcs.c:324; on USB control-transfer failure the local uint8_t data (declared at :301, never initialized) is read at :325 (data & MCS7840_DEV_GPIO_4PORTS) to choose the 2-vs-4-port configuration. The next register read (MODE, :339) checks its return correctly β€” only the GPIO read omits the check.

Verdict (short)

REPRODUCED on the stock-codegen harness kernel (shared rig with DF-2343; see ../DF-2343/). With the GPIO control transfer failing, sc_numports followed a groom byte placed on the kernel stack below attach()'s frame β€” 4/4 deterministic:

groom byte (uninit slot content) bit0 sc_numports
0xA5 1 4
0x00 0 2
0x01 1 4
0x42 0 2

data has its address taken (passed as &data at :324), so it lives in attach()'s stack frame and reads whatever the call chain left there β€” the harness grooms exactly that region (groom_stack() immediately before umcs7840_attach(); no intervening call). With the read succeeding, the decision follows the device value instead (0x01β†’4, 0x00β†’2), proving the sim is not the decider. Attach succeeds with the garbage-derived port count either way.

Impact: none beyond wrong configuration (Info, as filed). The uninitialized byte only selects 2 vs 4; the per-port port numbers are hardcoded constants within UMCS7840_MAX_PORTS (umcs.c:328-336), so no out-of-bounds indexing follows. Worst realistic case with a failing/buggy MCS7840 device at attach: wrong number of ttyU units created (a 2-port device exposing 4 units, or vice versa), or an odd attach failure downstream. No info leak (the byte is not printed), no memory-safety consequence, no user trigger (attach-time only, requires the hardware to misbehave). PRIVESC: none* β€” explicitly closed.

Fix (fix_2344.diff): check the return like the MODE read does and fail attach (goto detach β†’ ENXIO). Validated on the same rig: GPIO-failure attach now returns ENXIO (rc=6) deterministically for every poison byte, sc_numports stays 0, and a good device still attaches with 4 ports (no regression).

Rig

Shared with DF-2343 (verbatim-interposition harness, kernel-compiled): umcs_harness.c #includes the sha256-gated unmodified umcs.c (33f9ea99…bef67); usbd_do_request_proc is simulated with faithful no-write-on-error semantics (on STALL the caller's buffer is NOT written β€” exactly like the real control-transfer path β€” which is what leaves data uninitialized); everything else (ucom_attach, make_dev, locks) is stock. Scenario ./umcshctl 4 <byte> 3 = fail the GPIO read + groom the stack with <byte> + call the real umcs7840_attach().

How to reproduce

  1. Guest up; push ../DF-2343/ to guest:/root/umcs; sh prep.sh; sh build.sh; reboot into UMCSH.
  2. sh run1_baseline.sh (or directly): ./umcshctl 4 0x01 0; ./umcshctl 5; ./umcshctl 4 0x00 0; ./umcshctl 5 (sanity: device value decides) then ./umcshctl 4 0xA5 3; ./umcshctl 5; ./umcshctl 4 0x00 3; … (read fails: groom byte decides β€” see run.log).
  3. Fix A/B: patch < fix_2344.diff in guest /usr/src/sys/bus/u4b/serial/, rebuild, reboot, sh run3_fixed.sh β†’ ENXIO for all poison bytes, good device still 4 ports.

Files

(sources shared with ../DF-2343/, copied here for self-containment) umcs_harness.c, umcsh.h, umcshctl.c, tioc0.c, prep.sh, build.sh build.log FULL stock-codegen kernel build transcript (shared) run.log the DF-2344 battery: baseline poison runs + fixed A/B fix_2344.diff the validated return-check fix env.txt guest environment VERDICT.md full narrative manifest.json, verdict.json

VERDICT.md
↓ download raw

DF-2344 VERDICT β€” uninitialized stack byte decides port count in umcs7840_attach

Status: reproduced (stock codegen). Impact: none beyond wrong configuration (Info, as filed). No privesc.

1. Line accuracy (confirmed)

  • umcs.c:301 β€” uint8_t data; local in umcs7840_attach(), no initializer.
  • umcs.c:324 β€” umcs7840_get_reg_sync(sc, MCS7840_DEV_REG_GPIO, &data); return value discarded (compare the MODE read at :339: if (!umcs7840_get_reg_sync(sc, MCS7840_DEV_REG_MODE, &data))).
  • umcs7840_get_reg_sync (umcs.c:914-934) fills *data only via the control transfer; on usbd_do_request_proc error it returns before writing (the len!=1 path at :928-930 also returns without writing).
  • umcs.c:325 β€” if (data & MCS7840_DEV_GPIO_4PORTS) (bit 0x01, umcs.h:245) β†’ sc_numports = 4 (branch body :326-331, port numbers hardcoded 0..3) else sc_numports = 2 (:333-336, port numbers 0 and 2). Both outcomes stay within UMCS7840_MAX_PORTS-bounded arrays.

2. Mechanical verification (stock -O2 codegen β€” no codegen caveat

for this finding; data is address-taken, hence a memory slot in attach()'s frame at -O2 too)

Rig: shared verbatim-interposition harness (../DF-2343/). The simulated usbd_do_request_proc faithfully does NOT write the caller buffer when it fails (stall), reproducing the real error semantics. groom_stack() burns an 8 KB pad of the chosen byte immediately below the caller frame, with no call between groom and attach (an earlier harness version had a kprintf in between which clobbered the pad β€” all four poisons then read bit0-clear; fixed by reordering, which itself demonstrates the decision reads live stack residue).

Results (run.log; kernel = UMCSH stock-codegen build, 2026-10-18 21:28):

scenario GPIO read slot content sc_numports attach()
sanity OK β†’ 0x01 0x01 (written) 4 0
sanity OK β†’ 0x00 0x00 (written) 2 0
fail + groom 0xA5 STALL (no write) 0xA5 (residue) 4 0
fail + groom 0x00 STALL (no write) 0x00 (residue) 2 0
fail + groom 0x01 STALL (no write) 0x01 (residue) 4 0
fail + groom 0x42 STALL (no write) 0x42 (residue) 2 0

The port-count decision followed the groom byte in 4/4 failing-read runs and the device value in the succeeding runs β€” the uninitialized data slot is the sole input when the read fails, and attach succeeds either way (creating 4 or 2 /dev/ttyU* units from garbage).

3. Reachability and impact honesty

Trigger: a (failing or protocol-buggy) MCS7840/7820 USB adapter at driver attach β€” i.e., malicious/defective hardware, at plug-in or boot. No unprivileged-user software path reaches this decision (the GPIO read happens once, in attach, before any ttyU* node exists). Impact ceiling: wrong port count β†’ wrong set of tty units (e.g., a 2-port device exposed as 4 ports whose extra bulk endpoints do not exist β€” usbd_transfer_setup for them would fail on real hardware and detach), or an attach that should have failed succeeding with a bogus configuration. No OOB access (port indices are compile-time constants within bounds), no leak, no controllable primitive beyond the 2/4 bit. CWE-457 confirmed; Info severity is right.

PRIVESC: none (team directive) β€” the uninitialized byte gates only a 2-vs-4 selection among in-bounds, constant port-number tables. There is no write primitive, no indexing by the tainted value, and no path to uid=0.

4. Fix validation (fix_2344.diff)

Check the GPIO read return and goto detach (device_printf first), matching the MODE-read idiom. goto detach is safe pre-ucom_attach: umcs7840_detach β†’ ucom_detach returns immediately while UCOM_FLAG_ATTACHED is clear (usb_serial.c:369-371); transfer unsetup is NULL-safe. A/B on the same rig (run.log, fixed section):

  • GPIO-fail + every poison byte (0xA5/0x00/0x01/0x42): attach β†’ rc=6 ENXIO deterministically, rd=1 (exactly one control read attempted), sc_numports stays 0, no tty units created β€” poison irrelevant.
  • Good device (GPIO 0x01): still attaches, 4 ports, ttyU0.0-3 created β€” no regression.
  • Console print: umcs7840: failed to read GPIO register visible.

Validated on both the -O0-semantics and stock -O2 fixed kernels (run3.log / run5_fixed_o2.log in ../DF-2343/ β€” identical ENXIO behavior).

Guest left clean afterwards (vm.sh reset with-src).

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.

(No fix.diff for this item: documented recommendation only - see Fix above.)

Fix verification

not_testable
per-fix-DF-2344

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (baseline poison battery + fixed A/B, verbatim ctl output); ../DF-2343/run1.log and run3.log (full batteries); ../DF-2343/dmesg-era kernel prints RD reg 0x07 STALLED (no byte written to caller buffer) then found N active ports

PoC changes

shared rig built for DF-2343; groom-before-attach ordering fixed after an intervening kprintf clobbered the poisoned pad (all poisons then read bit0-clear - itself a clean demonstration that the slot reads live stack residue)

Verified recommended fix

check the umcs7840_get_reg_sync return for the GPIO register and fail attach (goto detach) like the MODE read at umcs.c:339

Verdict

Confirmed and reproduced on the stock-codegen verbatim-interposition rig (shared with DF-2343): umcs7840_attach discards the return of the GPIO register read (umcs.c:324), and when the control transfer fails the uninitialized stack byte at the address-taken data slot (umcs.c:301) alone decides the port count at umcs.c:325. With the read failing and the kernel stack groomed below attach() frame, sc_numports followed the groom byte 4/4 (0xA5->4, 0x00->2, 0x01->4, 0x42->2) while attach() returned 0 each time; with the read succeeding it followed the device value (0x01->4, 0x00->2), proving the residue is the decider. The simulated control transfer faithfully skips writing the caller buffer on error, exactly like the real path (umcs.c:914-933). Impact is configuration-only, as filed (Info): the taint selects 2-vs-4 among hardcoded in-bounds port-number tables (umcs.c:328-336), no OOB indexing, no leak, no userland trigger (attach-time, requires failing/buggy hardware). PRIVESC: none - closed explicitly, no write or indexing primitive exists. Fix validated on the same rig: checking the return and failing attach (goto detach, ENXIO) makes the outcome deterministic for every poison byte while good devices still attach with the right port count; no regression at either codegen.