Commit 8bee879fc2

Pat Tullmann <pat.github@tullmann.org>
2024-02-19 21:51:04
test/link/glibc_compat: Add C test case for glibc versions
glibc_runtime_check.c is a simple test case that exercises glibc functions that might smoke out linking problems with Zig's C compiler. The build.zig compiles it against a variety of glibc versions. Also document and test glibc v2.2.5 (from 2002) as the oldest working glibc target for C binaries.
1 parent a4e0107
Changed files (3)
lib
libc
glibc
test
lib/libc/glibc/README.md
@@ -31,7 +31,9 @@ The GNU C Library supports a very wide set of platforms and architectures.
 The current Zig support for glibc only includes Linux.
 
 Zig supports glibc versions back to v2.17 (2012) as the Zig standard
-library depends on symbols that were introduced in 2.17.
+library depends on symbols that were introduced in 2.17. When used as a C
+or C++ compiler (i.e., `zig cc`) zig supports glibc versions back to
+v2.2.5.
 
 ## Glibc stubs
 
test/link/glibc_compat/build.zig
@@ -28,7 +28,107 @@ pub fn build(b: *std.Build) void {
         test_step.dependOn(&exe.step);
     }
 
-    // Build & run against a sampling of supported glibc versions
+    // Build & run a C test case against a sampling of supported glibc versions
+    for ([_][]const u8{
+        // "native-linux-gnu.2.0", // fails with a pile of missing symbols.
+        "native-linux-gnu.2.2.5",
+        "native-linux-gnu.2.4",
+        "native-linux-gnu.2.12",
+        "native-linux-gnu.2.16",
+        "native-linux-gnu.2.22",
+        "native-linux-gnu.2.28",
+        "native-linux-gnu.2.33",
+        "native-linux-gnu.2.38",
+        "native-linux-gnu",
+    }) |t| {
+        const target = b.resolveTargetQuery(std.Target.Query.parse(
+            .{ .arch_os_abi = t },
+        ) catch unreachable);
+
+        const glibc_ver = target.result.os.version_range.linux.glibc;
+
+        const exe = b.addExecutable(.{
+            .name = t,
+            .target = target,
+        });
+        exe.addCSourceFile(.{ .file = b.path("glibc_runtime_check.c") });
+        exe.linkLibC();
+
+        // Only try running the test if the host glibc is known to be good enough.  Ideally, the Zig
+        // test runner would be able to check this, but see https://github.com/ziglang/zig/pull/17702#issuecomment-1831310453
+        if (running_glibc_ver) |running_ver| {
+            if (glibc_ver.order(running_ver) == .lt) {
+                const run_cmd = b.addRunArtifact(exe);
+                run_cmd.skip_foreign_checks = true;
+                run_cmd.expectExitCode(0);
+
+                test_step.dependOn(&run_cmd.step);
+            }
+        }
+        const check = exe.checkObject();
+
+        // __errno_location is always a dynamically linked symbol
+        check.checkInDynamicSymtab();
+        check.checkExact("0 0 UND FUNC GLOBAL DEFAULT __errno_location");
+
+        // before v2.32 fstat redirects through __fxstat, afterwards its a
+        // normal dynamic symbol
+        check.checkInDynamicSymtab();
+        if (glibc_ver.order(.{ .major = 2, .minor = 32, .patch = 0 }) == .lt) {
+            check.checkExact("0 0 UND FUNC GLOBAL DEFAULT __fxstat");
+
+            check.checkInSymtab();
+            check.checkContains("FUNC LOCAL HIDDEN fstat");
+        } else {
+            check.checkExact("0 0 UND FUNC GLOBAL DEFAULT fstat");
+
+            check.checkInSymtab();
+            check.checkNotPresent("__fxstat");
+        }
+
+        // before v2.26 reallocarray is not supported
+        check.checkInDynamicSymtab();
+        if (glibc_ver.order(.{ .major = 2, .minor = 26, .patch = 0 }) == .lt) {
+            check.checkNotPresent("reallocarray");
+        } else {
+            check.checkExact("0 0 UND FUNC GLOBAL DEFAULT reallocarray");
+        }
+
+        // before v2.38 strlcpy is not supported
+        check.checkInDynamicSymtab();
+        if (glibc_ver.order(.{ .major = 2, .minor = 38, .patch = 0 }) == .lt) {
+            check.checkNotPresent("strlcpy");
+        } else {
+            check.checkExact("0 0 UND FUNC GLOBAL DEFAULT strlcpy");
+        }
+
+        // v2.16 introduced getauxval()
+        check.checkInDynamicSymtab();
+        if (glibc_ver.order(.{ .major = 2, .minor = 16, .patch = 0 }) == .lt) {
+            check.checkNotPresent("getauxval");
+        } else {
+            check.checkExact("0 0 UND FUNC GLOBAL DEFAULT getauxval");
+        }
+
+        // Always have dynamic "exit", "pow", and "powf" references
+        check.checkInDynamicSymtab();
+        check.checkExact("0 0 UND FUNC GLOBAL DEFAULT exit");
+        check.checkInDynamicSymtab();
+        check.checkExact("0 0 UND FUNC GLOBAL DEFAULT pow");
+        check.checkInDynamicSymtab();
+        check.checkExact("0 0 UND FUNC GLOBAL DEFAULT powf");
+
+        // An atexit local symbol is defined, and depends on undefined dynamic
+        // __cxa_atexit.
+        check.checkInSymtab();
+        check.checkContains("FUNC LOCAL HIDDEN atexit");
+        check.checkInDynamicSymtab();
+        check.checkExact("0 0 UND FUNC GLOBAL DEFAULT __cxa_atexit");
+
+        test_step.dependOn(&check.step);
+    }
+
+    // Build & run a Zig test case against a sampling of supported glibc versions
     for ([_][]const u8{
         "native-linux-gnu.2.17", // Currently oldest supported, see #17769
         "native-linux-gnu.2.23",
test/link/glibc_compat/glibc_runtime_check.c
@@ -0,0 +1,114 @@
+/*
+ * Exercise complicating glibc symbols from C code.  Complicating symbols
+ * are ones that have moved between glibc versions, or use floating point
+ * parameters, or have otherwise tripped up the Zig glibc compatibility
+ * code.
+ */
+#include <assert.h>
+#include <errno.h>
+#include <features.h>
+#include <fcntl.h>
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/auxv.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+/* errno is compilcated (thread-local, dynamically provided, etc). */
+static void check_errno()
+{
+	int invalid_fd = open("/doesnotexist", O_RDONLY);
+	assert(invalid_fd == -1);
+	assert(errno == ENOENT);
+}
+
+/* fstat has moved around in glibc (between libc_nonshared and libc) */
+static void check_fstat()
+{
+	int self_fd = open("/proc/self/exe", O_RDONLY);
+
+	struct stat statbuf = {0};
+	int rc = fstat(self_fd, &statbuf);
+
+	assert(rc == 0);
+
+	assert(statbuf.st_dev != 0);
+	assert(statbuf.st_ino != 0);
+	assert(statbuf.st_mode != 0);
+	assert(statbuf.st_size > 0);
+	assert(statbuf.st_blocks > 0);
+	assert(statbuf.st_ctim.tv_sec > 0);
+
+	close(self_fd);
+}
+
+/* Some targets have a complicated ABI for floats and doubles */
+static void check_fp_abi()
+{
+	// Picked "pow" as it takes and returns doubles
+	assert(pow(10.0, 10.0) == 10000000000.0);
+	assert(powf(10.0f, 10.0f) == 10000000000.0f);
+}
+
+/* strlcpy introduced in glibc 2.38 */
+static void check_strlcpy()
+{
+#if (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 38) || (__GLIBC__ > 2)
+	char target[4] = {0};
+	strlcpy(target, "this is a source string", 4);
+
+	assert(strcmp(target, "thi") == 0);
+#endif
+}
+
+/* reallocarray introduced in glibc 2.26 */
+static void check_reallocarray()
+{
+#if (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 26) || (__GLIBC__ > 2)
+	const size_t el_size = 32;
+	void* base = reallocarray(NULL, 10, el_size);
+	void* grown = reallocarray(base, 100, el_size);
+
+	assert(base != NULL);
+	assert(grown != NULL);
+
+	free(grown);
+#endif
+}
+
+/* getauxval introduced in glibc 2.16 */
+static void check_getauxval()
+{
+#if (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 16) || (__GLIBC__ > 2)
+	int pgsz = getauxval(AT_PAGESZ);
+	assert(pgsz >= 4*1024);
+#endif
+}
+
+/* atexit() is part of libc_nonshared */
+static void force_exit_0()
+{
+	exit(0);
+}
+
+static void check_atexit()
+{
+	int rc = atexit(force_exit_0);
+	assert(rc == 0);
+}
+
+int main() {
+	int rc;
+
+	check_errno();
+	check_fstat();
+	check_fp_abi();
+	check_strlcpy();
+	check_reallocarray();
+	check_getauxval();
+	check_atexit();
+
+	exit(99); // exit code overridden by atexit handler
+}