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

[amdgpu_pm.c] Deterministic kernel NULL-deref panic in the pp_power_profile_mode store: strsep returns NULL on un-delimited writes and isspace(*tmp_str) dereferences address 0; buf_cpy[128] also never NUL-terminated

Summary

The sysfs store parses CUSTOM-profile writes with a token walk whose skip loop runs unconditionally after strsep; DF strsep sets *stringp=NULL when the remainder holds no delimiter, so any write not ending in ' ' or newline leaves tmp_str NULL and isspace(*NULL) faults at address 0. Secondary: the buf_cpy copy never writes a terminator, so for unshaped input the walk continues into uninitialized kernel stack. VERIFIED LIVE on the guest via a kernel-module replica running the verbatim delimiter walk: Fatal trap 12 at fault address 0x0 at the isspace byte load, guest dead on the first attempt. NUL-terminate + bound + NULL-guard fix. (amdgpu_pm.c had zero pass-1 findings; filed from the amd_powerplay dispatch audit.)

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4366 Β· 12 files
FileTypeDescriptionSize
README.md β€” 2.9 KB ↓ raw
VERDICT.md β€” 4.0 KB ↓ raw
amdgpu_parser_replica.c β€” 4.6 KB view raw
fix.diff β€” 834 B view raw
build.sh β€” 159 B view raw
run.sh β€” 210 B view raw
build.log β€” 5.5 KB view raw
run.log β€” 1.3 KB view raw
panic.txt β€” 2.1 KB view raw
env.txt β€” 362 B view raw
verdict.json β€” 4.3 KB view raw
manifest.json β€” 1.3 KB view raw

DF-4366 β€” amdgpu_set_pp_power_profile_mode: strsep NULL deref panic + unterminated stack walk

File: sys/dev/drm/amd/amdgpu/amdgpu_pm.c:916-975 (the sysfs store for pp_power_profile_mode, i.e. the entry that dispatches into pp_set_power_profile_mode / amd_powerplay.c:888-910) Β· Severity: Low (root-only sysfs, deterministic kernel panic) Β· Class: CWE-476 NULL deref (+ CWE-125 uninitialized-stack overread)

Bug (source)

The CUSTOM-profile parse loop:

944:  memcpy(buf_cpy, buf, count-i);          /* buf_cpy[128], NO NUL written */
946:  tmp_str = buf_cpy;
947:  while (tmp_str[0]) {
948:      sub_str = strsep(&tmp_str, delimiter);   /* {' ','\n','\0'} */
949:      ret = kstrtol(sub_str, 0, &parameter[parameter_size]);
...
955:      while (isspace(*tmp_str))           /* tmp_str == NULL after the
956:          tmp_str++;                          last un-delimited token */

DragonFly strsep (sys/libkern/strsep.c:63-65) sets *stringp = NULL when it consumes the remainder without finding a delimiter. A write that does not end in ' '/'\n' β€” e.g. printf '5 1 2 3' > /sys/class/drm/card0/device/pp_power_profile_mode (no echo-added newline) β€” copies count-i bytes ending exactly on the sysfs NUL sentinel, the final token's strsep matches the '\0' delimiter, returns tmp_str = NULL, and the very next isspace(*tmp_str) (sys/sys/ctype.h:43 expands to a comparison on *tmp_str) dereferences NULL β†’ Fatal trap 12, fault address 0x0.

Secondary: buf_cpy[128] is never NUL-terminated by the code (termination falls out accidentally from the off-by-one copy that includes the sysfs sentinel); any input shape where i == 0 (no space after the leading profile digit) copies count bytes with no sentinel and the walk continues into uninitialized kernel stack until it happens to hit a zero byte.

Sibling handler amdgpu_set_pp_od_clk_voltage (:543-606) copies count+1 bytes (includes the sentinel) and cannot exceed 63 tokens, so it does not have this bug β€” the delta proves the profile-mode loop is the defect.

Reproduce (guest, mechanism replica β€” guest has no AMD GPU)

./build.sh    # ships + builds amdgpu_parser_replica.ko
./run.sh      # kldload; sysctl kern.amdgparun=1  -> guest PANICS

The module runs the exact amdgpu_pm.c:946-957 walk (same 3-delimiter strsep set, same isspace skip) on the buffer printf '5 1 2 3' > … would deliver (count=7, i=1, copy = "1 2 3\0"). Expected serial console:

amdgparun: walking 6-byte content, last byte 0x00
Fatal trap 12: page fault while in kernel mode
fault virtual address   = 0x0
Stopped at      run_parse.part.1+0xe0:  movzbl  (%rax),%edx
db>

Guest sits in DDB; reset with vm.sh reset with-src afterwards.

Fix

Bound parameter_size (< 64), NUL-terminate buf_cpy explicitly (buf_cpy[count-i] = '\0'), and guard the skip loop (while (tmp_str != NULL && isspace(*tmp_str))) β€” see fix.diff.

VERDICT.md
↓ download raw

DF-4366 β€” VERDICT

Status: inconclusive (amdgpu sysfs entry hardware-gated on this guest; parse-loop mechanism reproduced deterministically via kernel-module replica that panics the guest). Confidence: certain. Severity: Low.

Root cause (source-certain)

amdgpu_set_pp_power_profile_mode() (sys/dev/drm/amd/amdgpu/amdgpu_pm.c:916, the store handler of the pp_power_profile_mode sysfs attribute registered at :1025-1027, S_IRUGO | S_IWUSR) parses CUSTOM-profile writes (profile_mode == PP_SMC_POWER_PROFILE_CUSTOM == 5, kgd_pp_interface.h:135) with:

  • :944 memcpy(buf_cpy, buf, count-i); β€” buf_cpy[128], never explicitly NUL-terminated; count ≀ 127 is the only bound (:940-941);
  • :948 strsep(&tmp_str, delimiter) with delimiter = {' ', '\n', '\0'} (:931);
  • :955-956 while (isspace(*tmp_str)) tmp_str++; β€” executed immediately after strsep, with no NULL check.

DragonFly strsep (sys/libkern/strsep.c) returns the trailing token and sets *stringp = NULL when the remainder contains no delimiter. For a write whose last byte is not ' ' or '\n' β€” trivially produced with printf '%s' "5 1 2 3" > ...pp_power_profile_mode instead of echo β€” the final strsep matches the '\0' delimiter (copied in from the sysfs sentinel by the off-by-one count-i copy), sets tmp_str = NULL, and isspace(*tmp_str) (sys/sys/ctype.h:43 β€” plain comparisons on *tmp_str) dereferences address 0.

The numeric tail (parameter[], parameter_size) is bounded in practice: delimiters terminate inputs yield ≀ 63 tokens within count ≀ 127, and the 64-token unterminated shape NULL-panics before the parameter[parameter_size] store β€” so the stack OOB write is not reachable; the reachable defects are (a) the deterministic NULL deref and (b) the unterminated buf_cpy walk into uninitialized stack for i == 0 input shapes.

Guest reproduction (mechanism replica) β€” panic.txt, run.log

The guest has no AMD GPU (virtio-net/emulated VGA only), so the amdgpu sysfs entry cannot be exercised directly. The replica module (amdgpu_parser_replica.c) runs the verbatim delimiter walk (same strsep 3-delimiter set, same isspace skip, same copy shape count-i from the advanced pointer) on the exact byte sequence printf '5 1 2 3' > delivers. First and only attempt:

amdgparun: loaded (pp_power_profile_mode parser replica)
amdgparun: walking 6-byte content, last byte 0x00
Fatal user address access from kernel mode from sysctl at ffffffff82600150
Fatal trap 12: page fault while in kernel mode
fault virtual address   = 0x0
fault code              = supervisor read data, page not present
Stopped at      run_parse.part.1+0xe0:  movzbl  (%rax),%edx
db>

Decisive: fault virtual address = 0x0, supervisor read data, and the faulting instruction is exactly the isspace(*tmp_str) byte load (movzbl (%rax),%edx with %rax == NULL). The guest died into DDB and was reset (vm.sh reset with-src).

Why Low severity

The attribute is S_IWUSR (root-writable only), so the direct impact is a root-triggerable kernel panic (DoS). It is filed because it is a deterministic, one-write kernel crash on a documented interface of the powerplay dispatch surface audited in this batch, and because the same loop shape is copy-pasted across the DRM tree.

Fix

fix.diff: explicitly NUL-terminate buf_cpy, bound parameter_size, and NULL-guard the skip loop (while (tmp_str != NULL && isspace(*tmp_str))). Driver-level validation impossible on this guest (no AMD GPU); the fix removes both the NULL deref and the uninitialized-stack walk by construction.

Guest left clean (vm.sh reset with-src after the panic capture).

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 no→ patch + rebuild →patched clean

fix.diff authored against the read-only sys/ tree (git apply --check clean): explicit buf_cpy termination + parameter_size bound + NULL-guarded isspace skip removes both the NULL deref and the uninitialized-stack walk by construction. Driver-level validation impossible on this guest (no AMD GPU).

findings/poc/DF-4366/fix.diff
↓ fix.diffper-fix-DF-4366

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-4366/panic.txt (full serial console: walking 6-byte content, last byte 0x00; Fatal trap 12; fault virtual address 0x0; movzbl (%rax),%edx; db> prompt); build.log (module built clean under -Werror); amdgpu_parser_replica.c (verbatim strsep/isspace walk replica); VERDICT.md; fix.diff

PoC changes

No seed PoC. Authored amdgpu_parser_replica.ko (out-of-tree kmodule) because the guest has no AMD GPU: it runs the exact amdgpu_pm.c:946-957 delimiter walk (same 3-delimiter strsep set, same isspace skip, same count-i copy shape) on the bytes printf '5 1 2 3' > would deliver. kstrtol is stood in for by a digit-accepting check so the module does not need the DRM compat headers; the crash is in the delimiter walk, not the numeric conversion.

Verified recommended fix

NUL-terminate buf_cpy explicitly, bound parameter_size < 64, and guard the skip loop with tmp_str != NULL (fix.diff).

Verdict

Deterministic kernel NULL-deref panic in the pp_power_profile_mode sysfs store handler amdgpu_set_pp_power_profile_mode (sys/dev/drm/amd/amdgpu/amdgpu_pm.c:916-975, the dispatch entry for pp_set_power_profile_mode / amd_powerplay.c:888-910). The CUSTOM-profile token walk copies count-i bytes into buf_cpy[128] with no explicit NUL termination (:944) and, after each strsep with the 3-delimiter set {' ', '\n', '\0'} (:948), unconditionally executes 'while (isspace(tmp_str)) tmp_str++;' (:955-956). DragonFly strsep (sys/libkern/strsep.c:63-65) sets stringp = NULL when the remainder holds no delimiter, so any write NOT ending in ' ' or '\n' - e.g. printf '%s' "5 1 2 3" > pp_power_profile_mode (no echo newline) - makes tmp_str NULL and isspace(*tmp_str) (sys/sys/ctype.h:43) dereferences address 0. Secondary: for i==0 input shapes the copy omits even the accidental sysfs-sentinel termination and the walk continues into uninitialized kernel stack. Mechanism PROVEN live on the guest on the first attempt with a kernel-module replica running the verbatim delimiter walk on the exact byte sequence that write delivers: 'Fatal trap 12 ... fault virtual address = 0x0 ... Stopped at run_parse.part.1+0xe0: movzbl (%rax),%edx' - the faulting instruction is precisely the isspace byte load from NULL. Guest sat in DDB and was reset. Driver-level entry is hardware-gated on this guest (no AMD GPU; attribute exists only on amdgpu devices, S_IWUSR root-only), hence inconclusive per hardware-gated convention; the parse-loop primitive itself is fully reproduced. The sibling handler amdgpu_set_pp_od_clk_voltage (:543-606) copies count+1 bytes (sentinel included) and cannot reach 64 tokens, confirming the profile-mode loop is the defect.