Follow @Openwall on Twitter for new release announcements and other news
[<prev] [next>] [<thread-prev] [day] [month] [year] [list]
Message-ID: <20260805141033.841558-11-matthias.goergens@gmail.com>
Date: Wed,  5 Aug 2026 22:10:33 +0800
From: Matthias Goergens <matthias.goergens@...il.com>
To: musl@...ts.openwall.com
Cc: Matthias Goergens <matthias.goergens@...il.com>
Subject: [PATCH 10/10] regression: test TZif version and footer parsing

The TZif v1 version byte is NUL, so conforming v1 files must
parse, and a POSIX footer must only be accepted exactly at the end
of the selected data block, never by scanning into binary
transition data or before the mapping. Each case runs in a child
so a crashing implementation fails the case instead of the test.

Covered by the musl patch "time: fix TZif version and footer
parsing".
---
 src/regression/tzif-version-footer.c | 140 +++++++++++++++++++++++++++
 1 file changed, 140 insertions(+)
 create mode 100644 src/regression/tzif-version-footer.c

diff --git a/src/regression/tzif-version-footer.c b/src/regression/tzif-version-footer.c
new file mode 100644
index 0000000..65f28f9
--- /dev/null
+++ b/src/regression/tzif-version-footer.c
@@ -0,0 +1,140 @@
+// RFC 9636 encodes the TZif v1 version byte as NUL, but musl treated
+// only the literal byte '1' as v1, reading every conforming v1 file as
+// though a second header followed its data. Footer detection also
+// scanned backwards from any terminal newline without a lower bound,
+// so it could run before the mapping or mistake a newline in binary
+// transition data for the footer delimiter. Valid v1 files must parse,
+// and a footer must only be accepted exactly at the end of the
+// selected data block.
+// Each case runs in a child so a crashing implementation fails the
+// case instead of aborting the whole test.
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+#include <sys/wait.h>
+#include "test.h"
+
+static void put_header(unsigned char *p, char version)
+{
+	memcpy(p, "TZif", 4);
+	p[4] = version;
+	p[35] = 1; /* one transition */
+	p[39] = 1; /* one ttinfo record */
+	p[43] = 4; /* four abbreviation bytes */
+}
+
+static size_t put_block(unsigned char *p, int wide, int binary_newline)
+{
+	size_t n = wide ? 8 : 4;
+
+	/* a transition at 10 deliberately contains a binary newline byte */
+	p[n-1] = binary_newline ? 10 : 11;
+	p[n] = 0;
+	memcpy(p+n+7, "UTC", 4);
+	return n + 1 + 6 + 4;
+}
+
+static int write_tzif(const char *path, int mode)
+{
+	unsigned char data[160] = {0};
+	size_t size;
+	FILE *f;
+
+	if (mode < 2) {
+		/* minimal TZif v1: NUL version byte, no transitions */
+		memcpy(data, "TZif", 4);
+		data[39] = 1; /* one ttinfo record */
+		data[43] = 4; /* four abbreviation bytes */
+		memcpy(data+50, "UTC", 4);
+		size = 54;
+	} else {
+		put_header(data, '2');
+		size = 44 + put_block(data+44, 0, mode != 4);
+		put_header(data+size, '2');
+		size += 44 + put_block(data+size+44, 1, mode != 4);
+	}
+	if (mode == 1 || mode == 3 || mode == 4)
+		data[size++] = '\n'; /* terminal newline without a footer */
+	else if (mode == 2) {
+		memcpy(data + size, "\nXST-2\n", 7);
+		size += 7;
+	}
+	f = fopen(path, "wb");
+	if (!f)
+		return -1;
+	if (fwrite(data, 1, size, f) != size) {
+		fclose(f);
+		return -1;
+	}
+	return fclose(f);
+}
+
+static const char *const mode_name[] = {
+	"valid footerless TZif v1",
+	"TZif v1 with invalid terminal newline",
+	"genuine TZif v2 with POSIX footer",
+	"malformed TZif v2 with newline in binary data",
+	"malformed TZif v2 with terminal newline only",
+};
+
+static int run_mode(int mode)
+{
+	char path[] = "/tmp/libc-test-tzif-XXXXXX";
+	time_t t = 0;
+	struct tm out;
+	const char *want_zone = mode == 2 ? "XST" : "UTC";
+	long want_offset = mode == 2 ? 7200 : 0;
+	int fd = mkstemp(path);
+
+	if (fd < 0 || close(fd) || write_tzif(path, mode)) {
+		fprintf(stderr, "mode %d: setup failed\n", mode);
+		return 65;
+	}
+	if (setenv("TZ", path, 1))
+		return 66;
+	tzset();
+	unlink(path);
+	if (!localtime_r(&t, &out))
+		return 67;
+	if (!out.tm_zone || strcmp(out.tm_zone, want_zone) ||
+	    !tzname[0] || strcmp(tzname[0], want_zone) ||
+	    out.tm_gmtoff != want_offset || out.tm_isdst) {
+		fprintf(stderr, "mode %d (%s): zone=%s tzname=%s gmtoff=%ld isdst=%d, want %s/%ld/0\n",
+			mode, mode_name[mode],
+			out.tm_zone ? out.tm_zone : "(null)",
+			tzname[0] ? tzname[0] : "(null)",
+			out.tm_gmtoff, out.tm_isdst, want_zone, want_offset);
+		return 68;
+	}
+	return 0;
+}
+
+int main(void)
+{
+	int mode;
+
+	for (mode = 0; mode < 5; mode++) {
+		pid_t pid = fork();
+		int status;
+
+		if (pid == 0)
+			_exit(run_mode(mode));
+		if (pid < 0 || waitpid(pid, &status, 0) != pid) {
+			t_error("fork/waitpid failed for mode %d\n", mode);
+			continue;
+		}
+		if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
+			if (WIFSIGNALED(status))
+				t_error("%s: killed by signal %d\n",
+					mode_name[mode], WTERMSIG(status));
+			else
+				t_error("%s: failed with status %d\n",
+					mode_name[mode],
+					WIFEXITED(status) ? WEXITSTATUS(status) : status);
+		}
+	}
+	return t_status;
+}
-- 
2.55.0

Powered by blists - more mailing lists

Confused about mailing lists and their use? Read about mailing lists on Wikipedia and check out these guidelines on proper formatting of your messages.