|
|
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;
|