Root Cause Analysis
1. Mechanism and Sequence of Events
The crash manifests during task wakeup in the scheduler's Pressure Stall Information (PSI) accounting path:
hrtimer_wakeup()
-> wake_up_process()
-> try_to_wake_up()
-> ttwu_queue() -> ttwu_do_activate() -> activate_task()
-> enqueue_task() -> psi_enqueue()
-> psi_task_change()
Inside psi_task_change() (kernel/sched/psi.c:916), the kernel evaluates the PSI group for the awakened task:
for_each_group(group, task_psi_group(task))
psi_group_change(group, cpu, clear, set, now, true);
task_psi_group(task) evaluates:
cgroup_psi(task_dfl_cgroup(task));
Where task_dfl_cgroup(task) reads task_css_set(task)->dfl_cgrp (include/linux/cgroup.h:607).
In the assembly of psi_task_change():
- Register
%r13 contains task->cgroups (struct css_set *), pointing to 0xffff888054570000.
- The instruction
mov 0x88(%r13), %r15 loads cset->dfl_cgrp (which is located at byte offset 0x88 of struct css_set) into %r15.
cgroup_psi(cgrp) calls cgroup_ino(cgrp) (include/linux/psi.h:39):
static inline struct psi_group *cgroup_psi(struct cgroup *cgrp)
{
return cgroup_ino(cgrp) == 1 ? &psi_system : cgrp->psi;
}
cgroup_ino(cgrp) accesses cgrp->kn (include/linux/cgroup.h:724), which is at offset 0x1a0 of struct cgroup:
static inline ino_t cgroup_ino(struct cgroup *cgrp)
{
return kernfs_ino(cgrp->kn);
}
- In KASAN-instrumented code, before dereferencing
cgrp->kn, the compiler computes &cgrp->kn using lea 0x1a0(%r15), %rdi and checks its shadow memory byte.
However, the value loaded into %r15 was 0xffffffffffffffff (-1). Computing 0xffffffffffffffff + 0x1a0 wraps around in 64-bit arithmetic to 0x000000000000019f. Checking the KASAN shadow address for 0x19f results in 0xdffffc0000000000 + (0x19f >> 3) = 0xdffffc0000000033, which is a non-canonical address on x86-64. Accessing this address immediately triggers a General Protection Fault (#GP), which kasan_non_canonical_hook() decodes as a null-ptr-deref in range [0x0000000000000198-0x000000000000019f].
2. Root Cause
In normal kernel operation, cset->dfl_cgrp is never set to -1 or ERR_PTR; it always points to &cgrp_dfl_root.cgrp or an allocated struct cgroup.
The reproducer triggers this corruption through proc_bus_pci_write() in drivers/pci/proc.c:
- The reproducer mounts
proc at ./proc and opens ./proc/bus/pci/00/00.0 with O_RDWR.
- It repeatedly writes
"R-" and "-1\0" into the file descriptor.
- On the emulated platform (
Q35 + ICH9), PCI device 00:00.0 is the Host Bridge / Memory Controller Hub (MCH).
proc_bus_pci_write() does not protect against writing to critical host bridge configuration registers. Writing arbitrary bytes into the host bridge configuration space reconfigures internal physical address decoding registers (such as PCIEXBAR, PAM regions, and TOLUD).
- As a result, the physical memory range containing the physical frame for the
struct css_set at 0xffff888054570000 is unmapped from system DRAM and remapped as unassigned/floating MMIO space.
- Subsequent memory reads to that physical address range return unassigned bus reads (
0xff bytes). Thus, reading cset->dfl_cgrp at 0xffff888054570088 reads eight 0xff bytes (0xffffffffffffffff), while KASAN shadow memory (residing in a different physical address range) remains intact.
- When the scheduler encounters
cset->dfl_cgrp == 0xffffffffffffffff, the kernel attempts to dereference the wild pointer, causing a fatal general protection fault.