Don't invoke error handlers when the callee errors.

I originally made a forced unwind out of a compartment invoke the
caller's error handler (if it existed) with a special error code.  The
idea was that you could use error handlers to jump to recovery paths,
rather than checking return values.

This seemed like a good idea at the time, but turned out to be a
mistake.  The only code that has ever used this behaviour was the test
suite.  Other error handlers have special-case logic to check if this is
the reason and ignore it if so.  It also added a lot of confusing
control flow in the switcher, which is bad because the switcher is the
core of the TCB.

Fixes #302
diff --git a/sdk/core/switcher/entry.S b/sdk/core/switcher/entry.S
index 80af180..8ef9cca 100644
--- a/sdk/core/switcher/entry.S
+++ b/sdk/core/switcher/entry.S
@@ -485,24 +485,11 @@
 	csrw               CSR_MSHWMB, x1
 #endif
 	cspecialw          mepcc, ct2
-	csb                zero, TrustedStack_offset_inForcedUnwind(csp)
 	// c2 is csp, which will be loaded last and will overwrite the trusted
 	// stack pointer with the thread's stack pointer.
 	reloadRegisters c1, cgp, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, csp
 	mret
 
-// If we detect an invalid entry and there is no error handler installed, we want
-// to resume rather than unwind.
-.Linvalid_entry:
-// Mark this threads as in the middle of a forced unwind.
-	li                 a0, 1
-	csb                a0, TrustedStack_offset_inForcedUnwind(ctp)
-// Make sure we don't leak anything to the compartment.
-// Registers might been used by the call and therefore need zeroing.
-	zeroAllRegistersExcept a0, s0, s1, sp, a2, gp
-// Store an error value in return registers, which will be passed to the
-// caller on unwind. a1 is zeroed by zeroAllRegistersExcept.
-	li                 a0, -1
 // We are starting a forced unwind.  This is reached either when we are unable
 // to run an error handler, or when we do run an error handler and it instructs
 // us to return.  This treats all register values as undefined on entry.
@@ -510,57 +497,32 @@
 	// Pop the trusted stack frame.
 	cjal               .Lpop_trusted_stack_frame
 	cmove              cra, ca2
-.Lout_of_trusted_stack:
-	cmove              ct0, csp
-	// Fetch the trusted stack pointer.
-	cspecialr          csp, mtdc
-	// csp now points to the save reg frame that we can use.
-	// Spill all of the registers that we want to propagate to the caller:
-	// c1(cra), c2(csp), c3(cgp), c8(cs0), c9(cs1), c10(ca0), c11(ca1)
-	csc                ct0, TrustedStack_offset_csp(csp)
-	spillRegisters c1, cgp, c8, c9, c10, c11
-	// Store an unsealed version of cra in the mepcc slot, where it will be
-	// used for mret later.  mret requires an unsealed capability in mepcc, so
-	// we have to unseal it if it is sealed.
-	LoadCapPCC         cs0, compartment_switcher_sealing_key
-	// ca2 at this point was loaded by .Lpop_trusted_stack_frame from the pcc
-	// in the trusted stack and so should always be sealed as a sentry type.
-	cgettype           gp, cra
-	csetaddr           cs0, cs0, gp
-	cunseal            cra, cra, cs0
-	csc                cra, TrustedStack_offset_mepcc(csp)
-	clw                t0, TrustedStack_offset_mstatus(csp)
-	// If gp==2 then the we need to disable interrupts on return, otherwise we
-	// need to enable them.  The interrupt enable bit is bit 7.  We want to set
-	// bit 7 if interrupts are enabled, clear it if they are disabled, but not
-	// toggle any other bits.
-	// Clear the interrupt enable bit unconditionally
-	andi               t0, t0, ~0x80
-	// Set it again if we should have interrupts enabled
-	li                 a3, 2
-	beq                gp, a3, .Ldo_not_enable
-	ori                t0, t0, 0x80
-.Ldo_not_enable:
-	csw                t0, TrustedStack_offset_mstatus(csp)
+	// Zero all registers apart from RA, GP, SP and return args.
+	// cra, cs0, cs1, and cgp were restored from the compartment's stack
+	// csp restored from the trusted stack.
+	// ca0, used for first return value
+	// ca1, used for second return value
+	zeroAllRegistersExcept ra, sp, gp, s0, s1, a0, a1
+	li                 a0, -ECOMPARTMENTFAIL
+	li                 a1, 0
+	cret
 
-	// Zero all registers that we aren't explicitly restoring to avoid leaks
-	// from the faulting callee to the caller.
-	csc                cnull, TrustedStack_offset_c4(csp)
-	csc                cnull, TrustedStack_offset_c5(csp)
-	csc                cnull, TrustedStack_offset_c6(csp)
-	csc                cnull, TrustedStack_offset_c7(csp)
-	csc                cnull, TrustedStack_offset_c12(csp)
-	csc                cnull, TrustedStack_offset_c13(csp)
-	csc                cnull, TrustedStack_offset_c14(csp)
-	csc                cnull, TrustedStack_offset_c15(csp)
-	// Mark this threads as in the middle of a forced unwind.
-	li                 a0, 1
-	csb                a0, TrustedStack_offset_inForcedUnwind(csp)
-	// Spill a fake status and cap cause (CHERI fault, no cause)
-	li                 a0, 0x1c
-	csw                a0, TrustedStack_offset_mcause(csp)
-	csrw               mtval, zero
-	// Fall through to handle error
+
+// If we have run out of trusted stack, then just restore the caller's state
+// and return an error value.
+.Lout_of_trusted_stack:
+	// Restore the spilled values
+	clc                cs0, SPILL_SLOT_cs0(csp)
+	clc                cs1, SPILL_SLOT_cs1(csp)
+	clc                cra, SPILL_SLOT_pcc(csp)
+	clc                cgp, SPILL_SLOT_cgp(csp)
+	cincoffset         csp, csp, SPILL_SLOT_SIZE
+	// Set the return registers
+	li                 a0, -ENOTENOUGHTRUSTEDSTACK
+	li                 a1, 0
+	// Zero everything else
+	zeroAllRegistersExcept ra, sp, gp, s0, s1, a0, a1
+	cret
 
 // If we have a possibly recoverable error, see if we have a useful error
 // handler.  At this point, the register state will have been saved in the
@@ -600,7 +562,7 @@
 	// See if we can find a handler:
 	clhu               tp, TrustedStack_offset_frameoffset(csp)
 	li                 t1, TrustedStack_offset_frames
-	beq                tp, t1, .Lend_of_stack
+	beq                tp, t1, .Lreset_mepcc_and_install_context
 	addi               tp, tp, -TrustedStackFrame_size
 
 	// ctp points to the current available trusted stack frame.
@@ -649,7 +611,7 @@
 	// A value of 0xffff indicates no error handler
 	// Give up if there is no error handler for this compartment.
 	li                 s1, 0xffff
-	beq                s0, s1, .Lno_handler_found
+	beq                s0, s1, .Lforce_unwind
 
 	// The stack may have had its tag cleared at this point, so for stackless
 	// handlers we need to restore the on-entry stack.
@@ -677,10 +639,6 @@
 
 .Lhandler_found:
 
-	// If we have found a handler, mark this threads as no longer on the
-	// force-unwind path.  Any future fault will trigger a forced unwind.
-	csb                zero, TrustedStack_offset_inForcedUnwind(csp)
-
 	// Increment the handler invocation count.
 	clhu               s1, TrustedStackFrame_offset_errorHandlerCount(ctp)
 	addi               s1, s1, 1
@@ -816,23 +774,11 @@
 	j                  .Lhandle_error
 
 
-// We have reached the end of the stack.  If we are in a forced unwind then we
-// just install the context, if we've gone off the top of the stack then we
-// should report this gracefully.
-.Lend_of_stack:
-	clb                a2, TrustedStack_offset_inForcedUnwind(csp)
-	bnez               a2, .Lreset_mepcc_and_install_context
 	// Value 24 is reserved for custom use.
 .Lset_mcause_and_exit_thread:
 	csrw               mcause, 24
 	j                  .Lthread_exit
 
-// No handler was found.  If we are in the middle of unwinding, then we want to
-// just install the context but if this is a fault then we keep going up the
-// stack.
-.Lno_handler_found:
-	clb                a2, TrustedStack_offset_inForcedUnwind(csp)
-	beqz               a2, .Lforce_unwind
 	// The continue-resume path expects the location that we will mret to to be
 	// in ct2.  If we're just resuming, then resume from the stashed link
 	// register value.
diff --git a/sdk/core/switcher/trusted-stack-assembly.h b/sdk/core/switcher/trusted-stack-assembly.h
index 0511ac1..02fc248 100644
--- a/sdk/core/switcher/trusted-stack-assembly.h
+++ b/sdk/core/switcher/trusted-stack-assembly.h
@@ -28,12 +28,12 @@
 
 // Size of everything up to this point
 #	define TSTACK_REGFRAME_SZ (19 * 8)
-// frameoffset, inForcedUnwind and padding
+// frameoffset and padding
 #	define TSTACK_HEADER_SZ 16
 #else
 // Size of everything up to this point
 #	define TSTACK_REGFRAME_SZ ((17 * 8) + (2 * 4))
-// frameoffset, inForcedUnwind and padding
+// frameoffset and padding
 #	define TSTACK_HEADER_SZ 8
 #endif
 // The basic trusted stack is the size of the save area, 8 bytes of state for
@@ -47,7 +47,6 @@
                        TSTACK_REGFRAME_SZ + TSTACK_HEADER_SZ)
 EXPORT_ASSEMBLY_OFFSET(TrustedStack, frameoffset, TSTACK_REGFRAME_SZ)
 EXPORT_ASSEMBLY_OFFSET(TrustedStack, threadID, TSTACK_REGFRAME_SZ + 2)
-EXPORT_ASSEMBLY_OFFSET(TrustedStack, inForcedUnwind, TSTACK_REGFRAME_SZ + 4)
 
 EXPORT_ASSEMBLY_OFFSET(TrustedStackFrame, csp, 0)
 EXPORT_ASSEMBLY_OFFSET(TrustedStackFrame, calleeExportTable, 8)
diff --git a/sdk/core/switcher/tstack.h b/sdk/core/switcher/tstack.h
index 0875249..45af06e 100644
--- a/sdk/core/switcher/tstack.h
+++ b/sdk/core/switcher/tstack.h
@@ -59,17 +59,12 @@
 	 * The ID of the current thread.  Never modified during execution.
 	 */
 	uint16_t threadID;
-	/**
-	 * Flag indicating whether this thread is in the process of a forced
-	 * unwind.  If so, this is one, otherwise it is zero.
-	 */
-	uint8_t inForcedUnwind;
 	// Padding up to multiple of 16-bytes.
 	uint8_t padding[
 #ifdef CONFIG_MSHWM
-	  11
+	  12
 #else
-	  3
+	  4
 #endif
 	];
 	/**
diff --git a/sdk/include/errno.h b/sdk/include/errno.h
index 07cf199..35aa193 100644
--- a/sdk/include/errno.h
+++ b/sdk/include/errno.h
@@ -89,6 +89,7 @@
 #define ENOTRECOVERABLE 131 // State not recoverable.
 #define EOVERFLOW 139       // Value too large to be stored in data type.
 #define ENOTENOUGHSTACK 140 // Insufficient stack space for cross-compartment call.
+#define ENOTENOUGHTRUSTEDSTACK 141 // Insufficient stack space for cross-compartment call.
 #define EWOULDBLOCK EAGAIN  // Operation would block.
 #define ENOTSUP EOPNOTSUPP  // Not supported.
 #define __ELASTERROR 2000   // Users can add values starting here.
diff --git a/sdk/include/fail-simulator-on-error.h b/sdk/include/fail-simulator-on-error.h
index da8f4eb..d26dceb 100644
--- a/sdk/include/fail-simulator-on-error.h
+++ b/sdk/include/fail-simulator-on-error.h
@@ -67,11 +67,6 @@
 			DebugErrorHandler::log(
 			  "Thread exit CSP={}, PCC={}", stackCapability, frame->pcc);
 		}
-		else if (exceptionCode == CHERI::CauseCode::None)
-		{
-			// An unwind occurred from a called compartment, just resume.
-			return ErrorRecoveryBehaviour::InstallContext;
-		}
 		else
 		{
 			// An unexpected error -- log it and end the simulation
diff --git a/tests/allocator-test.cc b/tests/allocator-test.cc
index c64eaac..97f19c9 100644
--- a/tests/allocator-test.cc
+++ b/tests/allocator-test.cc
@@ -615,7 +615,7 @@
 /**
  * Allocator test entry point.
  */
-void test_allocator()
+int test_allocator()
 {
 	GlobalConstructors::run();
 
@@ -682,4 +682,5 @@
 	TEST(quotaLeft == MALLOC_QUOTA,
 	     "After alloc and free from 0x100000-byte quota, {} bytes left",
 	     quotaLeft);
+	return 0;
 }
diff --git a/tests/check_pointer-test.cc b/tests/check_pointer-test.cc
index a678de8..c079185 100644
--- a/tests/check_pointer-test.cc
+++ b/tests/check_pointer-test.cc
@@ -128,7 +128,8 @@
 	     "with a raw capability.");
 }
 
-void test_check_pointer()
+int test_check_pointer()
 {
 	check_pointer_strict_mode(&object);
+	return 0;
 }
diff --git a/tests/compartment_calls-test.cc b/tests/compartment_calls-test.cc
index 68fa93f..2f0cff1 100644
--- a/tests/compartment_calls-test.cc
+++ b/tests/compartment_calls-test.cc
@@ -44,7 +44,7 @@
 	TEST(ret == 0, "compartment_call_inner returend {}", ret);
 }
 
-void test_compartment_call()
+int test_compartment_call()
 {
 	bool outTestFailed = false;
 	int  ret           = 0;
@@ -67,10 +67,9 @@
 	     csp);
 
 	test_number_of_arguments();
-	ret = test_incorrect_export_table_with_handler(nullptr);
-	TEST(ret == -1, "Test incorrect entry point with error handler failed");
 
 	test_incorrect_export_table(nullptr, &outTestFailed);
 	TEST(outTestFailed == false,
 	     "Test incorrect entry point without error handler failed");
+	return 0;
 }
diff --git a/tests/compartment_calls.h b/tests/compartment_calls.h
index a93c81d..b740f31 100644
--- a/tests/compartment_calls.h
+++ b/tests/compartment_calls.h
@@ -43,6 +43,6 @@
   bool *outTestFailed);
 __cheri_compartment(
   "compartment_calls_inner_with_"
-  "handler") int test_incorrect_export_table_with_handler(__cheri_callback void (*fn)());
+  "handler") int test_incorrect_export_table_with_handler(__cheri_callback int (*fn)());
 __cheri_compartment("compartment_calls_outer") void compartment_call_outer();
-constexpr int ConstantValue = 0x41414141;
\ No newline at end of file
+constexpr int ConstantValue = 0x41414141;
diff --git a/tests/compartment_calls_inner_with_handler.cc b/tests/compartment_calls_inner_with_handler.cc
deleted file mode 100644
index d82ca8f..0000000
--- a/tests/compartment_calls_inner_with_handler.cc
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright Microsoft and CHERIoT Contributors.
-// SPDX-License-Identifier: MIT
-
-#define TEST_NAME "Compartment calls (inner compartment)"
-#include "compartment_calls.h"
-#include "tests.hh"
-#include <cheri.hh>
-#include <errno.h>
-#include <tuple>
-
-using namespace CHERI;
-
-extern "C" ErrorRecoveryBehaviour
-compartment_error_handler(ErrorState *frame, size_t mcause, size_t mtval)
-{
-	return ErrorRecoveryBehaviour::ForceUnwind;
-}
-
-int test_incorrect_export_table_with_handler(__cheri_callback void (*fn)())
-{
-	/*
-	 * Trigger a cross-compartment call with an invalid export entry.
-	 */
-
-	debug_log(
-	  "test an incorrect export table entry with error handler installed");
-
-	fn();
-
-	TEST(false, "Should be unreachable");
-
-	return 0;
-}
\ No newline at end of file
diff --git a/tests/crash_recovery-test.cc b/tests/crash_recovery-test.cc
index 884c8d5..0254654 100644
--- a/tests/crash_recovery-test.cc
+++ b/tests/crash_recovery-test.cc
@@ -36,7 +36,7 @@
 	return ErrorRecoveryBehaviour::InstallContext;
 }
 
-void test_crash_recovery()
+int test_crash_recovery()
 {
 	debug_log("Calling crashy compartment indirectly");
 	test_crash_recovery_outer(0);
@@ -46,30 +46,34 @@
 	          "nested call crashed");
 
 	debug_log("Calling crashy compartment to fault and unwind");
-	test_crash_recovery_inner(0);
+	void *ret = test_crash_recovery_inner(0);
 	check_stack();
-	debug_log("Calling crashy compartment returned (crashes: {})", crashes);
-	TEST(crashes == 1, "Failed to notice crash");
+	debug_log("Calling crashy compartment returned ({})", ret);
+	TEST(crashes == 0, "Should not have crashed");
+	TEST(ret != nullptr, "Failed to notice crash");
 
 	debug_log("Calling crashy compartment to return normally");
-	test_crash_recovery_inner(1);
+	ret = test_crash_recovery_inner(1);
 	check_stack();
 	debug_log("Calling crashy compartment returned (crashes: {})", crashes);
-	TEST(crashes == 1, "Should not have crashed");
+	TEST(crashes == 0, "Should not have crashed");
+	TEST(ret == nullptr, "Failed to notice crash");
 	debug_log("Returning normally from crash test");
 
 	debug_log("Calling crashy compartment to double fault and unwind");
-	test_crash_recovery_inner(2);
+	ret = test_crash_recovery_inner(2);
 	check_stack();
 	debug_log("Calling crashy compartment returned (crashes: {})", crashes);
-	TEST(crashes == 2, "Failed to notice crash");
+	TEST(crashes == 0, "Should not have crashed");
+	TEST(ret != nullptr, "Failed to notice crash");
 
 	debug_log(
 	  "Calling crashy compartment to corrupt CSP in stack pointer and unwind");
-	test_crash_recovery_inner(3);
+	ret = test_crash_recovery_inner(3);
 	check_stack();
 	debug_log("Calling crashy compartment returned (crashes: {})", crashes);
-	TEST(crashes == 3, "Failed to notice crash");
+	TEST(crashes == 0, "Should not have crashed");
+	TEST(ret != nullptr, "Failed to notice crash");
 
 	ptraddr_t handlerCount = switcher_handler_invocation_count_reset();
 	TEST(handlerCount == crashes * 2,
@@ -90,5 +94,6 @@
 		// does not return and so will not generate code following it.
 		asm volatile("c.unimp");
 	}
-	TEST(crashes == 3 + MaxCrashes, "Failed to notice crash");
+	TEST(crashes == MaxCrashes, "Failed to notice crash");
+	return 0;
 }
diff --git a/tests/debug-test.c b/tests/debug-test.c
index 03c008c..9be9854 100644
--- a/tests/debug-test.c
+++ b/tests/debug-test.c
@@ -1,7 +1,7 @@
 #include <compartment.h>
 #include <debug.h>
 
-__cheri_compartment("debug_test") void test_debug_c()
+__cheri_compartment("debug_test") int test_debug_c()
 {
 	unsigned char x = 'c';
 	_Bool         t = true;
@@ -22,4 +22,5 @@
 	CHERIOT_INVARIANT(true, "Testing C++ invariant failure");
 	CHERIOT_INVARIANT(
 	  true, "Testing C++ invariant failure: 42:{}", 42, 1, 3, 4, "oops");
+	return 0;
 }
diff --git a/tests/debug-test.cc b/tests/debug-test.cc
index e44bb65..5828f38 100644
--- a/tests/debug-test.cc
+++ b/tests/debug-test.cc
@@ -2,7 +2,7 @@
 #include <compartment.h>
 #include <debug.h>
 
-void test_debug_cxx()
+int test_debug_cxx()
 {
 	unsigned char x = 'c';
 	CHERIOT_DEBUG_LOG("Debug messages",
@@ -19,4 +19,5 @@
 	CHERIOT_INVARIANT(true, "Testing C++ invariant failure");
 	CHERIOT_INVARIANT(
 	  true, "Testing C++ invariant failure: 42:{}", 42, 1, 3, 4, "oops");
+	return 0;
 }
diff --git a/tests/eventgroup-test.cc b/tests/eventgroup-test.cc
index aed68c1..12cebed 100644
--- a/tests/eventgroup-test.cc
+++ b/tests/eventgroup-test.cc
@@ -25,7 +25,7 @@
 	}
 } // namespace
 
-void test_eventgroup()
+int test_eventgroup()
 {
 	EventGroup *group;
 
@@ -70,4 +70,5 @@
 	ret = eventgroup_clear(&t, group, &bits, 0b100);
 	TEST(ret == 0, "Failed to clear event group bits: {}", ret);
 	TEST(bits == 0b1000, "Bits should be 0b1000, but is {}", bits);
+	return 0;
 }
diff --git a/tests/futex-test.cc b/tests/futex-test.cc
index 970253f..4abfe6c 100644
--- a/tests/futex-test.cc
+++ b/tests/futex-test.cc
@@ -21,7 +21,7 @@
                                         true);
 #endif
 
-void test_futex()
+int test_futex()
 {
 	static uint32_t futex;
 	int             ret;
@@ -185,4 +185,5 @@
 	     "PI futex with a zero thread ID returned {}, should be {}",
 	     ret,
 	     -EINVAL);
+	return 0;
 }
diff --git a/tests/list-test.cc b/tests/list-test.cc
index 7db652e..5ea7700 100644
--- a/tests/list-test.cc
+++ b/tests/list-test.cc
@@ -63,7 +63,7 @@
  */
 ds::linked_list::Sentinel<LinkedObject::ObjectRing> objects = {};
 
-void test_list()
+int test_list()
 {
 	debug_log("Testing the list implementation.");
 
@@ -238,4 +238,5 @@
 	     heapAtEnd);
 
 	debug_log("Done testing the list.");
+	return 0;
 }
diff --git a/tests/locks-test.cc b/tests/locks-test.cc
index 7b8db43..d85fea2 100644
--- a/tests/locks-test.cc
+++ b/tests/locks-test.cc
@@ -396,7 +396,7 @@
 
 } // namespace
 
-void test_locks()
+int test_locks()
 {
 	test_lock(flagLock);
 	test_lock(flagLockPriorityInherited);
@@ -412,4 +412,5 @@
 	test_ticket_lock_ordering();
 	test_ticket_lock_overflow();
 	test_recursive_mutex();
+	return 0;
 }
diff --git a/tests/misc-test.cc b/tests/misc-test.cc
index 28352b1..f537067 100644
--- a/tests/misc-test.cc
+++ b/tests/misc-test.cc
@@ -170,7 +170,7 @@
 	     permissions);
 }
 
-void test_misc()
+int test_misc()
 {
 	check_timeouts();
 	check_memchr();
@@ -200,4 +200,5 @@
 	                      void, test_word, true, false, false, false),
 	                    4,
 	                    {Permission::Global, Permission::Load});
+	return 0;
 }
diff --git a/tests/mmio-test.cc b/tests/mmio-test.cc
index 4b090e5..bfffdc1 100644
--- a/tests/mmio-test.cc
+++ b/tests/mmio-test.cc
@@ -14,7 +14,7 @@
 	     p);
 }
 
-void test_mmio()
+int test_mmio()
 {
 	check_permissions(
 	  MMIO_CAPABILITY_WITH_PERMISSIONS(Uart, uart, false, false, false, false),
@@ -74,4 +74,5 @@
 	   Permission::Store,
 	   Permission::LoadStoreCapability,
 	   Permission::LoadMutable});
+	return 0;
 }
diff --git a/tests/multiwaiter-test.cc b/tests/multiwaiter-test.cc
index b899be9..d0f380f 100644
--- a/tests/multiwaiter-test.cc
+++ b/tests/multiwaiter-test.cc
@@ -14,7 +14,7 @@
 using namespace CHERI;
 using namespace thread_pool;
 
-void test_multiwaiter()
+int test_multiwaiter()
 {
 	static uint32_t futex  = 0;
 	static uint32_t futex2 = 0;
@@ -130,4 +130,5 @@
 
 	free(queueMemory);
 	multiwaiter_delete(MALLOC_CAPABILITY, mw);
+	return 0;
 }
diff --git a/tests/queue-test.cc b/tests/queue-test.cc
index 5239f19..da18b87 100644
--- a/tests/queue-test.cc
+++ b/tests/queue-test.cc
@@ -185,10 +185,11 @@
 	debug_log("All FreeRTOS queue tests successful");
 }
 
-void test_queue()
+int test_queue()
 {
 	test_queue_unsealed();
 	test_queue_sealed();
 	test_queue_freertos();
 	debug_log("All queue tests successful");
+	return 0;
 }
diff --git a/tests/stack-test.cc b/tests/stack-test.cc
index 95659c0..c814bf9 100644
--- a/tests/stack-test.cc
+++ b/tests/stack-test.cc
@@ -125,7 +125,7 @@
  *	- compartment stack with incorrect permissions
  *  - invalid compartment stack
  */
-void test_stack()
+int test_stack()
 {
 	int ret = test_with_small_stack(144);
 	TEST(ret == 0,
@@ -178,4 +178,5 @@
 	expect_handler(false);
 
 	test_stack_invalid_on_call(callback);
+	return 0;
 }
diff --git a/tests/static_sealing-test.cc b/tests/static_sealing-test.cc
index 9730719..046d704 100644
--- a/tests/static_sealing-test.cc
+++ b/tests/static_sealing-test.cc
@@ -15,9 +15,10 @@
                                        test,
                                        42);
 
-void test_static_sealing()
+int test_static_sealing()
 {
 	// Get a pointer to it and ask for it to be unsealed.
 	Sealed<TestType> value{STATIC_SEALED_VALUE(test)};
 	test_static_sealed_object(value);
+	return 0;
 }
diff --git a/tests/stdio-test.cc b/tests/stdio-test.cc
index a23aa0b..b7f2ac2 100644
--- a/tests/stdio-test.cc
+++ b/tests/stdio-test.cc
@@ -3,7 +3,7 @@
 #include "tests.hh"
 #include <stdio.h>
 
-void test_stdio()
+int test_stdio()
 {
 	debug_log("Printing 'Hello, world!' to stdout");
 	printf("Hello, world!\n");
@@ -19,4 +19,5 @@
 	TEST(strcmp(buffer, "-42") == 0,
 	     "snprintf(\"%d\", -42) gave {}",
 	     std::string_view{buffer, BufferSize});
+	return 0;
 }
diff --git a/tests/test-runner.cc b/tests/test-runner.cc
index f599156..0c85f14 100644
--- a/tests/test-runner.cc
+++ b/tests/test-runner.cc
@@ -11,6 +11,9 @@
 
 namespace
 {
+	/// Have we detected a crash in any of the compartments?
+	volatile bool crashDetected = false;
+
 	/**
 	 * Read the cycle counter.
 	 */
@@ -33,16 +36,28 @@
 	 */
 	void run_timed(const char *msg, auto &&fn)
 	{
-		int startCycles = rdcycle();
-		fn();
+		bool failed      = false;
+		int  startCycles = rdcycle();
+		if constexpr (std::is_same_v<std::invoke_result_t<decltype(fn)>, void>)
+		{
+			fn();
+		}
+		else
+		{
+			failed = (fn() != 0);
+		}
 		int cycles = rdcycle();
-		debug_log("{} finished in {} cycles", msg, cycles - startCycles);
+
+		if (failed)
+		{
+			debug_log("{} failed", msg);
+			crashDetected = true;
+		}
+		else
+			debug_log("{} finished in {} cycles", msg, cycles - startCycles);
 	}
 } // namespace
 
-/// Have we detected a crash in any of the compartments?
-volatile bool crashDetected = false;
-
 extern "C" enum ErrorRecoveryBehaviour
 compartment_error_handler(struct ErrorState *frame, size_t mcause, size_t mtval)
 {
diff --git a/tests/tests.hh b/tests/tests.hh
index c805bde..14780ed 100644
--- a/tests/tests.hh
+++ b/tests/tests.hh
@@ -5,25 +5,25 @@
 #include <debug.hh>
 #include <thread.h>
 
-__cheri_compartment("eventgroup_test") void test_eventgroup();
-__cheri_compartment("mmio_test") void test_mmio();
-__cheri_compartment("allocator_test") void test_allocator();
-__cheri_compartment("thread_pool_test") void test_thread_pool();
-__cheri_compartment("futex_test") void test_futex();
-__cheri_compartment("queue_test") void test_queue();
-__cheri_compartment("locks_test") void test_locks();
-__cheri_compartment("list_test") void test_list();
-__cheri_compartment("crash_recovery_test") void test_crash_recovery();
-__cheri_compartment("multiwaiter_test") void test_multiwaiter();
-__cheri_compartment("stack_test") void test_stack();
-__cheri_compartment("compartment_calls_test") void test_compartment_call();
-__cheri_compartment("check_pointer_test") void test_check_pointer();
-__cheri_compartment("misc_test") void test_misc();
-__cheri_compartment("static_sealing_test") void test_static_sealing();
-__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();
+__cheri_compartment("eventgroup_test") int test_eventgroup();
+__cheri_compartment("mmio_test") int test_mmio();
+__cheri_compartment("allocator_test") int test_allocator();
+__cheri_compartment("thread_pool_test") int test_thread_pool();
+__cheri_compartment("futex_test") int test_futex();
+__cheri_compartment("queue_test") int test_queue();
+__cheri_compartment("locks_test") int test_locks();
+__cheri_compartment("list_test") int test_list();
+__cheri_compartment("crash_recovery_test") int test_crash_recovery();
+__cheri_compartment("multiwaiter_test") int test_multiwaiter();
+__cheri_compartment("stack_test") int test_stack();
+__cheri_compartment("compartment_calls_test") int test_compartment_call();
+__cheri_compartment("check_pointer_test") int test_check_pointer();
+__cheri_compartment("misc_test") int test_misc();
+__cheri_compartment("static_sealing_test") int test_static_sealing();
+__cheri_compartment("stdio_test") int test_stdio();
+__cheri_compartment("debug_test") int test_debug_cxx();
+__cheri_compartment("debug_test") int test_debug_c();
+__cheri_compartment("unwind_cleanup_test") int test_unwind_cleanup();
 
 // Simple tests don't need a separate compartment.
 void test_global_constructors();
diff --git a/tests/thread_pool-test.cc b/tests/thread_pool-test.cc
index 4499d09..52a08e0 100644
--- a/tests/thread_pool-test.cc
+++ b/tests/thread_pool-test.cc
@@ -44,7 +44,7 @@
 	return ErrorRecoveryBehaviour::InstallContext;
 }
 
-void test_thread_pool()
+int test_thread_pool()
 {
 	// We can't share stack variables, so create a heap allocation that we can
 	// capture as an explicit pointer.
@@ -137,7 +137,7 @@
 	Timeout t{3};
 	thread_sleep(&t);
 	TEST(interrupted, "Worker thread was not interrupted");
-	return;
+	return 0;
 	static cheriot::atomic<uint32_t> barrier{3};
 	auto                             barrierWait = []() {
         uint32_t value = barrier--;
@@ -156,4 +156,5 @@
 	async(barrierWait);
 	barrierWait();
 	debug_log("Thread pool quiesced");
+	return 0;
 }
diff --git a/tests/unwind_cleanup-test.cc b/tests/unwind_cleanup-test.cc
index 6cbf390..e368562 100644
--- a/tests/unwind_cleanup-test.cc
+++ b/tests/unwind_cleanup-test.cc
@@ -127,7 +127,7 @@
 
 } // namespace
 
-void test_unwind_cleanup()
+int test_unwind_cleanup()
 {
 	test_setjmp();
 	test_on_error();
@@ -140,4 +140,5 @@
 	test_from_stack_overflow();
 	test_from_trap();
 	debug_log("Test unwind_cleanup passed");
+	return 0;
 }
diff --git a/tests/xmake.lua b/tests/xmake.lua
index df7727b..7ecc6cb 100644
--- a/tests/xmake.lua
+++ b/tests/xmake.lua
@@ -79,8 +79,6 @@
 test("stack")
 compartment("compartment_calls_inner")
     add_files("compartment_calls_inner.cc")
-compartment("compartment_calls_inner_with_handler")
-    add_files("compartment_calls_inner_with_handler.cc")
 test("compartment_calls")
 test("check_pointer")
 -- Test various APIs that are too small to deserve their own test file
@@ -119,7 +117,7 @@
     add_deps("multiwaiter_test")
     add_deps("ccompile_test")
     add_deps("stack_test", "stack_integrity_thread")
-    add_deps("compartment_calls_test", "compartment_calls_inner", "compartment_calls_inner_with_handler")
+    add_deps("compartment_calls_test", "compartment_calls_inner")
     add_deps("check_pointer_test")
     add_deps("misc_test")
     add_deps("stdio_test")