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

get_bytes_per_element() returns 0 for dm_444_8/dm_mono_8 and unhandled enum values -> integer divide-by-zero (#DE kernel panic) at get_surf_rq_param():641 for linear surfaces, dead-code #DE at :745, and dml_log2(0)=-127 UB-shift garbage programming

Summary

The switch has no case for the legal enum value dm_444_8/dm_mono_8 nor out-of-set values, returning 0. With bpe==0: LINE integer division at :641 (linear branch; read downstream so -O2 cannot eliminate), a second dead-code #DE at :745, and dml_log2(0)=-127 making all 1<<log2/*>>log2* expressions UB shifts producing garbage - even the non-crashing tiled path silently programs nonsense RQ/DLG registers. Latent: the sole caller maps default->dm_444_32 today; armed the moment any caller passes the 8-bit mono class. Harness compiles the REAL unmodified file with realistic DCN1 config: mono8_linear SIGFPE 3/3, normal + fixed complete. Handle-format + zero-guard fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4198 Β· 15 files
FileTypeDescriptionSize
README.md β€” 3.5 KB ↓ raw
VERDICT.md β€” 5.9 KB ↓ raw
dml1_harness.c β€” 4.3 KB view raw
dm_services.h β€” 1.2 KB view raw
dml1_display_rq_dlg_calc.fixed.c β€” 66.2 KB view raw
build.sh β€” 168 B view raw
run.sh β€” 89 B view raw
build.log β€” 537 B view raw
run.log β€” 1.3 KB view raw
run.2.log β€” 39 B view raw
run.3.log β€” 1.6 KB view raw
env.txt β€” 419 B view raw
fix.diff β€” 1009 B view raw
manifest.json β€” 1.4 KB view raw
verdict.json β€” 4.4 KB view raw

DF-4198 β€” get_bytes_per_element() returns 0 for dm_444_8/dm_mono_8 and unhandled formats β†’ integer divide-by-zero (#DE) in get_surf_rq_param + dml_log2(0)=-127 UB-shift family

File under audit: sys/dev/drm/amd/display/dc/dml/dml1_display_rq_dlg_calc.c (pass 2)

What

get_bytes_per_element() (dml1_display_rq_dlg_calc.c:40-65) handles only dm_444_16/32/64 and dm_420_8/10. The legal enum value dm_444_8 (= dm_mono_8, display_mode_enums.h:42-43) β€” and any out-of-set enum β€” yields ret_val = 0. With bytes_per_element == 0 the file then:

  • live #DE: get_surf_rq_param() line 641, linear surfaces: blk256_width = 256 / bytes_per_element; β†’ integer division by zero β†’ kernel #DE β†’ panic (SIGFPE in the model harness).
  • dead-code #DE (source-level): line 745 meta_blk_width = meta_blk_bytes * 256 / bytes_per_element / meta_blk_height; β€” meta_blk_width is never read, so gcc -O2 eliminates it (harness confirmed: tiled mono8 does NOT trap), but the source division is still against the same zero.
  • UB shift family: dml_log2(0) returns -127 (dcn_bw_log bit-trick, calcs/dcn_calc_math.c:112-126: log2f-mantissa approximation of 0.0 gives -127), so every downstream 1 << log2_* / >> log2_* (lines 636/652-653/678/684/708-709/719/736-739/739) and log2_xxx arithmetic gets -127-based garbage; even non-linear mono8 silently programs wrong RQ/DLG registers (harness: sw=1919 h=2, stored_swath=0 β€” nonsense for a 1-byte format).

Reachability (honest)

The only in-tree caller of dml1_rq_dlg_get_rq_params is dcn_bw_calc_rq_dlg_ttu() (calcs/dcn_calcs.c:487), and its format mapping (calcs/dcn_calcs.c:353-377) maps VIDEO_420→dm_420_8/10, ARGB16161616→dm_444_64, everything else→dm_444_32 — it never produces dm_444_8 today. The defect is therefore latent*: one new format mapping (or a DCN1 caller passing dm_mono_8, which the enum explicitly defines as an alias) turns it into a kernel panic on modeset validation. Additionally all of it sits behind DCN1 (Raven) hardware. Arithmetic certainty, trigger currently absent — hence Low severity.

How it is proven here

dml1_harness.c compiles the REAL, unmodified dml1_display_rq_dlg_calc.c (plus real dcn_calc_math.c, dml_common_defs.c, real headers; only dm_services.h is a stub providing the non-fatal WARN_ON-style ASSERT, matching os_types.h:76 release semantics) on the DragonFly guest and drives it with a realistic DCN1 config (1920x1080, det_buffer 164 KB, vmm_page 4096):

$ ./dml1_harness normal        # dm_444_32 (what dcn_calcs maps) β€” OK, sane regs
$ ./dml1_harness mono8         # dm_444_8, tiled β€” NO trap (line 745 is dead
                               #   code at -O2) but silently WRONG values
$ ./dml1_harness mono8_linear  # dm_444_8 + dm_sw_linear β€” SIGFPE at :641
Floating point exception (core dumped)   rc=136   (3/3 runs)

dml1_harness_fixed is the SAME harness compiled with fix.diff applied to the real dml1 file: all three modes complete rc=0 and mono8 now produces sane values (bpe=1).

Reproduce

./build.sh          # copies the real files from /usr/src, builds baseline+fixed
./run.sh            # baseline mono8_linear -> SIGFPE; fixed -> clean

Fix

fix.diff: (1) handle dm_444_8 β†’ 1 byte/element in get_bytes_per_element (mono 8bpc is 1 byte per element, matching the enum alias); (2) defense-in-depth: bail out of get_surf_rq_param when bytes_per_element == 0 (ASSERT is only WARN_ON in release builds, so the explicit return is what prevents the fault).

VERDICT.md
↓ download raw

DF-4198 VERDICT

Status: inconclusive (hardware-gated: guest has no AMD/DCN GPU, and the in-tree caller never passes dm_444_8 today) β€” the divide-by-zero itself is proven EXACTLY on the DragonFly guest by compiling and running the REAL, unmodified dml1_display_rq_dlg_calc.c with crafted inputs; the fault is a SIGFPE (userland #DE analog). Impact class if reached in-kernel: integer divide exception β†’ kernel panic (DoS); also silent wrong register programming for non-linear unhandled formats. Not memory corruption; uid=0 chain N/A.

Root cause (path:line)

  • sys/dev/drm/amd/display/dc/dml/dml1_display_rq_dlg_calc.c:40-65 β€” get_bytes_per_element() has no case for dm_444_8/dm_mono_8 (display_mode_enums.h:42-43) nor for out-of-set values β†’ returns 0.
  • :641 β€” blk256_width = 256 / bytes_per_element; (surf_linear branch) β†’ live integer division by zero (blk256_width is read at :652/:676/:684/ :688-695, so gcc -O2 cannot remove it β€” confirmed: harness traps).
  • :745 β€” meta_blk_width = meta_blk_bytes * 256 / bytes_per_element / meta_blk_height; β€” second zero divisor in source; dead code at -O2 (meta_blk_width never read β†’ eliminated; harness proves tiled mono8 does NOT trap), and meta_blk_height = blk256_height * 64 is 0 as well for bpe=0 (get_blk256_size:77-95 leaves 0 for unknown bpe).
  • dml_log2(0) == -127 (calcs/dcn_calc_math.c:112-126 bit-trick) β†’ log2_bytes_per_element = -127 (:636) poisons 1 << … / >> … expressions (UB shifts; x86 masks the count, producing garbage rather than a fault) β€” this is why even the non-crashing paths compute nonsense (harness mono8: sw=1919 h=2 stored_swath=0).

Reachability analysis (what keeps this Low)

  1. Sole caller: dcn_bw_calc_rq_dlg_ttu() at calcs/dcn_calcs.c:487, fed by pipe_ctx_to_e2e_pipe_params() whose format switch (calcs/dcn_calcs.c:353) maps VIDEO_420_→dm_420_8/10, ARGB16161616→dm_444_64, default→dm_444_32. dm_444_8 is never produced today; the enum alias dm_mono_8 exists precisely because the format class is part of the DML input vocabulary, so any future mapping addition (upstream DCN reworks, new callers) arms the panic.
  2. Everything sits behind DCN1 (Raven) hardware: dcn_calcs runs during dcn10_validate_bandwidth; no AMD GPU can be attached to this guest (ls /dev/dri β†’ ENOENT).

Trigger when armed: modeset validation with an 8-bit mono/444 plane in linear layout β†’ #DE in task context β†’ panic.

Reproduction (guest, DragonFly 6.5-DEVELOPMENT, cc 8.3, -O2)

Baseline (real, unmodified file from sys/):

$ ./dml1_harness normal          # source_format=dm_444_32
OK: rq_l sw=1928 h=8 dpte_row_h=128 meta_row_h=64 ... stored_swath=123392 (assert-warns=0 dtrace=13)   rc=0
$ ./dml1_harness mono8           # dm_444_8, tiled β€” no trap (dead :745), wrong values
OK: rq_l sw=1919 h=2 dpte_row_h=32 ... stored_swath=0 ...    rc=0
$ ./dml1_harness mono8_linear    # dm_444_8 + dm_sw_linear β€” LIVE :641 division
Floating point exception (core dumped)    rc=136        (3/3 runs, run.2.log)

Patched (fix.diff applied to the same real file, dml1_display_rq_dlg_calc.fixed.c):

$ ./dml1_harness_fixed normal        rc=0  (identical regs to baseline normal)
$ ./dml1_harness_fixed mono8         rc=0  sw=1936 h=16 dpte_row_h=256 ... stored_swath=61952  (sane 1-byte-format values)
$ ./dml1_harness_fixed mono8_linear  rc=0  sw=2304 h=1 dpte_row_h=128 ... stored_swath=4608

The fixed binary's mono8* outputs are plausible for bpe=1 (swath width doubles vs bpe=4? no β€” narrows in bytes; the printed sanity: h=16/h=1 are power-of-two swath heights as required).

Killed negatives (dce_calcs checklist applied to this file)

  • Divisions by mode/socBB clocks: t_calc_us = 24/min_dcfclk_mhz (:1146), line_time_in_us = htotal / pclk (:1206), delay ratios (:1231-1232), line_wait / line_time (:1293), prefetch_bw = ... / t_pre_us (:1392), flip_bw / total_flip_bytes (:1393-1394) β€” all double divisions; a zero/NaN operand yields inf/NaN β†’ (unsigned int) cast β†’ 0x80000000-style garbage (cvttsd2si), NOT a trap. No #DE from the double sites.
  • / vratio_l/c (:1546/:1550), / dpte_groups_per_row_ub (:1518/:1523), / meta_chunks_per_row_ub (:1528), hratios_cur0 (:1840) β€” doubles, same as above.
  • data_pitch divisions (:505/:832): data_pitch comes from pipe->plane_res.scl_data.viewport.width (calcs/dcn_calcs.c:285), i.e. a validated non-degenerate plane viewport, not a raw UABI pitch; zero would require a 0-width visible plane, which atomic validation rejects β€” speculative only, not filed.
  • Array indexing: the file contains no arrays β€” struct walks only; no OOB writes (tq/surf structs are fixed-size members).
  • u32 fixed-point accumulation overflow (:305/:331/:727/:1366-1383): bounded by validated mode dimensions (viewport ≀ 16k, bpe ≀ 8, lsw ≀ ~64) β€” max ~10^8, no overflow.
  • meta_blk_width dead store + dml_round_to_multiple(...)/64.0 truncation (:261-266): logic quirks upstream-inherited, no fault.

Fix validation

fix.diff (sys/dev/drm/amd/display/dc/dml/dml1_display_rq_dlg_calc.c): adds the missing dm_444_8β†’1 mapping and an explicit bpe==0 bail-out in get_surf_rq_param (ASSERT alone is WARN_ON in release builds, os_types.h:76). Validated end-to-end on the REAL source file in the harness: baseline mono8_linear SIGFPE 3/3 vs fixed 0/3 with sane outputs. Kernel-level build is not possible on this guest (no AMD hardware to load amdgpu DC; building the amdgpu module is possible but cannot be exercised), so fix_status: inconclusive β€” semantic validation is at real-source model level.

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

inconclusive
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied to the REAL dml1_display_rq_dlg_calc.c and recompiled in the harness: baseline mono8_linear SIGFPE 3/3 vs fixed 0/3 with sane outputs (sw=2304 h=1 dpte_row_h=128); normal-mode outputs byte-identical to baseline. Kernel-level validation not possible on this guest (no AMD GPU to load the amdgpu DC path), so kernel-level fix status stays inconclusive; real-source-level validation passed cleanly.

run.log/run.3.log baseline-vs-patched contrast lines; dml1_display_rq_dlg_calc.fixed.c (shipped, = fix.diff applied); fix.diff
↓ fix.diffper-fix-DF-4198

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log baseline mono8_linear 'Floating point exception' rc=136 vs fixed mono8_linear 'OK: rq_l sw=2304 h=1 ... stored_swath=4608' rc=0; run.2.log 3/3 determinism; run.3.log end-to-end pack run (build.sh+run.sh) against the guest's /usr/src

PoC changes

harness written fresh (no seed): REAL kernel sources compiled unmodified; dm_services.h stub supplies DC_LOG_DML no-op and non-fatal ASSERT (os_types.h:76 release semantics: WARN_ON); DCN1-realistic ip/soc params (det_buffer 164KB, dpte_buffer 64 pte reqs, vmm_page 4096, 1920x1080 viewport)

Verified recommended fix

Handle dm_444_8 (dm_mono_8) -> 1 byte/element in get_bytes_per_element and bail out of get_surf_rq_param when bytes_per_element==0 (ASSERT is only WARN_ON in release builds; see fix.diff)

Verdict

Hardware-gated (guest has no AMD/DCN GPU) AND currently mapping-gated (the sole caller, calcs/dcn_calcs.c:353-377, maps every non-420/64bpp UABI format to dm_444_32 and never emits dm_444_8), so the in-kernel panic is not reachable today β€” but the arithmetic defect is certain and was proven EXACTLY on the DragonFly guest by compiling the REAL, unmodified dml1_display_rq_dlg_calc.c (plus real dcn_calc_math.c/dml_common_defs.c/DML headers from the guest's /usr/src, only dm_services.h stubbed to release-build WARN_ON semantics) and driving it with a realistic DCN1 config: source_format=dm_444_8 (the enum value aliased dm_mono_8, display_mode_enums.h:42-43) plus dm_sw_linear executes blk256_width = 256 / bytes_per_element at dml1_display_rq_dlg_calc.c:641 with bytes_per_element==0 -> integer division by zero -> SIGFPE (rc=136, 3/3 runs), the userland analog of the kernel #DE panic. Second zero-divisor at :745 is dead code at -O2 (meta_blk_width unused) and dml_log2(0)==-127 (dcn_bw_log bit-trick) silently programs garbage RQ/DLG registers for non-linear unhandled formats (harness mono8: stored_swath=0, nonsense). Patched real file (fix.diff) : all three modes rc=0 with sane 1-byte-per-element values. Same verification convention as the DF-3869 dce_calcs #DE family.