Explore Subobject Bounds

In the CheriABI run-time environment, bounds are typically associated with memory allocations rather than C types. For example, if a heap memory allocation is made for 1024 bytes, and the structure within it is 768 bytes, then the bounds associated with a pointer will be for the allocation size rather than the structure size.

Subobject Overflows

With subobject bounds, enforcement occurs on C-language objects within allocations. This exercise is similar to earlier buffer-overflow exercises, but is for such an intra-object overflow. In our example, we consider an array within another structure, overflowing onto an integer in the same allocation.

  1. Compile buffer-overflow-subobject.c with a baseline target and binary name of buffer-overflow-subobject-baseline, and with a CHERI-enabled target and binary name of buffer-overflow-subobject-cheri.

  2. As in the prior exercises, run the binaries.

  3. Explore why the CHERI binary didn't fail. Run buffer-overflow-subobject-cheri under gdb and examine the bounds of the buffer argument to fill_buf(). To what do they correspond?

  4. Recompile the buffer-overflow-subobject-cheri binary with the compiler flags -Xclang -cheri-bounds=subobject-safe.

  5. Run the program to demonstrate that the buffer overflow is now caught.

  6. Run the program under gdb and examine the bounds again. What has changed?

Deliberately Using Larger Bounds

Operations like &object->field that move from super-object to sub-object are very natural in C, and there is no similarly concise syntax for the reverse operation. Nevertheless, C programs occasionally do make use of containerof constructs to do exactly that: derive a pointer to the superobject given a pointer to a subobject within.

A common example is intrusive linked lists, as found, for example, in the BSD <sys/queue.h>. subobject-list.c is an extremely minimal example of such, which we will use to explore the behavior of CHERI C here.

  1. Compile subobject-list.c for your CHERI-enabled target to subobject-list-cheri and run it.

  2. What is the length (limit - base) for capabilities to...

    • the sentinel node (&l)
    • a next pointer (ile_next) to a non-sentinel element
    • a previous-next pointer (ile_prevnp) to a non-sentinel element
  3. Recompile this program, now with -Xclang -cheri-bounds=subobject-safe, and run the result. What happens and why?

  4. The CheriBSD system headers have been extended so that examples like this which use the <sys/cdefs.h> definition of __containerof (or things built atop that) will trip static assertions. Try compiling again with -Xclang -cheri-bounds=subobject-safe -DUSE_CDEFS_CONTAINEROF and observe what the compiler tells you.

  5. Make the suggested change, marking struct ilist_elem as `` and recompile once again (with the same flags as just above). Run the resulting program and observe its output. Which bounds have not been narrowed? Which have? Why is that OK?

Source Files

Subobject Overflows

buffer-overflow-subobject.c

/*
 * SPDX-License-Identifier: BSD-2-Clause-DARPA-SSITH-ECATS-HR0011-18-C-0016
 * Copyright (c) 2020 SRI International
 */
#include <stdio.h>

struct buf {
	char buffer[128];
	int i;
} b;

#pragma weak fill_buf
void
fill_buf(char *buf, size_t len)
{
	for (size_t i = 0; i <= len; i++)
		buf[i] = 'b';
}

int
main(void)
{
	b.i = 'c';
	printf("b.i = %c\n", b.i);

	fill_buf(b.buffer, sizeof(b.buffer));

	printf("b.i = %c\n", b.i);

	return 0;
}

#include "asserts.inc"

asserts.inc

/*
 * SPDX-License-Identifier: BSD-2-Clause-DARPA-SSITH-ECATS-HR0011-18-C-0016
 * Copyright (c) 2020 SRI International
 */
#include <stddef.h>

_Static_assert(sizeof(b.buffer) == offsetof(struct buf, i),
    "There must be no padding in struct buf between buffer and i members");

Deliberately Using Larger Bounds

subobject-list.c

/*
 * SPDX-License-Identifier: BSD-2-Clause
 * Copyright (c) 2022 Microsoft Corporation
 *
 * This exercise investigates a circular doubly-linked list with sentinels.
 */
#include <stdio.h>

/*
 * A list element is an intrusive structure (subobject) with a pointer to the
 * next list element and a pointer to the previous node's next pointer.  In the
 * case of an empty list, ile_prevnp points to ile_next.
 */
struct ilist_elem {
	struct ilist_elem **ile_prevnp;
	struct ilist_elem *ile_next;
};

static void
ilist_init_sentinel(struct ilist_elem *s) {
	s->ile_next = s;
	s->ile_prevnp = &s->ile_next;
}

static void
ilist_insert_after(struct ilist_elem *p, struct ilist_elem *n) {
	n->ile_next = p->ile_next;
	p->ile_next = n;
	n->ile_next->ile_prevnp = &n->ile_next;
	n->ile_prevnp = &p->ile_next;
}

static void
ilist_remove(struct ilist_elem *e) {
	e->ile_next->ile_prevnp = e->ile_prevnp;
	*(e->ile_prevnp) = e->ile_next;
}

#define ILIST_FOREACH(h, c) \
	for(c = (h)->ile_next; c != (h); c = c->ile_next)

#ifdef USE_CDEFS_CONTAINEROF
#define ILIST_CONTAINER(elem, type, field) \
	(((elem) == NULL) ? ((type *)NULL) : __containerof((elem), type, field))
#else
#define ILIST_CONTAINER(elem, type, field) \
	(((elem) == NULL) ? ((type *)NULL) : \
		__DEQUALIFY(type*, (const volatile char *)(elem) - \
			__offsetof(type, field)))
#endif

struct obj {
	int val;
	struct ilist_elem ilist __subobject_use_container_bounds;
};

struct ilist_elem l; /* Sentinel element serves as list head */
struct obj obj1 = {1, {}};
struct obj obj2 = {2, {}};
struct obj obj3 = {3, {}};

int
main() {
	struct ilist_elem *cursor;

	ilist_init_sentinel(&l);
	ilist_insert_after(&l, &obj2.ilist);
	ilist_insert_after(&obj2.ilist, &obj3.ilist);
	ilist_insert_after(&l, &obj1.ilist);
	ilist_remove(&obj2.ilist);

	printf("Traversing list=%#p first=%#p lastnp=%#p\n",
		&l, l.ile_next, l.ile_prevnp);
	ILIST_FOREACH(&l, cursor) {
		struct obj *co = ILIST_CONTAINER(cursor, struct obj, ilist);
		printf(" Ilist cursor=%#p\n  next=%#p\n  prevnp=%#p\n",
			cursor, cursor->ile_next, cursor->ile_prevnp);
		printf("  val field at %#p\n",
			/*
			 * This ugly bit of syntax is unfortunate, but avoids
			 * a subobject-bounds-induced trap that isn't the first
			 * one you should think about.  I'm sorry.  Just pretend
			 * this says "&co->val" and, for extra credit, later,
			 * explain why it isn't spelled like that.
			 */
			((char *)co) + __offsetof(struct obj, val));
	}

	printf("Traversing list again, accessing superobject field...\n");
	ILIST_FOREACH(&l, cursor) {
		struct obj *co = ILIST_CONTAINER(cursor, struct obj, ilist);
		printf(" Ilist cursor=%#p value=%d (at %#p)\n", cursor,
			co->val, &co->val);
	}
}

Courseware

This exercise has presentation materials available.