aboutsummaryrefslogtreecommitdiff
path: root/lib/mlibc/tests/rtdl
diff options
context:
space:
mode:
authorIan Moffett <ian@osmora.org>2024-03-07 17:28:00 -0500
committerIan Moffett <ian@osmora.org>2024-03-07 17:28:32 -0500
commitbd5969fc876a10b18613302db7087ef3c40f18e1 (patch)
tree7c2b8619afe902abf99570df2873fbdf40a4d1a1 /lib/mlibc/tests/rtdl
parenta95b38b1b92b172e6cc4e8e56a88a30cc65907b0 (diff)
lib: Add mlibc
Signed-off-by: Ian Moffett <ian@osmora.org>
Diffstat (limited to 'lib/mlibc/tests/rtdl')
-rw-r--r--lib/mlibc/tests/rtdl/dl_iterate_phdr/libbar.c3
-rw-r--r--lib/mlibc/tests/rtdl/dl_iterate_phdr/libfoo.c1
-rw-r--r--lib/mlibc/tests/rtdl/dl_iterate_phdr/meson.build7
-rw-r--r--lib/mlibc/tests/rtdl/dl_iterate_phdr/test.c91
-rw-r--r--lib/mlibc/tests/rtdl/dladdr_local/libfoo.c1
-rw-r--r--lib/mlibc/tests/rtdl/dladdr_local/meson.build5
-rw-r--r--lib/mlibc/tests/rtdl/dladdr_local/test.c21
-rw-r--r--lib/mlibc/tests/rtdl/ld_library_path/libfoo.c1
-rw-r--r--lib/mlibc/tests/rtdl/ld_library_path/meson.build19
-rw-r--r--lib/mlibc/tests/rtdl/ld_library_path/test.c16
-rw-r--r--lib/mlibc/tests/rtdl/meson.build56
-rw-r--r--lib/mlibc/tests/rtdl/noload-promote/libfoo.c1
-rw-r--r--lib/mlibc/tests/rtdl/noload-promote/meson.build5
-rw-r--r--lib/mlibc/tests/rtdl/noload-promote/test.c22
-rw-r--r--lib/mlibc/tests/rtdl/preinit/libfoo.c18
-rw-r--r--lib/mlibc/tests/rtdl/preinit/meson.build18
-rw-r--r--lib/mlibc/tests/rtdl/preinit/test.c44
-rw-r--r--lib/mlibc/tests/rtdl/rtld_next/libbar.c16
-rw-r--r--lib/mlibc/tests/rtdl/rtld_next/libfoo.c17
-rw-r--r--lib/mlibc/tests/rtdl/rtld_next/meson.build25
-rw-r--r--lib/mlibc/tests/rtdl/rtld_next/test.c26
-rw-r--r--lib/mlibc/tests/rtdl/scope1/libbar.c14
-rw-r--r--lib/mlibc/tests/rtdl/scope1/libfoo.c9
-rw-r--r--lib/mlibc/tests/rtdl/scope1/meson.build7
-rw-r--r--lib/mlibc/tests/rtdl/scope1/test.c90
-rw-r--r--lib/mlibc/tests/rtdl/scope2/libbar.c5
-rw-r--r--lib/mlibc/tests/rtdl/scope2/libbaz.c3
-rw-r--r--lib/mlibc/tests/rtdl/scope2/libfoo.c3
-rw-r--r--lib/mlibc/tests/rtdl/scope2/meson.build9
-rw-r--r--lib/mlibc/tests/rtdl/scope2/test.c36
-rw-r--r--lib/mlibc/tests/rtdl/scope3/libbar.c8
-rw-r--r--lib/mlibc/tests/rtdl/scope3/libbaz.c7
-rw-r--r--lib/mlibc/tests/rtdl/scope3/libfoo.c5
-rw-r--r--lib/mlibc/tests/rtdl/scope3/meson.build9
-rw-r--r--lib/mlibc/tests/rtdl/scope3/test.c39
-rw-r--r--lib/mlibc/tests/rtdl/scope4/libbar.c3
-rw-r--r--lib/mlibc/tests/rtdl/scope4/libbaz.c1
-rw-r--r--lib/mlibc/tests/rtdl/scope4/libfoo.c3
-rw-r--r--lib/mlibc/tests/rtdl/scope4/meson.build9
-rw-r--r--lib/mlibc/tests/rtdl/scope4/test.c36
-rw-r--r--lib/mlibc/tests/rtdl/scope5/libfoo.c1
-rw-r--r--lib/mlibc/tests/rtdl/scope5/meson.build5
-rw-r--r--lib/mlibc/tests/rtdl/scope5/test.c27
-rw-r--r--lib/mlibc/tests/rtdl/soname/libbar.c1
-rw-r--r--lib/mlibc/tests/rtdl/soname/libfoo.c1
-rw-r--r--lib/mlibc/tests/rtdl/soname/meson.build17
-rw-r--r--lib/mlibc/tests/rtdl/soname/test.c33
-rw-r--r--lib/mlibc/tests/rtdl/tls_align/libbar.c1
-rw-r--r--lib/mlibc/tests/rtdl/tls_align/libfoo.c1
-rw-r--r--lib/mlibc/tests/rtdl/tls_align/meson.build7
-rw-r--r--lib/mlibc/tests/rtdl/tls_align/test.c10
51 files changed, 813 insertions, 0 deletions
diff --git a/lib/mlibc/tests/rtdl/dl_iterate_phdr/libbar.c b/lib/mlibc/tests/rtdl/dl_iterate_phdr/libbar.c
new file mode 100644
index 0000000..41ccc56
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/dl_iterate_phdr/libbar.c
@@ -0,0 +1,3 @@
+// Bar needs to have a relocation against foo in order to set DT_NEEDED.
+int foo(void);
+int bar() { return foo(); }
diff --git a/lib/mlibc/tests/rtdl/dl_iterate_phdr/libfoo.c b/lib/mlibc/tests/rtdl/dl_iterate_phdr/libfoo.c
new file mode 100644
index 0000000..9fe07f8
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/dl_iterate_phdr/libfoo.c
@@ -0,0 +1 @@
+int foo() { return 0; }
diff --git a/lib/mlibc/tests/rtdl/dl_iterate_phdr/meson.build b/lib/mlibc/tests/rtdl/dl_iterate_phdr/meson.build
new file mode 100644
index 0000000..acb679e
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/dl_iterate_phdr/meson.build
@@ -0,0 +1,7 @@
+libfoo = shared_library('foo', 'libfoo.c')
+libbar = shared_library('bar', 'libbar.c', build_rpath: test_rpath, link_with: libfoo)
+test_depends = [libfoo, libbar]
+
+libfoo_native = shared_library('native-foo', 'libfoo.c', native: true)
+libbar_native = shared_library('native-bar', 'libbar.c', build_rpath: test_rpath, link_with: libfoo_native, native: true)
+test_native_depends = [libfoo_native, libbar_native]
diff --git a/lib/mlibc/tests/rtdl/dl_iterate_phdr/test.c b/lib/mlibc/tests/rtdl/dl_iterate_phdr/test.c
new file mode 100644
index 0000000..5d48a41
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/dl_iterate_phdr/test.c
@@ -0,0 +1,91 @@
+#include <assert.h>
+#include <link.h>
+#include <string.h>
+#include <dlfcn.h>
+#include <stdio.h>
+
+#ifdef USE_HOST_LIBC
+#define LDSO_PATTERN "ld-linux-"
+#define LIBFOO "libnative-foo.so"
+#define LIBBAR "libnative-bar.so"
+#else
+#define LDSO_PATTERN "ld.so"
+#define LIBFOO "libfoo.so"
+#define LIBBAR "libbar.so"
+#endif
+
+struct result {
+ int found_ldso;
+ int found_self;
+ int found_foo;
+ int found_bar;
+};
+
+static int ends_with(const char *suffix, const char *s) {
+ size_t suffix_len = strlen(suffix);
+ size_t s_len = strlen(s);
+ if (s_len < suffix_len)
+ return 0;
+ else {
+ return !strcmp(suffix, s + s_len - suffix_len);
+ }
+}
+
+static int contains(const char *pattern, const char *s) {
+ return !!strstr(s, pattern);
+}
+
+static int callback(struct dl_phdr_info *info, size_t size, void *data) {
+ assert(size == sizeof(struct dl_phdr_info));
+ struct result *found = (struct result *) data;
+
+ printf("%s\n", info->dlpi_name);
+ fflush(stdout);
+
+ if (ends_with("foo.so", info->dlpi_name))
+ found->found_foo++;
+ if (ends_with("bar.so", info->dlpi_name))
+ found->found_bar++;
+ if (contains(LDSO_PATTERN, info->dlpi_name))
+ found->found_ldso++;
+
+ if (!strcmp("", info->dlpi_name))
+ found->found_self++;
+
+ assert(info->dlpi_phdr);
+ return 0;
+}
+
+int main() {
+ struct result found = { 0 };
+ assert(!dl_iterate_phdr(callback, &found));
+ assert(found.found_ldso == 1);
+ assert(found.found_self == 1);
+ assert(found.found_foo == 0);
+ assert(found.found_bar == 0);
+ printf("---\n");
+
+ memset(&found, 0, sizeof(found));
+ void *bar = dlopen(LIBBAR, RTLD_LOCAL | RTLD_NOW);
+ assert(bar);
+ assert(!dl_iterate_phdr(callback, &found));
+ assert(found.found_ldso == 1);
+ assert(found.found_self == 1);
+ assert(found.found_bar == 1);
+ assert(found.found_foo == 1); // Since bar depends on foo.
+ printf("---\n");
+
+ memset(&found, 0, sizeof(found));
+ void *foo = dlopen(LIBFOO, RTLD_GLOBAL | RTLD_NOW);
+ assert(foo);
+ assert(!dl_iterate_phdr(callback, &found));
+ assert(found.found_ldso == 1);
+ assert(found.found_self == 1);
+ assert(found.found_foo == 1);
+ assert(found.found_bar == 1);
+ printf("---\n");
+
+ dlclose(bar);
+ dlclose(foo);
+ return 0;
+}
diff --git a/lib/mlibc/tests/rtdl/dladdr_local/libfoo.c b/lib/mlibc/tests/rtdl/dladdr_local/libfoo.c
new file mode 100644
index 0000000..2903a3d
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/dladdr_local/libfoo.c
@@ -0,0 +1 @@
+char foo_global[] = "";
diff --git a/lib/mlibc/tests/rtdl/dladdr_local/meson.build b/lib/mlibc/tests/rtdl/dladdr_local/meson.build
new file mode 100644
index 0000000..4ae6bb3
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/dladdr_local/meson.build
@@ -0,0 +1,5 @@
+libfoo = shared_library('foo', 'libfoo.c')
+test_depends = [libfoo]
+
+libfoo_native = shared_library('native-foo', 'libfoo.c', native: true)
+test_native_depends = [libfoo_native]
diff --git a/lib/mlibc/tests/rtdl/dladdr_local/test.c b/lib/mlibc/tests/rtdl/dladdr_local/test.c
new file mode 100644
index 0000000..c64d259
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/dladdr_local/test.c
@@ -0,0 +1,21 @@
+#include <dlfcn.h>
+#include <assert.h>
+
+#ifdef USE_HOST_LIBC
+#define LIBFOO "libnative-foo.so"
+#else
+#define LIBFOO "libfoo.so"
+#endif
+
+int main() {
+ void *foo_handle = dlopen(LIBFOO, RTLD_LOCAL | RTLD_NOW);
+ assert(foo_handle);
+
+ char *foo_global = (char *)dlsym(foo_handle, "foo_global");
+ assert(foo_global);
+
+ Dl_info info;
+ assert(dladdr((const void *)foo_global, &info) != 0);
+
+ assert(dlclose(foo_handle) == 0);
+}
diff --git a/lib/mlibc/tests/rtdl/ld_library_path/libfoo.c b/lib/mlibc/tests/rtdl/ld_library_path/libfoo.c
new file mode 100644
index 0000000..85e6cd8
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/ld_library_path/libfoo.c
@@ -0,0 +1 @@
+void foo() {}
diff --git a/lib/mlibc/tests/rtdl/ld_library_path/meson.build b/lib/mlibc/tests/rtdl/ld_library_path/meson.build
new file mode 100644
index 0000000..95629be
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/ld_library_path/meson.build
@@ -0,0 +1,19 @@
+# Remove the RPATH and set the LD_LIBRARY_PATH environment variable
+# instead when running tests to make sure the library can be found
+# in a directory specified by the user at runtime
+
+test_rpath = '$ORIGIN/'
+
+test_ld_path = meson.build_root() / 'tests' / 'rtdl' / test_name
+test_env += ['LD_LIBRARY_PATH=' + test_ld_path]
+test_native_env += ['LD_LIBRARY_PATH=' + test_ld_path]
+
+libfoo = shared_library('foo', 'libfoo.c',
+ dependencies: libc_dep,
+ link_args: test_additional_link_args,
+)
+
+libfoo_native = shared_library('native-foo', 'libfoo.c',
+ link_args: ['-ldl'] + test_additional_link_args,
+ native: true
+)
diff --git a/lib/mlibc/tests/rtdl/ld_library_path/test.c b/lib/mlibc/tests/rtdl/ld_library_path/test.c
new file mode 100644
index 0000000..15ca200
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/ld_library_path/test.c
@@ -0,0 +1,16 @@
+#include <dlfcn.h>
+#include <stdio.h>
+#include <assert.h>
+#include <string.h>
+
+#ifdef USE_HOST_LIBC
+#define LIBFOO "libnative-foo.so"
+#else
+#define LIBFOO "libfoo.so"
+#endif
+
+int main() {
+ void *foo = dlopen(LIBFOO, RTLD_NOW);
+ assert(foo);
+ dlclose(foo);
+}
diff --git a/lib/mlibc/tests/rtdl/meson.build b/lib/mlibc/tests/rtdl/meson.build
new file mode 100644
index 0000000..1331899
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/meson.build
@@ -0,0 +1,56 @@
+rtdl_test_cases = [
+ 'dl_iterate_phdr',
+ 'dladdr_local',
+ 'ld_library_path',
+ 'noload-promote',
+ 'rtld_next',
+ 'soname',
+ 'preinit',
+ 'scope1',
+ 'scope2',
+ 'scope3',
+ 'scope4',
+ 'scope5',
+ 'tls_align',
+]
+
+foreach test_name : rtdl_test_cases
+ test_rpath = meson.build_root() / 'tests' / 'rtdl' / test_name / ''
+ test_rpath += ':$ORIGIN/' # Workaround old and buggy qemu-user on CI
+
+ test_env = []
+ test_link_with = []
+ test_depends = []
+ test_native_env = []
+ test_native_link_with = []
+ test_native_depends = []
+ test_additional_link_args = []
+
+ # Build the needed DSOs for the test. This sets the variables above.
+ subdir(test_name)
+
+ exec = executable('rtdl-' + test_name, [test_name / 'test.c', test_sources],
+ link_with: test_link_with,
+ dependencies: libc_dep,
+ build_rpath: test_rpath,
+ override_options: test_override_options,
+ c_args: test_c_args,
+ link_args: test_link_args + test_additional_link_args,
+ )
+ test(test_name, exec, env: test_env, suite: 'rtdl', depends: test_depends)
+
+ if build_tests_host_libc and not host_libc_excluded_test_cases.contains(test_name)
+ exec = executable('host-libc-' + test_name, test_name / 'test.c',
+ link_with: test_native_link_with,
+ dependencies: rtlib_deps,
+ build_rpath: test_rpath,
+ # Don't use ASan here, due to a bug that breaks dlopen() + DT_RUNPATH:
+ # https://bugzilla.redhat.com/show_bug.cgi?id=1449604
+ override_options: 'b_sanitize=undefined',
+ c_args: ['-D_GNU_SOURCE', '-DUSE_HOST_LIBC'],
+ link_args: ['-ldl'] + test_additional_link_args,
+ native: true,
+ )
+ test(test_name, exec, env: test_native_env, suite: ['host-libc', 'rtdl'], depends: test_native_depends)
+ endif
+endforeach
diff --git a/lib/mlibc/tests/rtdl/noload-promote/libfoo.c b/lib/mlibc/tests/rtdl/noload-promote/libfoo.c
new file mode 100644
index 0000000..85e6cd8
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/noload-promote/libfoo.c
@@ -0,0 +1 @@
+void foo() {}
diff --git a/lib/mlibc/tests/rtdl/noload-promote/meson.build b/lib/mlibc/tests/rtdl/noload-promote/meson.build
new file mode 100644
index 0000000..4ae6bb3
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/noload-promote/meson.build
@@ -0,0 +1,5 @@
+libfoo = shared_library('foo', 'libfoo.c')
+test_depends = [libfoo]
+
+libfoo_native = shared_library('native-foo', 'libfoo.c', native: true)
+test_native_depends = [libfoo_native]
diff --git a/lib/mlibc/tests/rtdl/noload-promote/test.c b/lib/mlibc/tests/rtdl/noload-promote/test.c
new file mode 100644
index 0000000..0a6c55c
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/noload-promote/test.c
@@ -0,0 +1,22 @@
+#include <dlfcn.h>
+#include <assert.h>
+#include <stddef.h>
+
+#ifdef USE_HOST_LIBC
+#define LIBFOO "libnative-foo.so"
+#else
+#define LIBFOO "libfoo.so"
+#endif
+
+int main() {
+ void *foo = dlopen(LIBFOO, RTLD_LOCAL | RTLD_NOW);
+ assert(foo);
+
+ assert(dlsym(RTLD_DEFAULT, "foo") == NULL);
+
+ // Opening a library with RTLD_NOLOAD | RTLD_GLOBAL should promote it to the global scope.
+ assert(dlopen(LIBFOO, RTLD_NOLOAD | RTLD_GLOBAL | RTLD_NOW) == foo);
+ assert(dlsym(RTLD_DEFAULT, "foo") != NULL);
+
+ assert(dlopen("does-not-exist.so.1337", RTLD_NOLOAD | RTLD_GLOBAL | RTLD_NOW) == NULL);
+}
diff --git a/lib/mlibc/tests/rtdl/preinit/libfoo.c b/lib/mlibc/tests/rtdl/preinit/libfoo.c
new file mode 100644
index 0000000..9c834ea
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/preinit/libfoo.c
@@ -0,0 +1,18 @@
+#include <stdio.h>
+#include <assert.h>
+
+int fooDone = 0;
+
+// DSOs do not support pre-initialization functions.
+
+__attribute__((constructor))
+void fooInit() {
+ dprintf(1, "initialization function called in foo\n");
+
+ assert(fooDone == 0);
+ fooDone++;
+}
+
+int isFooDone() {
+ return fooDone;
+}
diff --git a/lib/mlibc/tests/rtdl/preinit/meson.build b/lib/mlibc/tests/rtdl/preinit/meson.build
new file mode 100644
index 0000000..1a7f398
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/preinit/meson.build
@@ -0,0 +1,18 @@
+if host_machine.cpu_family() == 'riscv64'
+ # gp isn't initialized until after crt1.o runs, so to access
+ # globals in our pre-initializers we must disable it.
+ test_additional_link_args = ['-Wl,--no-relax']
+endif
+
+libfoo = shared_library('foo', 'libfoo.c',
+ dependencies: libc_dep,
+ override_options: 'b_sanitize=none',
+ link_args: test_additional_link_args,
+)
+test_link_with = [libfoo]
+
+libfoo_native = shared_library('native-foo', 'libfoo.c',
+ link_args: ['-ldl'] + test_additional_link_args,
+ native: true
+)
+test_native_link_with = [libfoo_native]
diff --git a/lib/mlibc/tests/rtdl/preinit/test.c b/lib/mlibc/tests/rtdl/preinit/test.c
new file mode 100644
index 0000000..5b5d5e8
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/preinit/test.c
@@ -0,0 +1,44 @@
+#include <stdio.h>
+#include <assert.h>
+
+int mainDone = 0;
+
+int isFooDone();
+
+void preInit1() {
+ // Use dprintf because stdout might not be initialized yet.
+ dprintf(1, "pre-initialization function 1 called in main executable\n");
+
+ assert(isFooDone() == 0);
+ assert(mainDone == 0);
+ mainDone++;
+}
+
+void preInit2() {
+ dprintf(1, "pre-initialization function 2 called in main executable\n");
+
+ assert(isFooDone() == 0);
+ assert(mainDone == 1);
+ mainDone++;
+}
+
+__attribute__((constructor))
+void mainInit() {
+ dprintf(1, "initialization function called in main executable\n");
+
+ assert(isFooDone() == 1);
+ assert(mainDone == 2);
+ mainDone++;
+}
+
+// Manually register the pre-initialization functions.
+__attribute__((used, section(".preinit_array")))
+static void (*preinitFunctions[])(void) = {
+ &preInit1,
+ &preInit2,
+};
+
+int main() {
+ assert(isFooDone() == 1);
+ assert(mainDone == 3);
+}
diff --git a/lib/mlibc/tests/rtdl/rtld_next/libbar.c b/lib/mlibc/tests/rtdl/rtld_next/libbar.c
new file mode 100644
index 0000000..c0950c5
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/rtld_next/libbar.c
@@ -0,0 +1,16 @@
+#include <dlfcn.h>
+
+typedef char *charFn(void);
+
+__attribute__((weak))
+char *definedInBoth() {
+ return "bar";
+}
+
+charFn *barGetDefault() {
+ return (charFn *)dlsym(RTLD_DEFAULT, "definedInBoth");
+}
+
+charFn *barGetNext() {
+ return (charFn *)dlsym(RTLD_NEXT, "definedInBoth");
+}
diff --git a/lib/mlibc/tests/rtdl/rtld_next/libfoo.c b/lib/mlibc/tests/rtdl/rtld_next/libfoo.c
new file mode 100644
index 0000000..1d46b73
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/rtld_next/libfoo.c
@@ -0,0 +1,17 @@
+#include <dlfcn.h>
+
+typedef char *charFn(void);
+
+__attribute__((weak))
+char *definedInBoth() {
+ return "foo";
+}
+
+charFn *fooGetDefault() {
+ return (charFn *)dlsym(RTLD_DEFAULT, "definedInBoth");
+}
+
+charFn *fooGetNext() {
+ return (charFn *)dlsym(RTLD_NEXT, "definedInBoth");
+}
+
diff --git a/lib/mlibc/tests/rtdl/rtld_next/meson.build b/lib/mlibc/tests/rtdl/rtld_next/meson.build
new file mode 100644
index 0000000..d156c24
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/rtld_next/meson.build
@@ -0,0 +1,25 @@
+# Prevent tail calls, as it breaks libc's 'calling object' detection.
+no_tail_calls = '-fno-optimize-sibling-calls'
+
+libfoo = shared_library('foo', 'libfoo.c',
+ dependencies: libc_dep,
+ c_args: no_tail_calls,
+)
+libbar = shared_library('bar', 'libbar.c',
+ dependencies: libc_dep,
+ c_args: no_tail_calls,
+)
+test_link_with = [libfoo, libbar] # foo is linked before bar
+
+libfoo_native = shared_library('native-foo', 'libfoo.c',
+ c_args: [no_tail_calls, '-D_GNU_SOURCE'],
+ link_args: '-ldl',
+ native: true
+)
+libbar_native = shared_library('native-bar', 'libbar.c',
+ c_args: [no_tail_calls, '-D_GNU_SOURCE'],
+ link_args: '-ldl',
+ native: true
+)
+test_native_link_with = [libfoo_native, libbar_native]
+
diff --git a/lib/mlibc/tests/rtdl/rtld_next/test.c b/lib/mlibc/tests/rtdl/rtld_next/test.c
new file mode 100644
index 0000000..3e90a63
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/rtld_next/test.c
@@ -0,0 +1,26 @@
+#include <string.h>
+#include <assert.h>
+
+typedef char *charFn(void);
+charFn *fooGetDefault(void);
+charFn *fooGetNext(void);
+charFn *barGetDefault(void);
+charFn *barGetNext(void);
+
+int main() {
+ charFn *ret;
+
+ ret = fooGetDefault();
+ assert(ret != NULL);
+ assert(!strcmp(ret(), "foo"));
+
+ ret = fooGetNext();
+ assert(ret != NULL);
+ assert(!strcmp(ret(), "bar"));
+
+ ret = barGetDefault();
+ assert(ret != NULL);
+ assert(!strcmp(ret(), "foo"));
+
+ assert(barGetNext() == NULL);
+}
diff --git a/lib/mlibc/tests/rtdl/scope1/libbar.c b/lib/mlibc/tests/rtdl/scope1/libbar.c
new file mode 100644
index 0000000..ecf043e
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/scope1/libbar.c
@@ -0,0 +1,14 @@
+char *foo(void);
+char *foo_global(void);
+
+char *bar() {
+ return "bar";
+}
+
+char *bar_calls_foo() {
+ return foo();
+}
+
+char *bar_calls_foo_global() {
+ return foo_global();
+}
diff --git a/lib/mlibc/tests/rtdl/scope1/libfoo.c b/lib/mlibc/tests/rtdl/scope1/libfoo.c
new file mode 100644
index 0000000..b4e1b8c
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/scope1/libfoo.c
@@ -0,0 +1,9 @@
+char *foo() {
+ return "foo";
+}
+
+char global[] = "foo global";
+
+char *foo_global() {
+ return global;
+}
diff --git a/lib/mlibc/tests/rtdl/scope1/meson.build b/lib/mlibc/tests/rtdl/scope1/meson.build
new file mode 100644
index 0000000..acb679e
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/scope1/meson.build
@@ -0,0 +1,7 @@
+libfoo = shared_library('foo', 'libfoo.c')
+libbar = shared_library('bar', 'libbar.c', build_rpath: test_rpath, link_with: libfoo)
+test_depends = [libfoo, libbar]
+
+libfoo_native = shared_library('native-foo', 'libfoo.c', native: true)
+libbar_native = shared_library('native-bar', 'libbar.c', build_rpath: test_rpath, link_with: libfoo_native, native: true)
+test_native_depends = [libfoo_native, libbar_native]
diff --git a/lib/mlibc/tests/rtdl/scope1/test.c b/lib/mlibc/tests/rtdl/scope1/test.c
new file mode 100644
index 0000000..c19915d
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/scope1/test.c
@@ -0,0 +1,90 @@
+#include <stddef.h>
+#include <dlfcn.h>
+#include <assert.h>
+#include <string.h>
+#include <stdio.h>
+
+#ifdef USE_HOST_LIBC
+#define LIBFOO "libnative-foo.so"
+#define LIBBAR "libnative-bar.so"
+#else
+#define LIBFOO "libfoo.so"
+#define LIBBAR "libbar.so"
+#endif
+
+typedef char *strfn(void);
+
+int main() {
+ // We haven't dlopen'd these libs yet, so symbol resolution should fail.
+ assert(dlsym(RTLD_DEFAULT, "foo") == NULL);
+ assert(dlsym(RTLD_DEFAULT, "bar") == NULL);
+
+ assert(!dlopen(LIBFOO, RTLD_NOLOAD));
+ assert(!dlopen(LIBBAR, RTLD_NOLOAD));
+
+ void *foo_handle = dlopen(LIBFOO, RTLD_LOCAL | RTLD_NOW);
+ assert(foo_handle);
+ assert(dlopen(LIBFOO, RTLD_NOLOAD | RTLD_NOW));
+
+ strfn *foo_sym = dlsym(foo_handle, "foo");
+ assert(foo_sym);
+ assert(foo_sym());
+ assert(!strcmp(foo_sym(), "foo"));
+
+ strfn *foo_global_sym = dlsym(foo_handle, "foo_global");
+ assert(foo_global_sym);
+ assert(foo_global_sym());
+ assert(!strcmp(foo_global_sym(), "foo global"));
+
+ assert(dlsym(foo_handle, "doesnotexist") == NULL);
+
+ // Nested opening should work
+ assert(dlopen(LIBFOO, RTLD_LOCAL | RTLD_NOW) == foo_handle);
+ assert(dlopen(LIBFOO, RTLD_NOLOAD | RTLD_NOW));
+
+ // Since we've loaded the same library twice, the addresses should be the same
+ assert(dlsym(foo_handle, "foo") == foo_sym);
+ assert(dlsym(foo_handle, "foo_global") == foo_global_sym);
+
+ // libfoo was opened with RTLD_LOCAL, so we shouldn't be able to lookup
+ // its symbols in the global namespace.
+ assert(dlsym(RTLD_DEFAULT, "foo") == NULL);
+
+ {
+ void *bar_handle = dlopen(LIBBAR, RTLD_GLOBAL | RTLD_NOW);
+ assert(bar_handle);
+ assert(dlopen(LIBBAR, RTLD_NOLOAD | RTLD_NOW));
+
+ strfn *bar_sym = dlsym(bar_handle, "bar");
+ assert(bar_sym);
+ assert(bar_sym());
+ assert(!strcmp(bar_sym(), "bar"));
+
+ strfn *bar_calls_foo_sym = dlsym(bar_handle, "bar_calls_foo");
+ assert(bar_calls_foo_sym);
+ assert(bar_calls_foo_sym());
+ assert(!strcmp(bar_calls_foo_sym(), "foo"));
+
+ strfn *bar_calls_foo_global_sym = dlsym(bar_handle, "bar_calls_foo_global");
+ assert(bar_calls_foo_global_sym);
+ assert(bar_calls_foo_global_sym());
+ assert(!strcmp(bar_calls_foo_global_sym(), "foo global"));
+
+ // libbar was opened with RTLD_GLOBAL, so we can find symbols by
+ // searching in the global scope.
+ strfn *new_bar_sym = dlsym(RTLD_DEFAULT, "bar");
+ assert(new_bar_sym);
+ assert(new_bar_sym == bar_sym);
+
+ // Note that we loaded libbar with RTLD_GLOBAL, which should pull
+ // in libfoo's symbols globally too.
+ strfn *new_foo_sym = dlsym(RTLD_DEFAULT, "foo");
+ assert(new_foo_sym);
+ assert(new_foo_sym == foo_sym);
+
+ assert(dlclose(bar_handle) == 0);
+ }
+
+ assert(dlclose(foo_handle) == 0);
+ assert(dlclose(foo_handle) == 0);
+}
diff --git a/lib/mlibc/tests/rtdl/scope2/libbar.c b/lib/mlibc/tests/rtdl/scope2/libbar.c
new file mode 100644
index 0000000..4783c58
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/scope2/libbar.c
@@ -0,0 +1,5 @@
+char *foo_baz_conflict(void);
+
+char *bar_calls_foo_baz_conflict() {
+ return foo_baz_conflict();
+}
diff --git a/lib/mlibc/tests/rtdl/scope2/libbaz.c b/lib/mlibc/tests/rtdl/scope2/libbaz.c
new file mode 100644
index 0000000..fc73adc
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/scope2/libbaz.c
@@ -0,0 +1,3 @@
+char *foo_baz_conflict() {
+ return "resolved to baz";
+}
diff --git a/lib/mlibc/tests/rtdl/scope2/libfoo.c b/lib/mlibc/tests/rtdl/scope2/libfoo.c
new file mode 100644
index 0000000..9f7b881
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/scope2/libfoo.c
@@ -0,0 +1,3 @@
+char *foo_baz_conflict() {
+ return "resolved to foo";
+}
diff --git a/lib/mlibc/tests/rtdl/scope2/meson.build b/lib/mlibc/tests/rtdl/scope2/meson.build
new file mode 100644
index 0000000..938272c
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/scope2/meson.build
@@ -0,0 +1,9 @@
+libfoo = shared_library('foo', 'libfoo.c')
+libbar = shared_library('bar', 'libbar.c', build_rpath: test_rpath, link_with: libfoo)
+libbaz = shared_library('baz', 'libbaz.c')
+test_depends = [libfoo, libbar, libbaz]
+
+libfoo_native = shared_library('native-foo', 'libfoo.c', native: true)
+libbar_native = shared_library('native-bar', 'libbar.c', build_rpath: test_rpath, link_with: libfoo_native, native: true)
+libbaz_native = shared_library('native-baz', 'libbaz.c', native: true)
+test_native_depends = [libfoo_native, libbar_native, libbaz_native]
diff --git a/lib/mlibc/tests/rtdl/scope2/test.c b/lib/mlibc/tests/rtdl/scope2/test.c
new file mode 100644
index 0000000..f4f42dc
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/scope2/test.c
@@ -0,0 +1,36 @@
+#include <stddef.h>
+#include <dlfcn.h>
+#include <assert.h>
+#include <string.h>
+#include <stdio.h>
+
+#ifdef USE_HOST_LIBC
+#define LIBBAR "libnative-bar.so"
+#define LIBBAZ "libnative-baz.so"
+#else
+#define LIBBAR "libbar.so"
+#define LIBBAZ "libbaz.so"
+#endif
+
+typedef char *strfn(void);
+
+int main() {
+ void *baz = dlopen(LIBBAZ, RTLD_LAZY | RTLD_GLOBAL);
+ assert(baz);
+
+ // At this point, baz is loaded in the global scope. When we load bar locally,
+ // there is a relocation to `foo_baz_conflict` which is defined in both
+ // foo (which is a dependency of bar), and baz. In this case baz should win
+ // since we search the global scope first.
+
+ void *bar = dlopen(LIBBAR, RTLD_LAZY | RTLD_LOCAL);
+ assert(bar);
+
+ strfn *bfn = dlsym(bar, "bar_calls_foo_baz_conflict");
+ assert(!strcmp(bfn(), "resolved to baz"));
+
+ // TODO: Test RTLD_DEEPBIND and DT_SYMBOLIC once we implement it.
+
+ dlclose(bar);
+ dlclose(baz);
+}
diff --git a/lib/mlibc/tests/rtdl/scope3/libbar.c b/lib/mlibc/tests/rtdl/scope3/libbar.c
new file mode 100644
index 0000000..dc377b6
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/scope3/libbar.c
@@ -0,0 +1,8 @@
+int g = 1;
+
+int call_foo();
+
+int call_bar() {
+ return call_foo();
+}
+
diff --git a/lib/mlibc/tests/rtdl/scope3/libbaz.c b/lib/mlibc/tests/rtdl/scope3/libbaz.c
new file mode 100644
index 0000000..32524cc
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/scope3/libbaz.c
@@ -0,0 +1,7 @@
+int g = 2;
+
+int call_foo();
+
+int call_baz() {
+ return call_foo();
+}
diff --git a/lib/mlibc/tests/rtdl/scope3/libfoo.c b/lib/mlibc/tests/rtdl/scope3/libfoo.c
new file mode 100644
index 0000000..bc86319
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/scope3/libfoo.c
@@ -0,0 +1,5 @@
+int g = 0;
+
+int call_foo() {
+ return g;
+}
diff --git a/lib/mlibc/tests/rtdl/scope3/meson.build b/lib/mlibc/tests/rtdl/scope3/meson.build
new file mode 100644
index 0000000..0c98583
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/scope3/meson.build
@@ -0,0 +1,9 @@
+libfoo = shared_library('foo', 'libfoo.c')
+libbar = shared_library('bar', 'libbar.c', build_rpath: test_rpath, link_with: libfoo)
+libbaz = shared_library('baz', 'libbaz.c', build_rpath: test_rpath, link_with: libfoo)
+test_depends = [libfoo, libbar, libbaz]
+
+libfoo_native = shared_library('native-foo', 'libfoo.c', native: true)
+libbar_native = shared_library('native-bar', 'libbar.c', build_rpath: test_rpath, link_with: libfoo_native, native: true)
+libbaz_native = shared_library('native-baz', 'libbaz.c', build_rpath: test_rpath, link_with: libfoo_native, native: true)
+test_native_depends = [libfoo_native, libbar_native, libbaz_native]
diff --git a/lib/mlibc/tests/rtdl/scope3/test.c b/lib/mlibc/tests/rtdl/scope3/test.c
new file mode 100644
index 0000000..30fc662
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/scope3/test.c
@@ -0,0 +1,39 @@
+#include <dlfcn.h>
+#include <stdio.h>
+#include <assert.h>
+
+#ifdef USE_HOST_LIBC
+#define LIBBAR "libnative-bar.so"
+#define LIBBAZ "libnative-baz.so"
+#else
+#define LIBBAR "libbar.so"
+#define LIBBAZ "libbaz.so"
+#endif
+
+int main() {
+ // In this test, we have bar -> foo and baz -> foo (where -> means 'depends on').
+ // All three objects contain a definition of a symbol g. Bar calls into foo to retrieve
+ // what foo thinks g is, but since bar appears earlier in the scope than foo, bar's copy
+ // of g wins.
+ //
+ // Next, we load baz, which is identical to bar. When baz calls into foo to retrieve g,
+ // foo still sees bar's definition of g, so bar's copy of g wins.
+ //
+ // Swapping the load order of bar and baz should therefore change the value of g which
+ // foo sees. This behaviour is why dlmopen exists. If we ever implement that, we should
+ // write a similar test and assert that the calls return different results.
+
+ void *libbar = dlopen(LIBBAR, RTLD_LAZY | RTLD_LOCAL);
+ int (*call_bar)(void) = dlsym(libbar, "call_bar");
+ printf("call_bar: %d\n", call_bar());
+ assert(call_bar() == 1);
+
+ void *libbaz = dlopen(LIBBAZ, RTLD_LAZY | RTLD_LOCAL);
+ int (*call_baz)(void) = dlsym(libbaz, "call_baz");
+ printf("call_baz: %d\n", call_baz());
+ assert(call_baz() == 1);
+
+
+ dlclose(libbar);
+ dlclose(libbaz);
+}
diff --git a/lib/mlibc/tests/rtdl/scope4/libbar.c b/lib/mlibc/tests/rtdl/scope4/libbar.c
new file mode 100644
index 0000000..514e456
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/scope4/libbar.c
@@ -0,0 +1,3 @@
+// Bar needs to have a relocation against foo in order to set DT_NEEDED.
+void foo(void);
+void bar() { foo(); }
diff --git a/lib/mlibc/tests/rtdl/scope4/libbaz.c b/lib/mlibc/tests/rtdl/scope4/libbaz.c
new file mode 100644
index 0000000..256a0e3
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/scope4/libbaz.c
@@ -0,0 +1 @@
+void baz() {}
diff --git a/lib/mlibc/tests/rtdl/scope4/libfoo.c b/lib/mlibc/tests/rtdl/scope4/libfoo.c
new file mode 100644
index 0000000..6710db7
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/scope4/libfoo.c
@@ -0,0 +1,3 @@
+// Foo needs to have a relocation against baz in order to set DT_NEEDED.
+void baz(void);
+void foo() { baz(); }
diff --git a/lib/mlibc/tests/rtdl/scope4/meson.build b/lib/mlibc/tests/rtdl/scope4/meson.build
new file mode 100644
index 0000000..804a40c
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/scope4/meson.build
@@ -0,0 +1,9 @@
+libbaz = shared_library('baz', 'libbaz.c')
+libfoo = shared_library('foo', 'libfoo.c', build_rpath: test_rpath, link_with: libbaz)
+libbar = shared_library('bar', 'libbar.c', build_rpath: test_rpath, link_with: libfoo)
+test_depends = [libfoo, libbar, libbaz]
+
+libbaz_native = shared_library('native-baz', 'libbaz.c', native: true)
+libfoo_native = shared_library('native-foo', 'libfoo.c', build_rpath: test_rpath, link_with: libbaz_native, native: true)
+libbar_native = shared_library('native-bar', 'libbar.c', build_rpath: test_rpath, link_with: libfoo_native, native: true)
+test_native_depends = [libfoo_native, libbar_native, libbaz_native]
diff --git a/lib/mlibc/tests/rtdl/scope4/test.c b/lib/mlibc/tests/rtdl/scope4/test.c
new file mode 100644
index 0000000..2365e26
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/scope4/test.c
@@ -0,0 +1,36 @@
+#include <dlfcn.h>
+#include <stdio.h>
+#include <assert.h>
+
+#ifdef USE_HOST_LIBC
+#define LIBFOO "libnative-foo.so"
+#define LIBBAR "libnative-bar.so"
+#define LIBBAZ "libnative-baz.so"
+#else
+#define LIBFOO "libfoo.so"
+#define LIBBAR "libbar.so"
+#define LIBBAZ "libbaz.so"
+#endif
+
+int main() {
+ // In this test, we have foo -> baz, bar -> foo (where '->' means 'depends on').
+ // We first load foo with RTLD_LOCAL, and then load bar with RTLD_GLOBAL.
+ // This should bring foo and bar into the global scope.
+
+ void *foo = dlopen(LIBFOO, RTLD_LOCAL | RTLD_NOW);
+ assert(foo);
+ assert(dlsym(foo, "foo"));
+ assert(dlsym(foo, "baz"));
+ assert(!dlsym(RTLD_DEFAULT, "foo"));
+ assert(!dlsym(RTLD_DEFAULT, "baz"));
+
+ void *bar = dlopen(LIBBAR, RTLD_GLOBAL | RTLD_NOW);
+ assert(bar);
+ assert(dlsym(bar, "bar"));
+ assert(dlsym(RTLD_DEFAULT, "bar"));
+ assert(dlsym(RTLD_DEFAULT, "foo"));
+ assert(dlsym(RTLD_DEFAULT, "baz"));
+
+ dlclose(foo);
+ dlclose(bar);
+}
diff --git a/lib/mlibc/tests/rtdl/scope5/libfoo.c b/lib/mlibc/tests/rtdl/scope5/libfoo.c
new file mode 100644
index 0000000..85e6cd8
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/scope5/libfoo.c
@@ -0,0 +1 @@
+void foo() {}
diff --git a/lib/mlibc/tests/rtdl/scope5/meson.build b/lib/mlibc/tests/rtdl/scope5/meson.build
new file mode 100644
index 0000000..22ade5b
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/scope5/meson.build
@@ -0,0 +1,5 @@
+libfoo = shared_library('foo', 'libfoo.c')
+test_link_with = [libfoo]
+
+libfoo_native = shared_library('native-foo', 'libfoo.c', native: true)
+test_native_link_with = [libfoo_native]
diff --git a/lib/mlibc/tests/rtdl/scope5/test.c b/lib/mlibc/tests/rtdl/scope5/test.c
new file mode 100644
index 0000000..2c11c2e
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/scope5/test.c
@@ -0,0 +1,27 @@
+#include <dlfcn.h>
+#include <stdio.h>
+#include <assert.h>
+
+#ifdef USE_HOST_LIBC
+#define LIBFOO "libnative-foo.so"
+#else
+#define LIBFOO "libfoo.so"
+#endif
+
+// We need to have a relocation against foo for DT_NEEDED.
+void foo();
+void bar() { foo(); }
+
+int main() {
+ // In this test, we have exec -> foo (where '->' means 'depends on').
+ // This means that foo is in the global scope due to DT_NEEDED.
+ // We then dlopen it again with RTLD_LOCAL, which should just return
+ // the already-loaded object, but used to crash in the mlibc linker instead.
+
+ void *foo = dlopen(LIBFOO, RTLD_LOCAL | RTLD_NOW);
+ assert(foo);
+ assert(dlsym(foo, "foo"));
+ assert(dlsym(RTLD_DEFAULT, "foo"));
+
+ dlclose(foo);
+}
diff --git a/lib/mlibc/tests/rtdl/soname/libbar.c b/lib/mlibc/tests/rtdl/soname/libbar.c
new file mode 100644
index 0000000..7f9f475
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/soname/libbar.c
@@ -0,0 +1 @@
+char *name() { return "bar"; }
diff --git a/lib/mlibc/tests/rtdl/soname/libfoo.c b/lib/mlibc/tests/rtdl/soname/libfoo.c
new file mode 100644
index 0000000..abe0999
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/soname/libfoo.c
@@ -0,0 +1 @@
+char *name() { return "foo"; }
diff --git a/lib/mlibc/tests/rtdl/soname/meson.build b/lib/mlibc/tests/rtdl/soname/meson.build
new file mode 100644
index 0000000..1eb97a8
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/soname/meson.build
@@ -0,0 +1,17 @@
+# Create two libraries with the same SONAME.
+bar_soname = '-Wl,-soname=libbar.so'
+
+libfoo = shared_library('foo', 'libfoo.c', link_args: '-Wl,-soname=libbar.so')
+libbar = shared_library('bar', 'libbar.c', link_args: '-Wl,-soname=libbar.so')
+test_depends = [libfoo, libbar]
+
+libfoo_native = shared_library('native-foo', 'libfoo.c',
+ link_args: ['-ldl', '-Wl,-soname=libnative-bar.so'],
+ native: true
+)
+libbar_native = shared_library('native-bar', 'libbar.c',
+ link_args: ['-ldl', '-Wl,-soname=libnative-bar.so'],
+ native: true
+)
+test_native_depends = [libfoo_native, libbar_native]
+
diff --git a/lib/mlibc/tests/rtdl/soname/test.c b/lib/mlibc/tests/rtdl/soname/test.c
new file mode 100644
index 0000000..23e5b7a
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/soname/test.c
@@ -0,0 +1,33 @@
+#include <dlfcn.h>
+#include <stdio.h>
+#include <assert.h>
+#include <string.h>
+
+#ifdef USE_HOST_LIBC
+#define LIBFOO "libnative-foo.so"
+#define LIBBAR "libnative-bar.so"
+#else
+#define LIBFOO "libfoo.so"
+#define LIBBAR "libbar.so"
+#endif
+
+int main() {
+ void *foo = dlopen(LIBFOO, RTLD_NOW);
+ void *bar = dlopen(LIBBAR, RTLD_NOW);
+ assert(foo);
+ assert(bar);
+
+ // Since these libraries have the same SONAME, they should return the same thing.
+ assert(foo == bar);
+
+ char *(*fooSym)(void) = dlsym(foo, "name");
+ char *(*barSym)(void) = dlsym(bar, "name");
+ assert(fooSym && barSym);
+ assert(fooSym() && barSym());
+ printf("foo: name() = \"%s\"\n", fooSym());
+ printf("bar: name() = \"%s\"\n", barSym());
+ assert(!strcmp(fooSym(), barSym()));
+
+ dlclose(foo);
+ dlclose(bar);
+}
diff --git a/lib/mlibc/tests/rtdl/tls_align/libbar.c b/lib/mlibc/tests/rtdl/tls_align/libbar.c
new file mode 100644
index 0000000..3f8d6a7
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/tls_align/libbar.c
@@ -0,0 +1 @@
+_Thread_local __attribute__((aligned(8))) char bar_thread_local[8] = "Hello!";
diff --git a/lib/mlibc/tests/rtdl/tls_align/libfoo.c b/lib/mlibc/tests/rtdl/tls_align/libfoo.c
new file mode 100644
index 0000000..8d98177
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/tls_align/libfoo.c
@@ -0,0 +1 @@
+_Thread_local __attribute__((aligned(16))) char foo_thread_local[8] = "Hello!";
diff --git a/lib/mlibc/tests/rtdl/tls_align/meson.build b/lib/mlibc/tests/rtdl/tls_align/meson.build
new file mode 100644
index 0000000..61d0fd9
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/tls_align/meson.build
@@ -0,0 +1,7 @@
+libfoo = shared_library('foo', 'libfoo.c')
+libbar = shared_library('bar', 'libbar.c')
+test_link_with = [libfoo, libbar]
+
+libfoo_native = shared_library('native-foo', 'libfoo.c', native: true)
+libbar_native = shared_library('native-bar', 'libbar.c', native: true)
+test_native_link_with = [libfoo_native, libbar_native]
diff --git a/lib/mlibc/tests/rtdl/tls_align/test.c b/lib/mlibc/tests/rtdl/tls_align/test.c
new file mode 100644
index 0000000..4381831
--- /dev/null
+++ b/lib/mlibc/tests/rtdl/tls_align/test.c
@@ -0,0 +1,10 @@
+#include <assert.h>
+#include <stdint.h>
+
+extern _Thread_local char foo_thread_local[];
+extern _Thread_local char bar_thread_local[];
+
+int main() {
+ assert(!((uintptr_t)foo_thread_local & (16 - 1)));
+ assert(!((uintptr_t)bar_thread_local & (8 - 1)));
+}