Skip to content

cap transitions of a VTIMEZONE RRULE - #2047

Open
shamsManna wants to merge 5 commits into
PyCalendar:mainfrom
shamsManna:vtimezone-rrule-transition-cap
Open

shamsManna wants to merge 5 commits into
PyCalendar:mainfrom
shamsManna:vtimezone-rrule-transition-cap

Conversation

@shamsManna

@shamsManna shamsManna commented Oct 29, 2026 •

Copy link
Copy Markdown

Linked issue

No issue opened yet. Related to the VALARM REPEAT cap from GHSA-qjcq-q7h7-r74v, which this mirrors.

Description

  1. timezones built from a VTIMEZONE expand its RRULE from DTSTART with no bound. the pytz provider lists every transition up to 2038 inside Calendar.from_ical (FREQ=HOURLY from 1970: 1.7 s, 245 MB at parse). with the default zoneinfo provider, dateutil's tzical walks all transitions before a date on each offset lookup (FREQ=MINUTELY: 36 s and 1.6 GB for a single event.DTSTART.utcoffset()). FREQ=SECONDLY exhausts memory, so a ~400 byte calendar is enough to take down whatever parses it and reads an event date.
  2. Timezone.to_tz and Timezone.get_transitions now reject an RRULE that doesn't have FREQ=YEARLY, raising the existing Invalid iCalendar VTIMEZONE ValueError.

real-world VTIMEZONE rules repeat yearly, and the full test suite passes on both providers.

Repro:

BEGIN:VCALENDAR
BEGIN:VTIMEZONE
TZID:Custom
BEGIN:STANDARD
DTSTART:19700101T000000
RRULE:FREQ=SECONDLY
TZOFFSETFROM:+0100
TZOFFSETTO:+0000
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:19700601T000000
RRULE:FREQ=YEARLY
TZOFFSETFROM:+0000
TZOFFSETTO:+0100
END:DAYLIGHT
END:VTIMEZONE
BEGIN:VEVENT
UID:a
DTSTART;TZID=Custom:20250101T120000
END:VEVENT
END:VCALENDAR

Calendar.from_ical(ics).events[0].DTSTART.utcoffset()

Checklist

  • I added a change log entry, following the instructions in all subsections under Change log requirements.
  • I followed icalendar's Artificial intelligence policy and disclosed my Responsible AI use in my commit messages, if applicable.
  • I added or updated tests, if applicable.
  • I ran and ensured all tests pass locally by following Run tests.
  • I added or edited documentation as necessary, both as docstrings to be rendered in the API documentation and narrative documentation, following the Style guide.

Timezones created from a VTIMEZONE expand its RRULE from DTSTART: the
pytz provider lists every transition up to 2038 while parsing, and
dateutil's tzical walks all transitions before a date when the zoneinfo
provider computes an offset. A VTIMEZONE with RRULE:FREQ=MINUTELY took
36 s and 1.6 GB for one utcoffset() call, FREQ=SECONDLY exhausts memory.

Reject an RRULE with more than MAX_TIMEZONE_TRANSITIONS_PER_YEAR (100)
transitions in the year after its first transition, mirroring
MAX_ALARM_REPEAT. -1 disables the cap.

AI use: Claude Opus 5.5 (claude-opus-5-5). Used to search the code base
for unbounded expansion of untrusted input, measure the VTIMEZONE RRULE
case, and draft the fix, tests and change log entry, which I reviewed.
Output: (this commit)
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for the pull request, @shamsManna! A maintainer will review it shortly.

@github-actions

Copy link
Copy Markdown
Contributor

Mixed activity

Activity patterns show a mix of organic and automated signals.

View full analysis →

Evidence
  • Rapid fork→PR pattern: 11/11 fork branches followed by upstream PRs within 17s
  • Young account: Account is 38 days old

Last 5 PRs:

This is an automated analysis by AgentScan

@github-actions github-actions Bot added the ai-suspicion This contribution is possibly created with lots of AI help without enough human understanding. label Oct 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

One or more PR-quality checks did not pass. Review the PR Quality Checks summary in the workflow run for the failed checks and their details, then make any necessary updates.

Checklist items may remain unchecked while work is in progress. Please tick each item as you complete it.

If you used AI assistance, follow the project's Responsible AI use policy and provide the required disclosures.

@github-actions github-actions Bot added the agentscan:needs-review AgentScan flagged this item for review because it has mixed signals label Oct 29, 2026
Comment thread src/icalendar/cal/timezone.py Resolved
angatha
angatha previously approved these changes Nov 7, 2026

@angatha angatha left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically fine. I pass this to @stevepiercy for the few doc strings.

@stevepiercy stevepiercy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks solid. I have minor tweaks to the docstrings and news fragment, otherwise it's ready to merge. Thank you for your contribution, @shamsManna!

Comment thread src/icalendar/config.py Outdated
"""

MAX_TIMEZONE_TRANSITIONS_PER_YEAR: int = 100
"""Cap on transitions a ``VTIMEZONE`` ``RRULE`` may produce within a year.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not necessary to make RFC components into inline literals. We're working on that throughout the docstrings, as the previous one shows we have a lot more work ahead of us. 😉

Suggested change
"""Cap on transitions a ``VTIMEZONE`` ``RRULE`` may produce within a year.
"""Cap the transitions that a VTIMEZONE RRULE may produce within a year.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one is moot now: with the FREQ=YEARLY check from the latest review the config option is gone.

Comment thread src/icalendar/config.py Outdated
Comment on lines +23 to +28
A ``STANDARD`` or ``DAYLIGHT`` component of a ``VTIMEZONE`` usually repeats yearly.
Timezones created from a ``VTIMEZONE`` expand its ``RRULE`` from ``DTSTART``, so
an ``RRULE`` such as ``FREQ=SECONDLY`` can exhaust memory or CPU when a date
in this timezone is used.
A ``VTIMEZONE`` whose ``RRULE`` produces more transitions within a year is invalid.
Set to ``-1`` to disable the cap, which should be done only for fully trusted input.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, but keep the values of components as inline literals.

Suggested change
A ``STANDARD`` or ``DAYLIGHT`` component of a ``VTIMEZONE`` usually repeats yearly.
Timezones created from a ``VTIMEZONE`` expand its ``RRULE`` from ``DTSTART``, so
an ``RRULE`` such as ``FREQ=SECONDLY`` can exhaust memory or CPU when a date
in this timezone is used.
A ``VTIMEZONE`` whose ``RRULE`` produces more transitions within a year is invalid.
Set to ``-1`` to disable the cap, which should be done only for fully trusted input.
A STANDARD or DAYLIGHT component of a VTIMEZONE usually repeats yearly.
Timezones created from a VTIMEZONE expand its RRULE from DTSTART, so
an RRULE such as ``FREQ=SECONDLY`` can exhaust memory or CPU when a date
in this timezone is used.
A VTIMEZONE whose RRULE produces more than the given number of transitions
within a year is invalid. Set to ``-1`` to disable the cap, which should be
done only for fully trusted input.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here, MAX_TIMEZONE_TRANSITIONS_PER_YEAR was removed along with the transition count.

Comment thread src/icalendar/cal/timezone.py Outdated
"""Check that the RRULEs of the timezone components are not too frequent.

Raises:
ValueError: If an ``RRULE`` produces more than

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to make RFC components into inline literals.

Suggested change
ValueError: If an ``RRULE`` produces more than
ValueError: If an RRULE produces more than

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the docstring got rewritten for the FREQ=YEARLY check. I kept your style there: no literals for RRULE, VTIMEZONE or DTSTART, and YEARLY and FREQ=SECONDLY as literals.

@@ -0,0 +1 @@
Reject a ``VTIMEZONE`` whose ``RRULE`` produces more than :data:`~icalendar.config.MAX_TIMEZONE_TRANSITIONS_PER_YEAR` transitions within a year, ``100`` by default. Timezones created from a ``VTIMEZONE`` expand its ``RRULE`` from ``DTSTART``, so a calendar with an ``RRULE`` such as ``FREQ=SECONDLY`` in a ``VTIMEZONE`` could exhaust memory and CPU when it was parsed or when a date in that timezone was used. I used AI to assist me with this change. @shamsManna

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to quote RFC components as inline literals, but values should be.

Suggested change
Reject a ``VTIMEZONE`` whose ``RRULE`` produces more than :data:`~icalendar.config.MAX_TIMEZONE_TRANSITIONS_PER_YEAR` transitions within a year, ``100`` by default. Timezones created from a ``VTIMEZONE`` expand its ``RRULE`` from ``DTSTART``, so a calendar with an ``RRULE`` such as ``FREQ=SECONDLY`` in a ``VTIMEZONE`` could exhaust memory and CPU when it was parsed or when a date in that timezone was used. I used AI to assist me with this change. @shamsManna
Reject a VTIMEZONE whose RRULE produces more than :data:`~icalendar.config.MAX_TIMEZONE_TRANSITIONS_PER_YEAR` transitions within a year, ``100`` by default. Timezones created from a VTIMEZONE expand its RRULE from DTSTART, so a calendar with an RRULE such as ``FREQ=SECONDLY`` in a VTIMEZONE could exhaust memory and CPU when it was parsed or when a date in that timezone was used. I used AI to assist me with this change. @shamsManna

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rewrote it for the FREQ=YEARLY check, following your suggestion: RFC components without literals, FREQ=YEARLY and FREQ=SECONDLY as values.

@niccokunzmann niccokunzmann left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Timezones should only have a yearly RRULE.

Any other frequency can raise an error.

If you believe otherwise, please prove so by showing an example from the timezone database.

As such, any complicated calculations are not needed. We can raise an error when the frequency is not yearly.

Also what should be checked is that this works in iCal, jCal and xCal.

assert timezone.to_tz(tzp, lookup_tzid=False) is not None


PR_EXAMPLE = """BEGIN:VCALENDAR

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Such examples MUST be moved into a .ics file.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved it to calendars/pr_2047_vtimezone_secondly_rrule_ical.ics, with the same calendar as _jcal.jcal and _xcal.xml next to it. the other frequencies now start from timezones/pacific_fiji.ics and swap in the RRULE instead of building a VTIMEZONE from a string.

Replace the per-year transition count and MAX_TIMEZONE_TRANSITIONS_PER_YEAR
with a check that every RRULE of a STANDARD or DAYLIGHT component has
FREQ=YEARLY. Move the test calendar into .ics, .jcal and .xml files and
check the timezone is rejected in all three formats.
@shamsManna

Copy link
Copy Markdown
Author

switched to that. to_tz and get_transitions now raise the Invalid iCalendar VTIMEZONE ValueError for any RRULE that isn't FREQ=YEARLY, and the per-year count and MAX_TIMEZONE_TRANSITIONS_PER_YEAR are gone. every VTIMEZONE in the test data is already yearly and the full suite passes.

the example is now in .ics, .jcal and .xml files, and the tests check all three are rejected through to_tz and get_transitions, with both providers and with LazyCalendar. in jCal and xCal the event's DTSTART stays naive because the TZID isn't resolved against the VTIMEZONE there, so those tests go through the timezone directly instead of utcoffset(). with timezone.py from main the module still fails in under a second (40 failed, only the two YEARLY controls pass).

one case the FREQ check doesn't cover is a YEARLY rule with BYHOUR/BYMINUTE/BYSECOND lists. FREQ=YEARLY;BYMONTH=1;BYMONTHDAY=1;BYHOUR=0,...,23;BYMINUTE=0,...,59;BYSECOND=0,...,59 is 86,400 transitions a year, and with pytz an ~800 byte calendar like that takes 38 s and 2.1 GB inside Calendar.from_ical (zoneinfo: about 1.2 s and 300 MB per offset lookup). I can also reject more than one value in BYHOUR, BYMINUTE or BYSECOND here, or leave it for a separate PR, whichever you prefer.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agentscan:needs-review AgentScan flagged this item for review because it has mixed signals ai-suspicion This contribution is possibly created with lots of AI help without enough human understanding.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants