__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)
PoC verification
Evidence pack
findings/poc/DF-5580 Β· 36 filesDF-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 == 0divides by zero β x86-64 #DE β kernel panic (__udivti3.c:116-117and: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 hardwaredivpaths, not here.- Only
__udivti3is provided (no__umodti3); in-kernel 128-bit modulo with a runtime divisor cannot link β none exists in-tree.
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:231q.s.low = (n.s.low << (n_utword_bits - sr)) & ((long)(int)(n_udword_bits - sr) >> 63);β the shift count128 - srreaches 126 whensr = 2, butn.s.lowis 64-bit: UB wheneversr β€ 64. The mask(int)(64-sr) >> 63is 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)with64 - srwrapped to ~2^64 whensr > 64;n.s.low >> (sr - n_udword_bits)withsr - 64wrapped negative whensr < 64. Masked the same way.__udivti3.c:238-239βn.s.high >> srwithsr β [64,127]: count β₯ width. Masked.__udivti3.c:240-244βn.s.high >> (sr - n_udword_bits)wrapped negative whensr < 64;n.s.low >> srwhensr β₯ 64. Masked.__udivti3.c:274-278β in thed.s.high != 0pathsr β [1,64]; atsr == 64bothn.s.high >> srandn.s.low >> srshift 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)
- Userspace differential, baseline source.
df5580_diff.cincludes the kernel file verbatim, compiled by the guest's gcc 8.3 β the same compiler that built the running kernel (.commentinenv.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 tosr β [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). - 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), includingshift=4294967291β the negative-wrappedsr β 64count class at:236/:240. - In-kernel verification. KLD
df5580.koresolves the kernel's own global__udivti3(nm /boot/kernel/kernelβffffffff80bdabb0 T __udivti3); its/dev/df5580ioctl 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). - Fix validation (see fix fields).
fix.diffreplaces the two masked-select blocks with the explicitif (sr == 64) / else if (sr < 64) / elseandif (sr < 64) / elseinitialization 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 (-Werrorclean) as KLDdf5580fix.koand 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 == 0reaches% d.s.lowat: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 hardwarediv, 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. __umodti3absence: only__udivti3is 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(seemod_build.logflags). - 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
fixedfix.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
Confirmed kernel references
- sys/platform/pc64/x86_64/__udivti3.c:231-244
- sys/platform/pc64/x86_64/__udivti3.c:274-278
- sys/platform/pc64/x86_64/__udivti3.c:203-204
- sys/platform/pc64/x86_64/__udivti3.c:250-257
- sys/platform/pc64/x86_64/__udivti3.c:301-302
- sys/sys/systm.h:505-513
- sys/platform/pc64/conf/files:151
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.
No comments yet.