timespec2fattime: post-2107 timestamps silently wrap the 7-bit FAT year field and truncate t2 64β32 bits β far-future utimes() persisted as arbitrary plausible dates (year-4.4M stored as 2023-11-23)
| Field | Value |
|---|---|
| ID | DF-2956 |
| 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 | 157, 173, 184, 188 |
| 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
On the write path, dates past 2107-12-31 (the FAT range end) are not clamped: (a) l=t2/LYC makes (l4)<<9 exceed the 16-bit ddp year field for l4>127, wrapping modulo 2^16; (b) t2=t1/DAY truncates int64 tv_sec to unsigned 32 bits for tv_sec >= 2^3286400. VERIFIED on the stock kernel: utimes(2108-01-01) persists on-disk MDate=0x0021 (1980-01-01) and stats back as 1980-01-01; utimes(year 4,461,763) persists MDate=0x5777 and stats as a plausible 2023-11-23 05:22:06. Unprivileged (file owner on -u 1001 mount; itimespecfix imposes no tv_sec upper bound). All stores stay within the caller's uint16_t (defined unsigned wrap; mtab walk bounded m<=47) β integrity only, no memory safety. Unprivileged user fabricates arbitrary plausible modification times on any writable msdosfs β defeats forensic timeline analysis and time-based auditing on removable media. Distinct from DF-0200 (negative tv_sec path). Fix validated (saturate at T2107 mirroring the existing 1980 clamp): rebuilt kernel #1 clamps all post-2107 inputs to 2107-12-31 preserving time-of-day; aliases gone.
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-2956 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.2 KB | β raw | |
| VERDICT.md | β | 2.8 KB | β raw | |
| verdict.json | β | 3.6 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-2956 β post-2107 tv_sec silently wraps year field / truncates to 32 bits in timespec2fattime
Where
sys/kern/subr_fattime.c:157 and :173 (timespec2fattime):
t2 = t1 / DAY; /* :157 int64 -> unsigned 32-bit TRUNCATION */
...
l = t2 / LYC;
*ddp = (l * 4) << 9; /* :173 year field is 7 bits; l*4 > 127 wraps mod 2^16 */
Two stacked overflows on the write path:
- Year-field width: dates past 2107-12-31 need
l*4 > 127;(l*4)<<9wraps modulo 2^16 when stored into theuint16_tdate word, aliasing far-future dates onto arbitrary dates in 1980..2107. - 64β32 truncation: for
tv_sec >= 2^32*86400(~year 14.5M) theunsigned t2 = t1 / DAYassignment truncates, aliasing astronomically far dates onto arbitrary recent ones.
Reachable by an unprivileged file owner: itimespecfix
(sys/kern/kern_time.c:1047) enforces only tv_sec >= 0, no upper bound;
kern_futimens gates on file-owner-or-write (vfs_syscalls.c:3847);
msdosfs_setattr (msdosfs_vnops.c:400-421) allows explicit utimes when
cr_uid == pmp->pm_uid β i.e. any user-uid-mounted msdosfs (desktop
removable-media automounts use the console user's uid).
Distinct from known DF-0200 (which is the negative tv_sec path).
Observed (stock INVARIANTS kernel #0)
| requested utimes() | on-disk MDate | stat shows |
|---|---|---|
| 2108-01-01 00:00Z | 0x0021 (1980-01-01) |
1980-01-01 |
| 4147-08-08 08:00Z | 0xeef8 |
2099-07-24 |
| year 4461763 | 0x5777 (2023-11-23) |
2023-11-23 (plausible!) |
A timestamp of year ~4.4 million is silently stored and reported as 2023-11-23 05:22:06 β indistinguishable from a genuine recent mtime.
Reproduce
Same procedure as DF-2955 (see run.sh); use the X cases:
./fattime_poc /mnt/X1 4354819200 # 2108-01-01 -> 1980-01-01
./fattime_poc /mnt/X3 140737488355327 # year 4.4M -> 2023-11-23
umount /mnt && ./findentry /tmp/fat.img X1 # MDate bytes 21 00
Fix
fix.diff adds a saturating clamp after t2 -= T1980:
if (t2 > T2107) /* ((2108-1980)*YEAR + 31 - 1) */
t2 = T2107;
Validated on rebuilt kernel #1: all post-2107 inputs saturate to 2107-12-31
(preserving time-of-day), on-disk encoding 0xff9f; the 1980/2023 aliases
are gone (poc_fixed.log).
DF-2956 VERDICT
status: reproduced (impact: timestamp-integrity / correctness β no memory
corruption; per the honest-impact enum this maps to impact: none)
What was run
Same guest/session as DF-2955 (stock kernel #0 β patched #1).
-
Math harness (
harness_guest.logsection B):timespec2fattimewith post-2107 tv_sec: - 2108-01-01 βdd=0x0021(1980-01-01) - 4147-08-08 βdd=0xeef8β decodes 2099-07-24 - year 5236 βdd=0x705cβ decodes 2036-02-28 - year 4461763 (2^47-ish) βdd=0x5777β decodes 2023-11-23 - 2^62 βdd=0xf450β decodes 2102-02-16 -
End-to-end kernel PoC (
poc_root.log): on the real msdosfs mount,utimes()with explicit times: - X1 4354819200 (2108-01-01) β stat 1980-01-01 00:00:00; on-disk entry bytes 24-25 =21 00βMDate=0x0021(findentry.log) β bit-for-bit the harness prediction. - X2 68718441600 (4147) β stat 2099-07-24. - X3 140737488355327 (year 4,461,763) β stat 2023-11-23 05:22:06; on-diskMDate=0x5777(2023-11-23) β the 64β32 truncation oft2 = t1 / DAY(subr_fattime.c:157) aliases an absurd timestamp onto a plausible recent one. -
Unprivileged (
poc_unpriv.log):mount_msdos -u 1001+ run as uid 1001 (file owner) β X-case MISMATCHs identical, no root.
Root cause chain
utimes/futimens (no upper bound: itimespecfix kern_time.c:1047-1052;
owner/write gate: vfs_syscalls.c:3847 naccess_lva(NLC_OWN|NLC_WRITE)) β
msdosfs_setattr (msdosfs_vnops.c:419-420; allowed when cr_uid == pm_uid) β
timespec2fattime (sys/kern/subr_fattime.c:139):
t2 = t1 / DAY truncates int64βuint32 (:157); (l * 4) << 9 overflows the
7-bit year field into the 16-bit *ddp (:173) for l4 > 127. Result:
far-future dates persist as arbitrary* dates in 1980..2107 with no error.
Distinct from known DF-0200 (negative tv_sec path; here tv_sec is positive and huge).
Why not higher severity
Writes only wrap into the caller's uint16_t date word (defined unsigned
wrap; table walks bounded m β€ 47 β no OOB). Downstream table decomposition
remains in-bounds for any truncated t2 (verified by construction and fuzzing
the harness with t1 up to 2^62). Silent timestamp fabrication (integrity),
same class as DF-0199/DF-0200 (Low).
Fix validation
fix.diff adds #define T2107 ((2108-1980)*YEAR + 31 - 1) and saturates
t2 > T2107 β T2107 before the 2100 adjustment (plus the DF-2955 hunk; one
rebuild validated both).
Patched kernel #1 (poc_fixed.log):
- X1 2108-01-01 β stat 2107-12-31 00:00:00, on-disk MDate=0xff9f
(maximal representable FAT date).
- X2 β 2107-12-31 08:00:00 (time-of-day preserved).
- X3 β 2107-12-31 05:22:06 β the 2023-11-23 alias is gone.
fix_status: fixed.
Guest was reset (vm.sh reset with-src) after validation.
Fix verification
fixedRebuilt X86_64_GENERIC with fix.diff (T2107 saturation; build+install RC=0). Baseline: X1->1980-01-01 (disk 0x0021), X3->2023-11-23 (disk 0x5777). Patched: X1->2107-12-31 00:00 with disk MDate=0xff9f (maximal representable date), X2->2107-12-31 08:00, X3->2107-12-31 05:22:06. All silent aliases eliminated; no regressions in control or W cases.
poc_fixed.log (X-case outputs + findentry X1 on-disk 9f ff after fix)
Confirmed kernel references
Detail
Evidence (decisive lines)
['run.log X-cases: 4354819200 (2108-01-01) -> stat 315532800 (1980-01-01); 68718441600 (4147) -> 2099-07-24; 140737488355327 -> 1700716926 (2023-11-23 05:22:06)', 'findentry.log: X1 on-disk MDate bytes 21 00 (0x0021=1980-01-01); X3 bytes 77 57 (0x5777=2023-11-23) matching harness dd predictions bit-for-bit', 'harness_guest.log section B: dd values 0021/eef8/705c/5777/f450 for post-2107 inputs', 'poc_unpriv.log: uid=1001 user reproduces X3 -> 2023-11-23 on -u 1001 mount', 'poc_fixed.log: X1->2107-12-31 00:00 (disk 0xff9f), X2->2107-12-31 08:00, X3->2107-12-31 05:22:06']
PoC changes
fattime_poc.c written fresh (explicit utimes + stat readback is the whole trigger); findentry.c added for on-disk proof; fattime_host.c section B enumerates the alias map
Verified recommended fix
Saturate: after t2 -= T1980 clamp t2 to T2107 ((2108-1980)*YEAR+31-1) in timespec2fattime
Verdict
timespec2fattime() silently aliases far-future timestamps onto arbitrary 1980..2107 dates via two stacked overflows: (1) the 7-bit FAT year field overflows when (l4)<<9 exceeds 16 bits for dates past 2107-12-31, and (2) t2 = t1/DAY truncates int64->uint32 for tv_sec >= 2^3286400. On the stock guest kernel, utimes(2108-01-01) persists MDate=0x0021 (1980-01-01) and utimes(year 4,461,763) persists MDate=0x5777 which stats as a plausible 2023-11-23 mtime with time-of-day preserved. Unprivileged (uid 1001, -u 1001 mount, file owner; itimespecfix imposes no upper bound on tv_sec). Writes are confined to the caller's uint16_t (defined unsigned wrap; table walks stay in bounds m<=47), so impact is silent timestamp fabrication (integrity), not memory corruption. Saturating clamp T2107 validated on a rebuilt kernel: all post-2107 inputs clamp to 2107-12-31 preserving time-of-day; aliases gone.
No comments yet.