Use @jrtc27's approach to enable large sealed objects.

Allocating large sealed objects is difficult because sealed objects have
a header that contains the virtual sealing type and both the full
(sealed) object including the header and the (unsealed) part with the
header excluded must be representable.

@jrtc27 pointed out that there's no real need for the header to be at
the start of the allocation.  This neat trick lets us lay out the
objects as:

```
|Padding|Header|Unsealed Object|
        ^
        Address of sealed capability is here.
```

As long as we first round up the size of the unsealed object to be
representable, we can always then create a larger allocation that
contains padding at the start to preserve representability of the sealed
object.

This removes the restriction that we can't have sealed objects larger
than 4072 bytes, which will eliminate some annoying redirection in a few
places.

Fixes #242
diff --git a/sdk/core/allocator/main.cc b/sdk/core/allocator/main.cc
index f2299d2..9c585ce 100644
--- a/sdk/core/allocator/main.cc
+++ b/sdk/core/allocator/main.cc
@@ -375,6 +375,7 @@
 		auto  key = STATIC_SEALING_TYPE(MallocKey);
 		auto *capability =
 		  token_unseal<PrivateAllocatorCapabilityState>(key, in.get());
+		Debug::log("Unsealed: {}", capability);
 		if (!capability)
 		{
 			Debug::log("Invalid malloc capability {}", in);
@@ -1057,14 +1058,14 @@
 	  __noinline allocate_sealed_unsealed(Timeout      *timeout,
 	                                      SObj          heapCapability,
 	                                      SealingKey    key,
-	                                      size_t        sz,
+	                                      size_t        requestedSize,
 	                                      PermissionSet permissions)
 	{
-		if (!check_pointer<PermissionSet{Permission::Load, Permission::Store}>(
-		      timeout))
+		if (!check_timeout_pointer(timeout))
 		{
 			return {nullptr, nullptr};
 		}
+
 		if (!permissions.can_derive_from(key.permissions()))
 		{
 			Debug::log(
@@ -1072,11 +1073,28 @@
 			return {nullptr, nullptr};
 		}
 
-		if (sz > 0xfe8 - ObjHdrSize)
+		// Round up the size to the next representable size.  This ensures
+		// that, once we've added the header space, we have an allocation where
+		// both the object (from the end) and the header (with padding at the
+		// start) are representable.
+		size_t unsealedSize = CHERI::representable_length(requestedSize);
+		// Very large sizes may be rounded 'up' to zero.  Don't allow this.
+		if (unsealedSize == 0)
 		{
-			Debug::log("Cannot allocate sealed object of {} bytes, too large",
-			           sz);
-			// TODO: Properly handle imprecision.
+			Debug::log("Requested size {} is not representable", requestedSize);
+			return {nullptr, nullptr};
+		}
+
+		// It shouldn't be possible to overflow the add due to the way the
+		// rounding works, but this is not guaranteed in future capability
+		// encodings, so we'll do a tiny bit of extra work here to avoid
+		// accidentally introducing a security vulnerability in a future
+		// encoding.
+		size_t sealedSize = unsealedSize + ObjHdrSize;
+		if (__builtin_add_overflow(ObjHdrSize, unsealedSize, &sealedSize))
+		{
+			Debug::log("Requested size {} is too large to include header",
+			           requestedSize);
 			return {nullptr, nullptr};
 		}
 
@@ -1087,19 +1105,32 @@
 			return {nullptr, nullptr};
 		}
 		SealedAllocation obj{static_cast<SObj>(malloc_internal(
-		  sz + ObjHdrSize, std::move(g), capability, timeout, true))};
+		  sealedSize, std::move(g), capability, timeout, true))};
 		if (obj == nullptr)
 		{
 			Debug::log("Underlying allocation failed for sealed object");
 			return {nullptr, nullptr};
 		}
+		obj.address() = obj.top() - sealedSize;
+		// Round down the base to the heap alignment size.
+		// This ensures that the header is aligned and gives the same alignment
+		// as a normal allocation.  We will already be this aligned for most
+		// requested sizes.  The allocator always aligns the top and bottom of
+		// any allocations on a `MallocAlignment` boundary, and also on a
+		// representable boundary (whichever is stricter).  This extra
+		// alignment step ensures that this is also true for both the start of
+		// the header and the start of the unsealed object.  If the
+		// representable-length rounding of the requested size increased the
+		// requested alignment beyond 8, this will be a no-op.
+		obj.align_down(MallocAlignment);
 
 		obj->type   = key.address();
 		auto sealed = obj;
 		sealed.seal(SEALING_CAP());
 		obj.address() += ObjHdrSize; // Exclude the header.
-		obj.bounds() = obj.length() - ObjHdrSize;
-		Debug::log("Allocated sealed {}, unsealed {}", sealed, obj);
+		obj.bounds() = obj.top() - obj.address();
+		Debug::Assert(
+		  obj.is_valid(), "Unsealed object {} is not representable", obj);
 		return {sealed, obj};
 	}
 } // namespace
@@ -1127,14 +1158,14 @@
 	return nullptr;
 }
 
-__cheriot_minimum_stack(0x260) SObj
+__cheriot_minimum_stack(0x270) SObj
   token_sealed_unsealed_alloc(Timeout *timeout,
                               SObj     heapCapability,
                               SKey     key,
                               size_t   sz,
                               void   **unsealed)
 {
-	STACK_CHECK(0x260);
+	STACK_CHECK(0x270);
 	if (!check_timeout_pointer(timeout))
 	{
 		return INVALID_SOBJ;
@@ -1173,23 +1204,18 @@
 __noinline static SealedAllocation unseal_internal(SKey rawKey, SObj obj)
 {
 	SealingKey key{rawKey};
+	Capability unsealedInner = token_unseal<void>(key, obj);
+	if (!unsealedInner.is_valid())
+	{
+		return nullptr;
+	}
 
 	if (!key.permissions().contains(Permission::Unseal))
 	{
 		return nullptr;
 	}
 
-	auto unsealed = unseal_if_valid(obj);
-	if (!unsealed)
-	{
-		return nullptr;
-	}
-	if (unsealed->type != key.address())
-	{
-		return nullptr;
-	}
-
-	return unsealed;
+	return unseal_if_valid(obj);
 }
 
 __cheriot_minimum_stack(0x250) int token_obj_destroy(SObj heapCapability,
diff --git a/sdk/core/allocator/token.h b/sdk/core/allocator/token.h
index 1c75670..e9ce8f7 100644
--- a/sdk/core/allocator/token.h
+++ b/sdk/core/allocator/token.h
@@ -17,6 +17,29 @@
 /// Opaque type.  Sealing keys don't really point to anything.
 struct SKeyStruct;
 
+/**
+ * The structure that represents a sealed object.
+ *
+ * The `data` field represents the unsealed object.  There is some subtle
+ * complexity here: We must be able to precisely represent the unsealed object
+ * but we must *also* be able to precisely represent the entire sealed object.
+ *
+ * For larger objects, the alignment of the top and bottom are stricter and so
+ * we cannot simply place this header at the start of the allocation.  Instead,
+ * we arrange the objects in the allocation as follows:
+ *
+ * ```
+ * |Padding|Header|Unsealed Object|
+ *         ^
+ *         Address of sealed capability is here.
+ * ```
+ *
+ * This means that there is a small amount of padding at the start of the
+ * allocation, but the address of the capability points to the start of the
+ * header.  For anything below 4072 bytes with the current encoding, this
+ * amount of padding is zero and then we gradually increase it as the object
+ * size increases.
+ */
 struct SObjStruct
 {
 	/// The sealing type for this object.
diff --git a/sdk/core/token_library/token_unseal.S b/sdk/core/token_library/token_unseal.S
index 2c9ca62..8391902 100644
--- a/sdk/core/token_library/token_unseal.S
+++ b/sdk/core/token_library/token_unseal.S
@@ -80,9 +80,17 @@
   bne t0, t1, .Lexit_failure
 
   /* Subset bounds to ->data */
+  // Get the top into t1
   cgetlen         t1, ca0
+  cgetbase        t0, ca0
+  add             t1, t1, t0
+  // Move the address to the start of the data
   cincoffset      ca0, ca0, TokenSObj_offset_data
-  addi            t1, t1, -TokenSObj_offset_data
+  // Subtract the address of the (to-be-returned-unsealed) data from the top to
+  // give the length.
+  sub             t1, t1, a0
+  // Set the new bounds, using an exact setting so that any errors in the
+  // allocator's alignment turn into an untagged capability here.
   csetboundsexact ca0, ca0, t1
 
   /* And that's an unwrap. */
diff --git a/sdk/include/cheri.hh b/sdk/include/cheri.hh
index 59eb773..8e18db3 100644
--- a/sdk/include/cheri.hh
+++ b/sdk/include/cheri.hh
@@ -990,6 +990,33 @@
 		{
 			return ptr[index];
 		}
+
+		/**
+		 * Returns true if the capability is `align`-byte aligned, false
+		 * otherwise.
+		 */
+		bool is_aligned(size_t align)
+		{
+			return __builtin_is_aligned(ptr, align);
+		}
+
+		/**
+		 * Aligns the capability down to the nearest `align`-byte boundary.
+		 */
+		Capability &align_down(size_t align)
+		{
+			ptr = __builtin_align_down(ptr, align);
+			return *this;
+		}
+
+		/**
+		 * Aligns the capability up to the nearest `align`-byte boundary.
+		 */
+		Capability &align_up(size_t align)
+		{
+			ptr = __builtin_align_up(ptr, align);
+			return *this;
+		}
 	};
 
 	/**
diff --git a/tests/allocator-test.cc b/tests/allocator-test.cc
index 084a29a..c64eaac 100644
--- a/tests/allocator-test.cc
+++ b/tests/allocator-test.cc
@@ -1,6 +1,7 @@
 // Copyright Microsoft and CHERIoT Contributors.
 // SPDX-License-Identifier: MIT
 // Use a large quota for this compartment.
+#include "token.h"
 #define MALLOC_QUOTA 0x100000
 #define TEST_NAME "Allocator"
 
@@ -472,6 +473,49 @@
 		debug_log("Hazard pointer tests done");
 	}
 
+	void test_large_token(size_t tokenSize)
+	{
+		void      *unsealedCapability;
+		auto       sealingCapability = STATIC_SEALING_TYPE(sealingTest);
+		Capability sealedPointer =
+		  token_sealed_unsealed_alloc(&noWait,
+		                              MALLOC_CAPABILITY,
+		                              sealingCapability,
+		                              tokenSize,
+		                              &unsealedCapability);
+		TEST(sealedPointer.is_valid(),
+		     "Failed to allocate large sealed capability that requires padding "
+		     "for the header");
+		TEST(sealedPointer.is_sealed(), "Failed to allocate sealed capability");
+		TEST(!Capability{unsealedCapability}.is_sealed(),
+		     "Failed to allocate sealed capability");
+		size_t unsealedLength = Capability{unsealedCapability}.length();
+		TEST(unsealedLength >= tokenSize,
+		     "Length of unsealed capability is not {}: {}",
+		     tokenSize,
+		     unsealedCapability);
+		TEST(sealedPointer.length() >= unsealedLength + 8,
+		     "Length of unsealed capability is not the unsealed size plus the "
+		     "header size: {}",
+		     unsealedCapability);
+		TEST(sealedPointer.address() + 4 <
+		       Capability{unsealedCapability}.address(),
+		     "Header for the sealed capability ({}) is not before the start of "
+		     "the unsealed capability ({})",
+		     sealedPointer,
+		     unsealedCapability);
+		Capability unsealedLarge =
+		  token_unseal(sealingCapability, Sealed<void>{sealedPointer.get()});
+		TEST(unsealedLarge == Capability{unsealedCapability},
+		     "Unsealing large capability gave a different capability to the "
+		     "expected one ({} != {})",
+		     unsealedLarge,
+		     unsealedCapability);
+		int destroyed = token_obj_destroy(
+		  MALLOC_CAPABILITY, sealingCapability, sealedPointer);
+		TEST(destroyed == 0, "Failed to destroy large sealed capability");
+	}
+
 	/**
 	 * Test the sealing APIs.  Tests that we can allocate and free sealed
 	 * objects, that we can't collect them with the wrong capabilities, and that
@@ -484,6 +528,35 @@
 	 */
 	__noinline void test_token()
 	{
+		debug_log("Testing token allocation");
+		size_t validSizes[] = {
+		  128, 0xfe9, 8193, 511, 511 << 1, 511 << 2, 511 << 3};
+		for (size_t tokenSize : validSizes)
+		{
+			debug_log("Testing (expected valid) token size {}", tokenSize);
+			test_large_token(tokenSize);
+		}
+		size_t invalidSizes[] = {0, 0xffffffff};
+		for (size_t tokenSize : invalidSizes)
+		{
+			debug_log("Testing (expected invalid) token size {}", tokenSize);
+			auto       sealingCapability = STATIC_SEALING_TYPE(sealingTest);
+			void      *unsealedCapability;
+			Capability sealedPointer =
+			  token_sealed_unsealed_alloc(&noWait,
+			                              MALLOC_CAPABILITY,
+			                              sealingCapability,
+			                              tokenSize,
+			                              &unsealedCapability);
+			TEST(!sealedPointer.is_valid(),
+			     "Allocated {} for invalid token size {}",
+			     sealedPointer,
+			     tokenSize);
+			TEST(!Capability{unsealedCapability}.is_valid(),
+			     "Allocated {} for invalid token size {}",
+			     unsealedCapability,
+			     tokenSize);
+		}
 		auto    sealingCapability = STATIC_SEALING_TYPE(sealingTest);
 		Timeout noWait{0};
 		void   *unsealedCapability;
@@ -492,14 +565,6 @@
 		     heap_quota_remaining(SECOND_HEAP),
 		     SECOND_HEAP_QUOTA);
 		Capability sealedPointer = token_sealed_unsealed_alloc(
-		  &noWait, SECOND_HEAP, sealingCapability, 4096, &unsealedCapability);
-		// Note: If this test fails because the allocator can handle larger
-		// allocations, then this test can be removed.  If it fails because the
-		// allocator is incorrectly using imprecise bounds, please fix it!
-		TEST(!sealedPointer.is_valid(),
-		     "Successfully allocated capability that is too large for precise "
-		     "bounds");
-		sealedPointer = token_sealed_unsealed_alloc(
 		  &noWait, SECOND_HEAP, sealingCapability, 128, &unsealedCapability);
 		TEST(sealedPointer.is_valid(), "Failed to allocate capability");
 		TEST(sealedPointer.is_sealed(), "Failed to allocate sealed capability");