Skip to content

sudoers: fix leap year calculation in getdate() - #595

Merged
millert merged 1 commit into
sudo-project:mainfrom
UOS-liang:getdate-leap-year
Nov 12, 2026
Merged

millert merged 1 commit into
sudo-project:mainfrom
UOS-liang:getdate-leap-year

Conversation

@UOS-liang

@UOS-liang UOS-liang commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

get_date() (in plugins/sudoers/getdate.y) counts the days between
EPOCH and the requested year with:

for (i = EPOCH; i < Year; i++)
	Julian += 365 + (i % 4 == 0);

Every year divisible by 4 is treated as a leap year. Century years not
divisible by 400 (2100, 2200, 2300, ...) are counted with one extra day,
so dates in later years are computed too large by one day per such year
that has passed. For example:

Jan 1, 2101 -> Jan 2, 2101
Jan 1, 2300 -> Jan 3, 2300

The DaysInMonth[1] calculation a few lines above already uses the full
Gregorian rule; the day-count loop was missed.

Fix

Use the same rule for the year loop:

for (i = EPOCH; i < Year; i++)
	Julian += 365 + (i % 4 == 0 && (i % 100 != 0 || i % 400 == 0));

getdate.c is a generated file but is checked in, and it carries a copy
of Convert(), so both files are updated identically.

This function is used by sudoreplay to parse fromdate/todate.

Testing

Built sudo from this branch in a mock UOS 25 x86_64 chroot with
gcc 12.3.1 (configure --disable-nls --disable-python && make -j),
with no new warnings.

For a behavior test, a small harness was linked against the generated
getdate.o and the old implementation (the single i % 4 == 0
expression) was compiled side by side. Results with TZ=UTC, compared
with GNU date -u:

input old (broken) patched GNU date
Jan 1, 2100 4102444800 4102444800 4102444800
Jan 1, 2101 4134067200 4133980800 4133980800
Jan 1, 2200 7258204800 7258118400 7258118400
Jan 1, 2300 10413964800 10413792000 10413792000
Jan 1, 2400 13569724800 13569465600 13569465600

The old values are exactly one day (86400 s) too large per non-leap
century year; the patched values match GNU date. Note that 2100 itself
is still correct in the old code because the loop only counts years
before the requested one; the error starts with 2101, when 2100 is
included in the sum.

Fixes #446

get_date() counts the days between EPOCH and the requested year with

  Julian += 365 + (i % 4 == 0);

which treats every year divisible by 4 as a leap year.  Century years
that are not divisible by 400 (2100, 2200, 2300, ...) are therefore
counted with an extra day.  Any date in a later year is then computed
too large by one day for every such year that has passed, e.g.
"Jan 1, 2101" is returned as Jan 2, and "Jan 1, 2300" is off by two
days.

Use the full Gregorian rule, as is already done for DaysInMonth[1].
The generated getdate.c contains a copy of the same function, so update
it as well.

Fixes: sudo-project#446
Signed-off-by: mahailiang <[email protected]>

@millert millert 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.

Looks correct. I guess this counts as "future-proofing".

@millert
millert merged commit d9ed926 into sudo-project:main Nov 12, 2026
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.

Is leap year calculation omitted?

2 participants