[RFC] elf: Implement glibc.elf.map_shared tunable

Message ID lhuld8pxcq3.fsf@oldenburg.str.redhat.com (mailing list archive)
State RFC
Headers
Series [RFC] elf: Implement glibc.elf.map_shared tunable |

Checks

Context Check Description
redhat-pt-bot/TryBot-apply_patch success Patch applied to master at the time it was sent
redhat-pt-bot/TryBot-32bit success Build for i686
linaro-tcwg-bot/tcwg_glibc_build--master-aarch64 warning Skipped because it is an RFC
linaro-tcwg-bot/tcwg_glibc_build--master-arm warning Skipped because it is an RFC

Commit Message

Florian Weimer Sept. 25, 2026, 9:06 p.m. UTC
  This tunable can be used to disable certain forms of run-time
patching of shared objects, such as Linux uprobes.

This could be helpful to valgrind because it needs the unpatched
instruction to work correctly.  It cannot trigger the kernel uprobes
handler for uprobes traps.

We've also occasionally seen run-time patching of glibc and other
components going wrong.  This tunable could provide a quick check that
the direct patching after mprotect permission changes (as implemented in
Unifined Communication X <https://github.com/openucx/ucx>, for example)
is not causing the problem.

Comments?

Thanks,
Florian

---
 elf/Makefile                        |  3 ++
 elf/dl-map-segments.h               | 23 +++++++++--
 elf/dl-object.c                     |  3 ++
 elf/dl-tunables.list                |  5 +++
 elf/tst-elf-map_shared.c            | 76 +++++++++++++++++++++++++++++++++++++
 manual/tunables.texi                | 17 +++++++++
 sysdeps/generic/ldsodefs.h          |  3 ++
 sysdeps/mach/hurd/dl-sysdep.c       |  3 ++
 sysdeps/unix/sysv/linux/dl-sysdep.c |  3 ++
 9 files changed, 133 insertions(+), 3 deletions(-)


base-commit: 3edeb81021feff11a3d0e934d68446f6a67dd1a3
  

Comments

Mark Wielaard Sept. 28, 2026, 11:14 a.m. UTC | #1
Hi Florian,

On Fri, Sep 25, 2026 at 11:06:28PM +0200, Florian Weimer wrote:
> This tunable can be used to disable certain forms of run-time
> patching of shared objects, such as Linux uprobes.
> 
> This could be helpful to valgrind because it needs the unpatched
> instruction to work correctly.  It cannot trigger the kernel uprobes
> handler for uprobes traps.

I have tried to backport it to 2.43 (as fedora 44 has it).  See
attached. I think I got the test wrong because it fails (while it
doesn't fail when applied to trunk).

This seems to work fine with a valgrind patched to always add
GLIBC_TUNABLES=glibc.elf.map_shared=1 (and without the
__libc_setenv_freemem clearenv removal patch).

> We've also occasionally seen run-time patching of glibc and other
> components going wrong.  This tunable could provide a quick check that
> the direct patching after mprotect permission changes (as implemented in
> Unifined Communication X <https://github.com/openucx/ucx>, for example)
> is not causing the problem.

I don't really follow what is happening here, so cannot comment if
this is helpful or not. But this seems to be the part (not adding
PROT_WRITE) that the testcase tests.

> diff --git a/elf/Makefile b/elf/Makefile
> index 307d603ff0..4dc62ce84a 100644
> --- a/elf/Makefile
> +++ b/elf/Makefile
> @@ -463,6 +463,7 @@ tests += \
>    tst-dlopenrpath \
>    tst-dlsym-error \
>    tst-dst-needed-minstack \
> +  tst-elf-map_shared \
>    tst-filterobj \
>    tst-filterobj-dlopen \
>    tst-glibc-hwcaps \

OK, test added alphabetically.

> @@ -3830,3 +3831,5 @@ LDFLAGS-tst-origin-secure-victim += \
>  $(objpfx)tst-origin-secure.out: $(objpfx)tst-origin-secure-victim \
>  			       $(objpfx)libtst-origin-secure-mod.so \
>  			       $(objpfx)tst-origin-secure-evilmod.so
> +
> +tst-elf-map_shared-TUNABLES += glibc.elf.map_shared=1

This part doesn't seem to work in my backport.

> diff --git a/elf/dl-map-segments.h b/elf/dl-map-segments.h
> index 1854fab7dc..ce710fe23f 100644
> --- a/elf/dl-map-segments.h
> +++ b/elf/dl-map-segments.h
> @@ -22,13 +22,29 @@
>  
>  /* Map a segment and align it properly.  */
>  
> +/* Replace MAP_PRIVATE with MAP_SHARED if glibc.elf.map_shared=1 is
> +   active and the mapping is not writable.  */

Note the comment of the next function is now misplaced before the
comment to this function.

> +static inline int
> +_dl_apply_map_shared (int prot, int flags)
> +{
> +#ifdef SHARED
> +  if (_dl_force_map_shared && (prot & PROT_WRITE) == 0)
> +    {
> +      flags &= ~MAP_PRIVATE;
> +      flags |= MAP_SHARED;
> +    }
> +#endif
> +  return flags;
> +}

Remove MAP_PRIVATE, add MAP_SHARED if no PROT_WRITE.

>  static __always_inline ElfW(Addr)
>  _dl_map_segment (const struct loadcmd *c, ElfW(Addr) mappref,
>  		 const size_t maplength, int fd)
>  {
> +  int flags = _dl_apply_map_shared (c->prot, MAP_COPY | MAP_FILE);
>    if (__glibc_likely (c->mapalign <= GLRO(dl_pagesize)))
>      return (ElfW(Addr)) __mmap ((void *) mappref, maplength, c->prot,
> -				MAP_COPY|MAP_FILE, fd, c->mapoff);
> +				flags, fd, c->mapoff);

Note function comment lost, see above.
Uses _dl_apply_map_shared now to set flags.

>    /* If the segment alignment > the page size, allocate enough space to
>       ensure that the segment can be properly aligned.  */
> @@ -45,7 +61,7 @@ _dl_map_segment (const struct loadcmd *c, ElfW(Addr) mappref,
>    ElfW(Addr) map_start_aligned = ALIGN_UP (map_start, c->mapalign);
>    map_start_aligned = (ElfW(Addr)) __mmap ((void *) map_start_aligned,
>  					   maplength, c->prot,
> -					   MAP_COPY|MAP_FILE|MAP_FIXED,
> +					   flags|MAP_FIXED,
>  					   fd, c->mapoff);
>    if (__glibc_unlikely ((void *) map_start_aligned == MAP_FAILED))
>      __munmap ((void *) map_start, maplen);

likewise, but add MAP_FIXED which isn't included in flags above.

> @@ -137,11 +153,12 @@ _dl_map_segments (struct link_map *l, int fd,
>  
>    do
>      {
> +      int flags = _dl_apply_map_shared (c.prot, MAP_COPY | MAP_FILE);
>        if (c.mapend > c.mapstart
>            /* Map the segment contents from the file.  */
>            && (__mmap ((void *) (l->l_addr + c.mapstart),
>                        c.mapend - c.mapstart, c.prot,
> -                      MAP_FIXED|MAP_COPY|MAP_FILE,
> +                      MAP_FIXED|flags,
>                        fd, c.mapoff)
>                == MAP_FAILED))
>          return DL_MAP_SEGMENTS_ERROR_MAP_SEGMENT;

Here MAP_FIXED could be added to flags directly, but matches what is
done in _dl_map_segment.

> diff --git a/elf/dl-object.c b/elf/dl-object.c
> index 8153e5ca28..290b981d53 100644
> --- a/elf/dl-object.c
> +++ b/elf/dl-object.c
> @@ -24,6 +24,9 @@
>  
>  #include <assert.h>
>  
> +#ifdef SHARED
> +bool _dl_force_map_shared;
> +#endif
>  
>  /* Add the new link_map NEW to the end of the namespace list.  */
>  void
> diff --git a/elf/dl-tunables.list b/elf/dl-tunables.list
> index 111649f145..3cd584763a 100644
> --- a/elf/dl-tunables.list
> +++ b/elf/dl-tunables.list
> @@ -30,6 +30,11 @@ glibc {
>        minval: 0
>        maxval: 1
>      }
> +    map_shared {
> +      type: INT_32
> +      minval: 0
> +      maxval: 1
> +    }
>    }

OK, adds map_shared under glibc.elf. as new tunable
  
>    malloc {
> diff --git a/elf/tst-elf-map_shared.c b/elf/tst-elf-map_shared.c
> new file mode 100644
> index 0000000000..83c4f21cc3
> --- /dev/null
> +++ b/elf/tst-elf-map_shared.c
> @@ -0,0 +1,76 @@
> +/* Test that glibc.elf.map_shared=1 blocks mprotect calls on libc.so.6.
> +   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 <dlfcn.h>
> +#include <errno.h>
> +#include <gnu/lib-names.h>
> +#include <stdint.h>
> +#include <string.h>
> +#include <support/check.h>
> +#include <support/xdlfcn.h>
> +#include <support/xunistd.h>
> +#include <sys/mman.h>
> +
> +static int
> +do_test (void)
> +{
> +  void *handle = xdlopen (LIBC_SO, RTLD_NOLOAD | RTLD_LAZY);
> +  void *libc_map;
> +  TEST_COMPARE (dlinfo (handle, RTLD_DI_LINKMAP, &libc_map), 0);
> +
> +  /* An arbitrary symbol that is part of libc.so.6, but is unlikely to
> +     be interposed.  Use dlsym to avoid picking up a PLT stub in the
> +     main program.  */
> +  void *fptr = xdlsym (handle, "accept");
> +
> +  /* A string literal from libc.so should reside in read-only memory.  */
> +  const void *dptr = strerrorname_np (EPERM);
> +  TEST_COMPARE_STRING (dptr, "EPERM");
> +
> +  /* Check that the symbols are indeed in libc.  */
> +  {
> +    Dl_info info;
> +    void *map;
> +    TEST_VERIFY (dladdr1 (fptr, &info, &map, RTLD_DL_LINKMAP));
> +    TEST_VERIFY (map == libc_map);
> +
> +    TEST_VERIFY (dladdr1 (dptr, &info, &map, RTLD_DL_LINKMAP));
> +    TEST_VERIFY (map == libc_map);
> +  }
> +
> +  xdlclose (handle);
> +
> +  long int page_size = xsysconf (_SC_PAGESIZE);
> +  void *page_start;
> +
> +  /* The kernel cannot trigger copy-on-write due to MAP_SHARED, but
> +     the executable was opened in read-only mode.  So the mprotect
> +     cannot succeed because the page cannot be made writable.  */
> +
> +  page_start = (void *) ((uintptr_t) fptr  & -(uintptr_t) page_size);
> +  TEST_COMPARE (mprotect (page_start, page_size, PROT_WRITE), -1);
> +  TEST_COMPARE (errno, EACCES);
> +
> +  page_start = (void *) ((uintptr_t) dptr  & -(uintptr_t) page_size);
> +  TEST_COMPARE (mprotect (page_start, page_size, PROT_WRITE), -1);
> +  TEST_COMPARE (errno, EACCES);
> +
> +  return 0;
> +}

This tests one cannot add PROT_WRITE anymore.

> +#include <support/test-driver.c>
> diff --git a/manual/tunables.texi b/manual/tunables.texi
> index bbff2f5fdd..5f37be4503 100644
> --- a/manual/tunables.texi
> +++ b/manual/tunables.texi
> @@ -889,3 +889,20 @@ This tunable has an effect only on systems that support Transparent
>  Huge Pages (currently Linux).  On other systems, or if THP is disabled
>  at the kernel level, enabling this tunable has no effect.
>  @end deftp
> +
> +@deftp Tunable glibc.elf.map_shared
> +If this tunable is set to @code{1}, the dynamic linker will map
> +non-writable load segments using @code{MAP_SHARED} instead of
> +@code{MAP_PRIVATE}.  As the files have been opened read-only, this
> +prevents using @code{mprotect} with @code{PROT_WRITE} to make these
> +segments writable.
> +
> +Enabling this @code{MAP_SHARED} mode does not prevent other forms of
> +modification, for example using @code{ptrace}, or by mapping over the
> +memory using @code{MAP_FIXED}.  However, on Linux, this mode disables
> +uprobes because these breakpoint traps depend on copy-on-write semantics
> +which are unavailable with @code{MAP_SHARED} mappings.
> +
> +The default value is @code{0}, which means that the dynamic linker uses
> +@code{MAP_PRIVATE} regardless of load segment permissions.
> +@end deftp

Clear docs.

> diff --git a/sysdeps/generic/ldsodefs.h b/sysdeps/generic/ldsodefs.h
> index 305ca6e0df..00a99417f8 100644
> --- a/sysdeps/generic/ldsodefs.h
> +++ b/sysdeps/generic/ldsodefs.h
> @@ -918,6 +918,9 @@ rtld_hidden_proto (_dl_catch_exception)
>  struct link_map *_dl_lookup_map (Lmid_t nsid, const char *name)
>     attribute_hidden;
>  
> +/* If true, the glibc.elf.map_shared=1 tunable was set.  */
> +extern bool _dl_force_map_shared attribute_hidden;
> +
>  /* Open the shared object NAME and map in its segments.
>     LOADER's DT_RPATH is used in searching for NAME.
>     If the object is already opened, returns its existing map.  */
> diff --git a/sysdeps/mach/hurd/dl-sysdep.c b/sysdeps/mach/hurd/dl-sysdep.c
> index 29acbe1c76..2879ee51e1 100644
> --- a/sysdeps/mach/hurd/dl-sysdep.c
> +++ b/sysdeps/mach/hurd/dl-sysdep.c
> @@ -103,6 +103,9 @@ _dl_sysdep_start (void **start_argptr,
>        /* Initialize DSO sorting algorithm after tunables.  */
>        _dl_sort_maps_init ();
>  
> +      _dl_force_map_shared
> +	= TUNABLE_GET_FULL (glibc, elf, map_shared, int32_t, NULL);
> +
>  #ifdef DL_SYSDEP_INIT
>        DL_SYSDEP_INIT;
>  #endif

Is this useful on hurd?

> diff --git a/sysdeps/unix/sysv/linux/dl-sysdep.c b/sysdeps/unix/sysv/linux/dl-sysdep.c
> index c2701f274c..f2d2b27efe 100644
> --- a/sysdeps/unix/sysv/linux/dl-sysdep.c
> +++ b/sysdeps/unix/sysv/linux/dl-sysdep.c
> @@ -112,6 +112,9 @@ _dl_sysdep_start (void **start_argptr,
>    /* Initialize DSO sorting algorithm after tunables.  */
>    _dl_sort_maps_init ();
>  
> +  _dl_force_map_shared
> +    = TUNABLE_GET_FULL (glibc, elf, map_shared, int32_t, NULL);
> +
>    __brk (NULL);			/* Initialize the break.  */
>  
>  #ifdef DL_PLATFORM_INIT

Ack, init tunable early.

Thanks,

Mark
From 4c17befc13982d04b72bc9e133049e464a926c24 Mon Sep 17 00:00:00 2001
From: Florian Weimer <fweimer@redhat.com>
Date: Fri, 25 Sep 2026 23:06:28 +0200
Subject: [PATCH] elf: Implement glibc.elf.map_shared tunable

This tunable can be used to disable certain forms of run-time
patching of shared objects, such as Linux uprobes.

This could be helpful to valgrind because it needs the unpatched
instruction to work correctly.  It cannot trigger the kernel uprobes
handler for uprobes traps.

We've also occasionally seen run-time patching of glibc and other
components going wrong.  This tunable could provide a quick check that
the direct patching after mprotect permission changes (as implemented in
Unifined Communication X <https://github.com/openucx/ucx>, for example)
is not causing the problem.

mjw: Backport to 2.43. Note testcase fails, but testing with valgrind
tweaked to always add GLIBC_TUNABLES=glibc.elf.map_shared=1 works.
---
 elf/Makefile                        |  3 ++
 elf/dl-map-segments.h               | 23 +++++++--
 elf/dl-object.c                     |  3 ++
 elf/dl-tunables.list                |  7 +++
 elf/tst-elf-map_shared.c            | 76 +++++++++++++++++++++++++++++
 manual/tunables.texi                | 17 +++++++
 sysdeps/generic/ldsodefs.h          |  3 ++
 sysdeps/mach/hurd/dl-sysdep.c       |  3 ++
 sysdeps/unix/sysv/linux/dl-sysdep.c |  3 ++
 9 files changed, 135 insertions(+), 3 deletions(-)
 create mode 100644 elf/tst-elf-map_shared.c

diff --git a/elf/Makefile b/elf/Makefile
index 01cc51636eee..07a33b5bccaa 100644
--- a/elf/Makefile
+++ b/elf/Makefile
@@ -434,6 +434,7 @@ tests += \
   tst-dlopenfail-2 \
   tst-dlopenrpath \
   tst-dlsym-error \
+  tst-elf-map_shared \
   tst-filterobj \
   tst-filterobj-dlopen \
   tst-glibc-hwcaps \
@@ -3534,3 +3535,5 @@ $(objpfx)tst-origin.out: tst-origin.sh $(objpfx)tst-origin
 	$(evaluate-test)
 
 $(objpfx)tst-dlopen-sgid.out: $(objpfx)tst-dlopen-sgid-mod.so
+
+tst-elf-map_shared-TUNABLES += glibc.elf.map_shared=1
diff --git a/elf/dl-map-segments.h b/elf/dl-map-segments.h
index 139004ae2f46..5d25b2674864 100644
--- a/elf/dl-map-segments.h
+++ b/elf/dl-map-segments.h
@@ -22,13 +22,29 @@
 
 /* Map a segment and align it properly.  */
 
+/* Replace MAP_PRIVATE with MAP_SHARED if glibc.elf.map_shared=1 is
+   active and the mapping is not writable.  */
+static inline int
+_dl_apply_map_shared (int prot, int flags)
+{
+#ifdef SHARED
+  if (_dl_force_map_shared && (prot & PROT_WRITE) == 0)
+    {
+      flags &= ~MAP_PRIVATE;
+      flags |= MAP_SHARED;
+    }
+#endif
+  return flags;
+}
+
 static __always_inline ElfW(Addr)
 _dl_map_segment (const struct loadcmd *c, ElfW(Addr) mappref,
 		 const size_t maplength, int fd)
 {
+  int flags = _dl_apply_map_shared (c->prot, MAP_COPY | MAP_FILE);
   if (__glibc_likely (c->mapalign <= GLRO(dl_pagesize)))
     return (ElfW(Addr)) __mmap ((void *) mappref, maplength, c->prot,
-				MAP_COPY|MAP_FILE, fd, c->mapoff);
+				flags, fd, c->mapoff);
 
   /* If the segment alignment > the page size, allocate enough space to
      ensure that the segment can be properly aligned.  */
@@ -45,7 +61,7 @@ _dl_map_segment (const struct loadcmd *c, ElfW(Addr) mappref,
   ElfW(Addr) map_start_aligned = ALIGN_UP (map_start, c->mapalign);
   map_start_aligned = (ElfW(Addr)) __mmap ((void *) map_start_aligned,
 					   maplength, c->prot,
-					   MAP_COPY|MAP_FILE|MAP_FIXED,
+					   flags|MAP_FIXED,
 					   fd, c->mapoff);
   if (__glibc_unlikely ((void *) map_start_aligned == MAP_FAILED))
     __munmap ((void *) map_start, maplen);
@@ -135,11 +151,12 @@ _dl_map_segments (struct link_map *l, int fd,
 
   while (c < &loadcmds[nloadcmds])
     {
+      int flags = _dl_apply_map_shared (c->prot, MAP_COPY | MAP_FILE);
       if (c->mapend > c->mapstart
           /* Map the segment contents from the file.  */
           && (__mmap ((void *) (l->l_addr + c->mapstart),
                       c->mapend - c->mapstart, c->prot,
-                      MAP_FIXED|MAP_COPY|MAP_FILE,
+                      MAP_FIXED|flags,
                       fd, c->mapoff)
               == MAP_FAILED))
         return DL_MAP_SEGMENTS_ERROR_MAP_SEGMENT;
diff --git a/elf/dl-object.c b/elf/dl-object.c
index 8153e5ca28e5..290b981d53c2 100644
--- a/elf/dl-object.c
+++ b/elf/dl-object.c
@@ -24,6 +24,9 @@
 
 #include <assert.h>
 
+#ifdef SHARED
+bool _dl_force_map_shared;
+#endif
 
 /* Add the new link_map NEW to the end of the namespace list.  */
 void
diff --git a/elf/dl-tunables.list b/elf/dl-tunables.list
index 5bc5f037920d..83fede352483 100644
--- a/elf/dl-tunables.list
+++ b/elf/dl-tunables.list
@@ -24,6 +24,13 @@
 # env_alias: An alias environment variable
 
 glibc {
+  elf {
+    map_shared {
+      type: INT_32
+      minval: 0
+      maxval: 1
+    }
+  }
   malloc {
     check {
       type: INT_32
diff --git a/elf/tst-elf-map_shared.c b/elf/tst-elf-map_shared.c
new file mode 100644
index 000000000000..83c4f21cc3ef
--- /dev/null
+++ b/elf/tst-elf-map_shared.c
@@ -0,0 +1,76 @@
+/* Test that glibc.elf.map_shared=1 blocks mprotect calls on libc.so.6.
+   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 <dlfcn.h>
+#include <errno.h>
+#include <gnu/lib-names.h>
+#include <stdint.h>
+#include <string.h>
+#include <support/check.h>
+#include <support/xdlfcn.h>
+#include <support/xunistd.h>
+#include <sys/mman.h>
+
+static int
+do_test (void)
+{
+  void *handle = xdlopen (LIBC_SO, RTLD_NOLOAD | RTLD_LAZY);
+  void *libc_map;
+  TEST_COMPARE (dlinfo (handle, RTLD_DI_LINKMAP, &libc_map), 0);
+
+  /* An arbitrary symbol that is part of libc.so.6, but is unlikely to
+     be interposed.  Use dlsym to avoid picking up a PLT stub in the
+     main program.  */
+  void *fptr = xdlsym (handle, "accept");
+
+  /* A string literal from libc.so should reside in read-only memory.  */
+  const void *dptr = strerrorname_np (EPERM);
+  TEST_COMPARE_STRING (dptr, "EPERM");
+
+  /* Check that the symbols are indeed in libc.  */
+  {
+    Dl_info info;
+    void *map;
+    TEST_VERIFY (dladdr1 (fptr, &info, &map, RTLD_DL_LINKMAP));
+    TEST_VERIFY (map == libc_map);
+
+    TEST_VERIFY (dladdr1 (dptr, &info, &map, RTLD_DL_LINKMAP));
+    TEST_VERIFY (map == libc_map);
+  }
+
+  xdlclose (handle);
+
+  long int page_size = xsysconf (_SC_PAGESIZE);
+  void *page_start;
+
+  /* The kernel cannot trigger copy-on-write due to MAP_SHARED, but
+     the executable was opened in read-only mode.  So the mprotect
+     cannot succeed because the page cannot be made writable.  */
+
+  page_start = (void *) ((uintptr_t) fptr  & -(uintptr_t) page_size);
+  TEST_COMPARE (mprotect (page_start, page_size, PROT_WRITE), -1);
+  TEST_COMPARE (errno, EACCES);
+
+  page_start = (void *) ((uintptr_t) dptr  & -(uintptr_t) page_size);
+  TEST_COMPARE (mprotect (page_start, page_size, PROT_WRITE), -1);
+  TEST_COMPARE (errno, EACCES);
+
+  return 0;
+}
+
+#include <support/test-driver.c>
diff --git a/manual/tunables.texi b/manual/tunables.texi
index 7956df919bee..58f6df406693 100644
--- a/manual/tunables.texi
+++ b/manual/tunables.texi
@@ -767,3 +767,20 @@ whose size exceeds the available memory; in that case, an out of memory
 error will be printed at program startup, the profiler will be
 disabled, and no @file{gmon.out} file will be generated.
 @end deftp
+
+@deftp Tunable glibc.elf.map_shared
+If this tunable is set to @code{1}, the dynamic linker will map
+non-writable load segments using @code{MAP_SHARED} instead of
+@code{MAP_PRIVATE}.  As the files have been opened read-only, this
+prevents using @code{mprotect} with @code{PROT_WRITE} to make these
+segments writable.
+
+Enabling this @code{MAP_SHARED} mode does not prevent other forms of
+modification, for example using @code{ptrace}, or by mapping over the
+memory using @code{MAP_FIXED}.  However, on Linux, this mode disables
+uprobes because these breakpoint traps depend on copy-on-write semantics
+which are unavailable with @code{MAP_SHARED} mappings.
+
+The default value is @code{0}, which means that the dynamic linker uses
+@code{MAP_PRIVATE} regardless of load segment permissions.
+@end deftp
diff --git a/sysdeps/generic/ldsodefs.h b/sysdeps/generic/ldsodefs.h
index 0fcc14999c25..f69d46922bc1 100644
--- a/sysdeps/generic/ldsodefs.h
+++ b/sysdeps/generic/ldsodefs.h
@@ -912,6 +912,9 @@ rtld_hidden_proto (_dl_catch_exception)
 struct link_map *_dl_lookup_map (Lmid_t nsid, const char *name)
    attribute_hidden;
 
+/* If true, the glibc.elf.map_shared=1 tunable was set.  */
+extern bool _dl_force_map_shared attribute_hidden;
+
 /* Open the shared object NAME and map in its segments.
    LOADER's DT_RPATH is used in searching for NAME.
    If the object is already opened, returns its existing map.  */
diff --git a/sysdeps/mach/hurd/dl-sysdep.c b/sysdeps/mach/hurd/dl-sysdep.c
index 0e348d644077..ed06c5dcd4b1 100644
--- a/sysdeps/mach/hurd/dl-sysdep.c
+++ b/sysdeps/mach/hurd/dl-sysdep.c
@@ -103,6 +103,9 @@ _dl_sysdep_start (void **start_argptr,
       /* Initialize DSO sorting algorithm after tunables.  */
       _dl_sort_maps_init ();
 
+      _dl_force_map_shared
+	= TUNABLE_GET_FULL (glibc, elf, map_shared, int32_t, NULL);
+
 #ifdef DL_SYSDEP_INIT
       DL_SYSDEP_INIT;
 #endif
diff --git a/sysdeps/unix/sysv/linux/dl-sysdep.c b/sysdeps/unix/sysv/linux/dl-sysdep.c
index cb1f94ee23a7..a431ae9482f4 100644
--- a/sysdeps/unix/sysv/linux/dl-sysdep.c
+++ b/sysdeps/unix/sysv/linux/dl-sysdep.c
@@ -112,6 +112,9 @@ _dl_sysdep_start (void **start_argptr,
   /* Initialize DSO sorting algorithm after tunables.  */
   _dl_sort_maps_init ();
 
+  _dl_force_map_shared
+    = TUNABLE_GET_FULL (glibc, elf, map_shared, int32_t, NULL);
+
   __brk (NULL);			/* Initialize the break.  */
 
 #ifdef DL_PLATFORM_INIT
  
Florian Weimer Sept. 28, 2026, 11:38 a.m. UTC | #2
* Mark Wielaard:

>> We've also occasionally seen run-time patching of glibc and other
>> components going wrong.  This tunable could provide a quick check that
>> the direct patching after mprotect permission changes (as implemented in
>> Unifined Communication X <https://github.com/openucx/ucx>, for example)
>> is not causing the problem.
>
> I don't really follow what is happening here, so cannot comment if
> this is helpful or not. But this seems to be the part (not adding
> PROT_WRITE) that the testcase tests.

Yes, ucx does this to overwrite our mmap implementation.

>> @@ -3830,3 +3831,5 @@ LDFLAGS-tst-origin-secure-victim += \
>>  $(objpfx)tst-origin-secure.out: $(objpfx)tst-origin-secure-victim \
>>  			       $(objpfx)libtst-origin-secure-mod.so \
>>  			       $(objpfx)tst-origin-secure-evilmod.so
>> +
>> +tst-elf-map_shared-TUNABLES += glibc.elf.map_shared=1
>
> This part doesn't seem to work in my backport.

Yes, you have to use

tst-elf-map_shared-ENV += GLIBC_TUNABLES=glibc.elf.map_shared=1

instead.  (*-TUNABLES seems to be a fairly recent addition.)

>> diff --git a/elf/dl-map-segments.h b/elf/dl-map-segments.h
>> index 1854fab7dc..ce710fe23f 100644
>> --- a/elf/dl-map-segments.h
>> +++ b/elf/dl-map-segments.h
>> @@ -22,13 +22,29 @@
>>  
>>  /* Map a segment and align it properly.  */
>>  
>> +/* Replace MAP_PRIVATE with MAP_SHARED if glibc.elf.map_shared=1 is
>> +   active and the mapping is not writable.  */
>
> Note the comment of the next function is now misplaced before the
> comment to this function.

I thought the comment applied to the entire file.

Thanks,
Florian
  
Mark Wielaard Sept. 28, 2026, 1 p.m. UTC | #3
Hi Florian,

On Mon, Sep 28, 2026 at 01:38:13PM +0200, Florian Weimer wrote:
> >> @@ -3830,3 +3831,5 @@ LDFLAGS-tst-origin-secure-victim += \
> >>  $(objpfx)tst-origin-secure.out: $(objpfx)tst-origin-secure-victim \
> >>  			       $(objpfx)libtst-origin-secure-mod.so \
> >>  			       $(objpfx)tst-origin-secure-evilmod.so
> >> +
> >> +tst-elf-map_shared-TUNABLES += glibc.elf.map_shared=1
> >
> > This part doesn't seem to work in my backport.
> 
> Yes, you have to use
> 
> tst-elf-map_shared-ENV += GLIBC_TUNABLES=glibc.elf.map_shared=1
> 
> instead.  (*-TUNABLES seems to be a fairly recent addition.)

Thanks, that works indeed, the test now passes as expected.

> >> diff --git a/elf/dl-map-segments.h b/elf/dl-map-segments.h
> >> index 1854fab7dc..ce710fe23f 100644
> >> --- a/elf/dl-map-segments.h
> >> +++ b/elf/dl-map-segments.h
> >> @@ -22,13 +22,29 @@
> >>  
> >>  /* Map a segment and align it properly.  */
> >>  
> >> +/* Replace MAP_PRIVATE with MAP_SHARED if glibc.elf.map_shared=1 is
> >> +   active and the mapping is not writable.  */
> >
> > Note the comment of the next function is now misplaced before the
> > comment to this function.
> 
> I thought the comment applied to the entire file.

Fair. I didn't know if each function needs a comment.

Cheers,

Mark
  
Florian Weimer Sept. 28, 2026, 1:13 p.m. UTC | #4
* Mark Wielaard:

> Hi Florian,
>
> On Mon, Sep 28, 2026 at 01:38:13PM +0200, Florian Weimer wrote:
>> >> @@ -3830,3 +3831,5 @@ LDFLAGS-tst-origin-secure-victim += \
>> >>  $(objpfx)tst-origin-secure.out: $(objpfx)tst-origin-secure-victim \
>> >>  			       $(objpfx)libtst-origin-secure-mod.so \
>> >>  			       $(objpfx)tst-origin-secure-evilmod.so
>> >> +
>> >> +tst-elf-map_shared-TUNABLES += glibc.elf.map_shared=1
>> >
>> > This part doesn't seem to work in my backport.
>> 
>> Yes, you have to use
>> 
>> tst-elf-map_shared-ENV += GLIBC_TUNABLES=glibc.elf.map_shared=1
>> 
>> instead.  (*-TUNABLES seems to be a fairly recent addition.)
>
> Thanks, that works indeed, the test now passes as expected.
>
>> >> diff --git a/elf/dl-map-segments.h b/elf/dl-map-segments.h
>> >> index 1854fab7dc..ce710fe23f 100644
>> >> --- a/elf/dl-map-segments.h
>> >> +++ b/elf/dl-map-segments.h
>> >> @@ -22,13 +22,29 @@
>> >>  
>> >>  /* Map a segment and align it properly.  */
>> >>  
>> >> +/* Replace MAP_PRIVATE with MAP_SHARED if glibc.elf.map_shared=1 is
>> >> +   active and the mapping is not writable.  */
>> >
>> > Note the comment of the next function is now misplaced before the
>> > comment to this function.
>> 
>> I thought the comment applied to the entire file.
>
> Fair. I didn't know if each function needs a comment.

May I consider this a proper review for inclusion into the glibc
sources?

Thanks,
Florian
  
Mark Wielaard Sept. 28, 2026, 1:43 p.m. UTC | #5
Hi Florian,

On Mon, Sep 28, 2026 at 03:13:23PM +0200, Florian Weimer wrote:
> >> > Note the comment of the next function is now misplaced before the
> >> > comment to this function.
> >> 
> >> I thought the comment applied to the entire file.
> >
> > Fair. I didn't know if each function needs a comment.
> 
> May I consider this a proper review for inclusion into the glibc
> sources?

Please feel free to add

Reviewed-by: Mark Wielaard <mark@klomp.org>

But I only looked at/tested the interaction with valgrind and ebpf
uprobes. Which seems to work as expected. It would be good if someone
gave their opinion on the PROT_WRITE EACCESS "protection" this
adds. Which also works, but I must admit I don't know if I really
understand the bigger picture there.

Cheers,

Mark
  
Adhemerval Zanella Oct. 8, 2026, 4:35 p.m. UTC | #6
On 25/09/26 23:06, Florian Weimer wrote:
> This tunable can be used to disable certain forms of run-time
> patching of shared objects, such as Linux uprobes.
> 
> This could be helpful to valgrind because it needs the unpatched
> instruction to work correctly.  It cannot trigger the kernel uprobes
> handler for uprobes traps.
> 
> We've also occasionally seen run-time patching of glibc and other
> components going wrong.  This tunable could provide a quick check that
> the direct patching after mprotect permission changes (as implemented in
> Unifined Communication X <https://github.com/openucx/ucx>, for example)
> is not causing the problem.
> 
> Comments?

I am not sure about this tunable, this only covers objects ld.so maps and
valgrind (or any tool like USDT and uprobes) already need to wrap all mmpa
anyway to handle loader and the executable itself.

Also it turns silent patching into an EACCES or a crash, which is useful but
at some time tools already cover parts of it: 

 * UCX has its own knobs (UCX_MEM_MMAP_HOOK_MODE=reloc, UCX_MEM_EVENTS=no).
 * strace -e mprotect shows the patching.
 * PR_SET_MDWE / systemd MemoryDenyWriteExecute= block patchers that go through W+X.

So this would be *another* knob and I am not sure how often it would be
useful.

> 
> Thanks,
> Florian
> 
> ---
>  elf/Makefile                        |  3 ++
>  elf/dl-map-segments.h               | 23 +++++++++--
>  elf/dl-object.c                     |  3 ++
>  elf/dl-tunables.list                |  5 +++
>  elf/tst-elf-map_shared.c            | 76 +++++++++++++++++++++++++++++++++++++
>  manual/tunables.texi                | 17 +++++++++
>  sysdeps/generic/ldsodefs.h          |  3 ++
>  sysdeps/mach/hurd/dl-sysdep.c       |  3 ++
>  sysdeps/unix/sysv/linux/dl-sysdep.c |  3 ++
>  9 files changed, 133 insertions(+), 3 deletions(-)
> 
> diff --git a/elf/Makefile b/elf/Makefile
> index 307d603ff0..4dc62ce84a 100644
> --- a/elf/Makefile
> +++ b/elf/Makefile
> @@ -463,6 +463,7 @@ tests += \
>    tst-dlopenrpath \
>    tst-dlsym-error \
>    tst-dst-needed-minstack \
> +  tst-elf-map_shared \
>    tst-filterobj \
>    tst-filterobj-dlopen \
>    tst-glibc-hwcaps \
> @@ -3830,3 +3831,5 @@ LDFLAGS-tst-origin-secure-victim += \
>  $(objpfx)tst-origin-secure.out: $(objpfx)tst-origin-secure-victim \
>  			       $(objpfx)libtst-origin-secure-mod.so \
>  			       $(objpfx)tst-origin-secure-evilmod.so
> +
> +tst-elf-map_shared-TUNABLES += glibc.elf.map_shared=1
> diff --git a/elf/dl-map-segments.h b/elf/dl-map-segments.h
> index 1854fab7dc..ce710fe23f 100644
> --- a/elf/dl-map-segments.h
> +++ b/elf/dl-map-segments.h
> @@ -22,13 +22,29 @@
>  
>  /* Map a segment and align it properly.  */
>  
> +/* Replace MAP_PRIVATE with MAP_SHARED if glibc.elf.map_shared=1 is
> +   active and the mapping is not writable.  */
> +static inline int
> +_dl_apply_map_shared (int prot, int flags)
> +{
> +#ifdef SHARED
> +  if (_dl_force_map_shared && (prot & PROT_WRITE) == 0)
> +    {
> +      flags &= ~MAP_PRIVATE;
> +      flags |= MAP_SHARED;
> +    }
> +#endif
> +  return flags;
> +}
> +
>  static __always_inline ElfW(Addr)
>  _dl_map_segment (const struct loadcmd *c, ElfW(Addr) mappref,
>  		 const size_t maplength, int fd)
>  {
> +  int flags = _dl_apply_map_shared (c->prot, MAP_COPY | MAP_FILE);
>    if (__glibc_likely (c->mapalign <= GLRO(dl_pagesize)))
>      return (ElfW(Addr)) __mmap ((void *) mappref, maplength, c->prot,
> -				MAP_COPY|MAP_FILE, fd, c->mapoff);
> +				flags, fd, c->mapoff);
>  
>    /* If the segment alignment > the page size, allocate enough space to
>       ensure that the segment can be properly aligned.  */
> @@ -45,7 +61,7 @@ _dl_map_segment (const struct loadcmd *c, ElfW(Addr) mappref,
>    ElfW(Addr) map_start_aligned = ALIGN_UP (map_start, c->mapalign);
>    map_start_aligned = (ElfW(Addr)) __mmap ((void *) map_start_aligned,
>  					   maplength, c->prot,
> -					   MAP_COPY|MAP_FILE|MAP_FIXED,
> +					   flags|MAP_FIXED,
>  					   fd, c->mapoff);
>    if (__glibc_unlikely ((void *) map_start_aligned == MAP_FAILED))
>      __munmap ((void *) map_start, maplen);
> @@ -137,11 +153,12 @@ _dl_map_segments (struct link_map *l, int fd,
>  
>    do
>      {
> +      int flags = _dl_apply_map_shared (c.prot, MAP_COPY | MAP_FILE);
>        if (c.mapend > c.mapstart
>            /* Map the segment contents from the file.  */
>            && (__mmap ((void *) (l->l_addr + c.mapstart),
>                        c.mapend - c.mapstart, c.prot,
> -                      MAP_FIXED|MAP_COPY|MAP_FILE,
> +                      MAP_FIXED|flags,
>                        fd, c.mapoff)
>                == MAP_FAILED))
>          return DL_MAP_SEGMENTS_ERROR_MAP_SEGMENT;

Wouldn't this break the zero-fill path below, where it call mprotect(prot|PROT_WRITE)
for p_memsz > p_filesz?

Also I think this whole scheme breaks DT_TEXTREL (which is not really an issue).


> diff --git a/elf/dl-object.c b/elf/dl-object.c
> index 8153e5ca28..290b981d53 100644
> --- a/elf/dl-object.c
> +++ b/elf/dl-object.c
> @@ -24,6 +24,9 @@
>  
>  #include <assert.h>
>  
> +#ifdef SHARED
> +bool _dl_force_map_shared;
> +#endif

Shouldn't it be _rtld_global_ro (like dl_dso_sort_algo)? Another option is just
to read the tunable where it is used.

>  
>  /* Add the new link_map NEW to the end of the namespace list.  */
>  void
> diff --git a/elf/dl-tunables.list b/elf/dl-tunables.list
> index 111649f145..3cd584763a 100644
> --- a/elf/dl-tunables.list
> +++ b/elf/dl-tunables.list
> @@ -30,6 +30,11 @@ glibc {
>        minval: 0
>        maxval: 1
>      }
> +    map_shared {
> +      type: INT_32
> +      minval: 0
> +      maxval: 1
> +    }
>    }
>  
>    malloc {
> diff --git a/elf/tst-elf-map_shared.c b/elf/tst-elf-map_shared.c
> new file mode 100644
> index 0000000000..83c4f21cc3
> --- /dev/null
> +++ b/elf/tst-elf-map_shared.c
> @@ -0,0 +1,76 @@
> +/* Test that glibc.elf.map_shared=1 blocks mprotect calls on libc.so.6.
> +   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 <dlfcn.h>
> +#include <errno.h>
> +#include <gnu/lib-names.h>
> +#include <stdint.h>
> +#include <string.h>
> +#include <support/check.h>
> +#include <support/xdlfcn.h>
> +#include <support/xunistd.h>
> +#include <sys/mman.h>
> +
> +static int
> +do_test (void)
> +{
> +  void *handle = xdlopen (LIBC_SO, RTLD_NOLOAD | RTLD_LAZY);
> +  void *libc_map;
> +  TEST_COMPARE (dlinfo (handle, RTLD_DI_LINKMAP, &libc_map), 0);
> +
> +  /* An arbitrary symbol that is part of libc.so.6, but is unlikely to
> +     be interposed.  Use dlsym to avoid picking up a PLT stub in the
> +     main program.  */
> +  void *fptr = xdlsym (handle, "accept");
> +
> +  /* A string literal from libc.so should reside in read-only memory.  */
> +  const void *dptr = strerrorname_np (EPERM);
> +  TEST_COMPARE_STRING (dptr, "EPERM");
> +
> +  /* Check that the symbols are indeed in libc.  */
> +  {
> +    Dl_info info;
> +    void *map;
> +    TEST_VERIFY (dladdr1 (fptr, &info, &map, RTLD_DL_LINKMAP));
> +    TEST_VERIFY (map == libc_map);
> +
> +    TEST_VERIFY (dladdr1 (dptr, &info, &map, RTLD_DL_LINKMAP));
> +    TEST_VERIFY (map == libc_map);
> +  }
> +
> +  xdlclose (handle);
> +
> +  long int page_size = xsysconf (_SC_PAGESIZE);
> +  void *page_start;
> +
> +  /* The kernel cannot trigger copy-on-write due to MAP_SHARED, but
> +     the executable was opened in read-only mode.  So the mprotect
> +     cannot succeed because the page cannot be made writable.  */
> +
> +  page_start = (void *) ((uintptr_t) fptr  & -(uintptr_t) page_size);
> +  TEST_COMPARE (mprotect (page_start, page_size, PROT_WRITE), -1);
> +  TEST_COMPARE (errno, EACCES);
> +
> +  page_start = (void *) ((uintptr_t) dptr  & -(uintptr_t) page_size);
> +  TEST_COMPARE (mprotect (page_start, page_size, PROT_WRITE), -1);
> +  TEST_COMPARE (errno, EACCES);
> +
> +  return 0;
> +}
> +
> +#include <support/test-driver.c>
> diff --git a/manual/tunables.texi b/manual/tunables.texi
> index bbff2f5fdd..5f37be4503 100644
> --- a/manual/tunables.texi
> +++ b/manual/tunables.texi
> @@ -889,3 +889,20 @@ This tunable has an effect only on systems that support Transparent
>  Huge Pages (currently Linux).  On other systems, or if THP is disabled
>  at the kernel level, enabling this tunable has no effect.
>  @end deftp
> +
> +@deftp Tunable glibc.elf.map_shared
> +If this tunable is set to @code{1}, the dynamic linker will map
> +non-writable load segments using @code{MAP_SHARED} instead of
> +@code{MAP_PRIVATE}.  As the files have been opened read-only, this
> +prevents using @code{mprotect} with @code{PROT_WRITE} to make these
> +segments writable.
> +
> +Enabling this @code{MAP_SHARED} mode does not prevent other forms of
> +modification, for example using @code{ptrace}, or by mapping over the
> +memory using @code{MAP_FIXED}.  However, on Linux, this mode disables
> +uprobes because these breakpoint traps depend on copy-on-write semantics
> +which are unavailable with @code{MAP_SHARED} mappings.

Is this really true? The check_vma_flags() in mm/gup.c rejects FOLL_FORCE 
writes to non-copy-on-write mappings. For instance:

$ cat << EOF > libfoo.c
int
foo (int x)
{
  return x + 1;
}
EOF

$ cat << EOF > main.c
#define _GNU_SOURCE
#include <err.h>
#include <fcntl.h>
#include <link.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

int foo (int);

static int
remap_shared (struct dl_phdr_info *info, size_t size, void *data)
{
  if (strstr (info->dlpi_name, "libfoo.so") == NULL)
    return 0;

  int fd = open (info->dlpi_name, O_RDONLY | O_CLOEXEC);
  if (fd < 0)
    err (1, "open (%s)", info->dlpi_name);

  uintptr_t pagesize = sysconf (_SC_PAGESIZE);
  for (int i = 0; i < info->dlpi_phnum; i++)
    {
      const ElfW(Phdr) *ph = &info->dlpi_phdr[i];
      if (ph->p_type != PT_LOAD || (ph->p_flags & PF_W) != 0)
	continue;

      uintptr_t start = (info->dlpi_addr + ph->p_vaddr) & -pagesize;
      uintptr_t end = (info->dlpi_addr + ph->p_vaddr + ph->p_filesz
		       + pagesize - 1) & -pagesize;
      int prot = ((ph->p_flags & PF_R ? PROT_READ : 0)
		  | (ph->p_flags & PF_X ? PROT_EXEC : 0));
      if (mmap ((void *) start, end - start, prot, MAP_SHARED | MAP_FIXED,
		fd, ph->p_offset & -pagesize) == MAP_FAILED)
	err (1, "mmap");
    }
  close (fd);
  return 1;
}

static void
show_maps (void)
{
  FILE *f = fopen ("/proc/self/maps", "r");
  char line[512];
  while (fgets (line, sizeof line, f) != NULL)
    if (strstr (line, "libfoo.so") != NULL)
      fputs (line, stdout);
  fclose (f);
}

int
main (int argc, char *argv[])
{
  if (argc < 2 || strcmp (argv[1], "private") != 0)
    dl_iterate_phdr (remap_shared, NULL);
  show_maps ();
  printf ("foo (41) = %d\n", foo (41));
  return 0;
}
EOF

$ gcc -g -O0 -shared -fPIC -o libfoo.so libfoo.c
$ gcc -g -O0 -o main main.c -L. -lfoo -Wl,-rpath,'$ORIGIN'

$ gdb -q -batch -ex 'break show_maps' -ex run -ex 'break foo' -ex finish     -ex 'maint info breakpoints 2'     -ex 'set var *(unsigned char *) &foo = 0xcc' --args main
[...]
Breakpoint 1, show_maps () at main.c:45
45      {
Breakpoint 2 at 0x7ffff7fb2104: file libfoo.c, line 4.
7ffff7fb1000-7ffff7fb2000 r--s 00000000 00:33 2568672                    /tmp/aux/libfoo.so
7ffff7fb2000-7ffff7fb3000 r-xs 00001000 00:33 2568672                    /tmp/aux/libfoo.so
7ffff7fb3000-7ffff7fb4000 r--s 00002000 00:33 2568672                    /tmp/aux/libfoo.so
7ffff7fb4000-7ffff7fb5000 r--p 00002000 00:33 2568672                    /tmp/aux/libfoo.so
7ffff7fb5000-7ffff7fb6000 rw-p 00003000 00:33 2568672                    /tmp/aux/libfoo.so
main (argc=1, argv=0x7fffffffdca8) at main.c:60
60        printf ("foo (41) = %d\n", foo (41));
Num     Type           Disp Enb Address            What
2       breakpoint     keep y   <PENDING>          foo
2.1                         y   <PENDING>          foo
Cannot access memory at address 0x7ffff7fb20f9

$ gdb -q -batch -ex 'break show_maps' -ex run -ex 'break foo' -ex finish     -ex 'maint info breakpoints 2'     -ex 'set var *(unsigned char *) &foo = 0xcc' --args main private
Breakpoint 1 at 0x14f0: file main.c, line 45.
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, show_maps () at main.c:45
45      {
Breakpoint 2 at 0x7ffff7fb2104: file libfoo.c, line 4.
7ffff7fb1000-7ffff7fb2000 r--p 00000000 00:33 2568672                    /tmp/aux/libfoo.so
7ffff7fb2000-7ffff7fb3000 r-xp 00001000 00:33 2568672                    /tmp/aux/libfoo.so
7ffff7fb3000-7ffff7fb4000 r--p 00002000 00:33 2568672                    /tmp/aux/libfoo.so
7ffff7fb4000-7ffff7fb5000 r--p 00002000 00:33 2568672                    /tmp/aux/libfoo.so
7ffff7fb5000-7ffff7fb6000 rw-p 00003000 00:33 2568672                    /tmp/aux/libfoo.so
main (argc=2, argv=0x7fffffffdc98) at main.c:60
60        printf ("foo (41) = %d\n", foo (41));
Num     Type           Disp Enb Address            What
2       breakpoint     keep y   0x00007ffff7fb2104 in foo at libfoo.c:4 inf 1
2.1                         y   0x00007ffff7fb2104 in foo at libfoo.c:4 inf 1


Withot MAP_SHARED, the breakpoint has gone back to <PENDING>, and the
write gets the kernel's refusal. With private, the location keeps its 
address and the write succeeds.

This small semantic differences make me doubt if this should be in the
loader.

> +
> +The default value is @code{0}, which means that the dynamic linker uses
> +@code{MAP_PRIVATE} regardless of load segment permissions.
> +@end deftp
> diff --git a/sysdeps/generic/ldsodefs.h b/sysdeps/generic/ldsodefs.h
> index 305ca6e0df..00a99417f8 100644
> --- a/sysdeps/generic/ldsodefs.h
> +++ b/sysdeps/generic/ldsodefs.h
> @@ -918,6 +918,9 @@ rtld_hidden_proto (_dl_catch_exception)
>  struct link_map *_dl_lookup_map (Lmid_t nsid, const char *name)
>     attribute_hidden;
>  
> +/* If true, the glibc.elf.map_shared=1 tunable was set.  */
> +extern bool _dl_force_map_shared attribute_hidden;
> +
>  /* Open the shared object NAME and map in its segments.
>     LOADER's DT_RPATH is used in searching for NAME.
>     If the object is already opened, returns its existing map.  */
> diff --git a/sysdeps/mach/hurd/dl-sysdep.c b/sysdeps/mach/hurd/dl-sysdep.c
> index 29acbe1c76..2879ee51e1 100644
> --- a/sysdeps/mach/hurd/dl-sysdep.c
> +++ b/sysdeps/mach/hurd/dl-sysdep.c
> @@ -103,6 +103,9 @@ _dl_sysdep_start (void **start_argptr,
>        /* Initialize DSO sorting algorithm after tunables.  */
>        _dl_sort_maps_init ();
>  
> +      _dl_force_map_shared
> +	= TUNABLE_GET_FULL (glibc, elf, map_shared, int32_t, NULL);
> +
>  #ifdef DL_SYSDEP_INIT
>        DL_SYSDEP_INIT;
>  #endif
> diff --git a/sysdeps/unix/sysv/linux/dl-sysdep.c b/sysdeps/unix/sysv/linux/dl-sysdep.c
> index c2701f274c..f2d2b27efe 100644
> --- a/sysdeps/unix/sysv/linux/dl-sysdep.c
> +++ b/sysdeps/unix/sysv/linux/dl-sysdep.c
> @@ -112,6 +112,9 @@ _dl_sysdep_start (void **start_argptr,
>    /* Initialize DSO sorting algorithm after tunables.  */
>    _dl_sort_maps_init ();
>  
> +  _dl_force_map_shared
> +    = TUNABLE_GET_FULL (glibc, elf, map_shared, int32_t, NULL);
> +
>    __brk (NULL);			/* Initialize the break.  */
>  
>  #ifdef DL_PLATFORM_INIT
> 
> base-commit: 3edeb81021feff11a3d0e934d68446f6a67dd1a3
  
Sam James Oct. 8, 2026, 4:40 p.m. UTC | #7
Mark Wielaard <mark@klomp.org> writes:

> Hi Florian,
>
> On Mon, Sep 28, 2026 at 03:13:23PM +0200, Florian Weimer wrote:
>> >> > Note the comment of the next function is now misplaced before the
>> >> > comment to this function.
>> >> 
>> >> I thought the comment applied to the entire file.
>> >
>> > Fair. I didn't know if each function needs a comment.
>> 
>> May I consider this a proper review for inclusion into the glibc
>> sources?
>
> Please feel free to add
>
> Reviewed-by: Mark Wielaard <mark@klomp.org>
>
> But I only looked at/tested the interaction with valgrind and ebpf
> uprobes. Which seems to work as expected. It would be good if someone
> gave their opinion on the PROT_WRITE EACCESS "protection" this
> adds. Which also works, but I must admit I don't know if I really
> understand the bigger picture there.

IMO it's something we can tolerate as long as we're free to remove it in
future, which I think we say for all tunables (though not all tunables
are equal).

>
> Cheers,
>
> Mark
  
Adhemerval Zanella Oct. 8, 2026, 5:08 p.m. UTC | #8
On 08/10/26 18:40, Sam James wrote:
> Mark Wielaard <mark@klomp.org> writes:
> 
>> Hi Florian,
>>
>> On Mon, Sep 28, 2026 at 03:13:23PM +0200, Florian Weimer wrote:
>>>>>> Note the comment of the next function is now misplaced before the
>>>>>> comment to this function.
>>>>>
>>>>> I thought the comment applied to the entire file.
>>>>
>>>> Fair. I didn't know if each function needs a comment.
>>>
>>> May I consider this a proper review for inclusion into the glibc
>>> sources?
>>
>> Please feel free to add
>>
>> Reviewed-by: Mark Wielaard <mark@klomp.org>
>>
>> But I only looked at/tested the interaction with valgrind and ebpf
>> uprobes. Which seems to work as expected. It would be good if someone
>> gave their opinion on the PROT_WRITE EACCESS "protection" this
>> adds. Which also works, but I must admit I don't know if I really
>> understand the bigger picture there.
> 
> IMO it's something we can tolerate as long as we're free to remove it in
> future, which I think we say for all tunables (though not all tunables
> are equal).
Besides the issues I have raised, having a tunable as a requisite to make
such tools to work means that it would be an ABI de-facto. I am not sure
if this what we want for this debug knob.
  
Sam James Oct. 8, 2026, 5:21 p.m. UTC | #9
Adhemerval Zanella Netto <adhemerval.zanella@linaro.org> writes:

> On 08/10/26 18:40, Sam James wrote:
>> Mark Wielaard <mark@klomp.org> writes:
>> 
>>> Hi Florian,
>>>
>>> On Mon, Sep 28, 2026 at 03:13:23PM +0200, Florian Weimer wrote:
>>>>>>> Note the comment of the next function is now misplaced before the
>>>>>>> comment to this function.
>>>>>>
>>>>>> I thought the comment applied to the entire file.
>>>>>
>>>>> Fair. I didn't know if each function needs a comment.
>>>>
>>>> May I consider this a proper review for inclusion into the glibc
>>>> sources?
>>>
>>> Please feel free to add
>>>
>>> Reviewed-by: Mark Wielaard <mark@klomp.org>
>>>
>>> But I only looked at/tested the interaction with valgrind and ebpf
>>> uprobes. Which seems to work as expected. It would be good if someone
>>> gave their opinion on the PROT_WRITE EACCESS "protection" this
>>> adds. Which also works, but I must admit I don't know if I really
>>> understand the bigger picture there.
>> 
>> IMO it's something we can tolerate as long as we're free to remove it in
>> future, which I think we say for all tunables (though not all tunables
>> are equal).
> Besides the issues I have raised, having a tunable as a requisite to make
> such tools to work means that it would be an ABI de-facto. I am not sure
> if this what we want for this debug knob.

Sorry, I hadn't got to your email yet. You are right though, I was being
too optimistic. If people relied on it then we'd end up unable to remove
it without at least some blowback. It's all kind of a workaround for a
policy problem anyway..
  
Florian Weimer Oct. 8, 2026, 5:58 p.m. UTC | #10
* Adhemerval Zanella Netto:

> On 25/09/26 23:06, Florian Weimer wrote:
>> This tunable can be used to disable certain forms of run-time
>> patching of shared objects, such as Linux uprobes.
>> 
>> This could be helpful to valgrind because it needs the unpatched
>> instruction to work correctly.  It cannot trigger the kernel uprobes
>> handler for uprobes traps.
>> 
>> We've also occasionally seen run-time patching of glibc and other
>> components going wrong.  This tunable could provide a quick check that
>> the direct patching after mprotect permission changes (as implemented in
>> Unifined Communication X <https://github.com/openucx/ucx>, for example)
>> is not causing the problem.
>> 
>> Comments?
>
> I am not sure about this tunable, this only covers objects ld.so maps and
> valgrind (or any tool like USDT and uprobes) already need to wrap all mmpa
> anyway to handle loader and the executable itself.

But detecting this within valgrind is probably more complex than this
patch, and perhaps less reliable.  I'm not sure if it is a good idea to
apply this kludge to arbitrary mappings, so the caller needs to be
identified.

Thanks,
Florian
  
Adhemerval Zanella Oct. 8, 2026, 6:56 p.m. UTC | #11
On 08/10/26 19:58, Florian Weimer wrote:
> * Adhemerval Zanella Netto:
> 
>> On 25/09/26 23:06, Florian Weimer wrote:
>>> This tunable can be used to disable certain forms of run-time
>>> patching of shared objects, such as Linux uprobes.
>>>
>>> This could be helpful to valgrind because it needs the unpatched
>>> instruction to work correctly.  It cannot trigger the kernel uprobes
>>> handler for uprobes traps.
>>>
>>> We've also occasionally seen run-time patching of glibc and other
>>> components going wrong.  This tunable could provide a quick check that
>>> the direct patching after mprotect permission changes (as implemented in
>>> Unifined Communication X <https://github.com/openucx/ucx>, for example)
>>> is not causing the problem.
>>>
>>> Comments?
>>
>> I am not sure about this tunable, this only covers objects ld.so maps and
>> valgrind (or any tool like USDT and uprobes) already need to wrap all mmpa
>> anyway to handle loader and the executable itself.
> 
> But detecting this within valgrind is probably more complex than this
> patch, and perhaps less reliable.  I'm not sure if it is a good idea to
> apply this kludge to arbitrary mappings, so the caller needs to be
> identified.
But valgrind in theory would need to cover non glibc system anyway, like
musl, go, etc. Having this in valgrind seems the best approach IMHO, even
if more complex. Maybe by using an audit library or a systemtap probe to
get the loading events.
  

Patch

diff --git a/elf/Makefile b/elf/Makefile
index 307d603ff0..4dc62ce84a 100644
--- a/elf/Makefile
+++ b/elf/Makefile
@@ -463,6 +463,7 @@  tests += \
   tst-dlopenrpath \
   tst-dlsym-error \
   tst-dst-needed-minstack \
+  tst-elf-map_shared \
   tst-filterobj \
   tst-filterobj-dlopen \
   tst-glibc-hwcaps \
@@ -3830,3 +3831,5 @@  LDFLAGS-tst-origin-secure-victim += \
 $(objpfx)tst-origin-secure.out: $(objpfx)tst-origin-secure-victim \
 			       $(objpfx)libtst-origin-secure-mod.so \
 			       $(objpfx)tst-origin-secure-evilmod.so
+
+tst-elf-map_shared-TUNABLES += glibc.elf.map_shared=1
diff --git a/elf/dl-map-segments.h b/elf/dl-map-segments.h
index 1854fab7dc..ce710fe23f 100644
--- a/elf/dl-map-segments.h
+++ b/elf/dl-map-segments.h
@@ -22,13 +22,29 @@ 
 
 /* Map a segment and align it properly.  */
 
+/* Replace MAP_PRIVATE with MAP_SHARED if glibc.elf.map_shared=1 is
+   active and the mapping is not writable.  */
+static inline int
+_dl_apply_map_shared (int prot, int flags)
+{
+#ifdef SHARED
+  if (_dl_force_map_shared && (prot & PROT_WRITE) == 0)
+    {
+      flags &= ~MAP_PRIVATE;
+      flags |= MAP_SHARED;
+    }
+#endif
+  return flags;
+}
+
 static __always_inline ElfW(Addr)
 _dl_map_segment (const struct loadcmd *c, ElfW(Addr) mappref,
 		 const size_t maplength, int fd)
 {
+  int flags = _dl_apply_map_shared (c->prot, MAP_COPY | MAP_FILE);
   if (__glibc_likely (c->mapalign <= GLRO(dl_pagesize)))
     return (ElfW(Addr)) __mmap ((void *) mappref, maplength, c->prot,
-				MAP_COPY|MAP_FILE, fd, c->mapoff);
+				flags, fd, c->mapoff);
 
   /* If the segment alignment > the page size, allocate enough space to
      ensure that the segment can be properly aligned.  */
@@ -45,7 +61,7 @@  _dl_map_segment (const struct loadcmd *c, ElfW(Addr) mappref,
   ElfW(Addr) map_start_aligned = ALIGN_UP (map_start, c->mapalign);
   map_start_aligned = (ElfW(Addr)) __mmap ((void *) map_start_aligned,
 					   maplength, c->prot,
-					   MAP_COPY|MAP_FILE|MAP_FIXED,
+					   flags|MAP_FIXED,
 					   fd, c->mapoff);
   if (__glibc_unlikely ((void *) map_start_aligned == MAP_FAILED))
     __munmap ((void *) map_start, maplen);
@@ -137,11 +153,12 @@  _dl_map_segments (struct link_map *l, int fd,
 
   do
     {
+      int flags = _dl_apply_map_shared (c.prot, MAP_COPY | MAP_FILE);
       if (c.mapend > c.mapstart
           /* Map the segment contents from the file.  */
           && (__mmap ((void *) (l->l_addr + c.mapstart),
                       c.mapend - c.mapstart, c.prot,
-                      MAP_FIXED|MAP_COPY|MAP_FILE,
+                      MAP_FIXED|flags,
                       fd, c.mapoff)
               == MAP_FAILED))
         return DL_MAP_SEGMENTS_ERROR_MAP_SEGMENT;
diff --git a/elf/dl-object.c b/elf/dl-object.c
index 8153e5ca28..290b981d53 100644
--- a/elf/dl-object.c
+++ b/elf/dl-object.c
@@ -24,6 +24,9 @@ 
 
 #include <assert.h>
 
+#ifdef SHARED
+bool _dl_force_map_shared;
+#endif
 
 /* Add the new link_map NEW to the end of the namespace list.  */
 void
diff --git a/elf/dl-tunables.list b/elf/dl-tunables.list
index 111649f145..3cd584763a 100644
--- a/elf/dl-tunables.list
+++ b/elf/dl-tunables.list
@@ -30,6 +30,11 @@  glibc {
       minval: 0
       maxval: 1
     }
+    map_shared {
+      type: INT_32
+      minval: 0
+      maxval: 1
+    }
   }
 
   malloc {
diff --git a/elf/tst-elf-map_shared.c b/elf/tst-elf-map_shared.c
new file mode 100644
index 0000000000..83c4f21cc3
--- /dev/null
+++ b/elf/tst-elf-map_shared.c
@@ -0,0 +1,76 @@ 
+/* Test that glibc.elf.map_shared=1 blocks mprotect calls on libc.so.6.
+   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 <dlfcn.h>
+#include <errno.h>
+#include <gnu/lib-names.h>
+#include <stdint.h>
+#include <string.h>
+#include <support/check.h>
+#include <support/xdlfcn.h>
+#include <support/xunistd.h>
+#include <sys/mman.h>
+
+static int
+do_test (void)
+{
+  void *handle = xdlopen (LIBC_SO, RTLD_NOLOAD | RTLD_LAZY);
+  void *libc_map;
+  TEST_COMPARE (dlinfo (handle, RTLD_DI_LINKMAP, &libc_map), 0);
+
+  /* An arbitrary symbol that is part of libc.so.6, but is unlikely to
+     be interposed.  Use dlsym to avoid picking up a PLT stub in the
+     main program.  */
+  void *fptr = xdlsym (handle, "accept");
+
+  /* A string literal from libc.so should reside in read-only memory.  */
+  const void *dptr = strerrorname_np (EPERM);
+  TEST_COMPARE_STRING (dptr, "EPERM");
+
+  /* Check that the symbols are indeed in libc.  */
+  {
+    Dl_info info;
+    void *map;
+    TEST_VERIFY (dladdr1 (fptr, &info, &map, RTLD_DL_LINKMAP));
+    TEST_VERIFY (map == libc_map);
+
+    TEST_VERIFY (dladdr1 (dptr, &info, &map, RTLD_DL_LINKMAP));
+    TEST_VERIFY (map == libc_map);
+  }
+
+  xdlclose (handle);
+
+  long int page_size = xsysconf (_SC_PAGESIZE);
+  void *page_start;
+
+  /* The kernel cannot trigger copy-on-write due to MAP_SHARED, but
+     the executable was opened in read-only mode.  So the mprotect
+     cannot succeed because the page cannot be made writable.  */
+
+  page_start = (void *) ((uintptr_t) fptr  & -(uintptr_t) page_size);
+  TEST_COMPARE (mprotect (page_start, page_size, PROT_WRITE), -1);
+  TEST_COMPARE (errno, EACCES);
+
+  page_start = (void *) ((uintptr_t) dptr  & -(uintptr_t) page_size);
+  TEST_COMPARE (mprotect (page_start, page_size, PROT_WRITE), -1);
+  TEST_COMPARE (errno, EACCES);
+
+  return 0;
+}
+
+#include <support/test-driver.c>
diff --git a/manual/tunables.texi b/manual/tunables.texi
index bbff2f5fdd..5f37be4503 100644
--- a/manual/tunables.texi
+++ b/manual/tunables.texi
@@ -889,3 +889,20 @@  This tunable has an effect only on systems that support Transparent
 Huge Pages (currently Linux).  On other systems, or if THP is disabled
 at the kernel level, enabling this tunable has no effect.
 @end deftp
+
+@deftp Tunable glibc.elf.map_shared
+If this tunable is set to @code{1}, the dynamic linker will map
+non-writable load segments using @code{MAP_SHARED} instead of
+@code{MAP_PRIVATE}.  As the files have been opened read-only, this
+prevents using @code{mprotect} with @code{PROT_WRITE} to make these
+segments writable.
+
+Enabling this @code{MAP_SHARED} mode does not prevent other forms of
+modification, for example using @code{ptrace}, or by mapping over the
+memory using @code{MAP_FIXED}.  However, on Linux, this mode disables
+uprobes because these breakpoint traps depend on copy-on-write semantics
+which are unavailable with @code{MAP_SHARED} mappings.
+
+The default value is @code{0}, which means that the dynamic linker uses
+@code{MAP_PRIVATE} regardless of load segment permissions.
+@end deftp
diff --git a/sysdeps/generic/ldsodefs.h b/sysdeps/generic/ldsodefs.h
index 305ca6e0df..00a99417f8 100644
--- a/sysdeps/generic/ldsodefs.h
+++ b/sysdeps/generic/ldsodefs.h
@@ -918,6 +918,9 @@  rtld_hidden_proto (_dl_catch_exception)
 struct link_map *_dl_lookup_map (Lmid_t nsid, const char *name)
    attribute_hidden;
 
+/* If true, the glibc.elf.map_shared=1 tunable was set.  */
+extern bool _dl_force_map_shared attribute_hidden;
+
 /* Open the shared object NAME and map in its segments.
    LOADER's DT_RPATH is used in searching for NAME.
    If the object is already opened, returns its existing map.  */
diff --git a/sysdeps/mach/hurd/dl-sysdep.c b/sysdeps/mach/hurd/dl-sysdep.c
index 29acbe1c76..2879ee51e1 100644
--- a/sysdeps/mach/hurd/dl-sysdep.c
+++ b/sysdeps/mach/hurd/dl-sysdep.c
@@ -103,6 +103,9 @@  _dl_sysdep_start (void **start_argptr,
       /* Initialize DSO sorting algorithm after tunables.  */
       _dl_sort_maps_init ();
 
+      _dl_force_map_shared
+	= TUNABLE_GET_FULL (glibc, elf, map_shared, int32_t, NULL);
+
 #ifdef DL_SYSDEP_INIT
       DL_SYSDEP_INIT;
 #endif
diff --git a/sysdeps/unix/sysv/linux/dl-sysdep.c b/sysdeps/unix/sysv/linux/dl-sysdep.c
index c2701f274c..f2d2b27efe 100644
--- a/sysdeps/unix/sysv/linux/dl-sysdep.c
+++ b/sysdeps/unix/sysv/linux/dl-sysdep.c
@@ -112,6 +112,9 @@  _dl_sysdep_start (void **start_argptr,
   /* Initialize DSO sorting algorithm after tunables.  */
   _dl_sort_maps_init ();
 
+  _dl_force_map_shared
+    = TUNABLE_GET_FULL (glibc, elf, map_shared, int32_t, NULL);
+
   __brk (NULL);			/* Initialize the break.  */
 
 #ifdef DL_PLATFORM_INIT