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

tvtohz_high/tstohz_high/tvtohz_low/tstohz_low: signed int overflow at the sec == INT_MAX/hz boundary turns ~249-day timers into immediate fires (kernel UB, unprivileged)

Summary

All four tick converters guard only the seconds term (sec <= INT_MAX/hz) and then add the sub-second term β€” plus +1 in the _high variants β€” without headroom. At hz=100 a normalized {21474836 s, >=~990000 us} duration computes 2147483701 > INT_MAX; the (int) cast wraps negative (signed-overflow UB). Reproduced unprivileged uid=1001: kevent(EVFILT_TIMER, data=21474836999 ms) β€” a ~249-day one-shot β€” fires in 0.012-0.019 s, and setitimer(ITIMER_REAL, {21474836 s, 999999 us}) delivers SIGALRM in 0.017 s; the boundary-1 control does NOT fire within the 2 s window. Unpriv inputs reach it unvalidated via kevent EVFILT_TIMER (no sdata validation at attach) and via setitimer. No memory corruption: callout_reset clamps non-positive ticks to 1-tick minimum. Long-timer semantics corruption (watchdogs/retry timers firing spuriously). Headroom-reserving guard fix (4 hunks) dry-run validated.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5835 Β· 9 files
FileTypeDescriptionSize
tvtohz_ovf.c β€” 3.4 KB view raw
build.sh β€” 94 B view raw
run.sh β€” 124 B view raw
build.log β€” 243 B view raw
run.log β€” 640 B view raw
env.txt β€” 327 B view raw
VERDICT.md β€” 3.1 KB ↓ raw
fix.diff β€” 946 B view raw
verdict.json β€” 3.5 KB view raw
VERDICT.md
↓ download raw

DF-5835 VERDICT

Status: reproduced (unprivileged, 4 successful runs: root + uid=1001, kevent + setitimer variants). Impact: none (Low severity β€” functional corruption of long-timer semantics + kernel signed-overflow UB; no memory corruption because callout_reset() clamps non-positive tick counts to a minimum 1-tick timeout).

Root cause (path:line)

  • sys/kern/kern_clock.c:1198 β€” } else if (sec <= INT_MAX / hz) { followed by :1199 ticks = (int)(sec * hz + howmany((u_long)usec, ustick)) + 1;
  • The guard reserves no headroom for howmany(usec, ustick) (≀ hz) or the +1. At hz=100, sec = INT_MAX/hz = 21474836 with usec β‰₯ 989999-ish (kevent gives usec=999000 β†’ howmany=100) overflows: 2147483600 + 100 + 1 = 2147483701 β†’ (int) wrap β†’ -2147483595.
  • Same defect: tstohz_high (kern_clock.c:1229-1230), tvtohz_low (kern_clock.c:1257-1258: 2147483600 + 99 = 2147483699 also overflows), tstohz_low (kern_clock.c:1271-1272).

Attack path (unprivileged)

  1. kevent: EV_SET(..., EVFILT_TIMER, EV_ADD|EV_ONESHOT, 0, 21474836999, ...) β€” kern_event.c:480-501 filt_timerattach() performs no validation of kn->kn_sdata; kern_event.c:425-433 filt_timerreset() computes tv = {21474836 s, 999000 us} and calls tvtohz_high().
  2. setitimer: it_value = {21474836 s, 999999 us} passes itimerfix() (normalized, non-negative) and hits tvtohz_high() at sys/kern/kern_time.c:968 inside sys_setitimer() (ITIMER_REAL).

Why it is not memory corruption

sys/kern/kern_timeout.c:788-791 (callout_reset doc): "Callers may legally pass any value, even if 0 or negative … a minimum timeout of 1 tick will be enforced" β€” c->rtick = sc->curticks + to_ticks (negative β†’ past) is clamped to softticks + 1 in _callout_update_spinlocked() (kern_timeout.c:968). The wrapped value therefore produces an immediately-firing timer, not an OOB.

Observed (guest, hz=100, X86_64_GENERIC 6.5-DEVELOPMENT)

  • root: [kevent] boundary data=21474836999ms: fired after 0.012494 s (n=1) vs control data=21474835999ms: n=0 after 2.010011 s (NOT fired).
  • unprivileged (uid=1001): boundary fired after 0.015120 s; control not fired.
  • setitimer: [setitimer] boundary {21474836s,999999us}: SIGALRM after 0.017077 s (expected: ~249 days).

The boundary-1 control staying silent pins the overflow to exactly the predicted sec value β€” not to a generic "big value" mishandling.

Fix

fix.diff tightens all four guards to (INT_MAX - hz - 1) / hz. Max-sec check at hz=100: 21474835100 + 100 + 1 = 2147483601 ≀ INT_MAX βœ”; general: sechz ≀ INT_MAX-hz-1-r β‡’ +hz+1 ≀ INT_MAX-r ≀ INT_MAX βœ”. Not rebuilt on the guest (Low/functional, non-corruption) β€” fix_status: not_tested, baseline reproduction documented above.

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

not_testable
baseline reproduced→ patch + rebuild →patched clean

fix.diff authored from line-accurate root cause (guard tightened in all four functions); kernel rebuild + patched retest not performed for this Low-severity functional finding (no memory corruption involved).

findings/poc/DF-5835/fix.diff
↓ fix.diffper-fix-DF-5835

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log: '[kevent] boundary data=21474836999ms: fired after 0.012494 s (n=1)' + '[kevent] control data=21474835999ms: n=0 after 2.010011 s' + '[setitimer] boundary {21474836s,999999us}: SIGALRM after 0.017077 s' + identical unprivileged run (uid=1001, fired 0.015120 s). build.log: BUILD_OK + hz=100 clockrate. VERDICT.md carries the full arithmetic and path:line trace.

PoC changes

PoC authored fresh (no seed): kevent + setitimer rigs in one binary with boundary and control cases; comments initially mislabeled the magnitude as ~68 years (21474836999 ms is ~249 days) β€” fixed in prose, behavior unchanged; three builds/runs total across root and unprivileged users.

Verified recommended fix

Tighten the seconds guard in all four converters to (INT_MAX - hz - 1) / hz so sec*hz + sub-second term (+1) can never exceed INT_MAX (see fix.diff).

Verdict

REPRODUCED unprivileged (root and uid=1001): all four tick-conversion routines in kern_clock.c guard only the seconds term with sec <= INT_MAX / hz (kern_clock.c:1198/1229/1257/1271) and then add the sub-second term (+1 in the _high variants), so a normalized duration at exactly the boundary (sec=INT_MAX/hz=21474836 at hz=100 with large usec/nsec) computes sec*hz+ceil(usec/ustick)+1 = 2147483701 > INT_MAX β€” the (int) cast wraps to -2147483595 (signed-overflow UB). Live: kevent(EVFILT_TIMER, data=21474836999ms) one-shot ~249-day timer fires in 0.012-0.019s; setitimer(ITIMER_REAL,{21474836s,999999us}) delivers SIGALRM in 0.017s; the 1-second-of-ms-lower control (21474835999) does NOT fire within the 2s kevent timeout, pinning the effect to the exact predicted boundary. Impact: none beyond corrupted timer semantics (the negative count is clamped by callout_reset to a 1-tick minimum per kern_timeout.c:788-791/968 β€” no memory corruption); kernel-level signed-overflow UB on an unprivileged path.