#include "dpf/random.hpp" #include #include #include #include #include #include #include #include #include #include #include #include "dpf/bit.hpp" #include "dpf/xor_wrapper.hpp" #include "simde/simde/x86/avx2.h" namespace dpf_rng_test { std::uint64_t sample_from_other_tu(); } namespace { struct HookState { int calls = 0; std::size_t last_n = 0; unsigned char byte = 0xA5; bool use_typed = false; std::int32_t typed = 1; std::vector sizes; const unsigned char * script = nullptr; std::size_t script_i = 0; }; HookState g_hook; void reset_hook() { g_hook = HookState{}; dpf::detail::uniform_bytes_hook = nullptr; } void test_hook(void * dst, std::size_t n) { ++g_hook.calls; g_hook.last_n = n; g_hook.sizes.push_back(n); if (g_hook.script != nullptr) { std::memcpy(dst, g_hook.script + g_hook.script_i, n); g_hook.script_i += n; return; } if (g_hook.use_typed && n == sizeof(g_hook.typed)) { std::memcpy(dst, &g_hook.typed, n); return; } std::memset(dst, g_hook.byte, n); } void fill_byte(void * dst, std::size_t n, unsigned char byte) { std::memset(dst, byte, n); } template T load_bytes(unsigned char byte) { T value; fill_byte(&value, sizeof(value), byte); return value; } bool same_bytes(const void * a, const void * b, std::size_t n) { return std::memcmp(a, b, n) == 0; } template void expect_reconstruct_unsigned(T secret) { auto [share0, share1] = dpf::additively_share(secret); EXPECT_EQ(dpf::reconstruct(share0, share1), secret); } template void expect_reconstruct_signed(T secret) { auto [share0, share1] = dpf::additively_share(secret); using U = std::make_unsigned_t; EXPECT_EQ(static_cast(dpf::reconstruct(share0, share1)), static_cast(secret)); } } // namespace class RandomTest : public ::testing::Test { protected: void TearDown() override { reset_hook(); } }; TEST_F(RandomTest, UniformFillReturnsTheSameObject) { std::uint32_t value = 0; EXPECT_EQ(&dpf::uniform_fill(value), &value); } TEST_F(RandomTest, HookOverwritesEveryByte) { dpf::detail::uniform_bytes_hook = test_hook; g_hook.byte = 0xA5; struct Padded { std::uint8_t a; std::uint32_t b; }; Padded padded = dpf::uniform_sample(); EXPECT_EQ(g_hook.calls, 1); EXPECT_EQ(g_hook.last_n, sizeof(Padded)); auto expected = load_bytes(0xA5); EXPECT_TRUE(same_bytes(&padded, &expected, sizeof(Padded))); } TEST_F(RandomTest, HookSeesTheObjectSize) { dpf::detail::uniform_bytes_hook = test_hook; g_hook.byte = 0x3C; auto byte = dpf::uniform_sample(); auto block = dpf::uniform_sample(); std::array wide{}; dpf::uniform_fill(wide); EXPECT_EQ(byte, static_cast(0x3C)); EXPECT_EQ(g_hook.sizes, (std::vector{ 1u, sizeof(simde__m128i), 32u})); auto expected_block = load_bytes(0x3C); EXPECT_TRUE(same_bytes(&block, &expected_block, sizeof(block))); for (unsigned char b : wide) EXPECT_EQ(b, static_cast(0x3C)); } TEST_F(RandomTest, ClearingTheHookReadsTheDeviceAgain) { dpf::detail::uniform_bytes_hook = [](void * dst, std::size_t n) { fill_byte(dst, n, 0x11); }; EXPECT_EQ(dpf::uniform_sample(), 0x11111111u); dpf::detail::uniform_bytes_hook = nullptr; std::set> draws; for (int i = 0; i < 8; ++i) { std::array sample{}; dpf::uniform_fill(sample); draws.insert(sample); bool nonzero = false; for (unsigned char b : sample) nonzero = nonzero || b != 0; EXPECT_TRUE(nonzero); } EXPECT_EQ(draws.size(), 8u); } TEST_F(RandomTest, HookIsThreadLocal) { dpf::detail::uniform_bytes_hook = [](void * dst, std::size_t n) { fill_byte(dst, n, 0x11); }; std::uint32_t other = 0; std::thread worker([&]() { dpf::detail::uniform_bytes_hook = [](void * dst, std::size_t n) { fill_byte(dst, n, 0x22); }; other = dpf::uniform_sample(); dpf::detail::uniform_bytes_hook = nullptr; }); worker.join(); EXPECT_EQ(dpf::uniform_sample(), 0x11111111u); EXPECT_EQ(other, 0x22222222u); } TEST_F(RandomTest, BooleanSamplesUseOnlyTheLowBit) { const unsigned char raw[] = {0x00, 0x02, 0xFF}; g_hook.script = raw; dpf::detail::uniform_bytes_hook = test_hook; EXPECT_FALSE(dpf::uniform_sample()); EXPECT_FALSE(dpf::uniform_sample()); EXPECT_TRUE(dpf::uniform_sample()); g_hook.script_i = 0; EXPECT_EQ(dpf::uniform_sample(), dpf::bit::zero); EXPECT_EQ(dpf::uniform_sample(), dpf::bit::zero); EXPECT_EQ(dpf::uniform_sample(), dpf::bit::one); EXPECT_EQ(g_hook.calls, 6); } TEST_F(RandomTest, DeviceBooleanSamplesAreBothValues) { bool saw_false = false; bool saw_true = false; bool saw_zero = false; bool saw_one = false; for (int i = 0; i < 64; ++i) { bool bit = dpf::uniform_sample(); saw_false = saw_false || !bit; saw_true = saw_true || bit; dpf::bit as_bit = dpf::uniform_sample(); EXPECT_TRUE(as_bit == dpf::bit::zero || as_bit == dpf::bit::one); saw_zero = saw_zero || as_bit == dpf::bit::zero; saw_one = saw_one || as_bit == dpf::bit::one; } EXPECT_TRUE(saw_false); EXPECT_TRUE(saw_true); EXPECT_TRUE(saw_zero); EXPECT_TRUE(saw_one); } TEST_F(RandomTest, AdditiveSharesReconstructUnsigned) { expect_reconstruct_unsigned(0); expect_reconstruct_unsigned(1); expect_reconstruct_unsigned(255); expect_reconstruct_unsigned(0); expect_reconstruct_unsigned(1); expect_reconstruct_unsigned(0xFFFFFFFFu); expect_reconstruct_unsigned(0); expect_reconstruct_unsigned( std::numeric_limits::max()); dpf::detail::uniform_bytes_hook = test_hook; g_hook.byte = 0xFF; auto [share0, share1] = dpf::additively_share(0); EXPECT_EQ(g_hook.calls, 1); EXPECT_EQ(share0.raw(), 0xFFFFFFFFu); EXPECT_EQ(share1.raw(), 1u); EXPECT_EQ(dpf::reconstruct(share0, share1), 0u); } TEST_F(RandomTest, AdditiveSharesReconstructSignedExtremes) { dpf::detail::uniform_bytes_hook = test_hook; g_hook.use_typed = true; g_hook.typed = 1; constexpr auto secret = std::numeric_limits::min(); auto [share0, share1] = dpf::additively_share(secret); EXPECT_EQ(share0.raw(), 1); using U = std::uint32_t; EXPECT_EQ(static_cast(dpf::reconstruct(share0, share1)), static_cast(secret)); dpf::detail::uniform_bytes_hook = nullptr; expect_reconstruct_signed(std::numeric_limits::min()); expect_reconstruct_signed(std::numeric_limits::max()); expect_reconstruct_signed(-1); expect_reconstruct_signed(secret); expect_reconstruct_signed(std::numeric_limits::max()); expect_reconstruct_signed(-1); expect_reconstruct_signed(0); expect_reconstruct_signed(std::numeric_limits::min()); expect_reconstruct_signed(std::numeric_limits::max()); } TEST_F(RandomTest, AdditiveSharesReconstructBitAndXorGroup) { for (dpf::bit secret : {dpf::bit::zero, dpf::bit::one}) { auto [share0, share1] = dpf::additively_share(secret); EXPECT_TRUE(share0.raw() == dpf::bit::zero || share0.raw() == dpf::bit::one); EXPECT_TRUE(share1.raw() == dpf::bit::zero || share1.raw() == dpf::bit::one); EXPECT_EQ(dpf::reconstruct(share0, share1), secret); } dpf::xor_wrapper secret{0xA5A5A5A5u}; auto [share0, share1] = dpf::additively_share(secret); EXPECT_EQ(dpf::reconstruct(share0, share1), secret); } TEST_F(RandomTest, AdditiveShareDrawsOnce) { dpf::detail::uniform_bytes_hook = test_hook; g_hook.byte = 0x01; auto shares = dpf::additively_share(0x10u); EXPECT_EQ(g_hook.calls, 1); EXPECT_EQ(shares.first.raw(), 0x01010101u); EXPECT_EQ(dpf::reconstruct(shares.first, shares.second), 0x10u); } TEST_F(RandomTest, ForkedProcessesDoNotRepeatEntropy) { // A buffered stdio read of the device copies the unread buffer into the // child. Prime the generator, then compare the next draw on each side. dpf::uniform_sample(); int fds[2]; ASSERT_EQ(::pipe(fds), 0); pid_t pid = ::fork(); ASSERT_NE(pid, -1); if (pid == 0) { ::close(fds[0]); auto sample = dpf::uniform_sample>(); ssize_t wrote = ::write(fds[1], sample.data(), sample.size()); ::_exit(wrote == static_cast(sample.size()) ? 0 : 1); } ::close(fds[1]); auto parent = dpf::uniform_sample>(); std::array child{}; std::size_t got = 0; while (got < child.size()) { ssize_t n = ::read(fds[0], child.data() + got, child.size() - got); if (n <= 0) break; got += static_cast(n); } ::close(fds[0]); int status = 0; ASSERT_EQ(::waitpid(pid, &status, 0), pid); ASSERT_TRUE(WIFEXITED(status)); ASSERT_EQ(WEXITSTATUS(status), 0); ASSERT_EQ(got, child.size()); EXPECT_FALSE(same_bytes(parent.data(), child.data(), parent.size())); } TEST_F(RandomTest, ConcurrentReadsStayWellFormed) { constexpr int threads = 4; constexpr int draws = 128; std::vector> results(threads); std::vector workers; workers.reserve(threads); for (int t = 0; t < threads; ++t) { workers.emplace_back([&, t]() { results[t].reserve(draws); for (int i = 0; i < draws; ++i) results[t].push_back(dpf::uniform_sample()); }); } for (auto & worker : workers) worker.join(); std::set unique; for (const auto & row : results) { EXPECT_EQ(row.size(), static_cast(draws)); unique.insert(row.begin(), row.end()); } EXPECT_EQ(unique.size(), static_cast(threads * draws)); } TEST_F(RandomTest, OtherTranslationUnitCanSample) { auto here = dpf::uniform_sample(); auto there = dpf_rng_test::sample_from_other_tu(); EXPECT_NE(here, there); } TEST_F(RandomTest, DeviceLowBitTakesBothValues) { // Doerner–Shelat pad bits are `uniform_sample() & 1`. bool saw0 = false; bool saw1 = false; for (int i = 0; i < 64; ++i) { auto bit = static_cast(dpf::uniform_sample() & 1u); saw0 = saw0 || bit == 0u; saw1 = saw1 || bit == 1u; } EXPECT_TRUE(saw0); EXPECT_TRUE(saw1); }