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

GAS AccessWidth enumeration treated as a byte count: spec-legal dword _PCT GAS rejected ('Invalid control register GAS'), P-state domain permanently dead, cpufreq silently unavailable; qword misread as 4 bytes

Summary

AccessWidth is used directly as the byte count but is an ACPI enumeration (3=dword): AccessWidth=3 computes asz=3->EINVAL and flags the domain DEAD - any BIOS using the spec-correct dword encoding silently loses cpufreq; AccessWidth=4 (qword, 8B) misread as 4B. No over-length access possible, functional only. SSDT-injection A/B on the guest (rejected vs fixed kernel rebuilt with 1<<(w-1): 'available: 3000' returns).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5485 Β· 8 files
FileTypeDescriptionSize
README.md β€” 2.2 KB ↓ raw
VERDICT.md β€” 3.5 KB ↓ raw
dsdt_dword.aml β€” 595 B ↓ download
run.dword_accesswidth.log β€” 357 B view raw
env.txt β€” 741 B view raw
fix.diff β€” 639 B view raw
fix_build.log β€” 1.8 KB view raw
run.fixed.log β€” 359 B view raw

DF-5485 β€” GAS AccessWidth enumeration treated as byte count

acpi_pst_md_gas_asz() (sys/platform/pc64/acpica/acpi_pstate_machdep.c:564) reads the ACPI Generic Address Structure AccessWidth field as a byte count. It is an enumeration (1=byte 2=word 3=dword 4=qword), so:

  • AccessWidth=3 (dword) is rejected ("Invalid control register GAS", domain DEAD, cpufreq silently gone) β€” demonstrated on the guest with a spec-legal dword _PCT GAS (run.dword_accesswidth.log)
  • AccessWidth=4 (qword) is misread as a 4-byte access

Reproduce

Uses the DF-5483 rig verbatim (planted RSDP/RSDT/FACP/DSDT chain with _SB.C000..C005 carrying _PCT/_PSS):

# build the dword variant: take the working control DSDT/AML
# (SystemIO, port 0x80, AccessWidth=0, BitWidth=8) and change the ctrl
# GAS attribute bytes to AccessWidth=3 / BitWidth=32 at a 4-byte-clean
# port (0x100):
python3 - <<'EOF'
d = bytearray(open('dsdt_control.aml','rb').read())
co = bytes([0x01,0x08,0x00,0x00,0x80,0x00])      # SystemIO, BW=8, AW=0, port 0x80
cn = bytes([0x01,0x20,0x00,0x03,0x00,0x01])      # SystemIO, BW=32, AW=3, port 0x100
assert d.count(co) == 6
open('dsdt_dword.aml','wb').write(bytes(d).replace(co, cn))
EOF
python3 ../DF-5483/gen_pstate.py dsdt_dword.aml pstate_dword.bin

# guest setup identical to DF-5483 (rsdp hint + estforce preload), then:
QEMU_EXTRA="-machine accel=tcg -cpu SandyBridge \
  -device loader,file=<abs>/pstate_dword.bin,addr=0x40000000,force-raw=on" \
  dfbsd-qemu/vm.sh up 240

Expected (observed 2026-10-06, run.dword_accesswidth.log)

cpu_pst0: control reg 1 100              <- parse+allocation succeed
...
ACPI: domain1 P-State configuration check failed
cpu0: Invalid control register GAS       <- acpi_pst_md_gas_verify -> EINVAL
ACPI: domain0 P-State configuration check failed
# sysctl hw.acpi.cpu.px_dom0.available   -> unknown oid  (cpufreq gone)

Control run (AccessWidth=0) on the same rig boots with hw.acpi.cpu.px_dom0.available: 3000 β€” the delta is solely the access-size encoding.

Fix

fix.diff β€” decode the enumeration (1 << (AccessWidth-1), reject >4). Validated by guest rebuild (see fix_build.log + fix section in VERDICT.md).

VERDICT.md
↓ download raw

VERDICT β€” DF-5485

AccessWidth=3 (DWord) is rejected by the kernel; the P-state domain is

permanently killed. No security impact (over-strict, not under-strict);

filed as an ABI-correctness/hardening finding (Info).

Root cause

acpi_pst_md_gas_asz (sys/platform/pc64/acpica/acpi_pstate_machdep.c:564-583) treats the GAS AccessWidth field as a byte count:

if (gas->AccessWidth != 0)
    asz = gas->AccessWidth;      /* WRONG: enum, not bytes */
else
    asz = gas->BitWidth / NBBY;
switch (asz) { case 1: case 2: case 4: break; default: asz = 0; }

Per the ACPI spec (Generic Address Structure, "Access Size") AccessWidth is an enumeration: 1 = byte, 2 = word, 3 = dword, 4 = qword. Consequences:

  • AccessWidth=3 (dword, 4 bytes β€” perfectly legal firmware encoding) β†’ asz=3 β†’ falls to default: β†’ asz=0 β†’ acpi_pst_md_gas_verify (:599-601) returns EINVAL β†’ acpi_pst_intel_check_csr prints "Invalid control register GAS" and the whole P-state domain is flagged DEAD (acpi_pst_postattach, acpi_cpu_pstate.c:1129-1134): cpufreq silently unavailable.
  • AccessWidth=4 (qword, 8 bytes) β†’ asz=4 β†’ accepted as a 4-byte access β€” an under-length access to an 8-byte register (functional hazard; the switch should accept 8 and reject >8 after conversion).

Note the two accidental hits: AccessWidth=1 and 2 are both the enumeration and the byte count, which is why byte/word firmware works.

Guest demonstration (same rig as DF-5483)

Two boots, identical except the ctrl GAS attribute bytes:

run ctrl GAS result
control AccessWidth=0, BitWidth=8, port 0x80 domain alive, px_dom0.available: 3000
dword AccessWidth=3, BitWidth=32, port 0x100 (spec-legal dword GAS) cpu0: Invalid control register GAS + ACPI: domain0 P-State configuration check failed + ACPI: domain1 P-State configuration check failed; sysctl hw.acpi.cpu.px_dom0.available β†’ unknown oid

Full output in run.dword_accesswidth.log. The kernel adopted the table (cpu_pst0: control reg 1 100 β€” parse and port reservation succeeded) and then rejected the access-size encoding.

Why Info

Reachable only from firmware-supplied tables; the failure mode is a self-DoS of the cpufreq feature (and a per-CPU kprintf), not memory unsafety β€” the code errs on the side of rejection. Still worth fixing: any BIOS that encodes dword-access P-state registers (the ACPI-spec-correct encoding for 32-bit PERF_CTL-style ports) silently loses P-state control on DragonFly while working on other OSPMs.

fix.diff

Authored (fix.diff): decode the enumeration (1 << (AccessWidth-1), reject >4), keep the {1,2,4} byte-size acceptance (the res_read/res_write accessors only implement 1/2/4 β€” qword GAS stays rejected pending an 8-byte accessor, but now for the right reason: 8 βˆ‰ {1,2,4}, not "4 means 4").

Fix validation: the patched source was compiled into a kernel on the guest (make nativekernel, build log in fix_build.log) and re-run with the exact same dword blob β€” see fix section of the pack README for the before/after.

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 reproduced

Patched acpi_pst_md_gas_asz in the guest's /usr/src copy (fix.diff), rebuilt the kernel (make nativekernel KERNCONF=X86_64_GENERIC, fix_build.log), installed it, and rebooted with the IDENTICAL dword blob: the same tables that killed both domains on the stock kernel now boot through the check and expose hw.acpi.cpu.px_dom0.available β€” the spec-legal dword GAS is accepted. Baseline (stock kernel, same blob): 'Invalid control register GAS' + unknown oid. Before/after in run.dword_accesswidth.log / run.fixed.log.

fix.diff; fix_build.log (build); run.dword_accesswidth.log (baseline); run.fixed.log (patched kernel, same blob, sysctls present)
↓ fix.diffper-fix-DF-5485

Confirmed kernel references

Detail

Evidence (decisive lines)

run.dword_accesswidth.log: 'cpu_pst0: control reg 1 100' (table adopted, port reserved) then 'cpu0: Invalid control register GAS' + 'ACPI: domainN P-State configuration check failed' + sysctl unknown oid; control run in DF-5483/run.control_output.txt shows AccessWidth=0 booting to hw.acpi.cpu.px_dom0.available: 3000 β€” the delta is solely the access-size encoding

PoC changes

Derived from the DF-5483 rig: byte-patched the compiled control DSDT/AML ctrl-GAS attribute tuple (01 08 00 00 80 00 -> 01 20 00 03 00 01); status GAS kept byte/AccessWidth=0 at port 0x90 to isolate the dword-rejection to the ctrl register.

Verified recommended fix

Decode the enumeration: asz = 1 << (AccessWidth - 1) for AccessWidth in 1..4 (reject >4); keep the {1,2,4} byte-size acceptance in the switch.

Verdict

acpi_pst_md_gas_asz (acpi_pstate_machdep.c:564-583) treats the ACPI GAS AccessWidth enumeration (1=byte, 2=word, 3=dword, 4=qword) as a byte count. A spec-legal dword _PCT GAS (AccessWidth=3, BitWidth=32) therefore computes asz=3, falls to default -> 0, and acpi_pst_md_gas_verify returns EINVAL: 'cpu0: Invalid control register GAS', both P-state domains flagged DEAD, cpufreq silently gone (sysctl hw.acpi.cpu.px_dom0.available -> unknown oid) while the byte/word encodings work. Demonstrated on the guest with the DF-5483 rig by a one-attribute delta (AccessWidth 0->3, port 0x80/8-bit -> 0x100/32-bit) against a working control boot. AccessWidth=4 (qword) is dually misread as 4 bytes. Functional ABI-correctness bug with no security impact (over-strict rejection, no over-length access ever occurs since asz stays within {1,2,4} and reg+asz<=BitWidth/8): severity Info, confidence certain.