# DF-0149 — Signed-integer truncation in TLV walk size math (subr_module.c)

## Verdict: REPRODUCED (code-defect confirmed; boot-time-only, not runtime-triggerable)

The signed-truncation defect is **real and confirmed** by code inspection and a
standalone arithmetic harness. The vulnerable walk functions
(`preload_search_by_name` / `_by_type` / `_next_name` / `_info`,
`preload_delete_name`, `preload_bootstrap_relocate`) all run **at boot** over
`preload_metadata`, a blob handed to the kernel by the boot loader from the
kernel + preloaded-module image. There is **no unprivileged-userspace trigger**:
`preload_metadata` is immutable after boot, and the only runtime-reachable walker
(`sysctl debug.dump_modinfo`, `preload_dump_internal`) uses **unsigned** math
(`bptr += roundup(len, sizeof(u_long)) / sizeof(uint32_t)` with `uint32_t len`)
and is therefore **not** affected.

## The bug (sys/kern/subr_module.c, every walk, e.g. :79-81)

```c
int  next;                                   /* SIGNED */
...
next = sizeof(u_int32_t) * 2 + hdr[1];       /* hdr[1] is u_int32_t */
next = roundup(next, sizeof(u_long));        /* roundup is division-based, sign-preserving */
curp += next;
```

`sizeof(u_int32_t) * 2 + hdr[1]` is computed in `size_t` (unsigned 64-bit:
`8 + hdr[1]`), then **truncated to `int`** on assignment to `next`.
`roundup(x,y) = ((((x)+((y)-1))/(y))*(y))` (sys/sys/param.h:402) is
sign-preserving, so:

- `hdr[1] == 0x80000000` → `8 + 0x80000000 = 0x80000008` → `int` = **-2147483640**
  → `curp` walks **BACKWARD** past `preload_metadata` → OOB read of memory before
  the blob (and, in `preload_delete_name`/`_bootstrap_relocate`, OOB **write**).
- `hdr[1] == 0xFFFFFFF8` → `8 + 0xFFFFFFF8 = 0x100000000` → truncated `int` = **0**
  → `roundup(0,8)=0` → `curp` never advances → **infinite loop / boot hang**.

## Evidence (harness)

`tlv_truncation.c` replicates the exact kernel arithmetic over three crafted
metadata blobs:

```
A: benign (hdr[1]=6)         -> next=16  -> forward walk, hits terminator OK
B: hdr[1]=0x80000000         -> next=-2147483640 (0x80000008) -> BACKWARD walk
   step 1: WOULD READ at offset -2147483640 -- *** BEFORE *** the 16-byte blob
C: hdr[1]=0xFFFFFFF8         -> next=0  -> INFINITE LOOP (boot hang), never advances
```

(The harness bounds-checks its reads so it reports the divergence instead of
segfaulting; the real kernel performs no such check.)

## Exploit chain

`none` — boot-time-only code defect. `preload_metadata` originates from the
trusted boot loader / preloaded-module image; triggering requires a tampered
preloaded module or bootloader compromise (CVSS `PR:H`/`AC:H`, matching the
finding). No unprivileged-userspace path exists, so there is no escalation chain
to develop. The realistic impact ceiling is **boot failure / hang / OOB kernel
memory access during early boot** when a malicious module is preloaded.

## Fix

`fix.diff` changes the per-record advance variable from `int next` to
`size_t next` in **all six** walk functions (the same defect pattern repeats in
each). With an unsigned type the truncation cannot produce a negative value, so
`curp` can never walk backward and `next==0` (from `0x100000000` truncation)
becomes the true large forward advance instead. `git apply --check` passes.

## Fix validation

`not_testable`: the vulnerable paths run only at boot over boot-loader-supplied
metadata; there is no runtime syscall/sysctl that re-walks with signed math (the
sole runtime walker, `debug.dump_modinfo`, uses unsigned math and is unaffected).
Validated by `git apply --check` (clean) plus the harness showing the arithmetic
root cause. A patched kernel would exhibit identical runtime behavior on a
legitimate boot; the fix is a hardening of the boot-metadata trust boundary.
