@@ -67,6 +67,7 @@ libsupport-routines = \
support_format_netent \
support_fuse \
support_isolate_in_subprocess \
+ support_memprobe \
support_mutex_pi_monotonic \
support_need_proc \
support_open_and_compare_file_bytes \
@@ -332,6 +333,7 @@ tests = \
tst-support_descriptors \
tst-support_format_dns_packet \
tst-support_fuse \
+ tst-support_memprobe \
tst-support_quote_blob \
tst-support_quote_blob_wide \
tst-support_quote_string \
new file mode 100644
@@ -0,0 +1,43 @@
+/* Probing memory for protection state.
+ 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/>. */
+
+#ifndef SUPPORT_MEMPROBE_H
+#define SUPPORT_MEMPROBE_H
+
+/* Probe access status of memory ranges. These functions record a
+ failure (but do not terminate the process) if the memory range does
+ not match the expected protection flags. */
+
+#include <stddef.h>
+
+/* Asserts that SIZE bytes at ADDRESS are inaccessible. CONTEXT
+ is used for reporting errors. */
+void support_memprobe_noaccess (const char *context, const void *address,
+ size_t size);
+
+/* Asserts that SIZE bytes at ADDRESS read read-only. CONTEXT is used
+ for reporting errors. */
+void support_memprobe_readonly (const char *context, const void *address,
+ size_t size);
+
+/* Asserts that SIZE bytes at ADDRESS are readable and writable.
+ CONTEXT is used for reporting errors. */
+void support_memprobe_readwrite (const char *context, const void *address,
+ size_t size);
+
+#endif /* SUPPORT_MEMPROBE_H */
new file mode 100644
@@ -0,0 +1,251 @@
+/* Probing memory for protection state.
+ 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/>. */
+
+/* The implementation uses vfork for probing. As a result, it can be
+ used for testing page protections controlled by memory protection
+ keys, despite their problematic interaction with signal handlers
+ (bug 22396). */
+
+#include <support/memprobe.h>
+
+#include <atomic.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <support/check.h>
+#include <support/support.h>
+#include <support/xunistd.h>
+#include <sys/param.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <sys/resource.h>
+
+#ifdef __linux__
+# include <sys/prctl.h>
+#endif
+
+/* Make are more complete attempt to disable core dumps, even in the
+ presence of core catchers that ignore RLIMIT_CORE. Used after
+ vfork. */
+static void
+disable_coredumps (void)
+{
+#ifdef __linux__
+ prctl (PR_SET_DUMPABLE, 0 /* SUID_DUMP_DISABLE */, 0, 0);
+#endif
+ struct rlimit rl = {};
+ setrlimit (RLIMIT_CORE, &rl);
+}
+
+/* Restores all signals to SIG_DFL and unblocks them. */
+static void
+memprobe_sig_dfl_unblock (void)
+{
+ for (int sig = 1; sig < _NSIG; ++sig)
+ /* Ignore errors for those signals whose handler cannot be changed. */
+ (void) signal (sig, SIG_DFL);
+ sigset_t sigallset;
+ sigfillset (&sigallset);
+ sigprocmask (SIG_UNBLOCK, &sigallset, NULL);
+}
+
+/* Performs a 4-byte probe at the address aligned down. The internal
+ glibc atomics do not necessarily support one-byte access.
+ Accessing more bytes with a no-op write results in the same page
+ fault effects because of the alignment. */
+static inline void
+write_probe_at (volatile char *address)
+{
+ /* Used as an argument to force the compiler to emit an actual no-op
+ atomic instruction. */
+ static volatile uint32_t zero = 0;
+ uint32_t *ptr = (uint32_t *) ((uintptr_t) address & ~(uintptr_t) 3);
+ atomic_fetch_add_relaxed (ptr, zero);
+}
+
+/* Attempt to read or write the entire range in one go. If DO_WRITE,
+ perform a no-op write with an atomic OR with a zero second operand,
+ otherwise just a read. */
+static void
+memprobe_expect_access (const char *context, volatile char *address,
+ size_t size, volatile size_t *pindex, bool do_write)
+{
+ pid_t pid = vfork ();
+ TEST_VERIFY_EXIT (pid >= 0);
+ if (pid == 0)
+ {
+ memprobe_sig_dfl_unblock ();
+ disable_coredumps ();
+ /* *pindex is a volatile access, so the parent process can read
+ the correct index after an unexpected fault. */
+ if (do_write)
+ for (*pindex = 0; *pindex < size; *pindex += 4)
+ write_probe_at (address + *pindex);
+ else
+ for (*pindex = 0; *pindex < size; *pindex += 1)
+ address[*pindex]; /* Triggers volatile read. */
+ _exit (0);
+ }
+ int status;
+ xwaitpid (pid, &status, 0);
+ if (*pindex < size)
+ {
+ support_record_failure ();
+ printf ("error: %s: unexpected %s fault at address %p"
+ " (%zu bytes after %p, wait status %d)\n",
+ context, do_write ? "write" : "read", address + *pindex,
+ *pindex, address, status);
+ }
+ else
+ {
+ TEST_VERIFY (WIFEXITED (status));
+ TEST_COMPARE (WEXITSTATUS (status), 0);
+ }
+}
+
+/* Probe one byte for lack of access. Attempt a write for DO_WRITE,
+ otherwise a read. Returns false on failure. */
+static bool
+memprobe_expect_noaccess_1 (const char *context, volatile char *address,
+ size_t size, size_t index, bool do_write)
+{
+ pid_t pid = vfork ();
+ TEST_VERIFY_EXIT (pid >= 0);
+ if (pid == 0)
+ {
+ memprobe_sig_dfl_unblock ();
+ disable_coredumps ();
+ if (do_write)
+ write_probe_at (address + index);
+ else
+ address[index]; /* Triggers volatile read. */
+ _exit (0); /* Should not be executed due to fault. */
+ }
+
+ int status;
+ xwaitpid (pid, &status, 0);
+ if (WIFSIGNALED (status))
+ {
+ /* Accept SIGSEGV or SIGBUS. */
+ if (WTERMSIG (status) != SIGSEGV)
+ TEST_COMPARE (WTERMSIG (status), SIGBUS);
+ }
+ else
+ {
+ support_record_failure ();
+ printf ("error: %s: unexpected %s success at address %p"
+ " (%zu bytes after %p, wait status %d)\n",
+ context, do_write ? "write" : "read", address + index,
+ index, address, status);
+ return false;
+ }
+ return true;
+}
+
+/* Probe each byte individually because we expect a fault.
+
+ The implementation skips over bytes on the same page, so it assumes
+ that the subpage_prot system call is not used. */
+static void
+memprobe_expect_noaccess (const char *context, volatile char *address,
+ size_t size, bool do_write)
+{
+ if (size == 0)
+ return;
+
+ if (!memprobe_expect_noaccess_1 (context, address, size, 0, do_write))
+ return;
+
+ /* Round up to the next page. */
+ long int page_size = sysconf (_SC_PAGE_SIZE);
+ TEST_VERIFY_EXIT (page_size > 0);
+ size_t index;
+ {
+ uintptr_t next_page = roundup ((uintptr_t) address, page_size);
+ if (next_page < (uintptr_t) address
+ || next_page >= (uintptr_t) address + size)
+ /* Wrap around or after the end of the region. */
+ return;
+ index = next_page - (uintptr_t) address;
+ }
+
+ /* Probe in page increments. */
+ while (true)
+ {
+ if (!memprobe_expect_noaccess_1 (context, address, size, index,
+ do_write))
+ break;
+ size_t next_index = index + page_size;
+ if (next_index < index || next_index >= size)
+ /* Wrap around or after the end of the region. */
+ break;
+ index = next_index;
+ }
+}
+
+static void
+memprobe_range (const char *context, volatile char *address, size_t size,
+ bool expect_read, bool expect_write)
+{
+ /* Do not rely on the sharing nature of vfork because it could be
+ implemented as fork. */
+ size_t *pindex = support_shared_allocate (sizeof *pindex);
+
+ sigset_t oldset;
+ {
+ sigset_t sigallset;
+ sigfillset (&sigallset);
+ sigprocmask (SIG_BLOCK, &sigallset, &oldset);
+ }
+
+ if (expect_read)
+ {
+ memprobe_expect_access (context, address, size, pindex, false);
+ if (expect_write)
+ memprobe_expect_access (context, address, size, pindex, true);
+ else
+ memprobe_expect_noaccess (context, address, size, true);
+ }
+ else
+ {
+ memprobe_expect_noaccess (context, address, size, false);
+ TEST_VERIFY (!expect_write); /* Write-only probing not supported. */
+ }
+
+ sigprocmask (SIG_SETMASK, NULL, &oldset);
+ support_shared_free (pindex);
+}
+
+void support_memprobe_noaccess (const char *context, const void *address,
+ size_t size)
+{
+ memprobe_range (context, (volatile char *) address, size, false, false);
+}
+
+void support_memprobe_readonly (const char *context, const void *address,
+ size_t size)
+{
+ memprobe_range (context, (volatile char *) address, size, true, false);
+}
+
+void support_memprobe_readwrite (const char *context, const void *address,
+ size_t size)
+{
+ memprobe_range (context, (volatile char *) address, size, true, true);
+}
new file mode 100644
@@ -0,0 +1,111 @@
+/* Tests for <support/memprobe.h>.
+ 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 <support/memprobe.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <support/check.h>
+#include <support/next_to_fault.h>
+
+/* Expect a failed state in the test harness. */
+static void
+expect_failure (const char *context)
+{
+ if (!support_record_failure_is_failed ())
+ {
+ printf ("error: expected failure missing: %s\n", context);
+ exit (1);
+ }
+ support_record_failure_reset ();
+}
+
+static int
+do_test (void)
+{
+ static char rw_byte = 1;
+ support_memprobe_readwrite ("rw_byte", &rw_byte, 1);
+ support_record_failure_barrier ();
+
+ puts ("info: expected error for read-only to rw_byte");
+ support_memprobe_readonly ("rw_byte", &rw_byte, 1);
+ expect_failure ("read-only rw_byte");
+
+ puts ("info: expected error for no-access to rw_byte");
+ support_memprobe_noaccess ("rw_byte", &rw_byte, 1);
+ expect_failure ("no-access rw_byte");
+
+ static const char const_byte = 1;
+ support_memprobe_readonly ("const_byte", &const_byte, 1);
+ support_record_failure_barrier ();
+
+ puts ("info: expected error for no-access to const_byte");
+ support_memprobe_noaccess ("const_byte", &const_byte, 1);
+ expect_failure ("no-access const_byte");
+
+ puts ("info: expected error for read-write access to const_byte");
+ support_memprobe_readwrite ("const_byte", &const_byte, 1);
+ expect_failure ("read-write const_byte");
+
+ struct support_next_to_fault ntf = support_next_to_fault_allocate (3);
+ void *ntf_trailing = ntf.buffer + ntf.length;
+
+ /* The initial 3 bytes are accessible. */
+ support_memprobe_readwrite ("ntf init", ntf.buffer, ntf.length);
+ support_record_failure_barrier ();
+
+ puts ("info: expected error for read-only to ntf init");
+ support_memprobe_readonly ("ntf init", ntf.buffer, ntf.length);
+ expect_failure ("read-only ntf init");
+
+ puts ("info: expected error for no-access to ntf init");
+ support_memprobe_noaccess ("ntf init", ntf.buffer, ntf.length);
+ expect_failure ("no-access ntf init");
+
+ /* The trailing part after the allocated area is inaccessible. */
+ support_memprobe_noaccess ("ntf trailing", ntf_trailing, 1);
+ support_record_failure_barrier ();
+
+ puts ("info: expected error for read-only to ntf trailing");
+ support_memprobe_readonly ("ntf trailing", ntf_trailing, 1);
+ expect_failure ("read-only ntf trailing");
+
+ puts ("info: expected error for no-access to ntf trailing");
+ support_memprobe_readwrite ("ntf trailing", ntf_trailing, 1);
+ expect_failure ("read-write ntf trailing");
+
+ /* Both areas combined fail all checks due to inconsistent results. */
+ puts ("info: expected error for no-access to ntf overlap");
+ support_memprobe_noaccess ("ntf overlap ", ntf.buffer, ntf.length + 1);
+ expect_failure ("no-access ntf overlap");
+
+ puts ("info: expected error for read-only to ntf overlap");
+ support_memprobe_readonly ("ntf overlap", ntf.buffer, ntf.length + 1);
+ expect_failure ("read-only ntf overlap");
+
+ puts ("info: expected error for read-write to ntf overlap");
+ support_memprobe_readwrite ("ntf overlap", ntf.buffer, ntf.length + 1);
+ expect_failure ("read-write ntf overlap");
+
+
+ support_next_to_fault_free (&ntf);
+
+ return 0;
+}
+
+#include <support/test-driver.c>