diff mbox series

[v6,8/9] linux: Add support for PT_GNU_MUTABLE

Message ID 20250311171305.89091-9-adhemerval.zanella@linaro.org
State New
Headers show
Series Add support for memory sealing | expand

Commit Message

Adhemerval Zanella March 11, 2025, 5:09 p.m. UTC
Since memory sealing is applied for all the PT_LOAD segments,
a new program header is also supported so users can define a
region where sealing should not be applied (so users can later
initialize and change memory protection). The special section,
PT_GNU_MUTABLE (reference implemented at [1]),  marks a memory
region that should not be sealed if the GNU_PROPERTY_MEMORY_SEAL
attribute is present.

The section name starts with ".gnu.mutable" and has an alignment
and size of the defined maximum page size (-z max-page-size linker
option). For instance the code:

  #define GNU_MUTABLE_SECTION_NAME       ".gnu.mutable"

  unsigned char mutable_array1[64]
    __attribute__ ((section (GNU_MUTABLE_SECTION_NAME)))
       = { 0 };
  unsigned char mutable_array2[32]
    __attribute__ ((section (GNU_MUTABLE_SECTION_NAME)))
       = { 0 };

places both 'mutable_array1' and 'mutable_array2' on a page
aligned memory region with the size of a page.

The linker sets the alignment and size to simplify support for ABIs
with multiple page sizes, otherwise user would need to know the
maximum page size to correctly define the alignment and size of the
variable.

Checked on aarch64-linux-gnu and x86_64-linux-gnu.

[1] https://sourceware.org/git/?p=binutils-gdb.git;a=shortlog;h=refs/heads/azanella/pt_gnu_mutable
---
 configure                                     |  33 +++
 configure.ac                                  |  19 ++
 elf/dl-load.c                                 |   5 +
 elf/dl-reloc.c                                |  29 ++-
 elf/dl-support.c                              |   5 +
 elf/elf.h                                     |   2 +
 elf/rtld.c                                    |   5 +
 include/link.h                                |   4 +
 sysdeps/unix/sysv/linux/Makefile              |  23 ++
 .../sysv/linux/tst-dl_mseal-mutable-dlopen.c  |   1 +
 .../sysv/linux/tst-dl_mseal-mutable-mod.c     |  47 ++++
 .../sysv/linux/tst-dl_mseal-mutable-mod.h     |  33 +++
 .../sysv/linux/tst-dl_mseal-mutable-static.c  |   2 +
 .../unix/sysv/linux/tst-dl_mseal-mutable.c    | 242 ++++++++++++++++++
 14 files changed, 446 insertions(+), 4 deletions(-)
 create mode 100644 sysdeps/unix/sysv/linux/tst-dl_mseal-mutable-dlopen.c
 create mode 100644 sysdeps/unix/sysv/linux/tst-dl_mseal-mutable-mod.c
 create mode 100644 sysdeps/unix/sysv/linux/tst-dl_mseal-mutable-mod.h
 create mode 100644 sysdeps/unix/sysv/linux/tst-dl_mseal-mutable-static.c
 create mode 100644 sysdeps/unix/sysv/linux/tst-dl_mseal-mutable.c
diff mbox series

Patch

diff --git a/configure b/configure
index dda60ed91d..83c79147dd 100755
--- a/configure
+++ b/configure
@@ -7465,6 +7465,39 @@  have-z-memory-seal = $libc_cv_z_memory_seal"
 config_vars="$config_vars
 enable-memory-seal = $enable_memory_sealing"
 
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking PT_GNU_MUTABLE support" >&5
+printf %s "checking PT_GNU_MUTABLE support... " >&6; }
+if test ${libc_cv_gnu_mutable+y}
+then :
+  printf %s "(cached) " >&6
+else case e in #(
+  e) cat > conftest.c <<EOF
+int bar __attribute__ ((section (".gnu.mutable")));
+int main (void) { return bar; }
+EOF
+libc_cv_gnu_mutable=no
+if { ac_try='${CC-cc} $CFLAGS $CPPFLAGS $LDFLAGS
+		        $no_ssp -o conftest conftest.c
+			-nostdlib -nostartfiles
+			1>&5'
+  { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$ac_try\""; } >&5
+  (eval $ac_try) 2>&5
+  ac_status=$?
+  printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+  test $ac_status = 0; }; }
+then
+  if $READELF -lW conftest | grep 'GNU_MUTABLE' > /dev/null; then
+    libc_cv_gnu_mutable=yes
+  fi
+fi
+rm -r conftest* ;;
+esac
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $libc_cv_gnu_mutable" >&5
+printf "%s\n" "$libc_cv_gnu_mutable" >&6; }
+config_vars="$config_vars
+have-pt-gnu-mutable = $libc_cv_gnu_mutable"
+
 
 { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for GLOB_DAT reloc" >&5
 printf %s "checking for GLOB_DAT reloc... " >&6; }
diff --git a/configure.ac b/configure.ac
index d514179e1b..f62ccd4545 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1378,6 +1378,25 @@  fi
 LIBC_CONFIG_VAR([have-z-memory-seal], [$libc_cv_z_memory_seal])
 LIBC_CONFIG_VAR([enable-memory-seal], [$enable_memory_sealing])
 
+AC_CACHE_CHECK([PT_GNU_MUTABLE support],
+	       libc_cv_gnu_mutable, [dnl
+cat > conftest.c <<EOF
+int bar __attribute__ ((section (".gnu.mutable")));
+int main (void) { return bar; }
+EOF
+libc_cv_gnu_mutable=no
+if AC_TRY_COMMAND([${CC-cc} $CFLAGS $CPPFLAGS $LDFLAGS
+		        $no_ssp -o conftest conftest.c
+			-nostdlib -nostartfiles
+			1>&AS_MESSAGE_LOG_FD])
+then
+  if $READELF -lW conftest | grep 'GNU_MUTABLE' > /dev/null; then
+    libc_cv_gnu_mutable=yes
+  fi
+fi
+rm -r conftest*])
+LIBC_CONFIG_VAR([have-pt-gnu-mutable], [$libc_cv_gnu_mutable])
+
 
 AC_CACHE_CHECK(for GLOB_DAT reloc,
 	       libc_cv_has_glob_dat, [dnl
diff --git a/elf/dl-load.c b/elf/dl-load.c
index f104cc7544..a0d7d30e58 100644
--- a/elf/dl-load.c
+++ b/elf/dl-load.c
@@ -1220,6 +1220,11 @@  _dl_map_object_from_fd (const char *name, const char *origname, int fd,
 	  l->l_relro_addr = ph->p_vaddr;
 	  l->l_relro_size = ph->p_memsz;
 	  break;
+
+	case PT_GNU_MUTABLE:
+	  l->l_mutable_addr = ph->p_vaddr;
+	  l->l_mutable_size = ph->p_memsz;
+	  break;
 	}
 
     if (__glibc_unlikely (nloadcmds == 0))
diff --git a/elf/dl-reloc.c b/elf/dl-reloc.c
index 2b37676182..d706a57101 100644
--- a/elf/dl-reloc.c
+++ b/elf/dl-reloc.c
@@ -37,7 +37,6 @@ 
 # define bump_num_cache_relocations() ((void) 0)
 #endif
 
-
 /* We are trying to perform a static TLS relocation in MAP, but it was
    dynamically loaded.  This can only work if there is enough surplus in
    the static TLS area already allocated for each running thread.  If this
@@ -371,6 +370,29 @@  cannot apply additional memory protection after relocation");
     }
 }
 
+static void
+_dl_mseal_map_2 (const struct link_map *l, ElfW(Addr) map_start,
+		 ElfW(Addr) map_end)
+{
+  ElfW(Addr) mutable_start = 0, mutable_end = 0;
+  if (l->l_mutable_size != 0)
+    {
+      mutable_start = l->l_addr + l->l_mutable_addr;
+      mutable_end = mutable_start + l->l_mutable_size;
+    }
+
+  if (mutable_start >= map_start && mutable_end < map_end)
+    {
+      size_t seg1_size = mutable_start - map_start;
+      size_t seg2_size = map_end - mutable_end;
+      _dl_mseal ((void *) map_start, seg1_size, l->l_name);
+      if (seg2_size != 0)
+	_dl_mseal ((void *) mutable_end, seg2_size, l->l_name);
+    }
+  else
+    _dl_mseal ((void *) map_start, map_end - map_start, l->l_name);
+}
+
 static void
 _dl_mseal_map_1 (struct link_map *l, bool dep)
 {
@@ -388,8 +410,7 @@  _dl_mseal_map_1 (struct link_map *l, bool dep)
     return;
 
   if (l->l_contiguous)
-     _dl_mseal ((void *) l->l_map_start, l->l_map_end - l->l_map_start,
-		l->l_name);
+    _dl_mseal_map_2 (l, l->l_map_start, l->l_map_end);
   else
     {
       /* We can use the PT_LOAD segments because even if relro splits the
@@ -404,7 +425,7 @@  _dl_mseal_map_1 (struct link_map *l, bool dep)
 	      ElfW(Addr) mapstart = l->l_addr
 		  + (ph->p_vaddr & ~(GLRO(dl_pagesize) - 1));
 	      ElfW(Addr) allocend = l->l_addr + ph->p_vaddr + ph->p_memsz;
-	      _dl_mseal ((void *) mapstart, allocend - mapstart, l->l_name);
+	      _dl_mseal_map_2 (l, mapstart, allocend);
 	    }
 	    break;
 	}
diff --git a/elf/dl-support.c b/elf/dl-support.c
index ab74f3b51c..6227397237 100644
--- a/elf/dl-support.c
+++ b/elf/dl-support.c
@@ -334,6 +334,11 @@  _dl_non_dynamic_init (void)
 	_dl_main_map.l_relro_addr = ph->p_vaddr;
 	_dl_main_map.l_relro_size = ph->p_memsz;
 	break;
+
+      case PT_GNU_MUTABLE:
+	_dl_main_map.l_mutable_addr = ph->p_vaddr;
+	_dl_main_map.l_mutable_size = ph->p_memsz;
+	break;
       }
   /* Process program headers again, but scan them backwards so
      that PT_NOTE can be skipped if PT_GNU_PROPERTY exits.  */
diff --git a/elf/elf.h b/elf/elf.h
index f7d38eeffb..efde5bae73 100644
--- a/elf/elf.h
+++ b/elf/elf.h
@@ -729,6 +729,7 @@  typedef struct
 #define PT_GNU_RELRO	0x6474e552	/* Read-only after relocation */
 #define PT_GNU_PROPERTY	0x6474e553	/* GNU property */
 #define PT_GNU_SFRAME	0x6474e554	/* SFrame segment.  */
+#define PT_GNU_MUTABLE	0x6474f555	/* Like bss, but not immutable.  */
 #define PT_LOSUNW	0x6ffffffa
 #define PT_SUNWBSS	0x6ffffffa	/* Sun Specific segment */
 #define PT_SUNWSTACK	0x6ffffffb	/* Stack segment */
@@ -1352,6 +1353,7 @@  typedef struct
 
 /* Note section name of program property.   */
 #define NOTE_GNU_PROPERTY_SECTION_NAME ".note.gnu.property"
+#define GNU_MUTABLE_SECTION_NAME       ".gnu.mutable"
 
 /* Values used in GNU .note.gnu.property notes (NT_GNU_PROPERTY_TYPE_0).  */
 
diff --git a/elf/rtld.c b/elf/rtld.c
index 25058cb242..c2e2904aad 100644
--- a/elf/rtld.c
+++ b/elf/rtld.c
@@ -1209,6 +1209,11 @@  rtld_setup_main_map (struct link_map *main_map)
 	main_map->l_relro_addr = ph->p_vaddr;
 	main_map->l_relro_size = ph->p_memsz;
 	break;
+
+      case PT_GNU_MUTABLE:
+	main_map->l_mutable_addr = ph->p_vaddr;
+	main_map->l_mutable_size = ph->p_memsz;
+	break;
       }
   /* Process program headers again, but scan them backwards so
      that PT_NOTE can be skipped if PT_GNU_PROPERTY exits.  */
diff --git a/include/link.h b/include/link.h
index 677d82b38b..c77fbf10de 100644
--- a/include/link.h
+++ b/include/link.h
@@ -353,6 +353,10 @@  struct link_map
     ElfW(Addr) l_relro_addr;
     size_t l_relro_size;
 
+    /* Information used to not memory seal after relocations are done.  */
+    ElfW(Addr) l_mutable_addr;
+    size_t l_mutable_size;
+
     unsigned long long int l_serial;
   };
 
diff --git a/sysdeps/unix/sysv/linux/Makefile b/sysdeps/unix/sysv/linux/Makefile
index 8fe74be95f..2faa377fd5 100644
--- a/sysdeps/unix/sysv/linux/Makefile
+++ b/sysdeps/unix/sysv/linux/Makefile
@@ -708,6 +708,8 @@  modules-names += \
   tst-dl_mseal-dlopen-2-1 \
   tst-dl_mseal-mod-1 \
   tst-dl_mseal-mod-2 \
+  tst-dl_mseal-mutable-dlopen \
+  tst-dl_mseal-mutable-mod \
   tst-dl_mseal-preload \
   # modules-names
 
@@ -731,6 +733,10 @@  $(objpfx)tst-dl_mseal-noseal.out: \
   $(objpfx)tst-dl_mseal-dlopen-2.so \
   $(objpfx)tst-dl_mseal-dlopen-2-1.so
 
+$(objpfx)tst-dl_mseal-mutable.out: \
+  $(objpfx)tst-dl_mseal-mutable-mod.so \
+  $(objpfx)tst-dl_mseal-mutable-dlopen.so
+
 ifeq ($(enable-memory-seal),yes)
 CFLAGS-tst-dl_mseal.c += -DDEFAULT_MEMORY_SEAL
 CFLAGS-tst-dl_mseal-noseal.c += -DDEFAULT_MEMORY_SEAL
@@ -738,6 +744,8 @@  endif
 
 LDFLAGS-tst-dl_mseal = -Wl,--no-as-needed -Wl,-z,memory-seal
 LDFLAGS-tst-dl_mseal-static = -Wl,--no-as-needed -Wl,-z,memory-seal
+LDFLAGS-tst-dl_mseal-mutable = -Wl,--no-as-needed -Wl,-z,memory-seal
+LDFLAGS-tst-dl_mseal-mutable-static = -Wl,-z,memory-seal
 LDFLAGS-tst-dl_mseal-mod-1.so = -Wl,--no-as-needed -Wl,-z,memory-seal
 LDFLAGS-tst-dl_mseal-mod-2.so = -Wl,-z,memory-seal
 LDFLAGS-tst-dl_mseal-dlopen-1.so = -Wl,--no-as-needed
@@ -763,6 +771,21 @@  tst-dl_mseal-ARGS = -- $(host-test-program-cmd)
 tst-dl_mseal-static-ARGS = -- $(host-test-program-cmd)
 tst-dl_mseal-noseal-ARGS = -- $(host-test-program-cmd)
 tst-dl_mseal-static-noseal-ARGS = -- $(host-test-program-cmd)
+
+ifeq ($(have-pt-gnu-mutable),yes)
+tests-static += \
+  tst-dl_mseal-mutable-static \
+  # tests-static
+
+tests += \
+  tst-dl_mseal-mutable \
+  # tests
+
+LDFLAGS-tst-dl_mseal-mutable-mod.so = -Wl,-z,memory-seal
+LDFLAGS-tst-dl_mseal-mutable-dlopen.so = -Wl,-z,memory-seal
+
+$(objpfx)tst-dl_mseal-mutable: $(objpfx)tst-dl_mseal-mutable-mod.so
+endif # $(have-pt-gnu-mutable) == yes
 endif
 
 endif # $(subdir) == elf
diff --git a/sysdeps/unix/sysv/linux/tst-dl_mseal-mutable-dlopen.c b/sysdeps/unix/sysv/linux/tst-dl_mseal-mutable-dlopen.c
new file mode 100644
index 0000000000..325b2004b5
--- /dev/null
+++ b/sysdeps/unix/sysv/linux/tst-dl_mseal-mutable-dlopen.c
@@ -0,0 +1 @@ 
+#include "tst-dl_mseal-mutable-mod.c"
diff --git a/sysdeps/unix/sysv/linux/tst-dl_mseal-mutable-mod.c b/sysdeps/unix/sysv/linux/tst-dl_mseal-mutable-mod.c
new file mode 100644
index 0000000000..fb7cf03925
--- /dev/null
+++ b/sysdeps/unix/sysv/linux/tst-dl_mseal-mutable-mod.c
@@ -0,0 +1,47 @@ 
+/* Check if PT_OPENBSD_MUTABLE is correctly applied.
+   Copyright (C) 2025 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 <elf.h>
+#include "tst-dl_mseal-mutable-mod.h"
+
+static unsigned char mutable_array1[128]
+  __attribute__ ((section (GNU_MUTABLE_SECTION_NAME)))
+     = { 0 };
+static unsigned char mutable_array2[256]
+  __attribute__ ((section (GNU_MUTABLE_SECTION_NAME)))
+     = { 0 };
+
+static unsigned char immutable_array[256];
+
+struct array_t
+get_mutable_array1 (void)
+{
+  return (struct array_t) { mutable_array1, sizeof (mutable_array1) };
+}
+
+struct array_t
+get_mutable_array2 (void)
+{
+  return (struct array_t) { mutable_array2, sizeof (mutable_array2) };
+}
+
+struct array_t
+get_immutable_array (void)
+{
+  return (struct array_t) { immutable_array, sizeof (immutable_array) };
+}
diff --git a/sysdeps/unix/sysv/linux/tst-dl_mseal-mutable-mod.h b/sysdeps/unix/sysv/linux/tst-dl_mseal-mutable-mod.h
new file mode 100644
index 0000000000..cb0153756b
--- /dev/null
+++ b/sysdeps/unix/sysv/linux/tst-dl_mseal-mutable-mod.h
@@ -0,0 +1,33 @@ 
+/* Check if PT_OPENBSD_MUTABLE is correctly applied.
+   Copyright (C) 2025 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 <stddef.h>
+
+#define LIB_DLOPEN "tst-dl_mseal-mutable-dlopen.so"
+
+struct array_t
+{
+  unsigned char *arr;
+  size_t size;
+};
+
+typedef struct array_t (*get_array_t)(void);
+
+struct array_t get_mutable_array1 (void);
+struct array_t get_mutable_array2 (void);
+struct array_t get_immutable_array (void);
diff --git a/sysdeps/unix/sysv/linux/tst-dl_mseal-mutable-static.c b/sysdeps/unix/sysv/linux/tst-dl_mseal-mutable-static.c
new file mode 100644
index 0000000000..550ef3f056
--- /dev/null
+++ b/sysdeps/unix/sysv/linux/tst-dl_mseal-mutable-static.c
@@ -0,0 +1,2 @@ 
+#define TEST_STATIC 1
+#include "tst-dl_mseal-mutable.c"
diff --git a/sysdeps/unix/sysv/linux/tst-dl_mseal-mutable.c b/sysdeps/unix/sysv/linux/tst-dl_mseal-mutable.c
new file mode 100644
index 0000000000..2ea233d4d8
--- /dev/null
+++ b/sysdeps/unix/sysv/linux/tst-dl_mseal-mutable.c
@@ -0,0 +1,242 @@ 
+/* Check if PT_OPENBSD_MUTABLE is correctly applied.
+   Copyright (C) 2025 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 <link.h>
+#include <setjmp.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/mman.h>
+
+#include <libc-pointer-arith.h>
+#include <support/check.h>
+#include <support/test-driver.h>
+#include <support/xdlfcn.h>
+#include <support/xsignal.h>
+#include <support/xunistd.h>
+
+#include "tst-dl_mseal-mutable-mod.h"
+
+static long int pagesize;
+
+/* To check if the protection flags are correctly set, the thread tries
+   read/writes on it and checks if a SIGSEGV is generated.  */
+
+static volatile sig_atomic_t signal_jump_set;
+static sigjmp_buf signal_jmp_buf;
+
+static void
+sigsegv_handler (int sig)
+{
+  if (signal_jump_set == 0)
+    return;
+
+  siglongjmp (signal_jmp_buf, sig);
+}
+
+static bool
+try_access_buf (unsigned char *ptr, bool write)
+{
+  signal_jump_set = true;
+
+  bool failed = sigsetjmp (signal_jmp_buf, 0) != 0;
+  if (!failed)
+    {
+      if (write)
+	*(volatile unsigned char *)(ptr) = 'x';
+      else
+	*(volatile unsigned char *)(ptr);
+    }
+
+  signal_jump_set = false;
+  return !failed;
+}
+
+struct range_t
+{
+  const char *name;
+  unsigned char *start;
+  size_t size;
+  bool found;
+};
+
+static int
+callback (struct dl_phdr_info *info, size_t size, void *data)
+{
+  struct range_t *range = data;
+  if (strcmp (info->dlpi_name, range->name) != 0)
+    return 0;
+
+  for (size_t i = 0; i < info->dlpi_phnum; i++)
+    if (info->dlpi_phdr[i].p_type == PT_GNU_MUTABLE)
+      {
+	range->start = (unsigned char *) info->dlpi_phdr[i].p_vaddr;
+	range->size = info->dlpi_phdr[i].p_memsz;
+	range->found = true;
+	break;
+      }
+
+  return 0;
+}
+
+static bool
+find_mutable_range (void *addr, struct range_t *range)
+{
+  struct dl_find_object dlfo;
+  if (_dl_find_object (addr, &dlfo) != 0)
+    return false;
+
+  range->name = dlfo.dlfo_link_map->l_name;
+  range->found = false;
+  dl_iterate_phdr (callback, range);
+  if (range->found)
+    range->start = dlfo.dlfo_link_map->l_addr + range->start;
+
+  return range->found;
+}
+
+static bool
+__attribute_used__
+try_read_buf (unsigned char *ptr)
+{
+  return try_access_buf (ptr, false);
+}
+
+static bool
+__attribute_used__
+try_write_buf (unsigned char *ptr)
+{
+  return try_access_buf (ptr, true);
+}
+
+/* The GNU_MUTABLE_SECTION_NAME section is page-aligned and with a size
+   multiple of page size.  */
+
+unsigned char mutable_array1[64]
+  __attribute__ ((section (GNU_MUTABLE_SECTION_NAME)))
+     = { 0 };
+unsigned char mutable_array2[32]
+  __attribute__ ((section (GNU_MUTABLE_SECTION_NAME)))
+     = { 0 };
+
+unsigned char immutable_array[128];
+
+static void
+check_array (struct array_t *arr)
+{
+  TEST_COMPARE (try_write_buf (arr->arr), false);
+  TEST_COMPARE (try_write_buf (&arr->arr[arr->size/2]), false);
+  TEST_COMPARE (try_write_buf (&arr->arr[arr->size-1]), false);
+}
+
+static void
+check_mutable (struct array_t *mut1,
+	       struct array_t *mut2,
+	       struct array_t *imut)
+{
+  struct range_t range1;
+  struct range_t range2;
+
+  TEST_VERIFY_EXIT (find_mutable_range (mut1->arr, &range1));
+  TEST_VERIFY (mut1->arr >= range1.start);
+  TEST_VERIFY (mut1->arr + mut1->size <= range1.start + range1.size);
+
+  TEST_VERIFY_EXIT (find_mutable_range (mut2->arr, &range2));
+  TEST_VERIFY (mut2->arr >= range2.start);
+  TEST_VERIFY (mut2->arr + mut2->size <= range2.start + range2.size);
+
+  /* Assume that both array will be placed in the same page since their
+     combined size is less than pagesize.  */
+  TEST_VERIFY (range1.start == range2.start);
+  TEST_VERIFY (range2.size == range2.size);
+
+  if (test_verbose > 0)
+    printf ("mutable region: %-30s - %p-%p\n",
+	    range1.name[0] == '\0' ? "main program" : basename (range1.name),
+	    range1.start,
+	    range1.start + range1.size);
+
+  memset (mut1->arr, 0xaa, mut1->size);
+  memset (mut2->arr, 0xbb, mut2->size);
+  memset (imut->arr, 0xcc, imut->size);
+
+  /* Sanity check, imut should be immutable.  */
+  {
+    void *start = PTR_ALIGN_DOWN (imut->arr, pagesize);
+    TEST_COMPARE (mprotect (start, pagesize, PROT_READ), -1);
+    TEST_COMPARE (errno, EPERM);
+  }
+
+  /* Change permission of mutable region to just allow read.  */
+  xmprotect ((void *)range1.start, range1.size, PROT_READ);
+
+  check_array (mut1);
+  check_array (mut2);
+}
+
+static int
+do_test (void)
+{
+  pagesize = xsysconf (_SC_PAGESIZE);
+
+  {
+    struct sigaction sa = {
+      .sa_handler = sigsegv_handler,
+      .sa_flags = SA_NODEFER,
+    };
+    sigemptyset (&sa.sa_mask);
+    xsigaction (SIGSEGV, &sa, NULL);
+  }
+
+#define ARR_TO_RANGE(__arr) \
+  &((struct array_t) { __arr, sizeof (__arr) })
+
+  check_mutable (ARR_TO_RANGE (mutable_array1),
+		 ARR_TO_RANGE (mutable_array2),
+		 ARR_TO_RANGE (immutable_array));
+
+#ifndef TEST_STATIC
+  {
+    struct array_t mut1 = get_mutable_array1 ();
+    struct array_t mut2 = get_mutable_array2 ();
+    struct array_t imut = get_immutable_array ();
+    check_mutable (&mut1, &mut2, &imut);
+  }
+
+  {
+    void *h = xdlopen (LIB_DLOPEN, RTLD_NOW | RTLD_NODELETE);
+
+#define GET_ARRAY_DLOPEN(__name) \
+    ({ \
+       get_array_t f = xdlsym (h, __name); \
+       f(); \
+    })
+
+    struct array_t mut1 = GET_ARRAY_DLOPEN ("get_mutable_array1");
+    struct array_t mut2 = GET_ARRAY_DLOPEN ("get_mutable_array2");
+    struct array_t imut = GET_ARRAY_DLOPEN ("get_immutable_array");
+    check_mutable (&mut1, &mut2, &imut);
+  }
+#endif
+
+  return 0;
+}
+
+#include <support/test-driver.c>