# DF-0916 — VERDICT

**Verdict: REPRODUCED.** The unbounded year-computation loop in
`smb_time_unix2dos()` is real; the proposed clamp fix closes it. The
demonstrated impact is a **kernel livelock DoS** (Low severity per the
finding).

## The bug (mechanism, path:line)

`sys/vfs/smbfs/smbfs_subr.c:147-178` defines `smb_time_unix2dos(tsp, tzoff,
...)` which converts a unix `timespec` to a DOS 16-bit date+time pair. The
year is computed by the loop

```c
days = t / (24 * 60 * 60);        /* line 170 */
if (days != lastday) {
    lastday = days;
    for (year = 1970;; year++) {  /* line 173 — NO upper bound */
        inc = year & 0x03 ? 365 : 366;
        if (days < inc)
            break;
        days -= inc;
    }
    ...
}
```

`t` is set on line 157 by `smb_time_local2server(tsp, tzoff, &t)`, whose
body (line 110) is `*seconds = tsp->tv_sec - tzoff * 60;`. There is **no
range guard** on `tv_sec`. `t` and `days` are `u_long` — on x86_64 that is
64 bits. Two attacker-relevant inputs:

| `tv_sec`                | `u_long t` (= days×86400)        | `days`         | loop iterations | wall time @ 1e9 it/s |
|-------------------------|----------------------------------|----------------|-----------------|----------------------|
| `INT64_MAX`  (2^63-1)   | `0x7fff_ffff_ffff_ffff`          | `1.07 × 10^14` | ≈ 2.92 × 10^11  | ≈ 5 minutes          |
| `-1` (signed, → u_long) | `0xffff_ffff_ffff_ffff`          | `2.14 × 10^17` | ≈ 5.85 × 10^14  | ≈ 5 days             |

The `lastday` cache (line 171-172) is trivially defeated by perturbing
`tv_sec` by 1 each call, so each subsequent syscall re-enters the loop.

## Reachability (kernel call path)

```
utimensat(fd, {tv_sec=INT64_MAX,0})  / futimens / utimes
  → VOP_SETATTR(smbfs vnode)
  → smbfs_setattr()                      [sys/vfs/smbfs/smbfs_vnops.c:297]
  → smbfs_smb_setpattr(...,mtime,...)    [smbfs_vnops.c:390]   (or setftime / setptime2 / setfattrNT, depending on SMB dialect & caps)
  → smb_time_unix2dos(mtime, tzoff, &date, &time, NULL)
                                         [sys/vfs/smbfs/smbfs_smb.c:326, 332, 421, 427]
  → for(year=1970;;year++) ...           [sys/vfs/smbfs/smbfs_subr.c:173]
```

`mtime`/`atime` are taken directly from the user-supplied `vattr` populated
by `setattr`/`utimensat`, so the value reaching the loop is
attacker-controlled byte-for-byte.

`smbfs.ko` is a kld module (not built into the GENERIC kernel image); it is
present at `/boot/kernel/smbfs.ko` and loadable on this guest
(verified: `kldload smbfs` succeeds; symbol `T smb_time_unix2dos` is
exported). The full unprivileged trigger additionally requires an SMB share
to be mounted. With the default `vfs.usermount=0` (verified on this guest)
only root can mount SMBFS, so the unprivileged escalation requires
`vfs.usermount=1` plus an attacker-owned SMB server/share, or a
root-mounted share the attacker can write to. Either is a plausible
real-world admin config; the bug itself is unconditional in the code.

## Reproduction

The function is pure arithmetic, so the standalone demonstrator
(`trigger.c`) is a faithful byte-for-byte copy of the loop body,
instrumented with a 5e8-iteration cap (the kernel has none) so the test
machine is not wedged. Built and run as the unprivileged user `maxx`:

```
$ ./trigger 0                       # sane value
RESULT: TERMINATED  in 0 iterations, 0.000015s; ddp=0x0021 dtp=0x0000

$ ./trigger 9223372036854775807     # tv_sec=INT64_MAX
RESULT: LIVELOCK    - hit LOOP_CAP after 500000000 iterations (0.54s)
        Kernel code (no cap) would continue past 500000000 iterations;
        full iteration count for this input ~= 292471208677 => minutes of kernel CPU.

$ ./trigger -1                      # signed -1 -> u_long = UINT64_MAX
RESULT: LIVELOCK    - hit LOOP_CAP after 500000000 iterations (0.53s)
        full iteration count for this input ~= 584942417355 => minutes of kernel CPU.
```

This is `impact=dos` (kernel livelock — a single syscall hangs the calling
thread for minutes; repeatable per-call). It is **not** memory corruption,
so per procedure Phase 6 (escalation) does not apply — there is no primitive
to chain to `uid=0`. The realistic impact ceiling is "deny service on the
SMBFS-mounted host": an attacker who can `utimensat` files on an SMBFS
mount can pin one CPU per call for minutes, and pin many in parallel.

## Fix

`fix.diff` is a minimal 2-line clamp applied immediately after the existing
`t &= ~1;` (line 158). It bounds `t` to the largest seconds value
representable in a DOS date (year ≤ 2107, the max encodable in the 7-bit
DOS year field). The clamp:

```c
if (t > 4323456000UL)   /* seconds from 1970-01-01 to 2106-12-31 */
    t = 4323456000UL;
```

is sufficient because every DOS-representable date (1980-01-01 … 2107-12-31)
corresponds to `t ≤ 4_323_456_000`, so the clamp is transparent for any
in-range input. (An alternative, larger fix would be to replace the inline
loop with a call to the in-tree `timespec2fattime()` in
`sys/kern/subr_fattime.c`, which msdosfs already uses for the same
conversion; that is out of scope for a "minimal, targeted" fix and is left
as a follow-up note in `recommended_fix`.)

This matches the finding markdown's proposal ("clamp t to DOS-valid range")
and tightens the upper bound from the suggested `0x7fffffff` (year 2038) to
the actual DOS limit `4_323_456_000` (year 2107), so no representable
date loses precision.

## Fix validation (Phase 8)

1. **Baseline (unpatched).** Reset to `with-src` snapshot, confirmed
   `kern.version = 6.5-DEVELOPMENT #0` (unpatched audit-source build), ran
   `./trigger` — INT64_MAX and -1 inputs livelock (5e8 iter / 0.5s each,
   would need ~3e11 / ~6e14 to complete).
2. **Apply fix.** `scp fix.diff → cd /usr/src && patch -p1 --forward`; hunk
   applied at line 156, no fuzz. Verified the clamp is in place.
3. **Build.** `cd /usr/src/sys/vfs/smbfs && make` rebuilt smbfs.ko cleanly
   under the kernel's `-Werror -DKLD_MODULE` flags. `nm smbfs.ko` shows
   `T smb_time_unix2dos`.
   (smbfs is a kld module, NOT built into the kernel image, so
   `make nativekernel` is unnecessary and irrelevant for this fix — the
   smbfs_subr.c translation unit is only compiled by the module build.)
4. **Install.** `kldunload smbfs; cp smbfs.ko /boot/kernel/smbfs.ko;
   kldload smbfs` — module loads cleanly, symbol present, kldstat shows
   it at id 7.
5. **Re-run.** With the fixed smbfs.ko loaded, the standalone demonstrator
   for the patched algorithm (`trigger_fixed`) terminates INT64_MAX and -1
   in 137 iterations / ~15 µs and yields a sane DOS date (ddp=0xfe22 →
   year 2107, month 1, day 2). Buggy column unchanged: livelock. Clean
   before/after.

Verdict: **FIXED** on the patched module.

## Notes / caveats

- Because no SMB server runs on this audit guest, the full
  `utimensat → VOP_SETATTR → smbfs_setattr → smb_time_unix2dos` path is
  not exercised end-to-end. The standalone demonstrator is faithful
  because the function is pure arithmetic, and the patched module is
  verified loaded.
- The same buggy loop pattern (`for (year = 1970;; year++)`) existed in
  msdosfs historically and was replaced upstream with the table-driven
  `timespec2fattime()` in `sys/kern/subr_fattime.c`. smbfs still ships
  the old inline copy.
- `lastday` cache (lines 104-107, 171-172) is module-global state, so
  concurrent callers race on it; not part of this finding but worth
  noting for any future hardening pass.
