When a program explicitly links against the vDSO by name (e.g.
-l:linux-vdso.so.1), the dynamic linker exhibits two problems:
1. Symbol resolution always binds to the kernel's implicit vDSO rather
than the explicitly provided stub, because _dl_lookup_map returns the
already-loaded kernel vDSO when the soname matches.
2. Depending on the link order, startup crashes with an assertion:
Inconsistency detected by ld.so: rtld.c: dl_main:
Assertion `_dl_rtld_map.l_prev->l_next == _dl_rtld_map.l_next'
This happens because the kernel vDSO is inserted early in the link
map (right after the interpreter), so reusing it as a DT_NEEDED
dependency places it at a search-list position inconsistent with its
chain position. When rtld.c later re-inserts the interpreter into
the chain at symbol-search order, the adjacency invariant it relies
on no longer holds.
Both problems stem from the same root cause: _dl_lookup_map matching
the kernel vDSO when a DSO asks for it by name.
The fix adds lt_vdso as a new value in the l_type enum of struct
link_map, replacing the former __RTLD_VDSO mode flag. The kernel-mapped
vDSO link map is created with type lt_vdso (via setup-vdso.h), and
_dl_lookup_map skips such objects during name resolution. With the
implicit vDSO invisible to name resolution, an explicit dependency
triggers a fresh filesystem load of the stub at its proper position in
the chain, and the rtld reinsertion logic sees a consistent adjacency.
The audit allocation in _dl_new_object previously relied on __RTLD_VDSO
to allocate DL_NNS audit slots for the vDSO (needed before the audit
count is known). This is now handled by checking type == lt_vdso
directly.
The _dlfo_process_initial function in dl-find_object.c is updated to
treat lt_vdso link maps the same as lt_library (implicitly NODELETE),
preserving dladdr() behavior on vDSO addresses.
The two new tests check:
- tst-vdso-1: links against the stub with a normal dependency order
(-l:linux-vdso.so.1 before libc). Verifies that the stub's symbol
is resolved instead of the kernel vDSO.
- tst-vdso-2: uses a special link order (-lc -l:linux-vdso.so.1
-l:ld.so) that reproduces the exact BZ 33335 crash without the fix.
The tests are enabled for all Linux architectures that define
vdso-name (set by each arch's sysdep Makeconfig).
Checked on x86_64-linux-gnu, i686-linux-gnu, aarch64-linux-gnu,
powerpc-linux-gnu, and powerpc64-linux-gnu.
--
Changes from v2:
* Replace __RTLD_VDSO usage by lt_vdso.
* Fix _dlfo_process_initial to also considere lt_vdso.
* Move vdso-name to arch-specific Makeconfig.
Changes from v1:
* Use a bit from l_type instead of adding a new member at link_map.
* Move tests to sysdeps/unix/sysv/linux/.
---
elf/Makefile | 39 +++++++++++++++++++
elf/dl-find_object.c | 5 ++-
elf/dl-load.c | 8 +++-
elf/dl-object.c | 2 +-
elf/setup-vdso.h | 4 +-
elf/tst-vdso-1.c | 1 +
elf/tst-vdso-2.c | 1 +
elf/tst-vdso.c | 34 ++++++++++++++++
include/dlfcn.h | 2 -
include/link.h | 3 +-
sysdeps/unix/sysv/linux/aarch64/Makeconfig | 1 +
sysdeps/unix/sysv/linux/arm/Makeconfig | 1 +
sysdeps/unix/sysv/linux/hppa/Makeconfig | 1 +
sysdeps/unix/sysv/linux/i386/Makeconfig | 1 +
sysdeps/unix/sysv/linux/loongarch/Makeconfig | 1 +
sysdeps/unix/sysv/linux/mips/Makeconfig | 1 +
.../sysv/linux/powerpc/powerpc32/Makeconfig | 1 +
.../sysv/linux/powerpc/powerpc64/Makeconfig | 1 +
sysdeps/unix/sysv/linux/riscv/Makeconfig | 1 +
sysdeps/unix/sysv/linux/s390/Makeconfig | 1 +
.../unix/sysv/linux/sparc/sparc32/Makeconfig | 1 +
.../unix/sysv/linux/sparc/sparc64/Makeconfig | 1 +
sysdeps/unix/sysv/linux/tst-vdso-lib.c | 24 ++++++++++++
sysdeps/unix/sysv/linux/x86_64/Makeconfig | 1 +
24 files changed, 126 insertions(+), 10 deletions(-)
create mode 100644 elf/tst-vdso-1.c
create mode 100644 elf/tst-vdso-2.c
create mode 100644 elf/tst-vdso.c
create mode 100644 sysdeps/unix/sysv/linux/aarch64/Makeconfig
create mode 100644 sysdeps/unix/sysv/linux/arm/Makeconfig
create mode 100644 sysdeps/unix/sysv/linux/hppa/Makeconfig
create mode 100644 sysdeps/unix/sysv/linux/i386/Makeconfig
create mode 100644 sysdeps/unix/sysv/linux/loongarch/Makeconfig
create mode 100644 sysdeps/unix/sysv/linux/mips/Makeconfig
create mode 100644 sysdeps/unix/sysv/linux/powerpc/powerpc32/Makeconfig
create mode 100644 sysdeps/unix/sysv/linux/powerpc/powerpc64/Makeconfig
create mode 100644 sysdeps/unix/sysv/linux/riscv/Makeconfig
create mode 100644 sysdeps/unix/sysv/linux/s390/Makeconfig
create mode 100644 sysdeps/unix/sysv/linux/sparc/sparc32/Makeconfig
create mode 100644 sysdeps/unix/sysv/linux/sparc/sparc64/Makeconfig
create mode 100644 sysdeps/unix/sysv/linux/tst-vdso-lib.c
create mode 100644 sysdeps/unix/sysv/linux/x86_64/Makeconfig
@@ -1452,6 +1452,14 @@ ifeq ($(run-built-tests),yes)
tests-special += $(objpfx)tst-origin.out
endif
+ifneq (,$(vdso-name))
+tests += tst-vdso-1
+
+ifeq ($(run-built-tests),yes)
+tests-special += $(objpfx)tst-vdso-2.out
+endif
+endif
+
include ../Rules
ifeq (yes,$(build-shared))
@@ -3554,3 +3562,34 @@ $(objpfx)tst-dl-debug-exclude.out: tst-dl-debug-exclude.sh \
$(objpfx)tst-recursive-tls > $@; \
$(evaluate-test)
endif
+
+ifneq (,$(vdso-name))
+CFLAGS-tst-vdso-lib.c += $(no-stack-protector)
+$(objpfx)tst-vdso-lib.os: ../sysdeps/unix/sysv/linux/tst-vdso-lib.c $(before-compile)
+ $(compile-command.c) -UMODULE_NAME -DMODULE_NAME=testsuite
+$(objpfx)$(vdso-name).so: $(objpfx)tst-vdso-lib.os
+ $(LINK.o) -nodefaultlibs -Wl,--soname,$(vdso-name).so.1 -shared \
+ -o $@ -B$(csu-objpfx) $(LDFLAGS.so) $<
+$(objpfx)$(vdso-name).so.1: $(objpfx)$(vdso-name).so
+ $(make-link)
+
+# Link tst-vdso-1 with $(vdso-name).so, but without a full path.
+LDFLAGS-tst-vdso-1 += -Wl,-rpath,\$$ORIGIN -L$(subst :, -L,$(rpath-link))
+LDLIBS-tst-vdso-1 += -l:$(vdso-name).so
+$(objpfx)tst-vdso-1: +nolink-deps += $(objpfx)$(vdso-name).so
+$(objpfx)tst-vdso-1: $(objpfx)$(vdso-name).so.1
+
+# The tst-vdso-2 requires a special link rule to put the vDSO on the last
+# tag in the dynamic section (to trigger the circular dependency as
+# described by BZ 33335)
+$(objpfx)tst-vdso-2: +nolink-deps += $(objpfx)$(vdso-name).so
+$(objpfx)tst-vdso-2: $(objpfx)tst-vdso-2.o $(objpfx)$(vdso-name).so.1 $(libsupport)
+ $(LINK.o) -nodefaultlibs -o $@ $< \
+ $(libsupport) $(static-gnulib) $(common-objpfx)libc_nonshared.a \
+ $(link-libc-rpath) \
+ -Wl,-rpath,\$$ORIGIN -L$(subst :, -L,$(rpath-link)) \
+ -lc -l:$(vdso-name).so \
+ -Wl,--dynamic-linker=$(objpfx)ld.so,--no-as-needed $(objpfx)ld.so
+
+$(objpfx)tst-vdso-2.out: $(objpfx)tst-vdso-2
+endif
@@ -517,8 +517,9 @@ _dlfo_process_initial (void)
/* Skip the main map processed above, and proxy maps. */
if (l != main_map && l == l->l_real)
{
- /* lt_library link maps are implicitly NODELETE. */
- if (l->l_type == lt_library || l->l_nodelete_active)
+ /* lt_library and lt_vdso link maps are implicitly NODELETE. */
+ if (l->l_type == lt_library || l->l_type == lt_vdso
+ || l->l_nodelete_active)
{
/* The kernel may have loaded ld.so with gaps. */
if (!l->l_contiguous && is_rtld_link_map (l))
@@ -1904,8 +1904,12 @@ _dl_lookup_map (Lmid_t nsid, const char *name)
{
/* If the requested name matches the soname of a loaded object,
use that object. Elide this check for names that have not
- yet been opened. */
- if (__glibc_unlikely ((l->l_faked | l->l_removed) != 0))
+ yet been opened.
+
+ Also, avoid matching the vDSO; if the DSO requires it, assume it is
+ provided by a real object (rather than the implicitly loaded one). */
+ if (__glibc_unlikely ((l->l_faked | l->l_removed) != 0
+ || l->l_type == lt_vdso))
continue;
if (!_dl_name_match_p (name, l))
{
@@ -59,7 +59,7 @@ _dl_new_object (char *realname, const char *libname, int type,
{
#ifdef SHARED
unsigned int naudit;
- if (__glibc_unlikely ((mode & (__RTLD_OPENEXEC | __RTLD_VDSO)) != 0))
+ if (__glibc_unlikely ((mode & __RTLD_OPENEXEC) != 0 || type == lt_vdso))
{
if (mode & __RTLD_OPENEXEC)
{
@@ -29,8 +29,8 @@ setup_vdso (struct link_map *main_map __attribute__ ((unused)),
better be, since it's read-only and so we couldn't relocate it).
We just want our data structures to describe it as if we had just
mapped and relocated it normally. */
- struct link_map *l = _dl_new_object ((char *) "", "", lt_library, NULL,
- __RTLD_VDSO, LM_ID_BASE);
+ struct link_map *l = _dl_new_object ((char *) "", "", lt_vdso, NULL,
+ 0, LM_ID_BASE);
bool l_addr_set = false;
if (__glibc_likely (l != NULL))
{
new file mode 100644
@@ -0,0 +1 @@
+#include "tst-vdso.c"
new file mode 100644
@@ -0,0 +1 @@
+#include "tst-vdso.c"
new file mode 100644
@@ -0,0 +1,34 @@
+/* Check for explicit vDSO dependency (BZ 33335)
+ Copyright (C) 2026 Free Software Foundation, Inc.
+ This file is part of the GNU C Library.
+
+ The GNU C Library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ The GNU C Library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with the GNU C Library; if not, see
+ <https://www.gnu.org/licenses/>. */
+
+#include <time.h>
+#include <support/check.h>
+
+extern int __kernel_clock_gettime(clockid_t clk_id, struct timespec *tp);
+
+static int
+do_test (void)
+{
+ TEST_COMPARE (__kernel_clock_gettime (CLOCK_REALTIME,
+ &(struct timespec) { 0 }),
+ 42);
+
+ return 0;
+}
+
+#include <support/test-driver.c>
@@ -14,8 +14,6 @@ rtld_hidden_proto (_dl_find_object)
#define __RTLD_AUDIT 0x08000000
#define __RTLD_SECURE 0x04000000 /* Apply additional security checks. */
#define __RTLD_NOIFUNC 0x02000000 /* Suppress calling ifunc functions. */
-#define __RTLD_VDSO 0x01000000 /* Tell _dl_new_object the object is
- system-loaded. */
#define __LM_ID_CALLER -2
@@ -175,7 +175,8 @@ struct link_map
{
lt_executable, /* The main executable program. */
lt_library, /* Library needed by main executable. */
- lt_loaded /* Extra run-time loaded shared object. */
+ lt_loaded, /* Extra run-time loaded shared object. */
+ lt_vdso, /* The vDSO object provided by the kernel. */
} l_type:2;
unsigned int l_dt_relr_ref:1; /* Nonzero if GLIBC_ABI_DT_RELR is
referenced. */
new file mode 100644
@@ -0,0 +1 @@
+vdso-name := linux-vdso
new file mode 100644
@@ -0,0 +1 @@
+vdso-name := linux-vdso
new file mode 100644
@@ -0,0 +1 @@
+vdso-name := linux-vdso
new file mode 100644
@@ -0,0 +1 @@
+vdso-name := linux-gate
new file mode 100644
@@ -0,0 +1 @@
+vdso-name := linux-vdso
new file mode 100644
@@ -0,0 +1 @@
+vdso-name := linux-vdso
new file mode 100644
@@ -0,0 +1 @@
+vdso-name := linux-vdso32
new file mode 100644
@@ -0,0 +1 @@
+vdso-name := linux-vdso64
new file mode 100644
@@ -0,0 +1 @@
+vdso-name := linux-vdso
new file mode 100644
@@ -0,0 +1 @@
+vdso-name := linux-vdso64
new file mode 100644
@@ -0,0 +1 @@
+vdso-name := linux-gate
new file mode 100644
@@ -0,0 +1 @@
+vdso-name := linux-vdso
new file mode 100644
@@ -0,0 +1,24 @@
+/* Check for explicit vDSO dependency (BZ 33335)
+ Copyright (C) 2026 Free Software Foundation, Inc.
+ This file is part of the GNU C Library.
+
+ The GNU C Library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ The GNU C Library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with the GNU C Library; if not, see
+ <https://www.gnu.org/licenses/>. */
+
+#include <time.h>
+
+int __kernel_clock_gettime (clockid_t clk_id, struct timespec *tp)
+{
+ return 42;
+}
new file mode 100644
@@ -0,0 +1 @@
+vdso-name := linux-vdso