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)
PoC verification
Evidence pack
findings/poc/DF-5485 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
_PCTGAS (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 β DF-5485
Reproduced? YES (functional) β a spec-legal _PCT GAS with
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_csrprints "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
fixedPatched 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)
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.
No comments yet.