Divide-by-zero in dce_transform_get_max_num_of_supported_lines: prescaler_on + fully-off-CRTC plane (or underscan-inset stream dst) yields pixel_width==0 β ASSERT is WARN_ON-only, integer #DE kernel panic from an unprivileged atomic TEST_ONLY check
Summary
The >=16 guard tests the WRONG rectangle (viewport width = source pixels; a 640-wide-source/1-wide-dst plane flush off the CRTC edge keeps viewport.width=640, bypassing it), and calculate_recout clamps to EXACTLY zero for that geometry; prescaler_on (set unconditionally in dce_transform_construct) forces the zero recout to be used as pixel_width; ASSERT compiles to WARN_ON so execution falls through to 'return max_pixels_supports / pixel_width' -> #DE panic. Second zero source: the underscan hborder property insets stream->dst while src stays full-mode, giving recout.width=0 for a fully-visible 1px plane. Full static proof chain UABI-to-instruction in the pack. Same class as DF-3828/3857/3869. Return-0-on-degenerate fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4373 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.0 KB | β raw | |
| VERDICT.md | β | 4.6 KB | β raw | |
| trigger_df4373.c | β | 3.7 KB | view raw | |
| build.sh | β | 121 B | view raw | |
| run.sh | β | 258 B | view raw | |
| build.log | β | 47 B | view raw | |
| run.log | β | 189 B | view raw | |
| env.txt | β | 405 B | view raw | |
| fix.diff | β | 840 B | view raw | |
| verdict.json | β | 4.1 KB | view raw | |
| manifest.json | β | 1.4 KB | view raw |
DF-4373 β Divide-by-zero (#DE panic) in dce_transform_get_max_num_of_supported_lines
What
sys/dev/drm/amd/display/dc/dce/dce_transform.c:710 divides
max_pixels_supports / pixel_width guarded only by ASSERT(pixel_width)
(:675), and ASSERT in this tree compiles to WARN_ON only
(sys/dev/drm/amd/display/dc/os_types.h:48 β #define ASSERT(expr) WARN_ON(!(expr))).
Execution continues into the integer division; pixel_width == 0 produces a
divide-error fault in kernel mode β kernel panic (local DoS).
Who can trigger
Any DRM master holder β on a desktop/seat system that is the ordinary
unprivileged session user (logind gives the session DRM master). A
DRM_MODE_ATOMIC_TEST_ONLY commit suffices: the division happens on the
atomic-check path (dc_validate_global β resource_build_scaling).
Concrete trigger (real DCE100/110/112/120 hardware)
- CRTC: any mode, e.g. 640x480.
- Plane: source β₯ 16x16 (e.g. 640x480 dumb buffer), CRTC_X = 640 (== mode width: flush off the right edge), CRTC_Y = 0, CRTC_W = 1, CRTC_H = 480.
- Submit atomic commit (TEST_ONLY is enough).
Why this is accepted by every check:
- fill_rects_from_plane_state (amdgpu_dm.c:2210-2238) never clips to the
CRTC and only rejects zero-sized rects; clip_rect = dst_rect.
- dc_validate_plane (dc_resource.c:2891-2897) validates pixel format only.
- calculate_viewport (dc_resource.c:543): viewport.width = clip.width *
surf_src.width / dst_rect.width = 1 * 640 / 1 = 640 β viewport is in
SOURCE pixels, so the viewport.width < 16 guard (dc_resource.c:1122)
PASSES even though the visible recout is empty.
- calculate_recout (dc_resource.c:631-643): recout.x = 640; clamp at
:639-643 gives recout.width = (0 + 640) - 640 = 0 exactly.
- dce_transform_get_optimal_number_of_taps (dce_transform.c:896-907):
prescaler_on is hard-coded true at construction (:1359); viewport.width
(640) > recout.width (0) β pixel_width = recout.width = 0.
- dce_transform_get_max_num_of_supported_lines (:667-711): ASSERT no-ops β
division by zero at :710.
An underscan variant also works (connector "underscan hborder" property,
range 0..128 β amdgpu_display.c:605-607, stream dst inset at amdgpu_dm.c:2502-2506):
a 1px-wide plane fully inside the CRTC with hborder 128 on a 640-wide mode
gives recout.width = 1*512/640 = 0 at dc_resource.c:637-638.
Build / Run (probe)
./build.sh && ./run.shβ compiles the probe and attempts to open/dev/dri/card0. On machines without an AMD DC GPU it reports the device absence (this is what happens on the audit QEMU guest, which has only QEMU std VGA, vendor 0x1234).
Expected on real hardware
Immediate kernel panic: Fatal trap 12: divide error fault... (actually trap 0
DE) in dce_transform_get_max_num_of_supported_lines, during
amdgpu_dm_atomic_check.
Files
trigger_df4373.cβ standalone probe / ioctl sketchVERDICT.mdβ full static proof chain and honest reproduction statusfix.diffβ one-guard fix (returns 0 = "no lines supported" so validation fails cleanly)
DF-4373 VERDICT β divide-by-zero in dce_transform.c:710
Status: not_reproduced β target hardware absent (no AMD GPU in QEMU guest). The exploit chain is proven statically, link by link, from UABI to the faulting instruction.
Reproduction attempt (honest)
- Guest: DragonFly 6.5-DEVELOPMENT
#0 Thu Jul 2 06:02:54 UTC 2026, QEMU std VGA only (vgapci0@pci0:0:2:0 class=0x030000 chip=0x11111234). /dev/dridoes not exist;pciconf -lshows no vendor 0x1002 device; the amdgpu DC path (dce_transform.cis compiled into the amdgpu module, sys/dev/drm/amd/amdgpu/Makefile:12-13) cannot attach.- Probe built cleanly (
cc -O2) and run: exit 2, "cannot open /dev/dri/card0". Seebuild.log/run.log. - Therefore the division could not be executed. Per the audit's status
mapping, device-absent on the guest =
not_reproducedwith impactnoneobserved on this guest; the analysis below is the deliverable.
Why the bug is real (static proof chain, every step cited)
-
Unprivileged reachability. Modeset ioctls require DRM master; on any modern seat/desktop the session user holds DRM master (root not required otherwise). The faulting path runs during atomic check (
DRM_MODE_ATOMIC_TEST_ONLYsuffices): amdgpu_dm_atomic_check β dc_validate_global β resource_build_scaling (dc_resource.c:1119-1147) β transform_get_optimal_number_of_taps β dce_transform_get_max_num_of_supported_lines β division at :710. -
Plane rects are never clipped by amdgpu_dm.
fill_rects_from_plane_state(amdgpu_dm.c:2210-2238) copies rawcrtc_x/y/w/h, rejects only zero dimensions, and setsclip_rect = dst_rect(:2238). amdgpu_dm never callsdrm_atomic_helper_check_plane_state(verified: dm_plane_atomic_check at amdgpu_dm.c:3585-3603 only calls fill_rects + dc_validate_plane). -
Validation is format-only.
dc_validate_plane(dc_resource.c:2891): "TODO For now validates pixel format only" β returns DC_OK. -
The β₯16 guard tests the wrong rectangle.
calculate_viewport(dc_resource.c:543) computesviewport.width = clip.width * surf_src.width / plane dst_rect.widthβ SOURCE pixels. A plane with src 640 wide, dst 1 wide, positioned flush off the right edge keeps viewport.width = 640, soviewport.width < 16(dc_resource.c:1122) does NOT trip. -
recout clamps to exactly zero.
calculate_recout(dc_resource.c:631-643): with stream dst = [0,640) and raw plane clip.x = 640:recout.x = 0 + (640-0)*640/640 = 640; clamp at :639-643:recout.width = (0+640) - 640 = 0. (struct rect fields are signed ints, dc_hw_types.h:107-112; the zero is exact, not an underflow.) -
prescaler_on forces recout to be used. dce_transform.c:1359 sets
prescaler_on = trueunconditionally indce_transform_construct(used by DCE100/110/112/120 resource pools β dce100_resource.c:523-533, dce110_resource.c:539-549, dce112_resource.c:538-548, dce120_resource.c:743-753). dce_transform.c:900-902:viewport.width(640) > recout.width(0)βpixel_width = 0. -
ASSERT does not stop execution. os_types.h:48:
#define ASSERT(expr) WARN_ON(!(expr))β :675 warns and falls through toreturn (max_pixels_supports / pixel_width)at :710 β integer #DE in kernel mode β trap β kernel panic.
Alternative zero source (no off-CRTC positioning): underscan. The connector
property "underscan hborder" (range 0..128, amdgpu_display.c:605-607) insets
stream->dst (amdgpu_dm.c:2502-2506) while stream->src stays the full mode;
a fully-visible 1px-wide plane then yields recout.width = clip.width *
dst.width / src.width = 1*512/640 = 0 at dc_resource.c:637-638.
Impact
Local DoS (kernel panic) by an unprivileged session user on any DragonFly machine with a DCE100/110/112/120-era AMD GPU driven by the in-tree amdgpu DC code. Same class as DF-3828/DF-3857/DF-3869 (DCE watermark divide families).
Fix validation
Not possible on this guest (no AMD GPU; kernel build with the fix cannot be
exercised on DCE hardware). fix.diff is authored against the read-only tree
and returns 0 lines-supported for degenerate input, which makes
dce_transform_get_optimal_number_of_taps return false (:910-911 with
max_num_of_lines==0), failing validation cleanly instead of dividing.
fix_status: not_testable (hardware absent).
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 authored and git-apply-able against the read-only tree (verified with git apply --check); cannot build/boot a DCE kernel on the QEMU guest (no AMD GPU), so patched behavior could not be exercised. The guard makes get_optimal_number_of_taps return false for degenerate recout, failing atomic validation with EINVAL instead of panicking.
fix.diff; VERDICT.md 'Fix validation' section
Confirmed kernel references
- sys/dev/drm/amd/display/dc/dce/dce_transform.c:710
- sys/dev/drm/amd/display/dc/dce/dce_transform.c:675
- sys/dev/drm/amd/display/dc/dce/dce_transform.c:896
- sys/dev/drm/amd/display/dc/dce/dce_transform.c:1359
- sys/dev/drm/amd/display/dc/core/dc_resource.c:637
- sys/dev/drm/amd/display/dc/core/dc_resource.c:1122
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm.c:2210
- sys/dev/drm/amd/display/dc/os_types.h:48
Detail
Exploit chain
session user (DRM master) -> atomic TEST_ONLY commit, plane src 640x480, dst CRTC_X=640/CRTC_W=1 on a 640x480 CRTC -> amdgpu_dm_atomic_check -> dc_validate_global -> resource_build_scaling -> dce_transform_get_optimal_number_of_taps -> dce_transform_get_max_num_of_supported_lines -> integer divide by zero -> kernel panic (DoS). No memory-corruption primitive; impact ceiling is denial of service.
Evidence (decisive lines)
VERDICT.md (full cited chain, 7 steps); run.log (guest probe: /dev/dri absent, NO_AMD_PCI); build.log (cc 8.3 BUILD_OK); fix.diff (git-apply-able guard, applies clean); README.md (underscan variant + trigger recipe)
PoC changes
Seed sketch reduced to a standalone raw-ioctl probe (no libdrm on guest); full libdrm atomic trigger recipe documented in README.md/VERDICT.md since the guest lacks the required AMD GPU.
Verified recommended fix
Guard the division in dce_transform_get_max_num_of_supported_lines: return 0 (no lines supported) when pixels_per_entries==0 or pixel_width<=0 so validation fails cleanly.
Verdict
Divide-by-zero at sys/dev/drm/amd/display/dc/dce/dce_transform.c:710 (max_pixels_supports / pixel_width) guarded only by an ASSERT that compiles to WARN_ON (os_types.h:48). Reachable end-to-end from an unprivileged DRM-master session user with a UABI-legal atomic plane state: amdgpu_dm never clips plane rects to the CRTC (fill_rects_from_plane_state, amdgpu_dm.c:2210-2238) and dc_validate_plane checks format only (dc_resource.c:2891); a plane positioned flush off the right edge of the stream dst window makes calculate_recout's clamp yield recout.width == 0 exactly (dc_resource.c:639-643) while calculate_viewport keeps viewport.width >= 16 in source pixels (dc_resource.c:543), so the >=16 guard at dc_resource.c:1122 passes; prescaler_on is hard-coded true (dce_transform.c:1359) so pixel_width takes the 0 recout width and the integer division faults (#DE panic). Could not be executed on the audit QEMU guest because it has no AMD GPU (QEMU std VGA 0x1234:1111, no /dev/dri, no 0x1002 PCI device) β probe built and run on the guest, device absent. Same class as the DF-3828/DF-3857/DF-3869 DCE divide family. Local DoS (kernel panic) on real DCE100/110/112/120 hardware.
No comments yet.