# CVE-2026-81012: off-by-one write in hp_get_string_from_buffer()

**Patch:** [dc03f05e419f3460342fb7564884f244622634b6](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=dc03f05e419f3460342fb7564884f244622634b6) (mainline, landed in 7.3-rc1)
**Subsystem:** platform/x86, HP BIOS configuration driver
**Affected file:** drivers/platform/x86/hp/hp-bioscfg/bioscfg.c
**CVE record:** [CVE-2026-81012](https://www.cve.org/CVERecord?id=CVE-2026-81012)
**Introduced in:** 6.6, commit a34fc329b1895fc8a6eb12099adc47009421ba6a

## The bug in one sentence

The function that copies BIOS attribute strings out of a WMI buffer clamped the converted length with a strict greater-than comparison, so a string exactly the size of the destination left the NUL terminator landing one byte past the buffer.

## The classic shape

If you have read kernel security fixes for any length of time, you have seen this exact movie before: the buffer is sized correctly for the content, the length check is off by exactly one, and the unconditional terminator write is what escapes.

```c
conv_dst_size = size;
if (size > dst_size)
        conv_dst_size = dst_size - 1;

dst[conv_dst_size] = 0;
```

Walk through the edge case. When size equals dst_size, the comparison is false, conv_dst_size stays at dst_size, and the terminator goes to dst[dst_size]. If dst is char path[512], that write lands on byte 513. One byte past, with heap or struct adjacency deciding what gets clobbered.

Every fixed size destination that reaches this function was affected, and the list is not short:

* path[512]
* current_value[512]
* current_password[64] and current_value[64]
* each per entry buffer in encodings[][512]
* each per entry buffer in prerequisites[][512]

That last pair matters more than it looks. Enumeration attributes and prerequisites come straight out of firmware provided ACPI packages, so the string contents and lengths are firmware controlled. A BIOS that hands back maximally sized strings turns this from theoretical into a live one byte overwrite during attribute enumeration, before any user interaction with sysfs.

## The fix

One character, from greater-than to greater-than-or-equal:

```diff
 	conv_dst_size = size;
-	if (size > dst_size)
+	if (size >= dst_size)
 		conv_dst_size = dst_size - 1;
```

Full patch: [dc03f05e419f](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=dc03f05e419f3460342fb7564884f244622634b6)

Now any string at least as large as the destination gets truncated to dst_size minus one, leaving room for the terminator inside the buffer. Same shape of bug as the earlier off-by-one I fixed in hp_convert_hexstr_to_str(), which is precisely why I went looking for more of them after that one landed.

## How to trigger it

There is no direct command line trigger, because the input that has to hit the exact boundary comes from firmware, not from the user. The destination buffers are filled during attribute enumeration, when the driver walks the WMI string buffers the BIOS hands over and decodes them through hp_get_string_from_buffer().

What makes it fire on a real machine:

* a BIOS that reports an attribute path, current value, encoding, or prerequisite whose decoded length lands exactly on 512 or 64, the destination sizes
* the KASAN build then catches dst[dst_size] as a one byte slab write

The closest thing to a synthetic reproducer is patching the WMI buffer contents before the driver parses them (for example by hooking hp_wmi_obj_out of a test build) and planting a string of exactly dst_size bytes. That is how the boundary condition was verified before sending the patch. On machines where no attribute hits the exact size, the bug is simply dormant, which is the honest description of its reach.

## Score and classification

This one did get a CNA score, and it is the highest of the hp-bioscfg cluster:

* Kernel CNA (mirrored by NVD as Secondary): 8.4 HIGH, vector CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
* Red Hat re-scores it 5.5 MEDIUM with CWE-193 (off-by-one error), Bugzilla [2532131](https://bugzilla.redhat.com/show_bug.cgi?id=2532131)
* SUSE: 5.1 MEDIUM
* OffSeq: High, EPSS around 0.1 percent

The notable part of the CNA vector is PR:N, no privileges required. The reasoning is that the attribute enumeration path that populates those destination buffers runs when the driver binds, driven by firmware data, not by a privileged user action. Whether an 8.4 survives contact with real exploitability is debatable (you need specific HP firmware behavior to place an oversized string), but the no-privileges framing for an enumeration time overwrite is defensible, and the disagreement between 8.4 and 5.5 is a good illustration of why you should always read the vector, not just the number.

## Exploit code

The input that has to hit the exact boundary comes from firmware, not from userspace, so the proof of concept is a kernel test module that mirrors the vulnerable clamp and terminator logic with a string sized exactly to the destination buffer.

```c
/* hp_bioscfg_term_test.c: CVE-2026-81012
 * Kernel test module that calls the vulnerable clamp logic with a
 * decoded string exactly the size of the destination buffer.
 *
 * The vulnerable comparison is `size > dst_size`, so when size equals
 * dst_size, conv_dst_size stays at dst_size and the terminator write
 * dst[conv_dst_size] = 0 lands one byte past the buffer.
 *
 * Build: make -C /lib/modules/$(uname -r)/build M=$PWD modules
 * Load:  sudo insmod hp_bioscfg_term_test.ko
 * Watch: dmesg | tail -10  (KASAN slab-out-of-bounds WRITE on vulnerable kernel)
 */
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/string.h>

#define DST_SIZE 64   /* matches current_password[64] in the driver */

/* mirrors the vulnerable clamp from hp_get_string_from_buffer() */
static int test_terminator(u32 size, char *dst, u32 dst_size)
{
	u32 conv_dst_size;

	conv_dst_size = size;
	if (size > dst_size)          /* vulnerable: strict greater-than */
		conv_dst_size = dst_size - 1;

	memset(dst, 'A', conv_dst_size);
	dst[conv_dst_size] = 0;       /* writes past the buffer when equal */

	return conv_dst_size;
}

static int __init term_test_init(void)
{
	char *dst;

	dst = kmalloc(DST_SIZE, GFP_KERNEL);
	if (!dst)
		return -ENOMEM;

	pr_info("CVE-2026-81012: size=%u dst_size=%u\n", DST_SIZE, DST_SIZE);
	pr_info("CVE-2026-81012: `size > dst_size` is false, conv_dst_size stays %u\n",
		DST_SIZE);
	pr_info("CVE-2026-81012: writing dst[%u] = 0, one past the allocation\n",
		DST_SIZE);

	test_terminator(DST_SIZE, dst, DST_SIZE);

	/* On a vulnerable kernel with KASAN, dst[DST_SIZE] = 0 triggers:
	 *   BUG: KASAN: slab-out-of-bounds in test_terminator
	 *   Write of size 1 at addr ... (DST_SIZE-byte region)
	 *
	 * Fixed kernel: comparison is `size >= dst_size`, so
	 * conv_dst_size = dst_size - 1 and dst[dst_size - 1] = 0
	 * lands inside the buffer.
	 */

	kfree(dst);
	return 0;
}

static void __exit term_test_exit(void)
{
	pr_info("CVE-2026-81012 test module unloaded\n");
}

module_init(term_test_init);
module_exit(term_test_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("CVE-2026-81012: off-by-one terminator write PoC");
```

On a vulnerable kernel the `dst[DST_SIZE] = 0` write triggers KASAN immediately with `slab-out-of-bounds, write of size 1` against a 64-byte allocation. The same logic runs at enumeration time on every HP machine when the BIOS reports an attribute string that decodes to exactly 512 or 64 bytes, which is what earns the PR:N in the CVSS vector. Public status: no KEV, no public PoC before this note.

## Where the fix shipped

* 6.12.109: [3cc772d01547](https://git.kernel.org/stable/c/3cc772d0154799961f032e5a992d4a50523e291a)
* 6.18.50: [b15b334fbc3c](https://git.kernel.org/stable/c/b15b334fbc3c0c46440f8a892ebf62164fca23d6)
* 7.2.4: [ddf98cf33529](https://git.kernel.org/stable/c/ddf98cf33529714b3ba1a158afb1db5b0f759a1a)
* 7.3-rc1: [dc03f05e419f](https://git.kernel.org/stable/c/dc03f05e419f3460342fb7564884f244622634b6)

Debian: [DSA-6528-1](https://www.debian.org/security/2026/dsa-6528) and [DLA-4817-1](https://security-tracker.debian.org/tracker/DLA-4817-1). Ubuntu: noble and resolute fixes were still marked work in progress as of late October 2026.

## References

1. [Upstream commit dc03f05e419f](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=dc03f05e419f3460342fb7564884f244622634b6)
2. [CVE announcement on lore](https://lore.kernel.org/linux-cve-announce/2026091153-CVE-2026-81012-1f3c@gregkh/)
3. [NVD record](https://nvd.nist.gov/vuln/detail/CVE-2026-81012)
4. [Red Hat tracker](https://access.redhat.com/security/cve/cve-2026-81012)
5. [Ubuntu tracker](https://ubuntu.com/security/CVE-2026-81012)
6. [SentinelOne vulnerability database entry](https://www.sentinelone.com/vulnerability-database/cve-2026-81012/)
7. [freenode.net coverage of the hp-bioscfg hardening series](https://freenode.net/article/hp-bios-config-driver-gets-heap-overflow-and-oob-fixes)
