Skip to content

Fix infinite loop in nextDate() for impossible day-of-month/month combinations - #38

Open
mrvonkalus wants to merge 1 commit into
jaclarke:masterfrom
mrvonkalus:fix/impossible-date-infinite-loop
Open

Fix infinite loop in nextDate() for impossible day-of-month/month combinations#38
mrvonkalus wants to merge 1 commit into
jaclarke:masterfrom
mrvonkalus:fix/impossible-date-infinite-loop

Conversation

@mrvonkalus

Copy link
Copy Markdown

Fixes #37.

What

nextDate() never returns for expressions where no allowed month can contain any allowed day-of-month (0 0 31 4 * = April 31, 0 0 30 2 * = February 30, etc.) when the year field is unbounded — the default for every standard 5-field expression. Full analysis and reproduction in #37.

Root cause

CronosYearsField.nextYear() pushed fromYear unconditionally for * (any) year fields, with no maxValidYear ceiling — the range/step branch right below already applies it. On an impossible date, _nextYear()'s only exit (nextYear() === null) is therefore never taken, and the year counter increments without bound (observed past year 42,000,000 under instrumentation).

Fix

One line: bound the any branch by the same maxValidYear ceiling the range/step branch already uses.

if (item.any) {
  if (fromYear <= maxValidYear) years.push(fromYear);
}

The search now terminates after the bounded scan (to year 275759) and returns null.

Tests

Five regression tests in tests/api.test.ts ("Impossible day-of-month/month combinations"):

  • 0 0 31 4 *, 0 0 30 2 *, 0 0 31 2,4,6 *null (each completes in ~0.3–1.1s; without the fix they never complete, so these fail by timeout on unfixed code)
  • Controls unchanged: 0 0 31 4,5 * → 2029-05-31, 0 0 29 2 * → 2032-02-29 (leap year)

Verification

  • tests/api.test.ts: 18/18 pass, including the 5 new tests
  • Full suite: 150 passed, 13 failed — the same 13 fail identically on clean master (timezone/DST environment-dependent tests in timezone.test.ts; pre-existing, untouched by this change)

Disclosure

Found by differential fuzzing against a brute-force reference oracle; analysis, fix and verification produced by AI agents working under my direction, reviewed and run by me before filing. Details in #37.

Made with Cursor

…binations

For an unbounded (*) year field, CronosYearsField.nextYear() pushed
fromYear unconditionally, with no maxValidYear ceiling (the range/step
branch already applies it). When no allowed month can contain any allowed
day-of-month (e.g. "0 0 31 4 *" = April 31), _nextMonth() returns null for
every year, so _nextYear()'s only exit is never taken and the year counter
increments without bound: nextDate() pins a CPU core and never returns.

Bound the any-branch by maxValidYear, matching the range/step branch, so
the search terminates (scan to year 275759, then null). Adds regression
tests: impossible combinations return null quickly; valid controls
(May 31, leap-year Feb 29) still resolve.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

nextDate() never returns for impossible day-of-month/month combinations (e.g. April 31)

1 participant