# DF-5719 — Verification narrative (pass 2, GLM 5.3)

**Files audited:** `sys/libkern/strlen.c`, `sys/libkern/strnlen.c`,
`sys/libkern/bcd.c` (batch, IDs DF-5719..DF-5731 reserved; sole finding
of the batch is DF-5719 against bcd.c + the libkern.h macros).

## What was verified, and how

### 1. The OOB primitive — reproduced (on-guest, verbatim algorithm)

`bcd_rig.c` embeds byte-for-byte the three tables from
`sys/libkern/bcd.c:9-36` and the macro text from
`sys/sys/libkern.h:59-61`.  `MODE=2` copies each table into a mapping so
the object **ends exactly at a `PROT_NONE` guard page**, then evaluates
the verbatim macros with exactly the byte values the consumers pass:

* `bin2bcd(99)` (last in-bounds) → OK
* `bin2bcd(100)` (first OOB), `bin2bcd(200)` (the CDIOCPLAYTRACKS /
  CDIOCREADSUBCHANNEL `u_char`), `bin2bcd(255)` (max `u_char`) → SIGSEGV
* `bcd2bin(0x99)` → OK; `bcd2bin(0x9a)`, `bcd2bin(0xff)` (garbage
  CMOS byte, clock.c:558) → SIGSEGV

Decisive lines in `run.log`: `bin2bcd(200)  (ioctl byte) : SIGSEGV`,
`bcd2bin(0xff) (garbage CMOS) : SIGSEGV`.  This proves the shipped
macros leave their objects for every out-of-domain index — the
CWE-125 primitive.  (In the linked kernel the tables sit mid-`.rodata`,
so the read does not fault there; it returns adjacent `.rodata` bytes —
that is why the kernel never crashes, which is also why impact is Low.)

### 2. Semantics and the fix — reproduced (differential)

`MODE=3` proves exhaustively (rig3 in run.log):

* `bin2bcd` table ≡ `(i/10)<<4 | i%10` for all 0..99, and the fix wrap
  `% 100` is the identity on that domain (0 errors);
* `bcd2bin` table ≡ nibble arithmetic for every valid BCD byte
  0x00..0x99, and the fix wrap `% 154` is the identity there
  (0 errors);
* `hex2ascii` over its radix-36 domain (0 errors).

Note: my first fix candidate masked `bcd2bin` with `& 0x99` — the
differential caught it (`0x06 & 0x99 == 0x00`): a bit-mask is not a
nibble clamp.  The corrected wraps (`% 154`, `% 100`) are identity on
the valid domain and in-bounds for every input; `MODE=4` re-runs the
guard-page probes with the fixed macros → always in-bounds.

### 3. In-kernel execution path — live, traced, and demonstrated up to the drive

* The guest was booted (host helper `dfbsd-qemu/up-chinon.sh`) with a
  virtio-scsi CD whose INQUIRY is `CHINON  CD-ROM CDS-535` — the exact
  pattern in the quirk table (`scsi_cd.c:222-225`).  `devlist.txt`
  shows `<CHINON CD-ROM CDS-535 2.5+>` at scbus2 → the kernel assigned
  `CD_Q_BCD_TRACKS` at probe (`scsi_cd.c:701-707`, unconditional on the
  inquiry match).  No kernel patching: this is faithful simulation of
  the real 1980s hardware.
* `bcd_trigger.c` (root) sweeps `CDIOCREADSUBCHANNEL` track=0..255.
  Every call returns **EIO**.  Trace: the only EIO source in this branch
  is `cdreadsubchannel()` (scsi_cd.c:2163→3363 `cdrunccb`), which the
  call reached — i.e. it passed `args->track = bin2bcd(args->track)`
  (scsi_cd.c:2160-2161) first.  The EIO itself comes from the emulated
  drive: **QEMU 7.2 implements no READ SUB-CHANNEL (0x42) at all**
  (verified against the v7.2.0 sources of `hw/scsi/scsi-disk.c` and
  `hw/scsi/scsi-bus.c`; also confirmed with `passrep.c`, which replays
  the kernel's exact CCB via CAMIOCOMMAND and gets `CAM_DATA_RUN_ERR`
  with all-zero sense for every 0x42 variant, and `camcontrol cmd`
  probes).  So the one-byte copyback disclosure cannot complete **on
  this emulator**; on a real drive that answers 0x42 (as the CHINON
  does), the `_IOWR` copyback returns the OOB byte.
* The unprivileged angle is closed by DragonFly itself: `cdopen()`
  refuses non-`SYSCAP_RESTRICTEDROOT` openers (`scsi_cd.c:1006-1007`);
  `trigger_as_user.log` shows `open(/dev/cd0): Operation not permitted`
  for uid 1001 even with the node chmod'd 0666.  (FreeBSD, which lacks
  this gate, exposes the same code to operator-group users.)

### 4. Fix validation

* `fix.diff` (git-apply-able, `git apply --check -p1` passes) wraps the
  three macro indices into their objects; identity on the valid domain
  proven by rig3, in-bounds under guard placement proven by rig4.
* A patched-kernel build/reboot validation is **not possible on this
  guest**: baseline and patched kernels behave identically (EIO from
  the emulator's missing opcode), so there is no in-kernel observable
  to A/B.  Recorded as `not_testable` at the kernel level, validated at
  the algorithm level — same treatment as DF-5718 for an
  emulator/hardware-gated finding.

## Honest bottom line

* **Primitive (OOB read past `.rodata` tables, user/device-influenced
  index): certain, reproduced** with the verbatim kernel algorithm under
  guard pages on the DragonFly guest.
* **In-kernel path: live** (quirk matched, ioctl reaches the conversion;
  the byte's return trip needs a 0x42-capable drive, which QEMU 7.2
  lacks).
* **Security impact: Low** — all user-reachable conversions are behind
  both the legacy-hardware quirk and DragonFly's `SYSCAP_RESTRICTEDROOT`
  open gate; no crash (mid-`.rodata`), no write primitive, no unpriv→priv
  boundary.  The bug is a latent kernel OOB read worth fixing
  defensively (and it is genuinely reachable by hostile drive firmware,
  corrupt CMOS, or — on FreeBSD-derived stacks without the open gate —
  operator-group users).

## Negative results for the other two files in the batch

* `sys/libkern/strlen.c` — **clean**.  Compiled (`sys/conf/files:2226`),
  no inline/asm shadow (`sys/sys/libkern.h:134` is a plain prototype; no
  `static __inline`, no `.S` definition anywhere in `sys/`).  Byte loop
  at strlen.c:40-41 matches libc semantics exactly (34-case randomized
  differential, `shl` in run.log: IDENTICAL); reads exactly through the
  NUL (guard-page probe with NUL as the final byte: no fault).  The
  unbounded read is the C contract; over-read risk lives in callers, not
  here.
* `sys/libkern/strnlen.c` — **clean**.  Bounded exactly: reads
  `s[0..maxlen-1]`, never `s[maxlen]` (guard rig: `strnlen(buf,300)` on a
  300-byte non-NUL buffer ending at a guard page → 300, no fault;
  `strnlen(buf,301)` → SIGSEGV, i.e. the bound is exactly maxlen;
  `strnlen(buf,0)` → 0, no deref).  268-case differential vs libc
  including boundary maxlens: IDENTICAL.

## Artifacts

`run.log` (decisive suite), `trigger_as_root.log` / `trigger_as_user.log`
(in-kernel sweep + open gate), `dmesg.txt`, `devlist.txt` (CHINON probe),
`env.txt`, `build.sh`, `run.sh`, `fix.diff`, sources
(`bcd_rig.c`, `bcd_trigger.c`, `subq_test.c`, `passrep.c`,
`str_harness.c`, `dbg.c`), host-side `up-chinon.sh` in `dfbsd-qemu/`.

## 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.
