⬢ DragonFlyBSD Kernel Audit
DF-4146 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-4146",
  "status": "reproduced",
  "verdict": "REPRODUCED via the verbatim-code rig (68 blocks byte-identical to the guests /usr/src/sys, including the verbatim atom_parse_data_header and master-table dispatch). All three trust violations demonstrated at exact predicted addresses. (1) Unvalidated u16 array offsets: usStateArrayOffset=0x1000 on a 0x1000-byte image -> state_array=bios+0x1500 -> the :1767 ucNumEntries read faults past the BIOS image guard page; atom_parse_data_header demonstrably performs no extent validation. (2) Unbounded stride walk: ucNumDPMLevels=255 state at the image end -> :1788 idx[j] faults at exactly bios+image_end (j=1); with a readable tail the OOB bytes DRIVE PARSING -- idx[1..3] read past the state entry select clock entries 5/6/7 (sclk 5777/6777/7777) and idx[253] (state+254) reads the NON-CLOCK ARRAYs own ucNumEntries byte as a clock index (sclk 3777), num_levels=5 from a 1-entry state array. (3) ucNumEntries vs ucNumStates sizing mismatch: frev-4 image sizes power_state[] for ucNumStates=1 (radeon_atombios.c:2592, 48-byte entries measured) while the trinity loop runs to ucNumEntries=255 -> power_state[1].clock_info read at allocation_end+8 faults in the guard page; measured worst-case read reach 254*48+8 ~ 12.2 KB past a 1-entry allocation (finding estimated ~25 KB -- same class). The NULL-word variant (:1777 NULL power_state[1].clock_info) aborts parse with -EINVAL whose simulated swallow per radeon_pm.c:1448-1453 leads into the demonstrated DF-4143 NULL-boot_ps fault at 0x28. Gating: no AMD GPU on the audit guest (env.txt); kernel-context consequence is the panic chain above plus OOB heap reads used only as indices (exfiltration stays limited, as the finding itself states). fix.diff (offset validation against the table size captured by a second verbatim header parse + walk bound with unwind + i>=num_power_states loop bound) A/B-validated: every 4146 fault mode rejected cleanly at parse, canary/good unchanged.",
  "reproduced": true,
  "impact": "dos",
  "confidence": "certain",
  "exploit_chain": "Crafted VBIOS on a Trinity/Richland APU -> radeon_atombios_get_power_modes legacy pre-parse sizes power_state[] by pplib.ucNumStates (frev 4/5, radeon_atombios.c:2592) or ucNumOfPowerModeEntries (frev 1-3, :2120) -> trinity_dpm_init -> trinity_parse_power_table dereferences bios+data_offset+unvalidated u16 offsets (:1756-:1767), walks states with an unbounded 2+ucNumDPMLevels stride reading idx[j] past the states array and the image (:1787-:1804), and indexes power_state[i] for i<state_array->ucNumEntries (:1771/:1777) -> OOB reads of the BIOS image and up to ~12.2 KB past the legacy power_state allocation; a NULL/OOB word aborts parse with -EINVAL -> radeon_pm.c:1448 discards it -> enable -> DF-4143 NULL-boot_ps page fault (demonstrated chain). No unprivileged-userland trigger: attacker data is the GPU ROM.",
  "evidence": "run.log (4146-offset: FAULT at bios+0x1500; 4146-stride: FAULT at bios+0x2000 exactly; 4146-canary: 5 levels parsed from OOB bytes incl. the non-clock array header byte; 4146-powerguard: FAULT at power_state allocation end+8; 4146-nullabort: -EINVAL abort -> swallowed -> enable fault 0x28; FIXED: all rejected rc=-22, canary/good unchanged), run.2.log, build.log (68x identical + DF-4146 fix lines == fix.diff additions (21 lines)), rig/, VERDICT.md, env.txt",
  "kernel_refs": [
    "sys/dev/drm/radeon/trinity_dpm.c:1756",
    "sys/dev/drm/radeon/trinity_dpm.c:1767",
    "sys/dev/drm/radeon/trinity_dpm.c:1771",
    "sys/dev/drm/radeon/trinity_dpm.c:1777",
    "sys/dev/drm/radeon/trinity_dpm.c:1787",
    "sys/dev/drm/radeon/trinity_dpm.c:1804",
    "sys/dev/drm/radeon/atom.c:1366",
    "sys/dev/drm/radeon/radeon_atombios.c:2592",
    "sys/dev/drm/radeon/radeon_atombios.c:2693"
  ],
  "poc_changes": "Seed pack had no runnable PoC. Built the verbatim rig from scratch (shared with DF-4143/4144/4145). Measurement corrections vs the seed: sizeof(struct radeon_power_state)=48 on x86_64, so claim (3)s read reach is ~12.2 KB (not ~25 KB) past a 1-entry allocation; also demonstrated that the stride walk crosses into the ADJACENT non-clock array within the image (idx[253] reads its header byte), a stronger data-flow proof than the seeds. Incidental observation recorded for the orchestrator (no new finding filed): the VCE loop :1809-:1818 indexes clockInfo[] without a ucNumEntries check.",
  "attempts": 3,
  "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul  2 06:02:54 UTC 2026  root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64",
  "runtime_sec": 150,
  "guest_dirty": 0,
  "build_cmd": "cd rig && sh ./build.sh /usr/src DF-4146",
  "run_cmd": "cd rig && sh ./run.sh",
  "code_hash": "trinity_rig.c a68eb1438bc82fb79063491af6293aeddf208ec2cf4f46f0c6040a1aed69e1a3; trinity_slice_verbatim.c 6afb17099769a0c4c9cd500356a757a12cf22d8e9bd67a710b452236019ba7c6",
  "notes": "All fault addresses match layout arithmetic from the verbatim structs. The canary mode intentionally stays within the declared table size and therefore behaves identically in baseline and fixed builds (the fix bounds the walk to the table, which the canary walk respects by construction) -- it is the data-flow proof, not a memory-safety fault mode. pp_size is itself VBIOS-controlled: the fix closes the demonstrated image-escape modes; carrying the true BIOS image length through atom_context would be the complete fix (cross-file, out of scope). Guest left up and clean.",
  "recommended_fix": "Validate usStateArrayOffset/usClockInfoArrayOffset/usNonClockInfoArrayOffset (+minimal headers) against the PowerPlayInfo table size, bound the per-state walk (state+2+ucNumDPMLevels within the table) with unwind, and stop the loop at rdev->pm.num_power_states -- fix.diff, A/B validated.",
  "fix_status": "fixed",
  "fix_baseline_reproduced": true,
  "fix_patched_reproduced": false,
  "fix_verdict": "A/B validated in the verbatim rig on identical crafted images: baseline faults at bios+0x1500 (offset), bios+0x2000 (stride), power_state end+8 (count mismatch) and aborts into the DF-4143 chain (NULL word); fixed build rejects all four at parse (init rc=-22) with clean unwinding and the enable guard -- no fault; canary and good-table modes unchanged (no over-rejection of in-table layouts). Kernel-rebuild validation not performed (AMD-only path, guest has no AMD GPU); validated in the rig per the established protocol.",
  "fix_evidence": "run.log FIXED sections (RESULT: OK (fixed) parse rejected the crafted table ... / count-bound reject + enable guard, no fault); build.log (DF-4146 fix lines == fix.diff additions (21 lines)); fix.diff"
}