blob: 2b17ed56aaea9eac9dfb7f6e4e7431f65aeeca0c [file] [log] [blame]
// Copyright 2024 The Pigweed Authors
//
// 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
//
// https://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 "pw_allocator/guarded_allocator.h"
#include <cstddef>
#include <cstdint>
#include <limits>
#include <mutex>
#include "pw_allocator/first_fit_block_allocator.h"
#include "pw_allocator/sync_allocator_testing.h"
#include "pw_sync/interrupt_spin_lock.h"
#include "pw_sync/mutex.h"
#include "pw_unit_test/framework.h"
// TODO: https://pwbug.dev/365161669 - Express joinability as a build-system
// constraint.
#if PW_THREAD_JOINING_ENABLED
namespace {
// Test fixtures.
static constexpr size_t kCapacity = 8192;
using ::pw::allocator::GuardedAllocator;
using ::pw::allocator::test::Background;
using ::pw::allocator::test::BackgroundThreadCore;
using ::pw::allocator::test::SyncAllocatorTest;
using BlockAllocator = ::pw::allocator::FirstFitBlockAllocator<uint16_t>;
enum class Mode {
kValidateOne,
kValidateAll,
};
/// Thread body that validates a guarded allocator's blocks in the background.
template <typename GuardedAllocatorType>
class GuardedAllocatorTestThreadCore : public BackgroundThreadCore {
public:
GuardedAllocatorTestThreadCore(GuardedAllocatorType& allocator)
: allocator_(allocator) {}
void SetMode(Mode mode) {
std::lock_guard lock(mutex_);
mode_ = mode;
}
void* GetInvalid() const PW_LOCKS_EXCLUDED(mutex_) {
std::lock_guard lock(mutex_);
return invalid_;
}
/// Clobbers the byte at the given location and returns the original byte.
///
/// This is done in a thread-safe manner to allow the allocator to detect the
/// corruption without TSAN flagging it first.
void Corrupt(uint8_t* ptr) PW_LOCKS_EXCLUDED(mutex_) {
std::lock_guard lock(mutex_);
original_ = *ptr;
corrupted_ = ptr;
*corrupted_ ^= 0xFF;
}
/// Restores a corrupted byte to its original value.
void Restore() PW_LOCKS_EXCLUDED(mutex_) {
std::lock_guard lock(mutex_);
*corrupted_ = original_;
invalid_ = nullptr;
}
bool RunOnce() PW_LOCKS_EXCLUDED(mutex_) override {
std::lock_guard lock(mutex_);
switch (mode_) {
case Mode::kValidateOne:
invalid_ = allocator_.ValidateOne();
break;
case Mode::kValidateAll:
invalid_ = allocator_.ValidateAll();
break;
}
return invalid_ == nullptr;
}
private:
mutable ::pw::sync::Mutex mutex_;
Mode mode_ = Mode::kValidateOne;
void* invalid_ PW_GUARDED_BY(mutex_) = nullptr;
uint8_t* corrupted_ PW_GUARDED_BY(mutex_) = nullptr;
uint8_t original_ PW_GUARDED_BY(mutex_) = 0;
GuardedAllocatorType& allocator_;
};
/// Test fixture responsible for managing a guarded allocator and a
/// background thread that accesses it concurrently with unit tests.
///
/// @tparam LockType Synchronization type used by the allocator.
template <typename LockType>
class GuardedAllocatorTestBase : public SyncAllocatorTest {
protected:
using GuardedAllocatorType = GuardedAllocator<BlockAllocator, LockType>;
using BlockType = BlockAllocator::BlockType;
// This necessarily violates the encapsulation of GuardedAllocator in order
// to precisely simulate overflows of a single byte. Keep it in sync with the
// constant of the same name in guarded_allocator.cc
static constexpr size_t kMinPrefixSize = sizeof(size_t) * 2;
GuardedAllocatorTestBase() : guarded_(allocator_), core_(guarded_) {
allocator_.Init(buffer_);
}
void SetMode(Mode mode) { core_.SetMode(mode); }
GuardedAllocatorType& GetAllocator() override { return guarded_; }
BackgroundThreadCore& GetCore() override { return core_; }
void CheckValid() { EXPECT_EQ(core_.GetInvalid(), nullptr); }
// Unit tests
void TestValidateAllAfterInit() {
core_.SetMode(Mode::kValidateAll);
core_.RunOnce();
CheckValid();
}
void TestValidateAllAfterAllocation() {
pw::UniquePtr<uint8_t[]> bytes =
guarded_.template MakeUniqueArray<uint8_t>(64);
ASSERT_NE(bytes, nullptr);
core_.SetMode(Mode::kValidateAll);
core_.RunOnce();
CheckValid();
}
void TestDetectHeapUnderrun() {
Background background(core_);
pw::UniquePtr<uint8_t[]> bytes =
guarded_.template MakeUniqueArray<uint8_t>(64);
ASSERT_NE(bytes, nullptr);
// Modify the last byte of the prefix.
core_.Corrupt(bytes.get() - 1);
core_.Await();
EXPECT_GE(core_.GetInvalid(), bytes.get() - kMinPrefixSize);
EXPECT_LE(core_.GetInvalid(), bytes.get());
core_.Restore();
}
void TestDetectHeapOverrunFromPrev() {
Background background(core_);
pw::UniquePtr<uint8_t[]> bytes =
guarded_.template MakeUniqueArray<uint8_t>(64);
ASSERT_NE(bytes, nullptr);
// Modify the first byte of the prefix.
core_.Corrupt(bytes.get() - kMinPrefixSize);
core_.Await();
EXPECT_GE(core_.GetInvalid(), bytes.get() - kMinPrefixSize);
EXPECT_LE(core_.GetInvalid(), bytes.get());
core_.Restore();
}
void TestDetectHeapOverrun() {
Background background(core_);
pw::UniquePtr<uint8_t[]> bytes =
guarded_.template MakeUniqueArray<uint8_t>(64);
ASSERT_NE(bytes, nullptr);
// Modify the first byte of the suffix.
core_.Corrupt(bytes.get() + 64);
core_.Await();
EXPECT_GE(core_.GetInvalid(), bytes.get() - kMinPrefixSize);
EXPECT_LE(core_.GetInvalid(), bytes.get());
core_.Restore();
}
private:
alignas(BlockType::kAlignment) std::array<std::byte, kCapacity> buffer_;
BlockAllocator allocator_;
GuardedAllocatorType guarded_;
GuardedAllocatorTestThreadCore<GuardedAllocatorType> core_;
};
using GuardedAllocatorInterruptSpinLockTest =
GuardedAllocatorTestBase<::pw::sync::InterruptSpinLock>;
using GuardedAllocatorMutexTest = GuardedAllocatorTestBase<::pw::sync::Mutex>;
// Unit tests.
TEST_F(GuardedAllocatorInterruptSpinLockTest, GetCapacity) {
TestGetCapacity(kCapacity);
CheckValid();
}
TEST_F(GuardedAllocatorMutexTest, GetCapacity) {
TestGetCapacity(kCapacity);
CheckValid();
}
TEST_F(GuardedAllocatorInterruptSpinLockTest, AllocateDeallocate) {
TestAllocate();
CheckValid();
}
TEST_F(GuardedAllocatorMutexTest, AllocateDeallocate) {
TestAllocate();
CheckValid();
}
TEST_F(GuardedAllocatorInterruptSpinLockTest, Resize) {
TestResize();
CheckValid();
}
TEST_F(GuardedAllocatorMutexTest, Resize) {
TestResize();
CheckValid();
}
TEST_F(GuardedAllocatorInterruptSpinLockTest, Reallocate) {
TestReallocate();
CheckValid();
}
TEST_F(GuardedAllocatorMutexTest, Reallocate) {
TestReallocate();
CheckValid();
}
TEST_F(GuardedAllocatorInterruptSpinLockTest, ValidateAllAfterInit) {
TestValidateAllAfterInit();
}
TEST_F(GuardedAllocatorMutexTest, ValidateAllAfterInit) {
TestValidateAllAfterInit();
}
TEST_F(GuardedAllocatorInterruptSpinLockTest, ValidateAllAfterAllocation) {
TestValidateAllAfterAllocation();
}
TEST_F(GuardedAllocatorMutexTest, ValidateAllAfterAllocation) {
TestValidateAllAfterAllocation();
}
TEST_F(GuardedAllocatorInterruptSpinLockTest, DetectHeapUnderrun_ValidateOne) {
SetMode(Mode::kValidateOne);
TestDetectHeapUnderrun();
}
TEST_F(GuardedAllocatorInterruptSpinLockTest, DetectHeapUnderrun_ValidateAll) {
SetMode(Mode::kValidateAll);
TestDetectHeapUnderrun();
}
TEST_F(GuardedAllocatorMutexTest, DetectHeapUnderrun_ValidateOne) {
SetMode(Mode::kValidateOne);
TestDetectHeapUnderrun();
}
TEST_F(GuardedAllocatorMutexTest, DetectHeapUnderrun_ValidateAll) {
SetMode(Mode::kValidateAll);
TestDetectHeapUnderrun();
}
TEST_F(GuardedAllocatorInterruptSpinLockTest,
DetectHeapOverrunFromPrev_ValidateOne) {
SetMode(Mode::kValidateOne);
TestDetectHeapOverrunFromPrev();
}
TEST_F(GuardedAllocatorInterruptSpinLockTest,
DetectHeapOverrunFromPrev_ValidateAll) {
SetMode(Mode::kValidateAll);
TestDetectHeapOverrunFromPrev();
}
TEST_F(GuardedAllocatorMutexTest, DetectHeapOverrunFromPrev_ValidateOne) {
SetMode(Mode::kValidateOne);
TestDetectHeapOverrunFromPrev();
}
TEST_F(GuardedAllocatorMutexTest, DetectHeapOverrunFromPrev_ValidateAll) {
SetMode(Mode::kValidateAll);
TestDetectHeapOverrunFromPrev();
}
TEST_F(GuardedAllocatorInterruptSpinLockTest, DetectHeapOverrun_ValidateOne) {
SetMode(Mode::kValidateOne);
TestDetectHeapOverrun();
}
TEST_F(GuardedAllocatorInterruptSpinLockTest, DetectHeapOverrun_ValidateAll) {
SetMode(Mode::kValidateAll);
TestDetectHeapOverrun();
}
TEST_F(GuardedAllocatorMutexTest, DetectHeapOverrun_ValidateOne) {
SetMode(Mode::kValidateOne);
TestDetectHeapOverrun();
}
TEST_F(GuardedAllocatorMutexTest, DetectHeapOverrun_ValidateAll) {
SetMode(Mode::kValidateAll);
TestDetectHeapOverrun();
}
} // namespace
#endif // PW_THREAD_JOINING_ENABLED