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

corecstat attach-time kernel panic on original Atom: the chip-representative CPU of any model-0x1c/0x26/0x27/0x35/0x36 system falls into corecstat_init_atom_cpu() with sc_version==0, violating its own KKASSERT β€” boot/kldload panic on INVARIANTS builds, silently-wrong core-sensor init on production builds

Summary

The atom escape hatch diverts only non-representative CPUs; the representative CPU reaches init_atom_cpu whose first statement is KKASSERT(sc_version != 0) - false by construction for the five whitelisted original-Atom models. REPRODUCED on guest via CPUID-substitution KLD: panic 'assertion sc_version != 0 failed', DDB, guest down; production builds execute the impossible path (wrong Silvermont-only core C1/C6 probes, rdmsr_safe-gated). Version-guard fix validated with identical trigger.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4780 Β· 12 files
FileTypeDescriptionSize
README.md β€” 3.3 KB ↓ raw
VERDICT.md β€” 4.3 KB ↓ raw
patchcpu.c β€” 1.3 KB view raw
build.sh β€” 66 B view raw
run.sh β€” 90 B view raw
build.log β€” 2.6 KB view raw
run.log β€” 5.5 KB view raw
panic.txt β€” 1.5 KB view raw
fix_run.log β€” 2.0 KB view raw
env.txt β€” 723 B view raw
fix.diff β€” 631 B view raw
verdict.json β€” 4.0 KB view raw

DF-4780 β€” corecstat attach-time kernel panic on original Atom (KKASSERT(sc->sc_version != 0) reachable for exactly the models probe() whitelists)

File: sys/dev/powermng/corecstat/corecstat.c Severity: Low (attach-time kernel panic; debug/INVARIANTS builds panic, production builds execute the "impossible" path) Β· Confidence: certain Β· Bucket: base:powermng

Root cause

corecstat_attach() routes original Atoms (sc_is_atom=1, sc_version=0) like this (corecstat.c:342-362):

    if (node != NULL && cpu == BSRCPUMASK(node->members)) {
        ... /* chip-representative cpu: create cpu_nodeN sensordev + PKG sensors */
    } else if (sc->sc_is_atom && sc->sc_version == 0) {
        return (ENXIO);                    /* non-representative cpus bail */
    }

    if (sc->sc_is_atom)
        corecstat_init_atom_cpu(sc, cpu);  /* KKASSERT(sc->sc_version != 0) !! */
    else
        corecstat_init_core_cpu(sc, cpu);

and corecstat_init_atom_cpu() opens with:

    /* No Core C-state residency counters on old Atoms */
    KKASSERT(sc->sc_version != 0);         /* corecstat.c:424 */

The else if only diverts non-representative cpus. The chip-representative cpu of an original Atom (models 0x1c/0x26/0x27/0x35/0x36 β€” all explicitly whitelisted by corecstat_probe() at corecstat.c:186-191) falls straight into corecstat_init_atom_cpu() with sc_version == 0 β€” violating the driver's own invariant on every original-Atom system (child_no of a CHIP node is its child count, sys/kern/subr_cpu_topology.c:125, and is β‰₯1 on any real topology, so the representative cpu always takes the first branch).

  • INVARIANTS builds (KKASSERT active, sys/sys/systm.h:94-101 β€” LINT64-style configs, which include device corecstat): kernel panic at attach β€” boot-time doom if compiled in, panic on kldload otherwise. Reproduced on the guest: panic: assertion "sc->sc_version != 0" failed in corecstat_init_atom_cpu.
  • Production builds (KKASSERT compiled out, systm.h:116-119): the "impossible" path executes β€” the representative original-Atom cpu registers Silvermont-only core C1 (MSR 0x660) / C6 (0x3fd) sensor probes. Both are rdmsr_safe-gated, so the damage is wrong-behavior, not corruption. Live-observed on the guest (console lines msr node0 core5 C1 res. (0x00000660) not available from a version-0 softc).

Reproduce (guest)

sh build.sh     # builds: patchcpu.ko (CPUID substitution) + corecstat.ko
                # (stock source + "#define INVARIANTS 1" line 1 == debug-kernel build)
sh run.sh       # kldload patchcpu; kldload corecstat
# expected: cpu0..4 "attach returned 6" (ENXIO), cpu5: 3 pkg MSR rejections, then
#           panic: assertion "sc->sc_version != 0" failed in corecstat_init_atom_cpu
#           -> DDB, guest down (vm.sh status => down)

patchcpu only substitutes the CPUID model advertisement (cpu_id -> 0x00030661, family 06h model 36h Cedarview β€” a model the driver itself whitelists); the panicked code is the stock, unmodified driver.

Fix

fix.diff guards the atom dispatch (if (sc->sc_version != 0)) so old Atoms get only their package sensors, restoring the author's documented design ("Only Package C-States available, no Core C-states", corecstat.c:267). Validated on the guest: same trigger, fixed module β†’ no panic, no core C1/C6 probes, clean load/unload.

VERDICT.md
↓ download raw

VERDICT β€” DF-4780

Reproduced? YES β€” attach-time kernel panic, twice, deterministically

Trigger on the QEMU/KVM guest (stock kernel, family-0x0F CPU): patchcpu.ko substitutes only the CPUID model advertisement (cpu_id β†’ 0x00030661, family 06h model 36h Cedarview β€” a model corecstat_probe() itself whitelists at corecstat.c:189), then kldload corecstat.ko where corecstat.ko is the stock, unmodified driver source with #define INVARIANTS 1 on line 1 β€” i.e. exactly what any debug/INVARIANTS kernel build (LINT64-style; LINT64 contains device corecstat) compiles. Verified before loading that the module contains the KKASSERT panic string (grep -c assertion == 1).

Observed (full serial capture in panic.txt / run.log):

  • cpu0..cpu4 (non-representative): device_probe_and_attach: corecstatN attach returned 6 β€” the atom-v0 else if ENXIO path (corecstat.c:355-357).
  • cpu5 (chip representative, BSR of the CHIP node's members): package sensors probed via rdmsr_safe (PC2/PC4/PC6 lines, graceful), then

panic: assertion "sc->sc_version != 0" failed in corecstat_init_atom_cpu at corecstat.c:425 (pristine-tree line 424; +1 from the prepended define)

backtrace corecstat_attach β†’ device_doattach β†’ bus_generic_driver_added β†’ devclass_add_driver β†’ module_register_init β†’ Debugger("panic"), all CPUs stopped, guest down in DDB, ssh hung (RUN_RC=124), vm.sh status β‡’ down.

Why it fires (logic, not environment)

corecstat_attach()'s else if (sc->sc_is_atom && sc->sc_version == 0) return (ENXIO) (corecstat.c:355-357) diverts only non-representative cpus. The chip representative of an original Atom takes the first branch (CHIP nodes always have β‰₯1 child: child_no is the child count, sys/kern/subr_cpu_topology.c:125) and then falls into corecstat_init_atom_cpu() whose first statement is KKASSERT(sc->sc_version != 0) (corecstat.c:424) β€” false by construction for exactly the five original-Atom models the probe whitelist names (corecstat.c:186-191). Every original-Atom system hits this at attach (boot-time panic if the driver is compiled in, kldload panic otherwise) on INVARIANTS builds.

Production builds (the honest gap)

KKASSERT is INVARIANTS-only (sys/sys/systm.h:94-119). On production kernels the assert is compiled out and the "impossible" path executes: the representative original-Atom cpu registers core C1 (MSR 0x660, Silvermont-only) and C6 sensor probes. Both are rdmsr_safe-gated (corecstat.c:594-603), so this is wrong-behavior, not memory corruption. Live-captured in run.log ("PRODUCTION-build fall-through": msr node0 core5 C1 res. (0x00000660) not available from a version-0 softc). Severity Low (panic class; DF-4735-comparable): the panic needs an INVARIANTS build, which DragonFly ships as its standard debug configuration and which LINT64-style configs make concrete (device corecstat + INVARIANTS). No unprivileged trigger: attaching the driver requires boot or root kldload on original-Atom-class hardware (or a VMM advertising those CPUIDs, as demonstrated).

Exploit chain: none β€” panic/DoS class (no memory corruption)

Fix validation: FIXED

fix.diff restores the author's documented design ("Only Package C-States available, no Core C-states", corecstat.c:267) by guarding the atom dispatch on sc_version != 0. Same trigger, fixed module (INVARIANTS define identical to the baseline build): no panic, module loads (FIXED_LOADED), cpu5 probes only the three package MSRs β€” the core5 C1/C6 lines are gone β€” clean kldunload, guest stays up (vm.sh status β‡’ up). A pleasing compiler certificate: with the fix, -O2 interprocedural analysis proves the assert condition always true (single static call site now guarded) and dead-code-eliminates the panic path β€” the fixed .ko no longer even contains the "assertion … failed in" string.

Guest state

Returned clean: vm.sh reset with-src after all runs; no kernel or /usr/src modifications persisted (trigger + fixed modules lived in /tmp only, on wiped disks).

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

fixed
baseline reproduced→ patch + rebuild →patched clean

With fix.diff applied (same INVARIANTS define as the baseline build), the identical trigger no longer panics: module loads fully (FIXED_LOADED), cpu5 probes only the 3 package MSRs (the version-0 core C1/C6 probe lines are gone), kldunload clean, guest stays up. Baseline panicked at the same point. With the fix, -O2 interprocedural analysis proves the KKASSERT condition always true and eliminates the panic path from the binary entirely.

fix_run.log (PATCH_LOADED/FIXED_LOADED/LOAD_RC=0, only PC2/PC4/PC6 'not available' lines, UNLOAD_OK, GUEST_STILL_UP, vm.sh status up) vs run.log/panic.txt (baseline panic, guest down).
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0 (X86_64_GENERIC) + fixed corecstat.kld module (INVARIANTS)

Confirmed kernel references

Detail

Evidence (decisive lines)

panic.txt: full console with the KKASSERT panic + backtrace + DDB; run.log: baseline panic run (RUN_RC=124, vm.sh status down) AND production-build fall-through showing the core5 C1/C6 MSR probes from a version-0 softc; fix_run.log: fixed module, only 3 package MSR probes, no panic, GUEST_STILL_UP; build.log: INVARIANTS variant 11840 bytes with panic string present vs fixed variant where the compiler dead-code-eliminates the (now provably-true) assert.

PoC changes

Trigger uses a CPUID-advertisement substitution module (same pattern as the DF-4735 pack) instead of real Atom silicon; the panicked code is the stock unmodified driver. INVARIANTS is enabled for the module by prepending '#define INVARIANTS 1' (kmod CFLAGS are reset by bsd.kmod.mk, and the stock production kernel compiles KKASSERT to nothing).

Verified recommended fix

Guard the atom dispatch in corecstat_attach(): only call corecstat_init_atom_cpu() when sc->sc_version != 0 (see fix.diff).

Verdict

Reproduced deterministically (2 runs): on the stock QEMU guest, a CPUID-model substitution module (patchcpu -> family 06h model 36h, a model corecstat_probe() itself whitelists) plus the stock corecstat.ko compiled with INVARIANTS (= any debug/LINT64-style kernel build) panics at driver attach: 'panic: assertion "sc->sc_version != 0" failed in corecstat_init_atom_cpu' (corecstat.c:424), backtrace corecstat_attach -> devclass_add_driver -> module_register_init, guest dead in DDB. Root cause: the atom-v0 else-if ENXIO (corecstat.c:355-357) diverts only non-representative cpus; every original-Atom chip representative (models 0x1c/0x26/0x27/0x35/0x36) falls into corecstat_init_atom_cpu with version 0. On production kernels (KKASSERT compiled out, systm.h:94-119) the same 'impossible' path executes harmlessly-but-wrongly (live-captured: core C1 0x660/C6 probes from a version-0 softc; rdmsr_safe-gated). Fix (guard init_atom_cpu on sc_version != 0) validated: same trigger, no panic, core C1/C6 probes gone, clean unload, guest up.