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)
PoC verification
Evidence pack
findings/poc/DF-5708 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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<=0fixup β 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_testableNot 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.
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.
No comments yet.