Subject: bug#11281: DST has not effect on windows XP when system DST adjustment is disabled

From: Eli Zaretskii <[email-masked]>
Date: Mon, 26 May 2025 15:10:53 +0300

> Cc: [email-masked], [email-masked]
> Date: Sun, 25 May 2025 18:51:06 +0300
> From: Eli Zaretskii <[email-masked]>
> 
> > Date: Mon, 26 May 2025 00:08:23 +0900
> > From: Kazuhiro Ito <[email-masked]>
> > Cc: [email-masked], [email-masked]
> > 
> > > > > JST-9
> > > > > _daylight = 0
> > > > > _timezone = -32400
> > > > > _tzname[0] = JST
> > > > > _dstbias = -3600
> > > 
> > > Isn't this result strange?  If a timezone has no DST, why is _dstbias
> > > not zero?
> > 
> > I feel it is strange, too.  My understanding is that tzset does not
> > touch _dstbias when timezone is specified by TZ environmental variable
> > and it is the bug.  However when _daylight is zero, _dstbias value is
> > not used and the issue doesn't become prominent.
> 
> If tzset doesn't touch _dstbias, the results begin to make sense,
> because it means _dstbias is left at its value determine by previous
> calls, and does not necessarily correspond to the timezone set by the
> call to tzset.  But if that is the case, we should correct this bug by
> examining the value of TZ and assigning to _dstbias the value of
> either -3600 or zero, depending on whether the value of TZ does or
> doesn't specify the "DST" part.  IOW, blindly "correcting" the zero
> value of _dstbias is not right; we should instead go by the DST
> definition of the value in TZ.

I think I see the problem: when TZ is defined in the environment,
tzset updates _daylight, but doesn't touch _dstbias.  So if you start
from a timezone without DST and then switch to a timezone with DST,
_dstbias stays zero, and local time is computed incorrectly.

Therefore, I suggest the patches below.  Could you please try them?

> See above: I think I changed my mind, and we should set _dstbias
> according to the actual value of TZ, and only if TZ is indeed set

The patches below implement this, by using _daylight as the evidence
that TZ defines DST.

diff --git a/src/timefns.c b/src/timefns.c
index 4d296ff..8cf424b 100644
--- a/src/timefns.c
+++ b/src/timefns.c
@@ -189,6 +189,7 @@ emacs_localtime_rz (timezone_t tz, time_t const *t, struct tm *tm)
      display-time) are in real danger of missing timezone and DST
      changes.  Calling tzset before each localtime call fixes that.  */
   tzset ();
+  w32_fix_tzset ();
 #endif
   tm = localtime_rz (tz, t, tm);
   if (!tm && errno == ENOMEM)
@@ -306,6 +307,9 @@ tzlookup (Lisp_Object zone, bool settz)
       block_input ();
       emacs_setenv_TZ (zone_string);
       tzset ();
+#ifdef WINDOWSNT
+      w32_fix_tzset ();
+#endif
       timezone_t old_tz = local_tz;
       local_tz = new_tz;
       tzfree (old_tz);
diff --git a/src/w32.c b/src/w32.c
index 5de721a..a98a008 100644
--- a/src/w32.c
+++ b/src/w32.c
@@ -10289,6 +10289,30 @@ w32_read_registry (HKEY rootkey, Lisp_Object lkey, Lisp_Object lname)
 }
 
 
+/* mingw.org's MinGW doesn't declare _dstbias.  MinGW64 defines it as a
+   macro.  */
+#ifndef _dstbias
+__MINGW_IMPORT int _dstbias;
+#endif
+
+/* Fix a bug in MS implementation of 'tzset', to be called immediately
+   after 'tzset'.  */
+void
+w32_fix_tzset (void)
+{
+  char *tz_env = getenv ("TZ");
+
+  /* When TZ is defined in the environment, '_tzset' updates _daylight,
+     but not _dstbias.  Then if we are switching from a timezone without
+     DST to a timezone with DST, 'localtime' and friends will apply zero
+     DST bias, which is incorrect. (When TZ is not defined, '_tzset'
+     does update _dstbias using values obtained from Windows API
+     GetTimeZoneInformation.)  Here we fix that blunder by detecting
+     this situation and forcing _dstbias to be 1 hour.  */
+  if (tz_env && _daylight && !_dstbias)
+    _dstbias = -3600;
+}
+
 /* The Windows CRT functions are "optimized for speed", so they don't
    check for timezone and DST changes if they were last called less
    than 1 minute ago (see http://support.microsoft.com/kb/821231).  So
@@ -10299,6 +10323,7 @@ w32_read_registry (HKEY rootkey, Lisp_Object lkey, Lisp_Object lname)
 sys_localtime (const time_t *t)
 {
   tzset ();
+  w32_fix_tzset ();
   return localtime (t);
 }
 
diff --git a/src/w32.h b/src/w32.h
index ae3999f..9d9887e 100644
--- a/src/w32.h
+++ b/src/w32.h
@@ -234,6 +234,7 @@ #define FILE_DONT_CLOSE         0x1000
 extern int fchmodat (int, char const *, mode_t, int);
 extern int lchmod (char const *, mode_t);
 extern bool symlinks_supported (const char *);
+extern void w32_fix_tzset (void);

 /* Return total and free memory info.  */