From patchwork Fri May 2 16:26:31 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "Lau, David" X-Patchwork-Id: 111467 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 5AE903858CDA for ; Fri, 2 May 2025 16:27:19 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 5AE903858CDA Authentication-Results: sourceware.org; dkim=pass (2048-bit key, secure) header.d=fau.de header.i=@fau.de header.a=rsa-sha256 header.s=fau-2021 header.b=CxBN0rMa X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from mx-rz-1.rrze.uni-erlangen.de (mx-rz-1.rrze.uni-erlangen.de [IPv6:2001:638:a000:1025::14]) by sourceware.org (Postfix) with ESMTPS id 6A2AA3858D20 for ; Fri, 2 May 2025 16:26:37 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 6A2AA3858D20 Authentication-Results: sourceware.org; dmarc=none (p=none dis=none) header.from=fau.de Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=fau.de ARC-Filter: OpenARC Filter v1.0.0 sourceware.org 6A2AA3858D20 Authentication-Results: server2.sourceware.org; arc=none smtp.remote-ip=2001:638:a000:1025::14 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1746203197; cv=none; b=M3OFsNDlFQ/YBIwzMIppE65nZVmnMTnbU1wiaz1dR6K3p5gtzWxDSkzZ7UMcrdcCCKuUlcXQUxYQs1E53ghxm8u9auv752qdcbiB4NdYRB1zaMuxnSti2Ein/IPe5Rdv9LmDiMQ00eju0i+++vSk7OY54KjHAHekgYrM5RMoe/Y= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1746203197; c=relaxed/simple; bh=4o3HvWM8TGRlefUBgho+HZ9d1JaDtx21CcmhfmTiKMA=; h=DKIM-Signature:MIME-Version:Date:From:To:Subject:Message-ID; b=qIa/Va7uOJvsGMn26bZkVczKFfDGEAkjvXZ4OA+cFvtdWuWfcCTytpZ76FqqAFo+39MqSaFeqHnqh8xS7Atj6wVV7ijnnMKklhlY//LkogSFGszcD3HJKtAo0jMz7NY2Aj1VfCHRxTGqy5jPmchvZZ5wLrEvw4PKbkoU3hsYYA8= ARC-Authentication-Results: i=1; server2.sourceware.org DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 6A2AA3858D20 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=fau.de; s=fau-2021; t=1746203194; bh=3YoPq9DUAwwpnuaugchq4LU9MNqwiEzr4QMO+odB1GY=; h=Date:From:To:Cc:Subject:From:To:CC:Subject; b=CxBN0rMaEsKZksYwq3beZcWNaZQwpqZlstAbRM4yN/mbwLN3NpjqaCuki/fwbYtCX ztk/7qDgF27Jw41EW6sX16ANukilw+g+ca17gHltxXc0l0VPL6GBwSTRnUT3N37rJ2 lLON4eoMKP6yMeuaLzPMuPE4VgWD5tBIzi0tJv6+6qfWnneyf+WVhEqIG24Bmwltvp 0KLUWrdVX5phfCjECfSUb3MziYYAdcgumje3s/iSBTmf0qSNrQCNAm8IrpCG+p0x0d lB70JgmcPlQUABCI7OP6q2FKIpxefD0eMTJ3z6rNYDuzywhSSBmbhFSsm9lkZS8L4S xK5rcCbqbV2ZQ== Received: from mx-rz-smart.rrze.uni-erlangen.de (mx-rz-smart.rrze.uni-erlangen.de [IPv6:2001:638:a000:1025::1e]) (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 mx-rz-1.rrze.uni-erlangen.de (Postfix) with ESMTPS id 4ZpxBf6m3hz8tBc; Fri, 2 May 2025 18:26:34 +0200 (CEST) X-Virus-Scanned: amavisd-new at boeck2.rrze.uni-erlangen.de (RRZE) X-RRZE-Flag: Not-Spam X-RRZE-Submit-IP: 131.188.11.37 Received: from faumail.fau.de (smtp-auth.uni-erlangen.de [131.188.11.37]) (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) (Authenticated sender: U2FsdGVkX18zvgPHYsph6aOFFyuozw5K28zru6J4pQ8=) by smtp-auth.uni-erlangen.de (Postfix) with ESMTPSA id 4ZpxBb5KxXz8tWW; Fri, 2 May 2025 18:26:31 +0200 (CEST) Received: from ermkCmpMu713oPHczgE7IYWUxqP8Mc6vlJqBRMi0x2phS4NbiVoM+d1dKEy4nx4CAk2XmflG2iqHhm+TZ14vIA== (t1iTAevOoH+VE59yDd2hVhTaL5uunaJA4rrz782I6mM=) by faumail.uni-erlangen.de with HTTP (HTTP/1.1 POST); Fri, 02 May 2025 18:26:31 +0200 MIME-Version: 1.0 Date: Fri, 02 May 2025 18:26:31 +0200 From: "Lau, David" To: Libc Alpha Cc: Wilco Dijkstra Subject: [PATCH] malloc: Improved double free detection in the tcache. Message-ID: <24697cee5e28aba11ac81d39b93ebb25@fau.de> X-Sender: david.lau@fau.de X-Spam-Status: No, score=-12.9 required=5.0 tests=BAYES_00, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, GIT_PATCH_0, JMQ_SPF_NEUTRAL, KAM_SHORT, SPF_HELO_PASS, SPF_PASS, TXREP, UNPARSEABLE_RELAY 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 Problem: The previous double free detection did not account for an attacker to use a terminating null byte overflowing from the previous chunk to change the size of a memory chunk is being sorted into. So that the check in 'tcache_double_free_verify' would pass even though it is a double free. Solution: Let 'tcache_double_free_verify' iterate over all tcache entries to detect double frees. This patch only protects from buffer overflows by one byte. But I would argue that off by one errors are the most common errors to be made. Alternatives Considered: Store the size of a memory chunk in big endian and thus the chunk size would not get overwritten because entries in the tcache are not that big. Move the tcache_key before the actual memory chunk so that it does not have to be checked at all, this would work better in general but also it would increase the memory usage. Signed-off-by: David Lau --- malloc/Makefile | 2 +- malloc/malloc.c | 29 ++++++++++++---------- malloc/tst-tcfree4.c | 59 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 14 deletions(-) create mode 100644 malloc/tst-tcfree4.c + +#define TEST_FUNCTION do_test +#define EXPECTED_SIGNAL SIGABRT +#include diff --git a/malloc/Makefile b/malloc/Makefile index e2b2c1ae1b..3d3822db31 100644 --- a/malloc/Makefile +++ b/malloc/Makefile @@ -63,7 +63,7 @@ tests := \ tst-realloc \ tst-reallocarray \ tst-safe-linking \ - tst-tcfree1 tst-tcfree2 tst-tcfree3 \ + tst-tcfree1 tst-tcfree2 tst-tcfree3 tst-tcfree4 \ tst-trim1 \ tst-valloc \ # tests diff --git a/malloc/malloc.c b/malloc/malloc.c index 9d860eac9c..9f44f5ab07 100644 --- a/malloc/malloc.c +++ b/malloc/malloc.c @@ -3226,21 +3226,24 @@ tcache_available (size_t tc_idx) /* Verify if the suspicious tcache_entry is double free. It's not expected to execute very often, mark it as noinline. */ static __attribute__ ((noinline)) void -tcache_double_free_verify (tcache_entry *e, size_t tc_idx) +tcache_double_free_verify (tcache_entry *e) { tcache_entry *tmp; - size_t cnt = 0; - LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx); - for (tmp = tcache->entries[tc_idx]; - tmp; - tmp = REVEAL_PTR (tmp->next), ++cnt) + for (size_t tc_idx = 0; tc_idx < TCACHE_MAX_BINS; ++tc_idx) { - if (cnt >= mp_.tcache_count) - malloc_printerr ("free(): too many chunks detected in tcache"); - if (__glibc_unlikely (!aligned_OK (tmp))) - malloc_printerr ("free(): unaligned chunk detected in tcache 2"); - if (tmp == e) - malloc_printerr ("free(): double free detected in tcache 2"); + size_t cnt = 0; + LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx); + for (tmp = tcache->entries[tc_idx]; + tmp; + tmp = REVEAL_PTR (tmp->next), ++cnt) + { + if (cnt >= mp_.tcache_count) + malloc_printerr ("free(): too many chunks detected in tcache"); + if (__glibc_unlikely (!aligned_OK (tmp))) + malloc_printerr ("free(): unaligned chunk detected in tcache 2"); + if (tmp == e) + malloc_printerr ("free(): double free detected in tcache 2"); + } } /* No double free detected - it might be in a tcache of another thread, or user data that happens to match the key. Since we are not sure, @@ -3428,7 +3431,7 @@ __libc_free (void *mem) /* Check for double free - verify if the key matches. */ if (__glibc_unlikely (e->key == tcache_key)) - return tcache_double_free_verify (e, tc_idx); + return tcache_double_free_verify (e); if (__glibc_likely (tcache->counts[tc_idx] < mp_.tcache_count)) return tcache_put (p, tc_idx); diff --git a/malloc/tst-tcfree4.c b/malloc/tst-tcfree4.c new file mode 100644 index 0000000000..03850ddd12 --- /dev/null +++ b/malloc/tst-tcfree4.c @@ -0,0 +1,59 @@ +/* Test that malloc tcache catches double free. + 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 + . */ + +#include +#include +#include +#include +#include +#include +#include +#include + +/* Test for a double free where the size information gets overwritten by a + * terminating null byte. */ +static int +do_test (void) +{ + /* The payload is exactly 0x19 Bytes long: + * 0x18 bytes 'B' and one terminating null byte + */ + const char *payload = "BBBBBBBBBBBBBBBBBBBBBBBB"; + + char *volatile first_chunk + = malloc (strlen (payload)); // <-- off by one error + char *volatile second_chunk = malloc (0x118); + + // free the second chunk the first time now it is in the tcache with tc_idx = + free (second_chunk); + + // change the the size of the second_chunk using the terminating null byte if + // the PAYLOAD + strcpy (first_chunk, payload); + + // now the second_chunk has a new size + // calling free a second time will not trigger the double free detection + free (second_chunk); + + printf ("FAIL: tcache double free not detected\n"); + return 1; +}