# DF-5719 — libkern BCD lookup tables indexed without bounds (`bcd2bin`/`bin2bcd`/`hex2ascii` macros)

**File:** `sys/libkern/bcd.c` (+ macros in `sys/sys/libkern.h:59-61`)
**Severity:** Low · **Class:** CWE-125 out-of-bounds read · **Status:** primitive reproduced (guard-page rig, verbatim kernel algorithm); in-kernel completion gated (see VERDICT.md)

## What the bug is

`sys/libkern/bcd.c` exports three `.rodata` lookup tables sized *exactly*
for their valid domains:

| table | entries | valid index domain |
|---|---|---|
| `bcd2bin_data` (bcd.c:9-20) | 154 | BCD bytes `0x00..0x99` |
| `bin2bcd_data` (bcd.c:22-33) | 100 | `0..99` |
| `hex2ascii_data` (bcd.c:36) | 37 | `0..35` + NUL |

The macros in `sys/sys/libkern.h:59-61` index them **raw**:

```c
#define bcd2bin(bcd)    (bcd2bin_data[bcd])
#define bin2bcd(bin)    (bin2bcd_data[bin])
#define hex2ascii(hex)  (hex2ascii_data[hex])
```

Consumers feed these macros **unvalidated device- and ioctl-supplied
bytes**:

* `sys/bus/cam/scsi/scsi_cd.c:2044-2047` — `bin2bcd(args->start_track)` /
  `bin2bcd(args->end_track)` where `args` is the user's
  `CDIOCPLAYTRACKS` ioctl struct (`struct ioc_play_track`, all `u_char`,
  `sys/sys/cdio.h:112-118`), gated only by the `CD_Q_BCD_TRACKS` drive
  quirk.
* `scsi_cd.c:2160-2161` — `args->track = bin2bcd(args->track)` on
  `CDIOCREADSUBCHANNEL` (`_IOWR`: the struct is copied back, so on a
  compliant drive the OOB byte would be disclosed to the caller).
* `scsi_cd.c:2208/2262/2320/2338/2386/2414/2942/2964/2989` —
  `bcd2bin()` over drive-returned TOC/subchannel bytes.
* `sys/platform/pc64/isa/clock.c:558` — `bcd2bin(rtcin(port))` over raw
  CMOS RTC bytes (corrupt CMOS → OOB read at boot; wrong time only).

Any index ≥ 100 (`bin2bcd`) / ≥ 0x9a (`bcd2bin`) reads past the end of
the 100/154-byte `.rodata` object: an out-of-bounds kernel read with a
fully attacker-influenced index (on the quirk-gated paths), value used as
a SCSI CDB field and, on `CDIOCREADSUBCHANNEL`, copied back as a one-byte
kernel-`.rodata` oracle.

## Reachability on DragonFly (why Low, not higher)

* The BCD conversions only run when the drive has the `CD_Q_BCD_TRACKS`
  quirk — set at probe only for the 1980s **CHINON CDS-535**
  (`scsi_cd.c:222-225`).  Root can force it via `kern.cam.cd.N.quirks`.
* **DragonFly gates all cd(4) opens on `caps_priv_check_self(
  SYSCAP_RESTRICTEDROOT)`** (`scsi_cd.c:1006-1007`) — an unprivileged
  user gets `EPERM` from `open()` even with mode 0666 (verified on the
  guest).  (On FreeBSD the same code is reachable by operator-group
  users; DragonFly's caps check closes that door.)
* Remaining real-world triggers: legacy CHINON hardware, hostile/weird
  drive firmware, corrupt CMOS at boot, or a privileged caller.  No
  write primitive, no crash (read stays inside `.rodata`), no privilege
  boundary crossed.  Hence **Low** (matches "requires specific hardware
  or already-privileged user").

## Evidence in this pack

* `bcd_rig.c` — verbatim tables from `bcd.c` + verbatim macros from
  `libkern.h`:
  * `MODE=1` prints the real object sizes (100/154/37).
  * `MODE=2` places each object ending exactly at a `PROT_NONE` guard
    page and probes with the ioctl byte values (`bin2bcd(100/200/255)`,
    `bcd2bin(0x9a/0xff)`) → **SIGSEGV on every OOB probe, OK on the last
    in-bounds probe** — formal proof the shipped macros index past the
    object.
  * `MODE=3` exhaustive semantics differential (table ≡ BCD arithmetic
    over the whole valid domain; fix-wraps are the identity there).
  * `MODE=4` `fix.diff` macros under the same guard placement → always
    in-bounds.
* `bcd_trigger.c` — in-kernel `CDIOCREADSUBCHANNEL` sweep, track 0..255.
* `subq_test.c`, `passrep.c` — the investigation tools that established
  the QEMU limitation (see VERDICT.md).
* `run.log` — full on-guest suite output; `trigger_as_{root,user}.log`,
  `dmesg.txt`, `devlist.txt`, `env.txt` — guest state.
* `fix.diff` — bounds-wrapping macros (`git apply -p1` from repo root).

## Reproduce

On the DragonFly guest (with the CHINON drive configured via
`dfbsd-qemu/up-chinon.sh`):

```sh
sh build.sh
sh run.sh          # rig results + differentials + trigger sweep
```

Expected decisive lines (run.log):

```
bin2bcd(200)  (ioctl byte)     : SIGSEGV
bcd2bin(0xff) (garbage CMOS)   : SIGSEGV
bcd2bin vs arithmetic + fix-mask identity over valid BCD 0x00..0x99: MATCH
bin2bcd_fixed(255) : in-bounds (value=85)
```
