Tidy simulation_exit

Wrap the cross-call, which can fail, in a noreturn void wrapper that
can't.  Tweak callers appropriately.  Have the scheduler internally use
the platform layer everywhere.
diff --git a/sdk/core/scheduler/main.cc b/sdk/core/scheduler/main.cc
index d4d36a8..132103b 100644
--- a/sdk/core/scheduler/main.cc
+++ b/sdk/core/scheduler/main.cc
@@ -31,9 +31,10 @@
 /**
  * Exit simulation, reporting the error code given as the argument.
  */
-void simulation_exit(uint32_t code)
+int scheduler_simulation_exit(uint32_t code)
 {
 	platform_simulation_exit(code);
+	return -EPROTO;
 }
 #endif
 
@@ -228,8 +229,10 @@
 		           static_cast<uint32_t>(capcause),
 		           badcap);
 
+#ifdef SIMULATION
 		// If we're in simulation, exit here
-		simulation_exit(1);
+		platform_simulation_exit(1);
+#endif
 
 		for (;;)
 		{
@@ -300,9 +303,11 @@
 				// Make the current thread non-runnable.
 				if (Thread::exit())
 				{
+#ifdef SIMULATION
 					// If we have no threads left (not counting the idle
 					// thread), exit.
-					simulation_exit(0);
+					platform_simulation_exit(0);
+#endif
 				}
 				// We cannot continue exiting this thread, make sure we will
 				// pick a new one.
diff --git a/sdk/include/fail-simulator-on-error.h b/sdk/include/fail-simulator-on-error.h
index cad8bd7..1c656aa 100644
--- a/sdk/include/fail-simulator-on-error.h
+++ b/sdk/include/fail-simulator-on-error.h
@@ -67,11 +67,14 @@
 		DebugErrorHandler::log("Unhandled error {} at {}", mcause, frame->pcc);
 	}
 
-	simulation_exit(1);
+#ifdef SIMULATION
 	/*
-	 * simulation_exit may fail (say, we're not on a simulator or there isn't
+	 * simulation exit may fail (say, we're not on a simulator or there isn't
 	 * enough stack space to invoke the function.  In that case, just fall back
 	 * to forcibly unwinding.
 	 */
+	(void)scheduler_simulation_exit(1);
+#endif
+
 	return ErrorRecoveryBehaviour::ForceUnwind;
 }
diff --git a/sdk/include/simulator.h b/sdk/include/simulator.h
index 9de4a77..2d4733b 100644
--- a/sdk/include/simulator.h
+++ b/sdk/include/simulator.h
@@ -3,14 +3,34 @@
 
 #pragma once
 #include <compartment.h>
+#include <errno.h>
 #include <stdint.h>
 
 #ifdef SIMULATION
 /**
  * Exit simulation, reporting the error code given as the argument.
  */
-[[cheri::interrupt_state(disabled)]] void __cheri_compartment("sched")
-  simulation_exit(uint32_t code = 0);
-#else
-static inline void simulation_exit(uint32_t code){};
+[[cheri::interrupt_state(disabled)]] int __cheri_compartment("sched")
+  scheduler_simulation_exit(uint32_t code __if_cxx(= 0));
 #endif
+
+/**
+ * Exit the simulation, if we can, or fall back to an infinite loop.
+ */
+static inline void __attribute__((noreturn))
+simulation_exit(uint32_t code __if_cxx(= 0))
+{
+#ifdef SIMULATION
+	/*
+	 * This fails only if either we are out of (trusted) stack space for the
+	 * cross-call or the platform is misconfigured.  If either of those happen,
+	 * fall back to infinite looping.
+	 */
+	(void)scheduler_simulation_exit(code);
+#endif
+
+	while (true)
+	{
+		yield();
+	}
+};
diff --git a/tests/test-runner.cc b/tests/test-runner.cc
index c9f3acb..37d2958 100644
--- a/tests/test-runner.cc
+++ b/tests/test-runner.cc
@@ -66,10 +66,7 @@
 	if (mcause == 0x2)
 	{
 		debug_log("Test failure in test runner");
-#ifdef SIMULATION
 		simulation_exit(1);
-#endif
-		return ErrorRecoveryBehaviour::ForceUnwind;
 	}
 	debug_log("mcause: {}, pcc: {}", mcause, frame->pcc);
 	auto [reg, cause] = CHERI::extract_cheri_mtval(mtval);
@@ -162,13 +159,5 @@
 
 	TEST(crashDetected == false, "One or more tests failed");
 
-	// Exit the simulator if we are running in simulation.
-#ifdef SIMULATION
 	simulation_exit();
-#endif
-	// Infinite loop if we're not in simulation.
-	while (true)
-	{
-		yield();
-	}
 }