diff --git a/.github/workflows/CI-Linux.yml b/.github/workflows/CI-Linux.yml index 1016f87..8695e29 100644 --- a/.github/workflows/CI-Linux.yml +++ b/.github/workflows/CI-Linux.yml @@ -18,18 +18,22 @@ jobs: strategy: matrix: std: [11, 17] + mlock: [ON, OFF] steps: - uses: actions/checkout@v3 - name: Install dependencies run: sudo apt-get update && sudo apt-get install -y cmake - name: Configure - run: cmake -S . -B build -DHMACCPP_BUILD_TESTS=ON -DCMAKE_CXX_STANDARD=${{ matrix.std }} + run: cmake -S . -B build -DHMACCPP_BUILD_TESTS=ON -DHMACCPP_ENABLE_MLOCK=${{ matrix.mlock }} -DCMAKE_CXX_STANDARD=${{ matrix.std }} - name: Build run: cmake --build build - name: Run tests - run: ctest --test-dir build --output-on-failure -R "test_all|test_totp" + run: ctest --test-dir build --output-on-failure vcpkg: runs-on: ubuntu-latest + strategy: + matrix: + mlock: [ON, OFF] steps: - uses: actions/checkout@v3 - name: Install vcpkg @@ -38,8 +42,8 @@ jobs: ./vcpkg/bootstrap-vcpkg.sh ./vcpkg/vcpkg install gtest - name: Configure - run: cmake -S . -B build -DHMACCPP_BUILD_TESTS=ON -DCMAKE_TOOLCHAIN_FILE=./vcpkg/scripts/buildsystems/vcpkg.cmake + run: cmake -S . -B build -DHMACCPP_BUILD_TESTS=ON -DHMACCPP_ENABLE_MLOCK=${{ matrix.mlock }} -DCMAKE_TOOLCHAIN_FILE=./vcpkg/scripts/buildsystems/vcpkg.cmake - name: Build run: cmake --build build - name: Run tests - run: ctest --test-dir build --output-on-failure -R "test_all|test_totp" + run: ctest --test-dir build --output-on-failure diff --git a/.github/workflows/CI-Win.yml b/.github/workflows/CI-Win.yml index 416052e..0b54725 100644 --- a/.github/workflows/CI-Win.yml +++ b/.github/workflows/CI-Win.yml @@ -18,6 +18,7 @@ jobs: strategy: matrix: std: [11, 17] + mlock: [ON, OFF] steps: - uses: actions/checkout@v3 - uses: msys2/setup-msys2@v2 @@ -30,10 +31,10 @@ jobs: mingw-w64-x86_64-make - name: Configure shell: msys2 {0} - run: cmake -S . -B build -DHMACCPP_BUILD_TESTS=ON -DCMAKE_CXX_STANDARD=${{ matrix.std }} + run: cmake -S . -B build -DHMACCPP_BUILD_TESTS=ON -DHMACCPP_ENABLE_MLOCK=${{ matrix.mlock }} -DCMAKE_CXX_STANDARD=${{ matrix.std }} - name: Build shell: msys2 {0} run: cmake --build build - name: Run tests shell: msys2 {0} - run: ctest --test-dir build --output-on-failure -R "test_all|test_totp" + run: ctest --test-dir build --output-on-failure diff --git a/.github/workflows/CI-macOS.yml b/.github/workflows/CI-macOS.yml index 8eafd35..62ad687 100644 --- a/.github/workflows/CI-macOS.yml +++ b/.github/workflows/CI-macOS.yml @@ -18,6 +18,7 @@ jobs: strategy: matrix: std: [11, 17] + mlock: [ON, OFF] steps: - uses: actions/checkout@v3 - name: Install dependencies @@ -26,14 +27,17 @@ jobs: brew uninstall --ignore-dependencies cmake || true brew install cmake - name: Configure - run: cmake -S . -B build -DHMACCPP_BUILD_TESTS=ON -DCMAKE_CXX_STANDARD=${{ matrix.std }} + run: cmake -S . -B build -DHMACCPP_BUILD_TESTS=ON -DHMACCPP_ENABLE_MLOCK=${{ matrix.mlock }} -DCMAKE_CXX_STANDARD=${{ matrix.std }} - name: Build run: cmake --build build - name: Run tests - run: ctest --test-dir build --output-on-failure -R "test_all|test_totp" + run: ctest --test-dir build --output-on-failure vcpkg: runs-on: macos-latest + strategy: + matrix: + mlock: [ON, OFF] steps: - uses: actions/checkout@v3 - name: Install vcpkg @@ -42,8 +46,8 @@ jobs: ./vcpkg/bootstrap-vcpkg.sh ./vcpkg/vcpkg install gtest - name: Configure - run: cmake -S . -B build -DHMACCPP_BUILD_TESTS=ON -DCMAKE_TOOLCHAIN_FILE=./vcpkg/scripts/buildsystems/vcpkg.cmake + run: cmake -S . -B build -DHMACCPP_BUILD_TESTS=ON -DHMACCPP_ENABLE_MLOCK=${{ matrix.mlock }} -DCMAKE_TOOLCHAIN_FILE=./vcpkg/scripts/buildsystems/vcpkg.cmake - name: Build run: cmake --build build - name: Run tests - run: ctest --test-dir build --output-on-failure -R "test_all|test_totp" + run: ctest --test-dir build --output-on-failure diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cc991a..60c3a4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,33 @@ ## [Unreleased] +## [0.6.0] - Unreleased +### Security +- `secure_buffer` now coordinates page locks process-wide at page granularity, + preventing one buffer from unlocking a page still used by another buffer. +- Replacement, copy, assignment, and string-consumption paths wipe old storage + before release and lock replacement storage before copying secrets. + +### Changed +- **Breaking ABI change:** `secure_buffer` layout and move-operation exception + specification changed. Shared-library consumers must be fully rebuilt; do not + use a 0.6.0 DLL/SO as a drop-in replacement for a 0.5.x build. +- CI now runs the secure-buffer regression target with page locking both enabled + and disabled. +- CMake package compatibility is `ExactVersion` while the project remains + pre-1.0; a 0.x minor release may contain breaking changes. + +### Page-locking limitations +- The page-lock registry coordinates calls only within one loaded copy of + hmac-cpp. Separate statically linked modules have independent registries. +- Continuing after POSIX `fork()` without `exec()` is unsupported for page + locking because child processes do not inherit memory locks. + +### Upgrade +- Rebuild every consumer against the 0.6.0 headers and library together. +- The bundled vcpkg port remains at the latest tagged release until `v0.6.0` + is published. + ## [0.5.0] - 2025-09-07 ### Added - Lifecycle management APIs and hardened zeroing for `secure_buffer`. diff --git a/CMakeLists.txt b/CMakeLists.txt index 761a2be..e3472ee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.5) -project(hmac_cpp VERSION 0.5.0 LANGUAGES CXX) +project(hmac_cpp VERSION 0.6.0 LANGUAGES CXX) option(HMACCPP_BUILD_EXAMPLES "Build the example program" OFF) option(HMACCPP_BUILD_TESTS "Build the test suite" OFF) @@ -33,6 +33,7 @@ set(HMAC_HEADERS include/hmac_cpp/sha512.hpp include/hmac_cpp/secure_buffer.hpp include/hmac_cpp/memlock.hpp + include/hmac_cpp/page_lock_registry.hpp include/hmac_cpp/secret_string.hpp include/hmac_cpp/encoding.hpp include/hmac_cpp/version.hpp @@ -108,7 +109,7 @@ configure_package_config_file( write_basic_package_version_file( "${CMAKE_CURRENT_BINARY_DIR}/hmac_cppConfigVersion.cmake" VERSION ${PROJECT_VERSION} - COMPATIBILITY SameMajorVersion + COMPATIBILITY ExactVersion ) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/hmac_cppConfig.cmake" @@ -129,16 +130,26 @@ if(HMACCPP_BUILD_TESTS) DOWNLOAD_EXTRACT_TIMESTAMP TRUE ) FetchContent_MakeAvailable(googletest) - find_package(OpenSSL REQUIRED) - add_executable(test_all test_all.cpp) - target_link_libraries(test_all PRIVATE hmac_cpp gtest_main OpenSSL::Crypto) - target_include_directories(test_all PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) - add_test(NAME test_all COMMAND test_all) + + add_executable(test_secure_buffer test_secure_buffer.cpp) + target_link_libraries(test_secure_buffer PRIVATE hmac_cpp gtest_main) + target_include_directories(test_secure_buffer PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) + add_test(NAME test_secure_buffer COMMAND test_secure_buffer) add_executable(test_totp test_totp.cpp) target_link_libraries(test_totp PRIVATE hmac_cpp gtest_main) target_include_directories(test_totp PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) add_test(NAME test_totp COMMAND test_totp) + + find_package(OpenSSL QUIET) + if(OpenSSL_FOUND) + add_executable(test_all test_all.cpp) + target_link_libraries(test_all PRIVATE hmac_cpp gtest_main OpenSSL::Crypto) + target_include_directories(test_all PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) + add_test(NAME test_all COMMAND test_all) + else() + message(STATUS "OpenSSL not found; skipping test_all") + endif() endif() export(EXPORT hmac_cppTargets diff --git a/README.md b/README.md index 5d814f7..26cbe94 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,9 @@ CI covers Linux/Windows/macOS. Tested with GCC, Clang, and MSVC; requires C++11. ## 📈 Versioning / SemVer policy * Follows [Semantic Versioning](https://semver.org). -* MAJOR: breaking changes to headers or exported symbols. -* MINOR: backward-compatible additions. +* Before 1.0, a MINOR release may include breaking changes to headers or + exported symbols; CMake package discovery therefore requires an exact version. +* After 1.0, MAJOR denotes breaking changes and MINOR denotes backward-compatible additions. * PATCH: bug fixes and internal changes. Version macros live in ``: @@ -62,6 +63,17 @@ builds and controls symbol export/import for shared builds. `HMACCPP_ENABLE_MLOCK` toggles best-effort page locking via `mlock`/`VirtualLock`. Disable it if the platform lacks the necessary privileges. +### Page-locking limitations + +`secure_buffer` attempts page locking only when `HMACCPP_ENABLE_MLOCK` +is enabled; it is not page-locked by default. Lock ownership is coordinated only +inside one loaded copy of hmac-cpp. Statically linking separate copies into +multiple modules creates separate registries. + +On POSIX, continuing in a child process after `fork()` without `exec()` is not +supported for page locking: the child must immediately `exec()` or avoid using +the inherited secure buffers. + ### Build ```bash diff --git a/include/hmac_cpp/page_lock_registry.hpp b/include/hmac_cpp/page_lock_registry.hpp new file mode 100644 index 0000000..cb77a5a --- /dev/null +++ b/include/hmac_cpp/page_lock_registry.hpp @@ -0,0 +1,223 @@ +#ifndef HMAC_CPP_PAGE_LOCK_REGISTRY_HPP +#define HMAC_CPP_PAGE_LOCK_REGISTRY_HPP + +#include +#include +#include +#include +#include + +namespace hmac_cpp { +namespace detail { + +/// \brief Process-wide ownership registry for page-granular memory locks. +/// \tparam PageLocker Type exposing lock_page() and unlock_page(). +/// \tparam Mutex Mutex type used to synchronize access to the registry. +template +class page_lock_registry { +public: + explicit page_lock_registry(std::size_t page_size, PageLocker locker = PageLocker()) + : page_size_(page_size), locker_(locker) {} + + bool lock(void* ptr, std::size_t len) noexcept { + std::uintptr_t first = 0; + std::uintptr_t last = 0; + if (!normalize(ptr, len, first, last)) { + return false; + } + + try { + std::lock_guard guard(mutex_); + return lock_range(first, last); + } catch (...) { + return false; + } + } + + bool unlock(void* ptr, std::size_t len) noexcept { + std::uintptr_t first = 0; + std::uintptr_t last = 0; + if (!normalize(ptr, len, first, last)) { + return false; + } + + try { + std::lock_guard guard(mutex_); + if (!all_pages_owned(first, last)) { + return false; + } + + bool success = true; + for_each_page(first, last, [this, &success](std::uintptr_t page) { + typename page_states_type::iterator entry = page_states_.find(page); + page_state& state = entry->second; + if (state.owners > 1) { + --state.owners; + return true; + } + + state.owners = 0; + if (!state.os_locked || locker_.unlock_page(reinterpret_cast(page), page_size_)) { + page_states_.erase(entry); + } else { + success = false; + } + return true; + }); + return success; + } catch (...) { + return false; + } + } + +private: + struct page_state { + std::size_t owners{}; + bool os_locked{}; + bool pending_lock{}; + }; + + using page_states_type = std::map; + + bool normalize(void* ptr, std::size_t len, std::uintptr_t& first, + std::uintptr_t& last) const noexcept { + if (ptr == nullptr || len == 0 || page_size_ == 0) { + return false; + } + + const std::uintptr_t address = reinterpret_cast(ptr); + if (len - 1 > std::numeric_limits::max() - address) { + return false; + } + const std::uintptr_t end = address + len - 1; + first = address - address % page_size_; + last = end - end % page_size_; + return true; + } + + template + void for_each_page(std::uintptr_t first, std::uintptr_t last, Visitor visitor) noexcept { + for (std::uintptr_t page = first;; page += page_size_) { + if (!visitor(page) || page == last) { + return; + } + } + } + + bool all_pages_owned(std::uintptr_t first, std::uintptr_t last) const noexcept { + for (std::uintptr_t page = first;; page += page_size_) { + const typename page_states_type::const_iterator entry = page_states_.find(page); + if (entry == page_states_.end() || entry->second.owners == 0 || entry->second.pending_lock) { + return false; + } + if (page == last) { + return true; + } + } + } + + bool lock_range(std::uintptr_t first, std::uintptr_t last) noexcept { + try { + for_each_page(first, last, [this](std::uintptr_t page) { + page_states_.emplace(page, page_state{}); + return true; + }); + } catch (...) { + erase_unlocked_pages(first, last); + return false; + } + + bool overflow = false; + for_each_page(first, last, [this, &overflow](std::uintptr_t page) { + const typename page_states_type::const_iterator entry = page_states_.find(page); + if (entry->second.owners == std::numeric_limits::max()) { + overflow = true; + return false; + } + return true; + }); + if (overflow) { + erase_unlocked_pages(first, last); + return false; + } + + for_each_page(first, last, [this](std::uintptr_t page) { + typename page_states_type::iterator entry = page_states_.find(page); + page_state& state = entry->second; + if (state.owners != 0 || state.os_locked) { + return true; + } + if (!locker_.lock_page(reinterpret_cast(page), page_size_)) { + return false; + } + state.os_locked = true; + state.pending_lock = true; + return true; + }); + + if (has_unlocked_page(first, last)) { + rollback_pending_locks(first, last); + return false; + } + + for_each_page(first, last, [this](std::uintptr_t page) { + typename page_states_type::iterator entry = page_states_.find(page); + ++entry->second.owners; + entry->second.pending_lock = false; + return true; + }); + return true; + } + + bool has_unlocked_page(std::uintptr_t first, std::uintptr_t last) const noexcept { + for (std::uintptr_t page = first;; page += page_size_) { + const typename page_states_type::const_iterator entry = page_states_.find(page); + if (entry == page_states_.end() || (!entry->second.os_locked && entry->second.owners == 0)) { + return true; + } + if (page == last) { + return false; + } + } + } + + void erase_unlocked_pages(std::uintptr_t first, std::uintptr_t last) noexcept { + for_each_page(first, last, [this](std::uintptr_t page) { + typename page_states_type::iterator entry = page_states_.find(page); + if (entry != page_states_.end() && entry->second.owners == 0 && !entry->second.os_locked) { + page_states_.erase(entry); + } + return true; + }); + } + + void rollback_pending_locks(std::uintptr_t first, std::uintptr_t last) noexcept { + for_each_page(first, last, [this](std::uintptr_t page) { + typename page_states_type::iterator entry = page_states_.find(page); + if (entry == page_states_.end()) { + return true; + } + page_state& state = entry->second; + if (state.pending_lock) { + if (locker_.unlock_page(reinterpret_cast(page), page_size_)) { + page_states_.erase(entry); + } else { + state.pending_lock = false; + } + } else if (state.owners == 0 && !state.os_locked) { + page_states_.erase(entry); + } + return true; + }); + } + + std::size_t page_size_{}; + PageLocker locker_; + page_states_type page_states_; + Mutex mutex_; +}; + +} // namespace detail +} // namespace hmac_cpp + +#endif // HMAC_CPP_PAGE_LOCK_REGISTRY_HPP diff --git a/include/hmac_cpp/secure_buffer.hpp b/include/hmac_cpp/secure_buffer.hpp index c2d0c4b..35e461f 100644 --- a/include/hmac_cpp/secure_buffer.hpp +++ b/include/hmac_cpp/secure_buffer.hpp @@ -1,13 +1,17 @@ #ifndef HMAC_CPP_SECURE_BUFFER_HPP #define HMAC_CPP_SECURE_BUFFER_HPP +#include #include #include #include #include -#include -#include +#include +#include +#include #include +#include +#include #include "hmac_cpp/memlock.hpp" #if defined(HAVE_EXPLICIT_BZERO) @@ -27,6 +31,12 @@ namespace hmac_cpp { +/// \brief Default page-locking policy used by secure_buffer. +struct page_locker { + static bool lock(void* ptr, size_t len) noexcept { return lock_pages(ptr, len); } + static bool unlock(void* ptr, size_t len) noexcept { return unlock_pages(ptr, len); } +}; + /// \brief Securely zeroes a memory region. /// \param ptr Pointer to the memory to wipe. /// \param len Number of bytes to set to zero. @@ -45,83 +55,69 @@ static inline void secure_zero(void* ptr, size_t len) { } /// \brief Vector-like buffer that zeroizes its contents on destruction. +/// Page locking is best-effort: a failed lock leaves the buffer usable, and +/// is_locked() reports false. A successful lock always records its exact range. /// \tparam T Trivial value type stored in the buffer (defaults to uint8_t). /// \tparam LockOnAlloc Lock pages in memory on allocation. -template +/// \tparam Allocator Allocator used for the backing storage. +/// \tparam PageLocker Page-locking policy used when LockOnAlloc is true. +template, + class PageLocker = page_locker> struct secure_buffer { static_assert(std::is_trivial::value, "secure_buffer requires trivial type"); - secure_buffer() { - if (LockOnAlloc && !buf.empty()) { - locked_ = lock_pages(buf.data(), buf.size() * sizeof(T)); - } - } + using vector_type = std::vector; + + secure_buffer() = default; /// \brief Construct with n default-initialized elements. /// \param n Element count. - explicit secure_buffer(size_t n) : buf(n) { - if (LockOnAlloc && !buf.empty()) { - locked_ = lock_pages(buf.data(), buf.size() * sizeof(T)); - } - } + explicit secure_buffer(size_t n) { replace_with_zeroed(n); } /// \brief Construct from vector, moving its contents. /// \param v Source vector. - explicit secure_buffer(std::vector&& v) : buf(std::move(v)) { - if (LockOnAlloc && !buf.empty()) { - locked_ = lock_pages(buf.data(), buf.size() * sizeof(T)); - } + explicit secure_buffer(vector_type&& v) : buf(validate_and_move(v)) { + lock_current_range(); } /// \brief Construct from std::string rvalue and zeroize the source. /// \param s Source string. template::value, int>::type = 0> - explicit secure_buffer(std::string&& s) : buf(s.begin(), s.end()) { - if (LockOnAlloc && !buf.empty()) { - locked_ = lock_pages(buf.data(), buf.size() * sizeof(T)); - } - if (!s.empty()) { - secure_zero(&s[0], s.size()); - s.clear(); - } + explicit secure_buffer(std::string&& s) { + string_wipe_guard source_wiper(s); + assign(reinterpret_cast(s.data()), s.size()); } - secure_buffer(const secure_buffer& other) : buf(other.buf) { - if (LockOnAlloc && !buf.empty()) { - locked_ = lock_pages(buf.data(), buf.size() * sizeof(T)); - } - } + secure_buffer(const secure_buffer& other) { replace_with_copy(other.data(), other.size()); } secure_buffer& operator=(const secure_buffer& other) { if (this != &other) { - secure_zero(buf.data(), buf.size() * sizeof(T)); - if (locked_) { - unlock_pages(buf.data(), buf.size() * sizeof(T)); - } - buf = other.buf; - if (LockOnAlloc && !buf.empty()) { - locked_ = lock_pages(buf.data(), buf.size() * sizeof(T)); - } else { - locked_ = false; - } + replace_with_copy(other.data(), other.size()); } return *this; } - secure_buffer(secure_buffer&& other) noexcept - : buf(std::move(other.buf)), locked_(other.locked_) { - other.locked_ = false; + secure_buffer(secure_buffer&& other) + : buf(std::move(other.buf)), + locked_ptr_(other.locked_ptr_), + locked_bytes_(other.locked_bytes_) { + other.clear_locked_range(); } - secure_buffer& operator=(secure_buffer&& other) noexcept { + secure_buffer& operator=(secure_buffer&& other) { if (this != &other) { - secure_zero(buf.data(), buf.size() * sizeof(T)); - if (locked_) { - unlock_pages(buf.data(), buf.size() * sizeof(T)); + if (buf.get_allocator() == other.buf.get_allocator()) { + clear(); + buf = std::move(other.buf); + locked_ptr_ = other.locked_ptr_; + locked_bytes_ = other.locked_bytes_; + other.clear_locked_range(); + } else { + replace_with_copy(other.data(), other.size()); + other.clear(); } - buf = std::move(other.buf); - locked_ = other.locked_; - other.locked_ = false; } return *this; } @@ -130,61 +126,44 @@ struct secure_buffer { ~secure_buffer() noexcept { clear(); } /// \brief Check whether pages are locked. - bool is_locked() const noexcept { return locked_; } + bool is_locked() const noexcept { return locked_ptr_ != nullptr; } /// \brief Clear and deallocate the buffer. void clear() noexcept { - secure_zero(buf.data(), buf.size() * sizeof(T)); - if (locked_) { - unlock_pages(buf.data(), buf.size() * sizeof(T)); - locked_ = false; - } - buf.clear(); - buf.shrink_to_fit(); + secure_zero(buf.data(), byte_count_noexcept(buf.size())); + unlock_locked_range(); + vector_type empty(buf.get_allocator()); + buf.swap(empty); } - /// \brief Resize the buffer, zeroizing truncated data. + /// \brief Resize the buffer, zeroizing data before a reallocation releases it. void resize(size_t n) { - T* old_ptr = buf.data(); - size_t old_sz = buf.size(); - if (n < old_sz) { - secure_zero(old_ptr + n, (old_sz - n) * sizeof(T)); - } - buf.resize(n); - if (LockOnAlloc && old_ptr != buf.data()) { - if (locked_) { - unlock_pages(old_ptr, old_sz * sizeof(T)); - } - if (!buf.empty()) { - locked_ = lock_pages(buf.data(), buf.size() * sizeof(T)); - } else { - locked_ = false; + checked_byte_count(n); + const size_t old_sz = buf.size(); + if (n <= old_sz) { + if (n < old_sz) { + secure_zero(buf.data() + n, byte_count_noexcept(old_sz - n)); } + buf.resize(n); + return; } + + // std::vector::resize may release the old allocation before we can + // wipe it. Allocate and lock the replacement first so the old secret + // remains reachable until it has been zeroized and unlocked. + replace_with_copy(buf.data(), old_sz, n); } /// \brief Assign from raw pointer. void assign(const T* p, size_t n) { - secure_zero(buf.data(), buf.size() * sizeof(T)); - if (locked_) { - unlock_pages(buf.data(), buf.size() * sizeof(T)); - } - buf.assign(p, p + n); - if (LockOnAlloc && !buf.empty()) { - locked_ = lock_pages(buf.data(), buf.size() * sizeof(T)); - } else { - locked_ = false; - } + replace_with_copy(p, n); } /// \brief Assign from std::string rvalue and zeroize the source. template::value, int>::type = 0> void assign(std::string&& s) { + string_wipe_guard source_wiper(s); assign(reinterpret_cast(s.data()), s.size()); - if (!s.empty()) { - secure_zero(&s[0], s.size()); - s.clear(); - } } T* data() { return buf.data(); } @@ -194,14 +173,143 @@ struct secure_buffer { T& operator[](size_t i) { return buf[i]; } const T& operator[](size_t i) const { return buf[i]; } - typename std::vector::iterator begin() { return buf.begin(); } - typename std::vector::iterator end() { return buf.end(); } - typename std::vector::const_iterator begin() const { return buf.begin(); } - typename std::vector::const_iterator end() const { return buf.end(); } + typename vector_type::iterator begin() { return buf.begin(); } + typename vector_type::iterator end() { return buf.end(); } + typename vector_type::const_iterator begin() const { return buf.begin(); } + typename vector_type::const_iterator end() const { return buf.end(); } private: - std::vector buf; - bool locked_{}; + struct string_wipe_guard { + explicit string_wipe_guard(std::string& value) noexcept : value_(value) {} + ~string_wipe_guard() noexcept { wipe_string(value_); } + + std::string& value_; + }; + + struct replacement_guard { + replacement_guard(void* ptr, size_t bytes) noexcept : ptr_(ptr), bytes_(bytes) {} + + ~replacement_guard() noexcept { + if (!active_) { + return; + } + secure_zero(ptr_, bytes_); + if (locked_) { + PageLocker::unlock(ptr_, bytes_); + } + } + + void lock() noexcept { + if (LockOnAlloc && ptr_ != nullptr && bytes_ != 0) { + locked_ = PageLocker::lock(ptr_, bytes_); + } + } + + bool release() noexcept { + active_ = false; + return locked_; + } + + void* ptr_{}; + size_t bytes_{}; + bool locked_{}; + bool active_{true}; + }; + + void lock_current_range() { + if (LockOnAlloc && !buf.empty()) { + const size_t bytes = checked_byte_count(buf.size()); + set_locked_range(buf.data(), bytes, PageLocker::lock(buf.data(), bytes)); + } + } + + static size_t checked_byte_count(size_t count) { + if (count > std::numeric_limits::max() / sizeof(T)) { + throw std::length_error("secure_buffer size exceeds addressable byte range"); + } + return count * sizeof(T); + } + + static size_t byte_count_noexcept(size_t count) noexcept { + return count * sizeof(T); + } + + static void wipe_string(std::string& value) noexcept { + if (!value.empty()) { + secure_zero(&value[0], value.size()); + value.clear(); + } + } + + static vector_type validate_and_move(vector_type& source) { + checked_byte_count(source.size()); + return std::move(source); + } + + vector_type make_replacement(size_t n) { + checked_byte_count(n); + return vector_type(n, T{}, buf.get_allocator()); + } + + void replace_with_zeroed(size_t n) { + vector_type replacement = make_replacement(n); + replacement_guard guard(replacement.data(), checked_byte_count(replacement.size())); + guard.lock(); + replace_with_replacement(replacement, guard); + } + + void replace_with_copy(const T* source, size_t n) { + replace_with_copy(source, n, n); + } + + void replace_with_copy(const T* source, size_t source_size, size_t destination_size) { + if (source == nullptr && source_size != 0) { + throw std::invalid_argument("secure_buffer source cannot be null"); + } + if (source_size > destination_size) { + throw std::length_error("secure_buffer destination is smaller than source"); + } + + vector_type replacement = make_replacement(destination_size); + replacement_guard guard(replacement.data(), checked_byte_count(replacement.size())); + guard.lock(); + if (source_size != 0) { + std::copy_n(source, source_size, replacement.begin()); + } + replace_with_replacement(replacement, guard); + } + + void replace_with_replacement(vector_type& replacement, replacement_guard& guard) { + secure_zero(buf.data(), byte_count_noexcept(buf.size())); + unlock_locked_range(); + buf.swap(replacement); + set_locked_range(buf.data(), byte_count_noexcept(buf.size()), guard.release()); + } + + void unlock_locked_range() noexcept { + if (locked_ptr_ != nullptr) { + PageLocker::unlock(locked_ptr_, locked_bytes_); + } + clear_locked_range(); + } + + void set_locked_range(void* ptr, size_t bytes, bool locked) noexcept { + if (locked) { + locked_ptr_ = ptr; + locked_bytes_ = bytes; + } else { + clear_locked_range(); + } + } + + void clear_locked_range() noexcept { + locked_ptr_ = nullptr; + locked_bytes_ = 0; + } + + vector_type buf; + void* locked_ptr_{}; + size_t locked_bytes_{}; }; } // namespace hmac_cpp diff --git a/include/hmac_cpp/version.hpp b/include/hmac_cpp/version.hpp index df57d62..01e646a 100644 --- a/include/hmac_cpp/version.hpp +++ b/include/hmac_cpp/version.hpp @@ -2,8 +2,8 @@ #define HMAC_CPP_VERSION_HPP #define HMAC_CPP_VERSION_MAJOR 0 -#define HMAC_CPP_VERSION_MINOR 5 +#define HMAC_CPP_VERSION_MINOR 6 #define HMAC_CPP_VERSION_PATCH 0 -#define HMAC_CPP_VERSION "0.5.0" +#define HMAC_CPP_VERSION "0.6.0" #endif // HMAC_CPP_VERSION_HPP diff --git a/src/hmac_utils.cpp b/src/hmac_utils.cpp index a16d10e..5863863 100644 --- a/src/hmac_utils.cpp +++ b/src/hmac_utils.cpp @@ -135,7 +135,8 @@ namespace hmac_cpp { iterations, dk_len, prf); PageLockGuard lock(derived.data(), derived.size()); secure_buffer out(std::move(derived)); - lock.locked = false; + // The buffer registers its own lock. Let the temporary guard release + // its reference after ownership of the allocation has moved. return out; } diff --git a/src/memlock.cpp b/src/memlock.cpp index f0cf68e..d795fb6 100644 --- a/src/memlock.cpp +++ b/src/memlock.cpp @@ -1,16 +1,30 @@ #include "hmac_cpp/memlock.hpp" +#include "hmac_cpp/page_lock_registry.hpp" #if defined(HMAC_CPP_ENABLE_MLOCK) #if defined(_WIN32) #include namespace { -inline bool do_lock(void* p, size_t n) { return VirtualLock(p, n) != 0; } -inline bool do_unlock(void* p, size_t n){ return VirtualUnlock(p, n) != 0; } +struct os_page_locker { + bool lock_page(void* ptr, size_t len) const noexcept { return VirtualLock(ptr, len) != 0; } + bool unlock_page(void* ptr, size_t len) const noexcept { return VirtualUnlock(ptr, len) != 0; } +}; + +size_t system_page_size() noexcept { + SYSTEM_INFO info{}; + GetSystemInfo(&info); + return static_cast(info.dwPageSize); +} + +hmac_cpp::detail::page_lock_registry& registry() { + static hmac_cpp::detail::page_lock_registry value(system_page_size()); + return value; +} } namespace hmac_cpp { -bool lock_pages(void* ptr, size_t len) noexcept { return (ptr && len) ? do_lock(ptr, len) : false; } -bool unlock_pages(void* ptr, size_t len) noexcept { return (ptr && len) ? do_unlock(ptr, len) : false; } +bool lock_pages(void* ptr, size_t len) noexcept { return registry().lock(ptr, len); } +bool unlock_pages(void* ptr, size_t len) noexcept { return registry().unlock(ptr, len); } } #else @@ -19,32 +33,28 @@ bool unlock_pages(void* ptr, size_t len) noexcept { return (ptr && len) ? do_unl #include namespace { -inline void* page_align(void* p, size_t len, size_t& out_len) { - if (!p || !len) { out_len = 0; return p; } - long ps = sysconf(_SC_PAGESIZE); - if (ps <= 0) { out_len = 0; return p; } - uintptr_t addr = reinterpret_cast(p); - uintptr_t start = addr & ~static_cast(ps - 1); - uintptr_t end = (addr + len + ps - 1) & ~static_cast(ps - 1); - out_len = static_cast(end - start); - return reinterpret_cast(start); +struct os_page_locker { + bool lock_page(void* ptr, size_t len) const noexcept { return ::mlock(ptr, len) == 0; } + bool unlock_page(void* ptr, size_t len) const noexcept { return ::munlock(ptr, len) == 0; } +}; + +size_t system_page_size() noexcept { + const long value = sysconf(_SC_PAGESIZE); + return value > 0 ? static_cast(value) : 0; +} + +hmac_cpp::detail::page_lock_registry& registry() { + static hmac_cpp::detail::page_lock_registry value(system_page_size()); + return value; } } namespace hmac_cpp { bool lock_pages(void* ptr, size_t len) noexcept { - if (!ptr || !len) return false; - size_t alen = 0; - void* aptr = page_align(ptr, len, alen); - if (!aptr || !alen) return false; - return ::mlock(aptr, alen) == 0; + return registry().lock(ptr, len); } bool unlock_pages(void* ptr, size_t len) noexcept { - if (!ptr || !len) return false; - size_t alen = 0; - void* aptr = page_align(ptr, len, alen); - if (!aptr || !alen) return false; - return ::munlock(aptr, alen) == 0; + return registry().unlock(ptr, len); } } diff --git a/test_secure_buffer.cpp b/test_secure_buffer.cpp new file mode 100644 index 0000000..418d40b --- /dev/null +++ b/test_secure_buffer.cpp @@ -0,0 +1,639 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "hmac_cpp/page_lock_registry.hpp" +#include "hmac_cpp/secure_buffer.hpp" + +namespace { + +struct DeallocationRecord { + const void* ptr{}; + std::size_t elements{}; + bool was_zeroed{}; +}; + +struct RecordingAllocator { + using value_type = std::uint8_t; + + template + struct rebind { + using other = RecordingAllocator; + }; + + RecordingAllocator() noexcept = default; + + template + RecordingAllocator(const U&) noexcept {} + + std::uint8_t* allocate(std::size_t count) { + return std::allocator{}.allocate(count); + } + + void deallocate(std::uint8_t* ptr, std::size_t count) noexcept { + deallocations[deallocation_count++] = { + ptr, + count, + std::all_of(ptr, ptr + count, [](std::uint8_t value) { return value == 0; }) + }; + std::allocator{}.deallocate(ptr, count); + } + + static void reset() noexcept { deallocation_count = 0; } + + static std::array deallocations; + static std::size_t deallocation_count; +}; + +bool operator==(const RecordingAllocator&, const RecordingAllocator&) noexcept { return true; } +bool operator!=(const RecordingAllocator&, const RecordingAllocator&) noexcept { return false; } + +std::array RecordingAllocator::deallocations{}; +std::size_t RecordingAllocator::deallocation_count{}; + +template +struct StatefulAllocator { + using value_type = T; + + template + struct rebind { + using other = StatefulAllocator; + }; + + explicit StatefulAllocator(int value = 0) noexcept : id(value) {} + StatefulAllocator(const StatefulAllocator&) noexcept = default; + StatefulAllocator(StatefulAllocator&&) noexcept = default; + StatefulAllocator& operator=(const StatefulAllocator&) noexcept = default; + StatefulAllocator& operator=(StatefulAllocator&&) noexcept = default; + + template + StatefulAllocator(const StatefulAllocator& other) noexcept : id(other.id) {} + + T* allocate(std::size_t count) { + return std::allocator{}.allocate(count); + } + + void deallocate(T* ptr, std::size_t count) noexcept { + std::allocator{}.deallocate(ptr, count); + } + + int id{}; +}; + +template +bool operator==(const StatefulAllocator& lhs, const StatefulAllocator& rhs) noexcept { + return lhs.id == rhs.id; +} + +template +bool operator!=(const StatefulAllocator& lhs, const StatefulAllocator& rhs) noexcept { + return !(lhs == rhs); +} + +enum class PageLockEventType { Lock, Unlock }; + +struct PageLockEvent { + PageLockEventType type{}; + void* ptr{}; + std::size_t bytes{}; + bool was_zeroed_at_lock{}; + bool was_zeroed_at_unlock{}; +}; + +struct RecordingPageLocker { + static bool lock(void* ptr, std::size_t bytes) noexcept { + events[event_count++] = { + PageLockEventType::Lock, + ptr, + bytes, + std::all_of(static_cast(ptr), + static_cast(ptr) + bytes, + [](std::uint8_t value) { return value == 0; }), + false + }; + return lock_succeeds; + } + + static bool unlock(void* ptr, std::size_t bytes) noexcept { + events[event_count++] = { + PageLockEventType::Unlock, + ptr, + bytes, + false, + std::all_of(static_cast(ptr), + static_cast(ptr) + bytes, + [](std::uint8_t value) { return value == 0; }) + }; + return true; + } + + static void reset() noexcept { event_count = 0; } + + static std::array events; + static std::size_t event_count; + static bool lock_succeeds; +}; + +std::array RecordingPageLocker::events{}; +std::size_t RecordingPageLocker::event_count{}; +bool RecordingPageLocker::lock_succeeds{true}; + +struct ThrowingAllocator { + using value_type = std::uint8_t; + + template + struct rebind { + using other = ThrowingAllocator; + }; + + ThrowingAllocator() noexcept = default; + + template + ThrowingAllocator(const U&) noexcept {} + + std::uint8_t* allocate(std::size_t count) { + if (fail_allocations) { + throw std::bad_alloc(); + } + return std::allocator{}.allocate(count); + } + + void deallocate(std::uint8_t* ptr, std::size_t count) noexcept { + std::allocator{}.deallocate(ptr, count); + } + + static bool fail_allocations; +}; + +bool operator==(const ThrowingAllocator&, const ThrowingAllocator&) noexcept { return true; } +bool operator!=(const ThrowingAllocator&, const ThrowingAllocator&) noexcept { return false; } +bool ThrowingAllocator::fail_allocations{}; + +enum class RegistryEventType { Lock, Unlock }; + +struct RegistryEvent { + RegistryEventType type{}; + void* page{}; +}; + +struct RegistryPageLocker { + bool lock_page(void* page, std::size_t) const noexcept { + events[event_count++] = {RegistryEventType::Lock, page}; + return reinterpret_cast(page) != failing_page; + } + + bool unlock_page(void* page, std::size_t) const noexcept { + events[event_count++] = {RegistryEventType::Unlock, page}; + return reinterpret_cast(page) != failing_unlock_page; + } + + static void reset() noexcept { + event_count = 0; + failing_page = 0; + failing_unlock_page = 0; + } + + static std::array events; + static std::size_t event_count; + static std::uintptr_t failing_page; + static std::uintptr_t failing_unlock_page; +}; + +std::array RegistryPageLocker::events{}; +std::size_t RegistryPageLocker::event_count{}; +std::uintptr_t RegistryPageLocker::failing_page{}; +std::uintptr_t RegistryPageLocker::failing_unlock_page{}; + +struct ThrowingMutex { + void lock() { + throw std::system_error(std::make_error_code(std::errc::resource_unavailable_try_again)); + } + + void unlock() noexcept {} +}; + +using UnlockedBuffer = hmac_cpp::secure_buffer; +using LockedBuffer = hmac_cpp::secure_buffer; +using WideBuffer = hmac_cpp::secure_buffer, + RecordingPageLocker>; +using StatefulBuffer = hmac_cpp::secure_buffer, + RecordingPageLocker>; +using ThrowingBuffer = hmac_cpp::secure_buffer; + +TEST(SecureBufferTest, ResizeGrowthWipesOldAllocationBeforeDeallocate) { + UnlockedBuffer buffer(8); + for (std::size_t index = 0; index < buffer.size(); ++index) { + buffer[index] = static_cast(index + 1); + } + RecordingAllocator::reset(); + + buffer.resize(4096); + + ASSERT_EQ(RecordingAllocator::deallocation_count, 1u); + EXPECT_EQ(RecordingAllocator::deallocations[0].elements, 8u); + EXPECT_TRUE(RecordingAllocator::deallocations[0].was_zeroed); + for (std::size_t index = 0; index < 8; ++index) { + EXPECT_EQ(buffer[index], static_cast(index + 1)); + } + EXPECT_TRUE(std::all_of(buffer.begin() + 8, buffer.end(), + [](std::uint8_t value) { return value == 0; })); +} + +TEST(SecureBufferTest, ShrinkThenClearUnlocksTheOriginalRange) { + RecordingPageLocker::lock_succeeds = true; + LockedBuffer buffer(8192); + void* const original_ptr = buffer.data(); + RecordingPageLocker::reset(); + + buffer.resize(1); + buffer.clear(); + + ASSERT_EQ(RecordingPageLocker::event_count, 1u); + EXPECT_EQ(RecordingPageLocker::events[0].type, PageLockEventType::Unlock); + EXPECT_EQ(RecordingPageLocker::events[0].ptr, original_ptr); + EXPECT_EQ(RecordingPageLocker::events[0].bytes, 8192u); +} + +TEST(SecureBufferTest, ShrinkThenGrowLocksNewRangeBeforeUnlockingOriginalRange) { + RecordingPageLocker::lock_succeeds = true; + LockedBuffer buffer(8192); + void* const original_ptr = buffer.data(); + RecordingPageLocker::reset(); + + buffer.resize(1); + buffer.resize(16384); + + ASSERT_EQ(RecordingPageLocker::event_count, 2u); + EXPECT_EQ(RecordingPageLocker::events[0].type, PageLockEventType::Lock); + EXPECT_EQ(RecordingPageLocker::events[0].ptr, buffer.data()); + EXPECT_EQ(RecordingPageLocker::events[0].bytes, 16384u); + EXPECT_EQ(RecordingPageLocker::events[1].type, PageLockEventType::Unlock); + EXPECT_EQ(RecordingPageLocker::events[1].ptr, original_ptr); + EXPECT_EQ(RecordingPageLocker::events[1].bytes, 8192u); + EXPECT_TRUE(RecordingPageLocker::events[1].was_zeroed_at_unlock); + + void* const replacement_ptr = buffer.data(); + buffer.clear(); + ASSERT_EQ(RecordingPageLocker::event_count, 3u); + EXPECT_EQ(RecordingPageLocker::events[2].type, PageLockEventType::Unlock); + EXPECT_EQ(RecordingPageLocker::events[2].ptr, replacement_ptr); + EXPECT_EQ(RecordingPageLocker::events[2].bytes, 16384u); + EXPECT_TRUE(RecordingPageLocker::events[2].was_zeroed_at_unlock); +} + +TEST(SecureBufferTest, MultipleShrinksKeepTheOriginalLockRange) { + RecordingPageLocker::lock_succeeds = true; + LockedBuffer buffer(8192); + void* const original_ptr = buffer.data(); + RecordingPageLocker::reset(); + + buffer.resize(4097); + buffer.resize(1); + buffer.clear(); + + ASSERT_EQ(RecordingPageLocker::event_count, 1u); + EXPECT_EQ(RecordingPageLocker::events[0].type, PageLockEventType::Unlock); + EXPECT_EQ(RecordingPageLocker::events[0].ptr, original_ptr); + EXPECT_EQ(RecordingPageLocker::events[0].bytes, 8192u); + EXPECT_TRUE(RecordingPageLocker::events[0].was_zeroed_at_unlock); +} + +TEST(SecureBufferTest, CopyConstructionLocksAZeroedReplacementBeforeCopying) { + RecordingPageLocker::lock_succeeds = true; + LockedBuffer source(8); + std::fill(source.begin(), source.end(), static_cast(0xA5)); + RecordingPageLocker::reset(); + + LockedBuffer copy(source); + + ASSERT_EQ(RecordingPageLocker::event_count, 1u); + EXPECT_EQ(RecordingPageLocker::events[0].type, PageLockEventType::Lock); + EXPECT_EQ(RecordingPageLocker::events[0].ptr, copy.data()); + EXPECT_EQ(RecordingPageLocker::events[0].bytes, 8u); + EXPECT_TRUE(RecordingPageLocker::events[0].was_zeroed_at_lock); + EXPECT_TRUE(std::equal(source.begin(), source.end(), copy.begin())); +} + +TEST(SecureBufferTest, StringConstructionLocksAZeroedReplacementBeforeCopying) { + RecordingPageLocker::lock_succeeds = true; + std::string source("secret"); + RecordingPageLocker::reset(); + + LockedBuffer buffer(std::move(source)); + + ASSERT_EQ(RecordingPageLocker::event_count, 1u); + EXPECT_EQ(RecordingPageLocker::events[0].type, PageLockEventType::Lock); + EXPECT_EQ(RecordingPageLocker::events[0].ptr, buffer.data()); + EXPECT_EQ(RecordingPageLocker::events[0].bytes, 6u); + EXPECT_TRUE(RecordingPageLocker::events[0].was_zeroed_at_lock); + EXPECT_EQ(buffer.size(), 6u); + EXPECT_TRUE(source.empty()); +} + +TEST(SecureBufferTest, CopyAssignmentLocksReplacementBeforeReleasingDestination) { + RecordingPageLocker::lock_succeeds = true; + LockedBuffer source(8); + std::fill(source.begin(), source.end(), static_cast(0xA5)); + LockedBuffer destination(4); + void* const destination_ptr = destination.data(); + RecordingPageLocker::reset(); + + destination = source; + + ASSERT_EQ(RecordingPageLocker::event_count, 2u); + EXPECT_EQ(RecordingPageLocker::events[0].type, PageLockEventType::Lock); + EXPECT_EQ(RecordingPageLocker::events[0].ptr, destination.data()); + EXPECT_EQ(RecordingPageLocker::events[0].bytes, 8u); + EXPECT_TRUE(RecordingPageLocker::events[0].was_zeroed_at_lock); + EXPECT_EQ(RecordingPageLocker::events[1].type, PageLockEventType::Unlock); + EXPECT_EQ(RecordingPageLocker::events[1].ptr, destination_ptr); + EXPECT_EQ(RecordingPageLocker::events[1].bytes, 4u); + EXPECT_TRUE(RecordingPageLocker::events[1].was_zeroed_at_unlock); + EXPECT_TRUE(std::equal(source.begin(), source.end(), destination.begin())); +} + +TEST(SecureBufferTest, AssignLocksReplacementBeforeReleasingDestination) { + RecordingPageLocker::lock_succeeds = true; + const std::array source{{1, 2, 3, 4, 5, 6, 7, 8}}; + LockedBuffer destination(4); + void* const destination_ptr = destination.data(); + RecordingPageLocker::reset(); + + destination.assign(source.data(), source.size()); + + ASSERT_EQ(RecordingPageLocker::event_count, 2u); + EXPECT_EQ(RecordingPageLocker::events[0].type, PageLockEventType::Lock); + EXPECT_EQ(RecordingPageLocker::events[0].ptr, destination.data()); + EXPECT_EQ(RecordingPageLocker::events[0].bytes, 8u); + EXPECT_TRUE(RecordingPageLocker::events[0].was_zeroed_at_lock); + EXPECT_EQ(RecordingPageLocker::events[1].type, PageLockEventType::Unlock); + EXPECT_EQ(RecordingPageLocker::events[1].ptr, destination_ptr); + EXPECT_EQ(RecordingPageLocker::events[1].bytes, 4u); + EXPECT_TRUE(RecordingPageLocker::events[1].was_zeroed_at_unlock); + EXPECT_TRUE(std::equal(source.begin(), source.end(), destination.begin())); +} + +TEST(SecureBufferTest, MoveConstructionTransfersTheLockedRange) { + RecordingPageLocker::lock_succeeds = true; + LockedBuffer source(8); + void* const source_ptr = source.data(); + RecordingPageLocker::reset(); + + LockedBuffer destination(std::move(source)); + + EXPECT_EQ(destination.data(), source_ptr); + EXPECT_TRUE(destination.is_locked()); + EXPECT_FALSE(source.is_locked()); + EXPECT_EQ(RecordingPageLocker::event_count, 0u); + destination.clear(); + ASSERT_EQ(RecordingPageLocker::event_count, 1u); + EXPECT_EQ(RecordingPageLocker::events[0].type, PageLockEventType::Unlock); + EXPECT_EQ(RecordingPageLocker::events[0].ptr, source_ptr); + EXPECT_EQ(RecordingPageLocker::events[0].bytes, 8u); +} + +TEST(SecureBufferTest, MoveAssignmentTransfersTheLockedRangeForEqualAllocators) { + RecordingPageLocker::lock_succeeds = true; + LockedBuffer source(8); + void* const source_ptr = source.data(); + LockedBuffer destination(4); + void* const destination_ptr = destination.data(); + RecordingPageLocker::reset(); + + destination = std::move(source); + + EXPECT_EQ(destination.data(), source_ptr); + EXPECT_TRUE(destination.is_locked()); + EXPECT_FALSE(source.is_locked()); + ASSERT_EQ(RecordingPageLocker::event_count, 1u); + EXPECT_EQ(RecordingPageLocker::events[0].type, PageLockEventType::Unlock); + EXPECT_EQ(RecordingPageLocker::events[0].ptr, destination_ptr); + EXPECT_EQ(RecordingPageLocker::events[0].bytes, 4u); + destination.clear(); + ASSERT_EQ(RecordingPageLocker::event_count, 2u); + EXPECT_EQ(RecordingPageLocker::events[1].type, PageLockEventType::Unlock); + EXPECT_EQ(RecordingPageLocker::events[1].ptr, source_ptr); + EXPECT_EQ(RecordingPageLocker::events[1].bytes, 8u); +} + +TEST(SecureBufferTest, MoveAssignmentCopiesSafelyForUnequalAllocators) { + RecordingPageLocker::lock_succeeds = true; + StatefulBuffer::vector_type source_storage(8, static_cast(0xA5), + StatefulAllocator(1)); + StatefulBuffer source(std::move(source_storage)); + void* const source_ptr = source.data(); + StatefulBuffer destination(4); + void* const destination_ptr = destination.data(); + RecordingPageLocker::reset(); + + destination = std::move(source); + + ASSERT_EQ(RecordingPageLocker::event_count, 3u); + EXPECT_EQ(RecordingPageLocker::events[0].type, PageLockEventType::Lock); + EXPECT_EQ(RecordingPageLocker::events[0].ptr, destination.data()); + EXPECT_EQ(RecordingPageLocker::events[0].bytes, 8u); + EXPECT_TRUE(RecordingPageLocker::events[0].was_zeroed_at_lock); + EXPECT_EQ(RecordingPageLocker::events[1].type, PageLockEventType::Unlock); + EXPECT_EQ(RecordingPageLocker::events[1].ptr, destination_ptr); + EXPECT_EQ(RecordingPageLocker::events[1].bytes, 4u); + EXPECT_EQ(RecordingPageLocker::events[2].type, PageLockEventType::Unlock); + EXPECT_EQ(RecordingPageLocker::events[2].ptr, source_ptr); + EXPECT_EQ(RecordingPageLocker::events[2].bytes, 8u); + EXPECT_TRUE(RecordingPageLocker::events[1].was_zeroed_at_unlock); + EXPECT_TRUE(RecordingPageLocker::events[2].was_zeroed_at_unlock); + EXPECT_FALSE(source.is_locked()); + EXPECT_TRUE(std::all_of(destination.begin(), destination.end(), + [](std::uint8_t value) { return value == 0xA5; })); +} + +TEST(SecureBufferTest, FailedPageLockLeavesTheBufferUsableAndUnlocked) { + RecordingPageLocker::lock_succeeds = false; + RecordingPageLocker::reset(); + + LockedBuffer buffer(32); + + EXPECT_FALSE(buffer.is_locked()); + ASSERT_EQ(RecordingPageLocker::event_count, 1u); + EXPECT_EQ(RecordingPageLocker::events[0].type, PageLockEventType::Lock); + buffer.clear(); + EXPECT_EQ(RecordingPageLocker::event_count, 1u); + RecordingPageLocker::lock_succeeds = true; +} + +TEST(SecureBufferTest, LockRangeUsesByteSizeForWiderElementTypes) { + RecordingPageLocker::lock_succeeds = true; + RecordingPageLocker::reset(); + + WideBuffer buffer(3); + void* const buffer_ptr = buffer.data(); + + ASSERT_EQ(RecordingPageLocker::event_count, 1u); + EXPECT_EQ(RecordingPageLocker::events[0].type, PageLockEventType::Lock); + EXPECT_EQ(RecordingPageLocker::events[0].ptr, buffer_ptr); + EXPECT_EQ(RecordingPageLocker::events[0].bytes, 3u * sizeof(std::uint32_t)); + EXPECT_TRUE(RecordingPageLocker::events[0].was_zeroed_at_lock); + buffer.clear(); + ASSERT_EQ(RecordingPageLocker::event_count, 2u); + EXPECT_EQ(RecordingPageLocker::events[1].type, PageLockEventType::Unlock); + EXPECT_EQ(RecordingPageLocker::events[1].ptr, buffer_ptr); + EXPECT_EQ(RecordingPageLocker::events[1].bytes, 3u * sizeof(std::uint32_t)); +} + +TEST(SecureBufferTest, RejectsElementCountsWhoseByteSizeOverflows) { + hmac_cpp::secure_buffer buffer; + + EXPECT_THROW(buffer.resize(std::numeric_limits::max()), std::length_error); +} + +TEST(SecureBufferTest, StringAssignWipesTheSourceWhenReplacementAllocationFails) { + ThrowingAllocator::fail_allocations = false; + ThrowingBuffer destination(4); + destination[0] = 0x5A; + const void* const destination_ptr = destination.data(); + std::string source(128, 's'); + const char* const source_data = source.data(); + const std::size_t source_size = source.size(); + ThrowingAllocator::fail_allocations = true; + + EXPECT_THROW(destination.assign(std::move(source)), std::bad_alloc); + + ThrowingAllocator::fail_allocations = false; + EXPECT_TRUE(source.empty()); + EXPECT_TRUE(std::all_of(source_data, source_data + source_size, + [](char value) { return value == 0; })); + EXPECT_EQ(destination.data(), destination_ptr); + EXPECT_EQ(destination.size(), 4u); + EXPECT_EQ(destination[0], 0x5A); +} + +TEST(PageLockRegistryTest, SharedPageRemainsLockedUntilEveryOwnerReleasesIt) { + RegistryPageLocker::reset(); + hmac_cpp::detail::page_lock_registry registry(4096); + void* const first = reinterpret_cast(static_cast(0x1001)); + void* const second = reinterpret_cast(static_cast(0x1008)); + + ASSERT_TRUE(registry.lock(first, 8)); + ASSERT_TRUE(registry.lock(second, 8)); + ASSERT_TRUE(registry.unlock(first, 8)); + + ASSERT_EQ(RegistryPageLocker::event_count, 1u); + EXPECT_EQ(RegistryPageLocker::events[0].type, RegistryEventType::Lock); + EXPECT_EQ(reinterpret_cast(RegistryPageLocker::events[0].page), 0x1000u); + + ASSERT_TRUE(registry.unlock(second, 8)); + ASSERT_EQ(RegistryPageLocker::event_count, 2u); + EXPECT_EQ(RegistryPageLocker::events[1].type, RegistryEventType::Unlock); + EXPECT_EQ(reinterpret_cast(RegistryPageLocker::events[1].page), 0x1000u); +} + +TEST(PageLockRegistryTest, RollsBackPagesLockedBeforeAPartialFailure) { + RegistryPageLocker::reset(); + RegistryPageLocker::failing_page = 0x2000; + hmac_cpp::detail::page_lock_registry registry(4096); + void* const range = reinterpret_cast(static_cast(0x1ff8)); + + EXPECT_FALSE(registry.lock(range, 16)); + ASSERT_EQ(RegistryPageLocker::event_count, 3u); + EXPECT_EQ(RegistryPageLocker::events[0].type, RegistryEventType::Lock); + EXPECT_EQ(reinterpret_cast(RegistryPageLocker::events[0].page), 0x1000u); + EXPECT_EQ(RegistryPageLocker::events[1].type, RegistryEventType::Lock); + EXPECT_EQ(reinterpret_cast(RegistryPageLocker::events[1].page), 0x2000u); + EXPECT_EQ(RegistryPageLocker::events[2].type, RegistryEventType::Unlock); + EXPECT_EQ(reinterpret_cast(RegistryPageLocker::events[2].page), 0x1000u); + EXPECT_FALSE(registry.unlock(range, 16)); +} + +TEST(PageLockRegistryTest, UnlockFailureDoesNotCreateAPhantomOwner) { + RegistryPageLocker::reset(); + hmac_cpp::detail::page_lock_registry registry(4096); + void* const page = reinterpret_cast(static_cast(0x1001)); + + ASSERT_TRUE(registry.lock(page, 8)); + RegistryPageLocker::failing_unlock_page = 0x1000; + EXPECT_FALSE(registry.unlock(page, 8)); + EXPECT_FALSE(registry.unlock(page, 8)); + ASSERT_EQ(RegistryPageLocker::event_count, 2u); + + RegistryPageLocker::failing_unlock_page = 0; + ASSERT_TRUE(registry.lock(page, 8)); + ASSERT_EQ(RegistryPageLocker::event_count, 2u); + EXPECT_TRUE(registry.unlock(page, 8)); + ASSERT_EQ(RegistryPageLocker::event_count, 3u); + EXPECT_EQ(RegistryPageLocker::events[2].type, RegistryEventType::Unlock); +} + +TEST(PageLockRegistryTest, RetryAfterFailedMixedRangeUnlockPreservesOverlappingOwner) { + RegistryPageLocker::reset(); + hmac_cpp::detail::page_lock_registry registry(4096); + void* const mixed_range = reinterpret_cast(static_cast(0x1000)); + void* const overlapping_page = reinterpret_cast(static_cast(0x1004)); + void* const second_page = reinterpret_cast(static_cast(0x2000)); + + ASSERT_TRUE(registry.lock(mixed_range, 4097)); + ASSERT_TRUE(registry.lock(overlapping_page, 8)); + RegistryPageLocker::failing_unlock_page = 0x2000; + EXPECT_FALSE(registry.unlock(mixed_range, 4097)); + ASSERT_EQ(RegistryPageLocker::event_count, 3u); + + RegistryPageLocker::failing_unlock_page = 0; + EXPECT_FALSE(registry.unlock(mixed_range, 4097)); + ASSERT_EQ(RegistryPageLocker::event_count, 3u); + EXPECT_TRUE(registry.unlock(overlapping_page, 8)); + ASSERT_EQ(RegistryPageLocker::event_count, 4u); + EXPECT_EQ(RegistryPageLocker::events[3].type, RegistryEventType::Unlock); + EXPECT_EQ(RegistryPageLocker::events[3].page, mixed_range); + + ASSERT_TRUE(registry.lock(second_page, 8)); + ASSERT_EQ(RegistryPageLocker::event_count, 4u); + EXPECT_TRUE(registry.unlock(second_page, 8)); + ASSERT_EQ(RegistryPageLocker::event_count, 5u); +} + +TEST(PageLockRegistryTest, RollbackUnlockFailureCanBeReleasedByAFutureOwner) { + RegistryPageLocker::reset(); + hmac_cpp::detail::page_lock_registry registry(4096); + void* const mixed_range = reinterpret_cast(static_cast(0x1000)); + void* const first_page = reinterpret_cast(static_cast(0x1008)); + + RegistryPageLocker::failing_page = 0x2000; + RegistryPageLocker::failing_unlock_page = 0x1000; + EXPECT_FALSE(registry.lock(mixed_range, 4097)); + ASSERT_EQ(RegistryPageLocker::event_count, 3u); + + RegistryPageLocker::failing_page = 0; + RegistryPageLocker::failing_unlock_page = 0; + EXPECT_FALSE(registry.unlock(mixed_range, 4097)); + ASSERT_EQ(RegistryPageLocker::event_count, 3u); + ASSERT_TRUE(registry.lock(first_page, 8)); + ASSERT_EQ(RegistryPageLocker::event_count, 3u); + EXPECT_TRUE(registry.unlock(first_page, 8)); + ASSERT_EQ(RegistryPageLocker::event_count, 4u); + EXPECT_EQ(RegistryPageLocker::events[3].type, RegistryEventType::Unlock); + EXPECT_EQ(RegistryPageLocker::events[3].page, reinterpret_cast(static_cast(0x1000))); +} + +TEST(PageLockRegistryTest, MutexFailureReturnsFalseInsteadOfTerminating) { + RegistryPageLocker::reset(); + hmac_cpp::detail::page_lock_registry registry(4096); + void* const page = reinterpret_cast(static_cast(0x1001)); + + EXPECT_FALSE(registry.lock(page, 8)); + EXPECT_FALSE(registry.unlock(page, 8)); + EXPECT_EQ(RegistryPageLocker::event_count, 0u); +} + +} // namespace