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)
PoC verification
Evidence pack
findings/poc/DF-5330 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.ctrigger sourcebuild.logcompiler output (vulnerable build)run.logdecisive baseline run (vulnerable kernel) β hangrun.2.logpatched-kernel re-run β returns cleanlyenv.txtguest uname / sysctlsfix.diffgit-apply-able one-hunk fix (validated on guest)VERDICT.mdfull narrativeverdict.jsonmachine verdict (persist_poc.py schema)
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):
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()β synchronousresettodr()(kern/kern_time.c:167).resettodr():microtime(&tv)now returns the pre-epoch clock;tm = (unsigned long)-1; the year loop at clock.c:1175 spins inside crit.- 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 runsps axl | grep df5330also times out β process-table inspection needs resources gated by the dead CPU.kill -9cannot be delivered (the thread never leaves crit). Simpleechoon other CPUs still works: the system is half-frozen and useless, not panicked (dmesg/serial console show no crash). - Recovery required
kill -9of the QEMU process on the host: the guest ignoredshutdown(which would have entered the same spin fromresettodr_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:
- Applied
fix.diffto the guest's/usr/src, builtmake -j6 nativekernel KERNCONF=X86_64_GENERIC, installed, rebooted into the patched kernel. - Re-ran the exact same PoC:
settimeofday()returns immediately (settimeofday RETURNED (bug did not trigger)printed), the guest stays healthy, follow-uppsworks, RTC left untouched. - Also re-checked normal RTC write path still works (set clock forward,
resettodrwrites 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 nom < 12bound, but the preceding year loop guaranteestm <= 365on entry, somnever 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 %016lxneeds 34 bytes inchar buf[32]β ksnprintf truncates safely; cosmetic only.sysbeep()leaks timer2 acquisition whensysbeep_enable == 0(clock.c:507-510 returns withoutrelease_timer2()): console-beep-only logic bug, no security impact.- All sysctls in the file are CTLFLAG_RD; tunables are loader-time only.
hw.i8254.timestampis 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
fixedfix.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').
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.
No comments yet.