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

fattime2timespec: 32-bit unsigned DAY*day multiply wraps β€” every valid FAT date 2106-02-08..2107-12-31 decodes ~55.7 years in the past (2107-12-31 β†’ 1971-11-23)

Field Value
ID DF-2955
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N
CWE CWE-190
File sys/kern/subr_fattime.c
Lines 251 (fields :233, :249)
Area kern/msdosfs
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket base:kern
Reported pending
Known CVE none
CVE match novel

Summary

sys/kern/subr_fattime.c:251 computes tsp->tv_sec += DAY * day with DAY an int (86400) and day unsigned, so the product is 32-bit unsigned and wraps mod 2^32 once day >= 49711 (2106-02-08). A correctly-encoded, fully valid on-disk MDate=0xff9f (2107-12-31) stats as 1971-11-23 22:31:44; 692 of the 46699 days in the FAT range are affected. Reproduced on stock INVARIANTS kernel #0 both as root and as unprivileged uid 1001 on a -u 1001 mounted msdosfs; the on-disk directory entry was hexdumped to prove the encoder stored the valid date and only the decoder wraps. Full-range host harness (verbatim function bodies): first round-trip failure exactly 2106-02-08, 692 failing days. Impact is silent timestamp corruption (integrity): defeats time-based backup selection, make-style staleness logic, and forensic timelines; no memory-safety consequence. No uid=0 route. Fix validated (64-bit widen): rebuilt kernel #1 decodes exactly, control unchanged.

Timeline

  • 2026-09-02 Discovered during pass-2 audit of subr_fattime.c (GLM 5.3); reproduced root+unpriv + fix validated.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2955 Β· 15 files
FileTypeDescriptionSize
README.md β€” 2.2 KB ↓ raw
VERDICT.md β€” 2.9 KB ↓ raw
verdict.json β€” 3.4 KB view raw
fattime_poc.c β€” 1.2 KB view raw
findentry.c β€” 1.6 KB view raw
fattime_host.c β€” 8.2 KB view raw
build.sh β€” 194 B view raw
run.sh β€” 934 B view raw
harness_guest.log β€” 2.3 KB view raw
poc_root.log β€” 795 B view raw
poc_unpriv.log β€” 252 B view raw
findentry.log β€” 779 B view raw
poc_fixed.log β€” 1.1 KB view raw
env.txt β€” 267 B view raw
fix.diff β€” 1011 B view raw

DF-2955 β€” DAY * day 32-bit multiply wraps: valid FAT dates 2106-02-08..2107-12-31 decode as 1970/1971

Where

sys/kern/subr_fattime.c:251 (fattime2timespec):

tsp->tv_sec += DAY * day;

DAY is an int (86400) and day is unsigned (32-bit), so the product is computed in 32-bit unsigned arithmetic and wraps modulo 2^32 before being added to the 64-bit tv_sec. day (days since 1970) reaches 49711 on 2106-02-08, and 49711 * 86400 = 4,295,030,400 > 2^32-1, so every FAT timestamp from 2106-02-08 through the end of the representable range (2107-12-31) is decoded ~2^32 seconds (~55.7 years) in the past.

This is not the known DF-0199 day-0 underflow: the input date is a fully valid, correctly encoded FAT date; the encoder (timespec2fattime) stores it correctly on disk (verified: MDate=0xfc48 / 0xff9f) and only the decoder's multiply wraps.

Impact

Any msdosfs file dated 2106-02-08..2107-12-31 β€” whether written by a future/ misdated system (a user can set this today with utimes(); there is no upper bound on tv_sec in itimespecfix, kern_time.c:1047) or present on crafted media mounted by root (the DF-2902 crafted-attach threat model) β€” stats with an mtime in 1970/1971: e.g. 2107-12-31 05:00 UTC reads back as 1971-11-23 22:31:44. Timestamp-integrity only: the produced timespec is always canonical (tv_nsec in [0,1e9)), no memory-safety effect.

Reproduce (guest, root)

dd if=/dev/zero of=/tmp/fat.img bs=1m count=8
vnconfig -c /dev/vn0 /tmp/fat.img
newfs_msdos /dev/vn0
mount_msdos /dev/vn0 /mnt
touch /mnt/W1 /mnt/W2
./fattime_poc /mnt/W1 4295030400     # 2106-02-08 -> 1970-01-01 17:31:44
./fattime_poc /mnt/W2 4354750800     # 2107-12-31 -> 1971-11-23 22:31:44
umount /mnt && ./findentry /tmp/fat.img W2   # MDate=0xff9f = valid 2107-12-31

Unprivileged variant (owner-mounted media, see run.sh): mount_msdos -u 1001 + run as uid 1001 β†’ same MISMATCH (poc_unpriv.log).

Expected: MISMATCH, stat mtime = requested βˆ’ 2^32 s. Fixed kernel: both cases MATCH exactly (poc_fixed.log).

Fix

fix.diff (line 259 after patch): tsp->tv_sec += (time_t)DAY * day; Validated on a rebuilt X86_64_GENERIC kernel (#1, 2026-09-04): baseline bad behavior gone, 2106/2107 dates decode exactly.

VERDICT.md
↓ download raw

DF-2955 VERDICT

status: reproduced (impact: timestamp-integrity / correctness β€” no memory corruption; per the honest-impact enum this maps to impact: none)

What was run

Guest: DragonFly 6.5-DEVELOPMENT, stock INVARIANTS X86_64_GENERIC #0 (Thu Jul 2 06:02:54 UTC 2026), x86_64, 64-bit time_t.

  1. Math harness (fattime_host.c, verbatim function bodies, compiled with the guest cc): round-trips every day 1980-01-01..2107-12-31 through timespec2fattime β†’ fattime2timespec. First failure: 2106-02-08 (tv_sec 4295030400). Total failing days: 692 (exactly the days 2106-02-08..2107-12-31, i.e. the failure is purely the read path; the encoder is exact through 2107-12-31). See harness_guest.log section A.

  2. End-to-end kernel PoC (fattime_poc.c + real msdosfs mount): - dd+vnconfig+newfs_msdos+mount_msdos /dev/vn0 /mnt (FAT12). - utimes("/mnt/W1", 4295030400) β†’ stat: 1970-01-01 17:31:44 (= 4295030400 βˆ’ 2^32 = 63104 s). utimes("/mnt/W2", 4354750800) β†’ stat: 1971-11-23 22:31:44 (= 4354750800 βˆ’ 2^32). Control 2025 date MATCHes. (poc_root.log) - Unprivileged: remount mount_msdos -u 1001, run as uid 1001 on own file β†’ identical MISMATCH, no root involved. (poc_unpriv.log) - On-disk proof (findentry.c): W1 entry has MDate = 0xfc48 (year 126 = 2106, month 2, day 8), W2 has MDate = 0xff9f (2107-12-31) β€” i.e. the on-disk dates are valid; only fattime2timespec's tsp->tv_sec += DAY * day (subr_fattime.c:251, int Γ— unsigned = 32-bit unsigned multiply) wraps. (findentry.log)

Root cause chain

fattime2timespec (sys/kern/subr_fattime.c:219) ← msdosfs_getattr (sys/vfs/msdosfs/msdosfs_vnops.c:251,253-254) ← stat(2) on any msdosfs file whose on-disk date β‰₯ 2106-02-08 (crafted media at mount β€” DF-2902 threat model β€” or utimes() today, since itimespecfix (kern_time.c:1047) has no upper bound on tv_sec).

day(2107-12-31) = 50403 (days since 1970); 50403 Γ— 86400 = 4,354,819,200 > 2^32βˆ’1 β†’ product mod 2^32 = 59,851,904 β†’ 1971-11-14..23 depending on time-of-day. tv_nsec is unaffected ((dh%100)Γ—1e7 ≀ 9.9e8, always canonical).

Why not higher severity

The wrapped value is a valid timespec; no kernel consumer performs unsafe arithmetic on it (stat/cache/NFS-attr only). This is silent timestamp corruption (integrity), same class as the known DF-0199/DF-0200 (Low).

Fix validation

fix.diff: tsp->tv_sec += (time_t)DAY * day; (+ unrelated DF-2956 hunk in the same file, one build validates both).

  • Baseline kernel #0: W1/W2 MISMATCH as above.
  • Patched kernel (rebuilt make nativekernel KERNCONF=X86_64_GENERIC, #1 Fri Sep 4 04:56:16 UTC 2026, BUILD_RC=0/INSTALL_RC=0): W1 β†’ 4295030400 MATCH, W2 β†’ 4354750800 MATCH (poc_fixed.log). Control unchanged. fix_status: fixed.

Guest was reset (vm.sh reset with-src) after validation.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Rebuilt X86_64_GENERIC with fix.diff ((time_t)DAY cast; build+install RC=0). Baseline kernel #0: W1->1970-01-01, W2->1971-11-23. Patched kernel #1: W1 and W2 MATCH exactly (4295030400 / 4354750800); control date unchanged. Bad behavior gone.

poc_fixed.log (W1/W2 MATCH, rc=0); fix.diff in pack; build.log excerpt in notes of run
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Fri Sep 4 04:56:16 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

['run.log lines for W1/W2: requested 4295030400/4354750800 vs stat 63104 (1970-01-01 17:31:44) / 59783504 (1971-11-23 22:31:44), both = requested - 2^32', 'findentry.log: W1 MDate bytes 48 fc (0xfc48 = 2106-02-08), W2 9f ff (0xff9f = 2107-12-31) - valid on-disk dates, decoder-only bug', 'harness_guest.log section A: first round-trip failure 2106-02-08, 692 failing days of 46699', 'poc_unpriv.log: uid=1001 user reproduces on -u 1001 mounted fs, no root', 'poc_fixed.log: patched kernel -> W1/W2 MATCH exactly']

PoC changes

fattime_poc.c written fresh for this pack (utimes+stat readback); findentry.c helper added to prove on-disk directory-entry bytes; fattime_host.c harness carries verbatim function bodies for full-range math

Verified recommended fix

Widen the product: tsp->tv_sec += (time_t)DAY * day; in fattime2timespec

Verdict

fattime2timespec() computes tsp->tv_sec += DAY * day as a 32-bit unsigned product (int DAY x unsigned day), which wraps mod 2^32 for every FAT date from 2106-02-08 (day 49711) through 2107-12-31. Verified three ways on the guest: (1) a full-range round-trip harness shows exactly 692 failing days starting 2106-02-08, all read-path; (2) on a real msdosfs mount, utimes(2107-12-31) stats back as 1971-11-23 22:31:44 while the on-disk directory entry holds a perfectly valid MDate=0xff9f; (3) the same reproduces unprivileged (uid 1001, -u 1001 mount). Impact is silent timestamp corruption (integrity) only: the wrapped value is a canonical timespec, no memory-safety effect. Fix (time_t)DAY cast validated on a rebuilt X86_64_GENERIC kernel: both cases then MATCH exactly.