Add setjmp and setjmp-based error handling. This is modelled on the OpenStep approach, which built Objective-C exceptions atop setjmp. This maintains a linked list of on-stack jump buffers. You can unwind out of an error handler and so these can be used for cleanup in case of a trap.
diff --git a/scripts/run_clang_tidy_format.sh b/scripts/run_clang_tidy_format.sh index 6b17baf..aecda51 100755 --- a/scripts/run_clang_tidy_format.sh +++ b/scripts/run_clang_tidy_format.sh
@@ -35,7 +35,7 @@ # FreeRTOS-Compat headers follow FreeRTOS naming conventions and should be # excluded for now. Eventually they should be included for everything except # the identifier naming checks. -HEADERS=$(find ${DIRECTORIES} -name '*.h' -or -name '*.hh' | grep -v libc++ | grep -v third_party | grep -v 'std.*.h' | grep -v errno.h | grep -v strings.h | grep -v string.h | grep -v -assembly.h | grep -v cdefs.h | grep -v /riscv.h | grep -v inttypes.h | grep -v /cheri-builtins.h | grep -v c++-config | grep -v ctype.h | grep -v switcher.h | grep -v assert.h | grep -v std*.h | grep -v /build/ | grep -v microvium | grep -v FreeRTOS-Compat) +HEADERS=$(find ${DIRECTORIES} -name '*.h' -or -name '*.hh' | grep -v libc++ | grep -v third_party | grep -v 'std.*.h' | grep -v errno.h | grep -v strings.h | grep -v string.h | grep -v -assembly.h | grep -v cdefs.h | grep -v /riscv.h | grep -v inttypes.h | grep -v /cheri-builtins.h | grep -v c++-config | grep -v ctype.h | grep -v switcher.h | grep -v assert.h | grep -v std*.h | grep -v setjmp.h | grep -v unwind.h | grep -v /build/ | grep -v microvium | grep -v FreeRTOS-Compat) SOURCES=$(find ${DIRECTORIES} -name '*.cc' | grep -v /build/ | grep -v third_party | grep -v arith64.c) echo Headers: ${HEADERS}
diff --git a/sdk/include/setjmp.h b/sdk/include/setjmp.h new file mode 100644 index 0000000..d8d3cdf --- /dev/null +++ b/sdk/include/setjmp.h
@@ -0,0 +1,64 @@ +// Copyright Microsoft and CHERIoT Contributors. +// SPDX-License-Identifier: MIT +#pragma once + +/** + * This is a minimal implementation of setjmp/longjmp. + * + * CHERIoT cannot store a `jmp_buf` anywhere other than the stack without + * clearing tags (which will then cause `longjmp` to fail). + */ + +#include <stddef.h> +#include <stdint.h> + +/** + * Jump buffer for setjmp/longjmp. + */ +struct __jmp_buf +{ + uintptr_t __cs0; + uintptr_t __cs1; + uintptr_t __csp; + uintptr_t __cra; +}; + +/** + * C requires that `setjmp` and `longjmp` take a `jmp_buf` by reference and so + * this ends up being defined as an array of one element, which allows it to + * both be allocated and passed by reference. + */ +typedef struct __jmp_buf jmp_buf[1]; + +/** + * C `setjmp` function. Returns (up to) twice. First returns 0, returns a + * value passed to `longjmp` on the second return. + */ +__attribute__((returns_twice)) extern "C" int setjmp(jmp_buf env); +__asm__(".section .text.setjmp,\"awG\",@progbits,setjmp,comdat\n" + ".globl setjmp\n" + ".p2align 2\n" + ".type setjmp,@function\n" + "setjmp:\n" + " csc cs0, 0(ca0)\n" + " csc cs1, 8(ca0)\n" + " csc csp, 16(ca0)\n" + " csc cra, 24(ca0)\n" + " li a0, 0\n" + " cret\n"); + +/** + * C `longjmp` function. Does not return, jumps back to the `setjmp` call. + */ +extern "C" void longjmp(jmp_buf env, int val); +__asm__(".section .text.longjmp,\"awG\",@progbits,longjmp,comdat\n" + ".globl longjmp\n" + ".p2align 2\n" + ".type longjmp,@function\n" + "longjmp:\n" + " clc cs0, 0(ca0)\n" + " clc cs1, 8(ca0)\n" + " clc csp, 16(ca0)\n" + " clc cra, 24(ca0)\n" + " mv a0, a1\n" + " cjr cra\n");
diff --git a/sdk/include/unwind.h b/sdk/include/unwind.h new file mode 100644 index 0000000..9049828 --- /dev/null +++ b/sdk/include/unwind.h
@@ -0,0 +1,98 @@ +#pragma once +#include <cdefs.h> +#include <setjmp.h> + +/** + * On-stack linked list of cleanup handlers. + */ +struct CleanupList +{ + /// Next pointer. + CleanupList *next; + /// Jump buffer to return to. + __jmp_buf env; +}; + +/** + * Head of the cleanup list. + * + * This is stored in the space that the switcher reserves at the top of the + * stack. The stack is zeroed on entry to a compartment and so this will be + * null until explicitly written to. + */ +__always_inline static inline struct CleanupList **cleanup_list_head() +{ + void *csp = __builtin_cheri_stack_get(); + ptraddr_t top = + __builtin_cheri_base_get(csp) + __builtin_cheri_length_get(csp); + csp = __builtin_cheri_address_set(csp, top - 8); + return (struct CleanupList **)csp; +} + +/** + * Unwind the stack to the most recent `CHERIOT_HANDLER` block. + */ +__always_inline static inline void cleanup_unwind(void) +{ + CleanupList **__head = cleanup_list_head(); + CleanupList *__top = *__head; + *__head = __top->next; + longjmp(&__top->env, 1); +} + +/** + * Simple error handling macros. These are modelled on the OpenStep exception + * macros and are similarly built on top of `setjmp`. Code between + * `CHERIOT_DURING` and `CHERIOT_HANDLER` corresponds to a `try` block. Code + * between `CHERIOT_HANDLER` and `CHERIOT_END_HANDLER` corresponds to a `catch` + * block, though no exception value is actually thrown. + * + * Any automatic-storage values accessed in both blocks must be declared + * `volatile`. + */ +#define CHERIOT_DURING \ + { \ + CleanupList cleanupListEntry; \ + auto **__head = cleanup_list_head(); \ + cleanupListEntry.next = *__head; \ + *__head = &cleanupListEntry; \ + if (setjmp(&cleanupListEntry.env) == 0) \ + { +/// See CHERIOT_DURING. +#define CHERIOT_HANDLER \ + *__head = cleanupListEntry.next; \ + } \ + else \ + { \ + *__head = cleanupListEntry.next; + +/// See CHERIOT_DURING. +#define CHERIOT_END_HANDLER \ + } \ + } + +#ifdef __cplusplus + +/** + * On-error helper. Invokes `fn` and, if `cleanup_unwind` is called, invokes + * `err`. Destructors in between `fn` and the frame that calls + * `cleanup_unwind` are not called, but this function returns normally and so + * destructors of objects above this on the stack will be called normally. + */ +void on_error(auto fn, auto err) +{ + CHERIOT_DURING + fn(); + CHERIOT_HANDLER + err(); + CHERIOT_END_HANDLER +} + +/** + * On-error helper with no error handler (returns normally from forced unwind). + */ +void on_error(auto fn) +{ + on_error(fn, []() {}); +} +#endif
diff --git a/tests/test-runner.cc b/tests/test-runner.cc index dfc1d10..824de18 100644 --- a/tests/test-runner.cc +++ b/tests/test-runner.cc
@@ -115,6 +115,7 @@ run_timed("Debug helpers (C++)", test_debug_cxx); run_timed("Debug helpers (C)", test_debug_c); run_timed("MMIO", test_mmio); + run_timed("Unwind cleanup", test_unwind_cleanup); run_timed("stdio", test_stdio); run_timed("Static sealing", test_static_sealing); run_timed("Crash recovery", test_crash_recovery);
diff --git a/tests/tests.hh b/tests/tests.hh index 4193ed3..c805bde 100644 --- a/tests/tests.hh +++ b/tests/tests.hh
@@ -23,6 +23,7 @@ __cheri_compartment("stdio_test") void test_stdio(); __cheri_compartment("debug_test") void test_debug_cxx(); __cheri_compartment("debug_test") void test_debug_c(); +__cheri_compartment("unwind_cleanup_test") void test_unwind_cleanup(); // Simple tests don't need a separate compartment. void test_global_constructors();
diff --git a/tests/unwind_cleanup-test.cc b/tests/unwind_cleanup-test.cc new file mode 100644 index 0000000..42158d2 --- /dev/null +++ b/tests/unwind_cleanup-test.cc
@@ -0,0 +1,104 @@ +// Copyright CHERIoT Contributors. +// SPDX-License-Identifier: MIT + +#define TEST_NAME "Test unwind cleanup" +#include "locks.hh" +#include "tests.hh" +#include "unwind.h" + +extern "C" ErrorRecoveryBehaviour +compartment_error_handler(ErrorState *, size_t, size_t) +{ + cleanup_unwind(); + return ErrorRecoveryBehaviour::ForceUnwind; +} + +namespace +{ + void test_setjmp() + { + jmp_buf env; + volatile int x = 0; + if (int r = setjmp(env); r == 0) + { + TEST_EQUAL(x, 0, "setjmp should return 0 the first time"); + x = 42; + longjmp(env, 1); + } + else + { + TEST_EQUAL(r, 1, "setjmp should return 1 the second time"); + TEST_EQUAL( + x, 42, "On the second return, x should have been modified"); + x = 53; + } + TEST_EQUAL(x, 53, "After longjmp, x should have been modified"); + } + + FlagLock flagLock; + + void test_on_error() + { + LockGuard g(flagLock); + on_error([&]() { cleanup_unwind(); }, [&]() { g.unlock(); }); + TEST(!g, "on_error should lock the lock"); + } + + void test_on_error_raii_inner() + { + LockGuard g(flagLock); + // No handler. g's destructor runs after on_error returns. + on_error([&]() { cleanup_unwind(); }); + } + + void test_on_error_raii() + { + test_on_error_raii_inner(); + TEST(flagLock.try_lock(), "raii should have been dropped the lock"); + flagLock.unlock(); + } + + void test_c_macros() + { + volatile int x = 0; + CHERIOT_DURING + { + x = 42; + cleanup_unwind(); + } + CHERIOT_HANDLER + { + TEST_EQUAL(x, 42, "In the handler, x should have been modified"); + x = 53; + } + CHERIOT_END_HANDLER + TEST_EQUAL(x, 53, "After longjmp, object should have been modified"); + } + + void test_from_trap() + { + volatile int x = 0; + CHERIOT_DURING + { + x = 42; + __builtin_trap(); + } + CHERIOT_HANDLER + { + TEST_EQUAL(x, 42, "In the handler, x should have been modified"); + x = 53; + } + CHERIOT_END_HANDLER + TEST_EQUAL(x, 53, "After longjmp, object should have been modified"); + } + +} // namespace + +void test_unwind_cleanup() +{ + test_setjmp(); + test_on_error(); + test_c_macros(); + test_from_trap(); + debug_log("Test unwind_cleanup passed"); +}
diff --git a/tests/xmake.lua b/tests/xmake.lua index bbc1d78..e40c5ab 100644 --- a/tests/xmake.lua +++ b/tests/xmake.lua
@@ -88,6 +88,7 @@ on_load(function(target) target:values_set("shared_objects", { exampleK = 1024, test_word = 4 }, {expand = false}) end) +test("unwind_cleanup") includes(path.join(sdkdir, "lib")) @@ -122,6 +123,7 @@ add_deps("misc_test") add_deps("stdio_test") add_deps("debug_test") + add_deps("unwind_cleanup_test") -- Set the thread entry point to the test runner. on_load(function(target) target:values_set("board", "$(board)")