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.
-
Compile
buffer-overflow-subobject.c
with a baseline target and binary name ofbuffer-overflow-subobject-baseline
, and with a CHERI-enabled target and binary name ofbuffer-overflow-subobject-cheri
. -
As in the prior exercises, run the binaries.
-
Explore why the CHERI binary didn't fail. Run
buffer-overflow-subobject-cheri
undergdb
and examine the bounds of thebuffer
argument tofill_buf()
. To what do they correspond? -
Recompile the
buffer-overflow-subobject-cheri
binary with the compiler flags-Xclang -cheri-bounds=subobject-safe
. -
Run the program to demonstrate that the buffer overflow is now caught.
-
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.
-
Compile
subobject-list.c
for your CHERI-enabled target tosubobject-list-cheri
and run it. -
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
- the sentinel node (
-
Recompile this program, now with
-Xclang -cheri-bounds=subobject-safe
, and run the result. What happens and why? -
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. -
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.