[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)
PoC verification
Evidence pack
findings/poc/DF-4366 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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, ¶meter[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.
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:
:944memcpy(buf_cpy, buf, count-i);βbuf_cpy[128], never explicitly NUL-terminated;count β€ 127is the only bound (:940-941);:948strsep(&tmp_str, delimiter)withdelimiter = {' ', '\n', '\0'}(:931);:955-956while (isspace(*tmp_str)) tmp_str++;β executed immediately afterstrsep, 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
inconclusivefix.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
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.
No comments yet.