blob: 29a652b1b925fe0dbd036b6f20916cf116cb6f2e [file] [log] [blame]
// Copyright Microsoft and CHERIoT Contributors.
// SPDX-License-Identifier: MIT
#include "alloc.h"
#include "revoker.h"
#include "token.h"
#include <compartment.h>
#include <errno.h>
#include <futex.h>
#include <locks.hh>
#include <priv/riscv.h>
#include <riscvreg.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <thread.h>
#include <token.h>
#include <utils.hh>
using namespace CHERI;
Revocation::Revoker revoker;
namespace
{
/**
* Internal view of an allocator capability.
*
* TODO: For now, these are statically allocated. Eventually we will have
* some that are dynamically allocated. These should be ref counted so that
* we can avoid repeated validity checks.
*/
struct PrivateAllocatorCapabilityState
{
/// The remaining quota for this capability.
size_t quota;
/// A unique identifier for this pool.
uint16_t identifier;
};
static_assert(sizeof(PrivateAllocatorCapabilityState) <=
sizeof(AllocatorCapabilityState));
static_assert(alignof(PrivateAllocatorCapabilityState) <=
alignof(AllocatorCapabilityState));
// the global memory space
MState *gm;
/**
* A global lock for the allocator. This is acquired in public API
* functions, all internal functions should assume that it is held. If
* allocation fails for transient reasons then the lock will be dropped and
* reacquired over the yield.
*/
FlagLockPriorityInherited lock;
/**
* @brief Take a memory region and initialise a memory space for it. The
* MState structure will be placed at the beginning and the rest used as the
* real heap.
*
* @param tbase the capability to the region
* @param tsize size of the region
* @return pointer to the MState if can be initialised, nullptr otherwise.
*/
MState *mstate_init(Capability<void> tbase, size_t tsize)
{
if (!is_aligned(tbase) || !is_aligned(tsize))
{
return nullptr;
}
size_t msize = pad_request(sizeof(MState));
/*
* Each memory space has the MState structure at the beginning, followed
* by at least enough space for a smallest chunk, followed by a fake
* malloc header at the very end.
*
* The memory used to initialise a memory space must have enough bytes,
* but not too big that overflows what the compressed header can
* support.
*/
if (tsize < msize + MinChunkSize + sizeof(MChunkHeader) ||
tsize > MaxChunkSize)
{
return nullptr;
}
Capability m{tbase.cast<MState>()};
size_t hazardQuarantineSize =
Capability{MMIO_CAPABILITY_WITH_PERMISSIONS(
void *, hazard_pointers, true, true, true, false)}
.length();
m.bounds() = sizeof(*m);
m->heapStart = tbase;
m->heapStart.bounds() = tsize;
m->heapStart.address() += msize + hazardQuarantineSize;
m->init_bins();
// Carve off the front of the heap space to use for the hazard
// quarantine.
Capability hazardQuarantine = tbase;
hazardQuarantine.address() += msize;
hazardQuarantine.bounds() = hazardQuarantineSize;
m->hazardQuarantine = hazardQuarantine.cast<void *>();
m->mspace_firstchunk_add(
ds::pointer::offset<void>(tbase.get(), msize + hazardQuarantineSize),
tsize - msize - hazardQuarantineSize);
return m;
}
void check_gm()
{
if (gm == nullptr)
{
Capability heap = const_cast<void *>(
MMIO_CAPABILITY_WITH_PERMISSIONS(void,
heap,
/*load*/ true,
/*store*/ true,
/*capabilities*/ true,
/*loadMutable*/ true));
revoker.init();
gm = mstate_init(heap, heap.bounds());
Debug::Assert(gm != nullptr, "gm should not be null");
}
}
/**
* Futex value to allow a thread to wait for another thread to free an
* object.
*/
cheriot::atomic<int32_t> freeFutex = -1;
/**
* Helper that returns true if the timeout value permits sleeping.
*
* This assumes that the pointer was checked with `check_pointer` earlier
* but defends against the case where the timeout object was freed by
* another thread while the thread using it slept.
*/
bool may_block(Timeout *timeout)
{
return timeout->may_block();
}
/**
* Helper to reacquire the lock after sleeping. This adds any time passed
* as `elapsed` to the timeout and then tries to reacquire the lock,
* blocking for no longer than the remaining time on this timeout.
*
* Returns `true` if the lock has been successfully reacquired, `false`
* otherwise.
*/
bool reacquire_lock(Timeout *timeout,
LockGuard<decltype(lock)> &g,
Ticks elapsed = 0)
{
timeout->elapse(elapsed);
return g.try_lock(timeout);
}
/**
* Wait for the background revoker, if the revoker supports
* interrupt-driven notifications.
*
* Waits until either `timeout` expires or `epoch` has finished and then
* tries to reacquire the lock. Returns true if the epoch has passed and the
* lock has been reacquired, returns false and does *not* reacquire the lock
* in the case of any error.
*
*/
template<typename T = Revocation::Revoker>
bool wait_for_background_revoker(
Timeout *timeout,
uint32_t epoch,
LockGuard<decltype(lock)> &g,
T &r = revoker) requires(Revocation::SupportsInterruptNotification<T>)
{
// Release the lock before sleeping
g.unlock();
// Wait for the interrupt to fire, then try to reacquire the lock if
// the epoch is passed.
return r.wait_for_completion(timeout, epoch) && g.try_lock(timeout);
}
/**
* Wait for the background revoker, if the revoker does not support
* interrupt-driven notifications. This will yield and retry if the revoker
* has not yet finished.
*
* Waits until either `timeout` expires or `epoch` has finished and then
* tries to reacquire the lock. Returns true if the epoch has passed and the
* lock has been reacquired, returns false and does *not* reacquire the lock
* in the case of any error.
*
*/
template<typename T = Revocation::Revoker>
bool wait_for_background_revoker(
Timeout *timeout,
uint32_t epoch,
LockGuard<decltype(lock)> &g,
T &r = revoker) requires(!Revocation::SupportsInterruptNotification<T>)
{
// Yield while until a revocation pass has finished.
while (!revoker.has_revocation_finished_for_epoch<true>(epoch))
{
// Release the lock before sleeping
g.unlock();
Timeout smallSleep{1};
thread_sleep(&smallSleep);
if (!reacquire_lock(timeout, g, smallSleep.elapsed))
{
return false;
}
}
return true;
}
/**
* Malloc implementation. Allocates `bytes` bytes of memory. If `timeout`
* is greater than zero, may block for that many ticks. If `timeout` is the
* maximum value permitted by the type, may block indefinitely.
*
* Memory is allocated against the quota in `capability`, which must be
* unsealed by the caller.
*
* The lock guard own the lock on entry. It will drop the lock if a
* transient error occurs and attempt to reacquire it. If the lock cannot be
* reacquired during the permitted timeout then this returns nullptr.
*
* If `isSealedAllocation` is true, then the allocation is marked as sealed
* and excluded during `heap_free_all`.
*/
void *malloc_internal(size_t bytes,
LockGuard<decltype(lock)> &&g,
PrivateAllocatorCapabilityState *capability,
Timeout *timeout,
bool isSealedAllocation = false)
{
check_gm();
do
{
auto ret = gm->mspace_dispatch(bytes,
capability->quota,
capability->identifier,
isSealedAllocation);
if (std::holds_alternative<Capability<void>>(ret))
{
return std::get<Capability<void>>(ret);
}
// If the timeout is 0, fail now.
if (!may_block(timeout))
{
return nullptr;
}
// If there is enough memory in the quarantine to fulfil this
// allocation, try dequeing some things and retry.
auto *needsRevocation =
std::get_if<MState::AllocationFailureRevocationNeeded>(&ret);
if (needsRevocation)
{
// If we are able to dequeue some objects from quarantine then
// retry immediately, otherwise yield.
//
// It may take us several rounds through here to succeed or
// discover fragmentation-induced futility, since we do not,
// presently, consolidate chunks in quarantine and each chunk
// requires individual attention to merge back into the free
// pool (and consolidate with neighbors), and each round here
// moves at most O(1) chunks out of quarantine.
if (!gm->quarantine_dequeue())
{
Debug::log("Quarantine has enough memory to satisfy "
"allocation, kicking revoker");
revoker.system_bg_revoker_kick();
if constexpr (Revocation::Revoker::IsAsynchronous)
{
wait_for_background_revoker(
timeout, needsRevocation->waitingEpoch, g);
}
else
{
// Drop and reacquire the lock while yielding.
// Sleep for a single tick.
g.unlock();
Timeout smallSleep{0};
thread_sleep(&smallSleep);
if (!reacquire_lock(timeout, g, smallSleep.elapsed))
{
return nullptr;
}
}
}
continue;
}
// If the heap is full, wait for someone to free an allocation and
// then retry.
if (std::holds_alternative<
MState::AllocationFailureDeallocationNeeded>(ret))
{
Debug::log("Not enough free space to handle {}-byte "
"allocation, sleeping",
bytes);
// Use the current free space as the sleep futex value. This
// means that the `wait` call will fail if the amount of free
// memory changes between dropping the lock and waiting, unless
// a matched number of allocations and frees happen (in which
// case, we're happy to sleep because we still can't manage
// this allocation).
auto expected = gm->heapFreeSize;
freeFutex = expected;
// If there are things on the hazard list, wake after one tick
// and see if they have gone away. Otherwise, wait until we
// have some newly freed objects.
Timeout t{gm->hazard_quarantine_is_empty() ? timeout->remaining
: 1};
// Drop the lock while yielding
g.unlock();
freeFutex.wait(&t, expected);
timeout->elapse(t.elapsed);
Debug::log("Woke from futex wake");
if (!reacquire_lock(timeout, g))
{
return nullptr;
}
continue;
}
if (std::holds_alternative<MState::AllocationFailurePermanent>(ret))
{
return nullptr;
}
} while (may_block(timeout));
return nullptr;
}
/**
* Unseal an allocator capability and return it. Returns `nullptr` if this
* is not a heap capability.
*/
PrivateAllocatorCapabilityState *
malloc_capability_unseal(SealedAllocation in)
{
auto key = STATIC_SEALING_TYPE(MallocKey);
auto *capability =
token_unseal<PrivateAllocatorCapabilityState>(key, in.get());
if (!capability)
{
Debug::log("Invalid malloc capability {}", in);
return nullptr;
}
// Assign an identifier if this is the first time that we've seen this.
if (capability->identifier == 0)
{
static uint32_t nextIdentifier = 1;
if (nextIdentifier >= (1 << MChunkHeader::OwnerIDWidth))
{
return nullptr;
}
capability->identifier = nextIdentifier++;
}
return capability;
}
/**
* Object representing a claim. When a heap object is claimed, an instance
* of this structure exists to track the reference count per claimer.
*/
class Claim
{
/**
* The identifier of the owning allocation capability.
*/
uint16_t allocatorIdentifier = 0;
/**
* Next 'pointer' encoded as a shifted offset from the start of the
* heap.
*/
uint16_t encodedNext = 0;
/**
* Saturating reference count. We use one to indicate a single
* reference count rather than zero to slightly simplify the logic at
* the expense of saturating one increment earlier than we need to. It
* is highly unlikely that any vaguely sensible code will ever
* encounter the saturation case, so this is unlikely to be a problem.
* It might become one if we ever move to using a 16-bit reference
* count (which would require a different allocation path for claim
* structures to make sense).
*/
uint32_t referenceCount = 1;
/**
* Private constructor, creates a new claim with a single reference
* count.
*/
Claim(uint16_t identifier, uint16_t nextClaim)
: allocatorIdentifier(identifier), encodedNext(nextClaim)
{
}
/**
* Destructor is private, claims should always be destroyed via
* `destroy`.
*/
~Claim() = default;
friend class Iterator;
public:
/**
* Returns the owner of this claim.
*/
[[nodiscard]] uint16_t owner() const
{
return allocatorIdentifier;
}
/**
* Returns the value of the compressed next pointer.
*/
[[nodiscard]] uint16_t encoded_next() const
{
return encodedNext;
}
/**
* Claims list iterator. This wraps a next pointer and so can be used
* both to inspect a value and update it.
*/
class Iterator
{
/**
* Placeholder value for end iterators.
*/
static inline const uint16_t EndPlaceholder = 0;
/**
* A pointer to the encoded next pointer.
*/
uint16_t *encodedNextPointer =
const_cast<uint16_t *>(&EndPlaceholder);
public:
/**
* Default constructor returns a generic end iterator.
*/
Iterator() = default;
/// Copy constructor.
__always_inline Iterator(const Iterator &other) = default;
/// Constructor from an explicit next pointer.
__always_inline Iterator(uint16_t *nextPointer)
: encodedNextPointer(nextPointer)
{
}
/**
* Dereference. Returns the claim that this iterator points to.
*/
__always_inline Claim *operator*()
{
return Claim::from_encoded_offset(*encodedNextPointer);
}
/**
* Dereference. Returns the claim that this iterator points to.
*/
__always_inline Claim *operator->()
{
return Claim::from_encoded_offset(*encodedNextPointer);
}
/// Iteration termination condition.
__always_inline bool operator!=(const Iterator Other)
{
return *encodedNextPointer != *Other.encodedNextPointer;
}
/**
* Preincrement, moves to the next element.
*/
Iterator &operator++()
{
Claim *next = **this;
encodedNextPointer = &next->encodedNext;
return *this;
}
/**
* Assignment, replaces the claim that this iterator points to with
* the new one.
*/
Iterator &operator=(Claim *claim)
{
*encodedNextPointer = claim->encode_address();
return *this;
}
/**
* Returns the next pointer that this iterator refers to.
*/
uint16_t *pointer()
{
return encodedNextPointer;
}
};
/**
* Allocate a new claim. This will fail if space is not immediately
* available.
*
* Returns a pointer to the new allocation on success, nullptr on
* failure.
*/
static Claim *create(PrivateAllocatorCapabilityState &capability,
uint16_t next)
{
auto space = gm->mspace_dispatch(
sizeof(Claim), capability.quota, capability.identifier);
if (!std::holds_alternative<Capability<void>>(space))
{
return nullptr;
}
return new (std::get<Capability<void>>(space))
Claim(capability.identifier, next);
}
/**
* Destroy a claim, which must have been allocated with `capability`.
*/
static void destroy(PrivateAllocatorCapabilityState &capability,
Claim *claim)
{
Capability heap{gm->heapStart};
heap.address() = Capability{claim}.address();
auto chunk = MChunkHeader::from_body(heap);
capability.quota += chunk->size_get();
// We could skip quarantine for these objects, since we know that
// they haven't escaped, but they're small so it's probably not
// worthwhile.
gm->mspace_free(*chunk, sizeof(Claim));
}
/**
* Add a reference. If this would overflow, the reference is pinned
* and this never decrements.
*/
void reference_add()
{
if (referenceCount !=
std::numeric_limits<decltype(referenceCount)>::max())
{
referenceCount++;
}
}
/**
* Decrement the reference count and return whether this has dropped
* the reference count to 0.
*/
bool reference_remove()
{
if (referenceCount !=
std::numeric_limits<decltype(referenceCount)>::max())
{
referenceCount--;
}
return referenceCount == 0;
}
/**
* Decode an encoded offset and return a pointer to the claim.
*/
static Claim *from_encoded_offset(uint16_t offset)
{
if (offset == 0)
{
return nullptr;
}
Capability<Claim> ret{gm->heapStart.cast<Claim>()};
ret.address() += offset << MallocAlignShift;
ret.bounds() = sizeof(Claim);
return ret;
}
/**
* Encode the address of this object in a 16-bit value.
*/
uint16_t encode_address()
{
ptraddr_t address = Capability{this}.address();
address -= gm->heapStart.address();
Debug::Assert((address & MallocAlignMask) == 0,
"Claim at address {} is insufficiently aligned",
address);
address >>= MallocAlignShift;
Debug::Assert(address <= std::numeric_limits<uint16_t>::max(),
"Encoded claim address is too large: {}",
address);
return address;
}
};
static_assert(sizeof(Claim) <= (1 << MallocAlignShift),
"Claims should fit in the smallest possible allocation");
/**
* Find a claim if one exists. Returns a reference to the next pointer
* that refers to this claim.
*/
std::pair<uint16_t &, Claim *> claim_find(uint16_t owner,
MChunkHeader &chunk)
{
for (Claim::Iterator i{&chunk.claims}, end; i != end; ++i)
{
Claim *claim = *i;
if (claim->owner() == owner)
{
return {*i.pointer(), claim};
}
}
return {chunk.claims, nullptr};
}
/**
* Add a claim to a chunk, owned by `owner`. This returns true if the
* claim was successfully added, false otherwise.
*/
bool claim_add(PrivateAllocatorCapabilityState &owner, MChunkHeader &chunk)
{
Debug::log("Adding claim for {}", owner.identifier);
auto [next, claim] = claim_find(owner.identifier, chunk);
if (claim)
{
Debug::log("Adding second claim");
claim->reference_add();
return true;
}
bool isOwner = (chunk.ownerID == owner.identifier);
size_t size = chunk.size_get();
if (!isOwner)
{
if (owner.quota < size)
{
Debug::log("quota insufficient");
return false;
}
owner.quota -= size;
}
claim = Claim::create(owner, next);
if (claim != nullptr)
{
Debug::log("Allocated new claim");
// If this is the owner, remove the owner and downgrade our
// ownership to a claim. This simplifies the deallocation path.
if (isOwner)
{
chunk.ownerID = 0;
claim->reference_add();
}
next = claim->encode_address();
return true;
}
// If we failed to allocate the claim object, undo adding this to our
// quota.
if (!isOwner)
{
owner.quota += size;
}
Debug::log("Failed to add claim");
return false;
}
/**
* Drop a claim on an object by the specified allocator capability. If
* `reallyDrop` is false then this does not actually drop the claim but
* returns true if it *could have* dropped a claim.
* Returns true if a claim was dropped, false otherwise.
*/
bool claim_drop(PrivateAllocatorCapabilityState &owner,
MChunkHeader &chunk,
bool reallyDrop)
{
Debug::log(
"Trying to drop claim with {} ({})", owner.identifier, &owner);
auto [next, claim] = claim_find(owner.identifier, chunk);
// If there is no claim, fail.
if (claim == nullptr)
{
return false;
}
if (!reallyDrop)
{
return true;
}
// Drop the reference. If this results in the last reference going
// away, destroy this claim structure.
if (claim->reference_remove())
{
next = claim->encoded_next();
size_t size = chunk.size_get();
owner.quota += size;
Claim::destroy(owner, claim);
Debug::log("Dropped last claim, refunding {}-byte quota for {}",
size,
chunk.body());
}
return true;
}
/**
* Having found a chunk, try to free it with the provided owner. The size
* of the chunk is provided by the caller as `bodySize`. If `isPrecise` is
* false then this will drop a claim but will not free the object as the
* owner. If `reallyFree` is false then this will not actually perform the
* operation it will simply report whether it *would* succeed.
*
* Returns 0 on success, `-EPERM` if the provided owner cannot free this
* chunk.
*/
__noinline int heap_free_chunk(PrivateAllocatorCapabilityState &owner,
MChunkHeader &chunk,
size_t bodySize,
bool isPrecise = true,
bool reallyFree = true)
{
// If this is a precise allocation, see if we can free it as the
// original owner. You may drop claims with a capability that is a
// subset of the original but you may not free an object with a subset.
if (isPrecise && (chunk.owner() == owner.identifier))
{
if (!reallyFree)
{
return 0;
}
size_t chunkSize = chunk.size_get();
chunk.ownerID = 0;
if (chunk.claims == 0)
{
int ret = gm->mspace_free(chunk, bodySize);
// If free fails, don't manipulate the quota.
if (ret == 0)
{
owner.quota += chunkSize;
}
return ret;
}
// We've removed the owner, so refund the quota immediately, the
// free won't happen until the last claim goes away, but this is no
// longer the owner's responsibility.
owner.quota += chunkSize;
return 0;
}
// If this is an interior (but valid) pointer, see if we can drop a
// claim.
if (claim_drop(owner, chunk, reallyFree))
{
if ((chunk.claims == 0) && (chunk.ownerID == 0))
{
return gm->mspace_free(chunk, bodySize);
}
return 0;
}
return -EPERM;
}
__noinline int
heap_free_internal(SObj heapCapability, void *rawPointer, bool reallyFree)
{
auto *capability = malloc_capability_unseal(heapCapability);
if (capability == nullptr)
{
Debug::log("Invalid heap capability {}", heapCapability);
return -EPERM;
}
Capability<void> mem{rawPointer};
if (!mem.is_valid())
{
return -EINVAL;
}
check_gm();
// Find the chunk that corresponds to this allocation.
auto *chunk = gm->allocation_start(mem.address());
if (!chunk)
{
return -EINVAL;
}
ptraddr_t start = chunk->body().address();
size_t bodySize = gm->chunk_body_size(*chunk);
// Is the pointer that we're freeing a pointer to the entire allocation?
bool isPrecise = (start == mem.base()) && (bodySize == mem.length());
return heap_free_chunk(
*capability, *chunk, bodySize, isPrecise, reallyFree);
}
} // namespace
__cheriot_minimum_stack(0x80) ssize_t
heap_quota_remaining(struct SObjStruct *heapCapability)
{
STACK_CHECK(0x80);
LockGuard g{lock};
auto *cap = malloc_capability_unseal(heapCapability);
if (cap == nullptr)
{
return -1;
}
return cap->quota;
}
__cheriot_minimum_stack(0xb0) void heap_quarantine_empty()
{
STACK_CHECK(0xb0);
LockGuard g{lock};
while (gm->heapQuarantineSize > 0)
{
if (!gm->quarantine_dequeue())
{
revoker.system_bg_revoker_kick();
}
g.unlock();
yield();
g.lock();
}
}
__cheriot_minimum_stack(0x1f0) void *heap_allocate(Timeout *timeout,
SObj heapCapability,
size_t bytes)
{
STACK_CHECK(0x1f0);
if (!check_timeout_pointer(timeout))
{
return nullptr;
}
LockGuard g{lock};
auto *cap = malloc_capability_unseal(heapCapability);
if (cap == nullptr)
{
return nullptr;
}
if (!check_pointer<PermissionSet{Permission::Load, Permission::Store}>(
timeout))
{
return nullptr;
}
// Use the default memory space.
return malloc_internal(bytes, std::move(g), cap, timeout);
}
__cheriot_minimum_stack(0x1b0) ssize_t
heap_claim(SObj heapCapability, void *pointer)
{
STACK_CHECK(0x1b0);
LockGuard g{lock};
auto *cap = malloc_capability_unseal(heapCapability);
if (cap == nullptr)
{
Debug::log("Invalid heap cap");
return 0;
}
if (!Capability{pointer}.is_valid())
{
Debug::log("Invalid claimed cap");
return 0;
}
auto *chunk = gm->allocation_start(Capability{pointer}.address());
if (chunk == nullptr)
{
Debug::log("chunk not found");
return 0;
}
if (claim_add(*cap, *chunk))
{
return gm->chunk_body_size(*chunk);
}
Debug::log("failed to add claim");
return 0;
}
__cheriot_minimum_stack(0xe0) int heap_can_free(SObj heapCapability,
void *rawPointer)
{
STACK_CHECK(0xe0);
LockGuard g{lock};
return heap_free_internal(heapCapability, rawPointer, false);
}
__cheriot_minimum_stack(0x250) int heap_free(SObj heapCapability,
void *rawPointer)
{
STACK_CHECK(0x250);
LockGuard g{lock};
int ret = heap_free_internal(heapCapability, rawPointer, true);
if (ret != 0)
{
return ret;
}
// If there are any threads blocked allocating memory, wake them up.
if (freeFutex != -1)
{
Debug::log("Some threads are blocking on allocations, waking them");
freeFutex = -1;
freeFutex.notify_all();
}
return 0;
}
__cheriot_minimum_stack(0x180) ssize_t heap_free_all(SObj heapCapability)
{
STACK_CHECK(0x180);
LockGuard g{lock};
auto *capability = malloc_capability_unseal(heapCapability);
if (capability == nullptr)
{
Debug::log("Invalid heap capability {}", heapCapability);
return -EPERM;
}
auto chunk = gm->heapStart.cast<MChunkHeader>();
ptraddr_t heapEnd = chunk.top();
ssize_t freed = 0;
do
{
if (chunk->is_in_use() && !chunk->isSealedObject)
{
auto size = chunk->size_get();
if (heap_free_chunk(
*capability, *chunk, gm->chunk_body_size(*chunk)) == 0)
{
freed += size;
}
}
chunk = static_cast<MChunkHeader *>(chunk->cell_next());
} while (chunk.address() < heapEnd);
// If there are any threads blocked allocating memory, wake them up.
if ((freeFutex > 0) && (freed > 0))
{
Debug::log("Some threads are blocking on allocations, waking them");
freeFutex = 0;
freeFutex.notify_all();
}
return freed;
}
__cheriot_minimum_stack(0x1f0) void *heap_allocate_array(Timeout *timeout,
SObj heapCapability,
size_t nElements,
size_t elemSize)
{
STACK_CHECK(0x1f0);
if (!check_timeout_pointer(timeout))
{
return nullptr;
}
LockGuard g{lock};
auto *cap = malloc_capability_unseal(heapCapability);
if (cap == nullptr)
{
return nullptr;
}
// Use the default memory space.
size_t req;
if (__builtin_mul_overflow(nElements, elemSize, &req))
{
return nullptr;
}
if (!check_pointer<PermissionSet{Permission::Load, Permission::Store}>(
timeout))
{
return nullptr;
}
return malloc_internal(req, std::move(g), cap, timeout);
}
namespace
{
/**
* The next sealing type to allocate. Sealing types are allocated from the
* top down.
*/
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.
*/
SealedAllocation unseal_if_valid(SealedAllocation in)
{
// The input must be tagged and sealed with our type.
// 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);
return in.is_valid() ? in : SealedAllocation{nullptr};
}
/**
* Helper that allocates a sealed object and returns the sealed and
* unsealed capabilities to the object. Requires that the sealing key have
* all of the permissions in `permissions`.
*/
std::pair<SObj, void *>
__noinline allocate_sealed_unsealed(Timeout *timeout,
SObj heapCapability,
SealingKey key,
size_t sz,
PermissionSet permissions)
{
if (!check_pointer<PermissionSet{Permission::Load, Permission::Store}>(
timeout))
{
return {nullptr, nullptr};
}
if (!permissions.can_derive_from(key.permissions()))
{
Debug::log(
"Operation requires {}, cannot derive from {}", permissions, key);
return {nullptr, nullptr};
}
if (sz > 0xfe8 - ObjHdrSize)
{
Debug::log("Cannot allocate sealed object of {} bytes, too large",
sz);
// TODO: Properly handle imprecision.
return {nullptr, nullptr};
}
LockGuard g{lock};
auto *capability = malloc_capability_unseal(heapCapability);
if (capability == nullptr)
{
return {nullptr, nullptr};
}
SealedAllocation obj{static_cast<SObj>(malloc_internal(
sz + ObjHdrSize, std::move(g), capability, timeout, true))};
if (obj == nullptr)
{
Debug::log("Underlying allocation failed for sealed object");
return {nullptr, nullptr};
}
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);
return {sealed, obj};
}
} // namespace
SKey token_key_new()
{
// This needs protecting against races but doesn't touch any other data
// structures and so can have its own lock.
static FlagLock tokenLock;
LockGuard g{tokenLock};
auto keyRoot = software_sealing_key();
// For now, strip the user permissions. We might want to use them for
// permit-allocate and permit-free.
keyRoot.permissions() &=
{Permission::Global, Permission::Seal, Permission::Unseal};
// Allocate sealing types from the top.
if (keyRoot.base() < nextSealingType - 1)
{
auto key = keyRoot;
key.address() = --nextSealingType;
key.bounds() = 1;
Debug::log("Allocated sealing capability: {}", key);
return key;
}
return nullptr;
}
__cheriot_minimum_stack(0x250) SObj
token_sealed_unsealed_alloc(Timeout *timeout,
SObj heapCapability,
SKey key,
size_t sz,
void **unsealed)
{
STACK_CHECK(0x250);
if (!check_timeout_pointer(timeout))
{
return INVALID_SOBJ;
}
auto [sealed, obj] = allocate_sealed_unsealed(
timeout, heapCapability, key, sz, {Permission::Seal, Permission::Unseal});
{
LockGuard g{lock};
if (check_pointer<PermissionSet{
Permission::Store, Permission::LoadStoreCapability}>(unsealed))
{
*unsealed = obj;
return sealed;
}
}
heap_free(heapCapability, obj);
return INVALID_SOBJ;
}
__cheriot_minimum_stack(0x250) SObj token_sealed_alloc(Timeout *timeout,
SObj heapCapability,
SKey rawKey,
size_t sz)
{
STACK_CHECK(0x250);
return allocate_sealed_unsealed(
timeout, heapCapability, rawKey, sz, {Permission::Seal})
.first;
}
/**
* Helper used to unseal a sealed object with a given key. Performs all of the
* relevant checks and returns nullptr if this is not a valid key and object
* sealed with that key.
*/
__noinline static SealedAllocation unseal_internal(SKey rawKey, SObj obj)
{
SealingKey key{rawKey};
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;
}
__cheriot_minimum_stack(0x250) int token_obj_destroy(SObj heapCapability,
SKey key,
SObj object)
{
STACK_CHECK(0x250);
void *unsealed;
{
LockGuard g{lock};
unsealed = unseal_internal(key, object);
if (unsealed == nullptr)
{
return -EINVAL;
}
// At this point, we drop and reacquire the lock. This is better for
// code reuse and heap_free will catch races because it will check the
// revocation state.
// The key can't be revoked and so there is no race with the key going
// away after the check.
}
return heap_free(heapCapability, unsealed);
}
__cheriot_minimum_stack(0xe0) int token_obj_can_destroy(SObj heapCapability,
SKey key,
SObj object)
{
STACK_CHECK(0xe0);
void *unsealed;
{
LockGuard g{lock};
unsealed = unseal_internal(key, object);
if (unsealed == nullptr)
{
return -EINVAL;
}
// At this point, we drop and reacquire the lock. This is better for
// code reuse and heap_can_free will catch races because it will check
// the revocation state.
// The key can't be revoked and so there is no race with the key going
// away after the check.
}
return heap_can_free(heapCapability, unsealed);
}
size_t heap_available()
{
return gm->heapFreeSize;
}