Off-by-one NUL heap write in acpi_battery_clean_str past the _BIF oeminfo field
Summary
acpi_battery_clean_str() NUL-terminates a full buffer by writing to *str AFTER loop has already advanced str one byte past end. For i==len after post-increment str points to start+len - one byte PAST end of len-byte buffer - so *str=NUL is OOB write to index len. For oeminfo (last member of struct acpi_bif) with 32-byte non-NUL OEM string writes NUL to offset 164 = one byte past 164-byte union acpi_battery_ioctl_arg. Ioctl dispatcher heap-allocates exactly IOCPARM_LEN=164 bytes. OOB fires when _BIF returns oeminfo with no embedded NUL in 32 bytes (common on physical HW deterministic under QEMU with crafted SSDT). zoneindex rounds 164->176 so NUL lands in per-chunk padding no metadata corruption on default allocator but latent memory-safety bug. Reachable by any unprivileged local user via /dev/acpi (0644) hook dispatch before FWRITE gate. [PASS-2 NOTE: duplicate of DF-1928 - single 1-byte heap OOB via oeminfo[32] only (model/serial/type land on adjacent in-union fields)] [VERIFIED pass2: FALSE POSITIVE at runtime - 32-char no-NUL oeminfo never reaches bif.oeminfo because acpi_PkgStr E2BIG plus bzero rejects first, structurally unreachable precondition; latent off-by-one retained as hardening]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2375 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 927 B | β raw | |
| VERDICT.md | β | 1.7 KB | β raw | |
| env.txt | β | 145 B | view raw | |
| fix.diff | β | 367 B | view raw | |
| run.log | β | 666 B | view raw | |
| run.sh | β | 121 B | view raw | |
| verdict.json | β | 2.0 KB | view raw |
DF-2375 - acpi_battery_clean_str off-by-one: REFUTED at runtime
Why the OOB cannot fire through the only in-tree path
- bif.oeminfo is filled ONLY by acpi_PkgStr(..., 32) in acpi_cmbat_get_bif.
- acpi_PkgStr bzero()s dst then rejects strings of length >= 32 (E2BIG, acpi_package.c:84-86) - a 32-char oeminfo NEVER lands in bif.oeminfo.
- The ioctl path bzero()s the 164-byte kernel buffer before filling, so
attacker-prefilled user bytes cannot survive either.
=> bif.oeminfo always contains a NUL in bytes 0..31; the
if (i == len) *str = 0;branch is never taken.
Empirical (ACHARNESS boot + ssdt_acsim BAT0 with 32-char oeminfo)
- bif_oob 0 -> oeminfo[32]='' (empty: E2BIG path)
- poison:164:6 + ioctl + check:164:164 -> no OOB byte-164 write observed
The clean_str off-by-one itself is real as isolated code (latent hardening issue); fix.diff terminates the LAST byte instead of one past the buffer.
DF-2375 VERDICT
Status: not_reproduced Impact: none Confidence: certain Attempts: 3
FALSE POSITIVE at runtime: the OOB write precondition (32 non-NUL bytes in bif.oeminfo) is structurally unreachable. acpi_cmbat_get_bif fills oeminfo only through acpi_PkgStr(res, i, sc->bif.oeminfo, 32) (acpi_cmbat.c), and acpi_PkgStr rejects a 32-char string with E2BIG (++length 33 > size 32, acpi_package.c:84-86) leaving the bzero()d field empty - so bif.oeminfo ALWAYS contains a NUL within bytes 0..31 and acpi_battery_clean_str never reaches the *str=0 write. Empirically: SSDT _BIF with a 32-char no-NUL oeminfo string returns oeminfo='' through ACPIIO_BATT_GET_BIF (the string never lands), and a 0xFF-prefilled user ioctl buffer is wiped by the kernel bzero before the clean_str calls. The off-by-one in acpi_battery_clean_str (acpi_battery.c:331-333) is real as isolated code - retained as hardening (fix_2395.diff writes the last byte instead of one past).
Kernel references
sys/dev/acpica/acpi_battery.c:322sys/dev/acpica/acpi_battery.c:331sys/dev/acpica/acpi_battery.c:399sys/dev/acpica/acpi_package.c:84sys/dev/acpica/acpi_cmbat.c:445
Guest
DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #7: Sat Oct 24 00:08:04 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/ACHARNESS x86_64
Fix validation
See fix.diff and README.md for the baseline/fixed A/B outputs (run.log / fixed.log).
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
not_testableConfirmed kernel references
Detail
Evidence (decisive lines)
run.log (BIF unit=0 ... oeminfo[32]= - empty despite 32-char _BIF oeminfo); poison/check choreography showed no byte-164 OOB write
PoC changes
bif_oob.c written for this batch (unprivileged ACPIIO_BATT_GET_BIF with 0xFF prefill + poisoned 164-byte zone)
Verified recommended fix
see fix.diff
Verdict
FALSE POSITIVE at runtime: the OOB write precondition (32 non-NUL bytes in bif.oeminfo) is structurally unreachable. acpi_cmbat_get_bif fills oeminfo only through acpi_PkgStr(res, i, sc->bif.oeminfo, 32) (acpi_cmbat.c), and acpi_PkgStr rejects a 32-char string with E2BIG (++length 33 > size 32, acpi_package.c:84-86) leaving the bzero()d field empty - so bif.oeminfo ALWAYS contains a NUL within bytes 0..31 and acpi_battery_clean_str never reaches the *str=0 write. Empirically: SSDT _BIF with a 32-char no-NUL oeminfo string returns oeminfo= through ACPIIO_BATT_GET_BIF (the string never lands), and a 0xFF-prefilled user ioctl buffer is wiped by the kernel bzero before the clean_str calls. The off-by-one in acpi_battery_clean_str (acpi_battery.c:331-333) is real as isolated code - retained as hardening (fix_2395.diff writes the last byte instead of one past).
No comments yet.