# DF-5344 — cpu_brand assembled from CPUID 0x80000002..4 with no NUL termination → out-of-bounds read (and layout-dependent .bss disclosure / overflow) in printcpuinfo()

## Where

`sys/platform/pc64/x86_64/identcpu.c`

```c
 84: static char cpu_brand[48];
...
118: 	if (cpu_exthigh >= 0x80000004) {
119: 		brand = cpu_brand;
120: 		for (i = 0x80000002; i < 0x80000005; i++) {
121: 			do_cpuid(i, regs);
122: 			memcpy(brand, regs, sizeof(regs));
123: 			brand += sizeof(regs);
124: 		}
125: 	}
...
160: 	brand = cpu_brand;
161: 	while (*brand == ' ')
162: 		++brand;
163: 	if (*brand != '\0')
164: 		strcpy(cpu_model, brand);
```

The 3×16-byte CPUID brand leaves are copied into `cpu_brand[48]` with **no
terminating NUL ever written**. The brand string is fully controlled by the
hypervisor (`-cpu ...,model-id=` in QEMU) or, on bare metal, by CPU
firmware/microcode. With all 48 bytes non-NUL:

* the space-skip loop at :161 reads `cpu_brand[48]` out of bounds;
* `strcpy(cpu_model, brand)` at :164 reads past `cpu_brand[47]` hunting for a
  terminator and copies everything it finds into `cpu_model[128]`, which is
  exported world-readable via `sysctl hw.model` (SYSCTL_STRING at :76-78).

Real-kernel .bss adjacency on the shipped guest kernel
(`nm /boot/kernel/kernel`):

```
ffffffff818d9c20 b cpu_brand      # 48 bytes, ends at ...c50
ffffffff818d9c50 b hw_clockrate   # 4 bytes — still 0 when the strcpy runs
                                  # (it is only set at identcpu.c:169, after)
ffffffff818d9c60 b cpu_model
```

So on *this* build the walk terminates 1 byte past the array (disclosure
empty, write stays in bounds). Any future link order / earlier-initialized
neighbor turns the same defect into (a) disclosure of adjacent static kernel
data through `hw.model` to unprivileged users, or (b) with ≥80 contiguous
non-NUL bytes, a write past `cpu_model[128]`.

## How to reproduce (done, on the audit QEMU guest)

### 1. Live kernel proof (boot the guest with a 48-char, non-NUL brand)

```sh
MID=$(python3 -c "print('A'*48)")
qemu-img create -f qcow2 -b dfbsd-qemu/disk.qcow2 -F qcow2 /tmp/ovl-brand.qcow2
qemu-system-x86_64 -enable-kvm -m 2048 -smp 2 \
  -drive file=/tmp/ovl-brand.qcow2,if=virtio,format=qcow2 \
  -cpu "qemu64,model-id=$MID" \
  -netdev user,id=n0,hostfwd=tcp:127.0.0.1:5599-:22 \
  -device virtio-net-pci,netdev=n0 \
  -display none -serial file:boot-brand48.log -no-reboot
# once booted:
ssh -p 5599 maxx@127.0.0.1 'sysctl -n hw.model | wc -c'
```

Expected (and observed): `49` — hw.model holds **exactly 48 'A' bytes**. A
brand containing a NUL among bytes 0..47 could never yield 48 characters,
therefore the `strcpy` at :164 found its terminator **outside** the array:
an out-of-bounds read executed by the real guest kernel. Serial console
shows `CPU: AAAAAAAA…(48)… ` — `boot-brand48.log:32`; unprivileged
(`uid=1001`) `sysctl hw.model` shows the same 48 bytes — `run.2.log`.

### 2. Fault-localizing harness (guard page instead of sanitizer)

`brand_guard.c` places a 48-byte `cpu_brand` flush against a `PROT_NONE`
page and runs the *exact* identcpu.c:118-125 + :160-164 algorithm:

```sh
cc -O0 -g -o brand_guard brand_guard.c
./brand_guard 0   # 48 'A'        -> SIGSEGV (strcpy reads brand[48])  EXIT=139
./brand_guard 1   # 48 spaces     -> SIGSEGV (skip-loop reads brand[48]) EXIT=139
./brand_guard 2   # 47 'A' + NUL  -> survives, model = 47 chars        EXIT=0
```

Observed verbatim on the guest — see `run.log`.

## Files

* `brand_guard.c` — guard-page harness (exact algorithm replica)
* `boot-brand48.log` — full serial console of the 48-char-brand boot
* `build.log` / `run.log` / `run.2.log` — harness build + runs + sysctl/dmesg/id
* `env.txt` — guest environment
* `fix.diff` — one-line fix (force termination)
