# DF-4869 — mps debug/table layer: live-but-gated unvalidated NumEntries walk, dead config-page printers, sentinel+1 convention in mps_describe_table

## What

Three related latent defects in `sys/dev/raid/mps/mps_table.c` (twin of DF-4790 on
the mpr side):

1. **Unvalidated firmware-count walk in `mps_print_evt_sas`** (`mps_table.c:332-345`):
   for `MPI2_EVENT_SAS_TOPOLOGY_CHANGE_LIST` it walks `for (i = 0; i <
   data->NumEntries; i++) phy = &data->PHY[i];` where `NumEntries` is a
   firmware-supplied u8 (up to 255) and `PHY[]` entries are 4 bytes each
   (mpi2_ioc.h:825-844). The event buffer at this point is the **reply frame**
   itself (`sc->reply_frames + offset`, `sys/dev/raid/mps/mps.c:1530-1543`), sized
   `ReplyFrameSize * 4` bytes per frame (`mps.c:718`) — so a 255-entry walk reads
   up to ~1 KB past a ~96-128-byte frame: a firmware-controlled OOB read inside the
   reply DMA arena. The function is **live** — `mpssas_evt_handler` calls it on
   every SAS event (`sys/dev/raid/mps/mps_sas_lsi.c:131`) — but every field read
   sits inside `mps_dprint_field(MPS_EVENT, ...)`, gated on
   `sc->mps_debug & MPS_EVENT` (`mpsvar.h:609`, `:628-632`; default 0, root-set
   sysctl). Latent unless debugging is enabled.

2. **Dead printers**: `mps_print_sasdev0` (`mps_table.c:259-285`),
   `mps_print_expander1` (`:383-421`) and `mps_print_sasphy0` (`:423-447`) have
   **zero callers tree-wide** (only declared in `mps_table.h:47-50`). They parse
   config-page buffers whose backing allocation size is never validated against the
   page struct; any future caller inherits the unvalidated-fw-count/unbounded-read
   pattern (the same pattern that produced the live findings DF-4559/DF-4560 on
   this driver's sibling code).

3. **Sentinel+1 convention in `mps_describe_table`** (`mps_table.c:63-73`): when no
   code matches it returns `table[i+1].string` — one entry **past** the `{NULL, 0}`
   sentinel. In-bounds today only because all nine tables in this file ship an
   extra `"Unknown …"` entry after the sentinel (`:96-97`, `:106-107`, `:119-120`,
   `:127-128`, `:140-141`, `:150-151`, `:157-158`, `:166-167`, `:182-183`); the two
   external users (`mps_sas_lsi.c:671-673`) also conform. Any new table that ends
   at the sentinel turns the fallback into a one-entry OOB read of static memory.

Minor: `case MPI2_EVENT_SAS_DEVICE_STATUS_CHANGE` at `mps_table.c:363-377` is
missing its `break;` — falls through into `default: break;`, benign today.

## Trigger / threat

(1) fires only with `hw.mps.N.debug` (root-settable) at the MPS_EVENT bit and a
malicious/buggy controller sending a topology event with NumEntries larger than the
actual event data; (2) has no trigger path at all; (3) has no non-conforming table
today. Pure hardening / latent-pattern filing (Info), so the patterns are not
resurrected as-is — same rationale as DF-4790.

## Reproduce

No trigger path exists on the audit guest (no MPT2 hardware — see `env.txt`; and
for (2)/(3) no trigger path exists on any machine). Dead/latent code, verified by
tree-wide grep for callers (see VERDICT.md).

## Fix

`fix.diff` — bound the NumEntries walk by `EventDataLength`, return a literal
fallback in `mps_describe_table`, and add the missing `break;`. Preferred
alternative for (2): delete the three dead printers.
