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)
PoC verification
Evidence pack
findings/poc/DF-0916 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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.
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)
- Baseline (unpatched). Reset to
with-srcsnapshot, confirmedkern.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). - Apply fix.
scp fix.diff β cd /usr/src && patch -p1 --forward; hunk applied at line 156, no fuzz. Verified the clamp is in place. - Build.
cd /usr/src/sys/vfs/smbfs && makerebuilt smbfs.ko cleanly under the kernel's-Werror -DKLD_MODULEflags.nm smbfs.koshowsT smb_time_unix2dos. (smbfs is a kld module, NOT built into the kernel image, somake nativekernelis unnecessary and irrelevant for this fix β the smbfs_subr.c translation unit is only compiled by the module build.) - Install.
kldunload smbfs; cp smbfs.ko /boot/kernel/smbfs.ko; kldload smbfsβ module loads cleanly, symbol present, kldstat shows it at id 7. - 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_unix2dospath 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-driventimespec2fattime()insys/kern/subr_fattime.c. smbfs still ships the old inline copy. lastdaycache (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
fixedVALIDATED: before livelock (5e8 cap); after 137 iters 15us. smbfs.ko rebuilt -Werror + kldloaded.
BEFORE: LIVELOCK. AFTER: TERMINATED 137 iters. smbfs.ko kldload OK.
Confirmed kernel references
- sys/vfs/smbfs/smbfs_subr.c:110
- sys/vfs/smbfs/smbfs_subr.c:147
- sys/vfs/smbfs/smbfs_subr.c:157
- sys/vfs/smbfs/smbfs_subr.c:158
- sys/vfs/smbfs/smbfs_subr.c:170
- sys/vfs/smbfs/smbfs_subr.c:173
- sys/vfs/smbfs/smbfs_smb.c:326
- sys/vfs/smbfs/smbfs_smb.c:332
- sys/vfs/smbfs/smbfs_smb.c:421
- sys/vfs/smbfs/smbfs_smb.c:427
- sys/vfs/smbfs/smbfs_vnops.c:297
- sys/vfs/smbfs/smbfs_vnops.c:390
- sys/kern/subr_fattime.c:138
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.
No comments yet.