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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4373 Β· 11 files
FileTypeDescriptionSize
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)

  1. CRTC: any mode, e.g. 640x480.
  2. 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.
  3. 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 sketch
  • VERDICT.md β€” full static proof chain and honest reproduction status
  • fix.diff β€” one-guard fix (returns 0 = "no lines supported" so validation fails cleanly)
VERDICT.md
↓ download raw

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/dri does not exist; pciconf -l shows no vendor 0x1002 device; the amdgpu DC path (dce_transform.c is 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". See build.log / run.log.
  • Therefore the division could not be executed. Per the audit's status mapping, device-absent on the guest = not_reproduced with impact none observed on this guest; the analysis below is the deliverable.

Why the bug is real (static proof chain, every step cited)

  1. 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_ONLY suffices): 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.

  2. Plane rects are never clipped by amdgpu_dm. fill_rects_from_plane_state (amdgpu_dm.c:2210-2238) copies raw crtc_x/y/w/h, rejects only zero dimensions, and sets clip_rect = dst_rect (:2238). amdgpu_dm never calls drm_atomic_helper_check_plane_state (verified: dm_plane_atomic_check at amdgpu_dm.c:3585-3603 only calls fill_rects + dc_validate_plane).

  3. Validation is format-only. dc_validate_plane (dc_resource.c:2891): "TODO For now validates pixel format only" β†’ returns DC_OK.

  4. The β‰₯16 guard tests the wrong rectangle. calculate_viewport (dc_resource.c:543) computes viewport.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, so viewport.width < 16 (dc_resource.c:1122) does NOT trip.

  5. 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.)

  6. prescaler_on forces recout to be used. dce_transform.c:1359 sets prescaler_on = true unconditionally in dce_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.

  7. ASSERT does not stop execution. os_types.h:48: #define ASSERT(expr) WARN_ON(!(expr)) β€” :675 warns and falls through to return (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_testable
baseline no→ patch + rebuild →patched clean

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

Confirmed kernel references

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.