diff --git a/elf/Makefile b/elf/Makefile
index 9f3fadc37e..406bb3ad23 100644
--- a/elf/Makefile
+++ b/elf/Makefile
@@ -223,7 +223,7 @@ tests += restest1 preloadtest loadfail multiload origtest resolvfail \
 	 tst-tls-ie tst-tls-ie-dlmopen argv0test \
 	 tst-glibc-hwcaps tst-glibc-hwcaps-prepend tst-glibc-hwcaps-mask \
 	 tst-tls20 tst-tls21 tst-dlmopen-dlerror tst-dlmopen-gethostbyname \
-	 tst-dl-is_dso
+	 tst-dl-is_dso tst-ro-dynamic
 #	 reldep9
 tests-internal += loadtest unload unload2 circleload1 \
 	 neededtest neededtest2 neededtest3 neededtest4 \
@@ -359,7 +359,7 @@ modules-names = testobj1 testobj2 testobj3 testobj4 testobj5 testobj6 \
 		libmarkermod4-1 libmarkermod4-2 libmarkermod4-3 libmarkermod4-4 \
 		tst-tls20mod-bad tst-tls21mod tst-dlmopen-dlerror-mod \
 		tst-auxvalmod \
-		tst-dlmopen-gethostbyname-mod \
+		tst-dlmopen-gethostbyname-mod tst-ro-dynamic-mod \
 
 # Most modules build with _ISOMAC defined, but those filtered out
 # depend on internal headers.
@@ -1908,3 +1908,10 @@ $(objpfx)tst-getauxval-static.out: $(objpfx)tst-auxvalmod.so
 tst-getauxval-static-ENV = LD_LIBRARY_PATH=$(objpfx):$(common-objpfx)
 
 $(objpfx)tst-dlmopen-gethostbyname.out: $(objpfx)tst-dlmopen-gethostbyname-mod.so
+
+LDFLAGS-tst-ro-dynamic-mod = -Wl,--version-script=tst-ro-dynamic-mod.map
+$(objpfx)tst-ro-dynamic-mod.so: tst-ro-dynamic-mod.map
+$(objpfx)tst-ro-dynamic.out: tst-ro-dynamic.sh $(objpfx)tst-ro-dynamic-mod.so \
+			     $(objpfx)ld.so
+	$(SHELL) $^ '$(test-wrapper-env)' '$(run_program_env)' > $@; \
+	$(evaluate-test)
diff --git a/elf/dl-load.c b/elf/dl-load.c
index 650e4edc35..c33da0467e 100644
--- a/elf/dl-load.c
+++ b/elf/dl-load.c
@@ -1124,6 +1124,9 @@ _dl_map_object_from_fd (const char *name, const char *origname, int fd,
     * executable.  Other platforms default to a nonexecutable stack and don't
     * need PT_GNU_STACK to do so.  */
    uint_fast16_t stack_flags = DEFAULT_STACK_PERMS;
+#ifndef DL_RO_DYN_SECTION
+  bool readonly_dynamic = false;
+#endif
 
   {
     /* Scan the program header table, collecting its load commands.  */
@@ -1149,6 +1152,9 @@ _dl_map_object_from_fd (const char *name, const char *origname, int fd,
 		 such a segment to avoid a crash later.  */
 	      l->l_ld = (void *) ph->p_vaddr;
 	      l->l_ldnum = ph->p_memsz / sizeof (ElfW(Dyn));
+#ifndef DL_RO_DYN_SECTION
+	      readonly_dynamic = (ph->p_flags & PF_W) == 0;
+#endif
 	    }
 	  break;
 
@@ -1292,6 +1298,14 @@ _dl_map_object_from_fd (const char *name, const char *origname, int fd,
   else
     l->l_ld = (ElfW(Dyn) *) ((ElfW(Addr)) l->l_ld + l->l_addr);
 
+#ifndef DL_RO_DYN_SECTION
+  if (readonly_dynamic && l->l_addr != 0)
+    {
+      errstring = N_("dynamic section of object file not writable");
+      goto lose;
+    }
+#endif
+
   elf_get_dynamic_info (l, NULL);
 
   /* Make sure we are not dlopen'ing an object that has the
diff --git a/elf/rtld.c b/elf/rtld.c
index 878e6480f4..d8149f2bca 100644
--- a/elf/rtld.c
+++ b/elf/rtld.c
@@ -1456,6 +1456,10 @@ dl_main (const ElfW(Phdr) *phdr,
   /* And it was opened directly.  */
   ++main_map->l_direct_opencount;
 
+#ifndef DL_RO_DYN_SECTION
+  bool readonly_dynamic = false;
+#endif
+
   /* Scan the program header table for the dynamic section.  */
   for (ph = phdr; ph < &phdr[phnum]; ++ph)
     switch (ph->p_type)
@@ -1468,6 +1472,9 @@ dl_main (const ElfW(Phdr) *phdr,
 	/* This tells us where to find the dynamic section,
 	   which tells us everything we need to do.  */
 	main_map->l_ld = (void *) main_map->l_addr + ph->p_vaddr;
+#ifndef DL_RO_DYN_SECTION
+	readonly_dynamic = (ph->p_flags & PF_W) == 0;
+#endif
 	break;
       case PT_INTERP:
 	/* This "interpreter segment" was used by the program loader to
@@ -1612,6 +1619,11 @@ dl_main (const ElfW(Phdr) *phdr,
 
   if (! rtld_is_main)
     {
+#ifndef DL_RO_DYN_SECTION
+      if (readonly_dynamic && main_map->l_addr != 0)
+	_dl_fatal_printf ("dynamic section of executable is not writable\n");
+#endif
+
       /* Extract the contents of the dynamic section for easy access.  */
       elf_get_dynamic_info (main_map, NULL);
 
diff --git a/elf/tst-ro-dynamic-mod.c b/elf/tst-ro-dynamic-mod.c
new file mode 100644
index 0000000000..2bc87d0c3c
--- /dev/null
+++ b/elf/tst-ro-dynamic-mod.c
@@ -0,0 +1 @@
+int foo = 0;
diff --git a/elf/tst-ro-dynamic-mod.map b/elf/tst-ro-dynamic-mod.map
new file mode 100644
index 0000000000..dac92e0b94
--- /dev/null
+++ b/elf/tst-ro-dynamic-mod.map
@@ -0,0 +1,13 @@
+SECTIONS
+{
+  .data : { *(.data) } :text
+  .bss : { *(.bss) } :text
+  .dynamic : { *(.dynamic) } :text :dynamic
+  .text : { *(.text) } :text
+}
+
+PHDRS
+{
+  text PT_LOAD FLAGS(5) FILEHDR PHDRS;
+  dynamic PT_DYNAMIC FLAGS(4);
+}
diff --git a/elf/tst-ro-dynamic.sh b/elf/tst-ro-dynamic.sh
new file mode 100644
index 0000000000..3d7144e0e2
--- /dev/null
+++ b/elf/tst-ro-dynamic.sh
@@ -0,0 +1,36 @@
+#!/bin/sh
+# Ensure ld.so does not crash when verifying DSO with read-only dynamic
+# section.
+# Copyright (C) 2021 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/>.
+
+dso=$1
+rtld=$2
+test_wrapper_env=$3
+run_program_env=$4
+
+${test_wrapper_env} \
+${run_program_env} \
+$rtld --verify ${dso}
+
+ret=$?
+
+# ld.so should fail gracefully by returning 1.
+if [ $ret -ne 1 ]; then
+  echo "ld.so returned $ret"
+  exit 1
+fi
