Exercise heap overflows

This exercise demonstrates inter-object heap buffer overflows on baseline and CHERI-enabled architectures, and asks you to characterize and fix the bug detected by CHERI bounds enforcement.

  1. Compile buffer-overflow-heap.c for the baseline architecture to the binary buffer-overflow-heap-baseline and for the CHERI-aware architecture to buffer-overflow-heap-cheri.

  2. Run both versions, passing 0x20 as the (sole) command line argument. Observe that the CHERI version crashes with "In-address space security exception".

  3. Run the CHERI version, again with 0x20, under gdb and examine the crash in more detail. Where must the bounds on the capability implementing b1 have come from?

  4. Run both programs again, but now with 0x1001 as the argument. Draw a picture of the portion of the heap containing (the end of) b1 and (the start of) b2. There are, in some sense, two different ends of b1 in the baseline program and three in the CHERI program! What are they and how do they arise?

  5. While this program does crash on CHERI, again of a bounds violation, this happens slightly later than might be expected looking at the program's source. In particular, this program actually commits two out of bounds stores using the b1 capability. Examine the output carefully and describe, merely in terms of the mechanism, without venturing philosophical, why the first does not trigger a trap.

  6. Now consider the bigger picture. Since CHERI uses compressed capability bounds, what additional steps must be taken, and by whom, to ensure spatial safety of a C program?

Source Files

buffer-overflow-heap.c

/*
 * SPDX-License-Identifier: BSD-2-Clause
 * Copyright (c) 2022 Microsoft Corporation
 */
#include <assert.h>
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int
main(int argc, char **argv)
{
	char *b1, *b2;

	assert(argc == 2);
	char *p;
	size_t sz = (size_t)strtoull(argv[1], &p, 0);

	assert(sz > 0);
	assert(sz <= 0x8000);

	b1 = malloc(sz);
	assert(b1 != NULL);

	b2 = malloc(sz);
	assert(b2 != NULL);

#ifdef __CHERI_PURE_CAPABILITY__
	printf("sz=%zx, CRRL(sz)=%zx\n", sz,
	    __builtin_cheri_round_representable_length(sz));
	printf("b1=%#p b2=%#p diff=%tx\n", b1, b2, b2 - b1);
#else
	printf("b1=%p b2=%p diff=%tx\n", b1, b2, b2 - b1);
#endif

	/*
	 * The default CheriBSD malloc uses "size classes" for allocations.
	 * Check that we've landed "nearby".
	 */
	assert((ptraddr_t)(b1 + sz) <= (ptraddr_t)b2); 
	assert((ptraddr_t)(b1 + sz + sz/2) > (ptraddr_t)b2); 

	memset(b2, 'B', sz);

	printf("Overflowing by 1\n");
	memset(b1, 'A', sz + 1);

	printf("b2 begins: %.4s\n", b2);


	/* And now let's definitely make trouble */
	const size_t oversz = b2 - b1 + 2 - sz;
	printf("Overflowing by %zx\n", oversz);
	memset(b1 + sz, 'A', oversz);

	printf("b2 begins: %.4s\n", b2);
}

Courseware

This exercise has presentation materials available.