Re: [PATCH] time: Prevent time64_to_tm() day truncation on 32-bit
From: David Laight
Date: Tue Sep 01 2026 - 08:34:18 EST
On Tue, 1 Sep 2026 11:14:45 +0200
Thomas Weißschuh <thomas.weissschuh@xxxxxxxxxxxxx> wrote:
> On Sat, Aug 29, 2026 at 06:12:12PM +0200, Karl Mehltretter wrote:
> > time64_to_tm() accepts a 64-bit seconds value, but stores the quotient in
> > long. On a 32-bit kernel the day count therefore wraps when it reaches
> > 2^31, even though the corresponding year remains representable in
> > struct tm.
> >
> > This is reachable when formatting externally supplied timestamps. For
> > example, NILFS recovery copies the little-endian 64-bit ss_create field
> > from an on-disk segment summary into time64_t. Its sysfs attributes then
> > print that value with %ptTs, whose formatter calls time64_to_tm(). A
> > corrupted image can consequently produce an architecture-dependent printed
> > date.
> >
> > Keep the day quotient in s64 while leaving the seconds-within-day remainder
> > as long. Use div_s64_rem() for the weekday calculation so 32-bit builds do
> > not require compiler runtime division helpers. Add a KUnit case at the
> > first positive day count outside signed 32-bit range. The baseline i386
> > kernel returns a negative year and wrong calendar fields; the fixed kernel
> > returns the same result as x86_64.
> >
> > Fixes: e6c2682a1da3 ("time: Add time64_to_tm()")
>
> If this is supposed to be a fix which might get backported, the kunit
> test changes should probably be in a dedicated commit.
>
> > Assisted-by: LLM
> > Signed-off-by: Karl Mehltretter <kmehltretter@xxxxxxxxx>
>
> Reviewed-by: Thomas Weißschuh <thomas.weissschuh@xxxxxxxxxxxxx>
>
> > ---
> > Review notes:
> >
> > - On a test-only baseline, QEMU/i386 passed the existing case but failed
> > the wide-day case with tm_year=-5879541 and four other wrong fields.
> > Baseline x86_64 and fixed i386, x86_64 and lockdep x86_64 passed 2/2.
> > - A direct s64 modulo emitted __moddi3 on i386 and __aeabi_ldivmod on ARM
> > with Clang 21. The submitted form uses div_s64_rem(); W=1 builds of
> > both objects contain neither compiler-runtime reference.
> > - The narrow type came from time_to_tm() in the Fixes commit. The 2021
> > arithmetic rewrite kept it and tested only +/-80,000 years, below the
> > approximately 5.88-million-year 32-bit day boundary.
>
> We still have an overflow when year exceeds LONG_MAX on 32-bit.
> Not sure how relevant it is to handle years over 2 billion, however it
> is likely as relevant as years over 5.88 million.
By then the earth's rotation and orbit will have slowed enough that the
Gregorian correction that 3/4 centuries aren't leap years won't be enough
and the entire scheme will have to have changed.
The observant will also have realised that since December is 'month 10'
January is month 11 and February month 12.
So the year originally started in March, not even the 1st but on 'Lady day'
with is (IIRC) the 25th.
The UK tax year ended on Lady day until we switched from the Julian to
Gregorian calendar (to match most of Europe except Russia). The change 'lost'
10 days, but having a short tax year would cause too much grief - so the end
of the tax year was moved 10 days to April 5th keeping the year the same length.
It has been there ever since.
David
>
> >
> > kernel/time/time_test.c | 16 ++++++++++++++++
> > kernel/time/timeconv.c | 6 ++++--
> > 2 files changed, 20 insertions(+), 2 deletions(-)
> >
> > diff --git a/kernel/time/time_test.c b/kernel/time/time_test.c
> > index 1b99180da2881..8b718767b3baf 100644
> > --- a/kernel/time/time_test.c
> > +++ b/kernel/time/time_test.c
> > @@ -87,8 +87,24 @@ static void time64_to_tm_test_date_range(struct kunit *test)
> > }
> > }
> >
> > +static void time64_to_tm_test_wide_day_count(struct kunit *test)
> > +{
> > + /* 2^31 days: the first count that does not fit in a 32-bit long. */
> > + time64_t timestamp = (1LL << 31) * 86400;
> > + struct tm result;
> > +
> > + time64_to_tm(timestamp, 0, &result);
> > +
> > + KUNIT_EXPECT_EQ(test, result.tm_year, 5879680);
> > + KUNIT_EXPECT_EQ(test, result.tm_mon, 6);
> > + KUNIT_EXPECT_EQ(test, result.tm_mday, 12);
> > + KUNIT_EXPECT_EQ(test, result.tm_yday, 193);
> > + KUNIT_EXPECT_EQ(test, result.tm_wday, 6);
> > +}
> > +
> > static struct kunit_case time_test_cases[] = {
> > KUNIT_CASE_SLOW(time64_to_tm_test_date_range),
> > + KUNIT_CASE(time64_to_tm_test_wide_day_count),
> > {}
> > };
> >
> > diff --git a/kernel/time/timeconv.c b/kernel/time/timeconv.c
> > index 59b922c826e77..292a855827b7a 100644
> > --- a/kernel/time/timeconv.c
> > +++ b/kernel/time/timeconv.c
> > @@ -49,7 +49,8 @@ void time64_to_tm(time64_t totalsecs, int offset, struct tm *result)
> > u32 u32tmp, day_of_century, year_of_century, day_of_year, month, day;
> > u64 u64tmp, udays, century, year;
> > bool is_Jan_or_Feb, is_leap_year;
> > - long days, rem;
> > + s64 days;
> > + long rem;
>
> This now breaks reverse christmas-tree order.
>
> Tiniest of nitpicks: The reverse christmas tree looks better with the
> 's64' line below the 'long' one.
>
> > int remainder;
> >
> > days = div_s64_rem(totalsecs, SECS_PER_DAY, &remainder);
> > @@ -70,7 +71,8 @@ void time64_to_tm(time64_t totalsecs, int offset, struct tm *result)
> > result->tm_sec = rem % 60;
> >
> > /* January 1, 1970 was a Thursday. */
> > - result->tm_wday = (4 + days) % 7;
> > + div_s64_rem(days + 4, 7, &remainder);
> > + result->tm_wday = remainder;
> > if (result->tm_wday < 0)
> > result->tm_wday += 7;
> >
> >
> > base-commit: cf72cbb39da84b6f02f90c07f33b102fc10b16f0
> > --
> > 2.53.0
>