diff mbox series

[33/33] elf: Use memory protection keys for the protected memory allocator

Message ID 477cc628fed2769f25399d7674080dd257a80d46.1688499219.git.fweimer@redhat.com
State New
Headers show
Series RFC: RELRO link maps | expand

Commit Message

Florian Weimer July 4, 2023, 8:04 p.m. UTC
If protection keys are not supported by the system, fall back to
switching permission flags using mprotect.

A complication arises on x86 because the kernel supports protection
keys, but they are incompatible with dynamic linker requirements
(see bug 22396).  Therefore, protection key support is disabled by
default on x86, but glibc.rtld.protmem=3 can still force enabling
it.
---
 NEWS                                          |   4 +
 elf/Makefile                                  |  10 +
 elf/dl-diagnostics.c                          |   2 +
 elf/dl-protmem.c                              | 171 +++++++++++++++++-
 elf/dl-protmem.h                              |   9 +
 elf/dl-tunables.list                          |   6 +
 elf/tst-dl-protmem.c                          |  10 +
 elf/tst-relro-linkmap-disabled-mod1.c         |  46 +++++
 elf/tst-relro-linkmap-disabled-mod2.c         |   2 +
 elf/tst-relro-linkmap-disabled.c              |  64 +++++++
 elf/tst-rtld-list-tunables.exp                |   1 +
 manual/tunables.texi                          |  29 +++
 nptl/pthread_create.c                         |   8 +
 sysdeps/generic/dl-protmem-pkey.h             |  20 ++
 sysdeps/generic/ldsodefs.h                    |   5 +
 sysdeps/unix/sysv/linux/dl-protmem-pkey.h     |  23 +++
 sysdeps/unix/sysv/linux/dl-sysdep.c           |   2 +
 sysdeps/unix/sysv/linux/x86/dl-protmem-pkey.h |  26 +++
 18 files changed, 430 insertions(+), 8 deletions(-)
 create mode 100644 elf/tst-relro-linkmap-disabled-mod1.c
 create mode 100644 elf/tst-relro-linkmap-disabled-mod2.c
 create mode 100644 elf/tst-relro-linkmap-disabled.c
 create mode 100644 sysdeps/generic/dl-protmem-pkey.h
 create mode 100644 sysdeps/unix/sysv/linux/dl-protmem-pkey.h
 create mode 100644 sysdeps/unix/sysv/linux/x86/dl-protmem-pkey.h
diff mbox series

Patch

diff --git a/NEWS b/NEWS
index 709ee40e50..e79a422c33 100644
--- a/NEWS
+++ b/NEWS
@@ -48,6 +48,10 @@  Major new features:
 * The strlcpy and strlcat functions have been added.  They are derived
   from OpenBSD, and are expected to be added to a future POSIX version.
 
+* The dynamic linker keeps link maps and other data structures read-only
+  most of the time (RELRO link maps).  This behavior can be controlled
+  by the new glibc.rtld.protmem tunable.
+
 Deprecated and removed features, and other changes affecting compatibility:
 
 * In the Linux kernel for the hppa/parisc architecture some of the
diff --git a/elf/Makefile b/elf/Makefile
index c6a3c4678b..6d46852230 100644
--- a/elf/Makefile
+++ b/elf/Makefile
@@ -499,6 +499,7 @@  tests-internal += \
   tst-dlmopen2 \
   tst-ptrguard1 \
   tst-relro-linkmap \
+  tst-relro-linkmap-disabled \
   tst-stackguard1 \
   tst-tls-surplus \
   tst-tls3 \
@@ -863,6 +864,8 @@  modules-names += \
   tst-null-argv-lib \
   tst-p_alignmod-base \
   tst-p_alignmod3 \
+  tst-relro-linkmap-disabled-mod1 \
+  tst-relro-linkmap-disabled-mod2 \
   tst-relro-linkmap-mod1 \
   tst-relro-linkmap-mod2 \
   tst-relro-linkmap-mod3 \
@@ -3022,3 +3025,10 @@  LDFLAGS-tst-relro-linkmap = -Wl,-E
 $(objpfx)tst-relro-linkmap: $(objpfx)tst-relro-linkmap-mod1.so
 $(objpfx)tst-relro-linkmap.out: $(objpfx)tst-dlopenfailmod1.so \
   $(objpfx)tst-relro-linkmap-mod2.so $(objpfx)tst-relro-linkmap-mod3.so
+
+tst-relro-linkmap-disabled-ENV = GLIBC_TUNABLES=glibc.rtld.protmem=0
+$(objpfx)tst-relro-linkmap-disabled: \
+  $(objpfx)tst-relro-linkmap-disabled-mod1.so
+$(objpfx)tst-relro-linkmap-disabled.out: $(objpfx)tst-dlopenfailmod1.so \
+  $(objpfx)tst-relro-linkmap-disabled-mod2.so \
+  $(objpfx)tst-relro-linkmap-mod3.so
diff --git a/elf/dl-diagnostics.c b/elf/dl-diagnostics.c
index d742cf0a99..eb2eb96258 100644
--- a/elf/dl-diagnostics.c
+++ b/elf/dl-diagnostics.c
@@ -241,6 +241,8 @@  _dl_print_diagnostics (char **environ)
     ("dl_hwcaps_subdirs_active", _dl_hwcaps_subdirs_active ());
   _dl_diagnostics_print_labeled_value ("dl_pagesize", GLRO (dl_pagesize));
   _dl_diagnostics_print_labeled_string ("dl_platform", GLRO (dl_platform));
+  _dl_diagnostics_print_labeled_value ("dl_protmem_key",
+                                       (unsigned int) GLRO (dl_protmem_key));
   _dl_diagnostics_print_labeled_string
     ("dl_profile_output", GLRO (dl_profile_output));
   _dl_diagnostics_print_labeled_value
diff --git a/elf/dl-protmem.c b/elf/dl-protmem.c
index cd416e33a5..61ffb2a0de 100644
--- a/elf/dl-protmem.c
+++ b/elf/dl-protmem.c
@@ -20,11 +20,17 @@ 
 
 #include <dl-protmem.h>
 #include <dl-protmem-internal.h>
+#include <dl-protmem-pkey.h>
 
 #include <array_length.h>
 #include <assert.h>
 #include <sys/mman.h>
 
+#if IS_IN (rtld)
+# define TUNABLE_NAMESPACE rtld
+# include <dl-tunables.h>
+#endif
+
 /* Nesting counter for _dl_protmem_begin/_dl_protmem_end.  This is
    primaryly required because we may have a call sequence dlopen,
    malloc, dlopen.  Without the counter, _dl_protmem_end in the inner
@@ -39,6 +45,89 @@  _dl_protmem_state (void)
           - offsetof (struct dl_protmem_state, protmem));
 }
 
+/* Allocate the protection key and if successful, apply it to the
+   original region.   Return true if protected memory is enabled.  */
+static bool
+_dl_protmem_key_init (void *initial_region, size_t initial_size)
+{
+  GLRO (dl_protmem_key) = -1;
+
+#if IS_IN (rtld)                /* Disabled for tst-dl-protmem.  */
+  int pkey_config = TUNABLE_GET (protmem, size_t, NULL);
+  if (pkey_config == 0)
+    /* Disable the protected memory allocator completely.  */
+    return false;
+  if (pkey_config == 1)
+    /* Force the use of mprotect.   */
+    return true;
+
+# if DL_PROTMEM_PKEY_SUPPORT
+  /* For tunables values 2 or 3, potentially use memory protection
+     keys.  Do not enable protection keys for pkey_config == 2 by
+     default for !DL_PROTMEM_PKEY_SUPPORT.  Used on x86, see
+     sysdeps/unix/sysv/linux/x86/dl-protmem-pkey.h.  */
+  if (DL_PROTMEM_PKEY_ENABLE || pkey_config >= 3)
+    GLRO (dl_protmem_key) = pkey_alloc (0, 0);
+# endif /* !DL_PROTMEM_PKEY_SUPPORT */
+#endif /* IS_IN (rtld) */
+  return true;
+}
+
+/* Try to use the protection key to enable writing.  Return true if
+   protection keys are in use.  */
+static bool
+_dl_protmem_key_allow (void)
+{
+#if DL_PROTMEM_PKEY_SUPPORT
+  /* Enable write access at the beginning.  */
+  if (GLRO (dl_protmem_key) >= 0)
+    {
+      pkey_set (GLRO (dl_protmem_key), 0);
+      return true;
+    }
+#endif
+  return false;
+}
+
+/* Try to use the protection key to disable writing.  Return true if
+   protection keys are in use.  */
+static bool
+_dl_protmem_key_deny (void)
+{
+#if DL_PROTMEM_PKEY_SUPPORT
+  /* Enable write access at the beginning.  */
+  if (GLRO (dl_protmem_key) >= 0)
+    {
+      pkey_set (GLRO (dl_protmem_key), PKEY_DISABLE_WRITE);
+      return true;
+    }
+#endif
+  return false;
+}
+
+/* Creates an anonymous memory mapping as backing store.  Applies the
+   protection key if necessary.  Returns NULL on failure.  */
+static void *
+_dl_protmem_mmap (size_t size)
+{
+  void *result = __mmap (NULL, size, PROT_READ | PROT_WRITE,
+                         MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
+  if (result == MAP_FAILED)
+    return NULL;
+#if DL_PROTMEM_PKEY_SUPPORT
+  if (GLRO (dl_protmem_key) >= 0)
+    {
+      if (__pkey_mprotect (result, size, PROT_READ | PROT_WRITE,
+                           GLRO (dl_protmem_key)) != 0)
+        {
+          __munmap (result, size);
+          return NULL;
+        }
+    }
+#endif
+  return result;
+}
+
 /* Debugging allocator.  The real allocator is below.  */
 #if DL_PROTMEM_DEBUG
 void
@@ -52,6 +141,27 @@  _dl_protmem_init (void)
   _dl_protmem_begin_count = 1;
 }
 
+void
+_dl_protmem_init_2 (void)
+{
+  struct dl_protmem_state *state = _dl_protmem_state ();
+  if (!_dl_protmem_key_init (state, sizeof (struct dl_protmem_state)))
+    /* Make _dl_protmem_end a no-op.  */
+    ++_dl_protmem_begin_count;
+
+#if DL_PROTMEM_PKEY_SUPPORT
+  if (GLRO (dl_protmem_key) >= 0)
+    {
+      for (struct dl_protmem_header *hdr = state->root;
+           hdr != NULL; hdr = hdr->next)
+        if (__pkey_mprotect (hdr, hdr->size, PROT_READ | PROT_WRITE,
+                             GLRO (dl_protmem_key)) != 0)
+          _dl_fatal_printf ("\
+Fatal glibc eror: cannot apply protoection key to protected memory\n");
+    }
+#endif
+}
+
 void *
 _dl_protmem_allocate (size_t size)
 {
@@ -65,9 +175,8 @@  _dl_protmem_allocate (size_t size)
   if (__builtin_add_overflow (size, sizeof (*hdr), &total_size))
     return NULL;
 
-  hdr = __mmap (NULL, total_size, PROT_READ | PROT_WRITE,
-                MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
-  if (hdr == MAP_FAILED)
+  hdr = _dl_protmem_mmap (total_size);
+  if (hdr == NULL)
     return NULL;
   hdr->size = total_size;
 
@@ -113,6 +222,9 @@  _dl_protmem_begin (void)
   if (_dl_protmem_begin_count++ > 0)
     return;
 
+  if (_dl_protmem_key_allow ())
+    return;
+
   struct dl_protmem_state *state = _dl_protmem_state ();
   for (struct dl_protmem_header *hdr = state->root;
        hdr != NULL; hdr = hdr->next)
@@ -127,6 +239,9 @@  _dl_protmem_end (void)
   if (--_dl_protmem_begin_count > 0)
     return;
 
+  if (_dl_protmem_key_deny ())
+    return;
+
   struct dl_protmem_state *state = _dl_protmem_state ();
   for (struct dl_protmem_header *hdr = state->root;
        hdr != NULL; hdr = hdr->next)
@@ -329,6 +444,41 @@  _dl_protmem_init (void)
   _dl_protmem_begin_count = 1;
 }
 
+void
+_dl_protmem_init_2 (void)
+{
+  struct dl_protmem_state *state = _dl_protmem_state ();
+  if (!_dl_protmem_key_init (state, DL_PROTMEM_INITIAL_REGION_SIZE))
+    /* Make _dl_protmem_end a no-op.  */
+    ++_dl_protmem_begin_count;
+
+  /* Apply the protection key to the existing memory regions.  */
+#if DL_PROTMEM_PKEY_SUPPORT
+  if (GLRO (dl_protmem_key) >= 0)
+    {
+      size_t region_size = DL_PROTMEM_INITIAL_REGION_SIZE;
+      for (unsigned int i = 0; i < array_length (state->regions); ++i)
+        if (state->regions[i] != NULL)
+          {
+            if (__pkey_mprotect (state->regions[i], region_size,
+                                 PROT_READ | PROT_WRITE, GLRO (dl_protmem_key))
+                != 0)
+              {
+                if (i == 0)
+                  /* If the first pkey_mprotect failed, we can allow
+                     reuse of the key.  Otherwise, other memory still
+                     use the key.  */
+                  __pkey_free (GLRO (dl_protmem_key));
+                /* Always stop using protection keys on pkey_mprotect
+                   failure.  */
+                GLRO (dl_protmem_key) = -1;
+                break;
+              }
+          }
+    }
+#endif
+}
+
 void
 _dl_protmem_begin (void)
 {
@@ -336,6 +486,9 @@  _dl_protmem_begin (void)
       /* Already unprotected.  */
     return;
 
+  if (_dl_protmem_key_allow ())
+    return;
+
   struct dl_protmem_state *state = _dl_protmem_state ();
   size_t region_size = DL_PROTMEM_INITIAL_REGION_SIZE;
   for (unsigned int i = 0; i < array_length (state->regions); ++i)
@@ -343,8 +496,8 @@  _dl_protmem_begin (void)
       {
         if (__mprotect (state->regions[i], region_size,
                         PROT_READ | PROT_WRITE) != 0)
-          _dl_signal_error (ENOMEM, NULL, NULL,
-                            "Cannot make protected memory writable");
+            _dl_signal_error (ENOMEM, NULL, NULL,
+                              "Cannot make protected memory writable");
         region_size *= 2;
       }
 }
@@ -355,6 +508,9 @@  _dl_protmem_end (void)
   if (--_dl_protmem_begin_count > 0)
     return;
 
+  if (_dl_protmem_key_deny ())
+    return;
+
   struct dl_protmem_state *state = _dl_protmem_state ();
   size_t region_size = DL_PROTMEM_INITIAL_REGION_SIZE;
   for (unsigned int i = 0; i < array_length (state->regions); ++i)
@@ -444,9 +600,8 @@  _dl_protmem_allocate (size_t requested_size)
         {
           if (state->regions[i] == NULL && region_size >= requested_size)
             {
-              void *ptr = __mmap (NULL, region_size, PROT_READ | PROT_WRITE,
-                                  MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
-              if (ptr == MAP_FAILED)
+              void *ptr = _dl_protmem_mmap (region_size);
+              if (ptr == NULL)
                 return NULL;
               state->regions[i] = ptr;
               if (region_size == requested_size)
diff --git a/elf/dl-protmem.h b/elf/dl-protmem.h
index 59aeaf630d..3ce941cd9c 100644
--- a/elf/dl-protmem.h
+++ b/elf/dl-protmem.h
@@ -36,6 +36,10 @@ 
    functions below.  Implies the first _dl_protmem_begin call.  */
 void _dl_protmem_init (void) attribute_hidden;
 
+/* Second phase of initialization.  This enables configuration by
+   tunables.  */
+void _dl_protmem_init_2 (void) attribute_hidden;
+
 /* Frees memory allocated using _dl_protmem_allocate.  The passed size
    must be the same that was passed to _dl_protmem_allocate.
    Protected memory must be writable when this function is called.  */
@@ -67,6 +71,11 @@  void _dl_protmem_end (void) attribute_hidden;
 
 #include <stdlib.h>
 
+static inline void
+_dl_protmem_init (void)
+{
+}
+
 static inline void *
 _dl_protmem_allocate (size_t size)
 {
diff --git a/elf/dl-tunables.list b/elf/dl-tunables.list
index 695ba7192e..187b8e0e11 100644
--- a/elf/dl-tunables.list
+++ b/elf/dl-tunables.list
@@ -151,6 +151,12 @@  glibc {
       minval: 0
       default: 512
     }
+    protmem {
+      type: INT_32
+      default: 2
+      minval: 0
+      maxval: 3
+    }
   }
 
   mem {
diff --git a/elf/tst-dl-protmem.c b/elf/tst-dl-protmem.c
index 6061845ca7..82449ffb83 100644
--- a/elf/tst-dl-protmem.c
+++ b/elf/tst-dl-protmem.c
@@ -163,8 +163,17 @@  record_free (void *p, size_t size)
 #define SHARED
 #include <ldsodefs.h>
 
+/* We need to make available these internal functions under their
+   public names.  */
+#define __pkey_alloc pkey_alloc
+#define __pkey_free pkey_free
+#define __pkey_get pkey_get
+#define __pkey_mprotect pkey_mprotect
+#define __pkey_set pkey_set
+
 /* Create our own version of GLRO (dl_protmem).  */
 static struct rtld_protmem *dl_protmem;
+static int dl_protmem_key;
 #undef GLRO
 #define GLRO(x) x
 
@@ -265,6 +274,7 @@  do_test (void)
 {
   dl_protmem = _dl_protmem_bootstrap ();
   _dl_protmem_init ();
+  _dl_protmem_init_2 ();
 
   /* Perform a random allocations in a loop.  */
   srand (1);
diff --git a/elf/tst-relro-linkmap-disabled-mod1.c b/elf/tst-relro-linkmap-disabled-mod1.c
new file mode 100644
index 0000000000..7edc206132
--- /dev/null
+++ b/elf/tst-relro-linkmap-disabled-mod1.c
@@ -0,0 +1,46 @@ 
+/* Module with the checking function for read-write link maps.
+   Copyright (C) 2023 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 <link.h>
+#include <stdio.h>
+#include <unistd.h>
+
+/* Export for use by the main program, to avoid copy relocations on
+   _r_debug.  */
+struct r_debug_extended *const r_debug_extended_address
+  = (struct r_debug_extended *) &_r_debug;
+
+/* The real definition is in the main program.  */
+void
+check_rw_link_maps (const char *context)
+{
+  puts ("error: check_relro_link_maps not interposed");
+  _exit (1);
+}
+
+static void __attribute__ ((constructor))
+init (void)
+{
+  check_rw_link_maps ("ELF fini (DSO)");
+}
+
+static void __attribute__ ((constructor))
+fini (void)
+{
+  check_rw_link_maps ("ELF destructor (DSO)");
+}
diff --git a/elf/tst-relro-linkmap-disabled-mod2.c b/elf/tst-relro-linkmap-disabled-mod2.c
new file mode 100644
index 0000000000..33d2e78542
--- /dev/null
+++ b/elf/tst-relro-linkmap-disabled-mod2.c
@@ -0,0 +1,2 @@ 
+/* Same checking as the first module, but loaded via dlopen.  */
+#include "tst-relro-linkmap-disabled-mod1.c"
diff --git a/elf/tst-relro-linkmap-disabled.c b/elf/tst-relro-linkmap-disabled.c
new file mode 100644
index 0000000000..1093097aff
--- /dev/null
+++ b/elf/tst-relro-linkmap-disabled.c
@@ -0,0 +1,64 @@ 
+/* Verify that link maps are writable if configured so by tunable.
+   Copyright (C) 2023 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 <support/check.h>
+#include <support/memprobe.h>
+#include <support/xdlfcn.h>
+
+/* Defined in tst-relro-linkmap-disabled-mod.so.  */
+extern struct r_debug_extended *const r_debug_extended_address;
+
+/* Check that link maps are writable in all namespaces.  */
+void
+check_rw_link_maps (const char *context)
+{
+  for (struct r_debug_extended *r = r_debug_extended_address;
+       r != NULL; r = r->r_next)
+    for (struct link_map *l = r->base.r_map; l != NULL; l = l->l_next)
+      support_memprobe_readwrite (context, l, sizeof (*l));
+}
+
+static int
+do_test (void)
+{
+  check_rw_link_maps ("initial");
+
+  /* This is supposed to fail.  */
+  TEST_VERIFY (dlopen ("tst-dlopenfailmod1.so", RTLD_LAZY) == NULL);
+  check_rw_link_maps ("after failed dlopen");
+
+  void *handle = xdlopen ("tst-relro-linkmap-disabled-mod2.so", RTLD_LAZY);
+  check_rw_link_maps ("after dlopen");
+  xdlclose (handle);
+  check_rw_link_maps ("after dlclose");
+
+  /* NB: no checking inside the namespace.  */
+  handle = xdlmopen (LM_ID_NEWLM, "tst-relro-linkmap-mod3.so", RTLD_LAZY);
+  check_rw_link_maps ("after dlmopen");
+  xdlclose (handle);
+  check_rw_link_maps ("after dlclose 2");
+
+  handle = xdlopen ("tst-relro-linkmap-disabled-mod2.so", RTLD_LAZY);
+  check_rw_link_maps ("after dlopen 2");
+  /* Run the destructor during process exit.  */
+
+  return 0;
+}
+
+#include <support/test-driver.c>
diff --git a/elf/tst-rtld-list-tunables.exp b/elf/tst-rtld-list-tunables.exp
index 2233ea9c7c..e5dee30916 100644
--- a/elf/tst-rtld-list-tunables.exp
+++ b/elf/tst-rtld-list-tunables.exp
@@ -14,3 +14,4 @@  glibc.malloc.trim_threshold: 0x0 (min: 0x0, max: 0x[f]+)
 glibc.rtld.dynamic_sort: 2 (min: 1, max: 2)
 glibc.rtld.nns: 0x4 (min: 0x1, max: 0x10)
 glibc.rtld.optional_static_tls: 0x200 (min: 0x0, max: 0x[f]+)
+glibc.rtld.protmem: 2 (min: 0, max: 3)
diff --git a/manual/tunables.texi b/manual/tunables.texi
index 4ca0e42a11..5a638706b3 100644
--- a/manual/tunables.texi
+++ b/manual/tunables.texi
@@ -70,6 +70,7 @@  glibc.pthread.mutex_spin_count: 100 (min: 0, max: 32767)
 glibc.rtld.optional_static_tls: 0x200 (min: 0x0, max: 0xffffffffffffffff)
 glibc.malloc.tcache_max: 0x0 (min: 0x0, max: 0xffffffffffffffff)
 glibc.malloc.check: 0 (min: 0, max: 3)
+glibc.rtld.protmem: 2 (min: 0, max: 3)
 @end example
 
 @menu
@@ -334,6 +335,34 @@  changed once allocated at process startup.  The default allocation of
 optional static TLS is 512 bytes and is allocated in every thread.
 @end deftp
 
+@deftp Tunable glibc.rtld.protmem
+The dynamic linker supports various operating modes for its protected
+memory allocator.  The following settings are available.
+
+@table @code
+@item 0
+The protected memory allocator is disabled.  All memory remains writable
+during the life-time of the process.
+
+@item 1
+The protected memory allocator is enabled and unconditionally uses
+@code{mprotect} to switch protections on or off.
+
+@item 2
+The protected memory allocator is enabled and uses memory protection
+keys if supported by the system, and the memory protection key
+implementation provides full compatibility.  This is the default.
+
+@item 3
+The protected memory allocator is enabled.  If the system supports
+memory protection keys, they are used, even if there are
+incompatibilities.  Such incompatibilities exist on x86-64 because
+signal handlers disable access (including read access) to protected
+memory, which means that lazy binding will not work from signal handlers
+in this mode.
+@end table
+@end deftp
+
 @deftp Tunable glibc.rtld.dynamic_sort
 Sets the algorithm to use for DSO sorting, valid values are @samp{1} and
 @samp{2}.  For value of @samp{1}, an older O(n^3) algorithm is used, which is
diff --git a/nptl/pthread_create.c b/nptl/pthread_create.c
index 1ac8862ed2..e2b0fce3e6 100644
--- a/nptl/pthread_create.c
+++ b/nptl/pthread_create.c
@@ -376,6 +376,14 @@  start_thread (void *arg)
       __libc_fatal ("Fatal glibc error: rseq registration failed\n");
   }
 
+#ifdef SHARED
+  /* If the dynamic linker uses memory protection keys, new threads
+     may have to disable access because clone may have inherited
+     access if called from an write-enabled code region.  */
+  if (GLRO (dl_protmem_key) >= 0)
+    __pkey_set (GLRO (dl_protmem_key), PKEY_DISABLE_WRITE);
+#endif
+
 #ifndef __ASSUME_SET_ROBUST_LIST
   if (__nptl_set_robust_list_avail)
 #endif
diff --git a/sysdeps/generic/dl-protmem-pkey.h b/sysdeps/generic/dl-protmem-pkey.h
new file mode 100644
index 0000000000..3e42b491f8
--- /dev/null
+++ b/sysdeps/generic/dl-protmem-pkey.h
@@ -0,0 +1,20 @@ 
+/* Protection key support for the protected memory allocator.
+   Copyright (C) 2023 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/>.  */
+
+/* The generic implementation does not support memory protection keys.  */
+#define DL_PROTMEM_PKEY_SUPPORT 0
diff --git a/sysdeps/generic/ldsodefs.h b/sysdeps/generic/ldsodefs.h
index f47cdcf47b..028bb45b4a 100644
--- a/sysdeps/generic/ldsodefs.h
+++ b/sysdeps/generic/ldsodefs.h
@@ -678,6 +678,11 @@  struct rtld_global_ro
   EXTERN enum dso_sort_algorithm _dl_dso_sort_algo;
 
 #ifdef SHARED
+  /* Memory protection key for the memory allocator regions.  Used
+     during thread initialization, to revoke access if necessary.  Set
+     to -1 in _dl_protmem_init if protection keys are not available.  */
+  EXTERN int _dl_protmem_key;
+
   /* Pointer to the protected memory area.  */
   EXTERN struct rtld_protmem *_dl_protmem;
 
diff --git a/sysdeps/unix/sysv/linux/dl-protmem-pkey.h b/sysdeps/unix/sysv/linux/dl-protmem-pkey.h
new file mode 100644
index 0000000000..93dc3c5d4a
--- /dev/null
+++ b/sysdeps/unix/sysv/linux/dl-protmem-pkey.h
@@ -0,0 +1,23 @@ 
+/* Protection key support for the protected memory allocator.  Linux version.
+   Copyright (C) 2023 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/>.  */
+
+/* Linux supports the pkey_* interfaces.  */
+#define DL_PROTMEM_PKEY_SUPPORT 1
+
+/* Use a protection key if pkey_alloc succeeds.  */
+#define DL_PROTMEM_PKEY_ENABLE 1
diff --git a/sysdeps/unix/sysv/linux/dl-sysdep.c b/sysdeps/unix/sysv/linux/dl-sysdep.c
index 1b3dd869b5..e69cea2ecc 100644
--- a/sysdeps/unix/sysv/linux/dl-sysdep.c
+++ b/sysdeps/unix/sysv/linux/dl-sysdep.c
@@ -40,6 +40,7 @@ 
 #include <sys/utsname.h>
 #include <tls.h>
 #include <unistd.h>
+#include <dl-protmem.h>
 
 #include <dl-machine.h>
 #include <dl-hwcap-check.h>
@@ -108,6 +109,7 @@  _dl_sysdep_start (void **start_argptr,
   dl_hwcap_check ();
 
   __tunables_init (_environ);
+  _dl_protmem_init_2 ();
 
   /* Initialize DSO sorting algorithm after tunables.  */
   _dl_sort_maps_init ();
diff --git a/sysdeps/unix/sysv/linux/x86/dl-protmem-pkey.h b/sysdeps/unix/sysv/linux/x86/dl-protmem-pkey.h
new file mode 100644
index 0000000000..887374a3d9
--- /dev/null
+++ b/sysdeps/unix/sysv/linux/x86/dl-protmem-pkey.h
@@ -0,0 +1,26 @@ 
+/* Protection key support for the protected memory allocator.  x86 version.
+   Copyright (C) 2023 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/>.  */
+
+/* Linux supports the pkey_* interfaces.  */
+#define DL_PROTMEM_PKEY_SUPPORT 1
+
+/* Linux support is incompatible with signal handlers because the
+   kernel forces PKEY_DISABLE_ACCESS in signal handlers, which breaks
+   lazy binding and other dynamic linker features.  See bug 22396
+   comment 7.  */
+#define DL_PROTMEM_PKEY_ENABLE 0