blob: e41038a31e63c3b376262a95d0030670b8f57679 [file] [log] [blame]
// Copyright Microsoft and CHERIoT Contributors.
// SPDX-License-Identifier: MIT
#include <cdefs.h>
// memcpy is exposed as a libcall in the standard library headers but we want
// to ensure that our version is called directly and not exposed to anything
// else.
#undef __cheri_libcall
#define __cheri_libcall
#include <string.h>
#include "../switcher/tstack.h"
#include "constants.h"
#include "debug.hh"
#include "defines.h"
#include "types.h"
#include <cheri.hh>
#include <compartment.h>
#include <platform-uart.hh>
#include <priv/riscv.h>
#include <riscvreg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
using namespace CHERI;
namespace
{
/**
* Round up to a multiple of `Multiple`, which must be a power of two.
*/
template<size_t Multiple>
constexpr size_t round_up(size_t value)
{
static_assert((Multiple & (Multiple - 1)) == 0,
"Multiple must be a power of two");
return (value + Multiple - 1) & -Multiple;
}
static_assert(round_up<16>(15) == 16);
static_assert(round_up<16>(28) == 32);
static_assert(round_up<8>(17) == 24);
__BEGIN_DECLS
static_assert(CheckSize<CHERIOT_LOADER_TRUSTED_STACK_SIZE,
sizeof(TrustedStackGeneric<0>)>::value,
"Boot trusted stack sizes do not match.");
// 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(
CheckSize<sizeof(ThreadLoaderInfo), BOOT_THREADINFO_SZ>::value);
/**
* Reserved sealing types.
*/
enum SealingType
{
/**
* 0 represents unsealed.
*/
Unsealed = CheriSealTypeUnsealed,
/**
* Sentry that inherits interrupt status.
*/
SentryInheriting = CheriSealTypeSentryInheriting,
/// Alternative name: the default sentry type.
Sentry = SentryInheriting,
/**
* Sentry that disables interrupts on calls.
*/
SentryDisabling = CheriSealTypeSentryDisabling,
/**
* Sentry that enables interrupts on calls.
*/
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 = CheriSealTypeFirstDataSealingType,
/**
* The sealing type used for sealed export table entries.
*/
SealedImportTableEntries = CheriSealTypeSealedImportTableEntries,
/**
* 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.
*/
SealedTrustedStacks = CheriSealTypeSealedTrustedStacks,
/**
* The allocator has a sealing type for the software sealing mechanism
* with dynamically allocated objects.
*/
Allocator = 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.
*/
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 = CheriSealTypeFirstStaticSoftware,
/**
* The first sealing key in the space that the allocator will
* dynamically allocate for sealing types.
*/
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>() <= 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 =
Root::Permissions<Root::Type::Execute>;
/// PCC permissions for unprivileged compartments
constexpr auto UserPccPermissions =
Root::Permissions<Root::Type::Execute>.without(
Permission::AccessSystemRegisters);
template<typename T, typename U>
T align_up(T x, U align)
{
return __builtin_align_up(x, align);
}
/**
* Returns a capability of type T* derived from the root specified by Type,
* with the specified permissions. The start and length are given as
* arguments.
*/
template<typename T = void,
Root::Type Type = Root::Type::RWGlobal,
PermissionSet Permissions = Root::Permissions<Type>,
bool Precise = true>
Capability<T> build(ptraddr_t start, size_t length)
{
return static_cast<T *>(
Root::build_from_root<Type, Permissions, Precise>(start, length));
}
/**
* Builds a capability with bounds to access a single object of type `T`,
* which starts at address `start`. The root and permissions are specified
* as template arguments.
*/
template<typename T,
Root::Type Type = Root::Type::RWGlobal,
PermissionSet Permissions = Root::Permissions<Type>>
Capability<T> build(ptraddr_t start)
{
return build<T, Type, Permissions>(start, sizeof(T));
}
/**
* Builds a capability with bounds specified by `start` and `length`, which
* points to an object of type `T`, at address `address`. This is derived
* from the root and with the permissions given as template arguments.
*/
template<typename T = void,
Root::Type Type = Root::Type::RWGlobal,
PermissionSet Permissions = Root::Permissions<Type>>
Capability<T> build(ptraddr_t start, size_t length, ptraddr_t address)
{
Capability<T> ret{static_cast<T *>(
Root::build_from_root<Type, Permissions>(start, length))};
ret.address() = address;
return ret;
}
/**
* Build a capability covering a range specified by a range (
*/
template<typename T = void,
Root::Type Type = Root::Type::RWGlobal,
PermissionSet Permissions = Root::Permissions<Type>,
bool Precise = true>
Capability<T> build(auto &&range) requires(RawAddressRange<decltype(range)>)
{
return build<T, Type, Permissions, Precise>(range.start(),
range.size());
}
/**
* Build a capability to an object of type `T` from a range (start and size
* address).
*/
template<typename T = void,
Root::Type Type = Root::Type::RWGlobal,
PermissionSet Permissions = Root::Permissions<Type>>
Capability<T>
build(auto &&range,
ptraddr_t address) requires(RawAddressRange<decltype(range)>)
{
return build<T, Type, Permissions>(
range.start(), range.size(), address);
}
/**
* Build the PCC for a compartment. Permissions can be overridden for more
* / less privilege compartments.
*/
template<const PermissionSet Permissions = UserPccPermissions>
Capability<void> build_pcc(const auto &compartment)
{
return build<void, Root::Type::Execute, Permissions>(compartment.code);
}
/**
* Build a capability to a compartment's globals. Permissions can be
* overridden via a template parameter for non-default options.
*
* By default, this function returns a biased $cgp value: the address
* points to the middle of the range. This can be disabled by passing
* `false` as the second template parameter.
*/
Capability<void> build_cgp(const auto &compartment, bool bias = true)
{
auto cgp = build<void, Root::Type::RWGlobal>(compartment.data);
if (bias)
{
cgp.address() += (compartment.data.size() / 2);
}
return cgp;
}
/**
* Returns a sealing capability to use for statically allocated sealing
* keys.
*/
uint16_t allocate_static_sealing_key()
{
static uint16_t nextValue = FirstStaticSoftware;
// We currently stash the allocated key value in the export table. We
// could expand this a bit if we were a bit more clever in how we used
// that space, but 2^16 static sealing keys will require over 768 KiB
// of SRAM to store in the firmware, which seems excessive.
Debug::Invariant(nextValue < std::numeric_limits<uint16_t>::max(),
"Out of static sealing keys");
return nextValue++;
}
/**
* Returns a sealing capability in the sealing space with the specified
* type.
*/
void *build_static_sealing_key(uint16_t type)
{
static void *staticSealingRoot;
Debug::Invariant(type >= FirstStaticSoftware,
"{} is not a valid software sealing key",
type);
if (staticSealingRoot == nullptr)
{
staticSealingRoot =
build<void,
Root::Type::Seal,
PermissionSet{Permission::Global,
Permission::Seal,
Permission::Unseal,
Permission::User0}>(0, FirstDynamicSoftware);
}
Capability next = staticSealingRoot;
next.address() = type;
next.bounds() = 1;
Debug::Invariant(
next.is_valid(), "Invalid static sealing key {}", next);
return next;
}
template<typename T>
T *seal_entry(Capability<T> ptr, InterruptStatus status)
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wc99-designator"
constexpr SealingType Sentries[] = {
[int(InterruptStatus::Enabled)] = SentryEnabling,
[int(InterruptStatus::Disabled)] = SentryDisabling,
[int(InterruptStatus::Inherited)] = SentryInheriting};
#pragma clang diagnostic pop
Debug::Invariant(
unsigned(status) < 3, "Invalid interrupt status {}", int(status));
size_t otype = size_t{Sentries[int(status)]};
void *key = build<void, Root::Type::Seal>(otype, 1);
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.
*/
bool contains(const auto &range,
ptraddr_t addr,
size_t size) requires(RawAddressRange<decltype(range)>)
{
return (range.start() <= addr) &&
(range.start() + range.size() >= addr + size);
}
/**
* Helper to determine whether an address is within a range. The template
* parameter specifies the type that the object is expected to be. The
* object must be completely contained within the range.
*/
template<typename T = char>
bool contains(const auto &range,
ptraddr_t addr) requires(RawAddressRange<decltype(range)>)
{
return contains(range, addr, sizeof(T));
}
/**
* Helper class representing a range (which can be used with range-based
* for loops) built from pointers to contiguous memory.
*/
template<typename T>
class ContiguousPtrRange
{
/**
* Pointer to the first element.
*/
T *start;
/**
* Pointer to one past the last element.
*/
T *finish;
public:
/**
* Constructor, takes pointers to the beginning and end of an array.
*/
ContiguousPtrRange(T *s, T *e) : start(s), finish(e) {}
/**
* Returns a pointer to the start.
*/
T *begin()
{
return start;
}
/**
* Returns a pointer to the end.
*/
T *end()
{
return finish;
}
};
/**
* Build an range for use with range-based for loops iterating over objects
* of type `T` from a virtual address range.
*/
template<typename T, bool Precise = true>
ContiguousPtrRange<T>
build_range(const auto &range) requires(RawAddressRange<decltype(range)>)
{
Capability<T> start = build<T,
Root::Type::RWGlobal,
Root::Permissions<Root::Type::RWGlobal>,
Precise>(range);
Capability<T> end = start;
end.address() += range.size();
return {start, end};
}
/**
* The sealing key for the switcher, used to seal all jump targets in the
* import tables.
*/
Capability<void> switcherKey;
/**
* The sealing key for sealing trusted stacks.
*/
Capability<void> trustedStackKey;
/**
* Find an export table target. This looks for the `target` address within
* all of the export tables in the image. The `size` parameter is used if
* this is an MMIO import, the `lib` parameter is used to derive
* capabilities for the library compartment.
*/
void *find_export_target(const ImgHdr &image,
const auto &sourceCompartment,
ImportEntry &entry)
{
// Build an MMIO capability.
auto buildMMIO = [&]() {
Debug::log("Building mmio capability {} + {} (permissions: {})",
entry.address,
entry.size(),
entry.permissions());
if constexpr (std::is_same_v<
std::remove_cvref_t<decltype(sourceCompartment)>,
ImgHdr::PrivilegedCompartment>)
{
if (&sourceCompartment == &image.allocator())
{
if (entry.address == LA_ABS(__export_mem_heap) &&
(entry.size() == (LA_ABS(__export_mem_heap_end) -
LA_ABS(__export_mem_heap))))
{
ptraddr_t end = entry.address + entry.size();
Debug::log(
"Rounding heap ({}--{}) region", entry.address, end);
size_t sizeMask =
__builtin_cheri_representable_alignment_mask(
entry.size());
Debug::log("Applying mask {} to entry.size", sizeMask);
size_t roundedSize = entry.size() & sizeMask;
ptraddr_t roundedBase = end - roundedSize;
Debug::log("Rounding heap entry.size down from {} to "
"{} (rounded up "
"to {})",
entry.size(),
roundedSize,
__builtin_cheri_round_representable_length(
entry.size()));
Debug::Invariant(
(end & ~sizeMask) == 0,
"End of heap ({}) is not sufficiently aligned ({})",
end,
sizeMask);
Debug::log(
"Assigning rounded heap (in {}--{}) to the allocator",
roundedBase,
roundedBase + roundedSize);
Debug::Invariant(
roundedBase >= entry.address,
"Rounding heap base ({}) up to {} rounded down!",
entry.address,
roundedBase);
auto heap = build(roundedBase, roundedSize);
Debug::log("Heap: {}", heap);
Debug::Assert(heap.is_valid(),
"Heap capability rounding went wrong "
"somehow ({} is untagged)",
heap);
return heap;
}
}
}
// MMIO regions should be within the range of the MMIO space. As a
// special case (to be removed at some point when we have a generic
// way of setting up shared objects), allow the hazard-pointer
// region and hazard epoch to be excluded from this test.
Debug::Invariant(
((entry.address >= LA_ABS(__mmio_region_start)) &&
(entry.address + entry.size() <= LA_ABS(__mmio_region_end))) ||
((entry.address >= LA_ABS(__shared_objects_start)) &&
(entry.address + entry.size() <=
LA_ABS(__shared_objects_end))),
"{}--{} is not in the MMIO range ({}--{}) or the shared object "
"range ({}--{})",
entry.address,
entry.address + entry.size(),
LA_ABS(__mmio_region_start),
LA_ABS(__mmio_region_end),
LA_ABS(__shared_objects_start),
LA_ABS(__shared_objects_end));
auto ret = build(entry.address, entry.size());
// Remove any permissions that shouldn't be held here.
ret.permissions() &= entry.permissions();
return ret;
};
// Build an export table entry for the given compartment.
auto buildExportEntry = [&](const auto &compartment) {
auto exportEntry = build(compartment.exportTable, entry.address)
.template cast<ExportEntry>();
auto interruptStatus = exportEntry->interrupt_status();
Debug::Invariant((interruptStatus == InterruptStatus::Enabled) ||
(interruptStatus == InterruptStatus::Disabled),
"Functions exported from compartments must have "
"an explicit interrupt posture");
return build(compartment.exportTable, entry.address)
.seal(switcherKey);
};
// If the low bit is 1, then this is either an MMIO region or direct
// call via a sentry. The latter case covers code in shared
// (stateless) libraries and explicit interrupt-toggling sentries for
// code within the current compartment.
// First check if it's a sentry call.
if (entry.address & 1)
{
// Clear the low bit to give the real address.
auto possibleLibcall = entry.address & ~1U;
// Helper to create the target of a library call.
auto createLibCall = [&](Capability<void> pcc) {
// Libcall export table entries are just sentry capabilities to
// the real target. We need to get the address and interrupt
// status of the function from the target's export table and
// then construct a sentry of the correct kind derived from the
// compartment's PCC.
auto ent = build<ExportEntry>(possibleLibcall);
pcc.address() += ent->functionStart;
return seal_entry(pcc, ent->interrupt_status());
};
// If this is a libcall, set it up
for (auto &lib : image.libraries())
{
if (contains<ExportEntry>(lib.exportTable, possibleLibcall))
{
// TODO: Library export tables are not used after the
// loader has run, we could move them to the end of the
// image and make that space available for the heap.
return createLibCall(build_pcc(lib));
}
}
for (auto &compartment : image.privilegedCompartments)
{
if (!compartment.is_privileged_library())
{
continue;
}
if (contains<ExportEntry>(compartment.exportTable,
possibleLibcall))
{
// TODO: Privileged library export tables should be moved
// to the end of the image as well.
return createLibCall(build_pcc(compartment));
}
}
// The switcher is a special case, it needs a richer set of
// permission (access system register) than other compartments, but
// is exposed as a library.
if (contains<ExportEntry>(image.switcher.exportTable,
possibleLibcall))
{
auto ent = build<ExportEntry>(possibleLibcall);
auto pcc =
build<void, Root::Type::Execute, SwitcherPccPermissions>(
image.switcher.code);
pcc.address() += ent->functionStart;
return seal_entry(pcc, ent->interrupt_status());
}
// We also use the library calling convention for local callbacks,
// so see if this points to our own export table.
if (contains<ExportEntry>(sourceCompartment.exportTable,
possibleLibcall))
{
return createLibCall(build_pcc(sourceCompartment));
}
// Otherwise this is an MMIO space entry (we allow byte-granularity
// delegation of MMIO objects, so a low bit of 1 might be a
// coincidence).
return buildMMIO();
}
{
if (contains(
sourceCompartment.sealedObjects, entry.address, entry.size()))
{
auto sealingType =
build<uint32_t,
Root::Type::RWGlobal,
PermissionSet{Permission::Load, Permission::Store}>(
entry.address);
// 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
// yet, then we should update its first word to point to the
// sealing type.
if (*sealingType >
std::numeric_limits<
decltype(ExportEntry::functionStart)>::max())
{
auto typeAddress = *sealingType;
auto findExport = [&](auto &compartment) {
if (contains<ExportEntry>(compartment.exportTable,
typeAddress))
{
auto exportEntry = build<ExportEntry>(
compartment.exportTable, typeAddress);
Debug::Invariant(
exportEntry->is_sealing_type(),
"Sealed object points to invalid sealing type");
*sealingType = exportEntry->functionStart;
return true;
}
return false;
};
bool found = findExport(image.allocator());
found |= findExport(image.scheduler());
for (auto &compartment : image.compartments())
{
if (found)
{
break;
}
found = findExport(compartment);
}
Debug::Invariant(*sealingType != typeAddress,
"Invalid sealed object {}",
typeAddress);
}
Capability sealedObject = build(entry.address, entry.size());
// Seal with the allocator's sealing key
sealedObject.seal(
build<void, Root::Type::Seal>(StaticToken, 1));
Debug::log("Static sealed object: {}", sealedObject);
return sealedObject;
}
}
for (auto &compartment : image.privilegedCompartments)
{
if (contains<ExportEntry>(compartment.exportTable, entry.address))
{
return buildExportEntry(compartment);
}
}
for (auto &compartment : image.compartments())
{
if (contains<ExportEntry>(compartment.exportTable, entry.address))
{
return buildExportEntry(compartment);
}
}
return buildMMIO();
}
/**
* As a first pass, scan the import table of this compartment and resolve
* any static sealing types.
*/
void populate_static_sealing_keys(const ImgHdr &image,
const auto &compartment)
{
if (compartment.exportTable.size() == 0)
{
return;
}
const auto &importTable = compartment.import_table();
if (importTable.size() == 0)
{
return;
}
// The import table might not have strongly aligned bounds and so we
// are happy with an imprecise capability here.
auto impPtr = build<ImportTable,
Root::Type::RWGlobal,
Root::Permissions<Root::Type::RWGlobal>,
false>(importTable);
// FIXME: This should use a range-based for loop
for (int i = 0; i < (importTable.size() / sizeof(void *)) - 1; i++)
{
ptraddr_t importAddr = impPtr->imports[i].address;
size_t importSize = impPtr->imports[i].size();
// If the size is not 0, this isn't an import table entry.
if (importSize != 0)
{
continue;
}
// If the low bit is 1, it's either a library import or an MMIO
// import. Skip it either way.
if (importAddr & 1)
{
continue;
}
// If this points anywhere other than the current compartment's
// export table, it isn't a sealing capability entry.
if (!contains(compartment.exportTable, importAddr))
{
continue;
}
// Build an export table entry for the given compartment.
auto exportEntry =
build<ExportEntry>(compartment.exportTable, importAddr);
// If the export entry isn't a sealing type, this is not a
// reference to a sealing capability.
if (!exportEntry->is_sealing_type())
{
continue;
}
Debug::Invariant(exportEntry->functionStart == 0,
"Two import entries point to the same export "
"entry for a sealing key {}",
exportEntry);
// Allocate a new sealing key type.
exportEntry->functionStart = allocate_static_sealing_key();
Debug::log("Creating sealing key {}", exportEntry->functionStart);
// Build the sealing key corresponding to that type.
impPtr->imports[i].pointer =
build_static_sealing_key(exportEntry->functionStart);
}
}
/**
* Populate an import table. The import table is described by the
* `importTable` argument. The compartment switcher and the library
* compartment are passed as arguments.
*/
void populate_imports(const ImgHdr &image,
const auto &sourceCompartment,
void *switcher)
{
const auto &importTable = sourceCompartment.import_table();
if (importTable.size() == 0)
{
return;
}
Debug::log("Import table: {}, {} bytes",
importTable.start(),
importTable.size());
// The import table might not have strongly aligned bounds and so we
// are happy with an imprecise capability here.
auto importTablePointer = build<ImportTable,
Root::Type::RWStoreL,
Root::Permissions<Root::Type::RWStoreL>,
false>(importTable);
importTablePointer->switcher = switcher;
// FIXME: This should use a range-based for loop
for (int i = 0; i < (importTable.size() / sizeof(void *)) - 1; i++)
{
// If this is a sealing key then we will have initialised it
// already, skip it now.
if (Capability{importTablePointer->imports[i].pointer}.is_valid())
{
Debug::log("Skipping sealing type import");
continue;
}
importTablePointer->imports[i].pointer = find_export_target(
image, sourceCompartment, importTablePointer->imports[i]);
}
}
/**
* Construct the boot threads.
*/
void boot_threads_create(const ImgHdr &image, ThreadLoaderInfo *threadInfo)
{
// Two hazard pointers per thread. More makes free slow, fewer is hard
// to use.
static constexpr size_t HazardPointersPerThread = 2;
Capability<void *> hazardPointers =
build<void *,
Root::Type::RWGlobal,
PermissionSet{Permission::Store,
Permission::LoadStoreCapability}>(
LA_ABS(__cheriot_shared_object_allocator_hazard_pointers),
LA_ABS(__cheriot_shared_object_allocator_hazard_pointers_end) -
LA_ABS(__cheriot_shared_object_allocator_hazard_pointers));
// 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);
auto findCompartment = [&]() -> auto &
{
for (auto &compartment : image.compartments())
{
Debug::log("Looking in export table {}+{}",
compartment.exportTable.start(),
compartment.exportTable.size());
if (contains(compartment.exportTable, config.entryPoint))
{
return compartment;
}
}
Debug::Invariant(
false, "Compartment entry point is not a valid export");
__builtin_unreachable();
};
const auto &compartment = findCompartment();
Debug::log("Creating thread in compartment {}", &compartment);
auto pcc = build_pcc(compartment);
pcc.address() +=
build<ExportEntry>(config.entryPoint)->functionStart;
Debug::log("New thread's pcc will be {}", pcc);
void *cgp = build_cgp(compartment);
Debug::log("New thread's cgp will be {}", cgp);
auto threadTStack =
build<TrustedStack,
Root::Type::TrustedStack,
Root::Permissions<Root::Type::TrustedStack>,
false>(config.trustedStack);
threadTStack->mepcc = pcc;
threadTStack->cgp = cgp;
threadTStack->cra = threadInitialReturn;
// Stacks have store-local but not global permission.
auto stack =
build<void,
Root::Type::TrustedStack,
Root::Permissions<Root::Type::TrustedStack>.without(
Permission::Global),
false>(config.stack);
// Make sure that the thread's stack doesn't overlap the loader's
// stack (which will become the scheduler's stack).
Capability<void> csp = ({
register void *cspRegister asm("csp");
asm("" : "=C"(cspRegister));
cspRegister;
});
if (stack.top() <= csp.top())
{
Debug::Invariant(
stack.top() <= csp.base(),
"Thread stack {} for thread {} overlaps loader stack {}",
stack,
i,
csp);
}
if (stack.base() >= csp.base())
{
Debug::Invariant(
stack.base() >= csp.top(),
"Thread stack {} for thread {} overlaps loader stack {}",
stack,
i,
csp);
}
// Stack pointer points to the top of the stack.
stack.address() += config.stack.size();
// Reserve space at the start of the stack for error handling and so
// on.
stack.address() -= STACK_ENTRY_RESERVED_SPACE;
Debug::log("Thread's stack is {}", stack);
threadTStack->csp = stack;
// Set up the space for hazard pointers.
Capability threadHazardPointers{hazardPointers};
threadHazardPointers.address() += (i * HazardPointerSpace);
threadHazardPointers.bounds() = HazardPointerSpace;
threadTStack->hazardPointers = threadHazardPointers;
// Enable previous level interrupts and set the previous exception
// level to M mode.
threadTStack->mstatus =
(priv::MSTATUS_MPIE |
(priv::MSTATUS_PRV_M << priv::MSTATUS_MPP_SHIFT));
#ifdef CONFIG_MSHWM
threadTStack->mshwm = stack.top();
threadTStack->mshwmb = stack.base();
#endif
// Set the thread ID that the switcher will return for this thread.
// This is indexed from 1, so 0 can be used to indicate the idle
// thread.
threadTStack->threadID = i + 1;
threadTStack->frameoffset = offsetof(TrustedStack, frames[1]);
threadTStack->frames[0].calleeExportTable =
build(compartment.exportTable);
// Special case: The first frame has the initial csp.
threadTStack->frames[0].csp = stack;
Debug::log("Thread's trusted stack is {}", threadTStack);
threadTStack.seal(trustedStackKey);
threadInfo[i].trustedStack = threadTStack;
threadInfo[i].priority = config.priority;
i++;
}
Debug::log("Finished creating threads");
}
/**
* Resolve capability relocations.
*
* Note that this assumes that the firmware image was checked to ensure
* that no compartment ships with cap relocs that point to another
* compartment. This should be impossible due to how they flow through the
* linker but needs to be part of a static auditing pipeline.
*/
void populate_caprelocs(const ImgHdr &image)
{
// Helper to give the cap relocs section as a range.
struct
{
[[nodiscard]] ptraddr_t start() const
{
return LA_ABS(__cap_relocs);
}
[[nodiscard]] size_t size() const
{
return LA_ABS(__cap_relocs_end) - LA_ABS(__cap_relocs);
}
} capRelocsSection;
// Find the library compartment that contains an address in its code or
// data section.
auto findCompartment = [&](ptraddr_t address) -> auto &
{
Debug::log("Capreloc address is {}", address);
for (auto &compartment : image.libraries_and_compartments())
{
if (contains(compartment.code, address) ||
contains(compartment.data, address))
{
return compartment;
}
}
Debug::Invariant(false, "Cannot find compartment for cap reloc");
__builtin_unreachable();
};
Debug::log("Populating cap relocs the insecure way {} + {}",
capRelocsSection.start(),
capRelocsSection.size());
for (auto &reloc : build_range<CapReloc, false>(capRelocsSection))
{
Debug::log(
"Capreloc address: {}, base: {}", reloc.addr, reloc.base);
// Find the compartment that this relocation applies to.
const auto &compartment = findCompartment(reloc.addr);
// Compartment's PCC, used to derive function pointers.
auto pcc = build_pcc(compartment);
// Compartment's PCC with execute dropped. Used for pointers to
// read-only globals.
auto ropcc = pcc;
ropcc.permissions() &=
pcc.permissions().without(Permission::Execute);
// Compartment's globals region, used to derive pointers to globals
// and to write to globals.
auto cgp = build_cgp(compartment, false);
Capability<void> locationRegion;
// Cap relocs for a compartment must point to that compartment's
// code or data regions.
if (contains(compartment.code, reloc.addr))
{
locationRegion =
build<void, Root::Type::RWGlobal>(compartment.code);
}
else if (contains(compartment.data, reloc.addr))
{
locationRegion = cgp;
}
// The location is the address of the reloc, bounded to the region.
Capability<void *> location{locationRegion.cast<void *>()};
location.address() = reloc.addr;
size_t offset = reloc.offset;
Capability<void> cap;
if (reloc.is_function())
{
// If this is a function pointer, use the bounds of that
// compartment's PCC.
// FIXME: In our ABI the linker should emit function pointer
// bounds to be the whole .pcc section, not a single function.
offset += reloc.base - compartment.code.start();
cap = pcc;
}
else
{
if (contains(compartment.code, reloc.base))
{
cap = ropcc;
}
else if (contains(compartment.data, reloc.base))
{
cap = cgp;
}
else
{
Debug::Invariant(
false,
"Cap reloc points to something not owned by "
"the compartment.");
}
// Pointers to globals and read-only data should be bounded as
// requested. Here, we also use setbounds to check reloc.len,
// either untagged or an exception.
cap.address() = reloc.base;
cap.bounds().set_inexact(reloc.len);
}
cap.address() += offset;
Debug::log("Writing cap reloc {}\nto {}\nPCC {}\nCGP {}",
cap,
location,
pcc,
cgp);
*location = cap;
}
}
} // namespace
// The parameters are passed by the boot assembly sequence.
// XXX: arguments have capptr templates, 4 roots
extern "C" SchedulerEntryInfo loader_entry_point(const ImgHdr &imgHdr,
void *almightyPCC,
void *almightySeal,
void *almightyRW)
{
// This relies on a slightly surprising combination of two C++ features:
// - Flexible array members (C99, not technically part of C++ but supported
// basically everywhere).
// - Guaranteed return copy elision (C++17).
//
// This means that the caller can allocate a variable-sized structure and
// this definition will refer to the space allocated by the caller. In a
// CHERI system, the caller will also set bounds, and so this is actually a
// safe thing to do, on any other system it is a terrible idea. This means
// that `ret` points to the space on the stack that was set up by the
// caller and which can subsequently be passed to the scheduler.
SchedulerEntryInfo ret;
// Populate the 4 roots from system registers.
Root::install_root<Root::ISAType::Execute>(almightyPCC);
Root::install_root<Root::ISAType::Seal>(almightySeal);
Root::install_root<Root::ISAType::RW>(almightyRW);
auto uart =
build<volatile Uart,
Root::Type::RWGlobal,
PermissionSet{
Permission::Load, Permission::Store, Permission::Global}>(
LA_ABS(__export_mem_uart));
// Initialise the UART so that we can use it for debugging.
uart->init();
// Set up the UART that's used for debug output.
if constexpr (DebugLoader)
{
// Set the UART. `Debug::log` and `Debug::Invariant` work after this
// point.
LoaderWriter::set_uart(uart);
}
Debug::log("UART initialised!");
Debug::log("Header: {}", &imgHdr);
Debug::log("Magic number: {}", imgHdr.magic);
Debug::Invariant(imgHdr.is_magic_valid(),
"Invalid magic field in header: {}",
imgHdr.magic);
// Do some sanity checking on the headers.
ptraddr_t lastCodeEnd = LA_ABS(__compart_pccs);
ptraddr_t lastDataEnd = LA_ABS(__compart_cgps);
int i = 0;
Debug::log("Checking compartments");
for (auto &header : imgHdr.libraries_and_compartments())
{
Debug::log("Checking compartment headers for compartment {}", i);
Debug::Invariant(
header.code.start() >= LA_ABS(__compart_pccs),
"Compartment {} PCC ({}) is before the PCC section start {}",
i,
header.code.start(),
LA_ABS(__compart_pccs));
Debug::Invariant(
header.code.start() + header.code.size() <=
LA_ABS(__compart_pccs_end),
"Compartment {} PCC ({} + {}) extends after the PCC section end {}",
i,
header.code.start(),
header.code.size(),
LA_ABS(__compart_pccs_end));
Debug::Invariant(
header.code.start() >= lastCodeEnd,
"Compartment {} overlaps previous compartment ({} < {}",
i,
header.code.start(),
lastCodeEnd);
lastCodeEnd = header.code.start() + header.code.size();
if (header.data.size() != 0)
{
Debug::Invariant(
header.data.start() >= LA_ABS(__compart_cgps),
"Compartment {} CGP ({}) is before the CGP section start {}",
i,
header.data.start(),
LA_ABS(__compart_cgps));
Debug::Invariant(header.data.start() + header.data.size() <=
LA_ABS(__compart_cgps_end),
"Compartment {} CGP ({} + {}) extends after the "
"CGP section end {}",
i,
header.data.start(),
header.data.size(),
LA_ABS(__compart_cgps_end));
Debug::Invariant(
header.data.start() >= lastDataEnd,
"Compartment {} overlaps previous compartment ({} < {}",
i,
header.data.start(),
lastDataEnd);
}
lastDataEnd = header.data.start() + header.data.size();
i++;
}
populate_caprelocs(imgHdr);
auto switcherPCC = build<void, Root::Type::Execute, SwitcherPccPermissions>(
imgHdr.switcher.code);
// The switcher entry point is currently not an import table entry in
// compartments, the linker script inserts it as the first element. Making
// it a normal import will require a small compiler change. It is now
// exposed as a normal export, which enables exporting other things from
// the switcher later.
Debug::log("Setting compartment switcher");
auto switcherEntry =
build<ExportEntry>(imgHdr.switcher.exportTable.start() + 20,
imgHdr.switcher.exportTable.size());
switcherPCC.address() = switcherPCC.base() + switcherEntry->functionStart;
Debug::log("Setting compartment switcher address: {}",
switcherPCC.address());
switcherPCC = seal_entry(switcherPCC, InterruptStatus::Disabled);
auto setSealingKey =
[](const auto &compartment,
SealingType lower,
size_t length = 1,
size_t offset = 0,
PermissionSet permissions = PermissionSet{
Permission::Global, Permission::Seal, Permission::Unseal}) {
Debug::log("Creating sealing key {}+{} to store at {} ({}-{})",
lower,
length,
compartment.sealing_key() + offset,
compartment.code.start(),
compartment.code.start() + compartment.code.size());
// Writeable version of the compartment's PCC, for filling in the
// sealing key.
void **location = build<void *, Root::Type::RWStoreL>(
compartment.code, compartment.sealing_key() + offset);
Debug::log("Sealing key location: {}", location);
// Derive a sealing capability of the required length.
auto key =
CHERI::Capability{build<void, Root::Type::Seal>(lower, length)};
key.permissions() &= permissions;
// FIXME: Some compartments need only permit-unseal (e.g. the
// compartment switcher). Drop permit-seal and keep only
// permit-unseal once these are separated.
Debug::log("Sealing key: {}", key);
*location = key;
return key;
};
// Set up the sealing keys for the privileged components.
switcherKey =
setSealingKey(imgHdr.switcher, Sentry, SealedTrustedStacks - Sentry + 1);
// We need only the rights to seal things with the switcher's data sealing
// types, so drop all others and store those two types separately.
trustedStackKey = switcherKey;
trustedStackKey.address() = SealedTrustedStacks;
trustedStackKey.bounds() = 1;
switcherKey.address() = SealedImportTableEntries;
switcherKey.bounds() = 1;
setSealingKey(imgHdr.allocator(), Allocator);
setSealingKey(imgHdr.token_library(),
Allocator,
2, // Allocator and StaticToken
0,
PermissionSet{Permission::Global, Permission::Unseal});
constexpr size_t DynamicSealingLength =
std::numeric_limits<ptraddr_t>::max() - FirstDynamicSoftware + 1;
setSealingKey(imgHdr.allocator(),
FirstDynamicSoftware,
DynamicSealingLength,
sizeof(void *));
// Set up export tables
// Helper to construct a writeable pointer to an export table.
auto getExportTableHeader = [](const auto &range) {
auto header = build<ExportTable>(range);
Debug::Invariant(((header.address()) & 0x7) == 0,
"Export table {} is not capability aligned\n",
header);
return header;
};
for (auto &compartment : imgHdr.privilegedCompartments)
{
auto expTablePtr = getExportTableHeader(compartment.exportTable);
Debug::log("Error handler for compartment is {}",
expTablePtr->errorHandler);
expTablePtr->pcc = build_pcc(compartment);
expTablePtr->cgp = build_cgp(compartment);
}
for (auto &compartment : imgHdr.libraries_and_compartments())
{
auto expTablePtr = getExportTableHeader(compartment.exportTable);
Debug::log("Error handler for compartment is {}",
expTablePtr->errorHandler);
expTablePtr->pcc = build_pcc(compartment);
expTablePtr->cgp = build_cgp(compartment);
}
Debug::log("First pass to find sealing key imports");
// Populate import entries that refer to static sealing keys first.
for (auto &compartment : imgHdr.privilegedCompartments)
{
populate_static_sealing_keys(imgHdr, compartment);
}
for (auto &compartment : imgHdr.libraries_and_compartments())
{
populate_static_sealing_keys(imgHdr, compartment);
}
Debug::log("Creating import tables");
// Populate import tables.
for (auto &compartment : imgHdr.privilegedCompartments)
{
populate_imports(imgHdr, compartment, switcherPCC);
}
for (auto &compartment : imgHdr.libraries_and_compartments())
{
populate_imports(imgHdr, compartment, switcherPCC);
}
Debug::log("Creating boot threads\n");
boot_threads_create(imgHdr, ret.threads);
// Provide the switcher with the capabilities for entering the scheduler.
void *schedCGP = build_cgp(imgHdr.scheduler());
auto exceptionEntryOffset =
build<ExportEntry>(
imgHdr.scheduler().exportTable,
LA_ABS(
__export_sched__ZN5sched15exception_entryEP19TrustedStackGenericILj0EEjjj))
->functionStart;
auto schedExceptionEntry = build_pcc(imgHdr.scheduler());
schedExceptionEntry.address() += exceptionEntryOffset;
schedExceptionEntry =
seal_entry(schedExceptionEntry, InterruptStatus::Disabled);
*build<void *>(imgHdr.switcher.code, imgHdr.switcher.scheduler_pcc()) =
schedExceptionEntry;
*build<void *>(imgHdr.switcher.code, imgHdr.switcher.scheduler_cgp()) =
schedCGP;
// The scheduler will inherit our stack once we're done with it
Capability<void> csp = ({
register void *cspRegister asm("csp");
asm("" : "=C"(cspRegister));
cspRegister;
});
// Reset the stack pointer to the top.
csp.address() = csp.base() + csp.length();
// csp is a local capability so must be written via a store-local cap.
*build<void *, Root::Type::RWStoreL>(imgHdr.switcher.code,
imgHdr.switcher.scheduler_csp()) = csp;
Debug::log(
"Scheduler exception entry configured:\nPCC: {}\nCGP: {}\nCSP: {}",
schedExceptionEntry,
schedCGP,
csp);
#ifdef SOFTWARE_REVOKER
// If we are using a software revoker then we need to provide it with three
// terrifyingly powerful capabilities. These break some of the rules that
// we enforce for everything else, especially the last one, which is a
// stack capability that is reachable from a global. The only code that
// accesses these in the revoker is very small and amenable to auditing
// (the only memory accesses are a load and a store back at the same
// location, with interrupts disabled, to trigger the load barrier).
//
// We use imprecise set-bounds operations here because we need to ensure
// that the regions are completely scanned and scanning slightly more is
// not a problem unless the revoker is compromised. The software revoker
// already has a terrifying set of rights, so this doesn't really make
// things worse and is another good reason to use a hardware revoker.
// Given that hardware revokers are lower power, faster, and more secure,
// there's little reason for the software revoker to be used for anything
// other than testing.
auto scaryCapabilities = build<Capability<void>,
Root::Type::RWStoreL,
Root::Permissions<Root::Type::RWStoreL>,
/* Precise: */ false>(
imgHdr.privilegedCompartments.software_revoker().code.start(),
3 * sizeof(void *));
// Read-write capability to all globals. This is scary because a bug in
// the revoker could violate compartment isolation.
Debug::log("Writing scary capabilities for software revoker to {}",
scaryCapabilities);
scaryCapabilities[0] =
build(LA_ABS(__compart_cgps),
LA_ABS(__compart_cgps_end) - LA_ABS(__compart_cgps));
scaryCapabilities[0].address() = scaryCapabilities[0].base();
Debug::log("Wrote scary capability {}", scaryCapabilities[0]);
// Read-write capability to the whole heap. This is scary because a bug in
// the revoker could violate heap safety.
scaryCapabilities[1] =
build<void,
Root::Type::RWGlobal,
Root::Permissions<Root::Type::RWGlobal>,
false>(LA_ABS(__export_mem_heap),
LA_ABS(__export_mem_heap_end) - LA_ABS(__export_mem_heap));
scaryCapabilities[1].address() = scaryCapabilities[1].base();
Debug::log("Wrote scary capability {}", scaryCapabilities[1]);
// Read-write capability to the entire stack. This is scary because a bug
// in the revoker could violate thread isolation.
scaryCapabilities[2] =
build<void,
Root::Type::RWStoreL,
Root::Permissions<Root::Type::RWStoreL>,
false>(LA_ABS(__stack_space_start),
LA_ABS(__stack_space_end) - LA_ABS(__stack_space_start));
scaryCapabilities[2].address() = scaryCapabilities[2].base();
Debug::log("Wrote scary capability {}", scaryCapabilities[2]);
#endif
// Set up the exception entry point
auto exceptionEntry = build_pcc<SwitcherPccPermissions>(imgHdr.switcher);
exceptionEntry.address() = LA_ABS(exception_entry_asm);
asm volatile("cspecialw mtcc, %0" ::"C"(exceptionEntry.get()));
Debug::log("Set exception entry point to {}", exceptionEntry);
// Construct and enter the scheduler compartment.
auto schedPCC = build_pcc(imgHdr.scheduler());
// Set the scheduler entry point to the scheduler_entry export.
// TODO: We should probably have a separate scheduler linker script that
// exposes this. If the scheduler is no longer creating threads, we can
// probably just set up the trusted stack pointer to be the idle thread and
// invoke the exception entry point.
auto exportEntry = build<ExportEntry>(
imgHdr.scheduler().exportTable,
LA_ABS(__export_sched__ZN5sched15scheduler_entryEPK16ThreadLoaderInfo));
schedPCC.address() += exportEntry->functionStart;
Debug::log("Will return to scheduler entry point: {}", schedPCC);
ret.schedPCC = schedPCC;
ret.schedCGP = schedCGP;
return ret;
}
/**
* a dumb implementation, assuming no overlap and no capabilities
*/
void *memcpy(void *dst, const void *src, size_t n)
{
char *dst0 = static_cast<char *>(dst);
const char *src0 = static_cast<const char *>(src);
for (size_t i = 0; i < n; ++i)
{
dst0[i] = src0[i];
}
return dst0;
}