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

get_firmware_info_v3_2 can return crystal_frequency == 0 β€” divide-by-zero (#DE panic) in DCE clock-source PLL math

Summary

v3_2 stores crystal_frequency = dce_refclk_10khz*10 and only backfills when the smu_info table's revision.minor is exactly 2 or 3. The smu_info revision is parsed without any table-presence check (a missing table makes GET_IMAGE(type,0) return the image base whose magic yields minor=0x2A), and any other smu revision equally skips the fallback. BP_RESULT_OK with zero reference clock -> calc_pll_max_vco_construct -> calculate_fb_and_fractional_fb_divider divides by it on the first mode set -> #DE panic. v3_1 backfills unconditionally; v3_2 is the gap. The DF-4153 zero-reference-clock twin inside DC. Require-table + unconditional-fallback fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4167 Β· 20 files
FileTypeDescriptionSize
README.md β€” 3.9 KB ↓ raw
VERDICT.md β€” 10.1 KB ↓ raw
rig/gen_rig.sh β€” 8.0 KB view raw
rig/check_verbatim.sh β€” 4.6 KB view raw
rig/fw_slice_verbatim.h β€” 15.1 KB view raw
rig/fw_slice_verbatim.c β€” 20.4 KB view raw
rig/rig_shims.h β€” 5.0 KB view raw
rig/fw_rig.c β€” 14.6 KB view raw
rig/Makefile β€” 407 B ↓ download
rig/build.sh β€” 425 B view raw
rig/run.sh β€” 798 B view raw
build.log β€” 3.1 KB view raw
run.log β€” 14.1 KB view raw
run.2.log β€” 14.1 KB view raw
fix_validation.log β€” 4.6 KB view raw
code_hashes.txt β€” 650 B view raw
env.txt β€” 640 B view raw
fix.diff β€” 1.4 KB view raw
verdict.json β€” 8.9 KB view raw
manifest.json β€” 1.8 KB view raw

DF-4167 β€” get_firmware_info_v3_2 returns crystal_frequency == 0 β†’ divide-by-zero (#DE) in the DCE clock-source PLL math

Verified via the verbatim-code rig (rig/)

The production parser and the production PLL sink math were sed-extracted byte-identical from sys/ (gen_rig.sh), re-verified block-by-block on the DragonFly guest against /usr/src/sys (check_verbatim.sh: 34 blocks identical to production + the fix-variant equivalence proof), compiled on the guest, and driven with crafted ATOM images:

  • parser: bios_parser_get_firmware_info (:1119 dispatch) β†’ get_firmware_info_v3_2 (:1204) / get_firmware_info_v3_1 (:1157, control)
  • sink: calc_pll_max_vco_construct (:1142) β†’ calculate_pixel_clock_pll_dividers (:287) β†’ calc_pll_dividers_in_range (:245) β†’ calc_fb_divider_checking_tolerance (:189) β†’ calculate_fb_and_fractional_fb_divider (:130)

Hardware gating: the guest has only Bochs SVGA (0x1234:1111), no dev.drm, no amdgpu (env.txt) β€” the path never executes in this guest's kernel; in-kernel execution requires an AMD ASIC presenting the crafted table (flashed ROM, vfio-pci romfile=, malicious device), which is exactly the finding's threat model.

Build / run (on the DragonFly guest)

cd rig && sh ./build.sh /usr/src     # verbatim check (vs guest /usr/src/sys) + cc
cd rig && sh ./run.sh                # baseline (production) + fixed (fix.diff)

Modes

layout, fw-v32-nosmu (smu_info missing), fw-v32-smu1 (smu revision 1), fw-v32-smu2 / fw-v32-smu3 / fw-v31 (controls), sink-ssdp-nosmu, sink-ssdp-smu1, sink-ssdp-post-smu1 (full chain, SS/DP mode set with VBIOS-adjusted dividers β€” the :145 path), sink-plain-smu1 (observation).

Expected output (see run.log / run.2.log)

Baseline (production code): - fw-v32-nosmu: revision parsed at the image base (missing table β†’ GET_IMAGE(.., 0) β†’ bios base; bytes [2]=0x10 [3]=0xE0 β†’ major=16 minor=32) β†’ fallback SKIPPED β†’ BP_RESULT_OK with crystal_frequency = 0 - fw-v32-smu1: smu revision.minor = 1 β†’ fallback SKIPPED β†’ OK + 0 - sink-ssdp-post-smu1: SIGFPE si_code=2 (FPE_INTDIV) with RIP inside calculate_fb_and_fractional_fb_divider β€” the only zero divisor on that path is ref_freq_khz at dce_clock_source.c:145 β†’ kernel equivalent #DE panic - sink-plain-smu1: SIGFPE inside calculate_pixel_clock_pll_dividers (0/0 at :336) β€” pre-existing: v3.x never fills pll_info input frequencies (v3_1 equally affected); still faults in the fixed build β€” incidental observation, not DF-4167's gap - controls: smu2 β†’ 27000, smu3 β†’ 100000, v3.1 (zero refclk) β†’ 27000 (v3_1 backfills unconditionally β€” the contrast that isolates the gap)

Fixed (-DAPPLY_DF4167_FIX = fix.diff exactly, checker-proven): - fw-v32-nosmu β†’ BP_RESULT_BADBIOSTABLE (require-table check) - fw-v32-smu1 β†’ OK with 27000 - sink-ssdp-post-smu1 β†’ ref_freq 27000, full divider search completes (feedback_divider=11 … vco=307998), no fault on the identical image

Verdict

Reproduced (verbatim-rig evidence class): the exact production parser code returns BP_RESULT_OK with a zero reference clock on the two claimed triggers, and the exact production clock-source math divides by that zero at the cited line. fix.diff (require smu_info presence + unconditional fallback) eliminates it β€” A/B validated in the same rig. See VERDICT.md, verdict.json, build.log, run.log, run.2.log.

VERDICT.md
↓ download raw

DF-4167 β€” VERDICT (verify run, 2026-10-15)

Status: REPRODUCED (verbatim-rig evidence class). The parser accepts a zero reference clock with BP_RESULT_OK on both claimed triggers, and the production PLL math divides by that zero at the exact cited line (dce_clock_source.c:145) β€” SIGFPE/FPE_INTDIV with RIP inside the cited function, on the guest, in code byte-identical to the running kernel's source. In-kernel execution is AMD-ASIC-gated on this guest (no AMD GPU β€” proven, see env.txt). Fix A/B validated in the same rig: fixed.

1. What was verified and how

Same evidence class as DF-4165 (this file's sibling): the guest cannot run amdgpu/dc (Bochs SVGA 0x1234:1111 only, sysctl dev.drm unknown, no drm/amdgpu KLDs β€” env.txt), so the actual production code was compiled into a guest-side harness:

  • rig/gen_rig.sh sed-extracts, byte-identical from the read-only sys/ tree: get_atom_data_table_revision (bios_parser2.c:133-151), bios_parser_get_firmware_info (:1119-1155, revision dispatch), get_firmware_info_v3_1 (:1157-1202, control), get_firmware_info_v3_2 (:1204-1280, the defect), bios_get_image (bios_parser_helper.c:36-44), and the full production PLL sink chain from dce_clock_source.c: calculate_fb_and_fractional_fb_divider (:130-170), calc_fb_divider_checking_tolerance (:189-243), calc_pll_dividers_in_range (:245-285), calculate_pixel_clock_pll_dividers (:287-388), and calc_pll_max_vco_construct (:1142-1218) β€” plus every struct they touch (atomfirmware.h tables verbatim under the production #pragma pack(1) at :207/:2934, grph_object_ctrl_defs.h dc_firmware_info, clock_source.h pll_settings/calc_pll_clock_source*, the exact DATA_TABLES/GET_IMAGE macros).
  • rig/check_verbatim.sh re-proved all 34 blocks byte-identical, run both against the host sys/ tree and on the guest against /usr/src/sys, the tree its running kernel (Jul 2 2026 build) was compiled from: ALL VERBATIM CHECKS PASSED (build.log). The checker additionally applies fix.diff to the production function and proves the APPLY_DF4167_FIX variant compiled into rig_fixed is exactly that patch result (87 lines, OK … == fix.diff applied to bios_parser2.c:1204-1280).
  • Harness shims (rig_shims.h) touch nothing on the verified path: minimized dc_bios/bios_parser (first-member layout, container_of identity), div_u64/div_u64_rem = plain 64-bit hardware division (exactly what x86-64 kernels compile these to β€” including the divide-by-zero fault: kernel #DE ↔ userland SIGFPE/FPE_INTDIV), DC_LOG_ERROR log-only, kzallocβ†’placement, LE-host le*_to_cpu identity.

The crafted image is a minimal ATOM ROM: master data table v2_1 @0x20 with firmwareinfo v3.2 (format_revision 3, content_revision 2) @0x100 driving the production dispatch (switch (revision.major) case 3: minor 2 β†’ v3_2), dce_info v4_1 @0x200 with dce_refclk_10khz = 0, and β€” per mode β€” smu_info missing (master-table offset 0) or present with content_revision 1/2/3.

2. Observed (run.log, stable across run.2.log)

Parser level β€” the defect, exactly as filed

  • fw-v32-nosmu (smu_info table MISSING): the missing-table path proven β€” GET_IMAGE(header, DATA_TABLES(smu_info)==0) passes (0+4 < bios_size) and returns the image base, whose bytes become the "table header": revision parsed as major = bios[2]&0x3f = 16, minor = bios[3]&0x3f = 32 β€” neither 2 nor 3 β†’ fallback skipped β†’ BP_RESULT_OK with pll_info.crystal_frequency = 0. (The filed summary's byte arithmetic "minor=0x2A from 0x55 0xAA" is off in which byte lands in minor β€” the u16 structuresize eats bytes 0-1, minor comes from byte 3 β€” but the substance is identical: a missing table parses garbage that is not 2/3.)
  • fw-v32-smu1 (smu_info present, revision.minor = 1): fallback skipped β†’ OK + 0 β€” the "any other smu revision" trigger.
  • Controls: smu revision 2 β†’ 27000, revision 3 β†’ 100000 (the conditional fallback works when the revision matches); v3.1 firmwareinfo with zero refclk β†’ 27000 unconditionally (bios_parser2.c:1187-1188) β€” the exact contrast that isolates v3_2 as the gap.

Sink level β€” the #DE, at the cited line

  • sink-ssdp-post-smu1 (full production chain: parser β†’ calc_pll_max_vco_construct stores ref_freq_khz = 0 at dce_clock_source.c:1159 β†’ first mode set on the SS/DP path where the VBIOS AdjustPixelClock exec table has filled both reference_divider and pix_clk_post_divider (dce_clock_source.c:447-450)): SIGFPE si_code=2 (FPE_INTDIV), RIP inside calculate_fb_and_fractional_fb_divider (range proven via printed function bounds). On that path every other divisor is a compile-time or struct non-zero (fract factor 10^6, precision factor, post_div β‰₯ 1, ref_div = 1, constants); the only zero divisor is calc_pll_cs->ref_freq_khz at dce_clock_source.c:145 div_u64(feedback_divider, calc_pll_cs->ref_freq_khz) β€” kernel equivalent: #DE β†’ kernel panic on the first mode set.
  • Why both VBIOS-adjusted dividers are needed (traced in production source): with only reference_divider preset, v3_2's entirely-zero-filled pll_info (min/max output VCO = 0) makes max_post_divider = 0 and the function exits gracefully at the "Post divider range is invalid" check (dce_clock_source.c:358-362) before dividing (mode sink-ssdp-smu1: rc=0). With pix_clk_post_divider also preset (:304-306) the range check is bypassed exactly as in production and the :145 division runs.
  • sink-plain-smu1 (no VBIOS-adjusted dividers β€” plain mode set): SIGFPE inside calculate_pixel_clock_pll_dividers β€” 0/0 at :336-337 (ref_freq_khz / max_pll_input_freq_khz, both zero). Incidental and pre-existing: the divisor there is zero because v3.x never fills pll_info.min/max_input_pxl_clk_pll_frequency at all β€” v3_1 is equally affected (the fixed build still faults here with 27000/0). Recorded as an observation, NOT DF-4167's gap.

3. Primitive and impact

Primitive: crafted-VBIOS-controlled kernel divide-by-zero (#DE panic) at first mode set, driver-init→modeset timeframe. Gating: requires an AMD ASIC whose VBIOS carries firmwareinfo v3.2/v3.3 with dce_refclk_10khz = 0 and a missing/mismatched smu_info revision — i.e. crafted or corrupted GPU firmware (flashed ROM, vfio-pci romfile override, malicious device). No unprivileged userland trigger exists (the values come from the GPU ROM copied to kernel memory at attach).

In-kernel reachability of the faulting sink, traced precisely: calc_pll_max_vco_construct is called only by dce110_clk_src_construct (dce_clock_source.c:1220), used by dce80/dce100/dce110 clock sources β€” while dal_bios_parser_create (bios_parser_interface.c:41) tries firmware_parser_create (bios_parser2) first for every ASIC, and the v3_x dispatch is purely table-revision driven. A crafted new-format ROM on those ASICs therefore reaches get_firmware_info_v3_2 β†’ zero clock β†’ #DE. Genuine DCE11x/DCE12/DCN10 clock sources use dce112_clk_src_construct (:1323) which never runs the calc-PLL math; there the zero flows into dce_i2c_hw reference clock (dce_i2c_hw.c:108) and res_pool->ref_clock_inKhz (dc_resource.c:162) as misconfiguration (no division), and DDC timing silently breaks. The finding's intermediate chain ("dce120_clock_source_create β†’ dce112_clk_src_construct β†’ calc_pll_max_vco_construct") is off on the middle hop (dce112's construct does not call calc_pll_max_vco_construct in this tree) β€” the sink is reached via the dce110-family construct; the defect and the crash are unaffected.

Impact recorded as dos (crash-class floor demonstrated in the rig; the guest kernel never executed the path).

4. Fix validation (A/B, identical crafted image)

rig_fixed = same verbatim slice + exactly fix.diff (checker-proven equal to the patch applied to production) under -DAPPLY_DF4167_FIX:

  • fw-v32-nosmu β†’ BP_RESULT_BADBIOSTABLE (require-table check); the bad table no longer yields an OK result.
  • fw-v32-smu1 β†’ OK with 27000 (unconditional fallback).
  • sink-ssdp-post-smu1 β†’ ref_freq_khz = 27000, the full divider search completes (feedback_divider=11, fract=407407, post_div=2, vco=307998), no fault.
  • sink-plain-smu1 β†’ still faults at :336 with 27000/0 (pre-existing v3_1-equivalent unfilled-pll_info issue; out of scope for this fix β€” see notes; the fix's require-table path would also reject a fully-missing table before this could matter).

Baseline faults, patched slice computes on the same input β†’ fix validated (run.log FIXED sections). A rebuilt-kernel validation was not performed: the fix site executes in-kernel only on AMD ASICs absent from this guest (per the verbatim-rig protocol the fixed slice was A/B-validated in the same rig). fix.diff remains authored against the pristine read-only sys/ tree and was never applied there.

5. Honest limitations

  • Demonstrated with production instructions and production struct layouts, but in a userland mapping, not the kernel's text/heap; #DE ↔ SIGFPE is the standard equivalence for this rig class.
  • The :145 sink requires the VBIOS AdjustPixelClock table to supply non-zero reference_divider + pix_clk_post_divider (realistic for SS/DP mode sets and attacker-controlled under the crafted-ROM model), because v3_2's zero-filled pll_info otherwise trips the graceful post-divider-range error first.
  • The :336 0/0 fires even without those, but its decisive zero is the unfilled input-frequency field (pre-existing, v3_1-equivalent), not the crystal_frequency gap this finding is about.
  • The crafted image is synthetic (ROM header + master table + the three tables); a real crafted ROM must also satisfy earlier parser stages, which do not constrain these fields.

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

A/B validated in the verbatim rig: identical crafted images, same slice, only fix.diff under -DAPPLY_DF4167_FIX (checker-proven equal to the patch applied to the production function). Baseline: OK+zero clock on both triggers; SIGFPE at dce_clock_source.c:145 (SS/DP path). Fixed: missing smu_info -> BP_RESULT_BADBIOSTABLE (clock-source construction fails cleanly via calc_pll_max_vco_construct's != BP_RESULT_OK check); rev-1 smu_info -> 27000; full PLL divider search completes with no fault. The separate :336 0/0 (unfilled pll_info input freqs, v3_1-equivalent) is out of scope for this fix and still faults in sink-plain mode with 27000/0. Kernel-rebuild validation not performed: the fix site executes in-kernel only on AMD ASICs absent from this guest; per the verbatim-rig protocol the fixed slice was validated in the same rig. fix.diff authored against the pristine read-only sys/ tree, never applied there.

fix_validation.log (baseline vs fixed A/B extract); run.log FIXED sections; build.log (APPLY_DF4167_FIX variant == fix.diff applied to bios_parser2.c:1204-1280); fix.diff
↓ fix.diffper-fix-DF-4167

Confirmed kernel references

Detail

Exploit chain

Hardware/firmware attacker model only: craft VBIOS with firmwareinfo v3.2/v3.3 (content_revision 2/3) and dce_refclk_10khz=0 plus a missing or non-2/3 smu_info revision -> amdgpu attach -> dal_bios_parser_create -> bios_parser2 get_firmware_info_v3_2 returns OK with crystal_frequency=0 -> calc_pll_max_vco_construct stores ref_freq_khz=0 (dce_clock_source.c:1159) -> first mode set with VBIOS-adjusted dividers (SS/DP) -> div_u64(feedback_divider, 0) at :145 -> #DE kernel panic (DoS). No uid0 potential (crash-only primitive).

Evidence (decisive lines)

run.log / run.2.log (all modes baseline vs fixed, stable across two runs), fix_validation.log (A/B extract), build.log (34x 'identical to production' incl. guest /usr/src/sys + fix-variant==patch-application proof + zero-warning builds), VERDICT.md (full narrative incl. sink-reachability trace and limitations), rig/ (sources), env.txt (hardware gating), code_hashes.txt

PoC changes

Seed pack had no runnable PoC (status untested, hardware-gated). Built the verbatim rig from scratch: gen_rig.sh extracts the production parser slice AND the dce_clock_source.c PLL sink chain byte-identical (both sides of the finding); check_verbatim.sh proves block identity (host tree AND guest /usr/src/sys) and -- new vs the DF-4165 checker -- reconstructs the fixed function by applying fix.diff to the production text and proves the APPLY_DF4167_FIX variant equals it; harness drives parser modes (missing/rev1/rev2/rev3/v3.1) and three sink modes with SIGFPE site classification (function-range + only-zero-divisor analysis). Fixed two harness offset bugs found during the run (smu bootup_dcefclk and dce_refclk field offsets now via offsetof on the verbatim structs) and added fflush before child _exit so fixed-build child output is captured.

Verified recommended fix

Require the smu_info table (presence + header NULL check) and make the crystal_frequency fallback unconditional (27000, 100000 for smu rev 3) - fix.diff, A/B validated in the rig.

Verdict

REPRODUCED via the verbatim-code rig: get_firmware_info_v3_2 (bios_parser2.c:1204-1280), its dispatcher bios_parser_get_firmware_info (:1119-1155), the v3_1 control (:1157-1202), bios_get_image (bios_parser_helper.c:36-44) AND the full production PLL sink chain from dce_clock_source.c (calc_pll_max_vco_construct :1142-1218, calculate_pixel_clock_pll_dividers :287-388, calc_pll_dividers_in_range :245-285, calc_fb_divider_checking_tolerance :189-243, calculate_fb_and_fractional_fb_divider :130-170) were sed-extracted byte-identical from sys/ (34 blocks re-verified identical ON THE GUEST against /usr/src/sys, the tree its running kernel was built from; the checker also proves the APPLY_DF4167_FIX build is exactly fix.diff applied to the production function). Crafted ATOM image with firmwareinfo v3.2 + dce_refclk_10khz=0. Baseline production code: (a) smu_info table MISSING -> GET_IMAGE(..,0) returns the image base, its bytes are mis-parsed as the table header (major=bios[2]&0x3f=16, minor=bios[3]&0x3f=32), fallback skipped -> BP_RESULT_OK with pll_info.crystal_frequency=0; (b) smu_info present with revision.minor=1 -> same OK+0. Controls: smu rev 2->27000, rev 3->100000, v3.1+zero refclk->27000 UNCONDITIONALLY (:1187-1188), isolating v3_2 as the gap. Sink: feeding the parser result through the verbatim clock-source chain, the SS/DP mode-set path (VBIOS AdjustPixelClock fills reference_divider AND pix_clk_post_divider, dce_clock_source.c:447-450) reaches calculate_fb_and_fractional_fb_divider and dies of SIGFPE/FPE_INTDIV with RIP inside that function -- the only zero divisor on the path is ref_freq_khz at dce_clock_source.c:145 div_u64(feedback_divider, ref_freq_khz) (kernel equivalent: #DE panic at first mode set). Without the post-divider preset, v3_2's zero-filled pll_info trips the graceful 'Post divider range is invalid' exit first; without any VBIOS-adjusted divider the plain path 0/0s at :336-337 (ref_freq/max_pll_input, divisor zero from the separately pre-existing unfilled-pll_info issue that affects v3_1 equally -- still faults in the fixed build; recorded as incidental, not this finding's gap). In-kernel fault-path reachability traced: calc_pll_max_vco_construct is called only by dce110_clk_src_construct (dce80/dce100/dce110 clock sources), and dal_bios_parser_create tries bios_parser2 FIRST for every ASIC with purely table-revision-driven dispatch, so a crafted new-format ROM on those ASICs hits v3_2 -> #DE; genuine DCE11x/DCE12/DCN10 clock sources (dce112_clk_src_construct) never run the calc-PLL math and only misconfigure I2C/ref-clock consumers (dce_i2c_hw.c:108, dc_resource.c:162) -- the finding's middle-hop citation (dce112 construct calling calc_pll_max_vco_construct) does not hold in this tree, the dce110-family construct does. Gating: AMD ASIC + crafted/corrupted VBIOS (flashed ROM, vfio romfile, malicious device); no unprivileged userland trigger; guest has no AMD GPU (Bochs 0x1234:1111, no dev.drm, no amdgpu - env.txt), so the guest kernel never executes the path. fix.diff (require smu_info presence + NULL header check + unconditional 27000/100000 fallback) A/B-validated in the same rig on identical images: missing table -> BP_RESULT_BADBIOSTABLE, rev-1 table -> 27000, full PLL math completes (feedback_divider=11, vco=307998) with no fault.