Merge "sencha: track hardware memory map"
diff --git a/.clang-tidy b/.clang-tidy
index 43eb5ff..9c44d7f 100644
--- a/.clang-tidy
+++ b/.clang-tidy
@@ -1,4 +1,4 @@
-Checks:          'clang-diagnostic-*,-clang-diagnostic-error,google-readability-casting,readability-else-after-return,performance-unnecessary-copy-initialization,bugprone-use-after-move,modernize-use-nullptr,modernize-redundant-void-arg,modernize-return-braced-init-list,modernize-use-default-member-init,modernize-use-equals-default,modernize-use-equals-delete,modernize-use-nodiscard,modernize-use-override,cppcoreguidelines-avoid-goto,misc-unconventional-assign-operator,cppcoreguidelines-narrowing-conversions,bugprone-assert-side-effect,bugprone-bool-pointer-implicit-conversion,bugprone-copy-constructor-init,bugprone-forwarding-reference-overload,bugprone-macro-parentheses,bugprone-macro-repeated-side-effects,bugprone-move-forwarding-reference,bugprone-misplaced-widening-cast,bugprone-swapped-arguments,bugprone-undelegated-constructor,bugprone-unused-raii,cert-dcl21-cpp,llvm-namespace-comment,misc-static-assert,misc-redundant-expression,modernize-loop-convert,readability-non-const-parameter,readability-identifier-naming'
+Checks: 'clang-diagnostic-*,-clang-diagnostic-error,google-readability-casting,readability-else-after-return,performance-unnecessary-copy-initialization,bugprone-use-after-move,modernize-use-nullptr,modernize-redundant-void-arg,modernize-return-braced-init-list,modernize-use-default-member-init,modernize-use-equals-default,modernize-use-equals-delete,modernize-use-nodiscard,modernize-use-override,cppcoreguidelines-avoid-goto,misc-unconventional-assign-operator,cppcoreguidelines-narrowing-conversions,bugprone-assert-side-effect,bugprone-bool-pointer-implicit-conversion,bugprone-copy-constructor-init,bugprone-forwarding-reference-overload,bugprone-macro-parentheses,bugprone-macro-repeated-side-effects,bugprone-move-forwarding-reference,bugprone-misplaced-widening-cast,bugprone-swapped-arguments,bugprone-undelegated-constructor,bugprone-unused-raii,cert-dcl21-cpp,llvm-namespace-comment,misc-static-assert,misc-redundant-expression,modernize-loop-convert,readability-non-const-parameter,readability-identifier-naming,readability-braces-around-statements'
 FormatStyle: file
 UseColor: true
 WarningsAsErrors: false
diff --git a/.github/workflows/test-new-xmake-nightly.yml b/.github/workflows/test-new-xmake-nightly.yml
new file mode 100644
index 0000000..eac04ab
--- /dev/null
+++ b/.github/workflows/test-new-xmake-nightly.yml
@@ -0,0 +1,41 @@
+name: Nightly xmake test
+
+on:
+  schedule:
+    - cron: '0 0 * * *'
+  workflow_dispatch:
+
+jobs:
+  run-tests:
+    strategy:
+      fail-fast: false
+    runs-on: ubuntu-latest
+    container:
+      image: ghcr.io/cheriot-platform/devcontainer:latest
+      options: --user 1001
+    steps:
+    - name: Checkout repository and submodules
+      uses: actions/checkout@v3
+      with:
+        submodules: recursive
+    - name: Build latest xmake
+      run: |
+        xmake update dev
+        sudo apt remove -y xmake
+        echo ~/.xmake/profile
+        . ~/.xmake/profile
+    - name: Build tests
+      run: |
+        pwd
+        echo ~/.xmake/profile
+        which xmake
+        xmake --version
+        cd tests
+        xmake f --board=${{ matrix.board }} --sdk=/cheriot-tools/ ${{ matrix.build-flags }}
+        xmake
+    - name: Run tests
+      run: |
+        . ~/.xmake/profile
+        xmake --version
+        cd tests
+        xmake run
diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md
index d97e2b0..f601025 100644
--- a/docs/GettingStarted.md
+++ b/docs/GettingStarted.md
@@ -79,7 +79,7 @@
  - The LLVM-based toolchain with CHERIoT support
  - The emulator generated from the Sail formal model of the CHERIoT ISA
  - The xmake build tool
- - [Sonata only] u2futils to create images that Sonata's loader can boot
+ - [Sonata only] uf2utils to create images that Sonata's loader can boot
 
 Building LLVM is fairly simple, but requires a fast machine and several GiBs of disk space.
 Building the executable model requires a working ocaml installation.
@@ -224,7 +224,7 @@
 # apt install xmake
 ```
 
-### Installing u2futils
+### Installing uf2utils
 
 If you are working with Sonata, you will need to convert the ELF files that the linker produces to [USB Flashing Format (UF2)](https://github.com/microsoft/uf2).
 The firmware on the RPi 2040 on the Sonata board can then load these files onto the CHERIoT Ibex and run them.
diff --git a/examples/06.producer-consumer/producer.cc b/examples/06.producer-consumer/producer.cc
index a4ea318..417505a 100644
--- a/examples/06.producer-consumer/producer.cc
+++ b/examples/06.producer-consumer/producer.cc
@@ -16,17 +16,16 @@
 void __cheri_compartment("producer") run()
 {
 	// Allocate the queue
-	SObj sendHandle;
-	SObj receiveHandle;
+	SObj queue;
 	non_blocking<queue_create_sealed>(
-	  MALLOC_CAPABILITY, &sendHandle, &receiveHandle, sizeof(int), 16);
+	  MALLOC_CAPABILITY, &queue, sizeof(int), 16);
 	// Pass the queue handle to the consumer.
-	set_queue(receiveHandle);
+	set_queue(queue);
 	Debug::log("Starting producer loop");
 	// Loop, sending some numbers to the other thread.
 	for (int i = 1; i < 200; i++)
 	{
-		int ret = blocking_forever<queue_send_sealed>(sendHandle, &i);
+		int ret = blocking_forever<queue_send_sealed>(queue, &i);
 		// Abort if the queue send errors.
 		Debug::Invariant(ret == 0, "Queue send failed {}", ret);
 	}
diff --git a/examples/10.audit/README.md b/examples/10.audit/README.md
new file mode 100644
index 0000000..cd8d379
--- /dev/null
+++ b/examples/10.audit/README.md
@@ -0,0 +1,313 @@
+Auditing compartments
+=====================
+
+This example shows a simple use of the auditing tool.
+The code in this is very simple:
+
+ - A `caesar` compartment exposes interfaces to encrypt and decrypt data using a Caesar cypher.
+ - A `producer` compartment uses this to encrypt a value using a key held in a software capability.
+ - An `entry` compartment receives this encrypted data and forwards it to a `consumer` compartment.
+ - The `consumer` compartment receives the data and invokes the `caesar` compartment to decrypt it.
+
+This is a fairly contrived example but it provides a simple structure for exploring [`cheriot-audit`](https://github.com/CHERIoT-Platform/cheriot-audit).
+This tool applies [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) policies to CHERIoT firmware images.
+
+Note: For all of the `cheriot-audit` examples, we'll assume that you're running a command something like this:
+
+```sh
+$ cheriot-audit --board=../../sdk/boards/sail.json --firmware-report=build/cheriot/cheriot/release/caesar_example.json --module=caesar.rego  --query '{query}' | jq
+```
+
+You may need to specify a full path to cheriot-audit (it is in `/cheriot-tools/bin` in the dev container).
+The query will be shown before the results.
+Piping the output to `jq` is optional, but will give pretty-printed output for valid JSON (remove it for places where you get an error: `undefined` is not rendered by `jq`).
+
+The `--board` argument is the same as the board JSON file that you identified with the `--board` argument you passed to `xmake` (if you didn't pass a path to that, it will look in `sdk/boards`).
+The `--firmware` is the JSON file produced during the final firmware link step.
+The `--module` argument is the [`caesar.rego`](caesar.rego) file that contains the policy written for this example.
+Finally, the `--query` argument is the Rego query to run.
+
+Validating our Caesar capabilities
+----------------------------------
+
+The capabilities for the Caesar cypher (defined in [`caesar_cypher.h`](caesar_cypher.h)) contain three single-byte fields:
+
+ - A permit-encrypt permission.
+ - A permit-decrypt permission.
+ - A shift value (Caesar cypher is a simple substitution cypher that rotates the alphabet, and so the 'key' is the amount that each letter is shifted).
+
+These are all static sealed objects that are held by the compartment that can authorise encryption or decryption.
+To encrypt a message, you call the encrypt function exposed by the `caesar` compartment and pass it one of these capabilities.
+The compartment will dynamically check that it is the right kind of capability and that it permits sealing, then encrypt with the embedded shift amount.
+
+This has the property that any compartment that holds an authorising capability can request encryption or decryption, but does not itself know the key.
+
+To validate the properties of our keys, let's start by defining a rule that identifies whether an import is a Caesar capability:
+
+```rego
+# Check if an import is sealed with the Caesar capability type
+is_sealed_caesar_capability(capability) {
+    capability.kind = "SealedObject"
+    capability.sealing_type.compartment = "caesar"
+    capability.sealing_type.key = "CaesarCapabilityType"
+}
+```
+
+This is a unary rule (it takes one argument).
+Rego rules are similar to Prolog predicates.
+They are not functions that return true or false, they are logical expressions that either hold or don't.
+This distinction rarely matters, but it can be confusing because a failure will be reported in Rego as `undefined` instead of `false`.
+
+This rule holds (is true) if all of the rules listed on the lines between braces hold.
+The `=` operator in Rego is *unification*, not assignment.
+This means that it will try to find values on the left and right sides that allow the equality to hold.
+
+In English, this says that the argument is a sealed Caesar capability if (and only if) its kind if `SealedObject`, the compartment that owns the sealing type is `caesar` and the sealing type is the one that this compartment exports as `CaesarCapabilityType`.
+
+We can see how this works by using it with a Rego *comprehension*.
+Try running this query:
+
+```rego
+[ c | c = input.compartments[_].imports[_] ; data.caesar.is_sealed_caesar_capability(c) ]
+```
+
+This will evaluate to an array of values of `c`, where `c` is every import from any compartment, filtered by the rule that we've just written.
+The underscores are distinct anonymous variables.
+Because we never constrain these, they can be any value, and so this will find any compartment, and any import in that compartment, and then filter them.
+
+Note that we refer to our rule with a `data.caesar` prefix.
+All Rego modules are imported into the `data` namespace with the module name as the second-level namespace.
+
+The output should look something like this:
+
+```json
+[
+  {
+    "contents": "00015f00",
+    "kind": "SealedObject",
+    "sealing_type": {
+      "compartment": "caesar",
+      "key": "CaesarCapabilityType",
+      "provided_by": "build/cheriot/cheriot/release/caesar.compartment",
+      "symbol": "__export.sealing_type.caesar.CaesarCapabilityType"
+    }
+  },
+  {
+    "contents": "01005f00",
+    "kind": "SealedObject",
+    "sealing_type": {
+      "compartment": "caesar",
+      "key": "CaesarCapabilityType",
+      "provided_by": "build/cheriot/cheriot/release/caesar.compartment",
+      "symbol": "__export.sealing_type.caesar.CaesarCapabilityType"
+    }
+  }
+]
+```
+
+This has found the two capabilities that we expected to find looking for.
+
+Decoding our Caesar capabilities
+--------------------------------
+
+Seeing something like `"contents": "01005f00"` in the above example isn't that informative.
+Is this a valid set of values?
+The next step is to write something to decode these.
+
+We'll start with a helper to convert integers into C booleans:
+
+```rego
+value_as_boolean(value) = output {
+    value = 1
+    output = true
+}
+
+value_as_boolean(value) = output {
+    value = 0
+    output = false
+}
+```
+
+This is a Rego rule that has two definitions.
+The first will hold if the value is 1, and will set the result to `true`.
+The second will hold if the value is 0, and will set the result to `false`.
+If the value is anything other than 0 or 1, this rule will not hold.
+
+Try this with the following two queries:
+
+```rego
+data.caesar.value_as_boolean(1)
+data.caesar.value_as_boolean(2)
+```
+
+These should give `true` and `undefined`, respectively.
+Rego reports unification failure as `undefined` and this propagates upwards.
+Any rule that depends on a rule that is undefined will also be undefined.
+We can use this to make sure that the boolean values that we want are canonical true and false values, as well as decoding them.
+
+With these defined, let's move on to the Rego rule that decodes one of these capabilities:
+
+```rego
+decode_user_key_capability(capability) = decoded {
+    # Fail if this is not sealed with the Caesar capability type
+    is_sealed_caesar_capability(capability)
+    some permitEncrypt, permitDecrypt, shift
+
+    # Extract the values.  Each of these will fail if the value is not as expected.
+    # permitEncrypt is a (single-byte) boolean value at offset 0.
+    permitEncrypt = value_as_boolean(integer_from_hex_string(capability.contents, 0, 1))
+
+    # permitDecrypt is a (single-byte) boolean value at offset 1.
+    permitDecrypt = value_as_boolean(integer_from_hex_string(capability.contents, 1, 1))
+
+    # shift is a single-byte integer value at offset 2.
+    shift = integer_from_hex_string(capability.contents, 2, 1)
+    decoded = {
+        "permitEncrypt": permitEncrypt,
+        "permitDecrypt": permitDecrypt,
+        "shift": shift,
+    }
+}
+```
+
+This starts by depending on the rule that we defined first, which will cause this to fail for anything that isn't a capability of the expected type.
+Next, we define three local variables to hold the three fields that we expect.
+We'll extract each of these using `integer_from_hex_string`, a built-in function provided by `cheriot-audit`.
+This takes a string, an offset, and a length and will decode a little-endian integer from the hex string provided.
+We're extracting three one-byte integers at offsets 0, 1, and 2.
+We're then converting the first two to booleans.
+
+Finally, if all of that worked, we're returning a new object that mirrors the C structure that represents our capability.
+We can use this with another comprehension to extract and decode all valid capabilities.
+This is sufficiently useful that we'll define a new rule for it:
+
+```rego
+all_valid_caesar_capabilities = [{"owner": owner, "capability": decode_user_key_capability(c)} | c = input.compartments[owner].imports[_];         is_sealed_caesar_capability(c)]
+```
+
+This is a slightly more complex comprehension.
+The result is defining a new object with both the owner and the decoded capability.
+Try running this:
+
+```rego
+data.caesar.all_valid_caesar_capabilities
+```
+
+You should see something like this:
+
+```rego
+[
+  {
+    "capability": {
+      "permitDecrypt": true,
+      "permitEncrypt": false,
+      "shift": 95
+    },
+    "owner": "consumer"
+  },
+  {
+    "capability": {
+      "permitDecrypt": false,
+      "permitEncrypt": true,
+      "shift": 95
+    },
+    "owner": "producer"
+  }
+]
+```
+
+Check these values with the ones declared in the source code (in [`producer.cc`](producer.cc) and [`consumer.cc`](consumer.cc)).
+
+Defining our requirements
+-------------------------
+
+Now that we have all of the helpers that let us inspect the linked image, let's define a `valid` rule that defines the policy for our linked compartment.
+We'll start here by depending on the `valid` rule from the RTOS itself:
+
+```rego
+    data.rtos.valid
+```
+
+This performs some sanity checking on the RTOS core, such as ensuring that only the allocator can read hazard-pointer slots, that allocator capabilities are all valid, and so on.
+
+Next, we'll make sure that we have the right number of Caesar capabilities and that the number of *valid* Caesar capabilities is the same:
+
+```rego
+    # There are two things sealed with the Caesar capability type
+    count([c | c = input.compartments[owner].imports[_]; is_sealed_caesar_capability(c)]) = 2
+
+    # Both of them are valid Caesar capabilities
+    count(all_valid_caesar_capabilities) = 2
+```
+
+The first of these will find everything that is sealed as a Caesar capability, the second will find only ones that decode correctly.
+We know that only the producer and consumer compartments should hold these capabilities, so we check that the number found is two.
+
+Having done that, we extract the two capabilities that we expect to exist:
+
+```rego
+    some producerCapability, consumerCapability
+    producerCapability = [c | c = all_valid_caesar_capabilities[_]; c.owner = "producer"][0].capability
+    consumerCapability = [c | c = all_valid_caesar_capabilities[_]; c.owner = "consumer"][0].capability
+```
+
+Each of these starts with a comprehension that filters the set of all Caesar capabilities to find the ones with the named owner and then extracts the capability.
+Note that, in this case, we can assume that there is a single value here and so hard code array index 0.
+If that assumption is incorrect then either our previous assertion that there are two capabilities in total, or a later assertion where we inspect properties of the capabilities, will fail.
+
+Now that we have these, ensure that the producer is permitted to encrypt and the consumer to decrypt, but not vice versa.
+
+```rego
+    producerCapability.permitEncrypt = true
+    producerCapability.permitDecrypt = false
+    consumerCapability.permitEncrypt = false
+    consumerCapability.permitDecrypt = true
+```
+
+We expect the consumer to be able to decrypt things encrypted by the producer, so let's also make sure that their shift values are the same:
+
+```rego
+producerCapability.shift = consumerCapability.shift
+```
+
+Finally, for some defence in depth, let's make sure that the producer is the only caller of the encrypt function and the consumer the only caller of the decrypt function:
+
+```rego
+    data.compartment.compartment_call_allow_list("caesar", "caesar_encrypt.*", {"producer"})
+    data.compartment.compartment_call_allow_list("caesar", "caesar_decrypt.*", {"consumer"})
+```
+
+This is not necessary in theory, because these require an authorising capability and so another compartment calling them should fail.
+Another compartment trying to call them is definitely a bug though, so it's worth checking.
+Similarly, let's make sure that the producer and consumer are called only from the entry compartment
+
+```rego
+    data.compartment.compartment_call_allow_list("producer", "produce_message.*", {"entry"})
+    data.compartment.compartment_call_allow_list("consumer", "consume_message.*", {"entry"})
+```
+
+Putting this all together, we can now run a very simple query:
+
+```rego
+data.caesar.valid
+```
+
+If this all worked, the result should be simply `true`.
+
+A policy like this can be included in CI to make sure that everything that you commit meets the policy.
+It can drive key release for code signing, so that you never sign a firmware image that doesn't meet your policy.
+Along the way to writing the final policy, we built a set of tools for introspection on the firmware image, so you can query properties.
+
+Note on cryptography
+--------------------
+
+This example uses a Caesar Cypher.
+This was chosen because it is easy to implement, not because it is secure.
+Post-quantum encryption algorithms are designed to be robust against hypothetical quantum computers.
+Modern classical encryption algorithms are robust against attacks by large classical computers.
+The Caesar Cypher is not robust against a person with a pen and paper.
+
+Under no circumstances should you copy the encryption portion of this code into anything that needs to be robust in the presence of an adversary with ten minutes and a piece of paper.
+Breaking Caesar Cyphers is a fun thing for small children to do, not a challenge for a real cryptanalyst.
+
+All of that said, the same mechanisms used in this example *can* be used with more sensible encryption schemes to ensure key confidentiality.
diff --git a/examples/10.audit/caesar.rego b/examples/10.audit/caesar.rego
new file mode 100644
index 0000000..d310288
--- /dev/null
+++ b/examples/10.audit/caesar.rego
@@ -0,0 +1,77 @@
+package caesar
+
+# Check if an import is sealed with the Caesar capability type
+is_sealed_caesar_capability(capability) {
+	capability.kind = "SealedObject"
+	capability.sealing_type.compartment = "caesar"
+	capability.sealing_type.key = "CaesarCapabilityType"
+}
+
+# Helpers for converting C integers to booleans.
+# These fail if the input is not either 1 or 0
+value_as_boolean(value) = output {
+	value = 1
+	output = true
+}
+
+value_as_boolean(value) = output {
+	value = 0
+	output = false
+}
+
+decode_caesar_capability(capability) = decoded {
+	# Fail if this is not sealed with the Caesar capability type
+	is_sealed_caesar_capability(capability)
+	some permitEncrypt, permitDecrypt, shift
+
+	# Extract the values.  Each of these will fail if the value is not as expected.
+	# permitEncrypt is a (single-byte) boolean value at offset 0.
+	permitEncrypt = value_as_boolean(integer_from_hex_string(capability.contents, 0, 1))
+
+	# permitDecrypt is a (single-byte) boolean value at offset 1.
+	permitDecrypt = value_as_boolean(integer_from_hex_string(capability.contents, 1, 1))
+
+	# shift is a single-byte integer value at offset 2.
+	shift = integer_from_hex_string(capability.contents, 2, 1)
+	decoded = {
+		"permitEncrypt": permitEncrypt,
+		"permitDecrypt": permitDecrypt,
+		"shift": shift,
+	}
+}
+
+# Helper to extract all valid Caesar capabilities in the firmware image
+all_valid_caesar_capabilities = [{"owner": owner, "capability": decode_caesar_capability(c)} | c = input.compartments[owner].imports[_]; is_sealed_caesar_capability(c)]
+
+# Helper predicate to check that this is valid.
+valid {
+	# Make sure that the RTOS configuration is valid.
+	data.rtos.valid
+
+	# There are two things sealed with the Caesar capability type
+	count([c | c = input.compartments[owner].imports[_]; is_sealed_caesar_capability(c)]) = 2
+
+	# Both of them are valid Caesar capabilities
+	count(all_valid_caesar_capabilities) = 2
+
+	# Extract the producer and consumer's capabilities
+	some producerCapability, consumerCapability
+	producerCapability = [c | c = all_valid_caesar_capabilities[_]; c.owner = "producer"][0].capability
+	consumerCapability = [c | c = all_valid_caesar_capabilities[_]; c.owner = "consumer"][0].capability
+
+	# Check permissions
+	producerCapability.permitEncrypt = true
+	producerCapability.permitDecrypt = false
+	consumerCapability.permitEncrypt = false
+	consumerCapability.permitDecrypt = true
+
+	# Make sure that the shift (encryption keys) are the same.
+	producerCapability.shift = consumerCapability.shift
+
+	# Make sure that only the producer and consumer compartments call the encrypt and decrypt functions
+	data.compartment.compartment_call_allow_list("caesar", "caesar_encrypt.*", {"producer"})
+	data.compartment.compartment_call_allow_list("caesar", "caesar_decrypt.*", {"consumer"})
+	# Make sure that only the entry compartment calls the produce and consume functions
+	data.compartment.compartment_call_allow_list("producer", "produce_message.*", {"entry"})
+	data.compartment.compartment_call_allow_list("consumer", "consume_message.*", {"entry"})
+}
diff --git a/examples/10.audit/caesar_cypher.cc b/examples/10.audit/caesar_cypher.cc
new file mode 100644
index 0000000..fd87dac
--- /dev/null
+++ b/examples/10.audit/caesar_cypher.cc
@@ -0,0 +1,72 @@
+#include "caesar_cypher.h"
+#include <compartment-macros.h>
+#include <debug.hh>
+#include <errno.h>
+
+using Debug = ConditionalDebug<true, "Caesar">;
+
+int caesar_encrypt(SObj        capability,
+                   const char *input,
+                   char       *output,
+                   size_t      length)
+{
+	auto *cypherState = token_unseal<CaesarCapability>(
+	  STATIC_SEALING_TYPE(CaesarCapabilityType), capability);
+	if (cypherState == nullptr)
+	{
+		return -EINVAL;
+	}
+	if (!cypherState->permitEncrypt)
+	{
+		return -EPERM;
+	}
+	for (int i = 0; i < length; i++)
+	{
+		uint8_t c = input[i];
+		// Replace control characters with 'X'
+		if ((c < 32) || (c >= 127))
+		{
+			c = 'X';
+		}
+		// Reset the base to 0.
+		c -= 32;
+		// Perform the encryption
+		c += cypherState->shift;
+		if (c > 94)
+		{
+			c -= 94;
+		}
+		c += 32;
+		output[i] = c;
+	}
+	return 0;
+}
+
+int caesar_decrypt(SObj        capability,
+                   const char *input,
+                   char       *output,
+                   size_t      length)
+{
+	auto *cypherState = token_unseal<CaesarCapability>(
+	  STATIC_SEALING_TYPE(CaesarCapabilityType), capability);
+	if (cypherState == nullptr)
+	{
+		return -EINVAL;
+	}
+	if (!cypherState->permitDecrypt)
+	{
+		return -EPERM;
+	}
+	for (int i = 0; i < length; i++)
+	{
+		int c = input[i];
+		// Perform the encryption
+		c -= cypherState->shift;
+		if (c < 32)
+		{
+			c += 94;
+		}
+		output[i] = c;
+	}
+	return 0;
+}
diff --git a/examples/10.audit/caesar_cypher.h b/examples/10.audit/caesar_cypher.h
new file mode 100644
index 0000000..1692c73
--- /dev/null
+++ b/examples/10.audit/caesar_cypher.h
@@ -0,0 +1,29 @@
+#include <token.h>
+
+struct CaesarCapability
+{
+	bool    permitEncrypt;
+	bool    permitDecrypt;
+	uint8_t shift;
+};
+
+#define DECLARE_AND_DEFINE_CAESAR_CAPABILITY(                                  \
+  name, permitEncrypt, permitDecrypt, shift)                                   \
+	DECLARE_AND_DEFINE_STATIC_SEALED_VALUE(struct CaesarCapability,            \
+	                                       caesar,                             \
+	                                       CaesarCapabilityType,               \
+	                                       name,                               \
+	                                       permitEncrypt,                      \
+	                                       permitDecrypt,                      \
+	                                       shift)
+
+int __cheri_compartment("caesar")
+  caesar_encrypt(SObj capability, const char *input, char *output, size_t len);
+
+int __cheri_compartment("caesar")
+  caesar_decrypt(SObj capability, const char *input, char *output, size_t len);
+
+ssize_t __cheri_compartment("producer")
+  produce_message(char *buffer, size_t length);
+void __cheri_compartment("consumer")
+  consume_message(const char *buffer, size_t length);
diff --git a/examples/10.audit/consumer.cc b/examples/10.audit/consumer.cc
new file mode 100644
index 0000000..29f8bb8
--- /dev/null
+++ b/examples/10.audit/consumer.cc
@@ -0,0 +1,19 @@
+// Copyright Microsoft and CHERIoT Contributors.
+// SPDX-License-Identifier: MIT
+
+#include "caesar_cypher.h"
+#include <debug.hh>
+#include <errno.h>
+
+using Debug = ConditionalDebug<true, "Consumer">;
+
+DECLARE_AND_DEFINE_CAESAR_CAPABILITY(encrypt, false, true, 95);
+
+void consume_message(const char *buffer, size_t length)
+{
+	std::string decrypted;
+	decrypted.resize(length);
+	caesar_decrypt(
+	  STATIC_SEALED_VALUE(encrypt), buffer, decrypted.data(), length);
+	Debug::log("Decrypted message: '{}'", decrypted);
+}
diff --git a/examples/10.audit/entry.cc b/examples/10.audit/entry.cc
new file mode 100644
index 0000000..ae15563
--- /dev/null
+++ b/examples/10.audit/entry.cc
@@ -0,0 +1,24 @@
+// Copyright Microsoft and CHERIoT Contributors.
+// SPDX-License-Identifier: MIT
+
+#include "caesar_cypher.h"
+#include <debug.hh>
+
+using Debug = ConditionalDebug<true, "Entry compartment">;
+
+char buffer[1024];
+
+/// Thread entry point.
+void __cheri_compartment("entry") entry()
+{
+	ssize_t length = produce_message(buffer, sizeof(buffer));
+	if (length < 0)
+	{
+		Debug::log("Failed to get encrypted message");
+		return;
+	}
+	Debug::log("Received encrypted message: '{}' ({} bytes)",
+	           std::string_view{buffer, static_cast<size_t>(length)},
+	           length);
+	consume_message(buffer, length);
+}
diff --git a/examples/10.audit/producer.cc b/examples/10.audit/producer.cc
new file mode 100644
index 0000000..fbafc45
--- /dev/null
+++ b/examples/10.audit/producer.cc
@@ -0,0 +1,27 @@
+// Copyright Microsoft and CHERIoT Contributors.
+// SPDX-License-Identifier: MIT
+
+#include "caesar_cypher.h"
+#include <debug.hh>
+#include <errno.h>
+
+using Debug = ConditionalDebug<true, "Producer">;
+
+DECLARE_AND_DEFINE_CAESAR_CAPABILITY(encrypt, true, false, 95);
+
+ssize_t produce_message(char *buffer, size_t length)
+{
+	const std::string_view Plaintext = "Hello, World!";
+	if (length < Plaintext.size())
+	{
+		return -ENOMEM;
+	}
+	Debug::log("Encrypting message '{}'", Plaintext);
+	int ret = caesar_encrypt(
+	  STATIC_SEALED_VALUE(encrypt), Plaintext.data(), buffer, Plaintext.size());
+	if (ret < 0)
+	{
+		return ret;
+	}
+	return Plaintext.size();
+}
diff --git a/examples/10.audit/xmake.lua b/examples/10.audit/xmake.lua
new file mode 100644
index 0000000..71cb288
--- /dev/null
+++ b/examples/10.audit/xmake.lua
@@ -0,0 +1,42 @@
+-- Copyright Microsoft and CHERIoT Contributors.
+-- SPDX-License-Identifier: MIT
+
+set_project("CHERIoT Compartmentalised hello world (more secure)")
+sdkdir = "../../sdk"
+includes(sdkdir)
+set_toolchains("cheriot-clang")
+
+option("board")
+    set_default("sail")
+
+compartment("caesar")
+    -- This compartment uses C++ thread-safe static initialisation and so
+    -- depends on the C++ runtime.
+    add_files("caesar_cypher.cc")
+
+compartment("entry")
+    add_files("entry.cc")
+
+compartment("producer")
+    add_files("producer.cc")
+compartment("consumer")
+    add_files("consumer.cc")
+
+-- Firmware image for the example.
+firmware("caesar_example")
+    -- Both compartments require memcpy
+    add_deps("freestanding", "debug", "string")
+    add_deps("entry", "caesar")
+    add_deps("producer", "consumer")
+    on_load(function(target)
+        target:values_set("board", "$(board)")
+        target:values_set("threads", {
+            {
+                compartment = "entry",
+                priority = 1,
+                entry_point = "entry",
+                stack_size = 0x400,
+                trusted_stack_frames = 3
+            }
+        }, {expand = false})
+    end)
diff --git a/sdk/boards/sonata-0.2.json b/sdk/boards/sonata-0.2.json
index 6faa442..308904a 100644
--- a/sdk/boards/sonata-0.2.json
+++ b/sdk/boards/sonata-0.2.json
@@ -71,6 +71,7 @@
         "SUNBURST",
         "SUNBURST_SHADOW_BASE=0x30000000",
         "SUNBURST_SHADOW_SIZE=0x4000",
+        "DEFAULT_UART_BAUD_RATE=115200",
         "ipconfigDRIVER_INCLUDED_RX_IP_CHECKSUM=1",
         "ipconfigDRIVER_INCLUDED_TX_IP_CHECKSUM=1"
     ],
diff --git a/sdk/compartment.ldscript b/sdk/compartment.ldscript
index ba46ec4..9a602da 100644
--- a/sdk/compartment.ldscript
+++ b/sdk/compartment.ldscript
@@ -36,7 +36,7 @@
 		# If there is a compartment error handler, make sure that it is before
 		# anything that can have linker relaxations so that its displacement
 		# from __compartment_code_start is a constant.
-		*(.compartment_error_handler_stackless));
+		*(.compartment_error_handler_stackless);
 		*(.compartment_error_handler);
 		*(.text .text.*);
 	}
diff --git a/sdk/core/allocator/alloc.h b/sdk/core/allocator/alloc.h
index 1e890ea..8ab0c09 100644
--- a/sdk/core/allocator/alloc.h
+++ b/sdk/core/allocator/alloc.h
@@ -746,7 +746,9 @@
 	TChunk *leftmost_child()
 	{
 		if (child[0] != nullptr)
+		{
 			return child[0];
+		}
 		return child[1];
 	}
 
diff --git a/sdk/core/allocator/main.cc b/sdk/core/allocator/main.cc
index 8568af9..b5336ce 100644
--- a/sdk/core/allocator/main.cc
+++ b/sdk/core/allocator/main.cc
@@ -17,6 +17,17 @@
 #include <token.h>
 #include <utils.hh>
 
+/**
+ * The sealing key for dynamically allocated software-sealed objects.
+ */
+__attribute__((section(".sealing_key1"))) void *allocatorSealingKey;
+
+/**
+ * The root for the software sealing key.
+ */
+__attribute__((section(".sealing_key2"))) Capability<SKeyStruct>
+  softwareSealingKey;
+
 using namespace CHERI;
 
 Revocation::Revoker revoker;
@@ -1021,19 +1032,6 @@
 	uint32_t nextSealingType = std::numeric_limits<uint32_t>::max();
 
 	/**
-	 * Returns the root for the software sealing key.
-	 */
-	__always_inline Capability<SKeyStruct> software_sealing_key()
-	{
-		SKeyStruct *ret;
-		__asm("1:   "
-		      " auipcc      %0, %%cheriot_compartment_hi(__sealingkey2)\n"
-		      " clc         %0, %%cheriot_compartment_lo_i(1b)(%0)\n"
-		      : "=C"(ret));
-		return {ret};
-	}
-
-	/**
 	 * Helper that unseals `in` if it is a valid sealed capability sealed with
 	 * our hardware sealing key.  Returns the unsealed pointer, `nullptr` if it
 	 * cannot be helped.
@@ -1044,8 +1042,7 @@
 		// FIXME: At the moment the ISA is still shuffling types around, but
 		// eventually we want to know the type statically and don't need dynamic
 		// instructions.
-		Capability key{SEALING_CAP()};
-		in.unseal(key);
+		in.unseal(allocatorSealingKey);
 		return in.is_valid() ? in : SealedAllocation{nullptr};
 	}
 
@@ -1126,7 +1123,7 @@
 
 		obj->type   = key.address();
 		auto sealed = obj;
-		sealed.seal(SEALING_CAP());
+		sealed.seal(allocatorSealingKey);
 		obj.address() += ObjHdrSize; // Exclude the header.
 		obj.bounds() = obj.top() - obj.address();
 		Debug::Assert(
@@ -1141,7 +1138,7 @@
 	// structures and so can have its own lock.
 	static FlagLock tokenLock;
 	LockGuard       g{tokenLock};
-	auto            keyRoot = software_sealing_key();
+	auto            keyRoot = softwareSealingKey;
 	// For now, strip the user permissions.  We might want to use them for
 	// permit-allocate and permit-free.
 	keyRoot.permissions() &=
diff --git a/sdk/core/allocator/token.h b/sdk/core/allocator/token.h
index e9ce8f7..42c7343 100644
--- a/sdk/core/allocator/token.h
+++ b/sdk/core/allocator/token.h
@@ -92,3 +92,5 @@
 
 EXPORT_ASSEMBLY_OFFSET(TokenSObj, type, 0);
 EXPORT_ASSEMBLY_OFFSET(TokenSObj, data, 8);
+EXPORT_ASSEMBLY_NAME(CheriSealTypeAllocator, 11);
+EXPORT_ASSEMBLY_NAME(CheriSealTypeStaticToken, 12);
diff --git a/sdk/core/loader/boot.cc b/sdk/core/loader/boot.cc
index a72541f..e41038a 100644
--- a/sdk/core/loader/boot.cc
+++ b/sdk/core/loader/boot.cc
@@ -16,6 +16,7 @@
 #include "defines.h"
 #include "types.h"
 #include <cheri.hh>
+#include <compartment.h>
 #include <platform-uart.hh>
 #include <priv/riscv.h>
 #include <riscvreg.h>
@@ -49,6 +50,11 @@
 	// It must also be aligned sufficiently for trusted stacks, so ensure that
 	// we've captured that requirement above.
 	static_assert(alignof(TrustedStack) <= 16);
+
+	static_assert(sizeof(ErrorState) == offsetof(TrustedStack, hazardPointers));
+	static_assert(offsetof(ErrorState, pcc) == offsetof(TrustedStack, mepcc));
+	static_assert(offsetof(ErrorState, registers) ==
+	              offsetof(TrustedStack, cra));
 	__END_DECLS
 
 	static_assert(
@@ -62,12 +68,12 @@
 		/**
 		 * 0 represents unsealed.
 		 */
-		Unsealed = 0,
+		Unsealed = CheriSealTypeUnsealed,
 
 		/**
 		 * Sentry that inherits interrupt status.
 		 */
-		SentryInheriting,
+		SentryInheriting = CheriSealTypeSentryInheriting,
 
 		/// Alternative name: the default sentry type.
 		Sentry = SentryInheriting,
@@ -75,22 +81,32 @@
 		/**
 		 * Sentry that disables interrupts on calls.
 		 */
-		SentryDisabling,
+		SentryDisabling = CheriSealTypeSentryDisabling,
 
 		/**
 		 * Sentry that enables interrupts on calls.
 		 */
-		SentryEnabling,
+		SentryEnabling = CheriSealTypeSentryEnabling,
+
+		/**
+		 * Return sentry that disables interrupts on return
+		 */
+		ReturnSentryDisabling = CheriSealTypeReturnSentryDisabling,
+
+		/**
+		 * Return sentry that enables interrupts on return
+		 */
+		ReturnSentryEnabling = CheriSealTypeReturnSentryEnabling,
 
 		/**
 		 * Marker for the first sealing type that's valid for data capabilities.
 		 */
-		FirstDataSealingType = 9,
+		FirstDataSealingType = CheriSealTypeFirstDataSealingType,
 
 		/**
 		 * The sealing type used for sealed export table entries.
 		 */
-		SealedImportTableEntries = FirstDataSealingType,
+		SealedImportTableEntries = CheriSealTypeSealedImportTableEntries,
 
 		/**
 		 * The compartment switcher has a sealing type for the trusted stack.
@@ -98,38 +114,74 @@
 		 * This must be the second data sealing type so that we can also permit
 		 * the switcher to unseal sentries and export table entries.
 		 */
-		SealedTrustedStacks,
+		SealedTrustedStacks = CheriSealTypeSealedTrustedStacks,
 
 		/**
-		 * The scheduler has a sealing type for waitable objects.
+		 * The allocator has a sealing type for the software sealing mechanism
+		 * with dynamically allocated objects.
 		 */
-		Scheduler,
+		Allocator = CheriSealTypeAllocator,
 
 		/**
-		 * The allocator has a sealing type for the software sealing mechanism.
+		 * The loader reserves a sealing type for the software sealing
+		 * mechanism.  The permit-unseal capability for this is destroyed after
+		 * the loader has run, which guarantees that anything sealed with this
+		 * type was present in the original firmware image.  The token library
+		 * has the only permit-unseal capability for this type.
 		 */
-		Allocator,
+		StaticToken = CheriSealTypeStaticToken,
 
 		/**
 		 * The first sealing key that is reserved for use by the allocator's
 		 * software sealing mechanism and used for static sealing types,
 		 */
-		FirstStaticSoftware = 16,
+		FirstStaticSoftware = CheriSealTypeFirstStaticSoftware,
 
 		/**
 		 * The first sealing key in the space that the allocator will
 		 * dynamically allocate for sealing types.
 		 */
-		FirstDynamicSoftware = 0x1000000,
+		FirstDynamicSoftware = CheriSealTypeFirstDynamicSoftware,
 	};
 
+	// 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,
+	              "If this fails, update switcher/entry.S to the new value");
+	static_assert(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),
+	              "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,
+	              "If this fails, update token_unseal.S to the new value");
+
 	// We currently have a 3-bit hardware otype, with different sealing spaces
 	// for code and data capabilities, giving the range 0-0xf reserved for
 	// hardware use. Assert that we're not using more than we need (two in the
 	// enum are outside of the hardware space).
-	static_assert(magic_enum::enum_count<SealingType>() <= 10,
+	static_assert(magic_enum::enum_count<SealingType>() <= 12,
 	              "Too many sealing types reserved for a 3-bit otype field");
 
+} // namespace
+
+/*
+ * Unusually late, include this where we have access to the above enum
+ * SealingType, but early enough that the constants defined herein are available
+ * to the rest of the code.
+ */
+#include "../switcher/misc-assembly.h"
+
+namespace
+{
+
 	constexpr auto StoreLPerm = Root::Permissions<Root::Type::RWStoreL>;
 	/// PCC permissions for the switcher.
 	constexpr auto SwitcherPccPermissions =
@@ -306,6 +358,23 @@
 		return ptr.seal(key);
 	}
 
+	template<InterruptStatus Status, typename T>
+	T *seal_return(Capability<T> ptr)
+	{
+		static_assert((Status == InterruptStatus::Enabled) ||
+		              (Status == InterruptStatus::Disabled));
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wc99-designator"
+		constexpr SealingType Sentries[] = {
+		  [int(InterruptStatus::Enabled)]  = ReturnSentryEnabling,
+		  [int(InterruptStatus::Disabled)] = ReturnSentryDisabling};
+#pragma clang diagnostic pop
+		size_t otype = size_t{Sentries[int(Status)]};
+		void  *key   = build<void, Root::Type::Seal>(otype, 1);
+		return ptr.seal(key);
+	}
+
 	/**
 	 * Helper to determine whether an object, given by a start address and size,
 	 * is completely contained within a specified range.
@@ -582,12 +651,6 @@
 				        Root::Type::RWGlobal,
 				        PermissionSet{Permission::Load, Permission::Store}>(
 				    entry.address);
-				// Is the software sealing type owned by the scheduler?  If so,
-				// we're going to seal the object with the scheduler's sealing
-				// type, not the allocator's.  This lets the scheduler export
-				// software-defined capabilities without adding the allocator
-				// to the TCB for availability.
-				bool isSchedulerObject = false;
 				// TODO: This currently places a restriction that data memory
 				// can't be in the low 64 KiB of the address space.  That may be
 				// too restrictive. If we haven't visited this sealed object
@@ -613,11 +676,7 @@
                         return false;
 					};
 					bool found = findExport(image.allocator());
-					if (!found && findExport(image.scheduler()))
-					{
-						found             = true;
-						isSchedulerObject = true;
-					}
+					found |= findExport(image.scheduler());
 					for (auto &compartment : image.compartments())
 					{
 						if (found)
@@ -632,8 +691,8 @@
 				}
 				Capability sealedObject = build(entry.address, entry.size());
 				// Seal with the allocator's sealing key
-				sealedObject.seal(build<void, Root::Type::Seal>(
-				  isSchedulerObject ? Scheduler : Allocator, 1));
+				sealedObject.seal(
+				  build<void, Root::Type::Seal>(StaticToken, 1));
 				Debug::log("Static sealed object: {}", sealedObject);
 				return sealedObject;
 			}
@@ -784,6 +843,21 @@
 		// Space per thread for hazard pointers.
 		static constexpr size_t HazardPointerSpace =
 		  HazardPointersPerThread * sizeof(void *);
+
+		/*
+		 * Construct a return sentry with which to populate initial thread
+		 * register files, as if they had been entered by the switcher rather
+		 * than by fiat of initial construction.  The switcher will detect the
+		 * trusted stack underflow and will signal the scheduler that the thread
+		 * has exited and should not be brought back on core.
+		 */
+		auto threadInitialReturn =
+		  build<void, Root::Type::Execute, SwitcherPccPermissions>(
+		    image.switcher.code);
+		threadInitialReturn.address() += image.switcher.crossCallReturnEntry;
+		threadInitialReturn =
+		  seal_return<InterruptStatus::Disabled>(threadInitialReturn);
+
 		for (size_t i = 0; const auto &config : image.threads())
 		{
 			Debug::log("Creating thread {}", i);
@@ -819,6 +893,7 @@
 			        false>(config.trustedStack);
 			threadTStack->mepcc = pcc;
 			threadTStack->cgp   = cgp;
+			threadTStack->cra   = threadInitialReturn;
 			// Stacks have store-local but not global permission.
 			auto stack =
 			  build<void,
@@ -1184,11 +1259,10 @@
 	trustedStackKey.bounds()  = 1;
 	switcherKey.address()     = SealedImportTableEntries;
 	switcherKey.bounds()      = 1;
-	setSealingKey(imgHdr.scheduler(), Scheduler);
 	setSealingKey(imgHdr.allocator(), Allocator);
 	setSealingKey(imgHdr.token_library(),
 	              Allocator,
-	              1,
+	              2, // Allocator and StaticToken
 	              0,
 	              PermissionSet{Permission::Global, Permission::Unseal});
 	constexpr size_t DynamicSealingLength =
diff --git a/sdk/core/loader/types.h b/sdk/core/loader/types.h
index ec488c9..7a29847 100644
--- a/sdk/core/loader/types.h
+++ b/sdk/core/loader/types.h
@@ -363,6 +363,12 @@
 			AddressRange code;
 
 			/**
+			 * The PCC-relative location of the cross-compartment call return
+			 * path, used to build the initial return addresses for threads.
+			 */
+			uint16_t crossCallReturnEntry;
+
+			/**
 			 * The PCC-relative location of the sealing key, which the
 			 * compartment switcher will use to unseal import table entries.
 			 */
@@ -447,7 +453,7 @@
 			 * The distance from the start of the code region to the end of the
 			 * import table.
 			 */
-			uint16_t importTableSize;
+			AddressRange importTable;
 
 			/**
 			 * The export table for the scheduler.
@@ -465,11 +471,7 @@
 			 */
 			[[nodiscard]] AddressRange import_table() const
 			{
-				// Skip the sealing keys
-				const size_t SealingKeysSize = 2 * sizeof(void *);
-				return {
-				  code.start() + SealingKeysSize,
-				  static_cast<uint16_t>(importTableSize - SealingKeysSize)};
+				return importTable;
 			}
 
 			/**
@@ -612,7 +614,7 @@
 			// This is a random 32-bit number and should be changed whenever
 			// the compartment header layout changes to provide some sanity
 			// checking.
-			return magic == 0x6cef3879;
+			return magic == 0xca2b63de;
 		}
 
 		/**
diff --git a/sdk/core/scheduler/common.h b/sdk/core/scheduler/common.h
index 565152d..ea9ccfd 100644
--- a/sdk/core/scheduler/common.h
+++ b/sdk/core/scheduler/common.h
@@ -8,6 +8,7 @@
 #include <cheri.hh>
 #include <debug.hh>
 #include <stdlib.h>
+#include <token.h>
 #include <type_traits>
 
 namespace
@@ -45,57 +46,14 @@
 	StackUsageCheck<StackMode, expected, __PRETTY_FUNCTION__> stackCheck
 
 	/**
-	 * Base class for types that are exported from the scheduler with a common
-	 * sealing type.  Includes an inline type marker.
+	 * Base class for sealed objects that are exported from the scheduler.
+	 *
+	 * Subclasses must implement a static `sealing_type` method that returns
+	 * the sealing key.
 	 */
+	template<bool IsDynamic>
 	struct Handle
 	{
-		protected:
-		/**
-		 * The real type of this subclass.
-		 *
-		 * This must be 32 bits and must be the first word of the class, so that
-		 * we are layout-compatible with the static sealing capabilities.  We
-		 * use low-value numbers for things that we dynamically allocate to
-		 * check types.  Anything that is statically allocated will have this
-		 * field initialised by the loader.
-		 *
-		 * Note that allocator-allocated and static types have a word of
-		 * padding here.  This is necessary to ensure that the base of the sub
-		 * object is capability-aligned.  In cases created with subclassing,
-		 * this is not required - the compiler will insert padding if it needs
-		 * to, but we can also put small fields in this space.  For anything
-		 * statically allocated, we will need to handle this separately.
-		 */
-		enum class Type : uint32_t
-		{
-			Invalid = 0,
-
-			/**
-			 * Multiwaiter type.
-			 */
-			MultiWaiter,
-
-			/**
-			 * Used only as the type marker, not for comparisons.  This
-			 * indicates that the type uses a dynamically allocated type value
-			 * that is provided in the `dynamic_type_marker()` static function.
-			 */
-			Dynamic,
-		} type;
-
-		/**
-		 * Constructor, takes the type of the subclass.
-		 */
-		Handle(Type type) : type(type) {}
-		~Handle()
-		{
-			__clang_ignored_warning_push("-Watomic-alignment") __atomic_store_n(
-			  reinterpret_cast<uint8_t *>(&type), 0, __ATOMIC_RELAXED);
-			__clang_ignored_warning_pop()
-		}
-
-		public:
 		/**
 		 * Unseal `unsafePointer` as a pointer to an object of the specified
 		 * type.  Returns nullptr if `unsafePointer` is not a valid sealed
@@ -106,6 +64,7 @@
 		{
 			return static_cast<Handle *>(unsafePointer)->unseal_as<T>();
 		}
+
 		/**
 		 * Unseal this object as the specified type.  Returns nullptr if this
 		 * is not a valid sealed object of the correct type.
@@ -116,25 +75,18 @@
 			static_assert(std::is_base_of_v<Handle, T>,
 			              "Cannot down-cast something that is not a subclass "
 			              "of Handle");
-			static_assert(offsetof(T, type) == 0,
-			              "Type field must be at the start of the object");
-			auto unsealed = compart_unseal(this);
-			if constexpr (T::TypeMarker == Type::Dynamic)
+			void *result;
+			if constexpr (IsDynamic)
 			{
-				if (unsealed.is_valid() && uint32_t(unsealed->type) ==
-				                             T::dynamic_type_marker().address())
-				{
-					return unsealed.cast<T>();
-				}
+				result = token_obj_unseal_dynamic(T::sealing_type(),
+				                                  reinterpret_cast<SObj>(this));
 			}
 			else
 			{
-				if (unsealed.is_valid() && unsealed->type == T::TypeMarker)
-				{
-					return unsealed.cast<T>();
-				}
+				result = token_obj_unseal_static(T::sealing_type(),
+				                                 reinterpret_cast<SObj>(this));
 			}
-			return nullptr;
+			return static_cast<T *>(result);
 		}
 	};
 
@@ -191,151 +143,4 @@
 	void exception_entry_asm(void);
 	__END_DECLS
 
-	/**
-	 * Wrapper around `std::unique_ptr` for objects allocated with a specific
-	 * heap capability.  The scheduler is not authorised to allocate memory
-	 * except on behalf of callers.
-	 */
-	template<typename T>
-	class HeapObject
-	{
-		/**
-		 * Deleter for use with `std::unique_ptr`, for memory allocated with an
-		 * explicit capability.
-		 */
-		class Deleter
-		{
-			/**
-			 * The capability that should authorise access to the heap.
-			 */
-			struct SObjStruct *heapCapability;
-
-			public:
-			/**
-			 * Constructor, captures the capability to use for deallocation.
-			 */
-			__always_inline Deleter(struct SObjStruct *heapCapability)
-			  : heapCapability(heapCapability)
-			{
-			}
-
-			/**
-			 * Apply function, calls the destructor and cleans up the underlying
-			 * memory.
-			 */
-			__always_inline void operator()(T *object)
-			{
-				object->~T();
-				heap_free(heapCapability, object);
-			}
-		};
-
-		protected:
-		/**
-		 * The underlying unique pointer.
-		 */
-		std::unique_ptr<T, Deleter> pointer = {nullptr, nullptr};
-
-		public:
-		/// Default constructor, creates a null object.
-		HeapObject() = default;
-		/**
-		 * Constructor for use with externally heap-allocated objects and
-		 * placement new.  Takes ownership of `allocatedObject`. The heap
-		 * capability will be used to free the object on destruction.
-		 */
-		HeapObject(struct SObjStruct *heapCapability, T *allocatedObject)
-		  : pointer(allocatedObject, heapCapability)
-		{
-			if (!__builtin_cheri_tag_get(allocatedObject))
-			{
-				pointer = nullptr;
-			}
-		}
-
-		/**
-		 * Attempt to allocate memory for, and construct, an instance of `T`
-		 * with the specified arguments.  If memory allocation fails, this will
-		 * construct an object wrapping a null pointer and not call the
-		 * constructor.  The bool-conversion operator can be used to check for
-		 * success.
-		 */
-		template<typename... Args>
-		HeapObject(Timeout           *timeout,
-		           struct SObjStruct *heapCapability,
-		           Args... args)
-		  : pointer(static_cast<T *>(
-		              heap_allocate(timeout, heapCapability, sizeof(T))),
-		            {heapCapability})
-		{
-			if (__builtin_cheri_tag_get(pointer.raw()))
-			{
-				new (pointer.get()) T(std::forward<Args>(args)...);
-			}
-			else
-			{
-				pointer = nullptr;
-			}
-		}
-
-		/**
-		 * Convert to bool.  Returns true if the unique pointer owns a non-null
-		 * pointer.
-		 */
-		operator bool()
-		{
-			return !!pointer;
-		}
-
-		/**
-		 * Returns the wrapped pointer without transferring ownership.
-		 */
-		T *get()
-		{
-			return pointer.get();
-		}
-
-		/**
-		 * Returns the wrapped pointer, transferring ownership to the caller.
-		 */
-		T *release()
-		{
-			return pointer.release();
-		}
-	};
-
-	/**
-	 * Wrapper for a buffer of a dynamic length, allocated on the heap owned by
-	 * another compartment.
-	 */
-	struct HeapBuffer : public HeapObject<char>
-	{
-		HeapBuffer() = default;
-
-		/**
-		 * Construct an array of `count` elemets, each of which is `size` bytes
-		 * long, using `heapCapability` to authorise allocation.
-		 */
-		HeapBuffer(Timeout           *timeout,
-		           struct SObjStruct *heapCapability,
-		           size_t             size,
-		           size_t             count)
-		  : HeapObject(
-		      heapCapability,
-		      static_cast<char *>(
-		        heap_allocate_array(timeout, heapCapability, size, count)))
-		{
-		}
-
-		/**
-		 * Update the address of the wrapped pointer.
-		 */
-		void set_address(ptraddr_t address)
-		{
-			CHERI::Capability old{pointer.release()};
-			old.address() = address;
-			pointer.reset(old);
-		}
-	};
-
 } // namespace
diff --git a/sdk/core/scheduler/main.cc b/sdk/core/scheduler/main.cc
index 18c0d7b..678c784 100644
--- a/sdk/core/scheduler/main.cc
+++ b/sdk/core/scheduler/main.cc
@@ -247,7 +247,9 @@
 		simulation_exit(1);
 
 		for (;;)
+		{
 			wfi();
+		}
 	}
 
 	[[cheri::interrupt_state(disabled)]] TrustedStack *
@@ -256,6 +258,15 @@
 	                                               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)
@@ -348,7 +359,7 @@
 	template<typename T>
 	int typed_op(void *sealed, auto &&fn)
 	{
-		auto *unsealed = Handle::unseal<T>(sealed);
+		auto *unsealed = T::template unseal<T>(sealed);
 		// If we can't unseal the sealed capability and have it be of the
 		// correct type then return an error.
 		if (!unsealed)
@@ -396,29 +407,6 @@
 		});
 	}
 
-	/**
-	 * Return path from `*_create` functions.  Performs a return check with
-	 * interrupts disabled and stores the object managed by `object` via `ret`
-	 * (consuming ownership) if `ret` has valid permissions.  Returns the value
-	 * that the caller should return to the public API.
-	 *
-	 * This function runs with interrupts disabled so that the majority of
-	 * `*_create` can have them enabled.
-	 */
-	template<typename T>
-	[[cheri::interrupt_state(disabled)]] int write_result(void         **ret,
-	                                                      HeapObject<T> &object)
-	{
-		if (!check_pointer<PermissionSet{Permission::Store,
-		                                 Permission::LoadStoreCapability}>(ret))
-		{
-			return -EINVAL;
-		}
-
-		*ret = compart_seal(object.release());
-		return 0;
-	}
-
 } // namespace sched
 
 using namespace sched;
@@ -594,7 +582,12 @@
 		return error;
 	}
 
-	return write_result(reinterpret_cast<void **>(ret), mw);
+	// This can trap, but only if the caller has provided a bad pointer.
+	// In this case, the caller can leak memory, but only memory allocated
+	// against its own quota.
+	*reinterpret_cast<void **>(ret) = mw;
+
+	return 0;
 }
 
 __cheriot_minimum_stack(0x70) int multiwaiter_delete(
@@ -668,30 +661,19 @@
 namespace
 {
 	/**
-	 *
+	 * An interrupt capability.
 	 */
-	struct InterruptCapability : Handle
+	struct InterruptCapability : Handle</*IsDynamic=*/false>
 	{
 		/**
-		 * Type marker used by `Handle`, tells it to use the dynamic path.
+		 * Sealing type used by `Handle`.
 		 */
-		static constexpr auto TypeMarker = Handle::Type::Dynamic;
-
-		/**
-		 * Dynamic type marker used by `Handle`.
-		 */
-		static Capability<void> dynamic_type_marker()
+		static SKey sealing_type()
 		{
 			return STATIC_SEALING_TYPE(InterruptKey);
 		}
 
 		/**
-		 * Padding for compatibility with the token layout.  This can go away
-		 * at some point.
-		 */
-		uint32_t padding;
-
-		/**
 		 * The public structure state.
 		 */
 		InterruptCapabilityState state;
@@ -702,8 +684,9 @@
   0x30) const uint32_t *interrupt_futex_get(struct SObjStruct *sealed)
 {
 	STACK_CHECK(0x30);
-	auto     *interruptCapability = Handle::unseal<InterruptCapability>(sealed);
-	uint32_t *result              = nullptr;
+	auto *interruptCapability =
+	  InterruptCapability::unseal<InterruptCapability>(sealed);
+	uint32_t *result = nullptr;
 	if (interruptCapability && interruptCapability->state.mayWait)
 	{
 		InterruptController::master()
@@ -722,7 +705,8 @@
   0x20) int interrupt_complete(struct SObjStruct *sealed)
 {
 	STACK_CHECK(0x20);
-	auto *interruptCapability = Handle::unseal<InterruptCapability>(sealed);
+	auto *interruptCapability =
+	  InterruptCapability::unseal<InterruptCapability>(sealed);
 	if (interruptCapability && interruptCapability->state.mayComplete)
 	{
 		InterruptController::master().interrupt_complete(
diff --git a/sdk/core/scheduler/multiwait.h b/sdk/core/scheduler/multiwait.h
index 23710dc..e1fdb5f 100644
--- a/sdk/core/scheduler/multiwait.h
+++ b/sdk/core/scheduler/multiwait.h
@@ -101,13 +101,16 @@
 	/**
 	 * Multiwaiter object.  This contains space for all of the triggers.
 	 */
-	class MultiWaiterInternal : public Handle
+	class MultiWaiterInternal : public Handle</*IsDynamic*/ true>
 	{
 		public:
 		/**
-		 * Type marker used for `Handle::unseal_as`.
+		 * Sealing type used by `Handle`.
 		 */
-		static constexpr auto TypeMarker = Handle::Type::MultiWaiter;
+		static SKey sealing_type()
+		{
+			return STATIC_SEALING_TYPE(MultiWaiterKey);
+		}
 
 		private:
 		/**
@@ -176,12 +179,13 @@
 		 * Factory method.  Creates a multiwaiter of the specified size.  On
 		 * failure, sets `error` to the errno constant corresponding to the
 		 * failure reason and return `nullptr`.
+		 *
+		 * The result is a *sealed* multiwaiter handle.
 		 */
-		static HeapObject<MultiWaiterInternal>
-		create(Timeout           *timeout,
-		       struct SObjStruct *heapCapability,
-		       size_t             length,
-		       int               &error)
+		static SObj create(Timeout           *timeout,
+		                   struct SObjStruct *heapCapability,
+		                   size_t             length,
+		                   int               &error)
 		{
 			static_assert(sizeof(MultiWaiterInternal) <= 2 * sizeof(void *),
 			              "Header for event queue is too large");
@@ -190,17 +194,21 @@
 				error = -EINVAL;
 				return {};
 			}
-			void *q = heap_allocate(timeout,
-			                        heapCapability,
-			                        sizeof(MultiWaiterInternal) +
-			                          (length * sizeof(EventWaiter)));
-			if (!__builtin_cheri_tag_get(q))
+			void *memory = nullptr;
+			SObj  sealed = token_sealed_unsealed_alloc(
+			   timeout,
+			   heapCapability,
+			   sealing_type(),
+			   sizeof(MultiWaiterInternal) + (length * sizeof(EventWaiter)),
+			   &memory);
+			if (!memory)
 			{
 				error = -ENOMEM;
-				return {};
+				return nullptr;
 			}
+			new (memory) MultiWaiterInternal(length);
 			error = 0;
-			return {heapCapability, new (q) MultiWaiterInternal(length)};
+			return sealed;
 		}
 
 		/**
@@ -369,9 +377,7 @@
 		/**
 		 * Private constructor, called only from the factory method (`create`).
 		 */
-		MultiWaiterInternal(size_t length) : Handle(TypeMarker), Length(length)
-		{
-		}
+		MultiWaiterInternal(size_t length) : Length(length) {}
 
 		/**
 		 * Priority-sorted wait queue for threads that are blocked on a
diff --git a/sdk/core/switcher/entry.S b/sdk/core/switcher/entry.S
index 8ef9cca..387a2d1 100644
--- a/sdk/core/switcher/entry.S
+++ b/sdk/core/switcher/entry.S
@@ -3,6 +3,7 @@
 
 #include "export-table-assembly.h"
 #include "trusted-stack-assembly.h"
+#include "misc-assembly.h"
 #include <errno.h>
 
 .include "assembly-helpers.s"
@@ -27,6 +28,25 @@
 #define SPILL_SLOT_pcc 24
 #define SPILL_SLOT_SIZE 32
 
+/*
+ * The switcher uniformly speaks of registers using their RISC-V ELF psABI names
+ * and not their raw index, as, broadly speaking, we use registers in a similar
+ * way to C functions.  However, it's probably convenient to have a mapping
+ * readily accessible, so here 'tis:
+ *
+ *  #     x0   x1 x2 x3 x4 x5 x6 x7 x8 x9 x10 x11 x12 x13 x14 x15
+ *  psABI zero ra sp gp tp t0 t1 t2 s0 s1 a0  a1  a2  a3  a4  a5
+ *
+ * When we use the psABI name without a 'c' prefix, we are sometimes meaning to
+ * refer to the address component of the capability.
+ *
+ * Despite the use of psABI names and conformance at the interface (argument
+ * registers used for arguments, return address register used for its canonical
+ * purpose, &c), one should not read too much of the psABI calling convention
+ * into the code here.  Within the switcher, the machine is a raw register
+ * machine and C is a distant, high-level language.
+ */
+
 switcher_code_start:
 
 # Global for the sealing key.  Stored in the switcher's code section.
@@ -164,11 +184,19 @@
 	.p2align 2
 	.type __Z26compartment_switcher_entryz,@function
 __Z26compartment_switcher_entryz:
-	cincoffset        csp, csp, -SPILL_SLOT_SIZE
-	csc               cs0, SPILL_SLOT_cs0(csp)
-	csc               cs1, SPILL_SLOT_cs1(csp)
-	csc               cgp, SPILL_SLOT_cgp(csp)
-	csc               cra, SPILL_SLOT_pcc(csp)
+	/*
+	 * Spill caller-save registers carefully.  If we find ourselves unable to do
+	 * so, we'll return an error to the caller (via the exception path; see
+	 * .Lhandle_error_in_switcher).  The error handling path assumes that the
+	 * first spill is to the lowest address and guaranteed to trap if any would.
+	 */
+	cincoffset        ct2, csp, -SPILL_SLOT_SIZE
+.Lswitcher_entry_first_spill:
+	csc               cs0, SPILL_SLOT_cs0(ct2)
+	csc               cs1, SPILL_SLOT_cs1(ct2)
+	csc               cgp, SPILL_SLOT_cgp(ct2)
+	csc               cra, SPILL_SLOT_pcc(ct2)
+	cmove             csp, ct2
 	// before we access any privileged state, we can verify the
 	// compartment's csp is valid. If not, force unwind.
 	// Note that this check is purely to protect the callee, not the switcher
@@ -207,7 +235,6 @@
 	// table entry on the trusted stack, a fault here will cause a forced
 	// unwind until we set the correct one.
 	csh                s1, TrustedStack_offset_frameoffset(ct2)
-#ifndef CONFIG_NO_SWITCHER_SAFETY
 	// Chop off the stack.
 	cgetaddr           s0, csp
 	cgetbase           s1, csp
@@ -219,25 +246,18 @@
 	// Read the stack high water mark (which is 16-byte aligned)
 	csrr               gp, CSR_MSHWM
 	// Skip zeroing if high water mark >= stack pointer
-	bge                t2, sp, after_zero
+	bge                gp, sp, .Lafter_zero
 	// Use stack high water mark as base address for zeroing.  If this faults
 	// then it will trigger a force unwind.  This can happen only if the caller
 	// is doing something bad.
 	csetaddr           ct2, csp, gp
 #endif
 	zero_stack         t2, s0, gp
-after_zero:
-	// Reserve space for unwind state and so on.
-	cincoffset 	       csp, csp, -STACK_ENTRY_RESERVED_SPACE
-#ifdef CONFIG_MSHWM
-	// store new stack top as stack high water mark
-	csrw               CSR_MSHWM, sp
-#endif
-#endif // CONFIG_NO_SWITCHER_SAFETY
-.Lout:
+.Lafter_zero:
+
 	// Fetch the sealing key
 	LoadCapPCC         cs0, compartment_switcher_sealing_key
-	li                 gp, 9
+	li                 gp, SEAL_TYPE_SealedImportTableEntries
 	csetaddr           cs0, cs0, gp
 	// The target capability is in ct1. Unseal, check tag and load the entry point offset.
 	cunseal            ct1, ct1, cs0
@@ -258,10 +278,25 @@
 	// At this point, we have already truncated the stack and so the length of
 	// the stack is the length that the callee can use.
 	cgetlen            t2, csp
-	// Include the space we reserved for the unwind state.
-	addi               t2, t2, -STACK_ENTRY_RESERVED_SPACE
+	/*
+	 * Include the space we reserved for the unwind state.
+	 *
+	 * tp holds the number of required stack bytes, a value between 0 and 0x7F8
+	 * (the result of an unsigned byte load left shifted by 3).  Given this
+	 * extremely limited range, adding STACK_ENTRY_RESERVED_SPACE will not cause
+	 * overflow (while instead subtracting it from the available length, in t2,
+	 * might underflow).
+	 */
+	addi               tp, tp, STACK_ENTRY_RESERVED_SPACE
 	bgtu               tp, t2, .Lstack_too_small
 
+	// Reserve space for unwind state and so on.
+	cincoffset 	       csp, csp, -STACK_ENTRY_RESERVED_SPACE
+#ifdef CONFIG_MSHWM
+	// store new stack top as stack high water mark
+	csrw               CSR_MSHWM, sp
+#endif
+
 	// Get the flags field into tp
 	clbu               tp, ExportEntry_offset_flags(ct1)
 	cgetbase           s1, ct1
@@ -298,19 +333,75 @@
 	csrsi              mstatus, 0x8
 .Lskip_interrupt_disable:
 	// Registers passed to the callee are:
-	// c1 (ra), c2 (csp), and c3 (cgp) are passed unconditionally.
+	// cra (c1), csp (c2), and cgp (c3) are passed unconditionally.
 	// ca0-ca5 (c10-c15) and ct0 (c5) are either passed as arguments or cleared
 	// above.  This should add up to 10 registers, with the remaining 5 being
 	// cleared now:
 	zeroRegisters      tp, t1, t2, s0, s1
 	cjalr              cra
 
-.Lskip_compartment_call:
+	.globl switcher_skip_compartment_call
+switcher_skip_compartment_call:
 	// If we are doing a forced unwind of the trusted stack then we do almost
 	// exactly the same as a normal unwind.  We will jump here from the
-	// exception path.
-	cjal               .Lpop_trusted_stack_frame
-	cmove              cra, ca2
+	// exception path (.Lforce_unwind)
+
+	/*
+	 * Pop a frame from the trusted stack, leaving all registers in the state
+	 * expected by the caller of a cross-compartment call.  The callee is
+	 * responsible for zeroing argument and temporary registers.
+	 *
+	 * The below should not fault before returning back to the caller. If a
+	 * fault occurs there must be a serious bug elsewhere.
+	 */
+
+	cspecialr          ctp, mtdc
+	clear_hazard_slots ctp, ct2
+	// make sure there is a frame left in the trusted stack
+	clhu               t2, TrustedStack_offset_frameoffset(ctp)
+	li                 tp, TrustedStack_offset_frames
+	// Move to the previous trusted stack frame.
+	addi               t2, t2, -TrustedStackFrame_size
+	// If this is the first trusted stack frame, then the csp that we would be
+	// loading is the csp on entry, which does not have a spilled area.  In
+	// this case, we would fault when loading, so would exit the thread, but we
+	// should instead gracefully exit the thread.
+	bgeu               tp, t2, .Lcommon_defer_irqs_and_thread_exit
+	cspecialr          ctp, mtdc
+	cincoffset         ct1, ctp, t2
+	// Restore the stack pointer.  All other spilled values are spilled there.
+	clc                csp, TrustedStackFrame_offset_csp(ct1)
+	// Update the current frame offset.
+	csh                t2, TrustedStack_offset_frameoffset(ctp)
+	// Do the loads *after* moving the trusted stack pointer.  In theory, the
+	// checks in `check_compartment_stack_integrity` make it impossible for
+	// this to fault, but if we do fault here then we'd end up in an infinite
+	// loop trying repeatedly to pop the same trusted stack frame.  This would
+	// be bad.  Instead, we move the trusted stack pointer *first* and so, if
+	// the accesses to the untrusted stack fault, we will detect a fault in the
+	// switcher, enter the force-unwind path, and pop the frame for the
+	// compartment that gave us a malicious csp.
+	clc                cs0, SPILL_SLOT_cs0(csp)
+	clc                cs1, SPILL_SLOT_cs1(csp)
+	clc                cra, SPILL_SLOT_pcc(csp)
+	clc                cgp, SPILL_SLOT_cgp(csp)
+	cincoffset         csp, csp, SPILL_SLOT_SIZE
+#ifdef CONFIG_MSHWM
+	// read the stack high water mark, which is 16-byte aligned
+	// we will use this as base address for stack clearing
+	// note that it cannot be greater than stack top as we
+	// we set it to stack top when we pushed to trusted stack frame
+	csrr               tp, CSR_MSHWM
+#else
+	cgetbase           tp, csp
+#endif
+	cgetaddr           t1, csp
+	csetaddr           ct2, csp, tp
+	zero_stack         t2, t1, tp
+#ifdef CONFIG_MSHWM
+	csrw               CSR_MSHWM, sp
+#endif
+
 	// Zero all registers apart from RA, GP, SP and return args.
 	// cra, csp and cgp needed for the compartment
 	// cs0 saved and restored on trusted stack
@@ -318,6 +409,7 @@
 	// ca0, used for first return value
 	// ca1, used for second return value
 	zeroAllRegistersExcept ra, sp, gp, s0, s1, a0, a1
+.Ljust_return:
 	cret
 
 	// If the stack is too small, we don't do the call, but to avoid leaking
@@ -327,7 +419,7 @@
 .Lstack_too_small:
 	li                 a0, -ENOTENOUGHSTACK
 	li                 a1, 0
-	j                  .Lskip_compartment_call
+	j                  switcher_skip_compartment_call
 .size compartment_switcher_entry, . - compartment_switcher_entry
 
 	// the entry point of all exceptions and interrupts
@@ -367,8 +459,8 @@
 
 	// csp now points to the save reg frame that we can use.
 	// The guest csp (c2) is now in mtdc. Will be spilled later, but we
-	// spill all other registers now.
-	spillRegisters     c1, cgp, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15
+	// spill all the other 14 registers now.
+	spillRegisters     cra, cgp, ctp, ct0, ct1, ct2, cs0, cs1, ca0, ca1, ca2, ca3, ca4, ca5
 
 	// If a thread has exited then it will set a fake value in the mcause so
 	// that the scheduler knows not to try to resume it.
@@ -398,7 +490,7 @@
 	// If we hit one of the exception conditions that we should let
 	// compartments handle then deliver it to the compartment.
 	// CHERI exception code.
-	li                 a0, 0x1c
+	li                 a0, MCAUSE_CHERI
 	beq                a0, t1, .Lhandle_error
 	// Misaligned instruction, instruction access, illegal instruction,
 	// breakpoint, misaligned load, load fault, misaligned store, and store
@@ -443,7 +535,7 @@
 
 	// Switch onto the new thread's trusted stack
 	LoadCapPCC         ct0, compartment_switcher_sealing_key
-	li                 gp, 10
+	li                 gp, SEAL_TYPE_SealedTrustedStacks
 	csetaddr           ct0, ct0, gp
 	cunseal            csp, ca0, ct0
 	clw                t0, TrustedStack_offset_mcause(csp)
@@ -457,11 +549,11 @@
 	// mret, so reentrancy is no longer a concern.
 	cspecialw          mtdc, csp
 
-	// If mcause is 25, then we will jump into the error handler: another
-	// thread has signalled that this thread should be interrupted.  25 is a
-	// reserved exception number that we repurpose to indicate explicit
-	// interruption.
-	li                 t1, 25
+	// If mcause is MCAUSE_THREAD_INTERRUPT, then we will jump into the error
+	// handler: another thread has signalled that this thread should be
+	// interrupted.  MCAUSE_THREAD_INTERRUPT is a reserved exception number that
+	// we repurpose to indicate explicit interruption.
+	li                 t1, MCAUSE_THREAD_INTERRUPT
 	beq                t0, t1, .Lhandle_injected_error
 
 	// Environment call from M-mode is exception code 11.
@@ -476,36 +568,27 @@
 // ct2 to be the pcc to jump to.  All other registers are in unspecified states
 // and will be overwritten when we install the context.
 .Linstall_context:
-	clw                x1, TrustedStack_offset_mstatus(csp)
-	csrw               mstatus, x1
+	clw                ra, TrustedStack_offset_mstatus(csp)
+	csrw               mstatus, ra
 #ifdef CONFIG_MSHWM
-	clw                x1, TrustedStack_offset_mshwm(csp)
-	csrw               CSR_MSHWM, x1
-	clw                x1, TrustedStack_offset_mshwmb(csp)
-	csrw               CSR_MSHWMB, x1
+	clw                ra, TrustedStack_offset_mshwm(csp)
+	csrw               CSR_MSHWM, ra
+	clw                ra, TrustedStack_offset_mshwmb(csp)
+	csrw               CSR_MSHWMB, ra
 #endif
 	cspecialw          mepcc, ct2
-	// c2 is csp, which will be loaded last and will overwrite the trusted
-	// stack pointer with the thread's stack pointer.
-	reloadRegisters c1, cgp, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, csp
+	// csp (c2) will be loaded last and will overwrite the trusted stack pointer
+	// with the thread's stack pointer.
+	reloadRegisters cra, cgp, ctp, ct0, ct1, ct2, cs0, cs1, ca0, ca1, ca2, ca3, ca4, ca5, csp
 	mret
 
 // We are starting a forced unwind.  This is reached either when we are unable
 // to run an error handler, or when we do run an error handler and it instructs
 // us to return.  This treats all register values as undefined on entry.
 .Lforce_unwind:
-	// Pop the trusted stack frame.
-	cjal               .Lpop_trusted_stack_frame
-	cmove              cra, ca2
-	// Zero all registers apart from RA, GP, SP and return args.
-	// cra, cs0, cs1, and cgp were restored from the compartment's stack
-	// csp restored from the trusted stack.
-	// ca0, used for first return value
-	// ca1, used for second return value
-	zeroAllRegistersExcept ra, sp, gp, s0, s1, a0, a1
 	li                 a0, -ECOMPARTMENTFAIL
 	li                 a1, 0
-	cret
+	j                  switcher_skip_compartment_call
 
 
 // If we have run out of trusted stack, then just restore the caller's state
@@ -554,15 +637,15 @@
 	auipcc             ct0, 0
 	clc                ct1, TrustedStack_offset_mepcc(csp)
 	cgetbase           t0, ct0
-	cgetbase           t1, ct1
-	beq                t0, t1, .Lforce_unwind
+	cgetbase           tp, ct1
+	beq                t0, tp, .Lhandle_error_in_switcher
 
 	// Load the interrupted thread's stack pointer into ct0
 	clc                ct0, TrustedStack_offset_csp(csp)
 	// See if we can find a handler:
 	clhu               tp, TrustedStack_offset_frameoffset(csp)
 	li                 t1, TrustedStack_offset_frames
-	beq                tp, t1, .Lreset_mepcc_and_install_context
+	beq                tp, t1, .Lset_mcause_and_exit_thread
 	addi               tp, tp, -TrustedStackFrame_size
 
 	// ctp points to the current available trusted stack frame.
@@ -618,22 +701,22 @@
 	// Get the previous trusted stack frame
 
 	// Load the caller's csp
-	clc                ca0, TrustedStackFrame_offset_csp(ctp)
+	clc                ct0, TrustedStackFrame_offset_csp(ctp)
 
 	// If this is the top stack frame, then the csp field is the value on
 	// entry.  If it's any other frame then we need to go to the previous one
 	cincoffset         cs1, csp, TrustedStack_offset_frames
-	beq                s1, t1, .Lrecovered_stack
+	beq                s1, tp, .Lrecovered_stack
 
 	// The address of the stack pointer will point to the bottom of the
 	// caller's save area, so we set the bounds to be the base up to the
 	// current address.
-	cgetaddr           a1, ca0
-	cgetbase           a2, ca0
+	cgetaddr           a1, ct0
+	cgetbase           a2, ct0
 	sub                a1, a1, a2
-	csetaddr           ca0, ca0, a2
+	csetaddr           ct0, ct0, a2
 	// The code that installs the context expects csp to be in ct0
-	csetboundsexact    ct0, ca0, a1
+	csetboundsexact    ct0, ct0, a1
 .Lrecovered_stack:
 	li                 a0, 1
 
@@ -682,9 +765,9 @@
 	ccleartag          cs1, cs1
 	csc                cs1, 0(ct0)
 	// Source for context copy.
-	cincoffset         ca2, csp, TrustedStack_offset_c1
+	cincoffset         ca2, csp, TrustedStack_offset_cra
 	// Destination for context copy
-	cincoffset         ca3, ct0, TrustedStack_offset_c1
+	cincoffset         ca3, ct0, TrustedStack_offset_cra
 	copyContext        ca3, ca2, cs1, a4
 
 	// Set up the arguments for the call
@@ -694,6 +777,9 @@
 	cmove              csp, ca0
 
 .Linvoke_error_handler:
+	// Enable interrupts before invoking the handler
+	csrsi              mstatus, 0x8
+
 	// Clear all registers except:
 	// cra is set by cjalr.  csp and cgp are needed for the called compartment.
 	// ca0, used for the register state
@@ -703,6 +789,15 @@
 	// Call the handler.
 	cjalr              cra
 
+	/*
+	 * Now that we're back, defer interrupts again before we do anything that
+	 * manipulates the TrustedStack.
+	 *
+	 * TODO: Eventually we'd like to move this down onto the paths where it
+	 * actually matters and let most of this code run with IRQs enabled.
+	 */
+	csrci              mstatus, 0x8
+
 	// Move the return value to a register that will be cleared in a forced
 	// unwind and zero the return registers.
 	move               s0, a0
@@ -747,9 +842,9 @@
 	csetaddr           ct2, ct0, ra
 	// Now copy everything else from the stack into the saved context
 	// Source
-	cincoffset         ca2, csp, TrustedStack_offset_c1
+	cincoffset         ca2, csp, TrustedStack_offset_cra
 	// Destination
-	cincoffset         ca3, ct1, TrustedStack_offset_c1
+	cincoffset         ca3, ct1, TrustedStack_offset_cra
 	copyContext        ca3, ca2, cs1, a4
 	// Increment the handler invocation count.  We have now returned and
 	// finished touching any data from the error handler that might cause a
@@ -766,84 +861,55 @@
 
 .Lhandle_injected_error:
 #ifdef CONFIG_MSHWM
-	clw                x1, TrustedStack_offset_mshwm(csp)
-	csrw               CSR_MSHWM, x1
-	clw                x1, TrustedStack_offset_mshwmb(csp)
-	csrw               CSR_MSHWMB, x1
+	clw                ra, TrustedStack_offset_mshwm(csp)
+	csrw               CSR_MSHWM, ra
+	clw                ra, TrustedStack_offset_mshwmb(csp)
+	csrw               CSR_MSHWMB, ra
 #endif
 	j                  .Lhandle_error
 
+.Lcommon_defer_irqs_and_thread_exit:
+	csrci               mstatus, 0x8
+	// Fall-through, now that IRQs are off
 
 	// Value 24 is reserved for custom use.
 .Lset_mcause_and_exit_thread:
-	csrw               mcause, 24
+	csrw               mcause, MCAUSE_THREAD_EXIT
+	// The thread exit code expects the trusted stack pointer to be in csp and
+	// the stack pointer to be in mtdc.  After thread exit, we don't need the
+	// stack pointer so just put zero there.
+	zeroOne            sp
+	cspecialrw         csp, mtdc, csp
 	j                  .Lthread_exit
 
-	// The continue-resume path expects the location that we will mret to to be
-	// in ct2.  If we're just resuming, then resume from the stashed link
-	// register value.
-.Lreset_mepcc_and_install_context:
-	clc                ct2, TrustedStack_offset_mepcc(csp)
+	/*
+	 * Some switcher instructions' traps are handled specially, by looking at
+	 * the offset of mepcc.  Otherwise, we're off to a force unwind.
+	 */
+.Lhandle_error_in_switcher:
+	auipcc             ctp, %cheriot_compartment_hi(.Lswitcher_entry_first_spill)
+	cincoffset         ctp, ctp, %cheriot_compartment_lo_i(.Lhandle_error_in_switcher)
+	bne                t1, tp, .Lforce_unwind
+	li                 a0, -ENOTENOUGHSTACK
+	li                 a1, 0
+
+	/*
+	 * Cause the interrupted thread to resume as if a return had just executed.
+	 * We do this by vectoring to a `cjalr ra` (`cret`) instruction through
+	 * `mepcc`; whee!  Overwrites the stored context a0 and a1 with the current
+	 * values of those registers, effectively passing them through
+	 * .Linstall_context.
+	 */
+.Linstall_return_context:
+	auipcc             ct2, %cheriot_compartment_hi(.Ljust_return)
+	cincoffset         ct2, ct2, %cheriot_compartment_lo_i(.Linstall_return_context)
+	csc                ca0, TrustedStack_offset_ca0(csp)
+	csc                ca1, TrustedStack_offset_ca1(csp)
 	j                  .Linstall_context
 
+
 .size exception_entry_asm, . - exception_entry_asm
 
-/**
- * Pops a frame from the trusted stack.  Leaves all registers in the state
- * expected by the caller of a cross-compartment call, except for the return
- * address which is left in ca2.  The callee is responsible for zeroing
- * argument and temporary registers.
- */
-.Lpop_trusted_stack_frame:
-	// The below should not fault before returning back to the caller. If a fault occurs there must
-	// be a serious bug elsewhere.
-	cspecialr          ctp, mtdc
-	clear_hazard_slots ctp, ct2
-	// make sure there is a frame left in the trusted stack
-	clhu               t2, TrustedStack_offset_frameoffset(ctp)
-	li                 tp, TrustedStack_offset_frames
-	bgeu               tp, t2, .Lset_mcause_and_exit_thread
-	cspecialr          ctp, mtdc
-	addi               t2, t2, -TrustedStackFrame_size
-	cincoffset         ct1, ctp, t2
-	// Restore the stack pointer.  All other spilled values are spilled there.
-	clc                csp, TrustedStackFrame_offset_csp(ct1)
-	// Update the current frame offset.
-	csh                t2, TrustedStack_offset_frameoffset(ctp)
-	// Do the loads *after* moving the trusted stack pointer.  In theory, the
-	// checks in `check_compartment_stack_integrity` make it impossible for
-	// this to fault, but if we do fault here then we'd end up in an infinite
-	// loop trying repeatedly to pop the same trusted stack frame.  This would
-	// be bad.  Instead, we move the trusted stack pointer *first* and so, if
-	// the accesses to the untrusted stack fault, we will detect a fault in the
-	// switcher, enter the force-unwind path, and pop the frame for the
-	// compartment that gave us a malicious csp.
-	clc                cs0, SPILL_SLOT_cs0(csp)
-	clc                cs1, SPILL_SLOT_cs1(csp)
-	clc                ca2, SPILL_SLOT_pcc(csp)
-	clc                cgp, SPILL_SLOT_cgp(csp)
-	cincoffset         csp, csp, SPILL_SLOT_SIZE
-#ifndef CONFIG_NO_SWITCHER_SAFETY
-#ifdef CONFIG_MSHWM
-	// read the stack high water mark, which is 16-byte aligned
-	// we will use this as base address for stack clearing
-	// note that it cannot be greater than stack top as we 
-	// we set it to stack top when we pushed to trusted stack frame
-	csrr               tp, CSR_MSHWM
-#else
-	cgetbase           tp, csp
-#endif
-	cgetaddr           t1, csp
-	csetaddr           ct2, csp, tp
-	zero_stack         t2, t1, tp
-#ifdef CONFIG_MSHWM
-	csrw               CSR_MSHWM, sp
-#endif
-#endif // CONFIG_NO_SWITCHER_SAFETY
-	cret
-
-
-
 /*******************************************************************************
  * Switcher-exported library functions.
  *
@@ -914,7 +980,7 @@
 	// Load the unsealing key into a register that we will clobber two
 	// instructions later.
 	LoadCapPCC         ca1, compartment_switcher_sealing_key
-	li                 a2, 10
+	li                 a2, SEAL_TYPE_SealedTrustedStacks
 	csetaddr           ca1, ca1, a2
 	// The target capability is in ct1. Unseal, check tag and load the entry point offset.
 	cunseal            ca1, ca0, ca1
@@ -960,7 +1026,7 @@
 
 	// Mark the thread as interrupted.
 	// Store a magic value in mcause
-	li                 a2, 25
+	li                 a2, MCAUSE_THREAD_INTERRUPT
 	csw                a2, TrustedStack_offset_mcause(ca1)
 	// Return success
 	li                 a0, 1
@@ -973,7 +1039,7 @@
 	.type __Z23switcher_current_threadv,@function
 __Z23switcher_current_threadv:
 	LoadCapPCC         ca0, compartment_switcher_sealing_key
-	li                 a1, 10
+	li                 a1, SEAL_TYPE_SealedTrustedStacks
 	csetaddr           ca0, ca0, a1
 	cspecialr          ca1, mtdc
 	cseal              ca0, ca1, ca0
diff --git a/sdk/core/switcher/misc-assembly.h b/sdk/core/switcher/misc-assembly.h
new file mode 100644
index 0000000..f93293c
--- /dev/null
+++ b/sdk/core/switcher/misc-assembly.h
@@ -0,0 +1,43 @@
+// Copyright CHERIoT Contributors.
+// SPDX-License-Identifier: MIT
+
+#pragma once
+#include <assembly-helpers.h>
+
+/*
+ * Constant to represent the raw permissions of the compartment CSP.  We use
+ * this in the switcher, to verify the permissions of the CSP that comes from
+ * the compartment are exactly what we expect.
+ */
+EXPORT_ASSEMBLY_EXPRESSION(COMPARTMENT_STACK_PERMISSIONS,
+                           (CHERI::PermissionSet{
+                             CHERI::Permission::Load,
+                             CHERI::Permission::Store,
+                             CHERI::Permission::LoadStoreCapability,
+                             CHERI::Permission::LoadMutable,
+                             CHERI::Permission::StoreLocal,
+                             CHERI::Permission::LoadGlobal}
+                              .as_raw()),
+                           0x7e)
+
+/**
+ * Space reserved at the top of a stack on entry to the compartment.
+ *
+ * This *must* be a multiple of 16, which is the stack alignment.
+ */
+#define STACK_ENTRY_RESERVED_SPACE 16
+
+#ifdef __cplusplus
+using namespace priv;
+#endif
+
+EXPORT_ASSEMBLY_NAME(MCAUSE_THREAD_EXIT, 24)
+EXPORT_ASSEMBLY_NAME(MCAUSE_THREAD_INTERRUPT, 25)
+EXPORT_ASSEMBLY_NAME(MCAUSE_CHERI, 28)
+
+EXPORT_ASSEMBLY_EXPRESSION(SEAL_TYPE_SealedImportTableEntries,
+                           SealingType::SealedImportTableEntries,
+                           9)
+EXPORT_ASSEMBLY_EXPRESSION(SEAL_TYPE_SealedTrustedStacks,
+                           SealingType::SealedTrustedStacks,
+                           10)
diff --git a/sdk/core/switcher/trusted-stack-assembly.h b/sdk/core/switcher/trusted-stack-assembly.h
index 02fc248..7969662 100644
--- a/sdk/core/switcher/trusted-stack-assembly.h
+++ b/sdk/core/switcher/trusted-stack-assembly.h
@@ -4,21 +4,21 @@
 #pragma once
 #include <assembly-helpers.h>
 EXPORT_ASSEMBLY_OFFSET(TrustedStack, mepcc, 0 * 8)
-EXPORT_ASSEMBLY_OFFSET(TrustedStack, c1, 1 * 8)
+EXPORT_ASSEMBLY_OFFSET(TrustedStack, cra, 1 * 8)
 EXPORT_ASSEMBLY_OFFSET(TrustedStack, csp, 2 * 8)
 EXPORT_ASSEMBLY_OFFSET(TrustedStack, cgp, 3 * 8)
-EXPORT_ASSEMBLY_OFFSET(TrustedStack, c4, 4 * 8)
-EXPORT_ASSEMBLY_OFFSET(TrustedStack, c5, 5 * 8)
-EXPORT_ASSEMBLY_OFFSET(TrustedStack, c6, 6 * 8)
-EXPORT_ASSEMBLY_OFFSET(TrustedStack, c7, 7 * 8)
-EXPORT_ASSEMBLY_OFFSET(TrustedStack, c8, 8 * 8)
-EXPORT_ASSEMBLY_OFFSET(TrustedStack, c9, 9 * 8)
-EXPORT_ASSEMBLY_OFFSET(TrustedStack, c10, 10 * 8)
-EXPORT_ASSEMBLY_OFFSET(TrustedStack, c11, 11 * 8)
-EXPORT_ASSEMBLY_OFFSET(TrustedStack, c12, 12 * 8)
-EXPORT_ASSEMBLY_OFFSET(TrustedStack, c13, 13 * 8)
-EXPORT_ASSEMBLY_OFFSET(TrustedStack, c14, 14 * 8)
-EXPORT_ASSEMBLY_OFFSET(TrustedStack, c15, 15 * 8)
+EXPORT_ASSEMBLY_OFFSET(TrustedStack, ctp, 4 * 8)
+EXPORT_ASSEMBLY_OFFSET(TrustedStack, ct0, 5 * 8)
+EXPORT_ASSEMBLY_OFFSET(TrustedStack, ct1, 6 * 8)
+EXPORT_ASSEMBLY_OFFSET(TrustedStack, ct2, 7 * 8)
+EXPORT_ASSEMBLY_OFFSET(TrustedStack, cs0, 8 * 8)
+EXPORT_ASSEMBLY_OFFSET(TrustedStack, cs1, 9 * 8)
+EXPORT_ASSEMBLY_OFFSET(TrustedStack, ca0, 10 * 8)
+EXPORT_ASSEMBLY_OFFSET(TrustedStack, ca1, 11 * 8)
+EXPORT_ASSEMBLY_OFFSET(TrustedStack, ca2, 12 * 8)
+EXPORT_ASSEMBLY_OFFSET(TrustedStack, ca3, 13 * 8)
+EXPORT_ASSEMBLY_OFFSET(TrustedStack, ca4, 14 * 8)
+EXPORT_ASSEMBLY_OFFSET(TrustedStack, ca5, 15 * 8)
 EXPORT_ASSEMBLY_OFFSET(TrustedStack, hazardPointers, 16 * 8)
 EXPORT_ASSEMBLY_OFFSET(TrustedStack, mstatus, 17 * 8)
 EXPORT_ASSEMBLY_OFFSET(TrustedStack, mcause, (17 * 8) + 4)
@@ -57,18 +57,3 @@
 
 #define TSTACKOFFSET_FIRSTFRAME                                                \
 	(TrustedStack_offset_frameoffset + TSTACK_HEADER_SZ)
-
-/* Constant to represent the raw permissions of the compartment CSP.
- *  We use this in the switcher, to verify the CSP comes from the
- *  compartment is exactly what we expect.
- *  This represents the following permissions:
- *  Load, Store, LoadStoreCapability, LoadMutable StoreLocal and LoadGlobal
- */
-#define COMPARTMENT_STACK_PERMISSIONS 0x7e
-
-/**
- * Space reserved at the top of a stack on entry to the compartment.
- *
- * This *must* be a multiple of 16, which is the stack alignment.
- */
-#define STACK_ENTRY_RESERVED_SPACE 16
diff --git a/sdk/core/switcher/tstack.h b/sdk/core/switcher/tstack.h
index 45af06e..dc8338b 100644
--- a/sdk/core/switcher/tstack.h
+++ b/sdk/core/switcher/tstack.h
@@ -32,21 +32,21 @@
 struct TrustedStackGeneric
 {
 	void  *mepcc;
-	void  *c1;
-	void  *csp;
-	void  *cgp;
-	void  *c4;
-	void  *c5;
-	void  *c6;
-	void  *c7;
-	void  *c8;
-	void  *c9;
-	void  *c10;
-	void  *c11;
-	void  *c12;
-	void  *c13;
-	void  *c14;
-	void  *c15;
+	void  *cra; // c1
+	void  *csp; // c2
+	void  *cgp; // c3
+	void  *ctp; // c4
+	void  *ct0; // c5
+	void  *ct1; // c6
+	void  *ct2; // c7
+	void  *cs0; // c8
+	void  *cs1; // c9
+	void  *ca0; // c10
+	void  *ca1; // c11
+	void  *ca2; // c12
+	void  *ca3; // c13
+	void  *ca4; // c14
+	void  *ca5; // c15
 	void  *hazardPointers;
 	size_t mstatus;
 	size_t mcause;
@@ -77,13 +77,3 @@
 using TrustedStack = TrustedStackGeneric<0>;
 
 #include "trusted-stack-assembly.h"
-
-static_assert(
-  CheckSize<COMPARTMENT_STACK_PERMISSIONS,
-            CHERI::PermissionSet{CHERI::Permission::Load,
-                                 CHERI::Permission::Store,
-                                 CHERI::Permission::LoadStoreCapability,
-                                 CHERI::Permission::LoadMutable,
-                                 CHERI::Permission::StoreLocal,
-                                 CHERI::Permission::LoadGlobal}
-              .as_raw()>::value);
diff --git a/sdk/core/token_library/token_unseal.S b/sdk/core/token_library/token_unseal.S
index 8391902..1dccc04 100644
--- a/sdk/core/token_library/token_unseal.S
+++ b/sdk/core/token_library/token_unseal.S
@@ -2,24 +2,27 @@
 #include <cheri-builtins.h>
 #include "../allocator/token.h"
 
-/*
- * An in-assembler implementation of
+	.hidden __sealingkey
+	.type   __sealingkey,@object
+	.section    .sealing_key1,"aw",@progbits
+	.globl  __sealingkey
+	.p2align    3
+__sealingkey:
+	.chericap   0
+	.size   __sealingkey, 8
+
+
+.section .text,"ax",@progbits
+
+.p2align 1
+
+/**
+ * The core of unsealing:
  *
- * [[cheri::interrupt_state(disabled)]] void *__cheri_libcall
- * token_obj_unseal(struct SKeyStruct *, struct SObjStruct *);
- *
- * The name has been manually mangled as per the C++ rules.
+ *   void *token_unseal_internal(struct SKeyStruct *, struct SObjStruct *, int);
  */
 
-	.section .text._Z16token_obj_unsealP10SKeyStructP10SObjStruct,"axG", \
-		@progbits,_Z16token_obj_unsealP10SKeyStructP10SObjStruct,comdat
-
-	.hidden  _Z16token_obj_unsealP10SKeyStructP10SObjStruct
-	.globl   _Z16token_obj_unsealP10SKeyStructP10SObjStruct
-	.p2align 1
-	.type    _Z16token_obj_unsealP10SKeyStructP10SObjStruct,@function
-
-_Z16token_obj_unsealP10SKeyStructP10SObjStruct:
+.Ltoken_unseal_internal:
   /*
    * Register allocation:
    *
@@ -28,9 +31,11 @@
    *
    *  - ca1 holds the user's sealed object pointer
    *
-   *  - t0/ct0 holds a copy of the user key
+   *  - a2 contains the expected sealing type.
    *
-   *  - t1/ct1 is used within each local computation and never holds secrets
+   *  - t0 holds a copy of the user key's address field (authorized type)
+   *
+   *  - t1 is used within each local computation and never holds secrets
    */
 
   /* Verify key tag */
@@ -48,8 +53,8 @@
   andi     t1, t1, CHERI_PERM_UNSEAL
   beqz     t1, .Lexit_failure
 
-  /* Copy key capability to scratch register */
-  cmove ct0, ca0
+  /* Copy key type to scratch register */
+  cgetaddr t0, ca0
 
   /*
    * Load unsealing root capability, to be clobbered by return value
@@ -59,6 +64,7 @@
 .Lload_sealing_key:
   auipcc ca0, %cheriot_compartment_hi(__sealingkey)
   clc    ca0, %cheriot_compartment_lo_i(.Lload_sealing_key)(ca0)
+  csetaddr ca0, ca0, a2
 
   /* Unseal, clobbering authority */
   cunseal ca0, ca1, ca0
@@ -73,17 +79,12 @@
    */
   clw t1, TokenSObj_offset_type(ca0)
 
-  /*
-   * Verify that the loaded value matches the address of the key (via as-integer
-   * access to capability register ct0).
-   */
+  /* Verify that the loaded value matches the address of the key. */
   bne t0, t1, .Lexit_failure
 
   /* Subset bounds to ->data */
   // Get the top into t1
-  cgetlen         t1, ca0
-  cgetbase        t0, ca0
-  add             t1, t1, t0
+  cgettop         t1, ca0
   // Move the address to the start of the data
   cincoffset      ca0, ca0, TokenSObj_offset_data
   // Subtract the address of the (to-be-returned-unsealed) data from the top to
@@ -101,8 +102,60 @@
   cmove ca0, cnull
   cret
 
+/**
+ * An in-assembler implementation of
+ *
+ * [[cheri::interrupt_state(disabled)]] void *__cheri_libcall
+ * token_obj_unseal(struct SKeyStruct *, struct SObjStruct *);
+ *
+ * The name has been manually mangled as per the C++ rules.
+ */
+	.hidden _Z16token_obj_unsealP10SKeyStructP10SObjStruct
+	.globl  _Z16token_obj_unsealP10SKeyStructP10SObjStruct
+_Z16token_obj_unsealP10SKeyStructP10SObjStruct:
+	cgettype a2, ca1
+	j        .Ltoken_unseal_internal
+
+/**
+ * An in-assembler implementation of
+ *
+ * [[cheri::interrupt_state(disabled)]] void *__cheri_libcall
+ * token_obj_unseal_static(struct SKeyStruct *, struct SObjStruct *);
+ *
+ * The name has been manually mangled as per the C++ rules.
+ */
+	.hidden  _Z23token_obj_unseal_staticP10SKeyStructP10SObjStruct
+	.globl   _Z23token_obj_unseal_staticP10SKeyStructP10SObjStruct
+_Z23token_obj_unseal_staticP10SKeyStructP10SObjStruct:
+	li       a2, CheriSealTypeStaticToken
+	j        .Ltoken_unseal_internal
+
+/**
+ * An in-assembler implementation of
+ *
+ * [[cheri::interrupt_state(disabled)]] void *__cheri_libcall
+ * token_obj_unseal_dynamic(struct SKeyStruct *, struct SObjStruct *);
+ *
+ * The name has been manually mangled as per the C++ rules.
+ */
+	.hidden  _Z16token_obj_unsealP10SKeyStructP10SObjStruct
+	.globl   _Z16token_obj_unsealP10SKeyStructP10SObjStruct
+_Z24token_obj_unseal_dynamicP10SKeyStructP10SObjStruct:
+	li       a2, CheriSealTypeAllocator
+	j        .Ltoken_unseal_internal
+
 /* TODO: Eventually this goes away, when the assembler can generate it for us */
 CHERIOT_EXPORT_LIBCALL \
   _Z16token_obj_unsealP10SKeyStructP10SObjStruct, \
   0 /* No stack usage */, \
   0b00010010 /* IRQs deferred, zero two registers */
+
+CHERIOT_EXPORT_LIBCALL \
+  _Z23token_obj_unseal_staticP10SKeyStructP10SObjStruct, \
+  0 /* No stack usage */, \
+  0b00010010 /* IRQs deferred, zero two registers */
+
+CHERIOT_EXPORT_LIBCALL \
+  _Z24token_obj_unseal_dynamicP10SKeyStructP10SObjStruct, \
+  0 /* No stack usage */, \
+  0b00010010 /* IRQs deferred, zero two registers */
diff --git a/sdk/firmware.ldscript.in b/sdk/firmware.ldscript.in
index 9fc9a29..a9a3956 100644
--- a/sdk/firmware.ldscript.in
+++ b/sdk/firmware.ldscript.in
@@ -46,6 +46,8 @@
 	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);
@@ -55,6 +57,8 @@
 	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);
@@ -66,6 +70,8 @@
 	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);
@@ -156,6 +162,8 @@
 		LONG(.compartment_switcher_start);
 		# Compartment switcher end
 		SHORT(.compartment_switcher_end - .compartment_switcher_start);
+		# Cross-compartment call return path
+		SHORT(switcher_skip_compartment_call - .compartment_switcher_start);
 		# Compartment switcher sealing key
 		SHORT(compartment_switcher_sealing_key - .compartment_switcher_start);
 		# Switcher's copy of the scheduler's PCC.
@@ -178,8 +186,10 @@
 		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_start);
+		SHORT(.scheduler_import_end - .scheduler_import_start);
 		# Address of scheduler export table
 		LONG(.scheduler_export_table);
 		# Size of the scheduler export table
@@ -197,8 +207,10 @@
 		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_start);
+		SHORT(.allocator_import_end - .allocator_import_start);
 		# Address of allocator export table
 		LONG(.allocator_export_table);
 		# Size of the allocator export table
@@ -215,8 +227,10 @@
 		# 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_start);
+		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
@@ -232,7 +246,7 @@
 		# loader versions.
 		# New versions of this can be generated with:
 		# head /dev/random | shasum | cut -c 0-8
-		LONG(0x6cef3879);
+		LONG(0xca2b63de);
 		# Number of library headers.
 		SHORT(@library_count@);
 		# Number of compartment headers.
diff --git a/sdk/include/FreeRTOS-Compat/queue.h b/sdk/include/FreeRTOS-Compat/queue.h
index 088d588..9b8f072 100644
--- a/sdk/include/FreeRTOS-Compat/queue.h
+++ b/sdk/include/FreeRTOS-Compat/queue.h
@@ -8,17 +8,7 @@
 /**
  * Queue handle.  This is used to reference queues in the API functions.
  */
-typedef struct
-{
-	/**
-	 * The real handle, holds pointers to the relevant elements of the queue.
-	 */
-	struct QueueHandle handle;
-	/**
-	 * The pointer used to free the queue.
-	 */
-	void *freePointer;
-} * QueueHandle_t;
+typedef struct MessageQueue *QueueHandle_t;
 
 /**
  * Receive a message on a queue.  The message is received into `buffer`, which
@@ -34,7 +24,7 @@
 xQueueReceive(QueueHandle_t queueHandle, void *buffer, TickType_t waitTicks)
 {
 	struct Timeout timeout = {0, waitTicks};
-	int            rv = queue_receive(&timeout, &queueHandle->handle, buffer);
+	int            rv = queue_receive(&timeout, queueHandle, buffer);
 
 	if (rv == 0)
 		return pdPASS;
@@ -54,7 +44,7 @@
                                           TickType_t    waitTicks)
 {
 	struct Timeout timeout = {0, waitTicks};
-	int            rv      = queue_send(&timeout, &queueHandle->handle, buffer);
+	int            rv      = queue_send(&timeout, queueHandle, buffer);
 
 	if (rv == 0)
 		return pdPASS;
@@ -86,24 +76,13 @@
 static inline QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength,
                                          UBaseType_t uxItemSize)
 {
-	QueueHandle_t  ret;
+	QueueHandle_t  ret = NULL;
 	struct Timeout timeout = {0, UnlimitedTimeout};
-	ret                    = (QueueHandle_t)malloc(sizeof(*ret));
-	if (!ret)
-	{
-		return NULL;
-	}
 	int rc = queue_create(&timeout,
 	                      MALLOC_CAPABILITY,
-	                      &ret->handle,
-	                      &ret->freePointer,
+	                      &ret,
 	                      uxItemSize,
 	                      uxQueueLength);
-	if (rc)
-	{
-		free(ret);
-		return NULL;
-	}
 	return ret;
 }
 #endif
@@ -113,8 +92,7 @@
  */
 static inline void vQueueDelete(QueueHandle_t xQueue)
 {
-	free(xQueue->freePointer);
-	free(xQueue);
+	queue_destroy(MALLOC_CAPABILITY, xQueue);
 }
 
 /**
@@ -126,7 +104,7 @@
 static inline UBaseType_t uxQueueMessagesWaiting(const QueueHandle_t xQueue)
 {
 	size_t ret;
-	int    rv = queue_items_remaining(&xQueue->handle, &ret);
+	int    rv = queue_items_remaining(xQueue, &ret);
 
 	assert(rv == 0);
 
diff --git a/sdk/include/assembly-helpers.h b/sdk/include/assembly-helpers.h
index cdc92d7..73b0a68 100644
--- a/sdk/include/assembly-helpers.h
+++ b/sdk/include/assembly-helpers.h
@@ -22,6 +22,25 @@
 };
 
 /**
+ * Export a macro into assembly named `name` with value `value`.  In C++, this
+ * macro will report an error if the provided value does not equal the constexpr
+ * evaluation of `expression`.
+ */
+#	define EXPORT_ASSEMBLY_NAME(name, val)                                    \
+		static_assert(CheckSize<name, val>::value,                             \
+		              "Value provided for assembly is incorrect");
+
+/**
+ * Export a macro into assembly named `name` with value `value`.  In C++, this
+ * macro will report an error if the provided value does not equal the constexpr
+ * evaluation of `expression`.
+ */
+#	define EXPORT_ASSEMBLY_EXPRESSION(name, expression, val)                  \
+		static constexpr size_t name = expression;                             \
+		static_assert(CheckSize<name, val>::value,                             \
+		              "Value provided for assembly is incorrect");
+
+/**
  * Export a macro into assembly of the form `{structure}_offset_{field}`.  The
  * value of this macro will be `value`.  In C++, this macro will report an error
  * if the provided value does not match the compiler's understanding of the
@@ -61,13 +80,19 @@
 		static_assert(CheckSize<sizeof(structure), val>::value,                \
 		              "Size provided for assembly is incorrect");
 #elif defined(__ASSEMBLER__)
+#	define EXPORT_ASSEMBLY_NAME(name, value)                                  \
+		.set name, value
+#	define EXPORT_ASSEMBLY_EXPRESSION(name, expression, value)                \
+		.set name, value
 #	define EXPORT_ASSEMBLY_OFFSET_NAMED(structure, field, value, name)        \
-		.set name value
+		.set name, value
 #	define EXPORT_ASSEMBLY_OFFSET(structure, field, value)                    \
 		.set structure##_offset_##field, value
 #	define EXPORT_ASSEMBLY_SIZE(structure, value) .set structure##_size, value
 #else
-#	define EXPORT_ASSEMBLY_OFFSET(structure, field, name, value)
+#	define EXPORT_ASSEMBLY_NAME(name, value)
+#	define EXPORT_ASSEMBLY_EXPRESSION(name, expression, value)
+#	define EXPORT_ASSEMBLY_OFFSET(structure, field, name)
 #	define EXPORT_ASSEMBLY_SIZE(structure, name, value)
 #	define EXPORT_ASSEMBLY_OFFSET_NAMED(structure, field, value, name)
 #endif
diff --git a/sdk/include/cdefs.h b/sdk/include/cdefs.h
index 2242e7e..0a91697 100644
--- a/sdk/include/cdefs.h
+++ b/sdk/include/cdefs.h
@@ -61,7 +61,6 @@
 #define __section(x) __attribute__((section(x)))
 #define __alloc_size(x) __attribute__((alloc_size(x)))
 #define __alloc_align(x) __attribute__((alloc_align(x)))
-#define __cheri_callback __attribute__((cheri_ccallback))
 #if __has_attribute(cheriot_minimum_stack)
 #	define __cheriot_minimum_stack(x) __attribute__((cheriot_minimum_stack(x)))
 #else
@@ -69,15 +68,34 @@
 	  "cheriot_minimum_stack attribute not supported, please update your compiler"
 #	define __cheriot_minimum_stack(x)
 #endif
+
 // When running clang-tidy, we use the same compile flags for everything and so
 // will get errors about things being defined in the wrong compartment, so
 // define away the compartment name and pretend everything is local for now.
-#ifdef CLANG_TIDY
+#if defined(CLANG_TIDY) || defined(__CHERIOT_BAREMETAL__)
 #	define __cheri_compartment(x)
 #else
 #	define __cheri_compartment(x) __attribute__((cheri_compartment(x)))
 #endif
-#define __cheri_libcall __attribute__((cheri_libcall))
+
+// Define the CHERIoT calling-convention attributes macros to nothing if we're
+// targeting bare metal and to the correct attributes if we're targeting the
+// RTOS.
+#ifdef __CHERIOT_BAREMETAL__
+#	define __cheri_libcall
+#	define __cheri_callback
+#else
+#	define __cheri_libcall __attribute__((cheri_libcall))
+#	define __cheri_callback __attribute__((cheri_ccallback))
+
+/**
+ * Define the symbol for the libcall that the compiler will expand the `strlen`
+ * builtin to.  This builtin is used internally in libc++ (and possibly in
+ * other places) to avoid the namespace pollution from including `string.h` but
+ * is either constant folded in the front end or expanded to a libcall.
+ */
+unsigned __builtin_strlen(const char *str) __asm__("_Z6strlenPKc");
+#endif
 
 #define offsetof(a, b) __builtin_offsetof(a, b)
 
@@ -107,14 +125,6 @@
 #	define __clang_ignored_warning_pop()
 #endif
 
-/**
- * Define the symbol for the libcall that the compiler will expand the `strlen`
- * builtin to.  This builtin is used internally in libc++ (and possibly in
- * other places) to avoid the namespace pollution from including `string.h` but
- * is either constant folded in the front end or expanded to a libcall.
- */
-unsigned __builtin_strlen(const char *str) __asm__("_Z6strlenPKc");
-
 #if !defined(CLANG_TIDY) && !__has_builtin(__builtin_cheri_top_get)
 #	error Your compiler is too old for this version of CHERIoT RTOS, please upgrade to a newer version
 #endif
diff --git a/sdk/include/cheri.h b/sdk/include/cheri.h
index f7cc6bc..b10d059 100644
--- a/sdk/include/cheri.h
+++ b/sdk/include/cheri.h
@@ -9,6 +9,359 @@
 struct Timeout;
 
 /**
+ * The complete set of architectural permissions.
+ */
+enum CHERIPermission
+{
+	/**
+	 * Capability refers to global memory (this capability may be stored
+	 * anywhere).
+	 */
+	CheriPermissionGlobal = 0,
+	/**
+	 * Global capabilities can be loaded through this capability.  Without
+	 *  this permission, any capability loaded via this capability will
+	 *  have `Global` and `LoadGlobal` removed.
+	 */
+	CheriPermissionLoadGlobal = 1,
+	/**
+	 * Capability may be used to store.  Any store via a capability without
+	 * this permission will trap.
+	 */
+	CheriPermissionStore = 2,
+	/**
+	 * Capabilities with store permission may be loaded through this
+	 * capability.  Without this, any loaded capability will have
+	 * `LoadMutable` and `Store` removed.
+	 */
+	CheriPermissionLoadMutable = 3,
+	/**
+	 * This capability may be used to store capabilities that do not have
+	 * `Global` permission.
+	 */
+	CheriPermissionStoreLocal = 4,
+	/**
+	 * This capability can be used to load.
+	 */
+	CheriPermissionLoad = 5,
+	/**
+	 * Any load and store permissions on this capability convey the right to
+	 * load or store capabilities in addition to data.
+	 */
+	CheriPermissionLoadStoreCapability = 6,
+	/**
+	 * If installed as the program counter capability, running code may
+	 * access privileged system registers.
+	 */
+	CheriPermissionAccessSystemRegisters = 7,
+	/**
+	 * This capability may be used as a jump target and used to execute
+	 * instructions.
+	 */
+	CheriPermissionExecute = 8,
+	/**
+	 * This capability may be used to unseal other capabilities.  The
+	 * 'address' range is in the sealing type namespace and not in the
+	 * memory namespace.
+	 */
+	CheriPermissionUnseal = 9,
+	/**
+	 * This capability may be used to seal other capabilities.  The
+	 * 'address' range is in the sealing type namespace and not in the
+	 * memory namespace.
+	 */
+	CheriPermissionSeal = 10,
+	/**
+	 * Software defined permission bit, no architectural meaning.
+	 */
+	CheriPermissionUser0 = 11
+};
+
+/**
+ * The codes used in the cause field of the mtval CSR when the processor
+ * takes a CHERI exception.
+ */
+enum CHERICauseCode
+{
+	/**
+	 * No exception. This value is passed to the error handler after a
+	 * forced unwind in a called compartment.
+	 */
+	CheriCauseCodeNone = 0,
+	/**
+	 * Attempted to use a capability outside its bounds.
+	 */
+	CheriCauseCodeBoundsViolation = 1,
+	/**
+	 * Attempted to use an untagged capability to authorize something.
+	 */
+	CheriCauseCodeTagViolation = 2,
+	/**
+	 * Attempted to use a sealed capability to authorize something.
+	 */
+	CheriCauseCodeSealViolation = 3,
+	/**
+	 * Attempted to jump to a capability without `Permission::Execute`.
+	 */
+	CheriCauseCodePermitExecuteViolation = 0x11,
+	/**
+	 * Attempted to load via a capability without `Permission::Load`.
+	 */
+	CheriCauseCodePermitLoadViolation = 0x12,
+	/**
+	 * Attempted to store via a capability without `Permission::Store`.
+	 */
+	CheriCauseCodePermitStoreViolation = 0x13,
+	/**
+	 * Attempted to store a tagged capability via a capability without
+	 * `Permission::LoadStoreCapability`.
+	 */
+	CheriCauseCodePermitStoreCapabilityViolation = 0x15,
+	/**
+	 * Attempted to store a tagged capability without `Permission::Global`
+	 * via capability without `Permission::StoreLocal`.
+	 */
+	CheriCauseCodePermitStoreLocalCapabilityViolation = 0x16,
+	/**
+	 * Attempted to access a restricted CSR or SCR with PCC without
+	 * `Permission::AccessSystemRegisters`.
+	 */
+	CheriCauseCodePermitAccessSystemRegistersViolation = 0x18,
+	/**
+	 * Used to represent a value that has no valid meaning in hardware.
+	 */
+	CheriCauseCodeInvalid = -1
+};
+
+/**
+ * Register numbers as reported in cap idx field of  `mtval` CSR when
+ * a CHERI exception is taken. Values less than 32 refer to general
+ * purpose registers and others to SCRs (of these, only PCC can actually
+ * cause an exception).
+ */
+enum CHERIRegisterNumber
+{
+	/**
+	 * The zero register, which always contains the `NULL` capability.
+	 */
+	CheriRegisterNumberCzr = 0x0,
+	/**
+	 * `$c1` / `$cra` used by the ABI as the return address.
+	 * Not preserved across calls.
+	 */
+	CheriRegisterNumberCra = 0x1,
+	/**
+	 * `$c2` / `$csp` used by the ABI as the stack pointer.
+	 * Preserved across calls.
+	 */
+	CheriRegisterNumberCsp = 0x2,
+	/**
+	 * `$c3` / `$cgp` used by the ABI as the global pointer.
+	 * Not allocatable by the compiler, set by the switcher on compartment
+	 * entry.
+	 */
+	CheriRegisterNumberCgp = 0x3,
+	/**
+	 * `$c4` / `$ctp` used by the ABI as the thread pointer.
+	 * Currently unused by the compiler.
+	 * Not preserved across compartment calls.
+	 */
+	CheriRegisterNumberCtp = 0x4,
+	/**
+	 * `$c5` / `$ct0` used by the ABI as temporary register.
+	 * Not preserved across calls.
+	 */
+	CheriRegisterNumberCT0 = 0x5,
+	/**
+	 * `$c6` / `$ct1` used by the ABI as temporary register.
+	 * Not preserved across calls.
+	 */
+	CheriRegisterNumberCT1 = 0x6,
+	/**
+	 * `$c7` / `$ct2` used by the ABI as temporary register.
+	 * Not preserved across calls.
+	 */
+	CheriRegisterNumberCT2 = 0x7,
+	/**
+	 * `$c8` / `$cs0` used by the ABI as a callee-saved register.
+	 * Preserved across calls.
+	 */
+	CheriRegisterNumberCS0 = 0x8,
+	/**
+	 * `$c9` / `$cs1` used by the ABI as a callee-saved register.
+	 * Preserved across calls.
+	 */
+	CheriRegisterNumberCS1 = 0x9,
+	/**
+	 * `$c10` / `$ca0` used by the ABI as an argument register.
+	 * Not preserved across calls.
+	 */
+	CheriRegisterNumberCA0 = 0xa,
+	/**
+	 * `$c11` / `$ca1` used by the ABI as an argument register.
+	 * Not preserved across calls.
+	 */
+	CheriRegisterNumberCA1 = 0xb,
+	/**
+	 * `$c12` / `$ca2` used by the ABI as an argument register.
+	 * Not preserved across calls.
+	 */
+	CheriRegisterNumberCA2 = 0xc,
+	/**
+	 * `$c13` / `$ca3` used by the ABI as an argument register.
+	 * Not preserved across calls.
+	 */
+	CheriRegisterNumberCA3 = 0xd,
+	/**
+	 * `$c14` / `$ca4` used by the ABI as an argument register.
+	 * Not preserved across calls.
+	 */
+	CheriRegisterNumberCA4 = 0xe,
+	/**
+	 * `$c15` / `$ca5` used by the ABI as an argument register.
+	 * Not preserved across calls.
+	 */
+	CheriRegisterNumberCA5 = 0xf,
+	/**
+	 * The Program Counter Capability.
+	 *
+	 * Special capability register used to authorize instruction fetch. The
+	 * address is that of the faulting instruction. Also used for accessing
+	 * read-only globals.
+	 */
+	CheriRegisterNumberPcc = 0x20,
+	/**
+	 * Machine-mode Trap Code Capability.
+	 *
+	 * Special capability register that
+	 * is installed in PCC when the CPU takes a trap. The address has the
+	 * same semantics as the RISC-V `mtvec` CSR. Only accessible when PCC
+	 * has the AccessSystemRegisters permission.
+	 */
+	CheriRegisterNumberMtcc = 0x3c,
+	/**
+	 * Machine-mode Tusted Data Capability.
+	 *
+	 * Special capability register that contains the memory root capability
+	 * on boot. Only accessible when PCC has the AccessSystemRegisters
+	 * permission.  Use by the RTOS to store a capability to the trusted
+	 * stack.
+	 */
+	CheriRegisterNumberMtdc = 0x3d,
+	/**
+	 * Machine-mode Scratch Capability. Special capabiltiy register that
+	 * contains the sealing root capability on boot. Only accessible when
+	 * PCC has the AccessSystemRegisters permission.
+	 */
+	CheriRegisterNumberMScratchC = 0x3e,
+	/**
+	 * Machine-mode Exception Program Counter Capability. Special capability
+	 * register that contains the PCC of the faulting instruction on trap.
+	 * The address has the same semantics as the RISC-V `mepc` CSR. Only
+	 * accessible when PCC has the AccessSystemRegisters permission.
+	 */
+	CheriRegisterNumberMepcc = 0x3f,
+	/**
+	 * Indicates a value that is not used by the hardware to refer to a
+	 * register.
+	 */
+	CheriRegisterNumberInvalid = -1
+};
+
+/**
+ * Sealing types.
+ */
+enum CHERISealingType
+{
+	/**
+	 * 0 represents unsealed.
+	 */
+	CheriSealTypeUnsealed = 0,
+
+	/**
+	 * Sentry that inherits interrupt status.
+	 */
+	CheriSealTypeSentryInheriting,
+
+	/**
+	 * Sentry that disables interrupts on calls.
+	 */
+	CheriSealTypeSentryDisabling,
+
+	/**
+	 * Sentry that enables interrupts on calls.
+	 */
+	CheriSealTypeSentryEnabling,
+
+	/**
+	 * Return sentry that disables interrupts on return
+	 */
+	CheriSealTypeReturnSentryDisabling,
+
+	/**
+	 * Return sentry that enables interrupts on return
+	 */
+	CheriSealTypeReturnSentryEnabling,
+
+	/**
+	 * Marker for the first sealing type that's valid for data capabilities.
+	 */
+	CheriSealTypeFirstDataSealingType = 9,
+
+	/**
+	 * The sealing type used for sealed export table entries.
+	 *
+	 * This is RTOS- and not CHERIoT-specific.
+	 */
+	CheriSealTypeSealedImportTableEntries = CheriSealTypeFirstDataSealingType,
+
+	/**
+	 * The compartment switcher has a sealing type for the trusted stack.
+	 *
+	 * This must be the second data sealing type so that we can also permit
+	 * the switcher to unseal sentries and export table entries.
+	 *
+	 * This is RTOS- and not CHERIoT-specific.
+	 */
+	CheriSealTypeSealedTrustedStacks,
+
+	/**
+	 * The allocator has a sealing type for the software sealing mechanism
+	 * with dynamically allocated objects.
+	 *
+	 * This is RTOS- and not CHERIoT-specific.
+	 */
+	CheriSealTypeAllocator,
+
+	/**
+	 * The loader reserves a sealing type for the software sealing
+	 * mechanism.  The permit-unseal capability for this is destroyed after
+	 * the loader has run, which guarantees that anything sealed with this
+	 * type was present in the original firmware image.  The token library
+	 * has the only permit-unseal capability for this type.
+	 *
+	 * This is RTOS- and not CHERIoT-specific.
+	 */
+	CheriSealTypeStaticToken,
+
+	/**
+	 * The first sealing key that is reserved for use by the allocator's
+	 * software sealing mechanism and used for static sealing types,
+	 *
+	 * Architecturally, this is the smallest non-interpreted sealing type.
+	 */
+	CheriSealTypeFirstStaticSoftware = 16,
+
+	/**
+	 * The first sealing key in the space that the allocator will
+	 * dynamically allocate for sealing types.
+	 *
+	 * This is RTOS- and not CHERIoT-specific.
+	 */
+	CheriSealTypeFirstDynamicSoftware = 0x1000000
+};
+
+/**
  * Checks that `ptr` is valid, unsealed, has at least `rawPermissions`, and has
  * at least `space` bytes after the current offset.
  *
diff --git a/sdk/include/cheri.hh b/sdk/include/cheri.hh
index 8e18db3..4663e17 100644
--- a/sdk/include/cheri.hh
+++ b/sdk/include/cheri.hh
@@ -22,64 +22,64 @@
 		 * Capability refers to global memory (this capability may be stored
 		 * anywhere).
 		 */
-		Global = 0,
+		Global = CheriPermissionGlobal,
 		/**
 		 * Global capabilities can be loaded through this capability.  Without
 		 *  this permission, any capability loaded via this capability will
 		 *  have `Global` and `LoadGlobal` removed.
 		 */
-		LoadGlobal = 1,
+		LoadGlobal = CheriPermissionLoadGlobal,
 		/**
 		 * Capability may be used to store.  Any store via a capability without
 		 * this permission will trap.
 		 */
-		Store = 2,
+		Store = CheriPermissionStore,
 		/**
 		 * Capabilities with store permission may be loaded through this
 		 * capability.  Without this, any loaded capability will have
 		 * `LoadMutable` and `Store` removed.
 		 */
-		LoadMutable = 3,
+		LoadMutable = CheriPermissionLoadMutable,
 		/**
 		 * This capability may be used to store capabilities that do not have
 		 * `Global` permission.
 		 */
-		StoreLocal = 4,
+		StoreLocal = CheriPermissionStoreLocal,
 		/**
 		 * This capability can be used to load.
 		 */
-		Load = 5,
+		Load = CheriPermissionLoad,
 		/**
 		 * Any load and store permissions on this capability convey the right to
 		 * load or store capabilities in addition to data.
 		 */
-		LoadStoreCapability = 6,
+		LoadStoreCapability = CheriPermissionLoadStoreCapability,
 		/**
 		 * If installed as the program counter capability, running code may
 		 * access privileged system registers.
 		 */
-		AccessSystemRegisters = 7,
+		AccessSystemRegisters = CheriPermissionAccessSystemRegisters,
 		/**
 		 * This capability may be used as a jump target and used to execute
 		 * instructions.
 		 */
-		Execute = 8,
+		Execute = CheriPermissionExecute,
 		/**
 		 * This capability may be used to unseal other capabilities.  The
 		 * 'address' range is in the sealing type namespace and not in the
 		 * memory namespace.
 		 */
-		Unseal = 9,
+		Unseal = CheriPermissionUnseal,
 		/**
 		 * This capability may be used to seal other capabilities.  The
 		 * 'address' range is in the sealing type namespace and not in the
 		 * memory namespace.
 		 */
-		Seal = 10,
+		Seal = CheriPermissionSeal,
 		/**
 		 * Software defined permission bit, no architectural meaning.
 		 */
-		User0 = 11
+		User0 = CheriPermissionUser0
 	};
 
 	/**
@@ -361,6 +361,30 @@
 	};
 
 	/**
+	 * Rounds `len` up to a CHERI representable length for the current
+	 * architecture.
+	 */
+	__always_inline inline size_t representable_length(size_t length)
+	{
+		return __builtin_cheri_round_representable_length(length);
+	}
+
+	/**
+	 * Returns the alignment mask required for a given length.
+	 */
+	__always_inline inline size_t representable_alignment_mask(size_t length)
+	{
+		return __builtin_cheri_representable_alignment_mask(length);
+	}
+
+	/// Can the range [base, base + size) be precisely covered by a capability?
+	inline bool is_precise_range(ptraddr_t base, size_t size)
+	{
+		return (base & ~representable_alignment_mask(size)) == 0 &&
+		       representable_length(size) == size;
+	}
+
+	/**
 	 * Helper class for accessing capability properties on pointers.
 	 */
 	template<typename T>
@@ -390,12 +414,12 @@
 		 */
 		class PropertyProxyBase
 		{
+			protected:
 			/**
 			 * The capability that this proxy refers to.
 			 */
 			Capability &cap;
 
-			protected:
 			/**
 			 * Replaces the underlying capability
 			 */
@@ -521,6 +545,68 @@
 				set(__builtin_cheri_bounds_set(ptr(), bounds));
 				return *this;
 			}
+
+			private:
+			BoundsProxy &set_inexact_at_most_slow(size_t bounds)
+			{
+				ptraddr_t newBaseAddress = this->cap.address();
+
+				// The number of bits in CHERIoT's capability encoding's
+				// mantissa.  This is part of the capability encoding and
+				// so, ideally, wouldn't be hard coded here.
+				static constexpr size_t MantissaBits = 9;
+
+				// The maximum possible representable length given the new
+				// base is a full mantissa width of 1s followed by 0s with
+				// its least significant 1 aligned to the least significant
+				// 1 in the base address.
+				size_t maximumLength = ((1 << MantissaBits) - 1)
+				                       << __builtin_ctz(newBaseAddress);
+
+				// Ensure that the requested length is representable by
+				// making sure that it fits within a mantissa width,
+				// rounding down by dropping any lower bits.  This might be
+				// excessive by up to one bit position, because the
+				// representable alignment mask is designed to work with the
+				// rounding-up inexact bounds setting instruction.  As a result,
+				// we might not return the largest possible representable
+				// length, but we won't return a wildly too small one, either.
+				size_t alignedLength =
+				  bounds & representable_alignment_mask(bounds);
+
+				// Select the smaller of those two lengths.
+				bounds = std::min<size_t>(alignedLength, maximumLength);
+				*this  = bounds;
+				return *this;
+			}
+
+			public:
+			/**
+			 * Set the bounds to `length` if `length` is representable with the
+			 * current alignment of `buffer`. If not, then select a smaller
+			 * `length` that is representable.  Unlike set_inexact(), the
+			 * resulting base will always be the current address; that is, there
+			 * will be no padding below the current address.
+			 *
+			 * The caller must call .length() on the resulting capability to
+			 * determine the imposed bounds.
+			 *
+			 * See is_precise_range().
+			 */
+			__always_inline BoundsProxy &set_inexact_at_most(size_t bounds)
+			{
+				// Just try to set the requested bounds, first.  If that works,
+				// there's no need for bit-twiddling at all.
+				Capability p = ptr();
+				p.bounds()   = bounds;
+				if (p.is_valid())
+				{
+					set(static_cast<T *>(p));
+					return *this;
+				}
+
+				return set_inexact_at_most_slow(bounds);
+			}
 		};
 
 		/**
@@ -1020,30 +1106,6 @@
 	};
 
 	/**
-	 * Rounds `len` up to a CHERI representable length for the current
-	 * architecture.
-	 */
-	__always_inline inline size_t representable_length(size_t length)
-	{
-		return __builtin_cheri_round_representable_length(length);
-	}
-
-	/**
-	 * Returns the alignment mask required for a given length.
-	 */
-	__always_inline inline size_t representable_alignment_mask(size_t length)
-	{
-		return __builtin_cheri_representable_alignment_mask(length);
-	}
-
-	/// Can the range [base, base + size) be precisely covered by a capability?
-	inline bool is_precise_range(ptraddr_t base, size_t size)
-	{
-		return (base & ~representable_alignment_mask(size)) == 0 &&
-		       representable_length(size) == size;
-	}
-
-	/**
 	 * Concept that matches pointers.
 	 */
 	template<typename T>
@@ -1169,50 +1231,53 @@
 		 * No exception. This value is passed to the error handler after a
 		 * forced unwind in a called compartment.
 		 */
-		None = 0,
+		None = CheriCauseCodeNone,
 		/**
 		 * Attempted to use a capability outside its bounds.
 		 */
-		BoundsViolation = 1,
+		BoundsViolation = CheriCauseCodeBoundsViolation,
 		/**
 		 * Attempted to use an untagged capability to authorize something.
 		 */
-		TagViolation = 2,
+		TagViolation = CheriCauseCodeTagViolation,
 		/**
 		 * Attempted to use a sealed capability to authorize something.
 		 */
-		SealViolation = 3,
+		SealViolation = CheriCauseCodeSealViolation,
 		/**
 		 * Attempted to jump to a capability without `Permission::Execute`.
 		 */
-		PermitExecuteViolation = 0x11,
+		PermitExecuteViolation = CheriCauseCodePermitExecuteViolation,
 		/**
 		 * Attempted to load via a capability without `Permission::Load`.
 		 */
-		PermitLoadViolation = 0x12,
+		PermitLoadViolation = CheriCauseCodePermitLoadViolation,
 		/**
 		 * Attempted to store via a capability without `Permission::Store`.
 		 */
-		PermitStoreViolation = 0x13,
+		PermitStoreViolation = CheriCauseCodePermitStoreViolation,
 		/**
 		 * Attempted to store a tagged capability via a capability without
 		 * `Permission::LoadStoreCapability`.
 		 */
-		PermitStoreCapabilityViolation = 0x15,
+		PermitStoreCapabilityViolation =
+		  CheriCauseCodePermitStoreCapabilityViolation,
 		/**
 		 * Attempted to store a tagged capability without `Permission::Global`
 		 * via capability without `Permission::StoreLocal`.
 		 */
-		PermitStoreLocalCapabilityViolation = 0x16,
+		PermitStoreLocalCapabilityViolation =
+		  CheriCauseCodePermitStoreLocalCapabilityViolation,
 		/**
 		 * Attempted to access a restricted CSR or SCR with PCC without
 		 * `Permission::AccessSystemRegisters`.
 		 */
-		PermitAccessSystemRegistersViolation = 0x18,
+		PermitAccessSystemRegistersViolation =
+		  CheriCauseCodePermitAccessSystemRegistersViolation,
 		/**
 		 * Used to represent a value that has no valid meaning in hardware.
 		 */
-		Invalid = -1
+		Invalid = CheriCauseCodeInvalid,
 	};
 
 	/**
@@ -1226,84 +1291,84 @@
 		/**
 		 * The zero register, which always contains the `NULL` capability.
 		 */
-		CZR = 0x0,
+		CZR = CheriRegisterNumberCzr,
 		/**
 		 * `$c1` / `$cra` used by the ABI as the return address.
 		 * Not preserved across calls.
 		 */
-		CRA = 0x1,
+		CRA = CheriRegisterNumberCra,
 		/**
 		 * `$c2` / `$csp` used by the ABI as the stack pointer.
 		 * Preserved across calls.
 		 */
-		CSP = 0x2,
+		CSP = CheriRegisterNumberCsp,
 		/**
 		 * `$c3` / `$cgp` used by the ABI as the global pointer.
 		 * Not allocatable by the compiler, set by the switcher on compartment
 		 * entry.
 		 */
-		CGP = 0x3,
+		CGP = CheriRegisterNumberCgp,
 		/**
 		 * `$c4` / `$ctp` used by the ABI as the thread pointer.
 		 * Currently unused by the compiler.
 		 * Not preserved across compartment calls.
 		 */
-		CTP = 0x4,
+		CTP = CheriRegisterNumberCtp,
 		/**
 		 * `$c5` / `$ct0` used by the ABI as temporary register.
 		 * Not preserved across calls.
 		 */
-		CT0 = 0x5,
+		CT0 = CheriRegisterNumberCT0,
 		/**
 		 * `$c6` / `$ct1` used by the ABI as temporary register.
 		 * Not preserved across calls.
 		 */
-		CT1 = 0x6,
+		CT1 = CheriRegisterNumberCT1,
 		/**
 		 * `$c7` / `$ct2` used by the ABI as temporary register.
 		 * Not preserved across calls.
 		 */
-		CT2 = 0x7,
+		CT2 = CheriRegisterNumberCT2,
 		/**
 		 * `$c8` / `$cs0` used by the ABI as a callee-saved register.
 		 * Preserved across calls.
 		 */
-		CS0 = 0x8,
+		CS0 = CheriRegisterNumberCS0,
 		/**
 		 * `$c9` / `$cs1` used by the ABI as a callee-saved register.
 		 * Preserved across calls.
 		 */
-		CS1 = 0x9,
+		CS1 = CheriRegisterNumberCS1,
 		/**
 		 * `$c10` / `$ca0` used by the ABI as an argument register.
 		 * Not preserved across calls.
 		 */
-		CA0 = 0xa,
+		CA0 = CheriRegisterNumberCA0,
 		/**
 		 * `$c11` / `$ca1` used by the ABI as an argument register.
 		 * Not preserved across calls.
 		 */
-		CA1 = 0xb,
+		CA1 = CheriRegisterNumberCA1,
 		/**
 		 * `$c12` / `$ca2` used by the ABI as an argument register.
 		 * Not preserved across calls.
 		 */
-		CA2 = 0xc,
+		CA2 = CheriRegisterNumberCA2,
 		/**
 		 * `$c13` / `$ca3` used by the ABI as an argument register.
 		 * Not preserved across calls.
 		 */
-		CA3 = 0xd,
+		CA3 = CheriRegisterNumberCA3,
 		/**
 		 * `$c14` / `$ca4` used by the ABI as an argument register.
 		 * Not preserved across calls.
 		 */
-		CA4 = 0xe,
+		CA4 = CheriRegisterNumberCA4,
 		/**
 		 * `$c15` / `$ca5` used by the ABI as an argument register.
 		 * Not preserved across calls.
 		 */
-		CA5 = 0xf,
+		CA5 = CheriRegisterNumberCA5,
 		/**
 		 * The Program Counter Capability.
 		 *
@@ -1311,7 +1376,7 @@
 		 * address is that of the faulting instruction. Also used for accessing
 		 * read-only globals.
 		 */
-		PCC = 0x20,
+		PCC = CheriRegisterNumberPcc,
 		/**
 		 * Machine-mode Trap Code Capability.
 		 *
@@ -1320,7 +1385,7 @@
 		 * same semantics as the RISC-V `mtvec` CSR. Only accessible when PCC
 		 * has the AccessSystemRegisters permission.
 		 */
-		MTCC = 0x3c,
+		MTCC = CheriRegisterNumberMtcc,
 		/**
 		 * Machine-mode Tusted Data Capability.
 		 *
@@ -1329,25 +1394,25 @@
 		 * permission.  Use by the RTOS to store a capability to the trusted
 		 * stack.
 		 */
-		MTDC = 0x3d,
+		MTDC = CheriRegisterNumberMtdc,
 		/**
 		 * Machine-mode Scratch Capability. Special capabiltiy register that
 		 * contains the sealing root capability on boot. Only accessible when
 		 * PCC has the AccessSystemRegisters permission.
 		 */
-		MScratchC = 0x3e,
+		MScratchC = CheriRegisterNumberMScratchC,
 		/**
 		 * Machine-mode Exception Program Counter Capability. Special capability
 		 * register that contains the PCC of the faulting instruction on trap.
 		 * The address has the same semantics as the RISC-V `mepc` CSR. Only
 		 * accessible when PCC has the AccessSystemRegisters permission.
 		 */
-		MEPCC = 0x3f,
+		MEPCC = CheriRegisterNumberMepcc,
 		/**
 		 * Indicates a value that is not used by the hardware to refer to a
 		 * register.
 		 */
-		Invalid = -1
+		Invalid = CheriRegisterNumberInvalid,
 	};
 
 	/**
diff --git a/sdk/include/compartment-macros.h b/sdk/include/compartment-macros.h
index ef0a024..ed728f1 100644
--- a/sdk/include/compartment-macros.h
+++ b/sdk/include/compartment-macros.h
@@ -142,16 +142,6 @@
  */
 #define DEVICE_EXISTS(x) defined(DEVICE_EXISTS_##x)
 
-#define SEALING_CAP()                                                          \
-	({                                                                         \
-		void *ret;                                                             \
-		__asm("1:	"                                                            \
-		      "	auipcc		%0, %%cheriot_compartment_hi(__sealingkey)\n"          \
-		      "	clc			%0, %%cheriot_compartment_lo_i(1b)(%0)\n"                \
-		      : "=C"(ret));                                                    \
-		ret;                                                                   \
-	})
-
 /**
  * Helper macro, used by `STATIC_SEALING_TYPE`.  Do not use this directly, it
  * exists to avoid error-prone copying and pasting of the mangled name for a
diff --git a/sdk/include/compartment.h b/sdk/include/compartment.h
index 6113c83..00079dc 100644
--- a/sdk/include/compartment.h
+++ b/sdk/include/compartment.h
@@ -4,52 +4,18 @@
 #pragma once
 #include <cdefs.h>
 #include <compartment-macros.h>
+#include <stddef.h>
 #include <stdint.h>
 
 #ifdef __cplusplus
 #	include <cheri.hh>
-template<typename T>
-static inline CHERI::Capability<T> compart_seal(T *in)
-{
-	void *key = SEALING_CAP();
-
-	return CHERI::Capability{in}.seal(key);
-}
-
-template<typename T>
-static inline CHERI::Capability<T> compart_unseal(T *in)
-{
-	void *key = SEALING_CAP();
-
-	return CHERI::Capability{in}.unseal(key);
-}
-
-template<typename T>
-static inline auto compart_unseal(void *in)
-{
-	return compart_unseal(static_cast<T *>(in));
-}
-#else
-#	include <cheri-builtins.h>
-static inline void *compart_seal(void *in)
-{
-	void *key = SEALING_CAP();
-
-	return cseal(in, key);
-}
-
-static inline void *compart_unseal(void *in)
-{
-	void *key = SEALING_CAP();
-
-	return cunseal(in, key);
-}
 #endif
 
 /**
  * State for error handlers to use.
  *
- * Note: This structure should have the same layout as the register-save area.
+ * Note: This structure should have the same layout as the register-save area
+ * (that is, the initial sequence of a TrustedStack, up through ca5, inclusive).
  */
 struct ErrorState
 {
diff --git a/sdk/include/fail-simulator-on-error.h b/sdk/include/fail-simulator-on-error.h
index d26dceb..cad8bd7 100644
--- a/sdk/include/fail-simulator-on-error.h
+++ b/sdk/include/fail-simulator-on-error.h
@@ -42,56 +42,36 @@
 {
 	if (mcause == priv::MCAUSE_CHERI)
 	{
+		// An unexpected error -- log it and end the simulation with error.
+		// Note: handle CZR differently as `get_register_value` will return a
+		// nullptr which we cannot dereference.
+
 		auto [exceptionCode, registerNumber] =
 		  CHERI::extract_cheri_mtval(mtval);
-		// The thread entry point is called with a NULL return address so the
-		// cret at the end of the entry point function will trap if it is
-		// reached. We don't want to treat this as an error but thankfully we
-		// detect it quite specifically by checking for all of:
-		// 1) CHERI cause is tag violation
-		// 2) faulting register is CRA
-		// 3) value of CRA is NULL
-		// 4) we've reached the top of the thread's stack
-		CHERI::Capability stackCapability{
-		  frame->get_register_value<CHERI::RegisterNumber::CSP>()};
-		CHERI::Capability returnCapability{
-		  frame->get_register_value<CHERI::RegisterNumber::CRA>()};
-		// The top of the stack is 16 bytes above the stack pointer on entry,
-		// to provide space for unwind lists and so on.
-		if (registerNumber == CHERI::RegisterNumber::CRA &&
-		    returnCapability.address() == 0 &&
-		    exceptionCode == CHERI::CauseCode::TagViolation &&
-		    (stackCapability.top() - 16) == stackCapability.address())
-		{
-			// looks like thread exit -- just log it then ForceUnwind
-			DebugErrorHandler::log(
-			  "Thread exit CSP={}, PCC={}", stackCapability, frame->pcc);
-		}
-		else
-		{
-			// An unexpected error -- log it and end the simulation
-			// with error.  Note: handle CZR differently as
-			// `get_register_value` will return a nullptr which we
-			// cannot dereference.
-			DebugErrorHandler::log(
-			  "{} error at {} (return address: {}), with capability register "
-			  "{}: {}",
-			  exceptionCode,
-			  frame->pcc,
-			  frame->get_register_value<CHERI::RegisterNumber::CRA>(),
-			  registerNumber,
-			  registerNumber == CHERI::RegisterNumber::CZR
-			    ? nullptr
-			    : *frame->get_register_value(registerNumber));
-			simulation_exit(1);
-		}
+
+		DebugErrorHandler::log(
+		  "{} error at {} (return address: {}), with capability register "
+		  "{}: {}",
+		  exceptionCode,
+		  frame->pcc,
+		  frame->get_register_value<CHERI::RegisterNumber::CRA>(),
+		  registerNumber,
+		  registerNumber == CHERI::RegisterNumber::CZR
+		    ? nullptr
+		    : *frame->get_register_value(registerNumber));
 	}
 	else
 	{
 		// other error (e.g. __builtin_trap causes ReservedInstruciton)
 		// log and end simulation with error.
 		DebugErrorHandler::log("Unhandled error {} at {}", mcause, frame->pcc);
-		simulation_exit(1);
 	}
+
+	simulation_exit(1);
+	/*
+	 * 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.
+	 */
 	return ErrorRecoveryBehaviour::ForceUnwind;
 }
diff --git a/sdk/include/platform/sunburst/platform-uart.hh b/sdk/include/platform/sunburst/platform-uart.hh
index a9acf85..09f217d 100644
--- a/sdk/include/platform/sunburst/platform-uart.hh
+++ b/sdk/include/platform/sunburst/platform-uart.hh
@@ -4,6 +4,10 @@
 #include_next <platform-uart.hh>
 #pragma pop_macro("CHERIOT_PLATFORM_CUSTOM_UART")
 
+#ifndef DEFAULT_UART_BAUD_RATE
+#	define DEFAULT_UART_BAUD_RATE 921'600
+#endif
+
 /**
  * OpenTitan UART
  *
@@ -128,6 +132,23 @@
 		ControlTransmitEnable = 1 << 0,
 	};
 
+	/// Status Register Fields
+	enum : uint32_t
+	{
+		/// Receive FIFO is empty.
+		StatusReceiveEmpty = 1 << 5,
+		/// Receive logic is idle.
+		StatusReceiveIdle = 1 << 4,
+		/// Transmit FIFO is empty and all bits have been transmitted.
+		StatusTransmitIdle = 1 << 3,
+		/// Transmit FIFO is empty; transmission may still be occurring.
+		StatusTransmitEmpty = 1 << 2,
+		/// Receive FIFO is full.
+		StatusReceiveFull = 1 << 1,
+		/// Transmit FIFO is full.
+		StatusTransmitFull = 1 << 0,
+	};
+
 	/// The encoding for different transmit watermark levels.
 	enum class TransmitWatermark
 	{
@@ -181,8 +202,8 @@
 	/// Clears the contents of the receive and transmit FIFOs.
 	void fifos_clear() volatile
 	{
-		fifoCtrl = (fifoCtrl & ~0b11) | FifoControlTransmitReset |
-		           FifoControlReceiveReset;
+		fifoCtrl =
+		  fifoCtrl | FifoControlTransmitReset | FifoControlReceiveReset;
 	}
 
 	/**
@@ -204,7 +225,7 @@
 	 */
 	void receive_watermark(ReceiveWatermark level) volatile
 	{
-		fifoCtrl = static_cast<uint32_t>(level) << 5 | (fifoCtrl & 0b11100011);
+		fifoCtrl = static_cast<uint32_t>(level) << 2 | (fifoCtrl & 0b11100011);
 	}
 
 	/// Enable the given interrupt.
@@ -219,7 +240,7 @@
 		interruptEnable = interruptEnable & ~interrupt;
 	}
 
-	void init(unsigned baudRate = 115'200) volatile
+	void init(unsigned baudRate = DEFAULT_UART_BAUD_RATE) volatile
 	{
 		// Nco = 2^20 * baud rate / cpu frequency
 		const uint32_t Nco =
@@ -240,12 +261,12 @@
 
 	bool can_write() volatile
 	{
-		return transmit_fifo_level() < 32;
+		return !(status & StatusTransmitFull);
 	}
 
 	bool can_read() volatile
 	{
-		return receive_fifo_level() > 0;
+		return !(status & StatusReceiveEmpty);
 	}
 
 	/**
diff --git a/sdk/include/platform/sunburst/platform-usbdev.hh b/sdk/include/platform/sunburst/platform-usbdev.hh
new file mode 100644
index 0000000..1a279bc
--- /dev/null
+++ b/sdk/include/platform/sunburst/platform-usbdev.hh
@@ -0,0 +1,681 @@
+// SPDX-FileCopyrightText: CHERIoT contributors
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma once
+#include <cdefs.h>
+#include <optional>
+#include <stdint.h>
+#include <utils.hh>
+
+/**
+ * A driver for OpenTitan USB Device, which is used in the Sonata system.
+ *
+ * This peripheral's source and documentation can be found at:
+ * https://github.com/lowRISC/opentitan/tree/ab878b5d3578939a04db72d4ed966a56a869b2ed/hw/ip/usbdev
+ *
+ * With rendered register documentation served at:
+ * https://opentitan.org/book/hw/ip/usbdev/doc/registers.html
+ *
+ * An incredibly brief overview of how the USB device and it's buffers:
+ * Data packet ingress and egress goes via a pool of 64 byte buffers living
+ * in a 2kB SRAM packet buffer, which is accessable as a large MMIO region.
+ * The software manages these buffers using buffer IDs as references.
+ * IDs are pushed or popped into different FIFOs by either the software or
+ * device depending on whether they contain a packet to be sent, are available
+ * for a packet to be received into them, or contain a packet that has been
+ * received.
+ *
+ * See https://opentitan.org/book/hw/ip/usbdev/doc/programmers_guide.html for
+ * more information.
+ */
+class OpenTitanUsbdev : private utils::NoCopyNoMove
+{
+	public:
+	/// Supported sizes for the USB Device.
+	static constexpr uint8_t MaxPacketLength = 64u;
+	static constexpr uint8_t BufferCount     = 32u;
+	static constexpr uint8_t MaxEndpoints    = 12u;
+
+	/**
+	 * The offset from the start of the USB Device MMIO region at which
+	 * packet buffer memory begins.
+	 */
+	static constexpr uint32_t BufferStartAddress = 0x800u;
+
+	/// Device Registers
+	uint32_t interruptState;
+	uint32_t interruptEnable;
+	uint32_t interruptTest;
+	uint32_t alertTest;
+	uint32_t usbControl;
+	uint32_t endpointOutEnable;
+	uint32_t endpointInEnable;
+	uint32_t usbStatus;
+	uint32_t availableOutBuffer;
+	uint32_t availableSetupBuffer;
+	uint32_t receiveBuffer;
+	/// Register to enable receive SETUP transactions
+	uint32_t receiveEnableSetup;
+	/// Register to enable receive OUT transactions
+	uint32_t receiveEnableOut;
+	/// Register to set NAK (Not/Negated Acknowledge) after OUT transactions
+	uint32_t setNotAcknowledgeOut;
+	/// Register showing ACK receival to indicate a successful IN send
+	uint32_t inSent;
+	/// Registers for controlling the stalling of OUT and IN endpoints
+	uint32_t outStall;
+	uint32_t inStall;
+	/**
+	 * IN transaction configuration registers. There is one register per
+	 * endpoint for the USB device.
+	 */
+	uint32_t configIn[MaxEndpoints];
+	/**
+	 * Registers for configuring which endpoints should be treated as
+	 * isochronous endpoints. This means that if the corresponding bit is set,
+	 * then that no handshake packet will be sent for an OUT/IN transaction on
+	 * that endpoint.
+	 */
+	uint32_t outIsochronous;
+	uint32_t inIsochronous;
+	/// Registers for configuring if endpoints data toggle on transactions
+	uint32_t outDataToggle;
+	uint32_t inDataToggle;
+
+	private:
+	/**
+	 * Registers to sense/drive the USB PHY pins. That is, these registers can
+	 * be used to respectively read out the state of the USB device inputs and
+	 * outputs, or to control the inputs and outputs from software. These
+	 * registers are kept private as they are intended to be used for debugging
+	 * purposes or during chip testing, and not in actual software.
+	 */
+	[[maybe_unused]] uint32_t phyPinsSense;
+	[[maybe_unused]] uint32_t phyPinsDrive;
+
+	public:
+	/// Config register for the USB PHY pins.
+	uint32_t phyConfig;
+
+	/// Interrupt definitions for OpenTitan's USB Device.
+	enum class UsbdevInterrupt : uint32_t
+	{
+		/// Interrupt asserted whilst the receive FIFO (buffer) is not empty.
+		PacketReceived = 1u << 0,
+		/**
+		 * Interrupt asserted when a packet was sent as part of an IN
+		 * transaction, but not cleared from the `inSent` register.
+		 */
+		PacketSent = 1u << 1,
+		/**
+		 * Interrupt raised when VBUS (power supply) is lost, i.e. the link to
+		 * the USB host controller has been disconnected.
+		 */
+		Disconnected = 1u << 2,
+		/**
+		 * Interrupt raised when the link is active, but a Start of Frame (SOF)
+		 * packet has not been received within a given timeout threshold, which
+		 * is set to 4.096 milliseconds.
+		 */
+		HostLost = 1u << 3,
+		/**
+		 * Interrupt raised when a Bus Reset condition is indicated on the link
+		 * by the link being held in an SE0 state (Single Ended Zero, both lines
+		 * being pulled low) for longer than 3 microseconds.
+		 */
+		LinkReset = 1u << 4,
+		/**
+		 * Interrupt raised when the link has entered the suspend state, due to
+		 * being idle for more than 3 milliseconds.
+		 */
+		LinkSuspend = 1u << 5,
+		///  Interrupt raised on link transition from suspended to non-idle.
+		LinkResume = 1u << 6,
+		/// Interrupt asserted whilst the Available OUT buffer is empty.
+		AvailableOutEmpty = 1u << 7,
+		///  Interrupt asserted whilst the Receive buffer is full.
+		ReceiveFull = 1u << 8,
+		/**
+		 * Interrupt raised when the Available OUT buffer or the Available SETUP
+		 * buffer overflows.
+		 */
+		AvailableBufferOverflow = 1u << 9,
+		///  Interrupt raised when an error occurs during an IN transaction.
+		LinkInError = 1u << 10,
+		/**
+		 * Interrupt raised when a CRC (cyclic redundancy check) error occurs on
+		 * a received packet; i.e. there was an error in transmission.
+		 */
+		RedundancyCheckError = 1u << 11,
+		///  Interrupt raised when an invalid Packet Identifier is received.
+		PacketIdentifierError = 1u << 12,
+		///  Interrupt raised when a bit stuffing violation is detected.
+		BitstuffingError = 1u << 13,
+		/**
+		 * Interrupt raised when the USB frame number is updated with a valid
+		 * SOF (Start of Frame) packet.
+		 */
+		FrameUpdated = 1u << 14,
+		///  Interrupt raised when VBUS (power supply) is detected.
+		Powered = 1u << 15,
+		///  Interrupt raised when an error occurs during an OUT transaction.
+		LinkOutError = 1u << 16,
+		///  Interrupt asserted whilst the Available SETUP buffer is empty.
+		AvailableSetupEmpty = 1u << 17,
+	};
+
+	/**
+	 * Definitions of fields (and their locations) for the USB Control register
+	 * (offset 0x10).
+	 *
+	 * https://opentitan.org/book/hw/ip/usbdev/doc/registers.html#usbctrl
+	 */
+	enum class UsbControlField : uint32_t
+	{
+		Enable           = 1u << 0,
+		ResumeLinkActive = 1u << 1,
+		DeviceAddress    = 0x7Fu << 16,
+	};
+
+	/**
+	 * Definitions of fields (and their locations) for the USB Status register
+	 * (offset 0x1c).
+	 *
+	 * https://opentitan.org/book/hw/ip/usbdev/doc/registers.html#usbstat
+	 */
+	enum class UsbStatusField : uint32_t
+	{
+		Frame               = 0x7FFu << 0,
+		HostLost            = 1u << 11,
+		LinkState           = 0x7u << 12,
+		Sense               = 1u << 15,
+		AvailableOutDepth   = 0xFu << 16,
+		AvailableSetupDepth = 0x7u << 20,
+		AvailableOutFull    = 1u << 23,
+		ReceiveDepth        = 0xFu << 24,
+		AvailableSetupFull  = 1u << 30,
+		ReceiveEmpty        = 1u << 31,
+	};
+
+	/**
+	 * Definitions of fields (and their locations) for the Receive FIFO
+	 * buffer register (offset 0x28).
+	 *
+	 * https://opentitan.org/book/hw/ip/usbdev/doc/registers.html#rxfifo
+	 */
+	enum class ReceiveBufferField : uint32_t
+	{
+		BufferId   = 0x1Fu << 0,
+		Size       = 0x7Fu << 8,
+		Setup      = 1u << 19,
+		EndpointId = 0xFu << 20,
+	};
+
+	/**
+	 * Definitions of fields (and their locations) for a Config In register
+	 * (where there is one such register for each endpoint). These are
+	 * the registers with offsets 0x44 up to (and not including) 0x74.
+	 *
+	 * https://opentitan.org/book/hw/ip/usbdev/doc/registers.html#configin
+	 */
+	enum class ConfigInField : uint32_t
+	{
+		BufferId = 0x1Fu << 0,
+		Size     = 0x7Fu << 8,
+		Sending  = 1u << 29,
+		Pending  = 1u << 30,
+		Ready    = 1u << 31,
+	};
+
+	/**
+	 * Definitions of fields (and their locations) for the PHY Config
+	 * Register (offset 0x8c).
+	 *
+	 * https://opentitan.org/book/hw/ip/usbdev/doc/registers.html#phy_config
+	 */
+	enum class PhyConfigField : uint32_t
+	{
+		UseDifferentialReceiver = 1u << 0,
+		//  Other PHY Configuration fields are omitted.
+	};
+
+	/**
+	 * Ensure that the Available OUT and Available SETUP buffers are kept
+	 * supplied with buffers for packet reception.
+	 *
+	 * @param bufferBitmap A bitmap of the buffers that are not currently
+	 * committed (where 1 corresponds to not in use).
+	 * @returns The updated bitmap after supplying buffers.
+	 */
+	[[nodiscard]] uint64_t supply_buffers(uint64_t bufferBitmap) volatile
+	{
+		constexpr uint32_t SetupFullBit =
+		  uint32_t(UsbStatusField::AvailableSetupFull);
+		constexpr uint32_t OutFullBit =
+		  uint32_t(UsbStatusField::AvailableOutFull);
+
+		for (uint8_t index = 0; index < BufferCount; index++)
+		{
+			const uint32_t Buffer = (1u << index);
+			if (!(bufferBitmap & Buffer))
+			{
+				continue; // Skip buffers that are not available
+			}
+
+			// If a buffer is available, and either Available SETUP or OUT are
+			// not yet full, then commit that buffer and mark it as in use.
+			if (usbStatus & SetupFullBit)
+			{
+				if (usbStatus & OutFullBit)
+				{
+					break; // Both are full - stop trying to supply buffers.
+				}
+				availableOutBuffer = index;
+			}
+			else
+			{
+				availableSetupBuffer = index;
+			}
+			bufferBitmap &= ~Buffer;
+		}
+		return bufferBitmap;
+	}
+
+	/**
+	 * Enable a specified interrupt / interrupts.
+	 */
+	void interrupt_enable(UsbdevInterrupt interrupt) volatile
+	{
+		interruptEnable = interruptEnable | uint32_t(interrupt);
+	}
+
+	/**
+	 * Disable a specified interrupt / interrupts.
+	 */
+	void interrupt_disable(UsbdevInterrupt interrupt) volatile
+	{
+		interruptEnable = interruptEnable & ~uint32_t(interrupt);
+	}
+
+	/**
+	 * Initialise the USB device, ensuring that packet buffers are available for
+	 * reception and that the PHY has been appropriately configured. Note that
+	 * at this stage, endpoints have not been configured and the device has not
+	 * been connected to the USB.
+	 *
+	 * @param bufferBitmap An out-parameter, to initialise a bitmap of the
+	 * buffers that are not currently commited (1 corresponds to not in use).
+	 *
+	 * @returns 0 if initialisation is sucessful, and non-zero otherwise.
+	 */
+	[[nodiscard]] int init(uint64_t &bufferBitmap) volatile
+	{
+		bufferBitmap = supply_buffers((uint64_t(1u) << BufferCount) - 1u);
+		phyConfig    = uint32_t(PhyConfigField::UseDifferentialReceiver);
+		return 0;
+	}
+
+	/**
+	 * Set up the configuration of an OUT endpoint for the USB device.
+	 *
+	 * @param endpointId The ID of the OUT endpoint to configure.
+	 * @param enabled Whether the OUT endpoint should be enabled or not.
+	 * @param setup Whether SETUP transactions should be enabled for the
+	 * endpoint.
+	 * @param isochronous Whether the endpoint should operate isochronously or
+	 * non-isochronously.
+	 *
+	 * @returns 0 if configuration is successful, and non-zero otherwise.
+	 */
+	[[nodiscard]] int out_endpoint_configure(uint8_t endpointId,
+	                                         bool    enabled,
+	                                         bool    setup,
+	                                         bool    isochronous) volatile
+	{
+		if (endpointId >= MaxEndpoints)
+		{
+			return -1;
+		}
+		const uint32_t Mask = 1u << endpointId;
+		endpointOutEnable = (endpointOutEnable & ~Mask) | (enabled ? Mask : 0u);
+		outIsochronous = (outIsochronous & ~Mask) | (isochronous ? Mask : 0u);
+		receiveEnableSetup = (receiveEnableSetup & ~Mask) | (setup ? Mask : 0u);
+		receiveEnableOut   = (receiveEnableOut & ~Mask) | (enabled ? Mask : 0u);
+		return 0;
+	}
+
+	/**
+	 * Set up the configuration of an IN endpoint for the USB device.
+	 *
+	 * @param endpointId The ID of the IN endpoint to configure
+	 * @param enabled Whether the IN endpoint should be enabled or not.
+	 * @param isochronous Whether the endpoint should operate isochronously or
+	 * non-isochronously.
+	 *
+	 * @returns 0 if configuration is successful, and non-zero otherwise.
+	 */
+	[[nodiscard]] int in_endpoint_configure(uint8_t endpointId,
+	                                        bool    enabled,
+	                                        bool    isochronous) volatile
+	{
+		if (endpointId >= MaxEndpoints)
+		{
+			return -1;
+		}
+		const uint32_t Mask = 1u << endpointId;
+		endpointInEnable = (endpointInEnable & ~Mask) | (enabled ? Mask : 0u);
+		inIsochronous    = (inIsochronous & ~Mask) | (isochronous ? Mask : 0u);
+		return 0;
+	}
+
+	/**
+	 * Set the STALL state of a specified endpoint pair (both IN and OUT).
+	 *
+	 * @param endpointId The ID of the endpoint pair to modify.
+	 * @param stalling Whether the endpoints are stalling or not.
+	 *
+	 * @returns 0 if successful, and non-zero otherwise.
+	 */
+	[[nodiscard]] int endpoint_stalling_set(uint8_t endpointId,
+	                                        bool    stalling) volatile
+	{
+		if (endpointId >= MaxEndpoints)
+		{
+			return -1;
+		}
+		const uint32_t Mask = 1u << endpointId;
+		outStall            = (outStall & ~Mask) | (stalling ? Mask : 0u);
+		inStall             = (inStall & ~Mask) | (stalling ? Mask : 0u);
+		return 0;
+	}
+
+	/**
+	 * Connect the device to the USB, indicating its presence to the USB host
+	 * controller. Endpoints must already have been configured at this point
+	 * because traffic may be received imminently.
+	 *
+	 * @returns 0 if successful, and non-zero otherwise.
+	 * @returns -1 if endpoint 0 isn't enabled,
+	 *             suggesting the endpoints haven't been configured.
+	 */
+	[[nodiscard]] int connect() volatile
+	{
+		if (!(endpointInEnable & endpointOutEnable & 0b1))
+		{
+			return -1;
+		}
+		usbControl = usbControl | uint32_t(UsbControlField::Enable);
+		return 0;
+	}
+
+	/**
+	 * Disconnect the device from the USB.
+	 *
+	 * @returns 0 if successful, and non-zero otherwise.
+	 */
+	void disconnect() volatile
+	{
+		usbControl = usbControl & ~uint32_t(UsbControlField::Enable);
+	}
+
+	/**
+	 * Check whether the USB device is connected (i.e. pullup enabled).
+	 *
+	 * @returns True to indicate it is connected, and false otherwise.
+	 */
+	[[nodiscard]] bool connected() volatile
+	{
+		return (usbControl & uint32_t(UsbControlField::Enable));
+	}
+
+	/**
+	 * Set the device address on the USB; this address will have been supplied
+	 * by the USB host controller in the standard `SET_ADDRESS` Control
+	 * Transfer.
+	 *
+	 * @param address The device address to set on the USB.
+	 *
+	 * @returns 0 if successful, and non-zero otherwise.
+	 */
+	[[nodiscard]] int device_address_set(uint8_t address) volatile
+	{
+		if (address >= 0x80)
+		{
+			return -1; // Device addresses are only 7 bits long.
+		}
+		constexpr uint32_t Mask = uint32_t(UsbControlField::DeviceAddress);
+		usbControl              = (usbControl & ~Mask) | (address << 16);
+		return 0;
+	}
+
+	/**
+	 * Check and retrieve the endpoint and buffer numbers of a
+	 * recently-collected IN data packet. The caller is responsible for reusing
+	 * or releasing the buffer.
+	 *
+	 * @param endpointId An out-parameter, to which the ID of the endpoint for
+	 * a recently-collected IN data packet will be written.
+	 * @param bufferId An out-parameter, to which the ID of the buffer for a
+	 * recently-collected IN data packet will be written.
+	 *
+	 * @returns 0 if successful, and non-zero otherwise.
+	 */
+	[[nodiscard]] int retrieve_collected_packet(uint8_t &endpointId,
+	                                            uint8_t &bufferId) volatile
+	{
+		constexpr uint32_t BufferIdMask = uint32_t(ConfigInField::BufferId);
+		uint32_t           sent         = inSent;
+
+		// Clear the first encountered packet sent indication.
+		for (endpointId = 0; endpointId < MaxEndpoints; endpointId++)
+		{
+			const uint32_t EndpointBit = 1u << endpointId;
+			if (sent & EndpointBit)
+			{
+				// Clear the `in_sent` bit for this specific endpoint, and
+				// indicate which buffer has been released.
+				inSent   = EndpointBit;
+				bufferId = (configIn[endpointId] & BufferIdMask);
+				return 0;
+			}
+		}
+
+		// If no packet sent indications were found, then fail.
+		return -1;
+	}
+
+	/**
+	 * Present a packet on the specified IN endpoint for collection by the USB
+	 * host controller.
+	 *
+	 * @param bufferId The buffer to use to store the packet.
+	 * @param endpointId The IN endpoint used to send the packet.
+	 * @param data The packet to be transmitted.
+	 * @param size The size of the packet.
+	 */
+	void packet_send(uint8_t         bufferId,
+	                 uint8_t         endpointId,
+	                 const uint32_t *data,
+	                 uint8_t         size) volatile
+	{
+		// Transmission of zero length packets is common over USB
+		if (size > 0)
+		{
+			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;
+	}
+
+	/// The information associated with a received packet
+	struct ReceiveBufferInfo
+	{
+		uint32_t info;
+		/// The endpoint ID the received packet was received on
+		constexpr uint8_t endpoint_id()
+		{
+			return (info & uint32_t(ReceiveBufferField::EndpointId)) >> 20;
+		}
+		/// The size of the received packet
+		constexpr uint16_t size()
+		{
+			return (info & uint32_t(ReceiveBufferField::Size)) >> 8;
+		}
+		/// Whether the received packet was a setup packet
+		constexpr bool is_setup()
+		{
+			return (info & 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;
+		}
+	};
+
+	/**
+	 * If a packet has been received, removes the packet's buffer from the
+	 * receive FIFO giving it's information and ownership to the user.
+	 *
+	 * `packet_data_get` can be used to retrieve the packet's data.
+	 *
+	 * @returns Information about the received packet, if a packet had been
+	 * received.
+	 */
+	[[nodiscard]] std::optional<ReceiveBufferInfo> packet_take() volatile
+	{
+		if (!(usbStatus & uint32_t(UsbStatusField::ReceiveDepth)))
+		{
+			return {}; // No packets received
+		}
+		return ReceiveBufferInfo{receiveBuffer};
+	}
+
+	/**
+	 * Retrieves the data from a buffer containing a received packet.
+	 *
+	 * @param destination A destination buffer to read the packet's data into.
+	 */
+	void packet_data_get(ReceiveBufferInfo bufferInfo,
+	                     uint32_t         *destination) volatile
+	{
+		const auto [id, size] =
+		  std::pair{bufferInfo.buffer_id(), bufferInfo.size()};
+		// Reception of Zero Length Packets occurs in the Status Stage of IN
+		// Control Transfers.
+		if (size > 0)
+		{
+			usbdev_transfer(destination, buffer(id), size, false);
+		}
+	}
+
+	private:
+	/**
+	 * Return a pointer to the given offset within the USB device register
+	 * space; this is used to access the packet buffer memory.
+	 *
+	 * @param bufferId The buffer number to access the packet buffer memory for
+	 *
+	 * @returns A pointer to the buffer's memory.
+	 */
+	uint32_t *buffer(uint8_t bufferId) volatile
+	{
+		const uint32_t Offset = BufferStartAddress + bufferId * MaxPacketLength;
+		const uintptr_t Address = reinterpret_cast<uintptr_t>(this) + Offset;
+		return const_cast<uint32_t *>(reinterpret_cast<uint32_t *>(Address));
+	}
+
+	/**
+	 * Perform a transfer to or from packet buffer memory. This function is
+	 * hand-optimised to perform a faster, unrolled, word-based data transfer
+	 * for efficiency.
+	 *
+	 * @param destination A pointer to transfer the source data to.
+	 * @param source A pointer to the data to be transferred.
+	 * @param size The size of the data pointed to by `source`.
+	 * @param toDevice True if the transfer is to the device (e.g. when sending
+	 * a packet), and False if not (e.g. when receiving a packet).
+	 */
+	static void usbdev_transfer(uint32_t       *destination,
+	                            const uint32_t *source,
+	                            uint8_t         size,
+	                            bool            toDevice)
+	{
+		// Unroll word transfer. Each word transfer is 4 bytes, so we must round
+		// to the closest multiple of (4 * words) when unrolling.
+		constexpr uint8_t  UnrollFactor = 4u;
+		constexpr uint32_t UnrollMask   = (UnrollFactor * 4u) - 1;
+
+		// Round down to the previous multiple for unrolling
+		const uint32_t  UnrollSize = (size & ~UnrollMask);
+		const uint32_t *sourceEnd  = reinterpret_cast<uint32_t *>(
+          reinterpret_cast<uintptr_t>(source) + UnrollSize);
+
+		// This is manulally unrolled for two reasons:
+		// 1. We can't do partial writes to the USB packet buffer,
+		//    which memcpy will attempt and causes a BUS fault.
+		// 2. In the sonata system at the time of writing,
+		//    the core clock is 40MHz compared to the USB device's 48MHz.
+		//    This approach was found to be significantly faster than when
+		//    left to compiler to optimisation.
+		//
+		// Ensure the unrolling here matches `UnrollFactor`.
+		while (source < sourceEnd)
+		{
+			destination[0] = source[0];
+			destination[1] = source[1];
+			destination[2] = source[2];
+			destination[3] = source[3];
+			destination += UnrollFactor;
+			source += UnrollFactor;
+		}
+
+		// Copy the remaining whole words.
+		for (size &= UnrollMask; size >= UnrollFactor; size -= UnrollFactor)
+		{
+			*destination++ = *source++;
+		}
+		if (size == 0)
+		{
+			return;
+		}
+
+		// Copy trailing tail bytes, as USBDEV only supports 32-bit accesses.
+		if (toDevice)
+		{
+			// Collect final bytes into a word.
+			const volatile uint8_t *trailingBytes =
+			  reinterpret_cast<const volatile uint8_t *>(source);
+			uint32_t partialWord = trailingBytes[0];
+			if (size > 1)
+			{
+				partialWord |= trailingBytes[1] << 8;
+			}
+			if (size > 2)
+			{
+				partialWord |= trailingBytes[2] << 16;
+			}
+			// Write the final word to the device.
+			*destination = partialWord;
+		}
+		else
+		{
+			volatile uint8_t *destinationBytes =
+			  reinterpret_cast<volatile uint8_t *>(destination);
+			// Collect the final word from the device.
+			const uint32_t TrailingBytes = *source;
+			// Unpack it into final bytes.
+			destinationBytes[0] = static_cast<uint8_t>(TrailingBytes);
+			if (size > 1)
+			{
+				destinationBytes[1] = static_cast<uint8_t>(TrailingBytes >> 8);
+			}
+			if (size > 2)
+			{
+				destinationBytes[2] = static_cast<uint8_t>(TrailingBytes >> 16);
+			}
+		}
+	}
+};
diff --git a/sdk/include/priv/riscv.h b/sdk/include/priv/riscv.h
index d5c616e..d8cc51c 100644
--- a/sdk/include/priv/riscv.h
+++ b/sdk/include/priv/riscv.h
@@ -75,6 +75,7 @@
 	constexpr size_t MCAUSE_LOAD_PAGE_FAULT     = 13;
 	constexpr size_t MCAUSE_STORE_PAGE_FAULT    = 15;
 	constexpr size_t MCAUSE_THREAD_EXIT         = 24;
+	constexpr size_t MCAUSE_THREAD_INTERRUPT    = 25;
 	constexpr size_t MCAUSE_CHERI               = 28;
 
 	constexpr size_t MSTATUS_UIE = (1 << 0);
diff --git a/sdk/include/queue.h b/sdk/include/queue.h
index dc16e72..68743da 100644
--- a/sdk/include/queue.h
+++ b/sdk/include/queue.h
@@ -31,50 +31,71 @@
 #include <timeout.h>
 
 /**
- * A handle to a queue endpoint.
+ * Structure representing a queue.  This structure represents the queue
+ * metadata, the buffer is stored at the end.
  *
- * Dropping permissions can make this a receive-only or a send-only handle.
+ * A queue is a ring buffer of fixed-sized elements with a producer and consumer
+ * counter.
  */
-struct QueueHandle
+struct MessageQueue
 {
 	/**
-	 * The size of one element in this queue.
+	 * The size of one element in this queue.  This should not be modified after
+	 * construction.
 	 */
 	size_t elementSize;
 	/**
-	 * The size of the queue.
+	 * The size of the queue.  This should not be modified after construction.
 	 */
 	size_t queueSize;
 	/**
-	 * The buffer used for storing queue elements.
-	 */
-	void *buffer;
-	/**
 	 * The producer counter.
 	 */
-	_Atomic(uint32_t) *producer;
+	_Atomic(uint32_t) producer;
 	/**
 	 * The consumer counter.
 	 */
-	_Atomic(uint32_t) *consumer;
+	_Atomic(uint32_t) consumer;
+#ifdef __cplusplus
+	MessageQueue(size_t elementSize, size_t queueSize)
+	  : elementSize(elementSize), queueSize(queueSize)
+	{
+	}
+#endif
 };
 
+_Static_assert(sizeof(struct MessageQueue) % sizeof(void *) == 0,
+               "MessageQueue structure must end correctly aligned for storing "
+               "capabilities.");
+
 __BEGIN_DECLS
 
 /**
+ * Returns the allocation size needed for a queue with the specified number and
+ * size of elements.  This can be used to statically allocate queues.
+ *
+ * Returns the allocation size on success, or `-EINVAL` if the arguments would
+ * cause an overflow.
+ */
+ssize_t __cheri_libcall queue_allocation_size(size_t elementSize,
+                                              size_t elementCount);
+
+/**
  * Allocates space for a queue using `heapCapability` and stores a handle to it
- * via `outQueue`.  The underlying allocation (which is necessary to free the
- * queue) is returned via `outAllocation`.
+ * via `outQueue`.
  *
  * The queue is has space for `elementCount` entries.  Each entry is a fixed
  * size, `elementSize` bytes.
+ *
+ * Returns 0 on success, `-ENOMEM` on allocation failure, and `-EINVAL` if the
+ * arguments are invalid (for example, if the requested number of elements
+ * multiplied by the element size would overflow).
  */
-int __cheri_libcall queue_create(Timeout            *timeout,
-                                 struct SObjStruct  *heapCapability,
-                                 struct QueueHandle *outQueue,
-                                 void              **outAllocation,
-                                 size_t              elementSize,
-                                 size_t              elementCount);
+int __cheri_libcall queue_create(Timeout              *timeout,
+                                 struct SObjStruct    *heapCapability,
+                                 struct MessageQueue **outQueue,
+                                 size_t                elementSize,
+                                 size_t                elementCount);
 
 /**
  * Destroys a queue. This wakes up all threads waiting to produce or consume,
@@ -87,29 +108,8 @@
  * Returns 0 on success. On failure, returns `-EPERM` if the queue handle is
  * restricted (see comment above).
  */
-int __cheri_libcall queue_destroy(struct SObjStruct  *heapCapability,
-                                  struct QueueHandle *handle);
-
-/**
- * Convert a queue handle returned from `queue_create` into one that can be
- * used *only* for receiving.
- *
- * Note: This is primarily defence in depth.  A malicious holder of this queue
- * handle can still set the consumer counter to invalid values.
- */
-struct QueueHandle __cheri_libcall
-queue_make_receive_handle(struct QueueHandle handle);
-
-/**
- * Convert a queue handle returned from `queue_create` into one that can be
- * used *only* for sending.
- *
- * Note: This is primarily defence in depth.  A malicious holder of this queue
- * handle can still set the producer counter to invalid values and overwrite
- * arbitrary queue locations.
- */
-struct QueueHandle __cheri_libcall
-queue_make_send_handle(struct QueueHandle handle);
+int __cheri_libcall queue_destroy(struct SObjStruct   *heapCapability,
+                                  struct MessageQueue *handle);
 
 /**
  * Send a message to the queue specified by `handle`.  This expects to be able
@@ -122,9 +122,9 @@
  * This expected to be called with a valid queue handle.  It does not validate
  * that this is correct.  It uses `safe_memcpy` and so will check the buffer.
  */
-int __cheri_libcall queue_send(Timeout            *timeout,
-                               struct QueueHandle *handle,
-                               const void         *src);
+int __cheri_libcall queue_send(Timeout             *timeout,
+                               struct MessageQueue *handle,
+                               const void          *src);
 
 /**
  * Receive a message over a queue specified by `handle`.  This expects to be
@@ -135,9 +135,9 @@
  * Returns 0 on success, `-ETIMEOUT` if the timeout was exhausted, `-EINVAL` on
  * invalid arguments.
  */
-int __cheri_libcall queue_receive(Timeout            *timeout,
-                                  struct QueueHandle *handle,
-                                  void               *dst);
+int __cheri_libcall queue_receive(Timeout             *timeout,
+                                  struct MessageQueue *handle,
+                                  void                *dst);
 
 /**
  * Returns the number of items in the queue specified by `handle` via `items`.
@@ -150,27 +150,28 @@
  * may change in between the return of this function and the caller acting on
  * the result.
  */
-int __cheri_libcall queue_items_remaining(struct QueueHandle *handle,
-                                          size_t             *items);
+int __cheri_libcall queue_items_remaining(struct MessageQueue *handle,
+                                          size_t              *items);
 
 /**
  * Allocate a new message queue that is managed by the message queue
- * compartment.  This is returned as two sealed pointers to send and receive
- * ends of the queue.
+ * compartment.  The resulting queue handle (returned in `outQueue`) is a
+ * sealed capability to a queue that can be used for both sending and
+ * receiving.
  */
 int __cheri_compartment("message_queue")
   queue_create_sealed(Timeout            *timeout,
                       struct SObjStruct  *heapCapability,
-                      struct SObjStruct **outQueueSend,
-                      struct SObjStruct **outQueReceive,
+                      struct SObjStruct **outQueue,
                       size_t              elementSize,
                       size_t              elementCount);
 
 /**
- * Destroy a queue using a sealed queue endpoint handle.  The queue is not
- * actually freed until *both* endpoints are destroyed, which means that you
- * can safely call this from the sending end without the receiving end losing
- * access to messages held in the queue.
+ * Destroy a queue handle.  If this is called on a restricted endpoint
+ * (returned from `queue_receive_handle_create_sealed` or
+ * `queue_send_handle_create_sealed`), this frees only the handle.  If called
+ * with the queue handle returned from `queue_create_sealed`, this will destroy
+ * the queue.
  */
 int __cheri_compartment("message_queue")
   queue_destroy_sealed(Timeout           *timeout,
@@ -215,7 +216,7 @@
  */
 void __cheri_libcall
 multiwaiter_queue_receive_init(struct EventWaiterSource *source,
-                               struct QueueHandle       *handle);
+                               struct MessageQueue      *handle);
 
 /**
  * Initialise an event waiter source so that it will wait for the queue to be
@@ -224,7 +225,7 @@
  */
 void __cheri_libcall
 multiwaiter_queue_send_init(struct EventWaiterSource *source,
-                            struct QueueHandle       *handle);
+                            struct MessageQueue      *handle);
 
 /**
  * Initialise an event waiter source as in `multiwaiter_queue_receive_init`,
@@ -250,4 +251,32 @@
   multiwaiter_queue_send_init_sealed(struct EventWaiterSource *source,
                                      struct SObjStruct        *handle);
 
+/**
+ * Convert a queue handle returned from `queue_create_sealed` into one that can
+ * be used *only* for receiving.
+ *
+ * Returns 0 on success and writes the resulting restricted handle via
+ * `outHandle`.  Returns `-ENOMEM` on allocation failure or `-EINVAL` if the
+ * handle is not valid.
+ */
+int __cheri_compartment("message_queue")
+  queue_receive_handle_create_sealed(struct Timeout     *timeout,
+                                     struct SObjStruct  *heapCapability,
+                                     struct SObjStruct  *handle,
+                                     struct SObjStruct **outHandle);
+
+/**
+ * Convert a queue handle returned from `queue_create_sealed` into one that can
+ * be used *only* for sending.
+ *
+ * Returns 0 on success and writes the resulting restricted handle via
+ * `outHandle`.  Returns `-ENOMEM` on allocation failure or `-EINVAL` if the
+ * handle is not valid.
+ */
+int __cheri_compartment("message_queue")
+  queue_send_handle_create_sealed(struct Timeout     *timeout,
+                                  struct SObjStruct  *heapCapability,
+                                  struct SObjStruct  *handle,
+                                  struct SObjStruct **outHandle);
+
 __END_DECLS
diff --git a/sdk/include/setjmp-assembly.h b/sdk/include/setjmp-assembly.h
new file mode 100644
index 0000000..5c2bc30
--- /dev/null
+++ b/sdk/include/setjmp-assembly.h
@@ -0,0 +1,11 @@
+// Copyright CHERIoT Contributors.
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <assembly-helpers.h>
+
+EXPORT_ASSEMBLY_OFFSET(__jmp_buf, __cs0, 0)
+EXPORT_ASSEMBLY_OFFSET(__jmp_buf, __cs1, 8)
+EXPORT_ASSEMBLY_OFFSET(__jmp_buf, __csp, 16)
+EXPORT_ASSEMBLY_OFFSET(__jmp_buf, __cra, 24)
diff --git a/sdk/include/setjmp.h b/sdk/include/setjmp.h
index d8d3cdf..9710ccd 100644
--- a/sdk/include/setjmp.h
+++ b/sdk/include/setjmp.h
@@ -30,11 +30,14 @@
  */
 typedef struct __jmp_buf jmp_buf[1];
 
+#include <setjmp-assembly.h>
+
+__BEGIN_DECLS
 /**
  * C `setjmp` function.  Returns (up to) twice.  First returns 0, returns a
  * value passed to `longjmp` on the second return.
  */
-__attribute__((returns_twice)) extern "C" int setjmp(jmp_buf env);
+__attribute__((returns_twice)) int setjmp(jmp_buf env);
 __asm__(".section .text.setjmp,\"awG\",@progbits,setjmp,comdat\n"
         ".globl setjmp\n"
         ".p2align 2\n"
@@ -50,7 +53,7 @@
 /**
  * C `longjmp` function.  Does not return, jumps back to the `setjmp` call.
  */
-extern "C" void longjmp(jmp_buf env, int val);
+__attribute__((__noreturn__)) void longjmp(jmp_buf env, int val);
 __asm__(".section .text.longjmp,\"awG\",@progbits,longjmp,comdat\n"
         ".globl longjmp\n"
         ".p2align 2\n"
@@ -62,3 +65,4 @@
         "	clc	cra, 24(ca0)\n"
         "	mv	a0, a1\n"
         "	cjr	cra\n");
+__END_DECLS
diff --git a/sdk/include/stdbool.h b/sdk/include/stdbool.h
index c4013ec..debc62b 100644
--- a/sdk/include/stdbool.h
+++ b/sdk/include/stdbool.h
@@ -5,9 +5,11 @@
 #define _STDBOOL_H_
 
 #ifndef __cplusplus
+#if __STDC_VERSION__ < 202000
 typedef _Bool bool;
 #	define true 1
 #	define false 0
+#endif
 #endif // __cplusplus
 
 #endif // _STDBOOL_H_
diff --git a/sdk/include/stdlib.h b/sdk/include/stdlib.h
index 6955d23..adc2143 100644
--- a/sdk/include/stdlib.h
+++ b/sdk/include/stdlib.h
@@ -329,6 +329,6 @@
 
 static inline void yield(void)
 {
-	__asm volatile("ecall");
+	__asm volatile("ecall" ::: "memory");
 }
 __END_DECLS
diff --git a/sdk/include/token.h b/sdk/include/token.h
index 3c7ba20..c25c04b 100644
--- a/sdk/include/token.h
+++ b/sdk/include/token.h
@@ -69,17 +69,51 @@
                      size_t);
 
 /**
- * Unseal the obj given the key.
+ * Unseal the object given the key.
  *
- * The key must have the permit-unseal permission.
+ * The key may be either a static or dynamic key (i.e. one created with the
+ * `STATIC_SEALING_TYPE` macro or with `token_key_new`) and the object may be
+ * either allocated dynamically (via the token APIs) or statically (via the
+ * `DEFINE_STATIC_SEALED_VALUE` macro).
  *
- * @return unsealed obj if key and obj are valid and they match. nullptr
- * otherwise
+ * Returns the unsealed object if the key and object are valid and of the
+ * correct type, null otherwise.
+ *
+ * This function is equivalent to calling both `token_obj_unseal_static` and
+ * `token_obj_unseal_dynamic` and returning the result of the first one that
+ * succeeds, or null if both fail.
  */
 [[cheri::interrupt_state(disabled)]] void *
   __cheri_libcall token_obj_unseal(SKey, SObj);
 
 /**
+ * Unseal the object given the key.
+ *
+ * The key must be a static sealing key (i.e. one created with the
+ * `STATIC_SEALING_TYPE` macro) and the object must be a statically sealed
+ * object (i.e. one created with the `DEFINE_STATIC_SEALED_VALUE` macro).
+ *
+ * Returns the unsealed object if the key and object are valid and of the
+ * correct type, null otherwise.
+ */
+[[cheri::interrupt_state(disabled)]] void *
+  __cheri_libcall token_obj_unseal_static(SKey, SObj);
+
+/**
+ * Unseal the object given the key.
+ *
+ * The key may be either a static or dynamic key (i.e. one created with the
+ * `STATIC_SEALING_TYPE` macro or with `token_key_new`) and the object must be
+ * allocated dynamically with `token_sealed_alloc` or
+ * `token_sealed_unsealed_alloc`.
+ *
+ * Returns the unsealed object if the key and object are valid and of the
+ * correct type, null otherwise.
+ */
+[[cheri::interrupt_state(disabled)]] void *
+  __cheri_libcall token_obj_unseal_dynamic(SKey, SObj);
+
+/**
  * Destroy the obj given its key, freeing memory.
  *
  * The key must have the permit-unseal permission.
diff --git a/sdk/include/unwind-assembly.h b/sdk/include/unwind-assembly.h
new file mode 100644
index 0000000..377d97f
--- /dev/null
+++ b/sdk/include/unwind-assembly.h
@@ -0,0 +1,12 @@
+// Copyright CHERIoT Contributors.
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <assembly-helpers.h>
+#include <setjmp-assembly.h>
+
+#define INVOCATION_LOCAL_UNWIND_LIST_OFFSET 8
+
+EXPORT_ASSEMBLY_OFFSET(CleanupList, next, 0)
+EXPORT_ASSEMBLY_OFFSET(CleanupList, env, 8)
diff --git a/sdk/include/unwind.h b/sdk/include/unwind.h
index 7a1c898..ce6a25a 100644
--- a/sdk/include/unwind.h
+++ b/sdk/include/unwind.h
@@ -9,11 +9,13 @@
 struct CleanupList
 {
 	/// Next pointer.
-	CleanupList *next;
+	struct CleanupList *next;
 	/// Jump buffer to return to.
-	__jmp_buf env;
+	struct __jmp_buf env;
 };
 
+#include <unwind-assembly.h>
+
 /**
  * Head of the cleanup list.
  *
@@ -25,7 +27,8 @@
 {
 	void     *csp = __builtin_cheri_stack_get();
 	ptraddr_t top = __builtin_cheri_top_get(csp);
-	csp           = __builtin_cheri_address_set(csp, top - 8);
+	csp           = __builtin_cheri_address_set(
+	            csp, top - INVOCATION_LOCAL_UNWIND_LIST_OFFSET);
 	return (struct CleanupList **)csp;
 }
 
@@ -34,9 +37,9 @@
  */
 __always_inline static inline void cleanup_unwind(void)
 {
-	CleanupList **__head = cleanup_list_head();
-	CleanupList  *__top  = *__head;
-	*__head              = __top->next;
+	struct CleanupList **__head = cleanup_list_head();
+	struct CleanupList  *__top  = *__head;
+	*__head                     = __top->next;
 	switcher_handler_invocation_count_reset();
 	longjmp(&__top->env, 1);
 }
@@ -53,10 +56,10 @@
  */
 #define CHERIOT_DURING                                                         \
 	{                                                                          \
-		CleanupList cleanupListEntry;                                          \
-		auto      **__head    = cleanup_list_head();                           \
-		cleanupListEntry.next = *__head;                                       \
-		*__head               = &cleanupListEntry;                             \
+		struct CleanupList   cleanupListEntry;                                 \
+		struct CleanupList **__head = cleanup_list_head();                     \
+		cleanupListEntry.next       = *__head;                                 \
+		*__head                     = &cleanupListEntry;                       \
 		if (setjmp(&cleanupListEntry.env) == 0)                                \
 		{
 /// See CHERIOT_DURING.
diff --git a/sdk/lib/freestanding/memcpy.c b/sdk/lib/freestanding/memcpy.c
index 56ed012..5b7c791 100644
--- a/sdk/lib/freestanding/memcpy.c
+++ b/sdk/lib/freestanding/memcpy.c
@@ -49,7 +49,9 @@
 typedef void *word;
 
 #define wsize sizeof(word)
-_Static_assert(wsize != 0 && (wsize & (wsize - 1)) == 0);
+_Static_assert(wsize != 0);
+_Static_assert((wsize & (wsize - 1)) == 0);
+
 #define wmask (wsize - 1)
 
 void *__cheri_libcall memcpy(void *dst0, const void *src0, size_t length)
diff --git a/sdk/lib/queue/README.md b/sdk/lib/queue/README.md
new file mode 100644
index 0000000..b5837a3
--- /dev/null
+++ b/sdk/lib/queue/README.md
@@ -0,0 +1,12 @@
+Message queues
+==============
+
+Message queues, as described in [`queue.h`](../../include/queue.h).
+
+This directory provides two targets.
+
+ - The message queue library (`message_queue_library`) provides APIs for message queues that can be shared between two threads in the same compartment.
+ - The message queue compartment (`message_queue`) wraps these in APIs that can be used from different compartments.
+
+The library uses the `setjmp`-based error handler (see: [`unwind.h`](../../include/unwind.h)) to recover from invalid bounds or permissions.
+If you are using the library and want to be robust in the presence of CHERI exceptions, you should either add `unwind_error_handler` as a dependency of your compartment or provide an error handler that calls `cleanup_unwind`.
diff --git a/sdk/lib/queue/queue.cc b/sdk/lib/queue/queue.cc
index 879f8c6..716ad77 100644
--- a/sdk/lib/queue/queue.cc
+++ b/sdk/lib/queue/queue.cc
@@ -7,11 +7,12 @@
 #include <stdint.h>
 #include <timeout.h>
 #include <type_traits>
+#include <unwind.h>
 
 using namespace CHERI;
 using cheriot::atomic;
 
-using Debug = ConditionalDebug<false, "Queue library">;
+using Debug = ConditionalDebug<false, "MessageQueue library">;
 
 #ifdef __cplusplus
 using MessageQueueCounter = atomic<uint32_t>;
@@ -208,15 +209,15 @@
 	/**
 	 * Returns a pointer to the element in the queue indicated by `counter`.
 	 */
-	Capability<void> buffer_at_counter(struct QueueHandle &handle,
-	                                   uint32_t            counter)
+	Capability<void> buffer_at_counter(struct MessageQueue &handle,
+	                                   uint32_t             counter)
 	{
 		// Handle wrap for the second run around the counter.
 		size_t index =
 		  counter >= handle.queueSize ? counter - handle.queueSize : counter;
 		auto             offset = index * handle.elementSize;
-		Capability<void> pointer{handle.buffer};
-		pointer.address() += offset;
+		Capability<void> pointer{&handle};
+		pointer.address() += sizeof(MessageQueue) + offset;
 		return pointer;
 	}
 
@@ -332,102 +333,30 @@
 		  old, (old & HighBitFlagLock::reserved_bits()) | value));
 	}
 
-	/// Permissions for read-only access to a counter.
-	static constexpr PermissionSet ReadOnly{Permission::Global,
-	                                        Permission::Load};
-	/// Permissions for read-only access to a buffer.
-	static constexpr PermissionSet ReadOnlyCapability{
-	  Permission::Global,
-	  Permission::Load,
-	  Permission::LoadStoreCapability,
-	  Permission::LoadGlobal,
-	  Permission::LoadGlobal};
-	/// Permissions for write-only access to a buffer.
-	static constexpr PermissionSet WriteOnlyCapability{
-	  Permission::Global,
-	  Permission::Store,
-	  Permission::LoadStoreCapability};
-
-	/**
-	 * Helper to drop our short-lived claims.
-	 */
-	void drop_claims()
-	{
-		Timeout t{0};
-		heap_claim_fast(&t, nullptr, nullptr);
-	}
-
-	void bound_queue_buffer(struct QueueHandle handle)
-	{
-		Capability buffer   = handle.buffer;
-		Capability producer = handle.producer;
-		// Restrict the bounds using the address of the producer, which
-		// comes immediately after the queue buffer. This should be
-		// strictly equivalent to calculating the size of the queue
-		// from the number of elements and the size of elements (we
-		// assert that below).
-		buffer.bounds() = producer.address() - buffer.address();
-		Debug::Assert(
-		  [&]() -> bool {
-			  size_t bufferSize;
-			  bool   overflow = __builtin_mul_overflow(
-			      handle.queueSize, handle.elementSize, &bufferSize);
-			  bufferSize = CHERI::representable_length(bufferSize);
-			  return (!overflow) && (buffer.bounds() == bufferSize);
-		  },
-		  "Mismatch between the size of the queue as reported by `queueSize` "
-		  "and `elementSize` and its real size.");
-	}
-
 } // namespace
 
-struct QueueHandle queue_make_receive_handle(struct QueueHandle handle)
-{
-	Capability buffer   = handle.buffer;
-	Capability producer = handle.producer;
-	bound_queue_buffer(handle);
-	buffer.permissions() &= ReadOnlyCapability;
-	producer.permissions() &= ReadOnly;
-	handle.buffer   = buffer;
-	handle.producer = producer;
-	return handle;
-}
-
-struct QueueHandle queue_make_send_handle(struct QueueHandle handle)
-{
-	Capability buffer   = handle.buffer;
-	Capability consumer = handle.consumer;
-	bound_queue_buffer(handle);
-	buffer.permissions() &= WriteOnlyCapability;
-	consumer.permissions() &= ReadOnly;
-	handle.buffer   = buffer;
-	handle.consumer = consumer;
-	return handle;
-}
-
-int queue_destroy(struct SObjStruct *heapCapability, struct QueueHandle *handle)
+int queue_destroy(struct SObjStruct   *heapCapability,
+                  struct MessageQueue *handle)
 {
 	int ret = 0;
 	// Only upgrade the locks for destruction if we know that we will be
 	// able to free the queue at the end. This will fail if passed a
 	// restricted buffer, which will happen if `queue_destroy` is called on
 	// a restricted queue.
-	if (ret = heap_can_free(heapCapability, handle->buffer); ret != 0)
+	if (ret = heap_can_free(heapCapability, handle); ret != 0)
 	{
 		return ret;
 	}
 
-	auto           *producer = handle->producer;
-	HighBitFlagLock producerLock{*producer};
+	HighBitFlagLock producerLock{handle->producer};
 	producerLock.upgrade_for_destruction();
 
-	auto           *consumer = handle->consumer;
-	HighBitFlagLock consumerLock{*consumer};
+	HighBitFlagLock consumerLock{handle->consumer};
 	consumerLock.upgrade_for_destruction();
 
 	// This should not fail because of the `heap_can_free` check, unless we
 	// run out of stack.
-	if (ret = heap_free(heapCapability, handle->buffer); ret != 0)
+	if (ret = heap_free(heapCapability, handle); ret != 0)
 	{
 		return ret;
 	}
@@ -435,27 +364,16 @@
 	return ret;
 }
 
-int queue_create(Timeout            *timeout,
-                 struct SObjStruct  *heapCapability,
-                 struct QueueHandle *outQueue,
-                 void              **outAllocation,
-                 size_t              elementSize,
-                 size_t              elementCount)
+ssize_t queue_allocation_size(size_t elementSize, size_t elementCount)
 {
 	size_t bufferSize;
 	size_t allocSize;
 	bool   overflow =
 	  __builtin_mul_overflow(elementCount, elementSize, &bufferSize);
-	// We must be able to accurately represent the buffer, so round it up to a
-	// representable length.
-	bufferSize = CHERI::representable_length(bufferSize);
 	static constexpr size_t CounterSize = sizeof(uint32_t);
-	// Round up the size to be correctly aligned for the counters at the end
-	// (if necessary) and add two counters worth of space.
-	overflow |= __builtin_add_overflow(
-	  bufferSize,
-	  (2 * CounterSize) + (CounterSize - (bufferSize & (CounterSize - 1))),
-	  &allocSize);
+	// We also need space for the header
+	overflow |=
+	  __builtin_add_overflow(sizeof(MessageQueue), bufferSize, &allocSize);
 	if (overflow)
 	{
 		return -EINVAL;
@@ -471,6 +389,21 @@
 		return -EINVAL;
 	}
 
+	return allocSize;
+}
+
+int queue_create(Timeout              *timeout,
+                 struct SObjStruct    *heapCapability,
+                 struct MessageQueue **outQueue,
+                 size_t                elementSize,
+                 size_t                elementCount)
+{
+	ssize_t allocSize = queue_allocation_size(elementSize, elementCount);
+	if (allocSize < 0)
+	{
+		return allocSize;
+	}
+
 	// Allocate the space for the queue.
 	Capability buffer{heap_allocate(timeout, heapCapability, allocSize)};
 	if (!buffer.is_valid())
@@ -478,86 +411,79 @@
 		return -ENOMEM;
 	}
 
-	Capability<std::atomic<uint32_t>> producer{
-	  buffer.cast<std::atomic<uint32_t>>()};
-	Capability<std::atomic<uint32_t>> consumer{
-	  buffer.cast<std::atomic<uint32_t>>()};
-	// Make the producer and consumer point after the buffer
-	producer.address() += bufferSize;
-	consumer.address() += bufferSize + CounterSize;
-	// Set their bounds to 4 bytes.
-	producer.bounds() = CounterSize;
-	consumer.bounds() = CounterSize;
-	// The pointer used to free the allocation
-	*outAllocation = buffer;
-	Debug::log("Created queue with buffer: {}", buffer);
-	// The handle
-	*outQueue = {elementSize, elementCount, buffer, producer, consumer};
-
+	*outQueue = new (buffer.get()) MessageQueue(elementSize, elementCount);
 	return 0;
 }
 
-int queue_send(Timeout *timeout, struct QueueHandle *handle, const void *src)
+int queue_send(Timeout *timeout, struct MessageQueue *handle, const void *src)
 {
 	Debug::log("Send called on: {}", handle);
-	auto *producer   = handle->producer;
-	auto *consumer   = handle->consumer;
+	auto *producer   = &handle->producer;
+	auto *consumer   = &handle->consumer;
 	bool  shouldWake = false;
 	{
 		Debug::log("Lock word: {}", producer->load());
 		HighBitFlagLock l{*producer};
 		if (LockGuard g{l, timeout})
 		{
-			uint32_t producerCounter = counter_load(producer);
-			uint32_t consumerValue   = consumer->load();
-			uint32_t consumerCounter =
-			  consumerValue & ~(HighBitFlagLock::reserved_bits());
-			Debug::log("Producer counter: {}, consumer counter: {}, Size: {}",
-			           producerCounter,
-			           consumerCounter,
-			           handle->queueSize);
-			while (is_full(handle->queueSize, producerCounter, consumerCounter))
+			volatile int ret = 0;
+			// In an error-handling context, try to add the element to the
+			// queue.  If the permissions on src are invalid, or either `handle`
+			// or `src` is freed concurrently, we will hit the path that returns
+			// -EPERM.  The counter update happens last, so any failure will
+			// simply leave the queue in the old state.
+			on_error(
+			  [&] {
+				  uint32_t producerCounter = counter_load(producer);
+				  uint32_t consumerValue   = consumer->load();
+				  uint32_t consumerCounter =
+				    consumerValue & ~(HighBitFlagLock::reserved_bits());
+				  Debug::log(
+				    "Producer counter: {}, consumer counter: {}, Size: {}",
+				    producerCounter,
+				    consumerCounter,
+				    handle->queueSize);
+				  while (is_full(
+				    handle->queueSize, producerCounter, consumerCounter))
+				  {
+					  // Wait on the value to change.  If we hit this path while
+					  // the consumer lock is held, then the high bits will be
+					  // set.  Make sure that we yield.
+					  if (consumer->wait(timeout, consumerValue) == -ETIMEDOUT)
+					  {
+						  Debug::log("Timed out on futex");
+						  ret = -ETIMEDOUT;
+						  return;
+					  }
+					  consumerValue = consumer->load();
+					  consumerCounter =
+					    consumerValue & ~(HighBitFlagLock::reserved_bits());
+				  }
+				  auto entry = buffer_at_counter(*handle, producerCounter);
+				  Debug::log("Send copying {} bytes from {} to {}",
+				             handle->elementSize,
+				             src,
+				             entry);
+				  memcpy(entry, src, handle->elementSize);
+				  counter_store(
+				    &handle->producer,
+				    increment_and_wrap(handle->queueSize, producerCounter));
+				  // Check if the queue was empty before we updated the producer
+				  // counter.  By the time that we reach this point, anything on
+				  // the consumer side will be on the path to a futex_wait with
+				  // the old version of the producer counter and so will bounce
+				  // out again.
+				  shouldWake =
+				    is_empty(producerCounter, counter_load(consumer));
+			  },
+			  [&]() {
+				  ret = -EPERM;
+				  Debug::log("Error in send");
+			  });
+			if (ret != 0)
 			{
-				// Wait on the value to change.  If we hit this path while the
-				// consumer lock is held, then the high bits will be set.  Make
-				// sure that we yield.
-				if (consumer->wait(timeout, consumerValue) == -ETIMEDOUT)
-				{
-					Debug::log("Timed out on futex");
-					return -ETIMEDOUT;
-				}
-				consumerValue = consumer->load();
-				consumerCounter =
-				  consumerValue & ~(HighBitFlagLock::reserved_bits());
+				return ret;
 			}
-			auto entry = buffer_at_counter(*handle, producerCounter);
-			if (int claim = heap_claim_fast(timeout, handle->buffer, src);
-			    claim != 0)
-			{
-				Debug::log("Claim failed: {}", claim);
-				return claim;
-			}
-			if (!check_pointer<PermissionSet{Permission::Load}, false>(
-			      src, handle->elementSize))
-			{
-				drop_claims();
-				Debug::log("Load / bounds check failed: {}");
-				return -EPERM;
-			}
-			Debug::log("Send copying {} bytes from {} to {}",
-			           handle->elementSize,
-			           src,
-			           entry);
-			memcpy(entry, src, handle->elementSize);
-			drop_claims();
-			counter_store(
-			  handle->producer,
-			  increment_and_wrap(handle->queueSize, producerCounter));
-			// Check if the queue was empty before we updated the producer
-			// counter.  By the time that we reach this point, anything on the
-			// consumer side will be on the path to a futex_wait with the old
-			// version of the producer counter and so will bounce out again.
-			shouldWake = is_empty(producerCounter, counter_load(consumer));
 		}
 		else
 		{
@@ -567,71 +493,77 @@
 	}
 	if (shouldWake)
 	{
-		handle->producer->notify_all();
+		handle->producer.notify_all();
 	}
 	return 0;
 }
 
-int queue_receive(Timeout *timeout, struct QueueHandle *handle, void *dst)
+int queue_receive(Timeout *timeout, struct MessageQueue *handle, void *dst)
 {
 	Debug::log("Receive called on: {}", handle);
-	auto *producer   = handle->producer;
-	auto *consumer   = handle->consumer;
+	auto *producer   = &handle->producer;
+	auto *consumer   = &handle->consumer;
 	bool  shouldWake = false;
 	{
 		HighBitFlagLock l{*consumer};
 		if (LockGuard g{l, timeout})
 		{
-			uint32_t producerValue = producer->load();
-			uint32_t producerCounter =
-			  producerValue & ~(HighBitFlagLock::reserved_bits());
-			uint32_t consumerCounter = counter_load(consumer);
-			Debug::log("Producer counter: {}, consumer counter: {}, Size: {}",
-			           producerCounter,
-			           consumerCounter,
-			           handle->queueSize);
-			while (is_empty(producerCounter, consumerCounter))
+			volatile int ret = 0;
+			// In an error-handling context, try to add the element to the
+			// queue.  If the permissions on `dst` are invalid, or either
+			// `handle` or `dst` is freed concurrently, we will hit the path
+			// that returns `-EPERM`.  The counter update happens last, so any
+			// failure will simply leave the queue in the old state.
+			on_error(
+			  [&] {
+				  uint32_t producerValue = producer->load();
+				  uint32_t producerCounter =
+				    producerValue & ~(HighBitFlagLock::reserved_bits());
+				  uint32_t consumerCounter = counter_load(consumer);
+				  Debug::log(
+				    "Producer counter: {}, consumer counter: {}, Size: {}",
+				    producerCounter,
+				    consumerCounter,
+				    handle->queueSize);
+				  while (is_empty(producerCounter, consumerCounter))
+				  {
+					  // Wait on the value to change.  If we hit this path while
+					  // the producer lock is held, then the high bits will be
+					  // set.  Make sure that we yield.
+					  if (producer->wait(timeout, producerValue) == -ETIMEDOUT)
+					  {
+						  ret = -ETIMEDOUT;
+						  return;
+					  }
+					  producerValue = producer->load();
+					  producerCounter =
+					    producerValue & ~(HighBitFlagLock::reserved_bits());
+				  }
+				  auto entry = buffer_at_counter(*handle, consumerCounter);
+				  Debug::log("Receive copying {} bytes from {} to {}",
+				             handle->elementSize,
+				             entry,
+				             dst);
+				  memcpy(dst, entry, handle->elementSize);
+				  counter_store(
+				    consumer,
+				    increment_and_wrap(handle->queueSize, consumerCounter));
+				  // Check if the queue was full before we updated the consumer
+				  // counter.  By the time that we reach this point, anything on
+				  // the producer side will be on the path to a futex_wait with
+				  // the old version of the consumer counter and so will bounce
+				  // out again.
+				  shouldWake = is_full(
+				    handle->queueSize, counter_load(producer), consumerCounter);
+			  },
+			  [&]() {
+				  ret = -EPERM;
+				  Debug::log("Error in receive");
+			  });
+			if (ret != 0)
 			{
-				// Wait on the value to change.  If we hit this path while the
-				// producer lock is held, then the high bits will be set.  Make
-				// sure that we yield.
-				if (producer->wait(timeout, producerValue) == -ETIMEDOUT)
-				{
-					return -ETIMEDOUT;
-				}
-				producerValue = producer->load();
-				producerCounter =
-				  producerValue & ~(HighBitFlagLock::reserved_bits());
+				return ret;
 			}
-			auto entry = buffer_at_counter(*handle, consumerCounter);
-			if (int claim = heap_claim_fast(timeout, handle->buffer, dst);
-			    claim != 0)
-			{
-				return claim;
-			}
-			if (!check_pointer<PermissionSet{Permission::Store}, false>(
-			      dst, handle->elementSize))
-			{
-				drop_claims();
-				Debug::log("Check pointer failed with {} for {} byte write",
-				           dst,
-				           handle->elementSize);
-				return -EPERM;
-			}
-			Debug::log("Receive copying {} bytes from {} to {}",
-			           handle->elementSize,
-			           entry,
-			           dst);
-			memcpy(dst, entry, handle->elementSize);
-			drop_claims();
-			counter_store(
-			  consumer, increment_and_wrap(handle->queueSize, consumerCounter));
-			// Check if the queue was full before we updated the consumer
-			// counter.  By the time that we reach this point, anything on the
-			// producer side will be on the path to a futex_wait with the old
-			// version of the consumer counter and so will bounce out again.
-			shouldWake = is_full(
-			  handle->queueSize, counter_load(producer), consumerCounter);
 		}
 		else
 		{
@@ -639,17 +571,19 @@
 			return -ETIMEDOUT;
 		}
 	}
+	// If the queue is concurrently freed, this can trap, but we won't leak any
+	// locks.
 	if (shouldWake)
 	{
-		handle->consumer->notify_all();
+		handle->consumer.notify_all();
 	}
 	return 0;
 }
 
-int queue_items_remaining(struct QueueHandle *handle, size_t *items)
+int queue_items_remaining(struct MessageQueue *handle, size_t *items)
 {
-	auto producerCounter = counter_load(handle->producer);
-	auto consumerCounter = counter_load(handle->consumer);
+	auto producerCounter = counter_load(&handle->producer);
+	auto consumerCounter = counter_load(&handle->consumer);
 	*items =
 	  items_remaining(handle->queueSize, producerCounter, consumerCounter);
 	Debug::log("Producer counter: {}, consumer counter: {}, items: {}",
@@ -660,20 +594,20 @@
 }
 
 void multiwaiter_queue_send_init(struct EventWaiterSource *source,
-                                 struct QueueHandle       *handle)
+                                 struct MessageQueue      *handle)
 {
-	uint32_t producer   = counter_load(handle->producer);
-	uint32_t consumer   = counter_load(handle->consumer);
-	source->eventSource = handle->consumer;
+	uint32_t producer   = counter_load(&handle->producer);
+	uint32_t consumer   = counter_load(&handle->consumer);
+	source->eventSource = &handle->consumer;
 	source->value =
 	  is_full(handle->queueSize, producer, consumer) ? consumer : -1;
 }
 
 void multiwaiter_queue_receive_init(struct EventWaiterSource *source,
-                                    struct QueueHandle       *handle)
+                                    struct MessageQueue      *handle)
 {
-	uint32_t producer   = counter_load(handle->producer);
-	uint32_t consumer   = counter_load(handle->consumer);
-	source->eventSource = handle->producer;
+	uint32_t producer   = counter_load(&handle->producer);
+	uint32_t consumer   = counter_load(&handle->consumer);
+	source->eventSource = &handle->producer;
 	source->value       = is_empty(producer, consumer) ? producer : -1;
 }
diff --git a/sdk/lib/queue/queue_compartment.cc b/sdk/lib/queue/queue_compartment.cc
index 96f4845..9aec00a 100644
--- a/sdk/lib/queue/queue_compartment.cc
+++ b/sdk/lib/queue/queue_compartment.cc
@@ -8,10 +8,14 @@
 
 using namespace CHERI;
 
-using Debug = ConditionalDebug<false, "Queue compartment">;
+using Debug = ConditionalDebug<false, "MessageQueue compartment">;
 
 namespace
 {
+	__always_inline SKey handle_key()
+	{
+		return STATIC_SEALING_TYPE(MessageQueueHandle);
+	}
 	__always_inline SKey receive_key()
 	{
 		return STATIC_SEALING_TYPE(ReceiveHandle);
@@ -21,88 +25,62 @@
 		return STATIC_SEALING_TYPE(SendHandle);
 	}
 
-	struct QueueEndpoint
+	/**
+	 * Wrapper used for restricted endpoints.  This is used to provide
+	 * capabilities that allow only sending or receiving.  Instances of this
+	 * will be sealed with either `send_key()` or `receive_key()` to define
+	 * their types.
+	 */
+	struct RestrictedEndpoint
 	{
-		QueueHandle handle;
-		void       *allocation;
-		// Lock that protects against double free.
-		FlagLockPriorityInherited lock;
+		MessageQueue *handle;
 	};
 
+	/**
+	 * Unseal something that is either a queue handle or a restricted endpoint
+	 * with the specified key.
+	 */
+	MessageQueue *unseal(SKey key, SObj handle)
+	{
+		MessageQueue *queue = nullptr;
+		if (auto *unsealed =
+		      token_unseal(key, Sealed<RestrictedEndpoint>{handle}))
+		{
+			queue = unsealed->handle;
+		}
+		else if (auto *unsealed =
+		           token_unseal(handle_key(), Sealed<MessageQueue>{handle}))
+		{
+			queue = unsealed;
+		}
+		return queue;
+	}
+
 } // namespace
 
 int queue_create_sealed(Timeout            *timeout,
                         struct SObjStruct  *heapCapability,
-                        struct SObjStruct **outQueueSend,
-                        struct SObjStruct **outQueueReceive,
+                        struct SObjStruct **outQueue,
                         size_t              elementSize,
                         size_t              elementCount)
 {
-	if (!check_timeout_pointer(timeout))
+	ssize_t allocSize = queue_allocation_size(elementSize, elementCount);
+	if (allocSize < 0)
 	{
-		return -EPERM;
+		return -EINVAL;
 	}
 
-	// Allocate the queue endpoints
-	auto [send, sendSealed] =
-	  token_allocate<QueueEndpoint>(timeout, heapCapability, send_key());
-	if (!send)
+	void *unsealed = nullptr;
+	// Allocate the space for the queue.
+	auto sealed = token_sealed_unsealed_alloc(
+	  timeout, heapCapability, handle_key(), allocSize, &unsealed);
+	if (!unsealed)
 	{
-		return timeout->may_block() ? -ENOMEM : -ETIMEDOUT;
-	}
-	auto [receive, receiveSealed] =
-	  token_allocate<QueueEndpoint>(timeout, heapCapability, receive_key());
-	if (!receive)
-	{
-		token_obj_destroy(heapCapability, send_key(), sendSealed);
-		return timeout->may_block() ? -ENOMEM : -ETIMEDOUT;
+		return -ENOMEM;
 	}
 
-	// Bidirectional queue handle
-	QueueHandle handle;
-	// The pointer to the queue that is used when freeing
-	void *freeBuffer;
-	// Allocate the queue object
-	int ret = queue_create(
-	  timeout, heapCapability, &handle, &freeBuffer, elementSize, elementCount);
-	if (ret != 0)
-	{
-		token_obj_destroy(heapCapability, send_key(), sendSealed);
-		token_obj_destroy(heapCapability, receive_key(), receiveSealed);
-		return ret;
-	}
-
-	send->handle        = queue_make_send_handle(handle);
-	send->allocation    = freeBuffer;
-	receive->handle     = queue_make_receive_handle(handle);
-	receive->allocation = freeBuffer;
-	// Add a second claim on the buffer so that we can free the queue by freeing
-	// it twice, once in each endpoint.
-	// TODO we should check the return value of `heap_claim`
-	heap_claim(heapCapability, freeBuffer);
-
-	if (int claimed = heap_claim_fast(timeout, outQueueSend, outQueueReceive);
-	    claimed != 0)
-	{
-		return claimed;
-	}
-	if (!check_pointer<PermissionSet{Permission::Load,
-	                                 Permission::Store,
-	                                 Permission::LoadStoreCapability}>(
-	      outQueueReceive, sizeof(void *)) ||
-	    !check_pointer<PermissionSet{Permission::Load,
-	                                 Permission::Store,
-	                                 Permission::LoadStoreCapability}>(
-	      outQueueSend, sizeof(void *)))
-	{
-		// Free twice because we claimed it once in addition to the original
-		// allocation.
-		heap_free(heapCapability, freeBuffer);
-		heap_free(heapCapability, freeBuffer);
-		return -EPERM;
-	}
-	*outQueueSend    = sendSealed;
-	*outQueueReceive = receiveSealed;
+	new (unsealed) MessageQueue(elementSize, elementCount);
+	*outQueue = sealed;
 	return 0;
 }
 
@@ -110,99 +88,131 @@
                          struct SObjStruct *heapCapability,
                          struct SObjStruct *queueHandle)
 {
-	if (!check_timeout_pointer(timeout))
+	if (token_obj_unseal(handle_key(), queueHandle) != nullptr)
 	{
-		return -EPERM;
+		token_obj_destroy(heapCapability, handle_key(), queueHandle);
+		return 0;
 	}
-	Debug::log("Destroying queue {}", queueHandle);
-	auto  token = receive_key();
-	auto *end   = token_unseal(token, Sealed<QueueEndpoint>{queueHandle});
-	// This function takes either endpoint, so we need to try unsealing with
-	// both keys.
-	if (!end)
+	if (token_obj_unseal(send_key(), queueHandle) != nullptr)
 	{
-		token = send_key();
-		end   = token_unseal(token, Sealed<QueueEndpoint>{queueHandle});
+		token_obj_destroy(heapCapability, send_key(), queueHandle);
+		return 0;
 	}
-	if (!end)
+	if (token_obj_unseal(receive_key(), queueHandle) != nullptr)
 	{
-		return -EINVAL;
+		token_obj_destroy(heapCapability, receive_key(), queueHandle);
+		return 0;
 	}
-	// Don't bother with a lock guard: we will destroy this lock if we reach the
-	// end. If we lose a race here, this will trap and we will implicitly return
-	// `-ECOMPARTMENTFAIL`.
-	if (!end->lock.try_lock(timeout))
-	{
-		return -ETIMEDOUT;
-	}
-	if (heap_free(heapCapability, end->allocation) != 0)
-	{
-		end->lock.unlock();
-		return -EPERM;
-	}
-	token_obj_destroy(heapCapability, token, queueHandle);
-	return 0;
+	return -EINVAL;
 }
 
 int queue_send_sealed(Timeout           *timeout,
                       struct SObjStruct *handle,
                       const void        *src)
 {
-	auto *end = token_unseal(send_key(), Sealed<QueueEndpoint>{handle});
-	// If we failed to unseal, or if the timeout pointer is invalid, invalid
-	// argument error.
-	if (!end || !check_timeout_pointer(timeout))
+	MessageQueue *queue = unseal(send_key(), handle);
+	if (!queue || !check_timeout_pointer(timeout))
 	{
 		return -EINVAL;
 	}
-	return queue_send(timeout, &end->handle, src);
+	return queue_send(timeout, queue, src);
 }
 
 int queue_receive_sealed(Timeout *timeout, struct SObjStruct *handle, void *dst)
 {
-	auto *end = token_unseal(receive_key(), Sealed<QueueEndpoint>{handle});
-	// If we failed to unseal, or if the timeout is on the heap, invalid
-	// argument error.
-	if (!end || !check_timeout_pointer(timeout))
+	MessageQueue *queue = unseal(receive_key(), handle);
+	if (!queue || !check_timeout_pointer(timeout))
 	{
 		return -EINVAL;
 	}
-	return queue_receive(timeout, &end->handle, dst);
+	return queue_receive(timeout, queue, dst);
 }
 
 int multiwaiter_queue_receive_init_sealed(struct EventWaiterSource *source,
                                           struct SObjStruct        *handle)
 {
-	auto *end = token_unseal(receive_key(), Sealed<QueueEndpoint>{handle});
-	if (!end)
+	MessageQueue *queue = unseal(receive_key(), handle);
+	if (!queue)
 	{
 		return -EINVAL;
 	}
-	multiwaiter_queue_receive_init(source, &end->handle);
+	multiwaiter_queue_receive_init(source, queue);
 	return 0;
 }
 
 int multiwaiter_queue_send_init_sealed(struct EventWaiterSource *source,
                                        struct SObjStruct        *handle)
 {
-	auto *end = token_unseal(send_key(), Sealed<QueueEndpoint>{handle});
-	if (!end)
+	MessageQueue *queue = unseal(send_key(), handle);
+	if (!queue)
 	{
 		return -EINVAL;
 	}
-	multiwaiter_queue_send_init(source, &end->handle);
+	multiwaiter_queue_send_init(source, queue);
 	return 0;
 }
 
-int queue_items_remaining_sealed(struct SObjStruct *queueHandle, size_t *items)
+int queue_items_remaining_sealed(struct SObjStruct *handle, size_t *items)
 {
-	auto *end = token_unseal(receive_key(), Sealed<QueueEndpoint>{queueHandle});
+	MessageQueue *queue = unseal(send_key(), handle);
 	// This function takes either endpoint, so we need to try unsealing with
 	// both keys.
-	if (!end)
+	if (!queue)
 	{
-		end = token_unseal(send_key(), Sealed<QueueEndpoint>{queueHandle});
+		if (auto *unsealed =
+		      token_unseal(receive_key(), Sealed<RestrictedEndpoint>{handle}))
+		{
+			queue = unsealed->handle;
+		}
 	}
-	queue_items_remaining(&end->handle, items);
+	if (!queue)
+	{
+		return -EINVAL;
+	}
+	queue_items_remaining(queue, items);
+	return 0;
+}
+
+int queue_receive_handle_create_sealed(struct Timeout     *timeout,
+                                       struct SObjStruct  *heapCapability,
+                                       struct SObjStruct  *handle,
+                                       struct SObjStruct **outHandle)
+{
+	MessageQueue *queue =
+	  token_unseal(handle_key(), Sealed<MessageQueue>(handle));
+	if (!queue)
+	{
+		return -EINVAL;
+	}
+	auto [unsealed, sealed] = token_allocate<RestrictedEndpoint>(
+	  timeout, heapCapability, receive_key());
+	if (!unsealed)
+	{
+		return -ENOMEM;
+	}
+	unsealed->handle = queue;
+	*outHandle       = sealed;
+	return 0;
+}
+
+int queue_send_handle_create_sealed(struct Timeout     *timeout,
+                                    struct SObjStruct  *heapCapability,
+                                    struct SObjStruct  *handle,
+                                    struct SObjStruct **outHandle)
+{
+	MessageQueue *queue =
+	  token_unseal(handle_key(), Sealed<MessageQueue>(handle));
+	if (!queue)
+	{
+		return -EINVAL;
+	}
+	auto [unsealed, sealed] =
+	  token_allocate<RestrictedEndpoint>(timeout, heapCapability, send_key());
+	if (!unsealed)
+	{
+		return -ENOMEM;
+	}
+	unsealed->handle = queue;
+	*outHandle       = sealed;
 	return 0;
 }
diff --git a/sdk/lib/queue/xmake.lua b/sdk/lib/queue/xmake.lua
index 53ec1f2..113b580 100644
--- a/sdk/lib/queue/xmake.lua
+++ b/sdk/lib/queue/xmake.lua
@@ -9,6 +9,7 @@
   add_files("queue.cc")
 
 compartment("message_queue")
+  add_deps("unwind_error_handler")
   add_deps("message_queue_library")
   set_default(false)
   add_files("queue_compartment.cc")
diff --git a/sdk/lib/stdio/printf.cc b/sdk/lib/stdio/printf.cc
index 8fc5f51..579b6b6 100644
--- a/sdk/lib/stdio/printf.cc
+++ b/sdk/lib/stdio/printf.cc
@@ -88,7 +88,9 @@
 			*++p = upper ? toupper(c) : c;
 		} while (num /= base);
 		if (lenp)
+		{
 			*lenp = p - nbuf;
+		}
 		return (p);
 	}
 
@@ -158,7 +160,9 @@
 			while ((ch = static_cast<unsigned char>(*fmt++)) != '%' || stop)
 			{
 				if (ch == '\0')
+				{
 					return (retval);
+				}
 				putchar(ch);
 			}
 			percent   = fmt - 1;
@@ -229,21 +233,31 @@
 						n  = n * 10 + ch - '0';
 						ch = *fmt;
 						if (ch < '0' || ch > '9')
+						{
 							break;
+						}
 					}
 					if (dot)
+					{
 						dwidth = n;
+					}
 					else
+					{
 						width = n;
+					}
 					goto reswitch; // NOLINT
 				case 'b':
 					num = static_cast<unsigned int>(va_arg(ap, int));
 					p   = va_arg(ap, char *);
 					for (q = ksprintn(nbuf, num, *p++, nullptr, 0); *q;)
+					{
 						putchar(*q--);
+					}
 
 					if (num == 0)
+					{
 						break;
+					}
 
 					for (tmp = 0; *p;)
 					{
@@ -252,15 +266,23 @@
 						{
 							putchar(tmp ? ',' : '<');
 							for (; (n = *p) > ' '; ++p)
+							{
 								putchar(n);
+							}
 							tmp = 1;
 						}
 						else
+						{
 							for (; *p > ' '; ++p)
+							{
 								continue;
+							}
+						}
 					}
 					if (tmp)
+					{
 						putchar('>');
+					}
 					break;
 				case 'c':
 					putchar(va_arg(ap, int));
@@ -269,15 +291,21 @@
 					up = va_arg(ap, unsigned char *);
 					p  = va_arg(ap, char *);
 					if (!width)
+					{
 						width = 16;
+					}
 					while (width--)
 					{
 						putchar(hex2ascii(*up >> 4));
 						putchar(hex2ascii(*up & 0x0f));
 						up++;
 						if (width)
+						{
 							for (q = p; *q; q++)
+							{
 								putchar(*q);
+							}
+						}
 					}
 					break;
 				case 'd':
@@ -292,7 +320,9 @@
 						cflag = 1;
 					}
 					else
+					{
 						hflag = 1;
+					}
 					goto reswitch; // NOLINT
 				case 'j':
 					jflag = 1;
@@ -304,23 +334,39 @@
 						qflag = 1;
 					}
 					else
+					{
 						lflag = 1;
+					}
 					goto reswitch; // NOLINT
 				case 'n':
 					if (jflag)
+					{
 						*(va_arg(ap, intmax_t *)) = retval;
+					}
 					else if (qflag)
+					{
 						*(va_arg(ap, long long *)) = retval;
+					}
 					else if (lflag)
+					{
 						*(va_arg(ap, long *)) = retval;
+					}
 					else if (zflag)
+					{
 						*(va_arg(ap, size_t *)) = retval;
+					}
 					else if (hflag)
+					{
 						*(va_arg(ap, short *)) = static_cast<short>(retval);
+					}
 					else if (cflag)
+					{
 						*(va_arg(ap, char *)) = retval;
+					}
 					else
+					{
 						*(va_arg(ap, int *)) = retval;
+					}
 					break;
 				case 'o':
 					base = 8;
@@ -338,8 +384,10 @@
 				case 'r':
 					base = radix;
 					if (sign)
+					{
 						goto handle_sign; // NOLINT
-					goto handle_nosign;   // NOLINT
+					}
+					goto handle_nosign; // NOLINT
 				case 's':
 					p = va_arg(ap, char *);
 					if (p == nullptr)
@@ -400,39 +448,71 @@
 				handle_nosign:
 					sign = 0;
 					if (jflag)
+					{
 						num = va_arg(ap, uintmax_t);
+					}
 					else if (qflag)
+					{
 						num = va_arg(ap, unsigned long long);
+					}
 					else if (tflag)
+					{
 						num = va_arg(ap, ptrdiff_t);
+					}
 					else if (lflag)
+					{
 						num = va_arg(ap, unsigned long);
+					}
 					else if (zflag)
+					{
 						num = va_arg(ap, size_t);
+					}
 					else if (hflag)
+					{
 						num = static_cast<unsigned short>(va_arg(ap, int));
+					}
 					else if (cflag)
+					{
 						num = static_cast<unsigned char>(va_arg(ap, int));
+					}
 					else
+					{
 						num = va_arg(ap, unsigned int);
+					}
 					goto number; // NOLINT
 				handle_sign:
 					if (jflag)
+					{
 						num = va_arg(ap, intmax_t);
+					}
 					else if (qflag)
+					{
 						num = va_arg(ap, long long);
+					}
 					else if (tflag)
+					{
 						num = va_arg(ap, ptrdiff_t);
+					}
 					else if (lflag)
+					{
 						num = va_arg(ap, long);
+					}
 					else if (zflag)
+					{
 						num = va_arg(ap, ssize_t);
+					}
 					else if (hflag)
+					{
 						num = static_cast<short>(va_arg(ap, int));
+					}
 					else if (cflag)
+					{
 						num = static_cast<char>(va_arg(ap, int));
+					}
 					else
+					{
 						num = va_arg(ap, int);
+					}
 				number:
 					if (sign && static_cast<intmax_t>(num) < 0)
 					{
@@ -444,22 +524,36 @@
 					if (sharpflag && num != 0)
 					{
 						if (base == 8)
+						{
 							tmp++;
+						}
 						else if (base == 16)
+						{
 							tmp += 2;
+						}
 					}
 					if (neg)
+					{
 						tmp++;
+					}
 
 					if (!ladjust && padc == '0')
+					{
 						dwidth = width - tmp;
+					}
 					width -= tmp + (dwidth > n ? dwidth : n);
 					dwidth -= n;
 					if (!ladjust)
+					{
 						while (width-- > 0)
+						{
 							putchar(' ');
+						}
+					}
 					if (neg)
+					{
 						putchar('-');
+					}
 					if (sharpflag && num != 0)
 					{
 						if (base == 8)
@@ -473,19 +567,29 @@
 						}
 					}
 					while (dwidth-- > 0)
+					{
 						putchar('0');
+					}
 
 					while (*p)
+					{
 						putchar(*p--);
+					}
 
 					if (ladjust)
+					{
 						while (width-- > 0)
+						{
 							putchar(' ');
+						}
+					}
 
 					break;
 				default:
 					while (percent < fmt)
+					{
 						putchar(*percent++);
+					}
 					/*
 					 * Since we ignore an formatting argument it is no
 					 * longer safe to obey the remaining formatting
@@ -524,7 +628,9 @@
 	};
 	int retval = kvprintf(format, callback, &info, 10, ap);
 	if (info.remain >= 1)
+	{
 		*info.str++ = '\0';
+	}
 	return (retval);
 }
 
diff --git a/sdk/lib/unwind_error_handler/unwind.S b/sdk/lib/unwind_error_handler/unwind.S
index 6e56a98..6c74c06 100644
--- a/sdk/lib/unwind_error_handler/unwind.S
+++ b/sdk/lib/unwind_error_handler/unwind.S
@@ -1,16 +1,29 @@
+#include <unwind-assembly.h>
+
+/**
+ * A direct re-implementation of unwind.h's cleanup_unwind() as a stackless
+ * error handler.
+ *
+ * If there is no registered CleanupList structure (equivalently, there's no
+ * CHERIOT_DURING block active at the time of the fault), then this requests
+ * unwnding out of the compartment.  Otherwise, we will longjmp() out to the
+ * indicated handler (that is, the CHERIOT_HANDLER block associated with the
+ * current CHERIOT_DURING block), having reset the compartment error handler
+ * invocation counter to zero.
+ */
 .section .compartment_error_handler_stackless,"aw",@progbits
 .globl compartment_error_handler_stackless
 .p2align 2
 .type compartment_error_handler_stackless,@function
 compartment_error_handler_stackless:
-// Get the head of the error list.
+// Get the head of the error list (see cleanup_list_head)
 	cgettop      t0, csp
 	csetaddr     csp, csp, t0
-	clc          cs0, -8(csp)
+	clc          cs0, -INVOCATION_LOCAL_UNWIND_LIST_OFFSET(csp)
 	beqz         s0, .Lforce_unwind
-// Pop the top error from the list. */
-	clc          ct0, 0(cs0)
-	csc          ct0, -8(csp)
+// Pop the top error from the list
+	clc          ct0, CleanupList_offset_next(cs0)
+	csc          ct0, -INVOCATION_LOCAL_UNWIND_LIST_OFFSET(csp)
 // Mark this error handler as having finished.  We may still trap again
 // and reenter this, but now that we've popped the top element from the
 // stack we will run some different cleanup code next time. */
@@ -19,11 +32,12 @@
 	clc             ct2, %cheriot_compartment_lo_i(.Llookup_reset)(ct2)
 	cjalr           ct2
 // longjmp to the error handler.
-	clc          cs1, 16(cs0)
-	clc          csp, 24(cs0)
-	clc          cra, 32(cs0)
-	clc          cs0, 8(cs0)
+	clc          cs1, (CleanupList_offset_env + __jmp_buf_offset___cs1)(cs0)
+	clc          csp, (CleanupList_offset_env + __jmp_buf_offset___csp)(cs0)
+	clc          cra, (CleanupList_offset_env + __jmp_buf_offset___cra)(cs0)
+	clc          cs0, (CleanupList_offset_env + __jmp_buf_offset___cs0)(cs0)
 	cjr          cra
+
 .Lforce_unwind:
 	li           a0, 1
 	cret
diff --git a/sdk/privileged-compartment.ldscript b/sdk/privileged-compartment.ldscript
index 9cb984f..c8f1b0a 100644
--- a/sdk/privileged-compartment.ldscript
+++ b/sdk/privileged-compartment.ldscript
@@ -17,17 +17,19 @@
 		# Array of compartment exports
 		*(.compartment_exports .compartment_exports.*);
 	}
+	# Reserve space at the start for privileged compartments that need special sealing keys.
+	.compartment_sealing_keys : ALIGN(8)
+	{
+		# Start of the compartment's PCC region
+		HIDDEN(__compartment_pcc_start = .);
+		# The sealing keys for this compartment is before the import table, if they exist
+		*(.sealing_key1*)
+		*(.sealing_key2*)
+	}
 	# Lay out the compartment imports section.  This will end up on PCC.
 	.compartment_import_table : ALIGN(8)
 	{
 		# Array of compartment imports 
-		HIDDEN(__compartment_pcc_start = .);
-		# The sealing key for this compartment is before the import table.
-		HIDDEN(__sealingkey = .);
-		. = . + 8;
-		# Space for a second sealing key, used only by the allocator.
-		HIDDEN(__sealingkey2 = .);
-		. = . + 8;
 		# The first import table entry is the compartment switcher.
 		HIDDEN(.compartment_switcher = .);
 		. = . + 8;
diff --git a/sdk/xmake.lua b/sdk/xmake.lua
index da170b7..0f26d6e 100644
--- a/sdk/xmake.lua
+++ b/sdk/xmake.lua
@@ -653,11 +653,9 @@
 				"\n\t\tSHORT(.software_revoker_end - .software_revoker_start);" ..
 				"\n\t\tLONG(.software_revoker_globals);" ..
 				"\n\t\tSHORT(SIZEOF(.software_revoker_globals));" ..
-				-- The import table offset is computed from the start by code
-				-- that assumes that the first two words are space for sealing
-				-- keys, so we set it to 16 here to provide a computed size of
-				-- 0.
-				"\n\t\tSHORT(16)" ..
+				-- The software revoker has no import table.
+				"\n\t\tLONG(0)" ..
+				"\n\t\tSHORT(0)" ..
 				"\n\t\tLONG(.software_revoker_export_table);" ..
 				"\n\t\tSHORT(.software_revoker_export_table_end - .software_revoker_export_table);\n" ..
 				"\n\t\tLONG(0);" ..
diff --git a/tests.extra/regress-thread_exit_IRQ/README.md b/tests.extra/regress-thread_exit_IRQ/README.md
new file mode 100644
index 0000000..e0e9f2f
--- /dev/null
+++ b/tests.extra/regress-thread_exit_IRQ/README.md
@@ -0,0 +1,6 @@
+This checks for a specific bug fixed in 2024/11, where it was possible to
+invoke the scheduler's exception entrypoint, which must run with IRQs deferred,
+with IRQs enabled if a thread exited from its initial activation via a slightly
+unusual path.
+
+See https://github.com/CHERIoT-Platform/cheriot-rtos/pull/346
diff --git a/tests.extra/regress-thread_exit_IRQ/helper.cc b/tests.extra/regress-thread_exit_IRQ/helper.cc
new file mode 100644
index 0000000..023b4fe
--- /dev/null
+++ b/tests.extra/regress-thread_exit_IRQ/helper.cc
@@ -0,0 +1,5 @@
+#include "helper.h"
+[[cheri::interrupt_state(enabled)]] void* help(void)
+{
+	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
new file mode 100644
index 0000000..578659e
--- /dev/null
+++ b/tests.extra/regress-thread_exit_IRQ/helper.h
@@ -0,0 +1,2 @@
+#include <compartment.h>
+void* __cheri_compartment("helper") help(void);
diff --git a/tests.extra/regress-thread_exit_IRQ/top.cc b/tests.extra/regress-thread_exit_IRQ/top.cc
new file mode 100644
index 0000000..57087ce
--- /dev/null
+++ b/tests.extra/regress-thread_exit_IRQ/top.cc
@@ -0,0 +1,5 @@
+#include "helper.h"
+void __cheri_compartment("top") entry()
+{
+	asm volatile ("cmove cra, %0; cret" : : "C"(help()));
+}
diff --git a/tests.extra/regress-thread_exit_IRQ/xmake.lua b/tests.extra/regress-thread_exit_IRQ/xmake.lua
new file mode 100644
index 0000000..38462b2
--- /dev/null
+++ b/tests.extra/regress-thread_exit_IRQ/xmake.lua
@@ -0,0 +1,29 @@
+set_project("CHERIoT Scheduler IRQ Exception PoC")
+sdkdir = "../../sdk"
+includes(sdkdir)
+set_toolchains("cheriot-clang")
+
+option("board")
+    set_default("sail")
+
+compartment("helper")
+    add_files("helper.cc")
+
+compartment("top")
+    add_files("top.cc")
+
+firmware("top_compartment")
+    add_deps("freestanding", "debug")
+    add_deps("top", "helper")
+    on_load(function(target)
+        target:values_set("board", "$(board)")
+        target:values_set("threads", {
+            {
+                compartment = "top",
+                priority = 1,
+                entry_point = "entry",
+                stack_size = 0x200,
+                trusted_stack_frames = 1
+            }
+        }, {expand = false})
+    end)
diff --git a/tests/allocator-test.cc b/tests/allocator-test.cc
index 97f19c9..17faf66 100644
--- a/tests/allocator-test.cc
+++ b/tests/allocator-test.cc
@@ -666,7 +666,9 @@
 	  !array.is_valid(), "Allocating too large an array succeeded: {}", array);
 	array = heap_allocate_array(&t, MALLOC_CAPABILITY, 16, 2);
 	TEST(array.is_valid(), "Allocating array failed: {}", array);
-	TEST(array.length() == 32,
+	// If there's heap fragmentation, this may be rounded up to 40 because we
+	// can't use the 8 bytes after the end for another object.
+	TEST((array.length() >= 32) && (array.length() <= 40),
 	     "Allocating array returned incorrect length: {}",
 	     array);
 	ret = heap_free(MALLOC_CAPABILITY, array);
diff --git a/tests/ccompile-test.c b/tests/ccompile-test.c
index 7463b7c..da5401c 100644
--- a/tests/ccompile-test.c
+++ b/tests/ccompile-test.c
@@ -9,6 +9,7 @@
 #include <assert.h>
 #include <cdefs.h>
 #include <cheri-builtins.h>
+#include <cheri.h>
 #include <compartment.h>
 #include <ctype.h>
 #include <errno.h>
@@ -38,4 +39,4 @@
 #include <time.h>
 #include <timeout.h>
 #include <token.h>
-#include <cheri.h>
+#include <unwind.h>
diff --git a/tests/compartment_calls-test.cc b/tests/compartment_calls-test.cc
index 2f0cff1..a8ed1ee 100644
--- a/tests/compartment_calls-test.cc
+++ b/tests/compartment_calls-test.cc
@@ -22,26 +22,26 @@
 	int value = ConstantValue;
 
 	ret = compartment_call_inner(value);
-	TEST(ret == 0, "compartment_call_inner returend {}", ret);
+	TEST(ret == 0, "compartment_call_inner returned {}", ret);
 
 	ret = compartment_call_inner(value, value);
-	TEST(ret == 0, "compartment_call_inner returend {}", ret);
+	TEST(ret == 0, "compartment_call_inner returned {}", ret);
 
 	ret = compartment_call_inner(value, value, &value);
-	TEST(ret == 0, "compartment_call_inner returend {}", ret);
+	TEST(ret == 0, "compartment_call_inner returned {}", ret);
 
 	ret = compartment_call_inner(value, value, &value, value);
-	TEST(ret == 0, "compartment_call_inner returend {}", ret);
+	TEST(ret == 0, "compartment_call_inner returned {}", ret);
 
 	ret = compartment_call_inner(value, value, &value, value, &value);
-	TEST(ret == 0, "compartment_call_inner returend {}", ret);
+	TEST(ret == 0, "compartment_call_inner returned {}", ret);
 
 	ret = compartment_call_inner(value, value, &value, value, &value, value);
-	TEST(ret == 0, "compartment_call_inner returend {}", ret);
+	TEST(ret == 0, "compartment_call_inner returned {}", ret);
 
 	ret =
 	  compartment_call_inner(value, value, &value, value, &value, value, value);
-	TEST(ret == 0, "compartment_call_inner returend {}", ret);
+	TEST(ret == 0, "compartment_call_inner returned {}", ret);
 }
 
 int test_compartment_call()
diff --git a/tests/crash_recovery-test.cc b/tests/crash_recovery-test.cc
index 0254654..48dc278 100644
--- a/tests/crash_recovery-test.cc
+++ b/tests/crash_recovery-test.cc
@@ -10,9 +10,18 @@
 int               crashes = 0;
 std::atomic<bool> expectFault;
 
+static void test_irqs_are_enabled()
+{
+	void *r = __builtin_return_address(0);
+	TEST(__builtin_cheri_type_get(r) == CheriSealTypeReturnSentryEnabling,
+	     "Calling context has IRQs disabled");
+}
+
 extern "C" enum ErrorRecoveryBehaviour
 compartment_error_handler(struct ErrorState *frame, size_t mcause, size_t mtval)
 {
+	test_irqs_are_enabled();
+
 	crashes++;
 	if (mcause == 0x2)
 	{
diff --git a/tests/misc-test.cc b/tests/misc-test.cc
index f537067..dc09d77 100644
--- a/tests/misc-test.cc
+++ b/tests/misc-test.cc
@@ -170,12 +170,59 @@
 	     permissions);
 }
 
+// This test is somewhat intimately familiar with parameters of CHERIoT's
+// capability encoding and so might need revision if that changes.
+void check_capability_set_inexact_at_most()
+{
+	void *p = malloc(3128);
+
+	debug_log("Test Capability::BoundsProxy::set_inexact_at_most with {}", p);
+
+	// Too many bits for mantissa, regardless of base alignment
+	{
+		Capability<void> q      = {p};
+		size_t           reqlen = 2047;
+		q.bounds().set_inexact_at_most(reqlen);
+		debug_log("Requesting 2047 gives {}: {}", q.length(), q);
+		TEST(q.is_valid(), "set_inexact_at_most untagged");
+		TEST(q.length() < 2047, "set_inexact_at_most failed to truncate");
+		TEST(q.base() == q.address(), "set_inexact_at_most nonzero offset");
+	}
+
+	// Fits in mantissa, but not reachable from misaligned base
+	{
+		Capability<void> q = {p};
+		q.address() += 2;
+		size_t reqlen = 1024;
+		q.bounds().set_inexact_at_most(reqlen);
+		debug_log("Requesting 1024 at align 2 gives {}: {}", q.length(), q);
+		TEST(q.is_valid(), "set_inexact_at_most untagged");
+		TEST(q.length() < 1024, "set_inexact_at_most failed to truncate");
+		TEST(q.base() == q.address(), "set_inexact_at_most nonzero offset");
+	}
+
+	// Fits in mantissa and reachable from misaligned base
+	{
+		Capability<void> q = {p};
+		q.address() += 1;
+		size_t reqlen = 511;
+		q.bounds().set_inexact_at_most(reqlen);
+		debug_log("Requesting 511 at align 1 gives {}: {}", q.length(), q);
+		TEST(q.is_valid(), "set_inexact_at_most untagged");
+		TEST(q.length() == 511, "set_inexact_at_most truncated unnecessarily");
+		TEST(q.base() == q.address(), "set_inexact_at_most nonzero offset");
+	}
+
+	free(p);
+}
+
 int test_misc()
 {
 	check_timeouts();
 	check_memchr();
 	check_memrchr();
 	check_pointer_utilities();
+	check_capability_set_inexact_at_most();
 	debug_log("Testing shared objects.");
 	check_shared_object("exampleK",
 	                    SHARED_OBJECT(void, exampleK),
diff --git a/tests/multiwaiter-test.cc b/tests/multiwaiter-test.cc
index d0f380f..d9d99e9 100644
--- a/tests/multiwaiter-test.cc
+++ b/tests/multiwaiter-test.cc
@@ -70,16 +70,14 @@
 	TEST(events[0].value == 0, "Futex reports wake but none occurred");
 	TEST(events[1].value == 1, "Futex reports no wake");
 
-	QueueHandle queue;
-	void       *queueMemory;
+	MessageQueue *queue;
 	t.remaining = 0;
-	ret =
-	  queue_create(&t, MALLOC_CAPABILITY, &queue, &queueMemory, sizeof(int), 1);
+	ret         = queue_create(&t, MALLOC_CAPABILITY, &queue, sizeof(int), 1);
 
 	TEST(ret == 0, "Queue create failed:", ret);
 	int     val = 0;
 	Timeout noWait{0};
-	ret = queue_send(&noWait, &queue, &val);
+	ret = queue_send(&noWait, queue, &val);
 	TEST(ret == 0, "Queue send failed: {}", ret);
 
 	debug_log("Testing queue, blocked on send");
@@ -87,12 +85,12 @@
 		sleep(1);
 		int     val;
 		Timeout noWait{0};
-		int     ret = queue_receive(&noWait, &queue, &val);
+		int     ret = queue_receive(&noWait, queue, &val);
 		TEST(ret == 0, "Background receive failed: {}", ret);
 		TEST(val == 0, "Background receive returned incorrect value: {}", ret);
 		debug_log("Background thread made queue ready to send");
 	});
-	multiwaiter_queue_send_init(&events[0], &queue);
+	multiwaiter_queue_send_init(&events[0], queue);
 	t.remaining = 6;
 	ret         = multiwaiter_wait(&t, mw, events, 1);
 	TEST(ret == 0, "multiwaiter returned {}, expected 0", ret);
@@ -103,23 +101,23 @@
 		sleep(1);
 		int     val = 1;
 		Timeout noWait{0};
-		int     ret = queue_send(&noWait, &queue, &val);
+		int     ret = queue_send(&noWait, queue, &val);
 		TEST(ret == 0, "Background send failed: {}", ret);
 		debug_log("Background thread made queue ready to receive");
 	});
-	multiwaiter_queue_receive_init(&events[0], &queue);
+	multiwaiter_queue_receive_init(&events[0], queue);
 	t   = 10;
 	ret = multiwaiter_wait(&t, mw, events, 1);
 	TEST(ret == 0, "multiwaiter returned {}, expected 0", ret);
 	TEST(events[0].value == 1, "Queue did not return ready to receive");
-	ret = queue_receive(&noWait, &queue, &val);
+	ret = queue_receive(&noWait, queue, &val);
 	TEST(ret == 0, "Queue ready to receive but receive returned {}", ret);
 	TEST(val == 1, "Incorrect value returned from queue: {}", val);
 
 	debug_log("Testing waiting on a queue and a futex");
 	futex = 0;
 	setFutex(&futex, 1);
-	multiwaiter_queue_receive_init(&events[0], &queue);
+	multiwaiter_queue_receive_init(&events[0], queue);
 	events[1]   = {&futex, EventWaiterFutex, 0};
 	t.remaining = 6;
 	ret         = multiwaiter_wait(&t, mw, events, 2);
@@ -128,7 +126,6 @@
 	     "Queue reports ready to receive but should be empty.");
 	TEST(events[1].value == 1, "Futex reports no wake");
 
-	free(queueMemory);
 	multiwaiter_delete(MALLOC_CAPABILITY, mw);
 	return 0;
 }
diff --git a/tests/queue-test.cc b/tests/queue-test.cc
index da18b87..b8c36dc 100644
--- a/tests/queue-test.cc
+++ b/tests/queue-test.cc
@@ -4,7 +4,7 @@
 #include "compartment.h"
 #include "token.h"
 #include <cstdlib>
-#define TEST_NAME "Queue"
+#define TEST_NAME "MessageQueue"
 #include "tests.hh"
 #include <FreeRTOS-Compat/queue.h>
 #include <debug.hh>
@@ -28,77 +28,76 @@
 
 void test_queue_unsealed()
 {
-	char               bytes[ItemSize];
-	static QueueHandle queue;
-	static void       *queueMemory;
-	Timeout            timeout{0, 0};
+	char                 bytes[ItemSize];
+	static MessageQueue *queue;
+	Timeout              timeout{0, 0};
 	debug_log("Testing queue send operations");
 	auto checkSpace = [&](size_t         expected,
 	                      SourceLocation loc = SourceLocation::current()) {
 		size_t items;
-		queue_items_remaining(&queue, &items);
+		queue_items_remaining(queue, &items);
 		TEST(items == expected,
-		     "Queue test line {} reports {} items, should contain {}",
+		     "MessageQueue test line {} reports {} items, should contain {}",
 		     loc.line(),
 		     items,
 		     expected);
 	};
-	int rv = queue_create(
-	  &timeout, MALLOC_CAPABILITY, &queue, &queueMemory, ItemSize, MaxItems);
-	TEST(queue.elementSize == ItemSize,
-	     "Queue element size is {}, expected {}",
-	     queue.elementSize,
+	int rv =
+	  queue_create(&timeout, MALLOC_CAPABILITY, &queue, ItemSize, MaxItems);
+	TEST(queue->elementSize == ItemSize,
+	     "MessageQueue element size is {}, expected {}",
+	     queue->elementSize,
 	     ItemSize);
-	TEST(queue.queueSize == MaxItems,
-	     "Queue size is {}, expected {}",
-	     queue.queueSize,
+	TEST(queue->queueSize == MaxItems,
+	     "MessageQueue size is {}, expected {}",
+	     queue->queueSize,
 	     MaxItems);
-	TEST(rv == 0, "Queue creation failed with {}", rv);
-	rv = queue_send(&timeout, &queue, Message[0]);
+	TEST(rv == 0, "MessageQueue creation failed with {}", rv);
+	rv = queue_send(&timeout, queue, Message[0]);
 	checkSpace(1);
 	TEST(rv == 0, "Sending the first message failed with {}", rv);
 	checkSpace(1);
-	rv = queue_send(&timeout, &queue, Message[1]);
+	rv = queue_send(&timeout, queue, Message[1]);
 	TEST(rv == 0, "Sending the second message failed with {}", rv);
 	checkSpace(2);
-	// Queue is full, it should time out.
+	// MessageQueue is full, it should time out.
 	timeout.remaining = 5;
-	rv                = queue_send(&timeout, &queue, Message[1]);
+	rv                = queue_send(&timeout, queue, Message[1]);
 	TEST(rv == -ETIMEDOUT,
 	     "Sending to a full queue didn't time out as expected, returned {}",
 	     rv);
 	checkSpace(2);
 	debug_log("Testing queue receive operations");
 	timeout.remaining = 10;
-	rv                = queue_receive(&timeout, &queue, bytes);
+	rv                = queue_receive(&timeout, queue, bytes);
 	TEST(rv == 0, "Receiving the first message failed with {}", rv);
 	TEST(memcmp(Message[0], bytes, ItemSize) == 0,
 	     "First message received but not as expected. Got {}",
 	     bytes);
 	checkSpace(1);
-	rv = queue_receive(&timeout, &queue, bytes);
+	rv = queue_receive(&timeout, queue, bytes);
 	TEST(rv == 0, "Receiving the second message failed with {}", rv);
 	TEST(memcmp(Message[1], bytes, ItemSize) == 0,
 	     "Second message received but not as expected. Got {}",
 	     bytes);
 	checkSpace(0);
 	timeout.remaining = 5;
-	rv                = queue_receive(&timeout, &queue, bytes);
+	rv                = queue_receive(&timeout, queue, bytes);
 	TEST(
 	  rv == -ETIMEDOUT,
 	  "Receiving from an empty queue didn't time out as expected, returned {}",
 	  rv);
 	// Check that the items remaining calculations are correct after overflow.
-	queue_send(&timeout, &queue, Message[1]);
+	queue_send(&timeout, queue, Message[1]);
 	checkSpace(1);
-	queue_receive(&timeout, &queue, bytes);
+	queue_receive(&timeout, queue, bytes);
 	checkSpace(0);
-	queue_send(&timeout, &queue, Message[1]);
+	queue_send(&timeout, queue, Message[1]);
 	checkSpace(1);
-	queue_receive(&timeout, &queue, bytes);
+	queue_receive(&timeout, queue, bytes);
 	checkSpace(0);
-	rv = queue_destroy(MALLOC_CAPABILITY, &queue);
-	TEST(rv == 0, "Queue deletion failed with {}", rv);
+	rv = queue_destroy(MALLOC_CAPABILITY, queue);
+	TEST(rv == 0, "MessageQueue deletion failed with {}", rv);
 	debug_log("All queue library tests successful");
 }
 
@@ -108,10 +107,18 @@
 	Timeout t{1};
 	SObj    receiveHandle;
 	SObj    sendHandle;
+	SObj    queue;
 	char    bytes[ItemSize];
-	int     ret = queue_create_sealed(
-	      &t, MALLOC_CAPABILITY, &sendHandle, &receiveHandle, ItemSize, MaxItems);
-	TEST(ret == 0, "Queue creation failed with {}", ret);
+	int     ret =
+	  queue_create_sealed(&t, MALLOC_CAPABILITY, &queue, ItemSize, MaxItems);
+	TEST(ret == 0, "MessageQueue creation failed with {}", ret);
+	ret = queue_receive_handle_create_sealed(
+	  &t, MALLOC_CAPABILITY, queue, &receiveHandle);
+	TEST(
+	  ret == 0, "MessageQueue receive endpoint creation failed with {}", ret);
+	ret = queue_send_handle_create_sealed(
+	  &t, MALLOC_CAPABILITY, queue, &sendHandle);
+	TEST(ret == 0, "MessageQueue send endpoint creation failed with {}", ret);
 
 	t   = UnlimitedTimeout;
 	ret = queue_send_sealed(&t, receiveHandle, Message[1]);
@@ -159,11 +166,14 @@
 
 	t   = 1;
 	ret = queue_destroy_sealed(&t, MALLOC_CAPABILITY, sendHandle);
-	TEST(ret == 0, "Queue send destruction failed with {}", ret);
+	TEST(ret == 0, "MessageQueue send destruction failed with {}", ret);
 
 	t   = 1;
 	ret = queue_destroy_sealed(&t, MALLOC_CAPABILITY, receiveHandle);
-	TEST(ret == 0, "Queue receive destruction failed with {}", ret);
+	TEST(ret == 0, "MessageQueue receive destruction failed with {}", ret);
+
+	ret = queue_destroy_sealed(&t, MALLOC_CAPABILITY, queue);
+	TEST(ret == 0, "MessageQueue destruction failed with {}", ret);
 
 	TEST(heap_quota_remaining(MALLOC_CAPABILITY) == heapSpace,
 	     "Heap space leaked");
diff --git a/tests/stack-test.cc b/tests/stack-test.cc
index c814bf9..b3b894f 100644
--- a/tests/stack-test.cc
+++ b/tests/stack-test.cc
@@ -150,6 +150,12 @@
 	expect_handler(false);
 	exhaust_thread_stack();
 
+	debug_log("exhausting the compartment stack during a switcher call");
+	expect_handler(false);
+	threadStackTestFailed = true;
+	exhaust_thread_stack_spill(callback);
+	TEST(threadStackTestFailed == false, "switcher did not return error");
+
 	debug_log("modifying stack permissions on fault");
 	PermissionSet compartmentStackPermissions = get_stack_permissions();
 	for (auto permissionToRemove : compartmentStackPermissions)
diff --git a/tests/stack_integrity_thread.cc b/tests/stack_integrity_thread.cc
index 7a629c5..c897432 100644
--- a/tests/stack_integrity_thread.cc
+++ b/tests/stack_integrity_thread.cc
@@ -94,6 +94,40 @@
 	TEST(false, "Should be unreachable");
 }
 
+/**
+ * Arrange to exhaust the stack inside the cross-compartment switcher's spill of
+ * 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)())
+{
+	register auto      rfn asm("ct1") = fn;
+	register uintptr_t res asm("ca0") = 0;
+
+	__asm__ volatile(
+	  // Save the stack to put back later
+	  "cmove     cs0, csp\n"
+
+	  // Shrink the available stack space
+	  "cgetbase  s1, csp\n"
+	  "addi      s1, s1, %[stackleft]\n"
+	  "csetaddr  csp, csp, s1\n"
+
+	  // Make the call
+	  "1:\n"
+	  "auipcc ct2, %%cheriot_compartment_hi(.compartment_switcher)\n"
+	  "clc ct2, %%cheriot_compartment_lo_i(1b)(ct2)\n"
+	  "cjalr ct2\n"
+
+	  "cmove     csp, cs0\n"
+	  : /* outs */ "+C"(res)
+	  : /* ins */[stackleft] "i"(sizeof(void *))
+	  : /* clobbers */ "ct2", "cs0", "cs1");
+
+	*threadStackTestFailed = false;
+	TEST(res == -ENOTENOUGHSTACK, "Bad return {}", res);
+}
+
 void set_csp_permissions_on_fault(PermissionSet newPermissions)
 {
 	__asm__ volatile(
diff --git a/tests/stack_tests.h b/tests/stack_tests.h
index fbc757b..93ad94b 100644
--- a/tests/stack_tests.h
+++ b/tests/stack_tests.h
@@ -8,6 +8,8 @@
   __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(
   PermissionSet newPermissions);
 __cheri_compartment("stack_integrity_thread") void set_csp_permissions_on_call(
diff --git a/tests/static_sealing-test.cc b/tests/static_sealing-test.cc
index 046d704..09a7407 100644
--- a/tests/static_sealing-test.cc
+++ b/tests/static_sealing-test.cc
@@ -19,6 +19,5 @@
 {
 	// Get a pointer to it and ask for it to be unsealed.
 	Sealed<TestType> value{STATIC_SEALED_VALUE(test)};
-	test_static_sealed_object(value);
-	return 0;
+	return test_static_sealed_object(value);
 }
diff --git a/tests/static_sealing.h b/tests/static_sealing.h
index 5a3402b..99858dd 100644
--- a/tests/static_sealing.h
+++ b/tests/static_sealing.h
@@ -10,5 +10,5 @@
 	int value;
 };
 
-void __cheri_compartment("static_sealing_inner")
+int __cheri_compartment("static_sealing_inner")
   test_static_sealed_object(Sealed<TestType>);
diff --git a/tests/static_sealing_inner.cc b/tests/static_sealing_inner.cc
index 872bf6f..b8d56a7 100644
--- a/tests/static_sealing_inner.cc
+++ b/tests/static_sealing_inner.cc
@@ -4,10 +4,11 @@
 #define TEST_NAME "Static sealing (inner compartment)"
 #include "static_sealing.h"
 #include "tests.hh"
+#include <fail-simulator-on-error.h>
 
 using namespace CHERI;
 
-void test_static_sealed_object(Sealed<TestType> obj)
+int test_static_sealed_object(Sealed<TestType> obj)
 {
 	// Get our static sealing key.
 	SKey       key = STATIC_SEALING_TYPE(SealingType);
@@ -48,4 +49,5 @@
 	                                  Permission::Global}>(unsealed, 1)),
 	     "Incorrect permissions on unsealed statically sealed object {}",
 	     unsealed);
+	return 0;
 }
diff --git a/tests/test-runner.cc b/tests/test-runner.cc
index 0c85f14..c9f3acb 100644
--- a/tests/test-runner.cc
+++ b/tests/test-runner.cc
@@ -24,7 +24,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)
 		__asm__ volatile("csrr %0, mcycle" : "=r"(cycles));
 #else
 		__asm__ volatile("rdcycle %0" : "=r"(cycles));
@@ -54,7 +54,9 @@
 			crashDetected = true;
 		}
 		else
+		{
 			debug_log("{} finished in {} cycles", msg, cycles - startCycles);
+		}
 	}
 } // namespace