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)
PoC verification
Evidence pack
findings/poc/DF-2344 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
- Guest up; push
../DF-2343/to guest:/root/umcs;sh prep.sh;sh build.sh; reboot into UMCSH. 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).- Fix A/B:
patch < fix_2344.diffin 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
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 inumcs7840_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*dataonly via the control transfer; onusbd_do_request_procerror 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) elsesc_numports = 2(:333-336, port numbers 0 and 2). Both outcomes stay withinUMCS7840_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 registervisible.
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_testableConfirmed 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.
No comments yet.