hwclock: fix musl breakage of settimeofday(tz)

function                                             old     new   delta
set_kernel_timezone_and_clock                          -     119    +119
set_kernel_tz                                          -      28     +28
hwclock_main                                         480     301    -179
------------------------------------------------------------------------------
(add/remove: 2/0 grow/shrink: 0/1 up/down: 147/-179)          Total: -32 bytes

Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
diff --git a/util-linux/hwclock.c b/util-linux/hwclock.c
index 791525f..44cb479 100644
--- a/util-linux/hwclock.c
+++ b/util-linux/hwclock.c
@@ -36,6 +36,19 @@
 #include <sys/utsname.h>
 #include "rtc_.h"
 
+
+//musl has no __MUSL__ or similar define to check for,
+//but its <sys/types.h> has these lines:
+// #define __NEED_fsblkcnt_t
+// #define __NEED_fsfilcnt_t
+#if defined(__linux__) && defined(__NEED_fsblkcnt_t) && defined(__NEED_fsfilcnt_t)
+# define LIBC_IS_MUSL 1
+# include <sys/syscall.h>
+#else
+# define LIBC_IS_MUSL 0
+#endif
+
+
 /* diff code is disabled: it's not sys/hw clock diff, it's some useless
  * "time between hwclock was started and we saw CMOS tick" quantity.
  * It's useless since hwclock is started at a random moment,
@@ -116,26 +129,73 @@
 #endif
 }
 
+static void set_kernel_tz(const struct timezone *tz)
+{
+#if LIBC_IS_MUSL
+	/* musl libc does not pass tz argument to syscall
+	 * because "it's deprecated by POSIX, therefore it's fine
+	 * if we gratuitously break stuff" :(
+	 */
+#if !defined(SYS_settimeofday) && defined(SYS_settimeofday_time32)
+# define SYS_settimeofday SYS_settimeofday_time32
+#endif
+	int ret = syscall(SYS_settimeofday, NULL, tz);
+#else
+	int ret = settimeofday(NULL, tz);
+#endif
+	if (ret)
+		bb_simple_perror_msg_and_die("settimeofday");
+}
+
+/*
+ * At system boot, kernel may set system time from RTC,
+ * but it knows nothing about timezones. If RTC is in local time,
+ * then system time is wrong - it is offset by timezone.
+ * --systz option corrects system time if RTC is in local time,
+ * and (always) sets in-kernel timezone.
+ *
+ * This is an alternate option to --hctosys that does not read the
+ * hardware clock.
+ *
+ * util-linux's code has this comment:
+ *  RTC   | settimeofday calls
+ *  ------|-------------------------------------------------
+ *  Local | 1) warps system time*, sets PCIL* and kernel tz
+ *  UTC   | 1st) locks warp_clock 2nd) sets kernel tz
+ *               * only on first call after boot
+ * (PCIL is "persistent_clock_is_local" kernel internal flag,
+ * it makes kernel save RTC in local time, not UTC.)
+ */
+static void set_kernel_timezone_and_clock(int utc, const struct timeval *hctosys)
+{
+	time_t cur;
+	struct tm *broken;
+	struct timezone tz = { 0 };
+
+	/* if --utc, prevent kernel's warp_clock() with a dummy call */
+	if (utc)
+		set_kernel_tz(&tz);
+
+	/* Set kernel's timezone offset based on userspace one */
+	cur = time(NULL);
+	broken = localtime(&cur);
+	tz.tz_minuteswest = -broken->tm_gmtoff / 60;
+	/*tz.tz_dsttime = 0; already is */
+	set_kernel_tz(&tz); /* MIGHT warp_clock() if 1st call since boot */
+
+	if (hctosys) { /* it's --hctosys: set time too */
+		if (settimeofday(hctosys, NULL))
+			bb_simple_perror_msg_and_die("settimeofday");
+	}
+}
+
 static void to_sys_clock(const char **pp_rtcname, int utc)
 {
 	struct timeval tv;
-	struct timezone tz;
-
-	tz.tz_minuteswest = timezone / 60;
-	/* ^^^ used to also subtract 60*daylight, but it's wrong:
-	 * daylight!=0 means "this timezone has some DST
-	 * during the year", not "DST is in effect now".
-	 */
-	tz.tz_dsttime = 0;
-
-	/* glibc v2.31+ returns an error if both args are non-NULL */
-	if (settimeofday(NULL, &tz))
-		bb_simple_perror_msg_and_die("settimeofday");
 
 	tv.tv_sec = read_rtc(pp_rtcname, NULL, utc);
 	tv.tv_usec = 0;
-	if (settimeofday(&tv, NULL))
-		bb_simple_perror_msg_and_die("settimeofday");
+	return set_kernel_timezone_and_clock(utc, &tv);
 }
 
 static void from_sys_clock(const char **pp_rtcname, int utc)
@@ -261,39 +321,6 @@
 		close(rtc);
 }
 
-/*
- * At system boot, kernel may set system time from RTC,
- * but it knows nothing about timezones. If RTC is in local time,
- * then system time is wrong - it is offset by timezone.
- * This option corrects system time if RTC is in local time,
- * and (always) sets in-kernel timezone.
- *
- * This is an alternate option to --hctosys that does not read the
- * hardware clock.
- */
-static void set_system_clock_timezone(int utc)
-{
-	struct timeval tv;
-	struct tm *broken;
-	struct timezone tz;
-
-	gettimeofday(&tv, NULL);
-	broken = localtime(&tv.tv_sec);
-	tz.tz_minuteswest = timezone / 60;
-	if (broken->tm_isdst > 0)
-		tz.tz_minuteswest -= 60;
-	tz.tz_dsttime = 0;
-	gettimeofday(&tv, NULL);
-	if (!utc)
-		tv.tv_sec += tz.tz_minuteswest * 60;
-
-	/* glibc v2.31+ returns an error if both args are non-NULL */
-	if (settimeofday(NULL, &tz))
-		bb_simple_perror_msg_and_die("settimeofday");
-	if (settimeofday(&tv, NULL))
-		bb_simple_perror_msg_and_die("settimeofday");
-}
-
 //usage:#define hwclock_trivial_usage
 //usage:	IF_LONG_OPTS(
 //usage:       "[-swu] [--systz] [--localtime] [-f FILE]"
@@ -333,7 +360,6 @@
 	const char *rtcname = NULL;
 	unsigned opt;
 	int utc;
-
 #if ENABLE_LONG_OPTS
 	static const char hwclock_longopts[] ALIGN1 =
 		"localtime\0" No_argument "l" /* short opt is non-standard */
@@ -345,10 +371,6 @@
 		"rtc\0"       Required_argument "f"
 		;
 #endif
-
-	/* Initialize "timezone" (libc global variable) */
-	tzset();
-
 	opt = getopt32long(argv,
 		"^lurswtf:" "\0" "r--wst:w--rst:s--wrt:t--rsw:l--u:u--l",
 		hwclock_longopts,
@@ -366,7 +388,7 @@
 	else if (opt & HWCLOCK_OPT_SYSTOHC)
 		from_sys_clock(&rtcname, utc);
 	else if (opt & HWCLOCK_OPT_SYSTZ)
-		set_system_clock_timezone(utc);
+		set_kernel_timezone_and_clock(utc, NULL);
 	else
 		/* default HWCLOCK_OPT_SHOW */
 		show_clock(&rtcname, utc);