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

SLEEPQ_HASH misplaced mask causes massive OOB index into sleepq_chains array

Summary

SLEEPQ_HASH(:82-83) = ((wchan>>10) ^ (wchan & SLEEPQ_HMASK)) -- the & SLEEPQ_HMASK masks ONLY the second XOR operand, not the whole result. For typical kernel pointers, hash returns ~54-bit value, but sleepq_chains has only 1024 entries. SLEEPQ_LOOKUP(:85) = &sleepq_chains[huge] -> wild OOB pointer -> spin_lock on arbitrary address. IMMEDIATE panic/memory corruption on first call. Currently DEAD CODE (zero in-tree callers, compiled into kernel). Fires the instant Linux KPI compat is wired up.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0139 Β· 11 files
FileTypeDescriptionSize
sleepq_hash_oob.c trigger-source standalone arithmetic harness replicating SLEEPQ_HASH, proves OOB index 4.6 KB view raw
build.sh build-script cc -Wall -O2 -o sleepq_hash_oob sleepq_hash_oob.c 195 B view raw
run.sh run-script ./sleepq_hash_oob 115 B view raw
build.log build-log final successful build 13 B view raw
run.log run-log decisive run, OOB indices for all 6 wchan samples 1012 B view raw
kernel_syms.txt evidence nm shows sleepq_* symbols present in /boot/kernel/kernel (compiled in) but zero callers 253 B view raw
env.txt environment uname + cc version 188 B view raw
fix.diff suggested-fix add outer & SLEEPQ_HMASK to the whole XOR result 549 B view raw
VERDICT.md verdict full narrative 3.8 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
VERDICT.md verdict full narrative
↓ download raw

DF-0139 β€” SLEEPQ_HASH misplaced mask β†’ massive OOB index into sleepq_chains

Verdict: REPRODUCED (code-defect confirmed; latent β€” dead code at runtime)

The macro defect is real and confirmed by code inspection and a standalone arithmetic harness. The buggy code path is, however, dead code on the current guest: subr_sleepqueue.c exports sleepq_lock()/sleepq_add()/etc. (symbols present in /boot/kernel/kernel) but has zero in-tree callers β€” the file is a FreeBSD/Linux-KPI compatibility shim awaiting a consumer. So there is no live runtime trigger today; the defect fires the instant any caller is wired up (which is the file's documented sole purpose: "ONLY USE THIS FOR FREEBSD COMPATIBILITY, E.G. THE LINUX KPI").

The bug (sys/kern/subr_sleepqueue.c:82-83)

#define SLEEPQ_HSIZE    1024
#define SLEEPQ_HMASK    (SLEEPQ_HSIZE - 1)            /* 0x3FF, 10 bits */
#define SLEEPQ_HASH(wchan)  ((((uintptr_t)(wchan) >> 10) ^ \
                              ((uintptr_t)(wchan) & SLEEPQ_HMASK)))
#define SLEEPQ_LOOKUP(wchan) &sleepq_chains[SLEEPQ_HASH(wchan)]

The & SLEEPQ_HMASK masks only the second XOR operand (wchan & 0x3FF), not the whole XOR result. wchan >> 10 for any kernel pointer (high virtual address bits 0xffff_8...) yields a ~54-bit value; XOR-ing in 10 low bits leaves all the high bits intact, so the returned hash is a 54-bit value while sleepq_chains has only 1024 (10-bit) slots.

SLEEPQ_LOOKUP() then computes &sleepq_chains[huge] β€” a wild pointer ~hundreds of petabytes past the array β€” which sleepq_lock() (line 182) immediately feeds to spin_lock(), and sleepq_wclookup() (line 120) feeds to TAILQ_FOREACH. First call β†’ instant panic or silent corruption of unrelated kernel memory.

Evidence (harness)

sleepq_hash_oob.c replicates the macro byte-for-byte (uintptr_t == unsigned long on the 64-bit kernel) and runs it over 6 realistic kernel wait-channel pointers (kmem direct-map, kernel-text symbols, malloc'd structs, the global array address itself). All 6 produce indices far outside [0,1023]:

0xffff800000000000  -> 0x003fffe000000000  *** OOB ***
0xffffffff80b34000  -> 0x003fffffffe02cd0  *** OOB ***
0xffff8000deadbeef  -> 0x003fffe00037a980  *** OOB ***
(array @ 0x401fc0)  -> 0x00000000000013c7  *** OOB ***   (even a low address overflows!)

SLEEPQ_LOOKUP(0xffff8000deadbeef) lands 576,456,354,373,644,288 bytes past the array start. No live panic is possible because nothing calls the API.

Exploit chain

none β€” this is a latent code defect, not a memory-corruption primitive reachable from userspace. There is no live caller, so there is no trigger, no slab, no victim object, no uid=0 chain to develop. The realistic impact ceiling if/when Linux KPI is wired up is immediate panic or arbitrary kernel memory corruption on first sleepq use (Critical-grade), but on the current default guest the impact is none (dead code).

Fix

fix.diff adds the missing outer mask so the whole XOR result is reduced to the hash range:

#define SLEEPQ_HASH(wchan)  (((((uintptr_t)(wchan) >> 10) ^ \
                              ((uintptr_t)(wchan) & SLEEPQ_HMASK))) & SLEEPQ_HMASK)

git apply --check passes. This is a one-token (& SLEEPQ_HMASK) fix at the root cause; no caller logic changes.

Fix validation

not_testable: there is no live caller to exercise on a booted guest, so a before/after runtime comparison is impossible. The fix is validated by git apply --check (clean) and by the harness showing the fixed macro (SLEEPQ_HASH_FIXED in the source, column "fixed") returns in-range indices (0x0, 0xd0, 0x180, … all < 1024) for every sample. Building a patched kernel would change no observable runtime behavior because nothing calls the API.

Fix verification

not_testable

compile+harness validated

see evidence pack

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

REPRODUCED (harness). SLEEPQ_HASH misplaced mask -> massive OOB index. Zero callers in tree (dead code).