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)
PoC verification
Evidence pack
findings/poc/DF-4167 Β· 20 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
- File:
sys/dev/drm/amd/display/dc/bios/bios_parser2.c:1204-1280(get_firmware_info_v3_2) - Sink:
sys/dev/drm/amd/display/dc/dce/dce_clock_source.c:145(div_u64(feedback_divider, calc_pll_cs->ref_freq_khz)), fed viacalc_pll_max_vco_construct(:1159) - Class: divide-by-zero (#DE kernel panic / DoS), CWE-369. Hardware-gated (AMD ASIC + crafted VBIOS image). The DF-4153 zero-reference-clock twin inside DC.
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.
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.shsed-extracts, byte-identical from the read-onlysys/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), andcalc_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.hdc_firmware_info, clock_source.hpll_settings/calc_pll_clock_source*, the exactDATA_TABLES/GET_IMAGEmacros).rig/check_verbatim.shre-proved all 34 blocks byte-identical, run both against the hostsys/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 theAPPLY_DF4167_FIXvariant compiled intorig_fixedis 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_ERRORlog-only, kzallocβplacement, LE-hostle*_to_cpuidentity.
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_OKwithpll_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_constructstoresref_freq_khz = 0at dce_clock_source.c:1159 β first mode set on the SS/DP path where the VBIOS AdjustPixelClock exec table has filled bothreference_dividerandpix_clk_post_divider(dce_clock_source.c:447-450)):SIGFPE si_code=2 (FPE_INTDIV), RIP insidecalculate_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 iscalc_pll_cs->ref_freq_khzat dce_clock_source.c:145div_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_dividerpreset, v3_2's entirely-zero-filledpll_info(min/max output VCO = 0) makesmax_post_divider = 0and the function exits gracefully at the "Post divider range is invalid" check (dce_clock_source.c:358-362) before dividing (modesink-ssdp-smu1: rc=0). Withpix_clk_post_divideralso preset (:304-306) the range check is bypassed exactly as in production and the:145division runs. - sink-plain-smu1 (no VBIOS-adjusted dividers β plain mode set):
SIGFPEinsidecalculate_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 fillspll_info.min/max_input_pxl_clk_pll_frequencyat 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
:145sink requires the VBIOS AdjustPixelClock table to supply non-zeroreference_divider+pix_clk_post_divider(realistic for SS/DP mode sets and attacker-controlled under the crafted-ROM model), because v3_2's zero-filledpll_infootherwise trips the graceful post-divider-range error first. - The
:3360/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
fixedA/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
Confirmed kernel references
- sys/dev/drm/amd/display/dc/bios/bios_parser2.c:1229
- sys/dev/drm/amd/display/dc/bios/bios_parser2.c:1257
- sys/dev/drm/amd/display/dc/bios/bios_parser2.c:1259
- sys/dev/drm/amd/display/dc/bios/bios_parser2.c:1187
- sys/dev/drm/amd/display/dc/dce/dce_clock_source.c:1159
- sys/dev/drm/amd/display/dc/dce/dce_clock_source.c:145
- sys/dev/drm/amd/display/dc/dce/dce_clock_source.c:336
- sys/dev/drm/amd/display/dc/bios/bios_parser_helper.c:40
- sys/dev/drm/amd/display/dc/bios/bios_parser_interface.c:41
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.
No comments yet.