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

Dead stats module carries a latent length==0 off-by-one OOB write in mod_stats_update_event (unsigned wrap β†’ event_string[-1]); module not compiled by any Makefile β€” delete it or guard

Summary

length==0 wraps copy_length-1 to 0xFFFFFFFF and writes one byte before the array (net-zero clobbered by the following entry_id store); gcc -Warray-bounds flags the exact expression in the byte-exact clone run on the guest. Module not compiled by any Makefile (dead) - filed Info so it is deleted or guarded before wiring. Guard-or-delete fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4912 Β· 10 files
FileTypeDescriptionSize
README.md β€” 2.1 KB ↓ raw
VERDICT.md β€” 3.2 KB ↓ raw
stats_len0_demo.c β€” 2.1 KB view raw
build.sh β€” 104 B view raw
run.sh β€” 89 B view raw
run.log β€” 843 B view raw
env.txt β€” 347 B view raw
fix.diff β€” 557 B view raw
verdict.json β€” 2.7 KB view raw
manifest.json β€” 878 B view raw

DF-4912 β€” stats module: latent OOB write in mod_stats_update_event on length == 0, in code that is not compiled into any kernel build

File: sys/dev/drm/amd/display/modules/stats/stats.c:316-321 Severity: Info | CWE-787 (latent) / dead code | confidence: certain

Two facts, both proven without hardware

  1. The module is dead code. sys/dev/drm/amd/amdgpu/Makefile:49-51 builds display/modules/{freesync,color,info_packet} β€” not modules/stats. No file in sys/ references modules/stats or calls any mod_stats_* function (repo-wide rg β€” zero hits outside the module itself and its header). The object is never compiled, never linked, never callable.

  2. Latent defect. If the module were ever wired up: c copy_length = length; /* stats.c:316 */ if (length > MOD_STATS_EVENT_STRING_MAX) copy_length = MOD_STATS_EVENT_STRING_MAX; memcpy(&events[index].event_string, event_string, copy_length); events[index].event_string[copy_length - 1] = '\0'; /* :321 */ With length == 0 this executes event_string[-1] = '\0' β€” a one-byte write before the 512-byte array, i.e. into the high byte of entry_id (offset 3 of struct stats_event_cache, little-endian) β€” CWE-787 off-by-one. The very next statement (events[index].entry_id = core_stats->entry_id;, :323) rewrites that exact word, so the corruption is self-clobbering; net effect on the struct is zero. No caller exists to pass 0 today.

Build / run (semantic demonstration of the off-by-one only)

./build.sh   # cc -o stats_len0_demo stats_len0_demo.c
./run.sh     # standalone extraction of the exact kernel logic; shows the
             # write lands at buffer[-1] and is then overwritten

The demo proves the index arithmetic; it does NOT execute kernel code (none exists in the build).

Recommendation

Delete sys/dev/drm/amd/display/modules/stats/ (matches upstream direction β€” the module was never wired into the amdgpu driver), or β€” if it is kept for future wiring β€” apply fix.diff (guard length == 0 / clamp to MOD_STATS_EVENT_STRING_MAX - 1).

VERDICT.md
↓ download raw

VERDICT β€” DF-4912

status: untested β€” deliberately: the vulnerable code is not part of any kernel build, so there is no kernel artifact to test. reproduced: 0 (kernel path) / semantic off-by-one demonstrated in userspace (run.log). impact: none (dead code + self-clobbering write). Severity Info.

Fact 1 β€” the module is dead code (certain)

  • sys/dev/drm/amd/amdgpu/Makefile:49-51 compiles display/modules/freesync, display/modules/color, display/modules/info_packet β€” stats is absent (confirmed both in the audit tree and in the guest's /usr/src, run.log).
  • Repo-wide search (rg -rn "modules/stats" sys/ and rg mod_stats_*): zero hits outside modules/stats/stats.c itself and modules/inc/mod_stats.h. No allocation, no call, no linkage. sys/conf/files has no stats entry.

Fact 2 β€” latent off-by-one OOB write (certain, demonstrated)

mod_stats_update_event (stats.c:296-326):

copy_length = length;                                  /* :316 */
if (length > MOD_STATS_EVENT_STRING_MAX)               /* :317 */
    copy_length = MOD_STATS_EVENT_STRING_MAX;
memcpy(&events[index].event_string, event_string, copy_length);
events[index].event_string[copy_length - 1] = '\0';    /* :321 β€” OOB at [-1] when length==0 */

length == 0 β†’ copy_length - 1 wraps to 0xFFFFFFFF (unsigned) β†’ the compiler itself proves it: building the exact clone with gcc 8.3 on the guest emits warning: array subscript 4294967295 is above array bounds of 'char[512]' [-Warray-bounds] (run.log). At runtime the write lands at event_string[-1] = offset 3 of struct stats_event_cache = the MSB of entry_id on little-endian β€” and stats.c:323 then assigns entry_id wholesale, clobbering the OOB byte. Net memory effect: zero. No caller exists to pass length == 0 regardless.

Other stats.c candidates checked and killed

  • mod_stats_reset_data (:283-287): memset(core_stats->time, 0, sizeof * 0) when disabled β€” memset(NULL, 0, 0), benign in the kernel; entries/event bounds in all four update functions are correctly guarded (:310, :340, :385, :433); v_sync_time_in_us[num_vsyncs] bounded by the < MOD_STATS_NUM_VSYNCS check (:392); time[index - 1] safe because index starts at 1 (:166); concurrent update threads can at worst double-increment the index past entries but every write is re-guarded, so no OOB; mod_stats_dump guards both array walks (:235, :239).
  • kcalloc(entries ≀ 0x01000000, ~120B) β‰ˆ 2 GB max on crafted persistent data β€” allocation failure is handled (:148, :156), and dm_read_persistent_data has no live backing store for this key on DF.

Fix

Delete the module (matches upstream direction; it was never wired into the amdgpu driver) or apply fix.diff (guard length == 0, clamp to MOD_STATS_EVENT_STRING_MAX - 1).

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

Not applicable: the defective code is not compiled into any kernel or module build; there is no baseline to patch-and-compare.

sys/dev/drm/amd/amdgpu/Makefile:49-51 (no stats entry); fix.diff provided for the delete-later alternative.
↓ fix.diffper-fix-DF-4912

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (guest: Makefile lists freesync/color/info_packet only; -Warray-bounds warning 'array subscript 4294967295'; demo output 'PROVEN: write landed at event_string[-1]'), VERDICT.md (killed-candidates list), fix.diff.

PoC changes

No seeded PoC; wrote stats_len0_demo.c (byte-exact clone of the kernel logic); first build failed on missing , fixed and rerun.

Verified recommended fix

Delete modules/stats (never wired into the amdgpu build) or guard length==0 and clamp copy_length to MOD_STATS_EVENT_STRING_MAX-1 (fix.diff).

Verdict

sys/dev/drm/amd/display/modules/stats/stats.c is dead code: sys/dev/drm/amd/amdgpu/Makefile:49-51 builds modules/{freesync,color,info_packet} but not modules/stats, and no file in sys/ calls any mod_stats_* function (verified on the guest's /usr/src too). Inside the never-compiled module, mod_stats_update_event has a latent off-by-one: with length==0, copy_length-1 wraps unsigned and event_string[-1]='\0' writes one byte before the 512-byte array (into entry_id's MSB; stats.c:323 then rewrites entry_id wholesale, so net corruption is zero). gcc 8.3's -Warray-bounds flags the exact expression when the clone is built (run.log); the standalone demo shows the write lands at offset 3 and is clobbered. Info severity: recommend deleting the module or applying fix.diff.