Merge remote-tracking branch 'upstream/main' into update

NB: this requires the 2025-01-21 toolchain (or later)

Change-Id: If26a2fae909bcc734de2a7f024eba27a244fdece
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 5a38f98..71defea 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -2,7 +2,7 @@
   "image": "ghcr.io/cheriot-platform/devcontainer:latest",
   "remoteUser": "cheriot",
   "containerUser": "cheriot",
-  "onCreateCommand": "git config --global --add safe.directory /workspaces/cheriot-rtos && git submodule init && git submodule update && cd tests && xmake f --sdk=/cheriot-tools/ && xmake project -k compile_commands .. && cd .. && for I in ex*/[[:digit:]]* ; do echo $I ; cd $I ; xmake f --sdk=/cheriot-tools/ && xmake project -k compile_commands . && cd ../.. ; done",
+  "onCreateCommand": "git config --global --add safe.directory /workspaces/cheriot-rtos && git submodule update --init --recursive && ./scripts/generate_compile_commands.sh",
   "customizations": {
     "vscode": {
       "extensions": [
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 1e29fe1..98ce086 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -7,6 +7,11 @@
     branches: [ main ]
   merge_group:
   workflow_dispatch:
+    inputs:
+      devcontainer:
+        description: 'Set to override default build container'
+        type: string
+        required: false
 
 jobs:
   run-tests:
@@ -17,17 +22,17 @@
         include:
           - sonata: false
           - build-type: debug
-            build-flags: --debug-loader=y --debug-scheduler=y --debug-allocator=y -m debug
+            build-flags: --debug-loader=y --debug-scheduler=y --debug-allocator=y --allocator-rendering=y -m debug
           - build-type: release
             build-flags: --debug-loader=n --debug-scheduler=n --debug-allocator=n -m release --stack-usage-check-allocator=y --stack-usage-check-scheduler=y
           - board: sonata-simulator
             build-type: release
-            build-flags: --debug-loader=n --debug-scheduler=n --debug-allocator=n -m release --stack-usage-check-allocator=y --stack-usage-check-scheduler=y --testing-model-output=y
+            build-flags: --debug-loader=n --debug-scheduler=n --debug-allocator=n -m release --stack-usage-check-allocator=y --stack-usage-check-scheduler=y
             sonata: true
       fail-fast: false
     runs-on: ubuntu-latest
     container:
-      image: ghcr.io/cheriot-platform/devcontainer:latest
+      image: ${{ inputs.devcontainer || 'ghcr.io/cheriot-platform/devcontainer:latest' }}
       options: --user 1001
     steps:
     - name: Checkout repository and submodules
@@ -40,8 +45,6 @@
         xmake f --board=${{ matrix.board }} --sdk=/cheriot-tools/ ${{ matrix.build-flags }}
         xmake
     - name: Run tests
-      # Test suite needs HyperRAM support to be added to RTOS because SRAM is not big enough.
-      if: ${{ !matrix.sonata }}
       run: |
         cd tests
         xmake run
@@ -83,6 +86,8 @@
       uses: actions/checkout@v4
       with:
         submodules: recursive
+    - name: Generate compiler_commands.json files
+      run: ./scripts/generate_compile_commands.sh
     - name: Run clang-format and clang-tidy
       run: ./scripts/run_clang_tidy_format.sh /cheriot-tools/bin
 
diff --git a/.github/workflows/test-new-xmake-nightly.yml b/.github/workflows/test-new-xmake-nightly.yml
index eac04ab..e4e9664 100644
--- a/.github/workflows/test-new-xmake-nightly.yml
+++ b/.github/workflows/test-new-xmake-nightly.yml
@@ -5,6 +5,15 @@
     - cron: '0 0 * * *'
   workflow_dispatch:
 
+defaults:
+  run:
+    # The `sh` used by default does not understand `source` which `xmake` uses
+    # in its profile script.  (And POSIX requires only dot, `.`, leaving
+    # `source` unspecified.  See
+    # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html
+    # for far too much detai.)
+    shell: bash
+
 jobs:
   run-tests:
     strategy:
@@ -18,24 +27,40 @@
       uses: actions/checkout@v3
       with:
         submodules: recursive
-    - name: Build latest xmake
+    # `xmake update` spawns `xmake` in the background, which is challenging.
+    # Work around this by using daemontools' fghack "anti-backgrounding tool";
+    # see https://cr.yp.to/daemontools/fghack.html and
+    # https://github.com/xmake-io/xmake/issues/6030 for discussion.
+    - name: "Install daemontools"
       run: |
-        xmake update dev
+        sudo apt install -y daemontools
+    - name: "Build latest xmake and prune upstream's"
+      run: |
+        mkdir -p ~/.local
+        fghack xmake update dev
         sudo apt remove -y xmake
-        echo ~/.xmake/profile
-        . ~/.xmake/profile
+    - name: "Reasonableness check: have a look around ~/.local"
+      run: |
+        find ~/.local
+        ls -la ~/.local/bin/xmake
+    - name: "Integrate xmake, generating profile script"
+      run: |
+        ~/.local/bin/xmake update --integrate
+    - name: "Reasonableness check: dump the resulting profile script"
+      run: |
+        ls -l ~/.xmake/profile
+        cat ~/.xmake/profile
     - name: Build tests
       run: |
         pwd
-        echo ~/.xmake/profile
+        . ~/.xmake/profile
         which xmake
         xmake --version
         cd tests
-        xmake f --board=${{ matrix.board }} --sdk=/cheriot-tools/ ${{ matrix.build-flags }}
+        xmake f --board=sail --sdk=/cheriot-tools/ --mode=release
         xmake
     - name: Run tests
       run: |
         . ~/.xmake/profile
-        xmake --version
         cd tests
         xmake run
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
deleted file mode 100644
index 9cd81d7..0000000
--- a/azure-pipelines.yml
+++ /dev/null
@@ -1,149 +0,0 @@
-trigger:
-- core
-
-resources:
-  pipelines:
-  - pipeline: LLVM
-    project: CHERI-MCU
-    source: LLVM
-  - pipeline: Flute-TCM
-    project: CHERI-MCU
-    source: Flute-TCM
-  - pipeline: sail-cheri-mcu
-    project: CHERI-MCU
-    source: sail-cheri-mcu
-
-jobs:
-############################################## Linux Builds
-- job:
-  displayName: RTOS tests
-  pool:
-    vmImage: ubuntu-20.04
-  timeoutInMinutes: 300
-  strategy:
-    matrix:
-      HardwareRevokerRelease:
-        board: flute
-        flags:  --debug-loader=n --debug-scheduler=n --debug-allocator=n
-        mode: release
-      SoftwareRevokerRelease:
-        board: flute-software-revoker
-        flags:  --debug-loader=n --debug-scheduler=n --debug-allocator=n
-        mode: release
-      SailRelease:
-        board: sail
-        flags:  --debug-loader=n --debug-scheduler=n --debug-allocator=n
-        mode: release
-      HardwareRevokerDebug:
-        board: flute
-        flags:  --debug-loader=y --debug-scheduler=y --debug-allocator=y
-        mode: debug
-      SoftwareRevokerDebug:
-        board: flute-software-revoker
-        flags:  --debug-loader=y --debug-scheduler=y --debug-allocator=y
-        mode: debug
-      SailDebug:
-        board: sail
-        flags: --debug-loader=y --debug-scheduler=y --debug-allocator=y
-        mode: debug
-  steps:
-  - checkout: self
-    submodules: recursive
-  - download: LLVM
-  - download: Flute-TCM
-  - download: sail-cheri-mcu
-  - script: |
-      set -eo pipefail
-      sudo add-apt-repository ppa:xmake-io/xmake
-      sudo apt update
-      sudo apt install xmake
-    displayName: 'Installing dependencies'
-  - script: |
-      chmod +x $(Pipeline.Workspace)/$(resources.triggeringAlias)/LLVM/LLVM/bin/* \
-        $(Pipeline.Workspace)/$(resources.triggeringAlias)/Flute-TCM/FluteSimulator/* \
-        $(Pipeline.Workspace)/$(resources.triggeringAlias)/sail-cheri-mcu/SailSimulator/*
-      echo $(Pipeline.Workspace)/$(resources.triggeringAlias)/LLVM
-      echo $(Pipeline.Workspace)
-      ls -R $(Pipeline.Workspace)
-    displayName: 'See where anything is installed'
-  - script: |
-      ls $(Pipeline.Workspace)/$(resources.triggeringAlias)/LLVM/LLVM/bin/
-      echo xmake f -P . --board=$(board) --sdk=$(Pipeline.Workspace)/$(resources.triggeringAlias)/LLVM/LLVM/ $(flags) -m $(mode)
-      xmake f -P . --board=$(board) --sdk=$(Pipeline.Workspace)/$(resources.triggeringAlias)/LLVM/LLVM/ $(flags) -m $(mode)
-    workingDirectory: 'tests'
-    displayName: 'Configure the build'
-  - script: |
-      xmake -P . -v
-    workingDirectory: 'tests'
-    displayName: 'Building the test suite'
-  - script: |
-      $(Pipeline.Workspace)/$(resources.triggeringAlias)/sail-cheri-mcu/SailSimulator/cheriot_sim -p --no-trace build/cheriot/cheriot/$(mode)/test-suite
-    condition: startsWith(variables['board'],'sail')
-    workingDirectory: 'tests'
-    displayName: 'Running the test suite on Sail'
-  - script: |
-      export PATH=$(Pipeline.Workspace)/$(resources.triggeringAlias)/Flute-TCM/FluteSimulator:$PATH
-      for I in `seq 32768` ; do echo 00000000 >> tail.hex ; done
-      elf_to_hex build/cheriot/cheriot/$(mode)/test-suite Mem.hex
-      hex_to_tcm_hex.sh
-      cp tail.hex Mem-TCM-tags-0.hex
-      exe_HW_sim +tohost | tee sim.log
-      EXIT_CODE=$(expr $(printf '%d' $(grep -E -e 'tohost_value is 0x[0-9a-zA-Z]+' -o  sim.log  | awk '{print $3}')) / 2)
-      echo "Exit code: $EXIT_CODE"
-      exit $EXIT_CODE
-    condition: startsWith(variables['board'],'flute')
-    workingDirectory: 'tests'
-    displayName: 'Running the test suite on Flute'
-  - script: |
-      set -eo pipefail
-      for example_dir in $PWD/examples/*/; do
-        cd $example_dir
-        echo Building $example_dir
-        xmake f --board=$(board) --sdk=$(Pipeline.Workspace)/$(resources.triggeringAlias)/LLVM/LLVM/ $(flags) -m $(mode)
-        xmake
-      done
-    displayName: 'Building the examples'
-  - script: |
-      set -eo pipefail
-      for example_dir in $PWD/examples/*/; do
-        cd $example_dir
-        echo Running $example_dir
-        example_name=$(basename ${example_dir#*.})
-        $(Pipeline.Workspace)/$(resources.triggeringAlias)/sail-cheri-mcu/SailSimulator/cheriot_sim \
-          build/cheriot/cheriot/$(mode)/${example_name}
-      done
-    condition: startsWith(variables['board'],'sail')
-    displayName: 'Running the examples'
-
-- job:
-  displayName: Check coding style
-  pool:
-    vmImage: ubuntu-20.04
-  timeoutInMinutes: 300
-  steps:
-  - checkout: self
-    submodules: recursive
-  - download: LLVM
-  - script: |
-      chmod +x $(Pipeline.Workspace)/$(resources.triggeringAlias)/LLVM/LLVM/bin/*
-      echo $(Pipeline.Workspace)/$(resources.triggeringAlias)/LLVM
-      echo $(Pipeline.Workspace)
-      ls -R $(Pipeline.Workspace)
-    displayName: 'See where anything is installed'
-  - script: |
-      ./scripts/run_clang_tidy_format.sh $(Pipeline.Workspace)/$(resources.triggeringAlias)/LLVM/LLVM/bin/
-    displayName: 'Running clang-tidy and clang-format'
-
-- job:
-  displayName: Compliance checks
-  pool:
-    vmImage: windows-latest
-  steps:
-  - task: securedevelopmentteam.vss-secure-development-tools.build-task-credscan.CredScan@2
-    displayName: 'Run CredScan'
-    inputs:
-      debugMode: false
-  - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0
-    displayName: 'Component Detection'
-  - task: securedevelopmentteam.vss-secure-development-tools.build-task-publishsecurityanalysislogs.PublishSecurityAnalysisLogs@3
-    displayName: 'Publish Security Analysis Logs'
diff --git a/benchmarks/timing.h b/benchmarks/timing.h
index 066323e..2d4bf04 100644
--- a/benchmarks/timing.h
+++ b/benchmarks/timing.h
@@ -12,7 +12,7 @@
 		// On Sail, report the number of instructions, the cycle count is
 		// meaningless.
 		__asm__ volatile("csrr %0, minstret" : "=r"(cycles));
-#elifdef IBEX
+#elif defined(IBEX)
 		// CHERIoT-Ibex does not yet implement rdcycle, so read the CSR
 		// directly.
 		__asm__ volatile("csrr %0, mcycle" : "=r"(cycles));
diff --git a/compile_flags.txt b/compile_flags.txt
index ae0cf0a..0ed75a0 100644
--- a/compile_flags.txt
+++ b/compile_flags.txt
@@ -22,6 +22,7 @@
 -DDEBUG_LOADER=true
 -DDEBUG_ALLOCATOR=true
 -DDEBUG_SCHEDULER=true
+-DDEBUG_CXXRT=true
 -DSAIL
 -DCPU_TIMER_HZ=2000
 -DTICK_RATE_HZ=10
diff --git a/docs/Allocator.md b/docs/Allocator.md
index 96f3c5f..fe8a0b2 100644
--- a/docs/Allocator.md
+++ b/docs/Allocator.md
@@ -29,10 +29,10 @@
           "contents": "00001000 00000000 00000000 00000000 00000000 00000000",
           "kind": "SealedObject",
           "sealing_type": {
-            "compartment": "alloc",
+            "compartment": "allocator",
             "key": "MallocKey",
             "provided_by": "build/cheriot/cheriot/release/cherimcu.allocator.compartment",
-            "symbol": "__export.sealing_type.alloc.MallocKey"
+            "symbol": "__export.sealing_type.allocator.MallocKey"
           }
         },
 ```
diff --git a/docs/WritingADeviceDriver.md b/docs/WritingADeviceDriver.md
index 305a4df..f8ffeb7 100644
--- a/docs/WritingADeviceDriver.md
+++ b/docs/WritingADeviceDriver.md
@@ -118,10 +118,10 @@
           "contents": "10000101",
           "kind": "SealedObject",
           "sealing_type": {
-            "compartment": "sched",
+            "compartment": "scheduler",
             "key": "InterruptKey",
             "provided_by": "build/cheriot/cheriot/release/example-firmware.scheduler.compartment",
-            "symbol": "__export.sealing_type.sched.InterruptKey"
+            "symbol": "__export.sealing_type.scheduler.InterruptKey"
           }
 ```
 
@@ -194,11 +194,11 @@
 ### Platform includes
 
 Each board description contains a set of include paths.
-For example, our Flute prototype platform has this:
+For example, our Ibex prototype platform has this:
 
 ```json
     "driver_includes" : [
-        "../include/platform/flute",
+        "../include/platform/ibex",
         "../include/platform/generic-riscv"
     ],
 ```
diff --git a/examples/02.hello_compartment/hello.cc b/examples/02.hello_compartment/hello.cc
index 2a8421b..d71cfa1 100644
--- a/examples/02.hello_compartment/hello.cc
+++ b/examples/02.hello_compartment/hello.cc
@@ -4,7 +4,7 @@
 #include "hello.h"
 // This header adds an error handler that writes to the UART on error.
 // Uncomment it and see that the compartmentalisation policy no longer passes.
-//#include <fail-simulator-on-error.h>
+// #include <fail-simulator-on-error.h>
 
 /// Thread entry point.
 void __cheri_compartment("hello") entry()
diff --git a/examples/04.temporal_safety/allocate.cc b/examples/04.temporal_safety/allocate.cc
index b2cb79b..71c736f 100644
--- a/examples/04.temporal_safety/allocate.cc
+++ b/examples/04.temporal_safety/allocate.cc
@@ -84,9 +84,9 @@
 		Debug::log("heap quota: {}", heap_quota_remaining(MALLOC_CAPABILITY));
 	}
 
-	// Sub object with a fast claim
+	// Sub object with an ephemeral claim
 	{
-		Debug::log("----- Sub object with a fast claim -----");
+		Debug::log("----- Sub object with an ephemeral claim -----");
 		void *x = malloc(100);
 
 		CHERI::Capability y{x};
@@ -96,12 +96,12 @@
 		Debug::log("Sub Object: {}", y);
 		Debug::log("heap quota: {}", heap_quota_remaining(MALLOC_CAPABILITY));
 
-		// Add a fast claim for y
+		// Add an ephemeral claim for y
 		Timeout t{10};
-		heap_claim_fast(&t, y);
+		heap_claim_ephemeral(&t, y);
 
 		// In this freeing x will invalidate both x & y because free
-		// is a cross compartment call, which releases any fast claims.
+		// is a cross compartment call, which releases any ephemeral claims.
 		free(x);
 		Debug::log("After free");
 		Debug::log("Allocated : {}", x);
@@ -119,7 +119,7 @@
 		Debug::log("Allocated : {}", x);
 		Debug::log("heap quota: {}", heap_quota_remaining(MALLOC_CAPABILITY));
 
-		// Get the claimant compartment to make a fast claim
+		// Get the claimant compartment to make a ephemeral claim
 		make_claim(x);
 
 		// free x.  We get out quota back but x remains valid as
diff --git a/examples/05.sealing/identifier.cc b/examples/05.sealing/identifier.cc
index c66db96..08b2c5d 100644
--- a/examples/05.sealing/identifier.cc
+++ b/examples/05.sealing/identifier.cc
@@ -36,7 +36,7 @@
 	// capabilities.
 	auto [unsealed, sealed] =
 	  blocking_forever<token_allocate<Identifier>>(MALLOC_CAPABILITY, key());
-	if (sealed == nullptr)
+	if (!sealed.is_valid())
 	{
 		return nullptr;
 	}
diff --git a/scripts/common.sh b/scripts/common.sh
new file mode 100644
index 0000000..4026df4
--- /dev/null
+++ b/scripts/common.sh
@@ -0,0 +1,20 @@
+# Common functions to include in multiple scripts
+
+function error() {
+    echo "Error: $1"
+    exit 1
+}
+
+function ensure_cheriot_rtos_root () {
+    [ -d sdk ] || error "Please run this script from the root of the cheriot-rtos repository."
+}
+
+function find_sdk () {
+    if [ -n "$1" ]; then
+        SDK="$(readlink -f $1)"
+    elif [ -d "/cheriot-tools/bin" ]; then
+        SDK=/cheriot-tools
+    else
+        error "No SDK found, please provide as first argument."
+    fi
+}
diff --git a/scripts/generate_compile_commands.sh b/scripts/generate_compile_commands.sh
new file mode 100755
index 0000000..a05cbee
--- /dev/null
+++ b/scripts/generate_compile_commands.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+
+# Generate compile commands files for all known projects in this repo
+
+
+. "$(dirname $0)"/common.sh
+
+ensure_cheriot_rtos_root
+
+find_sdk $1
+
+echo "Using SDK=$SDK"
+
+# Generate compile_commands.json for all of the extra tests and examples.
+for dir in tests.extra/*/ ex*/[[:digit:]]* ; do
+    echo Generating compile_commands.json for $dir
+    (cd $dir && xmake f --sdk="${SDK}" && xmake project -k compile_commands)
+done
+
+# Generate the top-level compile-commands.json
+cd tests && xmake f --sdk="${SDK}" && xmake project -k compile_commands ..
diff --git a/scripts/includes/helper_find_llvm_install.sh b/scripts/includes/helper_find_llvm_install.sh
index f9b5bc1..f4cad6b 100644
--- a/scripts/includes/helper_find_llvm_install.sh
+++ b/scripts/includes/helper_find_llvm_install.sh
@@ -32,7 +32,7 @@
 	LLVM_TOOL=$(find_llvm_tool $1)
 
 	if [ ! -x ${LLVM_TOOL} ] ; then
-		echo Unable to locate $1, please set TOOLS_PATH to the directory containing the LLVM toolchain.
+		echo Unable to locate $1, please set TOOLS_PATH to the directory containing the LLVM toolchain. >&2
 		exit 1
 	fi
 
diff --git a/scripts/model_output/sonata-simulator/examples/audit.txt b/scripts/model_output/sonata-simulator/examples/audit.txt
deleted file mode 100644
index f2abe0c..0000000
--- a/scripts/model_output/sonata-simulator/examples/audit.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Producer: Encrypting message 'Hello, World!'
-Entry compartment: Received encrypted message: 'Ifmmp-!Xpsme"' (13 bytes)
-Consumer: Decrypted message: 'Hello, World!'
diff --git a/scripts/model_output/sonata-simulator/examples/error_handling.txt b/scripts/model_output/sonata-simulator/examples/error_handling.txt
deleted file mode 100644
index ed3cf77..0000000
--- a/scripts/model_output/sonata-simulator/examples/error_handling.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-UART compartment: Message provided by caller: hello
-UART compartment: Detected BoundsViolation(0x1) trying to write to UART.  Register CS0(0x8) contained invalid value: 0x101b10 (v:1 0x101b0b-0x101b10 l:0x5 o:0x0 p: - RWcgml -- ---)
-UART compartment: Message provided by caller: 
-UART compartment: Detected PermitLoadViolation(0x12) trying to write to UART.  Register CS0(0x8) contained invalid value: 0x101b0b (v:1 0x101b0b-0x101b10 l:0x5 o:0x0 p: - -W---- -- ---)
-UART compartment: Message provided by caller: Non-malicious string
diff --git a/scripts/model_output/sonata-simulator/examples/hello_compartment.txt b/scripts/model_output/sonata-simulator/examples/hello_compartment.txt
deleted file mode 100644
index 31e3a2a..0000000
--- a/scripts/model_output/sonata-simulator/examples/hello_compartment.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-UART compartment: Hello world
-UART compartment: Hello from the stack
diff --git a/scripts/model_output/sonata-simulator/examples/hello_safe_compartment.txt b/scripts/model_output/sonata-simulator/examples/hello_safe_compartment.txt
deleted file mode 100644
index 0ec9819..0000000
--- a/scripts/model_output/sonata-simulator/examples/hello_safe_compartment.txt
+++ /dev/null
Binary files differ
diff --git a/scripts/model_output/sonata-simulator/examples/hello_world.txt b/scripts/model_output/sonata-simulator/examples/hello_world.txt
deleted file mode 100644
index bcb68d3..0000000
--- a/scripts/model_output/sonata-simulator/examples/hello_world.txt
+++ /dev/null
@@ -1 +0,0 @@
-Hello world compartment: Hello world
diff --git a/scripts/model_output/sonata-simulator/examples/javascript.txt b/scripts/model_output/sonata-simulator/examples/javascript.txt
deleted file mode 100644
index 6fc8329..0000000
--- a/scripts/model_output/sonata-simulator/examples/javascript.txt
+++ /dev/null
@@ -1,10 +0,0 @@
-Hello, World!
-array[0] = 2
-array[1] = 4
-array[2] = 6
-array[3] = 8
-array[4] = 10
-JavaScript hello compartment: Microvium is using 0x1a6 bytes of memory, including 0x60 bytes of heap
-JavaScript hello compartment: Running GC
-JavaScript hello compartment: Microvium is using 0x86 bytes of memory, including 0x14 bytes of heap
-JavaScript hello compartment: Peak heap used: 0x60 bytes, peak stack used: 0x32 bytes
diff --git a/scripts/model_output/sonata-simulator/examples/memory_safety.txt b/scripts/model_output/sonata-simulator/examples/memory_safety.txt
deleted file mode 100644
index 4ebed1f..0000000
--- a/scripts/model_output/sonata-simulator/examples/memory_safety.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-Memory safety compartment: Demonstrate memory safety
-Memory safety compartment: Trigger stack linear overflow
-Memory safety compartment: Detected error in instruction 0x107cac (v:0 0x107820-0x108820 l:0x1000 o:0x0 p: G R-cgm- X- ---)
-Memory safety compartment: Detected BoundsViolation(0x1): Register CA1(0xb) contained invalid value: 0x101ad0 (v:1 0x101ad0-0x101b10 l:0x40 o:0x0 p: - RWcgml -- ---)
-Memory safety compartment: Trigger heap linear overflow
-Memory safety compartment: Detected error in instruction 0x107ab2 (v:0 0x107820-0x108820 l:0x1000 o:0x0 p: G R-cgm- X- ---)
-Memory safety compartment: Detected BoundsViolation(0x1): Register CA0(0xa) contained invalid value: 0x109910 (v:1 0x109910-0x109a10 l:0x100 o:0x0 p: G RWcgm- -- ---)
-Memory safety compartment: Trigger heap nonlinear overflow
-Memory safety compartment: Detected error in instruction 0x107b0c (v:0 0x107820-0x108820 l:0x1000 o:0x0 p: G R-cgm- X- ---)
-Memory safety compartment: Detected BoundsViolation(0x1): Register CA0(0xa) contained invalid value: 0x109a18 (v:1 0x109a18-0x109b18 l:0x100 o:0x0 p: G RWcgm- -- ---)
-Memory safety compartment: Trigger heap use after free
-Memory safety compartment: Detected error in instruction 0x107a58 (v:0 0x107820-0x108820 l:0x1000 o:0x0 p: G R-cgm- X- ---)
-Memory safety compartment: Detected TagViolation(0x2): Register CA0(0xa) contained invalid value: 0x109b20 (v:0 0x109b20-0x109c20 l:0x100 o:0x0 p: G RWcgm- -- ---)
-Memory safety compartment: Trigger storing a stack pointer 0x101ad0 (v:1 0x101ad0-0x101ae0 l:0x10 o:0x0 p: - RWcgml -- ---) into global
-Memory safety compartment: tmp: 0x101ad0 (v:0 0x101ad0-0x101ae0 l:0x10 o:0x0 p: - RWcgml -- ---)
-Memory safety compartment: Detected error in instruction 0x107a28 (v:0 0x107820-0x108820 l:0x1000 o:0x0 p: G R-cgm- X- ---)
-Memory safety compartment: Detected TagViolation(0x2): Register CS0(0x8) contained invalid value: 0x101ad0 (v:0 0x101ad0-0x101ae0 l:0x10 o:0x0 p: - RWcgml -- ---)
diff --git a/scripts/model_output/sonata-simulator/examples/producer-consumer.txt b/scripts/model_output/sonata-simulator/examples/producer-consumer.txt
deleted file mode 100644
index 7a77111..0000000
--- a/scripts/model_output/sonata-simulator/examples/producer-consumer.txt
+++ /dev/null
@@ -1,203 +0,0 @@
-Consumer: Queue set to 0x10a720 (v:1 0x10a720-0x10a778 l:0x58 o:0xb p: G RWcgm- -- ---)
-Producer: Starting producer loop
-Consumer: Waiting for messages
-Consumer: Read 1 from queue
-Consumer: Read 2 from queue
-Consumer: Read 3 from queue
-Consumer: Read 4 from queue
-Consumer: Read 5 from queue
-Consumer: Read 6 from queue
-Consumer: Read 7 from queue
-Consumer: Read 8 from queue
-Consumer: Read 9 from queue
-Consumer: Read 10 from queue
-Consumer: Read 11 from queue
-Consumer: Read 12 from queue
-Consumer: Read 13 from queue
-Consumer: Read 14 from queue
-Consumer: Read 15 from queue
-Consumer: Read 16 from queue
-Consumer: Read 17 from queue
-Consumer: Read 18 from queue
-Consumer: Read 19 from queue
-Consumer: Read 20 from queue
-Consumer: Read 21 from queue
-Consumer: Read 22 from queue
-Consumer: Read 23 from queue
-Consumer: Read 24 from queue
-Consumer: Read 25 from queue
-Consumer: Read 26 from queue
-Consumer: Read 27 from queue
-Consumer: Read 28 from queue
-Consumer: Read 29 from queue
-Consumer: Read 30 from queue
-Consumer: Read 31 from queue
-Consumer: Read 32 from queue
-Consumer: Read 33 from queue
-Consumer: Read 34 from queue
-Consumer: Read 35 from queue
-Consumer: Read 36 from queue
-Consumer: Read 37 from queue
-Consumer: Read 38 from queue
-Consumer: Read 39 from queue
-Consumer: Read 40 from queue
-Consumer: Read 41 from queue
-Consumer: Read 42 from queue
-Consumer: Read 43 from queue
-Consumer: Read 44 from queue
-Consumer: Read 45 from queue
-Consumer: Read 46 from queue
-Consumer: Read 47 from queue
-Consumer: Read 48 from queue
-Consumer: Read 49 from queue
-Consumer: Read 50 from queue
-Consumer: Read 51 from queue
-Consumer: Read 52 from queue
-Consumer: Read 53 from queue
-Consumer: Read 54 from queue
-Consumer: Read 55 from queue
-Consumer: Read 56 from queue
-Consumer: Read 57 from queue
-Consumer: Read 58 from queue
-Consumer: Read 59 from queue
-Consumer: Read 60 from queue
-Consumer: Read 61 from queue
-Consumer: Read 62 from queue
-Consumer: Read 63 from queue
-Consumer: Read 64 from queue
-Consumer: Read 65 from queue
-Consumer: Read 66 from queue
-Consumer: Read 67 from queue
-Consumer: Read 68 from queue
-Consumer: Read 69 from queue
-Consumer: Read 70 from queue
-Consumer: Read 71 from queue
-Consumer: Read 72 from queue
-Consumer: Read 73 from queue
-Consumer: Read 74 from queue
-Consumer: Read 75 from queue
-Consumer: Read 76 from queue
-Consumer: Read 77 from queue
-Consumer: Read 78 from queue
-Consumer: Read 79 from queue
-Consumer: Read 80 from queue
-Consumer: Read 81 from queue
-Consumer: Read 82 from queue
-Consumer: Read 83 from queue
-Consumer: Read 84 from queue
-Consumer: Read 85 from queue
-Consumer: Read 86 from queue
-Consumer: Read 87 from queue
-Consumer: Read 88 from queue
-Consumer: Read 89 from queue
-Consumer: Read 90 from queue
-Consumer: Read 91 from queue
-Consumer: Read 92 from queue
-Consumer: Read 93 from queue
-Consumer: Read 94 from queue
-Consumer: Read 95 from queue
-Consumer: Read 96 from queue
-Consumer: Read 97 from queue
-Consumer: Read 98 from queue
-Consumer: Read 99 from queue
-Consumer: Read 100 from queue
-Consumer: Read 101 from queue
-Consumer: Read 102 from queue
-Consumer: Read 103 from queue
-Consumer: Read 104 from queue
-Consumer: Read 105 from queue
-Consumer: Read 106 from queue
-Consumer: Read 107 from queue
-Consumer: Read 108 from queue
-Consumer: Read 109 from queue
-Consumer: Read 110 from queue
-Consumer: Read 111 from queue
-Consumer: Read 112 from queue
-Consumer: Read 113 from queue
-Consumer: Read 114 from queue
-Consumer: Read 115 from queue
-Consumer: Read 116 from queue
-Consumer: Read 117 from queue
-Consumer: Read 118 from queue
-Consumer: Read 119 from queue
-Consumer: Read 120 from queue
-Consumer: Read 121 from queue
-Consumer: Read 122 from queue
-Consumer: Read 123 from queue
-Consumer: Read 124 from queue
-Consumer: Read 125 from queue
-Consumer: Read 126 from queue
-Consumer: Read 127 from queue
-Consumer: Read 128 from queue
-Consumer: Read 129 from queue
-Consumer: Read 130 from queue
-Consumer: Read 131 from queue
-Consumer: Read 132 from queue
-Consumer: Read 133 from queue
-Consumer: Read 134 from queue
-Consumer: Read 135 from queue
-Consumer: Read 136 from queue
-Consumer: Read 137 from queue
-Consumer: Read 138 from queue
-Consumer: Read 139 from queue
-Consumer: Read 140 from queue
-Consumer: Read 141 from queue
-Consumer: Read 142 from queue
-Consumer: Read 143 from queue
-Consumer: Read 144 from queue
-Consumer: Read 145 from queue
-Consumer: Read 146 from queue
-Consumer: Read 147 from queue
-Consumer: Read 148 from queue
-Consumer: Read 149 from queue
-Consumer: Read 150 from queue
-Consumer: Read 151 from queue
-Consumer: Read 152 from queue
-Consumer: Read 153 from queue
-Consumer: Read 154 from queue
-Consumer: Read 155 from queue
-Consumer: Read 156 from queue
-Consumer: Read 157 from queue
-Consumer: Read 158 from queue
-Consumer: Read 159 from queue
-Consumer: Read 160 from queue
-Consumer: Read 161 from queue
-Consumer: Read 162 from queue
-Consumer: Read 163 from queue
-Consumer: Read 164 from queue
-Consumer: Read 165 from queue
-Consumer: Read 166 from queue
-Consumer: Read 167 from queue
-Consumer: Read 168 from queue
-Consumer: Read 169 from queue
-Consumer: Read 170 from queue
-Consumer: Read 171 from queue
-Consumer: Read 172 from queue
-Consumer: Read 173 from queue
-Consumer: Read 174 from queue
-Consumer: Read 175 from queue
-Consumer: Read 176 from queue
-Consumer: Read 177 from queue
-Consumer: Read 178 from queue
-Consumer: Read 179 from queue
-Consumer: Read 180 from queue
-Consumer: Read 181 from queue
-Consumer: Read 182 from queue
-Producer: Producer sent all messages to consumer
-Consumer: Read 183 from queue
-Consumer: Read 184 from queue
-Consumer: Read 185 from queue
-Consumer: Read 186 from queue
-Consumer: Read 187 from queue
-Consumer: Read 188 from queue
-Consumer: Read 189 from queue
-Consumer: Read 190 from queue
-Consumer: Read 191 from queue
-Consumer: Read 192 from queue
-Consumer: Read 193 from queue
-Consumer: Read 194 from queue
-Consumer: Read 195 from queue
-Consumer: Read 196 from queue
-Consumer: Read 197 from queue
-Consumer: Read 198 from queue
-Consumer: Read 199 from queue
diff --git a/scripts/model_output/sonata-simulator/examples/sealing.txt b/scripts/model_output/sonata-simulator/examples/sealing.txt
deleted file mode 100644
index f6d7fd9..0000000
--- a/scripts/model_output/sonata-simulator/examples/sealing.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-Identifier service: Allocated identifier, sealed capability: 0x109410 (v:1 0x109410-0x109420 l:0x10 o:0xb p: G RWcgm- -- ---)
-unsealed capability: 0x109418 (v:1 0x109418-0x109420 l:0x8 o:0x0 p: G RWcgm- -- ---)
-Caller compartment: Allocated identifier to hold the value 42: 0x109410 (v:1 0x109410-0x109420 l:0x10 o:0xb p: G RWcgm- -- ---)
-Caller compartment: Value is 42
-Caller compartment: Dangling pointer: 0x109410 (v:0 0x109410-0x109420 l:0x10 o:0xb p: G RWcgm- -- ---)
diff --git a/scripts/model_output/sonata-simulator/examples/temporal_safety.txt b/scripts/model_output/sonata-simulator/examples/temporal_safety.txt
deleted file mode 100644
index 37f990e..0000000
--- a/scripts/model_output/sonata-simulator/examples/temporal_safety.txt
+++ /dev/null
@@ -1,52 +0,0 @@
-Allocating compartment: ----- Simple Case -----
-Allocating compartment: Allocated: 0x109910 (v:1 0x109910-0x109940 l:0x30 o:0x0 p: G RWcgm- -- ---)
-Allocating compartment: Use after free: 0x109910 (v:0 0x109910-0x109940 l:0x30 o:0x0 p: G RWcgm- -- ---)
-Allocating compartment: ----- Sub object -----
-Allocating compartment: Allocated : 0x109948 (v:1 0x109948-0x1099b0 l:0x68 o:0x0 p: G RWcgm- -- ---)
-Allocating compartment: Sub Object: 0x109961 (v:1 0x109961-0x109993 l:0x32 o:0x0 p: G RWcgm- -- ---)
-Allocating compartment: heap quota: 3984
-Allocating compartment: After free of sub object
-Allocating compartment: Allocated : 0x109948 (v:1 0x109948-0x1099b0 l:0x68 o:0x0 p: G RWcgm- -- ---)
-Allocating compartment: Sub Object: 0x109961 (v:1 0x109961-0x109993 l:0x32 o:0x0 p: G RWcgm- -- ---)
-Allocating compartment: heap quota: 3984
-Allocating compartment: After free of allocation
-Allocating compartment: Allocated : 0x109948 (v:0 0x109948-0x1099b0 l:0x68 o:0x0 p: G RWcgm- -- ---)
-Allocating compartment: Sub Object: 0x109961 (v:0 0x109961-0x109993 l:0x32 o:0x0 p: G RWcgm- -- ---)
-Allocating compartment: heap quota: 4096
-Allocating compartment: ----- Sub object with a claim -----
-Allocating compartment: Allocated : 0x1099b8 (v:1 0x1099b8-0x109a20 l:0x68 o:0x0 p: G RWcgm- -- ---)
-Allocating compartment: Sub Object: 0x1099d1 (v:1 0x1099d1-0x109a03 l:0x32 o:0x0 p: G RWcgm- -- ---)
-Allocating compartment: heap quota: 3984
-Allocating compartment: heap quota after claim: 3968
-Allocating compartment: After free of allocation
-Allocating compartment: Allocated : 0x1099b8 (v:1 0x1099b8-0x109a20 l:0x68 o:0x0 p: G RWcgm- -- ---)
-Allocating compartment: Sub Object: 0x1099d1 (v:1 0x1099d1-0x109a03 l:0x32 o:0x0 p: G RWcgm- -- ---)
-Allocating compartment: heap quota: 3968
-Allocating compartment: After free of sub object
-Allocating compartment: Allocated : 0x1099b8 (v:0 0x1099b8-0x109a20 l:0x68 o:0x0 p: G RWcgm- -- ---)
-Allocating compartment: Sub Object: 0x1099d1 (v:0 0x1099d1-0x109a03 l:0x32 o:0x0 p: G RWcgm- -- ---)
-Allocating compartment: heap quota: 4096
-Allocating compartment: ----- Sub object with a fast claim -----
-Allocating compartment: Allocated : 0x109a38 (v:1 0x109a38-0x109aa0 l:0x68 o:0x0 p: G RWcgm- -- ---)
-Allocating compartment: Sub Object: 0x109a51 (v:1 0x109a51-0x109a83 l:0x32 o:0x0 p: G RWcgm- -- ---)
-Allocating compartment: heap quota: 3984
-Allocating compartment: After free
-Allocating compartment: Allocated : 0x109a38 (v:0 0x109a38-0x109aa0 l:0x68 o:0x0 p: G RWcgm- -- ---)
-Allocating compartment: Sub Object: 0x109a51 (v:0 0x109a51-0x109a83 l:0x32 o:0x0 p: G RWcgm- -- ---)
-Allocating compartment: heap quota: 4096
-Allocating compartment: ----- Claim in another compartment -----
-Allocating compartment: Allocated : 0x109aa8 (v:1 0x109aa8-0x109ab8 l:0x10 o:0x0 p: G RWcgm- -- ---)
-Allocating compartment: heap quota: 4072
-Claimant compartment: Initial quota: 4096
-Claimant compartment: Make Claim : 0x109aa8 (v:1 0x109aa8-0x109ab8 l:0x10 o:0x0 p: G RWcgm- -- ---)
-Claimant compartment: heap quota: 4056
-Allocating compartment: After free: 0x109aa8 (v:1 0x109aa8-0x109ab8 l:0x10 o:0x0 p: G RWcgm- -- ---)
-Allocating compartment: heap quota: 4096
-Claimant compartment: Show Claim : 0x109aa8 (v:1 0x109aa8-0x109ab8 l:0x10 o:0x0 p: G RWcgm- -- ---)
-Claimant compartment: Initial quota: 4056
-Claimant compartment: Make Claim : 0x109ad0 (v:1 0x109ad0-0x109ae0 l:0x10 o:0x0 p: G RWcgm- -- ---)
-Claimant compartment: heap quota: 4056
-Allocating compartment: After make claim
-Allocating compartment: x: 0x109aa8 (v:0 0x109aa8-0x109ab8 l:0x10 o:0x0 p: G RWcgm- -- ---)
-Allocating compartment: y: 0x109ad0 (v:1 0x109ad0-0x109ae0 l:0x10 o:0x0 p: G RWcgm- -- ---)
-Claimant compartment: Show Claim : 0x109ad0 (v:1 0x109ad0-0x109ae0 l:0x10 o:0x0 p: G RWcgm- -- ---)
diff --git a/scripts/run-flute.sh b/scripts/run-flute.sh
deleted file mode 100755
index 107c9e3..0000000
--- a/scripts/run-flute.sh
+++ /dev/null
@@ -1,45 +0,0 @@
-#!/bin/sh
-if [ -z "${FLUTE_BUILD}" ] ; then
-	echo The FLUTE_BUILD environment variable should be set to the flute build directory
-	exit 0
-fi
-# This script depends on non-portable GNU extensions, so prefer the g-prefixed
-# versions if they exist
-TAIL=tail
-if which gtail  ; then TAIL=gtail ; fi
-HEAD=head
-if which ghead  ; then HEAD=ghead ; fi
-PASTE=paste
-if which gpaste  ; then PASTE=gpaste ; fi
-echo Using ${TAIL}, ${HEAD}, and ${PASTE}
-
-if [ ! -f tail.hex ] ; then
-	for I in $(seq 0 32768) ; do
-		echo 00000000 >> tail.hex
-	done
-fi
-
-${FLUTE_BUILD}/../../Tests/elf_to_hex/elf_to_hex $1 Mem.hex
-
-awk '{print substr($0,33,8); print substr($0,0,8)}' Mem.hex > 1u-0.hex
-awk '{print substr($0,41,8); print substr($0,9,8)}' Mem.hex > 0u-0.hex
-awk '{print substr($0,49,8); print substr($0,17,8)}' Mem.hex > 1l-0.hex
-awk '{print substr($0,57,8); print substr($0,25,8)}' Mem.hex > 0l-0.hex
-
-${TAIL} -n +3 1u-0.hex > 1u-1.hex
-${TAIL} -n +3 0u-0.hex > 0u-1.hex
-${TAIL} -n +3 1l-0.hex > 1l-1.hex
-${TAIL} -n +3 0l-0.hex > 0l-1.hex
-
-${HEAD} -n -4 1u-1.hex > 1u-2.hex
-${HEAD} -n -4 0u-1.hex > 0u-2.hex
-${HEAD} -n -4 1l-1.hex > 1l-2.hex
-${HEAD} -n -4 0l-1.hex > 0l-2.hex
-
-${PASTE} -d \\n 1l-2.hex 1u-2.hex > 1-0.hex
-${PASTE} -d \\n 0l-2.hex 0u-2.hex > 0-0.hex
-
-cat 1-0.hex tail.hex | ${HEAD} -n 32768 > Mem-TCM-1.hex
-cat 0-0.hex tail.hex | ${HEAD} -n 32768 > Mem-TCM-0.hex
-
-${FLUTE_BUILD}/exe_HW_sim +tohost > /dev/null
diff --git a/scripts/run-sonata-1.0.sh b/scripts/run-sonata-1.0.sh
new file mode 100755
index 0000000..d54a780
--- /dev/null
+++ b/scripts/run-sonata-1.0.sh
@@ -0,0 +1,44 @@
+#!/bin/sh
+
+set -e
+
+FIRMWARE_ELF=$1
+
+SCRIPT_DIRECTORY="$(dirname "$(realpath "$0")")"
+. ${SCRIPT_DIRECTORY}/includes/helper_find_llvm_install.sh
+
+STRIP=$(find_llvm_tool_required llvm-strip)
+
+if ! command -v uf2conv > /dev/null ; then
+	echo "uf2conv not found.  On macOS / Linux systems with Python3 installed, you can install it with:"
+	echo "python3 -m pip install --pre -U git+https://github.com/makerdiary/uf2utils.git@main"
+	exit 1
+fi
+
+# Strip the ELF file
+${STRIP} ${FIRMWARE_ELF} -o ${FIRMWARE_ELF}.strip
+# Convert the stripped elf to a UF2 (Microsoft USB Flashing Format) file
+uf2conv ${FIRMWARE_ELF}.strip -b0x00000000 -f0x6CE29E60 -co ${FIRMWARE_ELF}.slot1.uf2
+uf2conv ${FIRMWARE_ELF}.strip -b0x10000000 -f0x6CE29E60 -co ${FIRMWARE_ELF}.slot2.uf2
+uf2conv ${FIRMWARE_ELF}.strip -b0x20000000 -f0x6CE29E60 -co ${FIRMWARE_ELF}.slot3.uf2
+
+
+# Try to copy the firmware to the SONATA drive, if we can find one.
+try_copy()
+{
+	if [ -f $1/SONATA/OPTIONS.TXT ] ; then
+		cp ${FIRMWARE_ELF}.slot1.uf2 $1/SONATA/firmware.uf2
+		echo "Firmware copied to $1/SONATA/"
+		exit
+	fi
+}
+
+# Try some common mount points
+try_copy /Volumes/
+try_copy /run/media/$USER/
+try_copy /run/media/
+try_copy /mnt/
+
+cp ${FIRMWARE_ELF}.slot1.uf2 firmware.uf2
+
+echo "Please copy $(pwd)/firmware.uf2 to the SONATA drive to load."
diff --git a/scripts/run-sonata-sim.sh b/scripts/run-sonata-sim.sh
index 1b4c1a3..6367b69 100755
--- a/scripts/run-sonata-sim.sh
+++ b/scripts/run-sonata-sim.sh
@@ -4,7 +4,7 @@
 
 # Specify the default environment variables if they haven't been already.
 : "${SONATA_SIMULATOR:=/cheriot-tools/bin/sonata_simulator}"
-: "${SONATA_SIMULATOR_BOOT_STUB:=/cheriot-tools/elf/sonata_simulator_boot_stub}"
+: "${SONATA_SIMULATOR_BOOT_STUB:=/cheriot-tools/elf/sonata_simulator_hyperram_boot_stub}"
 : "${SONATA_SIMULATOR_UART_LOG=uart0.log}"
 
 if [ -z "$1" ] ; then
@@ -25,27 +25,14 @@
 # Remove old uart log
 rm -f "${SONATA_SIMULATOR_UART_LOG}"
 
-# If a second argument is provided, check content of UART log.
-if [ -n "$2" ] ; then
-	# Run the simulator in the background.
-	${SONATA_SIMULATOR} -E "${SONATA_SIMULATOR_BOOT_STUB}" -E "$1" &
-	LOOP_TRACKER=0
-	while (( LOOP_TRACKER <= 60 ))
-	do
-		sleep 1s
-		# Returns 0 if found and 1 if not.
-		MATCH_FOUND=$(grep -q -F -f "$2" "${SONATA_SIMULATOR_UART_LOG}"; echo $?)
-		if (( MATCH_FOUND == 0 )) ; then
-			# Match was found so exit with success
-			pkill -P $$
-			exit 0
-		fi
-		LOOP_TRACKER=$((LOOP_TRACKER+1))
-	done
-	# Timeout was hit so no success.
-	pkill -P $$
+if ! ${SONATA_SIMULATOR} -E "${SONATA_SIMULATOR_BOOT_STUB}" -E "$1"; then
+	echo "Simulator exited with failure! UART output:"
+	cat "${SONATA_SIMULATOR_UART_LOG}"
 	exit 4
-else
-	# If there is no second argument, run simulator in foreground.
-	${SONATA_SIMULATOR} -E "${SONATA_SIMULATOR_BOOT_STUB}" -E "$1"
+fi
+
+# Check to see if the output indicates failure
+if grep -i failure "${SONATA_SIMULATOR_UART_LOG}"; then
+	echo "Log output contained 'failure'"
+	exit 5
 fi
diff --git a/scripts/run-sonata.sh b/scripts/run-sonata.sh
index fc4238f..86a6236 100755
--- a/scripts/run-sonata.sh
+++ b/scripts/run-sonata.sh
@@ -9,10 +9,10 @@
 
 OBJCOPY=$(find_llvm_tool_required llvm-objcopy)
 
-command -v uf2conv > /dev/null
-if [ ! $? ] ; then
+if ! command -v uf2conv > /dev/null ; then
 	echo "uf2conv not found.  On macOS / Linux systems with Python3 installed, you can install it with:"
 	echo "python3 -m pip install --pre -U git+https://github.com/makerdiary/uf2utils.git@main"
+	exit 1
 fi
 
 # Convert the ELF file to a binary file
diff --git a/scripts/run_clang_tidy_format.sh b/scripts/run_clang_tidy_format.sh
index aecda51..2bce847 100755
--- a/scripts/run_clang_tidy_format.sh
+++ b/scripts/run_clang_tidy_format.sh
@@ -26,7 +26,7 @@
 else
 	PARALLEL_JOBS=$(sysctl -n kern.smp.cpus)
 fi
-DIRECTORIES="sdk tests examples"
+DIRECTORIES="sdk tests examples tests.extra"
 # Standard headers should be included once we move to a clang-tidy that
 # supports NOLINTBEGIN to disable specific checks over a whole file.
 # In particular, modernize-redundant-void-arg should be disabled in any header
@@ -35,24 +35,23 @@
 # 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 setjmp.h | grep -v unwind.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 | grep -v sunburst/v0.2)
 SOURCES=$(find ${DIRECTORIES} -name '*.cc' | grep -v /build/ | grep -v third_party | grep -v arith64.c)
 
 echo Headers: ${HEADERS}
 echo Sources: ${SOURCES}
-rm -f tidy-*.fail
 
+${CLANG_FORMAT} -i ${HEADERS} ${SOURCES}
+if ! git diff --exit-code ${HEADERS} ${SOURCES} ; then
+	echo clang-format applied changes
+	exit 1
+fi
+
+rm -f tidy.fail-*
 # sh syntax is -c "string" [name [args ...]], so "tidy" here is the name and not included in "$@"
-echo ${HEADERS} ${SOURCES} | xargs -P${PARALLEL_JOBS} -n5 sh -c "${CLANG_TIDY} -export-fixes=\$(mktemp -p. tidy.fail-XXXX) \$@" tidy
+echo ${HEADERS} ${SOURCES} | xargs -P${PARALLEL_JOBS} -n1 sh -c "${CLANG_TIDY} --extra-arg=-DCLANG_TIDY -export-fixes=\$(mktemp -p. tidy.fail-XXXX) \$@" tidy
 if [ $(find . -maxdepth 1 -name 'tidy.fail-*' -size +0 | wc -l) -gt 0 ] ; then
 	# clang-tidy put non-empty output in one of the tidy-*.fail files
 	cat tidy.fail-*
 	exit 1
 fi
-
-${CLANG_FORMAT} -i ${HEADERS} ${SOURCES}
-if git diff --exit-code ${HEADERS} ${SOURCES} ; then
-	exit 0
-fi
-echo clang-format applied changes
-exit 1
diff --git a/sdk/boards/flute-debug-uart.json b/sdk/boards/flute-debug-uart.json
deleted file mode 100644
index b0dd865..0000000
--- a/sdk/boards/flute-debug-uart.json
+++ /dev/null
@@ -1,52 +0,0 @@
-{
-	"devices" :
-	{
-		"clint" : {
-			"start"  : 0x2000000,
-			"length" : 0x10000
-		},
-		"plic" : {
-			"start"  : 0xc000000,
-			"length" : 0x400000
-		},
-		"uart" : {
-			"start" : 0x10000100,
-			"end"   : 0x10000200
-		},
-		"ethernet" : {
-			"start" : 0x10000100,
-			"end"   : 0x10000200
-		},
-		"shadow" : {
-			"start" : 0x40000000,
-			"end"   : 0x40001000
-		},
-		"shadowctrl" : {
-			"start" : 0x40001000,
-			"end"   : 0x40001028
-		}
-	},
-	"instruction_memory" : {
-		"start" : 0x80000000,
-		"end"   : 0x80040000
-	},
-	"heap" : {
-		"end"   : 0x80040000
-	},
-	"revoker" : "hardware",
-	"stack_high_water_mark" : true,
-	"driver_includes" : [
-		"../include/platform/flute",
-		"../include/platform/generic-riscv"
-	],
-	"defines" : [
-		"FLUTE",
-		"FLUTE_SHADOW_BASE=0x40000000U",
-		"FLUTE_SHADOW_SIZE=0x1000U"
-	],
-	"timer_hz" : 40000,
-	"tickrate_hz" : 10,
-	"simulator" : "${sdk}/../scripts/run-flute.sh",
-	"simulation" : true
-}
-
diff --git a/sdk/boards/flute-no-revoker.json b/sdk/boards/flute-no-revoker.json
deleted file mode 100644
index aca256b..0000000
--- a/sdk/boards/flute-no-revoker.json
+++ /dev/null
@@ -1,43 +0,0 @@
-{
-	"devices" :
-	{
-		"clint" : {
-			"start"  : 0x2000000,
-			"length" : 0x10000
-		},
-		"plic" : {
-			"start"  : 0xc000000,
-			"length" : 0x400000
-		},
-		"uart" : {
-			"start" : 0x10000100,
-			"end"   : 0x10000200
-		},
-		"shadow" : {
-			"start" : 0x40000000,
-			"end"   : 0x40001000
-		}
-	},
-	"instruction_memory" : {
-		"start" : 0x80000000,
-		"end"   : 0x80040000
-	},
-	"heap" : {
-		"end"   : 0x80040000
-	},
-	"defines" : [
-		"FLUTE",
-		"FLUTE_SHADOW_BASE=0x40000000U",
-		"FLUTE_SHADOW_SIZE=0x1000U"
-	],
-	"stack_high_water_mark" : false,
-	"driver_includes" : [
-		"../include/platform/flute",
-		"../include/platform/generic-riscv"
-	],
-	"timer_hz" : 40000,
-	"tickrate_hz" : 10,
-	"simulator" : "${sdk}/../scripts/run-flute.sh",
-	"simulation" : true
-}
-
diff --git a/sdk/boards/flute-software-revoker.json b/sdk/boards/flute-software-revoker.json
deleted file mode 100644
index 0968fd2..0000000
--- a/sdk/boards/flute-software-revoker.json
+++ /dev/null
@@ -1,44 +0,0 @@
-{
-	"devices" :
-	{
-		"clint" : {
-			"start"  : 0x2000000,
-			"length" : 0x10000
-		},
-		"plic" : {
-			"start"  : 0xc000000,
-			"length" : 0x400000
-		},
-		"uart" : {
-			"start" : 0x10000100,
-			"end"   : 0x10000200
-		},
-		"shadow" : {
-			"start" : 0x40000000,
-			"end"   : 0x40001000
-		}
-	},
-	"instruction_memory" : {
-		"start" : 0x80000000,
-		"end"   : 0x80040000
-	},
-	"heap" : {
-		"end"   : 0x80040000
-	},
-	"stack_high_water_mark" : false,
-	"driver_includes" : [
-		"../include/platform/flute",
-		"../include/platform/generic-riscv"
-	],
-	"defines" : [
-		"FLUTE",
-		"FLUTE_SHADOW_BASE=0x40000000U",
-		"FLUTE_SHADOW_SIZE=0x1000U"
-	],
-	"timer_hz" : 40000,
-	"tickrate_hz" : 10,
-	"revoker" : "software",
-	"simulator" : "${sdk}/../scripts/run-flute.sh",
-	"simulation" : true
-}
-
diff --git a/sdk/boards/flute.json b/sdk/boards/flute.json
deleted file mode 100644
index 42586f5..0000000
--- a/sdk/boards/flute.json
+++ /dev/null
@@ -1,51 +0,0 @@
-{
-	"devices" :
-	{
-		"clint" : {
-			"start"  : 0x2000000,
-			"length" : 0x10000
-		},
-		"plic" : {
-			"start"  : 0xc000000,
-			"length" : 0x400000
-		},
-		"uart" : {
-			"start" : 0x10000000,
-			"end"   : 0x10000100
-		},
-		"ethernet" : {
-			"start" : 0x10000100,
-			"end"   : 0x10000200
-		},
-		"shadow" : {
-			"start" : 0x40000000,
-			"end"   : 0x40001000
-		},
-		"shadowctrl" : {
-			"start" : 0x40001000,
-			"end"   : 0x40001028
-		}
-	},
-	"instruction_memory" : {
-		"start" : 0x80000000,
-		"end"   : 0x80040000
-	},
-	"heap" : {
-		"end"   : 0x80040000
-	},
-	"driver_includes" : [
-		"../include/platform/flute",
-		"../include/platform/generic-riscv"
-	],
-	"defines" : [
-		"FLUTE",
-		"FLUTE_SHADOW_BASE=0x40000000U",
-		"FLUTE_SHADOW_SIZE=0x1000U"
-	],
-	"timer_hz" : 40000,
-	"tickrate_hz" : 10,
-	"revoker" : "hardware",
-	"simulator" : "${sdk}/../scripts/run-flute.sh",
-	"simulation" : true
-}
-
diff --git a/sdk/boards/ibex-arty-a7-100.json b/sdk/boards/ibex-arty-a7-100.json
deleted file mode 100644
index db54172..0000000
--- a/sdk/boards/ibex-arty-a7-100.json
+++ /dev/null
@@ -1,76 +0,0 @@
-{
-    "devices": {
-        "clint": {
-            "start": 0x14001000,
-            "length": 0x1000
-        },
-        "plic": {
-            "start": 0x10000000,
-            "end": 0x10400000
-        },
-        "revoker": {
-            "start": 0x14000000,
-            "length": 0x1000
-        },
-        "uart": {
-            "start": 0x8f00b000,
-            "end":   0x8f00b100
-        },
-        "shadow" : {
-            "start": 0x200fe000,
-            "length": 0x2000
-        },
-        "gpio_led0" : {
-            "start": 0x8f00f000,
-            "length": 0x800
-        },
-        "kunyan_ethernet": {
-            "start": 0x14004000,
-            "end": 0x14008000
-        }
-    },
-    "instruction_memory": {
-        "start": 0x20040000,
-        "end": 0x20080000
-    },
-    "heap": {
-        "end": 0x20080000
-    },
-    "interrupts": [
-        {
-            "name": "RevokerInterrupt",
-            "number": 1,
-            "priority": 2
-        },
-        {
-            "name": "UARTInterrupt",
-            "number": 2,
-            "priority": 3,
-            "edge_triggered": true
-        },
-        {
-            "name": "EthernetTransmitInterrupt",
-            "number": 3,
-            "priority": 3
-        },
-        {
-            "name": "EthernetReceiveInterrupt",
-            "number": 4,
-            "priority": 3
-        }
-    ],
-    "defines" : [
-        "IBEX",
-        "IBEX_SAFE"
-    ],
-    "driver_includes" : [
-        "${sdk}/include/platform/arty-a7",
-        "${sdk}/include/platform/synopsis",
-        "${sdk}/include/platform/ibex",
-        "${sdk}/include/platform/generic-riscv"
-    ],
-    "timer_hz" : 33000000,
-    "tickrate_hz" : 100,
-    "revoker" : "hardware",
-    "stack_high_water_mark" : true
-}
diff --git a/sdk/boards/ibex-arty-a7-100.patch b/sdk/boards/ibex-arty-a7-100.patch
new file mode 100644
index 0000000..e9ccd3d
--- /dev/null
+++ b/sdk/boards/ibex-arty-a7-100.patch
@@ -0,0 +1,77 @@
+{
+  "base": "ibex-safe-simulator",
+  "patch": [
+    {
+      "op": "add",
+      "path": "/devices/gpio_led0",
+      "value": {
+        "start": 0x8f00f000,
+        "length": 0x800
+      }
+    },
+    {
+      "op": "add",
+      "path": "/kunyan_ethernet",
+      "value": {
+        "start": 0x14004000,
+        "end": 0x14008000
+      }
+    },
+    {
+      "op": "add",
+      "path": "/interrupts/1",
+      "value": {
+        "name": "UARTInterrupt",
+        "number": 2,
+        "priority": 3,
+        "edge_triggered": true
+      }
+    },
+    {
+      "op": "add",
+      "path": "/interrupts/2",
+      "value": {
+        "name": "EthernetTransmitInterrupt",
+        "number": 3,
+        "priority": 3
+      }
+    },
+    {
+      "op": "add",
+      "path": "/interrupts/3",
+      "value": {
+        "name": "EthernetReceiveInterrupt",
+        "number": 4,
+        "priority": 3
+      }
+    },
+    {
+      "op": "add",
+      "path": "/driver_includes/0",
+      "value": "${sdk}/include/platform/synopsis"
+    },
+    {
+      "op": "add",
+      "path": "/driver_includes/0",
+      "value": "${sdk}/include/platform/arty-a7"
+    },
+    {
+      "op": "replace",
+      "path": "/timer_hz",
+      "value": 33000000
+    },
+    {
+      "op": "replace",
+      "path": "/tickrate_hz",
+      "value": 100
+    },
+    {
+      "op": "remove",
+      "path": "/simulation"
+    },
+    {
+      "op": "remove",
+      "path": "/run_command"
+    }
+  ]
+}
diff --git a/sdk/boards/ibex-safe-simulator.json b/sdk/boards/ibex-safe-simulator.json
index 87ac417..cdc1920 100644
--- a/sdk/boards/ibex-safe-simulator.json
+++ b/sdk/boards/ibex-safe-simulator.json
@@ -32,7 +32,8 @@
         {
             "name": "RevokerInterrupt",
             "number": 1,
-            "priority": 2
+            "priority": 2,
+            "edge_triggered": true
         }
     ],
     "defines" : [
@@ -48,5 +49,5 @@
     "revoker" : "hardware",
     "stack_high_water_mark" : true,
     "simulation": true,
-    "simulator" : "${sdk}/../scripts/run-ibex-safe-sim.sh"
+    "run_command" : "${sdk}/../scripts/run-ibex-safe-sim.sh"
 }
diff --git a/sdk/boards/sail.json b/sdk/boards/sail.json
index 95e1689..fecd0b7 100644
--- a/sdk/boards/sail.json
+++ b/sdk/boards/sail.json
@@ -38,6 +38,6 @@
     "tickrate_hz" : 10,
     "revoker" : "software",
     "stack_high_water_mark" : true,
-    "simulator" : "cheriot_sim",
+    "run_command" : "cheriot_sim",
     "simulation": true
 }
diff --git a/sdk/boards/sonata-0.2.json b/sdk/boards/sonata-0.2.json
index 9747b93..3b78434 100644
--- a/sdk/boards/sonata-0.2.json
+++ b/sdk/boards/sonata-0.2.json
@@ -73,7 +73,8 @@
         "SUNBURST_SHADOW_SIZE=0x4000",
         "DEFAULT_UART_BAUD_RATE=115200",
         "ipconfigDRIVER_INCLUDED_RX_IP_CHECKSUM=1",
-        "ipconfigDRIVER_INCLUDED_TX_IP_CHECKSUM=1"
+        "ipconfigDRIVER_INCLUDED_TX_IP_CHECKSUM=1",
+        "CHERIOT_NO_SAIL_83"
     ],
     "driver_includes" : [
         "../include/platform/sunburst/v0.2",
@@ -84,6 +85,6 @@
     "tickrate_hz" : 100,
     "revoker" : "software",
     "stack_high_water_mark" : true,
-    "simulator" : "${sdk}/../scripts/run-sonata.sh",
+    "run_command" : "${sdk}/../scripts/run-sonata.sh",
     "simulation": false
 }
diff --git a/sdk/boards/sonata-1.0.patch b/sdk/boards/sonata-1.0.patch
new file mode 100644
index 0000000..8521866
--- /dev/null
+++ b/sdk/boards/sonata-1.0.patch
@@ -0,0 +1,15 @@
+{
+  "base": "sonata-1.1",
+  "patch": [
+    {
+      "op": "add",
+      "path": "/defines/0",
+      "value": "CHERIOT_NO_SAIL_83"
+    },
+    {
+      "op": "add",
+      "path": "/cxflags",
+      "value": "-mllvm -enable-machine-outliner=never"
+    }
+  ]
+}
diff --git a/sdk/boards/sonata-prerelease.json b/sdk/boards/sonata-1.1.json
similarity index 75%
rename from sdk/boards/sonata-prerelease.json
rename to sdk/boards/sonata-1.1.json
index bd07567..1dd81f2 100644
--- a/sdk/boards/sonata-prerelease.json
+++ b/sdk/boards/sonata-1.1.json
@@ -12,6 +12,14 @@
             "start" : 0x80001030,
             "end"   : 0x80001038
         },
+        "pinmux_pins_sinks": {
+            "start" : 0x80005000,
+            "length": 0x00000055
+        },
+        "pinmux_block_sinks": {
+            "start" : 0x80005800,
+            "length": 0x00000046
+        },
         "rgbled" : {
             "start" : 0x80009000,
             "end"   : 0x80009020
@@ -40,6 +48,26 @@
             "start" : 0x80102000,
             "end"   : 0x80102034
         },
+        "spi_lcd": {
+            "start" : 0x80300000,
+            "end"   : 0x80301000
+        },
+        "spi_ethmac": {
+            "start" : 0x80301000,
+            "end"   : 0x80302000
+        },
+        "spi0": {
+            "start" : 0x80302000,
+            "end"   : 0x80303000
+        },
+        "spi1": {
+            "start" : 0x80303000,
+            "end"   : 0x80304000
+        },
+        "spi2": {
+            "start" : 0x80304000,
+            "end"   : 0x80305000
+        },
         "usbdev": {
             "start" : 0x80400000,
             "end"   : 0x80401000
@@ -49,9 +77,13 @@
             "end"   : 0x88400000
         }
     },
+    "data_memory": {
+        "start" : 0x00101000,
+        "end" :   0x00120000
+    },
     "instruction_memory": {
-        "start": 0x00101000,
-        "end":   0x00120000
+        "start": 0x40000000,
+        "end":   0x40100000
     },
     "heap": {
         "end": 0x00120000
@@ -63,7 +95,8 @@
         "SUNBURST_SHADOW_BASE=0x30000000",
         "SUNBURST_SHADOW_SIZE=0x800",
         "ipconfigDRIVER_INCLUDED_RX_IP_CHECKSUM=1",
-        "ipconfigDRIVER_INCLUDED_TX_IP_CHECKSUM=1"
+        "ipconfigDRIVER_INCLUDED_TX_IP_CHECKSUM=1",
+        "STDERR_TO_STDOUT=1"
     ],
     "driver_includes" : [
         "../include/platform/sunburst",
@@ -74,13 +107,14 @@
     "tickrate_hz" : 100,
     "revoker" : "hardware",
     "stack_high_water_mark" : true,
-    "simulator" : "${sdk}/../scripts/run-sonata.sh",
+    "run_command" : "${sdk}/../scripts/run-sonata-1.0.sh",
     "simulation": false,
     "interrupts": [
         {
             "name": "RevokerInterrupt",
             "number": 1,
-            "priority": 2
+            "priority": 2,
+            "edge_triggered": true
         },
         {
             "name": "EthernetInterrupt",
diff --git a/sdk/boards/sonata-prerelease.patch b/sdk/boards/sonata-prerelease.patch
new file mode 100644
index 0000000..931cd7e
--- /dev/null
+++ b/sdk/boards/sonata-prerelease.patch
@@ -0,0 +1,4 @@
+{
+  "base": "sonata-1.1",
+  "patch": [ ]
+}
diff --git a/sdk/boards/sonata-simulator.json b/sdk/boards/sonata-simulator.json
deleted file mode 100644
index c2073f7..0000000
--- a/sdk/boards/sonata-simulator.json
+++ /dev/null
@@ -1,142 +0,0 @@
-{
-    "devices": {
-        "shadow" : {
-            "start" : 0x30000000,
-            "end"   : 0x30000800
-        },
-        "pwm": {
-            "start" : 0x80001000,
-            "length": 0x00001000
-        },
-        "rgbled" : {
-            "start" : 0x80009000,
-            "end"   : 0x80009020
-        },
-        "revoker": {
-            "start" : 0x8000A000,
-            "length": 0x00001000
-        },
-        "adc": {
-            "start" : 0x8000B000,
-            "length": 0x00001000
-        },
-        "clint": {
-            "start" : 0x80040000,
-            "end"   : 0x80050000
-        },
-        "uart": {
-            "start" : 0x80100000,
-            "end"   : 0x80100034
-        },
-        "uart1": {
-            "start" : 0x80101000,
-            "end"   : 0x80101034
-        },
-        "uart2": {
-            "start" : 0x80102000,
-            "end"   : 0x80102034
-        },
-        "usbdev": {
-            "start" : 0x80400000,
-            "end"   : 0x80401000
-        },
-        "plic": {
-            "start" : 0x88000000,
-            "end"   : 0x88400000
-        }
-    },
-    "instruction_memory": {
-        "start": 0x00101000,
-        "end":   0x00120000
-    },
-    "heap": {
-        "end": 0x00120000
-    },
-    "revokable_memory_start": 0x00100000,
-    "defines" : [
-        "IBEX",
-        "SUNBURST",
-        "SUNBURST_SHADOW_BASE=0x30000000",
-        "SUNBURST_SHADOW_SIZE=0x800",
-        "ipconfigDRIVER_INCLUDED_RX_IP_CHECKSUM=1",
-        "ipconfigDRIVER_INCLUDED_TX_IP_CHECKSUM=1"
-    ],
-    "driver_includes" : [
-        "../include/platform/sunburst",
-        "../include/platform/ibex",
-        "../include/platform/generic-riscv"
-    ],
-    "timer_hz" : 40000000,
-    "tickrate_hz" : 100,
-    "revoker" : "hardware",
-    "stack_high_water_mark" : true,
-    "simulator" : "${sdk}/../scripts/run-sonata-sim.sh",
-    "simulation": true,
-    "interrupts": [
-        {
-            "name": "RevokerInterrupt",
-            "number": 1,
-            "priority": 2
-        },
-        {
-            "name": "EthernetInterrupt",
-            "number": 2,
-            "priority": 3
-        },
-        {
-            "name": "UsbDevInterrupt",
-            "number": 3,
-            "priority": 3
-        },
-        {
-            "name": "Uart0Interrupt",
-            "number": 8,
-            "priority": 3
-        },
-        {
-            "name": "Uart1Interrupt",
-            "number": 9,
-            "priority": 3
-        },
-        {
-            "name": "Uart2Interrupt",
-            "number": 10,
-            "priority": 3
-        },
-        {
-            "name": "I2c0Interrupt",
-            "number": 16,
-            "priority": 3
-        },
-        {
-            "name": "I2c1Interrupt",
-            "number": 17,
-            "priority": 3
-        },
-        {
-            "name": "SpiLcdInterrupt",
-            "number": 24,
-            "priority": 3
-        },
-        {
-            "name": "SpiEthmacInterrupt",
-            "number": 25,
-            "priority": 3
-        },
-        {
-            "name": "Spi0Interrupt",
-            "number": 26,
-            "priority": 3
-        },
-        {
-            "name": "Spi1Interrupt",
-            "number": 27,
-            "priority": 3
-        },
-        {
-            "name": "Spi2Interrupt",
-            "number": 28,
-            "priority": 3
-        }
-    ]
-}
diff --git a/sdk/boards/sonata-simulator.patch b/sdk/boards/sonata-simulator.patch
new file mode 100644
index 0000000..353edbd
--- /dev/null
+++ b/sdk/boards/sonata-simulator.patch
@@ -0,0 +1,15 @@
+{
+  "base": "sonata-1.1",
+  "patch": [
+    {
+      "op": "replace",
+      "path": "/simulation",
+      "value": true
+    },
+    {
+      "op": "replace",
+      "path": "/run_command",
+      "value": "${sdk}/../scripts/run-sonata-sim.sh"
+    }
+  ]
+}
diff --git a/sdk/core/allocator/alloc.h b/sdk/core/allocator/alloc.h
index 9560be0..4260b82 100644
--- a/sdk/core/allocator/alloc.h
+++ b/sdk/core/allocator/alloc.h
@@ -157,19 +157,17 @@
 {
 
 	template<auto F, typename D>
-	concept Decoder = requires(D d)
-	{
+	concept Decoder = requires(D d) {
 		{
 			F(d)
-			} -> std::same_as<size_t>;
+		} -> std::same_as<size_t>;
 	};
 
 	template<auto F, typename D>
-	concept Encoder = requires(size_t s)
-	{
+	concept Encoder = requires(size_t s) {
 		{
 			F(s)
-			} -> std::same_as<D>;
+		} -> std::same_as<D>;
 	};
 
 	/**
@@ -177,7 +175,7 @@
 	 * a proxy for a pointer.
 	 */
 	template<typename T, typename D, bool Positive, auto Decode, auto Encode>
-	requires Decoder<Decode, D> && Encoder<Encode, D>
+	    requires Decoder<Decode, D> && Encoder<Encode, D>
 	class Proxy
 	{
 		CHERI::Capability<void> ctx;
@@ -286,8 +284,7 @@
  *       - Collected in a treebin ring, using either/both the TChunk linkages
  *         or/and the MChunk::ring links present in body().
  */
-struct __packed __aligned(MallocAlignment)
-MChunkHeader
+struct __packed __aligned(MallocAlignment) MChunkHeader
 {
 	/**
 	 * Each chunk has a 16-bit metadata field that is used to store a small
@@ -589,8 +586,7 @@
  * feed an unsafe_remove'd MChunk to such a function or to simply build a new
  * MChunk header in the heap.
  */
-class __packed __aligned(MallocAlignment)
-MChunk
+class __packed __aligned(MallocAlignment) MChunk
 {
 	friend class MChunkAssertions;
 	friend class TChunk;
@@ -710,8 +706,7 @@
  * Since we have enough room (large/tree chunks are at least 65 bytes), we just
  * put full capabilities here, and the format probably won't change, ever.
  */
-class __packed __aligned(MallocAlignment)
-TChunk
+class __packed __aligned(MallocAlignment) TChunk
 {
 	friend class TChunkAssertions;
 	friend class MState;
@@ -1505,7 +1500,7 @@
 	 */
 	static bool __always_inline capaligned_range_do(void  *start,
 	                                                size_t size,
-	                                                bool (*fn)(void **))
+	                                                bool   (*fn)(void **))
 	{
 		Debug::Assert((size & (sizeof(void *) - 1)) == 0,
 		              "Cap range is not aligned");
@@ -2811,4 +2806,163 @@
 	{
 		ABORT();
 	}
+
+#if HEAP_RENDER
+	public:
+	/**
+	 * "Render" the heap for debugging.
+	 */
+	template<bool Asserts = true, bool Chatty = true>
+	void render()
+	{
+		using RenderDebug = ConditionalDebug<Chatty, "Allocator heap">;
+
+		size_t measuredAllocated = 0, measuredFree = 0, measuredQuarantined = 0;
+
+		auto toAddr = [](void *p) {
+			return static_cast<ptraddr_t>(CHERI::Capability{p}.address());
+		};
+
+		auto header =
+		  static_cast<MChunkHeader *>(static_cast<void *>(heapStart));
+
+		RenderDebug::log("Dumping MState={} start={} end={}",
+		                 toAddr(this),
+		                 toAddr(heapStart),
+		                 heapStart.top());
+
+		while (toAddr(header) != heapStart.top())
+		{
+			RenderDebug::log("  header {}: size={} inuse={} pinuse={}",
+			                 toAddr(header),
+			                 header->size_get(),
+			                 header->isCurrInUse,
+			                 header->isPrevInUse);
+
+			if (!header->is_in_use())
+			{
+				measuredFree += header->size_get();
+
+				auto chunk = MChunk::from_header(header);
+				if (!ds::linked_list::is_singleton(&chunk->ring))
+				{
+					RenderDebug::log(
+					  "   free ring <{},{}>",
+					  toAddr(MChunk::from_ring(chunk->ring.cell_prev())),
+					  toAddr(MChunk::from_ring(chunk->ring.cell_next())));
+				}
+				else
+				{
+					RenderDebug::log("   free ring empty");
+				}
+
+				if (!is_small(header->size_get()))
+				{
+					auto t = TChunk::from_mchunk(chunk);
+
+					if (t->is_tree_ring())
+					{
+						RenderDebug::log("   tree ring, index={}", t->index);
+					}
+					else
+					{
+						RenderDebug::log(
+						  "   tree, index={} parent={} children=[{},{}]",
+						  t->index,
+						  toAddr(t->parent),
+						  toAddr(t->child[0]),
+						  toAddr(t->child[1]));
+					}
+				}
+			}
+			else
+			{
+				measuredAllocated += header->size_get();
+			}
+
+			header = header->cell_next();
+		}
+
+		auto showQuarantineRing = [&](ChunkFreeLink *&p) {
+			auto header = MChunkHeader::from_body(MChunk::from_ring(p));
+			RenderDebug::log(
+			  "   quarantined {} size={}", toAddr(header), header->size_get());
+			measuredQuarantined += header->size_get();
+			if constexpr (Asserts)
+			{
+				RenderDebug::invariant(ds::linked_list::is_well_formed(p),
+				                       "Quarantine list node malformed");
+			}
+			return false;
+		};
+
+		{
+			decltype(quarantinePendingRing)::Ix head = 0, tail = 0;
+
+			quarantinePendingRing.head_get(head);
+			quarantinePendingRing.tail_get(tail);
+			RenderDebug::log(" Quarantine status: empty={} head={} tail={}",
+			                 quarantinePendingRing.is_empty(),
+			                 head,
+			                 tail);
+		}
+
+		if (quarantineFinishedSentinel.is_empty())
+		{
+			RenderDebug::log("  finished ring empty");
+		}
+		else
+		{
+			RenderDebug::log("  finished ring <{},{}>:",
+			                 toAddr(quarantineFinishedSentinel.last()),
+			                 toAddr(quarantineFinishedSentinel.first()));
+			quarantine_finished_get()->search(showQuarantineRing);
+		}
+
+		for (size_t ix = 0; ix < QuarantineRings; ix++)
+		{
+			if (quarantinePendingChunks[ix].is_empty())
+			{
+				RenderDebug::log(
+				  "  index={} epoch={} empty", ix, quarantinePendingEpoch[ix]);
+			}
+			else
+			{
+				RenderDebug::log(
+				  "  index={} epoch={}:", ix, quarantinePendingEpoch[ix]);
+				quarantine_pending_get(ix)->search(showQuarantineRing);
+			}
+		}
+
+		auto measuredTotal = measuredAllocated + measuredFree;
+
+		RenderDebug::log(
+		  "Sizes: alloc={} free={} (expect {}) quar={} (expect {}) "
+		  "total={} (expect {})",
+		  measuredAllocated,
+		  measuredFree,
+		  heapFreeSize,
+		  measuredQuarantined,
+		  heapQuarantineSize,
+		  measuredTotal,
+		  heapTotalSize);
+
+		if constexpr (Asserts)
+		{
+			RenderDebug::invariant(measuredFree == heapFreeSize,
+			                       "Bad accounting in free size: {} {}",
+			                       std::make_tuple(measuredFree, heapFreeSize));
+
+			RenderDebug::invariant(
+			  measuredQuarantined == heapQuarantineSize,
+			  "Bad accounting in quarantine size: {} {}",
+			  std::make_tuple(measuredQuarantined, heapQuarantineSize));
+
+			RenderDebug::invariant(
+			  measuredTotal == heapTotalSize,
+			  "Bad accounting in total size: {} {}",
+			  std::make_tuple(measuredTotal, heapTotalSize));
+		}
+	}
+#endif
 };
diff --git a/sdk/core/allocator/alloc_config.h b/sdk/core/allocator/alloc_config.h
index 522ed8a..65af7af 100644
--- a/sdk/core/allocator/alloc_config.h
+++ b/sdk/core/allocator/alloc_config.h
@@ -34,5 +34,15 @@
 #endif
   ;
 
-#define STACK_CHECK(expected)                                                  \
-	StackUsageCheck<StackMode, expected, __PRETTY_FUNCTION__> stackCheck
+#if defined(__CHERIOT__) && (__CHERIOT__ >= 20250108)
+#	define STACK_CHECK(expected)                                              \
+		static_assert((expected) == __cheriot_minimum_stack__,                 \
+		              "Explicit stack check does not match annotation!");      \
+		StackUsageCheck<StackMode,                                             \
+		                __cheriot_minimum_stack__,                             \
+		                __PRETTY_FUNCTION__>                                   \
+		  stackCheck
+#else
+#	define STACK_CHECK(expected)                                              \
+		StackUsageCheck<StackMode, expected, __PRETTY_FUNCTION__> stackCheck
+#endif
diff --git a/sdk/core/allocator/main.cc b/sdk/core/allocator/main.cc
index bb21962..ad838be 100644
--- a/sdk/core/allocator/main.cc
+++ b/sdk/core/allocator/main.cc
@@ -187,11 +187,11 @@
 	 *
 	 */
 	template<typename T = Revocation::Revoker>
-	bool wait_for_background_revoker(
-	  Timeout                   *timeout,
-	  uint32_t                   epoch,
-	  LockGuard<decltype(lock)> &g,
-	  T &r = revoker) requires(Revocation::SupportsInterruptNotification<T>)
+	bool wait_for_background_revoker(Timeout                   *timeout,
+	                                 uint32_t                   epoch,
+	                                 LockGuard<decltype(lock)> &g,
+	                                 T                         &r = revoker)
+	    requires(Revocation::SupportsInterruptNotification<T>)
 	{
 		// Release the lock before sleeping
 		g.unlock();
@@ -212,19 +212,22 @@
 	 *
 	 */
 	template<typename T = Revocation::Revoker>
-	bool wait_for_background_revoker(
-	  Timeout                   *timeout,
-	  uint32_t                   epoch,
-	  LockGuard<decltype(lock)> &g,
-	  T &r = revoker) requires(!Revocation::SupportsInterruptNotification<T>)
+	bool wait_for_background_revoker(Timeout                   *timeout,
+	                                 uint32_t                   epoch,
+	                                 LockGuard<decltype(lock)> &g,
+	                                 T                         &r = revoker)
+	    requires(!Revocation::SupportsInterruptNotification<T>)
 	{
 		// Yield while until a revocation pass has finished.
-		while (!revoker.has_revocation_finished_for_epoch<true>(epoch))
+		while (!revoker.has_revocation_finished_for_epoch(epoch))
 		{
 			// Release the lock before sleeping
 			g.unlock();
 			Timeout smallSleep{1};
-			thread_sleep(&smallSleep);
+			if (thread_sleep(&smallSleep) < 0)
+			{
+				return false;
+			}
 			if (!reacquire_lock(timeout, g, smallSleep.elapsed))
 			{
 				return false;
@@ -313,7 +316,11 @@
 						// Sleep for a single tick.
 						g.unlock();
 						Timeout smallSleep{1};
-						thread_sleep(&smallSleep);
+						if (thread_sleep(&smallSleep) < 0)
+						{
+							/* Unable to sleep; bail out */
+							return nullptr;
+						}
 						if (!reacquire_lock(timeout, g, smallSleep.elapsed))
 						{
 							return nullptr;
@@ -852,7 +859,7 @@
 	return cap->quota;
 }
 
-__cheriot_minimum_stack(0xc0) void heap_quarantine_empty()
+__cheriot_minimum_stack(0xc0) int heap_quarantine_empty()
 {
 	STACK_CHECK(0xc0);
 	LockGuard g{lock};
@@ -866,14 +873,16 @@
 		yield();
 		g.lock();
 	}
+
+	return 0;
 }
 
-__cheriot_minimum_stack(0x210) void *heap_allocate(Timeout *timeout,
+__cheriot_minimum_stack(0x220) void *heap_allocate(Timeout *timeout,
                                                    SObj     heapCapability,
                                                    size_t   bytes,
                                                    uint32_t flags)
 {
-	STACK_CHECK(0x210);
+	STACK_CHECK(0x220);
 	if (!check_timeout_pointer(timeout))
 	{
 		return nullptr;
@@ -884,11 +893,6 @@
 	{
 		return nullptr;
 	}
-	if (!check_pointer<PermissionSet{Permission::Load, Permission::Store}>(
-	      timeout))
-	{
-		return nullptr;
-	}
 	// Use the default memory space.
 	return malloc_internal(bytes, std::move(g), cap, timeout, false, flags);
 }
@@ -935,11 +939,8 @@
 	return heap_free_internal(heapCapability, rawPointer, false);
 }
 
-__cheriot_minimum_stack(0x260) int heap_free(SObj  heapCapability,
-                                             void *rawPointer)
+int heap_free_nostackcheck(SObj heapCapability, void *rawPointer)
 {
-	// If this value changes, update `heap_can_free` as well.
-	STACK_CHECK(0x260);
 	LockGuard g{lock};
 	int       ret = heap_free_internal(heapCapability, rawPointer, true);
 	if (ret != 0)
@@ -958,9 +959,17 @@
 	return 0;
 }
 
-__cheriot_minimum_stack(0x190) ssize_t heap_free_all(SObj heapCapability)
+__cheriot_minimum_stack(0x260) int heap_free(SObj  heapCapability,
+                                             void *rawPointer)
 {
-	STACK_CHECK(0x190);
+	// If this value changes, update `heap_can_free` as well.
+	STACK_CHECK(0x260);
+	return heap_free_nostackcheck(heapCapability, rawPointer);
+}
+
+__cheriot_minimum_stack(0x1a0) ssize_t heap_free_all(SObj heapCapability)
+{
+	STACK_CHECK(0x1a0);
 	LockGuard g{lock};
 	auto     *capability = malloc_capability_unseal(heapCapability);
 	if (capability == nullptr)
@@ -997,13 +1006,13 @@
 	return freed;
 }
 
-__cheriot_minimum_stack(0x210) void *heap_allocate_array(Timeout *timeout,
+__cheriot_minimum_stack(0x220) void *heap_allocate_array(Timeout *timeout,
                                                          SObj   heapCapability,
                                                          size_t nElements,
                                                          size_t elemSize,
                                                          uint32_t flags)
 {
-	STACK_CHECK(0x210);
+	STACK_CHECK(0x220);
 	if (!check_timeout_pointer(timeout))
 	{
 		return nullptr;
@@ -1175,16 +1184,24 @@
 	auto [sealed, obj] = allocate_sealed_unsealed(
 	  timeout, heapCapability, key, sz, {Permission::Seal, Permission::Unseal});
 	{
+		/*
+		 * Write the unsealed capability through the out parameter, while
+		 * holding the allocator lock.  That's a little heavy-handed, but it
+		 * suffices to ensure that it won't be freed out from under us, so
+		 * if it passes `check_pointer`, then the store won't trap.
+		 */
 		LockGuard g{lock};
 		if (check_pointer<PermissionSet{
 		      Permission::Store, Permission::LoadStoreCapability}>(unsealed))
 		{
 			*unsealed = obj;
-			return sealed;
 		}
 	}
-	heap_free(heapCapability, obj);
-	return INVALID_SOBJ;
+	/*
+	 * Regardless of whether we were able to store the unsealed pointer, return
+	 * the sealed object.
+	 */
+	return sealed;
 }
 
 __cheriot_minimum_stack(0x260) SObj token_sealed_alloc(Timeout *timeout,
@@ -1239,7 +1256,7 @@
 		// The key can't be revoked and so there is no race with the key going
 		// away after the check.
 	}
-	return heap_free(heapCapability, unsealed);
+	return heap_free_nostackcheck(heapCapability, unsealed);
 }
 
 __cheriot_minimum_stack(0xf0) int token_obj_can_destroy(SObj heapCapability,
@@ -1268,3 +1285,11 @@
 {
 	return gm->heapFreeSize;
 }
+
+[[cheri::interrupt_state(disabled)]] int heap_render()
+{
+#if HEAP_RENDER
+	gm->render();
+#endif
+	return 0;
+}
diff --git a/sdk/core/allocator/revoker.h b/sdk/core/allocator/revoker.h
index e484b4f..38c9698 100644
--- a/sdk/core/allocator/revoker.h
+++ b/sdk/core/allocator/revoker.h
@@ -23,21 +23,22 @@
 	 * provided by the board search.
 	 */
 	template<typename T>
-	concept IsHardwareRevokerDevice = requires(T v, uint32_t epoch)
-	{
-		{v.init()};
+	concept IsHardwareRevokerDevice = requires(T v, uint32_t epoch) {
+		{
+			v.init()
+		};
 		{
 			v.system_epoch_get()
-			} -> std::same_as<uint32_t>;
+		} -> std::same_as<uint32_t>;
 		{
 			v.template has_revocation_finished_for_epoch<true>(epoch)
-			} -> std::same_as<uint32_t>;
+		} -> std::same_as<uint32_t>;
 		{
 			v.template has_revocation_finished_for_epoch<false>(epoch)
-			} -> std::same_as<uint32_t>;
+		} -> std::same_as<uint32_t>;
 		{
 			v.system_bg_revoker_kick()
-			} -> std::same_as<void>;
+		} -> std::same_as<void>;
 	};
 
 	/**
@@ -47,14 +48,12 @@
 	 * timeout expired.
 	 */
 	template<typename T>
-	concept SupportsInterruptNotification = requires(T        v,
-	                                                 Timeout *timeout,
-	                                                 uint32_t epoch)
-	{
-		{
-			v.wait_for_completion(timeout, epoch)
-			} -> std::same_as<bool>;
-	};
+	concept SupportsInterruptNotification =
+	  requires(T v, Timeout *timeout, uint32_t epoch) {
+		  {
+			  v.wait_for_completion(timeout, epoch)
+		  } -> std::same_as<bool>;
+	  };
 
 	/**
 	 * Class for interacting with the shadow bitmap.  This bitmap controls the
@@ -275,7 +274,7 @@
 	         size_t TCMBaseAddr,
 	         template<typename, size_t>
 	         typename Revoker>
-	requires IsHardwareRevokerDevice<Revoker<WordT, TCMBaseAddr>>
+	    requires IsHardwareRevokerDevice<Revoker<WordT, TCMBaseAddr>>
 	class HardwareAccelerator : public Bitmap<WordT, TCMBaseAddr>,
 	                            public Revoker<WordT, TCMBaseAddr>
 	{
@@ -389,7 +388,7 @@
 			// time that it's queried.
 			if ((current & 1) == 1)
 			{
-				revoker_tick();
+				(void)revoker_tick();
 				current = *epoch;
 			}
 			// We want to know if current is greater than epoch, but current
@@ -415,7 +414,7 @@
 		/// Start revocation running.
 		void system_bg_revoker_kick()
 		{
-			revoker_tick();
+			(void)revoker_tick();
 		}
 	};
 
diff --git a/sdk/core/allocator/software_revoker.h b/sdk/core/allocator/software_revoker.h
index 561e021..11066ca 100644
--- a/sdk/core/allocator/software_revoker.h
+++ b/sdk/core/allocator/software_revoker.h
@@ -6,10 +6,14 @@
 
 /**
  * Prod the software revoker to do some work.  This does not do a complete
- * revocation pass, it will scan a region of memory and then return.
+ * revocation pass; it will scan a region of memory and then return.
+ *
+ * Returns 0 on success, a compartment invocation failure indication
+ * (-ENOTENOUGHSTACK, -ENOTENOUGHTRUSTEDSTACK) if it cannot be invoked, or
+ * possibly -ECOMPARTMENTFAIL if the software revoker compartment is damaged.
  */
 [[cheri::interrupt_state(disabled)]] __cheri_compartment(
-  "software_revoker") void revoker_tick();
+  "software_revoker") int revoker_tick();
 
 /**
  * Returns a read-only capability to the current revocation epoch.  If the low
diff --git a/sdk/core/loader/boot.S b/sdk/core/loader/boot.S
index ae915cb..953db9e 100644
--- a/sdk/core/loader/boot.S
+++ b/sdk/core/loader/boot.S
@@ -65,7 +65,7 @@
 	la_abs			s0, loader_entry_point
 	csetaddr		cra, cra, s0
 	// Base and size of the GP of loader
-	// Flute doesn't support unaligned loads, so we have to load the base as
+	// Old sails don't support unaligned loads, so we have to load the base as
 	// bytes
 	clbu			s0, IMAGE_HEADER_LOADER_DATA_START_OFFSET+3(ca1)
 	sll				s0, s0, 8
@@ -157,14 +157,7 @@
 	ecall
 	// The idle thread sleeps and only waits for interrupts.
 .Lidle_loop:
-	// There is a bug in the Flute hardware revoker that means it stops during
-	// wfi, but we want it to run here.  Flute is simulation only at the
-	// moment, so we don't care that the nop is power-inefficient.
-#if defined(TEMPORAL_SAFETY) && defined(FLUTE) && !defined(SOFTWARE_REVOKER)
-	nop
-#else
 	wfi
-#endif
 	j				.Lidle_loop
 
 .Lfill_block:
diff --git a/sdk/core/loader/boot.cc b/sdk/core/loader/boot.cc
index 20ff56f..d5d5405 100644
--- a/sdk/core/loader/boot.cc
+++ b/sdk/core/loader/boot.cc
@@ -147,20 +147,21 @@
 	// The switcher assembly includes the types of import table entries and
 	// trusted stacks.  This enumeration and the assembly must be kept in sync.
 	// This will fail if the enumeration value changes.
-	static_assert(int(SealedImportTableEntries) == 9,
+	static_assert(static_cast<int>(SealedImportTableEntries) == 9,
 	              "If this fails, update switcher/entry.S to the new value");
-	static_assert(int(SealedTrustedStacks) == 10,
+	static_assert(static_cast<int>(SealedTrustedStacks) == 10,
 	              "If this fails, update switcher/entry.S to the new value");
 
 	// The allocator and static sealing types must be contiguous so that the
 	// token library can hold a permit-unseal capability for both.
-	static_assert(int(Allocator) + 1 == int(StaticToken),
+	static_assert(static_cast<int>(Allocator) + 1 ==
+	                static_cast<int>(StaticToken),
 	              "Allocator and StaticToken must be consecutive");
 
 	// The token library includes the types for allocator and statically sealed
 	// objects.  This enumeration and the assembly must be kept in sync.  This
 	// will fail if the enumeration value changes.
-	static_assert(int(Allocator) == 11,
+	static_assert(static_cast<int>(Allocator) == 11,
 	              "If this fails, update token_unseal.S to the new value");
 
 	// We currently have a 3-bit hardware otype, with different sealing spaces
@@ -248,7 +249,8 @@
 	         Root::Type    Type        = Root::Type::RWGlobal,
 	         PermissionSet Permissions = Root::Permissions<Type>,
 	         bool          Precise     = true>
-	Capability<T> build(auto &&range) requires(RawAddressRange<decltype(range)>)
+	Capability<T> build(auto &&range)
+	    requires(RawAddressRange<decltype(range)>)
 	{
 		return build<T, Type, Permissions, Precise>(range.start(),
 		                                            range.size());
@@ -261,9 +263,8 @@
 	template<typename T                = void,
 	         Root::Type    Type        = Root::Type::RWGlobal,
 	         PermissionSet Permissions = Root::Permissions<Type>>
-	Capability<T>
-	build(auto    &&range,
-	      ptraddr_t address) requires(RawAddressRange<decltype(range)>)
+	Capability<T> build(auto &&range, ptraddr_t address)
+	    requires(RawAddressRange<decltype(range)>)
 	{
 		return build<T, Type, Permissions>(
 		  range.start(), range.size(), address);
@@ -347,13 +348,14 @@
 #pragma clang diagnostic push
 #pragma clang diagnostic ignored "-Wc99-designator"
 		constexpr SealingType Sentries[] = {
-		  [int(InterruptStatus::Enabled)]   = SentryEnabling,
-		  [int(InterruptStatus::Disabled)]  = SentryDisabling,
-		  [int(InterruptStatus::Inherited)] = SentryInheriting};
+		  [static_cast<int>(InterruptStatus::Enabled)]   = SentryEnabling,
+		  [static_cast<int>(InterruptStatus::Disabled)]  = SentryDisabling,
+		  [static_cast<int>(InterruptStatus::Inherited)] = SentryInheriting};
 #pragma clang diagnostic pop
-		Debug::Invariant(
-		  unsigned(status) < 3, "Invalid interrupt status {}", int(status));
-		size_t otype = size_t{Sentries[int(status)]};
+		Debug::Invariant(static_cast<unsigned>(status) < 3,
+		                 "Invalid interrupt status {}",
+		                 static_cast<int>(status));
+		size_t otype = size_t{Sentries[static_cast<int>(status)]};
 		void  *key   = build<void, Root::Type::Seal>(otype, 1);
 		return ptr.seal(key);
 	}
@@ -367,10 +369,11 @@
 #pragma clang diagnostic push
 #pragma clang diagnostic ignored "-Wc99-designator"
 		constexpr SealingType Sentries[] = {
-		  [int(InterruptStatus::Enabled)]  = ReturnSentryEnabling,
-		  [int(InterruptStatus::Disabled)] = ReturnSentryDisabling};
+		  [static_cast<int>(InterruptStatus::Enabled)] = ReturnSentryEnabling,
+		  [static_cast<int>(InterruptStatus::Disabled)] =
+		    ReturnSentryDisabling};
 #pragma clang diagnostic pop
-		size_t otype = size_t{Sentries[int(Status)]};
+		size_t otype = size_t{Sentries[static_cast<int>(Status)]};
 		void  *key   = build<void, Root::Type::Seal>(otype, 1);
 		return ptr.seal(key);
 	}
@@ -379,9 +382,8 @@
 	 * Helper to determine whether an object, given by a start address and size,
 	 * is completely contained within a specified range.
 	 */
-	bool contains(const auto &range,
-	              ptraddr_t   addr,
-	              size_t      size) requires(RawAddressRange<decltype(range)>)
+	bool contains(const auto &range, ptraddr_t addr, size_t size)
+	    requires(RawAddressRange<decltype(range)>)
 	{
 		return (range.start() <= addr) &&
 		       (range.start() + range.size() >= addr + size);
@@ -393,8 +395,8 @@
 	 * object must be completely contained within the range.
 	 */
 	template<typename T = char>
-	bool contains(const auto &range,
-	              ptraddr_t   addr) requires(RawAddressRange<decltype(range)>)
+	bool contains(const auto &range, ptraddr_t addr)
+	    requires(RawAddressRange<decltype(range)>)
 	{
 		return contains(range, addr, sizeof(T));
 	}
@@ -444,8 +446,8 @@
 	 * of type `T` from a virtual address range.
 	 */
 	template<typename T, bool Precise = true>
-	ContiguousPtrRange<T>
-	build_range(const auto &range) requires(RawAddressRange<decltype(range)>)
+	ContiguousPtrRange<T> build_range(const auto &range)
+	    requires(RawAddressRange<decltype(range)>)
 	{
 		Capability<T> start = build<T,
 		                            Root::Type::RWGlobal,
@@ -668,8 +670,8 @@
                             auto exportEntry = build<ExportEntry>(
                               compartment.exportTable, typeAddress);
                             Debug::Invariant(
-							   exportEntry->is_sealing_type(),
-							   "Sealed object points to invalid sealing type");
+                              exportEntry->is_sealing_type(),
+                              "Sealed object points to invalid sealing type");
                             *sealingType = exportEntry->functionStart;
                             return true;
                         }
@@ -863,8 +865,7 @@
 		for (size_t i = 0; const auto &config : image.threads())
 		{
 			Debug::log("Creating thread {}", i);
-			auto findCompartment = [&]() -> auto &
-			{
+			auto findCompartment = [&]() -> auto & {
 				for (auto &compartment : image.compartments())
 				{
 					Debug::log("Looking in export table {}+{}",
@@ -1000,8 +1001,7 @@
 
 		// Find the library compartment that contains an address in its code or
 		// data section.
-		auto findCompartment = [&](ptraddr_t address) -> auto &
-		{
+		auto findCompartment = [&](ptraddr_t address) -> auto & {
 			Debug::log("Capreloc address is {}", address);
 			for (auto &compartment : image.libraries_and_compartments())
 			{
@@ -1364,7 +1364,7 @@
 	  build<ExportEntry>(
 	    imgHdr.scheduler().exportTable,
 	    LA_ABS(
-	      __export_sched__ZN5sched15exception_entryEP19TrustedStackGenericILj0EEjjj))
+	      __export_scheduler__Z15exception_entryP19TrustedStackGenericILj0EEjjj))
 	    ->functionStart;
 	auto schedExceptionEntry = build_pcc(imgHdr.scheduler());
 	schedExceptionEntry.address() += exceptionEntryOffset;
@@ -1392,57 +1392,43 @@
 	  csp);
 
 #ifdef SOFTWARE_REVOKER
-	// If we are using a software revoker then we need to provide it with three
+	// If we are using a software revoker then we need to provide it with some
 	// terrifyingly powerful capabilities.  These break some of the rules that
-	// we enforce for everything else, especially the last one, which is a
-	// stack capability that is reachable from a global.  The only code that
+	// we enforce for everything else (e.g. they may point to stacks
+	// but are reachable from a global).  The only code that
 	// accesses these in the revoker is very small and amenable to auditing
 	// (the only memory accesses are a load and a store back at the same
 	// location, with interrupts disabled, to trigger the load barrier).
-	//
-	// We use imprecise set-bounds operations here because we need to ensure
-	// that the regions are completely scanned and scanning slightly more is
+
+	// The scary capabilities are stored at the beginning of the software
+	// revoker compartment. At the moment we only need one.
+	auto scaryCapabilities = build<Capability<void>,
+	                               Root::Type::RWStoreL,
+	                               Root::Permissions<Root::Type::RWStoreL>,
+	                               /* Precise: */ true>(
+	  imgHdr.privilegedCompartments.software_revoker().code.start(),
+	  sizeof(void *));
+	Debug::log("Writing scary capabilities for software revoker to {}",
+	           scaryCapabilities);
+	// Construct a capability to all RW globals, stacks and heap.
+	// We use an imprecise set-bounds operation here because we need to ensure
+	// that the region is completely scanned and scanning slightly more is
 	// not a problem unless the revoker is compromised.  The software revoker
 	// already has a terrifying set of rights, so this doesn't really make
 	// things worse and is another good reason to use a hardware revoker.
 	// Given that hardware revokers are lower power, faster, and more secure,
 	// there's little reason for the software revoker to be used for anything
 	// other than testing.
-	auto scaryCapabilities = build<Capability<void>,
-	                               Root::Type::RWStoreL,
-	                               Root::Permissions<Root::Type::RWStoreL>,
-	                               /* Precise: */ false>(
-	  imgHdr.privilegedCompartments.software_revoker().code.start(),
-	  3 * sizeof(void *));
-	// Read-write capability to all globals.  This is scary because a bug in
-	// the revoker could violate compartment isolation.
-	Debug::log("Writing scary capabilities for software revoker to {}",
-	           scaryCapabilities);
-	scaryCapabilities[0] =
-	  build(LA_ABS(__compart_cgps),
-	        LA_ABS(__compart_cgps_end) - LA_ABS(__compart_cgps));
+	scaryCapabilities[0] = build<void,
+	                             Root::Type::RWStoreL,
+	                             Root::Permissions<Root::Type::RWStoreL>,
+	                             /* Precise: */ false>(
+	  LA_ABS(__revoker_scan_start),
+	  LA_ABS(__export_mem_heap_end) - LA_ABS(__revoker_scan_start));
 	scaryCapabilities[0].address() = scaryCapabilities[0].base();
-	Debug::log("Wrote scary capability {}", scaryCapabilities[0]);
-	// Read-write capability to the whole heap.  This is scary because a bug in
-	// the revoker could violate heap safety.
-	scaryCapabilities[1] =
-	  build<void,
-	        Root::Type::RWGlobal,
-	        Root::Permissions<Root::Type::RWGlobal>,
-	        false>(LA_ABS(__export_mem_heap),
-	               LA_ABS(__export_mem_heap_end) - LA_ABS(__export_mem_heap));
-	scaryCapabilities[1].address() = scaryCapabilities[1].base();
-	Debug::log("Wrote scary capability {}", scaryCapabilities[1]);
-	// Read-write capability to the entire stack.  This is scary because a bug
-	// in the revoker could violate thread isolation.
-	scaryCapabilities[2] =
-	  build<void,
-	        Root::Type::RWStoreL,
-	        Root::Permissions<Root::Type::RWStoreL>,
-	        false>(LA_ABS(__stack_space_start),
-	               LA_ABS(__stack_space_end) - LA_ABS(__stack_space_start));
-	scaryCapabilities[2].address() = scaryCapabilities[2].base();
-	Debug::log("Wrote scary capability {}", scaryCapabilities[2]);
+	Debug::log("Wrote scary cap[0]={} requested_start={}",
+	           scaryCapabilities[0],
+	           LA_ABS(__revoker_scan_start));
 #endif
 
 	// Set up the exception entry point
@@ -1460,7 +1446,7 @@
 	// invoke the exception entry point.
 	auto exportEntry = build<ExportEntry>(
 	  imgHdr.scheduler().exportTable,
-	  LA_ABS(__export_sched__ZN5sched15scheduler_entryEPK16ThreadLoaderInfo));
+	  LA_ABS(__export_scheduler__Z15scheduler_entryPK16ThreadLoaderInfo));
 	schedPCC.address() += exportEntry->functionStart;
 
 	Debug::log("Will return to scheduler entry point: {}", schedPCC);
diff --git a/sdk/core/loader/debug.hh b/sdk/core/loader/debug.hh
index a89ea7f..24af5ed 100644
--- a/sdk/core/loader/debug.hh
+++ b/sdk/core/loader/debug.hh
@@ -27,11 +27,10 @@
 
 		/// Concept for something that can be lazily called to produce a bool.
 		template<typename T>
-		concept LazyAssertion = requires(T v)
-		{
+		concept LazyAssertion = requires(T v) {
 			{
 				v()
-				} -> IsBool;
+			} -> IsBool;
 		};
 	} // namespace DebugConcepts
 
@@ -151,7 +150,7 @@
 			}
 			std::array<char, 10> buf;
 			const char           Digits[] = "0123456789";
-			for (int i = int(buf.size() - 1); i >= 0; i--)
+			for (int i = static_cast<int>(buf.size() - 1); i >= 0; i--)
 			{
 				buf.at(static_cast<size_t>(i)) = Digits[s % 10];
 				s /= 10;
@@ -181,7 +180,7 @@
 			const char          Hexdigits[] = "0123456789abcdef";
 			// Length of string including null terminator
 			static_assert(sizeof(Hexdigits) == 0x11);
-			for (long i = long(buf.size() - 1); i >= 0; i--)
+			for (long i = static_cast<long>(buf.size() - 1); i >= 0; i--)
 			{
 				buf.at(static_cast<size_t>(i)) = Hexdigits[s & 0xf];
 				s >>= 4;
@@ -248,7 +247,7 @@
 		 * Append an enumerated type value.
 		 */
 		template<typename T>
-		requires DebugConcepts::IsEnum<T>
+		    requires DebugConcepts::IsEnum<T>
 		void append(T e)
 		{
 			// `magic_enum::enum_name` requires cap relocs, so don't use it in
@@ -482,7 +481,8 @@
 			 * Constructor, performs the assertion check.
 			 */
 			template<typename T>
-			requires DebugConcepts::IsBool<T> __always_inline
+			    requires DebugConcepts::IsBool<T>
+			__always_inline
 			Assert(T           condition,
 			       const char *fmt,
 			       Args... args,
@@ -513,7 +513,8 @@
 			 * where the assertion condition has side effects.
 			 */
 			template<typename T>
-			requires DebugConcepts::LazyAssertion<T> __always_inline
+			    requires DebugConcepts::LazyAssertion<T>
+			__always_inline
 			Assert(T         &&condition,
 			       const char *fmt,
 			       Args... args,
diff --git a/sdk/core/loader/types.h b/sdk/core/loader/types.h
index 90e190e..830c041 100644
--- a/sdk/core/loader/types.h
+++ b/sdk/core/loader/types.h
@@ -273,21 +273,21 @@
 	 * Helper concept for determining if something is an address.
 	 */
 	template<typename T>
-	concept IsAddress = std::same_as<T, ptraddr_t> ||
-	  std::same_as<T, ptraddr_t &> || std::same_as<T, const ptraddr_t &>;
+	concept IsAddress =
+	  std::same_as<T, ptraddr_t> || std::same_as<T, ptraddr_t &> ||
+	  std::same_as<T, const ptraddr_t &>;
 
 	/**
 	 * Concept for a raw address range.  This exposes a range of addresses.
 	 */
 	template<typename T>
-	concept RawAddressRange = requires(T range)
-	{
+	concept RawAddressRange = requires(T range) {
 		{
 			range.size()
-			} -> IsAddress;
+		} -> IsAddress;
 		{
 			range.start()
-			} -> IsAddress;
+		} -> IsAddress;
 	};
 
 	/**
@@ -1067,11 +1067,11 @@
 		/**
 		 * The mask to isolate the bits that describe interrupt status.
 		 */
-		static constexpr uint8_t InterruptStatusMask = uint8_t(0b11)
-		                                               << InterruptStatusShift;
+		static constexpr uint8_t InterruptStatusMask =
+		  static_cast<uint8_t>(0b11) << InterruptStatusShift;
 
 		static constexpr uint8_t InterruptStatusSwitcherMask =
-		  uint8_t(0b10) << InterruptStatusShift;
+		  static_cast<uint8_t>(0b10) << InterruptStatusShift;
 
 		/*
 		 * The switcher tests the high bit of the InterruptStatus word of
@@ -1080,11 +1080,13 @@
 		 * that its understanding is correct.
 		 */
 		static_assert(
-		  ((int(InterruptStatus::Enabled) << InterruptStatusShift) &
+		  ((static_cast<int>(InterruptStatus::Enabled)
+		    << InterruptStatusShift) &
 		   InterruptStatusSwitcherMask) == 0,
 		  "Switcher interpretation of InterruptStatus no longer correct");
 		static_assert(
-		  ((int(InterruptStatus::Disabled) << InterruptStatusShift) &
+		  ((static_cast<int>(InterruptStatus::Disabled)
+		    << InterruptStatusShift) &
 		   InterruptStatusSwitcherMask) != 0,
 		  "Switcher interpretation of InterruptStatus no longer correct");
 
@@ -1096,7 +1098,8 @@
 		 * their first word initialised to point to this, the loader will
 		 * set them up to instead hold the value of the sealing key.
 		 */
-		static constexpr uint8_t SealingTypeEntry = uint8_t(0b100000);
+		static constexpr uint8_t SealingTypeEntry =
+		  static_cast<uint8_t>(0b100000);
 
 		static_assert((InterruptStatusMask & SealingTypeEntry) == 0);
 
@@ -1129,7 +1132,7 @@
 		{
 			uint8_t status =
 			  (flags & InterruptStatusMask) >> InterruptStatusShift;
-			return InterruptStatus(status);
+			return static_cast<InterruptStatus>(status);
 		}
 
 		/**
diff --git a/sdk/core/scheduler/main.cc b/sdk/core/scheduler/main.cc
index d4d36a8..2983289 100644
--- a/sdk/core/scheduler/main.cc
+++ b/sdk/core/scheduler/main.cc
@@ -14,7 +14,6 @@
 #include <futex.h>
 #include <interrupt.h>
 #include <locks.hh>
-#include <new>
 #include <priv/riscv.h>
 #include <riscvreg.h>
 #include <simulator.h>
@@ -31,9 +30,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
 
@@ -121,6 +121,26 @@
 	 */
 	static constexpr auto UnboundedSleep = std::numeric_limits<uint32_t>::max();
 
+	enum FutexWakeKind
+	{
+		/**
+		 * The futex wake did not make any threads runnable that would be
+		 * scheduled preemptively.
+		 */
+		NoYield,
+		/**
+		 * The futex wake made a thread at the current priority level runnable,
+		 * the caller should ensure that there is a timer interrupt scheduled
+		 * to make the current thread yield later.
+		 */
+		YieldLater,
+		/**
+		 * The futex wake made a thread at a higher priority level runnable,
+		 * the caller should yield to allow the other thread to run immediately.
+		 */
+		YieldNow,
+	};
+
 	/**
 	 * Helper that wakes a set of up to `count` threads waiting on the futex
 	 * whose address is given by the `key` parameter.
@@ -133,11 +153,10 @@
 	 *    dropped back to the previous priority.
 	 *  - The number of sleeper that were awoken.
 	 */
-	std::tuple<bool, bool, int>
+	std::tuple<bool, int>
 	futex_wake(ptraddr_t key,
 	           uint32_t  count = std::numeric_limits<uint32_t>::max())
 	{
-		bool shouldYield                    = false;
 		bool shouldRecalculatePriorityBoost = false;
 		// The number of threads that we've woken, this is the return value on
 		// success.
@@ -149,7 +168,8 @@
 			  {
 				  shouldRecalculatePriorityBoost |=
 				    thread->futexPriorityInheriting;
-				  shouldYield = thread->ready(Thread::WakeReason::Futex);
+				  thread->ready(Thread::WakeReason::Futex);
+				  Debug::log("futex_wake woke thread {}", thread->id_get());
 				  count--;
 				  woke++;
 			  }
@@ -162,15 +182,12 @@
 			  MultiWaiterInternal::wake_waiters(key, count);
 			count -= multiwaitersWoken;
 			woke += multiwaitersWoken;
-			shouldYield |= (multiwaitersWoken > 0);
 		}
-		return {shouldYield, shouldRecalculatePriorityBoost, woke};
+		Debug::log("futex_wake on {} woke {} waiters", key, woke);
+
+		return {shouldRecalculatePriorityBoost, woke};
 	}
 
-} // namespace
-
-namespace sched
-{
 	using namespace priv;
 
 	/**
@@ -192,30 +209,6 @@
 		return &(reinterpret_cast<Thread *>(threadSpaces))[threadId - 1];
 	}
 
-	[[cheri::interrupt_state(disabled)]] void __cheri_compartment("sched")
-	  scheduler_entry(const ThreadLoaderInfo *info)
-	{
-		Debug::Invariant(Capability{info}.length() ==
-		                   sizeof(*info) * CONFIG_THREADS_NUM,
-		                 "Thread info is {} bytes, expected {} for {} threads",
-		                 Capability{info}.length(),
-		                 sizeof(*info) * CONFIG_THREADS_NUM,
-		                 CONFIG_THREADS_NUM);
-
-		for (size_t i = 0; auto *threadSpace : threadSpaces)
-		{
-			Debug::log("Created thread for trusted stack {}",
-			           info[i].trustedStack);
-			Thread *th = new (threadSpace)
-			  Thread(info[i].trustedStack, i + 1, info[i].priority);
-			th->ready(Thread::WakeReason::Timer);
-			i++;
-		}
-
-		InterruptController::master_init();
-		Timer::interrupt_setup();
-	}
-
 	static void __dead2 sched_panic(size_t mcause, size_t mepc, size_t mtval)
 	{
 		size_t capcause = mtval & 0x1f;
@@ -228,8 +221,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 (;;)
 		{
@@ -237,101 +232,6 @@
 		}
 	}
 
-	[[cheri::interrupt_state(disabled)]] TrustedStack *
-	  __cheri_compartment("sched") exception_entry(TrustedStack *sealedTStack,
-	                                               size_t        mcause,
-	                                               size_t        mepc,
-	                                               size_t        mtval)
-	{
-		if constexpr (DebugScheduler)
-		{
-			/* Ensure that we got here from an IRQ-s deferred context */
-			Capability returnAddress{__builtin_return_address(0)};
-			Debug::Assert(
-			  returnAddress.type() == CheriSealTypeReturnSentryDisabling,
-			  "Scheduler exception_entry called from IRQ-enabled context");
-		}
-
-		// The cycle count value the last time the scheduler returned.
-		bool schedNeeded;
-		if constexpr (Accounting)
-		{
-			uint64_t  currentCycles = rdcycle64();
-			auto     *thread        = Thread::current_get();
-			uint64_t &threadCycleCounter =
-			  thread ? thread->cycles : Thread::idleThreadCycles;
-			auto elapsedCycles = currentCycles - cyclesAtLastSchedulingEvent;
-			threadCycleCounter += elapsedCycles;
-		}
-
-		ExceptionGuard g{[=]() { sched_panic(mcause, mepc, mtval); }};
-
-		bool tick = false;
-		switch (mcause)
-		{
-			// Explicit yield call
-			case MCAUSE_ECALL_MACHINE:
-			{
-				schedNeeded           = true;
-				Thread *currentThread = Thread::current_get();
-				tick = currentThread && currentThread->is_ready();
-				break;
-			}
-			case MCAUSE_INTR | MCAUSE_MTIME:
-				schedNeeded = true;
-				tick        = true;
-				break;
-			case MCAUSE_INTR | MCAUSE_MEXTERN:
-				schedNeeded = false;
-				InterruptController::master().do_external_interrupt().and_then(
-				  [&](uint32_t &word) {
-					  // Increment the futex word so that anyone preempted on
-					  // the way into the scheduler sleeping on its old value
-					  // will still see this update.
-					  word++;
-					  // Wake anyone sleeping on this futex.  Interrupt futexes
-					  // are not priority inheriting.
-					  std::tie(schedNeeded, std::ignore, std::ignore) =
-					    futex_wake(Capability{&word}.address());
-				  });
-				tick = schedNeeded;
-				break;
-			case MCAUSE_THREAD_EXIT:
-				// Make the current thread non-runnable.
-				if (Thread::exit())
-				{
-					// If we have no threads left (not counting the idle
-					// thread), exit.
-					simulation_exit(0);
-				}
-				// We cannot continue exiting this thread, make sure we will
-				// pick a new one.
-				schedNeeded  = true;
-				tick         = true;
-				sealedTStack = nullptr;
-				break;
-			default:
-				sched_panic(mcause, mepc, mtval);
-		}
-		if (tick || !Thread::any_ready())
-		{
-			Timer::expiretimers();
-		}
-		auto newContext =
-		  schedNeeded ? Thread::schedule(sealedTStack) : sealedTStack;
-#if 0
-		Debug::log("Thread: {}",
-		           Thread::current_get() ? Thread::current_get()->id_get() : 0);
-#endif
-		Timer::update();
-
-		if constexpr (Accounting)
-		{
-			cyclesAtLastSchedulingEvent = rdcycle64();
-		}
-		return newContext;
-	}
-
 	/**
 	 * Helper template to dispatch an operation to a typed value.  The first
 	 * argument is a sealed capability provided by the caller.  The second is a
@@ -396,12 +296,135 @@
 		});
 	}
 
-} // namespace sched
+} // namespace
 
-using namespace sched;
+[[cheri::interrupt_state(disabled)]] int __cheri_compartment("scheduler")
+  scheduler_entry(const ThreadLoaderInfo *info)
+{
+	Debug::Invariant(Capability{info}.length() ==
+	                   sizeof(*info) * CONFIG_THREADS_NUM,
+	                 "Thread info is {} bytes, expected {} for {} threads",
+	                 Capability{info}.length(),
+	                 sizeof(*info) * CONFIG_THREADS_NUM,
+	                 CONFIG_THREADS_NUM);
+
+	for (size_t i = 0; auto *threadSpace : threadSpaces)
+	{
+		Debug::log("Created thread for trusted stack {}", info[i].trustedStack);
+		Thread *th = new (threadSpace)
+		  Thread(info[i].trustedStack, i + 1, info[i].priority);
+		th->ready(Thread::WakeReason::Timer);
+		i++;
+	}
+
+	InterruptController::master_init();
+	Timer::interrupt_setup();
+
+	return 0;
+}
+
+[[cheri::interrupt_state(disabled)]] TrustedStack *
+  __cheri_compartment("scheduler") exception_entry(TrustedStack *sealedTStack,
+                                                   size_t        mcause,
+                                                   size_t        mepc,
+                                                   size_t        mtval)
+{
+	if constexpr (DebugScheduler)
+	{
+		/* Ensure that we got here from an IRQ-s deferred context */
+		Capability returnAddress{__builtin_return_address(0)};
+		Debug::Assert(
+		  returnAddress.type() == CheriSealTypeReturnSentryDisabling,
+		  "Scheduler exception_entry called from IRQ-enabled context");
+	}
+
+	// The cycle count value the last time the scheduler returned.
+	bool schedNeeded;
+	if constexpr (Accounting)
+	{
+		uint64_t  currentCycles = rdcycle64();
+		auto     *thread        = Thread::current_get();
+		uint64_t &threadCycleCounter =
+		  thread ? thread->cycles : Thread::idleThreadCycles;
+		auto elapsedCycles = currentCycles - cyclesAtLastSchedulingEvent;
+		threadCycleCounter += elapsedCycles;
+	}
+
+	ExceptionGuard g{[=]() { sched_panic(mcause, mepc, mtval); }};
+
+	bool tick = false;
+	switch (mcause)
+	{
+		// Explicit yield call
+		case MCAUSE_ECALL_MACHINE:
+		{
+			schedNeeded           = true;
+			Thread *currentThread = Thread::current_get();
+			tick                  = currentThread && currentThread->is_ready();
+			break;
+		}
+		case MCAUSE_INTR | MCAUSE_MTIME:
+			schedNeeded = true;
+			tick        = true;
+			break;
+		case MCAUSE_INTR | MCAUSE_MEXTERN:
+			schedNeeded = false;
+			InterruptController::master().do_external_interrupt().and_then(
+			  [&](uint32_t &word) {
+				  // Increment the futex word so that anyone preempted on
+				  // the way into the scheduler sleeping on its old value
+				  // will still see this update.
+				  word++;
+				  // Wake anyone sleeping on this futex.  Interrupt futexes
+				  // are not priority inheriting.
+				  int woke;
+				  Debug::log("Waking waiters on interrupt futex {}", &word);
+				  std::tie(std::ignore, woke) =
+				    futex_wake(Capability{&word}.address());
+				  schedNeeded |= (woke > 0);
+			  });
+			tick = schedNeeded;
+			break;
+		case MCAUSE_THREAD_EXIT:
+			// Make the current thread non-runnable.
+			if (Thread::exit())
+			{
+#ifdef SIMULATION
+				// If we have no threads left (not counting the idle
+				// thread), exit.
+				platform_simulation_exit(0);
+#endif
+			}
+			// We cannot continue exiting this thread, make sure we will
+			// pick a new one.
+			schedNeeded  = true;
+			tick         = true;
+			sealedTStack = nullptr;
+			break;
+		default:
+			sched_panic(mcause, mepc, mtval);
+	}
+	if (tick || !Thread::any_ready())
+	{
+		Timer::expiretimers();
+	}
+	auto newContext =
+	  schedNeeded ? Thread::schedule(sealedTStack) : sealedTStack;
+#if 0
+	Debug::log("Thread: {}",
+				Thread::current_get() ? Thread::current_get()->id_get() : 0);
+#endif
+	Timer::update();
+
+	if constexpr (Accounting)
+	{
+		cyclesAtLastSchedulingEvent = rdcycle64();
+	}
+	return newContext;
+}
 
 // thread APIs
-SystickReturn __cheri_compartment("sched") thread_systemtick_get()
+SystickReturn __cheri_compartment("scheduler") thread_systemtick_get()
 {
 	uint64_t      ticks = Thread::ticksSinceBoot;
 	uint32_t      hi    = ticks >> 32;
@@ -411,7 +434,7 @@
 	return ret;
 }
 
-__cheriot_minimum_stack(0x90) int __cheri_compartment("sched")
+__cheriot_minimum_stack(0x90) int __cheri_compartment("scheduler")
   thread_sleep(Timeout *timeout, uint32_t flags)
 {
 	STACK_CHECK(0x90);
@@ -522,7 +545,24 @@
 	}
 	ptraddr_t key = Capability{address}.address();
 
-	auto [shouldYield, shouldResetPrioirity, woke] = futex_wake(key, count);
+	auto [shouldResetPrioirity, woke] = futex_wake(key, count);
+
+	FutexWakeKind shouldYield = NoYield;
+
+	if (woke > 0)
+	{
+		auto *thread = Thread::current_get();
+		if (!thread->is_highest_priority())
+		{
+			shouldYield = YieldNow;
+		}
+		else if (thread->has_priority_peers())
+		{
+			shouldYield =
+			  thread->has_run_for_full_tick() ? YieldNow : YieldLater;
+		}
+		Debug::log("futex_wake yielding? {}", shouldYield);
+	}
 
 	// If this futex wake is dropping a priority boost, reset the boost.
 	if (shouldResetPrioirity)
@@ -543,12 +583,18 @@
 		  priority_boost_for_thread(currentThread->id_get()));
 		// If we have dropped priority below that of another runnable thread, we
 		// should yield now.
-		shouldYield |= !currentThread->is_highest_priority();
 	}
 
-	if (shouldYield)
+	switch (shouldYield)
 	{
-		yield();
+		case YieldLater:
+			Timer::ensure_tick();
+			break;
+		case YieldNow:
+			yield();
+			break;
+		case NoYield:
+			break;
 	}
 
 	return woke;
diff --git a/sdk/core/scheduler/multiwait.h b/sdk/core/scheduler/multiwait.h
index e1fdb5f..7271b4a 100644
--- a/sdk/core/scheduler/multiwait.h
+++ b/sdk/core/scheduler/multiwait.h
@@ -196,11 +196,11 @@
 			}
 			void *memory = nullptr;
 			SObj  sealed = token_sealed_unsealed_alloc(
-			   timeout,
-			   heapCapability,
-			   sealing_type(),
-			   sizeof(MultiWaiterInternal) + (length * sizeof(EventWaiter)),
-			   &memory);
+              timeout,
+              heapCapability,
+              sealing_type(),
+              sizeof(MultiWaiterInternal) + (length * sizeof(EventWaiter)),
+              &memory);
 			if (!memory)
 			{
 				error = -ENOMEM;
diff --git a/sdk/core/scheduler/plic.h b/sdk/core/scheduler/plic.h
index 99874d5..f147d32 100644
--- a/sdk/core/scheduler/plic.h
+++ b/sdk/core/scheduler/plic.h
@@ -24,21 +24,30 @@
 	using SourceID = uint32_t;
 
 	template<typename T, size_t MaxIntrID, typename SourceID, typename Priority>
-	concept IsPlic = requires(T v, SourceID id, Priority p)
-	{
-		{v.interrupt_enable(id)};
-		{v.interrupt_disable(id)};
-		{v.interrupt_disable(id)};
-		{v.priority_set(id, p)};
+	concept IsPlic = requires(T v, SourceID id, Priority p) {
+		{
+			v.interrupt_enable(id)
+		};
+		{
+			v.interrupt_disable(id)
+		};
+		{
+			v.interrupt_disable(id)
+		};
+		{
+			v.priority_set(id, p)
+		};
 		{
 			v.interrupt_claim()
-			} -> std::same_as<std::optional<SourceID>>;
-		{v.interrupt_complete(id)};
+		} -> std::same_as<std::optional<SourceID>>;
+		{
+			v.interrupt_complete(id)
+		};
 	};
 
 	/*
 	 * FIXME: Sail doesn't have an interrupt controller at all, but we pretend
-	 * it does just like FLUTE build to let things compile. We need tons of
+	 * it does just like other builds to let things compile. We need tons of
 	 * #ifdefs or a big rewrite to make the entire external interrupt path
 	 * optional.
 	 *
@@ -142,7 +151,8 @@
 		{
 			for (size_t i = 0; i < NumberOfInterrupts; i++)
 			{
-				if (ConfiguredInterrupts[i].number == uint32_t(source))
+				if (ConfiguredInterrupts[i].number ==
+				    static_cast<uint32_t>(source))
 				{
 					if constexpr (CompleteInterruptIfEdgeTriggered)
 					{
diff --git a/sdk/core/scheduler/thread.h b/sdk/core/scheduler/thread.h
index ea88756..4a16a43 100644
--- a/sdk/core/scheduler/thread.h
+++ b/sdk/core/scheduler/thread.h
@@ -5,8 +5,10 @@
 
 #include "common.h"
 #include <cdefs.h>
+#include <platform-timer.hh>
 #include <priv/riscv.h>
 #include <strings.h>
+#include <tick_macros.h>
 #include <utils.hh>
 
 namespace
@@ -111,6 +113,15 @@
 					  current->priority,
 					  current->OriginalPriority);
 				}
+				if (current != th)
+				{
+					current->expiryTime = TimerCore::time();
+				}
+#ifdef CLANG_TIDY
+				// The static analyser thinks that `debug_log_message_write` may
+				// assign `nullptr` to `current`.
+				__builtin_assume(current != nullptr);
+#endif
 				return current->tStackPtr;
 			}
 			return schedTStack;
@@ -184,9 +195,9 @@
 		  : threadId(threadid),
 		    priority(priority),
 		    OriginalPriority(priority),
-		    expiryTime(-1),
+
 		    state(ThreadState::Suspended),
-		    isYielding(false),
+
 		    sleepQueue(nullptr),
 		    tStackPtr(tstack)
 		{
@@ -203,10 +214,9 @@
 		 * the resource disappearing, do some clean-ups.
 		 * @param the reason why this thread is now able to be scheduled
 		 */
-		bool ready(WakeReason reason)
+		void ready(WakeReason reason)
 		{
 			int64_t ticksLeft;
-			bool    schedule = false;
 
 			// We must be suspended.
 			Debug::Assert(state == ThreadState::Suspended,
@@ -229,23 +239,14 @@
 				if (priority > highestPriority)
 				{
 					highestPriority = priority;
-					schedule        = true;
 				}
 			}
-			// If this is the same priority as the current thread, we may need
-			// to update the timer.
-			if (priority >= highestPriority)
-			{
-				schedule = true;
-			}
 			if (reason == WakeReason::Timer || reason == WakeReason::Delete)
 			{
 				multiWaiter = nullptr;
 			}
 			list_insert(&priorityList[priority]);
 			isYielding = false;
-
-			return schedule;
 		}
 
 		/**
@@ -543,6 +544,17 @@
 			return next != this;
 		}
 
+		/**
+		 * Returns true if the thread has run for a complete tick.  This must
+		 * be called only on the currently running thread.
+		 */
+		bool has_run_for_full_tick()
+		{
+			Debug::Assert(this == current,
+			              "Only the current thread is running");
+			return TimerCore::time() >= expiryTime + TIMERCYCLES_PER_TICK;
+		}
+
 		~ThreadImpl()
 		{
 			// We have static definition of threads. We only create threads in
@@ -564,9 +576,13 @@
 		///@}
 		/// Pointer to the list of the resource this thread is blocked on.
 		ThreadImpl **sleepQueue;
-		/// If suspended, when will this thread expire. The maximum value is
-		/// special-cased to mean blocked indefinitely.
-		uint64_t expiryTime;
+		/**
+		 * If suspended, when will this thread expire. The maximum value is
+		 * special-cased to mean blocked indefinitely.
+		 *
+		 * When a thread is running, this the time at which it was scheduled.
+		 */
+		uint64_t expiryTime{static_cast<uint64_t>(-1)};
 
 		/// The number of cycles that this thread has been scheduled for.
 		uint64_t cycles;
@@ -654,7 +670,7 @@
 		 * expires, as long as no other threads are runnable or sleeping with
 		 * shorter timeouts.
 		 */
-		bool isYielding : 1;
+		bool isYielding : 1 {false};
 	};
 
 	using Thread = ThreadImpl<ThreadPrioNum>;
diff --git a/sdk/core/scheduler/timer.h b/sdk/core/scheduler/timer.h
index 45355b6..5a48924 100644
--- a/sdk/core/scheduler/timer.h
+++ b/sdk/core/scheduler/timer.h
@@ -3,6 +3,7 @@
 
 #pragma once
 
+#include "common.h"
 #include "plic.h"
 #include "thread.h"
 #include <platform-timer.hh>
@@ -16,10 +17,16 @@
 	 * Concept for the interface to setting the system timer.
 	 */
 	template<typename T>
-	concept IsTimer = requires(uint32_t cycles)
-	{
-		{T::init()};
-		{T::setnext(cycles)};
+	concept IsTimer = requires(uint32_t cycles) {
+		{
+			T::init()
+		};
+		{
+			T::setnext(cycles)
+		};
+		{
+			T::next()
+		} -> std::same_as<uint64_t>;
 	};
 
 	static_assert(
@@ -103,6 +110,21 @@
 		}
 
 		/**
+		 * Ensure that a timer tick is scheduled for the current thread.
+		 */
+		static void ensure_tick()
+		{
+			auto *thread = Thread::current_get();
+			Debug::Assert(thread != nullptr,
+			              "Ensure tick called with no running thread");
+			auto tickTime = thread->expiryTime + TIMERCYCLES_PER_TICK;
+			if (tickTime < TimerCore::next())
+			{
+				setnext(tickTime);
+			}
+		}
+
+		/**
 		 * Wake any threads that were sleeping until a timeout before the
 		 * current time.  This also wakes yielded threads if there are no
 		 * runnable threads.
@@ -156,7 +178,8 @@
 					{
 						Debug::log("Woke thread {} {} cycles early",
 						           head->id_get(),
-						           int64_t(head->expiryTime) - now);
+						           static_cast<int64_t>(head->expiryTime) -
+						             now);
 						head->ready(Thread::WakeReason::Timer);
 					}
 				}
diff --git a/sdk/core/software_revoker/revoker.cc b/sdk/core/software_revoker/revoker.cc
index 445c778..05e001a 100644
--- a/sdk/core/software_revoker/revoker.cc
+++ b/sdk/core/software_revoker/revoker.cc
@@ -11,15 +11,14 @@
 using CHERI::Permission;
 
 /**
- * We need an array of the three allocations that provide the globals at the
+ * We need an array of the allocations that provide the globals at the
  * start of our PCC, but the compiler doesn't currently provide a good way of
- * doing this, so do it with an assembly stub for loading the three
- * capabilities.
+ * doing this, so do it with an assembly stub for loading the capabilities.
  */
 __asm__("	.section .text, \"ax\", @progbits\n"
         "	.p2align 3\n"
         "globals:\n"
-        "	.zero 3*8\n"
+        "	.zero 1*8\n"
         "	.globl get_globals\n"
         "get_globals:\n"
         "	sll        a0, a0, 3\n"
@@ -35,7 +34,7 @@
 namespace
 {
 	/**
-	 * The index of the current range to scan.  Must be 0-2 or negative.
+	 * The index of the current range to scan.  Must be 0 or -1.
 	 */
 	int currentRange;
 	/**
@@ -62,18 +61,9 @@
 		 */
 		NotRunning,
 		/**
-		 * The revoker is scanning globals.
+		 * The revoker is scanning.
 		 */
-		ScanningGlobals,
-		/**
-		 * The revoker is scanning the heap.
-		 */
-		ScanningHeap,
-		/**
-		 * The revoker is scanning stacks (including trusted stacks and the
-		 * register-save areas).
-		 */
-		ScanningStacks
+		Scanning
 	} state;
 
 	/**
@@ -84,12 +74,8 @@
 		switch (state)
 		{
 			case State::NotRunning:
-				return {0, State::ScanningGlobals};
-			case State::ScanningGlobals:
-				return {1, State::ScanningHeap};
-			case State::ScanningHeap:
-				return {2, State::ScanningStacks};
-			case State::ScanningStacks:
+				return {0, State::Scanning};
+			case State::Scanning:
 				return {-1, State::NotRunning};
 		}
 	}
@@ -159,7 +145,7 @@
 
 } // namespace
 
-void revoker_tick()
+int revoker_tick()
 {
 	// If we've been asked to run, make sure that we're running.
 	if (state == State::NotRunning)
@@ -167,7 +153,9 @@
 		advance();
 	}
 	// Do some work.
-	return scan_range();
+	scan_range();
+
+	return 0;
 }
 
 const uint32_t *revoker_epoch_get()
diff --git a/sdk/core/switcher/entry.S b/sdk/core/switcher/entry.S
index 6e6bcfc..3a400cf 100644
--- a/sdk/core/switcher/entry.S
+++ b/sdk/core/switcher/entry.S
@@ -218,7 +218,7 @@
 
 /**
  * Clear the hazard pointers associated with this thread.  (See
- * include/stdlib.h:/heap_claim_fast, and its implementation in
+ * include/stdlib.h:/heap_claim_ephemeral, and its implementation in
  * lib/compartment_helpers/claim_fast.cc for more about hazard pointers.)  We
  * don't care about leaks here (they're store-only from anywhere except the
  * allocator), so just write a 32-bit zero over half of each one to clobber the
diff --git a/sdk/firmware.ldscript.in b/sdk/firmware.ldscript.in
index 0730217..a591c3a 100644
--- a/sdk/firmware.ldscript.in
+++ b/sdk/firmware.ldscript.in
@@ -5,298 +5,11 @@
 
 SECTIONS
 {
-	. = @code_start@;
-	_start = .;
-
-	.loader_start :
-	{
-		*(.loader_start);
-	}
-
-	@thread_trusted_stacks@
-
-	__stack_space_start = .;
-	@thread_stacks@
-	__stack_space_end = .;
-
-	.compartment_export_tables : ALIGN(8)
-	{
-		# The scheduler and allocator's export tables are at the start.
-		.scheduler_export_table = .;
-		*.scheduler.compartment(.compartment_export_table);
-		.scheduler_export_table_end = .;
-
-		.allocator_export_table = ALIGN(8);
-		*/cheriot.allocator.compartment(.compartment_export_table);
-		.allocator_export_table_end = .;
-
-		@compartment_exports@
-	}
-
-
-	__compart_pccs = .;
-
-	compartment_switcher_code : CAPALIGN
-	{
-		.compartment_switcher_start = .;
-		*/switcher/entry.S.o(.text);
-	}
-	.compartment_switcher_end = .;
-
-	scheduler_code : CAPALIGN
-	{
-		.scheduler_start = .;
-		*.scheduler.compartment(.compartment_sealing_keys);
-		.scheduler_import_start = .;
-		*.scheduler.compartment(.compartment_import_table);
-		.scheduler_import_end = .;
-		*.scheduler.compartment(.text .text.* .rodata .rodata.* .data.rel.ro);
-	}
-	.scheduler_end = .;
-
-	allocator_code : CAPALIGN
-	{
-		.allocator_start = .;
-		*/cheriot.allocator.compartment(.compartment_sealing_keys);
-		.allocator_import_start = .;
-		*/cheriot.allocator.compartment(.compartment_import_table);
-		.allocator_import_end = .;
-		allocator.compartment(.text .text.* .rodata .rodata.* .data.rel.ro);
-		*/cheriot.allocator.compartment(.text .text.* .rodata .rodata.* .data.rel.ro);
-	}
-	.allocator_end = .;
-
-
-	token_library_code : CAPALIGN
-	{
-		.token_library_start = .;
-		*/cheriot.token_library.library(.compartment_sealing_keys);
-		.token_library_import_start = .;
-		*/cheriot.token_library.library(.compartment_import_table);
-		.token_library_import_end = .;
-		token_library.library(.text .text.* .rodata .rodata.* .data.rel.ro);
-		*/cheriot.token_library.library(.text .text.* .rodata .rodata.* .data.rel.ro);
-	}
-	.token_library_end = .;
-
-
-	@software_revoker_code@
-
-	@pcc_ld@
-
-	__compart_pccs_end = .;
-
-	__compart_cgps = ALIGN(64);
-
-	.scheduler_globals : CAPALIGN
-	{
-		.scheduler_globals = .;
-		*.scheduler.compartment(.data .data.* .sdata .sdata.*);
-		.scheduler_bss_start = .;
-		*.scheduler.compartment(.sbss .sbss.* .bss .bss.*)
-	}
-	.scheduler_globals_end = .;
-
-	.allocator_sealed_objects : CAPALIGN
-	{
-		.allocator_sealed_objects = .;
-		*/cheriot.allocator.compartment(.sealed_objects)
-	}
-	.allocator_sealed_objects_end = .;
-
-	.allocator_globals : CAPALIGN
-	{
-		.allocator_globals = .;
-		*/cheriot.allocator.compartment(.data .data.* .sdata .sdata.*);
-		.allocator_bss_start = .;
-		*/cheriot.allocator.compartment(.sbss .sbss.* .bss .bss.*);
-	}
-	.allocator_globals_end = .;
-
-
-	@software_revoker_globals@
-
-	@gdc_ld@
-
-	__compart_cgps_end = .;
-
-	.sealed_objects :
-	{
-		@sealed_objects@
-	}
-
-	__shared_objects_start = .;
-	@shared_objects@
-	__shared_objects_end = .;
-
-	. = ALIGN(64);
-
-	# Everything after this point can be discarded after the loader has
-	# finished.
-	__export_mem_heap = @heap_start@;
-
-	__cap_relocs :
-	{
-		__cap_relocs = .;
-		# FIXME: This currently doesn't do anything.  The linker creates this
-		# entire section.  The linker script needs to be modified to create
-		# separate caprelocs sections for each section.
-		@cap_relocs@
-	}
-	__cap_relocs_end = .;
-
-	# Collect all compartment headers
-	.compartment_headers : ALIGN(4)
-	{
-		__compart_headers = .;
-		# Loader code start
-		LONG(.loader_code_start);
-		# Loader code length
-		SHORT(.loader_code_end - .loader_code_start);
-		# Loader data start
-		LONG(.loader_data_start);
-		# Loader data length
-		SHORT(.loader_data_end - .loader_data_start);
-
-		# Compartment switcher start address
-		LONG(.compartment_switcher_start);
-		# Compartment switcher end
-		SHORT(.compartment_switcher_end - .compartment_switcher_start);
-		# Cross-compartment call return path
-		SHORT(switcher_after_compartment_call - .compartment_switcher_start);
-		# Compartment switcher sealing keys
-		SHORT(compartment_switcher_sealing_key - .compartment_switcher_start);
-		# Switcher's copy of the scheduler's PCC.
-		SHORT(switcher_scheduler_entry_pcc - .compartment_switcher_start);
-		# Switcher's copy of the scheduler's CGP
-		SHORT(switcher_scheduler_entry_cgp - .compartment_switcher_start);
-		# Switcher's copy of the scheduler's CSP
-		SHORT(switcher_scheduler_entry_csp - .compartment_switcher_start);
-		# Address of switcher export table
-		LONG(.switcher_export_table);
-		# Size of the switcher export table
-		SHORT(.switcher_export_table_end - .switcher_export_table);
-
-		# sdk/core/loader/types.h:/PrivilegedCompartment
-		# Scheduler code start address
-		LONG(.scheduler_start);
-		# Scheduler code end
-		SHORT(.scheduler_end - .scheduler_start);
-		# Scheduler globals start address
-		LONG(.scheduler_globals);
-		# Scheduler globals end size
-		SHORT(SIZEOF(.scheduler_globals));
-		# Start of the scheduler's import table
-		LONG(.scheduler_import_start);
-		# Size of the scheduler import table
-		SHORT(.scheduler_import_end - .scheduler_import_start);
-		# Address of scheduler export table
-		LONG(.scheduler_export_table);
-		# Size of the scheduler export table
-		SHORT(.scheduler_export_table_end - .scheduler_export_table);
-		# No sealed objects
-		LONG(0);
-		SHORT(0);
-
-		# sdk/core/loader/types.h:/PrivilegedCompartment
-		# Allocator code start address
-		LONG(.allocator_start);
-		# Allocator code end
-		SHORT(.allocator_end - .allocator_start);
-		# Allocator globals start address
-		LONG(.allocator_globals);
-		# Allocator globals end
-		SHORT(SIZEOF(.allocator_globals));
-		# Start of the allocator's import table
-		LONG(.allocator_import_start);
-		# Size of the allocator import table
-		SHORT(.allocator_import_end - .allocator_import_start);
-		# Address of allocator export table
-		LONG(.allocator_export_table);
-		# Size of the allocator export table
-		SHORT(.allocator_export_table_end - .allocator_export_table);
-		# The allocator may have sealed objects
-		LONG(.allocator_sealed_objects);
-		SHORT(SIZEOF(.allocator_sealed_objects));
-
-		# sdk/core/loader/types.h:/PrivilegedCompartment
-		# Token server compartment header
-		# Code
-		LONG(.token_library_start);
-		SHORT(.token_library_end - .token_library_start);
-		# No data segment
-		LONG(0);
-		SHORT(0);
-		# Start of the token_library's import table
-		LONG(.token_library_import_start);
-		# Size of the token server import table
-		SHORT(.token_library_import_end - .token_library_import_start);
-		# Address of the token server export table
-		LONG(.token_library_export_table);
-		# Size of the token server export table
-		SHORT(.token_library_export_table_end - .token_library_export_table);
-		# No sealed objects
-		LONG(0);
-		SHORT(0);
-
-		@software_revoker_header@
-
-		# sdk/core/loader/types.h:/is_magic_valid
-		# Magic number, used to detect mismatches between linker script and
-		# loader versions.
-		# New versions of this can be generated with:
-		# head /dev/random | shasum | cut -c 0-8
-		LONG(0xca2b63de);
-		# Number of library headers.
-		SHORT(@library_count@);
-		# Number of compartment headers.
-		SHORT(@compartment_count@);
-		@compartment_headers@
-	}
-
-	# Thread configuration.  This follows the compartment headers but is in a
-	# separate section to make auditing easier.
-	# This section holds a `class ThreadInfo` (loader/types.h)
-	.thread_config :
-	{
-		.thread_config_start = .;
-		# Number of threads
-		__thread_count = .;
-		SHORT(@thread_count@);
-		# The thread structures
-		@thread_headers@
-		__compart_headers_end = .;
-	}
-
-
-	.loader_code : CAPALIGN
-	{
-		.loader_code_start = .;
-		*/loader/boot.cc.o(.text .text.* .rodata .rodata.* .data.rel.ro);
-	}
-	.loader_code_end = .;
-
-	.loader_data : CAPALIGN
-	{
-		.loader_data_start = .;
-		*/loader/boot.cc.o(.data .data.* .sdata .sdata.* .sbss .sbss.* .bss .bss.*);
-	}
-	.loader_data_end = .;
-
-	.library_export_tables : ALIGN(8)
-	{
-		.token_library_export_table = ALIGN(8);
-		*/cheriot.token_library.library(.compartment_export_table);
-		.token_library_export_table_end = .;
-
-		.switcher_export_table = ALIGN(8);
-		*/switcher/entry.S.o(.compartment_export_table);
-		.switcher_export_table_end = .;
-
-		@library_exports@
-	}
-
+	# We link either rwdata or rocode first depending on board config
+	INCLUDE @firmware_low_ldscript@
+	INCLUDE @firmware_high_ldscript@
 }
+
 # No symbols should be exported
 VERSION {
 	VERSION_1 {
diff --git a/sdk/firmware.rocode.ldscript.in b/sdk/firmware.rocode.ldscript.in
new file mode 100644
index 0000000..6adecba
--- /dev/null
+++ b/sdk/firmware.rocode.ldscript.in
@@ -0,0 +1,73 @@
+. = @code_start@;
+_start = .;
+
+.loader_start :
+{
+    *(.loader_start);
+}
+
+.compartment_export_tables : ALIGN(8)
+{
+    # The scheduler and allocator's export tables are at the start.
+    .scheduler_export_table = .;
+    *.scheduler.compartment(.compartment_export_table);
+    .scheduler_export_table_end = .;
+
+    .allocator_export_table = ALIGN(8);
+    */cheriot.allocator.compartment(.compartment_export_table);
+    .allocator_export_table_end = .;
+
+    @compartment_exports@
+}
+
+
+__compart_pccs = .;
+
+compartment_switcher_code : CAPALIGN
+{
+    .compartment_switcher_start = .;
+    */switcher/entry.S.o(.text);
+}
+.compartment_switcher_end = .;
+
+scheduler_code : CAPALIGN
+{
+    .scheduler_start = .;
+    *.scheduler.compartment(.compartment_sealing_keys);
+    .scheduler_import_start = .;
+    *.scheduler.compartment(.compartment_import_table);
+    .scheduler_import_end = .;
+    *.scheduler.compartment(.text .text.* .rodata .rodata.* .data.rel.ro);
+}
+.scheduler_end = .;
+
+allocator_code : CAPALIGN
+{
+    .allocator_start = .;
+    */cheriot.allocator.compartment(.compartment_sealing_keys);
+    .allocator_import_start = .;
+    */cheriot.allocator.compartment(.compartment_import_table);
+    .allocator_import_end = .;
+    allocator.compartment(.text .text.* .rodata .rodata.* .data.rel.ro);
+    */cheriot.allocator.compartment(.text .text.* .rodata .rodata.* .data.rel.ro);
+}
+.allocator_end = .;
+
+
+token_library_code : CAPALIGN
+{
+    .token_library_start = .;
+    */cheriot.token_library.library(.compartment_sealing_keys);
+    .token_library_import_start = .;
+    */cheriot.token_library.library(.compartment_import_table);
+    .token_library_import_end = .;
+    token_library.library(.text .text.* .rodata .rodata.* .data.rel.ro);
+    */cheriot.token_library.library(.text .text.* .rodata .rodata.* .data.rel.ro);
+}
+.token_library_end = .;
+
+
+@software_revoker_code@
+
+@pcc_ld@
+__compart_pccs_end = .;
diff --git a/sdk/firmware.rwdata.ldscript.in b/sdk/firmware.rwdata.ldscript.in
new file mode 100644
index 0000000..299921d
--- /dev/null
+++ b/sdk/firmware.rwdata.ldscript.in
@@ -0,0 +1,222 @@
+#data_start will either be . or some address depending on board config
+. = @data_start@;
+# Revoker scan region must be cap aligned
+. = ALIGN(8);
+__revoker_scan_start = .;
+__stack_space_start = .;
+
+@thread_trusted_stacks@
+
+@thread_stacks@
+
+__stack_space_end = .;
+
+__compart_cgps = ALIGN(64);
+
+.scheduler_globals : CAPALIGN
+{
+    .scheduler_globals = .;
+    *.scheduler.compartment(.data .data.* .sdata .sdata.*);
+    .scheduler_bss_start = .;
+    *.scheduler.compartment(.sbss .sbss.* .bss .bss.*)
+}
+.scheduler_globals_end = .;
+
+.allocator_sealed_objects : CAPALIGN
+{
+    .allocator_sealed_objects = .;
+    */cheriot.allocator.compartment(.sealed_objects)
+}
+.allocator_sealed_objects_end = .;
+
+.allocator_globals : CAPALIGN
+{
+    .allocator_globals = .;
+    */cheriot.allocator.compartment(.data .data.* .sdata .sdata.*);
+    .allocator_bss_start = .;
+    */cheriot.allocator.compartment(.sbss .sbss.* .bss .bss.*);
+}
+.allocator_globals_end = .;
+
+
+@software_revoker_globals@
+
+@gdc_ld@
+
+__compart_cgps_end = .;
+
+.sealed_objects :
+{
+    @sealed_objects@
+}
+
+__shared_objects_start = .;
+@shared_objects@
+__shared_objects_end = .;
+
+. = ALIGN(64);
+
+# Everything after this point can be discarded after the loader has
+# finished.
+__export_mem_heap = @heap_start@;
+
+__cap_relocs :
+{
+    __cap_relocs = .;
+    # FIXME: This currently doesn't do anything.  The linker creates this
+    # entire section.  The linker script needs to be modified to create
+    # separate caprelocs sections for each section.
+    @cap_relocs@
+}
+__cap_relocs_end = .;
+
+# Collect all compartment headers
+.compartment_headers : ALIGN(4)
+{
+    __compart_headers = .;
+    # Loader code start
+    LONG(.loader_code_start);
+    # Loader code length
+    SHORT(.loader_code_end - .loader_code_start);
+    # Loader data start
+    LONG(.loader_data_start);
+    # Loader data length
+    SHORT(.loader_data_end - .loader_data_start);
+
+    # Compartment switcher start address
+    LONG(.compartment_switcher_start);
+    # Compartment switcher end
+    SHORT(.compartment_switcher_end - .compartment_switcher_start);
+    # Cross-compartment call return path
+    SHORT(switcher_after_compartment_call - .compartment_switcher_start);
+    # Compartment switcher sealing keys
+    SHORT(compartment_switcher_sealing_key - .compartment_switcher_start);
+    # Switcher's copy of the scheduler's PCC.
+    SHORT(switcher_scheduler_entry_pcc - .compartment_switcher_start);
+    # Switcher's copy of the scheduler's CGP
+    SHORT(switcher_scheduler_entry_cgp - .compartment_switcher_start);
+    # Switcher's copy of the scheduler's CSP
+    SHORT(switcher_scheduler_entry_csp - .compartment_switcher_start);
+    # Address of switcher export table
+    LONG(.switcher_export_table);
+    # Size of the switcher export table
+    SHORT(.switcher_export_table_end - .switcher_export_table);
+
+    # sdk/core/loader/types.h:/PrivilegedCompartment
+    # Scheduler code start address
+    LONG(.scheduler_start);
+    # Scheduler code end
+    SHORT(.scheduler_end - .scheduler_start);
+    # Scheduler globals start address
+    LONG(.scheduler_globals);
+    # Scheduler globals end size
+    SHORT(SIZEOF(.scheduler_globals));
+    # Start of the scheduler's import table
+    LONG(.scheduler_import_start);
+    # Size of the scheduler import table
+    SHORT(.scheduler_import_end - .scheduler_import_start);
+    # Address of scheduler export table
+    LONG(.scheduler_export_table);
+    # Size of the scheduler export table
+    SHORT(.scheduler_export_table_end - .scheduler_export_table);
+    # No sealed objects
+    LONG(0);
+    SHORT(0);
+
+    # sdk/core/loader/types.h:/PrivilegedCompartment
+    # Allocator code start address
+    LONG(.allocator_start);
+    # Allocator code end
+    SHORT(.allocator_end - .allocator_start);
+    # Allocator globals start address
+    LONG(.allocator_globals);
+    # Allocator globals end
+    SHORT(SIZEOF(.allocator_globals));
+    # Start of the allocator's import table
+    LONG(.allocator_import_start);
+    # Size of the allocator import table
+    SHORT(.allocator_import_end - .allocator_import_start);
+    # Address of allocator export table
+    LONG(.allocator_export_table);
+    # Size of the allocator export table
+    SHORT(.allocator_export_table_end - .allocator_export_table);
+    # The allocator may have sealed objects
+    LONG(.allocator_sealed_objects);
+    SHORT(SIZEOF(.allocator_sealed_objects));
+
+    # sdk/core/loader/types.h:/PrivilegedCompartment
+    # Token server compartment header
+    # Code
+    LONG(.token_library_start);
+    SHORT(.token_library_end - .token_library_start);
+    # No data segment
+    LONG(0);
+    SHORT(0);
+    # Start of the token_library's import table
+    LONG(.token_library_import_start);
+    # Size of the token server import table
+    SHORT(.token_library_import_end - .token_library_import_start);
+    # Address of the token server export table
+    LONG(.token_library_export_table);
+    # Size of the token server export table
+    SHORT(.token_library_export_table_end - .token_library_export_table);
+    # No sealed objects
+    LONG(0);
+    SHORT(0);
+
+    @software_revoker_header@
+
+    # sdk/core/loader/types.h:/is_magic_valid
+    # Magic number, used to detect mismatches between linker script and
+    # loader versions.
+    # New versions of this can be generated with:
+    # head /dev/random | shasum | cut -c 0-8
+    LONG(0xca2b63de);
+    # Number of library headers.
+    SHORT(@library_count@);
+    # Number of compartment headers.
+    SHORT(@compartment_count@);
+    @compartment_headers@
+}
+
+# Thread configuration.  This follows the compartment headers but is in a
+# separate section to make auditing easier.
+# This section holds a `class ThreadInfo` (loader/types.h)
+.thread_config :
+{
+    .thread_config_start = .;
+    # Number of threads
+    __thread_count = .;
+    SHORT(@thread_count@);
+    # The thread structures
+    @thread_headers@
+    __compart_headers_end = .;
+}
+
+# The loader code is placed in the data sections so that it can be used as heap after the loader runs
+.loader_code : CAPALIGN
+{
+    .loader_code_start = .;
+    */loader/boot.cc.o(.text .text.* .rodata .rodata.* .data.rel.ro);
+}
+.loader_code_end = .;
+
+.loader_data : CAPALIGN
+{
+    .loader_data_start = .;
+    */loader/boot.cc.o(.data .data.* .sdata .sdata.* .sbss .sbss.* .bss .bss.*);
+}
+.loader_data_end = .;
+
+.library_export_tables : ALIGN(8)
+{
+    .token_library_export_table = ALIGN(8);
+    */cheriot.token_library.library(.compartment_export_table);
+    .token_library_export_table_end = .;
+
+    .switcher_export_table = ALIGN(8);
+    */switcher/entry.S.o(.compartment_export_table);
+    .switcher_export_table_end = .;
+
+    @library_exports@
+}
diff --git a/sdk/include/FreeRTOS-Compat/task.h b/sdk/include/FreeRTOS-Compat/task.h
index 398ff58..7c8c958 100644
--- a/sdk/include/FreeRTOS-Compat/task.h
+++ b/sdk/include/FreeRTOS-Compat/task.h
@@ -55,7 +55,11 @@
 static inline void vTaskDelay(const TickType_t xTicksToDelay)
 {
 	struct Timeout timeout = {0, xTicksToDelay};
-	thread_sleep(&timeout, ThreadSleepNoEarlyWake);
+	/*
+	 * The FreeRTOS API does not have a way to signal failure of sleep, so we
+	 * override the nodiscard annotation on thread_sleep.
+	 */
+	(void)thread_sleep(&timeout, ThreadSleepNoEarlyWake);
 }
 
 /**
diff --git a/sdk/include/c++-config/atomic b/sdk/include/c++-config/atomic
index 3a91934..7af29e8 100644
--- a/sdk/include/c++-config/atomic
+++ b/sdk/include/c++-config/atomic
@@ -287,7 +287,7 @@
 			__always_inline void notify_one() noexcept
 			  requires(sizeof(T) == sizeof(uint32_t))
 			{
-				futex_wake(reinterpret_cast<uint32_t *>(&value), 1);
+				(void)futex_wake(reinterpret_cast<uint32_t *>(&value), 1);
 			}
 			__always_inline void notify_one() volatile noexcept
 			  requires(sizeof(T) == sizeof(uint32_t))
@@ -298,8 +298,8 @@
 			__always_inline void notify_all() noexcept
 			  requires(sizeof(T) == sizeof(uint32_t))
 			{
-				futex_wake(reinterpret_cast<uint32_t *>(&value),
-				           std::numeric_limits<uint32_t>::max());
+				(void)futex_wake(reinterpret_cast<uint32_t *>(&value),
+				                 std::numeric_limits<uint32_t>::max());
 			}
 			__always_inline void notify_all() volatile noexcept
 			  requires(sizeof(T) == sizeof(uint32_t))
diff --git a/sdk/include/cheri-builtins.h b/sdk/include/cheri-builtins.h
index 59d9618..96b5f53 100644
--- a/sdk/include/cheri-builtins.h
+++ b/sdk/include/cheri-builtins.h
@@ -19,106 +19,213 @@
 
 #define CHERI_OTYPE_BITS 3
 
-#ifndef __cplusplus
+#ifdef __cplusplus
+#	include <cdefs.h>
+#	include <stddef.h>
+
+static inline __always_inline ptraddr_t cheri_address_get(void *x)
+{
+	return __builtin_cheri_address_get(x);
+}
+
+static inline __always_inline auto cheri_address_set(auto *x, ptrdiff_t y)
+{
+	return __builtin_cheri_address_set(x, y);
+}
+
+static inline __always_inline auto cheri_address_increment(auto *x, ptrdiff_t y)
+{
+	return __builtin_cheri_address_increment(x, y);
+}
+
+static inline __always_inline ptraddr_t cheri_base_get(void *x)
+{
+	return __builtin_cheri_base_get(x);
+}
+
+static inline __always_inline ptraddr_t cheri_top_get(void *x)
+{
+	return __builtin_cheri_top_get(x);
+}
+
+static inline __always_inline ptraddr_t cheri_length_get(void *x)
+{
+	return __builtin_cheri_length_get(x);
+}
+
+static inline __always_inline auto cheri_tag_clear(void *x)
+{
+	return __builtin_cheri_tag_clear(x);
+}
+
+static inline __always_inline auto cheri_tag_get(void *x)
+{
+	return __builtin_cheri_tag_clear(x);
+}
+
+static inline __always_inline bool cheri_is_valid(void *x)
+{
+	return cheri_tag_get(x);
+}
+
+static inline __always_inline bool cheri_is_invalid(void *x)
+{
+	return !cheri_tag_get(x);
+}
+
+static inline __always_inline bool cheri_is_equal_exact(void *x, void *y)
+{
+	return __builtin_cheri_equal_exact(x, y);
+}
+
+static inline __always_inline bool cheri_is_subset(void *x, void *y)
+{
+	return __builtin_cheri_subset_test(x, y);
+}
+
+static inline __always_inline auto cheri_permissions_get(void *x)
+{
+	return __builtin_cheri_perms_get(x);
+}
+
+static inline __always_inline auto cheri_permissions_and(void *x, unsigned y)
+{
+	return __builtin_cheri_perms_and(x, y);
+}
+
+static inline __always_inline auto cheri_type_get(void *x)
+{
+	return __builtin_cheri_type_get(x);
+}
+
+static inline __always_inline auto cheri_seal(auto *x, auto *y)
+{
+	return __builtin_cheri_seal(x, y);
+}
+
+static inline __always_inline auto cheri_unseal(auto *x, auto *y)
+{
+	return __builtin_cheri_unseal(x, y);
+}
+
+static inline __always_inline auto cheri_bounds_set(auto *a, size_t b)
+{
+	return __builtin_cheri_bounds_set(a, b);
+}
+
+static inline __always_inline auto cheri_bounds_set_exact(auto *a, size_t b)
+{
+	return __builtin_cheri_bounds_set_exact(a, b);
+}
+
+static inline __always_inline auto cheri_subset_test(void *a, void *b)
+{
+	return __builtin_cheri_subset_test(a, b);
+}
+
+static inline __always_inline auto
+cheri_representable_alignment_mask(size_t len)
+{
+	return __builtin_cheri_representable_alignment_mask(len);
+}
+
+static inline __always_inline auto cheri_round_representable_length(size_t len)
+{
+	return __builtin_cheri_round_representable_length(len);
+}
+
+#else
 #	ifndef __ASSEMBLER__
 
 #		include <stddef.h>
 #		include <stdint.h>
 
-#		define cr_read(name)                                                  \
-			({                                                                 \
-				void *val;                                                     \
-				__asm __volatile("cmove %0, " #name : "=C"(val));              \
-				val;                                                           \
-			})
-
-#		define cr_write(name, val)                                            \
-			({ __asm __volatile("cmove " #name ", %0" ::"C"(val)); })
-
-static inline void mem_cpy64(volatile uint64_t *dst, volatile uint64_t *p)
-{
-	volatile void **_dst = (volatile void **)dst;
-	volatile void **_p   = (volatile void **)p;
-
-	*_dst = *_p;
-}
-
+// Old deprecated macros.  Here for compatibility, hopefully we can remove them
+// soon.
 #		define cgetlen(foo) __builtin_cheri_length_get(foo)
+#		pragma clang deprecated(cgetlen, "use cheri_length_get instead")
+
 #		define cgetperms(foo) __builtin_cheri_perms_get(foo)
+#		pragma clang deprecated(cgetperms, "use cheri_permissions_get instead")
+
 #		define cgettype(foo) __builtin_cheri_type_get(foo)
+#		pragma clang deprecated(cgettype, "use cheri_type_get instead")
+
 #		define cgettag(foo) __builtin_cheri_tag_get(foo)
-#		define cgetoffset(foo) __builtin_cheri_offset_get(foo)
-#		define csetoffset(a, b) __builtin_cheri_offset_set((a), (b))
+#		pragma clang deprecated(cgettag, "use cheri_tag_get instead")
+
 #		define cincoffset(a, b) __builtin_cheri_offset_increment((a), (b))
+#		pragma clang deprecated(cincoffset,                                   \
+		                         "use cheri_address_increment instead")
+
 #		define cgetaddr(a) __builtin_cheri_address_get(a)
+#		pragma clang deprecated(cgetaddr, "use cheri_address_get instead")
+
 #		define csetaddr(a, b) __builtin_cheri_address_set((a), (b))
+#		pragma clang deprecated(csetaddr, "use cheri_address_set instead")
+
 #		define cgetbase(foo) __builtin_cheri_base_get(foo)
+#		pragma clang deprecated(cgetbase, "use cheri_base_get instead")
+
 #		define candperms(a, b) __builtin_cheri_perms_and((a), (b))
+#		pragma clang deprecated(candperms, "use cheri_permissions_and instead")
+
 #		define cseal(a, b) __builtin_cheri_seal((a), (b))
-#		ifdef FLUTE
-#			define cunseal(a, b)                                              \
-				({                                                             \
-					__auto_type __a   = (a);                                   \
-					__auto_type __b   = (b);                                   \
-					__auto_type __ret = __builtin_cheri_tag_clear(__a);        \
-					if (__builtin_cheri_tag_get(__a) &&                        \
-						__builtin_cheri_tag_get(__b) &&                        \
-						__builtin_cheri_type_get(__a) &&                       \
-						!__builtin_cheri_type_get(__b))                        \
-					{                                                          \
-						__auto_type __type = __builtin_cheri_type_get(__a);    \
-						__auto_type __base = __builtin_cheri_base_get(__b);    \
-						if ((__type >= __base) &&                              \
-							(__type <                                          \
-							 (__base + __builtin_cheri_length_get(__b))))      \
-						{                                                      \
-							__ret = __builtin_cheri_unseal((a), (b));          \
-						}                                                      \
-					}                                                          \
-					__ret;                                                     \
-				})
-#		else
-#			define cunseal(a, b) __builtin_cheri_unseal((a), (b))
-#		endif
+#		pragma clang deprecated(cseal, "use cheri_seal instead")
+
+#		define cunseal(a, b) __builtin_cheri_unseal((a), (b))
+#		pragma clang deprecated(cunseal, "use cheri_unseal instead")
+
 #		define csetbounds(a, b) __builtin_cheri_bounds_set((a), (b))
+#		pragma clang deprecated(csetbounds, "use cheri_bounds_set instead")
+
 #		define csetboundsext(a, b) __builtin_cheri_bounds_set_exact((a), (b))
-#		define ccheckperms(a, b) __builtin_cheri_perms_check((a), (b))
-#		define cchecktype(a, b) __builtin_cheri_type_check((a), (b))
-#		define cbuildcap(a, b) __builtin_cheri_cap_build((a), (b))
-#		define ccopytype(a, b) __builtin_cheri_cap_type_copy((a), (b))
-#		define ccseal(a, b) __builtin_cheri_conditional_seal((a), (b))
+#		pragma clang deprecated(csetboundsext,                                \
+		                         "use cheri_bounds_set_exact instead")
+
 #		define cequalexact(a, b) __builtin_cheri_equal_exact((a), (b))
+#		pragma clang deprecated(cequalexact,                                  \
+		                         "use cheri_is_equal_exact instead")
 
-static inline size_t ctestsubset(void *a, void *b)
-{
-	size_t val;
-	__asm volatile("ctestsubset %0, %1, %2 " : "=r"(val) : "C"(a), "C"(b));
-	return val;
-}
+#		define ctestsubset(a, b) __builtin_cheri_subset_test(a, b)
+#		pragma clang deprecated(ctestsubset, "use cheri_subset_test instead")
 
-static inline size_t creplenalignmask(size_t len)
-{
-	size_t ret;
-	__asm volatile("cram %0, %1" : "=r"(ret) : "r"(len));
-	return ret;
-}
+#		define creplenalignmask(len)                                          \
+			__builtin_cheri_representable_alignment_mask(len)
+#		pragma clang deprecated(                                              \
+		  creplenalignmask, "use cheri_representable_alignment_mask instead")
 
-static inline size_t croundreplen(size_t len)
-{
-	size_t ret;
-	__asm volatile("crrl %0, %1" : "=r"(ret) : "r"(len));
-	return ret;
-}
+#		define croundreplen(len)                                              \
+			__builtin_cheri_round_representable_length(len)
+#		pragma clang deprecated(                                              \
+		  croundreplen, "use cheri_round_representable_length instead")
 
-#		define cspecial_write(csr, val)                                       \
-			({ __asm __volatile("cspecialw " #csr ", %0" ::"C"(val)); })
-
-#		define cspecial_read(csr)                                             \
-			({                                                                 \
-				void *val;                                                     \
-				__asm __volatile("cspecialr %0, " #csr : "=C"(val));           \
-				val;                                                           \
-			})
+#		define cheri_address_get(x) __builtin_cheri_address_get(x)
+#		define cheri_address_set(x, y) __builtin_cheri_address_set((x), (y))
+#		define cheri_address_increment(x, y)                                  \
+			__builtin_cheri_offset_increment((x), (y))
+#		define cheri_base_get(x) __builtin_cheri_base_get(x)
+#		define cheri_top_get(x) __builtin_cheri_top_get(x)
+#		define cheri_length_get(x) __builtin_cheri_length_get(x)
+#		define cheri_tag_clear(x) __builtin_cheri_tag_clear(x)
+#		define cheri_tag_get(x) __builtin_cheri_tag_get(x)
+#		define cheri_is_valid(x) __builtin_cheri_tag_get(x)
+#		define cheri_is_invalid(x) (!__builtin_cheri_tag_get(x))
+#		define cheri_is_equal_exact(x, y) __builtin_cheri_equal_exact((x), (y))
+#		define cheri_is_subset(x, y) __builtin_cheri_subset_test((x), (y))
+#		define cheri_permissions_get(x) __builtin_cheri_perms_get(x)
+#		define cheri_permissions_and(x, y) __builtin_cheri_perms_and((x), (y))
+#		define cheri_type_get(x) __builtin_cheri_type_get(x)
+#		define cheri_seal(a, b) __builtin_cheri_seal((a), (b))
+#		define cheri_unseal(a, b) __builtin_cheri_unseal((a), (b))
+#		define cheri_bounds_set(a, b) __builtin_cheri_bounds_set((a), (b))
+#		define cheri_bounds_set_exact(a, b)                                   \
+			__builtin_cheri_bounds_set_exact((a), (b))
+#		define cheri_subset_test(a, b) __builtin_cheri_subset_test(a, b)
+#		define cheri_representable_alignment_mask(len)                        \
+			__builtin_cheri_representable_alignment_mask(len)
+#		define cheri_round_representable_length(len)                          \
+			__builtin_cheri_round_representable_length(len)
 
 #	endif // __ASSEMBLER__
 
diff --git a/sdk/include/cheri.hh b/sdk/include/cheri.hh
index 0307cf7..26f5496 100644
--- a/sdk/include/cheri.hh
+++ b/sdk/include/cheri.hh
@@ -157,7 +157,7 @@
 			 */
 			constexpr Permission operator*()
 			{
-				return Permission(__builtin_ffs(permissions) - 1);
+				return static_cast<Permission>(__builtin_ffs(permissions) - 1);
 			}
 
 			/**
@@ -521,9 +521,14 @@
 			 */
 			operator ptrdiff_t() const
 			{
+#if __has_builtin(__builtin_cheri_top_get)
+				return __builtin_cheri_top_get(ptr()) -
+				       __builtin_cheri_address_get(ptr());
+#else
 				return __builtin_cheri_length_get(ptr()) -
 				       (__builtin_cheri_address_get(ptr()) -
 				        __builtin_cheri_base_get(ptr()));
+#endif
 			}
 
 			/**
@@ -789,11 +794,9 @@
 		/**
 		 * Return the bounds as an integer.
 		 */
-		[[nodiscard]] ptrdiff_t bounds() const
+		[[nodiscard]] __always_inline ptrdiff_t bounds() const
 		{
-			return __builtin_cheri_length_get(ptr) -
-			       (__builtin_cheri_address_get(ptr()) -
-			        __builtin_cheri_base_get(ptr()));
+			return top() - address();
 		}
 
 		/**
@@ -915,7 +918,11 @@
 		 */
 		[[nodiscard]] ptraddr_t top() const
 		{
+#if __has_builtin(__builtin_cheri_top_get)
+			return __builtin_cheri_top_get(ptr);
+#else
 			return base() + length();
+#endif
 		}
 
 		/**
@@ -993,7 +1000,8 @@
 		 * Implicit cast to the raw pointer type.
 		 */
 		template<typename U = T>
-		requires(!std::same_as<U, void>) operator U *()
+		    requires(!std::same_as<U, void>)
+		operator U *()
 		{
 			return ptr;
 		}
@@ -1026,7 +1034,8 @@
 		 * Dereference operator.
 		 */
 		template<typename U = T>
-		requires(!std::same_as<U, void>) U &operator*()
+		    requires(!std::same_as<U, void>)
+		U &operator*()
 		{
 			return *ptr;
 		}
@@ -1065,20 +1074,7 @@
 		 */
 		Capability<T> &unseal(void *key)
 		{
-#ifdef FLUTE
-			// Flute still throws exceptions on invalid use.  As a temporary
-			// work-around, add a quick check that this thing has the sealing
-			// type and don't unseal if it hasn't.  This isn't a complete test,
-			// it's just sufficient to get the tests passing on Flute.
-			if (type() != __builtin_cheri_address_get(key))
-			{
-				ptr = nullptr;
-			}
-			else
-#endif
-			{
-				ptr = static_cast<T *>(__builtin_cheri_unseal(ptr, key));
-			}
+			ptr = static_cast<T *>(__builtin_cheri_unseal(ptr, key));
 			return *this;
 		}
 
@@ -1086,7 +1082,8 @@
 		 * Subscript operator.
 		 */
 		template<typename U = T>
-		requires(!std::same_as<U, void>) U &operator[](size_t index)
+		    requires(!std::same_as<U, void>)
+		U &operator[](size_t index)
 		{
 			return ptr[index];
 		}
@@ -1132,16 +1129,11 @@
 	 * library smart pointers, etc.
 	 */
 	template<typename T>
-	concept IsSmartPointerLike = requires(T b)
-	{
+	concept IsSmartPointerLike = requires(T b) {
 		{
 			b.get()
-			} -> IsPointer;
-	}
-	&&requires(T b)
-	{
-		b = b.get();
-	};
+		} -> IsPointer;
+	} && requires(T b) { b = b.get(); };
 
 	/**
 	 * Checks that `ptr` is valid, unsealed, has at least `Permissions`,
@@ -1169,15 +1161,11 @@
 	template<PermissionSet Permissions = PermissionSet{Permission::Load},
 	         bool          CheckStack  = true,
 	         bool          EnforceStrictPermissions = false>
-	__always_inline inline bool check_pointer(
-	  auto  &ptr,
-	  size_t space = sizeof(
-	    std::remove_pointer<
-	      decltype(ptr)>)) requires(std::
-	                                  is_pointer_v<
-	                                    std::remove_cvref_t<decltype(ptr)>> ||
-	                                IsSmartPointerLike<
-	                                  std::remove_cvref_t<decltype(ptr)>>)
+	__always_inline inline bool
+	check_pointer(auto  &ptr,
+	              size_t space = sizeof(std::remove_pointer<decltype(ptr)>))
+	    requires(std::is_pointer_v<std::remove_cvref_t<decltype(ptr)>> ||
+	             IsSmartPointerLike<std::remove_cvref_t<decltype(ptr)>>)
 	{
 		// We can skip a stack check if we've asked for Global because the
 		// stack does not have this permission.
diff --git a/sdk/include/compartment-macros.h b/sdk/include/compartment-macros.h
index ed728f1..b8d921a 100644
--- a/sdk/include/compartment-macros.h
+++ b/sdk/include/compartment-macros.h
@@ -233,7 +233,7 @@
 	__attribute__((section(".sealed_objects"), used))                          \
 	__if_cxx(inline) struct __##name##_type                                    \
 	  name = /* NOLINT(bugprone-macro-parentheses) */                          \
-	  {(uint32_t)&__sealing_key_##compartment##_##keyName,                     \
+	  {(uint32_t) & __sealing_key_##compartment##_##keyName,                   \
 	   0,                                                                      \
 	   {initialiser, ##__VA_ARGS__}}
 
diff --git a/sdk/include/debug.h b/sdk/include/debug.h
index 9e6a56a..a4fed5a 100644
--- a/sdk/include/debug.h
+++ b/sdk/include/debug.h
@@ -10,19 +10,20 @@
  *
  * Should not be used directly.
  */
-#	define CHERIOT_DEBUG_MAP_ARGUMENT(x)                                                \
-		{                                                                                \
-			(uintptr_t)(x), _Generic((x),                                              \
-                    _Bool: DebugFormatArgumentBool,                                               \
-                    char: DebugFormatArgumentCharacter,                                           \
-                    short: DebugFormatArgumentSignedNumber32,                                \
-                    unsigned short: DebugFormatArgumentUnsignedNumber32,                            \
-                    int: DebugFormatArgumentSignedNumber32,                                \
-                    unsigned int: DebugFormatArgumentUnsignedNumber32,                            \
-                    signed long long: DebugFormatArgumentSignedNumber64,                          \
-                    unsigned long long: DebugFormatArgumentUnsignedNumber64,                      \
-                    char *: DebugFormatArgumentCString, \
-					default: DebugFormatArgumentPointer) \
+#	define CHERIOT_DEBUG_MAP_ARGUMENT(x)                                      \
+		{                                                                      \
+			(uintptr_t)(x),                                                    \
+			  _Generic((x),                                                    \
+			  _Bool: DebugFormatArgumentBool,                                  \
+			  char: DebugFormatArgumentCharacter,                              \
+			  short: DebugFormatArgumentSignedNumber32,                        \
+			  unsigned short: DebugFormatArgumentUnsignedNumber32,             \
+			  int: DebugFormatArgumentSignedNumber32,                          \
+			  unsigned int: DebugFormatArgumentUnsignedNumber32,               \
+			  signed long long: DebugFormatArgumentSignedNumber64,             \
+			  unsigned long long: DebugFormatArgumentUnsignedNumber64,         \
+			  char *: DebugFormatArgumentCString,                              \
+			  default: DebugFormatArgumentPointer)                             \
 		}
 
 /**
diff --git a/sdk/include/debug.hh b/sdk/include/debug.hh
index af0732e..6c6bfa7 100644
--- a/sdk/include/debug.hh
+++ b/sdk/include/debug.hh
@@ -27,11 +27,10 @@
 
 	/// Concept for something that can be lazily called to produce a bool.
 	template<typename T>
-	concept LazyAssertion = requires(T v)
-	{
+	concept LazyAssertion = requires(T v) {
 		{
 			v()
-			} -> IsBool;
+		} -> IsBool;
 	};
 
 	template<typename T>
@@ -80,6 +79,44 @@
 	 * Write a 64-bit signed integer.
 	 */
 	virtual void write(int64_t) = 0;
+	/**
+	 * Write a single byte as hex with no leading 0x.
+	 */
+	virtual void write_hex_byte(uint8_t) = 0;
+	/**
+	 * Write an integer as hex.
+	 */
+	template<typename T>
+	__always_inline void write_hex(T x)
+	    requires(std::integral<T>)
+	{
+		if constexpr (sizeof(T) <= 4)
+		{
+			write(static_cast<uint32_t>(x));
+		}
+		else
+		{
+			write(static_cast<uint64_t>(x));
+		}
+	}
+	/**
+	 * Write an integer as decimal.
+	 */
+	template<typename T>
+	__always_inline void write_decimal(T x)
+	    requires(std::integral<T>)
+	{
+		if constexpr (sizeof(T) <= 4)
+		{
+			write(
+			  static_cast<int32_t>(static_cast<std::make_unsigned_t<T>>(x)));
+		}
+		else
+		{
+			write(
+			  static_cast<int64_t>(static_cast<std::make_unsigned_t<T>>(x)));
+		}
+	}
 };
 
 /**
@@ -87,12 +124,12 @@
  * magic_enum to provide a string and then a numeric value.
  */
 template<typename T>
-void debug_enum_helper(uintptr_t    value,
-                       DebugWriter &writer) requires DebugConcepts::IsEnum<T>
+void debug_enum_helper(uintptr_t value, DebugWriter &writer)
+    requires DebugConcepts::IsEnum<T>
 {
 	writer.write(magic_enum::enum_name<T>(static_cast<T>(value)));
 	writer.write('(');
-	writer.write(uint32_t(value));
+	writer.write(static_cast<uint32_t>(value));
 	writer.write(')');
 }
 
@@ -413,7 +450,7 @@
  */
 template<typename... Args>
 __always_inline inline void
-make_debug_arguments_list(DebugFormatArgument *arguments, Args... args)
+make_debug_arguments_list(DebugFormatArgument *arguments, Args &...args)
 {
 	if constexpr (sizeof...(Args) > 0)
 	{
@@ -654,7 +691,8 @@
 			 * Constructor, performs the assertion check.
 			 */
 			template<typename T>
-			requires DebugConcepts::IsBool<T> __always_inline
+			    requires DebugConcepts::IsBool<T>
+			__always_inline
 			Assert(T           condition,
 			       const char *fmt,
 			       Args... args,
@@ -685,7 +723,8 @@
 			 * where the assertion condition has side effects.
 			 */
 			template<typename T>
-			requires DebugConcepts::LazyAssertion<T> __always_inline
+			    requires DebugConcepts::LazyAssertion<T>
+			__always_inline
 			Assert(T         &&condition,
 			       const char *fmt,
 			       Args... args,
@@ -715,6 +754,27 @@
 		Invariant(T, const char *, Ts &&...) -> Invariant<Ts...>;
 
 		/**
+		 * Overt wrapper function around Invariant.  Sometimes template
+		 * deduction guides just don't cut it.  At the cost of a
+		 * std::make_tuple at the call site, we can still take advantage
+		 * of much of the machinery here.
+		 */
+		template<typename T, typename... Args>
+		__always_inline static void
+		invariant(T                   condition,
+		          const char         *fmt,
+		          std::tuple<Args...> args = std::make_tuple(),
+		          SourceLocation      loc  = SourceLocation::current())
+		{
+			std::apply(
+			  [&](Args... iargs) {
+				  Invariant<Args...>(
+				    condition, fmt, std::forward<Args>(iargs)..., loc);
+			  },
+			  args);
+		}
+
+		/**
 		 * Deduction guide, allows `Assert` to be used as if it were a
 		 * function.
 		 */
@@ -727,6 +787,27 @@
 		 */
 		template<typename... Ts>
 		Assert(auto, const char *, Ts &&...) -> Assert<Ts...>;
+
+		/**
+		 * Overt wrapper function around Assert.  Sometimes template
+		 * deduction guides just don't cut it.  At the cost of a
+		 * std::make_tuple at the call site, we can still take advantage
+		 * of much of the machinery here.
+		 */
+		template<typename T, typename... Args>
+		__always_inline static void
+		assertion(T                   condition,
+		          const char         *fmt,
+		          std::tuple<Args...> args = std::make_tuple(),
+		          SourceLocation      loc  = SourceLocation::current())
+		{
+			std::apply(
+			  [&](Args... iargs) {
+				  Assert<Args...>(
+				    condition, fmt, std::forward<Args>(iargs)..., loc);
+			  },
+			  args);
+		}
 	};
 
 	enum class StackCheckMode
diff --git a/sdk/include/ds/pointer.h b/sdk/include/ds/pointer.h
index 598c726..64126a2 100644
--- a/sdk/include/ds/pointer.h
+++ b/sdk/include/ds/pointer.h
@@ -47,40 +47,39 @@
 		 */
 		template<typename P, typename T>
 		concept Proxies = std::same_as<T, typename P::Type> &&
-		  requires(P &proxy, P &proxy2, T *ptr)
-		{
-			/* Probe for operator=(T*) */
-			{
-				proxy = ptr
-				} -> std::same_as<P &>;
+		                  requires(P &proxy, P &proxy2, T *ptr) {
+			                  /* Probe for operator=(T*) */
+			                  {
+				                  proxy = ptr
+			                  } -> std::same_as<P &>;
 
-			/* Probe for operator T*() */
-			{
-				ptr == proxy
-				} -> std::same_as<bool>;
+			                  /* Probe for operator T*() */
+			                  {
+				                  ptr == proxy
+			                  } -> std::same_as<bool>;
 
-			/* TODO: How to probe for operator-> ? */
+			                  /* TODO: How to probe for operator-> ? */
 
-			/* Probe for operator==(T*) */
-			{
-				proxy == ptr
-				} -> std::same_as<bool>;
+			                  /* Probe for operator==(T*) */
+			                  {
+				                  proxy == ptr
+			                  } -> std::same_as<bool>;
 
-			/* Probe for operator==(P&) */
-			{
-				proxy == proxy2
-				} -> std::same_as<bool>;
+			                  /* Probe for operator==(P&) */
+			                  {
+				                  proxy == proxy2
+			                  } -> std::same_as<bool>;
 
-			/* Probe for operator<=>(T*) */
-			{
-				proxy <=> ptr
-				} -> std::same_as<std::strong_ordering>;
+			                  /* Probe for operator<=>(T*) */
+			                  {
+				                  proxy <=> ptr
+			                  } -> std::same_as<std::strong_ordering>;
 
-			/* Probe for operator<=>(P) */
-			{
-				proxy <=> proxy2
-				} -> std::same_as<std::strong_ordering>;
-		};
+			                  /* Probe for operator<=>(P) */
+			                  {
+				                  proxy <=> proxy2
+			                  } -> std::same_as<std::strong_ordering>;
+		                  };
 
 		/**
 		 * Pointer references are pointer proxies, shockingly enough.
diff --git a/sdk/include/ds/xoroshiro.h b/sdk/include/ds/xoroshiro.h
index 500851f..b05baa4 100644
--- a/sdk/include/ds/xoroshiro.h
+++ b/sdk/include/ds/xoroshiro.h
@@ -66,7 +66,7 @@
 				{
 					for (int b = 0; b < 64; b++)
 					{
-						if (Jump[i] & uint64_t(1) << b)
+						if (Jump[i] & static_cast<uint64_t>(1) << b)
 						{
 							s0 ^= x;
 							s1 ^= y;
@@ -124,7 +124,8 @@
 			/**
 			 * Jump.  If supported, this is equivalent to 2^64 calls to next().
 			 */
-			void jump() requires(Jump0 != 0) && (Jump1 != 0)
+			void jump()
+			    requires(Jump0 != 0) && (Jump1 != 0)
 			{
 				jump(Jump0, Jump1);
 			}
@@ -133,7 +134,8 @@
 			 * Jump a *really* long way.  If supported, this is equivalent to
 			 * 2^96 calls to next().
 			 */
-			void long_jump() requires(LongJump0 != 0) && (LongJump1 != 0)
+			void long_jump()
+			    requires(LongJump0 != 0) && (LongJump1 != 0)
 			{
 				jump(LongJump0, LongJump1);
 			}
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/function_wrapper.hh b/sdk/include/function_wrapper.hh
index 50ca248..81ec439 100644
--- a/sdk/include/function_wrapper.hh
+++ b/sdk/include/function_wrapper.hh
@@ -83,8 +83,8 @@
 	 * This is a non-owning reference, delete its copy and move
 	 * constructors to avoid accidental copies.
 	 */
-	FunctionWrapper(FunctionWrapper &)  = delete;
-	FunctionWrapper(FunctionWrapper &&) = delete;
+	FunctionWrapper(FunctionWrapper &)             = delete;
+	FunctionWrapper(FunctionWrapper &&)            = delete;
 	FunctionWrapper &operator=(FunctionWrapper &&) = delete;
 
 	/**
diff --git a/sdk/include/futex.h b/sdk/include/futex.h
index 482919a..42a9676 100644
--- a/sdk/include/futex.h
+++ b/sdk/include/futex.h
@@ -6,15 +6,17 @@
 #include <stdint.h>
 #include <timeout.h>
 
-enum [[clang::flag_enum]] FutexWaitFlags{
-  /// No flags
-  FutexNone = 0,
-  /**
-   * This futex uses priority inheritance.  The low 16 bits of the futex word
-   * are assumed to hold the thread ID of the thread that currently holds the
-   * lock.
-   */
-  FutexPriorityInheritance = (1 << 0)};
+enum [[clang::flag_enum]] FutexWaitFlags
+{
+	/// No flags
+	FutexNone = 0,
+	/**
+	 * This futex uses priority inheritance.  The low 16 bits of the futex word
+	 * are assumed to hold the thread ID of the thread that currently holds the
+	 * lock.
+	 */
+	FutexPriorityInheritance = (1 << 0)
+};
 
 /**
  * Compare the value at `address` to `expected` and, if they match, sleep the
@@ -36,7 +38,7 @@
  *  - `-EINVAL` if the arguments are invalid.
  *  - `-ETIMEOUT` if the timeout expires.
  */
-[[cheri::interrupt_state(disabled)]] int __cheri_compartment("sched")
+[[cheri::interrupt_state(disabled)]] int __cheri_compartment("scheduler")
   futex_timed_wait(Timeout        *ticks,
                    const uint32_t *address,
                    uint32_t        expected,
@@ -70,5 +72,5 @@
  * The return value for a successful call is the number of threads that were
  * woken.  `-EINVAL` is returned for invalid arguments.
  */
-[[cheri::interrupt_state(disabled)]] int __cheri_compartment("sched")
+[[cheri::interrupt_state(disabled)]] int __cheri_compartment("scheduler")
   futex_wake(uint32_t *address, uint32_t count);
diff --git a/sdk/include/interrupt.h b/sdk/include/interrupt.h
index 7419c60..bcdd8e6 100644
--- a/sdk/include/interrupt.h
+++ b/sdk/include/interrupt.h
@@ -72,7 +72,7 @@
  */
 #define DECLARE_INTERRUPT_CAPABILITY(name)                                     \
 	DECLARE_STATIC_SEALED_VALUE(                                               \
-	  struct InterruptCapabilityState, sched, InterruptKey, name);
+	  struct InterruptCapabilityState, scheduler, InterruptKey, name);
 
 /**
  * Helper macro to define an interrupt capability.  The three arguments after
@@ -82,7 +82,7 @@
  */
 #define DEFINE_INTERRUPT_CAPABILITY(name, number, mayWait, mayComplete)        \
 	DEFINE_STATIC_SEALED_VALUE(struct InterruptCapabilityState,                \
-	                           sched,                                          \
+	                           scheduler,                                      \
 	                           InterruptKey,                                   \
 	                           name,                                           \
 	                           number,                                         \
@@ -108,7 +108,7 @@
  *
  * Returns `nullptr` on failure.
  */
-__cheri_compartment("sched") const uint32_t *interrupt_futex_get(
+__cheri_compartment("scheduler") const uint32_t *interrupt_futex_get(
   struct SObjStruct *);
 
 /**
@@ -120,4 +120,4 @@
  * Returns 0 on success or `-EPERM` if the argument does not authorise this
  * operation.
  */
-__cheri_compartment("sched") int interrupt_complete(struct SObjStruct *);
+__cheri_compartment("scheduler") int interrupt_complete(struct SObjStruct *);
diff --git a/sdk/include/locks.h b/sdk/include/locks.h
index c33474a..ef8a718 100644
--- a/sdk/include/locks.h
+++ b/sdk/include/locks.h
@@ -95,7 +95,7 @@
  *
  * Note: if the object is deallocated while trying to acquire the lock, then
  * this will fault.  In many cases, this is called at a compartment boundary
- * and so this is fine.  If it is not acceptable, use `heap_claim_fast` to
+ * and so this is fine.  If it is not acceptable, use `heap_claim_ephemeral` to
  * ensure that the object remains live until after the call.
  */
 int __cheri_libcall
diff --git a/sdk/include/locks.hh b/sdk/include/locks.hh
index 6819f84..4b15761 100644
--- a/sdk/include/locks.hh
+++ b/sdk/include/locks.hh
@@ -102,7 +102,8 @@
 	 * documentation of `flaglock_priority_inheriting_get_owner_thread_id`
 	 * for more information.
 	 */
-	__always_inline uint16_t get_owner_thread_id() requires(IsPriorityInherited)
+	__always_inline uint16_t get_owner_thread_id()
+	    requires(IsPriorityInherited)
 	{
 		return flaglock_priority_inheriting_get_owner_thread_id(&state);
 	}
@@ -235,18 +236,20 @@
 using FlagLockPriorityInherited = FlagLockGeneric<true>;
 
 template<typename T>
-concept Lockable = requires(T l)
-{
-	{l.lock()};
-	{l.unlock()};
+concept Lockable = requires(T l) {
+	{
+		l.lock()
+	};
+	{
+		l.unlock()
+	};
 };
 
 template<typename T>
-concept TryLockable = Lockable<T> && requires(T l, Timeout *t)
-{
+concept TryLockable = Lockable<T> && requires(T l, Timeout *t) {
 	{
 		l.try_lock(t)
-		} -> std::same_as<bool>;
+	} -> std::same_as<bool>;
 };
 
 static_assert(TryLockable<NoLock>);
@@ -275,8 +278,8 @@
 	}
 
 	/// Constructor, attempts to acquire the lock with a timeout.
-	[[nodiscard]] explicit LockGuard(Lock &lock, Timeout *timeout) requires(
-	  TryLockable<Lock>)
+	[[nodiscard]] explicit LockGuard(Lock &lock, Timeout *timeout)
+	    requires(TryLockable<Lock>)
 	  : wrappedLock(&lock), isOwned(false)
 	{
 		try_lock(timeout);
@@ -326,7 +329,8 @@
 	 * it with the specified timeout. This must be called with the lock
 	 * unlocked.  Returns true if the lock has been acquired, false otherwise.
 	 */
-	bool try_lock(Timeout *timeout) requires(TryLockable<Lock>)
+	bool try_lock(Timeout *timeout)
+	    requires(TryLockable<Lock>)
 	{
 		LockDebug::Assert(!isOwned, "Trying to lock an already-locked lock");
 		isOwned = wrappedLock->try_lock(timeout);
diff --git a/sdk/include/multiwaiter.h b/sdk/include/multiwaiter.h
index 418228c..8b22817 100644
--- a/sdk/include/multiwaiter.h
+++ b/sdk/include/multiwaiter.h
@@ -34,52 +34,24 @@
 #include <timeout.h>
 
 /**
- * The kind of event source to wait for in a multiwaiter.
- */
-enum EventWaiterKind
-{
-	/// Event source is an event channel.
-	EventWaiterEventChannel,
-	/// Event source is a futex.
-	EventWaiterFutex
-};
-
-enum [[clang::flag_enum]] EventWaiterEventChannelFlags{
-  /// Automatically clear the bits we waited on.
-  EventWaiterEventChannelClearOnExit = (1 << 24),
-  /// Notify when all bits were set.
-  EventWaiterEventChannelWaitAll = (1 << 26)};
-
-/**
  * Structure describing a change to the set of managed event sources for an
  * event waiter.
  */
 struct EventWaiterSource
 {
 	/**
-	 * A pointer to the event source.  For a futex, this should be the memory
-	 * address.  For other sources, it should be a pointer to an object of the
-	 * corresponding type.
+	 * A pointer to the event source.  This is the futex that is monitored for
+	 * the multiwaiter.
 	 */
 	void *eventSource;
 	/**
-	 * The kind of the event source.  This must match the pointer type.
-	 */
-	enum EventWaiterKind kind;
-	/**
-	 * Event-specific configuration.  This field is modified during the wait
-	 * call.  The interpretation of this depends on `kind`:
+	 * Event value.  This field is modified during the wait
+	 * call.
 	 *
-	 * - `EventWaiterEventChannel`: The low 24 bits contain the bits to
-	 *   monitor, the top bit indicates whether this event is triggered if all
-	 *   of the bits are set (true) or some of them (false).  On return, this
-	 *   contains the bits that have been set during the call.
-	 * - `EventWaiterFutex`: This indicates the value to compare the futex word
-	 *   against.  If they mismatch, the event fires immediately.
+	 * This indicates the value to compare the futex word against.  If they
+	 * mismatch, the event fires immediately.
 	 *
-	 * If waiting for a futex, signal the event immediately if the value
-	 * does not match.  On return, this is set to 1 if the futex is
-	 * signaled, 0 otherwise.
+	 * On return, this is set to 1 if the futex is signaled, 0 otherwise.
 	 */
 	uint32_t value;
 };
@@ -94,7 +66,7 @@
  * Create a multiwaiter object.  This is a stateful object that can wait on at
  * most `maxItems` event sources.
  */
-[[cheri::interrupt_state(disabled)]] int __cheri_compartment("sched")
+[[cheri::interrupt_state(disabled)]] int __cheri_compartment("scheduler")
   multiwaiter_create(Timeout             *timeout,
                      struct SObjStruct   *heapCapability,
                      struct MultiWaiter **ret,
@@ -103,7 +75,7 @@
 /**
  * Destroy a multiwaiter object.
  */
-[[cheri::interrupt_state(disabled)]] int __cheri_compartment("sched")
+[[cheri::interrupt_state(disabled)]] int __cheri_compartment("scheduler")
   multiwaiter_delete(struct SObjStruct *heapCapability, struct MultiWaiter *mw);
 
 /**
@@ -118,7 +90,7 @@
  *  - If the timeout is reached without any events being triggered then this
  *    returns -ETIMEOUT.
  */
-[[cheri::interrupt_state(disabled)]] int __cheri_compartment("sched")
+[[cheri::interrupt_state(disabled)]] int __cheri_compartment("scheduler")
   multiwaiter_wait(Timeout                  *timeout,
                    struct MultiWaiter       *waiter,
                    struct EventWaiterSource *events,
diff --git a/sdk/include/platform/arty-a7/platform-ethernet.hh b/sdk/include/platform/arty-a7/platform-ethernet.hh
index dc5392a..d568178 100644
--- a/sdk/include/platform/arty-a7/platform-ethernet.hh
+++ b/sdk/include/platform/arty-a7/platform-ethernet.hh
@@ -420,10 +420,10 @@
 	mdio_write(uint8_t phyAddress, PHYRegister registerAddress, uint16_t data)
 	{
 		mdio_wait_for_ready();
-		auto    &mdioAddress = mmio_register<RegisterOffset::MDIOAddress>();
-		auto    &mdioWrite   = mmio_register<RegisterOffset::MDIODataWrite>();
-		uint32_t writeCommand =
-		  (0 << 10) | (phyAddress << 5) | uint32_t(registerAddress);
+		auto    &mdioAddress  = mmio_register<RegisterOffset::MDIOAddress>();
+		auto    &mdioWrite    = mmio_register<RegisterOffset::MDIODataWrite>();
+		uint32_t writeCommand = (0 << 10) | (phyAddress << 5) |
+		                        static_cast<uint32_t>(registerAddress);
 		mdioAddress = writeCommand;
 		mdioWrite   = data;
 		mdio_start_transaction();
@@ -437,7 +437,7 @@
 		mdio_wait_for_ready();
 		auto    &mdioAddress = mmio_register<RegisterOffset::MDIOAddress>();
 		uint32_t readCommand =
-		  (1 << 10) | (phyAddress << 5) | uint8_t(registerAddress);
+		  (1 << 10) | (phyAddress << 5) | static_cast<uint8_t>(registerAddress);
 		mdioAddress = readCommand;
 		mdio_start_transaction();
 		mdio_wait_for_ready();
@@ -755,7 +755,7 @@
 		// does not check the pointer which is coming from external
 		// untrusted components.
 		Timeout t{10};
-		if ((heap_claim_fast(&t, buffer) < 0) ||
+		if ((heap_claim_ephemeral(&t, buffer) < 0) ||
 		    (!CHERI::check_pointer<CHERI::PermissionSet{
 		       CHERI::Permission::Load}>(buffer, length)))
 		{
diff --git a/sdk/include/platform/concepts/entropy.h b/sdk/include/platform/concepts/entropy.h
index e0933a7..ff6218c 100644
--- a/sdk/include/platform/concepts/entropy.h
+++ b/sdk/include/platform/concepts/entropy.h
@@ -7,15 +7,14 @@
  * Concept for an Ethernet adaptor.
  */
 template<typename T>
-concept IsEntropySource = requires(T source)
-{
+concept IsEntropySource = requires(T source) {
 	/**
 	 * Must export a flag indicating whether this is a cryptographically
 	 * secure random number.
 	 */
 	{
 		T::IsSecure
-		} -> std::convertible_to<const bool>;
+	} -> std::convertible_to<const bool>;
 
 	/**
 	 * Must return a random number.  All bits of the value type are assumed to
@@ -24,7 +23,7 @@
 	 */
 	{
 		source()
-		} -> std::same_as<typename T::ValueType>;
+	} -> std::same_as<typename T::ValueType>;
 
 	/**
 	 * Must provide a method that reseeds the entropy source.  If this is a
@@ -32,5 +31,7 @@
 	 * may be an empty function.  There are no constraints on the return type
 	 * of this.
 	 */
-	{source.reseed()};
+	{
+		source.reseed()
+	};
 };
diff --git a/sdk/include/platform/concepts/ethernet.hh b/sdk/include/platform/concepts/ethernet.hh
index 700bb82..b5421bf 100644
--- a/sdk/include/platform/concepts/ethernet.hh
+++ b/sdk/include/platform/concepts/ethernet.hh
@@ -11,17 +11,16 @@
  * This is not required to be copyable.
  */
 template<typename T>
-concept ReceivedEthernetFrame = requires(T frame)
-{
+concept ReceivedEthernetFrame = requires(T frame) {
 	{
 		frame->length
-		} -> std::convertible_to<uint16_t>;
+	} -> std::convertible_to<uint16_t>;
 	{
 		frame->buffer
-		} -> std::convertible_to<const uint8_t *>;
+	} -> std::convertible_to<const uint8_t *>;
 	{
 		frame->buffer
-		} -> std::convertible_to<bool>;
+	} -> std::convertible_to<bool>;
 };
 
 /**
@@ -31,14 +30,13 @@
 concept EthernetAdaptor = requires(T                      adaptor,
                                    const uint8_t         *buffer,
                                    uint16_t               length,
-                                   std::array<uint8_t, 6> macAddress)
-{
+                                   std::array<uint8_t, 6> macAddress) {
 	/**
 	 * The default MAC address for this adaptor.  Must return a 6-byte array.
 	 */
 	{
 		T::mac_address_default()
-		} -> std::same_as<std::array<uint8_t, 6>>;
+	} -> std::same_as<std::array<uint8_t, 6>>;
 	/**
 	 * Is the default MAC address unique?  If the device (e.g. soft MAC)
 	 * doesn't have its own hardware MAC address then callers may prefer to
@@ -46,22 +44,26 @@
 	 */
 	{
 		T::has_unique_mac_address()
-		} -> std::convertible_to<bool>;
+	} -> std::convertible_to<bool>;
 	/**
 	 * Set the MAC address of this adaptor.
 	 */
-	{adaptor.mac_address_set(macAddress)};
+	{
+		adaptor.mac_address_set(macAddress)
+	};
 	/**
 	 * Set the MAC address of this adaptor to the default value.
 	 */
-	{adaptor.mac_address_set()};
+	{
+		adaptor.mac_address_set()
+	};
 
 	/**
 	 * Check if PHY link is up.
 	 */
 	{
 		adaptor.phy_link_status()
-		} -> std::convertible_to<bool>;
+	} -> std::convertible_to<bool>;
 
 	/**
 	 * Receive a frame.  Returns an optional value (convertible to bool) that
@@ -70,7 +72,7 @@
 	 */
 	{
 		adaptor.receive_frame()
-		} -> ReceivedEthernetFrame;
+	} -> ReceivedEthernetFrame;
 
 	/**
 	 * Send a frame identified by a base and length.  Returns true if the frame
@@ -85,7 +87,7 @@
 		  buffer, length, [](const uint8_t *buffer, uint16_t length) {
 			  return true;
 		  })
-		} -> std::same_as<bool>;
+	} -> std::same_as<bool>;
 
 	/**
 	 * Read the current interrupt counter for receive interrupts.  If this
@@ -94,7 +96,7 @@
 	 */
 	{
 		adaptor.receive_interrupt_value()
-		} -> std::same_as<uint32_t>;
+	} -> std::same_as<uint32_t>;
 
 	/**
 	 * Called after `receive_frame` fails to return new frames to block until a
@@ -105,5 +107,5 @@
 	 */
 	{
 		adaptor.receive_interrupt_complete(nullptr, 0)
-		} -> std::same_as<int>;
+	} -> std::same_as<int>;
 };
diff --git a/sdk/include/platform/flute/platform-early_boot.inc b/sdk/include/platform/flute/platform-early_boot.inc
deleted file mode 100644
index f8532a5..0000000
--- a/sdk/include/platform/flute/platform-early_boot.inc
+++ /dev/null
@@ -1,12 +0,0 @@
-	// Ugly hack. For some reason if I don't give this first load, subsequent mem
-	// ops will trap on Flute-TCM.
-	li				a3, 0x80000000
-	cspecialr		ca4, mtdc
-	csetaddr		ca4, ca4, a3
-	clc				c0, 0(ca4)
-	// The shadow memory may not be zeroed, ensure it is before we start or
-	// random capability loads will fail.
-	li				a0, FLUTE_SHADOW_BASE
-	csetaddr		ca0, ca4, a0
-	li				a1, FLUTE_SHADOW_BASE + FLUTE_SHADOW_SIZE
-	cjal			.Lfill_block
diff --git a/sdk/include/platform/flute/platform-hardware_revoker.hh b/sdk/include/platform/flute/platform-hardware_revoker.hh
deleted file mode 100644
index 759db15..0000000
--- a/sdk/include/platform/flute/platform-hardware_revoker.hh
+++ /dev/null
@@ -1,105 +0,0 @@
-// Copyright Microsoft and CHERIoT Contributors.
-// SPDX-License-Identifier: MIT
-
-#pragma once
-
-#include <cdefs.h>
-#include <compartment-macros.h>
-#include <riscvreg.h>
-#include <stddef.h>
-#include <stdint.h>
-
-namespace Flute
-{
-	template<typename WordT, size_t TCMBaseAddr>
-	class HardwareRevoker
-	{
-		private:
-		// layout of the shadow space control registers
-		struct ShadowCtrl
-		{
-			uint32_t base;
-			uint32_t pad0;
-			uint32_t top;
-			uint32_t pad1;
-			uint32_t epoch;
-			uint32_t pad2;
-			uint32_t go;
-			uint32_t pad4;
-		};
-		static_assert(offsetof(ShadowCtrl, epoch) == 16);
-		static_assert(offsetof(ShadowCtrl, go) == 24);
-
-		volatile ShadowCtrl *shadowCtrl;
-
-		public:
-		/**
-		 * Currently the only hardware revoker implementation is async which
-		 * sweeps memory in the background.
-		 */
-		static constexpr bool IsAsynchronous = true;
-
-		/**
-		 * Initialise a revoker instance.
-		 */
-		void init()
-		{
-			/**
-			 * These two symbols mark the region that needs revocation.  We
-			 * revoke capabilities everywhere from the start of compartment
-			 * globals to the end of the heap.
-			 */
-			extern char __compart_cgps, __export_mem_heap_end;
-
-			auto base        = LA_ABS(__compart_cgps);
-			auto top         = LA_ABS(__export_mem_heap_end);
-			shadowCtrl       = MMIO_CAPABILITY(ShadowCtrl, shadowctrl);
-			shadowCtrl->base = base;
-			shadowCtrl->top  = top;
-			// Clang tidy is checking headers as stand-alone compilation units
-			// and so doesn't know what Debug is defined to.
-#ifndef CLANG_TIDY
-			Debug::Invariant(base < top,
-			                 "Memory map has unexpected layout, base {} is "
-			                 "expected to be below top {}",
-			                 base,
-			                 top);
-#endif
-		}
-
-		/**
-		 * Returns the revocation epoch.  This is the number of revocations
-		 * that have started.
-		 */
-		uint32_t system_epoch_get()
-		{
-			asm volatile("" ::: "memory");
-			return shadowCtrl->epoch;
-		}
-
-		/**
-		 * Queries whether the specified revocation epoch has finished.
-		 */
-		template<bool AllowPartial = false>
-		uint32_t has_revocation_finished_for_epoch(uint32_t epoch)
-		{
-			asm volatile("" ::: "memory");
-			if (AllowPartial)
-			{
-				return shadowCtrl->epoch > epoch;
-			}
-			return shadowCtrl->epoch - epoch >= (2 + (epoch & 1));
-		}
-
-		// Start a revocation.
-		void system_bg_revoker_kick()
-		{
-			asm volatile("" ::: "memory");
-			shadowCtrl->go = 1;
-			asm volatile("" ::: "memory");
-		}
-	};
-} // namespace Flute
-
-template<typename WordT, size_t TCMBaseAddr>
-using HardwareRevoker = Flute::HardwareRevoker<WordT, TCMBaseAddr>;
diff --git a/sdk/include/platform/generic-riscv/platform-timer.hh b/sdk/include/platform/generic-riscv/platform-timer.hh
index 7b0d1f6..6fce600 100644
--- a/sdk/include/platform/generic-riscv/platform-timer.hh
+++ b/sdk/include/platform/generic-riscv/platform-timer.hh
@@ -52,7 +52,7 @@
 			timeHigh = *timerHigh;
 			timeLow  = *pmtimer;
 		} while (timeHigh != *timerHigh);
-		return (uint64_t(timeHigh) << 32) | timeLow;
+		return (static_cast<uint64_t>(timeHigh) << 32) | timeLow;
 	}
 
 	/**
@@ -80,6 +80,18 @@
 		*pmtimercmphigh = nextTime >> 32;
 	}
 
+	/**
+	 * Returns the time at which the next timer is scheduled.
+	 */
+	static uint64_t next()
+	{
+		volatile uint32_t *pmtimercmphigh = pmtimercmp + 1;
+		uint64_t           nextTimer      = *pmtimercmphigh;
+		nextTimer <<= 32;
+		nextTimer |= *pmtimercmp;
+		return nextTimer;
+	}
+
 	static void clear()
 	{
 		volatile uint32_t *pmtimercmphigh = pmtimercmp + 1;
diff --git a/sdk/include/platform/generic-riscv/platform-uart.hh b/sdk/include/platform/generic-riscv/platform-uart.hh
index 4b1c916..705a6fb 100644
--- a/sdk/include/platform/generic-riscv/platform-uart.hh
+++ b/sdk/include/platform/generic-riscv/platform-uart.hh
@@ -10,19 +10,22 @@
  * Concept for checking that a UART driver exposes the right interface.
  */
 template<typename T>
-concept IsUart = requires(volatile T *v, uint8_t byte)
-{
-	{v->init()};
+concept IsUart = requires(volatile T *v, uint8_t byte) {
+	{
+		v->init()
+	};
 	{
 		v->can_write()
-		} -> std::same_as<bool>;
+	} -> std::same_as<bool>;
 	{
 		v->can_read()
-		} -> std::same_as<bool>;
+	} -> std::same_as<bool>;
 	{
 		v->blocking_read()
-		} -> std::same_as<uint8_t>;
-	{v->blocking_write(byte)};
+	} -> std::same_as<uint8_t>;
+	{
+		v->blocking_write(byte)
+	};
 };
 
 /**
diff --git a/sdk/include/platform/ibex/platform-hardware_revoker.hh b/sdk/include/platform/ibex/platform-hardware_revoker.hh
index c039cce..6de65c7 100644
--- a/sdk/include/platform/ibex/platform-hardware_revoker.hh
+++ b/sdk/include/platform/ibex/platform-hardware_revoker.hh
@@ -87,9 +87,9 @@
 			 * revoke capabilities everywhere from the start of compartment
 			 * globals to the end of the heap.
 			 */
-			extern char __compart_cgps, __export_mem_heap_end;
+			extern char __revoker_scan_start, __export_mem_heap_end;
 
-			auto  base   = LA_ABS(__compart_cgps);
+			auto  base   = LA_ABS(__revoker_scan_start);
 			auto  top    = LA_ABS(__export_mem_heap_end);
 			auto &device = revoker_device();
 			device.base  = base;
@@ -121,7 +121,11 @@
 		}
 
 		/**
-		 * Queries whether the specified revocation epoch has finished.
+		 * Queries whether the specified revocation epoch has finished, or,
+		 * if `AllowPartial` is true, that it has (at least) started.
+		 *
+		 * `epoch` must be even, as memory leaves quarantine only when
+		 * revocation is not in progress.
 		 */
 		template<bool AllowPartial = false>
 		uint32_t has_revocation_finished_for_epoch(uint32_t epoch)
@@ -177,7 +181,7 @@
 				// futex word with respect to the read of the revocation epoch.
 				__c11_atomic_signal_fence(__ATOMIC_SEQ_CST);
 				// If the requested epoch has finished, return success.
-				if (has_revocation_finished_for_epoch<true>(epoch))
+				if (has_revocation_finished_for_epoch(epoch))
 				{
 					return true;
 				}
@@ -186,7 +190,7 @@
 				// There is a possible race: if the revocation pass finished
 				// before we requested the interrupt, we won't get the
 				// interrupt.  Check again before we wait.
-				if (has_revocation_finished_for_epoch<true>(epoch))
+				if (has_revocation_finished_for_epoch(epoch))
 				{
 					return true;
 				}
diff --git a/sdk/include/platform/sunburst/platform-ethernet.hh b/sdk/include/platform/sunburst/platform-ethernet.hh
index 0e9b3a7..8e69319 100644
--- a/sdk/include/platform/sunburst/platform-ethernet.hh
+++ b/sdk/include/platform/sunburst/platform-ethernet.hh
@@ -9,7 +9,6 @@
 #include <locks.hh>
 #include <optional>
 #include <platform/concepts/ethernet.hh>
-#include <platform/sunburst/platform-gpio.hh>
 #include <platform/sunburst/platform-spi.hh>
 #include <thread.h>
 #include <type_traits>
@@ -57,15 +56,6 @@
 	using Capability = CHERI::Capability<T>;
 
 	/**
-	 * GPIO output pins to be used
-	 */
-	enum class GpioPin : uint8_t
-	{
-		EthernetChipSelect = 13,
-		EthernetReset      = 14,
-	};
-
-	/**
 	 * SPI commands
 	 */
 	enum class SpiCommand : uint8_t
@@ -143,161 +133,171 @@
 	/**
 	 * Flag bits of the TransmitControl register.
 	 */
-	enum [[clang::flag_enum]] TransmitControl : uint16_t{
-	  TransmitEnable                 = 1 << 0,
-	  TransmitCrcEnable              = 1 << 1,
-	  TransmitPaddingEnable          = 1 << 2,
-	  TransmitFlowControlEnable      = 1 << 3,
-	  FlushTransmitQueue             = 1 << 4,
-	  TransmitChecksumGenerationIp   = 1 << 5,
-	  TransmitChecksumGenerationTcp  = 1 << 6,
-	  TransmitChecksumGenerationIcmp = 1 << 9,
+	enum [[clang::flag_enum]] TransmitControl : uint16_t
+	{
+		TransmitEnable                 = 1 << 0,
+		TransmitCrcEnable              = 1 << 1,
+		TransmitPaddingEnable          = 1 << 2,
+		TransmitFlowControlEnable      = 1 << 3,
+		FlushTransmitQueue             = 1 << 4,
+		TransmitChecksumGenerationIp   = 1 << 5,
+		TransmitChecksumGenerationTcp  = 1 << 6,
+		TransmitChecksumGenerationIcmp = 1 << 9,
 	};
 
 	/**
 	 * Flag bits of the ReceiveControl1 register.
 	 */
-	enum [[clang::flag_enum]] ReceiveControl1 : uint16_t{
-	  ReceiveEnable                                        = 1 << 0,
-	  ReceiveInverseFilter                                 = 1 << 1,
-	  ReceiveAllEnable                                     = 1 << 4,
-	  ReceiveUnicastEnable                                 = 1 << 5,
-	  ReceiveMulticastEnable                               = 1 << 6,
-	  ReceiveBroadcastEnable                               = 1 << 7,
-	  ReceiveMulticastAddressFilteringWithMacAddressEnable = 1 << 8,
-	  ReceiveErrorFrameEnable                              = 1 << 9,
-	  ReceiveFlowControlEnable                             = 1 << 10,
-	  ReceivePhysicalAddressFilteringWithMacAddressEnable  = 1 << 11,
-	  ReceiveIpFrameChecksumCheckEnable                    = 1 << 12,
-	  ReceiveTcpFrameChecksumCheckEnable                   = 1 << 13,
-	  ReceiveUdpFrameChecksumCheckEnable                   = 1 << 14,
-	  FlushReceiveQueue                                    = 1 << 15,
+	enum [[clang::flag_enum]] ReceiveControl1 : uint16_t
+	{
+		ReceiveEnable                                        = 1 << 0,
+		ReceiveInverseFilter                                 = 1 << 1,
+		ReceiveAllEnable                                     = 1 << 4,
+		ReceiveUnicastEnable                                 = 1 << 5,
+		ReceiveMulticastEnable                               = 1 << 6,
+		ReceiveBroadcastEnable                               = 1 << 7,
+		ReceiveMulticastAddressFilteringWithMacAddressEnable = 1 << 8,
+		ReceiveErrorFrameEnable                              = 1 << 9,
+		ReceiveFlowControlEnable                             = 1 << 10,
+		ReceivePhysicalAddressFilteringWithMacAddressEnable  = 1 << 11,
+		ReceiveIpFrameChecksumCheckEnable                    = 1 << 12,
+		ReceiveTcpFrameChecksumCheckEnable                   = 1 << 13,
+		ReceiveUdpFrameChecksumCheckEnable                   = 1 << 14,
+		FlushReceiveQueue                                    = 1 << 15,
 	};
 
 	/**
 	 * Flag bits of the ReceiveControl2 register.
 	 */
-	enum [[clang::flag_enum]] ReceiveControl2 : uint16_t{
-	  ReceiveSourceAddressFiltering            = 1 << 0,
-	  ReceiveIcmpFrameChecksumEnable           = 1 << 1,
-	  UdpLiteFrameEnable                       = 1 << 2,
-	  ReceiveIpv4Ipv6UdpFrameChecksumEqualZero = 1 << 3,
-	  ReceiveIpv4Ipv6FragmentFramePass         = 1 << 4,
-	  DataBurst4Bytes                          = 0b000 << 5,
-	  DataBurst8Bytes                          = 0b001 << 5,
-	  DataBurst16Bytes                         = 0b010 << 5,
-	  DataBurst32Bytes                         = 0b011 << 5,
-	  DataBurstSingleFrame                     = 0b100 << 5,
+	enum [[clang::flag_enum]] ReceiveControl2 : uint16_t
+	{
+		ReceiveSourceAddressFiltering            = 1 << 0,
+		ReceiveIcmpFrameChecksumEnable           = 1 << 1,
+		UdpLiteFrameEnable                       = 1 << 2,
+		ReceiveIpv4Ipv6UdpFrameChecksumEqualZero = 1 << 3,
+		ReceiveIpv4Ipv6FragmentFramePass         = 1 << 4,
+		DataBurst4Bytes                          = 0b000 << 5,
+		DataBurst8Bytes                          = 0b001 << 5,
+		DataBurst16Bytes                         = 0b010 << 5,
+		DataBurst32Bytes                         = 0b011 << 5,
+		DataBurstSingleFrame                     = 0b100 << 5,
 	};
 
 	/**
 	 * Flag bits of the ReceiveFrameHeaderStatus register.
 	 */
-	enum [[clang::flag_enum]] ReceiveFrameHeaderStatus : uint16_t{
-	  ReceiveCrcError                = 1 << 0,
-	  ReceiveRuntFrame               = 1 << 1,
-	  ReceiveFrameTooLong            = 1 << 2,
-	  ReceiveFrameType               = 1 << 3,
-	  ReceiveMiiError                = 1 << 4,
-	  ReceiveUnicastFrame            = 1 << 5,
-	  ReceiveMulticastFrame          = 1 << 6,
-	  ReceiveBroadcastFrame          = 1 << 7,
-	  ReceiveUdpFrameChecksumStatus  = 1 << 10,
-	  ReceiveTcpFrameChecksumStatus  = 1 << 11,
-	  ReceiveIpFrameChecksumStatus   = 1 << 12,
-	  ReceiveIcmpFrameChecksumStatus = 1 << 13,
-	  ReceiveFrameValid              = 1 << 15,
+	enum [[clang::flag_enum]] ReceiveFrameHeaderStatus : uint16_t
+	{
+		ReceiveCrcError                = 1 << 0,
+		ReceiveRuntFrame               = 1 << 1,
+		ReceiveFrameTooLong            = 1 << 2,
+		ReceiveFrameType               = 1 << 3,
+		ReceiveMiiError                = 1 << 4,
+		ReceiveUnicastFrame            = 1 << 5,
+		ReceiveMulticastFrame          = 1 << 6,
+		ReceiveBroadcastFrame          = 1 << 7,
+		ReceiveUdpFrameChecksumStatus  = 1 << 10,
+		ReceiveTcpFrameChecksumStatus  = 1 << 11,
+		ReceiveIpFrameChecksumStatus   = 1 << 12,
+		ReceiveIcmpFrameChecksumStatus = 1 << 13,
+		ReceiveFrameValid              = 1 << 15,
 	};
 
 	/**
 	 * Flag bits of the ReceiveQueueCommand register.
 	 */
-	enum [[clang::flag_enum]] ReceiveQueueCommand : uint16_t{
-	  ReleaseReceiveErrorFrame            = 1 << 0,
-	  StartDmaAccess                      = 1 << 3,
-	  AutoDequeueReceiveQueueFrameEnable  = 1 << 4,
-	  ReceiveFrameCountThresholdEnable    = 1 << 5,
-	  ReceiveDataByteCountThresholdEnable = 1 << 6,
-	  ReceiveDurationTimerThresholdEnable = 1 << 7,
-	  ReceiveIpHeaderTwoByteOffsetEnable  = 1 << 9,
-	  ReceiveFrameCountThresholdStatus    = 1 << 10,
-	  ReceiveDataByteCountThresholdstatus = 1 << 11,
-	  ReceiveDurationTimerThresholdStatus = 1 << 12,
+	enum [[clang::flag_enum]] ReceiveQueueCommand : uint16_t
+	{
+		ReleaseReceiveErrorFrame            = 1 << 0,
+		StartDmaAccess                      = 1 << 3,
+		AutoDequeueReceiveQueueFrameEnable  = 1 << 4,
+		ReceiveFrameCountThresholdEnable    = 1 << 5,
+		ReceiveDataByteCountThresholdEnable = 1 << 6,
+		ReceiveDurationTimerThresholdEnable = 1 << 7,
+		ReceiveIpHeaderTwoByteOffsetEnable  = 1 << 9,
+		ReceiveFrameCountThresholdStatus    = 1 << 10,
+		ReceiveDataByteCountThresholdstatus = 1 << 11,
+		ReceiveDurationTimerThresholdStatus = 1 << 12,
 	};
 
 	/**
 	 * Flag bits of the TransmitQueueCommand register.
 	 */
-	enum [[clang::flag_enum]] TransmitQueueCommand : uint16_t{
-	  ManualEnqueueTransmitQueueFrameEnable = 1 << 0,
-	  TransmitQueueMemoryAvailableMonitor   = 1 << 1,
-	  AutoEnqueueTransmitQueueFrameEnable   = 1 << 2,
+	enum [[clang::flag_enum]] TransmitQueueCommand : uint16_t
+	{
+		ManualEnqueueTransmitQueueFrameEnable = 1 << 0,
+		TransmitQueueMemoryAvailableMonitor   = 1 << 1,
+		AutoEnqueueTransmitQueueFrameEnable   = 1 << 2,
 	};
 
 	/**
 	 * Flag bits of the TransmitFrameDataPointer and ReceiveFrameDataPointer
 	 * register.
 	 */
-	enum [[clang::flag_enum]] FrameDataPointer : uint16_t{
-	  /**
-	   * When this bit is set, the frame data pointer register increments
-	   * automatically on accesses to the data register.
-	   */
-	  FrameDataPointerAutoIncrement = 1 << 14,
+	enum [[clang::flag_enum]] FrameDataPointer : uint16_t
+	{
+		/**
+		 * When this bit is set, the frame data pointer register increments
+		 * automatically on accesses to the data register.
+		 */
+		FrameDataPointerAutoIncrement = 1 << 14,
 	};
 
 	/**
 	 * Flags bits of the InterruptStatus and InterruptEnable registers.
 	 */
-	enum [[clang::flag_enum]] Interrupt : uint16_t{
-	  EnergyDetectInterrupt             = 1 << 2,
-	  LinkupDetectInterrupt             = 1 << 3,
-	  ReceiveMagicPacketDetectInterrupt = 1 << 4,
-	  ReceiveWakeupFrameDetectInterrupt = 1 << 5,
-	  TransmitSpaceAvailableInterrupt   = 1 << 6,
-	  ReceiveProcessStoppedInterrupt    = 1 << 7,
-	  TransmitProcessStoppedInterrupt   = 1 << 8,
-	  ReceiveOverrunInterrupt           = 1 << 11,
-	  ReceiveInterrupt                  = 1 << 13,
-	  TransmitInterrupt                 = 1 << 14,
-	  LinkChangeInterruptStatus         = 1 << 15,
+	enum [[clang::flag_enum]] Interrupt : uint16_t
+	{
+		EnergyDetectInterrupt             = 1 << 2,
+		LinkupDetectInterrupt             = 1 << 3,
+		ReceiveMagicPacketDetectInterrupt = 1 << 4,
+		ReceiveWakeupFrameDetectInterrupt = 1 << 5,
+		TransmitSpaceAvailableInterrupt   = 1 << 6,
+		ReceiveProcessStoppedInterrupt    = 1 << 7,
+		TransmitProcessStoppedInterrupt   = 1 << 8,
+		ReceiveOverrunInterrupt           = 1 << 11,
+		ReceiveInterrupt                  = 1 << 13,
+		TransmitInterrupt                 = 1 << 14,
+		LinkChangeInterruptStatus         = 1 << 15,
 	};
 
 	/**
 	 * Flags bits of the Port1Control register.
 	 */
-	enum [[clang::flag_enum]] Port1Control : uint16_t{
-	  Advertised10BTHalfDuplexCapability  = 1 << 0,
-	  Advertised10BTFullDuplexCapability  = 1 << 1,
-	  Advertised100BTHalfDuplexCapability = 1 << 2,
-	  Advertised100BTFullDuplexCapability = 1 << 3,
-	  AdvertisedFlowControlCapability     = 1 << 4,
-	  ForceDuplex                         = 1 << 5,
-	  ForceSpeed                          = 1 << 6,
-	  AutoNegotiationEnable               = 1 << 7,
-	  ForceMDIX                           = 1 << 9,
-	  DisableAutoMDIMDIX                  = 1 << 10,
-	  RestartAutoNegotiation              = 1 << 13,
-	  TransmitterDisable                  = 1 << 14,
-	  LedOff                              = 1 << 15,
+	enum [[clang::flag_enum]] Port1Control : uint16_t
+	{
+		Advertised10BTHalfDuplexCapability  = 1 << 0,
+		Advertised10BTFullDuplexCapability  = 1 << 1,
+		Advertised100BTHalfDuplexCapability = 1 << 2,
+		Advertised100BTFullDuplexCapability = 1 << 3,
+		AdvertisedFlowControlCapability     = 1 << 4,
+		ForceDuplex                         = 1 << 5,
+		ForceSpeed                          = 1 << 6,
+		AutoNegotiationEnable               = 1 << 7,
+		ForceMDIX                           = 1 << 9,
+		DisableAutoMDIMDIX                  = 1 << 10,
+		RestartAutoNegotiation              = 1 << 13,
+		TransmitterDisable                  = 1 << 14,
+		LedOff                              = 1 << 15,
 	};
 
 	/**
 	 * Flags bits of the Port1Status register.
 	 */
-	enum [[clang::flag_enum]] Port1Status : uint16_t{
-	  Partner10BTHalfDuplexCapability  = 1 << 0,
-	  Partner10BTFullDuplexCapability  = 1 << 1,
-	  Partner100BTHalfDuplexCapability = 1 << 2,
-	  Partner100BTFullDuplexCapability = 1 << 3,
-	  PartnerFlowControlCapability     = 1 << 4,
-	  LinkGood                         = 1 << 5,
-	  AutoNegotiationDone              = 1 << 6,
-	  MDIXStatus                       = 1 << 7,
-	  OperationDuplex                  = 1 << 9,
-	  OperationSpeed                   = 1 << 10,
-	  PolarityReverse                  = 1 << 13,
-	  HPMDIX                           = 1 << 15,
+	enum [[clang::flag_enum]] Port1Status : uint16_t
+	{
+		Partner10BTHalfDuplexCapability  = 1 << 0,
+		Partner10BTFullDuplexCapability  = 1 << 1,
+		Partner100BTHalfDuplexCapability = 1 << 2,
+		Partner100BTFullDuplexCapability = 1 << 3,
+		PartnerFlowControlCapability     = 1 << 4,
+		LinkGood                         = 1 << 5,
+		AutoNegotiationDone              = 1 << 6,
+		MDIXStatus                       = 1 << 7,
+		OperationDuplex                  = 1 << 9,
+		OperationSpeed                   = 1 << 10,
+		PolarityReverse                  = 1 << 13,
+		HPMDIX                           = 1 << 15,
 	};
 
 	/**
@@ -307,18 +307,6 @@
 	const uint32_t *receiveInterruptFutex;
 
 	/**
-	 * Set value of a GPIO output.
-	 */
-	inline void set_gpio_output_bit(GpioPin pin, bool value) const
-	{
-		uint32_t shift  = static_cast<uint8_t>(pin);
-		uint32_t output = gpio()->output;
-		output &= ~(1 << shift);
-		output |= value << shift;
-		gpio()->output = output;
-	}
-
-	/**
 	 * Read a register from the KSZ8851.
 	 */
 	[[nodiscard]] uint16_t register_read(RegisterOffset reg) const
@@ -350,11 +338,11 @@
 		           (byteEnable << 2) | (addr >> 6);
 		bytes[1] = (addr << 2) & 0b11110000;
 
-		set_gpio_output_bit(GpioPin::EthernetChipSelect, false);
+		spi()->chip_select_assert(true);
 		spi()->blocking_write(bytes, sizeof(bytes));
 		uint16_t val;
 		spi()->blocking_read(reinterpret_cast<uint8_t *>(&val), sizeof(val));
-		set_gpio_output_bit(GpioPin::EthernetChipSelect, true);
+		spi()->chip_select_assert(false);
 		return val;
 	}
 
@@ -371,11 +359,11 @@
 		           (byteEnable << 2) | (addr >> 6);
 		bytes[1] = (addr << 2) & 0b11110000;
 
-		set_gpio_output_bit(GpioPin::EthernetChipSelect, false);
+		spi()->chip_select_assert(true);
 		spi()->blocking_write(bytes, sizeof(bytes));
 		spi()->blocking_write(reinterpret_cast<uint8_t *>(&val), sizeof(val));
 		spi()->wait_idle();
-		set_gpio_output_bit(GpioPin::EthernetChipSelect, true);
+		spi()->chip_select_assert(false);
 	}
 
 	/**
@@ -397,20 +385,13 @@
 	}
 
 	/**
-	 * Helper.  Returns a pointer to the SPI device.
+	 * Helper. Returns a pointer to the SPI device.
 	 */
-	[[nodiscard, gnu::always_inline]] Capability<volatile SonataSpi> spi() const
+	[[nodiscard,
+	  gnu::always_inline]] Capability<volatile SonataSpi::EthernetMac>
+	spi() const
 	{
-		return MMIO_CAPABILITY(SonataSpi, spi2);
-	}
-
-	/**
-	 * Helper.  Returns a pointer to the GPIO device.
-	 */
-	[[nodiscard, gnu::always_inline]] Capability<volatile SonataGPIO>
-	gpio() const
-	{
-		return MMIO_CAPABILITY(SonataGPIO, gpio);
+		return MMIO_CAPABILITY(SonataSpi::EthernetMac, spi_ethmac);
 	}
 
 	/**
@@ -435,10 +416,10 @@
 	RecursiveMutex receiveBufferMutex;
 
 	/**
-	 * Reads and writes of the GPIO space use the same bits of the MMIO region
-	 * and so need to be protected.
+	 * Lock to protect reads/writes to the SPI Chip Selects, which use the
+	 * same bits of the MMIO region and thus need to be protected.
 	 */
-	FlagLockPriorityInherited gpioLock;
+	FlagLockPriorityInherited chipSelectLock;
 
 	/**
 	 * Buffer used by receive_frame.
@@ -455,9 +436,9 @@
 		receiveBuffer  = std::make_unique<uint8_t[]>(MaxFrameSize);
 
 		// Reset chip. It needs to be hold in reset for at least 10ms.
-		set_gpio_output_bit(GpioPin::EthernetReset, false);
+		spi()->reset_assert(true);
 		thread_millisecond_wait(20);
-		set_gpio_output_bit(GpioPin::EthernetReset, true);
+		spi()->reset_assert(false);
 
 		uint16_t chipId = register_read(RegisterOffset::ChipIdEnable);
 		Debug::log("Chip ID is {}", chipId);
@@ -626,7 +607,7 @@
 
 	std::optional<Frame> receive_frame()
 	{
-		LockGuard g{gpioLock};
+		LockGuard g{chipSelectLock};
 		if (framesToProcess == 0)
 		{
 			uint16_t isr = register_read(RegisterOffset::InterruptStatus);
@@ -700,7 +681,7 @@
 
 			// Start receiving via SPI.
 			uint8_t cmd = static_cast<uint8_t>(SpiCommand::ReadDma) << 6;
-			set_gpio_output_bit(GpioPin::EthernetChipSelect, false);
+			spi()->chip_select_assert(true);
 			spi()->blocking_write(&cmd, 1);
 
 			// Initial words are ReceiveFrameHeaderStatus and
@@ -710,7 +691,7 @@
 
 			spi()->blocking_read(receiveBuffer.get(), paddedLength);
 
-			set_gpio_output_bit(GpioPin::EthernetChipSelect, true);
+			spi()->chip_select_assert(false);
 
 			register_clear(RegisterOffset::ReceiveQueueCommand, StartDmaAccess);
 			framesToProcess -= 1;
@@ -753,7 +734,7 @@
 		// does not check the pointer which is coming from external
 		// untrusted components.
 		Timeout t{10};
-		if ((heap_claim_fast(&t, buffer) < 0) ||
+		if ((heap_claim_ephemeral(&t, buffer) < 0) ||
 		    (!CHERI::check_pointer<CHERI::PermissionSet{
 		       CHERI::Permission::Load}>(buffer, length)))
 		{
@@ -766,7 +747,7 @@
 			return false;
 		}
 
-		LockGuard g{gpioLock};
+		LockGuard g{chipSelectLock};
 
 		// Wait for the transmit buffer to be available on the device side.
 		// This needs to include the header.
@@ -782,7 +763,7 @@
 
 		// Start sending via SPI.
 		uint8_t cmd = static_cast<uint8_t>(SpiCommand::WriteDma) << 6;
-		set_gpio_output_bit(GpioPin::EthernetChipSelect, false);
+		spi()->chip_select_assert(true);
 		spi()->blocking_write(&cmd, 1);
 
 		uint32_t header = static_cast<uint32_t>(length) << 16;
@@ -792,7 +773,7 @@
 		spi()->blocking_write(transmitBuffer.get(), paddedLength);
 
 		spi()->wait_idle();
-		set_gpio_output_bit(GpioPin::EthernetChipSelect, true);
+		spi()->chip_select_assert(false);
 
 		// Stop QMU DMA transfer operation.
 		register_clear(RegisterOffset::ReceiveQueueCommand, StartDmaAccess);
diff --git a/sdk/include/platform/sunburst/platform-i2c.hh b/sdk/include/platform/sunburst/platform-i2c.hh
index ef93eee..9d86acb 100644
--- a/sdk/include/platform/sunburst/platform-i2c.hh
+++ b/sdk/include/platform/sunburst/platform-i2c.hh
@@ -182,105 +182,113 @@
 	};
 
 	/// Control Register Fields
-	enum [[clang::flag_enum]] : uint32_t{
-	  /// Enable Host I2C functionality
-	  ControlEnableHost = 1 << 0,
-	  /// Enable Target I2C functionality
-	  ControlEnableTarget = 1 << 1,
-	  /// Enable I2C line loopback test If line loopback is enabled, the
-	  /// internal design sees ACQ and RX data as "1"
-	  ControlLineLoopback = 1 << 2,
-	  /// Enable NACKing the address on a stretch timeout. This is a target
-	  /// mode feature. If enabled, a stretch timeout will cause the device to
-	  /// NACK the address byte. If disabled, it will ACK instead.
-	  ControlNackAddressAfterTimeout = 1 << 3,
-	  /// Enable ACK Control Mode, which works with the `targetAckControl`
-	  /// register to allow software to control upper-layer (N)ACKing.
-	  ControlAckControlEnable = 1 << 4,
-	  /// Enable the bus monitor in multi-controller mode.
-	  ControlMultiControllerMonitorEnable = 1 << 5,
-	  /// If set, causes a read transfer addressed to the this target to set
-	  /// the corresponding bit in the `targetEvents` register. While the
-	  /// `transmitPending` field is 1, subsequent read transactions will
-	  /// stretch the clock, even if there is data in the Transmit FIFO.
-	  ControlTransmitStretchEnable = 1 << 6,
+	enum [[clang::flag_enum]] : uint32_t
+	{
+		/// Enable Host I2C functionality
+		ControlEnableHost = 1 << 0,
+		/// Enable Target I2C functionality
+		ControlEnableTarget = 1 << 1,
+		/// Enable I2C line loopback test If line loopback is enabled, the
+		/// internal design sees ACQ and RX data as "1"
+		ControlLineLoopback = 1 << 2,
+		/// Enable NACKing the address on a stretch timeout. This is a target
+		/// mode feature. If enabled, a stretch timeout will cause the device to
+		/// NACK the address byte. If disabled, it will ACK instead.
+		ControlNackAddressAfterTimeout = 1 << 3,
+		/// Enable ACK Control Mode, which works with the `targetAckControl`
+		/// register to allow software to control upper-layer (N)ACKing.
+		ControlAckControlEnable = 1 << 4,
+		/// Enable the bus monitor in multi-controller mode.
+		ControlMultiControllerMonitorEnable = 1 << 5,
+		/// If set, causes a read transfer addressed to the this target to set
+		/// the corresponding bit in the `targetEvents` register. While the
+		/// `transmitPending` field is 1, subsequent read transactions will
+		/// stretch the clock, even if there is data in the Transmit FIFO.
+		ControlTransmitStretchEnable = 1 << 6,
 	};
 
 	/// Status Register Fields
-	enum [[clang::flag_enum]] : uint32_t{
-	  /// Host mode Format FIFO is full
-	  StatusFormatFull = 1 << 0,
-	  /// Host mode Receive FIFO is full
-	  StatusReceiveFull = 1 << 1,
-	  /// Host mode Format FIFO is empty
-	  StatusFormatEmpty = 1 << 2,
-	  /// Host functionality is idle. No Host transaction is in progress
-	  StatusHostIdle = 1 << 3,
-	  /// Target functionality is idle. No Target transaction is in progress
-	  StatusTargetIdle = 1 << 4,
-	  /// Host mode Receive FIFO is empty
-	  SmatusReceiveEmpty = 1 << 5,
-	  /// Target mode Transmit FIFO is full
-	  StatusTransmitFull = 1 << 6,
-	  /// Target mode Acquired FIFO is full
-	  StatusAcquiredFull = 1 << 7,
-	  /// Target mode Transmit FIFO is empty
-	  StatusTransmitEmpty = 1 << 8,
-	  /// Target mode Acquired FIFO is empty
-	  StatusAcquiredEmpty = 1 << 9,
-	  /// Target mode stretching at (N)ACK phase due to zero count
-	  /// in the `targetAckControl` register.
-	  StatusAckControlStretch = 1 << 10,
+	enum [[clang::flag_enum]] : uint32_t
+	{
+		/// Host mode Format FIFO is full
+		StatusFormatFull = 1 << 0,
+		/// Host mode Receive FIFO is full
+		StatusReceiveFull = 1 << 1,
+		/// Host mode Format FIFO is empty
+		StatusFormatEmpty = 1 << 2,
+		/// Host functionality is idle. No Host transaction is in progress
+		StatusHostIdle = 1 << 3,
+		/// Target functionality is idle. No Target transaction is in progress
+		StatusTargetIdle = 1 << 4,
+		/// Host mode Receive FIFO is empty
+		SmatusReceiveEmpty = 1 << 5,
+		/// Target mode Transmit FIFO is full
+		StatusTransmitFull = 1 << 6,
+		/// Target mode Acquired FIFO is full
+		StatusAcquiredFull = 1 << 7,
+		/// Target mode Transmit FIFO is empty
+		StatusTransmitEmpty = 1 << 8,
+		/// Target mode Acquired FIFO is empty
+		StatusAcquiredEmpty = 1 << 9,
+		/// Target mode stretching at (N)ACK phase due to zero count
+		/// in the `targetAckControl` register.
+		StatusAckControlStretch = 1 << 10,
 	};
 
 	/// FormatData Register Fields
-	enum [[clang::flag_enum]] : uint32_t{
-	  /// Issue a START condition before transmitting BYTE.
-	  FormatDataStart = 1 << 8,
-	  /// Issue a STOP condition after this operation
-	  FormatDataStop = 1 << 9,
-	  /// Read BYTE bytes from I2C. (256 if BYTE==0)
-	  FormatDataReadBytes = 1 << 10,
-	  /**
-	   * Do not NACK the last byte read, let the read
-	   * operation continue
-	   */
-	  FormatDataReadCount = 1 << 11,
-	  /// Do not signal an exception if the current byte is not ACK’d
-	  FormatDataNakOk = 1 << 12,
+	enum [[clang::flag_enum]] : uint32_t
+	{
+		/// Issue a START condition before transmitting BYTE.
+		FormatDataStart = 1 << 8,
+		/// Issue a STOP condition after this operation
+		FormatDataStop = 1 << 9,
+		/// Read BYTE bytes from I2C. (256 if BYTE==0)
+		FormatDataReadBytes = 1 << 10,
+		/**
+		 * Do not NACK the last byte read, let the read
+		 * operation continue
+		 */
+		FormatDataReadCount = 1 << 11,
+		/// Do not signal an exception if the current byte is not ACK’d
+		FormatDataNakOk = 1 << 12,
 	};
 
 	/// FifoControl Register Fields
-	enum [[clang::flag_enum]] : uint32_t{
-	  /// Receive fifo reset. Write 1 to the register resets it. Read returns 0
-	  FifoControlReceiveReset = 1 << 0,
-	  /// Format fifo reset. Write 1 to the register resets it. Read returns 0
-	  FifoControlFormatReset = 1 << 1,
-	  /// Acquired FIFO reset. Write 1 to the register resets it. Read returns 0
-	  FifoControlAcquiredReset = 1 << 7,
-	  /// Transmit FIFO reset. Write 1 to the register resets it. Read returns 0
-	  FifoControlTransmitReset = 1 << 8,
+	enum [[clang::flag_enum]] : uint32_t
+	{
+		/// Receive fifo reset. Write 1 to the register resets it. Read returns
+		/// 0
+		FifoControlReceiveReset = 1 << 0,
+		/// Format fifo reset. Write 1 to the register resets it. Read returns 0
+		FifoControlFormatReset = 1 << 1,
+		/// Acquired FIFO reset. Write 1 to the register resets it. Read returns
+		/// 0
+		FifoControlAcquiredReset = 1 << 7,
+		/// Transmit FIFO reset. Write 1 to the register resets it. Read returns
+		/// 0
+		FifoControlTransmitReset = 1 << 8,
 	};
 
 	/// ControllerEvents Register Fields
-	enum [[clang::flag_enum]] : uint32_t{
-	  /// Controller FSM is halted due to receiving an unexpected NACK.
-	  ControllerEventsNack = 1 << 0,
-	  /**
-	   * Controller FSM is halted due to a Host-Mode active transaction being
-	   * ended by the `hostNackHandlerTimeout` mechanism.
-	   */
-	  ControllerEventsUnhandledNackTimeout = 1 << 1,
-	  /**
-	   * Controller FSM is halted due to a Host-Mode active transaction being
-	   * terminated because of a bus timeout activated by `timeoutControl`.
-	   */
-	  ControllerEventsBusTimeout = 1 << 2,
-	  /**
-	   * Controller FSM is halted due to a Host-Mode active transaction being
-	   * terminated because of lost arbitration.
-	   */
-	  ControllerEventsArbitrationLost = 1 << 3,
+	enum [[clang::flag_enum]] : uint32_t
+	{
+		/// Controller FSM is halted due to receiving an unexpected NACK.
+		ControllerEventsNack = 1 << 0,
+		/**
+		 * Controller FSM is halted due to a Host-Mode active transaction being
+		 * ended by the `hostNackHandlerTimeout` mechanism.
+		 */
+		ControllerEventsUnhandledNackTimeout = 1 << 1,
+		/**
+		 * Controller FSM is halted due to a Host-Mode active transaction being
+		 * terminated because of a bus timeout activated by `timeoutControl`.
+		 */
+		ControllerEventsBusTimeout = 1 << 2,
+		/**
+		 * Controller FSM is halted due to a Host-Mode active transaction being
+		 * terminated because of lost arbitration.
+		 */
+		ControllerEventsArbitrationLost = 1 << 3,
 	};
 
 	// Referred to as 'RX FIFO' in the documentation
diff --git a/sdk/include/platform/sunburst/platform-pinmux.hh b/sdk/include/platform/sunburst/platform-pinmux.hh
new file mode 100644
index 0000000..3a1017e
--- /dev/null
+++ b/sdk/include/platform/sunburst/platform-pinmux.hh
@@ -0,0 +1,421 @@
+/**
+ * SPDX-FileCopyrightText: lowRISC contributors
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * This file is generated by ./util/topgen.py
+ * in https://github.com/lowRISC/sonata-system
+ */
+
+#pragma once
+#include <cheri.hh>
+#include <debug.hh>
+#include <stdint.h>
+#include <utils.hh>
+
+namespace SonataPinmux
+{
+	/// The number of pin sinks (pin outputs)
+	static constexpr size_t NumPinSinks = 85;
+
+	/// The number of block sinks (block inputs)
+	static constexpr size_t NumBlockSinks = 70;
+
+	/// Flag to set when debugging the driver for UART log messages.
+	static constexpr bool DebugDriver = false;
+
+	/// Helper for conditional debug logs and assertions.
+	using Debug = ConditionalDebug<DebugDriver, "Pinmux">;
+
+	/// The disable bit that disables a sink
+	constexpr uint8_t SourceDisabled = 0;
+
+	/// The bit that resets a sink to it's default value
+	constexpr uint8_t SourceDefault = 1;
+
+	/**
+	 * Each pin sink is configured by an 8-bit register. This enum maps pin sink
+	 * names to the offset of their configuration registers. The offsets are
+	 * relative to the first pin sink register.
+	 *
+	 * Documentation sources:
+	 * 1. https://lowrisc.github.io/sonata-system/doc/ip/pinmux/
+	 * 2.
+	 * https://github.com/lowRISC/sonata-system/blob/v1.0/data/pins_sonata.xdc
+	 * 3.
+	 * https://github.com/newaetech/sonata-pcb/blob/649b11c2fb758f798966605a07a8b6b68dd434e9/sonata-schematics-r09.pdf
+	 */
+	enum class PinSink : uint16_t
+	{
+		// Breaking enum naming convention to match cononical names used in PCB
+		// schematic documentation.
+		// When available, it would be neater to use NOLINTBEGIN and NOLINTEND.
+		ser0_tx      = 0x000, // NOLINT(readability-identifier-naming)
+		ser1_tx      = 0x001, // NOLINT(readability-identifier-naming)
+		rs232_tx     = 0x002, // NOLINT(readability-identifier-naming)
+		rs485_tx     = 0x003, // NOLINT(readability-identifier-naming)
+		scl0         = 0x004, // NOLINT(readability-identifier-naming)
+		sda0         = 0x005, // NOLINT(readability-identifier-naming)
+		scl1         = 0x006, // NOLINT(readability-identifier-naming)
+		sda1         = 0x007, // NOLINT(readability-identifier-naming)
+		rph_g0       = 0x008, // NOLINT(readability-identifier-naming)
+		rph_g1       = 0x009, // NOLINT(readability-identifier-naming)
+		rph_g2_sda   = 0x00a, // NOLINT(readability-identifier-naming)
+		rph_g3_scl   = 0x00b, // NOLINT(readability-identifier-naming)
+		rph_g4       = 0x00c, // NOLINT(readability-identifier-naming)
+		rph_g5       = 0x00d, // NOLINT(readability-identifier-naming)
+		rph_g6       = 0x00e, // NOLINT(readability-identifier-naming)
+		rph_g7       = 0x00f, // NOLINT(readability-identifier-naming)
+		rph_g8       = 0x010, // NOLINT(readability-identifier-naming)
+		rph_g9       = 0x011, // NOLINT(readability-identifier-naming)
+		rph_g10      = 0x012, // NOLINT(readability-identifier-naming)
+		rph_g11      = 0x013, // NOLINT(readability-identifier-naming)
+		rph_g12      = 0x014, // NOLINT(readability-identifier-naming)
+		rph_g13      = 0x015, // NOLINT(readability-identifier-naming)
+		rph_txd0     = 0x016, // NOLINT(readability-identifier-naming)
+		rph_rxd0     = 0x017, // NOLINT(readability-identifier-naming)
+		rph_g16      = 0x018, // NOLINT(readability-identifier-naming)
+		rph_g17      = 0x019, // NOLINT(readability-identifier-naming)
+		rph_g18      = 0x01a, // NOLINT(readability-identifier-naming)
+		rph_g19      = 0x01b, // NOLINT(readability-identifier-naming)
+		rph_g20      = 0x01c, // NOLINT(readability-identifier-naming)
+		rph_g21      = 0x01d, // NOLINT(readability-identifier-naming)
+		rph_g22      = 0x01e, // NOLINT(readability-identifier-naming)
+		rph_g23      = 0x01f, // NOLINT(readability-identifier-naming)
+		rph_g24      = 0x020, // NOLINT(readability-identifier-naming)
+		rph_g25      = 0x021, // NOLINT(readability-identifier-naming)
+		rph_g26      = 0x022, // NOLINT(readability-identifier-naming)
+		rph_g27      = 0x023, // NOLINT(readability-identifier-naming)
+		ah_tmpio0    = 0x024, // NOLINT(readability-identifier-naming)
+		ah_tmpio1    = 0x025, // NOLINT(readability-identifier-naming)
+		ah_tmpio2    = 0x026, // NOLINT(readability-identifier-naming)
+		ah_tmpio3    = 0x027, // NOLINT(readability-identifier-naming)
+		ah_tmpio4    = 0x028, // NOLINT(readability-identifier-naming)
+		ah_tmpio5    = 0x029, // NOLINT(readability-identifier-naming)
+		ah_tmpio6    = 0x02a, // NOLINT(readability-identifier-naming)
+		ah_tmpio7    = 0x02b, // NOLINT(readability-identifier-naming)
+		ah_tmpio8    = 0x02c, // NOLINT(readability-identifier-naming)
+		ah_tmpio9    = 0x02d, // NOLINT(readability-identifier-naming)
+		ah_tmpio10   = 0x02e, // NOLINT(readability-identifier-naming)
+		ah_tmpio11   = 0x02f, // NOLINT(readability-identifier-naming)
+		ah_tmpio12   = 0x030, // NOLINT(readability-identifier-naming)
+		ah_tmpio13   = 0x031, // NOLINT(readability-identifier-naming)
+		mb1          = 0x032, // NOLINT(readability-identifier-naming)
+		mb2          = 0x033, // NOLINT(readability-identifier-naming)
+		mb4          = 0x034, // NOLINT(readability-identifier-naming)
+		mb5          = 0x035, // NOLINT(readability-identifier-naming)
+		mb6          = 0x036, // NOLINT(readability-identifier-naming)
+		mb7          = 0x037, // NOLINT(readability-identifier-naming)
+		mb10         = 0x038, // NOLINT(readability-identifier-naming)
+		pmod0_1      = 0x039, // NOLINT(readability-identifier-naming)
+		pmod0_2      = 0x03a, // NOLINT(readability-identifier-naming)
+		pmod0_3      = 0x03b, // NOLINT(readability-identifier-naming)
+		pmod0_4      = 0x03c, // NOLINT(readability-identifier-naming)
+		pmod0_7      = 0x03d, // NOLINT(readability-identifier-naming)
+		pmod0_8      = 0x03e, // NOLINT(readability-identifier-naming)
+		pmod0_9      = 0x03f, // NOLINT(readability-identifier-naming)
+		pmod0_10     = 0x040, // NOLINT(readability-identifier-naming)
+		pmod1_1      = 0x041, // NOLINT(readability-identifier-naming)
+		pmod1_2      = 0x042, // NOLINT(readability-identifier-naming)
+		pmod1_3      = 0x043, // NOLINT(readability-identifier-naming)
+		pmod1_4      = 0x044, // NOLINT(readability-identifier-naming)
+		pmod1_7      = 0x045, // NOLINT(readability-identifier-naming)
+		pmod1_8      = 0x046, // NOLINT(readability-identifier-naming)
+		pmod1_9      = 0x047, // NOLINT(readability-identifier-naming)
+		pmod1_10     = 0x048, // NOLINT(readability-identifier-naming)
+		pmodc_1      = 0x049, // NOLINT(readability-identifier-naming)
+		pmodc_2      = 0x04a, // NOLINT(readability-identifier-naming)
+		pmodc_3      = 0x04b, // NOLINT(readability-identifier-naming)
+		pmodc_4      = 0x04c, // NOLINT(readability-identifier-naming)
+		pmodc_5      = 0x04d, // NOLINT(readability-identifier-naming)
+		pmodc_6      = 0x04e, // NOLINT(readability-identifier-naming)
+		appspi_d0    = 0x04f, // NOLINT(readability-identifier-naming)
+		appspi_clk   = 0x050, // NOLINT(readability-identifier-naming)
+		appspi_cs    = 0x051, // NOLINT(readability-identifier-naming)
+		microsd_cmd  = 0x052, // NOLINT(readability-identifier-naming)
+		microsd_clk  = 0x053, // NOLINT(readability-identifier-naming)
+		microsd_dat3 = 0x054, // NOLINT(readability-identifier-naming)
+	};
+
+	/**
+	 * Each block sink is configured by an 8-bit register. This enum maps block
+	 * sink names to the offset of their configuration registers. The offsets
+	 * are relative to the first block sink register.
+	 *
+	 * For GPIO block reference:
+	 *   gpio_0 = Raspberry Pi Header Pins
+	 *   gpio_1 = Arduino Shield Header Pins
+	 *   gpio_2 = Pmod0 Pins
+	 *   gpio_3 = Pmod1 Pins
+	 *   gpio_4 = PmodC Pins
+	 *
+	 * Documentation source:
+	 * https://lowrisc.github.io/sonata-system/doc/ip/pinmux/
+	 */
+	enum class BlockSink : uint16_t
+	{
+		// Breaking enum naming convention to match cononical names used in
+		// documentation.
+		// When available, it would be neater to use NOLINTBEGIN and NOLINTEND.
+		gpio_0_ios_0  = 0x000, // NOLINT(readability-identifier-naming)
+		gpio_0_ios_1  = 0x001, // NOLINT(readability-identifier-naming)
+		gpio_0_ios_2  = 0x002, // NOLINT(readability-identifier-naming)
+		gpio_0_ios_3  = 0x003, // NOLINT(readability-identifier-naming)
+		gpio_0_ios_4  = 0x004, // NOLINT(readability-identifier-naming)
+		gpio_0_ios_5  = 0x005, // NOLINT(readability-identifier-naming)
+		gpio_0_ios_6  = 0x006, // NOLINT(readability-identifier-naming)
+		gpio_0_ios_7  = 0x007, // NOLINT(readability-identifier-naming)
+		gpio_0_ios_8  = 0x008, // NOLINT(readability-identifier-naming)
+		gpio_0_ios_9  = 0x009, // NOLINT(readability-identifier-naming)
+		gpio_0_ios_10 = 0x00a, // NOLINT(readability-identifier-naming)
+		gpio_0_ios_11 = 0x00b, // NOLINT(readability-identifier-naming)
+		gpio_0_ios_12 = 0x00c, // NOLINT(readability-identifier-naming)
+		gpio_0_ios_13 = 0x00d, // NOLINT(readability-identifier-naming)
+		gpio_0_ios_14 = 0x00e, // NOLINT(readability-identifier-naming)
+		gpio_0_ios_15 = 0x00f, // NOLINT(readability-identifier-naming)
+		gpio_0_ios_16 = 0x010, // NOLINT(readability-identifier-naming)
+		gpio_0_ios_17 = 0x011, // NOLINT(readability-identifier-naming)
+		gpio_0_ios_18 = 0x012, // NOLINT(readability-identifier-naming)
+		gpio_0_ios_19 = 0x013, // NOLINT(readability-identifier-naming)
+		gpio_0_ios_20 = 0x014, // NOLINT(readability-identifier-naming)
+		gpio_0_ios_21 = 0x015, // NOLINT(readability-identifier-naming)
+		gpio_0_ios_22 = 0x016, // NOLINT(readability-identifier-naming)
+		gpio_0_ios_23 = 0x017, // NOLINT(readability-identifier-naming)
+		gpio_0_ios_24 = 0x018, // NOLINT(readability-identifier-naming)
+		gpio_0_ios_25 = 0x019, // NOLINT(readability-identifier-naming)
+		gpio_0_ios_26 = 0x01a, // NOLINT(readability-identifier-naming)
+		gpio_0_ios_27 = 0x01b, // NOLINT(readability-identifier-naming)
+		gpio_1_ios_0  = 0x01c, // NOLINT(readability-identifier-naming)
+		gpio_1_ios_1  = 0x01d, // NOLINT(readability-identifier-naming)
+		gpio_1_ios_2  = 0x01e, // NOLINT(readability-identifier-naming)
+		gpio_1_ios_3  = 0x01f, // NOLINT(readability-identifier-naming)
+		gpio_1_ios_4  = 0x020, // NOLINT(readability-identifier-naming)
+		gpio_1_ios_5  = 0x021, // NOLINT(readability-identifier-naming)
+		gpio_1_ios_6  = 0x022, // NOLINT(readability-identifier-naming)
+		gpio_1_ios_7  = 0x023, // NOLINT(readability-identifier-naming)
+		gpio_1_ios_8  = 0x024, // NOLINT(readability-identifier-naming)
+		gpio_1_ios_9  = 0x025, // NOLINT(readability-identifier-naming)
+		gpio_1_ios_10 = 0x026, // NOLINT(readability-identifier-naming)
+		gpio_1_ios_11 = 0x027, // NOLINT(readability-identifier-naming)
+		gpio_1_ios_12 = 0x028, // NOLINT(readability-identifier-naming)
+		gpio_1_ios_13 = 0x029, // NOLINT(readability-identifier-naming)
+		gpio_2_ios_0  = 0x02a, // NOLINT(readability-identifier-naming)
+		gpio_2_ios_1  = 0x02b, // NOLINT(readability-identifier-naming)
+		gpio_2_ios_2  = 0x02c, // NOLINT(readability-identifier-naming)
+		gpio_2_ios_3  = 0x02d, // NOLINT(readability-identifier-naming)
+		gpio_2_ios_4  = 0x02e, // NOLINT(readability-identifier-naming)
+		gpio_2_ios_5  = 0x02f, // NOLINT(readability-identifier-naming)
+		gpio_2_ios_6  = 0x030, // NOLINT(readability-identifier-naming)
+		gpio_2_ios_7  = 0x031, // NOLINT(readability-identifier-naming)
+		gpio_3_ios_0  = 0x032, // NOLINT(readability-identifier-naming)
+		gpio_3_ios_1  = 0x033, // NOLINT(readability-identifier-naming)
+		gpio_3_ios_2  = 0x034, // NOLINT(readability-identifier-naming)
+		gpio_3_ios_3  = 0x035, // NOLINT(readability-identifier-naming)
+		gpio_3_ios_4  = 0x036, // NOLINT(readability-identifier-naming)
+		gpio_3_ios_5  = 0x037, // NOLINT(readability-identifier-naming)
+		gpio_3_ios_6  = 0x038, // NOLINT(readability-identifier-naming)
+		gpio_3_ios_7  = 0x039, // NOLINT(readability-identifier-naming)
+		gpio_4_ios_0  = 0x03a, // NOLINT(readability-identifier-naming)
+		gpio_4_ios_1  = 0x03b, // NOLINT(readability-identifier-naming)
+		gpio_4_ios_2  = 0x03c, // NOLINT(readability-identifier-naming)
+		gpio_4_ios_3  = 0x03d, // NOLINT(readability-identifier-naming)
+		gpio_4_ios_4  = 0x03e, // NOLINT(readability-identifier-naming)
+		gpio_4_ios_5  = 0x03f, // NOLINT(readability-identifier-naming)
+		uart_0_rx     = 0x040, // NOLINT(readability-identifier-naming)
+		uart_1_rx     = 0x041, // NOLINT(readability-identifier-naming)
+		uart_2_rx     = 0x042, // NOLINT(readability-identifier-naming)
+		spi_0_cipo    = 0x043, // NOLINT(readability-identifier-naming)
+		spi_1_cipo    = 0x044, // NOLINT(readability-identifier-naming)
+		spi_2_cipo    = 0x045, // NOLINT(readability-identifier-naming)
+	};
+
+	/**
+	 * Returns the number of sources available for a pin sink (output pin).
+	 *
+	 * @param pin_sink The pin sink to query.
+	 * @returns The number of sources available for the given sink.
+	 */
+	static constexpr uint8_t sources_number(PinSink pinSink)
+	{
+		switch (pinSink)
+		{
+			case PinSink::pmod0_2:
+			case PinSink::pmod1_2:
+				return 5;
+			case PinSink::rph_g18:
+			case PinSink::rph_g20:
+			case PinSink::rph_g21:
+			case PinSink::ah_tmpio10:
+			case PinSink::ah_tmpio11:
+			case PinSink::pmod0_4:
+			case PinSink::pmod1_4:
+				return 4;
+			case PinSink::ser1_tx:
+			case PinSink::rph_g0:
+			case PinSink::rph_g1:
+			case PinSink::rph_g2_sda:
+			case PinSink::rph_g3_scl:
+			case PinSink::rph_g7:
+			case PinSink::rph_g8:
+			case PinSink::rph_g10:
+			case PinSink::rph_g11:
+			case PinSink::rph_g12:
+			case PinSink::rph_g13:
+			case PinSink::rph_txd0:
+			case PinSink::rph_g16:
+			case PinSink::rph_g17:
+			case PinSink::rph_g19:
+			case PinSink::ah_tmpio1:
+			case PinSink::ah_tmpio3:
+			case PinSink::ah_tmpio5:
+			case PinSink::ah_tmpio6:
+			case PinSink::ah_tmpio9:
+			case PinSink::ah_tmpio13:
+			case PinSink::pmod0_1:
+			case PinSink::pmod0_3:
+			case PinSink::pmod0_8:
+			case PinSink::pmod0_9:
+			case PinSink::pmod0_10:
+			case PinSink::pmod1_1:
+			case PinSink::pmod1_3:
+			case PinSink::pmod1_8:
+			case PinSink::pmod1_9:
+			case PinSink::pmod1_10:
+				return 3;
+			default:
+				return 2;
+		}
+	}
+
+	/**
+	 * Returns the number of sources available for a block sink (block input).
+	 *
+	 * @param block_sink The block sink to query.
+	 * @returns The number of sources available for the given sink.
+	 */
+	static constexpr uint8_t sources_number(BlockSink blockSink)
+	{
+		switch (blockSink)
+		{
+			case BlockSink::uart_1_rx:
+				return 6;
+			case BlockSink::uart_2_rx:
+				return 5;
+			case BlockSink::spi_1_cipo:
+			case BlockSink::spi_2_cipo:
+				return 4;
+			case BlockSink::spi_0_cipo:
+				return 3;
+			default:
+				return 2;
+		}
+	}
+
+	/**
+	 * A handle to a sink configuration register. This can be used to select
+	 * the source of the handle's associated sink.
+	 */
+	template<typename SinkEnum>
+	struct Sink
+	{
+		CHERI::Capability<volatile uint8_t> reg;
+		const SinkEnum                      SinkId;
+
+		/**
+		 * Select a source to connect to the sink.
+		 *
+		 * To see the sources available for a given sink see the Sonata system
+		 * documentation:
+		 * https://lowrisc.github.io/sonata-system/doc/ip/pinmux/
+		 *
+		 * Note, source 0 disconnects the sink from any source disabling it,
+		 * and source 1 is the default source for any given sink.
+		 */
+		bool select(uint8_t source)
+		{
+			if (source >= sources_number(SinkId))
+			{
+				Debug::log("{} is outside the range of valid sources, [0-{}), "
+				           "of pin {}.",
+				           source,
+				           sources_number(SinkId),
+				           SinkId);
+				return false;
+			}
+			*reg = 1 << source;
+			return true;
+		}
+
+		/// Disconnect the sink from all available sources.
+		void disable()
+		{
+			*reg = 1 << SourceDisabled;
+		}
+
+		/// Reset the sink to it's default source.
+		void default_selection()
+		{
+			*reg = 1 << SourceDefault;
+		}
+	};
+
+	namespace
+	{
+		template<typename SinkEnum>
+		// This is used by `BlockSinks` and `PinSinks`
+		// to return a capability to a single sink's configuration register.
+		inline Sink<SinkEnum> get_sink(volatile uint8_t *baseRegister,
+		                               const SinkEnum    SinkId)
+		{
+			CHERI::Capability reg = {baseRegister +
+			                         static_cast<ptrdiff_t>(SinkId)};
+			reg.bounds()          = sizeof(uint8_t);
+			return Sink<SinkEnum>{reg, SinkId};
+		};
+	} // namespace
+
+	/**
+	 * A driver for the Sonata system's pin multiplexed output pins.
+	 *
+	 * The Sonata system's Pin Multiplexer (pinmux) has two sets of registers:
+	 * the pin sink registers and the block sink registers. This structure
+	 * provides access to the pin sinks registers. Pin sinks are output onto the
+	 * Sonata system's pins, which can be connected to a number of block outputs
+	 * (their sources). The sources each sink can connect to are limited. See
+	 * the documentation for the possible sources for a given pin:
+	 *
+	 * https://lowrisc.github.io/sonata-system/doc/ip/pinmux/
+	 */
+	struct PinSinks : private utils::NoCopyNoMove
+	{
+		volatile uint8_t registers[NumPinSinks];
+
+		/// Returns a handle to a pin sink (an output pin).
+		Sink<PinSink> get(PinSink sink) volatile
+		{
+			return get_sink<PinSink>(registers, sink);
+		};
+	};
+
+	/**
+	 * A driver for the Sonata system's pin multiplexed block inputs.
+	 *
+	 * The Sonata system's Pin Multiplexer (pinmux) has two sets of registers:
+	 * the pin sink registers and the block sink registers. This structure
+	 * provides access to the block sinks registers. Block sinks are inputs into
+	 * the Sonata system's devices that can be connected to a number of system
+	 * input pins (their sources). The sources each sink can connect to are
+	 * limited. See the documentation for the possible sources for a given pin:
+	 *
+	 * https://lowrisc.github.io/sonata-system/doc/ip/pinmux
+	 */
+	struct BlockSinks : private utils::NoCopyNoMove
+	{
+		volatile uint8_t registers[NumBlockSinks];
+
+		/// Returns a handle to a block sink (a block input).
+		Sink<BlockSink> get(BlockSink sink) volatile
+		{
+			return get_sink<BlockSink>(registers, sink);
+		};
+	};
+} // namespace SonataPinmux
diff --git a/sdk/include/platform/sunburst/platform-rgbctrl.hh b/sdk/include/platform/sunburst/platform-rgbctrl.hh
index 1e5a153..067d9e0 100644
--- a/sdk/include/platform/sunburst/platform-rgbctrl.hh
+++ b/sdk/include/platform/sunburst/platform-rgbctrl.hh
@@ -33,23 +33,25 @@
 	uint32_t status;
 
 	/// Control Register Fields
-	enum [[clang::flag_enum]] ControlFields : uint32_t{
-	  /// Write 1 to set RGB LEDs to specified colours.
-	  ControlSet = 1 << 0,
-	  /**
-	   * Write 1 to turn off RGB LEDs.
-	   * Write to ControlSet to turn on again.
-	   */
-	  ControlOff = 1 << 1,
+	enum [[clang::flag_enum]] ControlFields : uint32_t
+	{
+		/// Write 1 to set RGB LEDs to specified colours.
+		ControlSet = 1 << 0,
+		/**
+		 * Write 1 to turn off RGB LEDs.
+		 * Write to ControlSet to turn on again.
+		 */
+		ControlOff = 1 << 1,
 	};
 
 	/// Status Register Fields
-	enum [[clang::flag_enum]] StatusFields : uint32_t{
-	  /**
-	   * When asserted controller is idle and new colours can be set,
-	   * otherwise writes to regLed0, regLed1, and control are ignored.
-	   */
-	  StatusIdle = 1 << 0,
+	enum [[clang::flag_enum]] StatusFields : uint32_t
+	{
+		/**
+		 * When asserted controller is idle and new colours can be set,
+		 * otherwise writes to regLed0, regLed1, and control are ignored.
+		 */
+		StatusIdle = 1 << 0,
 	};
 
 	/**
diff --git a/sdk/include/platform/sunburst/platform-simulation_exit.hh b/sdk/include/platform/sunburst/platform-simulation_exit.hh
new file mode 100644
index 0000000..a158585
--- /dev/null
+++ b/sdk/include/platform/sunburst/platform-simulation_exit.hh
@@ -0,0 +1,30 @@
+// Copyright Microsoft and CHERIoT Contributors.
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#ifdef SIMULATION
+#	include <stdint.h>
+#	include <platform-uart.hh>
+#	include <string_view>
+
+static void platform_simulation_exit(uint32_t code)
+{
+	auto uart =
+#	if DEVICE_EXISTS(uart0)
+	  MMIO_CAPABILITY(Uart, uart0);
+#	elif DEVICE_EXISTS(uart)
+	  MMIO_CAPABILITY(Uart, uart);
+#	else
+#		error No UART found in platform_simulation_exit
+#	endif
+	// Writing the following magic string to the UART will cause the sonata
+	// simulator to exit.
+	const char *magicString =
+	  "Safe to exit simulator.\xd8\xaf\xfb\xa0\xc7\xe1\xa9\xd7";
+	while (char ch = *magicString++)
+	{
+		uart->blocking_write(ch);
+	}
+}
+#endif
diff --git a/sdk/include/platform/sunburst/platform-spi.hh b/sdk/include/platform/sunburst/platform-spi.hh
index fd8aa19..e835640 100644
--- a/sdk/include/platform/sunburst/platform-spi.hh
+++ b/sdk/include/platform/sunburst/platform-spi.hh
@@ -2,53 +2,30 @@
 #include <cdefs.h>
 #include <debug.hh>
 #include <stdint.h>
+#include <utils.hh>
 
-/**
- * A Simple Driver for the Sonata's SPI.
- *
- * Documentation source can be found at:
- * https://github.com/lowRISC/sonata-system/blob/1a59633d2515d4fe186a07d53e49ff95c18d9bbf/doc/ip/spi.md
- *
- * Rendered documentation is served from:
- * https://lowrisc.org/sonata-system/doc/ip/spi.html
- */
-struct SonataSpi
+namespace SonataSpi
 {
-	/**
-	 * The Sonata SPI block doesn't currently have support for interrupts.
-	 * The following registers are reserved for future use.
-	 */
-	uint32_t interruptState;
-	uint32_t interruptEnable;
-	uint32_t interruptTest;
-	/**
-	 * Configuration register. Controls how the SPI block transmits and
-	 * receives data. This register can be modified only whilst the SPI block
-	 * is idle.
-	 */
-	uint32_t configuration;
-	/**
-	 * Controls the operation of the SPI block. This register can
-	 * be modified only whilst the SPI block is idle.
-	 */
-	uint32_t control;
-	/// Status information about the SPI block
-	uint32_t status;
-	/**
-	 * Writes to this begin an SPI operation.
-	 * Writes are ignored when the SPI block is active.
-	 */
-	uint32_t start;
-	/**
-	 * Data from the receive FIFO. When read the data is popped from the FIFO.
-	 * If the FIFO is empty data read is undefined.
-	 */
-	uint32_t receiveFifo;
-	/**
-	 * Bytes written here are pushed to the transmit FIFO. If the FIFO is full
-	 * writes are ignored.
-	 */
-	uint32_t transmitFifo;
+	/// Sonata SPI Interrupts
+	typedef enum [[clang::flag_enum]] : uint32_t
+	{
+		/// Raised when a SPI operation completes and the block has become idle.
+		InterruptComplete = 1 << 4,
+		/*
+		 * Asserted whilst the transmit FIFO level is at or below the
+		 * transmit watermark.
+		 */
+		InterruptTransmitWatermark = 1 << 3,
+		/// Asserted whilst the transmit FIFO is empty.
+		InterruptTransmitEmpty = 1 << 2,
+		/*
+		 * Asserted whilst the receive FIFO level is at or above the receive
+		 * watermark.
+		 */
+		InterruptReceiveWatermark = 1 << 1,
+		/// Asserted whilst the receive FIFO is full.
+		InterruptReceiveFull = 1 << 0,
+	} Interrupt;
 
 	/// Configuration Register Fields
 	enum : uint32_t
@@ -110,6 +87,14 @@
 		 * interrupt will trigger at different points
 		 */
 		ControlReceiveWatermarkMask = 0xf << 8,
+		/**
+		 * Internal loopback function enabled when set to 1.
+		 */
+		ControlInternalLoopback = 1 << 30,
+		/**
+		 * Software reset performed when written as 1.
+		 */
+		ControlSoftwareReset = 1u << 31,
 	};
 
 	/// Status Register Fields
@@ -140,96 +125,289 @@
 		StartByteCountMask = 0x7ffu,
 	};
 
-	/// Flag set when we're debugging this driver.
-	static constexpr bool DebugSonataSpi = false;
-
-	/// Helper for conditional debug logs and assertions.
-	using Debug = ConditionalDebug<DebugSonataSpi, "Sonata SPI">;
+	/// Info Register Fields
+	enum : uint32_t
+	{
+		/// Maximum number of items in the transmit FIFO.
+		InfoTxFifoDepth = 0xffu << 0,
+		/// Maximum number of items in the receive FIFO.
+		InfoRxFifoDepth = 0xffu << 8,
+	};
 
 	/**
-	 * Initialises the SPI block
+	 * A driver for the Sonata's SPI device block.
 	 *
-	 * @param ClockPolarity When false, the clock is low when idle and the
-	 *        leading edge is positive. When true, the opposite behaviour is
-	 *        set.
-	 * @param ClockPhase When false, data is sampled on the leading edge and
-	 *        changes on the trailing edge. When true, the opposite behaviour is
-	 *        set.
-	 * @param MsbFirst When true, the first bit of each byte sent is the most
-	 *        significant bit, as oppose to the least significant bit.
-	 * @param HalfClockPeriod The length of a half period of the SPI clock,
-	 *        measured in system clock cycles reduced by 1.
+	 * Documentation source can be found at:
+	 * https://github.com/lowRISC/sonata-system/blob/1a59633d2515d4fe186a07d53e49ff95c18d9bbf/doc/ip/spi.md
+	 *
+	 * Rendered documentation is served from:
+	 * https://lowrisc.org/sonata-system/doc/ip/spi.html
 	 */
-	void init(const bool     ClockPolarity,
-	          const bool     ClockPhase,
-	          const bool     MsbFirst,
-	          const uint16_t HalfClockPeriod) volatile
+	template<size_t NumChipSelects = 4>
+	struct Generic : private utils::NoCopyNoMove
 	{
-		configuration = (ClockPolarity ? ConfigurationClockPolarity : 0) |
-		                (ClockPhase ? ConfigurationClockPhase : 0) |
-		                (MsbFirst ? ConfigurationMSBFirst : 0) |
-		                (HalfClockPeriod & ConfigurationHalfClockPeriodMask);
-	}
+		/// The current state of the SPI interrupts.
+		uint32_t interruptState;
+		/// Controls which interrupts are enabled.
+		uint32_t interruptEnable;
+		/// Allows one to manually trigger an interrupt for testing.
+		uint32_t interruptTest;
+		/**
+		 * Configuration register. Controls how the SPI block transmits and
+		 * receives data. This register can be modified only whilst the SPI
+		 * block is idle.
+		 */
+		uint32_t configuration;
+		/**
+		 * Controls the operation of the SPI block. This register can
+		 * be modified only whilst the SPI block is idle.
+		 */
+		uint32_t control;
+		/// Status information about the SPI block
+		uint32_t status;
+		/**
+		 * Writes to this begin an SPI operation.
+		 * Writes are ignored when the SPI block is active.
+		 */
+		uint32_t start;
+		/**
+		 * Data from the receive FIFO. When read the data is popped from the
+		 * FIFO. If the FIFO is empty data read is undefined.
+		 */
+		uint32_t receiveFifo;
+		/**
+		 * Bytes written here are pushed to the transmit FIFO. If the FIFO is
+		 * full writes are ignored.
+		 */
+		uint32_t transmitFifo;
+		/**
+		 * Information about the SPI controller. This register reports the
+		 * depths of the transmit and receive FIFOs within the controller.
+		 */
+		uint32_t info;
+		/**
+		 * Chip Select lines; each bit controls a chip select line.
+		 * When a bit set to zero, a chip select line is pulled low.
+		 * Multiple chip select lines can be pulled low at a time.
+		 */
+		uint32_t chipSelects;
 
-	/// Waits for the SPI device to become idle
-	void wait_idle() volatile
-	{
-		// Wait whilst IDLE field in STATUS is low
-		while ((status & StatusIdle) == 0) {}
-	}
+		/// Flag set when we're debugging this driver.
+		static constexpr bool DebugSonataSpi = false;
 
-	/**
-	 * Sends `len` bytes from the given `data` buffer,
-	 * where `len` is at most `0x7ff`.
-	 */
-	void blocking_write(const uint8_t data[], uint16_t len) volatile
-	{
-		Debug::Assert(len <= 0x7ff,
-		              "You can't transfer more than 0x7ff bytes at a time.");
-		len &= StartByteCountMask;
+		/// Helper for conditional debug logs and assertions.
+		using Debug = ConditionalDebug<DebugSonataSpi, "Sonata SPI">;
 
-		wait_idle();
-		control = ControlTransmitEnable;
-		start   = len;
-
-		uint32_t transmitAvailable = 0;
-		for (uint32_t i = 0; i < len; ++i)
+		/// Enable the given interrupt(s).
+		inline void interrupt_enable(Interrupt interrupt) volatile
 		{
-			if (transmitAvailable == 0)
+			interruptEnable = interruptEnable | interrupt;
+		}
+
+		/// Disable the given interrupt(s).
+		inline void interrupt_disable(Interrupt interrupt) volatile
+		{
+			interruptEnable = interruptEnable & ~interrupt;
+		}
+
+		/**
+		 * Initialises the SPI block
+		 *
+		 * @param ClockPolarity When false, the clock is low when idle and the
+		 *        leading edge is positive. When true, the opposite behaviour is
+		 *        set.
+		 * @param ClockPhase When false, data is sampled on the leading edge and
+		 *        changes on the trailing edge. When true, the opposite
+		 * behaviour is set.
+		 * @param MsbFirst When true, the first bit of each byte sent is the
+		 * most significant bit, as oppose to the least significant bit.
+		 * @param HalfClockPeriod The length of a half period of the SPI clock,
+		 *        measured in system clock cycles reduced by 1.
+		 */
+		void init(const bool     ClockPolarity,
+		          const bool     ClockPhase,
+		          const bool     MsbFirst,
+		          const uint16_t HalfClockPeriod) volatile
+		{
+			configuration =
+			  (ClockPolarity ? ConfigurationClockPolarity : 0) |
+			  (ClockPhase ? ConfigurationClockPhase : 0) |
+			  (MsbFirst ? ConfigurationMSBFirst : 0) |
+			  (HalfClockPeriod & ConfigurationHalfClockPeriodMask);
+
+			// Ensure that FIFOs are emptied of any stale data and the
+			// controller core has returned to Idle if it is presently active
+			// (eg. an incomplete previous operation, perhaps one that was
+			// interrupted/failed).
+			//
+			// Note: although, from a logical perspective, the three operations
+			// (i) tx clear, (ii) core reset and (iii) rx clear should be
+			// performed in that order, presently the implementation supports
+			// performing them as a single write.
+			control =
+			  ControlTransmitClear | ControlSoftwareReset | ControlReceiveClear;
+		}
+
+		/// Waits for the SPI device to become idle
+		void wait_idle() volatile
+		{
+			// Wait whilst IDLE field in STATUS is low
+			while ((status & StatusIdle) == 0) {}
+		}
+
+		/**
+		 * Sends `len` bytes from the given `data` buffer,
+		 * where `len` is at most `0x7ff`.
+		 */
+		void blocking_write(const uint8_t data[], uint16_t len) volatile
+		{
+			Debug::Assert(
+			  len <= StartByteCountMask,
+			  "You can't transfer more than 0x7ff bytes at a time.");
+			len &= StartByteCountMask;
+
+			wait_idle();
+			// Do not attempt a zero-byte transfer; not supported by the
+			// controller.
+			if (len)
 			{
-				while (transmitAvailable < 64)
+				control = ControlTransmitEnable;
+				start   = len;
+
+				uint32_t transmitAvailable = 0;
+				for (uint32_t i = 0; i < len; ++i)
 				{
-					// Read number of bytes in TX FIFO to calculate space
-					// available for more bytes
-					transmitAvailable = 64 - (status & StatusTxFifoLevel);
+					while (!transmitAvailable)
+					{
+						// Read number of bytes in TX FIFO to calculate space
+						// available for more bytes
+						transmitAvailable = 8 - (status & StatusTxFifoLevel);
+					}
+					transmitFifo = data[i];
+					transmitAvailable--;
 				}
 			}
-			transmitFifo = data[i];
-			transmitAvailable--;
 		}
-	}
 
-	/*
-	 * Receives `len` bytes and puts them in the `data` buffer,
-	 * where `len` is at most `0x7ff`.
-	 *
-	 * This method will block until the requested number of bytes
-	 * has been seen. There is currently no timeout.
-	 */
-	void blocking_read(uint8_t data[], uint16_t len) volatile
-	{
-		Debug::Assert(len <= 0x7ff,
-		              "You can't receive more than 0x7ff bytes at a time.");
-		len &= StartByteCountMask;
-		wait_idle();
-		control = ControlReceiveEnable;
-		start   = len;
-
-		for (uint32_t i = 0; i < len; ++i)
+		/*
+		 * Receives `len` bytes and puts them in the `data` buffer,
+		 * where `len` is at most `0x7ff`.
+		 *
+		 * This method will block until the requested number of bytes
+		 * has been seen. There is currently no timeout.
+		 */
+		void blocking_read(uint8_t data[], uint16_t len) volatile
 		{
-			// Wait for at least one byte to be available in the RX FIFO
-			while ((status & StatusRxFifoLevel) == 0) {}
-			data[i] = static_cast<uint8_t>(receiveFifo);
+			Debug::Assert(len <= StartByteCountMask,
+			              "You can't receive more than 0x7ff bytes at a time.");
+			len &= StartByteCountMask;
+			wait_idle();
+			// Do not attempt a zero-byte transfer; not supported by the
+			// controller.
+			if (len)
+			{
+				control = ControlReceiveEnable;
+				start   = len;
+
+				for (uint32_t i = 0; i < len; ++i)
+				{
+					// Wait for at least one byte to be available in the RX FIFO
+					while ((status & StatusRxFifoLevel) == 0) {}
+					data[i] = static_cast<uint8_t>(receiveFifo);
+				}
+			}
 		}
-	}
-};
+
+		/**
+		 * Asserts/de-asserts a given chip select.
+		 *
+		 * Note, SPI chip selects are active low signals, so the register bit is
+		 * zero when asserted and one when de-asserted.
+		 *
+		 * @tparam Index The index of the chip select to be set.
+		 * @tparam DeassertOthers Whether to de-assert all other chip selects.
+		 * @param Assert Whether to assert (true) or de-assert (false).
+		 */
+		template<uint8_t Index, bool DeassertOthers = true>
+		inline void chip_select_assert(const bool Assert = true) volatile
+		{
+			static_assert(Index < NumChipSelects,
+			              "SPI chip select index out of bounds");
+
+			const uint32_t State =
+			  DeassertOthers ? (1 << NumChipSelects) - 1 : chipSelects;
+
+			const uint32_t Bit = (1 << Index);
+			chipSelects        = Assert ? State & ~Bit : State | Bit;
+		}
+	};
+
+	/// A specialised driver for the SPI device connected to the Ethernet MAC.
+	class EthernetMac : public Generic<2>
+	{
+		enum : uint8_t
+		{
+			ChipSelectLine = 0,
+			ResetLine      = 1,
+		};
+
+		public:
+		/**
+		 * Assert the chip select line.
+		 * @param Assert Whether to assert (true) or de-assert (false) the chip
+		 * select line.
+		 */
+		inline void chip_select_assert(const bool Assert) volatile
+		{
+			this->Generic<2>::chip_select_assert<ChipSelectLine, false>(Assert);
+		}
+		/**
+		 * Assert the reset line.
+		 * @param Assert Whether to assert (true) or de-assert (false) the reset
+		 * line.
+		 */
+		inline void reset_assert(const bool Assert = true) volatile
+		{
+			this->Generic<2>::chip_select_assert<ResetLine, false>(Assert);
+		}
+	};
+
+	/// A specialised driver for the SPI device connected to the LCD screen.
+	class Lcd : public Generic<3>
+	{
+		enum : uint8_t
+		{
+			ChipSelectLine  = 0,
+			DataCommandLine = 1,
+			ResetLine       = 2,
+		};
+
+		public:
+		/**
+		 * Assert the chip select line.
+		 * @param Assert Whether to assert (true) or de-assert (false) the chip
+		 * select line.
+		 */
+		inline void chip_select_assert(const bool Assert) volatile
+		{
+			this->Generic<3>::chip_select_assert<ChipSelectLine, false>(Assert);
+		}
+		/**
+		 * Assert the chip select line.
+		 * @param Assert Whether to assert (true) or de-assert (false) the reset
+		 * line.
+		 */
+		inline void reset_assert(const bool Assert = true) volatile
+		{
+			this->Generic<3>::chip_select_assert<ResetLine, false>(Assert);
+		}
+		/**
+		 * Set the data/command line.
+		 * @param high Whether to set high (true) or low (false).
+		 */
+		inline void data_command_set(const bool High) volatile
+		{
+			this->Generic<3>::chip_select_assert<DataCommandLine, false>(!High);
+		}
+	};
+} // namespace SonataSpi
diff --git a/sdk/include/platform/sunburst/platform-uart.hh b/sdk/include/platform/sunburst/platform-uart.hh
index 09f217d..6513112 100644
--- a/sdk/include/platform/sunburst/platform-uart.hh
+++ b/sdk/include/platform/sunburst/platform-uart.hh
@@ -75,36 +75,37 @@
 	uint32_t timeoutControl;
 
 	/// OpenTitan UART Interrupts
-	typedef enum [[clang::flag_enum]]
-	: uint32_t{
-	    /// Raised if the transmit FIFO is empty.
-	    InterruptTransmitEmpty = 1 << 8,
-	    /// Raised if the receiver has detected a parity error.
-	    InterruptReceiveParityErr = 1 << 7,
-	    /// Raised if the receive FIFO has characters remaining in the FIFO
-	    /// without being
-	    /// retreived for the programmed time period.
-	    InterruptReceiveTimeout = 1 << 6,
-	    /// Raised if break condition has been detected on receive.
-	    InterruptReceiveBreakErr = 1 << 5,
-	    /// Raised if a framing error has been detected on receive.
-	    InterruptReceiveFrameErr = 1 << 4,
-	    /// Raised if the receive FIFO has overflowed.
-	    InterruptReceiveOverflow = 1 << 3,
-	    /// Raised if the transmit FIFO has emptied and no transmit is ongoing.
-	    InterruptTransmitDone = 1 << 2,
-	    /// Raised if the receive FIFO is past the high-water mark.
-	    InterruptReceiveWatermark = 1 << 1,
-	    /// Raised if the transmit FIFO is past the high-water mark.
-	    InterruptTransmitWatermark = 1 << 0,
-	  } OpenTitanUartInterrupt;
+	typedef enum [[clang::flag_enum]] : uint32_t
+	{
+		/// Raised if the transmit FIFO is empty.
+		InterruptTransmitEmpty = 1 << 8,
+		/// Raised if the receiver has detected a parity error.
+		InterruptReceiveParityErr = 1 << 7,
+		/// Raised if the receive FIFO has characters remaining in the FIFO
+		/// without being
+		/// retreived for the programmed time period.
+		InterruptReceiveTimeout = 1 << 6,
+		/// Raised if break condition has been detected on receive.
+		InterruptReceiveBreakErr = 1 << 5,
+		/// Raised if a framing error has been detected on receive.
+		InterruptReceiveFrameErr = 1 << 4,
+		/// Raised if the receive FIFO has overflowed.
+		InterruptReceiveOverflow = 1 << 3,
+		/// Raised if the transmit FIFO has emptied and no transmit is ongoing.
+		InterruptTransmitDone = 1 << 2,
+		/// Raised if the receive FIFO is past the high-water mark.
+		InterruptReceiveWatermark = 1 << 1,
+		/// Raised if the transmit FIFO is past the high-water mark.
+		InterruptTransmitWatermark = 1 << 0,
+	} OpenTitanUartInterrupt;
 
 	/// FIFO Control Register Fields
-	enum [[clang::flag_enum]] : uint32_t{
-	  /// Reset the transmit FIFO.
-	  FifoControlTransmitReset = 1 << 1,
-	  /// Reset the receive FIFO.
-	  FifoControlReceiveReset = 1 << 0,
+	enum [[clang::flag_enum]] : uint32_t
+	{
+		/// Reset the transmit FIFO.
+		FifoControlTransmitReset = 1 << 1,
+		/// Reset the receive FIFO.
+		FifoControlReceiveReset = 1 << 0,
 	};
 
 	/// Control Register Fields
diff --git a/sdk/include/platform/sunburst/platform-usbdev.hh b/sdk/include/platform/sunburst/platform-usbdev.hh
index 1a279bc..cc03ea5 100644
--- a/sdk/include/platform/sunburst/platform-usbdev.hh
+++ b/sdk/include/platform/sunburst/platform-usbdev.hh
@@ -250,9 +250,9 @@
 	[[nodiscard]] uint64_t supply_buffers(uint64_t bufferBitmap) volatile
 	{
 		constexpr uint32_t SetupFullBit =
-		  uint32_t(UsbStatusField::AvailableSetupFull);
+		  static_cast<uint32_t>(UsbStatusField::AvailableSetupFull);
 		constexpr uint32_t OutFullBit =
-		  uint32_t(UsbStatusField::AvailableOutFull);
+		  static_cast<uint32_t>(UsbStatusField::AvailableOutFull);
 
 		for (uint8_t index = 0; index < BufferCount; index++)
 		{
@@ -286,7 +286,7 @@
 	 */
 	void interrupt_enable(UsbdevInterrupt interrupt) volatile
 	{
-		interruptEnable = interruptEnable | uint32_t(interrupt);
+		interruptEnable = interruptEnable | static_cast<uint32_t>(interrupt);
 	}
 
 	/**
@@ -294,7 +294,7 @@
 	 */
 	void interrupt_disable(UsbdevInterrupt interrupt) volatile
 	{
-		interruptEnable = interruptEnable & ~uint32_t(interrupt);
+		interruptEnable = interruptEnable & ~static_cast<uint32_t>(interrupt);
 	}
 
 	/**
@@ -310,8 +310,10 @@
 	 */
 	[[nodiscard]] int init(uint64_t &bufferBitmap) volatile
 	{
-		bufferBitmap = supply_buffers((uint64_t(1u) << BufferCount) - 1u);
-		phyConfig    = uint32_t(PhyConfigField::UseDifferentialReceiver);
+		bufferBitmap =
+		  supply_buffers((static_cast<uint64_t>(1u) << BufferCount) - 1u);
+		phyConfig =
+		  static_cast<uint32_t>(PhyConfigField::UseDifferentialReceiver);
 		return 0;
 	}
 
@@ -404,7 +406,8 @@
 		{
 			return -1;
 		}
-		usbControl = usbControl | uint32_t(UsbControlField::Enable);
+		usbControl =
+		  usbControl | static_cast<uint32_t>(UsbControlField::Enable);
 		return 0;
 	}
 
@@ -415,7 +418,8 @@
 	 */
 	void disconnect() volatile
 	{
-		usbControl = usbControl & ~uint32_t(UsbControlField::Enable);
+		usbControl =
+		  usbControl & ~static_cast<uint32_t>(UsbControlField::Enable);
 	}
 
 	/**
@@ -425,7 +429,7 @@
 	 */
 	[[nodiscard]] bool connected() volatile
 	{
-		return (usbControl & uint32_t(UsbControlField::Enable));
+		return (usbControl & static_cast<uint32_t>(UsbControlField::Enable));
 	}
 
 	/**
@@ -443,8 +447,9 @@
 		{
 			return -1; // Device addresses are only 7 bits long.
 		}
-		constexpr uint32_t Mask = uint32_t(UsbControlField::DeviceAddress);
-		usbControl              = (usbControl & ~Mask) | (address << 16);
+		constexpr uint32_t Mask =
+		  static_cast<uint32_t>(UsbControlField::DeviceAddress);
+		usbControl = (usbControl & ~Mask) | (address << 16);
 		return 0;
 	}
 
@@ -463,8 +468,9 @@
 	[[nodiscard]] int retrieve_collected_packet(uint8_t &endpointId,
 	                                            uint8_t &bufferId) volatile
 	{
-		constexpr uint32_t BufferIdMask = uint32_t(ConfigInField::BufferId);
-		uint32_t           sent         = inSent;
+		constexpr uint32_t BufferIdMask =
+		  static_cast<uint32_t>(ConfigInField::BufferId);
+		uint32_t sent = inSent;
 
 		// Clear the first encountered packet sent indication.
 		for (endpointId = 0; endpointId < MaxEndpoints; endpointId++)
@@ -504,9 +510,10 @@
 			usbdev_transfer(buffer(bufferId), data, size, true);
 		}
 
-		constexpr uint32_t ReadyBit = uint32_t(ConfigInField::Ready);
-		configIn[endpointId]        = bufferId | (size << 8);
-		configIn[endpointId]        = configIn[endpointId] | ReadyBit;
+		constexpr uint32_t ReadyBit =
+		  static_cast<uint32_t>(ConfigInField::Ready);
+		configIn[endpointId] = bufferId | (size << 8);
+		configIn[endpointId] = configIn[endpointId] | ReadyBit;
 	}
 
 	/// The information associated with a received packet
@@ -516,22 +523,28 @@
 		/// The endpoint ID the received packet was received on
 		constexpr uint8_t endpoint_id()
 		{
-			return (info & uint32_t(ReceiveBufferField::EndpointId)) >> 20;
+			return (info &
+			        static_cast<uint32_t>(ReceiveBufferField::EndpointId)) >>
+			       20;
 		}
 		/// The size of the received packet
 		constexpr uint16_t size()
 		{
-			return (info & uint32_t(ReceiveBufferField::Size)) >> 8;
+			return (info & static_cast<uint32_t>(ReceiveBufferField::Size)) >>
+			       8;
 		}
 		/// Whether the received packet was a setup packet
 		constexpr bool is_setup()
 		{
-			return (info & uint32_t(ReceiveBufferField::Setup)) != 0;
+			return (info & static_cast<uint32_t>(ReceiveBufferField::Setup)) !=
+			       0;
 		}
 		/// The buffer ID used to store the received packet
 		constexpr uint8_t buffer_id()
 		{
-			return (info & uint32_t(ReceiveBufferField::BufferId)) >> 0;
+			return (info &
+			        static_cast<uint32_t>(ReceiveBufferField::BufferId)) >>
+			       0;
 		}
 	};
 
@@ -546,7 +559,7 @@
 	 */
 	[[nodiscard]] std::optional<ReceiveBufferInfo> packet_take() volatile
 	{
-		if (!(usbStatus & uint32_t(UsbStatusField::ReceiveDepth)))
+		if (!(usbStatus & static_cast<uint32_t>(UsbStatusField::ReceiveDepth)))
 		{
 			return {}; // No packets received
 		}
diff --git a/sdk/include/platform/sunburst/v0.2/platform-ethernet.hh b/sdk/include/platform/sunburst/v0.2/platform-ethernet.hh
new file mode 100644
index 0000000..f4bf3e3
--- /dev/null
+++ b/sdk/include/platform/sunburst/v0.2/platform-ethernet.hh
@@ -0,0 +1,822 @@
+#pragma once
+#include <array>
+#include <cheri.hh>
+#include <cstdint>
+#include <debug.hh>
+#include <futex.h>
+#include <interrupt.h>
+#include <locks.hh>
+#include <optional>
+#include <platform/concepts/ethernet.hh>
+#include <platform/sunburst/platform-gpio.hh>
+#include <platform/sunburst/v0.2/platform-spi.hh>
+#include <thread.h>
+
+DECLARE_AND_DEFINE_INTERRUPT_CAPABILITY(EthernetInterruptCapability,
+                                        InterruptName::EthernetInterrupt,
+                                        true,
+                                        true);
+
+/**
+ * The driver for KSZ8851 SPI Ethernet MAC.
+ */
+class Ksz8851Ethernet
+{
+	/**
+	 * Flag set when we're debugging this driver.
+	 */
+	static constexpr bool DebugEthernet = false;
+
+	/**
+	 * Flag set to log messages when frames are dropped.
+	 */
+	static constexpr bool DebugDroppedFrames = true;
+
+	/**
+	 * Maxmium size of a single Ethernet frame.
+	 */
+	static constexpr uint16_t MaxFrameSize = 1500;
+
+	/**
+	 * Helper for conditional debug logs and assertions.
+	 */
+	using Debug = ConditionalDebug<DebugEthernet, "Ethernet driver">;
+
+	/**
+	 * Helper for conditional debug logs and assertions for dropped frames.
+	 */
+	using DebugFrameDrops =
+	  ConditionalDebug<DebugDroppedFrames, "Ethernet driver">;
+
+	/**
+	 * Import the Capability helper from the CHERI namespace.
+	 */
+	template<typename T>
+	using Capability = CHERI::Capability<T>;
+
+	/**
+	 * GPIO output pins to be used
+	 */
+	enum class GpioPin : uint8_t
+	{
+		EthernetChipSelect = 13,
+		EthernetReset      = 14,
+	};
+
+	/**
+	 * SPI commands
+	 */
+	enum class SpiCommand : uint8_t
+	{
+		ReadRegister  = 0b00,
+		WriteRegister = 0b01,
+		// DMA in this context means that the Ethernet MAC is DMA directly
+		// from the SPI interface into its internal buffer, so it takes single
+		// SPI transaction for the entire frame. It is unrelated to whether
+		// SPI driver uses PIO or DMA for the SPI transaction.
+		ReadDma  = 0b10,
+		WriteDma = 0b11,
+	};
+
+	/**
+	 * The location of registers
+	 */
+	enum class RegisterOffset : uint8_t
+	{
+		ChipConfiguration = 0x08,
+		MacAddressLow     = 0x10,
+		MacAdressMiddle   = 0x12,
+		MacAddressHigh    = 0x14,
+		OnChipBusControl  = 0x20,
+		EepromControl     = 0x22,
+		MemoryBistInfo    = 0x24,
+		GlobalReset       = 0x26,
+
+		/* Wakeup frame registers omitted */
+
+		TransmitControl               = 0x70,
+		TransmitStatus                = 0x72,
+		ReceiveControl1               = 0x74,
+		ReceiveControl2               = 0x76,
+		TransmitQueueMemoryInfo       = 0x78,
+		ReceiveFrameHeaderStatus      = 0x7C,
+		ReceiveFrameHeaderByteCount   = 0x7E,
+		TransmitQueueCommand          = 0x80,
+		ReceiveQueueCommand           = 0x82,
+		TransmitFrameDataPointer      = 0x84,
+		ReceiveFrameDataPointer       = 0x86,
+		ReceiveDurationTimerThreshold = 0x8C,
+		ReceiveDataByteCountThreshold = 0x8E,
+		InterruptEnable               = 0x90,
+		InterruptStatus               = 0x92,
+		ReceiveFrameCountThreshold    = 0x9c,
+		TransmitNextTotalFrameSize    = 0x9E,
+
+		/* MAC address hash table registers omitted */
+
+		FlowControlLowWatermark               = 0xB0,
+		FlowControlHighWatermark              = 0xB2,
+		FlowControlOverrunWatermark           = 0xB4,
+		ChipIdEnable                          = 0xC0,
+		ChipGlobalControl                     = 0xC6,
+		IndirectAccessControl                 = 0xC8,
+		IndirectAccessDataLow                 = 0xD0,
+		IndirectAccessDataHigh                = 0xD2,
+		PowerManagementEventControl           = 0xD4,
+		GoSleepWakeUp                         = 0xD4,
+		PhyReset                              = 0xD4,
+		Phy1MiiBasicControl                   = 0xE4,
+		Phy1MiiBasicStatus                    = 0xE6,
+		Phy1IdLow                             = 0xE8,
+		Phy1High                              = 0xEA,
+		Phy1AutoNegotiationAdvertisement      = 0xEC,
+		Phy1AutoNegotiationLinkPartnerAbility = 0xEE,
+		Phy1SpecialControlStatus              = 0xF4,
+		Port1Control                          = 0xF6,
+		Port1Status                           = 0xF8,
+	};
+
+	using MACAddress = std::array<uint8_t, 6>;
+
+	/**
+	 * Flag bits of the TransmitControl register.
+	 */
+	enum [[clang::flag_enum]] TransmitControl : uint16_t{
+	  TransmitEnable                 = 1 << 0,
+	  TransmitCrcEnable              = 1 << 1,
+	  TransmitPaddingEnable          = 1 << 2,
+	  TransmitFlowControlEnable      = 1 << 3,
+	  FlushTransmitQueue             = 1 << 4,
+	  TransmitChecksumGenerationIp   = 1 << 5,
+	  TransmitChecksumGenerationTcp  = 1 << 6,
+	  TransmitChecksumGenerationIcmp = 1 << 9,
+	};
+
+	/**
+	 * Flag bits of the ReceiveControl1 register.
+	 */
+	enum [[clang::flag_enum]] ReceiveControl1 : uint16_t{
+	  ReceiveEnable                                        = 1 << 0,
+	  ReceiveInverseFilter                                 = 1 << 1,
+	  ReceiveAllEnable                                     = 1 << 4,
+	  ReceiveUnicastEnable                                 = 1 << 5,
+	  ReceiveMulticastEnable                               = 1 << 6,
+	  ReceiveBroadcastEnable                               = 1 << 7,
+	  ReceiveMulticastAddressFilteringWithMacAddressEnable = 1 << 8,
+	  ReceiveErrorFrameEnable                              = 1 << 9,
+	  ReceiveFlowControlEnable                             = 1 << 10,
+	  ReceivePhysicalAddressFilteringWithMacAddressEnable  = 1 << 11,
+	  ReceiveIpFrameChecksumCheckEnable                    = 1 << 12,
+	  ReceiveTcpFrameChecksumCheckEnable                   = 1 << 13,
+	  ReceiveUdpFrameChecksumCheckEnable                   = 1 << 14,
+	  FlushReceiveQueue                                    = 1 << 15,
+	};
+
+	/**
+	 * Flag bits of the ReceiveControl2 register.
+	 */
+	enum [[clang::flag_enum]] ReceiveControl2 : uint16_t{
+	  ReceiveSourceAddressFiltering            = 1 << 0,
+	  ReceiveIcmpFrameChecksumEnable           = 1 << 1,
+	  UdpLiteFrameEnable                       = 1 << 2,
+	  ReceiveIpv4Ipv6UdpFrameChecksumEqualZero = 1 << 3,
+	  ReceiveIpv4Ipv6FragmentFramePass         = 1 << 4,
+	  DataBurst4Bytes                          = 0b000 << 5,
+	  DataBurst8Bytes                          = 0b001 << 5,
+	  DataBurst16Bytes                         = 0b010 << 5,
+	  DataBurst32Bytes                         = 0b011 << 5,
+	  DataBurstSingleFrame                     = 0b100 << 5,
+	};
+
+	/**
+	 * Flag bits of the ReceiveFrameHeaderStatus register.
+	 */
+	enum [[clang::flag_enum]] ReceiveFrameHeaderStatus : uint16_t{
+	  ReceiveCrcError                = 1 << 0,
+	  ReceiveRuntFrame               = 1 << 1,
+	  ReceiveFrameTooLong            = 1 << 2,
+	  ReceiveFrameType               = 1 << 3,
+	  ReceiveMiiError                = 1 << 4,
+	  ReceiveUnicastFrame            = 1 << 5,
+	  ReceiveMulticastFrame          = 1 << 6,
+	  ReceiveBroadcastFrame          = 1 << 7,
+	  ReceiveUdpFrameChecksumStatus  = 1 << 10,
+	  ReceiveTcpFrameChecksumStatus  = 1 << 11,
+	  ReceiveIpFrameChecksumStatus   = 1 << 12,
+	  ReceiveIcmpFrameChecksumStatus = 1 << 13,
+	  ReceiveFrameValid              = 1 << 15,
+	};
+
+	/**
+	 * Flag bits of the ReceiveQueueCommand register.
+	 */
+	enum [[clang::flag_enum]] ReceiveQueueCommand : uint16_t{
+	  ReleaseReceiveErrorFrame            = 1 << 0,
+	  StartDmaAccess                      = 1 << 3,
+	  AutoDequeueReceiveQueueFrameEnable  = 1 << 4,
+	  ReceiveFrameCountThresholdEnable    = 1 << 5,
+	  ReceiveDataByteCountThresholdEnable = 1 << 6,
+	  ReceiveDurationTimerThresholdEnable = 1 << 7,
+	  ReceiveIpHeaderTwoByteOffsetEnable  = 1 << 9,
+	  ReceiveFrameCountThresholdStatus    = 1 << 10,
+	  ReceiveDataByteCountThresholdstatus = 1 << 11,
+	  ReceiveDurationTimerThresholdStatus = 1 << 12,
+	};
+
+	/**
+	 * Flag bits of the TransmitQueueCommand register.
+	 */
+	enum [[clang::flag_enum]] TransmitQueueCommand : uint16_t{
+	  ManualEnqueueTransmitQueueFrameEnable = 1 << 0,
+	  TransmitQueueMemoryAvailableMonitor   = 1 << 1,
+	  AutoEnqueueTransmitQueueFrameEnable   = 1 << 2,
+	};
+
+	/**
+	 * Flag bits of the TransmitFrameDataPointer and ReceiveFrameDataPointer
+	 * register.
+	 */
+	enum [[clang::flag_enum]] FrameDataPointer : uint16_t{
+	  /**
+	   * When this bit is set, the frame data pointer register increments
+	   * automatically on accesses to the data register.
+	   */
+	  FrameDataPointerAutoIncrement = 1 << 14,
+	};
+
+	/**
+	 * Flags bits of the InterruptStatus and InterruptEnable registers.
+	 */
+	enum [[clang::flag_enum]] Interrupt : uint16_t{
+	  EnergyDetectInterrupt             = 1 << 2,
+	  LinkupDetectInterrupt             = 1 << 3,
+	  ReceiveMagicPacketDetectInterrupt = 1 << 4,
+	  ReceiveWakeupFrameDetectInterrupt = 1 << 5,
+	  TransmitSpaceAvailableInterrupt   = 1 << 6,
+	  ReceiveProcessStoppedInterrupt    = 1 << 7,
+	  TransmitProcessStoppedInterrupt   = 1 << 8,
+	  ReceiveOverrunInterrupt           = 1 << 11,
+	  ReceiveInterrupt                  = 1 << 13,
+	  TransmitInterrupt                 = 1 << 14,
+	  LinkChangeInterruptStatus         = 1 << 15,
+	};
+
+	/**
+	 * Flags bits of the Port1Control register.
+	 */
+	enum [[clang::flag_enum]] Port1Control : uint16_t{
+	  Advertised10BTHalfDuplexCapability  = 1 << 0,
+	  Advertised10BTFullDuplexCapability  = 1 << 1,
+	  Advertised100BTHalfDuplexCapability = 1 << 2,
+	  Advertised100BTFullDuplexCapability = 1 << 3,
+	  AdvertisedFlowControlCapability     = 1 << 4,
+	  ForceDuplex                         = 1 << 5,
+	  ForceSpeed                          = 1 << 6,
+	  AutoNegotiationEnable               = 1 << 7,
+	  ForceMDIX                           = 1 << 9,
+	  DisableAutoMDIMDIX                  = 1 << 10,
+	  RestartAutoNegotiation              = 1 << 13,
+	  TransmitterDisable                  = 1 << 14,
+	  LedOff                              = 1 << 15,
+	};
+
+	/**
+	 * Flags bits of the Port1Status register.
+	 */
+	enum [[clang::flag_enum]] Port1Status : uint16_t{
+	  Partner10BTHalfDuplexCapability  = 1 << 0,
+	  Partner10BTFullDuplexCapability  = 1 << 1,
+	  Partner100BTHalfDuplexCapability = 1 << 2,
+	  Partner100BTFullDuplexCapability = 1 << 3,
+	  PartnerFlowControlCapability     = 1 << 4,
+	  LinkGood                         = 1 << 5,
+	  AutoNegotiationDone              = 1 << 6,
+	  MDIXStatus                       = 1 << 7,
+	  OperationDuplex                  = 1 << 9,
+	  OperationSpeed                   = 1 << 10,
+	  PolarityReverse                  = 1 << 13,
+	  HPMDIX                           = 1 << 15,
+	};
+
+	/**
+	 * The futex used to wait for interrupts when packets are available to
+	 * receive.
+	 */
+	const uint32_t *receiveInterruptFutex;
+
+	/**
+	 * Set value of a GPIO output.
+	 */
+	inline void set_gpio_output_bit(GpioPin pin, bool value) const
+	{
+		uint32_t shift  = static_cast<uint8_t>(pin);
+		uint32_t output = gpio()->output;
+		output &= ~(1 << shift);
+		output |= value << shift;
+		gpio()->output = output;
+	}
+
+	/**
+	 * Read a register from the KSZ8851.
+	 */
+	[[nodiscard]] uint16_t register_read(RegisterOffset reg) const
+	{
+		// KSZ8851 command have the following format:
+		//
+		// First byte:
+		// +---------+-------------+-------------------+
+		// | 7     6 | 5         2 | 1               0 |
+		// +---------+-------------+-------------------+
+		// | Command | Byte Enable | Address (bit 7-6) |
+		// +---------+-------------+-------------------+
+		//
+		// Second byte (for register read/write only):
+		// +-------------------+--------+
+		// | 7               4 | 3    0 |
+		// +-------------------+--------+
+		// | Address (bit 5-2) | Unused |
+		// +-------------------+--------+
+		//
+		// Note that the access is 32-bit since bit 1 & 0 of the address is not
+		// included. KSZ8851 have 16-bit registers so byte enable is used to
+		// determine which register to access from the 32 bits specified by the
+		// address.
+		uint8_t addr       = static_cast<uint8_t>(reg);
+		uint8_t byteEnable = (addr & 0x2) == 0 ? 0b0011 : 0b1100;
+		uint8_t bytes[2];
+		bytes[0] = (static_cast<uint8_t>(SpiCommand::ReadRegister) << 6) |
+		           (byteEnable << 2) | (addr >> 6);
+		bytes[1] = (addr << 2) & 0b11110000;
+
+		set_gpio_output_bit(GpioPin::EthernetChipSelect, false);
+		spi()->blocking_write(bytes, sizeof(bytes));
+		uint16_t val;
+		spi()->blocking_read(reinterpret_cast<uint8_t *>(&val), sizeof(val));
+		set_gpio_output_bit(GpioPin::EthernetChipSelect, true);
+		return val;
+	}
+
+	/**
+	 * Write a register to KSZ8851.
+	 */
+	void register_write(RegisterOffset reg, uint16_t val) const
+	{
+		// See register_read for command format.
+		uint8_t addr       = static_cast<uint8_t>(reg);
+		uint8_t byteEnable = (addr & 0x2) == 0 ? 0b0011 : 0b1100;
+		uint8_t bytes[2];
+		bytes[0] = (static_cast<uint8_t>(SpiCommand::WriteRegister) << 6) |
+		           (byteEnable << 2) | (addr >> 6);
+		bytes[1] = (addr << 2) & 0b11110000;
+
+		set_gpio_output_bit(GpioPin::EthernetChipSelect, false);
+		spi()->blocking_write(bytes, sizeof(bytes));
+		spi()->blocking_write(reinterpret_cast<uint8_t *>(&val), sizeof(val));
+		spi()->wait_idle();
+		set_gpio_output_bit(GpioPin::EthernetChipSelect, true);
+	}
+
+	/**
+	 * Set bits in a KSZ8851 register.
+	 */
+	void register_set(RegisterOffset reg, uint16_t mask) const
+	{
+		uint16_t old = register_read(reg);
+		register_write(reg, old | mask);
+	}
+
+	/**
+	 * Clear bits in a KSZ8851 register.
+	 */
+	void register_clear(RegisterOffset reg, uint16_t mask) const
+	{
+		uint16_t old = register_read(reg);
+		register_write(reg, old & ~mask);
+	}
+
+	/**
+	 * Helper.  Returns a pointer to the SPI device.
+	 */
+	[[nodiscard, gnu::always_inline]] Capability<volatile SonataSpi> spi() const
+	{
+		return MMIO_CAPABILITY(SonataSpi, spi2);
+	}
+
+	/**
+	 * Helper.  Returns a pointer to the GPIO device.
+	 */
+	[[nodiscard, gnu::always_inline]] Capability<volatile SonataGPIO>
+	gpio() const
+	{
+		return MMIO_CAPABILITY(SonataGPIO, gpio);
+	}
+
+	/**
+	 * Number of frames yet to be received since last interrupt acknowledgement.
+	 */
+	uint16_t framesToProcess = 0;
+
+	/**
+	 * Mutex protecting transmitBuffer if send_frame is reentered.
+	 */
+	RecursiveMutex transmitBufferMutex;
+
+	/**
+	 * Buffer used by send_frame.
+	 */
+	std::unique_ptr<uint8_t[]> transmitBuffer;
+
+	/**
+	 * Mutex protecting receiveBuffer if receive_frame is called before a
+	 * previous returned frame is dropped.
+	 */
+	RecursiveMutex receiveBufferMutex;
+
+	/**
+	 * Reads and writes of the GPIO space use the same bits of the MMIO region
+	 * and so need to be protected.
+	 */
+	FlagLockPriorityInherited gpioLock;
+
+	/**
+	 * Buffer used by receive_frame.
+	 */
+	std::unique_ptr<uint8_t[]> receiveBuffer;
+
+	public:
+	/**
+	 * Initialise a reference to the Ethernet device.
+	 */
+	Ksz8851Ethernet()
+	{
+		transmitBuffer = std::make_unique<uint8_t[]>(MaxFrameSize);
+		receiveBuffer  = std::make_unique<uint8_t[]>(MaxFrameSize);
+
+		// Reset chip. It needs to be hold in reset for at least 10ms.
+		set_gpio_output_bit(GpioPin::EthernetReset, false);
+		thread_millisecond_wait(20);
+		set_gpio_output_bit(GpioPin::EthernetReset, true);
+
+		uint16_t chipId = register_read(RegisterOffset::ChipIdEnable);
+		Debug::log("Chip ID is {}", chipId);
+
+		// Check the chip ID. The last nibble is revision ID and can be ignored.
+		Debug::Assert((chipId & 0xFFF0) == 0x8870, "Unexpected Chip ID");
+
+		// This is the initialisation sequence suggested by the programmer's
+		// guide.
+		register_write(RegisterOffset::TransmitFrameDataPointer,
+		               FrameDataPointer::FrameDataPointerAutoIncrement);
+		register_write(RegisterOffset::TransmitControl,
+		               TransmitControl::TransmitCrcEnable |
+		                 TransmitControl::TransmitPaddingEnable |
+		                 TransmitControl::TransmitFlowControlEnable |
+		                 TransmitControl::TransmitChecksumGenerationIp |
+		                 TransmitControl::TransmitChecksumGenerationTcp |
+		                 TransmitControl::TransmitChecksumGenerationIcmp);
+		register_write(RegisterOffset::ReceiveFrameDataPointer,
+		               FrameDataPointer::FrameDataPointerAutoIncrement);
+		// Configure Receive Frame Threshold for one frame.
+		register_write(RegisterOffset::ReceiveFrameCountThreshold, 0x0001);
+		register_write(RegisterOffset::ReceiveControl1,
+		               ReceiveControl1::ReceiveUnicastEnable |
+		                 ReceiveControl1::ReceiveMulticastEnable |
+		                 ReceiveControl1::ReceiveBroadcastEnable |
+		                 ReceiveControl1::ReceiveFlowControlEnable |
+		                 ReceiveControl1::
+		                   ReceivePhysicalAddressFilteringWithMacAddressEnable |
+		                 ReceiveControl1::ReceiveIpFrameChecksumCheckEnable |
+		                 ReceiveControl1::ReceiveTcpFrameChecksumCheckEnable |
+		                 ReceiveControl1::ReceiveUdpFrameChecksumCheckEnable);
+		// The frame data burst field in this register controls how many data
+		// from a frame is read per DMA operation. The programmer's guide has a
+		// 4 byte burst, but to reduce SPI transactions and improve performance
+		// we choose to use single-frame data burst which reads the entire
+		// Ethernet frame in a single SPI DMA.
+		register_write(
+		  RegisterOffset::ReceiveControl2,
+		  ReceiveControl2::UdpLiteFrameEnable |
+		    ReceiveControl2::ReceiveIpv4Ipv6UdpFrameChecksumEqualZero |
+		    ReceiveControl2::ReceiveIpv4Ipv6FragmentFramePass |
+		    ReceiveControl2::DataBurstSingleFrame);
+		register_write(
+		  RegisterOffset::ReceiveQueueCommand,
+		  ReceiveQueueCommand::ReceiveFrameCountThresholdEnable |
+		    ReceiveQueueCommand::AutoDequeueReceiveQueueFrameEnable);
+
+		// Programmer's guide have a step to set the chip in half-duplex when
+		// negotiation failed, but we omit the step since non-switching hubs and
+		// half-duplex Ethernet is rarely used these days.
+
+		register_set(RegisterOffset::Port1Control,
+		             Port1Control::RestartAutoNegotiation);
+
+		// Configure Low Watermark to 6KByte available buffer space out of
+		// 12KByte (unit is 4 bytes).
+		register_write(RegisterOffset::FlowControlLowWatermark, 0x0600);
+		// Configure High Watermark to 4KByte available buffer space out of
+		// 12KByte (unit is 4 bytes).
+		register_write(RegisterOffset::FlowControlHighWatermark, 0x0400);
+
+		// Clear the interrupt status
+		register_write(RegisterOffset::InterruptStatus, 0xFFFF);
+		receiveInterruptFutex =
+		  interrupt_futex_get(STATIC_SEALED_VALUE(EthernetInterruptCapability));
+		// Enable Receive interrupt
+		register_write(RegisterOffset::InterruptEnable, ReceiveInterrupt);
+
+		// Enable QMU Transmit.
+		register_set(RegisterOffset::TransmitControl,
+		             TransmitControl::TransmitEnable);
+		// Enable QMU Receive.
+		register_set(RegisterOffset::ReceiveControl1,
+		             ReceiveControl1::ReceiveEnable);
+	}
+
+	Ksz8851Ethernet(const Ksz8851Ethernet &) = delete;
+	Ksz8851Ethernet(Ksz8851Ethernet &&)      = delete;
+
+	/**
+	 * This device does not have a unique MAC address and so users must provide
+	 * a locally administered MAC address if more than one device is present on
+	 * the same network.
+	 */
+	static constexpr bool has_unique_mac_address()
+	{
+		return false;
+	}
+
+	static constexpr MACAddress mac_address_default()
+	{
+		return {0x3a, 0x30, 0x25, 0x24, 0xfe, 0x7a};
+	}
+
+	void mac_address_set(MACAddress address = mac_address_default())
+	{
+		register_write(RegisterOffset::MacAddressHigh,
+		               (address[0] << 8) | address[1]);
+		register_write(RegisterOffset::MacAdressMiddle,
+		               (address[2] << 8) | address[3]);
+		register_write(RegisterOffset::MacAddressLow,
+		               (address[4] << 8) | address[5]);
+	}
+
+	uint32_t receive_interrupt_value()
+	{
+		return *receiveInterruptFutex;
+	}
+
+	int receive_interrupt_complete(Timeout *timeout,
+	                               uint32_t lastInterruptValue)
+	{
+		// If there are frames to process, do not enter wait.
+		if (framesToProcess)
+		{
+			return 0;
+		}
+
+		// Our interrupt is level-triggered; if a frame happens to arrive
+		// between `receive_frame` call and we marking interrupt as received,
+		// it will trigger again immediately after we acknowledge it.
+
+		// Acknowledge the interrupt in the scheduler.
+		interrupt_complete(STATIC_SEALED_VALUE(EthernetInterruptCapability));
+		if (*receiveInterruptFutex == lastInterruptValue)
+		{
+			Debug::log("Acknowledged interrupt, sleeping on futex {}",
+			           receiveInterruptFutex);
+			return futex_timed_wait(
+			  timeout, receiveInterruptFutex, lastInterruptValue);
+		}
+		Debug::log("Scheduler announces interrupt has fired");
+		return 0;
+	}
+
+	/**
+	 * Simple class representing a received Ethernet frame.
+	 */
+	class Frame
+	{
+		public:
+		uint16_t            length;
+		Capability<uint8_t> buffer;
+
+		private:
+		friend class Ksz8851Ethernet;
+		LockGuard<RecursiveMutex> guard;
+
+		Frame(LockGuard<RecursiveMutex> &&guard,
+		      Capability<uint8_t>         buffer,
+		      uint16_t                    length)
+		  : guard(std::move(guard)), buffer(buffer), length(length)
+		{
+		}
+	};
+
+	/**
+	 * Check the link status of the PHY.
+	 */
+	bool phy_link_status()
+	{
+		uint16_t status = register_read(RegisterOffset::Port1Status);
+		return (status & Port1Status::LinkGood) != 0;
+	}
+
+	std::optional<Frame> receive_frame()
+	{
+		LockGuard g{gpioLock};
+		if (framesToProcess == 0)
+		{
+			uint16_t isr = register_read(RegisterOffset::InterruptStatus);
+			if (!(isr & ReceiveInterrupt))
+			{
+				return std::nullopt;
+			}
+
+			// Acknowledge the interrupt
+			register_write(RegisterOffset::InterruptStatus, ReceiveInterrupt);
+
+			// Read number of frames pending.
+			// Note that this is only updated when we acknowledge the interrupt.
+			framesToProcess =
+			  register_read(RegisterOffset::ReceiveFrameCountThreshold) >> 8;
+		}
+
+		// Get number of frames pending
+		for (; framesToProcess; framesToProcess--)
+		{
+			uint16_t status =
+			  register_read(RegisterOffset::ReceiveFrameHeaderStatus);
+			uint16_t length =
+			  register_read(RegisterOffset::ReceiveFrameHeaderByteCount) &
+			  0xFFF;
+			bool valid =
+			  (status & ReceiveFrameValid) &&
+			  !(status &
+			    (ReceiveCrcError | ReceiveRuntFrame | ReceiveFrameTooLong |
+			     ReceiveMiiError | ReceiveUdpFrameChecksumStatus |
+			     ReceiveTcpFrameChecksumStatus | ReceiveIpFrameChecksumStatus |
+			     ReceiveIcmpFrameChecksumStatus));
+
+			if (!valid)
+			{
+				DebugFrameDrops::log("Dropping frame with status: {}", status);
+
+				drop_error_frame();
+				continue;
+			}
+
+			if (length == 0)
+			{
+				DebugFrameDrops::log("Dropping frame with zero length");
+
+				drop_error_frame();
+				continue;
+			}
+
+			// The DMA transfer to the Ethernet MAC must be a multiple of 4
+			// bytes.
+			uint16_t paddedLength = (length + 3) & ~0x3;
+			if (paddedLength > MaxFrameSize)
+			{
+				DebugFrameDrops::log("Dropping frame that is too large: {}",
+				                     length);
+
+				drop_error_frame();
+				continue;
+			}
+
+			Debug::log("Receiving frame of length {}", length);
+
+			LockGuard guard{receiveBufferMutex};
+
+			// Reset receive frame pointer to zero and start DMA transfer
+			// operation.
+			register_write(RegisterOffset::ReceiveFrameDataPointer,
+			               FrameDataPointer::FrameDataPointerAutoIncrement);
+			register_set(RegisterOffset::ReceiveQueueCommand, StartDmaAccess);
+
+			// Start receiving via SPI.
+			uint8_t cmd = static_cast<uint8_t>(SpiCommand::ReadDma) << 6;
+			set_gpio_output_bit(GpioPin::EthernetChipSelect, false);
+			spi()->blocking_write(&cmd, 1);
+
+			// Initial words are ReceiveFrameHeaderStatus and
+			// ReceiveFrameHeaderByteCount which we have already know the value.
+			uint8_t dummy[8];
+			spi()->blocking_read(dummy, sizeof(dummy));
+
+			spi()->blocking_read(receiveBuffer.get(), paddedLength);
+
+			set_gpio_output_bit(GpioPin::EthernetChipSelect, true);
+
+			register_clear(RegisterOffset::ReceiveQueueCommand, StartDmaAccess);
+			framesToProcess -= 1;
+
+			Capability<uint8_t> boundedBuffer{receiveBuffer.get()};
+			boundedBuffer.bounds().set_inexact(length);
+			// Remove all permissions except load.  This also removes global, so
+			// that this cannot be captured.
+			boundedBuffer.permissions() &=
+			  CHERI::PermissionSet{CHERI::Permission::Load};
+
+			return Frame{std::move(guard), boundedBuffer, length};
+		}
+
+		return std::nullopt;
+	}
+
+	/**
+	 * Send a packet.  This will block if no buffer space is available on
+	 * device.
+	 *
+	 * The third argument is a callback that allows the caller to check the
+	 * frame before it's sent but after it's copied into memory that isn't
+	 * shared with other compartments.
+	 */
+	bool send_frame(const uint8_t *buffer, uint16_t length, auto &&check)
+	{
+		// The DMA transfer to the Ethernet MAC must be a multiple of 4 bytes.
+		uint16_t paddedLength = (length + 3) & ~0x3;
+		if (paddedLength > MaxFrameSize)
+		{
+			Debug::log("Frame size {} is larger than the maximum size", length);
+			return false;
+		}
+
+		LockGuard guard{transmitBufferMutex};
+
+		// We must check the frame pointer and its length. Although it
+		// is supplied by the firewall which is trusted, the firewall
+		// does not check the pointer which is coming from external
+		// untrusted components.
+		Timeout t{10};
+		if ((heap_claim_ephemeral(&t, buffer) < 0) ||
+		    (!CHERI::check_pointer<CHERI::PermissionSet{
+		       CHERI::Permission::Load}>(buffer, length)))
+		{
+			return false;
+		}
+
+		memcpy(transmitBuffer.get(), buffer, length);
+		if (!check(transmitBuffer.get(), length))
+		{
+			return false;
+		}
+
+		LockGuard g{gpioLock};
+
+		// Wait for the transmit buffer to be available on the device side.
+		// This needs to include the header.
+		while ((register_read(RegisterOffset::TransmitQueueMemoryInfo) &
+		        0xFFF) < length + 4)
+		{
+		}
+
+		Debug::log("Sending frame of length {}", length);
+
+		// Start DMA transfer operation.
+		register_set(RegisterOffset::ReceiveQueueCommand, StartDmaAccess);
+
+		// Start sending via SPI.
+		uint8_t cmd = static_cast<uint8_t>(SpiCommand::WriteDma) << 6;
+		set_gpio_output_bit(GpioPin::EthernetChipSelect, false);
+		spi()->blocking_write(&cmd, 1);
+
+		uint32_t header = static_cast<uint32_t>(length) << 16;
+		spi()->blocking_write(reinterpret_cast<uint8_t *>(&header),
+		                      sizeof(header));
+
+		spi()->blocking_write(transmitBuffer.get(), paddedLength);
+
+		spi()->wait_idle();
+		set_gpio_output_bit(GpioPin::EthernetChipSelect, true);
+
+		// Stop QMU DMA transfer operation.
+		register_clear(RegisterOffset::ReceiveQueueCommand, StartDmaAccess);
+
+		// Enqueue the frame for transmission.
+		register_set(
+		  RegisterOffset::TransmitQueueCommand,
+		  TransmitQueueCommand::ManualEnqueueTransmitQueueFrameEnable);
+
+		return true;
+	}
+
+	private:
+	void drop_error_frame()
+	{
+		register_set(RegisterOffset::ReceiveQueueCommand,
+		             ReleaseReceiveErrorFrame);
+		// Wait for confirmation of frame release before attempting to process
+		// next frame.
+		while (register_read(RegisterOffset::ReceiveQueueCommand) &
+		       ReleaseReceiveErrorFrame)
+		{
+		}
+	}
+};
+
+using EthernetDevice = Ksz8851Ethernet;
+
+static_assert(EthernetAdaptor<EthernetDevice>);
diff --git a/sdk/include/platform/sunburst/v0.2/platform-spi.hh b/sdk/include/platform/sunburst/v0.2/platform-spi.hh
new file mode 100644
index 0000000..fd8aa19
--- /dev/null
+++ b/sdk/include/platform/sunburst/v0.2/platform-spi.hh
@@ -0,0 +1,235 @@
+#pragma once
+#include <cdefs.h>
+#include <debug.hh>
+#include <stdint.h>
+
+/**
+ * A Simple Driver for the Sonata's SPI.
+ *
+ * Documentation source can be found at:
+ * https://github.com/lowRISC/sonata-system/blob/1a59633d2515d4fe186a07d53e49ff95c18d9bbf/doc/ip/spi.md
+ *
+ * Rendered documentation is served from:
+ * https://lowrisc.org/sonata-system/doc/ip/spi.html
+ */
+struct SonataSpi
+{
+	/**
+	 * The Sonata SPI block doesn't currently have support for interrupts.
+	 * The following registers are reserved for future use.
+	 */
+	uint32_t interruptState;
+	uint32_t interruptEnable;
+	uint32_t interruptTest;
+	/**
+	 * Configuration register. Controls how the SPI block transmits and
+	 * receives data. This register can be modified only whilst the SPI block
+	 * is idle.
+	 */
+	uint32_t configuration;
+	/**
+	 * Controls the operation of the SPI block. This register can
+	 * be modified only whilst the SPI block is idle.
+	 */
+	uint32_t control;
+	/// Status information about the SPI block
+	uint32_t status;
+	/**
+	 * Writes to this begin an SPI operation.
+	 * Writes are ignored when the SPI block is active.
+	 */
+	uint32_t start;
+	/**
+	 * Data from the receive FIFO. When read the data is popped from the FIFO.
+	 * If the FIFO is empty data read is undefined.
+	 */
+	uint32_t receiveFifo;
+	/**
+	 * Bytes written here are pushed to the transmit FIFO. If the FIFO is full
+	 * writes are ignored.
+	 */
+	uint32_t transmitFifo;
+
+	/// Configuration Register Fields
+	enum : uint32_t
+	{
+		/**
+		 * The length of a half period (i.e. positive edge to negative edge) of
+		 * the SPI clock, measured in system clock cycles reduced by 1. For
+		 * example, at a 50 MHz system clock, a value of 0 gives a 25 MHz SPI
+		 * clock, a value of 1 gives a 12.5 MHz SPI clock, a value of 2 gives
+		 * a 8.33 MHz SPI clock and so on.
+		 */
+		ConfigurationHalfClockPeriodMask = 0xffu << 0,
+		/*
+		 * When set the most significant bit (MSB) is the first bit sent and
+		 * received with each byte
+		 */
+		ConfigurationMSBFirst = 1u << 29,
+		/*
+		 * The phase of the spi_clk signal. when clockphase is 0, data is
+		 * sampled on the leading edge and changes on the trailing edge. The
+		 * first data bit is immediately available before the first leading edge
+		 * of the clock when transmission begins. When clockphase is 1, data is
+		 * sampled on the trailing edge and change on the leading edge.
+		 */
+		ConfigurationClockPhase = 1u << 30,
+		/*
+		 * The polarity of the spi_clk signal. When ClockPolarity is 0, clock is
+		 * low when idle and the leading edge is positive. When ClkPolarity is
+		 * 1, clock is high when idle and the leading edge is negative
+		 */
+		ConfigurationClockPolarity = 1u << 31,
+	};
+
+	/// Control Register Fields
+	enum : uint32_t
+	{
+		/// Write 1 to clear the transmit FIFO.
+		ControlTransmitClear = 1 << 0,
+		/// Write 1 to clear the receive FIFO.
+		ControlReceiveClear = 1 << 1,
+		/**
+		 * When set bytes from the transmit FIFO are sent. When clear the state
+		 * of the outgoing spi_cipo is undefined whilst the SPI clock is
+		 * running.
+		 */
+		ControlTransmitEnable = 1 << 2,
+		/**
+		 * When set incoming bits are written to the receive FIFO. When clear
+		 * incoming bits are ignored.
+		 */
+		ControlReceiveEnable = 1 << 3,
+		/**
+		 * The watermark level for the transmit FIFO, depending on the value
+		 * the interrupt will trigger at different points
+		 */
+		ControlTransmitWatermarkMask = 0xf << 4,
+		/**
+		 * The watermark level for the receive FIFO, depending on the value the
+		 * interrupt will trigger at different points
+		 */
+		ControlReceiveWatermarkMask = 0xf << 8,
+	};
+
+	/// Status Register Fields
+	enum : uint32_t
+	{
+		/// Number of items in the transmit FIFO.
+		StatusTxFifoLevel = 0xffu << 0,
+		/// Number of items in the receive FIFO.
+		StatusRxFifoLevel = 0xffu << 8,
+		/**
+		 * When set the transmit FIFO is full and any data written to it will
+		 * be ignored.
+		 */
+		StatusTxFifoFull = 1u << 16,
+		/**
+		 * When set the receive FIFO is empty and any data read from it will be
+		 * undefined.
+		 */
+		StatusRxFifoEmpty = 1u << 17,
+		/// When set the SPI block is idle and can accept a new start command.
+		StatusIdle = 1u << 18,
+	};
+
+	/// Start Register Fields
+	enum : uint32_t
+	{
+		/// Number of bytes to receive/transmit in the SPI operation
+		StartByteCountMask = 0x7ffu,
+	};
+
+	/// Flag set when we're debugging this driver.
+	static constexpr bool DebugSonataSpi = false;
+
+	/// Helper for conditional debug logs and assertions.
+	using Debug = ConditionalDebug<DebugSonataSpi, "Sonata SPI">;
+
+	/**
+	 * Initialises the SPI block
+	 *
+	 * @param ClockPolarity When false, the clock is low when idle and the
+	 *        leading edge is positive. When true, the opposite behaviour is
+	 *        set.
+	 * @param ClockPhase When false, data is sampled on the leading edge and
+	 *        changes on the trailing edge. When true, the opposite behaviour is
+	 *        set.
+	 * @param MsbFirst When true, the first bit of each byte sent is the most
+	 *        significant bit, as oppose to the least significant bit.
+	 * @param HalfClockPeriod The length of a half period of the SPI clock,
+	 *        measured in system clock cycles reduced by 1.
+	 */
+	void init(const bool     ClockPolarity,
+	          const bool     ClockPhase,
+	          const bool     MsbFirst,
+	          const uint16_t HalfClockPeriod) volatile
+	{
+		configuration = (ClockPolarity ? ConfigurationClockPolarity : 0) |
+		                (ClockPhase ? ConfigurationClockPhase : 0) |
+		                (MsbFirst ? ConfigurationMSBFirst : 0) |
+		                (HalfClockPeriod & ConfigurationHalfClockPeriodMask);
+	}
+
+	/// Waits for the SPI device to become idle
+	void wait_idle() volatile
+	{
+		// Wait whilst IDLE field in STATUS is low
+		while ((status & StatusIdle) == 0) {}
+	}
+
+	/**
+	 * Sends `len` bytes from the given `data` buffer,
+	 * where `len` is at most `0x7ff`.
+	 */
+	void blocking_write(const uint8_t data[], uint16_t len) volatile
+	{
+		Debug::Assert(len <= 0x7ff,
+		              "You can't transfer more than 0x7ff bytes at a time.");
+		len &= StartByteCountMask;
+
+		wait_idle();
+		control = ControlTransmitEnable;
+		start   = len;
+
+		uint32_t transmitAvailable = 0;
+		for (uint32_t i = 0; i < len; ++i)
+		{
+			if (transmitAvailable == 0)
+			{
+				while (transmitAvailable < 64)
+				{
+					// Read number of bytes in TX FIFO to calculate space
+					// available for more bytes
+					transmitAvailable = 64 - (status & StatusTxFifoLevel);
+				}
+			}
+			transmitFifo = data[i];
+			transmitAvailable--;
+		}
+	}
+
+	/*
+	 * Receives `len` bytes and puts them in the `data` buffer,
+	 * where `len` is at most `0x7ff`.
+	 *
+	 * This method will block until the requested number of bytes
+	 * has been seen. There is currently no timeout.
+	 */
+	void blocking_read(uint8_t data[], uint16_t len) volatile
+	{
+		Debug::Assert(len <= 0x7ff,
+		              "You can't receive more than 0x7ff bytes at a time.");
+		len &= StartByteCountMask;
+		wait_idle();
+		control = ControlReceiveEnable;
+		start   = len;
+
+		for (uint32_t i = 0; i < len; ++i)
+		{
+			// Wait for at least one byte to be available in the RX FIFO
+			while ((status & StatusRxFifoLevel) == 0) {}
+			data[i] = static_cast<uint8_t>(receiveFifo);
+		}
+	}
+};
diff --git a/sdk/include/simulator.h b/sdk/include/simulator.h
index 9de4a77..823faf2 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("scheduler")
+  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/sdk/include/stdio.h b/sdk/include/stdio.h
index 9448b5c..884924e 100644
--- a/sdk/include/stdio.h
+++ b/sdk/include/stdio.h
@@ -28,12 +28,16 @@
 #elif DEVICE_EXISTS(uart)
 #	define stdout MMIO_CAPABILITY(void, uart)
 #	define stdin MMIO_CAPABILITY(void uart)
+#else
+#error No device found for stdout and stderr
 #endif
 
-#if DEVICE_EXISTS(uart1)
+#if DEVICE_EXISTS(uart1) && !STDERR_TO_STDOUT
 #	define stderr MMIO_CAPABILITY(void, uart1)
 #elif defined(stdout)
 #	define stderr stdout
+#else
+#error No device found for stderr
 #endif
 
 int __cheri_libcall vfprintf(FILE *stream, const char *fmt, va_list ap);
diff --git a/sdk/include/stdlib.h b/sdk/include/stdlib.h
index 54a9dc6..83a2da0 100644
--- a/sdk/include/stdlib.h
+++ b/sdk/include/stdlib.h
@@ -55,7 +55,7 @@
  */
 #define DEFINE_ALLOCATOR_CAPABILITY(name, quota)                               \
 	DEFINE_STATIC_SEALED_VALUE(struct AllocatorCapabilityState,                \
-	                           alloc,                                          \
+	                           allocator,                                          \
 	                           MallocKey,                                      \
 	                           name,                                           \
 	                           (quota),                                        \
@@ -113,32 +113,33 @@
 	}
 }
 
-enum [[clang::flag_enum]] AllocateWaitFlags{
-  /**
-   * Non-blocking mode. This is equivalent to passing a timeout with no time
-   * remaining.
-   */
-  AllocateWaitNone = 0,
-  /**
-   * If there is enough memory in the quarantine to fulfil the allocation, wait
-   * for the revoker to free objects from the quarantine.
-   */
-  AllocateWaitRevocationNeeded = (1 << 0),
-  /**
-   * If the quota of the passed heap capability is exceeded, wait for other
-   * threads to free allocations.
-   */
-  AllocateWaitQuotaExceeded = (1 << 1),
-  /**
-   * If the heap memory is exhausted, wait for any other thread of the system
-   * to free allocations.
-   */
-  AllocateWaitHeapFull = (1 << 2),
-  /**
-   * Block on any of the above reasons. This is the default behavior.
-   */
-  AllocateWaitAny = (AllocateWaitRevocationNeeded | AllocateWaitQuotaExceeded |
-                     AllocateWaitHeapFull),
+enum [[clang::flag_enum]] AllocateWaitFlags
+{
+	/**
+	 * Non-blocking mode. This is equivalent to passing a timeout with no time
+	 * remaining.
+	 */
+	AllocateWaitNone = 0,
+	/**
+	 * If there is enough memory in the quarantine to fulfil the allocation,
+	 * wait for the revoker to free objects from the quarantine.
+	 */
+	AllocateWaitRevocationNeeded = (1 << 0),
+	/**
+	 * If the quota of the passed heap capability is exceeded, wait for other
+	 * threads to free allocations.
+	 */
+	AllocateWaitQuotaExceeded = (1 << 1),
+	/**
+	 * If the heap memory is exhausted, wait for any other thread of the system
+	 * to free allocations.
+	 */
+	AllocateWaitHeapFull = (1 << 2),
+	/**
+	 * Block on any of the above reasons. This is the default behavior.
+	 */
+	AllocateWaitAny = (AllocateWaitRevocationNeeded |
+	                   AllocateWaitQuotaExceeded | AllocateWaitHeapFull),
 };
 
 /**
@@ -175,7 +176,7 @@
  *
  * Memory returned from this interface is guaranteed to be zeroed.
  */
-void *__cheri_compartment("alloc")
+void *__cheri_compartment("allocator")
   heap_allocate(Timeout           *timeout,
                 struct SObjStruct *heapCapability,
                 size_t             size,
@@ -197,7 +198,7 @@
  *
  * Memory returned from this interface is guaranteed to be zeroed.
  */
-void *__cheri_compartment("alloc")
+void *__cheri_compartment("allocator")
   heap_allocate_array(Timeout           *timeout,
                       struct SObjStruct *heapCapability,
                       size_t             nmemb,
@@ -215,7 +216,7 @@
  * `pointer` is not valid, etc.), or `-ENOTENOUGHSTACK` if the stack is
  * insufficiently large to run the function.
  */
-ssize_t __cheri_compartment("alloc")
+ssize_t __cheri_compartment("allocator")
   heap_claim(struct SObjStruct *heapCapability, void *pointer);
 
 /**
@@ -236,9 +237,19 @@
  * This function is provided by the compartment_helpers library, which must be
  * linked for it to be available.
  */
-int __cheri_libcall heap_claim_fast(Timeout         *timeout,
-                                    const void      *ptr,
-                                    const void *ptr2 __if_cxx(= nullptr));
+int __cheri_libcall heap_claim_ephemeral(Timeout         *timeout,
+                                         const void      *ptr,
+                                         const void *ptr2 __if_cxx(= nullptr));
+
+__attribute__((deprecated("heap_claim_fast was a bad name.  This function has "
+                          "been renamed heap_claim_ephemeral")))
+__always_inline static int
+heap_claim_fast(Timeout         *timeout,
+                const void      *ptr,
+                const void *ptr2 __if_cxx(= nullptr))
+{
+	return heap_claim_ephemeral(timeout, ptr, ptr2);
+}
 
 /**
  * Free a heap allocation.
@@ -247,7 +258,7 @@
  * of a live heap allocation, or `-ENOTENOUGHSTACK` if the stack size is
  * insufficiently large to safely run the function.
  */
-int __cheri_compartment("alloc")
+int __cheri_compartment("allocator")
   heap_free(struct SObjStruct *heapCapability, void *ptr);
 
 /**
@@ -257,14 +268,14 @@
  * capability, or `-ENOTENOUGHSTACK` if the stack size is insufficiently large
  * to safely run the function.
  */
-ssize_t __cheri_compartment("alloc")
+ssize_t __cheri_compartment("allocator")
   heap_free_all(struct SObjStruct *heapCapability);
 
 /**
  * Returns 0 if the allocation can be freed with the given capability, a
  * negated errno value otherwise.
  */
-int __cheri_compartment("alloc")
+int __cheri_compartment("allocator")
   heap_can_free(struct SObjStruct *heapCapability, void *ptr);
 
 /**
@@ -272,7 +283,7 @@
  * `heapCapability` is not valid or if the stack is insufficient to run the
  * function.
  */
-ssize_t __cheri_compartment("alloc")
+ssize_t __cheri_compartment("allocator")
   heap_quota_remaining(struct SObjStruct *heapCapability);
 
 /**
@@ -281,8 +292,12 @@
  * This should be used only in testing, to place the system in a quiesced
  * state.  It can block indefinitely if another thread is allocating and
  * freeing memory while this runs.
+ *
+ * Returns 0 on success, a compartment invocation failure indication
+ * (-ENOTENOUGHSTACK, -ENOTENOUGHTRUSTEDSTACK) if it cannot be invoked, or
+ * possibly -ECOMPARTMENTFAIL if the allocator compartment is damaged.
  */
-void __cheri_compartment("alloc") heap_quarantine_empty(void);
+int __cheri_compartment("allocator") heap_quarantine_empty(void);
 
 /**
  * Returns true if `object` points to a valid heap address, false otherwise.
@@ -307,6 +322,15 @@
 	return (address >= heap_start) && (address < heap_end);
 }
 
+/**
+ * Dump a textual rendering of the heap's structure to the debug console.
+ *
+ * If the RTOS is not built with --allocator-rendering=y, this is a no-op.
+ *
+ * Returns zero on success, non-zero on error (e.g. compartment call failure).
+ */
+int __cheri_compartment("allocator") heap_render();
+
 static inline void __dead2 abort()
 {
 	panic();
@@ -328,7 +352,7 @@
 {
 	Timeout t   = {0, MALLOC_WAIT_TICKS};
 	void   *ptr = heap_allocate_array(
-	    &t, MALLOC_CAPABILITY, nmemb, size, AllocateWaitRevocationNeeded);
+      &t, MALLOC_CAPABILITY, nmemb, size, AllocateWaitRevocationNeeded);
 	if (!__builtin_cheri_tag_get(ptr))
 	{
 		ptr = NULL;
@@ -341,7 +365,7 @@
 }
 #endif
 
-size_t __cheri_compartment("alloc") heap_available(void);
+size_t __cheri_compartment("allocator") heap_available(void);
 
 static inline void yield(void)
 {
diff --git a/sdk/include/thread.h b/sdk/include/thread.h
index 2f156ce..dbc10f2 100644
--- a/sdk/include/thread.h
+++ b/sdk/include/thread.h
@@ -20,8 +20,8 @@
 	/// hi 32 bits
 	uint32_t hi;
 } SystickReturn;
-[[cheri::interrupt_state(disabled)]] SystickReturn __cheri_compartment("sched")
-  thread_systemtick_get(void);
+[[cheri::interrupt_state(disabled)]] SystickReturn
+  __cheri_compartment("scheduler") thread_systemtick_get(void);
 
 enum ThreadSleepFlags : uint32_t
 {
@@ -60,7 +60,7 @@
  * If you are using `thread_sleep` to elapse real time, pass
  * `ThreadSleepNoEarlyWake` as the flags argument to prevent early wakeups.
  */
-[[cheri::interrupt_state(disabled)]] int __cheri_compartment("sched")
+[[cheri::interrupt_state(disabled)]] int __cheri_compartment("scheduler")
   thread_sleep(struct Timeout *timeout, uint32_t flags __if_cxx(= 0));
 
 /**
@@ -78,7 +78,7 @@
  * This API is available only if the scheduler is built with accounting support
  * enabled.
  */
-__cheri_compartment("sched") uint64_t thread_elapsed_cycles_idle(void);
+__cheri_compartment("scheduler") uint64_t thread_elapsed_cycles_idle(void);
 
 /**
  * Returns the number of cycles accounted to the current thread.
@@ -86,7 +86,7 @@
  * This API is available only if the scheduler is built with accounting
  * support enabled.
  */
-__cheri_compartment("sched") uint64_t thread_elapsed_cycles_current(void);
+__cheri_compartment("scheduler") uint64_t thread_elapsed_cycles_current(void);
 
 /**
  * Returns the number of threads, including threads that have exited.
@@ -97,7 +97,7 @@
  *
  * The result of this is safe to cache: it will never change over time.
  */
-__cheri_compartment("sched") uint16_t thread_count();
+__cheri_compartment("scheduler") uint16_t thread_count();
 
 /**
  * Wait for the specified number of microseconds.  This is a busy-wait loop,
@@ -142,9 +142,9 @@
 {
 #ifdef SIMULATION
 	// In simulation builds, just yield once but don't bother trying to do
-	// anything sensible with time.
+	// anything sensible with time.  Ignore failures of attempts to sleep.
 	Timeout t = {0, 1};
-	thread_sleep(&t, 0);
+	(void)thread_sleep(&t, 0);
 	return milliseconds;
 #else
 	static const uint32_t CyclesPerMillisecond = CPU_TIMER_HZ / 1'000;
diff --git a/sdk/include/thread_pool.h b/sdk/include/thread_pool.h
index bbf7fbd..c8be4c2 100644
--- a/sdk/include/thread_pool.h
+++ b/sdk/include/thread_pool.h
@@ -45,10 +45,10 @@
   thread_pool_async(ThreadPoolCallback fn, void *data);
 
 /**
- * Run a thread pool.  This does not return and can be used as a thread entry
- * point.
+ * Run a thread pool.  This does not return, despite the claimed type, and can
+ * be used as a thread entry point.
  */
-void __cheri_compartment("thread_pool") thread_pool_run(void);
+int __cheri_compartment("thread_pool") thread_pool_run(void);
 __END_DECLS
 
 #ifdef __cplusplus
@@ -94,7 +94,14 @@
 				return;
 			}
 			(*fn)();
-			token_obj_destroy(MALLOC_CAPABILITY, key, static_cast<SObj>(rawFn));
+			/*
+			 * This fails only if the thread pool runner compartment can't make
+			 * cross-compartment calls to the allocator at all, since we're
+			 * in its initial trusted activation frame and near the beginning
+			 * (highest address) of its stack.
+			 */
+			(void)token_obj_destroy(
+			  MALLOC_CAPABILITY, key, static_cast<SObj>(rawFn));
 		}
 
 		/**
@@ -131,15 +138,18 @@
 	 * Asynchronously invoke a lambda.  This moves the lambda to the heap and
 	 * passes it to the thread pool's queue.  If the lambda copies any stack
 	 * objects by reference then the copy will fail.
+	 *
+	 * Returns 0 on success, or compartment-call failures (ENOTENOUGHSTACK,
+	 * ENOTENOUGHTRUSTEDSTACK) if the thread pool cannot be invoked.
 	 */
 	template<typename T>
-	void async(T &&lambda)
+	int async(T &&lambda)
 	{
 		// If this is a stateless function, just send a callback function
 		// pointer, don't copy zero bytes of state to the heap.
 		if constexpr (std::is_convertible_v<T, void (*)(void)>)
 		{
-			thread_pool_async(
+			return thread_pool_async(
 			  &detail::wrap_callback_function<std::remove_cvref_t<T>>, nullptr);
 		}
 		else
@@ -153,21 +163,26 @@
 			// type.
 			Timeout t{UnlimitedTimeout};
 			void   *sealed = token_sealed_unsealed_alloc(
-			    &t,
-			    MALLOC_CAPABILITY,
-			    detail::sealing_key_for_type<LambdaType>(),
-			    sizeof(lambda),
-			    &buffer);
-			// Copy the lambda into the new allocation.
-			// Note: We silence a warning here because we *do* want to
-			// explicitly move, not forward.
+              &t,
+              MALLOC_CAPABILITY,
+              detail::sealing_key_for_type<LambdaType>(),
+              sizeof(lambda),
+              &buffer);
+			/*
+			 * Copy the lambda into the new allocation.
+			 *
+			 * If the above allocation has failed, this will trap.
+			 *
+			 * Note: We silence a warning here because we *do* want to
+			 * explicitly move, not forward.
+			 */
 			T *copy = new (buffer) T(
 			  std::move(lambda)); // NOLINT(bugprone-move-forwarding-reference)
 			// Create the wrapper that will unseal and invoke the lambda.
 			ThreadPoolCallback invoke =
 			  &detail::wrap_callback_lambda<LambdaType>;
 			// Dispatch it.
-			thread_pool_async(invoke, sealed);
+			return thread_pool_async(invoke, sealed);
 		}
 	}
 } // namespace thread_pool
diff --git a/sdk/include/token.h b/sdk/include/token.h
index c25c04b..1bb7ec6 100644
--- a/sdk/include/token.h
+++ b/sdk/include/token.h
@@ -36,20 +36,23 @@
  * If the sealing keys have been exhausted then this will return
  * `INVALID_SKEY`.  This API is guaranteed never to block.
  */
-SKey __cheri_compartment("alloc") token_key_new(void);
+SKey __cheri_compartment("allocator") token_key_new(void);
 
 /**
  * Allocate a new object with size `sz`.
  *
  * An unsealed pointer to the newly allocated object is returned in
  * `*unsealed`, the sealed pointer is returned as the return value.
+ * An invalid `unsealed` pointer does not constitute an error; the caller will
+ * still be given the sealed return value, assuming allocation was otherwise
+ * successful.
  *
  * The `key` parameter must have both the permit-seal and permit-unseal
  * permissions.
  *
  * On error, this returns `INVALID_SOBJ`.
  */
-SObj __cheri_compartment("alloc")
+SObj __cheri_compartment("allocator")
   token_sealed_unsealed_alloc(Timeout           *timeout,
                               struct SObjStruct *heapCapability,
                               SKey               key,
@@ -62,7 +65,7 @@
  *
  * The key must have the permit-seal permission.
  */
-SObj __cheri_compartment("alloc")
+SObj __cheri_compartment("allocator")
   token_sealed_alloc(Timeout           *timeout,
                      struct SObjStruct *heapCapability,
                      SKey,
@@ -121,7 +124,7 @@
  * @return 0 if no errors. -EINVAL if key or obj not valid, or they don't
  * match, or double destroy.
  */
-int __cheri_compartment("alloc")
+int __cheri_compartment("allocator")
   token_obj_destroy(struct SObjStruct *heapCapability, SKey, SObj);
 
 /**
@@ -131,7 +134,7 @@
  * Returns 0 on success, `-EINVAL` if the key or object is not valid, or one of
  * the errors from `heap_can_free` if the free would fail for other reasons.
  */
-int __cheri_compartment("alloc")
+int __cheri_compartment("allocator")
   token_obj_can_destroy(SObj heapCapability, SKey key, SObj object);
 
 __END_DECLS
@@ -175,19 +178,32 @@
 	{
 		return reinterpret_cast<T *>(sealedPointer);
 	}
+	/**
+	 * Return the tag of the underlying pointer
+	 */
+	bool is_valid()
+	{
+		return __builtin_cheri_tag_get(get());
+	}
 };
 
 /**
  * Type-safe helper to allocate a sealed `T*`.  Returns the sealed and unsealed
  * pointers.
+ *
+ * Callers should check the sealed capability's tag to determine success.
  */
 template<typename T>
 __always_inline std::pair<T *, Sealed<T>>
 token_allocate(Timeout *timeout, struct SObjStruct *heapCapability, SKey key)
 {
-	void *unsealed;
-	SObj  sealed = token_sealed_unsealed_alloc(
-	   timeout, heapCapability, key, sizeof(T), &unsealed);
+	/*
+	 * Explicitly initialize unsealed, since callers like to check it, and not
+	 * the sealed result, for validity.
+	 */
+	void *unsealed = nullptr;
+	SObj  sealed   = token_sealed_unsealed_alloc(
+      timeout, heapCapability, key, sizeof(T), &unsealed);
 	return {static_cast<T *>(unsealed), Sealed<T>{sealed}};
 }
 
diff --git a/sdk/include/utils.hh b/sdk/include/utils.hh
index 917fc40..704ede8 100644
--- a/sdk/include/utils.hh
+++ b/sdk/include/utils.hh
@@ -38,12 +38,12 @@
 	class NoCopyNoMove
 	{
 		public:
-		NoCopyNoMove()                     = default;
-		NoCopyNoMove(const NoCopyNoMove &) = delete;
+		NoCopyNoMove()                                = default;
+		NoCopyNoMove(const NoCopyNoMove &)            = delete;
 		NoCopyNoMove &operator=(const NoCopyNoMove &) = delete;
 		NoCopyNoMove(NoCopyNoMove &&)                 = delete;
-		NoCopyNoMove &operator=(NoCopyNoMove &&) = delete;
-		~NoCopyNoMove()                          = default;
+		NoCopyNoMove &operator=(NoCopyNoMove &&)      = delete;
+		~NoCopyNoMove()                               = default;
 	};
 
 	/**
diff --git a/sdk/lib/compartment_helpers/README.md b/sdk/lib/compartment_helpers/README.md
index 90857ae..6343379 100644
--- a/sdk/lib/compartment_helpers/README.md
+++ b/sdk/lib/compartment_helpers/README.md
@@ -3,5 +3,5 @@
 
 This library includes functions that help securing compartment boundaries.
 
-- [`claim_fast.cc`] contains the `heap_claim_fast` function.
+- [`claim_fast.cc`] contains the `heap_claim_ephemeral` function.
 - [`check_pointer.cc`] contains the `check_pointer` function.
diff --git a/sdk/lib/compartment_helpers/claim_fast.cc b/sdk/lib/compartment_helpers/claim_fast.cc
index bf87ae4..0c33205 100644
--- a/sdk/lib/compartment_helpers/claim_fast.cc
+++ b/sdk/lib/compartment_helpers/claim_fast.cc
@@ -7,12 +7,12 @@
 #include <stdlib.h>
 #include <switcher.h>
 
-int heap_claim_fast(Timeout *timeout, const void *ptr, const void *ptr2)
+int heap_claim_ephemeral(Timeout *timeout, const void *ptr, const void *ptr2)
 {
 	void   **hazards = switcher_thread_hazard_slots();
 	auto    *epochCounter{const_cast<
       cheriot::atomic<uint32_t> *>(SHARED_OBJECT_WITH_PERMISSIONS(
-	     cheriot::atomic<uint32_t>, allocator_epoch, true, false, false, false))};
+      cheriot::atomic<uint32_t>, allocator_epoch, true, false, false, false))};
 	uint32_t epoch  = epochCounter->load();
 	int      values = 2;
 	// Skip processing pointers that don't refer to heap memory.
@@ -43,10 +43,11 @@
 			if (timeout->may_block())
 			{
 				Timeout t{1};
-				futex_timed_wait(&t,
-				                 reinterpret_cast<uint32_t *>(epochCounter),
-				                 epoch,
-				                 FutexPriorityInheritance);
+				(void)futex_timed_wait(
+				  &t,
+				  reinterpret_cast<uint32_t *>(epochCounter),
+				  epoch,
+				  FutexPriorityInheritance);
 				timeout->elapse(t.elapsed);
 			}
 			else
diff --git a/sdk/lib/cxxrt/guard.cc b/sdk/lib/cxxrt/guard.cc
index a60f690..629a535 100644
--- a/sdk/lib/cxxrt/guard.cc
+++ b/sdk/lib/cxxrt/guard.cc
@@ -1,12 +1,14 @@
 // Copyright Microsoft and CHERIoT Contributors.
 // SPDX-License-Identifier: MIT
 
-#include <cassert>
 #include <cdefs.h>
+#include <debug.hh>
 #include <futex.h>
 #include <limits>
 #include <stdint.h>
 
+using Debug = ConditionalDebug<DEBUG_CXXRT, "cxxrt">;
+
 /**
  * The helper functions need to expose an unmangled name because the compiler
  * inserts calls to them.  Declare them using the asm label extension.
@@ -33,7 +35,7 @@
 		/// The high half (second on a little-endian system).
 		uint32_t high;
 		/// The bit used for the lock (the high bit on a little-endian system)
-		static constexpr uint32_t LockBit = uint32_t(1) << 31;
+		static constexpr uint32_t LockBit = static_cast<uint32_t>(1) << 31;
 
 		public:
 		/**
@@ -54,6 +56,8 @@
 
 		/**
 		 * Acquire the lock.
+		 *
+		 * This is safe only in IRQ-deferred context.
 		 */
 		void lock()
 		{
@@ -62,7 +66,7 @@
 			{
 				futex_wait(&high, LockBit);
 			}
-			assert(high == 0);
+			Debug::Assert(high == 0, "Corrupt guard word at {}", this);
 			high = LockBit;
 		}
 
@@ -71,9 +75,12 @@
 		 */
 		void unlock()
 		{
-			assert(high == LockBit);
-			high = 0;
-			futex_wake(&high, std::numeric_limits<uint32_t>::max());
+			Debug::Assert(high == LockBit, "Corrupt guard word at {}", this);
+			high    = 0;
+			int res = futex_wake(&high, std::numeric_limits<uint32_t>::max());
+			Debug::Assert(res >= 0,
+			              "futex_wake failed for guard {}; possible deadlock",
+			              this);
 		}
 
 		/**
@@ -109,8 +116,8 @@
 void __cxa_guard_release(uint64_t *guard)
 {
 	auto *g = reinterpret_cast<GuardWord *>(guard);
-	assert(!g->is_initialised());
-	assert(g->is_locked());
+	Debug::Assert(!g->is_initialised(), "Releasing uninitialized guard {}", g);
+	Debug::Assert(g->is_locked(), "Releasing unlocked guard {}", g);
 	g->set_initialised();
 	g->unlock();
 }
diff --git a/sdk/lib/debug/debug.cc b/sdk/lib/debug/debug.cc
index 2642d9f..b61eb61 100644
--- a/sdk/lib/debug/debug.cc
+++ b/sdk/lib/debug/debug.cc
@@ -113,7 +113,7 @@
 			}
 			std::array<char, 10> buf;
 			const char           Digits[] = "0123456789";
-			for (int i = int(buf.size() - 1); i >= 0; i--)
+			for (int i = static_cast<int>(buf.size() - 1); i >= 0; i--)
 			{
 				buf[static_cast<size_t>(i)] = Digits[s % 10];
 				s /= 10;
@@ -146,7 +146,7 @@
 			}
 			std::array<char, 20> buf;
 			const char           Digits[] = "0123456789";
-			for (int i = int(buf.size() - 1); i >= 0; i--)
+			for (int i = static_cast<int>(buf.size() - 1); i >= 0; i--)
 			{
 				buf[static_cast<size_t>(i)] = Digits[s % 10];
 				s /= 10;
@@ -168,6 +168,14 @@
 		}
 
 		/**
+		 * Write a single byte with no prefix.
+		 */
+		void write_hex_byte(uint8_t byte) override
+		{
+			append_hex_word(byte);
+		}
+
+		/**
 		 * Write a 32-bit unsigned integer to the buffer as hex with no prefix.
 		 */
 		void append_hex_word(uint32_t s)
@@ -176,7 +184,7 @@
 			const char          Hexdigits[] = "0123456789abcdef";
 			// Length of string including null terminator
 			static_assert(sizeof(Hexdigits) == 0x11);
-			for (long i = long(buf.size() - 1); i >= 0; i--)
+			for (long i = static_cast<long>(buf.size() - 1); i >= 0; i--)
 			{
 				buf.at(static_cast<size_t>(i)) = Hexdigits[s & 0xf];
 				s >>= 4;
@@ -367,5 +375,5 @@
 	printer.write(function);
 	printer.write("\x1b[36m\n");
 	printer.format(format, arguments, argumentCount);
-	printer.write("\n");
+	printer.write("\033[0m\n");
 }
diff --git a/sdk/lib/event_group/event_group.cc b/sdk/lib/event_group/event_group.cc
index 04d0337..87f7ce7 100644
--- a/sdk/lib/event_group/event_group.cc
+++ b/sdk/lib/event_group/event_group.cc
@@ -38,7 +38,7 @@
                       EventGroup **outGroup)
 {
 	auto threads = thread_count();
-	if (threads == uint16_t(-1))
+	if (threads == static_cast<uint16_t>(-1))
 	{
 		return -ERANGE;
 	}
@@ -191,8 +191,7 @@
 		waiter.bitsSeen   = bits;
 		waiter.bitsSeen.notify_one();
 	}
-	heap_free(heapCapability, group);
-	return 0;
+	return heap_free(heapCapability, group);
 }
 
 int eventgroup_destroy(SObjStruct *heapCapability, EventGroup *group)
diff --git a/sdk/lib/microvium/xmake.lua b/sdk/lib/microvium/xmake.lua
index 3b87d81..ed7b141 100644
--- a/sdk/lib/microvium/xmake.lua
+++ b/sdk/lib/microvium/xmake.lua
@@ -9,3 +9,4 @@
   add_files("../../third_party/microvium/dist-c/microvium.c")
   add_includedirs("../../include/microvium", ".")
   add_defines("CHERIOT_NO_AMBIENT_MALLOC")
+  add_cflags("-Wno-constant-logical-operand")
diff --git a/sdk/lib/queue/queue_compartment.cc b/sdk/lib/queue/queue_compartment.cc
index 9aec00a..6ef2d50 100644
--- a/sdk/lib/queue/queue_compartment.cc
+++ b/sdk/lib/queue/queue_compartment.cc
@@ -90,18 +90,15 @@
 {
 	if (token_obj_unseal(handle_key(), queueHandle) != nullptr)
 	{
-		token_obj_destroy(heapCapability, handle_key(), queueHandle);
-		return 0;
+		return token_obj_destroy(heapCapability, handle_key(), queueHandle);
 	}
 	if (token_obj_unseal(send_key(), queueHandle) != nullptr)
 	{
-		token_obj_destroy(heapCapability, send_key(), queueHandle);
-		return 0;
+		return token_obj_destroy(heapCapability, send_key(), queueHandle);
 	}
 	if (token_obj_unseal(receive_key(), queueHandle) != nullptr)
 	{
-		token_obj_destroy(heapCapability, receive_key(), queueHandle);
-		return 0;
+		return token_obj_destroy(heapCapability, receive_key(), queueHandle);
 	}
 	return -EINVAL;
 }
@@ -186,7 +183,7 @@
 	}
 	auto [unsealed, sealed] = token_allocate<RestrictedEndpoint>(
 	  timeout, heapCapability, receive_key());
-	if (!unsealed)
+	if (!sealed.is_valid())
 	{
 		return -ENOMEM;
 	}
@@ -208,7 +205,7 @@
 	}
 	auto [unsealed, sealed] =
 	  token_allocate<RestrictedEndpoint>(timeout, heapCapability, send_key());
-	if (!unsealed)
+	if (!sealed.is_valid())
 	{
 		return -ENOMEM;
 	}
diff --git a/sdk/lib/thread_pool/thread_pool.cc b/sdk/lib/thread_pool/thread_pool.cc
index f58fcf9..5b8b305 100644
--- a/sdk/lib/thread_pool/thread_pool.cc
+++ b/sdk/lib/thread_pool/thread_pool.cc
@@ -47,7 +47,7 @@
 	return 0;
 }
 
-void __cheri_compartment("thread_pool") thread_pool_run()
+int __cheri_compartment("thread_pool") thread_pool_run()
 {
 	while (true)
 	{
diff --git a/sdk/xmake.lua b/sdk/xmake.lua
index 07a37c5..46d9032 100644
--- a/sdk/xmake.lua
+++ b/sdk/xmake.lua
@@ -25,6 +25,11 @@
 	set_description("Track per-thread cycle counts in the scheduler");
 	set_showmenu(true)
 
+option("allocator-rendering")
+	set_default(false)
+	set_description("Include heap_render() functionality in the allocator")
+	set_showmenu(true)
+
 function debugOption(name)
 	option("debug-" .. name)
 		set_default(false)
@@ -227,8 +232,9 @@
 	add_deps("locks")
 	add_deps("compartment_helpers")
 	on_load(function (target)
-		target:set("cheriot.compartment", "alloc")
+		target:set("cheriot.compartment", "allocator")
 		target:set('cheriot.debug-name', "allocator")
+		target:add('defines', "HEAP_RENDER=" .. tostring(get_config("allocator-rendering")))
 	end)
 
 target("cheriot.token_library")
@@ -247,21 +253,143 @@
 		target:set("cheriot.ldscript", "software_revoker.ldscript")
 	end)
 
--- Helper to get the board file for a given target
-local board_file = function(target)
-	local boardfile = target:values("board")
-	if not boardfile then
-		raise("target " .. target:name() .. " does not define a board name")
-	end
+-- Helper to find a board file given either the name of a board file or a path.
+local function board_file_for_name(boardName)
+	local boardfile = boardName
 	-- The directory containing the board file.
 	local boarddir = path.directory(boardfile);
-	if path.basename(boardfile) == boardfile then
+	-- If this isn't a path, look in the boards directory
+	if not os.isfile(boardfile) then
 		boarddir = path.join(scriptdir, "boards")
-		boardfile = path.join(boarddir, boardfile .. '.json')
+		local fullBoardPath = path.join(boarddir, boardfile .. '.json')
+		if not os.isfile(fullBoardPath) then
+			fullBoardPath = path.join(boarddir, boardfile .. '.patch')
+		end
+		if not os.isfile(fullBoardPath) then
+			print("unable to find board file " .. boardfile .. ".  Try specifying a full path")
+			return nil
+		end
+		boardfile = fullBoardPath
 	end
 	return boarddir, boardfile
 end
 
+-- Helper to get the board file for a given target
+local function board_file_for_target(target)
+	local boardName = target:values("board")
+	if not boardName then
+		print("target " .. target:name() .. " does not define a board name")
+		return nil
+	end
+	return board_file_for_name(boardName)
+end
+
+-- Helper to load a board file.  This must be passed the json object provided
+-- by import("core.base.json") because import does not work in helper
+-- functions at the top level.
+local function load_board_file(json, boardFile)
+	if path.extension(boardFile) == ".json" then
+		return json.loadfile(boardFile)
+	end
+	if path.extension(boardFile) ~= ".patch" then
+		print("unknown extension for board file: " .. boardFile)
+		return nil
+	end
+	local patch = json.loadfile(boardFile)
+	if not patch.base then
+		print("Board file " .. boardFile .. " does not specify a base")
+		return nil
+	end
+	local _, baseFile = board_file_for_name(patch.base)
+	local base = load_board_file(json, baseFile)
+
+	-- If a string value is a number, return it as number, otherwise return it
+	-- in its original form.
+	function asNumberIfNumber(value)
+		if tostring(tonumber(value)) == value then
+			return tonumber(value)
+		end
+		return value
+	end
+
+	-- Heuristic to tell a Lua table is probably an array in Lua
+	-- This is O(n), but n is usually very small, and this happens once per
+	-- build so this doesn't really matter.
+	function isarray(t)
+		local i = 1
+		for k, v in pairs(t) do
+			if k ~= i then
+				return false
+			end
+			i = i+1
+		end
+		return true
+	end
+
+	for _, p in ipairs(patch.patch) do
+		if not p.op then
+			print("missing op in "..json.encode(p))
+			return nil
+		end
+		if not p.path or (type(p.path) ~= "string") then
+			print("missing or invalid path in "..json.encode(p))
+			return nil
+		end
+
+		-- Parse the JSON Pointer into an array of filed names, converting
+		-- numbers into Lua numbers if we see them.  This is not quite right,
+		-- because it doesn't handle field names with / in them, but we don't
+		-- currently use those for anything.  It also assumes that we really do
+		-- mean array indexes when we say numbers.  If we have an object with
+		-- "3" as the key and try to replace 3, it will currently not do the
+		-- right thing.  
+		local objectPath = {}
+		for entry in string.gmatch(p.path, "/([^/]+)") do
+			table.insert(objectPath, asNumberIfNumber(entry))
+		end
+
+		if #objectPath < 1 then
+			print("invalid path in "..json.encode(p))
+			return nil
+		end
+
+		-- Last path object is the name of the key we're going to modify.
+		local nodeName = table.remove(objectPath)
+		-- Walk the path to find the object that we're going to modify.
+		local nodeToModify = base
+		for _, pathComponent in ipairs(objectPath) do
+			nodeToModify = nodeToModify[pathComponent]
+		end
+
+		-- JSON arrays are indexed from 0, Lua's are from 1.  If someone says
+		-- array index 0, we need to map that to 1, and so on.
+		local isArrayOperation = false
+		if (type(nodeName) == "number") and isarray(nodeToModify) then
+			nodeName = nodeName + 1
+			isArrayOperation = true
+		end
+
+		-- Handle the operation
+		if (p.op == "replace") or (p.op == "add") then
+			if not p.value then
+				print(tostring(p.op).. " requires a value, missing in ", json.encode(p))
+				return nil
+			end
+			if isArrayOperation and p.op == "add" then
+				table.insert(nodeToModify, nodeName, p.value)
+			else
+				nodeToModify[nodeName] = p.value
+			end
+		elseif p.op == "remove" then
+			nodeToModify[nodeName] = nil
+		else
+			print(tostring(p.op) .. " is not a valid operation in ", json.encode(p))
+			return nil
+		end
+	end
+	return base
+end
+
 -- Helper to visit all dependencies of a specified target exactly once and call
 -- a callback.
 local function visit_all_dependencies_of(target, callback)
@@ -278,34 +406,23 @@
 	visit(target)
 end
 
-
 -- Rule for defining a firmware image.
 rule("firmware")
 	on_run(function (target)
 		import("core.base.json")
 		import("core.project.config")
-		local boarddir, boardfile = board_file(target)
-		local board = json.loadfile(boardfile)
-		if not board.simulator then
+		local boarddir, boardfile = board_file_for_target(target)
+		local board = load_board_file(json, boardfile)
+		if (not board.run_command) and (not board.simulator) then
 			raise("board description " .. boardfile .. " does not define a run command")
 		end
-		local simulator = board.simulator
+		local simulator = board.run_command or board.simulator
 		simulator = string.gsub(simulator, "${(%w*)}", { sdk=scriptdir, board=boarddir })
 		local firmware = target:targetfile()
 		local directory = path.directory(firmware)
 		firmware = path.filename(firmware)
 		local run = function(simulator)
 			local simargs = { firmware }
-			if get_config("testing-model-output") then
-				modeldir = path.join(scriptdir,
-				                     "..",
-				                     "scripts",
-				                     "model_output",
-				                     path.basename(target:values("board")),
-				                     "examples")
-				local modelout = path.join(modeldir, firmware .. ".txt")
-				simargs[#simargs+1] = modelout
-			end
 			os.execv(simulator, simargs, { curdir = directory })
 		end
 		-- Try executing the simulator from the sdk directory, if it's there.
@@ -328,14 +445,16 @@
 	-- This must be after load so that dependencies are resolved.
 	after_load(function (target)
 		import("core.base.json")
+		import("core.project.config")
 
 		local function visit_all_dependencies(callback)
 			visit_all_dependencies_of(target, callback)
 		end
 
-		local boarddir, boardfile = board_file(target);
-		local board = json.loadfile(boardfile)
-
+		local boarddir, boardfile = board_file_for_target(target);
+		local board = load_board_file(json, boardfile)
+		print("Board file saved as ", target:targetfile()..".board.json")
+		json.savefile(target:targetfile()..".board.json", board)
 
 		-- Add defines to all dependencies.
 		local add_defines = function (defines)
@@ -344,6 +463,13 @@
 			end)
 		end
 
+		-- Add cxflags to all dependencies.
+		local add_cxflags = function (cxflags)
+			visit_all_dependencies(function (target)
+				target:add('cxflags', cxflags, {force = true})
+			end)
+		end
+
 		local software_revoker = false
 		if board.revoker then
 			local temporal_defines = { "TEMPORAL_SAFETY" }
@@ -388,6 +514,11 @@
 			add_defines(board.defines)
 		end
 
+		-- If this board defines any cxflags, add them to all targets
+		if board.cxflags then
+			add_cxflags(board.cxflags)
+		end
+
 		add_defines("CPU_TIMER_HZ=" .. math.floor(board.timer_hz))
 		add_defines("TICK_RATE_HZ=" .. math.floor(board.tickrate_hz))
 
@@ -428,7 +559,22 @@
 		mmio = format("__mmio_region_start = 0x%x;\n%s__mmio_region_end = 0x%x;\n__export_mem_heap_end = 0x%x;\n",
 			mmio_start, mmio, mmio_end, board.heap["end"])
 
-		local code_start = format("0x%x", board.instruction_memory.start)
+		local code_start = format("0x%x", board.instruction_memory.start);
+		-- Put the data either at the specified address if given, or directly after code
+		local data_start = board.data_memory and format("0x%x", board.data_memory.start) or '.';
+		local rwdata_ldscript = path.join(config.buildir(), target:name() .. "-firmware.rwdata.ldscript")
+		local rocode_ldscript = path.join(config.buildir(), target:name() .. "-firmware.rocode.ldscript")
+		if not board.data_memory or (board.instruction_memory.start < board.data_memory.start) then
+			-- If we're not explicilty given a data address or it's lower than the code address
+			-- then code needs to go first in the linker script.
+			firmware_low_ldscript = rocode_ldscript
+			firmware_high_ldscript = rwdata_ldscript
+		else
+			-- Otherwise the data is at a lower address than code (e.g. Sonata with SRAM and hyperram)
+			-- so it needs to go first.
+			firmware_low_ldscript = rwdata_ldscript;
+			firmware_high_ldscript = rocode_ldscript;
+		end
 
 		-- Set the start of memory that can be revoked.
 		-- By default, this is the start of code memory but it can be
@@ -622,8 +768,11 @@
 			software_revoker_header="",
 			sealed_objects="",
 			mmio=mmio,
+			data_start=data_start,
 			code_start=code_start,
 			heap_start=heap_start,
+			firmware_low_ldscript=firmware_low_ldscript,
+			firmware_high_ldscript=firmware_high_ldscript,
 			thread_count=#(threads),
 			thread_headers=thread_headers,
 			thread_trusted_stacks=thread_trusted_stacks,
@@ -754,7 +903,9 @@
 		import("core.project.config")
 		-- Get a specified linker script, or set the default to the compartment
 		-- linker script.
-		local linkerscript = path.join(config.buildir(), target:name() .. "-firmware.ldscript")
+		local linkerscript1 = path.join(config.buildir(), target:name() .. "-firmware.ldscript")
+		local linkerscript2 = path.join(config.buildir(), target:name() .. "-firmware.rocode.ldscript")
+		local linkerscript3 = path.join(config.buildir(), target:name() .. "-firmware.rwdata.ldscript")
 		-- Link using the firmware's linker script.
 		batchcmds:show_progress(opt.progress, "linking firmware " .. target:targetfile())
 		batchcmds:mkdir(target:targetdir())
@@ -767,11 +918,11 @@
 				table.insert(objects, dep:targetfile())
 			end
 		end)
-		batchcmds:vrunv(target:tool("ld"), table.join({"-n", "--script=" .. linkerscript, "--relax", "-o", target:targetfile(), "--compartment-report=" .. target:targetfile() .. ".json" }, objects), opt)
+		batchcmds:vrunv(target:tool("ld"), table.join({"-n", "--script=" .. linkerscript1, "--relax", "-o", target:targetfile(), "--compartment-report=" .. target:targetfile() .. ".json" }, objects), opt)
 		batchcmds:show_progress(opt.progress, "Creating firmware report " .. target:targetfile() .. ".json")
 		batchcmds:show_progress(opt.progress, "Creating firmware dump " .. target:targetfile() .. ".dump")
 		batchcmds:vexecv(target:tool("objdump"), {"-glxsdrS", "--demangle", target:targetfile()}, table.join(opt, {stdout = target:targetfile() .. ".dump"}))
-		batchcmds:add_depfiles(linkerscript)
+		batchcmds:add_depfiles(linkerscript1, linkerscript2, linkerscript3)
 		batchcmds:add_depfiles(objects)
 	end)
 
@@ -823,7 +974,7 @@
 		add_deps("locks", "crt", "atomic1")
 		add_deps("compartment_helpers")
 		on_load(function (target)
-			target:set("cheriot.compartment", "sched")
+			target:set("cheriot.compartment", "scheduler")
 			target:set('cheriot.debug-name', "scheduler")
 			target:add('defines', "SCHEDULER_ACCOUNTING=" .. tostring(get_config("scheduler-accounting")))
 		end)
@@ -840,6 +991,8 @@
 		-- The firmware linker script will be populated based on the set of
 		-- compartments.
 		add_configfiles(path.join(scriptdir, "firmware.ldscript.in"), {pattern = "@(.-)@", filename = name .. "-firmware.ldscript"})
+		add_configfiles(path.join(scriptdir, "firmware.rocode.ldscript.in"), {pattern = "@(.-)@", filename = name .. "-firmware.rocode.ldscript"})
+		add_configfiles(path.join(scriptdir, "firmware.rwdata.ldscript.in"), {pattern = "@(.-)@", filename = name .. "-firmware.rwdata.ldscript"})
 end
 
 -- Helper to create a library.
diff --git a/tests.extra/hardware_revoker_IRQs/README.md b/tests.extra/hardware_revoker_IRQs/README.md
new file mode 100644
index 0000000..bc8925c
--- /dev/null
+++ b/tests.extra/hardware_revoker_IRQs/README.md
@@ -0,0 +1 @@
+This is a minimal "the clock is ticking" test for a hardware revoker.
diff --git a/tests.extra/hardware_revoker_IRQs/top.cc b/tests.extra/hardware_revoker_IRQs/top.cc
new file mode 100644
index 0000000..7347934
--- /dev/null
+++ b/tests.extra/hardware_revoker_IRQs/top.cc
@@ -0,0 +1,71 @@
+#include <debug.hh>
+#include <fail-simulator-on-error.h>
+
+using Debug = ConditionalDebug<true, "top">;
+
+#if __has_include(<platform-hardware_revoker.hh>)
+#	include <platform-hardware_revoker.hh>
+
+using Revoker = HardwareRevoker<uint32_t, REVOKABLE_MEMORY_START>;
+
+#elif defined(CLANG_TIDY)
+
+struct Revoker
+{
+	static constexpr bool IsAsynchronous = true;
+
+	uint32_t system_epoch_get()
+	{
+		return 0;
+	}
+	int wait_for_completion(Timeout *, uint32_t)
+	{
+		return 0;
+	}
+	void system_bg_revoker_kick() {}
+	void init() {}
+};
+
+#else
+#	error No platform-hardware_revoker.hh found, are you building for the right platform?
+#endif
+
+static_assert(Revoker::IsAsynchronous, "This test is for async revokers");
+
+void __cheri_compartment("top") entry()
+{
+	Revoker r{};
+	r.init();
+
+	uint32_t epoch = r.system_epoch_get();
+	Debug::log("At startup, revocation epoch is {}; waiting...", epoch);
+
+	r.system_bg_revoker_kick();
+
+	for (int i = 0; i < 10; i++)
+	{
+		bool     res;
+		uint32_t newepoch;
+		Timeout  t{50};
+
+		res      = r.wait_for_completion(&t, epoch & ~1);
+		newepoch = r.system_epoch_get();
+
+		Debug::log("After wait: for {}, result {}, epoch now is {}, "
+		           "wait elapsed {} remaining {}",
+		           epoch,
+		           res,
+		           newepoch,
+		           t.elapsed,
+		           t.remaining);
+
+		Debug::Assert(t.remaining > 0,
+		              "Timed out waiting for revoker to advance");
+
+		if (res)
+		{
+			epoch = newepoch;
+			r.system_bg_revoker_kick();
+		}
+	}
+}
diff --git a/tests.extra/hardware_revoker_IRQs/xmake.lua b/tests.extra/hardware_revoker_IRQs/xmake.lua
new file mode 100644
index 0000000..a8c713f
--- /dev/null
+++ b/tests.extra/hardware_revoker_IRQs/xmake.lua
@@ -0,0 +1,26 @@
+set_project("Hardware Revoker IRQ Basic Functionality Test")
+sdkdir = "../../sdk"
+includes(sdkdir)
+set_toolchains("cheriot-clang")
+
+option("board")
+    set_default("ibex-safe-simulator")
+
+compartment("top")
+    add_files("top.cc")
+
+firmware("top_compartment")
+    add_deps("freestanding", "debug")
+    add_deps("top")
+    on_load(function(target)
+        target:values_set("board", "$(board)")
+        target:values_set("threads", {
+            {
+                compartment = "top",
+                priority = 1,
+                entry_point = "entry",
+                stack_size = 0x300,
+                trusted_stack_frames = 1
+            }
+        }, {expand = false})
+    end)
diff --git a/tests.extra/regress-thread_exit_IRQ/helper.cc b/tests.extra/regress-thread_exit_IRQ/helper.cc
index 023b4fe..0f065eb 100644
--- a/tests.extra/regress-thread_exit_IRQ/helper.cc
+++ b/tests.extra/regress-thread_exit_IRQ/helper.cc
@@ -1,5 +1,5 @@
 #include "helper.h"
-[[cheri::interrupt_state(enabled)]] void* help(void)
+[[cheri::interrupt_state(enabled)]] void *help()
 {
 	return __builtin_return_address(0);
 }
diff --git a/tests.extra/regress-thread_exit_IRQ/helper.h b/tests.extra/regress-thread_exit_IRQ/helper.h
index 578659e..0eaf329 100644
--- a/tests.extra/regress-thread_exit_IRQ/helper.h
+++ b/tests.extra/regress-thread_exit_IRQ/helper.h
@@ -1,2 +1,2 @@
 #include <compartment.h>
-void* __cheri_compartment("helper") help(void);
+void *__cheri_compartment("helper") help();
diff --git a/tests.extra/regress-thread_exit_IRQ/top.cc b/tests.extra/regress-thread_exit_IRQ/top.cc
index 57087ce..e1a87d7 100644
--- a/tests.extra/regress-thread_exit_IRQ/top.cc
+++ b/tests.extra/regress-thread_exit_IRQ/top.cc
@@ -1,5 +1,5 @@
 #include "helper.h"
 void __cheri_compartment("top") entry()
 {
-	asm volatile ("cmove cra, %0; cret" : : "C"(help()));
+	asm volatile("cmove cra, %0; cret" : : "C"(help()));
 }
diff --git a/tests/allocator-test.cc b/tests/allocator-test.cc
index 05a4ac8..87af6c3 100644
--- a/tests/allocator-test.cc
+++ b/tests/allocator-test.cc
@@ -28,6 +28,17 @@
 DECLARE_AND_DEFINE_ALLOCATOR_CAPABILITY(emptyHeap, 0);
 #define EMPTY_HEAP STATIC_SEALED_VALUE(emptyHeap)
 
+/* Used to test that the revoker sweeps static sealed capabilities */
+struct AllocatorTestStaticSealedType
+{
+	void *pointer;
+};
+DECLARE_AND_DEFINE_STATIC_SEALED_VALUE(struct AllocatorTestStaticSealedType,
+                                       allocator_test,
+                                       AllocatorTestCapabilityType,
+                                       allocatorTestStaticSealedValue,
+                                       0);
+
 namespace
 {
 	/**
@@ -39,12 +50,6 @@
 
 	Timeout noWait{0};
 
-	/**
-	 * Size of an allocation that is big enough that we'll exhaust memory before
-	 * we allocate `MaxAllocCount` of them.
-	 */
-	constexpr size_t BigAllocSize  = 1024 * 32;
-	constexpr size_t AllocSize     = 0xff0;
 	constexpr size_t MaxAllocCount = 16;
 	constexpr size_t TestIterations =
 #ifdef NDEBUG
@@ -57,6 +62,27 @@
 	std::vector<void *> allocations;
 
 	/**
+	 * Quick check of basic functionality before we get too carried away
+	 *
+	 * This is marked as noinline because otherwise the predict-false on the
+	 * test failures causes all of the log-message-and-fail blocks to be moved
+	 * to the end of the function and, if this is inlined, ends up with some
+	 * branches that are more than 2 KiB away from their targets.
+	 */
+	__noinline void test_preflight()
+	{
+		Timeout t{5};
+		void *volatile p = heap_allocate(&t, MALLOC_CAPABILITY, 16);
+		TEST(Capability{p}.is_valid(), "Unable to make first allocation");
+
+		int res = heap_free(MALLOC_CAPABILITY, p);
+		TEST_EQUAL(res, 0, "heap_free returned nonzero");
+		TEST(
+		  !Capability{p}.is_valid(),
+		  "Freed pointer still live; load barrier or revoker out of service?");
+	}
+
+	/**
 	 * Test the revoker by constantly allocating and freeing batches of
 	 * allocations. The total amount of allocations must greatly exceed the heap
 	 * size to force a constant stream of allocation failures and revocations.
@@ -66,9 +92,98 @@
 	 * This performance test should not fail. If it fails it's either the
 	 * allocations in one iteration exceed the total heap size, or the revoker
 	 * is buggy or too slow.
+	 *
+	 * This is marked as noinline because otherwise the predict-false on the
+	 * test failures causes all of the log-message-and-fail blocks to be moved
+	 * to the end of the function and, if this is inlined, ends up with some
+	 * branches that are more than 2 KiB away from their targets.
 	 */
-	void test_revoke()
+	__noinline void test_revoke(const size_t HeapSize)
 	{
+#ifdef TEMPORAL_SAFETY
+		{
+			static cheriot::atomic<int> state = 2;
+			int                         sleeps;
+
+			void *volatile pStack = malloc(16);
+			TEST(__builtin_cheri_tag_get(pStack),
+			     "Failed to allocate for test");
+
+			static void *volatile pGlobal = malloc(16);
+			TEST(__builtin_cheri_tag_get(pGlobal),
+			     "Failed to allocate for test");
+
+			auto unsealedToken = token_unseal<AllocatorTestStaticSealedType>(
+			  STATIC_SEALING_TYPE(AllocatorTestCapabilityType),
+			  STATIC_SEALED_VALUE(allocatorTestStaticSealedValue));
+			unsealedToken->pointer = pGlobal;
+
+			/*
+			 * Check that trusted stacks are swept.  This one is fun: we need to
+			 * somehow ensure that a freed pointer is in the register file of an
+			 * off-core thread.  async() and inline asm it is!
+			 */
+			async([=]() {
+				int ptag, scratch;
+
+				/*
+				 * Release the main thread, then busy spin, ensuring that our
+				 * test pointer is in a register throughout, then get its tag.
+				 */
+				__asm__ volatile("csw zero, 0(%[state])\n"
+				                 "1:\n"
+				                 "clw %[scratch], 0(%[state])\n"
+				                 "beqz %[scratch], 1b\n"
+				                 "cgettag %[out], %[p]\n"
+				                 : [out] "+&r"(ptag), [scratch] "=&r"(scratch)
+				                 : [p] "C"(pGlobal), [state] "C"(&state));
+
+				TEST(ptag == 0, "Revoker failed to sweep trusted stack");
+
+				/* Release the main thread */
+				state = 3;
+			});
+
+			sleeps = 0;
+			while (state.load() != 0)
+			{
+				TEST(sleep(1) >= 0, "Failed to sleep");
+				TEST(sleeps++ < 100, "Background thread not ready");
+			}
+
+			free(pStack);
+			free(pGlobal);
+			heap_quarantine_empty();
+
+			state = 1;
+
+			/* Check that globals are swept */
+			TEST(!__builtin_cheri_tag_get_temporal(pGlobal),
+			     "Revoker failed to sweep globals");
+
+			/* Check that the stack is swept */
+			TEST(!__builtin_cheri_tag_get_temporal(pStack),
+			     "Revoker failed to sweep stack");
+
+			TEST(!__builtin_cheri_tag_get_temporal(unsealedToken->pointer),
+			     "Revoker failed to sweep static sealed cap");
+
+			/* Wait for the async thread to have performed its test */
+			sleeps = 0;
+			while (state.load() != 3)
+			{
+				TEST(sleep(1) >= 0, "Failed to sleep");
+				TEST(sleeps++ < 100, "Background thread not finished");
+			}
+		}
+#else
+		debug_log("Skipping temporal safety checks");
+#endif
+
+		const size_t AllocSize = HeapSize / (MaxAllocCount + 2);
+		debug_log("test_revoke using {}-byte objects", AllocSize);
+
+		/* Repeatedly cycle quarantine */
 		allocations.resize(MaxAllocCount);
 		for (size_t i = 0; i < TestIterations; ++i)
 		{
@@ -101,8 +216,7 @@
 			  "Checked that all allocations have been deallocated ({} of {})",
 			  static_cast<int>(i),
 			  static_cast<int>(TestIterations));
-			Timeout t{1};
-			thread_sleep(&t);
+			TEST(sleep(1) >= 0, "Failed to sleep");
 		}
 		allocations.clear();
 	}
@@ -112,8 +226,18 @@
 	 * Test that we can do a long-running blocking allocation in one thread and
 	 * a free in another thread and make forward progress.
 	 */
-	void test_blocking_allocator()
+	void test_blocking_allocator(const size_t HeapSize)
 	{
+		/**
+		 * Size of an allocation that is big enough that we'll exhaust memory
+		 * before we allocate `MaxAllocCount` of them.
+		 */
+		const size_t BigAllocSize = HeapSize / (MaxAllocCount - 1);
+		TEST(BigAllocSize > 0,
+		     "Cannot guestimate big allocation size for our heap of {} bytes",
+		     HeapSize);
+		debug_log("BigAllocSize {} bytes", BigAllocSize);
+
 		allocations.resize(MaxAllocCount);
 		// Create the background worker before we try to exhaust memory.
 		async([]() {
@@ -122,8 +246,7 @@
 			freeStart.wait(0);
 			// One extra sleep to make sure that we're really in the blocking
 			// sleep.
-			Timeout t{2};
-			thread_sleep(&t);
+			TEST(sleep(2) >= 0, "Failed to sleep");
 			debug_log(
 			  "Deallocation thread resuming, freeing pool of allocations");
 			// Free all of the allocations to make space.
@@ -131,7 +254,9 @@
 			{
 				if (allocation != nullptr)
 				{
-					heap_free(MALLOC_CAPABILITY, allocation);
+					TEST_EQUAL(heap_free(MALLOC_CAPABILITY, allocation),
+					           0,
+					           "Could not free allocation");
 				}
 			}
 			// Notify the parent thread that we're done.
@@ -139,6 +264,13 @@
 			freeStart.notify_one();
 		});
 
+		/*
+		 * Empty the allocator's quarantine so that we're sure that the nullptr
+		 * failure we see below isn't because we aren't allowing the revocation
+		 * state machine to advance.
+		 */
+		heap_quarantine_empty();
+
 		bool memoryExhausted = false;
 		for (auto &allocation : allocations)
 		{
@@ -154,6 +286,8 @@
 			}
 		}
 		TEST(memoryExhausted, "Failed to exhaust memory");
+		debug_log("Calling heap_render");
+		TEST_EQUAL(heap_render(), 0, "heap_render returned non-zero");
 		debug_log("Trying a non-blocking allocation");
 		// nullptr check because we explicitly want to check for OOM
 		TEST(heap_allocate(&noWait, MALLOC_CAPABILITY, BigAllocSize) == nullptr,
@@ -237,9 +371,14 @@
 		  16, 64, 72, 96, 128, 256, 384, 1024};
 		static constexpr size_t NAllocSizes = std::size(AllocSizes);
 
+		static constexpr size_t NCachedFrees = 4 * MaxAllocCount;
+
 		ds::xoroshiro::P32R16 rand = {};
 		auto                  t    = Timeout(0); /* don't sleep */
 
+		std::vector<void *> cachedFrees;
+		cachedFrees.resize(NCachedFrees);
+
 		auto doAlloc = [&](size_t sz) {
 			CHERI::Capability p{heap_allocate(&t, MALLOC_CAPABILITY, sz)};
 
@@ -261,6 +400,8 @@
 
 			TEST(CHERI::Capability{p}.is_valid(), "Double free {}", p);
 
+			cachedFrees[rand.next() % NCachedFrees] = p;
+
 			free(p);
 		};
 
@@ -295,8 +436,15 @@
 					doFree();
 				}
 			}
+
+			for (void *p : cachedFrees)
+			{
+				TEST(!Capability{p}.is_valid(), "Detected necromancy: {}", p);
+			}
 		}
 
+		cachedFrees.clear();
+
 		for (auto allocation : allocations)
 		{
 			free(allocation);
@@ -330,11 +478,11 @@
             ssize_t claimSize = heap_claim(SECOND_HEAP, alloc);
             claimCount++;
             TEST((allocSize <= claimSize) &&
-			            (claimSize <= allocSize + CHERIOTHeapMinChunkSize),
-			          "{}-byte allocation claimed as {} bytes (claim number {})",
-			          allocSize,
-			          claimSize,
-			          claimCount);
+                   (claimSize <= allocSize + CHERIOTHeapMinChunkSize),
+                 "{}-byte allocation claimed as {} bytes (claim number {})",
+                 allocSize,
+                 claimSize,
+                 claimCount);
 		};
 		claim();
 		int ret = heap_free(SECOND_HEAP, alloc);
@@ -410,6 +558,7 @@
 
 	void test_hazards()
 	{
+		int sleeps;
 		debug_log("Before allocating, quota left: {}",
 		          heap_quota_remaining(SECOND_HEAP));
 		Timeout longTimeout{1000};
@@ -422,7 +571,7 @@
 		static cheriot::atomic<int> state = 0;
 		async([=]() {
 			Timeout t{1};
-			int     claimed = heap_claim_fast(&t, ptr, ptr2);
+			int     claimed = heap_claim_ephemeral(&t, ptr, ptr2);
 			TEST(claimed == 0, "Heap claim failed: {}", claimed);
 			state = 1;
 			while (state.load() == 1) {}
@@ -430,21 +579,22 @@
 			// Exiting this task will cause this closure to be freed, which
 			// will collect dangling hazard pointers.  Wait for long enough for
 			// the heap check to work.
-			t = 1;
-			thread_sleep(&t);
+			TEST(sleep(1) >= 0, "Failed to sleep");
 		});
 		// Allow the async function to run and establish hazards
+		sleeps = 0;
 		while (state.load() != 1)
 		{
-			Timeout t{1};
-			thread_sleep(&t);
+			TEST(sleep(1) >= 0, "Failed to sleep");
+			TEST(sleeps++ < 100,
+			     "Background thread failed to establish hazards");
 		}
 		debug_log("Before freeing, quota left: {}",
 		          heap_quota_remaining(SECOND_HEAP));
-		heap_free(SECOND_HEAP, ptr);
+		TEST_EQUAL(heap_free(SECOND_HEAP, ptr), 0, "First free failed");
 		debug_log("After free 1, quota left: {}",
 		          heap_quota_remaining(SECOND_HEAP));
-		heap_free(SECOND_HEAP, ptr2);
+		TEST_EQUAL(heap_free(SECOND_HEAP, ptr2), 0, "Second free failed");
 		debug_log("After free 2, quota left: {}",
 		          heap_quota_remaining(SECOND_HEAP));
 		TEST(Capability{ptr}.is_valid(),
@@ -455,21 +605,21 @@
 		     ptr2);
 		state = 2;
 		// Yield to allow the hazards to be dropped.
-		Timeout t{1};
-		thread_sleep(&t);
+		TEST(sleep(1) >= 0, "Failed to yield to drop hazards");
 		// Try a double free.  This may logically succeed, but should not affect
 		// our quota.
-		heap_free(SECOND_HEAP, ptr);
+		TEST_EQUAL(heap_free(SECOND_HEAP, ptr),
+		           -EPERM,
+		           "Attempt to free freed but hazarded pointer not EPERM");
 		// Sleep again to make sure that the lambda from our async is gone.
 		// The logs may make it take more than one quantum in debug builds.
 		// The next test requires all memory allocated from the malloc
 		// capability to be freed before it starts.
-		int sleeps = 0;
+		sleeps = 0;
 		while (heap_quota_remaining(MALLOC_CAPABILITY) < MALLOC_QUOTA &&
 		       heap_quota_remaining(MALLOC_CAPABILITY) > 0)
 		{
-			Timeout t{1};
-			thread_sleep(&t);
+			TEST(sleep(1) >= 0, "Failed to sleep");
 			TEST(sleeps++ < 100,
 			     "Sleeping for too long waiting for async lambda to be freed");
 		}
@@ -485,8 +635,9 @@
 	{
 		void      *unsealedCapability;
 		auto       sealingCapability = STATIC_SEALING_TYPE(sealingTest);
+		Timeout    t{AllocTimeout};
 		Capability sealedPointer =
-		  token_sealed_unsealed_alloc(&noWait,
+		  token_sealed_unsealed_alloc(&t,
 		                              MALLOC_CAPABILITY,
 		                              sealingCapability,
 		                              tokenSize,
@@ -616,6 +767,21 @@
 		     "{}, expected {}",
 		     heap_quota_remaining(SECOND_HEAP),
 		     SECOND_HEAP_QUOTA);
+
+		/*
+		 * Test the bad-outparam failure path of token_sealed_unsealled_alloc.
+		 * This is expected to still return the allocated object.
+		 */
+		sealedPointer = token_sealed_unsealed_alloc(
+		  &noWait, SECOND_HEAP, sealingCapability, 16, nullptr);
+		TEST(sealedPointer.is_valid(), "Invalid outparam case failed alloc");
+		TEST_EQUAL(
+		  token_obj_destroy(SECOND_HEAP, sealingCapability, sealedPointer),
+		  0,
+		  "Failed to free invalid-outparam sealed object");
+		TEST_EQUAL(heap_quota_remaining(SECOND_HEAP),
+		           SECOND_HEAP_QUOTA,
+		           "Invalid outparam path failed to restore quota");
 	}
 
 } // namespace
@@ -631,12 +797,9 @@
 	const ptraddr_t HeapEnd   = LA_ABS(__export_mem_heap_end);
 
 	const size_t HeapSize = HeapEnd - HeapStart;
-	TEST(BigAllocSize < HeapSize,
-	     "Big allocation size is too large for our heap ({} >= {})",
-	     BigAllocSize,
-	     BigAllocSize);
 	debug_log("Heap size is {} bytes", HeapSize);
 
+	test_preflight();
 	test_token();
 	test_hazards();
 
@@ -682,9 +845,9 @@
 	ret = heap_free(MALLOC_CAPABILITY, array);
 	TEST(ret == 0, "Freeing array failed: {}", ret);
 
-	test_blocking_allocator();
-	heap_quarantine_empty();
-	test_revoke();
+	test_blocking_allocator(HeapSize);
+	TEST_EQUAL(heap_quarantine_empty(), 0, "Could not flush quarantine");
+	test_revoke(HeapSize);
 	test_fuzz();
 	allocations.clear();
 	allocations.shrink_to_fit();
diff --git a/tests/compartment_calls-test.cc b/tests/compartment_calls-test.cc
index a8ed1ee..d8c8368 100644
--- a/tests/compartment_calls-test.cc
+++ b/tests/compartment_calls-test.cc
@@ -68,7 +68,10 @@
 
 	test_number_of_arguments();
 
-	test_incorrect_export_table(nullptr, &outTestFailed);
+	TEST_EQUAL(
+	  test_incorrect_export_table(nullptr, &outTestFailed),
+	  0,
+	  "Test incorrect entry point without error handler bad return value");
 	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 b740f31..c418218 100644
--- a/tests/compartment_calls.h
+++ b/tests/compartment_calls.h
@@ -38,11 +38,10 @@
   const int *x4,
   int        x5,
   int        x6);
-__cheri_compartment("compartment_calls_inner") void test_incorrect_export_table(
+__cheri_compartment("compartment_calls_inner") int test_incorrect_export_table(
   __cheri_callback void (*fn)(),
-  bool *outTestFailed);
+  bool                 *outTestFailed);
 __cheri_compartment(
   "compartment_calls_inner_with_"
   "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;
diff --git a/tests/compartment_calls_inner.cc b/tests/compartment_calls_inner.cc
index 43fd9d0..f0d1c8f 100644
--- a/tests/compartment_calls_inner.cc
+++ b/tests/compartment_calls_inner.cc
@@ -97,8 +97,8 @@
 	return 0;
 }
 
-void test_incorrect_export_table(__cheri_callback void (*fn)(),
-                                 bool *outTestFailed)
+int test_incorrect_export_table(__cheri_callback void (*fn)(),
+                                bool                 *outTestFailed)
 {
 	/*
 	 * Trigger a cross-compartment call with an invalid export entry.
@@ -111,4 +111,6 @@
 	fn();
 
 	*outTestFailed = false;
+
+	return 0;
 }
diff --git a/tests/crash_recovery-test.cc b/tests/crash_recovery-test.cc
index cc67b24..1de2977 100644
--- a/tests/crash_recovery-test.cc
+++ b/tests/crash_recovery-test.cc
@@ -49,7 +49,7 @@
 int test_crash_recovery()
 {
 	debug_log("Calling crashy compartment indirectly");
-	test_crash_recovery_outer(0);
+	TEST_EQUAL(test_crash_recovery_outer(0), 0, "Indirect crash failed");
 	check_stack();
 	TEST(crashes == 0, "Ran crash handler for outer compartment");
 	debug_log("Compartment with no error handler returned normally after "
diff --git a/tests/crash_recovery.h b/tests/crash_recovery.h
index 9c2c7bd..ebbdb56 100644
--- a/tests/crash_recovery.h
+++ b/tests/crash_recovery.h
@@ -6,7 +6,7 @@
 
 __cheri_compartment("crash_recovery_inner") void *test_crash_recovery_inner(
   int);
-__cheri_compartment("crash_recovery_outer") void test_crash_recovery_outer(int);
+__cheri_compartment("crash_recovery_outer") int test_crash_recovery_outer(int);
 
 /**
  * Checks that the stack is entirely full of zeroes below the current stack
diff --git a/tests/crash_recovery_outer.cc b/tests/crash_recovery_outer.cc
index 9d3513d..6b59b7e 100644
--- a/tests/crash_recovery_outer.cc
+++ b/tests/crash_recovery_outer.cc
@@ -6,7 +6,7 @@
 #include <cheri.hh>
 #include <errno.h>
 
-void test_crash_recovery_outer(int)
+int test_crash_recovery_outer(int)
 {
 	debug_log(
 	  "Calling crashy compartment from compartment with no error handler");
@@ -19,4 +19,5 @@
 	debug_log("Calling crashy compartment returned to compartment with no "
 	          "error handler.  Return value: {}",
 	          ret);
+	return 0;
 }
diff --git a/tests/futex-test.cc b/tests/futex-test.cc
index 4abfe6c..433f110 100644
--- a/tests/futex-test.cc
+++ b/tests/futex-test.cc
@@ -25,6 +25,7 @@
 {
 	static uint32_t futex;
 	int             ret;
+	int             sleeps;
 	// Make sure that waking a futex with no sleepers doesn't crash!
 	ret = futex_wake(&futex, 1);
 	TEST(ret == 0, "Waking a futex with no sleepers should return 0");
@@ -32,7 +33,7 @@
 	// has been set to 1.
 	async([]() {
 		futex = 1;
-		futex_wake(&futex, 1);
+		(void)futex_wake(&futex, 1);
 	});
 	debug_log("Calling blocking futex_wait");
 	ret = futex_wait(&futex, 0);
@@ -123,7 +124,7 @@
             while (state != 1)
             {
                 Timeout t{3};
-                thread_sleep(&t);
+                (void)thread_sleep(&t);
             }
             debug_log("Consuming all CPU on medium-priority thread");
             state = 2;
@@ -144,27 +145,31 @@
             debug_log("Low-priority thread finished, unlocking");
             state = 4;
             futex = 0;
-            futex_wake(&futex, 1);
+            (void)futex_wake(&futex, 1);
         }
 	};
 	async(priorityBug);
 	async(priorityBug);
 	debug_log("Waiting for background threads to enter the right state");
-	while (state != 2)
+	for (sleeps = 0; (sleeps < 100) && (state != 2); sleeps++)
 	{
-		Timeout t{3};
-		thread_sleep(&t);
+		TEST(sleep(3) >= 0, "Failed to sleep");
 	}
+	TEST(sleeps < 100, "Waited too long for background threads");
 	debug_log("High-priority thread attempting to acquire futex owned by "
 	          "low-priority thread without priority propagation");
 	state       = 3;
 	t.remaining = 1;
-	futex_timed_wait(&t, &futex, futex, FutexNone);
+	TEST_EQUAL(futex_timed_wait(&t, &futex, futex, FutexNone),
+	           -ETIMEDOUT,
+	           "futex_timed_wait failed");
 	TEST(futex != 0, "Made progress surprisingly!");
 	debug_log("High-priority thread attempting to acquire futex owned by "
 	          "low-priority thread with priority propagation");
 	t.remaining = 4;
-	futex_timed_wait(&t, &futex, futex, FutexPriorityInheritance);
+	TEST_EQUAL(futex_timed_wait(&t, &futex, futex, FutexPriorityInheritance),
+	           0,
+	           "futex_timed_wait failed");
 	TEST(futex == 0, "Failed to make progress!");
 
 	futex       = 1234;
diff --git a/tests/list-test.cc b/tests/list-test.cc
index 5ea7700..bf6f66a 100644
--- a/tests/list-test.cc
+++ b/tests/list-test.cc
@@ -174,7 +174,10 @@
 	// we do not use here), not to the removed element.
 	LinkedObject::ObjectRing *removedCell = objects.first();
 	ds::linked_list::remove(objects.first());
-	heap_free(MALLOC_CAPABILITY, LinkedObject::from_ring(removedCell));
+	TEST_EQUAL(
+	  heap_free(MALLOC_CAPABILITY, LinkedObject::from_ring(removedCell)),
+	  0,
+	  "Failed to free removed cell");
 	TEST(LinkedObject::from_ring(objects.first())->data == 1,
 	     "First element of the list is incorrect after removing the first "
 	     "element, expected {}, got {}",
@@ -191,7 +194,8 @@
 	{
 		struct LinkedObject *o = LinkedObject::from_ring(cell);
 		cell                   = cell->cell_next();
-		heap_free(MALLOC_CAPABILITY, o);
+		TEST_EQUAL(
+		  heap_free(MALLOC_CAPABILITY, o), 0, "Failed to free list object");
 		counter++;
 	}
 
@@ -213,7 +217,10 @@
 		  // removed cell. This is great here because we will free the
 		  // object anyways. We could also use `remove` here.
 		  auto l = ds::linked_list::unsafe_remove(cell);
-		  heap_free(MALLOC_CAPABILITY, LinkedObject::from_ring(cell));
+		  TEST_EQUAL(
+		    heap_free(MALLOC_CAPABILITY, LinkedObject::from_ring(cell)),
+		    0,
+		    "Failed to free searched object");
 		  // `l` is the predecessor of `cell` in the residual ring, so
 		  // this does exactly what we want when `::search` iterates.
 		  cell = l;
@@ -221,7 +228,9 @@
 		  return false;
 	  });
 	// `::search` does not visit the element passed (`middle`)
-	heap_free(MALLOC_CAPABILITY, LinkedObject::from_ring(middle));
+	TEST_EQUAL(heap_free(MALLOC_CAPABILITY, LinkedObject::from_ring(middle)),
+	           0,
+	           "Failed to free middle object");
 	counter++;
 
 	TEST(counter == NumberOfListElements - 1,
diff --git a/tests/misc-test.cc b/tests/misc-test.cc
index e39b1e5..3df4689 100644
--- a/tests/misc-test.cc
+++ b/tests/misc-test.cc
@@ -268,6 +268,7 @@
 	           o.permissions().without(Permission::Global),
 	           "Loading global sealed cap through non-LoadGlobal bad perms");
 
+#ifndef CHERIOT_NO_SAIL_83
 	/*
 	 * Use CAndPerm to shed Global from our o cap.
 	 * Spell this a little oddly to make sure we get CAndPerm with a mask of
@@ -278,6 +279,10 @@
 	oLocal2.without_permissions(Permission::Global);
 
 	TEST_EQUAL(oLocal2, OLocal1, "CAndPerm ~GL gone wrong");
+#else
+	debug_log(
+	  "Skipping test for cheriot-sail#83 because the ISA version is too old.");
+#endif
 }
 
 int test_misc()
diff --git a/tests/multiwaiter-test.cc b/tests/multiwaiter-test.cc
index b6b77dd..6cc0249 100644
--- a/tests/multiwaiter-test.cc
+++ b/tests/multiwaiter-test.cc
@@ -32,12 +32,12 @@
 	EventWaiterSource events[4];
 
 	debug_log("Testing error case: Invalid values");
-	events[0] = {nullptr, static_cast<EventWaiterKind>(5), 0};
+	events[0] = {nullptr, 0};
 	ret       = multiwaiter_wait(&t, mw, events, 1);
 	TEST(ret == -EINVAL, "multiwaiter returned {}, expected {}", ret, -EINVAL);
 
 	debug_log("Testing one futex, already ready");
-	events[0]   = {&futex, EventWaiterFutex, 1};
+	events[0]   = {&futex, 1};
 	t.remaining = 5;
 	ret         = multiwaiter_wait(&t, mw, events, 1);
 	TEST(ret == 0, "multiwaiter returned {}, expected 0", ret);
@@ -47,13 +47,13 @@
 			sleep(1);
 			debug_log("Waking futex from background thread");
 			*futexWord = value;
-			futex_wake(futexWord, 1);
+			TEST(futex_wake(futexWord, 1) >= 0, "futex_wait failed");
 		});
 	};
 
 	debug_log("Testing one futex, not yet ready");
 	setFutex(&futex, 1);
-	events[0]   = {&futex, EventWaiterFutex, 0};
+	events[0]   = {&futex, 0};
 	t.remaining = 6;
 	ret         = multiwaiter_wait(&t, mw, events, 1);
 	TEST(ret == 0, "multiwaiter returned {}, expected 0", ret);
@@ -62,8 +62,8 @@
 	futex  = 0;
 	futex2 = 2;
 	setFutex(&futex2, 3);
-	events[0]   = {&futex, EventWaiterFutex, 0};
-	events[1]   = {&futex2, EventWaiterFutex, 2};
+	events[0]   = {&futex, 0};
+	events[1]   = {&futex2, 2};
 	t.remaining = 6;
 	ret         = multiwaiter_wait(&t, mw, events, 2);
 	TEST(ret == 0, "multiwaiter returned {}, expected 0", ret);
@@ -118,7 +118,7 @@
 	futex = 0;
 	setFutex(&futex, 1);
 	multiwaiter_queue_receive_init(&events[0], queue);
-	events[1]   = {&futex, EventWaiterFutex, 0};
+	events[1]   = {&futex, 0};
 	t.remaining = 6;
 	ret         = multiwaiter_wait(&t, mw, events, 2);
 	TEST(ret == 0, "multiwait on futex and queue returned {}", ret);
diff --git a/tests/stack-test.cc b/tests/stack-test.cc
index b3b894f..205a398 100644
--- a/tests/stack-test.cc
+++ b/tests/stack-test.cc
@@ -27,16 +27,17 @@
 	return ErrorRecoveryBehaviour::ForceUnwind;
 }
 
-__cheri_callback void test_trusted_stack_exhaustion()
+__cheri_callback int test_trusted_stack_exhaustion()
 {
-	exhaust_trusted_stack(&test_trusted_stack_exhaustion,
-	                      &threadStackTestFailed);
+	return exhaust_trusted_stack(&test_trusted_stack_exhaustion,
+	                             &threadStackTestFailed);
 }
 
-__cheri_callback void cross_compartment_call()
+__cheri_callback int cross_compartment_call()
 {
 	TEST(false,
 	     "Cross compartment call with invalid CSP shouldn't be reachable");
+	return -EINVAL;
 }
 
 namespace
@@ -70,7 +71,10 @@
 	void expect_handler(bool handlerExpected)
 	{
 		debug_log("Expected to invoke the handler? {}", handlerExpected);
-		set_expected_behaviour(&threadStackTestFailed, handlerExpected);
+		TEST_EQUAL(
+		  set_expected_behaviour(&threadStackTestFailed, handlerExpected),
+		  0,
+		  "Failed to set expectations");
 	}
 
 	__attribute__((used)) extern "C" int test_small_stack()
@@ -114,7 +118,7 @@
 // Defeat the compiler optimisation that may turn our first call to this into a
 // call. If the compiler does this then we will fail on an even number of
 // cross-compartment calls not an odd number.
-__cheri_callback void (*volatile crossCompartmentCall)();
+__cheri_callback int (*volatile crossCompartmentCall)();
 
 /*
  * The stack tests should cover the edge-cases scenarios for both
@@ -139,7 +143,7 @@
 	TEST(ret == -ENOTENOUGHSTACK,
 	     "test_with_small_stack failed, returned {} with 128-byte stack",
 	     ret);
-	__cheri_callback void (*callback)() = cross_compartment_call;
+	__cheri_callback int (*callback)() = cross_compartment_call;
 
 	crossCompartmentCall = test_trusted_stack_exhaustion;
 	debug_log("exhaust trusted stack, do self recursion with a cheri_callback");
@@ -148,12 +152,15 @@
 
 	debug_log("exhausting the compartment stack");
 	expect_handler(false);
-	exhaust_thread_stack();
+	TEST_EQUAL(
+	  exhaust_thread_stack(), -ECOMPARTMENTFAIL, "exhaust_thread_stack failed");
 
 	debug_log("exhausting the compartment stack during a switcher call");
 	expect_handler(false);
 	threadStackTestFailed = true;
-	exhaust_thread_stack_spill(callback);
+	TEST_EQUAL(exhaust_thread_stack_spill(callback),
+	           0,
+	           "exhaust_thread_stack_spill failed");
 	TEST(threadStackTestFailed == false, "switcher did not return error");
 
 	debug_log("modifying stack permissions on fault");
@@ -164,7 +171,9 @@
 		  compartmentStackPermissions.without(permissionToRemove);
 		debug_log("Permissions: {}", permissions);
 		expect_handler(stack_is_mostly_valid(permissions));
-		set_csp_permissions_on_fault(permissions);
+		TEST_EQUAL(set_csp_permissions_on_fault(permissions),
+		           -ECOMPARTMENTFAIL,
+		           "Unexpected success with restricted permissions");
 	}
 
 	debug_log("modifying stack permissions on cross compartment call");
@@ -173,16 +182,22 @@
 		auto permissions =
 		  compartmentStackPermissions.without(permissionToRemove);
 		debug_log("Permissions: {}", permissions);
-		set_csp_permissions_on_call(permissions, callback);
+		TEST_EQUAL(set_csp_permissions_on_call(permissions, callback),
+		           -ECOMPARTMENTFAIL,
+		           "Unexpected success with restricted permissions");
 	}
 
 	debug_log("invalid stack on fault");
 	expect_handler(false);
-	test_stack_invalid_on_fault();
+	TEST_EQUAL(test_stack_invalid_on_fault(),
+	           -ECOMPARTMENTFAIL,
+	           "stack_invalid_on_fault failed");
 
 	debug_log("invalid stack on cross compartment call");
 	expect_handler(false);
+	TEST_EQUAL(test_stack_invalid_on_call(callback),
+	           -ECOMPARTMENTFAIL,
+	           "stack_invalid_on_call failed");
 
-	test_stack_invalid_on_call(callback);
 	return 0;
 }
diff --git a/tests/stack_integrity_thread.cc b/tests/stack_integrity_thread.cc
index c897432..bfeab94 100644
--- a/tests/stack_integrity_thread.cc
+++ b/tests/stack_integrity_thread.cc
@@ -64,14 +64,16 @@
  * Set up the handler expectations.  Takes the caller's error flag and
  * whether the handler is expected as arguments.
  */
-void set_expected_behaviour(bool *outTestFailed, bool handlerExpected)
+int set_expected_behaviour(bool *outTestFailed, bool handlerExpected)
 {
 	expectedHandler       = handlerExpected;
 	threadStackTestFailed = outTestFailed;
 	*outTestFailed        = handlerExpected;
+
+	return 0;
 }
 
-void exhaust_thread_stack()
+int exhaust_thread_stack()
 {
 	/* Move the compartment's stack near its end, in order to
 	 * trigger stack exhaustion while the switcher handles
@@ -92,6 +94,8 @@
 
 	*threadStackTestFailed = true;
 	TEST(false, "Should be unreachable");
+
+	return 0;
 }
 
 /**
@@ -99,7 +103,7 @@
  * callee-saved state.  The result should simply be an error return, rather than
  * a forced-unwind.
  */
-void exhaust_thread_stack_spill(__cheri_callback void (*fn)())
+int exhaust_thread_stack_spill(__cheri_callback int (*fn)())
 {
 	register auto      rfn asm("ct1") = fn;
 	register uintptr_t res asm("ca0") = 0;
@@ -126,52 +130,59 @@
 
 	*threadStackTestFailed = false;
 	TEST(res == -ENOTENOUGHSTACK, "Bad return {}", res);
+
+	return 0;
 }
 
-void set_csp_permissions_on_fault(PermissionSet newPermissions)
+int set_csp_permissions_on_fault(PermissionSet newPermissions)
 {
 	__asm__ volatile(
 	  "candperm csp, csp, %0\n"
 	  "csh      zero, 0(cnull)\n" ::"r"(newPermissions.as_raw()));
 
 	TEST(false, "Should be unreachable");
+	return -EINVAL;
 }
 
-void set_csp_permissions_on_call(PermissionSet newPermissions,
-                                 __cheri_callback void (*fn)())
+int set_csp_permissions_on_call(PermissionSet        newPermissions,
+                                __cheri_callback int (*fn)())
 {
 	CALL_CHERI_CALLBACK(fn, "candperm csp, csp, %1\n", newPermissions.as_raw());
 
 	TEST(false, "Should be unreachable");
+	return -EINVAL;
 }
 
-void test_stack_invalid_on_fault()
+int test_stack_invalid_on_fault()
 {
 	__asm__ volatile("ccleartag     csp, csp\n"
 	                 "csh           zero, 0(cnull)\n");
 
 	*threadStackTestFailed = true;
 	TEST(false, "Should be unreachable");
+	return -EINVAL;
 }
 
-void test_stack_invalid_on_call(__cheri_callback void (*fn)())
+int test_stack_invalid_on_call(__cheri_callback int (*fn)())
 {
 	// the `move zero, %1` is a no-op, just to have an operand
 	CALL_CHERI_CALLBACK(fn, "move zero, %1\nccleartag csp, csp\n", 0);
 
 	*threadStackTestFailed = true;
 	TEST(false, "Should be unreachable");
+	return -EINVAL;
 }
 
-void self_recursion(__cheri_callback void (*fn)())
+int self_recursion(__cheri_callback int (*fn)())
 {
 	(*fn)();
+	return 0;
 }
 
-void exhaust_trusted_stack(__cheri_callback void (*fn)(),
-                           bool *outLeakedSwitcherCapability)
+int exhaust_trusted_stack(__cheri_callback int (*fn)(),
+                          bool                *outLeakedSwitcherCapability)
 {
-	self_recursion(fn);
+	return self_recursion(fn);
 }
 
 int test_stack_requirement()
diff --git a/tests/stack_tests.h b/tests/stack_tests.h
index 93ad94b..a209e2e 100644
--- a/tests/stack_tests.h
+++ b/tests/stack_tests.h
@@ -4,28 +4,27 @@
 
 using namespace CHERI;
 
-__cheri_compartment("stack_integrity_thread") void exhaust_trusted_stack(
-  __cheri_callback void (*fn)(),
-  bool *outLeakedSwitcherCapability);
-__cheri_compartment("stack_integrity_thread") void exhaust_thread_stack();
-__cheri_compartment("stack_integrity_thread") void exhaust_thread_stack_spill(
-  __cheri_callback void (*fn)());
-__cheri_compartment("stack_integrity_thread") void set_csp_permissions_on_fault(
+__cheri_compartment("stack_integrity_thread") int exhaust_trusted_stack(
+  __cheri_callback int (*fn)(),
+  bool                *outLeakedSwitcherCapability);
+__cheri_compartment("stack_integrity_thread") int exhaust_thread_stack();
+__cheri_compartment("stack_integrity_thread") int exhaust_thread_stack_spill(
+  __cheri_callback int (*fn)());
+__cheri_compartment("stack_integrity_thread") int set_csp_permissions_on_fault(
   PermissionSet newPermissions);
-__cheri_compartment("stack_integrity_thread") void set_csp_permissions_on_call(
-  PermissionSet newPermissions,
-  __cheri_callback void (*fn)());
-__cheri_compartment(
-  "stack_integrity_thread") void test_stack_invalid_on_fault();
-__cheri_compartment("stack_integrity_thread") void test_stack_invalid_on_call(
-  __cheri_callback void (*fn)());
+__cheri_compartment("stack_integrity_thread") int set_csp_permissions_on_call(
+  PermissionSet        newPermissions,
+  __cheri_callback int (*fn)());
+__cheri_compartment("stack_integrity_thread") int test_stack_invalid_on_fault();
+__cheri_compartment("stack_integrity_thread") int test_stack_invalid_on_call(
+  __cheri_callback int (*fn)());
 
 /**
  * Sets what we expect to happen for this test.  Is a fault expected to invoke
  * the handler?  The fault handler will set or clear `*outTestFailed` when a
  * fault is received, depending on whether it was expected.
  */
-__cheri_compartment("stack_integrity_thread") void set_expected_behaviour(
+__cheri_compartment("stack_integrity_thread") int set_expected_behaviour(
   bool *outTestFailed,
   bool  handlerExpected);
 
diff --git a/tests/test-runner.cc b/tests/test-runner.cc
index c9f3acb..5aaf023 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);
@@ -82,7 +79,7 @@
 /**
  * Test suite entry point.  Runs all of the tests that we have defined.
  */
-void __cheri_compartment("test_runner") run_tests()
+int __cheri_compartment("test_runner") run_tests()
 {
 	// magic_enum is a pretty powerful stress-test of various bits of linkage.
 	// In generating `enum_values`, it generates constant strings and pointers
@@ -114,8 +111,9 @@
 		           "Iterator of PermissionSet failed");
 	}
 	// These need to be checked visually
-	debug_log("Trying to print 8-bit integer: {}", uint8_t(0x12));
-	debug_log("Trying to print unsigned 8-bit integer: {}", int8_t(34));
+	debug_log("Trying to print 8-bit integer: {}", static_cast<uint8_t>(0x12));
+	debug_log("Trying to print unsigned 8-bit integer: {}",
+	          static_cast<int8_t>(34));
 	debug_log("Trying to print char: {}", 'c');
 	debug_log("Trying to print 32-bit integer: {}", 12345);
 	debug_log("Trying to print 64-bit integer: {}", 123456789012345LL);
@@ -162,13 +160,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();
-	}
 }
diff --git a/tests/tests.hh b/tests/tests.hh
index 14780ed..79779b7 100644
--- a/tests/tests.hh
+++ b/tests/tests.hh
@@ -56,6 +56,6 @@
 inline Ticks sleep(Ticks ticks)
 {
 	Timeout t{ticks};
-	thread_sleep(&t);
+	TEST(thread_sleep(&t) >= 0, "Failed to sleep");
 	return t.elapsed;
 };
diff --git a/tests/thread_pool-test.cc b/tests/thread_pool-test.cc
index 52a08e0..9aaf861 100644
--- a/tests/thread_pool-test.cc
+++ b/tests/thread_pool-test.cc
@@ -75,8 +75,7 @@
 	int sleeps = 0;
 	while (counter < 2)
 	{
-		Timeout t{1};
-		thread_sleep(&t);
+		TEST(sleep(1) >= 0, "Failed to sleep");
 		TEST(sleeps < 100, "Gave up after too many sleeps");
 	}
 	debug_log("Yielded {} times for the thread pool to run our jobs", sleeps);
@@ -86,13 +85,8 @@
 	free(heapInt);
 
 	async([]() {
-		auto fast = thread_id_get();
-		auto slow = thread_id_get();
-		TEST(fast == slow,
-		     "Thread ID is different in fast ({}) and slow ({}) accessors",
-		     fast,
-		     slow);
-		TEST(fast != 1, "Thread ID for thread pool thread should not be 1");
+		TEST(thread_id_get() != 1,
+		     "Thread ID for thread pool thread should not be 1");
 	});
 
 	CHERI::Capability<void> mainThread{switcher_current_thread()};
@@ -125,8 +119,7 @@
 	{
 		if (!asyncThread)
 		{
-			Timeout t{1};
-			thread_sleep(&t);
+			TEST(sleep(1) >= 0, "Failed to sleep");
 		}
 	}
 	TEST(asyncThread, "Worker thread did not provide thread pointer");
@@ -134,8 +127,7 @@
 	bool ret         = switcher_interrupt_thread(asyncThread);
 	interruptStarted = true;
 	TEST(ret, "Interrupting worker thread failed: {}", ret);
-	Timeout t{3};
-	thread_sleep(&t);
+	TEST(sleep(3) >= 0, "Failed to sleep");
 	TEST(interrupted, "Worker thread was not interrupted");
 	return 0;
 	static cheriot::atomic<uint32_t> barrier{3};