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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4883 Β· 10 files
FileTypeDescriptionSize
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_log has exactly one in-tree consumer family: dc_fixpt_pow (fixed31_32.h:431-438 = exp(log(arg1)Β·arg2)) and the direct call in modules/color/color_gamma.c:159 (compute_hlg_oetf, guarded: threshold > b β‡’ positive argument).
  • compute_hlg_eotf (color_gamma.c:184-187) evaluates dc_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.

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.

VERDICT.md
↓ download raw

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 drives dc_fixpt_exp(res) to exactly 0 and the subsequent dc_fixpt_div(0, 0) traps integer divide-by-zero (SIGFPE) β€” the userspace mirror of the kernel's #DE through complete_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_testable
baseline no→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-4883

Confirmed kernel references

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.