Skip to content

libast/tm: DST offset and contemporaneous time zone fixes.#1008

Merged
McDutchie merged 13 commits into
ksh93:devfrom
pghvlaans:dst_offset
Jul 25, 2026
Merged

libast/tm: DST offset and contemporaneous time zone fixes.#1008
McDutchie merged 13 commits into
ksh93:devfrom
pghvlaans:dst_offset

Conversation

@pghvlaans

@pghvlaans pghvlaans commented Jul 16, 2026

Copy link
Copy Markdown

RE #976, 98800e9

Pre-u+m code in tminit.c sets the standard (ST) and daylight savings time (DST) offsets by checking times over the previous year of the time requested to compare DST status and offsets from UTC. If no offset change is detected, the DST offset is set equal to the ST offset.

Although assigning the DST offset in this way is counter-intuitive, it does not result in incorrect times being reported so long as two implicit assumptions are met:

  • If the requested time is DST, a change from ST occurred in the previous year.
  • If the offset changed over the previous year, this was the result of a DST transition.

The first assumption fails on musl Linux systems (which would not appear until well after the code in question was written). musl interprets POSIX timezone specifications with a DST name but without rules as being DST year-round. tminit.c therefore calculates both the DST and ST offsets as equal to the overall UTC offset, which is applied twice when reporting times. Because musl apparently disregards the top zoneinfo directory, even common aliases such as PST8PDT return wildly inaccurate results (rather than the one-hour errors that /bin/date would suggest on musl systems).

The second assumption is broken at all times and places between a historical timezone change and a DST shift or one year later, whichever comes first. Although Pacific/Apia between 2011-12-31 and 2012-04-01 yields the most striking results, there are many other examples.

Fixing these problems involves relatively minor changes to tminit.c and related tests:

  • In tminit.c, save and restore the UTC offset and DST status of the requested time; only break offset calculations for a DST shift.
  • In attributes.sh, use full POSIX timezone specifications in lieu of EST5EDT and PST8PDT. This is necessary because musl applies DST for the entire year and the time of the test is ST.
  • In printf.sh, add several TZ tests to account for different combinations of DST and ST designations across historical timezone changes. Also add an EST5EDT test in DST.

In addition, although 98800e9 introduced a fix for looking up times in zones with historical changes, it applied only to searches using seconds from the epoch. Other lookup formats still fail to change to contemporaneous zones. A convenient reproducer is:

TZ=Europe/London printf "%(%s)T\n" "1970-01-01 01:00:00"

The output should be 0 and not 3600.

  • In tmxdate.c, after running through tmxdate for the first time, compare the values of west, dst and isdst to ensure that a proper zone has been chosen. If the zones differ, this means that a historical change has occurred. Apply an offset and run again, repeating as necessary.
  • In printf.sh, add 'reverse' tests looking up epoch seconds by date.

@pghvlaans

Copy link
Copy Markdown
Author

Tested on Slackware64 -current (glibc), Void (musl), FreeBSD, OpenBSD, omnios (attributes and printf only) and NetBSD. I don't currently have a macOS VM.

@pghvlaans pghvlaans changed the title libast/tm/tminit.c: Fix DST offsets libast/tm/tminit.c: Time fixes. Jul 18, 2026
@pghvlaans pghvlaans changed the title libast/tm/tminit.c: Time fixes. libast/tm: DST offset and contemporaneous time zone fixes. Jul 18, 2026
@pghvlaans

Copy link
Copy Markdown
Author

8b003db broadens the scope a bit. I can revert it and make a different PR after this one is merged if that would be more convenient.

@McDutchie

Copy link
Copy Markdown

Many thanks for this solidly executed PR.

I don't think there's a real need to split the PR; your commit message does a nice job of explaining every distinct change. If it becomes necessary, we can always go back to the commit history of this PR to pick it apart further.

Unfortunately I don't have the time and inclination to become expert enough at the tm(3) code myself to give this a proper review, but your explanations do make sense to me. I've tested the PR and everything seems to work as described. There is good regression test coverage.

So, in it goes.

One thing I will edit first, though, is some of those one-letter variable names, to change them to more descriptive names. I'm really not a fan of that aspect of Glenn Fowler's coding style; it badly reduces the legibility of the code.

Comment thread src/lib/libast/tm/tmxdate.c Outdated
pghvlaans and others added 3 commits July 25, 2026 09:38
Apart from routine housekeeping:

src/lib/libast/tm/tminit.c:
- Rename one-letter variables for legibility.

src/lib/libast/tm/tmxdate.c:
- Move the check for historical time zone changes to the end of
  tmxdate(). This allows eliminating the 'first' and 'good_check'
  variables.
@McDutchie

Copy link
Copy Markdown

Changed a couple of things:

  • tminit.c: Rename one-letter variables for legibility.
  • tmxdate.c: Move the check for historical time zone changes to the end of tmxdate. This allows eliminating the first and good_check variables.

Does that look OK to you?

@pghvlaans

Copy link
Copy Markdown
Author

Yes, very sensible.

@pghvlaans

Copy link
Copy Markdown
Author

Actually, one very small point about the NEWS entry. The contemporaneous timezone issue wasn't musl-only.

@McDutchie

Copy link
Copy Markdown

Fixed, thanks.

@McDutchie
McDutchie merged commit 740d0c8 into ksh93:dev Jul 25, 2026
McDutchie added a commit that referenced this pull request Jul 25, 2026
Pre-u+m code in tminit.c sets the standard (ST) and daylight
savings time (DST) offsets by checking times over the previous year
of the time requested to compare DST status and offsets from UTC.
If no offset change is detected, the DST offset is set equal to the
ST offset.

Although assigning the DST offset in this way is counter-intuitive,
it does not result in incorrect times being reported so long as two
implicit assumptions are met:

  *  If the requested time is DST, a change from ST occurred in the
     previous year.
  *  If the offset changed over the previous year, this was the
     result of a DST transition.

The first assumption fails on Linux systems with musl libc (which
would not appear until well after the code in question was
written). musl interprets POSIX timezone specifications with a DST
name but without rules as being DST year-round. tminit.c therefore
calculates both the DST and ST offsets as equal to the overall UTC
offset, which is applied twice when reporting times. Because musl
apparently disregards the top zoneinfo directory, even common
aliases such as PST8PDT return wildly inaccurate results (rather
than the one-hour errors that /bin/date would suggest on musl
systems).

The second assumption is broken at all times and places between a
historical timezone change and a DST shift or one year later,
whichever comes first. Although Pacific/Apia between 2011-12-31 and
2012-04-01 yields the most striking results, there are many other
examples.

Fixing these problems involves relatively minor changes to tminit.c
and related tests:

src/lib/libast/tm/tminit.c:
- tmlocal(): Save and restore the UTC offset and DST status of the
  requested time; only break offset calculations for a DST shift.
- Rename the related one-letter variables for legibility.

src/cmd/ksh93/tests/attributes.sh:
- Use full POSIX timezone specifications in lieu of 'EST5EDT' and
  'PST8PDT'. This is necessary because musl applies DST for the
  entire year and the time of the test is ST.

src/cmd/ksh93/tests/printf.sh:
- Add several TZ tests to account for different combinations of DST
  and ST designations across historical timezone changes. Also add
  an EST5EDT test in DST.

In addition, although commit 580bc2d introduced a fix for looking
up times in zones with historical changes, it applied only to
searches using seconds from the epoch. Other lookup formats still
fail to change to contemporaneous zones. A convenient reproducer
is:

    TZ=Europe/London printf "%(%s)T\n" "1970-01-01 01:00:00"

The output should be '0' and not '3600'.

src/lib/libast/tm/tmxdate.c:
- After running through tmxdate() for the first time, compare the
  values of 'west', 'dst' and 'isdst' to ensure that a proper zone
  has been chosen. If the zones differ, this means that a
  historical change has occurred. Apply an offset and run again,
  repeating as necessary.

src/cmd/ksh93/tests/printf.sh:
- Add 'reverse' tests looking up epoch seconds by date.

Co-authored-by: Martijn Dekker <martijn@inlv.org>
Resolves: #976
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.

2 participants