ASSERT-only domain validation in the DC fixed-point library: dc_fixpt_log diverges into an integer 0/0 (#DE, kernel panic) for nonpositive arguments; exp/mul/sqr/from_fraction overflow-and-UB siblings
Summary
log validates only via ASSERT (a WARN_ON in production - the DF-1949 fact) and its Newton exit test compares raw units while each step for arg==0 moves 2^32 raw units - the loop can never terminate: res descends until exp(res) underflows to 0 and div(arg,0) -> #DE panic (or a permanent 100%-CPU loop). PROVEN on guest with verbatim code + production ASSERT semantics: log(2) converges 9 iters, log(0) traps SIGFPE ~21 iters, log(-1) ~2. Latent in-tree (color_gamma guards); future color paths reach a panic/unkillable loop. Saturate-at-domain-guard fix implementing the file's own TODO.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4883 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.2 KB | β raw | |
| VERDICT.md | β | 3.0 KB | β raw | |
| fixpt_harness.c | β | 10.8 KB | view raw | |
| build.sh | β | 114 B | view raw | |
| run.sh | β | 108 B | view raw | |
| build.log | β | 728 B | view raw | |
| run.log | β | 323 B | view raw | |
| env.txt | β | 1.1 KB | view raw | |
| fix.diff | β | 1.6 KB | view raw | |
| verdict.json | β | 4.0 KB | view raw |
DF-4883 β fixpt31_32.c ASSERT-only domain validation: dc_fixpt_log
diverges into 0/0 (#DE) for nonpositive arguments; related UB/overflow
family members
What
sys/dev/drm/amd/display/dc/basics/fixpt31_32.c validates its inputs only
with ASSERT(), which is WARN_ON (non-fatal) in production builds
(sys/dev/drm/amd/display/dc/os_types.h:76 β the same fact that underlies
DF-1949, the known div0 finding at :43-67, which is NOT re-reported here).
The new, halt-class instance is dc_fixpt_log (:378-407):
ASSERT(arg.value > 0); /* no-op in production */
/* TODO if arg is negative, return NaN */
/* TODO if arg is zero, return -INF */
do {
res1 = (res - 1) + arg/exp(res);
error = res - res1;
res = res1;
} while (abs_i64(error.value) > 100ULL); /* raw units: 100 β 2.3e-8 */
For arg == 0: every step decrements res by exactly 1.0 fixpt
(4294967296 raw units) β the exit test compares RAW units, so
|error| = 2^32 > 100 forever; res descends β1, β2, β3 β¦ until
dc_fixpt_exp(res) underflows to exactly 0, after which
dc_fixpt_div(arg, 0) β complete_integer_division_u64(Β·, 0) β
div64_u64_rem(Β·, 0) β #DE β kernel panic (or, if the divisor never
reaches exactly 0 on a given arch/path, an unbounded kernel loop).
For arg < 0: divergence is immediate and violent (res plummets), same terminal 0/0.
The file's own TODO comments (:386-387) acknowledge the missing domain handling.
Proof (behavioral, on the QEMU guest)
fixpt_harness.c copies the library functions VERBATIM (from_fraction,
mul, exp + Taylor series, log, and the header inlines) with production
ASSERT semantics (ASSERT(x) β no-op) and plain 64-bit //% for
div64_u64_rem. Run on the DragonFly guest (build.sh/run.sh):
[sanity] dc_fixpt_log(2.0) = 0.693147180 (expect ~0.693147181) after 9 iterations [child] SIGFPE (integer 0/0) trapped after ~21 log iterations [DF-4883] log(0.0): child exited status=136 [child] SIGFPE (integer 0/0) trapped after ~2 log iterations [DF-4883] log(-1.0): child exited status=136
log(2) converging correctly validates the harness; log(0) and
log(-1) trap integer-divide-by-zero (SIGFPE = the userspace mirror of
the kernel #DE). This proves the algorithm defect with the file's exact
code β see VERDICT.md for why the kernel-level manifestation is gated.
Kernel reachability in this vintage (honest assessment)
dc_fixpt_loghas exactly one in-tree consumer family:dc_fixpt_pow(fixed31_32.h:431-438 =exp(log(arg1)Β·arg2)) and the direct call inmodules/color/color_gamma.c:159(compute_hlg_oetf, guarded: threshold > b β positive argument).compute_hlg_eotf(color_gamma.c:184-187) evaluatesdc_fixpt_pow(in_x, 2)for in_x β€ Β½ β but its x grid (coordinates_x, color_gamma.c:52-66) starts at a positive power of two and never contains 0, so log never receives 0 there in practice.- The user-gamma paths (
translate_{from,to}_linear_space) feed strictly positive bases (branches at color_gamma.c:270-334). - Everything is behind AMD DC hardware (absent on the audit guest).
β behavior: certain; in-vintage kernel reachability: latent/speculative. Low severity.
Related family members folded into this finding (same root cause:
ASSERT-only validation, non-div0 instances)
- dc_fixpt_exp (:347-371): m from dc_fixpt_round(div(arg, ln2));
1LL << -m for m < -63 is UB (x86 masks the count); positive m is
truncated to unsigned char for dc_fixpt_shl (:365-367, up to 255 β
shift >= 64 UB) β garbage exponents, no trap.
- dc_fixpt_mul (:137-165) / dc_fixpt_sqr (:182-205) /
from_fraction (:81-108): all overflow checks are ASSERTs; signed
res.value <<= 32 at :141 is UB when the integer part exceeds 2^31;
silent garbage for out-of-31.32-range operands.
- -numerator/-arg.value/abs_i64 negate LLONG_MIN unchecked
(:35, :74-75, :126-127) β UB, wraps in practice.
Fix
Guard the log domain (implement the file's own TODO) β see fix.diff.
Build / run
cc -O2 -o fixpt_harness fixpt_harness.c # on the DFly guest ./fixpt_harness
Success criterion: sanity line prints β0.693147; the log(0)/log(-1) children die SIGFPE (status 136) instead of returning.
DF-4883 VERDICT
Status: inconclusive β the algorithmic defect is PROVEN on the guest
with the file's verbatim code; the kernel-level manifestation could not be
exercised because the guest has no AMD DC hardware and no in-vintage
caller currently passes a nonpositive argument to dc_fixpt_log.
What was reproduced (guest, DragonFly 6.5-DEVELOPMENT, cc)
fixpt_harness.c embeds the verbatim library code with production ASSERT
semantics (no-op, matching os_types.h:76) and integer 64-bit division
standing in for div64_u64_rem. Results (run.log):
dc_fixpt_log(2.0)β 0.693147180 in 9 iterations (baseline sanity: the library and harness behave correctly for in-domain input).dc_fixpt_log(0.0)β after ~21 iterations the Newton descent drivesdc_fixpt_exp(res)to exactly 0 and the subsequentdc_fixpt_div(0, 0)traps integer divide-by-zero (SIGFPE) β the userspace mirror of the kernel's #DE throughcomplete_integer_division_u64(fixpt31_32.c:43-55), i.e. the same unprotected sink as DF-1949, reached through a different, new root cause: log's missing domain check (fixpt31_32.c:385) and raw-unit exit test (fixpt31_32.c:404).dc_fixpt_log(-1.0)β diverges immediately, SIGFPE after ~2 iterations.
Key analytical detail (why log(0) loops at all): the exit condition
compares abs_i64(error.value) > 100ULL in RAW units; each iteration for
arg=0 changes res by exactly 1.0 fixpt = 2^32 raw, so the condition can
never be satisfied β the loop is unbounded by construction until the
div0 preempts it. On an architecture where the underflow never yields an
exact 0 divisor, this is a permanent kernel loop instead.
What was NOT demonstrated
Kernel impact. The only in-tree consumers are the color module (modules/color/color_gamma.c, compiled into amdgpu.ko per sys/dev/drm/amd/amdgpu/Makefile:339): its direct log call is domain- guarded, and its pow-based paths feed strictly positive bases (coordinates_x grid starts at a positive power of two, color_gamma.c:52- 66). No AMD GPU exists on the guest (env.txt), so the DC pipeline cannot be driven. Hence: behavior certain, kernel reachability latent.
Impact ceiling (if a caller ever passes arg β€ 0)
Kernel panic (#DE) or an unkillable 100%-CPU loop inside a DRM ioctl / commit path. Realistic ceiling today: latent defect of a vendored library, hardening priority. Low severity.
Fix validation
Not applicable on this guest (no AMD hardware to build/boot a DC path against). fix.diff implements the file's own TODO guard (return -INF-ish saturation for arg β€ 0), which by harness inspection makes log(0)/log(-1) return immediately. fix_status: not_applicable.
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_testablefix.diff implements the file's own TODO domain guard (arg<=0 -> saturated -INF return). Validated against the harness logic (guarded log returns immediately instead of trapping) but not boot-tested in a kernel: the kernel path requires AMD DC hardware absent on this guest.
findings/poc/DF-4883/fix.diff
Confirmed kernel references
- sys/dev/drm/amd/display/dc/basics/fixpt31_32.c:385
- sys/dev/drm/amd/display/dc/basics/fixpt31_32.c:389
- sys/dev/drm/amd/display/dc/basics/fixpt31_32.c:404
- sys/dev/drm/amd/display/dc/basics/fixpt31_32.c:365
- sys/dev/drm/amd/display/dc/basics/fixpt31_32.c:141
- sys/dev/drm/amd/display/dc/os_types.h:76
- sys/dev/drm/amd/display/include/fixed31_32.h:431
- sys/dev/drm/amd/display/modules/color/color_gamma.c:159
Detail
Exploit chain
(latent) DRM color-pipeline caller passes arg<=0 to dc_fixpt_log (directly or via dc_fixpt_pow = exp(log(base)*exp)) -> unbounded Newton descent -> exp(res) underflows to 0 -> dc_fixpt_div(arg,0) -> complete_integer_division_u64 -> #DE kernel panic (or permanent loop where the divisor never hits exactly 0). No such caller exists in this vintage on audited paths.
Evidence (decisive lines)
fixpt_harness.c (verbatim functions, production ASSERT semantics, only changes: no-op ASSERT, plain / % for div64, iteration counter + SIGFPE reporting, all marked HARNESS-ONLY), build.log, run.log (decisive lines: sanity 0.693147180/9 iters; SIGFPE ~21 iters for log(0); SIGFPE ~2 iters for log(-1)), VERDICT.md (reachability analysis of color_gamma.c:159/184-187, coordinates_x positivity), env.txt (no AMD GPU).
PoC changes
Harness copied library bodies verbatim; kernel-only constructs substituted and documented in-file: ASSERT -> no-op (production semantics), div64_u64_rem -> % and /, added iteration counter and SIGFPE handler (marked HARNESS-ONLY); scenarios run in forked children with 60s watchdog so a trap cannot kill the reporting parent.
Verified recommended fix
Implement the dc_fixpt_log TODO domain guard (return saturated -INF for arg<=0) and audit pow callers; optionally add range clamps in mul/sqr/from_fraction
Verdict
Algorithm-level defect PROVEN on the guest with verbatim library code (fixpt_harness.c, run.log): dc_fixpt_log diverges for nonpositive arguments because its ASSERT(arg.value>0) is a no-op in production (os_types.h:76) and the Newton exit test compares raw units (100 raw = 2.3e-8) while arg==0 moves res by 2^32 raw per step - log(0) traps integer 0/0 (SIGFPE, kernel mirror = #DE via the DF-1949 sink) after ~21 iterations, log(-1) after ~2; log(2) converges in 9 (sanity). Kernel manifestation NOT demonstrated: no AMD DC hardware on the guest and the only in-tree consumers (modules/color/color_gamma.c) domain-guard or feed strictly positive bases (coordinates_x grid never contains 0). Same-root-cause family members documented: dc_fixpt_exp 1LL<<-m / shl(u8 m) UB shifts (:365-371), mul/sqr/from_fraction ASSERT-only overflow + signed <<32 UB (:141), LLONG_MIN negations (:35,:74,:126). Low severity latent defect.
No comments yet.