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

build_evenly_distributed_points() with num_entries==0 writes 24 bytes past a kcalloc(3) heap object; num_entries==1 divides by zero (kernel #DE panic)

Summary

The exported color-module API passes ramp->num_entries unvalidated after allocating only num_entries+3 entries. n==0: the three unconditional 'extra' points write p[3] - 24 bytes past the 72-byte object - and p_last=p[-1] is an OOB read feeding those writes. n==1: dc_fixpt_from_fraction(0, n-1)=0/0 -> div64 #DE. Latent: the only in-tree caller validates {256,4096} and the module is not wired into any kernel config - an exported-API hazard for any future caller trusting a user-controlled LUT size. HARNESS-PROVEN on the guest with the verbatim kernel fixed-point engine: SIGSEGV at exactly obj+72 (5/5 at -O2 and -O0), SIGFPE for n==1, control n==256 faithful; fix guard kills both. num_entries>=2 validation fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4220 Β· 18 files
FileTypeDescriptionSize
README.md readme summary + reproduce instructions 3.3 KB ↓ raw
VERDICT.md verdict full narrative: reproduced arithmetic, reachability analysis, fix validation 5.5 KB ↓ raw
trigger/color_gamma_harness.c poc-source verbatim build_evenly_distributed_points + fault-catching scaffolding 7.2 KB view raw
trigger/fixpt31_32.c verbatim-kernel-copy sha256 == sys/dev/drm/amd/display/dc/basics/fixpt31_32.c 10.4 KB view raw
trigger/include/fixed31_32.h verbatim-kernel-copy sha256 == sys/dev/drm/amd/display/include/fixed31_32.h 11.2 KB view raw
trigger/dm_services.h stub-header release ASSERT semantics + exact div64_u64_rem 1.1 KB view raw
trigger/color_gamma_harness_fixed.c fixed-variant harness + the one-line guard from fix.diff 7.3 KB view raw
trigger/build.sh build-script 438 B view raw
trigger/run.sh run-script 297 B view raw
trigger/build.log build-log clean -O2 -Wall build on guest 30 B view raw
trigger/run.log run-log decisive run: SIGFPE + guard-page SIGSEGV + control PASS 576 B view raw
trigger/run.2.log run-log repeat run 576 B view raw
trigger/run.3.log run-log repeat run 576 B view raw
trigger/run.O0.log run-log -O0 build, faults identical 576 B view raw
trigger/fix_run.log fix-log patched variant: faults gone, control passes 604 B view raw
env.txt environment guest uname, compiler, sha256 fidelity record 913 B view raw
fix.diff suggested-fix boundary validation + in-function guard 1.4 KB view raw
verdict.json verdict machine verdict 3.8 KB view raw
README.md readme summary + reproduce instructions
↓ download raw

DF-4220 β€” color module accepts degenerate gamma point counts: n==0 β‡’ 24-byte heap OOB write, n==1 β‡’ divide-by-zero

File: sys/dev/drm/amd/display/modules/color/color_gamma.c (pass 2)

What

build_evenly_distributed_points() (color_gamma.c:1066-1107) is called with ramp->num_entries straight from the public module API (mod_color_calculate_regamma_params color_gamma.c:1407-1410; mod_color_calculate_degamma_params color_gamma.c:1617-1620) with no validation. For degenerate counts the function misbehaves with release-kernel semantics (ASSERT is WARN_ON, os_types.h:76 β€” a no-op):

  • n == 0: the callers allocate kcalloc(num_entries + 3) = 3 entries (color_gamma.c:1382-1384, 1392-1393, 1593-1595, 1602-1604) but the function always writes three "extra" points past the n loop points (color_gamma.c:1092-1106) β‡’ p[3] is a 24-byte heap OOB write past a 72-byte object, and p_last = p + n - 1 = p[-1] is an OOB read.
  • n == 1: the loop computes dc_fixpt_from_fraction(0, n-1) = 0/0 (color_gamma.c:1081-1082) β‡’ div64_u64_rem(x, 0) (math64.h:75-80, plain %//) β‡’ #DE β‡’ kernel panic on amd64.

Reachability (why this is Low / latent)

The only in-tree caller is amdgpu_dm_color.c, which validates num_entries ∈ {256, 4096} before calling (amdgpu_dm_color.c:148-157 and :270-277). The AMD display stack is a standalone module source tree not referenced by sys/conf/files (no dev/drm/amd entries) and not in the GENERIC kernel config β€” hardware (AMD GPU) and module gated. The defect is therefore an exported-API latent hazard: any future caller that trusts a user-controlled LUT size (e.g. a non-atomic legacy DRM gamma path or a port of newer amdgpu_dm) turns it into a heap overflow / panic.

Reproduce (arithmetic harness, on the audit guest)

cd findings/poc/DF-4220/trigger
./build.sh          # cc -O2 -o harness color_gamma_harness.c fixpt31_32.c
./run.sh

fixpt31_32.c and include/fixed31_32.h are verbatim copies of the kernel files (sha256-identical, see env.txt); build_evenly_distributed_points in color_gamma_harness.c is a verbatim copy of color_gamma.c:1066-1107; dm_services.h stubs ASSERT as a no-op (release semantics) and provides the exact div64_u64_rem from math64.h.

Expected output (guest DragonFly 6.5-DEVELOPMENT, gcc 8.3):

T3: PASS 256 points monotone 0..1, extras 0xaaaaaaab 0x80000000 0x66666666
T2: ... FAULT: SIGFPE at address 0x0        (n==1 β‡’ 0/0 divide; kernel: #DE panic)
T1: ... FAULT: SIGSEGV at address 0x800474000   (== obj end + guard page β‡’ p[3] OOB write)

T1 places the 72-byte object flush against a PROT_NONE guard page; the reported fault address is exactly obj + 72 β€” the write of the third extra point. Reproduced 4/4 runs at -O2 and 1/1 at -O0 (run.log, run.2.log, run.3.log, run.O0.log).

Fix validation

color_gamma_harness_fixed.c = same harness with the one-line guard from fix.diff inserted (if (numberof_points < 2) return;). Result (fix_run.log): T2/T1 RETURN WITHOUT FAULT, T3 control still PASS.

Files

  • VERDICT.md β€” full narrative
  • trigger/ β€” harness + verbatim kernel copies + build/run scripts + full logs
  • fix.diff β€” boundary validation + in-function guard
  • manifest.json / verdict.json
VERDICT.md verdict full narrative: reproduced arithmetic, reachability analysis, fix validation
↓ download raw

DF-4220 VERDICT β€” reproduced (arithmetic, on-guest harness); kernel path latent

Bottom line

build_evenly_distributed_points() in sys/dev/drm/amd/display/modules/color/color_gamma.c:1066-1107 trusts its numberof_points argument. The module's two public entry points pass ramp->num_entries through unvalidated (color_gamma.c:1407-1410 and 1617-1620) after allocating num_entries + _EXTRA_POINTS entries (color_gamma.c:1382-1384/1392-1393 and 1593-1595/1602-1604). Two degenerate inputs fault, with release-kernel semantics (ASSERT == WARN_ON no-op, sys/dev/drm/amd/display/dc/os_types.h:76):

  1. n == 0 β€” the do/while writes one point (p[0]), then the three "extra" points are written at p[1], p[2], p[3] (color_gamma.c:1092-1106). With the n==0-sized allocation of 3 entries (indices 0..2), p[3] is a 24-byte heap OOB write past a 72-byte kcalloc object; p_last = p + n - 1 = p - 1 (color_gamma.c:1078) is additionally an OOB read (24 bytes below the object) whose value feeds the three OOB writes.
  2. n == 1 β€” the loop's single iteration computes dc_fixpt_from_fraction(0, numberof_points - 1) = 0/0 (color_gamma.c:1081-1082) β‡’ complete_integer_division_u64 (sys/dev/drm/amd/display/dc/basics/fixpt31_32.c:43-55) calls div64_u64_rem(dividend, 0, ...) β€” plain %// per sys/dev/drm/include/linux/math64.h:75-80 β€” β‡’ integer divide-by-zero: SIGFPE in userspace, #DE β‡’ kernel panic in-kernel. The ASSERT(divisor) at fixpt31_32.c:50 does not prevent this in release builds.

How it was verified

Guest: DragonFly dfbsd 6.5-DEVELOPMENT #0 ... x86_64 (see env.txt). Harness trigger/color_gamma_harness.c:

  • compiles the verbatim kernel fixed-point engine (trigger/fixpt31_32.c, sha256 bc09111d… == kernel file) against the verbatim include/fixed31_32.h (sha256 a6bdd016…);
  • contains a verbatim copy of build_evenly_distributed_points (color_gamma.c:1066-1107) with the exact dividers the callers build (color_gamma.c:1401-1403);
  • dm_services.h stub reproduces release ASSERT semantics (no-op) and the exact div64_u64_rem from math64.h.

Results (4 runs at -O2, 1 at -O0 β€” run.log, run.2.log, run.3.log, run.O0.log):

  • T3 control n=256: monotone 0..1, extras exactly 2/3, 1/2, 2/5 (0xaaaaaaab / 0x80000000 / 0x66666666) β€” proves faithful arithmetic.
  • T2 n=1: SIGFPE caught (the 0/0 division). 5/5.
  • T1 n=0: 72-byte object placed flush against a PROT_NONE guard page β‡’ SIGSEGV at exactly obj+72 β€” the p[3] extra-point write. 5/5. (The p[-1] read stays mapped in this layout; it is OOB by construction and is the read that feeds the OOB writes on a real heap.)

So the defect is reproduced as module code executing the exact faulty arithmetic; impact=none because no reachable kernel path exists in this tree today: the sole in-tree caller validates num_entries ∈ {256, 4096} (amdgpu_dm_color.c:148-157, 270-277), and the whole AMD display tree is standalone module source not wired into sys/conf/files nor X86_64_GENERIC (no AMD GPU on the audit guest either). Threat: latent exported-API hazard β€” any future caller passing a user-controlled LUT size (legacy DRM gamma ioctl, newer amdgpu_dm ports) converts this into a heap overflow (n==0) or panic (n==1).

Exploit chain

None developed β€” the write is 24 bytes of partially-controlled fixed-point values at obj+72..95 on kmalloc-72-class memory, but the path is not reachable in-tree; escalation work would be against a hypothetical caller.

Fix validation

fix.diff adds (a) the missing boundary validation to both public entries (after the pre-existing early-returns so the documented !mapUserRamp && num_entries==0 ROM-SRGB fast path at color_gamma.c:1584-1589 keeps its current behavior), and (b) an in-function guard if (numberof_points < 2) return; replacing the ineffective ASSERT.

Validated in trigger/fix_run.log via color_gamma_harness_fixed.c (same harness + the in-function guard): T2/T1 RETURN WITHOUT FAULT, T3 control unchanged. fix_baseline_reproduced=1 (faults present), fix_patched_reproduced=0 (faults gone). A kernel-level rebuild was not performed: the file is not part of any kernel config in this tree (module source only), so a kernel build would validate nothing beyond what the harness already shows.

Kernel references

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 logic validated in the same on-guest harness (color_gamma_harness_fixed.c): baseline faults (SIGFPE + guard-page SIGSEGV) are gone with the one-line guard, control case unchanged. Kernel-level rebuild not applicable: color_gamma.c is standalone module source not compiled into any kernel config in this tree.

trigger/fix_run.log: 'T2: RETURNED WITHOUT FAULT', 'T1: RETURNED WITHOUT FAULT', T3 PASS β€” versus trigger/run.log faults on the unpatched variant.
↓ fix.diffper-fix-DF-4220

Confirmed kernel references

Detail

Evidence (decisive lines)

trigger/run.log: 'FAULT: SIGFPE at address 0x0' (T2, n==1) and 'FAULT: SIGSEGV at address 0x800474000' (T1, n==0; fault address == obj end = the p[3] extra-point write); T3 control PASS with extras exactly 2/3,1/2,2/5 proving faithful arithmetic; trigger/fix_run.log: with the one-line guard from fix.diff both fault tests RETURN WITHOUT FAULT. Verbatim copies sha256-matched in env.txt.

PoC changes

self-authored harness; stub dm_services.h provides release ASSERT semantics (no-op, os_types.h:76) and the exact div64_u64_rem from math64.h so a zero divisor raises SIGFPE exactly as the kernel takes #DE

Verified recommended fix

Validate ramp->num_entries >= 2 in mod_color_calculate_regamma_params/mod_color_calculate_degamma_params and replace the ineffective ASSERT with a hard 'if (numberof_points < 2) return;' in build_evenly_distributed_points

Verdict

Reproduced on the audit guest as module code: build_evenly_distributed_points() (color_gamma.c:1066-1107) with numberof_points==0 writes 3 'extra' points past a kcalloc(n+3)=3-entry object (24-byte heap OOB write at obj+72, SIGSEGV on a guard page, 5/5 runs at -O2/-O0) and with numberof_points==1 computes dc_fixpt_from_fraction(0,0) => divide-by-zero (SIGFPE; kernel #DE panic). impact=none because no kernel path is reachable in this tree: the only in-tree caller validates num_entries in {256,4096} (amdgpu_dm_color.c:148-157,270-277) and the AMD display tree is standalone module source not wired into sys/conf/files. Latent exported-API hazard: a future caller trusting a user-controlled LUT size turns it into heap overflow / panic.