From patchwork Thu Dec 7 10:30:50 2023 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Florian Weimer X-Patchwork-Id: 81629 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from server2.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id EB9BF385E02F for ; Thu, 7 Dec 2023 10:31:17 +0000 (GMT) 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 3CE3A385B836 for ; Thu, 7 Dec 2023 10:30:55 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 3CE3A385B836 Authentication-Results: sourceware.org; dmarc=pass (p=none dis=none) header.from=redhat.com Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=redhat.com ARC-Filter: OpenARC Filter v1.0.0 sourceware.org 3CE3A385B836 Authentication-Results: server2.sourceware.org; arc=none smtp.remote-ip=170.10.129.124 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1701945065; cv=none; b=GC1LU+7Veo1WUge37qDlscZHD1bt8SclQ+XiN94vnwkwKOxlOC0KYPLuLOkmGNiLgX/5d5EGgZbh7+HSG7KZz70sTaym5KrK5JMUVtYLksGhQzAKLkViP5D7vda/HcQd6mp8+rPKnS2+PizA20QlP5vq45sgqnqvHzF7pR9+wgI= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1701945065; c=relaxed/simple; bh=yTsUePXx78ZeSr/VdA1fWxI6OeunVDfVqYIGuFczN1M=; h=DKIM-Signature:From:To:Subject:Message-ID:Date:MIME-Version; b=K2bt5nzidWQ7hxGmq4ePix8Rx8BbEYyqiYzIu59Vzv5oQS/N/o/LMr8CfJ+20PlvdmIItTvWvlVYHwaDft6GGieyE1izu2PfQBDJvDzVXGL1y4o+bLaS2fadGMnspcBZXbWYpNSZpDBYH0fB1XXfqaGEe6BEiFk5aMhY5wMLjl8= ARC-Authentication-Results: i=1; server2.sourceware.org DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1701945054; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: in-reply-to:in-reply-to:references:references; bh=4OVTKRowWzv+OVf+mF2zowcx+ybkN8JMwdHVaNJbf7M=; b=RG+l0T3wDxeo5hsQpJnruBxjTN7XNZnGvN3l8aLADQrFSUzyHtaXcPIHPG+VxbXSemlMiG OEWG9E5O/AwkpDDSE5hIiyn2ApsGF9VJMiakGqs2BtkGk5B+GFPVqdpjY8v2WTLn2CmACR iJi/Lx9jmQREaZJk4CXkzFcat0OhBw8= Received: from mimecast-mx02.redhat.com (mimecast-mx02.redhat.com [66.187.233.88]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id us-mta-575-YzBdZb24Nr69Iqkbd7Wk3A-1; Thu, 07 Dec 2023 05:30:53 -0500 X-MC-Unique: YzBdZb24Nr69Iqkbd7Wk3A-1 Received: from smtp.corp.redhat.com (int-mx09.intmail.prod.int.rdu2.redhat.com [10.11.54.9]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by mimecast-mx02.redhat.com (Postfix) with ESMTPS id 0C6C5867950 for ; Thu, 7 Dec 2023 10:30:53 +0000 (UTC) Received: from oldenburg.str.redhat.com (unknown [10.39.192.131]) by smtp.corp.redhat.com (Postfix) with ESMTPS id 15E7B492BC6 for ; Thu, 7 Dec 2023 10:30:51 +0000 (UTC) From: Florian Weimer To: libc-alpha@sourceware.org Subject: [PATCH v3 01/32] support: Add for protection flags probing In-Reply-To: Message-ID: References: X-From-Line: c5aa4778c7c9536d4a58414c8c5045c1b0872c1e Mon Sep 17 00:00:00 2001 Date: Thu, 07 Dec 2023 11:30:50 +0100 User-Agent: Gnus/5.13 (Gnus v5.13) Emacs/28.3 (gnu/linux) MIME-Version: 1.0 X-Scanned-By: MIMEDefang 3.4.1 on 10.11.54.9 X-Mimecast-Spam-Score: 0 X-Mimecast-Originator: redhat.com X-Spam-Status: No, score=-10.6 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.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces+patchwork=sourceware.org@sourceware.org --- 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 9aa7f23a6e..556281121d 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 \ @@ -320,6 +321,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