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

resettodr() pre-epoch wall clock wraps the unsigned Y/M/D conversion into an unbounded loop under crit_enter() β€” permanent, silent, dump-less kernel hang

Summary

tv.tv_sec assigned to unsigned long tm then an unbounded year-subtraction loop under crit_enter() with masterclock_lock held: a pre-epoch clock (settimeofday(-1), or a legal clock pushed before epoch by the tz/adjkerntz adjustment) wraps tm to ~1.8e19 -> ~5e16-iteration spin. Guest-VERIFIED: settimeofday never returned, ps/kill -9 sessions timed out, no panic, guest unrecoverable without killing QEMU; shutdown path hangs identically (machine cannot power off). Privileged (SYSCAP_NOSETTIME; securelevel=-1 default disables the backward clamp). Range-guard fix validated on rebuilt kernel (returns immediately, normal RTC write intact).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5330 Β· 11 files
FileTypeDescriptionSize
README.md β€” 3.1 KB ↓ raw
VERDICT.md β€” 5.4 KB ↓ raw
df5330.c β€” 1.7 KB view raw
build.sh β€” 118 B view raw
run.sh β€” 177 B view raw
build.log β€” 67 B view raw
run.log β€” 2.7 KB view raw
run.2.log β€” 830 B view raw
env.txt β€” 494 B view raw
fix.diff β€” 1004 B view raw
verdict.json β€” 4.6 KB view raw

DF-5330 β€” resettodr() pre-epoch wall clock β†’ unbounded year loop β†’ permanent kernel hang

File: sys/platform/pc64/isa/clock.c (pass 2, GLM 5.3)

Summary

resettodr() (sys/platform/pc64/isa/clock.c:1148) converts the wall clock to RTC Y/M/D with:

unsigned long tm;
...
microtime(&tv);
tm = tv.tv_sec;                 /* clock.c:1158-1159: negative -> 0xFFFF...F */
crit_enter();                   /* clock.c:1161 */
writertc(RTC_STATUSB, RTCSB_HALT | RTCSB_24HR);
tm -= tz.tz_minuteswest * 60 + (wall_cmos_clock ? adjkerntz : 0);
writertc(RTC_SEC, bin2bcd(tm%60)); tm /= 60;
...
for (y = 1970, m = DAYSPERYEAR + LEAPYEAR(y);   /* clock.c:1175-1178 */
     tm >= m;
     y++,      m = DAYSPERYEAR + LEAPYEAR(y))
        tm -= m;

tm is unsigned long. Any wall clock before 1970-01-01 (or a legal clock pushed before the epoch by the tz_minuteswest/adjkerntz subtraction) wraps tm to ~1.8e19. The year loop then needs ~5e16 iterations (~years of CPU time) while crit_enter() defers preemption/interrupts on the CPU and the caller holds masterclock_lock exclusively. Result: permanent, unkillable kernel hang, no panic, no dump.

Reachability

settimeofday({tv_sec:-1}) β†’ sys_settimeofday() (kern/kern_time.c:655, privilege-checked with caps_priv_check_self(SYSCAP_NOSETTIME)) β†’ settime() (kern/kern_time.c:127) β†’ set_timeofday() then synchronous resettodr() (kern/kern_time.c:167). The syscall never returns.

Also reachable at shutdown (resettodr_on_shutdown, clock.c:1063) whenever the system clock is pre-epoch at poweroff, and via clock_settime() (kern_time.c:753 path). The securelevel>1 backward-step clamp (kern_time.c:148) does not apply at the default kern.securelevel = -1.

Privileged (root / SYSCAP_NOSETTIME) β†’ Low severity DoS, but certain and trivial to trigger; the hang is total and unrecoverable (worse than a panic: no crash dump, no watchdog story).

Reproduce

# host
cd dfbsd-qemu && ./vm.sh up            # if down
# copy df5330.c into the guest, then:
# guest (root)
cc -O -o /root/df5330 df5330.c
( /root/df5330 > /root/df5330.log 2>&1 & ); sleep 12
cat /root/df5330.log     # only the "setting..." line; no RETURNED marker
ps axl | grep df5330     # hangs; system degraded, no panic

Expected (vulnerable kernel): settimeofday() never returns; the guest wedges (ssh sessions hang mid-command; some userland on other CPUs limps); no panic on serial console. After ~90 s the observation command times out (RUN_RC=124).

Expected (patched kernel, fix.diff): settimeofday returns immediately; marker line settimeofday RETURNED (bug did not trigger) is printed; guest stays healthy; RTC is left untouched (resettodr bails out before writing date registers).

Artifacts

  • df5330.c trigger source
  • build.log compiler output (vulnerable build)
  • run.log decisive baseline run (vulnerable kernel) β€” hang
  • run.2.log patched-kernel re-run β€” returns cleanly
  • env.txt guest uname / sysctls
  • fix.diff git-apply-able one-hunk fix (validated on guest)
  • VERDICT.md full narrative
  • verdict.json machine verdict (persist_poc.py schema)
VERDICT.md
↓ download raw

DF-5330 β€” VERDICT

Finding: resettodr() (sys/platform/pc64/isa/clock.c:1148) converts the wall clock to RTC year/month/day using unsigned long tm = tv.tv_sec (clock.c:1151,1158-1159) followed by an unbounded year-subtraction loop (clock.c:1175-1178) executed under crit_enter() (clock.c:1161). A wall clock set before 1970-01-01 β€” or any clock pushed before the epoch by the tm -= tz.tz_minuteswest * 60 + (wall_cmos_clock ? adjkerntz : 0) adjustment (clock.c:1167) β€” wraps tm to ~1.8e19, so the loop needs ~5e16 iterations to terminate. It never does in practice: the spinning CPU has preemption/interrupts deferred (crit) and the caller (settime(), kern/kern_time.c:127) holds masterclock_lock exclusively across the whole resettodr() call (kern/kern_time.c:167). Permanent, unkillable kernel hang; no panic, no crash dump; the guest cannot even be shut down (the shutdown eventhandler calls resettodr() again, clock.c:1054/1063).

Reproduced? YES (baseline, vulnerable kernel)

Chain proven live on the QEMU guest (fresh with-src snapshot, kernel of 2026-07-02, kern.securelevel=-1):

  1. settimeofday({tv_sec:-1}) as root β†’ sys_settimeofday() (kern/kern_time.c:655; caps_priv_check_self(SYSCAP_NOSETTIME) passes) β†’ settime() (kern/kern_time.c:127) β†’ set_timeofday() β†’ synchronous resettodr() (kern/kern_time.c:167).
  2. resettodr(): microtime(&tv) now returns the pre-epoch clock; tm = (unsigned long)-1; the year loop at clock.c:1175 spins inside crit.
  3. Observed (run.log): the PoC prints only DF-5330: setting wall clock to tv_sec=-1 ...; settimeofday() never returns (no RETURNED marker, ever). The 90 s observation session times out (RUN_RC=124). A follow-up session that runs ps axl | grep df5330 also times out β€” process-table inspection needs resources gated by the dead CPU. kill -9 cannot be delivered (the thread never leaves crit). Simple echo on other CPUs still works: the system is half-frozen and useless, not panicked (dmesg/serial console show no crash).
  4. Recovery required kill -9 of the QEMU process on the host: the guest ignored shutdown (which would have entered the same spin from resettodr_on_shutdown).

Classification per the honest-status table: RUN_RC=124 + guest wedged, hang expected β†’ status=reproduced, impact=dos (privileged local DoS; severity Low because the trigger requires root / SYSCAP_NOSETTIME).

Why not higher severity?

The only entry points that reach resettodr() with attacker-influenced time are settimeofday()/clock_settime() (both caps_priv_check_self(SYSCAP_NOSETTIME)-gated, kern_time.c:661/753) and system shutdown (privileged). No unprivileged path was found. Impact for a privileged attacker is still notable because the hang is silent, total and dump-less β€” but by the project rubric this is Low.

Fix validation (fix.diff) β€” FIXED

fix.diff adds a range check after the local-time subtraction (clock.c:1168+) that bails out (restoring rtc_statusb and releasing crit) when tm exceeds a sane bound (year 9999), which also covers the wrapped pre-epoch case. Validated on the guest:

  1. Applied fix.diff to the guest's /usr/src, built make -j6 nativekernel KERNCONF=X86_64_GENERIC, installed, rebooted into the patched kernel.
  2. Re-ran the exact same PoC: settimeofday() returns immediately (settimeofday RETURNED (bug did not trigger) printed), the guest stays healthy, follow-up ps works, RTC left untouched.
  3. Also re-checked normal RTC write path still works (set clock forward, resettodr writes RTC, value in range) β€” no regression.

See run.2.log for the patched run.

Negative results / adjacent candidates considered and killed

  • resettodr month loop (clock.c:1185 for (m = 0; ; m++)) has no m < 12 bound, but the preceding year loop guarantees tm <= 365 on entry, so m never exceeds 11 in the current code (the fix's range guard makes this airtight). Not independently exploitable.
  • inittodr month OOB (daysinmonth[month-1], clock.c:1113) = known DF-1027, not re-reported. Unvalidated RTC day/hours/min/sec only skew the clock (no memory unsafety).
  • timer0_running (clock.c:220 vs 359): clkintr clears it without clock_lock while other CPUs read it under the lock β€” analyzed: the interrupt handler unconditionally re-dispatches all per-CPU systimer queues, so a skipped reload self-heals before the timer can be left dead. 15 years in production. Not reportable as demonstrated.
  • hw_i8254_timestamp (clock.c:1370): %016lx %016lx needs 34 bytes in char buf[32] β€” ksnprintf truncates safely; cosmetic only.
  • sysbeep() leaks timer2 acquisition when sysbeep_enable == 0 (clock.c:507-510 returns without release_timer2()): console-beep-only logic bug, no security impact.
  • All sysctls in the file are CTLFLAG_RD; tunables are loader-time only. hw.i8254.timestamp is world-readable but leaks only counter values (TSC is user-readable anyway).
  • muldivu64 (systm.h:505) uses 128-bit intermediates β€” no overflow in DELAY/CHECKTIMEOUT/i8254 math for any in-tree caller.

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied to guest /usr/src, kernel rebuilt (nativekernel, RC=0), installed, rebooted; exact PoC re-run: settimeofday returns immediately (POC_RC=0, RETURNED marker printed), ps/uptime healthy, and the normal in-range path still executes resettodr fully (valid settimeofday returns and clock changes apply). The previously-observed permanent hang is gone on the patched kernel.

run.2.log (patched-kernel PoC run + valid-time resettodr run); build/install logs quoted therein (Kernel build completed Fri Oct 2 22:24:14 UTC 2026, install RC=0, uname -v '#1: Fri Oct 2 22:20:46 UTC 2026').
↓ fix.diffper-fix-DF-5330

Confirmed kernel references

Detail

Exploit chain

root: settimeofday({tv_sec:-1}) -> sys_settimeofday (kern_time.c:655, privilege ok) -> settime (kern_time.c:127) -> set_timeofday + synchronous resettodr (kern_time.c:167) -> microtime returns pre-epoch time -> tm=(unsigned long)-1 -> year loop clock.c:1175 spins ~5e16 iterations inside crit_enter with masterclock_lock held -> cpu0 unrecoverable, timer/clock subsystem degraded system-wide, no panic, no dump, shutdown path (resettodr_on_shutdown, clock.c:1063) hangs identically. Unprivileged escalation: none (time-setting syscalls are SYSCAP_NOSETTIME-gated).

Evidence (decisive lines)

run.log: trigger printed 'DF-5330: setting wall clock to tv_sec=-1 ...' and settimeofday NEVER returned (no RETURNED marker); observation session RUN_RC=124; the ps/kill session also RC=124; dmesg+serial console show no panic; guest ignored shutdown (had to kill -9 qemu). run.2.log: same PoC on fix.diff-patched kernel prints 'settimeofday RETURNED (bug did not trigger)' with POC_RC=0, ps works, guest healthy, and valid-time settimeofday still runs the normal resettodr path ('clock set to ... ok (resettodr ran, returned)').

PoC changes

Seed concept only; wrote the 20-line trigger from scratch (settimeofday with tv_sec=-1, marker before/after). No fixes needed; built first try with base gcc80.

Verified recommended fix

In resettodr(), after computing local time (tm -= tz/adjkerntz), range-check tm (bail out restoring rtc_statusb if tm > seconds-to-year-9999, which also catches all pre-epoch wraps) before the Y/M/D conversion loops; see fix.diff.

Verdict

resettodr() in sys/platform/pc64/isa/clock.c stores the wall clock in an unsigned long (tm = tv.tv_sec, clock.c:1158-1159) and converts it to RTC Y/M/D with an unbounded year-subtraction loop (clock.c:1175-1178) executed under crit_enter() (clock.c:1161). settimeofday({tv_sec:-1}) as root reaches this synchronously (sys_settimeofday -> settime -> resettodr, kern/kern_time.c:655/127/167); the negative time wraps tm to ~1.8e19 and the loop spins ~5e16 iterations with preemption/interrupts deferred and masterclock_lock held. Verified live on the QEMU guest: the syscall never returned, follow-up ssh sessions hung (RUN_RC=124), kill -9 undeliverable, guest could not even be shut down, no panic/dump. One-line range guard (fix.diff) validated by full in-guest kernel rebuild: on the patched kernel settimeofday returns instantly and the guest stays healthy, while the normal in-range resettodr path (RTC write) still works. Privileged (root/SYSCAP_NOSETTIME) local permanent kernel hang -> Low severity DoS.