From patchwork Wed Dec 17 11:50:33 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Arjun Shankar X-Patchwork-Id: 126680 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from vm01.sourceware.org (localhost [127.0.0.1]) by sourceware.org (Postfix) with ESMTP id 678D14BA2E22 for ; Wed, 17 Dec 2025 12:05:29 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 678D14BA2E22 Authentication-Results: sourceware.org; dkim=pass (1024-bit key, unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256 header.s=mimecast20190719 header.b=cmh62C1B 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.133.124]) by sourceware.org (Postfix) with ESMTP id D6F9E4BA2E04 for ; Wed, 17 Dec 2025 12:04:46 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org D6F9E4BA2E04 Authentication-Results: sourceware.org; dmarc=pass (p=quarantine 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 D6F9E4BA2E04 Authentication-Results: server2.sourceware.org; arc=none smtp.remote-ip=170.10.133.124 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1765973087; cv=none; b=amvV5BPNHbH4FGhUoBd6JSJ/RfaiCRlq89Cmc7J97LSdNOEJAVSJJkBDutoj+7BwEy/gTC6rlPBnuI0iv0/uVw1OyMPN1ZResHA/qikvNH+iRFufH040GR32qhqTE69+wTyfRwtVDlkoaVSnsivEnPD83uN9fL5UjlIoPnl0F3M= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1765973087; c=relaxed/simple; bh=sr0bQlal/usFXlVZAuP2eAm6hqY+UWAB9umxuwK6Xec=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=YhvSlggMUyguVtHSAUM9w6YI/l41ECjDTxHKM6rxFmHoaTTSBjCPhOPZcyT7qbe/Ct26t1N5fxyEj/fjMtnlFqyl20FW5hpJMDVIlGVfWakQOrXxiTD3qQ/1AK4TXx9jdEwl4qItV4qLYIP24Cl0Nl8SFH20345chLYMdvUOpJc= ARC-Authentication-Results: i=1; server2.sourceware.org DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org D6F9E4BA2E04 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1765973086; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=t2LCTPHJ3oq9x5YhDzlERa/xuIdNh3VXpMlI4iwr6R4=; b=cmh62C1BNjEF4GRYUvgwObUt20df4oFetiV9o9Du5J6UnFfEb9gErH6w3IpGws093x3kls brrp0tEfY2kkjVtcImj6pCF1616xnfZ0XBuINGCXd6FVag/kOzeX2QuKoW7EQx6YnxxlI9 2or0Idp47i82uKJs+GwBxYCQmSMkT2g= Received: from mail-wr1-f71.google.com (mail-wr1-f71.google.com [209.85.221.71]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id us-mta-454-d3ASkRABMkq4-CPJ9sIRlg-1; Wed, 17 Dec 2025 07:04:45 -0500 X-MC-Unique: d3ASkRABMkq4-CPJ9sIRlg-1 X-Mimecast-MFC-AGG-ID: d3ASkRABMkq4-CPJ9sIRlg_1765973084 Received: by mail-wr1-f71.google.com with SMTP id ffacd0b85a97d-42fb724518bso2142285f8f.0 for ; Wed, 17 Dec 2025 04:04:45 -0800 (PST) X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1765973084; x=1766577884; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:x-gm-gg:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to; bh=t2LCTPHJ3oq9x5YhDzlERa/xuIdNh3VXpMlI4iwr6R4=; b=POQB2s823orKxCHWotVDLBzDOJ08qrI0xs/URddh+rpSx31fMTWDpnUCByu39nVShq B1gAUdxV/PKKm8KqjZuatXVJbP5AWVzsmaCThWR23qnIsxXyhraZVFMCEgQ1Kn9UswZ9 eA3Ft4evDRXka7eo0q8M7BHCUEBLrdhyz31ThxpTgLVK0j0aYosfabmZdyncOC+SSZrU GIVfqHXl7gbfngfnYFSwpBiAL67VDOc+c6ByM1tYyI2xOsExORNa9bHayA1KvUtcVLSI CB2e+pzg0/ZXaQJ2kusK94qMspte0Hf8rerEmPJPscl4NGyhhKDjau+xt5pWqm+qfAA9 4Npg== X-Gm-Message-State: AOJu0Yyn9yjU2ztFyTpBRLEb7hHYfFEiYLGwvuPT7+V1rkfwNx8vuWsS xF3f/4J6HS6feEkjPGLiquqcsNrFI02tf0YIl6dr5sKITeJiKe7SZVjrLWYGm1pgTKHxo3GIYWR qvcrgfLHXpdTtbDoij11xsEZ3XtbNiRAB1v9xKSOIPf2EhyqRjy9zKIso0PPLeuoG4iUVkZCL6i ipRuG4aHwVz3UXLKKecbAg/IKcZUvSvAIGlDxAIXnvKg== X-Gm-Gg: AY/fxX5rXOzJdE3GuFYwz+5UMgZw0DYAkx5sNE8aO2owlg1L3IffpfrKBziFPLwTvKS nett5sEAs3l4VfL5ZybJfC0RFDRXOGZM1jmbxUssVa7aqcwsLhTY5nNZ0Nxo4AXZvEYmau29cXm CDpbzLol3gPoc8O/Pd5UGo/G8Ck4ePARI0PBQDocED3eRhU8r2SDR49VK4BOt0hZXknMa+XvlcD LbLgIpEZwMTNkmJ5zO67WahHQ2o63BlS+ZA/iKAh4DiT3b0XmX0fr1ENUCZEsI7lDPK9BssFOCi OwEOXl0ZfIBXTu/vt6dZ15GI6aIxmZzezdMouJnMlsei8lQUm3+ffQ8oVmpzs2dgAxJTgDGfMAh 5mDZsAg== X-Received: by 2002:a05:6000:2303:b0:430:fd84:317a with SMTP id ffacd0b85a97d-430fd843502mr11141109f8f.38.1765973083272; Wed, 17 Dec 2025 04:04:43 -0800 (PST) X-Google-Smtp-Source: AGHT+IEwvchld8QuPyB0QBaHTfnYXRrEg9WtrMUYA9cRQCpSHNP9o3VUJ2Pfo+TXT8VCSzQ1yInQMQ== X-Received: by 2002:a05:6000:2303:b0:430:fd84:317a with SMTP id ffacd0b85a97d-430fd843502mr11141056f8f.38.1765973082615; Wed, 17 Dec 2025 04:04:42 -0800 (PST) Received: from x1ctwelve ([2a02:830a:f04d:c200:676:5d65:649:b2b8]) by smtp.gmail.com with ESMTPSA id ffacd0b85a97d-4310adef010sm4548351f8f.35.2025.12.17.04.04.41 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 17 Dec 2025 04:04:41 -0800 (PST) From: Arjun Shankar To: libc-alpha@sourceware.org Cc: Florian Weimer , Arjun Shankar Subject: [RFC PATCH] malloc: Harden malloc by protecting chunk fd/bk pointers Date: Wed, 17 Dec 2025 12:50:33 +0100 Message-ID: <20251217120435.3992786-1-arjun@redhat.com> X-Mailer: git-send-email 2.52.0 MIME-Version: 1.0 X-Mimecast-Spam-Score: 0 X-Mimecast-MFC-PROC-ID: ipvxrLe762XudbR-FvjSQa_r8wqj7Q5UMyThDMQiTM4_1765973084 X-Mimecast-Originator: redhat.com content-type: text/plain; charset="US-ASCII"; x-default=true X-Spam-Status: No, score=-12.8 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_BLOCKED, RCVD_IN_MSPIKE_H3, RCVD_IN_MSPIKE_WL, RCVD_IN_VALIDITY_RPBL_BLOCKED, RCVD_IN_VALIDITY_SAFE_BLOCKED, SPF_HELO_PASS, SPF_NONE, TXREP, URIBL_BLOCKED autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on 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 malloc is susceptible to "unsafe unlink" attacks that manipulate chunk fd/bk pointers before exploiting glibc's unlink_chunk. See: https://github.com/shellphish/how2heap/blob/master/glibc_2.41/unsafe_unlink.c This commit hardens malloc against such attacks. This is achieved by using PROTECT_PTR when storing fd and bk pointers in memory, thus making metadata manipulation harder. The changes are mostly mechanical. New setters and getters are defined for fd and bk pointers, using PROTECT_PTR and REVEAL_PTR respectively. They are then used for every access. A new test is also added to verify that the hardening works (by causing a SIGSEGV at an unlink_chunk pointer check during free). --- I ran into this problem while evaluating some old internal tests that looked like candidates for upstreaming. One of them turned out to be an old version of an unsafe-unlink exploit that has since been updated to the state it's in at the how2heap repository that I learned about from Florian. I was mostly curious if there can be some quick, low-cost hardening that can protect against such attacks and if I could learn a bit more about malloc while writing it. Is this patch useful, or a start towards something useful for glibc? --- malloc/Makefile | 6 ++ malloc/malloc.c | 182 ++++++++++++++++++++++++++------------------ malloc/tst-unlink.c | 84 ++++++++++++++++++++ 3 files changed, 197 insertions(+), 75 deletions(-) create mode 100644 malloc/tst-unlink.c diff --git a/malloc/Makefile b/malloc/Makefile index 5b3436dfd6..1c5a16741b 100644 --- a/malloc/Makefile +++ b/malloc/Makefile @@ -69,6 +69,7 @@ tests := \ tst-safe-linking \ tst-tcfree1 tst-tcfree2 tst-tcfree3 tst-tcfree4 \ tst-trim1 \ + tst-unlink \ tst-valloc \ # tests @@ -119,6 +120,7 @@ tests-exclude-malloc-check = \ tst-mxfast \ tst-safe-linking \ tst-tcfree4 \ + tst-unlink \ # tests-exclude-malloc-check # Run all tests with MALLOC_CHECK_=3 @@ -164,6 +166,7 @@ tests-exclude-largetcache = \ tst-malloc-usable \ tst-malloc-usable-tunables \ tst-mallocstate \ + tst-unlink \ # tests-exclude-largetcache tests-malloc-largetcache = \ @@ -194,6 +197,7 @@ tests-exclude-mcheck = \ tst-memalign-3 \ tst-mxfast \ tst-safe-linking \ + tst-unlink \ # tests-exclude-mcheck tests-mcheck = $(filter-out $(tests-exclude-mcheck) $(tests-static), $(tests)) @@ -456,6 +460,8 @@ CPPFLAGS-malloc.c += -DUSE_TCACHE=1 CFLAGS-tst-tcfree3.c += -fno-builtin-malloc -fno-builtin-free +CFLAGS-tst-unlink.c := -O0 + sLIBdir := $(shell echo $(slibdir) | sed 's,lib\(\|64\)$$,\\\\$$LIB,') $(objpfx)mtrace: mtrace.pl diff --git a/malloc/malloc.c b/malloc/malloc.c index 8dd7bbe4f4..6beab77315 100644 --- a/malloc/malloc.c +++ b/malloc/malloc.c @@ -1451,6 +1451,34 @@ checked_request2size (size_t req) __nonnull (1) /* Set size at footer (only when chunk is not in use) */ #define set_foot(p, s) (((mchunkptr) ((char *) (p) + (s)))->mchunk_prev_size = (s)) +/* Safe-Linking: + Use the same strategy used to protect single-linked lists of Fast-Bins + and TCache to protect regular double-linked chunk lists as well. */ + +static __always_inline void +set_fd (mchunkptr p, void *fd) +{ + p->fd = PROTECT_PTR (&p->fd, fd); +} + +static __always_inline void +set_bk (mchunkptr p, void *bk) +{ + p->bk = PROTECT_PTR (&p->bk, bk); +} + +static __always_inline mchunkptr +get_fd (mchunkptr p) +{ + return REVEAL_PTR (p->fd); +} + +static __always_inline mchunkptr +get_bk (mchunkptr p) +{ + return REVEAL_PTR (p->bk); +} + #pragma GCC poison mchunk_size #pragma GCC poison mchunk_prev_size @@ -1581,8 +1609,8 @@ typedef struct malloc_chunk *mbinptr; #define next_bin(b) ((mbinptr) ((char *) (b) + (sizeof (mchunkptr) << 1))) /* Reminders about list directionality within bins */ -#define first(b) ((b)->fd) -#define last(b) ((b)->bk) +#define first(b) (get_fd (b)) +#define last(b) (get_bk (b)) /* Indexing @@ -1663,14 +1691,14 @@ unlink_chunk (mstate av, mchunkptr p) if (chunksize (p) != prev_size (next_chunk (p))) malloc_printerr ("corrupted size vs. prev_size"); - mchunkptr fd = p->fd; - mchunkptr bk = p->bk; + mchunkptr fd = get_fd (p); + mchunkptr bk = get_bk (p); - if (__glibc_unlikely (fd->bk != p || bk->fd != p)) + if (__glibc_unlikely (get_bk (fd) != p || get_fd (bk) != p)) malloc_printerr ("corrupted double-linked list"); - fd->bk = bk; - bk->fd = fd; + set_bk (fd, bk); + set_fd (bk, fd); if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL) { if (p->fd_nextsize->bk_nextsize != p @@ -2000,7 +2028,8 @@ malloc_init_state (mstate av) for (i = 1; i < NBINS; ++i) { bin = bin_at (av, i); - bin->fd = bin->bk = bin; + set_fd (bin, bin); + set_bk (bin, bin); } #if MORECORE_CONTIGUOUS @@ -2187,8 +2216,8 @@ do_check_free_chunk (mstate av, mchunkptr p) assert (next == av->top || inuse (next)); /* ... and has minimally sane links */ - assert (p->fd->bk == p); - assert (p->bk->fd == p); + assert (get_bk (get_fd (p)) == p); + assert (get_fd (get_bk (p)) == p); } else /* markers are always of size SIZE_SZ */ assert (sz == SIZE_SZ); @@ -2385,7 +2414,7 @@ do_check_malloc_state (mstate av) assert (binbit); } - for (p = last (b); p != b; p = p->bk) + for (p = last (b); p != b; p = get_bk (p)) { /* each chunk claims to be free */ do_check_free_chunk (av, p); @@ -2397,8 +2426,8 @@ do_check_malloc_state (mstate av) idx = bin_index (size); assert (idx == i); /* lists are sorted */ - assert (p->bk == b || - (unsigned long) chunksize (p->bk) >= (unsigned long) chunksize (p)); + assert (get_bk (p) == b || + (unsigned long) chunksize (get_bk (p)) >= (unsigned long) chunksize (p)); if (!in_smallbin_range (size)) { @@ -4148,12 +4177,12 @@ _int_malloc (mstate av, size_t bytes) if ((victim = last (bin)) != bin) { - bck = victim->bk; - if (__glibc_unlikely (bck->fd != victim)) + bck = get_bk (victim); + if (__glibc_unlikely (get_fd (bck) != victim)) malloc_printerr ("malloc(): smallbin double linked list corrupted"); set_inuse_bit_at_offset (victim, nb); - bin->bk = bck; - bck->fd = bin; + set_bk (bin, bck); + set_fd (bck, bin); if (av != &main_arena) set_non_main_arena (victim); @@ -4175,12 +4204,12 @@ _int_malloc (mstate av, size_t bytes) { if (tc_victim != NULL) { - bck = tc_victim->bk; + bck = get_bk (tc_victim); set_inuse_bit_at_offset (tc_victim, nb); if (av != &main_arena) set_non_main_arena (tc_victim); - bin->bk = bck; - bck->fd = bin; + set_bk (bin, bck); + set_fd (bck, bin); tcache_put (tc_victim, tc_idx); } @@ -4237,9 +4266,9 @@ _int_malloc (mstate av, size_t bytes) for (;; ) { int iters = 0; - while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) + while ((victim = get_bk (unsorted_chunks (av))) != unsorted_chunks (av)) { - bck = victim->bk; + bck = get_bk (victim); size = chunksize (victim); mchunkptr next = chunk_at_offset (victim, size); @@ -4251,8 +4280,8 @@ _int_malloc (mstate av, size_t bytes) malloc_printerr ("malloc(): invalid next size (unsorted)"); if (__glibc_unlikely ((prev_size (next) & ~(SIZE_BITS)) != size)) malloc_printerr ("malloc(): mismatching next->prev_size (unsorted)"); - if (__glibc_unlikely (bck->fd != victim) - || __glibc_unlikely (victim->fd != unsorted_chunks (av))) + if (__glibc_unlikely (get_fd (bck) != victim) + || __glibc_unlikely (get_fd (victim) != unsorted_chunks (av))) malloc_printerr ("malloc(): unsorted double linked list corrupted"); if (__glibc_unlikely (prev_inuse (next))) malloc_printerr ("malloc(): invalid next->prev_inuse (unsorted)"); @@ -4273,9 +4302,11 @@ _int_malloc (mstate av, size_t bytes) /* split and reattach remainder */ remainder_size = size - nb; remainder = chunk_at_offset (victim, nb); - unsorted_chunks (av)->bk = unsorted_chunks (av)->fd = remainder; + set_bk (unsorted_chunks (av), remainder); + set_fd (unsorted_chunks (av), remainder); av->last_remainder = remainder; - remainder->bk = remainder->fd = unsorted_chunks (av); + set_bk (remainder, unsorted_chunks (av)); + set_fd (remainder, unsorted_chunks (av)); if (!in_smallbin_range (remainder_size)) { remainder->fd_nextsize = NULL; @@ -4294,8 +4325,8 @@ _int_malloc (mstate av, size_t bytes) } /* remove from unsorted list */ - unsorted_chunks (av)->bk = bck; - bck->fd = unsorted_chunks (av); + set_bk (unsorted_chunks (av), bck); + set_fd (bck, unsorted_chunks (av)); /* Take now instead of binning if exact fit */ @@ -4334,13 +4365,13 @@ _int_malloc (mstate av, size_t bytes) { victim_index = smallbin_index (size); bck = bin_at (av, victim_index); - fwd = bck->fd; + fwd = get_fd (bck); } else { victim_index = largebin_index (size); bck = bin_at (av, victim_index); - fwd = bck->fd; + fwd = get_fd (bck); /* maintain large bins in sorted order */ if (fwd != bck) @@ -4348,19 +4379,20 @@ _int_malloc (mstate av, size_t bytes) /* Or with inuse bit to speed comparisons */ size |= PREV_INUSE; /* if smaller than smallest, bypass loop below */ - assert (chunk_main_arena (bck->bk)); + assert (chunk_main_arena (get_bk (bck))); if ((unsigned long) (size) - < (unsigned long) chunksize_nomask (bck->bk)) + < (unsigned long) chunksize_nomask (get_bk (bck))) { fwd = bck; - bck = bck->bk; + bck = get_bk (bck); - if (__glibc_unlikely (fwd->fd->bk_nextsize->fd_nextsize != fwd->fd)) + if (__glibc_unlikely (get_fd (fwd)->bk_nextsize->fd_nextsize != get_fd (fwd))) malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)"); - victim->fd_nextsize = fwd->fd; - victim->bk_nextsize = fwd->fd->bk_nextsize; - fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim; + victim->fd_nextsize = get_fd (fwd); + victim->bk_nextsize = get_fd (fwd)->bk_nextsize; + get_fd (fwd)->bk_nextsize = victim; + victim->bk_nextsize->fd_nextsize = victim; } else { @@ -4374,7 +4406,7 @@ _int_malloc (mstate av, size_t bytes) if ((unsigned long) size == (unsigned long) chunksize_nomask (fwd)) /* Always insert in the second position. */ - fwd = fwd->fd; + fwd = get_fd (fwd); else { victim->fd_nextsize = fwd; @@ -4384,8 +4416,8 @@ _int_malloc (mstate av, size_t bytes) fwd->bk_nextsize = victim; victim->bk_nextsize->fd_nextsize = victim; } - bck = fwd->bk; - if (bck->fd != fwd) + bck = get_bk (fwd); + if (get_fd (bck) != fwd) malloc_printerr ("malloc(): largebin double linked list corrupted (bk)"); } } @@ -4394,10 +4426,10 @@ _int_malloc (mstate av, size_t bytes) } mark_bin (av, victim_index); - victim->bk = bck; - victim->fd = fwd; - fwd->bk = victim; - bck->fd = victim; + set_bk (victim, bck); + set_fd (victim, fwd); + set_bk (fwd, victim); + set_fd (bck, victim); #if USE_TCACHE /* If we've processed as many chunks as we're allowed while @@ -4447,8 +4479,8 @@ _int_malloc (mstate av, size_t bytes) list does not have to be rerouted. */ if (victim != last (bin) && chunksize_nomask (victim) - == chunksize_nomask (victim->fd)) - victim = victim->fd; + == chunksize_nomask (get_fd (victim))) + victim = get_fd (victim); remainder_size = size - nb; unlink_chunk (av, victim); @@ -4467,13 +4499,13 @@ _int_malloc (mstate av, size_t bytes) /* We cannot assume the unsorted list is empty and therefore have to perform a complete insert here. */ bck = unsorted_chunks (av); - fwd = bck->fd; - if (__glibc_unlikely (fwd->bk != bck)) + fwd = get_fd (bck); + if (__glibc_unlikely (get_bk (fwd) != bck)) malloc_printerr ("malloc(): corrupted unsorted chunks"); - remainder->bk = bck; - remainder->fd = fwd; - bck->fd = remainder; - fwd->bk = remainder; + set_bk (remainder, bck); + set_fd (remainder, fwd); + set_fd (bck, remainder); + set_bk (fwd, remainder); if (!in_smallbin_range (remainder_size)) { remainder->fd_nextsize = NULL; @@ -4571,13 +4603,13 @@ _int_malloc (mstate av, size_t bytes) /* We cannot assume the unsorted list is empty and therefore have to perform a complete insert here. */ bck = unsorted_chunks (av); - fwd = bck->fd; - if (__glibc_unlikely (fwd->bk != bck)) + fwd = get_fd (bck); + if (__glibc_unlikely (get_bk (fwd) != bck)) malloc_printerr ("malloc(): corrupted unsorted chunks 2"); - remainder->bk = bck; - remainder->fd = fwd; - bck->fd = remainder; - fwd->bk = remainder; + set_bk (remainder, bck); + set_fd (remainder, fwd); + set_fd (bck, remainder); + set_bk (fwd, remainder); /* advertise as last remainder */ if (in_smallbin_range (nb)) @@ -4878,8 +4910,8 @@ _int_free_create_chunk (mstate av, mchunkptr p, INTERNAL_SIZE_T size, This branch is first in the if-statement to help branch prediction on consecutive adjacent frees. */ bck = unsorted_chunks (av); - fwd = bck->fd; - if (__glibc_unlikely (fwd->bk != bck)) + fwd = get_fd (bck); + if (__glibc_unlikely (get_bk (fwd) != bck)) malloc_printerr ("free(): corrupted unsorted chunks"); p->fd_nextsize = NULL; p->bk_nextsize = NULL; @@ -4890,18 +4922,18 @@ _int_free_create_chunk (mstate av, mchunkptr p, INTERNAL_SIZE_T size, don't pollute the unsorted bin. */ int chunk_index = smallbin_index (size); bck = bin_at (av, chunk_index); - fwd = bck->fd; + fwd = get_fd (bck); - if (__glibc_unlikely (fwd->bk != bck)) + if (__glibc_unlikely (get_bk (fwd) != bck)) malloc_printerr ("free(): chunks in smallbin corrupted"); mark_bin (av, chunk_index); } - p->bk = bck; - p->fd = fwd; - bck->fd = p; - fwd->bk = p; + set_bk (p, bck); + set_fd (p, fwd); + set_fd (bck, p); + set_bk (fwd, p); set_head(p, size | PREV_INUSE); set_foot(p, size); @@ -5036,9 +5068,9 @@ static void malloc_consolidate(mstate av) } else clear_inuse_bit_at_offset(nextchunk, 0); - first_unsorted = unsorted_bin->fd; - unsorted_bin->fd = p; - first_unsorted->bk = p; + first_unsorted = get_fd (unsorted_bin); + set_fd (unsorted_bin, p); + set_bk (first_unsorted, p); if (!in_smallbin_range (size)) { p->fd_nextsize = NULL; @@ -5046,8 +5078,8 @@ static void malloc_consolidate(mstate av) } set_head(p, size | PREV_INUSE); - p->bk = unsorted_bin; - p->fd = first_unsorted; + set_bk (p, unsorted_bin); + set_fd (p, first_unsorted); set_foot(p, size); } @@ -5277,7 +5309,7 @@ mtrim (mstate av, size_t pad) { mbinptr bin = bin_at (av, i); - for (mchunkptr p = last (bin); p != bin; p = p->bk) + for (mchunkptr p = last (bin); p != bin; p = get_bk (p)) { INTERNAL_SIZE_T size = chunksize (p); @@ -5411,7 +5443,7 @@ int_mallinfo (mstate av, struct mallinfo2 *m) for (i = 1; i < NBINS; ++i) { b = bin_at (av, i); - for (p = last (b); p != b; p = p->bk) + for (p = last (b); p != b; p = get_bk (p)) { ++nblocks; avail += chunksize (p); @@ -6025,7 +6057,7 @@ __malloc_info (int options, FILE *fp) for (size_t i = 1; i < NBINS; ++i) { bin = bin_at (ar_ptr, i); - r = bin->fd; + r = get_fd (bin); sizes[NFASTBINS - 1 + i].from = ~((size_t) 0); sizes[NFASTBINS - 1 + i].to = sizes[NFASTBINS - 1 + i].total = sizes[NFASTBINS - 1 + i].count = 0; @@ -6041,7 +6073,7 @@ __malloc_info (int options, FILE *fp) sizes[NFASTBINS - 1 + i].to = MAX (sizes[NFASTBINS - 1 + i].to, r_size); - r = r->fd; + r = get_fd (r); } if (sizes[NFASTBINS - 1 + i].count == 0) diff --git a/malloc/tst-unlink.c b/malloc/tst-unlink.c new file mode 100644 index 0000000000..6721dd38f3 --- /dev/null +++ b/malloc/tst-unlink.c @@ -0,0 +1,84 @@ +/* Test malloc hardening against unsafe-unlink heap exploits. + 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 + +/* Useful definitions and macros that match the malloc implementation: */ +struct malloc_chunk +{ + /* Chunk header. */ + size_t mchunk_prev_size; + size_t mchunk_size; + + /* (in-use chunk) user data, OR (free chunk) metadata. */ + struct malloc_chunk* fd; + struct malloc_chunk* bk; +}; +typedef struct malloc_chunk * mchunkptr; +#define CHUNK_HDR_SZ (2 * sizeof (size_t)) +#define chunk2mem(p) ((void *) ((char *)(p) + CHUNK_HDR_SZ)) +#define mem2chunk(m) ((mchunkptr) ((char *)(m) - CHUNK_HDR_SZ)) +#define PREV_INUSE 0x1 +#define clear_inuse_bit_at_offset(p, s) \ + (((mchunkptr) (((char *) (p)) + (s)))->mchunk_size &= ~(PREV_INUSE)) + +/* Large allocations to avoid fastbins/tcache interactions. */ +#define ALLOC_SIZE (1024 + 64) + +void * global_ptr; + +static int +do_test (void) +{ + void *local_ptr; + + global_ptr = malloc (ALLOC_SIZE); + local_ptr = malloc (ALLOC_SIZE); + + /* Create a fake chunk inside the first block: */ + mchunkptr fake_chunk = global_ptr; + + /* 1. Set up the size so that it still ends at the old boundary. */ + fake_chunk->mchunk_size = ALLOC_SIZE; + + /* 2. Set up fd and bk pointers to point to before the *address* of the + global pointer in such a way that the value stored there (pointer back + to us) appears to be the 'bk' of some imaginary chunk that is our 'fd', + and vice versa. Pre-2.43 glibc used to store these pointers + unprotected. */ + fake_chunk->fd = (mchunkptr) ((char *) &global_ptr + - offsetof (struct malloc_chunk, bk)); + fake_chunk->bk = (mchunkptr) ((char *) &global_ptr + - offsetof (struct malloc_chunk, fd)); + + /* Adjust the second block's chunk metadata so that (1) its prev_size is + correct, and (2) its previous chunk appears to be unused. */ + mchunkptr local_chunk = mem2chunk (local_ptr); + local_chunk->mchunk_prev_size = ALLOC_SIZE; + clear_inuse_bit_at_offset (local_chunk, 0); + + /* This should SIGSEGV when unlink_chunk tries to reveal fd/bk. */ + free (local_ptr); + + return 0; +} + +#define EXPECTED_SIGNAL SIGSEGV +#include