# 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`
