Skip to content

Commit a72020f

Browse files
committed
Add blog about stack usage.
1 parent 2e59a9c commit a72020f

File tree

1 file changed

+73
-0
lines changed

1 file changed

+73
-0
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
---
2+
layout: post
3+
title: "Avoiding stack overflows on CHERIoT"
4+
date: 2024-05-01
5+
categories: rtos stack programming
6+
author: "David Chisnall"
7+
---
8+
9+
If you put code in a compartment and do nothing to protect the interfaces, there are a lot of ways that a caller can make that compartment crash.
10+
In some cases, this doesn't matter.
11+
The compartment exists to provide confidentiality and integrity, but not necessarily availability.
12+
The TLS compartment in the network stack is a good example of this.
13+
It provides strong flow isolation and so making it crash will impact only the TLS session of the caller.
14+
The caller can also simply call the close function on a TLS session and so we don't worry about their ability to break a TLS session, only about their ability to send plain text over the network or extract keying material from the TLS session.
15+
16+
In other contexts, availability is far more important.
17+
In the core of the RTOS, the scheduler and allocator both have availability requirements.
18+
If you can crash the allocator while it holds the lock over the heap, you can prevent any future memory allocation.
19+
If you can crash the scheduler in the middle of updating run queues, you may be able to prevent certain threads ever running.
20+
21+
In most platforms, it's easy to make a function crash by moving the stack pointer to near the end just before calling it.
22+
The function will run off the end of the stack and hit a guard page on MMU / MPU systems or the end of the stack capability on CHERI systems.
23+
This is even more crucial on embedded systems, where stacks tend to be small.
24+
Large desktop or server systems often have stacks of 1-4 MiBs, which are large enough for most programs to treat as infinite.
25+
Embedded systems may have stacks that are under 1 KiB.
26+
Even without the security implications from distrust between compartments, having systems fail because they ran out of stack space is far from ideal.
27+
28+
These problems are relevant on CHERIoT.
29+
The caller can constrain the amount of space available on the stack before a cross-compartment call.
30+
If the callee requires more stack space than the caller provides then this can cause problems.
31+
32+
From the start, the CHERIoT ABI has provided some mitigation for this.
33+
Every cross-compartment entry point is described by an entry in an export table.
34+
One of the fields in an export-table entry indicates the amount of stack space that a call requires.
35+
If the stack has less space than is available then the call will fail without invoking the callee at all.
36+
37+
This leads to an obvious problem: How do you set this value to something sensible?
38+
By default, the compiler sets it to the stack space required for the function that implements the entry point.
39+
This means that any function that doesn't call any other functions is fine.
40+
It also means that, if you put stack checks in the function *before* calling any other functions, then you can guarantee that they will work correctly.
41+
42+
This still leaves a lot of work to determine how much stack space you actually need.
43+
The compiler or other tooling could build a static control-flow graph for the current compilation unit, and possibly even the current compartment, but what happens if you call library functions?
44+
45+
It turns out that we already had the building block for a good solution.
46+
CHERIoT guarantees that you can't accidentally leak data left on the stack through a cross-compartment call.
47+
This is done by zeroing the portion of the stack that is going to be shared before and after a cross-compartment call.
48+
When we did this initially, we quickly realised that we were spending a lot of time zeroing memory that was already full of zeroes.
49+
This got worse the larger stacks were.
50+
If you had a 4 KiB stack and a function that used 128 bytes, you may still end up zeroing almost 4 KiB *twice* (once on call, once on return).
51+
52+
To fix this, we introduced the stack high-water mark.
53+
This is configured with the range of the stack and tracks all stores into that range.
54+
Any store below the current high-water mark (remember, stacks grow down, so the 'high'-water mark is actually at the bottom of the memory for the stack) moves the mark.
55+
The switcher can read this before and after the call and zero only memory that's used.
56+
If you start a thread, do some stack allocation, and then call a new compartment, we don't need to do any zeroing.
57+
If you call some internal functions, return, and then call another compartment, we zero only the bit of the stack that you've left data on.
58+
If you call a compartment that uses 128 bytes of stack space then we will zero 128 bytes of stack on return, independent of the stack size.
59+
60+
This means that we *already* had a mechanism for dynamically determining how much stack a particular compartment entry point needed.
61+
All that we needed to do was expose it.
62+
For C++, we've [wrapped this up in a class that lets you report the highest stack usage for a function across multiple invocations](https://cheriot.org/book/compartments.html#_ensuring_adequate_stack_space).
63+
A lot of functions have data-dependent control flow and so data-dependent worst-case stack usage.
64+
By running a set of tests over a function, you can find the worst-case stack usage.
65+
This class can either log the highest stack usage that it sees or can crash the compartment if the expected amount is exceeded.
66+
67+
Once you've made determined the maximum stack usage for a function, you can use the `__cheriot_minimum_stack` attribute to set the value in the export table.
68+
We've now done this for the allocator and the scheduler, which should make both robust.
69+
70+
Confidentiality and integrity were the primary goals for CHERIoT.
71+
Our first adversarial security evaluation did not find any ways of violating confidentiality or integrity.
72+
Since then, we've been working to add availability to the guarantees that we can provide.
73+
This is just one of many steps in making the CHERIoT platform a solid foundation for high-availability embedded systems.

0 commit comments

Comments
 (0)