[PATCH v4 04/14] support: Add <support/memprobe.h> for protection flags probing
Adhemerval Zanella Netto
adhemerval.zanella@linaro.org
Mon Mar 17 12:51:16 GMT 2025
On 02/02/25 18:13, Florian Weimer wrote:
> ---
> support/Makefile | 2 +
> support/memprobe.h | 43 ++++++
> support/support_memprobe.c | 251 +++++++++++++++++++++++++++++++++
> support/tst-support_memprobe.c | 111 +++++++++++++++
> 4 files changed, 407 insertions(+)
> create mode 100644 support/memprobe.h
> create mode 100644 support/support_memprobe.c
> create mode 100644 support/tst-support_memprobe.c
>
> diff --git a/support/Makefile b/support/Makefile
> index c55553c0cc..23394837d3 100644
> --- a/support/Makefile
> +++ b/support/Makefile
> @@ -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 \
> diff --git a/support/memprobe.h b/support/memprobe.h
> new file mode 100644
> index 0000000000..4a680de4a7
> --- /dev/null
> +++ b/support/memprobe.h
> @@ -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 */
> diff --git a/support/support_memprobe.c b/support/support_memprobe.c
> new file mode 100644
> index 0000000000..ad559abc2c
> --- /dev/null
> +++ b/support/support_memprobe.c
> @@ -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);
Maybe xsignal here?
> + 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 ();
I used a much simpler way on nptl/tst-guard1.c to check MADV_GUARD_INSTALL,
the only drawnback it uses a global variable instead of context one like
this API. Maybe add that this interface is intended to be used on multithread
environments, the commit message lacks some rationale why you need such complex
interface.
> + 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);
> +}
> diff --git a/support/tst-support_memprobe.c b/support/tst-support_memprobe.c
> new file mode 100644
> index 0000000000..586e09d563
> --- /dev/null
> +++ b/support/tst-support_memprobe.c
> @@ -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>
More information about the Libc-alpha
mailing list