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)
PoC verification
Evidence pack
findings/poc/DF-5835 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 :1199ticks = (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)
- kevent:
EV_SET(..., EVFILT_TIMER, EV_ADD|EV_ONESHOT, 0, 21474836999, ...)β kern_event.c:480-501filt_timerattach()performs no validation ofkn->kn_sdata; kern_event.c:425-433filt_timerreset()computestv = {21474836 s, 999000 us}and callstvtohz_high(). - setitimer:
it_value = {21474836 s, 999999 us}passesitimerfix()(normalized, non-negative) and hitstvtohz_high()at sys/kern/kern_time.c:968 insidesys_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 controldata=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_testablefix.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
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.
No comments yet.