| // Copyright Microsoft and CHERIoT Contributors. |
| // SPDX-License-Identifier: MIT |
| |
| #pragma once |
| #include "alloc_config.h" |
| #include "software_revoker.h" |
| #include <concepts> |
| #include <riscvreg.h> |
| #include <stdint.h> |
| #include <utils.hh> |
| |
| #if __has_include(<platform-hardware_revoker.hh>) |
| # include <platform-hardware_revoker.hh> |
| #elif defined(TEMPORAL_SAFETY) && !defined(SOFTWARE_REVOKER) |
| # error Hardware revoker requested but no hardware_revoker.hh found |
| #endif |
| |
| namespace Revocation |
| { |
| /** |
| * Concept for a hardware revoker. Boards can provide their own definition |
| * of this, which must be found in `<hardware_revoker.hh>` in a path |
| * provided by the board search. |
| */ |
| template<typename T> |
| concept IsHardwareRevokerDevice = requires(T v, uint32_t epoch) |
| { |
| {v.init()}; |
| { |
| v.system_epoch_get() |
| } -> std::same_as<uint32_t>; |
| { |
| v.template has_revocation_finished_for_epoch<true>(epoch) |
| } -> std::same_as<uint32_t>; |
| { |
| v.template has_revocation_finished_for_epoch<false>(epoch) |
| } -> std::same_as<uint32_t>; |
| { |
| v.system_bg_revoker_kick() |
| } -> std::same_as<void>; |
| }; |
| |
| /** |
| * If this revoker supports an interrupt to notify of completion then it |
| * must have a method that blocks waiting for the interrupt to fire. This |
| * method should return true if the epoch has been reached or false if the |
| * timeout expired. |
| */ |
| template<typename T> |
| concept SupportsInterruptNotification = requires(T v, |
| Timeout *timeout, |
| uint32_t epoch) |
| { |
| { |
| v.wait_for_completion(timeout, epoch) |
| } -> std::same_as<bool>; |
| }; |
| |
| /** |
| * Class for interacting with the shadow bitmap. This bitmap controls the |
| * behaviour of a hardware load barrier, which will invalidate capabilities |
| * in the register file if they point revoked memory. Only memory that is |
| * intended for use as a heap is required to use the load barrier and so |
| * the revocation bitmap is not required to span the whole of the address |
| * space. |
| * |
| * Template arguments are the size of the value to be used for reads and |
| * updates, which may vary depending on the width of the interface to the |
| * shadow memory, and the base address of the memory covered by the shadow |
| * bitmap. |
| * |
| * This currently assumes that all revocable memory is in a single |
| * contiguous region. |
| */ |
| template<typename WordT, size_t TCMBaseAddr> |
| class Bitmap |
| { |
| /// The pointer to the shadow bitmap region. |
| WordT *shadowCap; |
| |
| /// The number of bits in a single load or store of the shadow memory. |
| static constexpr size_t ShadowWordSizeBits = |
| utils::bytes2bits(sizeof(*shadowCap)); |
| |
| /// The shift required to translate an offset in bytes in memory into |
| /// an offset in bytes in the shadow memory. |
| static constexpr size_t ShadowWordShift = |
| utils::log2<ShadowWordSizeBits>(); |
| |
| /// The mask used to compute which bits to update in a word of the |
| /// revocation bitmap. |
| static constexpr size_t ShadowWordMask = (1U << ShadowWordShift) - 1; |
| |
| protected: |
| /// Initialise this class with a capability to the shadow bitmap. |
| void init() |
| { |
| shadowCap = const_cast<WordT *>(MMIO_CAPABILITY(WordT, shadow)); |
| } |
| |
| static constexpr size_t shadow_offset_bits(ptraddr_t addr) |
| { |
| return (addr - TCMBaseAddr) >> MallocAlignShift; |
| } |
| |
| static constexpr size_t shadow_word_index(size_t offsetBits) |
| { |
| return offsetBits >> ShadowWordShift; |
| } |
| |
| static constexpr WordT shadow_mask_bits_below_address(ptraddr_t addr) |
| { |
| size_t capoffset = shadow_offset_bits(addr); |
| return (WordT(1) << (capoffset & ShadowWordMask)) - 1; |
| } |
| |
| static constexpr WordT shadow_mask_bits_above_address(ptraddr_t addr) |
| { |
| return ~shadow_mask_bits_below_address(addr); |
| } |
| |
| /* |
| * Some quick verification that the union of these two masks is |
| * all bits set. |
| */ |
| static_assert((shadow_mask_bits_above_address(0) ^ |
| shadow_mask_bits_below_address(0)) == WordT(-1)); |
| static_assert((shadow_mask_bits_above_address(7) ^ |
| shadow_mask_bits_below_address(7)) == WordT(-1)); |
| static_assert((shadow_mask_bits_above_address(ShadowWordSizeBits - 1) ^ |
| shadow_mask_bits_below_address(ShadowWordSizeBits - |
| 1)) == WordT(-1)); |
| |
| public: |
| /** |
| * @brief Set or clear the single shadow bit for an address. |
| * |
| * @param fill true to set, false to clear the bit |
| */ |
| void shadow_paint_single(ptraddr_t addr, bool fill) |
| { |
| Debug::Assert(addr > TCMBaseAddr, |
| "Address {} is below the TCM base {}", |
| addr, |
| TCMBaseAddr); |
| size_t capoffset = shadow_offset_bits(addr); |
| WordT shadowWord = shadowCap[shadow_word_index(capoffset)]; |
| WordT mask = (WordT(1) << (capoffset & ShadowWordMask)); |
| |
| if (fill) |
| { |
| shadowWord |= mask; |
| } |
| else |
| { |
| shadowWord &= ~mask; |
| } |
| shadowCap[shadow_word_index(capoffset)] = shadowWord; |
| } |
| |
| /** |
| * @brief Set or clear the shadow bits for all addresses within [base, |
| * top). |
| * |
| * @param Fill true to set, false to clear the bits |
| */ |
| template<bool Fill> |
| __always_inline void shadow_paint_range(ptraddr_t base, ptraddr_t top) |
| { |
| size_t baseCapOffset = shadow_offset_bits(base); |
| size_t topCapOffset = shadow_offset_bits(top); |
| size_t baseWordIx = shadow_word_index(baseCapOffset); |
| size_t topWordIx = shadow_word_index(topCapOffset); |
| |
| WordT maskHi = shadow_mask_bits_below_address(top); |
| WordT maskLo = shadow_mask_bits_above_address(base); |
| |
| if (baseWordIx == topWordIx) |
| { |
| /* |
| * This object is entirely contained within one word of the |
| * bitmap. We must AND the mask{Hi,Lo} together, since those |
| * masks were assuming that the object ran to (or past) the |
| * respective ends of the word. |
| */ |
| WordT mask = maskHi & maskLo; |
| if (Fill) |
| { |
| shadowCap[baseWordIx] |= mask; |
| } |
| else |
| { |
| shadowCap[baseWordIx] &= ~mask; |
| } |
| |
| return; |
| } |
| |
| /* |
| * Otherwise, there are at least two words of the bitmap that need |
| * to be updated with the masks, and possibly some in between that |
| * just need to be set wholesale. |
| * |
| * We paint ranges "backwards", from highest address to lowest, so |
| * that we never create a window in which an interior pointer has a |
| * clear shadow bit while the lower adjacent address has an asserted |
| * shadow bit, as that would open the door to confusing the interior |
| * pointer with a pointer to the start of an object (recall that |
| * object headers are marked in the shadow bitmap). |
| * |
| * When clearing ranges, the order matters less. A correct |
| * allocator will have run revocation first, and so there should be |
| * no interior pointers (outside the allocator, anyway) to worry us. |
| */ |
| WordT midWord; |
| if constexpr (Fill) |
| { |
| shadowCap[topWordIx] |= maskHi; |
| midWord = ~WordT(0); |
| } |
| else |
| { |
| shadowCap[topWordIx] &= ~maskHi; |
| midWord = 0; |
| } |
| |
| /* |
| * This loop is underflow-safe, since topWordIx is strictly greater |
| * than baseWordIx after the test for equality above. |
| */ |
| for (size_t shadowWordIx = topWordIx - 1; baseWordIx < shadowWordIx; |
| shadowWordIx--) |
| { |
| shadowCap[shadowWordIx] = midWord; |
| } |
| |
| if constexpr (Fill) |
| { |
| shadowCap[baseWordIx] |= maskLo; |
| } |
| else |
| { |
| shadowCap[baseWordIx] &= ~maskLo; |
| } |
| } |
| |
| // Return the shadow bit at address addr. |
| bool shadow_bit_get(size_t addr) |
| { |
| size_t capoffset = shadow_offset_bits(addr); |
| WordT shadowWord = shadowCap[shadow_word_index(capoffset)]; |
| WordT mask = (WordT(1) << (capoffset & ShadowWordMask)); |
| |
| return (shadowWord & mask) != 0; |
| } |
| |
| /** |
| * @brief Check whether the argument of free() is valid. |
| * Because we use shadow bits to mark allocation boundaries, this has to |
| * be a method of a revoker instance. |
| * |
| * @return true if the input is valid |
| */ |
| bool is_free_cap_valid(CHERI::Capability<void> cap) |
| { |
| ptraddr_t base = cap.base(); |
| |
| return cap.is_valid() && (base & MallocAlignMask) == 0 && |
| shadow_bit_get(base - MallocAlignment) == 1 && |
| shadow_bit_get(base) == 0; |
| } |
| }; |
| |
| template<typename WordT, |
| size_t TCMBaseAddr, |
| template<typename, size_t> |
| typename Revoker> |
| requires IsHardwareRevokerDevice<Revoker<WordT, TCMBaseAddr>> |
| class HardwareAccelerator : public Bitmap<WordT, TCMBaseAddr>, |
| public Revoker<WordT, TCMBaseAddr> |
| { |
| public: |
| /** |
| * Currently the only hardware revoker implementation is async which |
| * sweeps memory in the background. |
| */ |
| static constexpr bool IsAsynchronous = true; |
| |
| /** |
| * Initialise a revoker instance. |
| */ |
| void init() |
| { |
| Bitmap<WordT, TCMBaseAddr>::init(); |
| Revoker<WordT, TCMBaseAddr>::init(); |
| } |
| }; |
| |
| /** |
| * Null implementation of the revoker interface. Provides no temporal |
| * safety. |
| */ |
| class NoTemporalSafety |
| { |
| public: |
| /** |
| * If there is no temporal memory safety, we treat sweeping as |
| * synchronous but infinitely fast which returns immediately. |
| */ |
| static constexpr bool IsAsynchronous = false; |
| void init() {} |
| void shadow_paint_single(size_t, bool) {} |
| template<bool Fill> |
| void shadow_paint_range(size_t, size_t) |
| { |
| } |
| uint32_t system_epoch_get() |
| { |
| return 0; |
| } |
| template<bool AllowPartial = true> |
| uint32_t has_revocation_finished_for_epoch(uint32_t previousEpoch) |
| { |
| return true; |
| } |
| void system_bg_revoker_kick() {} |
| bool is_free_cap_valid(void *) |
| { |
| return true; |
| } |
| bool shadow_bit_get(size_t addr) |
| { |
| Debug::Assert(false, |
| "shadow_bit_get should not be called on the revoker " |
| "with no temporal safety, its result is meaningless"); |
| return false; |
| } |
| }; |
| |
| /** |
| * Software revoker for use with a hardware load barrier. Uses a separate |
| * (very privileged!) compartment to scan all of memory. |
| */ |
| template<typename WordT, size_t TCMBaseAddr> |
| class SoftwareRevoker : public Bitmap<WordT, TCMBaseAddr> |
| { |
| private: |
| /** |
| * A (read-only) pointer to the revocation epoch. Incremented once |
| * when revocation starts and once when it finishes. |
| */ |
| const uint32_t *epoch; |
| |
| public: |
| /** |
| * Software sweeping is implemented synchronously now. The sweeping is |
| * done when memory is under pressure or malloc() failed. malloc() and |
| * free() only return when a certain amount of sweeping is done. |
| */ |
| static constexpr bool IsAsynchronous = false; |
| |
| /** |
| * Initialise the software revoker. |
| */ |
| void init() |
| { |
| Bitmap<WordT, TCMBaseAddr>::init(); |
| epoch = revoker_epoch_get(); |
| } |
| |
| /** |
| * Returns the revocation epoch. This is the number of revocations |
| * that have started or finished. It will be even if revocation is not |
| * running. |
| */ |
| uint32_t system_epoch_get() |
| { |
| return *epoch; |
| } |
| |
| /** |
| * Queries whether the specified revocation epoch has finished. |
| */ |
| template<bool AllowPartial = false> |
| uint32_t has_revocation_finished_for_epoch(uint32_t previousEpoch) |
| { |
| // If the revoker is running, prod it to do a bit more work every |
| // time that it's queried. |
| if ((*epoch & 1) == 1) |
| { |
| revoker_tick(); |
| } |
| if (AllowPartial) |
| { |
| return *epoch > previousEpoch; |
| } |
| // We want to check if a complete revocation pass has happened |
| // since the last query of the epoch. If the last query happened |
| // in the middle of revocation then the low bit will be 1. In this |
| // case, we need to ensure that the revocation epoch has |
| // incremented by 3 (finished the in-progress sweep, started and |
| // finished the next one). In the other case, where revocation was |
| // not in progress, we need to ensure only that one complete |
| // revocation pass has happened and so the epoch counter was |
| // incremented twice. |
| // |
| // We perform a subtraction first because unsigned overflow is |
| // well-defined in C to wrap and so, as long as we don't manage to |
| // do 2^31 revocation sweeps without the consumer noticing then |
| // this will give the correct result even in overflow cases. |
| return *epoch - previousEpoch >= (2 + (previousEpoch & 1)); |
| } |
| |
| /// Start revocation running. |
| void system_bg_revoker_kick() |
| { |
| revoker_tick(); |
| } |
| }; |
| |
| template<typename WordT, size_t TCMBaseAddr> |
| class FakeRevoker : public Bitmap<WordT, TCMBaseAddr> |
| { |
| private: |
| /** |
| * A (read-only) pointer to the revocation epoch. Incremented once |
| * when revocation starts and once when it finishes. |
| */ |
| uint32_t epoch; |
| |
| public: |
| /** |
| * Software sweeping is implemented synchronously now. The sweeping is |
| * done when memory is under pressure or malloc() failed. malloc() and |
| * free() only return when a certain amount of sweeping is done. |
| */ |
| static constexpr bool IsAsynchronous = false; |
| |
| /** |
| * Initialise the software revoker. |
| */ |
| void init() |
| { |
| Bitmap<WordT, TCMBaseAddr>::init(); |
| epoch = 0; |
| } |
| |
| /** |
| * Returns the revocation epoch. This is the number of revocations |
| * that have started or finished. It will be even if revocation is not |
| * running. |
| */ |
| uint32_t system_epoch_get() |
| { |
| return epoch; |
| } |
| |
| /** |
| * Queries whether the specified revocation epoch has finished. |
| */ |
| template<bool AllowPartial = false> |
| uint32_t has_revocation_finished_for_epoch(uint32_t previousEpoch) |
| { |
| return true; |
| } |
| |
| /// Start revocation running. Fake revocation completes instantly. |
| void system_bg_revoker_kick() |
| { |
| epoch++; |
| } |
| }; |
| /** |
| * The revoker to use for this configuration. |
| * |
| * FIXME: This should not hard-code the start address. |
| */ |
| using Revoker = |
| #ifdef TEMPORAL_SAFETY |
| # ifdef SOFTWARE_REVOKER |
| SoftwareRevoker<uint32_t, REVOKABLE_MEMORY_START> |
| # else |
| HardwareAccelerator<uint32_t, REVOKABLE_MEMORY_START, HardwareRevoker> |
| # endif |
| #else |
| # ifdef CHERIOT_FAKE_REVOKER |
| FakeRevoker<uint32_t, REVOKABLE_MEMORY_START>; |
| # else |
| NoTemporalSafety |
| # endif |
| #endif |
| ; |
| } // namespace Revocation |