blob: cde592b2ee35cfe285388884d077a80e01d2e13b [file] [log] [blame]
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "cheriot/cheriot_ibex_hw_revoker.h"
#include <sys/types.h>
#include <cstdint>
#include <cstring>
#include "cheriot/cheriot_register.h"
#include "googlemock/include/gmock/gmock.h"
#include "mpact/sim/generic/data_buffer.h"
#include "mpact/sim/generic/instruction.h"
#include "mpact/sim/util/memory/flat_demand_memory.h"
#include "mpact/sim/util/memory/tagged_flat_demand_memory.h"
#include "mpact/sim/util/memory/tagged_memory_interface.h"
#include "riscv//riscv_plic.h"
// This file contains unit tests for the CheriotIbexHWRevoker class.
namespace {
using ::mpact::sim::cheriot::CheriotIbexHWRevoker;
using ::mpact::sim::cheriot::CheriotRegister;
using ::mpact::sim::generic::DataBuffer;
using ::mpact::sim::generic::DataBufferFactory;
using ::mpact::sim::generic::Instruction;
using ::mpact::sim::generic::ReferenceCount;
using ::mpact::sim::riscv::RiscVPlicIrqInterface;
using ::mpact::sim::util::FlatDemandMemory;
using ::mpact::sim::util::TaggedFlatDemandMemory;
using ::mpact::sim::util::TaggedMemoryInterface;
constexpr uint64_t kRevocationBase = 0x200'0000;
constexpr uint64_t kHeapBase = 0x8001'0000;
constexpr uint64_t kSweepBase = 0x8000'0000;
// Mock plic source interface.
class MockPlicSource : public RiscVPlicIrqInterface {
public:
MockPlicSource() = default;
~MockPlicSource() override = default;
void SetIrq(bool irq_value) override { irq_value_ = irq_value; }
bool irq_value() const { return irq_value_; }
void set_irq_value(bool value) { irq_value_ = value; }
private:
bool irq_value_ = false;
};
// This class is used to capture memory load/store addresses.
class MemoryViewer : public TaggedMemoryInterface {
public:
MemoryViewer() = delete;
MemoryViewer(TaggedMemoryInterface *memory) : memory_(memory) {}
~MemoryViewer() override = default;
void Load(uint64_t address, DataBuffer *db, DataBuffer *tags,
Instruction *inst, ReferenceCount *context) override {
ld_address_ = address;
memory_->Load(address, db, tags, inst, context);
}
void Load(uint64_t address, DataBuffer *db, Instruction *inst,
ReferenceCount *context) override {
ld_address_ = address;
memory_->Load(address, db, inst, context);
}
void Load(DataBuffer *address_db, DataBuffer *mask_db, int el_size,
DataBuffer *db, Instruction *inst,
ReferenceCount *context) override {
ld_address_ = address_db->Get<uint64_t>(0);
memory_->Load(address_db, mask_db, el_size, db, inst, context);
}
void Store(uint64_t address, DataBuffer *db, DataBuffer *tags) override {
st_address_ = address;
memory_->Store(address, db, tags);
}
void Store(uint64_t address, DataBuffer *db) override {
st_address_ = address;
memory_->Store(address, db);
}
void Store(DataBuffer *address_db, DataBuffer *mask_db, int el_size,
DataBuffer *db) override {
st_address_ = address_db->Get<uint64_t>(0);
memory_->Store(address_db, mask_db, el_size, db);
}
uint64_t ld_address() const { return ld_address_; }
uint64_t st_address() const { return st_address_; }
private:
TaggedMemoryInterface *memory_ = nullptr;
uint64_t ld_address_ = 0;
uint64_t st_address_ = 0;
};
class CheriotIbexHwRevokerTest : public ::testing::Test {
protected:
CheriotIbexHwRevokerTest() {
db1_ = db_factory_.Allocate<uint8_t>(1);
db4_ = db_factory_.Allocate<uint32_t>(1);
db8_ = db_factory_.Allocate<uint32_t>(2);
db128_ = db_factory_.Allocate<uint8_t>(128);
plic_irq_ = new MockPlicSource();
heap_memory_ = new TaggedFlatDemandMemory(8);
memory_viewer_ = new MemoryViewer(heap_memory_);
revocation_memory_ = new FlatDemandMemory();
revoker_ =
new CheriotIbexHWRevoker(plic_irq_, kHeapBase, 0x8000, memory_viewer_,
kRevocationBase, revocation_memory_);
cap_reg_ = new CheriotRegister(nullptr, "cap");
cap_db_ = db_factory_.Allocate<uint32_t>(1);
cap_reg_->SetDataBuffer(cap_db_);
}
~CheriotIbexHwRevokerTest() override {
db1_->DecRef();
db4_->DecRef();
db8_->DecRef();
db128_->DecRef();
cap_db_->DecRef();
delete plic_irq_;
delete revoker_;
delete heap_memory_;
delete memory_viewer_;
delete revocation_memory_;
delete cap_reg_;
}
// Call to advance the revoker.
void AdvanceRevoker() { revoker_->SetValue(0); }
// Convenience method to set the revocation bit for the given address.
void RevokeAddress(uint64_t address) {
if (address < kHeapBase) return;
uint64_t offset = address - kHeapBase;
offset >>= 3;
auto bit = offset & 0x7;
offset >>= 3;
revocation_memory_->Load(kRevocationBase + offset, db1_, nullptr, nullptr);
uint8_t val = db1_->Get<uint8_t>(0);
val |= 1 << bit;
db1_->Set<uint8_t>(0, val);
revocation_memory_->Store(kRevocationBase + offset, db1_);
}
// This clears the revocation bits for the memory range [kHeapBase, kHeapBase
// + 0x8000].
void ClearRevocationBits() {
std::memset(db128_->raw_ptr(), 0, db128_->size<uint8_t>());
uint64_t address = kRevocationBase;
for (uint64_t i = 0; i < 0x100; ++i) {
revocation_memory_->Store(address, db128_);
address += db128_->size<uint8_t>();
}
}
// The following methods are convenience methods for accessing the MMRs of
// the hw revoker using the revoker's memory interface.
void SetStartAddress(uint32_t address) {
db4_->Set<uint32_t>(0, address);
revoker_->Store(CheriotIbexHWRevoker::kStartAddressOffset, db4_);
}
uint32_t GetStartAddress() {
revoker_->Load(CheriotIbexHWRevoker::kStartAddressOffset, db4_, nullptr,
nullptr);
return db4_->Get<uint32_t>(0);
}
void SetEndAddress(uint32_t address) {
db4_->Set<uint32_t>(0, address);
revoker_->Store(CheriotIbexHWRevoker::kEndAddressOffset, db4_);
}
uint32_t GetEndAddress() {
revoker_->Load(CheriotIbexHWRevoker::kEndAddressOffset, db4_, nullptr,
nullptr);
return db4_->Get<uint32_t>(0);
}
void SetGo(uint32_t go) {
db4_->Set<uint32_t>(0, go);
revoker_->Store(CheriotIbexHWRevoker::kGoOffset, db4_);
}
uint32_t GetGo() {
revoker_->Load(CheriotIbexHWRevoker::kGoOffset, db4_, nullptr, nullptr);
return db4_->Get<uint32_t>(0);
}
void SetEpoch(uint32_t epoch) {
db4_->Set<uint32_t>(0, epoch);
revoker_->Store(CheriotIbexHWRevoker::kEpochOffset, db4_);
}
uint32_t GetEpoch() {
revoker_->Load(CheriotIbexHWRevoker::kEpochOffset, db4_, nullptr, nullptr);
return db4_->Get<uint32_t>(0);
}
void SetStatus(uint32_t status) {
db4_->Set<uint32_t>(0, status);
revoker_->Store(CheriotIbexHWRevoker::kStatusOffset, db4_);
}
uint32_t GetStatus() {
revoker_->Load(CheriotIbexHWRevoker::kStatusOffset, db4_, nullptr, nullptr);
return db4_->Get<uint32_t>(0);
}
void SetInterruptEnable(uint32_t enable) {
db4_->Set<uint32_t>(0, enable);
revoker_->Store(CheriotIbexHWRevoker::kInterruptEnableOffset, db4_);
}
uint32_t GetInterruptEnable() {
revoker_->Load(CheriotIbexHWRevoker::kInterruptEnableOffset, db4_, nullptr,
nullptr);
return db4_->Get<uint32_t>(0);
}
// Convenience method to write a valid capability to memory with the given
// base.
void WriteCapability(uint64_t address, uint64_t base) {
cap_reg_->ResetMemoryRoot();
cap_reg_->SetAddress(base);
cap_reg_->SetBounds(base, 0x10);
db8_->Set<uint32_t>(0, cap_reg_->address());
db8_->Set<uint32_t>(1, cap_reg_->Compress());
db8_->set_latency(0);
db1_->Set<uint8_t>(0, true);
db1_->set_latency(0);
heap_memory_->Store(address, db8_, db1_);
}
// Convenience method to read a capability from memory with the given base.
CheriotRegister *ReadCapability(uint64_t address) {
heap_memory_->Load(address, db8_, db1_, nullptr, nullptr);
cap_reg_->Expand(db8_->Get<uint32_t>(0), db8_->Get<uint32_t>(1),
db1_->Get<uint8_t>(0));
return cap_reg_;
}
uint64_t GetLoadAddress() { return memory_viewer_->ld_address(); }
uint64_t GetStoreAddress() { return memory_viewer_->st_address(); }
MockPlicSource *plic_irq() { return plic_irq_; }
private:
CheriotRegister *cap_reg_ = nullptr;
DataBufferFactory db_factory_;
DataBuffer *db1_;
DataBuffer *db4_;
DataBuffer *db8_;
DataBuffer *db128_;
DataBuffer *cap_db_;
CheriotIbexHWRevoker *revoker_ = nullptr;
MockPlicSource *plic_irq_ = nullptr;
TaggedFlatDemandMemory *heap_memory_ = nullptr;
FlatDemandMemory *revocation_memory_ = nullptr;
MemoryViewer *memory_viewer_ = nullptr;
};
// Initial state should all be clear.
TEST_F(CheriotIbexHwRevokerTest, TestInitial) {
EXPECT_EQ(GetStartAddress(), 0);
EXPECT_EQ(GetEndAddress(), 0);
EXPECT_EQ(GetGo(), 0x5500'0000);
EXPECT_EQ(GetEpoch(), 0);
EXPECT_EQ(GetStatus(), 0);
EXPECT_EQ(GetInterruptEnable(), 0);
}
// No valid capabilities in the sweep range.
TEST_F(CheriotIbexHwRevokerTest, RevokeNone) {
SetStartAddress(kSweepBase);
SetEndAddress(kSweepBase + 0x100);
SetGo(1);
EXPECT_EQ(GetGo(), 0x5500'0001);
// Expect zero status.
EXPECT_EQ(GetStatus(), 0);
// Expect sweep to be started.
EXPECT_EQ(GetEpoch(), 1);
// Step through 256/8 - 1 capabilities.
int num = 0x100 / 8;
for (int i = 0; i < num; ++i) {
AdvanceRevoker();
EXPECT_EQ(GetLoadAddress(), kSweepBase + (i << 3));
EXPECT_EQ(GetEpoch(), 1);
EXPECT_EQ(GetStatus(), 0);
}
// Step through the next capability. The sweep should be done.
AdvanceRevoker();
// Notice the in progress bit is cleared.
EXPECT_EQ(GetEpoch(), (1 << 1) | 0);
// Interrupt status should be 0, as interrupt enable is off.
EXPECT_EQ(GetStatus(), 0);
}
TEST_F(CheriotIbexHwRevokerTest, RevokeOne) {
// Write a capability at the sweep base.
for (auto offset = 0; offset < 0x100; offset += 0x8) {
WriteCapability(kSweepBase + offset, kHeapBase + offset);
auto *cap = ReadCapability(kSweepBase);
EXPECT_TRUE(cap->tag());
}
// Revoke one capability.
RevokeAddress(kHeapBase + 0x20);
// Set the sweep range to include the capability.
SetStartAddress(kSweepBase);
SetEndAddress(kSweepBase + 0x100);
SetGo(1);
// Expect zero status.
EXPECT_EQ(GetStatus(), 0);
// Expect sweep to be started.
EXPECT_EQ(GetEpoch(), 1);
// Step through the sweep.
while ((GetEpoch() & 0x1) == 1) {
AdvanceRevoker();
}
// Since interrupt enable is not set, the status should be zero.
EXPECT_EQ(GetStatus(), 0);
// Verify that only the one revoked capability was invalidated.
for (auto offset = 0; offset < 0x100; offset += 0x8) {
auto *cap = ReadCapability(kSweepBase + offset);
if (offset == 0x20) {
EXPECT_FALSE(cap->tag());
} else {
EXPECT_TRUE(cap->tag());
}
}
}
TEST_F(CheriotIbexHwRevokerTest, RevokeWithInterrupt) {
// Write a capability at the sweep base.
for (auto offset = 0; offset < 0x100; offset += 0x8) {
WriteCapability(kSweepBase + offset, kHeapBase + offset);
auto *cap = ReadCapability(kSweepBase);
EXPECT_TRUE(cap->tag());
}
// Revoke one capability.
RevokeAddress(kHeapBase + 0x20);
// Set the sweep range to include the capability.
SetStartAddress(kSweepBase);
SetEndAddress(kSweepBase + 0x100);
// Enable interrupt.
SetInterruptEnable(1);
SetGo(1);
while ((GetEpoch() & 0x1) == 1) {
AdvanceRevoker();
}
EXPECT_EQ(GetStatus(), 1);
// Verify that the interrupt was set.
EXPECT_TRUE(plic_irq()->irq_value());
}
} // namespace