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

cyapawrite returns uninitialized stack `error` on zero-length writes β€” 32 bits of stale kernel stack leak to userland as the write() result/errno

Summary

cyapawrite declares error uninitialized; the only assignment is inside the copy loop whose guard is skipped entirely for a zero-length write, so error is read unassigned at three sites and returned to userland. The syscall layer has no zero-length short-circuit, so write(fd, buf, 0) deterministically returns a stale 32-bit kernel-stack value (prior syscall remnants - pointers/flags/lengths). Repeatable 32-bit kernel-stack sampling - weak info disclosure, KASLR-relevant when pointer fragments surface. Stack-poisoning harness 3/3 returns the poison. int error = 0 fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4229 Β· 12 files
FileTypeDescriptionSize
README.md β€” 2.4 KB ↓ raw
VERDICT.md β€” 3.0 KB ↓ raw
model_uninit_error.c β€” 4.1 KB view raw
build.sh β€” 110 B view raw
run.sh β€” 188 B view raw
build.log β€” 144 B view raw
run.log β€” 546 B view raw
run.2.log β€” 943 B view raw
env.txt β€” 576 B view raw
fix.diff β€” 325 B view raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 2.1 KB view raw

DF-4229 β€” cyapa: cyapawrite() returns an uninitialized stack int on zero-length writes β€” stale kernel-stack data leaks to userland as the write() result

Where

sys/dev/smbus/cyapa/cyapa.c

  • cyapa.c:791 β€” int error; declared, never initialized.
  • cyapa.c:802-809 β€” the copy loop while ((n = fifo_space(&sc->wfifo)) > 0 && uio->uio_resid) is skipped entirely when uio->uio_resid == 0 (zero-length write), so error is never assigned.
  • cyapa.c:815 β€” while (fifo_ready(...) && cmd_completed && error == 0) reads the uninitialized variable (when wfifo is non-empty).
  • cyapa.c:1054 β€” if (error == 0 && (cmd_completed || uio->uio_resid)) reads it unconditionally on this path.
  • cyapa.c:1056 β€” return error; β€” the stale 32-bit stack slot becomes the d_write return value β†’ dofilewrite β†’ write(2) result/errno in userland.

Reachability of the zero-length write (all cited)

Impact

The returned int is stale kernel-stack content of the calling thread (remnants of prior syscalls: pointers, flags, lengths β€” 32 bits). In userland it surfaces as write()'s return value / errno. Weak but real kernel-stack info disclosure, KASLR-relevant when pointer fragments are returned. Device node is 0600 root:wheel β‡’ attacker must hold an fd (root/Xorg or an inherited fd) β‡’ Low severity.

PoC β€” model_uninit_error.c (runs on any UNIX)

Stack-poison harness reproducing cyapawrite()'s exact control flow (cyapa.c:785-1056) for write(fd, buf, 0): - 3/3 runs on the audit guest returned the 0xABABABAB poison pattern through return error; β€” deterministic proof that the value flows from an uninitialized stack slot (run.log).

Fix

int error = 0; at cyapa.c:791 (fix.diff).

Build / run

cc -O0 -Wall -o model_uninit_error model_uninit_error.c
./model_uninit_error

(-O0 so error occupies a stack slot exactly as in the kernel frame; the kernel is built -O2 where the uninitialized load is equally present β€” error is live across the gotos so the compiler materializes it.)

VERDICT.md
↓ download raw

DF-4229 β€” VERDICT

Status: untested (kernel-side leak requires an open /dev/cyapa* fd, which requires the trackpad hardware absent from this guest; the control flow and the uninitialized stack read are reproduced deterministically by the exact-model harness β€” 3/3 poison leaks on the audit guest).

Root cause (line-accurate)

cyapawrite (cyapa.c:786-1057):

int error;                        /* cyapa.c:791  -- never initialized  */
...
again:
cyapa_lock(sc);
while ((n = fifo_space(&sc->wfifo)) > 0 && uio->uio_resid) {  /* :802 */
        if (n > uio->uio_resid)
                n = uio->uio_resid;
        error = uiomove(fifo_write(&sc->wfifo, 0), n, uio);   /* :805 */
        ...
}
cmd_completed = (fifo_ready(&sc->wfifo) != 0);                /* :814 */
while (fifo_ready(&sc->wfifo) && cmd_completed && error == 0) /* :815 uninit read */
...
if (error == 0 && (cmd_completed || uio->uio_resid))          /* :1054 uninit read */
        goto again;
return error;                                                 /* :1056 leak */

When uio->uio_resid == 0 the copy loop at :802 is skipped and error reaches :815/:1054/:1056 unassigned. The value is whatever the calling thread's kernel stack held at that offset (prior syscall frames).

Trigger path (cited every hop)

write(2) β†’ dofilewrite (sys/kern/sys_generic.c:506, no resid==0 short-circuit) β†’ vn_write (sys/kern/vfs_vnops.c) β†’ devfs_write (sys/vfs/devfs/devfs_vnops.c:1366) β†’ dev_dwrite (sys/kern/kern_device.c) β†’ cyapawrite with uio_resid == 0.

A second (narrower) entry β€” wfifo full at :802 with resid > 0 β€” also skips the copy loop, but wfifo-full-on-entry is not reachable through the driver's own state machine; the zero-length write is the clean trigger.

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

write(/dev/cyapa*, buf, 0) returned: 0xabababab (-1414812757)
RESULT: UNINITIALIZED STACK VALUE LEAKED (poison 0xABABABAB observed)   x3/3

The model reproduces cyapawrite()'s exact branch structure; the poison pattern placed in the stack region below the function's frame surfaces as the function's return value β€” the same data flow as the kernel leak.

Impact / severity

32 bits of stale kernel stack per call, repeatable at will by the fd holder. Device is 0600 root:wheel β†’ attacker must be root or hold an inherited fd (Xorg children). Low severity, kernleak class.

int error = 0; β€” one-line fix (fix.diff, applies clean).

Fix validation

Kernel-side validation needs the device (open fd on /dev/cyapa*); the fix is a trivial initializer whose correctness is self-evident against the traced path. fix_status = not_testable.

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

One-line initializer; kernel-side validation needs the physical device (absent in QEMU). Applies cleanly to sys/.

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

Confirmed kernel references

Detail

Exploit chain

hold/inherit fd on /dev/cyapa0-67 -> write(fd, NULL, 0) -> return path returns unassigned error -> stale kernel stack int observable as errno/retval; repeat to sample thread-stack contents (KASLR-relevant fragments)

Evidence (decisive lines)

run.log + build.log (3/3 poison 0xABABABAB through `return error;` model), VERDICT.md (every hop cited), fix.diff

PoC changes

n/a - pack authored fresh

Verified recommended fix

initialize: int error = 0; (fix.diff)

Verdict

Source-certain uninitialized-stack return in cyapawrite: zero-length write skips the uiomove loop so error (cyapa.c:791) is read at :815/:1054 and returned at :1056 as write()'s result. The exact-control-flow harness leaked a stack-poison pattern 3/3 runs on the guest (deterministic mechanism proof); the kernel-side leak itself needs an open /dev/cyapa* fd, impossible on this guest (driver never attaches - no SMBus hardware). 32-bit stale kernel-stack disclosure per call to the fd holder (0600 root:wheel node).