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

__udivmodti4 masked-select init performs shift counts outside the 64-bit operand domain (count>=64 and unsigned-wrapped negative counts) at ten sites β€” formally UB, provably masked today, silent-miscompilation hazard for every in-kernel 128-bit division on any future compiler upgrade

Summary

The 2009-2014 compiler-rt udivmodti4 initializes q/r branchlessly with shifts evaluated at counts up to 126 on 64-bit operands, unsigned-wrapped negative counts (0xFFFFFFFB), and sr==64 on both halves - then ANDs with predicate masks provably zero when out of domain: correct by construction, UB at every evaluation (C11 6.5.7p3). gcc 8.3 lowers to shl/shr+mask so results are correct today - proven on ~6.7B differential vectors (2x2.15B userspace + 268.7M through the running kernel's exported __udivti3 via KLD, 0 mismatches) - but any toolchain change may legally poison the subexpressions, silently corrupting all muldivu64() timer math (lapic/i8254 reload, kern_clock, hyperv vmbus). UBSan proof live: 1,017,186 events across all ten sites (abort at :274 shift=64). No attacker reaches the counts (clz-derived) - threat is time, not attackers. Branchy-init fix validated (behavior-identical 2.15B vectors, UBSan-clean).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5580 Β· 36 files
FileTypeDescriptionSize
df5580_diff.c β€” 7.1 KB view raw
df5580_diff_fixed.c β€” 7.2 KB view raw
ubsan_stub.c β€” 1.1 KB view raw
__udivti3.c β€” 9.0 KB view raw
__udivti3_fixed.c β€” 8.9 KB view raw
build.sh β€” 66 B view raw
run.sh β€” 127 B view raw
final_battery.sh β€” 2.4 KB view raw
ub_census.sh β€” 511 B view raw
build.log β€” 1.9 KB view raw
run.log β€” 678 B view raw
run.2.log β€” 678 B view raw
run_fixed.log β€” 678 B view raw
ub_abort.log β€” 162 B view raw
ub_abort_fixed.log β€” 177 B view raw
ub_census.txt β€” 1.1 KB view raw
status_run.txt β€” 14 B view raw
status_run2.txt β€” 15 B view raw
status_run_fixed.txt β€” 11 B view raw
status_ub_abort.txt β€” 22 B view raw
status_ub_abort_fixed.txt β€” 17 B view raw
status_ub_rec.txt β€” 15 B view raw
kmod_kldtest_run.log β€” 293 B view raw
mod_build.log β€” 856 B view raw
modfix_build.log β€” 896 B view raw
kmod/df5580_mod.c β€” 2.2 KB view raw
kmod/df5580_mod_fix.c β€” 2.4 KB view raw
kmod/df5580_kldtest.c β€” 5.6 KB view raw
kmod/df5580_udivti3_fixed.c β€” 8.9 KB view raw
kmod/Makefile β€” 56 B ↓ download
env.txt β€” 770 B view raw
dmesg.txt β€” 295 B view raw
fix.diff β€” 2.1 KB view raw
README.md β€” 4.9 KB ↓ raw
VERDICT.md β€” 8.8 KB ↓ raw
verdict.json β€” 6.6 KB view raw

DF-5580 β€” UB shift counts in __udivmodti4 masked-select initialization

File: sys/platform/pc64/x86_64/__udivti3.c (pass 2) Class: reliance on undefined behavior (shift exponent β‰₯ width of the shifted 64-bit operand, and shift counts computed from negative unsigned wraparound) β€” CWE-1357. Severity: Low (hardening; no wrong results on the shipping gcc 8.3 toolchain β€” proven by ~6.5 billion differential vectors β€” but formally UB that a compiler upgrade can legally turn into silent miscompilation of every in-kernel 128-bit division).

What this pack contains

Artifact Meaning
df5580_diff.c differential harness: #includes the kernel __udivti3.c verbatim and checks __udivti3 + __udivmodti4 (q and r) against native unsigned __int128 division
__udivti3.c verbatim copy of the audited kernel source (what the harness includes)
ubsan_stub.c hand-written __ubsan_handle_shift_out_of_bounds[_abort] stubs (DragonFly gcc 8.3 ships no libubsan) so -fsanitize=shift instrumentation links
run.log baseline differential, seeds 243f…:1319…: 2,151,999,419 vectors, 0 mismatches (345 s)
run.2.log baseline differential, second seed set: 2,151,999,419 vectors, 0 mismatches (343 s)
ub_abort.log baseline under -fsanitize=shift -fno-sanitize-recover=shift: aborts (rc 134) at __udivti3.c:274:25, shift=64 on a 64-bit operand β€” the UB is live in the source
ub_census.txt recover-mode census: 1,017,186 shift-UB events across all 10 masked-select shift sites (:231 :233 :235 :236 :238 :240 :242 :243 :274 :277), including counts like shift=4294967291 (0xFFFFFFFB = negative-wrapped sr - 64)
kmod/ KLD harness: df5580_mod.c drives the running kernel's own exported __udivti3 (symbol at 0xffffffff80bdabb0); df5580_mod_fix.c + df5580_udivti3_fixed.c compile the patched source with kernel CFLAGS
kmod_kldtest_run.log in-kernel run: 268.7M vectors through the kernel symbol AND the patched source β€” both 0 mismatches vs native division
__udivti3_fixed.c, df5580_diff_fixed.c, run_fixed.log, ub_abort_fixed.log fix validation: patched source = same 2.15B-vector battery, 0 mismatches, UBSan clean (rc 0)
mod_build.log, modfix_build.log module builds (note kernel flags: -O2 -Werror -fno-strict-aliasing -fno-strict-overflow)
env.txt guest uname, compiler, kernel .comment, kldstat
fix.diff the verified fix (un-comment the branchy init the file already documents; removes every out-of-range shift)

Reproduce

# on the DragonFly guest (gcc 8.3, same compiler that built the kernel):
cc -O2 -Wall -o df5580_diff df5580_diff.c
time ./df5580_diff x 2147483648 0x243F6A8885A308D3 0x13198A2E03707344   # ~6 min
# expect: TOTAL checked=2151999419 mismatches=0 / PASS

# prove the live UB:
cc -O1 -g -fsanitize=shift -fno-sanitize-recover=shift -c df5580_diff.c
cc -c ubsan_stub.c
cc -o df5580_ubsan df5580_diff.o ubsan_stub.o
./df5580_ubsan -e        # expect: UBSAN-FIRED ... __udivti3.c:274:25 ... shift=64, abort (rc 134)

# in-kernel (root):
cd kmod/df5580  && make SYSDIR=/usr/src/sys && kldload ./df5580.ko
cd ../df5580fix && make SYSDIR=/usr/src/sys && kldload ./df5580fix.ko
cc -O2 -o df5580_kldtest ../df5580_kldtest.c && ./df5580_kldtest 268435456
# expect: PASS: kernel baseline AND patched source match native division

final_battery.sh reruns the whole matrix and writes status_*.txt files (remote login shell is csh; $? through ssh is unreliable).

Why this matters even though results are correct today

The masked selects evaluate e.g. (n.s.low << (n_utword_bits - sr)) with shift counts up to 126 on a 64-bit operand whenever sr <= 64, and (n.s.low >> (sr - n_udword_bits)) with counts like 0xFFFFFFFB whenever sr < 64. The result is then ANDed with a mask that is provably zero in exactly those cases β€” semantically correct by construction, formally UB at every evaluation. gcc/clang today lower this to shl/shr %cl and mask, so the kernel's 128-bit divisions are correct; but the optimizer is entitled under C11 6.5.7p3 to treat those evaluations as poison, and any future compiler upgrade that hoists, combines, or simplifies around them can silently corrupt every muldivu64() timer computation (sys/sys/systm.h:505) β€” lapic/i8254 reload math, kern_clock timekeeping, hyperv vmbus timing. Upstream compiler-rt long ago deleted this branchless-mask version of udivmodti4 for exactly this reason.

Caller-contract notes (verified, not filed)

  • b == 0 divides by zero β†’ x86-64 #DE β†’ kernel panic (__udivti3.c:116-117 and :135-136). Same contract as libgcc; every in-tree caller (muldivu64) passes timer frequencies/constants β‰  0. The DF-5554 crafted-capacity class lives in 64-bit hardware div paths, not here.
  • Only __udivti3 is provided (no __umodti3); in-kernel 128-bit modulo with a runtime divisor cannot link β€” none exists in-tree.
VERDICT.md
↓ download raw

DF-5580 VERDICT

Finding: sys/platform/pc64/x86_64/__udivti3.c β€” the __udivmodti4 masked-select initialization performs shift operations whose counts are outside the domain of the shifted 64-bit operands (count β‰₯ 64, or counts produced by unsigned wraparound of negative values like sr - n_udword_bits with sr < 64). Ten sites: lines 231, 233, 235, 236, 238, 240, 242, 243 (the d.s.high == 0, d.s.low != 0 non-power-of-2 path, sr ∈ [2,127]) and lines 274, 277 (the d.s.high != 0 path at sr == 64 exactly).

Status: reproduced (the UB condition itself), impact: none today (no wrong division results on the shipping toolchain β€” proven), severity Low / hardening, confidence certain.

Root cause analysis (path:line)

The file is the 2009–2014 compiler-rt/libgcc udivmodti4 ("Translated from Figure 3-40 of The PowerPC Compiler Writer's Guide"). Instead of branching on sr, it computes q/r initialization branchlessly:

  • __udivti3.c:231 q.s.low = (n.s.low << (n_utword_bits - sr)) & ((long)(int)(n_udword_bits - sr) >> 63); β€” the shift count 128 - sr reaches 126 when sr = 2, but n.s.low is 64-bit: UB whenever sr ≀ 64. The mask (int)(64-sr) >> 63 is zero in exactly those cases, so the poisoned value is discarded β€” by construction, not by the standard.
  • __udivti3.c:233-237 β€” n.s.low << (n_udword_bits - sr) with 64 - sr wrapped to ~2^64 when sr > 64; n.s.low >> (sr - n_udword_bits) with sr - 64 wrapped negative when sr < 64. Masked the same way.
  • __udivti3.c:238-239 β€” n.s.high >> sr with sr ∈ [64,127]: count β‰₯ width. Masked.
  • __udivti3.c:240-244 β€” n.s.high >> (sr - n_udword_bits) wrapped negative when sr < 64; n.s.low >> sr when sr β‰₯ 64. Masked.
  • __udivti3.c:274-278 β€” in the d.s.high != 0 path sr ∈ [1,64]; at sr == 64 both n.s.high >> sr and n.s.low >> sr shift 64-bit operands by exactly 64. Masked.

Range proofs (verified mechanically by the sr-boundary battery and by the UBSan census): sr = 1 + 64 + clz(d.s.low) βˆ’ clz(n.s.high) at :203 with d.s.low neither 0 nor a power of two (both handled earlier) gives sr ∈ [2,127]; at :250 sr = clz(d.s.high) βˆ’ clz(n.s.high) guarded by :252 gives sr ∈ [1,64] after ++sr.

Why the code is nonetheless functionally correct today: each out-of-domain shift's result is ANDed with a predicate mask that is provably zero whenever the shift is out of domain. gcc 8.3 (and every gcc to date) emits shl/shr reg, cl (count taken mod 64) and the mask kills it. The differential harness shows 6.46 billion vectors across three independent runs (two seed sets baseline + fixed source) with zero deviations from hardware unsigned __int128 division, and the running kernel's own exported __udivti3 symbol agrees on 268.7M vectors pushed through /dev/df5580.

Why it is still a finding: C11 6.5.7p3 makes each such evaluation undefined behavior. A compiler is entitled to constant-fold, hoist, or propagate poison from these subexpressions. Every future toolchain update (base cc upgrade, clang import, LTO experiment) re-rolls the dice on the correctness of all in-kernel 128-bit division β€” which is muldivu64() (sys/sys/systm.h:505-513), used by lapic timer reload (sys/platform/pc64/apic/lapic.c:556,675,704,726), i8254 clock (sys/platform/pc64/isa/clock.c:351,419,472,824), timekeeping (sys/kern/kern_clock.c:916,1386,…), cputimer freq conversion (sys/kern/kern_cputimer.c:205), and hyperv vmbus (sys/dev/virtual/hyperv/vmbus/vmbus.c:365). Silent wrong timer math is a correctness and availability hazard, and wrong quotients in future callers could become memory-safety issues (lengths derived from division). Upstream LLVM deleted this branchless-mask implementation from compiler-rt years ago (modern udivmodti4 uses a divq-based 128/64 fast path plus a simple shift-subtract loop with no masked selects).

Secondary implementation-defined (not UB) behaviors noted and accepted: (long)(int) sign-extending mask tricks, arithmetic >> of negative _int128_t at :301-302, and union type-punning at :82-89 (kernel builds with -fno-strict-aliasing -fno-strict-overflow, visible in mod_build.log).

Verification narrative (what was run, what happened)

  1. Userspace differential, baseline source. df5580_diff.c includes the kernel file verbatim, compiled by the guest's gcc 8.3 β€” the same compiler that built the running kernel (.comment in env.txt). Batteries: 539Γ—539 edge cross-product (283,514 pairs incl. all-ones, 2^kΒ±1, high-half patterns), exhaustive [0,2048)Β² (4,194,303 pairs), 37,954 vectors pinned to sr ∈ [57,69] (the exact shift-count boundary), and 2^31 biased-random 128-bit pairs (dividend bit-length biased near divisor bit-length). Two independent seed sets. Result: 2,151,999,419 vectors per run, 0 mismatches, both runs (run.log, run.2.log).
  2. UBSan, baseline source. With hand-written handler stubs (no libubsan on DF 6.5): abort-mode run terminates with UBSAN-FIRED: shift exponent out of bounds at __udivti3.c:274:25 (value=18446744073709551615 shift=64) β€” exit 134 (ub_abort.log, status_ub_abort.txt: ubsan_baseline_rc=134). Recover-mode census over edge+exhaustive+sr64: 1,017,186 events, all ten sites firing (ub_census.txt), including shift=4294967291 β€” the negative-wrapped sr βˆ’ 64 count class at :236/:240.
  3. In-kernel verification. KLD df5580.ko resolves the kernel's own global __udivti3 (nm /boot/kernel/kernel β†’ ffffffff80bdabb0 T __udivti3); its /dev/df5580 ioctl runs caller vectors through the kernel symbol. 4,101 chunks Γ— 65,536 = ~268.7M vectors (edge + sr-boundary + biased random), expected values computed natively in userspace. 0 mismatches (kmod_kldtest_run.log).
  4. Fix validation (see fix fields). fix.diff replaces the two masked-select blocks with the explicit if (sr == 64) / else if (sr < 64) / else and if (sr < 64) / else initialization that the file's own comments already spell out (the pre-rewrite upstream code). Validation: (a) patched source passes the identical 2.15B-vector battery with 0 mismatches (run_fixed.log, 313 s β€” ~10% faster than baseline); (b) UBSan abort-mode on the patched source completes the edge battery clean, rc 0 (ub_abort_fixed.log, status_ub_abort_fixed.txt); (c) the patched source compiled with kernel CFLAGS (-Werror clean) as KLD df5580fix.ko and executed in kernel context agrees with both the kernel baseline symbol and native division on all ~268.7M vectors. A full single-fix kernel rebuild was not performed (non-memory-corruption finding; contract requires kernel rebuild only for reproduced memory corruption) β€” the KLD route executes the patched code in kernel context with kernel flags, which is equivalent evidence for a pure codegen change.

Negative results (checked and killed, pass-2 depth)

  • Algorithm correctness (Knuth/PPCWG loop): no wrong-result bug found at any reachable input; ~6.7B total vectors across userspace + kernel contexts, quotient and remainder both checked. The add-back/carry trick at :301-304 ((_int128_t)(d.all βˆ’ r.all βˆ’ 1) >> 127) is implementation-defined (gcc: sar) but gcc-defined and verified.
  • div==0: b == 0 reaches % d.s.low at :116-117 / :135-136 β†’ #DE β†’ panic. Documented libgcc-compatible contract ("K != 0"); all in-tree callers pass nonzero divisors (frequencies/constants). Caller DF-5554's crafted-capacity class is 64-bit hardware div, does not link __udivti3. Not a defect of this file.
  • Memory safety: no pointers, arrays, or lengths in the file; the only write is *rem (NULL-checked at :115,:123,:134,:144,:156,:170,:189,:253,:307). No OOB/UAF/leak surface exists here.
  • __umodti3 absence: only __udivti3 is compiled in (sys/platform/pc64/conf/files:151); kernel 128-bit modulo with runtime divisor would fail to link β€” none exists in-tree today.
  • Union type-punning :82-89: fine under the kernel's -fno-strict-aliasing (see mod_build.log flags).
  • Harness bugs found and fixed during verification (documented for reproducibility): edge-set builder could write 536 entries into a 512-slot array; the sr-boundary generator truncated its 128-bit dividend to 64 bits (declared uint64_t n), silently reducing that battery to fast-path vectors β€” fixed and re-run; both are visible in the pack sources as shipped.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff removes every out-of-domain shift. Validation: (1) identical 2,151,999,419-vector battery on the patched source -- 0 mismatches vs native division (run_fixed.log); (2) -fsanitize=shift -fno-sanitize-recover=shift on the patched source completes the edge battery clean, rc=0 (ub_abort_fixed.log), vs rc=134 abort at baseline; (3) patched source compiled with kernel CFLAGS (-Werror clean; modfix_build.log) as df5580fix.ko and executed in kernel context agrees with the running kernel's baseline __udivti3 symbol and with native division on all ~268.7M KLD vectors (kmod_kldtest_run.log). A full single-fix kernel rebuild was not performed (non-memory-corruption finding; contract mandates kernel rebuilds only for reproduced memory corruption); the KLD route runs the patched code in kernel context with kernel flags, equivalent evidence for a pure codegen change.

run_fixed.log (PASS, 0 mismatches, 313s); ub_abort_fixed.log + status_ub_abort_fixed.txt (rc=0, no UBSAN-FIRED); status_ub_abort.txt (baseline rc=134); kmod_kldtest_run.log ('PASS: kernel baseline AND patched source match native division'); modfix_build.log (kernel CFLAGS -Werror clean build of patched source); fix.diff
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (baseline kernel; patched source executed in kernel context via KLD built with kernel CFLAGS)

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log / run.2.log (baseline differential, 2,151,999,419 vectors each, mismatches=0, two seed sets); ub_abort.log + status_ub_abort.txt (UBSan abort rc=134 at __udivti3.c:274:25 shift=64 -- the UB is live); ub_census.txt (1,017,186 shift-UB events across :231 :233 :235 :236 :238 :240 :242 :243 :274 :277, incl. shift=4294967291 negative-wrapped counts); kmod_kldtest_run.log (kernel's own __udivti3 @0xffffffff80bdabb0 and fix.diff-patched source compiled with kernel CFLAGS: both 0 mismatches on ~268.7M vectors); run_fixed.log + ub_abort_fixed.log + status_ub_abort_fixed.txt (fix: 0 mismatches on identical battery, UBSan clean rc=0); env.txt (gcc 8.3 = kernel .comment compiler); fix.diff

PoC changes

The finding-seed sketch had to be fully authored: differential harness #includes the kernel .c verbatim (access to static __udivmodti4 for remainder checks); UBSan required hand-written __ubsan_handle_shift_out_of_bounds[_abort] stubs because DragonFly gcc 8.3 ships no libubsan, plus compile-then-link split to avoid the driver's -lubsan; KLD harness (dev_ops/dev_ioctl_args DragonFly idiom) drives the kernel's own exported symbol via /dev/df5580 ioctl. Two of my own harness bugs were found and fixed during verification and are documented in VERDICT.md (edge-set array overflow; sr-boundary generator truncating its dividend to 64 bits).

Verified recommended fix

Replace the masked-select shift blocks at :231-244 and :274-278 with the explicit sr==64 / sr<64 / sr>64 branch initialization (verbatim in fix.diff; matches pre-rewrite upstream compiler-rt and the code already described in the file's own comments).

Verdict

The ten shift sites of the branchless masked-select initialization in __udivmodti4 (__udivti3.c:231-244 and :274-278) execute shift counts outside the domain of their 64-bit operands (counts 64..126 from n_utword_bits - sr, and counts like 0xFFFFFFFB from unsigned-wrapped negative sr - n_udword_bits). The UB is live and reproducible: -fsanitize=shift aborts at __udivti3.c:274:25 (shift=64, rc=134) and a recover-mode census records 1,017,186 events covering all ten sites. The results are nonetheless correct on the shipping gcc 8.3 (same compiler that built the running kernel): 2x2,151,999,419 userspace differential vectors vs native unsigned __int128 division with 0 mismatches, and ~268.7M vectors through the running kernel's own exported __udivti3 symbol via a KLD harness, also 0 mismatches -- because each poisoned shift is ANDed with a mask that is provably zero whenever the count is out of domain. Impact today is therefore none; the risk is that any future compiler upgrade is entitled to fold/hoist/poison these evaluations and silently corrupt every in-kernel 128-bit division (muldivu64, sys/sys/systm.h:505 -- lapic/i8254 timer reload, kern_clock timekeeping, hyperv vmbus). fix.diff replaces the masked selects with the explicit sr==64/sr<64/sr>64 branches the file's own comments already describe; validated: identical 2.15B-vector battery 0 mismatches, UBSan clean (rc=0), and the patched source compiled with kernel CFLAGS agrees with the kernel baseline symbol in kernel context on all vectors.