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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2375 Β· 7 files
FileTypeDescriptionSize
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

  1. bif.oeminfo is filled ONLY by acpi_PkgStr(..., 32) in acpi_cmbat_get_bif.
  2. 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.
  3. 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.

VERDICT.md
↓ download raw

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

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_testable
↓ fix.diffper-fix-DF-2375

Confirmed 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).