diff --git a/elf/Makefile b/elf/Makefile
index b3fecb9997..af05d6707b 100644
--- a/elf/Makefile
+++ b/elf/Makefile
@@ -73,6 +73,7 @@ dl-routines = \
   dl-origin \
   dl-printf \
   dl-profile \
+  dl-protmem \
   dl-reloc \
   dl-runtime \
   dl-scope \
@@ -117,7 +118,7 @@ elide-routines.os = \
   # elide-routines.os
 
 # These object files are only included in the dynamically-linked libc.
-shared-only-routines = libc-dl_find_object
+shared-only-routines = dl-protmem libc-dl_find_object
 
 # ld.so uses those routines, plus some special stuff for being the program
 # interpreter and operating independent of libc.
@@ -495,6 +496,7 @@ tests-internal += \
   tst-dl_find_object-threads \
   tst-dlmopen2 \
   tst-ptrguard1 \
+  tst-relro-linkmap \
   tst-stackguard1 \
   tst-tls-surplus \
   tst-tls3 \
@@ -859,6 +861,9 @@ modules-names += \
   tst-null-argv-lib \
   tst-p_alignmod-base \
   tst-p_alignmod3 \
+  tst-relro-linkmap-mod1 \
+  tst-relro-linkmap-mod2 \
+  tst-relro-linkmap-mod3 \
   tst-relsort1mod1 \
   tst-relsort1mod2 \
   tst-ro-dynamic-mod \
@@ -3010,3 +3015,8 @@ LDFLAGS-tst-dlclose-lazy-mod1.so = -Wl,-z,lazy,--no-as-needed
 $(objpfx)tst-dlclose-lazy-mod1.so: $(objpfx)tst-dlclose-lazy-mod2.so
 $(objpfx)tst-dlclose-lazy.out: \
   $(objpfx)tst-dlclose-lazy-mod1.so $(objpfx)tst-dlclose-lazy-mod2.so
+
+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
diff --git a/elf/dl-close.c b/elf/dl-close.c
index 58a69f9b42..7fbd9a4814 100644
--- a/elf/dl-close.c
+++ b/elf/dl-close.c
@@ -33,6 +33,7 @@
 #include <tls.h>
 #include <stap-probe.h>
 #include <dl-find_object.h>
+#include <dl-protmem.h>
 
 #include <dl-unmap-segments.h>
 
@@ -130,6 +131,9 @@ _dl_close_worker (struct link_map_private *map, bool force)
       return;
     }
 
+  /* Actual changes are about to happen.  */
+  _dl_protmem_begin ();
+
   Lmid_t nsid = map->l_ns;
   struct link_namespaces *ns = &GL(dl_ns)[nsid];
 
@@ -249,7 +253,10 @@ _dl_close_worker (struct link_map_private *map, bool force)
 
 	  /* Call its termination function.  Do not do it for
 	     half-cooked objects.  Temporarily disable exception
-	     handling, so that errors are fatal.  */
+	     handling, so that errors are fatal.
+
+	     Link maps are writable during this call, but avoiding
+	     that is probably too costly.  */
 	  if (imap->l_rw->l_init_called)
 	    _dl_catch_exception (NULL, _dl_call_fini, imap);
 
@@ -343,8 +350,11 @@ _dl_close_worker (struct link_map_private *map, bool force)
 		  newp = (struct r_scope_elem **)
 		    malloc (new_size * sizeof (struct r_scope_elem *));
 		  if (newp == NULL)
-		    _dl_signal_error (ENOMEM, "dlclose", NULL,
-				      N_("cannot create scope list"));
+		    {
+		      _dl_protmem_end ();
+		      _dl_signal_error (ENOMEM, "dlclose", NULL,
+					N_("cannot create scope list"));
+		    }
 		}
 
 	      /* Copy over the remaining scope elements.  */
@@ -698,7 +708,7 @@ _dl_close_worker (struct link_map_private *map, bool force)
 	  if (imap == GL(dl_initfirst))
 	    GL(dl_initfirst) = NULL;
 
-	  free (imap);
+	  _dl_free_object (imap);
 	}
     }
 
@@ -743,6 +753,8 @@ _dl_close_worker (struct link_map_private *map, bool force)
     goto retry;
 
   dl_close_state = not_pending;
+
+  _dl_protmem_end ();
 }
 
 
diff --git a/elf/dl-libc_freeres.c b/elf/dl-libc_freeres.c
index 65fc70837a..88c0e444b8 100644
--- a/elf/dl-libc_freeres.c
+++ b/elf/dl-libc_freeres.c
@@ -18,6 +18,7 @@
 
 #include <ldsodefs.h>
 #include <dl-find_object.h>
+#include <dl-protmem.h>
 
 static bool
 free_slotinfo (struct dtv_slotinfo_list **elemp)
@@ -52,6 +53,10 @@ __rtld_libc_freeres (void)
   struct link_map_private *l;
   struct r_search_path_elem *d;
 
+  /* We are about to write to link maps.  This is not paired with
+     _dl_protmem_end because the process is going away anyway.  */
+  _dl_protmem_begin ();
+
   /* Remove all search directories.  */
   d = GL(dl_all_dirs);
   while (d != GLRO(dl_init_all_dirs))
diff --git a/elf/dl-load.c b/elf/dl-load.c
index 2c9809cc6f..3d446614cb 100644
--- a/elf/dl-load.c
+++ b/elf/dl-load.c
@@ -33,6 +33,7 @@
 #include <sys/types.h>
 #include <gnu/lib-names.h>
 #include <alloc_buffer.h>
+#include <dl-protmem.h>
 
 /* Type for the buffer we put the ELF header and hopefully the program
    header.  This buffer does not really have to be too large.  In most
@@ -943,7 +944,8 @@ _dl_map_object_from_fd (const char *name, const char *origname, int fd,
 	    free (l->l_libname);
 	  if (l != NULL && l->l_phdr_allocated)
 	    free ((void *) l->l_phdr);
-	  free (l);
+	  if (l != NULL)
+	    _dl_free_object (l);
 	  free (realname);
 	  _dl_signal_error (errval, name, NULL, errstring);
 	}
@@ -2247,6 +2249,22 @@ add_path (struct add_path_state *p, const struct r_search_path_struct *sps,
     }
 }
 
+/* Wrap cache_rpath to unprotect memory first if necessary.  */
+static bool
+cache_rpath_unprotect (struct link_map_private *l,
+		       struct r_search_path_struct *sp,
+		       int tag,
+		       const char *what,
+		       bool *unprotected)
+{
+  if (sp->dirs == NULL && !*unprotected)
+    {
+      _dl_protmem_begin ();
+      *unprotected = true;
+    }
+  return cache_rpath (l, sp, tag, what);
+}
+
 void
 _dl_rtld_di_serinfo (struct link_map_private *loader, Dl_serinfo *si,
 		     bool counting)
@@ -2264,6 +2282,7 @@ _dl_rtld_di_serinfo (struct link_map_private *loader, Dl_serinfo *si,
       .si = si,
       .allocptr = (char *) &si->dls_serpath[si->dls_cnt]
     };
+  bool unprotected = false;
 
 # define add_path(p, sps, flags) add_path(p, sps, 0) /* XXX */
 
@@ -2276,7 +2295,8 @@ _dl_rtld_di_serinfo (struct link_map_private *loader, Dl_serinfo *si,
       struct link_map_private *l = loader;
       do
 	{
-	  if (cache_rpath (l, &l->l_rpath_dirs, DT_RPATH, "RPATH"))
+	  if (cache_rpath_unprotect (l, &l->l_rpath_dirs, DT_RPATH,
+				     "RPATH", &unprotected))
 	    add_path (&p, &l->l_rpath_dirs, XXX_RPATH);
 	  l = l->l_loader;
 	}
@@ -2287,7 +2307,8 @@ _dl_rtld_di_serinfo (struct link_map_private *loader, Dl_serinfo *si,
 	{
 	  l = GL(dl_ns)[LM_ID_BASE]._ns_loaded;
 	  if (l != NULL && l->l_type != lt_loaded && l != loader)
-	    if (cache_rpath (l, &l->l_rpath_dirs, DT_RPATH, "RPATH"))
+	    if (cache_rpath_unprotect (l, &l->l_rpath_dirs, DT_RPATH,
+				       "RPATH", &unprotected))
 	      add_path (&p, &l->l_rpath_dirs, XXX_RPATH);
 	}
     }
@@ -2296,7 +2317,8 @@ _dl_rtld_di_serinfo (struct link_map_private *loader, Dl_serinfo *si,
   add_path (&p, &__rtld_env_path_list, XXX_ENV);
 
   /* Look at the RUNPATH information for this binary.  */
-  if (cache_rpath (loader, &loader->l_runpath_dirs, DT_RUNPATH, "RUNPATH"))
+  if (cache_rpath_unprotect (loader, &loader->l_runpath_dirs, DT_RUNPATH,
+			     "RUNPATH", &unprotected))
     add_path (&p, &loader->l_runpath_dirs, XXX_RUNPATH);
 
   /* XXX
@@ -2311,4 +2333,7 @@ _dl_rtld_di_serinfo (struct link_map_private *loader, Dl_serinfo *si,
     /* Count the struct size before the string area, which we didn't
        know before we completed dls_cnt.  */
     si->dls_size += (char *) &si->dls_serpath[si->dls_cnt] - (char *) si;
+
+  if (unprotected)
+    _dl_protmem_end ();
 }
diff --git a/elf/dl-object.c b/elf/dl-object.c
index 0741371b80..0ea3f6e2da 100644
--- a/elf/dl-object.c
+++ b/elf/dl-object.c
@@ -21,6 +21,7 @@
 #include <stdlib.h>
 #include <unistd.h>
 #include <ldsodefs.h>
+#include <dl-protmem.h>
 
 #include <assert.h>
 
@@ -89,15 +90,19 @@ _dl_new_object (char *realname, const char *libname, int type,
 # define audit_space 0
 #endif
 
-  new = calloc (sizeof (*new)
-		+ sizeof (struct link_map_private *)
-		+ sizeof (*newname) + libname_len, 1);
+  size_t l_size = (sizeof (*new)
+		   + sizeof (struct link_map_private *)
+		   + sizeof (*newname) + libname_len);
+
+  new = _dl_protmem_allocate (l_size);
   if (new == NULL)
     return NULL;
+  memset (new, 0, sizeof (*new));
+  new->l_size = l_size;
   new->l_rw = calloc (1, sizeof (*new->l_rw) + audit_space);
   if (new->l_rw == NULL)
     {
-      free (new);
+      _dl_protmem_free (new, l_size);
       return NULL;
     }
 
@@ -108,7 +113,7 @@ _dl_new_object (char *realname, const char *libname, int type,
   new->l_libname = newname
     = (struct libname_list *) (new->l_symbolic_searchlist.r_list + 1);
   newname->name = (char *) memcpy (newname + 1, libname, libname_len);
-  /* newname->next = NULL;	We use calloc therefore not necessary.  */
+  newname->next = NULL;
   newname->dont_free = 1;
 
   /* When we create the executable link map, or a VDSO link map, we start
@@ -143,12 +148,9 @@ _dl_new_object (char *realname, const char *libname, int type,
 
 #ifdef SHARED
   for (unsigned int cnt = 0; cnt < naudit; ++cnt)
-    /* No need to initialize bindflags due to calloc.  */
     link_map_audit_state (new, cnt)->cookie = (uintptr_t) new;
 #endif
 
-  /* new->l_global = 0;	We use calloc therefore not necessary.  */
-
   /* Use the 'l_scope_mem' array by default for the 'l_scope'
      information.  If we need more entries we will allocate a large
      array dynamically.  */
@@ -267,3 +269,9 @@ _dl_new_object (char *realname, const char *libname, int type,
 
   return new;
 }
+
+void
+_dl_free_object (struct link_map_private *l)
+{
+  _dl_protmem_free (l, l->l_size);
+}
diff --git a/elf/dl-open.c b/elf/dl-open.c
index cfa508a419..c2560f7eae 100644
--- a/elf/dl-open.c
+++ b/elf/dl-open.c
@@ -37,6 +37,7 @@
 #include <libc-early-init.h>
 #include <gnu/lib-names.h>
 #include <dl-find_object.h>
+#include <dl-protmem.h>
 
 #include <dl-prop.h>
 
@@ -174,6 +175,8 @@ add_to_global_update (struct link_map_private *new)
 {
   struct link_namespaces *ns = &GL (dl_ns)[new->l_ns];
 
+  _dl_protmem_begin ();
+
   /* Now add the new entries.  */
   unsigned int new_nlist = ns->_ns_main_searchlist->r_nlist;
   for (unsigned int cnt = 0; cnt < new->l_searchlist.r_nlist; ++cnt)
@@ -204,6 +207,8 @@ add_to_global_update (struct link_map_private *new)
 
   atomic_write_barrier ();
   ns->_ns_main_searchlist->r_nlist = new_nlist;
+
+  _dl_protmem_end ();
 }
 
 /* Search link maps in all namespaces for the DSO that contains the object at
@@ -516,6 +521,11 @@ dl_open_worker_begin (void *a)
 	args->nsid = call_map->l_ns;
     }
 
+  /* Prepare for link map updates.  If dl_open_worker below returns
+     normally, a matching _dl_protmem_end call is performed there.  On
+     an exception, the handler in the caller has to perform it.  */
+  _dl_protmem_begin ();
+
   /* The namespace ID is now known.  Keep track of whether libc.so was
      already loaded, to determine whether it is necessary to call the
      early initialization routine (or clear libc_map on error).  */
@@ -789,6 +799,10 @@ dl_open_worker (void *a)
       _dl_signal_exception (err, &ex, NULL);
   }
 
+  /* Make state read-only before running user code in ELF
+     constructors.  */
+  _dl_protmem_end ();
+
   if (!args->worker_continue)
     return;
 
@@ -922,6 +936,10 @@ no more namespaces available for dlmopen()"));
 	     the flag here.  */
 	}
 
+      /* Due to the exception, we did not end the protmem transaction
+	 before.  */
+      _dl_protmem_end ();
+
       /* Release the lock.  */
       __rtld_lock_unlock_recursive (GL(dl_load_lock));
 
diff --git a/elf/dl-protmem-internal.h b/elf/dl-protmem-internal.h
new file mode 100644
index 0000000000..ce50d174a6
--- /dev/null
+++ b/elf/dl-protmem-internal.h
@@ -0,0 +1,39 @@
+/* Protected memory allocator for ld.so.  Internal interfaces.
+   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/>.  */
+
+/* These declarations are needed by <dl-protmem_bootstrap.h>, which
+   has to be inlined into _dl_start.  */
+
+/* Header before all protected memory allocations.  */
+struct dl_protmem_header
+{
+  struct dl_protmem_header *next;
+  unsigned int size;
+};
+
+/* Singleton allocator state.  It also serves as the bootstrap
+   allocation.  */
+struct dl_protmem_state
+{
+  struct dl_protmem_header hdr;  /* For consistency with other allocations.  */
+  struct rtld_protmem protmem; /* GLRO (dl_protmem) points to this field.  */
+
+  /* Allocator state: Linked list of allocations.  Initially points to
+     this structure.  */
+  struct dl_protmem_header *root;
+};
diff --git a/elf/dl-protmem.c b/elf/dl-protmem.c
new file mode 100644
index 0000000000..f5a66868e6
--- /dev/null
+++ b/elf/dl-protmem.c
@@ -0,0 +1,132 @@
+/* Protected memory allocator for ld.so.
+   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 <ldsodefs.h>
+
+#include <dl-protmem.h>
+#include <dl-protmem-internal.h>
+
+#include <assert.h>
+#include <sys/mman.h>
+
+/* 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
+   dlopen would make a link map that is still being initialized
+   read-only.  */
+static unsigned int _dl_protmem_begin_count;
+
+static inline struct dl_protmem_state *
+_dl_protmem_state (void)
+{
+  return ((void *) GLRO (dl_protmem)
+          - offsetof (struct dl_protmem_state, protmem));
+}
+
+void
+_dl_protmem_init (void)
+{
+  /* Go back from the start of the protected memory area to the
+     wrapping bootstrap allocation.  */
+  struct dl_protmem_state *state = _dl_protmem_state ();
+  state->hdr.size = sizeof (struct dl_protmem_state);
+  state->root = &state->hdr;
+  _dl_protmem_begin_count = 1;
+}
+
+void *
+_dl_protmem_allocate (size_t size)
+{
+  assert (_dl_protmem_begin_count > 0);
+  assert (size > 0);
+
+  struct dl_protmem_header *hdr;
+
+  /* Add the header.  */
+  unsigned int total_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)
+    return NULL;
+  hdr->size = total_size;
+
+  /* Put the allocation on the list of allocations.  */
+  struct dl_protmem_state *state = _dl_protmem_state ();
+  hdr->next = state->root;
+  state->root = hdr;
+
+  /* Return aa pointer to the user data.  */
+  return (char *) hdr + sizeof (*hdr);
+}
+
+void
+_dl_protmem_free (void *ptr, size_t size)
+{
+  assert (_dl_protmem_begin_count > 0);
+
+  struct dl_protmem_header *hdr = ptr - sizeof (*hdr);
+  assert (hdr->size == size + sizeof (*hdr));
+
+  struct dl_protmem_state *state = _dl_protmem_state ();
+  if (hdr == state->root)
+    {
+      state->root = hdr->next;
+      (void) __munmap (hdr, hdr->size);
+      return;
+    }
+
+  for (struct dl_protmem_header *p = state->root; p != NULL; p = p ->next)
+    if (p->next == hdr)
+      {
+        p->next = hdr->next;
+        (void) __munmap (hdr, hdr->size);
+        return;
+      }
+  _dl_fatal_printf ("\
+Fatal glibc error: Protected memory allocation not found during free\n");
+}
+
+void
+_dl_protmem_begin (void)
+{
+  if (_dl_protmem_begin_count++ > 0)
+    return;
+
+  struct dl_protmem_state *state = _dl_protmem_state ();
+  for (struct dl_protmem_header *hdr = state->root;
+       hdr != NULL; hdr = hdr->next)
+    if (__mprotect (hdr, hdr->size, PROT_READ | PROT_WRITE) != 0)
+      _dl_signal_error (ENOMEM, NULL, NULL,
+                        "Cannot make protected memory writable");
+}
+
+void
+_dl_protmem_end (void)
+{
+  if (--_dl_protmem_begin_count > 0)
+    return;
+
+  struct dl_protmem_state *state = _dl_protmem_state ();
+  for (struct dl_protmem_header *hdr = state->root;
+       hdr != NULL; hdr = hdr->next)
+    /* If the mapping is left read-write, this is not fatal.  */
+    (void) __mprotect (hdr, hdr->size, PROT_READ);
+}
diff --git a/elf/dl-protmem.h b/elf/dl-protmem.h
new file mode 100644
index 0000000000..59aeaf630d
--- /dev/null
+++ b/elf/dl-protmem.h
@@ -0,0 +1,93 @@
+/* Protected memory allocator for ld.so.
+   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 protected memory allocation manages the memory for the GLPM
+   variables (in shared builds), and for additional memory managed by
+   _dl_protmem_allocate and _dl_protmem_free.
+
+   After a call to _dl_protmem_begin and until the matching call to
+   _dl_protmem_end, the GLPM variables and memory allocated using
+   _dl_protmem_allocate is writable.  _dl_protmem_begin and
+   _dl_protmem_end calls can be nested.  In this case, only the
+   outermost _dl_protmem_end call makes memory read-only.  */
+
+#ifndef DL_PROTMEM_H
+#define DL_PROTMEM_H
+
+#include <stddef.h>
+
+#ifdef SHARED
+/* Must be called after _dl_allocate_rtld_map and before any of the
+   functions below.  Implies the first _dl_protmem_begin call.  */
+void _dl_protmem_init (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.  */
+void _dl_protmem_free (void *ptr, size_t size) attribute_hidden;
+
+/* Allocate protected memory of SIZE bytes.  Returns NULL on
+   allocation failure.  Protected memory must be writable when this
+   function is called.  The allocation will be writable and contains
+   unspecified bytes (similar to malloc).  */
+void *_dl_protmem_allocate (size_t size) attribute_hidden
+  __attribute_malloc__ __attribute_alloc_size__ ((1))
+  __attr_dealloc (_dl_protmem_free, 1);
+
+/* _dl_protmem_begin makes protected memory writable, and
+   _dl_protmem_end makes it read-only again. Calls to these functions
+   must be paired.  Within this region, protected memory is writable.
+   See the initial description above.
+
+   Failure to make memory writable in _dl_protmem_end is communicated
+   via an ld.so exception, typically resulting in a dlopen failure.
+   This can happen after a call to fork if memory overcommitment is
+   disabled.  */
+void _dl_protmem_begin (void) attribute_hidden;
+void _dl_protmem_end (void) attribute_hidden;
+
+#else /*!SHARED */
+/* The protected memory allocator does not exist for static builds.
+   Use malloc directly.  */
+
+#include <stdlib.h>
+
+static inline void *
+_dl_protmem_allocate (size_t size)
+{
+  return calloc (size, 1);
+}
+
+static inline void
+_dl_protmem_free (void *ptr, size_t size)
+{
+  free (ptr);
+}
+
+static inline void
+_dl_protmem_begin (void)
+{
+}
+
+static inline void
+_dl_protmem_end (void)
+{
+}
+#endif /* !SHARED */
+
+#endif /* DL_PROTMEM_H */
diff --git a/elf/dl-protmem_bootstrap.h b/elf/dl-protmem_bootstrap.h
index 2ba0973d07..a9d763bc7b 100644
--- a/elf/dl-protmem_bootstrap.h
+++ b/elf/dl-protmem_bootstrap.h
@@ -17,6 +17,7 @@
    <https://www.gnu.org/licenses/>.  */
 
 #include <dl-early_mmap.h>
+#include <dl-protmem-internal.h>
 
 /* Return a pointer to the protected memory area, or NULL if
    allocation fails.  This function is called before self-relocation,
@@ -25,5 +26,10 @@
 static inline __attribute__ ((always_inline)) struct rtld_protmem *
 _dl_protmem_bootstrap (void)
 {
-  return _dl_early_mmap (sizeof (struct rtld_protmem));
+  /* The protected memory area is nested within the bootstrap
+     allocation.  */
+  struct dl_protmem_state *ptr = _dl_early_mmap (sizeof (*ptr));
+  if (ptr == NULL)
+    return NULL;
+  return &ptr->protmem;
 }
diff --git a/elf/rtld.c b/elf/rtld.c
index db06c85f39..9ce6406c04 100644
--- a/elf/rtld.c
+++ b/elf/rtld.c
@@ -54,6 +54,7 @@
 #include <dl-audit-check.h>
 #include <dl-call_tls_init_tp.h>
 #include <dl-protmem_bootstrap.h>
+#include <dl-protmem.h>
 
 #include <assert.h>
 
@@ -460,6 +461,10 @@ _dl_start_final (void *arg, struct dl_start_final_info *info)
   if (GLRO (dl_protmem) == NULL)
     _dl_fatal_printf ("Fatal glibc error: Cannot allocate link map\n");
 
+  /* Set up the protected memory allocator, transferring the rtld link
+     map allocation in GLRO (dl_rtld_map).  */
+  _dl_protmem_init ();
+
   __rtld_malloc_init_stubs ();
 
   /* Do not use an initializer for these members because it would
@@ -2385,6 +2390,11 @@ dl_main (const ElfW(Phdr) *phdr,
   /* Auditing checkpoint: we have added all objects.  */
   _dl_audit_activity_nsid (LM_ID_BASE, LA_ACT_CONSISTENT);
 
+  /* Most of the initialization work has happened by this point, and
+     it should not be necessary to make the link maps read-write after
+     this point.  */
+  _dl_protmem_end ();
+
   /* Notify the debugger all new objects are now ready to go.  We must re-get
      the address since by now the variable might be in another object.  */
   r = _dl_debug_update (LM_ID_BASE);
diff --git a/elf/tst-relro-linkmap-mod1.c b/elf/tst-relro-linkmap-mod1.c
new file mode 100644
index 0000000000..dd73d26936
--- /dev/null
+++ b/elf/tst-relro-linkmap-mod1.c
@@ -0,0 +1,42 @@
+/* Module with the checking function for read-only 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_relro_link_maps (const char *context)
+{
+  puts ("error: check_relro_link_maps not interposed");
+  _exit (1);
+}
+
+static void __attribute__ ((constructor))
+init (void)
+{
+  check_relro_link_maps ("ELF constructor (DSO)");
+}
+
+/* NB: destructor not checked.  Memory is writable when they run.  */
diff --git a/elf/tst-relro-linkmap-mod2.c b/elf/tst-relro-linkmap-mod2.c
new file mode 100644
index 0000000000..f022264ffd
--- /dev/null
+++ b/elf/tst-relro-linkmap-mod2.c
@@ -0,0 +1,2 @@
+/* Same checking as the first module, but loaded via dlopen.  */
+#include "tst-relro-linkmap-mod1.c"
diff --git a/elf/tst-relro-linkmap-mod3.c b/elf/tst-relro-linkmap-mod3.c
new file mode 100644
index 0000000000..b2b7349200
--- /dev/null
+++ b/elf/tst-relro-linkmap-mod3.c
@@ -0,0 +1,2 @@
+/* No checking possible because the check_relro_link_maps function
+   from the main program is inaccessible after dlopen.  */
diff --git a/elf/tst-relro-linkmap.c b/elf/tst-relro-linkmap.c
new file mode 100644
index 0000000000..08cfd32c52
--- /dev/null
+++ b/elf/tst-relro-linkmap.c
@@ -0,0 +1,112 @@
+/* Verify that link maps are read-only most of the time.
+   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 <support/memprobe.h>
+#include <support/check.h>
+#include <support/xdlfcn.h>
+#include <support/xunistd.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <support/support.h>
+
+static int do_test (void);
+#include <support/test-driver.c>
+
+/* This hack results in a definition of struct rtld_global_ro and
+   related data structures.  Do this after all the other header
+   inclusions, to minimize the impact.  This only works from the main
+   program due to tests-internal.  */
+#define SHARED
+#include <ldsodefs.h>
+
+/* Defined in tst-relro-linkmap-mod1.so.  */
+extern struct r_debug_extended *const r_debug_extended_address;
+
+/* Check that link maps are read-only in all namespaces.  */
+void
+check_relro_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)
+      {
+        char *ctx;
+
+        ctx = xasprintf ("%s: link map for %s", context, l->l_name);
+        support_memprobe_readonly (ctx, l_private (l),
+                                   sizeof (*l_private (l)));
+        free (ctx);
+        if (false)              /* Link map names are currently writable.  */
+          {
+            ctx = xasprintf ("%s: link map name for %s", context, l->l_name);
+            support_memprobe_readonly (ctx, l->l_name, strlen (l->l_name) + 1);
+            free (ctx);
+          }
+      }
+}
+
+static void __attribute__ ((constructor))
+init (void)
+{
+  check_relro_link_maps ("ELF constructor (main)");
+}
+
+static void __attribute__ ((destructor))
+deinit (void)
+{
+  /* _dl_fini does not make link maps writable.   */
+  check_relro_link_maps ("ELF destructor (main)");
+}
+
+static int
+do_test (void)
+{
+  check_relro_link_maps ("initial do_test");
+
+  /* Avoid copy relocations.  Do this from the main program because we
+     need access to internal headers.  */
+  {
+    struct rtld_global_ro *ro = xdlsym (RTLD_DEFAULT, "_rtld_global_ro");
+    check_relro_link_maps ("after _rtld_global_ro");
+    support_memprobe_readonly ("_rtld_global_ro", ro, sizeof (*ro));
+    support_memprobe_readonly ("GLPM", ro->_dl_protmem,
+                               sizeof (*ro->_dl_protmem));
+  }
+  support_memprobe_readwrite ("_rtld_global",
+                              xdlsym (RTLD_DEFAULT, "_rtld_global"),
+                              sizeof (struct rtld_global_ro));
+  check_relro_link_maps ("after _rtld_global");
+
+  /* This is supposed to fail.  */
+  TEST_VERIFY (dlopen ("tst-dlopenfailmod1.so", RTLD_LAZY) == NULL);
+  check_relro_link_maps ("after failed dlopen");
+
+  /* This should succeed.  */
+  void *handle = xdlopen ("tst-relro-linkmap-mod2.so", RTLD_LAZY);
+  check_relro_link_maps ("after successful dlopen");
+  xdlclose (handle);
+  check_relro_link_maps ("after dlclose 1");
+
+  handle = xdlmopen (LM_ID_NEWLM, "tst-relro-linkmap-mod3.so", RTLD_LAZY);
+  check_relro_link_maps ("after dlmopen");
+  xdlclose (handle);
+  check_relro_link_maps ("after dlclose 2");
+
+  return 0;
+}
diff --git a/include/link.h b/include/link.h
index a53c652941..e57cf93efd 100644
--- a/include/link.h
+++ b/include/link.h
@@ -164,6 +164,9 @@ struct link_map_private
        than one namespace.  */
     struct link_map_private *l_real;
 
+    /* Allocated size of this link map.  */
+    size_t l_size;
+
     /* Run-time writable fields.  */
     struct link_map_rw *l_rw;
 
diff --git a/sysdeps/generic/ldsodefs.h b/sysdeps/generic/ldsodefs.h
index 68f68c7bdb..7719e3af26 100644
--- a/sysdeps/generic/ldsodefs.h
+++ b/sysdeps/generic/ldsodefs.h
@@ -524,7 +524,10 @@ extern struct rtld_global _rtld_global __rtld_global_attribute__;
 #endif
 
 #ifdef SHARED
-/* Implementation structure for the protected memory area.  */
+/* Implementation structure for the protected memory area.  In static
+   builds, the protected memory area is just regular (.data) memory,
+   as there is no RELRO support anyway.  Some fields are only needed
+   for SHARED builds and are not included for static builds.  */
 struct rtld_protmem
 {
   /* Structure describing the dynamic linker itself.  */
@@ -1043,6 +1046,9 @@ struct link_map_private *_dl_new_object (char *realname,
 					 int mode, Lmid_t nsid)
      attribute_hidden;
 
+/* Deallocates the specified link map (only the link map itself).  */
+void _dl_free_object (struct link_map_private *) attribute_hidden;
+
 /* Relocate the given object (if it hasn't already been).
    SCOPE is passed to _dl_lookup_symbol in symbol lookups.
    If RTLD_LAZY is set in RELOC-MODE, don't relocate its PLT.  */
