Bug 269207 - localtime.c fails to detect mobile device timezone change
Summary: localtime.c fails to detect mobile device timezone change
Status: Closed FIXED
Alias: None
Product: Base System
Classification: Unclassified
Component: bin (show other bugs)
Version: CURRENT
Hardware: Any Any
: --- Affects Many People
Assignee: Dag-Erling Smørgrav
URL: https://reviews.freebsd.org/D53502
Keywords:
Depends on:
Blocks:
 
Reported: 2023-01-28 17:38 UTC by J.R. Oldroyd
Modified: 2026-04-02 12:26 UTC (History)
4 users (show)

See Also:
des: mfc-stable15+
des: mfc-stable14+


Attachments
patch to modify localtime() to check for /etc/localtime file changes (1.56 KB, patch)
2023-01-28 17:39 UTC, J.R. Oldroyd
no flags Details | Diff
simple test program to call localtime() repeatedly (321 bytes, text/plain)
2023-01-28 17:40 UTC, J.R. Oldroyd
no flags Details
Makefile to build test program and patched localtime.c in test directory (369 bytes, text/plain)
2023-01-28 17:41 UTC, J.R. Oldroyd
no flags Details
patch to modify localtime() to check for /etc/localtime file changes (1.51 KB, patch)
2025-02-27 02:47 UTC, Mark Linimon
no flags Details | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description J.R. Oldroyd 2023-01-28 17:38:24 UTC
Currently, localtime(3) determines the local timezone using either the TZ environment variable or the /etc/localtime file.  Once it has obtained the zone information, a flag is set, the zone information is cached and the information is re-used on subsequent calls to localtime() by the same program.

In situations where long-running programs persist over mobile device moves that span timezones, this behavior means that such programs will remain unaware of any system timezone changes, resulting in events occurring at times not expected by the user.

A real-world example is a laptop that executes cron(8) jobs at local times.  The user suspends the laptop, travels to a new timezone, resumes the laptop and updates the system timezone information using tzsetup(8) or other means.  Currently, cron(8) continues to execute jobs at times of the original timezone.  Desired behavior is that cron(8) starts to use the new timezone.  Similarly for other long-running programs.

Of course, long-running programs can be restarted after the timezone move.  However, this may be undesirable, and restarting system programs such as cron(8) is not something that non-technical users may be aware of the need for or even have sufficient privileges to do.

In localtime.c, the flag (the variable "lcl_is_set") is used both by tzset_basic() and tzsetwall_basic().

In tzset_basic(), the flag is set to the length of the zoneinfo provided in the TZ environment variable which, of course, will not change for the life of a long-running program.

In tzsetwall_basic(), the flag is set to -1 when the zone information is read from /etc/localtime.  As described above, this file could be changed during the life of a long-running program.  The use of the lcl_is_set flag is therefore not suitable in this case.

The attached patch removes the use of lcl_is_set from tzsetwall_basic() and replaces it with examination of the zone file's mtime in tzload() instead.  Once the zone information has been loaded from the file, the file's name and mtime are stored.  On subsequent calls, if these are unchanged, the cached zone information is returned.  Otherwise the file is re-read to obtain the new zone information.

When the TZ environment variable is unset, the existing code has the following calling sequence:
    localtime() => tzset_basic() => tzsetwall_basic() => tzload() => tzparse() => tzload()
The first call to tzload() is to load the /etc/localtime zone file.  The second call to tzload() is to load the TZDEFRULES ("posixrules") zone file.  The proposed patch is therefore aware of this and ignores that file when saving the zone file's name and mtime.

The patch also reorders tzload()'s _open() and _fstat() sequence to be a stat() followed by _open() in order to avoid unnecessarily opening an unchanged file and therefore triggering an unnecessary update of the inode's atime.

Long-running programs started with a timezone specification in the TZ environment variable are unaffected by this patch.  Even if /etc/localtime is modified for a new timezone, such programs will continue to use the timezone specification in their TZ variable.  This patch only affects programs for which the TZ environment variable is not set and so /etc/localtime is used, or programs calling tzsetwall(3) directly.

Also attached are a simple test program that calls localtime() repeatedly every 5 seconds and a Makefile to compile it and a patched localtime.c in a test directory. Run the program and then use tzsetup() or otherwise modify /etc/localtime to a different timezone while the program is running.
Comment 1 J.R. Oldroyd 2023-01-28 17:39:47 UTC
Created attachment 239768 [details]
patch to modify localtime() to check for /etc/localtime file changes
Comment 2 J.R. Oldroyd 2023-01-28 17:40:42 UTC
Created attachment 239769 [details]
simple test program to call localtime() repeatedly
Comment 3 J.R. Oldroyd 2023-01-28 17:41:18 UTC
Created attachment 239770 [details]
Makefile to build test program and patched localtime.c in test directory
Comment 4 Mark Linimon freebsd_committer freebsd_triage 2025-02-27 02:47:13 UTC
Created attachment 258016 [details]
patch to modify localtime() to check for /etc/localtime file changes

^Triage: rebase patch.
Comment 5 J.R. Oldroyd 2025-02-28 22:44:02 UTC
This appears to be obsolete now.

Recent updates to the localtime() code introduce DETECT_TZ_CHANGES which adds code to do the same as I'd proposed here two years ago.

The new functionality can be enabled by adding WITH_DETECT_TZ_CHANGES=1 to /etc/src.conf and then rebuilding libc(localtime.o).  I have tested this and cron(8) now does detect the timezone change.

Only comment is that it might be useful to make DETECT_TZ_CHANGES be enabled by default and have a WITHOUT_DETECT_TZ_CHANGES setting in src.conf to turn it off if needed.
Comment 6 J.R. Oldroyd 2025-03-05 21:15:14 UTC
The existing code added when DETECT_TZ_CHANGES is defined does not work completely as expected.

It does detect changes to /etc/localtime.

However, on occasion, it does not return the correct timezone, defaulting to "UTC" or timezone "   " (which comes from the WILDABBR code) and causing programs to receive a UTC result instead of a localtime result.

It seems to fail if called less than 61 seconds after system boot (this is because the change detection code uses CLOCK_MONOTONIC so it will not trigger until at least DETECT_TZ_CHANGES_INTERVAL=61 seconds after boot).  This causes programs started at boot to start in UTC timezone even though /etc/localtime is present and set to another timezone.  An easy fix for this is to return 1 when last_checked == 0.

It seems to also fail at other times too with an app switching back to UTC after already having learned the correct timezone.  I've not had time to debug this yet, but suspect it could have to do with calling tzsetwall() or other function that causes the timezone name to be reset.
Comment 7 Paul Eggert 2025-10-26 01:04:28 UTC
(In reply to J.R. Oldroyd from comment #6)
> It seems to fail if called less than 61 seconds after system boot
I independently noticed this bug, and I think it's fixed in my proposed patches in bug #290520.

> It seems to also fail at other times too with an app switching back to UTC
Yeah, that part of the code was pretty dicey. Although I think the same patches fixed it, it'd be nice to have a reproducer to confirm this.
Comment 8 Dag-Erling Smørgrav freebsd_committer freebsd_triage 2025-10-31 16:53:08 UTC
We've had (optional) time zone change detection since 2021.

The early-boot issue is easily fixed by initializing last_checked to TIME_T_MIN.

I'm not sure what you mean by “switching back to UTC”, though.  Can you give an example?
Comment 9 commit-hook freebsd_committer freebsd_triage 2025-11-02 13:54:13 UTC
A commit in branch main references this bug:

URL: https://cgit.FreeBSD.org/src/commit/?id=a38c2f99f81c2fc35c8ca209931c1c46e3e81023

commit a38c2f99f81c2fc35c8ca209931c1c46e3e81023
Author:     Dag-Erling Smørgrav <des@FreeBSD.org>
AuthorDate: 2025-11-02 13:51:42 +0000
Commit:     Dag-Erling Smørgrav <des@FreeBSD.org>
CommitDate: 2025-11-02 13:51:42 +0000

    tzcode: Fix early tz change detection

    Assume tzdata is not fresh if last_checked is zero, as comparing the
    current time to last_checked less than __tz_change_interval after boot
    may produce a false negative.

    While here, invert the return value from tzdata_is_fresh() to better
    match its new name (it was previously called recheck_tzdata(), so zero
    for fresh and non-zero for stale made sense, but it doesn't now).

    PR:             269207
    MFC after:      3 days
    Reviewed by:    imp
    Differential Revision:  https://reviews.freebsd.org/D53502

 contrib/tzcode/localtime.c | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)
Comment 10 commit-hook freebsd_committer freebsd_triage 2025-11-05 15:29:45 UTC
A commit in branch stable/15 references this bug:

URL: https://cgit.FreeBSD.org/src/commit/?id=f809d3f3ee1cf2d5b2c24283b42fa11871b8b6b8

commit f809d3f3ee1cf2d5b2c24283b42fa11871b8b6b8
Author:     Dag-Erling Smørgrav <des@FreeBSD.org>
AuthorDate: 2025-11-02 13:51:42 +0000
Commit:     Dag-Erling Smørgrav <des@FreeBSD.org>
CommitDate: 2025-11-05 13:54:58 +0000

    tzcode: Fix early tz change detection

    Assume tzdata is not fresh if last_checked is zero, as comparing the
    current time to last_checked less than __tz_change_interval after boot
    may produce a false negative.

    While here, invert the return value from tzdata_is_fresh() to better
    match its new name (it was previously called recheck_tzdata(), so zero
    for fresh and non-zero for stale made sense, but it doesn't now).

    PR:             269207
    MFC after:      3 days
    Reviewed by:    imp
    Differential Revision:  https://reviews.freebsd.org/D53502

    (cherry picked from commit a38c2f99f81c2fc35c8ca209931c1c46e3e81023)

 contrib/tzcode/localtime.c | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)
Comment 11 commit-hook freebsd_committer freebsd_triage 2026-04-02 11:21:23 UTC
A commit in branch stable/14 references this bug:

URL: https://cgit.FreeBSD.org/src/commit/?id=ea27af6d3db0bea7db6b344f4ef2848ba09b5091

commit ea27af6d3db0bea7db6b344f4ef2848ba09b5091
Author:     Dag-Erling Smørgrav <des@FreeBSD.org>
AuthorDate: 2025-11-02 13:51:42 +0000
Commit:     Dag-Erling Smørgrav <des@FreeBSD.org>
CommitDate: 2026-04-02 11:16:01 +0000

    tzcode: Fix early tz change detection

    Assume tzdata is not fresh if last_checked is zero, as comparing the
    current time to last_checked less than __tz_change_interval after boot
    may produce a false negative.

    While here, invert the return value from tzdata_is_fresh() to better
    match its new name (it was previously called recheck_tzdata(), so zero
    for fresh and non-zero for stale made sense, but it doesn't now).

    PR:             269207
    MFC after:      3 days
    Reviewed by:    imp
    Differential Revision:  https://reviews.freebsd.org/D53502

    (cherry picked from commit a38c2f99f81c2fc35c8ca209931c1c46e3e81023)

 contrib/tzcode/localtime.c | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)