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)
PoC verification
Evidence pack
findings/poc/DF-4220 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 thenloop points (color_gamma.c:1092-1106) βp[3]is a 24-byte heap OOB write past a 72-byte object, andp_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 narrativetrigger/β harness + verbatim kernel copies + build/run scripts + full logsfix.diffβ boundary validation + in-function guardmanifest.json/verdict.json
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):
- 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
kcallocobject;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. - 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) callsdiv64_u64_rem(dividend, 0, ...)β plain%//persys/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, sha256bc09111dβ¦== kernel file) against the verbatiminclude/fixed31_32.h(sha256a6bdd016β¦); - contains a verbatim copy of
build_evenly_distributed_points(color_gamma.c:1066-1107) with the exactdividersthe callers build (color_gamma.c:1401-1403); dm_services.hstub reproduces release ASSERT semantics (no-op) and the exactdiv64_u64_remfrom 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
- sys/dev/drm/amd/display/modules/color/color_gamma.c:1066-1107 (function)
- sys/dev/drm/amd/display/modules/color/color_gamma.c:1081-1082 (0/0 division)
- sys/dev/drm/amd/display/modules/color/color_gamma.c:1092-1106 (3 extra points β OOB write at n==0)
- sys/dev/drm/amd/display/modules/color/color_gamma.c:1382-1384, 1392-1393, 1593-1595, 1602-1604 (n+3 allocations)
- sys/dev/drm/amd/display/modules/color/color_gamma.c:1407-1410, 1617-1620 (unvalidated pass-through)
- sys/dev/drm/amd/display/dc/basics/fixpt31_32.c:43-55, 67-82 (division, ASSERT no-op)
- sys/dev/drm/amd/display/dc/os_types.h:72-77 (ASSERT == WARN_ON in release)
- sys/dev/drm/include/linux/math64.h:75-80 (div64_u64_rem)
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm_color.c:148-157, 270-277 (only in-tree caller validates)
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
inconclusiveFix 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.
Confirmed kernel references
- sys/dev/drm/amd/display/modules/color/color_gamma.c:1066-1107
- sys/dev/drm/amd/display/modules/color/color_gamma.c:1081-1082
- sys/dev/drm/amd/display/modules/color/color_gamma.c:1092-1106
- sys/dev/drm/amd/display/modules/color/color_gamma.c:1382-1384
- sys/dev/drm/amd/display/modules/color/color_gamma.c:1593-1595
- sys/dev/drm/amd/display/modules/color/color_gamma.c:1407-1410
- sys/dev/drm/amd/display/modules/color/color_gamma.c:1617-1620
- sys/dev/drm/amd/display/dc/basics/fixpt31_32.c:43-55
- sys/dev/drm/amd/display/dc/os_types.h:72-77
- sys/dev/drm/include/linux/math64.h:75-80
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm_color.c:148-157
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.
No comments yet.