Re: [PATCH] time: Prevent time64_to_tm() day truncation on 32-bit

From: Thomas Weißschuh

Date: Tue Sep 01 2026 - 05:26:02 EST


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.

>
> 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