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

skrandom() lacks seed-domain validation: seeds congruent to 0 mod (2^31-1) β€” reachable via oce's skrandom(karc4random()) at p=3/2^32 β€” park the kernel PRNG on an absorbing fixed point returning the constant 0x7fffffff for the lifetime of the boot

Summary

skrandom stores any u_long verbatim: for x == 0 (mod m) the Schrage step computes t=0 exactly and the fixup maps state m onto itself - krandom returns the constant 0x7fffffff forever, silently giving every consumer identical outputs (identical inode generation numbers, CHAP challenges, NAT ports). oce_mbox passes a full 32-bit word so 3/2^32 attaches on Emulex hardware brick the PRNG; the hpt guards only cover the 0 output. Demonstrated live: writing 0 and 2m via /dev/mem each collapsed randseed to 0x7fffffff and stayed across draws; control evolved. fold-into-domain + defensive clamp fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5708 Β· 14 files
FileTypeDescriptionSize
README.md β€” 2.8 KB ↓ raw
VERDICT.md β€” 2.4 KB ↓ raw
devmem.c β€” 1.4 KB view raw
minstd.c β€” 5.0 KB view raw
build.sh β€” 65 B view raw
run.sh β€” 89 B view raw
fix.diff β€” 892 B view raw
fixedpoint.log β€” 342 B view raw
calib9.log β€” 166 B view raw
calib10.log β€” 85 B view raw
rate.log β€” 29 B view raw
sanity.log β€” 242 B view raw
build4.log β€” 101 B view raw
env.log β€” 1.5 KB view raw

DF-5708 β€” skrandom() accepts out-of-domain seeds; MINSTD state has absorbing fixed points

File audited (pass 2): sys/libkern/random.c Class: PRNG domain violation / degenerate fixed point (CWE-670) Severity: Low Β· Confidence: certain (math + live kernel demonstration)

The defect

skrandom() (random.c:38-42) stores any caller-supplied u_long verbatim into randseed. krandom()'s Schrage step (random.c:60-66) computes t = 16807*lo - 2836*hi, and on t <= 0 adds 2^31-1 once. For any state x ≑ 0 (mod 2^31-1) β€” i.e. x ∈ {0, m, 2m, ...} for m = 2147483647 β€” the step computes exactly t = 0, the fixup yields t = m = 0x7fffffff, and from state m the step again computes 0 β†’ m: an absorbing fixed point. Every subsequent krandom() call returns the constant 0x7fffffff for the lifetime of the boot.

Reachable trigger: sys/dev/netif/oce/oce_mbox.c:791 calls skrandom(karc4random()) β€” a full 32-bit RC4 word β€” so with probability 3/2^32 per oce attach the drawn seed is 0, m, or 2m and the kernel PRNG is permanently bricked on real (Emulex 10Gb) hardware. The hpt RAID drivers guard only against the 0 output (do { stamp = krandom(); } while (stamp==0); β€” hpt27xx_os_bsd.c:221, hptmv/entry.c:2552, hptrr_os_bsd.c:203) and not against the fixed point.

Constant-output consequences (by code path, each consumer's output becomes a known constant): identical inode generation numbers on every fresh allocation (NFS stale-filehandle confusion β€” DF-2250 impact-1 via a different root cause), identical CHAP challenge words, identical NAT alias ports, identical LCP magics.

Secondary (documented, currently unreachable): for seeds β‰₯ ~2^38 the single += m fixup under-corrects and krandom() returns negative values sign-extended into u_long, violating the documented "[0, 2^31-1]" range (random.c:47); all current callers supply < 2^32 so this is a robustness note only.

Live proof (fixedpoint.log, DragonFly 6.5-DEVELOPMENT guest)

1) write seed=0:            after 8s:  0x7fffffff   (stuck)
   after 16s:               0x7fffffff
2) write seed=0xFFFFFFFE (=2m): after 8s: 0x7fffffff (one draw, then stuck)
3) control seed=0x12345:    after 8s:  0x46183131   (8 draws, evolving)

Writes are the root-equivalent of skrandom(0) / skrandom(2m) via /dev/mem at the randseed symbol; the draws in between are the six per-CPU loadav callouts (kern_synch.c:1431, measured 1.23/s).

Build / run

cc -O2 -o devmem devmem.c && cc -O2 -o minstd minstd.c
RS=0x$(nm /boot/kernel/kernel | grep -w randseed | cut -d' ' -f1)
./devmem w $RS 0; sleep 8; ./devmem r $RS   # -> 0x7fffffff forever
./devmem w $RS 0xFFFFFFFE; sleep 8; ./devmem r $RS

Fix

fix.diff: fold the seed into [1, 2^31-2] in skrandom() and add a defensive re-seed in krandom() when the state is outside the domain.

VERDICT.md
↓ download raw

DF-5708 VERDICT β€” reproduced (mechanism; impact: none at demonstration privilege)

Bottom line. The absorbing-fixed-point behavior of the shared MINSTD state is real and was demonstrated live on the guest kernel: writing 0 (the exact effect of skrandom(0)) or 0xFFFFFFFE (= 2*(2^31-1), both producible by skrandom(karc4random()) at oce attach, p = 3/2^32 per attach) pins randseed at 0x7fffffff permanently; every subsequent krandom() returns the constant 0x7fffffff. The demonstration used root's /dev/mem to place the state (equivalent to the skrandom call the oce driver makes in-kernel); no unprivileged trigger exists on this guest (no oce hardware), so the security impact ceiling on default configs is the probabilistic oce path plus foreign-kmodule/legacy misuse. Classified Low severity, impact "none" at demonstration privilege.

Proof detail

  • fixedpoint.log: three-arm experiment (0 / 2m / control) with the loadav-driven draw stream as the consumer; stuck states observed across multiple 8s windows; control evolves normally (8 draws / 8 s = 1/s, consistent with rate.log's 1.23/s six-CPU measurement).
  • Arithmetic: for x ∈ {0, m, 2m}: hi/lo split yields t = 16807lo - 2836hi = 0 exactly (0: 0-0; m: 168072836-283616807; 2m: 168075672-283633614 = 95329304... = 0), t<=0 fixup β†’ m, and state m maps to itself. Verified by hand and live.
  • Cross-checks that all 2^32 single-word seeds are otherwise safe: for x < 2^32, |t_before| < 2^31 so the single += m always lands in [1, m] β€” only the three multiples of m degenerate; wide seeds β‰₯ ~2^38 can transiently return out-of-range values (negative sign-extended), documented as a robustness note (no current caller).

Fix validation

Not rebuilt (not a memory-corruption class finding; fix validation not mandated). fix.diff folds seeds into the valid domain in skrandom() and defensively re-seeds krandom(); the patch is compile-trivial and the math is exercised by the same demonstration (any x≑0 mod m maps to a valid state under seed % 0x7ffffffe + 1).

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 reproduced→ patch + rebuild →patched clean

Not a memory-corruption class; no kernel rebuild mandated. fix.diff math is exercised by the same live demonstration (any x%%0 mod m maps to a valid state under seed%%0x7ffffffe+1).

N/A - no rebuild performed.
↓ fix.diffper-fix-DF-5708

Confirmed kernel references

Detail

Evidence (decisive lines)

fixedpoint.log: three-arm live run (0 -> stuck 0x7fffffff; 0xFFFFFFFE -> 0x7fffffff after one draw; control 0x12345 evolves, 8 draws/8s); calib9/10.log: exact Schrage stepping on the live kernel; sanity.log: randseed symbol + readback.

PoC changes

Built from scratch: /dev/mem state placement at the nm-resolved randseed symbol with the kernel's own loadav callouts as the draw consumer.

Verified recommended fix

Fold seeds into [1, 2^31-2] in skrandom() and defensively re-seed krandom() on out-of-domain state (fix.diff).

Verdict

skrandom() stores any seed verbatim; seeds congruent to 0 mod (2^31-1) - reachable as {0, m, 2m} from oce_mbox.c:791 skrandom(karc4random()) with p=3/2^32 per attach - park the generator on an absorbing fixed point returning the constant 0x7fffffff forever. Demonstrated live on the guest kernel via the root-equivalent /dev/mem state write: seed 0 and seed 0xFFFFFFFE both collapse to 0x7fffffff and stay across multiple loadav-driven draws; control seed evolves normally. Impact ceiling: constant i_gen (NFS stale-fh confusion), constant CHAP/NAT/LCP values on affected boots; no unprivileged trigger on this guest (no oce hardware), hence impact=none at demonstration privilege and Low severity. Secondary note: seeds >= ~2^38 transiently return out-of-documented-range values (negative sign-extended); unreachable with current callers.