1d4c55
From a1a486d70ebcc47a686ff5846875eacad0940e41 Mon Sep 17 00:00:00 2001
1d4c55
From: Eyal Itkin <eyalit@checkpoint.com>
1d4c55
Date: Fri, 20 Mar 2020 21:19:17 +0200
1d4c55
Subject: Add Safe-Linking to fastbins and tcache
1d4c55
1d4c55
Safe-Linking is a security mechanism that protects single-linked
1d4c55
lists (such as the fastbin and tcache) from being tampered by attackers.
1d4c55
The mechanism makes use of randomness from ASLR (mmap_base), and when
1d4c55
combined with chunk alignment integrity checks, it protects the "next"
1d4c55
pointers from being hijacked by an attacker.
1d4c55
1d4c55
While Safe-Unlinking protects double-linked lists (such as the small
1d4c55
bins), there wasn't any similar protection for attacks against
1d4c55
single-linked lists. This solution protects against 3 common attacks:
1d4c55
  * Partial pointer override: modifies the lower bytes (Little Endian)
1d4c55
  * Full pointer override: hijacks the pointer to an attacker's location
1d4c55
  * Unaligned chunks: pointing the list to an unaligned address
1d4c55
1d4c55
The design assumes an attacker doesn't know where the heap is located,
1d4c55
and uses the ASLR randomness to "sign" the single-linked pointers. We
1d4c55
mark the pointer as P and the location in which it is stored as L, and
1d4c55
the calculation will be:
1d4c55
  * PROTECT(P) := (L >> PAGE_SHIFT) XOR (P)
1d4c55
  * *L = PROTECT(P)
1d4c55
1d4c55
This way, the random bits from the address L (which start at the bit
1d4c55
in the PAGE_SHIFT position), will be merged with LSB of the stored
1d4c55
protected pointer. This protection layer prevents an attacker from
1d4c55
modifying the pointer into a controlled value.
1d4c55
1d4c55
An additional check that the chunks are MALLOC_ALIGNed adds an
1d4c55
important layer:
1d4c55
  * Attackers can't point to illegal (unaligned) memory addresses
1d4c55
  * Attackers must guess correctly the alignment bits
1d4c55
1d4c55
On standard 32 bit Linux machines, an attack will directly fail 7
1d4c55
out of 8 times, and on 64 bit machines it will fail 15 out of 16
1d4c55
times.
1d4c55
1d4c55
This proposed patch was benchmarked and it's effect on the overall
1d4c55
performance of the heap was negligible and couldn't be distinguished
1d4c55
from the default variance between tests on the vanilla version. A
1d4c55
similar protection was added to Chromium's version of TCMalloc
1d4c55
in 2012, and according to their documentation it had an overhead of
1d4c55
less than 2%.
1d4c55
1d4c55
Reviewed-by: DJ Delorie <dj@redhat.com>
1d4c55
Reviewed-by: Carlos O'Donell <carlos@redhat.com>
1d4c55
Reviewed-by: Adhemerval Zacnella <adhemerval.zanella@linaro.org>
1d4c55
1d4c55
diff --git a/malloc/malloc.c b/malloc/malloc.c
1d4c55
index f7cd29bc2f..1282863681 100644
1d4c55
--- a/malloc/malloc.c
1d4c55
+++ b/malloc/malloc.c
1d4c55
@@ -327,6 +327,18 @@ __malloc_assert (const char *assertion, const char *file, unsigned int line,
1d4c55
 # define MAX_TCACHE_COUNT UINT16_MAX
1d4c55
 #endif
1d4c55
 
1d4c55
+/* Safe-Linking:
1d4c55
+   Use randomness from ASLR (mmap_base) to protect single-linked lists
1d4c55
+   of Fast-Bins and TCache.  That is, mask the "next" pointers of the
1d4c55
+   lists' chunks, and also perform allocation alignment checks on them.
1d4c55
+   This mechanism reduces the risk of pointer hijacking, as was done with
1d4c55
+   Safe-Unlinking in the double-linked lists of Small-Bins.
1d4c55
+   It assumes a minimum page size of 4096 bytes (12 bits).  Systems with
1d4c55
+   larger pages provide less entropy, although the pointer mangling
1d4c55
+   still works.  */
1d4c55
+#define PROTECT_PTR(pos, ptr) \
1d4c55
+  ((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
1d4c55
+#define REVEAL_PTR(ptr)  PROTECT_PTR (&ptr, ptr)
1d4c55
 
1d4c55
 /*
1d4c55
   REALLOC_ZERO_BYTES_FREES should be set if a call to
1d4c55
@@ -2157,12 +2169,15 @@ do_check_malloc_state (mstate av)
1d4c55
 
1d4c55
       while (p != 0)
1d4c55
         {
1d4c55
+	  if (__glibc_unlikely (!aligned_OK (p)))
1d4c55
+	    malloc_printerr ("do_check_malloc_state(): " \
1d4c55
+			     "unaligned fastbin chunk detected");
1d4c55
           /* each chunk claims to be inuse */
1d4c55
           do_check_inuse_chunk (av, p);
1d4c55
           total += chunksize (p);
1d4c55
           /* chunk belongs in this bin */
1d4c55
           assert (fastbin_index (chunksize (p)) == i);
1d4c55
-          p = p->fd;
1d4c55
+	  p = REVEAL_PTR (p->fd);
1d4c55
         }
1d4c55
     }
1d4c55
 
1d4c55
@@ -2923,7 +2938,7 @@ tcache_put (mchunkptr chunk, size_t tc_idx)
1d4c55
      detect a double free.  */
1d4c55
   e->key = tcache;
1d4c55
 
1d4c55
-  e->next = tcache->entries[tc_idx];
1d4c55
+  e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
1d4c55
   tcache->entries[tc_idx] = e;
1d4c55
   ++(tcache->counts[tc_idx]);
1d4c55
 }
1d4c55
@@ -2934,9 +2949,11 @@ static __always_inline void *
1d4c55
 tcache_get (size_t tc_idx)
1d4c55
 {
1d4c55
   tcache_entry *e = tcache->entries[tc_idx];
1d4c55
-  tcache->entries[tc_idx] = e->next;
1d4c55
+  tcache->entries[tc_idx] = REVEAL_PTR (e->next);
1d4c55
   --(tcache->counts[tc_idx]);
1d4c55
   e->key = NULL;
1d4c55
+  if (__glibc_unlikely (!aligned_OK (e)))
1d4c55
+    malloc_printerr ("malloc(): unaligned tcache chunk detected");
1d4c55
   return (void *) e;
1d4c55
 }
1d4c55
 
1d4c55
@@ -2960,7 +2977,10 @@ tcache_thread_shutdown (void)
1d4c55
       while (tcache_tmp->entries[i])
1d4c55
 	{
1d4c55
 	  tcache_entry *e = tcache_tmp->entries[i];
1d4c55
-	  tcache_tmp->entries[i] = e->next;
1d4c55
+      if (__glibc_unlikely (!aligned_OK (e)))
1d4c55
+	malloc_printerr ("tcache_thread_shutdown(): " \
1d4c55
+			 "unaligned tcache chunk detected");
1d4c55
+	  tcache_tmp->entries[i] = REVEAL_PTR (e->next);
1d4c55
 	  __libc_free (e);
1d4c55
 	}
1d4c55
     }
1d4c55
@@ -3570,8 +3590,11 @@ _int_malloc (mstate av, size_t bytes)
1d4c55
       victim = pp;					\
1d4c55
       if (victim == NULL)				\
1d4c55
 	break;						\
1d4c55
+      pp = REVEAL_PTR (victim->fd);                                     \
1d4c55
+      if (__glibc_unlikely (!aligned_OK (pp)))                          \
1d4c55
+	malloc_printerr ("malloc(): unaligned fastbin chunk detected"); \
1d4c55
     }							\
1d4c55
-  while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim)) \
1d4c55
+  while ((pp = catomic_compare_and_exchange_val_acq (fb, pp, victim)) \
1d4c55
 	 != victim);					\
1d4c55
 
1d4c55
   if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
1d4c55
@@ -3583,8 +3606,11 @@ _int_malloc (mstate av, size_t bytes)
1d4c55
 
1d4c55
       if (victim != NULL)
1d4c55
 	{
1d4c55
+	  if (__glibc_unlikely (!aligned_OK (victim)))
1d4c55
+	    malloc_printerr ("malloc(): unaligned fastbin chunk detected");
1d4c55
+
1d4c55
 	  if (SINGLE_THREAD_P)
1d4c55
-	    *fb = victim->fd;
1d4c55
+	    *fb = REVEAL_PTR (victim->fd);
1d4c55
 	  else
1d4c55
 	    REMOVE_FB (fb, pp, victim);
1d4c55
 	  if (__glibc_likely (victim != NULL))
1d4c55
@@ -3605,8 +3631,10 @@ _int_malloc (mstate av, size_t bytes)
1d4c55
 		  while (tcache->counts[tc_idx] < mp_.tcache_count
1d4c55
 			 && (tc_victim = *fb) != NULL)
1d4c55
 		    {
1d4c55
+		      if (__glibc_unlikely (!aligned_OK (tc_victim)))
1d4c55
+			malloc_printerr ("malloc(): unaligned fastbin chunk detected");
1d4c55
 		      if (SINGLE_THREAD_P)
1d4c55
-			*fb = tc_victim->fd;
1d4c55
+			*fb = REVEAL_PTR (tc_victim->fd);
1d4c55
 		      else
1d4c55
 			{
1d4c55
 			  REMOVE_FB (fb, pp, tc_victim);
1d4c55
@@ -4196,11 +4224,15 @@ _int_free (mstate av, mchunkptr p, int have_lock)
1d4c55
 	    LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
1d4c55
 	    for (tmp = tcache->entries[tc_idx];
1d4c55
 		 tmp;
1d4c55
-		 tmp = tmp->next)
1d4c55
+		 tmp = REVEAL_PTR (tmp->next))
1d4c55
+        {
1d4c55
+	      if (__glibc_unlikely (!aligned_OK (tmp)))
1d4c55
+		malloc_printerr ("free(): unaligned chunk detected in tcache 2");
1d4c55
 	      if (tmp == e)
1d4c55
 		malloc_printerr ("free(): double free detected in tcache 2");
1d4c55
 	    /* If we get here, it was a coincidence.  We've wasted a
1d4c55
 	       few cycles, but don't abort.  */
1d4c55
+        }
1d4c55
 	  }
1d4c55
 
1d4c55
 	if (tcache->counts[tc_idx] < mp_.tcache_count)
1d4c55
@@ -4264,7 +4296,7 @@ _int_free (mstate av, mchunkptr p, int have_lock)
1d4c55
 	   add (i.e., double free).  */
1d4c55
 	if (__builtin_expect (old == p, 0))
1d4c55
 	  malloc_printerr ("double free or corruption (fasttop)");
1d4c55
-	p->fd = old;
1d4c55
+	p->fd = PROTECT_PTR (&p->fd, old);
1d4c55
 	*fb = p;
1d4c55
       }
1d4c55
     else
1d4c55
@@ -4274,7 +4306,8 @@ _int_free (mstate av, mchunkptr p, int have_lock)
1d4c55
 	     add (i.e., double free).  */
1d4c55
 	  if (__builtin_expect (old == p, 0))
1d4c55
 	    malloc_printerr ("double free or corruption (fasttop)");
1d4c55
-	  p->fd = old2 = old;
1d4c55
+	  old2 = old;
1d4c55
+	  p->fd = PROTECT_PTR (&p->fd, old);
1d4c55
 	}
1d4c55
       while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2))
1d4c55
 	     != old2);
1d4c55
@@ -4472,13 +4505,17 @@ static void malloc_consolidate(mstate av)
1d4c55
     if (p != 0) {
1d4c55
       do {
1d4c55
 	{
1d4c55
+	  if (__glibc_unlikely (!aligned_OK (p)))
1d4c55
+	    malloc_printerr ("malloc_consolidate(): " \
1d4c55
+			     "unaligned fastbin chunk detected");
1d4c55
+
1d4c55
 	  unsigned int idx = fastbin_index (chunksize (p));
1d4c55
 	  if ((&fastbin (av, idx)) != fb)
1d4c55
 	    malloc_printerr ("malloc_consolidate(): invalid chunk size");
1d4c55
 	}
1d4c55
 
1d4c55
 	check_inuse_chunk(av, p);
1d4c55
-	nextp = p->fd;
1d4c55
+	nextp = REVEAL_PTR (p->fd);
1d4c55
 
1d4c55
 	/* Slightly streamlined version of consolidation code in free() */
1d4c55
 	size = chunksize (p);
1d4c55
@@ -4896,8 +4933,13 @@ int_mallinfo (mstate av, struct mallinfo *m)
1d4c55
 
1d4c55
   for (i = 0; i < NFASTBINS; ++i)
1d4c55
     {
1d4c55
-      for (p = fastbin (av, i); p != 0; p = p->fd)
1d4c55
+      for (p = fastbin (av, i);
1d4c55
+	   p != 0;
1d4c55
+	   p = REVEAL_PTR (p->fd))
1d4c55
         {
1d4c55
+	  if (__glibc_unlikely (!aligned_OK (p)))
1d4c55
+	    malloc_printerr ("int_mallinfo(): " \
1d4c55
+			     "unaligned fastbin chunk detected");
1d4c55
           ++nfastblocks;
1d4c55
           fastavail += chunksize (p);
1d4c55
         }
1d4c55
@@ -5437,8 +5479,11 @@ __malloc_info (int options, FILE *fp)
1d4c55
 
1d4c55
 	      while (p != NULL)
1d4c55
 		{
1d4c55
+		  if (__glibc_unlikely (!aligned_OK (p)))
1d4c55
+		    malloc_printerr ("__malloc_info(): " \
1d4c55
+				     "unaligned fastbin chunk detected");
1d4c55
 		  ++nthissize;
1d4c55
-		  p = p->fd;
1d4c55
+		  p = REVEAL_PTR (p->fd);
1d4c55
 		}
1d4c55
 
1d4c55
 	      fastavail += nthissize * thissize;