[glibc/azanella/pointer-guard-hardening] elf: Clear ld.so's pointer guard copy after libc relocation

Adhemerval Zanella via Glibc-cvs <[email protected]> Fri, 29 May 2026 20:25:26 +0000 (GMT)
Newsgroups gmane.comp.lib.glibc.cvs
Message-ID <[email protected]>
https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=e44bbd943b0e77896e4ea16054a326490f8136ed

commit e44bbd943b0e77896e4ea16054a326490f8136ed
Author: Adhemerval Zanella <[email protected]>
Date:   Fri May 29 17:21:59 2026 -0300

    elf: Clear ld.so's pointer guard copy after libc relocation
    
    When the pointer guard is kept in the thread descriptor, ld.so still
    mangles its own setjmp/longjmp jmp_bufs with the global
    __pointer_chk_guard_local, because the dynamic linker uses setjmp before
    the TCB is initialized.  That leaves a second copy of the process-wide
    pointer guard at a fixed address inside ld.so (information leakage).
    
    Once libc.so has been relocated, switch the dynamic linker's exception
    handling to the libc setjmp/longjmp (which read the guard from the TCB)
    and clear __pointer_chk_guard_local.  This mirrors the minimal-malloc to
    libc-malloc handoff: __sigsetjmp/__longjmp are reached through RELRO
    function pointers via inline wrappers in <rtld-setjmp.h>, so dl-catch.c
    is unchanged, and the lookup reuses dl-minimal.c's libc-symbol binding.
    
    The handoff is opt-in per target via RTLD_USE_LIBC_SETJMP, defined in
    pointer_guard.h (only enable on x86_64 for now).
    
    Checked on x86_64-linux-gnu.

Diff:
---
 elf/dl-minimal.c                               | 58 +++++++++++++++++-----
 elf/rtld.c                                     | 13 +++++
 include/rtld-setjmp.h                          | 66 ++++++++++++++++++++++++++
 include/setjmp.h                               |  5 ++
 sysdeps/unix/sysv/linux/x86_64/pointer_guard.h |  5 +-
 5 files changed, 135 insertions(+), 12 deletions(-)

diff --git a/elf/dl-minimal.c b/elf/dl-minimal.c
index 591d49e3f5..cca42160a7 100644
--- a/elf/dl-minimal.c
+++ b/elf/dl-minimal.c
@@ -23,6 +23,7 @@
 #include <dl-sym-post.h>
 #include <_itoa.h>
 #include <dl-minimal-malloc.h>
+#include <rtld-setjmp.h>
 #include <stdio.h>
 #include <unistd.h>
 #include <errno.h>
@@ -54,12 +55,13 @@ __rtld_malloc_is_complete (void)
   return __rtld_malloc != &__minimal_malloc;
 }
 
-/* Lookup NAME at VERSION in the scope of MATCH.  */
+/* Lookup NAME in the scope of MAIN_MAP at VERSION, or at the default
+   (current) version if VERSION is NULL.  Used to bind the rtld
+   function-pointer handoffs to the real libc.so implementations.  */
 static void *
-lookup_malloc_symbol (struct link_map *main_map, const char *name,
-		      struct r_found_version *version)
+lookup_libc_symbol (struct link_map *main_map, const char *name,
+		    struct r_found_version *version)
 {
-
   const ElfW(Sym) *ref = NULL;
   lookup_t result = _dl_lookup_symbol_x (name, main_map, &ref,
 					 main_map->l_scope,
@@ -88,21 +90,55 @@ __rtld_malloc_init_real (struct link_map *main_map)
   version.hash = _dl_elf_hash (version.name);
   version.filename = NULL;
 
-  void *new_calloc = lookup_malloc_symbol (main_map, "calloc", &version);
-  void *new_free = lookup_malloc_symbol (main_map, "free", &version);
-  void *new_malloc = lookup_malloc_symbol (main_map, "malloc", &version);
-  void *new_realloc = lookup_malloc_symbol (main_map, "realloc", &version);
+  void *new_calloc = lookup_libc_symbol (main_map, "calloc", &version);
+  void *new_free = lookup_libc_symbol (main_map, "free", &version);
+  void *new_malloc = lookup_libc_symbol (main_map, "malloc", &version);
+  void *new_realloc = lookup_libc_symbol (main_map, "realloc", &version);
 
   /* Update the pointers in one go, so that any internal allocations
-     performed by lookup_malloc_symbol see a consistent
-     implementation.  */
+     performed by lookup_libc_symbol see a consistent implementation.  */
   __rtld_calloc = new_calloc;
   __rtld_free = new_free;
   __rtld_malloc = new_malloc;
   __rtld_realloc = new_realloc;
 }
 
-
+#ifdef RTLD_USE_LIBC_SETJMP
+__typeof (__sigsetjmp) *__rtld_setjmp attribute_relro;
+__rtld_longjmp_t *__rtld_longjmp attribute_relro;
+
+/* Bind a distinct name to the real (asm) symbol so it is called directly.  */
+extern __typeof (__longjmp) __rtld_real_longjmp
+  __asm__ (__SYMBOL_PREFIX "__longjmp") attribute_hidden;
+
+/* ld.so provides __longjmp (operating on the inner __jmp_buf) but not the
+   _longjmp wrapper used for the libc.so implementation, so adapt it.  */
+static void __attribute__ ((__noreturn__))
+rtld_local_longjmp (struct __jmp_buf_tag env[1], int val)
+{
+  __rtld_real_longjmp (env[0].__jmpbuf, val);
+  __builtin_unreachable ();
+}
+
+void
+__rtld_setjmp_init_stubs (void)
+{
+  __rtld_setjmp = &__sigsetjmp;
+  __rtld_longjmp = &rtld_local_longjmp;
+}
+
+void
+__rtld_setjmp_init_real (struct link_map *main_map)
+{
+  void *new_setjmp = lookup_libc_symbol (main_map, "__sigsetjmp", NULL);
+  void *new_longjmp = lookup_libc_symbol (main_map, "_longjmp", NULL);
+
+  __rtld_setjmp = new_setjmp;
+  __rtld_longjmp = new_longjmp;
+}
+#endif /* RTLD_USE_LIBC_SETJMP */
+
+
 /* Avoid signal frobnication in setjmp/longjmp.  Keeps things smaller.  */
 
 #include <setjmp.h>
diff --git a/elf/rtld.c b/elf/rtld.c
index 12e1b4dd71..1980ca74f3 100644
--- a/elf/rtld.c
+++ b/elf/rtld.c
@@ -52,6 +52,7 @@
 #include <dl-find_object.h>
 #include <dl-audit-check.h>
 #include <dl-call_tls_init_tp.h>
+#include <rtld-setjmp.h>
 
 #include <assert.h>
 
@@ -452,6 +453,9 @@ _dl_start_final (void *arg, struct dl_start_final_info *info)
   ElfW(Addr) start_addr;
 
   __rtld_malloc_init_stubs ();
+#ifdef RTLD_USE_LIBC_SETJMP
+  __rtld_setjmp_init_stubs ();
+#endif
 
   /* Do not use an initializer for these members because it would
      interfere with __rtld_static_init.  */
@@ -2351,6 +2355,15 @@ dl_main (const ElfW(Phdr) *phdr,
       __rtld_mutex_init ();
       __rtld_malloc_init_real (main_map);
 
+#ifdef RTLD_USE_LIBC_SETJMP
+      /* Switch the dynamic linker's exception-handling setjmp/longjmp to the
+	 libc.so implementations, which mangle jmp_bufs with the pointer guard
+	 in the thread descriptor.  After this the ld.so-local copy is no
+	 longer used, so clear it.  */
+      __rtld_setjmp_init_real (main_map);
+      __pointer_chk_guard_local = 0;
+#endif
+
       /* Update copy-relocated _r_debug if necessary.  */
       _dl_debug_post_relocate (main_map);
     }
diff --git a/include/rtld-setjmp.h b/include/rtld-setjmp.h
new file mode 100644
index 0000000000..e39797e986
--- /dev/null
+++ b/include/rtld-setjmp.h
@@ -0,0 +1,66 @@
+/* Redirection of setjmp/longjmp inside the dynamic linker.
+   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/>.  */
+
+/* Some targets mangle ld.so's own setjmp/longjmp jmp_bufs with the global
+   __pointer_chk_guard_local, because the dynamic linker's first uses of
+   setjmp happen before the TCB is set up (see pointer_guard.h and the
+   analysis of dl_main), while libc.so and the application mangle with the
+   copy in the thread descriptor.  On such targets __pointer_chk_guard_local
+   is a second copy of the process-wide pointer guard.  */
+
+#ifndef _RTLD_SETJMP_H
+#define _RTLD_SETJMP_H
+
+#if IS_IN (rtld)
+# include <pointer_guard.h> /* RTLD_USE_LIBC_SETJMP */
+#endif
+
+#ifdef RTLD_USE_LIBC_SETJMP
+
+# include <setjmp.h>
+
+extern __typeof (__sigsetjmp) *__rtld_setjmp attribute_hidden;
+typedef void __rtld_longjmp_t (struct __jmp_buf_tag __env[1], int __val);
+extern __rtld_longjmp_t *__rtld_longjmp attribute_hidden;
+
+__extern_inline int __attribute__ ((__returns_twice__))
+__sigsetjmp (struct __jmp_buf_tag __env[1], int __savemask)
+{
+  return __rtld_setjmp (__env, __savemask);
+}
+
+__extern_inline void __attribute__ ((__noreturn__))
+__longjmp (__jmp_buf __env, int __val)
+{
+  __rtld_longjmp ((struct __jmp_buf_tag *) (void *) __env, __val);
+  __builtin_unreachable ();
+}
+
+/* Activate the ld.so-local setjmp/longjmp.  Called after the first
+   self-relocation, before any use of the exception handling.  */
+void __rtld_setjmp_init_stubs (void) attribute_hidden;
+
+/* Switch to the libc.so setjmp/longjmp.  Called while the RELRO variables
+   are still writable, after libc.so has been relocated.  MAIN_MAP is the
+   link map of the executable.  */
+struct link_map;
+void __rtld_setjmp_init_real (struct link_map *main_map) attribute_hidden;
+
+#endif /* RTLD_USE_LIBC_SETJMP */
+
+#endif /* _RTLD_SETJMP_H */
diff --git a/include/setjmp.h b/include/setjmp.h
index 4997d0d7d2..2f4f126ca5 100644
--- a/include/setjmp.h
+++ b/include/setjmp.h
@@ -39,6 +39,11 @@ libc_hidden_proto (__sigsetjmp)
 extern __typeof (__sigsetjmp) __sigsetjmp attribute_hidden;
 # endif
 
+/* In the dynamic linker, redirect __sigsetjmp/__longjmp through the
+   setjmp/longjmp handoff pointers (analogous to how <stdlib.h> pulls in the
+   malloc redirection from <rtld-malloc.h>).  */
+# include <rtld-setjmp.h>
+
 #endif
 
 #endif
diff --git a/sysdeps/unix/sysv/linux/x86_64/pointer_guard.h b/sysdeps/unix/sysv/linux/x86_64/pointer_guard.h
index 4f7abc64f4..525f7cf0f7 100644
--- a/sysdeps/unix/sysv/linux/x86_64/pointer_guard.h
+++ b/sysdeps/unix/sysv/linux/x86_64/pointer_guard.h
@@ -24,7 +24,10 @@
 
 #if IS_IN (rtld)
 /* We cannot use the thread descriptor because in ld.so we use setjmp
-   earlier than the descriptor is initialized.  */
+   earlier than the descriptor is initialized.  Also, enable the use of
+   libc function after libc relocation to avoid the need to duplicate the
+   __pointer_chk_guard value on __pointer_chk_guard_local.  */
+# define RTLD_USE_LIBC_SETJMP	1
 # ifdef __ASSEMBLER__
 #  define PTR_MANGLE(reg)	xor __pointer_chk_guard_local(%rip), reg;     \
 				rol $2*LP_SIZE+1, reg