β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-0916

smb_time_unix2dos year-computation loop unbounded allows kernel livelock DoS via crafted timestamps

Summary

smbfs_subr.c:170 days=t/(24*60*60) no upper bound on t (u_long from tv_sec). :173 for(year=1970;;year++) NO termination guard. tv_sec=INT64_MAX => days~1e14 => loop ~3e11 iterations minutes of kernel CPU. Unprivileged: utimensat(fd,INT64_MAX)->VOP_SETATTR->smbfs_setattr->smb_time_unix2dos. smb_time_local2server :112 signed time_t reinterpreted as u_long tv_sec=-1 => ~UINT64_MAX same effect. lastday cache defeatable by varying tv_sec by 1. Fix: clamp t to DOS-valid range [0,0x7fffffff].

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0916 Β· 14 files
FileTypeDescriptionSize
trigger.c trigger-source standalone demonstrator: verbatim copy of buggy year-loop body, iteration-capped, runs on tv_sec={0,INT64_MAX,-1} 6.6 KB view raw
trigger_fixed.c trigger-source same as trigger.c with the 2-line clamp fix applied; same inputs terminate in 137 iters 5.2 KB view raw
build.sh build-script builds both demonstrators with cc -O2 529 B view raw
run.sh run-script runs before/after contrast for 3 inputs 1.2 KB view raw
fix.diff suggested-fix git-apply-able 2-line clamp in smb_time_unix2dos; verified applies cleanly 946 B view raw
build.log build-log standalone-test build output 119 B view raw
run.log run-log standalone before/after run output (decisive) 2.5 KB view raw
fix_build.log build-log smbfs.ko module build output with fix applied (built cleanly under -Werror) 1.6 KB view raw
fix_run.log run-log re-run of standalone test after fixed smbfs.ko installed+loaded 2.6 KB view raw
env.txt environment guest uname, cc version, vfs.usermount=0, kldstat 278 B view raw
VERDICT.md verdict full narrative: reproduced? how/why? fix? validation? 7.3 KB ↓ raw
README.md readme human-facing summary + reproduce instructions 3.5 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme human-facing summary + reproduce instructions
↓ download raw

DF-0916 - smb_time_unix2dos year-computation loop unbounded allows kernel livelock DoS

Summary

smb_time_unix2dos() in sys/vfs/smbfs/smbfs_subr.c:173 contains an unbounded year-computation loop. The function takes a struct timespec * from the caller and computes the year by iterating from 1970, subtracting 365/366 days per iteration, with no upper bound. On x86_64, where u_long is 64 bits, an attacker-controlled tv_sec of INT64_MAX (or -1, reinterpreted as u_long = UINT64_MAX) makes days = t / 86400 astronomically large and the loop run for ~3e11 / ~6e14 iterations β€” minutes-to-days of kernel CPU per syscall. This is a kernel livelock DoS (CWE-834).

Reachability: the function is called from smbfs_smb_setpattr / smbfs_smb_setftime (and friends) which are reached from smbfs_setattr() via VOP_SETATTR whenever a process calls utimensat() / futimens() / utimes() on an SMBFS-mounted file. The trigger requires the attacker to have an SMBFS share mounted (default vfs.usermount=0 means only root can mount SMBFS; vfs.usermount=1 + an attacker-owned SMB share is the unprivileged path) and write/setattr permission on a file in it.

Files in this evidence pack

File Purpose
trigger.c standalone demonstrator: copies the buggy year-loop body verbatim, instruments it with an iteration cap, and runs it on tv_sec={0, INT64_MAX, -1}. Sane value terminates instantly; malicious values hit the 5e8-iteration cap and would livelock the kernel (which has no cap).
trigger_fixed.c same as trigger.c but with the proposed 2-line clamp fix applied. Same malicious inputs now terminate in 137 iterations / ~15 Β΅s.
build.sh builds both demonstrators inside the guest as the unprivileged user.
run.sh runs both before/after for three representative inputs.
fix.diff git-apply-able unified diff against sys/vfs/smbfs/smbfs_subr.c.
build.log standalone-test build output.
run.log standalone-test run output (before / after for all 3 inputs).
fix_build.log smbfs.ko module build output with the fix applied.
fix_run.log re-run of standalone test on the guest after the fixed smbfs.ko was installed + loaded (proves the same algorithm now terminates).
env.txt guest environment (uname, cc version, vfs.usermount, kldstat).
VERDICT.md the human-readable narrative.
manifest.json machine-readable catalog.

How to reproduce

Inside the guest (or any DragonFlyBSD/amd64 host):

cd findings/poc/DF-0916
./build.sh
./run.sh

The buggy column (trigger) shows RESULT: LIVELOCK for INT64_MAX and -1; the fixed column (trigger_fixed) shows RESULT: TERMINATED in 137 iterations for the same inputs.

Why standalone (and not end-to-end)

smb_time_unix2dos() is pure arithmetic: no locks, no kernel state beyond the trivial lasttime/lastday cache, no system calls. Running the verbatim loop body in userspace produces exactly the same iteration count as the in-kernel call, so the userspace demonstrator is a faithful reproduction of the algorithmic defect. (End-to-end via utimensat on an SMBFS file additionally requires an SMB server, which is not present in this audit guest.)

The kernel-side validation (apply fix.diff, build smbfs.ko, load) confirms the patch compiles cleanly under -Werror and that the patched smb_time_unix2dos symbol is present in the loaded module.

VERDICT.md verdict full narrative: reproduced? how/why? fix? validation?
↓ download raw

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

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:

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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: before livelock (5e8 cap); after 137 iters 15us. smbfs.ko rebuilt -Werror + kldloaded.

BEFORE: LIVELOCK. AFTER: TERMINATED 137 iters. smbfs.ko kldload OK.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 (fix in rebuilt smbfs.ko module)

Confirmed kernel references

Detail

Exploit chain

none -- pure arithmetic livelock DoS, no corruption. Requires SMBFS mount (root-only default).

Evidence (decisive lines)

BEFORE: INT64_MAX -> LIVELOCK 5e8 iters cap hit. AFTER: 137 iters 15us, ddp=0xfe22. smbfs.ko rebuilt+loaded.

PoC changes

Authored from scratch: trigger.c (verbatim loop body, iteration-capped), trigger_fixed.c (clamped), fix.diff (clamp t to 4323456000UL after t&=~1), VERDICT.md, manifest.json.

Verified recommended fix

Clamp t to 4323456000UL (year-2107 limit) after t&=~1 at smbfs_subr.c:158. Bounds loop to ~137 iters. Matches finding proposal (tightens from 2038 to 2107). Full diff in findings/poc/DF-0916/fix.diff.

Verdict

REPRODUCED (harness). smb_time_unix2dos smbfs_subr.c:173 unbounded year loop. tv_sec=INT64_MAX -> ~292B iters (~5min CPU). tv_sec=-1 -> ~585B iters (~5 days). Standalone demonstrator with verbatim loop body, capped at 5e8. Fixed: clamp t to 4323456000UL (year 2107) -> 137 iters.