From patchwork Fri Jul 7 18:47:32 2023 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Florian Weimer X-Patchwork-Id: 1805017 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@legolas.ozlabs.org Authentication-Results: legolas.ozlabs.org; spf=pass (sender SPF authorized) smtp.mailfrom=sourceware.org (client-ip=8.43.85.97; helo=server2.sourceware.org; envelope-from=libc-alpha-bounces+incoming=patchwork.ozlabs.org@sourceware.org; receiver=) Authentication-Results: legolas.ozlabs.org; dkim=pass (1024-bit key; secure) header.d=sourceware.org header.i=@sourceware.org header.a=rsa-sha256 header.s=default header.b=nzTDNrtk; dkim-atps=neutral Received: from server2.sourceware.org (server2.sourceware.org [8.43.85.97]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (P-384) server-digest SHA384) (No client certificate requested) by legolas.ozlabs.org (Postfix) with ESMTPS id 4QyMpj5485z20ZC for ; Sat, 8 Jul 2023 04:47:57 +1000 (AEST) Received: from server2.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id B99523848B8B for ; Fri, 7 Jul 2023 18:47:55 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org B99523848B8B DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=sourceware.org; s=default; t=1688755675; bh=y9wuXk0Evgy/gByx6cOQ/dh1DFPVBOPYNebUdiUUQkc=; h=To:Subject:In-Reply-To:References:Date:List-Id:List-Unsubscribe: List-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To: From; b=nzTDNrtkVUJcZygkk/x2enUoLtCxc6BLdcklgxuJMC5lDE3sw4FvEvOwGfv4LMgJB GweGAIHVxMJ1XskzuThX27d0vkhkQfSzdSZO9NHg44SeIeO3Yw6jff98VOwQcH8pDK ufi2i4WhkaZfQ5MxsyY0UVd4xdRJ6N0Co98sMNrI= X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from us-smtp-delivery-124.mimecast.com (us-smtp-delivery-124.mimecast.com [170.10.129.124]) by sourceware.org (Postfix) with ESMTPS id D5B57384C69A for ; Fri, 7 Jul 2023 18:47:36 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org D5B57384C69A Received: from mimecast-mx02.redhat.com (mx3-rdu2.redhat.com [66.187.233.73]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id us-mta-499-vEcxz7qzMRCAZbId0BUxwg-1; Fri, 07 Jul 2023 14:47:35 -0400 X-MC-Unique: vEcxz7qzMRCAZbId0BUxwg-1 Received: from smtp.corp.redhat.com (int-mx05.intmail.prod.int.rdu2.redhat.com [10.11.54.5]) (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by mimecast-mx02.redhat.com (Postfix) with ESMTPS id 0918A2932496 for ; Fri, 7 Jul 2023 18:47:35 +0000 (UTC) Received: from oldenburg.str.redhat.com (unknown [10.2.16.31]) by smtp.corp.redhat.com (Postfix) with ESMTPS id D9D17F643A for ; Fri, 7 Jul 2023 18:47:33 +0000 (UTC) To: libc-alpha@sourceware.org Subject: [PATCH v2 01/32] support: Add for protection flags probing In-Reply-To: Message-ID: <1b310e022cd15b29e8f799f5494b471a6ab82f49.1688741159.git.fweimer@redhat.com> References: X-From-Line: 1b310e022cd15b29e8f799f5494b471a6ab82f49 Mon Sep 17 00:00:00 2001 Date: Fri, 07 Jul 2023 20:47:32 +0200 User-Agent: Gnus/5.13 (Gnus v5.13) Emacs/28.2 (gnu/linux) MIME-Version: 1.0 X-Scanned-By: MIMEDefang 3.1 on 10.11.54.5 X-Mimecast-Spam-Score: 0 X-Mimecast-Originator: redhat.com X-Spam-Status: No, score=-10.9 required=5.0 tests=BAYES_00, DKIMWL_WL_HIGH, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, GIT_PATCH_0, KAM_SHORT, RCVD_IN_DNSWL_NONE, RCVD_IN_MSPIKE_H4, RCVD_IN_MSPIKE_WL, SPF_HELO_NONE, SPF_NONE, TXREP, T_SCC_BODY_TEXT_LINE autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on server2.sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Florian Weimer via Libc-alpha From: Florian Weimer Reply-To: Florian Weimer Errors-To: libc-alpha-bounces+incoming=patchwork.ozlabs.org@sourceware.org Sender: "Libc-alpha" --- support/Makefile | 2 + support/memprobe.h | 43 ++++++ support/support_memprobe.c | 251 +++++++++++++++++++++++++++++++++ support/tst-support_memprobe.c | 118 ++++++++++++++++ 4 files changed, 414 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 917a858bd1..c2873c4b24 100644 --- a/support/Makefile +++ b/support/Makefile @@ -65,6 +65,7 @@ libsupport-routines = \ support_format_hostent \ support_format_netent \ support_isolate_in_subprocess \ + support_memprobe \ support_mutex_pi_monotonic \ support_need_proc \ support_openpty \ @@ -319,6 +320,7 @@ tests = \ tst-support_capture_subprocess \ tst-support_descriptors \ tst-support_format_dns_packet \ + 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..13295e7b8d --- /dev/null +++ b/support/memprobe.h @@ -0,0 +1,43 @@ +/* Probing memory for protection state. + 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 + . */ + +#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 + +/* 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..b599f9c70e --- /dev/null +++ b/support/support_memprobe.c @@ -0,0 +1,251 @@ +/* Probing memory for protection state. + 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 + . */ + +/* 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 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __linux__ +# include +#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); +} diff --git a/support/tst-support_memprobe.c b/support/tst-support_memprobe.c new file mode 100644 index 0000000000..51c1b7812f --- /dev/null +++ b/support/tst-support_memprobe.c @@ -0,0 +1,118 @@ +/* Tests for . + 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 + . */ + +#include + +#include +#include +#include +#include + +/* Used to exit early on error, to avoid masking them. */ +static void +check_barrier (void) +{ + if (support_record_failure_is_failed ()) + exit (1); +} + +/* 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); + check_barrier (); + + puts ("info: expected error for read-only to rw_byte"); + support_memprobe_readonly ("rw_byte", &rw_byte, 1); + + 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); + check_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); + check_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); + check_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