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

cyapa_poll_thread reads uninitialized regs.stat when the device stops responding β€” garbage byte drives the bootloader re-init state machine

Summary

The regs struct is a poll-thread local only written on smbus SUCCESS, but the reset decision reads regs.stat regardless of the transfer error. On the first failing iteration the byte is uninitialized stack and - after the 3s backoff - decides whether init_device re-issues bootloader handshakes every 3 seconds. No leak, no memory unsafety - hardening-class nondeterminism (malicious/glitching firmware makes stale stack data control recovery behavior). Decision-flip harness 2/2 each on the guest. bzero-before-loop fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4230 Β· 11 files
FileTypeDescriptionSize
README.md β€” 1.8 KB ↓ raw
VERDICT.md β€” 2.5 KB ↓ raw
model_uninit_regs.c β€” 3.4 KB view raw
build.sh β€” 108 B view raw
run.sh β€” 132 B view raw
build.log β€” 392 B view raw
run.log β€” 646 B view raw
env.txt β€” 576 B view raw
fix.diff β€” 474 B view raw
manifest.json β€” 1.1 KB view raw
verdict.json β€” 1.8 KB view raw

DF-4230 β€” cyapa: cyapa_poll_thread reads uninitialized regs.stat when the device stops responding β€” garbage byte drives the re-init state machine

Where

sys/dev/smbus/cyapa/cyapa.c

  • cyapa.c:1164 β€” struct cyapa_regs regs; β€” local, never initialized.
  • cyapa.c:1178-1184 β€” smbus_trans(..., (void *)&regs, sizeof(regs), NULL) only writes regs on success; on bus error/NAK regs keeps its previous contents (uninitialized on the first failing iteration).
  • cyapa.c:1191-1193 β€” the reset decision is evaluated regardless of the smbus error: c if (cyapa_reset || ((regs.stat & CYAPA_STAT_RUNNING) == 0 && (unsigned)(ticks - last_reset) > TIME_TO_RESET)) { β†’ reads the uninitialized regs.stat.
  • cyapa.c:1196 β€” consequence: init_device(sc->dev, NULL, sc->addr, 2) is invoked (bootloader handshake hammering on the SMBus) based on garbage.

Impact

Purely a logic/robustness defect: an uninitialized stack byte decides whether the driver spams the bus with bootloader commands every 3 seconds while the device is dead. No info leak (the byte is never printed), no memory unsafety. Info severity, hardening class.

PoC β€” model_uninit_regs.c

Exact branch model with an smbus-failure stub and stack poisoning (observed on the audit guest, run.log): - poison byte 0x2d (RUNNING clear) β†’ re-init triggered from garbage; - poison byte 0xcd (RUNNING set) β†’ re-init suppressed by garbage. Both decisions flip solely on the uninitialized byte β€” 2/2 each.

Fix

Zero regs once before the loop (fix.diff): bzero(&regs, sizeof(regs)); (or memset) at declaration, giving regs.stat = 0 β†’ RUNNING bit clear β†’ deterministic re-init behavior.

Build / run

cc -O0 -Wall -o model_uninit_regs model_uninit_regs.c
./model_uninit_regs 0x2d
./model_uninit_regs 0xcd
VERDICT.md
↓ download raw

DF-4230 β€” VERDICT

Status: untested (kernel-side run requires the trackpad hardware; the uninitialized-read mechanism and its control over the reset decision are demonstrated deterministically by the exact-branch harness on the guest).

Root cause (line-accurate)

struct cyapa_regs regs;                          /* cyapa.c:1164 uninit */
...
error = smbus_request_bus(bus, sc->dev, SMB_WAIT);
if (error == 0) {
        error = smbus_trans(..., (void *)&regs, sizeof(regs), NULL);
        if (error == 0)
                isidle = cyapa_raw_input(sc, &regs);   /* only success writes regs */
}
if (cyapa_reset ||
    ((regs.stat & CYAPA_STAT_RUNNING) == 0 &&          /* cyapa.c:1192 UNINIT READ */
     (unsigned)(ticks - last_reset) > TIME_TO_RESET)) {
        cyapa_reset = 0;
        last_reset = ticks;
        init_device(sc->dev, NULL, sc->addr, 2);       /* cyapa.c:1196 */
}

regs is only written on a successful transfer. When the device is absent/glitching (first iteration fails), regs.stat is uninitialized stack; after 3 s of failures the garbage bit-7 (CYAPA_STAT_RUNNING) decides whether init_device() bootloader handshakes are re-issued every 3 s. (isidle at cyapa.c:1168 also goes stale on failure β€” same family.)

Harness result (audit guest, cc 8.3, -O0)

poison 0x2d (RUNNING clear): after 31 failing iterations: reinited=1  <- from garbage
poison 0xcd (RUNNING set)  : after 40 failing iterations: reinited=0  <- from garbage

Both outcomes of the decision flip purely on the uninitialized byte (2/2 runs each) β€” the driver's bus-recovery behavior is controlled by stale stack data.

Why only Info

  • The value never leaves the kernel (no printf of regs on the failure path β€” cyapa_raw_input, which prints, only runs on success).
  • No memory-safety consequence; the worst case is unnecessary SMBus bootloader traffic while the device is dead.

Initialize regs before the poll loop (fix.diff): bzero(&regs, sizeof(regs)); β€” then stat=0 β‡’ RUNNING clear β‡’ the existing 3-second backoff governs re-init deterministically.

Fix validation

Hardware-gated (poll thread unreachable in QEMU). fix_status = not_testable; the fix is a one-line initialization.

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

not_testable
baseline no→ patch + rebuild →patched clean

Hardware-gated validation; one-line initialization against the traced path, applies cleanly to sys/.

findings/poc/DF-4230/fix.diff (git apply --check clean)
↓ fix.diffper-fix-DF-4230

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log + build.log (0x2d -> reinited, 0xcd -> not reinited, both from garbage byte), VERDICT.md, fix.diff

PoC changes

n/a - pack authored fresh

Verified recommended fix

bzero(&regs, sizeof(regs)) before the poll loop (fix.diff)

Verdict

Source-certain uninitialized stack read: regs (cyapa.c:1164) is only written on smbus success, but the reset decision at cyapa.c:1191-1193 reads regs.stat on failure paths too; harness shows the re-init decision flipping solely on the uninitialized byte (2/2 per branch on the guest). Kernel-side run is hardware-gated (poll thread absent in QEMU). Info severity: no leak (value never printed), no memory unsafety - only nondeterministic bus-recovery behavior while the device is dead.