Hi,

This patch adds an opt-in malloc configuration option placing canaries after
small allocations to detect heap overflows on free(...). It's intended to be
used alongside guard pages for large allocations. Since it's essentially
adding extra padding to all small allocations, a small heap overflow will be
rendered harmless.

The current implementation uses pointer-size canaries, but it could be easily
extended to allow bumping up the size of the canaries by passing the option
multiple times. The entry points into malloc account for the canary size when
it's enabled and then it's generated on allocation and checked on free. Small
allocations without room for a canary are simply turned into large
allocations. Some care needs to be taken to avoid clobbering the canary in the
junk filling code and realloc copying.

The canary is placed at the very end of the memory allocations so there will
often be slack space in between the real allocation and the canary preventing
small overflows from being detected. It would be much better at detecting
corruption with finer-grained size classes. The extreme would be every
multiple of the alignment, but logarithmic growth would be more realistic (see
jemalloc's size classes). Finer-grained size classes would also reduce the
memory overhead caused by allocations being pushed into the next size class by
the canary.

The canaries are currently generated with canary_value ^ hash(canary_address).
It would be best to avoid involving addresses to avoid introducing address
leaks via read overflows where there were none before, but it's the easiest
way to get unique canaries and is a minor issue to improve down the road.

I implemented this feature after porting OpenBSD malloc to Android (in
CopperheadOS) and it has found a few bugs in the app ecosystem. Note that I've
only heavily tested it there, not on OpenBSD itself. I'm not sure if you want
this feature but it seemed worth submitting.

Hopefully you don't mind a patch generated with Git. :)

diff --git a/stdlib/malloc.c b/stdlib/malloc.c
index 424dd77..65b5027 100644
--- a/stdlib/malloc.c
+++ b/stdlib/malloc.c
@@ -185,12 +185,14 @@ struct malloc_readonly {
        int     malloc_move;            /* move allocations to end of page? */
        int     malloc_realloc;         /* always realloc? */
        int     malloc_xmalloc;         /* xmalloc behaviour? */
+       size_t  malloc_canaries;        /* use canaries after chunks? */
        size_t  malloc_guard;           /* use guard pages after allocations? */
        u_int   malloc_cache;           /* free pages we cache */
 #ifdef MALLOC_STATS
        int     malloc_stats;           /* dump statistics at end */
 #endif
        u_int32_t malloc_canary;        /* Matched against ones in malloc_pool 
*/
+       uintptr_t malloc_chunk_canary;
 };
 
 /* This object is mapped PROT_READ after initialisation to prevent tampering */
@@ -526,6 +528,12 @@ omalloc_init(struct dir_info **dp)
                        case 'A':
                                mopts.malloc_abort = 1;
                                break;
+                       case 'c':
+                               mopts.malloc_canaries = 0;
+                               break;
+                       case 'C':
+                               mopts.malloc_canaries = sizeof(void *);
+                               break;
 #ifdef MALLOC_STATS
                        case 'd':
                                mopts.malloc_stats = 0;
@@ -619,6 +627,9 @@ omalloc_init(struct dir_info **dp)
        while ((mopts.malloc_canary = arc4random()) == 0)
                ;
 
+       arc4random_buf(&mopts.malloc_chunk_canary,
+           sizeof(mopts.malloc_chunk_canary));
+
        /*
         * Allocate dir_info with a guard page on either side. Also
         * randomise offset inside the page at which the dir_info
@@ -984,8 +995,15 @@ malloc_bytes(struct dir_info *d, size_t size, void *f)
        k += (lp - bp->bits) * MALLOC_BITS;
        k <<= bp->shift;
 
+       if (mopts.malloc_canaries && bp->size > 0) {
+               char *end = (char *)bp->page + k + bp->size;
+               uintptr_t *canary = (uintptr_t *)(end - mopts.malloc_canaries);
+               *canary = mopts.malloc_chunk_canary ^ hash(canary);
+       }
+
        if (mopts.malloc_junk == 2 && bp->size > 0)
-               memset((char *)bp->page + k, SOME_JUNK, bp->size);
+               memset((char *)bp->page + k, SOME_JUNK,
+                   bp->size - mopts.malloc_canaries);
        return ((char *)bp->page + k);
 }
 
@@ -999,6 +1017,13 @@ find_chunknum(struct dir_info *d, struct region_info *r, 
void *ptr)
        if (info->canary != d->canary1)
                wrterror("chunk info corrupted", NULL);
 
+       if (mopts.malloc_canaries && info->size > 0) {
+               char *end = (char *)ptr + info->size;
+               uintptr_t *canary = (uintptr_t *)(end - mopts.malloc_canaries);
+               if (*canary != (mopts.malloc_chunk_canary ^ hash(canary)))
+                       wrterror("chunk canary corrupted", ptr);
+       }
+
        /* Find the chunk number on the page */
        chunknum = ((uintptr_t)ptr & MALLOC_PAGEMASK) >> info->shift;
 
@@ -1121,7 +1146,7 @@ omalloc(size_t sz, int zero_fill, void *f)
                /* takes care of SOME_JUNK */
                p = malloc_bytes(pool, sz, f);
                if (zero_fill && p != NULL && sz > 0)
-                       memset(p, 0, sz);
+                       memset(p, 0, sz - mopts.malloc_canaries);
        }
 
        return p;
@@ -1176,6 +1201,8 @@ malloc(size_t size)
                malloc_recurse();
                return NULL;
        }
+       if (size > 0 && size <= MALLOC_MAXCHUNK)
+               size += mopts.malloc_canaries;
        r = omalloc(size, 0, CALLER);
        malloc_active--;
        _MALLOC_UNLOCK();
@@ -1242,7 +1269,7 @@ ofree(void *p)
                int i;
 
                if (mopts.malloc_junk && sz > 0)
-                       memset(p, SOME_FREEJUNK, sz);
+                       memset(p, SOME_FREEJUNK, sz - mopts.malloc_canaries);
                if (!mopts.malloc_freenow) {
                        if (find_chunknum(pool, r, p) == -1)
                                return;
@@ -1386,16 +1413,24 @@ gotit:
                }
        }
        if (newsz <= oldsz && newsz > oldsz / 2 && !mopts.malloc_realloc) {
-               if (mopts.malloc_junk == 2 && newsz > 0)
-                       memset((char *)p + newsz, SOME_JUNK, oldsz - newsz);
+               if (mopts.malloc_junk == 2 && newsz > 0 && newsz < oldsz) {
+                       size_t clearsz = oldsz - newsz;
+                       if (oldsz <= MALLOC_MAXCHUNK)
+                               clearsz -= mopts.malloc_canaries;
+                       memset((char *)p + newsz, SOME_JUNK, clearsz);
+               }
                STATS_SETF(r, f);
                return p;
        } else if (newsz != oldsz || mopts.malloc_realloc) {
                q = omalloc(newsz, 0, f);
                if (q == NULL)
                        return NULL;
-               if (newsz != 0 && oldsz != 0)
-                       memcpy(q, p, oldsz < newsz ? oldsz : newsz);
+               if (newsz != 0 && oldsz != 0) {
+                       size_t copysz = oldsz < newsz ? oldsz : newsz;
+                       if (copysz <= MALLOC_MAXCHUNK)
+                               copysz -= mopts.malloc_canaries;
+                       memcpy(q, p, copysz);
+               }
                ofree(p);
                return q;
        } else {
@@ -1420,6 +1455,8 @@ realloc(void *ptr, size_t size)
                malloc_recurse();
                return NULL;
        }
+       if (size > 0 && size <= MALLOC_MAXCHUNK)
+               size += mopts.malloc_canaries;
        r = orealloc(ptr, size, CALLER);
 
        malloc_active--;
@@ -1468,6 +1505,8 @@ calloc(size_t nmemb, size_t size)
        }
 
        size *= nmemb;
+       if (size > 0 && size <= MALLOC_MAXCHUNK)
+               size += mopts.malloc_canaries;
        r = omalloc(size, 1, CALLER);
 
        malloc_active--;
@@ -1595,6 +1634,8 @@ posix_memalign(void **memptr, size_t alignment, size_t 
size)
                malloc_recurse();
                goto err;
        }
+       if (size > 0 && size <= MALLOC_MAXCHUNK)
+               size += mopts.malloc_canaries;
        r = omemalign(alignment, size, 0, CALLER);
        malloc_active--;
        _MALLOC_UNLOCK();

Reply via email to