-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add example showing inter-compartment switch
This example adds a second compartment, and shows a possible design to achieve inter-compartment switching without (much) privilege escalation. In this file, we setup compartment information within array `comps` (`main.c:84`). This information will be used by the assembly function `switch_compartment`. Further, we save a capability to `switch_compartment()` in a register (`main.c:102`), to be able to call it from a bounded-PCC context, as well as a capability defining the area of memory with compartment information (`main.c:101`). These could be added to the memory area of each created compartment for better security, as well as sealed. Our two compartments provide one entry point each, `comp_f_fn` (`shared.S:93`), and `comp_g_fn` (`shared.S:109`). The design is that compartment `f` will perform a switch to compartment `g`, which performs some observable action (in this instance, sets a specific memory location to a specific integer value). The switching process in (almost) entirely bound within `switch_compartment`, on the execution side, and the memory area with compartment information, for memory access. We note almost, as the executable area is approximated (`main.c:96`).
- Loading branch information
Showing
3 changed files
with
324 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
/* This example adds a second compartment, and shows a possible design to | ||
* achieve inter-compartment switching without (much) privilege escalation. In | ||
* this file, we setup compartment information within array `comps` | ||
* (`main.c:84`). This information will be used by the assembly function | ||
* `switch_compartment`. Further, we save a capability to | ||
* `switch_compartment()` in a register (`main.c:102`), to be able to call it | ||
* from a bounded-PCC context, as well as a capability defining the area of | ||
* memory with compartment information (`main.c:101`). These could be added to | ||
* the memory area of each created compartment for better security, as well as | ||
* sealed. | ||
* | ||
* Our two compartments provide one entry point each, `comp_f_fn` | ||
* (`shared.S:93`), and `comp_g_fn` (`shared.S:109`). The design is that | ||
* compartment `f` will perform a switch to compartment `g`, which performs | ||
* some observable action (in this instance, sets a specific memory location to | ||
* a specific integer value). The switching process in (almost) entirely bound | ||
* within `switch_compartment`, on the execution side, and the memory area with | ||
* compartment information, for memory access. We note almost, as the | ||
* executable area is approximated (`main.c:96`). | ||
*/ | ||
|
||
#include "../../../include/common.h" | ||
#include "../../include/utils.h" | ||
|
||
#include <assert.h> | ||
#include <stddef.h> | ||
#include <stdint.h> | ||
#include <stdlib.h> | ||
#include <string.h> | ||
|
||
#if !defined(__CHERI_CAPABILITY_WIDTH__) || defined(__CHERI_PURE_CAPABILITY__) | ||
#error "This example only works on CHERI hybrid mode" | ||
#endif | ||
|
||
/******************************************************************************* | ||
* Assembly Functions & Consts | ||
******************************************************************************/ | ||
|
||
extern int switch_compartment(); | ||
extern void comp_f_fn(); | ||
extern void comp_g_fn(); | ||
|
||
/******************************************************************************* | ||
* Types & Consts | ||
******************************************************************************/ | ||
|
||
const size_t comp_stack_size = 2000; | ||
const size_t total_comp_size = 5000; | ||
size_t id = 0; | ||
|
||
/* Abstract representation of a compartment. Within 80 bytes we represent: | ||
* - an id (8B) | ||
* - address to the start of the compartment's stack (8B) | ||
* the size of the stack (8B) | ||
* - address to the start of the compartment's heap (8B) | ||
* the size of the heap (8B) | ||
* - alignment padding (8B) | ||
* - the ddc corresponding to the compartment (16B) | ||
* - the function within the compartment to be executed upon switching (16B) | ||
*/ | ||
struct comp | ||
{ | ||
size_t id; | ||
void *stack_addr; | ||
size_t stack_len; | ||
void *heap_addr; | ||
size_t heap_len; | ||
void *__capability ddc; | ||
void *__capability comp_fn; | ||
}; | ||
|
||
// ASM offsets, included here for validation | ||
#include "main.h" | ||
|
||
static_assert(COMP_SIZE == sizeof(struct comp), "Invalid `COMP_SIZE` provided"); | ||
static_assert(COMP_OFFSET_STK_ADDR == offsetof(struct comp, stack_addr), | ||
"Invalid `COMP_OFFSET_STK_ADDR` provided."); | ||
static_assert(COMP_OFFSET_STK_LEN == offsetof(struct comp, stack_len), | ||
"Invalid `COMP_OFFSET_STK_LEN` provided."); | ||
static_assert(COMP_OFFSET_DDC == offsetof(struct comp, ddc), "Invalid `COMP_OFFSET_DDC` provided."); | ||
static_assert(COMP_OFFSET_PCC == offsetof(struct comp, comp_fn), | ||
"Invalid `COMP_OFFSET_PCC` provided."); | ||
|
||
struct comp comps[COMP_COUNT]; | ||
|
||
/******************************************************************************* | ||
* Privileged Functions | ||
******************************************************************************/ | ||
|
||
/* This function stores the compartment data in memory, so that it can be | ||
* accessed by the compartments as needed. | ||
*/ | ||
void executive_switch(struct comp c) | ||
{ | ||
void *__capability switch_cap = (void *__capability) switch_compartment; | ||
switch_cap = cheri_bounds_set(switch_cap, 80 * 4); | ||
|
||
void *__capability comps_addr = (void *__capability) &comps; | ||
comps_addr = cheri_bounds_set(comps_addr, COMP_COUNT * COMP_SIZE); | ||
|
||
asm("mov c19, %w0\n\t" | ||
"mov c20, %w1\n\t" | ||
"mov x0, #0\n\t" | ||
"msr CID_EL0, c0" | ||
: | ||
: "r"(comps_addr), "r"(switch_cap)); | ||
|
||
switch_compartment(); | ||
} | ||
|
||
void add_comp(uint8_t *_start_addr, void (*_comp_fn)()) | ||
{ | ||
assert(id < COMP_COUNT); | ||
|
||
struct comp new_comp; | ||
new_comp.id = id; | ||
|
||
new_comp.stack_addr = (void *) _start_addr; | ||
new_comp.stack_len = comp_stack_size; | ||
new_comp.heap_addr = (void *) (_start_addr + comp_stack_size); | ||
new_comp.heap_len = total_comp_size - comp_stack_size; | ||
|
||
void *__capability comp_ddc = (void *__capability) _start_addr; | ||
comp_ddc = cheri_bounds_set(comp_ddc, total_comp_size); | ||
new_comp.ddc = comp_ddc; | ||
|
||
// Set up a capability pointing to the function we want to call within the | ||
// compartment. This will be loaded as the PCC when the function is called. | ||
void *__capability comp_fn = (void *__capability) _comp_fn; | ||
comp_fn = cheri_bounds_set(comp_fn, 40); | ||
new_comp.comp_fn = comp_fn; | ||
|
||
comps[id] = new_comp; | ||
++id; | ||
} | ||
|
||
/******************************************************************************* | ||
* Main | ||
******************************************************************************/ | ||
|
||
int main() | ||
{ | ||
uint8_t *comp_f = malloc(total_comp_size); | ||
add_comp(comp_f, comp_f_fn); | ||
uint8_t *comp_g = malloc(total_comp_size); | ||
add_comp(comp_g, comp_g_fn); | ||
|
||
executive_switch(comps[0]); | ||
|
||
// Check compartment did indeed execute | ||
assert(comp_g[4000] == 42); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
#define COMP_COUNT 2 | ||
#define COMP_SIZE 80 | ||
#define COMP_OFFSET_STK_ADDR 8 | ||
#define COMP_OFFSET_STK_LEN 16 | ||
#define COMP_OFFSET_DDC 48 | ||
#define COMP_OFFSET_PCC 64 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
// Copyright (c) 2021 The CapableVMs "CHERI Examples" Contributors. | ||
// SPDX-License-Identifier: MIT OR Apache-2.0 | ||
|
||
#include "main.h" | ||
|
||
.global comp_f_fn | ||
.global comp_g_fn | ||
|
||
.text | ||
.balign 4 | ||
|
||
/* The compartment switch function. Expects compartment information to be | ||
* stored in memory (defined by the capability stored in register `c19`). | ||
* Performs a compartment switch based on the id saved in `CID_EL0` (currently | ||
* just an integer). | ||
*/ | ||
.global switch_compartment | ||
.type switch_compartment, "function" | ||
switch_compartment: | ||
|
||
// Store entering compartment's DDC, and move to memory containing | ||
// compartment info | ||
mrs c2, DDC | ||
msr DDC, c19 | ||
|
||
// Get compartment to switch to data | ||
mrs c10, CID_EL0 | ||
mov x11, #COMP_SIZE | ||
mul x10, x10, x11 | ||
|
||
// Load PCC, including function we are jumping to within compartment | ||
add x11, x10, #COMP_OFFSET_PCC | ||
ldr c0, [x19, x11] | ||
|
||
// Load DDC | ||
add x11, x10, #COMP_OFFSET_DDC | ||
ldr c1, [x19, x11] | ||
|
||
// Setup SP | ||
mov x12, sp | ||
add x11, x10, #COMP_OFFSET_STK_ADDR | ||
ldr x11, [x19, x11] | ||
add x13, x10, #COMP_OFFSET_STK_LEN | ||
ldr x13, [x19, x13] | ||
add sp, x11, x13 | ||
|
||
// Derive a new clr to restore PCC, and store it. | ||
cvtp c11, lr | ||
|
||
// Install compartment DDC | ||
msr DDC, c1 | ||
|
||
// Save old DDC (c2), old SP (x12), old LR (c11) on stack | ||
stp c2, c11, [sp, #-48]! | ||
str x12, [sp, #32] | ||
|
||
// Stack layout at this point: | ||
// | ||
// `stack + size` -> ________________________ | ||
// sp + 40 -> [ <alignment pad> ] ^ | ||
// sp + 32 -> [ old SP ] | | ||
// sp + 24 -> [ old CLR (hi64) ] | | ||
// sp + 16 -> [ old CLR (lo64) ] | | ||
// sp + 8 -> [ old DDC (high 64) ] | DDC bounds | ||
// sp + 0 -> [ old DDC (low 64) ] | | ||
// : : | ||
// `stack` -> ________________________v | ||
|
||
bl clean+4 | ||
|
||
// Jump to the function within the compartment we are switching to (this | ||
// also sets PCC) | ||
blr c0 | ||
|
||
// Clean capabilities left in the return value. | ||
mov w0, w0 | ||
bl clean | ||
|
||
// Restore the caller's context and compartment. | ||
ldp c10, clr, [sp] | ||
ldr x12, [sp, #32] | ||
msr DDC, c10 | ||
mov x10, #0 | ||
mov sp, x12 | ||
|
||
ret clr | ||
|
||
/* Compartment from which we call the switcher to perform inter-compartment | ||
* transition. The call is via a capability, to update the PCC bounds | ||
* appropriately to cover `switch_compartment`. | ||
*/ | ||
.type comp_f_fn, "function" | ||
comp_f_fn: | ||
mov x0, #1 | ||
msr CID_EL0, c0 | ||
|
||
// Store the `clr` for exitting `comp_f_fn`; this is overwritten by | ||
// `switch_compartment`. | ||
str clr, [sp, #-16]! | ||
blr c20 | ||
ldr clr, [sp], #16 | ||
|
||
ret clr | ||
|
||
/* The function in this compartment just writes to some memory within its | ||
* bounds, to ensure it is properly called. | ||
*/ | ||
.type comp_g_fn, "function" | ||
comp_g_fn: | ||
mrs c10, DDC | ||
mov x11, 42 | ||
str x11, [x10, #4000] | ||
|
||
ret clr | ||
|
||
|
||
// Inner helper for cleaning capabilities from registers, either side of an | ||
// AAPCS64 function call where some level of distrust exists between caller | ||
// and callee. | ||
// | ||
// Depending on the trust model, this might not be required, but the process | ||
// is included here for demonstration purposes. Note that if data needs to | ||
// be scrubbed as well as capabilities, then NEON registers also need to be | ||
// cleaned. | ||
// | ||
// Callers should enter at an appropriate offset so that live registers | ||
// holding arguments and return values (c0-c7) are preserved. | ||
clean: | ||
mov x0, #0 | ||
mov x1, #0 | ||
mov x2, #0 | ||
mov x3, #0 | ||
mov x4, #0 | ||
mov x5, #0 | ||
mov x6, #0 | ||
mov x7, #0 | ||
mov x8, #0 | ||
mov x9, #0 | ||
mov x10, #0 | ||
mov x11, #0 | ||
mov x12, #0 | ||
mov x13, #0 | ||
mov x14, #0 | ||
mov x15, #0 | ||
mov x16, #0 | ||
mov x17, #0 | ||
// x18 is the "platform register" (for some platforms). If so, it needs to | ||
// be preserved, but here we assume that only the lower 64 bits are | ||
// required. | ||
mov x18, x18 | ||
// x19-x29 are callee-saved, but only the lower 64 bits. | ||
// mov x19, x19 | ||
// mov x20, x20 | ||
mov x21, x21 | ||
mov x22, x22 | ||
mov x23, x23 | ||
mov x24, x24 | ||
mov x25, x25 | ||
mov x26, x26 | ||
mov x27, x27 | ||
mov x28, x28 | ||
mov x29, x29 // FP | ||
// We need LR (x30) to return. The call to this helper already cleaned it. | ||
// Don't replace SP; this needs special handling by the caller anyway. | ||
ret | ||
|