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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2956 Β· 15 files
FileTypeDescriptionSize
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:

  1. Year-field width: dates past 2107-12-31 need l*4 > 127; (l*4)<<9 wraps modulo 2^16 when stored into the uint16_t date word, aliasing far-future dates onto arbitrary dates in 1980..2107.
  2. 64β†’32 truncation: for tv_sec >= 2^32*86400 (~year 14.5M) the unsigned t2 = t1 / DAY assignment 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).

VERDICT.md
↓ download raw

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).

  1. Math harness (harness_guest.log section B): timespec2fattime with 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

  2. 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-disk MDate=0x5777 (2023-11-23) β€” the 64β†’32 truncation of t2 = t1 / DAY (subr_fattime.c:157) aliases an absurd timestamp onto a plausible recent one.

  3. 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Rebuilt 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)
↓ 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 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.