* [PATCH] time: Prevent time64_to_tm() day truncation on 32-bit
@ 2026-08-29 16:12 Karl Mehltretter
2026-09-01 9:14 ` Thomas Weißschuh
0 siblings, 1 reply; 3+ messages in thread
From: Karl Mehltretter @ 2026-08-29 16:12 UTC (permalink / raw)
To: John Stultz, Thomas Gleixner
Cc: Karl Mehltretter, Stephen Boyd, Miroslav Lichvar, Deepa Dinamani,
Cassio Neri, linux-kernel
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()")
Assisted-by: LLM
Signed-off-by: Karl Mehltretter <kmehltretter@gmail.com>
---
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.
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;
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
^ permalink raw reply [flat|nested] 3+ messages in thread* Re: [PATCH] time: Prevent time64_to_tm() day truncation on 32-bit
2026-08-29 16:12 [PATCH] time: Prevent time64_to_tm() day truncation on 32-bit Karl Mehltretter
@ 2026-09-01 9:14 ` Thomas Weißschuh
2026-09-01 12:25 ` David Laight
0 siblings, 1 reply; 3+ messages in thread
From: Thomas Weißschuh @ 2026-09-01 9:14 UTC (permalink / raw)
To: Karl Mehltretter
Cc: John Stultz, Thomas Gleixner, Stephen Boyd, Miroslav Lichvar,
Deepa Dinamani, Cassio Neri, linux-kernel
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@gmail.com>
Reviewed-by: Thomas Weißschuh <thomas.weissschuh@linutronix.de>
> ---
> 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
^ permalink raw reply [flat|nested] 3+ messages in thread* Re: [PATCH] time: Prevent time64_to_tm() day truncation on 32-bit
2026-09-01 9:14 ` Thomas Weißschuh
@ 2026-09-01 12:25 ` David Laight
0 siblings, 0 replies; 3+ messages in thread
From: David Laight @ 2026-09-01 12:25 UTC (permalink / raw)
To: Thomas Weißschuh
Cc: Karl Mehltretter, John Stultz, Thomas Gleixner, Stephen Boyd,
Miroslav Lichvar, Deepa Dinamani, Cassio Neri, linux-kernel
On Tue, 1 Sep 2026 11:14:45 +0200
Thomas Weißschuh <thomas.weissschuh@linutronix.de> 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@gmail.com>
>
> Reviewed-by: Thomas Weißschuh <thomas.weissschuh@linutronix.de>
>
> > ---
> > 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
>
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-09-01 12:25 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-08-29 16:12 [PATCH] time: Prevent time64_to_tm() day truncation on 32-bit Karl Mehltretter
2026-09-01 9:14 ` Thomas Weißschuh
2026-09-01 12:25 ` David Laight
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®