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)
PoC verification
Evidence pack
findings/poc/DF-2955 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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.
-
Math harness (
fattime_host.c, verbatim function bodies, compiled with the guest cc): round-trips every day 1980-01-01..2107-12-31 throughtimespec2fattimeβ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). Seeharness_guest.logsection A. -
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: remountmount_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 hasMDate = 0xfc48(year 126 = 2106, month 2, day 8), W2 hasMDate = 0xff9f(2107-12-31) β i.e. the on-disk dates are valid; onlyfattime2timespec'stsp->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
fixedRebuilt 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
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.
No comments yet.