diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /mozglue/tests | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mozglue/tests')
28 files changed, 8139 insertions, 0 deletions
diff --git a/mozglue/tests/ShowSSEConfig.cpp b/mozglue/tests/ShowSSEConfig.cpp new file mode 100644 index 0000000000..a19b30198c --- /dev/null +++ b/mozglue/tests/ShowSSEConfig.cpp @@ -0,0 +1,125 @@ +/* vim: set shiftwidth=2 tabstop=8 autoindent cindent expandtab: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/SSE.h" +#include <stdio.h> + +#if defined(XP_WIN) +int wmain() +#else +int main() +#endif // defined(XP_WIN) +{ + printf("CPUID detection present: %s\n", +#ifdef MOZILLA_SSE_HAVE_CPUID_DETECTION + "yes" +#else + "no" +#endif + ); + +#ifdef MOZILLA_COMPILE_WITH_MMX +# define COMPILE_MMX_STRING "Y" +#else +# define COMPILE_MMX_STRING "-" +#endif +#ifdef MOZILLA_PRESUME_MMX +# define PRESUME_MMX_STRING "Y" +#else +# define PRESUME_MMX_STRING "-" +#endif + +#ifdef MOZILLA_COMPILE_WITH_SSE +# define COMPILE_SSE_STRING "Y" +#else +# define COMPILE_SSE_STRING "-" +#endif +#ifdef MOZILLA_PRESUME_SSE +# define PRESUME_SSE_STRING "Y" +#else +# define PRESUME_SSE_STRING "-" +#endif + +#ifdef MOZILLA_COMPILE_WITH_SSE2 +# define COMPILE_SSE2_STRING "Y" +#else +# define COMPILE_SSE2_STRING "-" +#endif +#ifdef MOZILLA_PRESUME_SSE2 +# define PRESUME_SSE2_STRING "Y" +#else +# define PRESUME_SSE2_STRING "-" +#endif + +#ifdef MOZILLA_COMPILE_WITH_SSE3 +# define COMPILE_SSE3_STRING "Y" +#else +# define COMPILE_SSE3_STRING "-" +#endif +#ifdef MOZILLA_PRESUME_SSE3 +# define PRESUME_SSE3_STRING "Y" +#else +# define PRESUME_SSE3_STRING "-" +#endif + +#ifdef MOZILLA_COMPILE_WITH_SSSE3 +# define COMPILE_SSSE3_STRING "Y" +#else +# define COMPILE_SSSE3_STRING "-" +#endif +#ifdef MOZILLA_PRESUME_SSSE3 +# define PRESUME_SSSE3_STRING "Y" +#else +# define PRESUME_SSSE3_STRING "-" +#endif + +#ifdef MOZILLA_COMPILE_WITH_SSE4A +# define COMPILE_SSE4A_STRING "Y" +#else +# define COMPILE_SSE4A_STRING "-" +#endif +#ifdef MOZILLA_PRESUME_SSE4A +# define PRESUME_SSE4A_STRING "Y" +#else +# define PRESUME_SSE4A_STRING "-" +#endif + +#ifdef MOZILLA_COMPILE_WITH_SSE4_1 +# define COMPILE_SSE4_1_STRING "Y" +#else +# define COMPILE_SSE4_1_STRING "-" +#endif +#ifdef MOZILLA_PRESUME_SSE4_1 +# define PRESUME_SSE4_1_STRING "Y" +#else +# define PRESUME_SSE4_1_STRING "-" +#endif + +#ifdef MOZILLA_COMPILE_WITH_SSE4_2 +# define COMPILE_SSE4_2_STRING "Y" +#else +# define COMPILE_SSE4_2_STRING "-" +#endif +#ifdef MOZILLA_PRESUME_SSE4_2 +# define PRESUME_SSE4_2_STRING "Y" +#else +# define PRESUME_SSE4_2_STRING "-" +#endif + + printf("Feature Presume Compile Support Use\n"); +#define SHOW_INFO(featurelc_, featureuc_) \ + printf("%7s %1s %1s %1s\n", #featurelc_, \ + PRESUME_##featureuc_##_STRING, COMPILE_##featureuc_##_STRING, \ + (mozilla::supports_##featurelc_() ? "Y" : "-")); + SHOW_INFO(mmx, MMX) + SHOW_INFO(sse, SSE) + SHOW_INFO(sse2, SSE2) + SHOW_INFO(sse3, SSE3) + SHOW_INFO(ssse3, SSSE3) + SHOW_INFO(sse4a, SSE4A) + SHOW_INFO(sse4_1, SSE4_1) + SHOW_INFO(sse4_2, SSE4_2) + return 0; +} diff --git a/mozglue/tests/TestBaseProfiler.cpp b/mozglue/tests/TestBaseProfiler.cpp new file mode 100644 index 0000000000..a3b0cde22e --- /dev/null +++ b/mozglue/tests/TestBaseProfiler.cpp @@ -0,0 +1,4452 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "BaseProfiler.h" + +#include "mozilla/Attributes.h" +#include "mozilla/BaseProfileJSONWriter.h" + +#ifdef MOZ_GECKO_PROFILER +# include "mozilla/BaseProfilerMarkerTypes.h" +# include "mozilla/BlocksRingBuffer.h" +# include "mozilla/leb128iterator.h" +# include "mozilla/ModuloBuffer.h" +# include "mozilla/PowerOfTwo.h" +# include "mozilla/ProfileBufferChunk.h" +# include "mozilla/ProfileBufferChunkManagerSingle.h" +# include "mozilla/ProfileBufferChunkManagerWithLocalLimit.h" +# include "mozilla/ProfileBufferControlledChunkManager.h" +# include "mozilla/ProfileChunkedBuffer.h" +# include "mozilla/Vector.h" +#endif // MOZ_GECKO_PROFILER + +#if defined(_MSC_VER) || defined(__MINGW32__) +# include <windows.h> +# include <mmsystem.h> +# include <process.h> +#else +# include <errno.h> +# include <string.h> +# include <time.h> +# include <unistd.h> +#endif + +#include <algorithm> +#include <atomic> +#include <iostream> +#include <random> +#include <thread> +#include <type_traits> +#include <utility> + +#ifdef MOZ_GECKO_PROFILER + +MOZ_MAYBE_UNUSED static void SleepMilli(unsigned aMilliseconds) { +# if defined(_MSC_VER) || defined(__MINGW32__) + Sleep(aMilliseconds); +# else + struct timespec ts = {/* .tv_sec */ static_cast<time_t>(aMilliseconds / 1000), + /* ts.tv_nsec */ long(aMilliseconds % 1000) * 1000000}; + struct timespec tr = {0, 0}; + while (nanosleep(&ts, &tr)) { + if (errno == EINTR) { + ts = tr; + } else { + printf("nanosleep() -> %s\n", strerror(errno)); + exit(1); + } + } +# endif +} + +MOZ_MAYBE_UNUSED static void WaitUntilTimeStampChanges( + const mozilla::TimeStamp& aTimeStampToCompare = + mozilla::TimeStamp::NowUnfuzzed()) { + while (aTimeStampToCompare == mozilla::TimeStamp::NowUnfuzzed()) { + SleepMilli(1); + } +} + +using namespace mozilla; + +void TestPowerOfTwoMask() { + printf("TestPowerOfTwoMask...\n"); + + static_assert(MakePowerOfTwoMask<uint32_t, 0>().MaskValue() == 0); + constexpr PowerOfTwoMask<uint32_t> c0 = MakePowerOfTwoMask<uint32_t, 0>(); + MOZ_RELEASE_ASSERT(c0.MaskValue() == 0); + + static_assert(MakePowerOfTwoMask<uint32_t, 0xFFu>().MaskValue() == 0xFFu); + constexpr PowerOfTwoMask<uint32_t> cFF = + MakePowerOfTwoMask<uint32_t, 0xFFu>(); + MOZ_RELEASE_ASSERT(cFF.MaskValue() == 0xFFu); + + static_assert(MakePowerOfTwoMask<uint32_t, 0xFFFFFFFFu>().MaskValue() == + 0xFFFFFFFFu); + constexpr PowerOfTwoMask<uint32_t> cFFFFFFFF = + MakePowerOfTwoMask<uint32_t, 0xFFFFFFFFu>(); + MOZ_RELEASE_ASSERT(cFFFFFFFF.MaskValue() == 0xFFFFFFFFu); + + struct TestDataU32 { + uint32_t mInput; + uint32_t mMask; + }; + // clang-format off + TestDataU32 tests[] = { + { 0, 0 }, + { 1, 1 }, + { 2, 3 }, + { 3, 3 }, + { 4, 7 }, + { 5, 7 }, + { (1u << 31) - 1, (1u << 31) - 1 }, + { (1u << 31), uint32_t(-1) }, + { (1u << 31) + 1, uint32_t(-1) }, + { uint32_t(-1), uint32_t(-1) } + }; + // clang-format on + for (const TestDataU32& test : tests) { + PowerOfTwoMask<uint32_t> p2m(test.mInput); + MOZ_RELEASE_ASSERT(p2m.MaskValue() == test.mMask); + for (const TestDataU32& inner : tests) { + if (p2m.MaskValue() != uint32_t(-1)) { + MOZ_RELEASE_ASSERT((inner.mInput % p2m) == + (inner.mInput % (p2m.MaskValue() + 1))); + } + MOZ_RELEASE_ASSERT((inner.mInput & p2m) == (inner.mInput % p2m)); + MOZ_RELEASE_ASSERT((p2m & inner.mInput) == (inner.mInput & p2m)); + } + } + + printf("TestPowerOfTwoMask done\n"); +} + +void TestPowerOfTwo() { + printf("TestPowerOfTwo...\n"); + + static_assert(MakePowerOfTwo<uint32_t, 1>().Value() == 1); + constexpr PowerOfTwo<uint32_t> c1 = MakePowerOfTwo<uint32_t, 1>(); + MOZ_RELEASE_ASSERT(c1.Value() == 1); + static_assert(MakePowerOfTwo<uint32_t, 1>().Mask().MaskValue() == 0); + + static_assert(MakePowerOfTwo<uint32_t, 128>().Value() == 128); + constexpr PowerOfTwo<uint32_t> c128 = MakePowerOfTwo<uint32_t, 128>(); + MOZ_RELEASE_ASSERT(c128.Value() == 128); + static_assert(MakePowerOfTwo<uint32_t, 128>().Mask().MaskValue() == 127); + + static_assert(MakePowerOfTwo<uint32_t, 0x80000000u>().Value() == 0x80000000u); + constexpr PowerOfTwo<uint32_t> cMax = MakePowerOfTwo<uint32_t, 0x80000000u>(); + MOZ_RELEASE_ASSERT(cMax.Value() == 0x80000000u); + static_assert(MakePowerOfTwo<uint32_t, 0x80000000u>().Mask().MaskValue() == + 0x7FFFFFFFu); + + struct TestDataU32 { + uint32_t mInput; + uint32_t mValue; + uint32_t mMask; + }; + // clang-format off + TestDataU32 tests[] = { + { 0, 1, 0 }, + { 1, 1, 0 }, + { 2, 2, 1 }, + { 3, 4, 3 }, + { 4, 4, 3 }, + { 5, 8, 7 }, + { (1u << 31) - 1, (1u << 31), (1u << 31) - 1 }, + { (1u << 31), (1u << 31), (1u << 31) - 1 }, + { (1u << 31) + 1, (1u << 31), (1u << 31) - 1 }, + { uint32_t(-1), (1u << 31), (1u << 31) - 1 } + }; + // clang-format on + for (const TestDataU32& test : tests) { + PowerOfTwo<uint32_t> p2(test.mInput); + MOZ_RELEASE_ASSERT(p2.Value() == test.mValue); + MOZ_RELEASE_ASSERT(p2.MaskValue() == test.mMask); + PowerOfTwoMask<uint32_t> p2m = p2.Mask(); + MOZ_RELEASE_ASSERT(p2m.MaskValue() == test.mMask); + for (const TestDataU32& inner : tests) { + MOZ_RELEASE_ASSERT((inner.mInput % p2) == (inner.mInput % p2.Value())); + } + } + + printf("TestPowerOfTwo done\n"); +} + +void TestLEB128() { + printf("TestLEB128...\n"); + + MOZ_RELEASE_ASSERT(ULEB128MaxSize<uint8_t>() == 2); + MOZ_RELEASE_ASSERT(ULEB128MaxSize<uint16_t>() == 3); + MOZ_RELEASE_ASSERT(ULEB128MaxSize<uint32_t>() == 5); + MOZ_RELEASE_ASSERT(ULEB128MaxSize<uint64_t>() == 10); + + struct TestDataU64 { + uint64_t mValue; + unsigned mSize; + const char* mBytes; + }; + // clang-format off + TestDataU64 tests[] = { + // Small numbers should keep their normal byte representation. + { 0u, 1, "\0" }, + { 1u, 1, "\x01" }, + + // 0111 1111 (127, or 0x7F) is the highest number that fits into a single + // LEB128 byte. It gets encoded as 0111 1111, note the most significant bit + // is off. + { 0x7Fu, 1, "\x7F" }, + + // Next number: 128, or 0x80. + // Original data representation: 1000 0000 + // Broken up into groups of 7: 1 0000000 + // Padded with 0 (msB) or 1 (lsB): 00000001 10000000 + // Byte representation: 0x01 0x80 + // Little endian order: -> 0x80 0x01 + { 0x80u, 2, "\x80\x01" }, + + // Next: 129, or 0x81 (showing that we don't lose low bits.) + // Original data representation: 1000 0001 + // Broken up into groups of 7: 1 0000001 + // Padded with 0 (msB) or 1 (lsB): 00000001 10000001 + // Byte representation: 0x01 0x81 + // Little endian order: -> 0x81 0x01 + { 0x81u, 2, "\x81\x01" }, + + // Highest 8-bit number: 255, or 0xFF. + // Original data representation: 1111 1111 + // Broken up into groups of 7: 1 1111111 + // Padded with 0 (msB) or 1 (lsB): 00000001 11111111 + // Byte representation: 0x01 0xFF + // Little endian order: -> 0xFF 0x01 + { 0xFFu, 2, "\xFF\x01" }, + + // Next: 256, or 0x100. + // Original data representation: 1 0000 0000 + // Broken up into groups of 7: 10 0000000 + // Padded with 0 (msB) or 1 (lsB): 00000010 10000000 + // Byte representation: 0x10 0x80 + // Little endian order: -> 0x80 0x02 + { 0x100u, 2, "\x80\x02" }, + + // Highest 32-bit number: 0xFFFFFFFF (8 bytes, all bits set). + // Original: 1111 1111 1111 1111 1111 1111 1111 1111 + // Groups: 1111 1111111 1111111 1111111 1111111 + // Padded: 00001111 11111111 11111111 11111111 11111111 + // Bytes: 0x0F 0xFF 0xFF 0xFF 0xFF + // Little Endian: -> 0xFF 0xFF 0xFF 0xFF 0x0F + { 0xFFFFFFFFu, 5, "\xFF\xFF\xFF\xFF\x0F" }, + + // Highest 64-bit number: 0xFFFFFFFFFFFFFFFF (16 bytes, all bits set). + // 64 bits, that's 9 groups of 7 bits, plus 1 (most significant) bit. + { 0xFFFFFFFFFFFFFFFFu, 10, "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x01" } + }; + // clang-format on + + for (const TestDataU64& test : tests) { + MOZ_RELEASE_ASSERT(ULEB128Size(test.mValue) == test.mSize); + // Prepare a buffer that can accomodate the largest-possible LEB128. + uint8_t buffer[ULEB128MaxSize<uint64_t>()]; + // Use a pointer into the buffer as iterator. + uint8_t* p = buffer; + // And write the LEB128. + WriteULEB128(test.mValue, p); + // Pointer (iterator) should have advanced just past the expected LEB128 + // size. + MOZ_RELEASE_ASSERT(p == buffer + test.mSize); + // Check expected bytes. + for (unsigned i = 0; i < test.mSize; ++i) { + MOZ_RELEASE_ASSERT(buffer[i] == uint8_t(test.mBytes[i])); + } + + // Move pointer (iterator) back to start of buffer. + p = buffer; + // And read the LEB128 we wrote above. + uint64_t read = ReadULEB128<uint64_t>(p); + // Pointer (iterator) should have also advanced just past the expected + // LEB128 size. + MOZ_RELEASE_ASSERT(p == buffer + test.mSize); + // And check the read value. + MOZ_RELEASE_ASSERT(read == test.mValue); + + // Testing ULEB128 reader. + ULEB128Reader<uint64_t> reader; + MOZ_RELEASE_ASSERT(!reader.IsComplete()); + // Move pointer back to start of buffer. + p = buffer; + for (;;) { + // Read a byte and feed it to the reader. + if (reader.FeedByteIsComplete(*p++)) { + break; + } + // Not complete yet, we shouldn't have reached the end pointer. + MOZ_RELEASE_ASSERT(!reader.IsComplete()); + MOZ_RELEASE_ASSERT(p < buffer + test.mSize); + } + MOZ_RELEASE_ASSERT(reader.IsComplete()); + // Pointer should have advanced just past the expected LEB128 size. + MOZ_RELEASE_ASSERT(p == buffer + test.mSize); + // And check the read value. + MOZ_RELEASE_ASSERT(reader.Value() == test.mValue); + + // And again after a Reset. + reader.Reset(); + MOZ_RELEASE_ASSERT(!reader.IsComplete()); + p = buffer; + for (;;) { + if (reader.FeedByteIsComplete(*p++)) { + break; + } + MOZ_RELEASE_ASSERT(!reader.IsComplete()); + MOZ_RELEASE_ASSERT(p < buffer + test.mSize); + } + MOZ_RELEASE_ASSERT(reader.IsComplete()); + MOZ_RELEASE_ASSERT(p == buffer + test.mSize); + MOZ_RELEASE_ASSERT(reader.Value() == test.mValue); + } + + printf("TestLEB128 done\n"); +} + +template <uint8_t byte, uint8_t... tail> +constexpr bool TestConstexprULEB128Reader(ULEB128Reader<uint64_t>& aReader) { + if (aReader.IsComplete()) { + return false; + } + const bool isComplete = aReader.FeedByteIsComplete(byte); + if (aReader.IsComplete() != isComplete) { + return false; + } + if constexpr (sizeof...(tail) == 0) { + return isComplete; + } else { + if (isComplete) { + return false; + } + return TestConstexprULEB128Reader<tail...>(aReader); + } +} + +template <uint64_t expected, uint8_t... bytes> +constexpr bool TestConstexprULEB128Reader() { + ULEB128Reader<uint64_t> reader; + if (!TestConstexprULEB128Reader<bytes...>(reader)) { + return false; + } + if (!reader.IsComplete()) { + return false; + } + if (reader.Value() != expected) { + return false; + } + + reader.Reset(); + if (!TestConstexprULEB128Reader<bytes...>(reader)) { + return false; + } + if (!reader.IsComplete()) { + return false; + } + if (reader.Value() != expected) { + return false; + } + + return true; +} + +static_assert(TestConstexprULEB128Reader<0x0u, 0x0u>()); +static_assert(!TestConstexprULEB128Reader<0x0u, 0x0u, 0x0u>()); +static_assert(TestConstexprULEB128Reader<0x1u, 0x1u>()); +static_assert(TestConstexprULEB128Reader<0x7Fu, 0x7Fu>()); +static_assert(TestConstexprULEB128Reader<0x80u, 0x80u, 0x01u>()); +static_assert(!TestConstexprULEB128Reader<0x80u, 0x80u>()); +static_assert(!TestConstexprULEB128Reader<0x80u, 0x01u>()); +static_assert(TestConstexprULEB128Reader<0x81u, 0x81u, 0x01u>()); +static_assert(TestConstexprULEB128Reader<0xFFu, 0xFFu, 0x01u>()); +static_assert(TestConstexprULEB128Reader<0x100u, 0x80u, 0x02u>()); +static_assert(TestConstexprULEB128Reader<0xFFFFFFFFu, 0xFFu, 0xFFu, 0xFFu, + 0xFFu, 0x0Fu>()); +static_assert( + !TestConstexprULEB128Reader<0xFFFFFFFFu, 0xFFu, 0xFFu, 0xFFu, 0xFFu>()); +static_assert(!TestConstexprULEB128Reader<0xFFFFFFFFu, 0xFFu, 0xFFu, 0xFFu, + 0xFFu, 0xFFu, 0x0Fu>()); +static_assert( + TestConstexprULEB128Reader<0xFFFFFFFFFFFFFFFFu, 0xFFu, 0xFFu, 0xFFu, 0xFFu, + 0xFFu, 0xFFu, 0xFFu, 0xFFu, 0xFFu, 0x01u>()); +static_assert( + !TestConstexprULEB128Reader<0xFFFFFFFFFFFFFFFFu, 0xFFu, 0xFFu, 0xFFu, 0xFFu, + 0xFFu, 0xFFu, 0xFFu, 0xFFu, 0xFFu>()); + +static void TestChunk() { + printf("TestChunk...\n"); + + static_assert(!std::is_default_constructible_v<ProfileBufferChunk>, + "ProfileBufferChunk should not be default-constructible"); + static_assert( + !std::is_constructible_v<ProfileBufferChunk, ProfileBufferChunk::Length>, + "ProfileBufferChunk should not be constructible from Length"); + + static_assert( + sizeof(ProfileBufferChunk::Header) == + sizeof(ProfileBufferChunk::Header::mOffsetFirstBlock) + + sizeof(ProfileBufferChunk::Header::mOffsetPastLastBlock) + + sizeof(ProfileBufferChunk::Header::mDoneTimeStamp) + + sizeof(ProfileBufferChunk::Header::mBufferBytes) + + sizeof(ProfileBufferChunk::Header::mBlockCount) + + sizeof(ProfileBufferChunk::Header::mRangeStart) + + sizeof(ProfileBufferChunk::Header::mProcessId) + + sizeof(ProfileBufferChunk::Header::mPADDING), + "ProfileBufferChunk::Header may have unwanted padding, please review"); + // Note: The above static_assert is an attempt at keeping + // ProfileBufferChunk::Header tightly packed, but some changes could make this + // impossible to achieve (most probably due to alignment) -- Just do your + // best! + + constexpr ProfileBufferChunk::Length TestLen = 1000; + + // Basic allocations of different sizes. + for (ProfileBufferChunk::Length len = 0; len <= TestLen; ++len) { + auto chunk = ProfileBufferChunk::Create(len); + static_assert( + std::is_same_v<decltype(chunk), UniquePtr<ProfileBufferChunk>>, + "ProfileBufferChunk::Create() should return a " + "UniquePtr<ProfileBufferChunk>"); + MOZ_RELEASE_ASSERT(!!chunk, "OOM!?"); + MOZ_RELEASE_ASSERT(chunk->BufferBytes() >= len); + MOZ_RELEASE_ASSERT(chunk->ChunkBytes() >= + len + ProfileBufferChunk::SizeofChunkMetadata()); + MOZ_RELEASE_ASSERT(chunk->RemainingBytes() == chunk->BufferBytes()); + MOZ_RELEASE_ASSERT(chunk->OffsetFirstBlock() == 0); + MOZ_RELEASE_ASSERT(chunk->OffsetPastLastBlock() == 0); + MOZ_RELEASE_ASSERT(chunk->BlockCount() == 0); + MOZ_RELEASE_ASSERT(chunk->ProcessId() == 0); + MOZ_RELEASE_ASSERT(chunk->RangeStart() == 0); + MOZ_RELEASE_ASSERT(chunk->BufferSpan().LengthBytes() == + chunk->BufferBytes()); + MOZ_RELEASE_ASSERT(!chunk->GetNext()); + MOZ_RELEASE_ASSERT(!chunk->ReleaseNext()); + MOZ_RELEASE_ASSERT(chunk->Last() == chunk.get()); + } + + // Allocate the main test Chunk. + auto chunkA = ProfileBufferChunk::Create(TestLen); + MOZ_RELEASE_ASSERT(!!chunkA, "OOM!?"); + MOZ_RELEASE_ASSERT(chunkA->BufferBytes() >= TestLen); + MOZ_RELEASE_ASSERT(chunkA->ChunkBytes() >= + TestLen + ProfileBufferChunk::SizeofChunkMetadata()); + MOZ_RELEASE_ASSERT(!chunkA->GetNext()); + MOZ_RELEASE_ASSERT(!chunkA->ReleaseNext()); + + constexpr ProfileBufferIndex chunkARangeStart = 12345; + chunkA->SetRangeStart(chunkARangeStart); + MOZ_RELEASE_ASSERT(chunkA->RangeStart() == chunkARangeStart); + + // Get a read-only span over its buffer. + auto bufferA = chunkA->BufferSpan(); + static_assert( + std::is_same_v<decltype(bufferA), Span<const ProfileBufferChunk::Byte>>, + "BufferSpan() should return a Span<const Byte>"); + MOZ_RELEASE_ASSERT(bufferA.LengthBytes() == chunkA->BufferBytes()); + + // Add the initial tail block. + constexpr ProfileBufferChunk::Length initTailLen = 10; + auto initTail = chunkA->ReserveInitialBlockAsTail(initTailLen); + static_assert( + std::is_same_v<decltype(initTail), Span<ProfileBufferChunk::Byte>>, + "ReserveInitialBlockAsTail() should return a Span<Byte>"); + MOZ_RELEASE_ASSERT(initTail.LengthBytes() == initTailLen); + MOZ_RELEASE_ASSERT(initTail.Elements() == bufferA.Elements()); + MOZ_RELEASE_ASSERT(chunkA->OffsetFirstBlock() == initTailLen); + MOZ_RELEASE_ASSERT(chunkA->OffsetPastLastBlock() == initTailLen); + + // Add the first complete block. + constexpr ProfileBufferChunk::Length block1Len = 20; + auto block1 = chunkA->ReserveBlock(block1Len); + static_assert( + std::is_same_v<decltype(block1), ProfileBufferChunk::ReserveReturn>, + "ReserveBlock() should return a ReserveReturn"); + MOZ_RELEASE_ASSERT(block1.mBlockRangeIndex.ConvertToProfileBufferIndex() == + chunkARangeStart + initTailLen); + MOZ_RELEASE_ASSERT(block1.mSpan.LengthBytes() == block1Len); + MOZ_RELEASE_ASSERT(block1.mSpan.Elements() == + bufferA.Elements() + initTailLen); + MOZ_RELEASE_ASSERT(chunkA->OffsetFirstBlock() == initTailLen); + MOZ_RELEASE_ASSERT(chunkA->OffsetPastLastBlock() == initTailLen + block1Len); + MOZ_RELEASE_ASSERT(chunkA->RemainingBytes() != 0); + + // Add another block to over-fill the ProfileBufferChunk. + const ProfileBufferChunk::Length remaining = + chunkA->BufferBytes() - (initTailLen + block1Len); + constexpr ProfileBufferChunk::Length overfill = 30; + const ProfileBufferChunk::Length block2Len = remaining + overfill; + ProfileBufferChunk::ReserveReturn block2 = chunkA->ReserveBlock(block2Len); + MOZ_RELEASE_ASSERT(block2.mBlockRangeIndex.ConvertToProfileBufferIndex() == + chunkARangeStart + initTailLen + block1Len); + MOZ_RELEASE_ASSERT(block2.mSpan.LengthBytes() == remaining); + MOZ_RELEASE_ASSERT(block2.mSpan.Elements() == + bufferA.Elements() + initTailLen + block1Len); + MOZ_RELEASE_ASSERT(chunkA->OffsetFirstBlock() == initTailLen); + MOZ_RELEASE_ASSERT(chunkA->OffsetPastLastBlock() == chunkA->BufferBytes()); + MOZ_RELEASE_ASSERT(chunkA->RemainingBytes() == 0); + + // Block must be marked "done" before it can be recycled. + chunkA->MarkDone(); + + // It must be marked "recycled" before data can be added to it again. + chunkA->MarkRecycled(); + + // Add an empty initial tail block. + Span<ProfileBufferChunk::Byte> initTail2 = + chunkA->ReserveInitialBlockAsTail(0); + MOZ_RELEASE_ASSERT(initTail2.LengthBytes() == 0); + MOZ_RELEASE_ASSERT(initTail2.Elements() == bufferA.Elements()); + MOZ_RELEASE_ASSERT(chunkA->OffsetFirstBlock() == 0); + MOZ_RELEASE_ASSERT(chunkA->OffsetPastLastBlock() == 0); + + // Block must be marked "done" before it can be destroyed. + chunkA->MarkDone(); + + chunkA->SetProcessId(123); + MOZ_RELEASE_ASSERT(chunkA->ProcessId() == 123); + + printf("TestChunk done\n"); +} + +static void TestChunkManagerSingle() { + printf("TestChunkManagerSingle...\n"); + + // Construct a ProfileBufferChunkManagerSingle for one chunk of size >=1000. + constexpr ProfileBufferChunk::Length ChunkMinBufferBytes = 1000; + ProfileBufferChunkManagerSingle cms{ChunkMinBufferBytes}; + + // Reference to base class, to exercize virtual methods. + ProfileBufferChunkManager& cm = cms; + +# ifdef DEBUG + const char* chunkManagerRegisterer = "TestChunkManagerSingle"; + cm.RegisteredWith(chunkManagerRegisterer); +# endif // DEBUG + + const auto maxTotalSize = cm.MaxTotalSize(); + MOZ_RELEASE_ASSERT(maxTotalSize >= ChunkMinBufferBytes); + + cm.SetChunkDestroyedCallback([](const ProfileBufferChunk&) { + MOZ_RELEASE_ASSERT( + false, + "ProfileBufferChunkManagerSingle should never destroy its one chunk"); + }); + + UniquePtr<ProfileBufferChunk> extantReleasedChunks = + cm.GetExtantReleasedChunks(); + MOZ_RELEASE_ASSERT(!extantReleasedChunks, "Unexpected released chunk(s)"); + + // First request. + UniquePtr<ProfileBufferChunk> chunk = cm.GetChunk(); + MOZ_RELEASE_ASSERT(!!chunk, "First chunk request should always work"); + MOZ_RELEASE_ASSERT(chunk->BufferBytes() >= ChunkMinBufferBytes, + "Unexpected chunk size"); + MOZ_RELEASE_ASSERT(!chunk->GetNext(), "There should only be one chunk"); + + // Keep address, for later checks. + const uintptr_t chunkAddress = reinterpret_cast<uintptr_t>(chunk.get()); + + extantReleasedChunks = cm.GetExtantReleasedChunks(); + MOZ_RELEASE_ASSERT(!extantReleasedChunks, "Unexpected released chunk(s)"); + + // Second request. + MOZ_RELEASE_ASSERT(!cm.GetChunk(), "Second chunk request should always fail"); + + extantReleasedChunks = cm.GetExtantReleasedChunks(); + MOZ_RELEASE_ASSERT(!extantReleasedChunks, "Unexpected released chunk(s)"); + + // Add some data to the chunk (to verify recycling later on). + MOZ_RELEASE_ASSERT(chunk->ChunkHeader().mOffsetFirstBlock == 0); + MOZ_RELEASE_ASSERT(chunk->ChunkHeader().mOffsetPastLastBlock == 0); + MOZ_RELEASE_ASSERT(chunk->RangeStart() == 0); + chunk->SetRangeStart(100); + MOZ_RELEASE_ASSERT(chunk->RangeStart() == 100); + Unused << chunk->ReserveInitialBlockAsTail(1); + Unused << chunk->ReserveBlock(2); + MOZ_RELEASE_ASSERT(chunk->ChunkHeader().mOffsetFirstBlock == 1); + MOZ_RELEASE_ASSERT(chunk->ChunkHeader().mOffsetPastLastBlock == 1 + 2); + + // Release the first chunk. + chunk->MarkDone(); + cm.ReleaseChunk(std::move(chunk)); + MOZ_RELEASE_ASSERT(!chunk, "chunk UniquePtr should have been moved-from"); + + // Request after release. + MOZ_RELEASE_ASSERT(!cm.GetChunk(), + "Chunk request after release should also fail"); + + // Check released chunk. + extantReleasedChunks = cm.GetExtantReleasedChunks(); + MOZ_RELEASE_ASSERT(!!extantReleasedChunks, + "Could not retrieve released chunk"); + MOZ_RELEASE_ASSERT(!extantReleasedChunks->GetNext(), + "There should only be one released chunk"); + MOZ_RELEASE_ASSERT( + reinterpret_cast<uintptr_t>(extantReleasedChunks.get()) == chunkAddress, + "Released chunk should be first requested one"); + + MOZ_RELEASE_ASSERT(!cm.GetExtantReleasedChunks(), + "Unexpected extra released chunk(s)"); + + // Another request after release. + MOZ_RELEASE_ASSERT(!cm.GetChunk(), + "Chunk request after release should also fail"); + + MOZ_RELEASE_ASSERT( + cm.MaxTotalSize() == maxTotalSize, + "MaxTotalSize() should not change after requests&releases"); + + // Reset the chunk manager. (Single-only non-virtual function.) + cms.Reset(std::move(extantReleasedChunks)); + MOZ_RELEASE_ASSERT(!extantReleasedChunks, + "Released chunk UniquePtr should have been moved-from"); + + MOZ_RELEASE_ASSERT( + cm.MaxTotalSize() == maxTotalSize, + "MaxTotalSize() should not change when resetting with the same chunk"); + + // 2nd round, first request. Theoretically async, but this implementation just + // immediately runs the callback. + bool ran = false; + cm.RequestChunk([&](UniquePtr<ProfileBufferChunk> aChunk) { + ran = true; + MOZ_RELEASE_ASSERT(!!aChunk); + chunk = std::move(aChunk); + }); + MOZ_RELEASE_ASSERT(ran, "RequestChunk callback not called immediately"); + ran = false; + cm.FulfillChunkRequests(); + MOZ_RELEASE_ASSERT(!ran, "FulfillChunkRequests should not have any effects"); + MOZ_RELEASE_ASSERT(!!chunk, "First chunk request should always work"); + MOZ_RELEASE_ASSERT(chunk->BufferBytes() >= ChunkMinBufferBytes, + "Unexpected chunk size"); + MOZ_RELEASE_ASSERT(!chunk->GetNext(), "There should only be one chunk"); + MOZ_RELEASE_ASSERT(reinterpret_cast<uintptr_t>(chunk.get()) == chunkAddress, + "Requested chunk should be first requested one"); + // Verify that chunk is empty and usable. + MOZ_RELEASE_ASSERT(chunk->ChunkHeader().mOffsetFirstBlock == 0); + MOZ_RELEASE_ASSERT(chunk->ChunkHeader().mOffsetPastLastBlock == 0); + MOZ_RELEASE_ASSERT(chunk->RangeStart() == 0); + chunk->SetRangeStart(200); + MOZ_RELEASE_ASSERT(chunk->RangeStart() == 200); + Unused << chunk->ReserveInitialBlockAsTail(3); + Unused << chunk->ReserveBlock(4); + MOZ_RELEASE_ASSERT(chunk->ChunkHeader().mOffsetFirstBlock == 3); + MOZ_RELEASE_ASSERT(chunk->ChunkHeader().mOffsetPastLastBlock == 3 + 4); + + // Second request. + ran = false; + cm.RequestChunk([&](UniquePtr<ProfileBufferChunk> aChunk) { + ran = true; + MOZ_RELEASE_ASSERT(!aChunk, "Second chunk request should always fail"); + }); + MOZ_RELEASE_ASSERT(ran, "RequestChunk callback not called"); + + // This one does nothing. + cm.ForgetUnreleasedChunks(); + + // Don't forget to mark chunk "Done" before letting it die. + chunk->MarkDone(); + chunk = nullptr; + + // Create a tiny chunk and reset the chunk manager with it. + chunk = ProfileBufferChunk::Create(1); + MOZ_RELEASE_ASSERT(!!chunk); + auto tinyChunkSize = chunk->BufferBytes(); + MOZ_RELEASE_ASSERT(tinyChunkSize >= 1); + MOZ_RELEASE_ASSERT(tinyChunkSize < ChunkMinBufferBytes); + MOZ_RELEASE_ASSERT(chunk->RangeStart() == 0); + chunk->SetRangeStart(300); + MOZ_RELEASE_ASSERT(chunk->RangeStart() == 300); + cms.Reset(std::move(chunk)); + MOZ_RELEASE_ASSERT(!chunk, "chunk UniquePtr should have been moved-from"); + MOZ_RELEASE_ASSERT(cm.MaxTotalSize() == tinyChunkSize, + "MaxTotalSize() should match the new chunk size"); + chunk = cm.GetChunk(); + MOZ_RELEASE_ASSERT(chunk->RangeStart() == 0, "Got non-recycled chunk"); + + // Enough testing! Clean-up. + Unused << chunk->ReserveInitialBlockAsTail(0); + chunk->MarkDone(); + cm.ForgetUnreleasedChunks(); + +# ifdef DEBUG + cm.DeregisteredFrom(chunkManagerRegisterer); +# endif // DEBUG + + printf("TestChunkManagerSingle done\n"); +} + +static void TestChunkManagerWithLocalLimit() { + printf("TestChunkManagerWithLocalLimit...\n"); + + // Construct a ProfileBufferChunkManagerWithLocalLimit with chunk of minimum + // size >=100, up to 1000 bytes. + constexpr ProfileBufferChunk::Length MaxTotalBytes = 1000; + constexpr ProfileBufferChunk::Length ChunkMinBufferBytes = 100; + ProfileBufferChunkManagerWithLocalLimit cmll{MaxTotalBytes, + ChunkMinBufferBytes}; + + // Reference to base class, to exercize virtual methods. + ProfileBufferChunkManager& cm = cmll; + +# ifdef DEBUG + const char* chunkManagerRegisterer = "TestChunkManagerWithLocalLimit"; + cm.RegisteredWith(chunkManagerRegisterer); +# endif // DEBUG + + MOZ_RELEASE_ASSERT(cm.MaxTotalSize() == MaxTotalBytes, + "Max total size should be exactly as given"); + + unsigned destroyedChunks = 0; + unsigned destroyedBytes = 0; + cm.SetChunkDestroyedCallback([&](const ProfileBufferChunk& aChunks) { + for (const ProfileBufferChunk* chunk = &aChunks; chunk; + chunk = chunk->GetNext()) { + destroyedChunks += 1; + destroyedBytes += chunk->BufferBytes(); + } + }); + + UniquePtr<ProfileBufferChunk> extantReleasedChunks = + cm.GetExtantReleasedChunks(); + MOZ_RELEASE_ASSERT(!extantReleasedChunks, "Unexpected released chunk(s)"); + + // First request. + UniquePtr<ProfileBufferChunk> chunk = cm.GetChunk(); + MOZ_RELEASE_ASSERT(!!chunk, + "First chunk immediate request should always work"); + const auto chunkActualBufferBytes = chunk->BufferBytes(); + MOZ_RELEASE_ASSERT(chunkActualBufferBytes >= ChunkMinBufferBytes, + "Unexpected chunk size"); + MOZ_RELEASE_ASSERT(!chunk->GetNext(), "There should only be one chunk"); + + // Keep address, for later checks. + const uintptr_t chunk1Address = reinterpret_cast<uintptr_t>(chunk.get()); + + extantReleasedChunks = cm.GetExtantReleasedChunks(); + MOZ_RELEASE_ASSERT(!extantReleasedChunks, "Unexpected released chunk(s)"); + + // Verify that ReleaseChunk accepts zero chunks. + cm.ReleaseChunk(nullptr); + MOZ_RELEASE_ASSERT(!extantReleasedChunks, "Unexpected released chunk(s)"); + + // For this test, we need to be able to get at least 2 chunks without hitting + // the limit. (If this failed, it wouldn't necessary be a problem with + // ProfileBufferChunkManagerWithLocalLimit, fiddle with constants at the top + // of this test.) + MOZ_RELEASE_ASSERT(chunkActualBufferBytes < 2 * MaxTotalBytes); + + unsigned chunk1ReuseCount = 0; + + // We will do enough loops to go through the maximum size a number of times. + const unsigned Rollovers = 3; + const unsigned Loops = Rollovers * MaxTotalBytes / chunkActualBufferBytes; + for (unsigned i = 0; i < Loops; ++i) { + // Add some data to the chunk. + MOZ_RELEASE_ASSERT(chunk->ChunkHeader().mOffsetFirstBlock == 0); + MOZ_RELEASE_ASSERT(chunk->ChunkHeader().mOffsetPastLastBlock == 0); + MOZ_RELEASE_ASSERT(chunk->RangeStart() == 0); + const ProfileBufferIndex index = 1 + i * chunkActualBufferBytes; + chunk->SetRangeStart(index); + MOZ_RELEASE_ASSERT(chunk->RangeStart() == index); + Unused << chunk->ReserveInitialBlockAsTail(1); + Unused << chunk->ReserveBlock(2); + MOZ_RELEASE_ASSERT(chunk->ChunkHeader().mOffsetFirstBlock == 1); + MOZ_RELEASE_ASSERT(chunk->ChunkHeader().mOffsetPastLastBlock == 1 + 2); + + // Request a new chunk. + bool ran = false; + UniquePtr<ProfileBufferChunk> newChunk; + cm.RequestChunk([&](UniquePtr<ProfileBufferChunk> aChunk) { + ran = true; + newChunk = std::move(aChunk); + }); + MOZ_RELEASE_ASSERT( + !ran, "RequestChunk should not immediately fulfill the request"); + cm.FulfillChunkRequests(); + MOZ_RELEASE_ASSERT(ran, "FulfillChunkRequests should invoke the callback"); + MOZ_RELEASE_ASSERT(!!newChunk, "Chunk request should always work"); + MOZ_RELEASE_ASSERT(newChunk->BufferBytes() == chunkActualBufferBytes, + "Unexpected chunk size"); + MOZ_RELEASE_ASSERT(!newChunk->GetNext(), "There should only be one chunk"); + + // Mark previous chunk done and release it. + WaitUntilTimeStampChanges(); // Force "done" timestamp to change. + chunk->MarkDone(); + cm.ReleaseChunk(std::move(chunk)); + + // And cycle to the new chunk. + chunk = std::move(newChunk); + + if (reinterpret_cast<uintptr_t>(chunk.get()) == chunk1Address) { + ++chunk1ReuseCount; + } + } + + // Expect all rollovers except 1 to destroy chunks. + MOZ_RELEASE_ASSERT(destroyedChunks >= (Rollovers - 1) * MaxTotalBytes / + chunkActualBufferBytes, + "Not enough destroyed chunks"); + MOZ_RELEASE_ASSERT(destroyedBytes == destroyedChunks * chunkActualBufferBytes, + "Mismatched destroyed chunks and bytes"); + MOZ_RELEASE_ASSERT(chunk1ReuseCount >= (Rollovers - 1), + "Not enough reuse of the first chunks"); + + // Check that chunk manager is reentrant from request callback. + bool ran = false; + bool ranInner = false; + UniquePtr<ProfileBufferChunk> newChunk; + cm.RequestChunk([&](UniquePtr<ProfileBufferChunk> aChunk) { + ran = true; + MOZ_RELEASE_ASSERT(!!aChunk, "Chunk request should always work"); + Unused << aChunk->ReserveInitialBlockAsTail(0); + WaitUntilTimeStampChanges(); // Force "done" timestamp to change. + aChunk->MarkDone(); + UniquePtr<ProfileBufferChunk> anotherChunk = cm.GetChunk(); + MOZ_RELEASE_ASSERT(!!anotherChunk); + Unused << anotherChunk->ReserveInitialBlockAsTail(0); + WaitUntilTimeStampChanges(); // Force "done" timestamp to change. + anotherChunk->MarkDone(); + cm.RequestChunk([&](UniquePtr<ProfileBufferChunk> aChunk) { + ranInner = true; + MOZ_RELEASE_ASSERT(!!aChunk, "Chunk request should always work"); + Unused << aChunk->ReserveInitialBlockAsTail(0); + WaitUntilTimeStampChanges(); // Force "done" timestamp to change. + aChunk->MarkDone(); + }); + MOZ_RELEASE_ASSERT( + !ranInner, "RequestChunk should not immediately fulfill the request"); + }); + MOZ_RELEASE_ASSERT(!ran, + "RequestChunk should not immediately fulfill the request"); + MOZ_RELEASE_ASSERT( + !ranInner, + "RequestChunk should not immediately fulfill the inner request"); + cm.FulfillChunkRequests(); + MOZ_RELEASE_ASSERT(ran, "FulfillChunkRequests should invoke the callback"); + MOZ_RELEASE_ASSERT(!ranInner, + "FulfillChunkRequests should not immediately fulfill " + "the inner request"); + cm.FulfillChunkRequests(); + MOZ_RELEASE_ASSERT( + ran, "2nd FulfillChunkRequests should invoke the inner request callback"); + + // Enough testing! Clean-up. + Unused << chunk->ReserveInitialBlockAsTail(0); + WaitUntilTimeStampChanges(); // Force "done" timestamp to change. + chunk->MarkDone(); + cm.ForgetUnreleasedChunks(); + + // Special testing of the release algorithm, to make sure released chunks get + // sorted. + constexpr unsigned RandomReleaseChunkLoop = 100; + // Build a vector of chunks, and mark them "done", ready to be released. + Vector<UniquePtr<ProfileBufferChunk>> chunksToRelease; + MOZ_RELEASE_ASSERT(chunksToRelease.reserve(RandomReleaseChunkLoop)); + Vector<TimeStamp> chunksTimeStamps; + MOZ_RELEASE_ASSERT(chunksTimeStamps.reserve(RandomReleaseChunkLoop)); + for (unsigned i = 0; i < RandomReleaseChunkLoop; ++i) { + UniquePtr<ProfileBufferChunk> chunk = cm.GetChunk(); + MOZ_RELEASE_ASSERT(chunk); + Unused << chunk->ReserveInitialBlockAsTail(0); + chunk->MarkDone(); + MOZ_RELEASE_ASSERT(!chunk->ChunkHeader().mDoneTimeStamp.IsNull()); + chunksTimeStamps.infallibleEmplaceBack(chunk->ChunkHeader().mDoneTimeStamp); + chunksToRelease.infallibleEmplaceBack(std::move(chunk)); + if (i % 10 == 0) { + // "Done" timestamps should *usually* increase, let's make extra sure some + // timestamps are actually different. + WaitUntilTimeStampChanges(); + } + } + // Shuffle the list. + std::random_device randomDevice; + std::mt19937 generator(randomDevice()); + std::shuffle(chunksToRelease.begin(), chunksToRelease.end(), generator); + // And release chunks one by one, checking that the list of released chunks + // is always sorted. + printf("TestChunkManagerWithLocalLimit - Shuffle test timestamps:"); + for (unsigned i = 0; i < RandomReleaseChunkLoop; ++i) { + printf(" %f", (chunksToRelease[i]->ChunkHeader().mDoneTimeStamp - + TimeStamp::ProcessCreation()) + .ToMicroseconds()); + cm.ReleaseChunk(std::move(chunksToRelease[i])); + cm.PeekExtantReleasedChunks([i](const ProfileBufferChunk* releasedChunks) { + MOZ_RELEASE_ASSERT(releasedChunks); + unsigned releasedChunkCount = 1; + for (;;) { + const ProfileBufferChunk* nextChunk = releasedChunks->GetNext(); + if (!nextChunk) { + break; + } + ++releasedChunkCount; + MOZ_RELEASE_ASSERT(releasedChunks->ChunkHeader().mDoneTimeStamp <= + nextChunk->ChunkHeader().mDoneTimeStamp); + releasedChunks = nextChunk; + } + MOZ_RELEASE_ASSERT(releasedChunkCount == i + 1); + }); + } + printf("\n"); + // Finally, the whole list of released chunks should have the exact same + // timestamps as the initial list of "done" chunks. + extantReleasedChunks = cm.GetExtantReleasedChunks(); + for (unsigned i = 0; i < RandomReleaseChunkLoop; ++i) { + MOZ_RELEASE_ASSERT(extantReleasedChunks, "Not enough released chunks"); + MOZ_RELEASE_ASSERT(extantReleasedChunks->ChunkHeader().mDoneTimeStamp == + chunksTimeStamps[i]); + Unused << std::exchange(extantReleasedChunks, + extantReleasedChunks->ReleaseNext()); + } + MOZ_RELEASE_ASSERT(!extantReleasedChunks, "Too many released chunks"); + +# ifdef DEBUG + cm.DeregisteredFrom(chunkManagerRegisterer); +# endif // DEBUG + + printf("TestChunkManagerWithLocalLimit done\n"); +} + +static bool IsSameMetadata( + const ProfileBufferControlledChunkManager::ChunkMetadata& a1, + const ProfileBufferControlledChunkManager::ChunkMetadata& a2) { + return a1.mDoneTimeStamp == a2.mDoneTimeStamp && + a1.mBufferBytes == a2.mBufferBytes; +}; + +static bool IsSameUpdate( + const ProfileBufferControlledChunkManager::Update& a1, + const ProfileBufferControlledChunkManager::Update& a2) { + // Final and not-an-update don't carry other data, so we can test these two + // states first. + if (a1.IsFinal() || a2.IsFinal()) { + return a1.IsFinal() && a2.IsFinal(); + } + if (a1.IsNotUpdate() || a2.IsNotUpdate()) { + return a1.IsNotUpdate() && a2.IsNotUpdate(); + } + + // Here, both are "normal" udpates, check member variables: + + if (a1.UnreleasedBytes() != a2.UnreleasedBytes()) { + return false; + } + if (a1.ReleasedBytes() != a2.ReleasedBytes()) { + return false; + } + if (a1.OldestDoneTimeStamp() != a2.OldestDoneTimeStamp()) { + return false; + } + if (a1.NewlyReleasedChunksRef().size() != + a2.NewlyReleasedChunksRef().size()) { + return false; + } + for (unsigned i = 0; i < a1.NewlyReleasedChunksRef().size(); ++i) { + if (!IsSameMetadata(a1.NewlyReleasedChunksRef()[i], + a2.NewlyReleasedChunksRef()[i])) { + return false; + } + } + return true; +} + +static void TestControlledChunkManagerUpdate() { + printf("TestControlledChunkManagerUpdate...\n"); + + using Update = ProfileBufferControlledChunkManager::Update; + + // Default construction. + Update update1; + MOZ_RELEASE_ASSERT(update1.IsNotUpdate()); + MOZ_RELEASE_ASSERT(!update1.IsFinal()); + + // Clear an already-cleared update. + update1.Clear(); + MOZ_RELEASE_ASSERT(update1.IsNotUpdate()); + MOZ_RELEASE_ASSERT(!update1.IsFinal()); + + // Final construction with nullptr. + const Update final(nullptr); + MOZ_RELEASE_ASSERT(final.IsFinal()); + MOZ_RELEASE_ASSERT(!final.IsNotUpdate()); + + // Copy final to cleared. + update1 = final; + MOZ_RELEASE_ASSERT(update1.IsFinal()); + MOZ_RELEASE_ASSERT(!update1.IsNotUpdate()); + + // Copy final to final. + update1 = final; + MOZ_RELEASE_ASSERT(update1.IsFinal()); + MOZ_RELEASE_ASSERT(!update1.IsNotUpdate()); + + // Clear a final update. + update1.Clear(); + MOZ_RELEASE_ASSERT(update1.IsNotUpdate()); + MOZ_RELEASE_ASSERT(!update1.IsFinal()); + + // Move final to cleared. + update1 = Update(nullptr); + MOZ_RELEASE_ASSERT(update1.IsFinal()); + MOZ_RELEASE_ASSERT(!update1.IsNotUpdate()); + + // Move final to final. + update1 = Update(nullptr); + MOZ_RELEASE_ASSERT(update1.IsFinal()); + MOZ_RELEASE_ASSERT(!update1.IsNotUpdate()); + + // Move from not-an-update (effectively same as Clear). + update1 = Update(); + MOZ_RELEASE_ASSERT(update1.IsNotUpdate()); + MOZ_RELEASE_ASSERT(!update1.IsFinal()); + + auto CreateBiggerChunkAfter = [](const ProfileBufferChunk& aChunkToBeat) { + while (TimeStamp::NowUnfuzzed() <= + aChunkToBeat.ChunkHeader().mDoneTimeStamp) { + ::SleepMilli(1); + } + auto chunk = ProfileBufferChunk::Create(aChunkToBeat.BufferBytes() * 2); + MOZ_RELEASE_ASSERT(!!chunk); + MOZ_RELEASE_ASSERT(chunk->BufferBytes() >= aChunkToBeat.BufferBytes() * 2); + Unused << chunk->ReserveInitialBlockAsTail(0); + chunk->MarkDone(); + MOZ_RELEASE_ASSERT(chunk->ChunkHeader().mDoneTimeStamp > + aChunkToBeat.ChunkHeader().mDoneTimeStamp); + return chunk; + }; + + update1 = Update(1, 2, nullptr, nullptr); + + // Create initial update with 2 released chunks and 1 unreleased chunk. + auto released = ProfileBufferChunk::Create(10); + ProfileBufferChunk* c1 = released.get(); + Unused << c1->ReserveInitialBlockAsTail(0); + c1->MarkDone(); + + released->SetLast(CreateBiggerChunkAfter(*c1)); + ProfileBufferChunk* c2 = c1->GetNext(); + + auto unreleased = CreateBiggerChunkAfter(*c2); + ProfileBufferChunk* c3 = unreleased.get(); + + Update update2(c3->BufferBytes(), c1->BufferBytes() + c2->BufferBytes(), c1, + c1); + MOZ_RELEASE_ASSERT(IsSameUpdate( + update2, + Update(c3->BufferBytes(), c1->BufferBytes() + c2->BufferBytes(), + c1->ChunkHeader().mDoneTimeStamp, + {{c1->ChunkHeader().mDoneTimeStamp, c1->BufferBytes()}, + {c2->ChunkHeader().mDoneTimeStamp, c2->BufferBytes()}}))); + // Check every field, this time only, after that we'll trust that the + // `SameUpdate` test will be enough. + MOZ_RELEASE_ASSERT(!update2.IsNotUpdate()); + MOZ_RELEASE_ASSERT(!update2.IsFinal()); + MOZ_RELEASE_ASSERT(update2.UnreleasedBytes() == c3->BufferBytes()); + MOZ_RELEASE_ASSERT(update2.ReleasedBytes() == + c1->BufferBytes() + c2->BufferBytes()); + MOZ_RELEASE_ASSERT(update2.OldestDoneTimeStamp() == + c1->ChunkHeader().mDoneTimeStamp); + MOZ_RELEASE_ASSERT(update2.NewlyReleasedChunksRef().size() == 2); + MOZ_RELEASE_ASSERT( + IsSameMetadata(update2.NewlyReleasedChunksRef()[0], + {c1->ChunkHeader().mDoneTimeStamp, c1->BufferBytes()})); + MOZ_RELEASE_ASSERT( + IsSameMetadata(update2.NewlyReleasedChunksRef()[1], + {c2->ChunkHeader().mDoneTimeStamp, c2->BufferBytes()})); + + // Fold into not-an-update. + update1.Fold(std::move(update2)); + MOZ_RELEASE_ASSERT(IsSameUpdate( + update1, + Update(c3->BufferBytes(), c1->BufferBytes() + c2->BufferBytes(), + c1->ChunkHeader().mDoneTimeStamp, + {{c1->ChunkHeader().mDoneTimeStamp, c1->BufferBytes()}, + {c2->ChunkHeader().mDoneTimeStamp, c2->BufferBytes()}}))); + + // Pretend nothing happened. + update2 = Update(c3->BufferBytes(), c1->BufferBytes() + c2->BufferBytes(), c1, + nullptr); + MOZ_RELEASE_ASSERT(IsSameUpdate( + update2, Update(c3->BufferBytes(), c1->BufferBytes() + c2->BufferBytes(), + c1->ChunkHeader().mDoneTimeStamp, {}))); + update1.Fold(std::move(update2)); + MOZ_RELEASE_ASSERT(IsSameUpdate( + update1, + Update(c3->BufferBytes(), c1->BufferBytes() + c2->BufferBytes(), + c1->ChunkHeader().mDoneTimeStamp, + {{c1->ChunkHeader().mDoneTimeStamp, c1->BufferBytes()}, + {c2->ChunkHeader().mDoneTimeStamp, c2->BufferBytes()}}))); + + // Pretend there's a new unreleased chunk. + c3->SetLast(CreateBiggerChunkAfter(*c3)); + ProfileBufferChunk* c4 = c3->GetNext(); + update2 = Update(c3->BufferBytes() + c4->BufferBytes(), + c1->BufferBytes() + c2->BufferBytes(), c1, nullptr); + MOZ_RELEASE_ASSERT( + IsSameUpdate(update2, Update(c3->BufferBytes() + c4->BufferBytes(), + c1->BufferBytes() + c2->BufferBytes(), + c1->ChunkHeader().mDoneTimeStamp, {}))); + update1.Fold(std::move(update2)); + MOZ_RELEASE_ASSERT(IsSameUpdate( + update1, + Update(c3->BufferBytes() + c4->BufferBytes(), + c1->BufferBytes() + c2->BufferBytes(), + c1->ChunkHeader().mDoneTimeStamp, + {{c1->ChunkHeader().mDoneTimeStamp, c1->BufferBytes()}, + {c2->ChunkHeader().mDoneTimeStamp, c2->BufferBytes()}}))); + + // Pretend the first unreleased chunk c3 has been released. + released->SetLast(std::exchange(unreleased, unreleased->ReleaseNext())); + update2 = + Update(c4->BufferBytes(), + c1->BufferBytes() + c2->BufferBytes() + c3->BufferBytes(), c1, c3); + MOZ_RELEASE_ASSERT(IsSameUpdate( + update2, + Update(c4->BufferBytes(), + c1->BufferBytes() + c2->BufferBytes() + c3->BufferBytes(), + c1->ChunkHeader().mDoneTimeStamp, + {{c3->ChunkHeader().mDoneTimeStamp, c3->BufferBytes()}}))); + update1.Fold(std::move(update2)); + MOZ_RELEASE_ASSERT(IsSameUpdate( + update1, + Update(c4->BufferBytes(), + c1->BufferBytes() + c2->BufferBytes() + c3->BufferBytes(), + c1->ChunkHeader().mDoneTimeStamp, + {{c1->ChunkHeader().mDoneTimeStamp, c1->BufferBytes()}, + {c2->ChunkHeader().mDoneTimeStamp, c2->BufferBytes()}, + {c3->ChunkHeader().mDoneTimeStamp, c3->BufferBytes()}}))); + + // Pretend c1 has been destroyed, so the oldest timestamp is now at c2. + released = released->ReleaseNext(); + c1 = nullptr; + update2 = Update(c4->BufferBytes(), c2->BufferBytes() + c3->BufferBytes(), c2, + nullptr); + MOZ_RELEASE_ASSERT(IsSameUpdate( + update2, Update(c4->BufferBytes(), c2->BufferBytes() + c3->BufferBytes(), + c2->ChunkHeader().mDoneTimeStamp, {}))); + update1.Fold(std::move(update2)); + MOZ_RELEASE_ASSERT(IsSameUpdate( + update1, + Update(c4->BufferBytes(), c2->BufferBytes() + c3->BufferBytes(), + c2->ChunkHeader().mDoneTimeStamp, + {{c2->ChunkHeader().mDoneTimeStamp, c2->BufferBytes()}, + {c3->ChunkHeader().mDoneTimeStamp, c3->BufferBytes()}}))); + + // Pretend c2 has been recycled to make unreleased c5, and c4 has been + // released. + auto recycled = std::exchange(released, released->ReleaseNext()); + recycled->MarkRecycled(); + Unused << recycled->ReserveInitialBlockAsTail(0); + recycled->MarkDone(); + released->SetLast(std::move(unreleased)); + unreleased = std::move(recycled); + ProfileBufferChunk* c5 = c2; + c2 = nullptr; + update2 = + Update(c5->BufferBytes(), c3->BufferBytes() + c4->BufferBytes(), c3, c4); + MOZ_RELEASE_ASSERT(IsSameUpdate( + update2, + Update(c5->BufferBytes(), c3->BufferBytes() + c4->BufferBytes(), + c3->ChunkHeader().mDoneTimeStamp, + {{c4->ChunkHeader().mDoneTimeStamp, c4->BufferBytes()}}))); + update1.Fold(std::move(update2)); + MOZ_RELEASE_ASSERT(IsSameUpdate( + update1, + Update(c5->BufferBytes(), c3->BufferBytes() + c4->BufferBytes(), + c3->ChunkHeader().mDoneTimeStamp, + {{c3->ChunkHeader().mDoneTimeStamp, c3->BufferBytes()}, + {c4->ChunkHeader().mDoneTimeStamp, c4->BufferBytes()}}))); + + // And send a final update. + update1.Fold(Update(nullptr)); + MOZ_RELEASE_ASSERT(update1.IsFinal()); + MOZ_RELEASE_ASSERT(!update1.IsNotUpdate()); + + printf("TestControlledChunkManagerUpdate done\n"); +} + +static void TestControlledChunkManagerWithLocalLimit() { + printf("TestControlledChunkManagerWithLocalLimit...\n"); + + // Construct a ProfileBufferChunkManagerWithLocalLimit with chunk of minimum + // size >=100, up to 1000 bytes. + constexpr ProfileBufferChunk::Length MaxTotalBytes = 1000; + constexpr ProfileBufferChunk::Length ChunkMinBufferBytes = 100; + ProfileBufferChunkManagerWithLocalLimit cmll{MaxTotalBytes, + ChunkMinBufferBytes}; + + // Reference to chunk manager base class. + ProfileBufferChunkManager& cm = cmll; + + // Reference to controlled chunk manager base class. + ProfileBufferControlledChunkManager& ccm = cmll; + +# ifdef DEBUG + const char* chunkManagerRegisterer = + "TestControlledChunkManagerWithLocalLimit"; + cm.RegisteredWith(chunkManagerRegisterer); +# endif // DEBUG + + MOZ_RELEASE_ASSERT(cm.MaxTotalSize() == MaxTotalBytes, + "Max total size should be exactly as given"); + + unsigned destroyedChunks = 0; + unsigned destroyedBytes = 0; + cm.SetChunkDestroyedCallback([&](const ProfileBufferChunk& aChunks) { + for (const ProfileBufferChunk* chunk = &aChunks; chunk; + chunk = chunk->GetNext()) { + destroyedChunks += 1; + destroyedBytes += chunk->BufferBytes(); + } + }); + + using Update = ProfileBufferControlledChunkManager::Update; + unsigned updateCount = 0; + ProfileBufferControlledChunkManager::Update update; + MOZ_RELEASE_ASSERT(update.IsNotUpdate()); + auto updateCallback = [&](Update&& aUpdate) { + ++updateCount; + update.Fold(std::move(aUpdate)); + }; + ccm.SetUpdateCallback(updateCallback); + MOZ_RELEASE_ASSERT(updateCount == 1, + "SetUpdateCallback should have triggered an update"); + MOZ_RELEASE_ASSERT(IsSameUpdate(update, Update(0, 0, TimeStamp{}, {}))); + updateCount = 0; + update.Clear(); + + UniquePtr<ProfileBufferChunk> extantReleasedChunks = + cm.GetExtantReleasedChunks(); + MOZ_RELEASE_ASSERT(!extantReleasedChunks, "Unexpected released chunk(s)"); + MOZ_RELEASE_ASSERT(updateCount == 1, + "GetExtantReleasedChunks should have triggered an update"); + MOZ_RELEASE_ASSERT(IsSameUpdate(update, Update(0, 0, TimeStamp{}, {}))); + updateCount = 0; + update.Clear(); + + // First request. + UniquePtr<ProfileBufferChunk> chunk = cm.GetChunk(); + MOZ_RELEASE_ASSERT(!!chunk, + "First chunk immediate request should always work"); + const auto chunkActualBufferBytes = chunk->BufferBytes(); + // Keep address, for later checks. + const uintptr_t chunk1Address = reinterpret_cast<uintptr_t>(chunk.get()); + MOZ_RELEASE_ASSERT(updateCount == 1, + "GetChunk should have triggered an update"); + MOZ_RELEASE_ASSERT( + IsSameUpdate(update, Update(chunk->BufferBytes(), 0, TimeStamp{}, {}))); + updateCount = 0; + update.Clear(); + + extantReleasedChunks = cm.GetExtantReleasedChunks(); + MOZ_RELEASE_ASSERT(!extantReleasedChunks, "Unexpected released chunk(s)"); + MOZ_RELEASE_ASSERT(updateCount == 1, + "GetExtantReleasedChunks should have triggered an update"); + MOZ_RELEASE_ASSERT( + IsSameUpdate(update, Update(chunk->BufferBytes(), 0, TimeStamp{}, {}))); + updateCount = 0; + update.Clear(); + + // For this test, we need to be able to get at least 2 chunks without hitting + // the limit. (If this failed, it wouldn't necessary be a problem with + // ProfileBufferChunkManagerWithLocalLimit, fiddle with constants at the top + // of this test.) + MOZ_RELEASE_ASSERT(chunkActualBufferBytes < 2 * MaxTotalBytes); + + ProfileBufferChunk::Length previousUnreleasedBytes = chunk->BufferBytes(); + ProfileBufferChunk::Length previousReleasedBytes = 0; + TimeStamp previousOldestDoneTimeStamp; + + unsigned chunk1ReuseCount = 0; + + // We will do enough loops to go through the maximum size a number of times. + const unsigned Rollovers = 3; + const unsigned Loops = Rollovers * MaxTotalBytes / chunkActualBufferBytes; + for (unsigned i = 0; i < Loops; ++i) { + // Add some data to the chunk. + const ProfileBufferIndex index = + ProfileBufferIndex(chunkActualBufferBytes) * i + 1; + chunk->SetRangeStart(index); + Unused << chunk->ReserveInitialBlockAsTail(1); + Unused << chunk->ReserveBlock(2); + + // Request a new chunk. + UniquePtr<ProfileBufferChunk> newChunk; + cm.RequestChunk([&](UniquePtr<ProfileBufferChunk> aChunk) { + newChunk = std::move(aChunk); + }); + MOZ_RELEASE_ASSERT(updateCount == 0, + "RequestChunk() shouldn't have triggered an update"); + cm.FulfillChunkRequests(); + MOZ_RELEASE_ASSERT(!!newChunk, "Chunk request should always work"); + MOZ_RELEASE_ASSERT(newChunk->BufferBytes() == chunkActualBufferBytes, + "Unexpected chunk size"); + MOZ_RELEASE_ASSERT(!newChunk->GetNext(), "There should only be one chunk"); + + MOZ_RELEASE_ASSERT(updateCount == 1, + "FulfillChunkRequests() after a request should have " + "triggered an update"); + MOZ_RELEASE_ASSERT(!update.IsFinal()); + MOZ_RELEASE_ASSERT(!update.IsNotUpdate()); + MOZ_RELEASE_ASSERT(update.UnreleasedBytes() == + previousUnreleasedBytes + newChunk->BufferBytes()); + previousUnreleasedBytes = update.UnreleasedBytes(); + MOZ_RELEASE_ASSERT(update.ReleasedBytes() <= previousReleasedBytes); + previousReleasedBytes = update.ReleasedBytes(); + MOZ_RELEASE_ASSERT(previousOldestDoneTimeStamp.IsNull() || + update.OldestDoneTimeStamp() >= + previousOldestDoneTimeStamp); + previousOldestDoneTimeStamp = update.OldestDoneTimeStamp(); + MOZ_RELEASE_ASSERT(update.NewlyReleasedChunksRef().empty()); + updateCount = 0; + update.Clear(); + + // Make sure the "Done" timestamp below cannot be the same as from the + // previous loop. + const TimeStamp now = TimeStamp::NowUnfuzzed(); + while (TimeStamp::NowUnfuzzed() == now) { + ::SleepMilli(1); + } + + // Mark previous chunk done and release it. + WaitUntilTimeStampChanges(); // Force "done" timestamp to change. + chunk->MarkDone(); + const auto doneTimeStamp = chunk->ChunkHeader().mDoneTimeStamp; + const auto bufferBytes = chunk->BufferBytes(); + cm.ReleaseChunk(std::move(chunk)); + + MOZ_RELEASE_ASSERT(updateCount == 1, + "ReleaseChunk() should have triggered an update"); + MOZ_RELEASE_ASSERT(!update.IsFinal()); + MOZ_RELEASE_ASSERT(!update.IsNotUpdate()); + MOZ_RELEASE_ASSERT(update.UnreleasedBytes() == + previousUnreleasedBytes - bufferBytes); + previousUnreleasedBytes = update.UnreleasedBytes(); + MOZ_RELEASE_ASSERT(update.ReleasedBytes() == + previousReleasedBytes + bufferBytes); + previousReleasedBytes = update.ReleasedBytes(); + MOZ_RELEASE_ASSERT(previousOldestDoneTimeStamp.IsNull() || + update.OldestDoneTimeStamp() >= + previousOldestDoneTimeStamp); + previousOldestDoneTimeStamp = update.OldestDoneTimeStamp(); + MOZ_RELEASE_ASSERT(update.OldestDoneTimeStamp() <= doneTimeStamp); + MOZ_RELEASE_ASSERT(update.NewlyReleasedChunksRef().size() == 1); + MOZ_RELEASE_ASSERT(update.NewlyReleasedChunksRef()[0].mDoneTimeStamp == + doneTimeStamp); + MOZ_RELEASE_ASSERT(update.NewlyReleasedChunksRef()[0].mBufferBytes == + bufferBytes); + updateCount = 0; + update.Clear(); + + // And cycle to the new chunk. + chunk = std::move(newChunk); + + if (reinterpret_cast<uintptr_t>(chunk.get()) == chunk1Address) { + ++chunk1ReuseCount; + } + } + + // Enough testing! Clean-up. + Unused << chunk->ReserveInitialBlockAsTail(0); + chunk->MarkDone(); + cm.ForgetUnreleasedChunks(); + MOZ_RELEASE_ASSERT( + updateCount == 1, + "ForgetUnreleasedChunks() should have triggered an update"); + MOZ_RELEASE_ASSERT(!update.IsFinal()); + MOZ_RELEASE_ASSERT(!update.IsNotUpdate()); + MOZ_RELEASE_ASSERT(update.UnreleasedBytes() == 0); + MOZ_RELEASE_ASSERT(update.ReleasedBytes() == previousReleasedBytes); + MOZ_RELEASE_ASSERT(update.NewlyReleasedChunksRef().empty() == 1); + updateCount = 0; + update.Clear(); + + ccm.SetUpdateCallback({}); + MOZ_RELEASE_ASSERT(updateCount == 1, + "SetUpdateCallback({}) should have triggered an update"); + MOZ_RELEASE_ASSERT(update.IsFinal()); + +# ifdef DEBUG + cm.DeregisteredFrom(chunkManagerRegisterer); +# endif // DEBUG + + printf("TestControlledChunkManagerWithLocalLimit done\n"); +} + +# define VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED( \ + aProfileChunkedBuffer, aStart, aEnd, aPushed, aCleared, aFailed) \ + { \ + ProfileChunkedBuffer::State state = (aProfileChunkedBuffer).GetState(); \ + MOZ_RELEASE_ASSERT(state.mRangeStart == (aStart)); \ + MOZ_RELEASE_ASSERT(state.mRangeEnd == (aEnd)); \ + MOZ_RELEASE_ASSERT(state.mPushedBlockCount == (aPushed)); \ + MOZ_RELEASE_ASSERT(state.mClearedBlockCount == (aCleared)); \ + MOZ_RELEASE_ASSERT(state.mFailedPutBytes == (aFailed)); \ + } + +static void TestChunkedBuffer() { + printf("TestChunkedBuffer...\n"); + + ProfileBufferBlockIndex blockIndex; + MOZ_RELEASE_ASSERT(!blockIndex); + MOZ_RELEASE_ASSERT(blockIndex == nullptr); + + // Create an out-of-session ProfileChunkedBuffer. + ProfileChunkedBuffer cb(ProfileChunkedBuffer::ThreadSafety::WithMutex); + + MOZ_RELEASE_ASSERT(cb.BufferLength().isNothing()); + + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED(cb, 1, 1, 0, 0, 0); + + int result = 0; + result = cb.ReserveAndPut( + []() { + MOZ_RELEASE_ASSERT(false); + return 1; + }, + [](Maybe<ProfileBufferEntryWriter>& aEW) { return aEW ? 2 : 3; }); + MOZ_RELEASE_ASSERT(result == 3); + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED(cb, 1, 1, 0, 0, 0); + + result = 0; + result = cb.Put( + 1, [](Maybe<ProfileBufferEntryWriter>& aEW) { return aEW ? 1 : 2; }); + MOZ_RELEASE_ASSERT(result == 2); + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED(cb, 1, 1, 0, 0, 0); + + blockIndex = cb.PutFrom(&result, 1); + MOZ_RELEASE_ASSERT(!blockIndex); + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED(cb, 1, 1, 0, 0, 0); + + blockIndex = cb.PutObjects(123, result, "hello"); + MOZ_RELEASE_ASSERT(!blockIndex); + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED(cb, 1, 1, 0, 0, 0); + + blockIndex = cb.PutObject(123); + MOZ_RELEASE_ASSERT(!blockIndex); + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED(cb, 1, 1, 0, 0, 0); + + auto chunks = cb.GetAllChunks(); + static_assert(std::is_same_v<decltype(chunks), UniquePtr<ProfileBufferChunk>>, + "ProfileChunkedBuffer::GetAllChunks() should return a " + "UniquePtr<ProfileBufferChunk>"); + MOZ_RELEASE_ASSERT(!chunks, "Expected no chunks when out-of-session"); + + bool ran = false; + result = 0; + result = cb.Read([&](ProfileChunkedBuffer::Reader* aReader) { + ran = true; + MOZ_RELEASE_ASSERT(!aReader); + return 3; + }); + MOZ_RELEASE_ASSERT(ran); + MOZ_RELEASE_ASSERT(result == 3); + + cb.ReadEach([](ProfileBufferEntryReader&) { MOZ_RELEASE_ASSERT(false); }); + + result = 0; + result = cb.ReadAt(nullptr, [](Maybe<ProfileBufferEntryReader>&& er) { + MOZ_RELEASE_ASSERT(er.isNothing()); + return 4; + }); + MOZ_RELEASE_ASSERT(result == 4); + + // Use ProfileBufferChunkManagerWithLocalLimit, which will give away + // ProfileBufferChunks that can contain 128 bytes, using up to 1KB of memory + // (including usable 128 bytes and headers). + constexpr size_t bufferMaxSize = 1024; + constexpr ProfileChunkedBuffer::Length chunkMinSize = 128; + ProfileBufferChunkManagerWithLocalLimit cm(bufferMaxSize, chunkMinSize); + cb.SetChunkManager(cm); + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED(cb, 1, 1, 0, 0, 0); + + // Let the chunk manager fulfill the initial request for an extra chunk. + cm.FulfillChunkRequests(); + + MOZ_RELEASE_ASSERT(cm.MaxTotalSize() == bufferMaxSize); + MOZ_RELEASE_ASSERT(cb.BufferLength().isSome()); + MOZ_RELEASE_ASSERT(*cb.BufferLength() == bufferMaxSize); + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED(cb, 1, 1, 0, 0, 0); + + // Write an int with the main `ReserveAndPut` function. + const int test = 123; + ran = false; + blockIndex = nullptr; + bool success = cb.ReserveAndPut( + []() { return sizeof(test); }, + [&](Maybe<ProfileBufferEntryWriter>& aEW) { + ran = true; + if (!aEW) { + return false; + } + blockIndex = aEW->CurrentBlockIndex(); + MOZ_RELEASE_ASSERT(aEW->RemainingBytes() == sizeof(test)); + aEW->WriteObject(test); + MOZ_RELEASE_ASSERT(aEW->RemainingBytes() == 0); + return true; + }); + MOZ_RELEASE_ASSERT(ran); + MOZ_RELEASE_ASSERT(success); + MOZ_RELEASE_ASSERT(blockIndex.ConvertToProfileBufferIndex() == 1); + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED( + cb, 1, 1 + ULEB128Size(sizeof(test)) + sizeof(test), 1, 0, 0); + + ran = false; + result = 0; + result = cb.Read([&](ProfileChunkedBuffer::Reader* aReader) { + ran = true; + MOZ_RELEASE_ASSERT(!!aReader); + // begin() and end() should be at the range edges (verified above). + MOZ_RELEASE_ASSERT( + aReader->begin().CurrentBlockIndex().ConvertToProfileBufferIndex() == + 1); + MOZ_RELEASE_ASSERT( + aReader->end().CurrentBlockIndex().ConvertToProfileBufferIndex() == 0); + // Null ProfileBufferBlockIndex clamped to the beginning. + MOZ_RELEASE_ASSERT(aReader->At(nullptr) == aReader->begin()); + MOZ_RELEASE_ASSERT(aReader->At(blockIndex) == aReader->begin()); + // At(begin) same as begin(). + MOZ_RELEASE_ASSERT(aReader->At(aReader->begin().CurrentBlockIndex()) == + aReader->begin()); + // At(past block) same as end(). + MOZ_RELEASE_ASSERT( + aReader->At(ProfileBufferBlockIndex::CreateFromProfileBufferIndex( + 1 + 1 + sizeof(test))) == aReader->end()); + + size_t read = 0; + aReader->ForEach([&](ProfileBufferEntryReader& er) { + ++read; + MOZ_RELEASE_ASSERT(er.RemainingBytes() == sizeof(test)); + const auto value = er.ReadObject<decltype(test)>(); + MOZ_RELEASE_ASSERT(value == test); + MOZ_RELEASE_ASSERT(er.RemainingBytes() == 0); + }); + MOZ_RELEASE_ASSERT(read == 1); + + read = 0; + for (auto er : *aReader) { + static_assert(std::is_same_v<decltype(er), ProfileBufferEntryReader>, + "ProfileChunkedBuffer::Reader range-for should produce " + "ProfileBufferEntryReader objects"); + ++read; + MOZ_RELEASE_ASSERT(er.RemainingBytes() == sizeof(test)); + const auto value = er.ReadObject<decltype(test)>(); + MOZ_RELEASE_ASSERT(value == test); + MOZ_RELEASE_ASSERT(er.RemainingBytes() == 0); + }; + MOZ_RELEASE_ASSERT(read == 1); + return 5; + }); + MOZ_RELEASE_ASSERT(ran); + MOZ_RELEASE_ASSERT(result == 5); + + // Read the int directly from the ProfileChunkedBuffer, without block index. + size_t read = 0; + cb.ReadEach([&](ProfileBufferEntryReader& er) { + ++read; + MOZ_RELEASE_ASSERT(er.RemainingBytes() == sizeof(test)); + const auto value = er.ReadObject<decltype(test)>(); + MOZ_RELEASE_ASSERT(value == test); + MOZ_RELEASE_ASSERT(er.RemainingBytes() == 0); + }); + MOZ_RELEASE_ASSERT(read == 1); + + // Read the int directly from the ProfileChunkedBuffer, with block index. + read = 0; + blockIndex = nullptr; + cb.ReadEach( + [&](ProfileBufferEntryReader& er, ProfileBufferBlockIndex aBlockIndex) { + ++read; + MOZ_RELEASE_ASSERT(!!aBlockIndex); + MOZ_RELEASE_ASSERT(!blockIndex); + blockIndex = aBlockIndex; + MOZ_RELEASE_ASSERT(er.RemainingBytes() == sizeof(test)); + const auto value = er.ReadObject<decltype(test)>(); + MOZ_RELEASE_ASSERT(value == test); + MOZ_RELEASE_ASSERT(er.RemainingBytes() == 0); + }); + MOZ_RELEASE_ASSERT(read == 1); + MOZ_RELEASE_ASSERT(!!blockIndex); + MOZ_RELEASE_ASSERT(blockIndex != nullptr); + + // Read the int from its block index. + read = 0; + result = 0; + result = cb.ReadAt(blockIndex, [&](Maybe<ProfileBufferEntryReader>&& er) { + ++read; + MOZ_RELEASE_ASSERT(er.isSome()); + MOZ_RELEASE_ASSERT(er->CurrentBlockIndex() == blockIndex); + MOZ_RELEASE_ASSERT(!er->NextBlockIndex()); + MOZ_RELEASE_ASSERT(er->RemainingBytes() == sizeof(test)); + const auto value = er->ReadObject<decltype(test)>(); + MOZ_RELEASE_ASSERT(value == test); + MOZ_RELEASE_ASSERT(er->RemainingBytes() == 0); + return 6; + }); + MOZ_RELEASE_ASSERT(result == 6); + MOZ_RELEASE_ASSERT(read == 1); + + // No changes after reads. + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED( + cb, 1, 1 + ULEB128Size(sizeof(test)) + sizeof(test), 1, 0, 0); + + // Steal the underlying ProfileBufferChunks from the ProfileChunkedBuffer. + chunks = cb.GetAllChunks(); + MOZ_RELEASE_ASSERT(!!chunks, "Expected at least one chunk"); + MOZ_RELEASE_ASSERT(!!chunks->GetNext(), "Expected two chunks"); + MOZ_RELEASE_ASSERT(!chunks->GetNext()->GetNext(), "Expected only two chunks"); + const ProfileChunkedBuffer::Length chunkActualSize = chunks->BufferBytes(); + MOZ_RELEASE_ASSERT(chunkActualSize >= chunkMinSize); + MOZ_RELEASE_ASSERT(chunks->RangeStart() == 1); + MOZ_RELEASE_ASSERT(chunks->OffsetFirstBlock() == 0); + MOZ_RELEASE_ASSERT(chunks->OffsetPastLastBlock() == 1 + sizeof(test)); + + // GetAllChunks() should have advanced the index one full chunk forward. + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED(cb, 1 + chunkActualSize, + 1 + chunkActualSize, 1, 0, 0); + + // Nothing more to read from the now-empty ProfileChunkedBuffer. + cb.ReadEach([](ProfileBufferEntryReader&) { MOZ_RELEASE_ASSERT(false); }); + cb.ReadEach([](ProfileBufferEntryReader&, ProfileBufferBlockIndex) { + MOZ_RELEASE_ASSERT(false); + }); + result = 0; + result = cb.ReadAt(nullptr, [](Maybe<ProfileBufferEntryReader>&& er) { + MOZ_RELEASE_ASSERT(er.isNothing()); + return 7; + }); + MOZ_RELEASE_ASSERT(result == 7); + + // Read the int from the stolen chunks. + read = 0; + ProfileChunkedBuffer::ReadEach( + chunks.get(), nullptr, + [&](ProfileBufferEntryReader& er, ProfileBufferBlockIndex aBlockIndex) { + ++read; + MOZ_RELEASE_ASSERT(aBlockIndex == blockIndex); + MOZ_RELEASE_ASSERT(er.RemainingBytes() == sizeof(test)); + const auto value = er.ReadObject<decltype(test)>(); + MOZ_RELEASE_ASSERT(value == test); + MOZ_RELEASE_ASSERT(er.RemainingBytes() == 0); + }); + MOZ_RELEASE_ASSERT(read == 1); + + // No changes after reads. + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED(cb, 1 + chunkActualSize, + 1 + chunkActualSize, 1, 0, 0); + + // Write lots of numbers (by memcpy), which should trigger Chunk destructions. + ProfileBufferBlockIndex firstBlockIndex; + MOZ_RELEASE_ASSERT(!firstBlockIndex); + ProfileBufferBlockIndex lastBlockIndex; + MOZ_RELEASE_ASSERT(!lastBlockIndex); + const size_t lots = 2 * bufferMaxSize / (1 + sizeof(int)); + for (size_t i = 1; i < lots; ++i) { + ProfileBufferBlockIndex blockIndex = cb.PutFrom(&i, sizeof(i)); + MOZ_RELEASE_ASSERT(!!blockIndex); + MOZ_RELEASE_ASSERT(blockIndex > firstBlockIndex); + if (!firstBlockIndex) { + firstBlockIndex = blockIndex; + } + MOZ_RELEASE_ASSERT(blockIndex > lastBlockIndex); + lastBlockIndex = blockIndex; + } + + ProfileChunkedBuffer::State stateAfterPuts = cb.GetState(); + ProfileBufferIndex startAfterPuts = stateAfterPuts.mRangeStart; + MOZ_RELEASE_ASSERT(startAfterPuts > 1 + chunkActualSize); + ProfileBufferIndex endAfterPuts = stateAfterPuts.mRangeEnd; + MOZ_RELEASE_ASSERT(endAfterPuts > startAfterPuts); + uint64_t pushedAfterPuts = stateAfterPuts.mPushedBlockCount; + MOZ_RELEASE_ASSERT(pushedAfterPuts > 0); + uint64_t clearedAfterPuts = stateAfterPuts.mClearedBlockCount; + MOZ_RELEASE_ASSERT(clearedAfterPuts > 0); + MOZ_RELEASE_ASSERT(stateAfterPuts.mFailedPutBytes == 0); + + // Read extant numbers, which should at least follow each other. + read = 0; + size_t i = 0; + cb.ReadEach( + [&](ProfileBufferEntryReader& er, ProfileBufferBlockIndex aBlockIndex) { + ++read; + MOZ_RELEASE_ASSERT(!!aBlockIndex); + MOZ_RELEASE_ASSERT(aBlockIndex > firstBlockIndex); + MOZ_RELEASE_ASSERT(aBlockIndex <= lastBlockIndex); + MOZ_RELEASE_ASSERT(er.RemainingBytes() == sizeof(size_t)); + const auto value = er.ReadObject<size_t>(); + if (i == 0) { + i = value; + } else { + MOZ_RELEASE_ASSERT(value == ++i); + } + MOZ_RELEASE_ASSERT(er.RemainingBytes() == 0); + }); + MOZ_RELEASE_ASSERT(read != 0); + MOZ_RELEASE_ASSERT(read < lots); + + // Read first extant number. + read = 0; + i = 0; + blockIndex = nullptr; + success = + cb.ReadAt(firstBlockIndex, [&](Maybe<ProfileBufferEntryReader>&& er) { + MOZ_ASSERT(er.isSome()); + ++read; + MOZ_RELEASE_ASSERT(er->CurrentBlockIndex() > firstBlockIndex); + MOZ_RELEASE_ASSERT(!!er->NextBlockIndex()); + MOZ_RELEASE_ASSERT(er->NextBlockIndex() > firstBlockIndex); + MOZ_RELEASE_ASSERT(er->NextBlockIndex() < lastBlockIndex); + blockIndex = er->NextBlockIndex(); + MOZ_RELEASE_ASSERT(er->RemainingBytes() == sizeof(size_t)); + const auto value = er->ReadObject<size_t>(); + MOZ_RELEASE_ASSERT(i == 0); + i = value; + MOZ_RELEASE_ASSERT(er->RemainingBytes() == 0); + return 7; + }); + MOZ_RELEASE_ASSERT(success); + MOZ_RELEASE_ASSERT(read == 1); + // Read other extant numbers one by one. + do { + bool success = + cb.ReadAt(blockIndex, [&](Maybe<ProfileBufferEntryReader>&& er) { + MOZ_ASSERT(er.isSome()); + ++read; + MOZ_RELEASE_ASSERT(er->CurrentBlockIndex() == blockIndex); + MOZ_RELEASE_ASSERT(!er->NextBlockIndex() || + er->NextBlockIndex() > blockIndex); + MOZ_RELEASE_ASSERT(!er->NextBlockIndex() || + er->NextBlockIndex() > firstBlockIndex); + MOZ_RELEASE_ASSERT(!er->NextBlockIndex() || + er->NextBlockIndex() <= lastBlockIndex); + MOZ_RELEASE_ASSERT(er->NextBlockIndex() + ? blockIndex < lastBlockIndex + : blockIndex == lastBlockIndex, + "er->NextBlockIndex() should only be null when " + "blockIndex is at the last block"); + blockIndex = er->NextBlockIndex(); + MOZ_RELEASE_ASSERT(er->RemainingBytes() == sizeof(size_t)); + const auto value = er->ReadObject<size_t>(); + MOZ_RELEASE_ASSERT(value == ++i); + MOZ_RELEASE_ASSERT(er->RemainingBytes() == 0); + return true; + }); + MOZ_RELEASE_ASSERT(success); + } while (blockIndex); + MOZ_RELEASE_ASSERT(read > 1); + + // No changes after reads. + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED( + cb, startAfterPuts, endAfterPuts, pushedAfterPuts, clearedAfterPuts, 0); + +# ifdef DEBUG + // cb.Dump(); +# endif + + cb.Clear(); + +# ifdef DEBUG + // cb.Dump(); +# endif + + ProfileChunkedBuffer::State stateAfterClear = cb.GetState(); + ProfileBufferIndex startAfterClear = stateAfterClear.mRangeStart; + MOZ_RELEASE_ASSERT(startAfterClear > startAfterPuts); + ProfileBufferIndex endAfterClear = stateAfterClear.mRangeEnd; + MOZ_RELEASE_ASSERT(endAfterClear == startAfterClear); + MOZ_RELEASE_ASSERT(stateAfterClear.mPushedBlockCount == 0); + MOZ_RELEASE_ASSERT(stateAfterClear.mClearedBlockCount == 0); + MOZ_RELEASE_ASSERT(stateAfterClear.mFailedPutBytes == 0); + + // Start writer threads. + constexpr int ThreadCount = 32; + std::thread threads[ThreadCount]; + for (int threadNo = 0; threadNo < ThreadCount; ++threadNo) { + threads[threadNo] = std::thread( + [&](int aThreadNo) { + ::SleepMilli(1); + constexpr int pushCount = 1024; + for (int push = 0; push < pushCount; ++push) { + // Reserve as many bytes as the thread number (but at least enough + // to store an int), and write an increasing int. + const bool success = + cb.Put(std::max(aThreadNo, int(sizeof(push))), + [&](Maybe<ProfileBufferEntryWriter>& aEW) { + if (!aEW) { + return false; + } + aEW->WriteObject(aThreadNo * 1000000 + push); + // Advance writer to the end. + for (size_t r = aEW->RemainingBytes(); r != 0; --r) { + aEW->WriteObject<char>('_'); + } + return true; + }); + MOZ_RELEASE_ASSERT(success); + } + }, + threadNo); + } + + // Wait for all writer threads to die. + for (auto&& thread : threads) { + thread.join(); + } + +# ifdef DEBUG + // cb.Dump(); +# endif + + ProfileChunkedBuffer::State stateAfterMTPuts = cb.GetState(); + ProfileBufferIndex startAfterMTPuts = stateAfterMTPuts.mRangeStart; + MOZ_RELEASE_ASSERT(startAfterMTPuts > startAfterClear); + ProfileBufferIndex endAfterMTPuts = stateAfterMTPuts.mRangeEnd; + MOZ_RELEASE_ASSERT(endAfterMTPuts > startAfterMTPuts); + MOZ_RELEASE_ASSERT(stateAfterMTPuts.mPushedBlockCount > 0); + MOZ_RELEASE_ASSERT(stateAfterMTPuts.mClearedBlockCount > 0); + MOZ_RELEASE_ASSERT(stateAfterMTPuts.mFailedPutBytes == 0); + + // Reset to out-of-session. + cb.ResetChunkManager(); + + ProfileChunkedBuffer::State stateAfterReset = cb.GetState(); + ProfileBufferIndex startAfterReset = stateAfterReset.mRangeStart; + MOZ_RELEASE_ASSERT(startAfterReset == endAfterMTPuts); + ProfileBufferIndex endAfterReset = stateAfterReset.mRangeEnd; + MOZ_RELEASE_ASSERT(endAfterReset == startAfterReset); + MOZ_RELEASE_ASSERT(stateAfterReset.mPushedBlockCount == 0); + MOZ_RELEASE_ASSERT(stateAfterReset.mClearedBlockCount == 0); + MOZ_RELEASE_ASSERT(stateAfterReset.mFailedPutBytes == 0); + + success = cb.ReserveAndPut( + []() { + MOZ_RELEASE_ASSERT(false); + return 1; + }, + [](Maybe<ProfileBufferEntryWriter>& aEW) { return !!aEW; }); + MOZ_RELEASE_ASSERT(!success); + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED(cb, startAfterReset, endAfterReset, + 0, 0, 0); + + success = + cb.Put(1, [](Maybe<ProfileBufferEntryWriter>& aEW) { return !!aEW; }); + MOZ_RELEASE_ASSERT(!success); + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED(cb, startAfterReset, endAfterReset, + 0, 0, 0); + + blockIndex = cb.PutFrom(&success, 1); + MOZ_RELEASE_ASSERT(!blockIndex); + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED(cb, startAfterReset, endAfterReset, + 0, 0, 0); + + blockIndex = cb.PutObjects(123, success, "hello"); + MOZ_RELEASE_ASSERT(!blockIndex); + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED(cb, startAfterReset, endAfterReset, + 0, 0, 0); + + blockIndex = cb.PutObject(123); + MOZ_RELEASE_ASSERT(!blockIndex); + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED(cb, startAfterReset, endAfterReset, + 0, 0, 0); + + chunks = cb.GetAllChunks(); + MOZ_RELEASE_ASSERT(!chunks, "Expected no chunks when out-of-session"); + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED(cb, startAfterReset, endAfterReset, + 0, 0, 0); + + cb.ReadEach([](ProfileBufferEntryReader&) { MOZ_RELEASE_ASSERT(false); }); + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED(cb, startAfterReset, endAfterReset, + 0, 0, 0); + + success = cb.ReadAt(nullptr, [](Maybe<ProfileBufferEntryReader>&& er) { + MOZ_RELEASE_ASSERT(er.isNothing()); + return true; + }); + MOZ_RELEASE_ASSERT(success); + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED(cb, startAfterReset, endAfterReset, + 0, 0, 0); + + printf("TestChunkedBuffer done\n"); +} + +static void TestChunkedBufferSingle() { + printf("TestChunkedBufferSingle...\n"); + + constexpr ProfileChunkedBuffer::Length chunkMinSize = 128; + + // Create a ProfileChunkedBuffer that will own&use a + // ProfileBufferChunkManagerSingle, which will give away one + // ProfileBufferChunk that can contain 128 bytes. + ProfileChunkedBuffer cbSingle( + ProfileChunkedBuffer::ThreadSafety::WithoutMutex, + MakeUnique<ProfileBufferChunkManagerSingle>(chunkMinSize)); + + MOZ_RELEASE_ASSERT(cbSingle.BufferLength().isSome()); + const ProfileChunkedBuffer::Length bufferBytes = *cbSingle.BufferLength(); + MOZ_RELEASE_ASSERT(bufferBytes >= chunkMinSize); + + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED(cbSingle, 1, 1, 0, 0, 0); + + // We will write this many blocks to fill the chunk. + constexpr size_t testBlocks = 4; + const ProfileChunkedBuffer::Length blockBytes = bufferBytes / testBlocks; + MOZ_RELEASE_ASSERT(ULEB128Size(blockBytes) == 1, + "This test assumes block sizes are small enough so that " + "their ULEB128-encoded size is 1 byte"); + const ProfileChunkedBuffer::Length entryBytes = + blockBytes - ULEB128Size(blockBytes); + + // First buffer-filling test: Try to write a too-big entry at the end of the + // chunk. + + // Write all but one block. + for (size_t i = 0; i < testBlocks - 1; ++i) { + cbSingle.Put(entryBytes, [&](Maybe<ProfileBufferEntryWriter>& aEW) { + MOZ_RELEASE_ASSERT(aEW.isSome()); + while (aEW->RemainingBytes() > 0) { + **aEW = '0' + i; + ++(*aEW); + } + }); + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED( + cbSingle, 1, 1 + blockBytes * (i + 1), i + 1, 0, 0); + } + + // Write the last block so that it's too big (by 1 byte) to fit in the chunk, + // this should fail. + const ProfileChunkedBuffer::Length remainingBytesForLastBlock = + bufferBytes - blockBytes * (testBlocks - 1); + MOZ_RELEASE_ASSERT(ULEB128Size(remainingBytesForLastBlock) == 1, + "This test assumes block sizes are small enough so that " + "their ULEB128-encoded size is 1 byte"); + const ProfileChunkedBuffer::Length entryToFitRemainingBytes = + remainingBytesForLastBlock - ULEB128Size(remainingBytesForLastBlock); + cbSingle.Put(entryToFitRemainingBytes + 1, + [&](Maybe<ProfileBufferEntryWriter>& aEW) { + MOZ_RELEASE_ASSERT(aEW.isNothing()); + }); + // The buffer state should not have changed, apart from the failed bytes. + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED( + cbSingle, 1, 1 + blockBytes * (testBlocks - 1), testBlocks - 1, 0, + remainingBytesForLastBlock + 1); + + size_t read = 0; + cbSingle.ReadEach([&](ProfileBufferEntryReader& aER) { + MOZ_RELEASE_ASSERT(aER.RemainingBytes() == entryBytes); + while (aER.RemainingBytes() > 0) { + MOZ_RELEASE_ASSERT(*aER == '0' + read); + ++aER; + } + ++read; + }); + MOZ_RELEASE_ASSERT(read == testBlocks - 1); + + // ~Interlude~ Test AppendContent: + // Create another ProfileChunkedBuffer that will use a + // ProfileBufferChunkManagerWithLocalLimit, which will give away + // ProfileBufferChunks that can contain 128 bytes, using up to 1KB of memory + // (including usable 128 bytes and headers). + constexpr size_t bufferMaxSize = 1024; + ProfileBufferChunkManagerWithLocalLimit cmTarget(bufferMaxSize, chunkMinSize); + ProfileChunkedBuffer cbTarget(ProfileChunkedBuffer::ThreadSafety::WithMutex, + cmTarget); + + // It should start empty. + cbTarget.ReadEach( + [](ProfileBufferEntryReader&) { MOZ_RELEASE_ASSERT(false); }); + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED(cbTarget, 1, 1, 0, 0, 0); + + // Copy the contents from cbSingle to cbTarget. + cbTarget.AppendContents(cbSingle); + + // And verify that we now have the same contents in cbTarget. + read = 0; + cbTarget.ReadEach([&](ProfileBufferEntryReader& aER) { + MOZ_RELEASE_ASSERT(aER.RemainingBytes() == entryBytes); + while (aER.RemainingBytes() > 0) { + MOZ_RELEASE_ASSERT(*aER == '0' + read); + ++aER; + } + ++read; + }); + MOZ_RELEASE_ASSERT(read == testBlocks - 1); + // The state should be the same as the source. + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED( + cbTarget, 1, 1 + blockBytes * (testBlocks - 1), testBlocks - 1, 0, 0); + +# ifdef DEBUG + // cbSingle.Dump(); + // cbTarget.Dump(); +# endif + + // Because we failed to write a too-big chunk above, the chunk was marked + // full, so that entries should be consistently rejected from now on. + cbSingle.Put(1, [&](Maybe<ProfileBufferEntryWriter>& aEW) { + MOZ_RELEASE_ASSERT(aEW.isNothing()); + }); + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED( + cbSingle, 1, 1 + blockBytes * ((testBlocks - 1)), testBlocks - 1, 0, + remainingBytesForLastBlock + 1 + ULEB128Size(1u) + 1); + + // Clear the buffer before the next test. + + cbSingle.Clear(); + // Clear() should move the index to the next chunk range -- even if it's + // really reusing the same chunk. + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED(cbSingle, 1 + bufferBytes, + 1 + bufferBytes, 0, 0, 0); + cbSingle.ReadEach( + [&](ProfileBufferEntryReader& aER) { MOZ_RELEASE_ASSERT(false); }); + + // Second buffer-filling test: Try to write a final entry that just fits at + // the end of the chunk. + + // Write all but one block. + for (size_t i = 0; i < testBlocks - 1; ++i) { + cbSingle.Put(entryBytes, [&](Maybe<ProfileBufferEntryWriter>& aEW) { + MOZ_RELEASE_ASSERT(aEW.isSome()); + while (aEW->RemainingBytes() > 0) { + **aEW = 'a' + i; + ++(*aEW); + } + }); + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED( + cbSingle, 1 + bufferBytes, 1 + bufferBytes + blockBytes * (i + 1), + i + 1, 0, 0); + } + + read = 0; + cbSingle.ReadEach([&](ProfileBufferEntryReader& aER) { + MOZ_RELEASE_ASSERT(aER.RemainingBytes() == entryBytes); + while (aER.RemainingBytes() > 0) { + MOZ_RELEASE_ASSERT(*aER == 'a' + read); + ++aER; + } + ++read; + }); + MOZ_RELEASE_ASSERT(read == testBlocks - 1); + + // Write the last block so that it fits exactly in the chunk. + cbSingle.Put(entryToFitRemainingBytes, + [&](Maybe<ProfileBufferEntryWriter>& aEW) { + MOZ_RELEASE_ASSERT(aEW.isSome()); + while (aEW->RemainingBytes() > 0) { + **aEW = 'a' + (testBlocks - 1); + ++(*aEW); + } + }); + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED( + cbSingle, 1 + bufferBytes, 1 + bufferBytes + blockBytes * testBlocks, + testBlocks, 0, 0); + + read = 0; + cbSingle.ReadEach([&](ProfileBufferEntryReader& aER) { + MOZ_RELEASE_ASSERT( + aER.RemainingBytes() == + ((read < testBlocks) ? entryBytes : entryToFitRemainingBytes)); + while (aER.RemainingBytes() > 0) { + MOZ_RELEASE_ASSERT(*aER == 'a' + read); + ++aER; + } + ++read; + }); + MOZ_RELEASE_ASSERT(read == testBlocks); + + // Because the single chunk has been filled, it shouldn't be possible to write + // more entries. + cbSingle.Put(1, [&](Maybe<ProfileBufferEntryWriter>& aEW) { + MOZ_RELEASE_ASSERT(aEW.isNothing()); + }); + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED( + cbSingle, 1 + bufferBytes, 1 + bufferBytes + blockBytes * testBlocks, + testBlocks, 0, ULEB128Size(1u) + 1); + + cbSingle.Clear(); + // Clear() should move the index to the next chunk range -- even if it's + // really reusing the same chunk. + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED(cbSingle, 1 + bufferBytes * 2, + 1 + bufferBytes * 2, 0, 0, 0); + cbSingle.ReadEach( + [&](ProfileBufferEntryReader& aER) { MOZ_RELEASE_ASSERT(false); }); + + // Clear() recycles the released chunk, so we should be able to record new + // entries. + cbSingle.Put(entryBytes, [&](Maybe<ProfileBufferEntryWriter>& aEW) { + MOZ_RELEASE_ASSERT(aEW.isSome()); + while (aEW->RemainingBytes() > 0) { + **aEW = 'x'; + ++(*aEW); + } + }); + VERIFY_PCB_START_END_PUSHED_CLEARED_FAILED( + cbSingle, 1 + bufferBytes * 2, + 1 + bufferBytes * 2 + ULEB128Size(entryBytes) + entryBytes, 1, 0, 0); + read = 0; + cbSingle.ReadEach([&](ProfileBufferEntryReader& aER) { + MOZ_RELEASE_ASSERT(read == 0); + MOZ_RELEASE_ASSERT(aER.RemainingBytes() == entryBytes); + while (aER.RemainingBytes() > 0) { + MOZ_RELEASE_ASSERT(*aER == 'x'); + ++aER; + } + ++read; + }); + MOZ_RELEASE_ASSERT(read == 1); + + printf("TestChunkedBufferSingle done\n"); +} + +static void TestModuloBuffer(ModuloBuffer<>& mb, uint32_t MBSize) { + using MB = ModuloBuffer<>; + + MOZ_RELEASE_ASSERT(mb.BufferLength().Value() == MBSize); + + // Iterator comparisons. + MOZ_RELEASE_ASSERT(mb.ReaderAt(2) == mb.ReaderAt(2)); + MOZ_RELEASE_ASSERT(mb.ReaderAt(2) != mb.ReaderAt(3)); + MOZ_RELEASE_ASSERT(mb.ReaderAt(2) < mb.ReaderAt(3)); + MOZ_RELEASE_ASSERT(mb.ReaderAt(2) <= mb.ReaderAt(2)); + MOZ_RELEASE_ASSERT(mb.ReaderAt(2) <= mb.ReaderAt(3)); + MOZ_RELEASE_ASSERT(mb.ReaderAt(3) > mb.ReaderAt(2)); + MOZ_RELEASE_ASSERT(mb.ReaderAt(2) >= mb.ReaderAt(2)); + MOZ_RELEASE_ASSERT(mb.ReaderAt(3) >= mb.ReaderAt(2)); + + // Iterators indices don't wrap around (even though they may be pointing at + // the same location). + MOZ_RELEASE_ASSERT(mb.ReaderAt(2) != mb.ReaderAt(MBSize + 2)); + MOZ_RELEASE_ASSERT(mb.ReaderAt(MBSize + 2) != mb.ReaderAt(2)); + + // Dereference. + static_assert(std::is_same<decltype(*mb.ReaderAt(0)), const MB::Byte&>::value, + "Dereferencing from a reader should return const Byte*"); + static_assert(std::is_same<decltype(*mb.WriterAt(0)), MB::Byte&>::value, + "Dereferencing from a writer should return Byte*"); + // Contiguous between 0 and MBSize-1. + MOZ_RELEASE_ASSERT(&*mb.ReaderAt(MBSize - 1) == + &*mb.ReaderAt(0) + (MBSize - 1)); + // Wraps around. + MOZ_RELEASE_ASSERT(&*mb.ReaderAt(MBSize) == &*mb.ReaderAt(0)); + MOZ_RELEASE_ASSERT(&*mb.ReaderAt(MBSize + MBSize - 1) == + &*mb.ReaderAt(MBSize - 1)); + MOZ_RELEASE_ASSERT(&*mb.ReaderAt(MBSize + MBSize) == &*mb.ReaderAt(0)); + // Power of 2 modulo wrapping. + MOZ_RELEASE_ASSERT(&*mb.ReaderAt(uint32_t(-1)) == &*mb.ReaderAt(MBSize - 1)); + MOZ_RELEASE_ASSERT(&*mb.ReaderAt(static_cast<MB::Index>(-1)) == + &*mb.ReaderAt(MBSize - 1)); + + // Arithmetic. + MB::Reader arit = mb.ReaderAt(0); + MOZ_RELEASE_ASSERT(++arit == mb.ReaderAt(1)); + MOZ_RELEASE_ASSERT(arit == mb.ReaderAt(1)); + + MOZ_RELEASE_ASSERT(--arit == mb.ReaderAt(0)); + MOZ_RELEASE_ASSERT(arit == mb.ReaderAt(0)); + + MOZ_RELEASE_ASSERT(arit++ == mb.ReaderAt(0)); + MOZ_RELEASE_ASSERT(arit == mb.ReaderAt(1)); + + MOZ_RELEASE_ASSERT(arit-- == mb.ReaderAt(1)); + MOZ_RELEASE_ASSERT(arit == mb.ReaderAt(0)); + + MOZ_RELEASE_ASSERT(arit + 3 == mb.ReaderAt(3)); + MOZ_RELEASE_ASSERT(arit == mb.ReaderAt(0)); + + MOZ_RELEASE_ASSERT(4 + arit == mb.ReaderAt(4)); + MOZ_RELEASE_ASSERT(arit == mb.ReaderAt(0)); + + // (Can't have assignments inside asserts, hence the split.) + const bool checkPlusEq = ((arit += 3) == mb.ReaderAt(3)); + MOZ_RELEASE_ASSERT(checkPlusEq); + MOZ_RELEASE_ASSERT(arit == mb.ReaderAt(3)); + + MOZ_RELEASE_ASSERT((arit - 2) == mb.ReaderAt(1)); + MOZ_RELEASE_ASSERT(arit == mb.ReaderAt(3)); + + const bool checkMinusEq = ((arit -= 2) == mb.ReaderAt(1)); + MOZ_RELEASE_ASSERT(checkMinusEq); + MOZ_RELEASE_ASSERT(arit == mb.ReaderAt(1)); + + // Random access. + MOZ_RELEASE_ASSERT(&arit[3] == &*(arit + 3)); + MOZ_RELEASE_ASSERT(arit == mb.ReaderAt(1)); + + // Iterator difference. + MOZ_RELEASE_ASSERT(mb.ReaderAt(3) - mb.ReaderAt(1) == 2); + MOZ_RELEASE_ASSERT(mb.ReaderAt(1) - mb.ReaderAt(3) == MB::Index(-2)); + + // Only testing Writer, as Reader is just a subset with no code differences. + MB::Writer it = mb.WriterAt(0); + MOZ_RELEASE_ASSERT(it.CurrentIndex() == 0); + + // Write two characters at the start. + it.WriteObject('x'); + it.WriteObject('y'); + + // Backtrack to read them. + it -= 2; + // PeekObject should read without moving. + MOZ_RELEASE_ASSERT(it.PeekObject<char>() == 'x'); + MOZ_RELEASE_ASSERT(it.CurrentIndex() == 0); + // ReadObject should read and move past the character. + MOZ_RELEASE_ASSERT(it.ReadObject<char>() == 'x'); + MOZ_RELEASE_ASSERT(it.CurrentIndex() == 1); + MOZ_RELEASE_ASSERT(it.PeekObject<char>() == 'y'); + MOZ_RELEASE_ASSERT(it.CurrentIndex() == 1); + MOZ_RELEASE_ASSERT(it.ReadObject<char>() == 'y'); + MOZ_RELEASE_ASSERT(it.CurrentIndex() == 2); + + // Checking that a reader can be created from a writer. + MB::Reader it2(it); + MOZ_RELEASE_ASSERT(it2.CurrentIndex() == 2); + // Or assigned. + it2 = it; + MOZ_RELEASE_ASSERT(it2.CurrentIndex() == 2); + + // Iterator traits. + static_assert(std::is_same<std::iterator_traits<MB::Reader>::difference_type, + MB::Index>::value, + "ModuloBuffer::Reader::difference_type should be Index"); + static_assert(std::is_same<std::iterator_traits<MB::Reader>::value_type, + MB::Byte>::value, + "ModuloBuffer::Reader::value_type should be Byte"); + static_assert(std::is_same<std::iterator_traits<MB::Reader>::pointer, + const MB::Byte*>::value, + "ModuloBuffer::Reader::pointer should be const Byte*"); + static_assert(std::is_same<std::iterator_traits<MB::Reader>::reference, + const MB::Byte&>::value, + "ModuloBuffer::Reader::reference should be const Byte&"); + static_assert(std::is_base_of< + std::input_iterator_tag, + std::iterator_traits<MB::Reader>::iterator_category>::value, + "ModuloBuffer::Reader::iterator_category should be derived " + "from input_iterator_tag"); + static_assert(std::is_base_of< + std::forward_iterator_tag, + std::iterator_traits<MB::Reader>::iterator_category>::value, + "ModuloBuffer::Reader::iterator_category should be derived " + "from forward_iterator_tag"); + static_assert(std::is_base_of< + std::bidirectional_iterator_tag, + std::iterator_traits<MB::Reader>::iterator_category>::value, + "ModuloBuffer::Reader::iterator_category should be derived " + "from bidirectional_iterator_tag"); + static_assert( + std::is_same<std::iterator_traits<MB::Reader>::iterator_category, + std::random_access_iterator_tag>::value, + "ModuloBuffer::Reader::iterator_category should be " + "random_access_iterator_tag"); + + // Use as input iterator by std::string constructor (which is only considered + // with proper input iterators.) + std::string s(mb.ReaderAt(0), mb.ReaderAt(2)); + MOZ_RELEASE_ASSERT(s == "xy"); + + // Write 4-byte number at index 2. + it.WriteObject(int32_t(123)); + MOZ_RELEASE_ASSERT(it.CurrentIndex() == 6); + // And another, which should now wrap around (but index continues on.) + it.WriteObject(int32_t(456)); + MOZ_RELEASE_ASSERT(it.CurrentIndex() == MBSize + 2); + // Even though index==MBSize+2, we can read the object we wrote at 2. + MOZ_RELEASE_ASSERT(it.ReadObject<int32_t>() == 123); + MOZ_RELEASE_ASSERT(it.CurrentIndex() == MBSize + 6); + // And similarly, index MBSize+6 points at the same location as index 6. + MOZ_RELEASE_ASSERT(it.ReadObject<int32_t>() == 456); + MOZ_RELEASE_ASSERT(it.CurrentIndex() == MBSize + MBSize + 2); +} + +void TestModuloBuffer() { + printf("TestModuloBuffer...\n"); + + // Testing ModuloBuffer with default template arguments. + using MB = ModuloBuffer<>; + + // Only 8-byte buffers, to easily test wrap-around. + constexpr uint32_t MBSize = 8; + + // MB with self-allocated heap buffer. + MB mbByLength(MakePowerOfTwo32<MBSize>()); + TestModuloBuffer(mbByLength, MBSize); + + // MB taking ownership of a provided UniquePtr to a buffer. + auto uniqueBuffer = MakeUnique<uint8_t[]>(MBSize); + MB mbByUniquePtr(MakeUnique<uint8_t[]>(MBSize), MakePowerOfTwo32<MBSize>()); + TestModuloBuffer(mbByUniquePtr, MBSize); + + // MB using part of a buffer on the stack. The buffer is three times the + // required size: The middle third is where ModuloBuffer will work, the first + // and last thirds are only used to later verify that ModuloBuffer didn't go + // out of its bounds. + uint8_t buffer[MBSize * 3]; + // Pre-fill the buffer with a known pattern, so we can later see what changed. + for (size_t i = 0; i < MBSize * 3; ++i) { + buffer[i] = uint8_t('A' + i); + } + MB mbByBuffer(&buffer[MBSize], MakePowerOfTwo32<MBSize>()); + TestModuloBuffer(mbByBuffer, MBSize); + + // Check that only the provided stack-based sub-buffer was modified. + uint32_t changed = 0; + for (size_t i = MBSize; i < MBSize * 2; ++i) { + changed += (buffer[i] == uint8_t('A' + i)) ? 0 : 1; + } + // Expect at least 75% changes. + MOZ_RELEASE_ASSERT(changed >= MBSize * 6 / 8); + + // Everything around the sub-buffer should be unchanged. + for (size_t i = 0; i < MBSize; ++i) { + MOZ_RELEASE_ASSERT(buffer[i] == uint8_t('A' + i)); + } + for (size_t i = MBSize * 2; i < MBSize * 3; ++i) { + MOZ_RELEASE_ASSERT(buffer[i] == uint8_t('A' + i)); + } + + // Check that move-construction is allowed. This verifies that we do not + // crash from a double free, when `mbByBuffer` and `mbByStolenBuffer` are both + // destroyed at the end of this function. + MB mbByStolenBuffer = std::move(mbByBuffer); + TestModuloBuffer(mbByStolenBuffer, MBSize); + + // Check that only the provided stack-based sub-buffer was modified. + changed = 0; + for (size_t i = MBSize; i < MBSize * 2; ++i) { + changed += (buffer[i] == uint8_t('A' + i)) ? 0 : 1; + } + // Expect at least 75% changes. + MOZ_RELEASE_ASSERT(changed >= MBSize * 6 / 8); + + // Everything around the sub-buffer should be unchanged. + for (size_t i = 0; i < MBSize; ++i) { + MOZ_RELEASE_ASSERT(buffer[i] == uint8_t('A' + i)); + } + for (size_t i = MBSize * 2; i < MBSize * 3; ++i) { + MOZ_RELEASE_ASSERT(buffer[i] == uint8_t('A' + i)); + } + + // This test function does a `ReadInto` as directed, and checks that the + // result is the same as if the copy had been done manually byte-by-byte. + // `TestReadInto(3, 7, 2)` copies from index 3 to index 7, 2 bytes long. + // Return the output string (from `ReadInto`) for external checks. + auto TestReadInto = [](MB::Index aReadFrom, MB::Index aWriteTo, + MB::Length aBytes) { + constexpr uint32_t TRISize = 16; + + // Prepare an input buffer, all different elements. + uint8_t input[TRISize + 1] = "ABCDEFGHIJKLMNOP"; + const MB mbInput(input, MakePowerOfTwo32<TRISize>()); + + // Prepare an output buffer, different from input. + uint8_t output[TRISize + 1] = "abcdefghijklmnop"; + MB mbOutput(output, MakePowerOfTwo32<TRISize>()); + + // Run ReadInto. + auto writer = mbOutput.WriterAt(aWriteTo); + mbInput.ReaderAt(aReadFrom).ReadInto(writer, aBytes); + + // Do the same operation manually. + uint8_t outputCheck[TRISize + 1] = "abcdefghijklmnop"; + MB mbOutputCheck(outputCheck, MakePowerOfTwo32<TRISize>()); + auto readerCheck = mbInput.ReaderAt(aReadFrom); + auto writerCheck = mbOutputCheck.WriterAt(aWriteTo); + for (MB::Length i = 0; i < aBytes; ++i) { + *writerCheck++ = *readerCheck++; + } + + // Compare the two outputs. + for (uint32_t i = 0; i < TRISize; ++i) { +# ifdef TEST_MODULOBUFFER_FAILURE_DEBUG + // Only used when debugging failures. + if (output[i] != outputCheck[i]) { + printf( + "*** from=%u to=%u bytes=%u i=%u\ninput: '%s'\noutput: " + "'%s'\ncheck: '%s'\n", + unsigned(aReadFrom), unsigned(aWriteTo), unsigned(aBytes), + unsigned(i), input, output, outputCheck); + } +# endif + MOZ_RELEASE_ASSERT(output[i] == outputCheck[i]); + } + +# ifdef TEST_MODULOBUFFER_HELPER + // Only used when adding more tests. + printf("*** from=%u to=%u bytes=%u output: %s\n", unsigned(aReadFrom), + unsigned(aWriteTo), unsigned(aBytes), output); +# endif + + return std::string(reinterpret_cast<const char*>(output)); + }; + + // A few manual checks: + constexpr uint32_t TRISize = 16; + MOZ_RELEASE_ASSERT(TestReadInto(0, 0, 0) == "abcdefghijklmnop"); + MOZ_RELEASE_ASSERT(TestReadInto(0, 0, TRISize) == "ABCDEFGHIJKLMNOP"); + MOZ_RELEASE_ASSERT(TestReadInto(0, 5, TRISize) == "LMNOPABCDEFGHIJK"); + MOZ_RELEASE_ASSERT(TestReadInto(5, 0, TRISize) == "FGHIJKLMNOPABCDE"); + + // Test everything! (16^3 = 4096, not too much.) + for (MB::Index r = 0; r < TRISize; ++r) { + for (MB::Index w = 0; w < TRISize; ++w) { + for (MB::Length len = 0; len < TRISize; ++len) { + TestReadInto(r, w, len); + } + } + } + + printf("TestModuloBuffer done\n"); +} + +void TestBlocksRingBufferAPI() { + printf("TestBlocksRingBufferAPI...\n"); + + // Create a 16-byte buffer, enough to store up to 3 entries (1 byte size + 4 + // bytes uint64_t). + constexpr uint32_t MBSize = 16; + uint8_t buffer[MBSize * 3]; + for (size_t i = 0; i < MBSize * 3; ++i) { + buffer[i] = uint8_t('A' + i); + } + + // Start a temporary block to constrain buffer lifetime. + { + BlocksRingBuffer rb(BlocksRingBuffer::ThreadSafety::WithMutex, + &buffer[MBSize], MakePowerOfTwo32<MBSize>()); + +# define VERIFY_START_END_PUSHED_CLEARED(aStart, aEnd, aPushed, aCleared) \ + { \ + BlocksRingBuffer::State state = rb.GetState(); \ + MOZ_RELEASE_ASSERT(state.mRangeStart.ConvertToProfileBufferIndex() == \ + (aStart)); \ + MOZ_RELEASE_ASSERT(state.mRangeEnd.ConvertToProfileBufferIndex() == \ + (aEnd)); \ + MOZ_RELEASE_ASSERT(state.mPushedBlockCount == (aPushed)); \ + MOZ_RELEASE_ASSERT(state.mClearedBlockCount == (aCleared)); \ + } + + // All entries will contain one 32-bit number. The resulting blocks will + // have the following structure: + // - 1 byte for the LEB128 size of 4 + // - 4 bytes for the number. + // E.g., if we have entries with `123` and `456`: + // .-- Index 0 reserved for empty ProfileBufferBlockIndex, nothing there. + // | .-- first readable block at index 1 + // | |.-- first block at index 1 + // | ||.-- 1 byte for the entry size, which is `4` (32 bits) + // | ||| .-- entry starts at index 2, contains 32-bit int + // | ||| | .-- entry and block finish *after* index 5 (so 6) + // | ||| | | .-- second block starts at index 6 + // | ||| | | | etc. + // | ||| | | | .-- End readable blocks: 11 + // v vvv v v V v + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + // - S[4 | int(123) ] [4 | int(456) ]E + + // Empty buffer to start with. + // Start&end indices still at 1 (0 is reserved for the default + // ProfileBufferBlockIndex{} that cannot point at a valid entry), nothing + // cleared. + VERIFY_START_END_PUSHED_CLEARED(1, 1, 0, 0); + + // Default ProfileBufferBlockIndex. + ProfileBufferBlockIndex bi0; + if (bi0) { + MOZ_RELEASE_ASSERT(false, + "if (ProfileBufferBlockIndex{}) should fail test"); + } + if (!bi0) { + } else { + MOZ_RELEASE_ASSERT(false, + "if (!ProfileBufferBlockIndex{}) should succeed test"); + } + MOZ_RELEASE_ASSERT(!bi0); + MOZ_RELEASE_ASSERT(bi0 == bi0); + MOZ_RELEASE_ASSERT(bi0 <= bi0); + MOZ_RELEASE_ASSERT(bi0 >= bi0); + MOZ_RELEASE_ASSERT(!(bi0 != bi0)); + MOZ_RELEASE_ASSERT(!(bi0 < bi0)); + MOZ_RELEASE_ASSERT(!(bi0 > bi0)); + + // Default ProfileBufferBlockIndex can be used, but returns no valid entry. + rb.ReadAt(bi0, [](Maybe<ProfileBufferEntryReader>&& aMaybeReader) { + MOZ_RELEASE_ASSERT(aMaybeReader.isNothing()); + }); + + // Push `1` directly. + MOZ_RELEASE_ASSERT( + rb.PutObject(uint32_t(1)).ConvertToProfileBufferIndex() == 1); + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + // - S[4 | int(1) ]E + VERIFY_START_END_PUSHED_CLEARED(1, 6, 1, 0); + + // Push `2` through ReserveAndPut, check output ProfileBufferBlockIndex. + auto bi2 = rb.ReserveAndPut([]() { return sizeof(uint32_t); }, + [](Maybe<ProfileBufferEntryWriter>& aEW) { + MOZ_RELEASE_ASSERT(aEW.isSome()); + aEW->WriteObject(uint32_t(2)); + return aEW->CurrentBlockIndex(); + }); + static_assert(std::is_same<decltype(bi2), ProfileBufferBlockIndex>::value, + "All index-returning functions should return a " + "ProfileBufferBlockIndex"); + MOZ_RELEASE_ASSERT(bi2.ConvertToProfileBufferIndex() == 6); + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + // - S[4 | int(1) ] [4 | int(2) ]E + VERIFY_START_END_PUSHED_CLEARED(1, 11, 2, 0); + + // Check single entry at bi2, store next block index. + auto i2Next = + rb.ReadAt(bi2, [bi2](Maybe<ProfileBufferEntryReader>&& aMaybeReader) { + MOZ_RELEASE_ASSERT(aMaybeReader.isSome()); + MOZ_RELEASE_ASSERT(aMaybeReader->CurrentBlockIndex() == bi2); + MOZ_RELEASE_ASSERT(aMaybeReader->NextBlockIndex() == nullptr); + size_t entrySize = aMaybeReader->RemainingBytes(); + MOZ_RELEASE_ASSERT(aMaybeReader->ReadObject<uint32_t>() == 2); + // The next block index is after this block, which is made of the + // entry size (coded as ULEB128) followed by the entry itself. + return bi2.ConvertToProfileBufferIndex() + ULEB128Size(entrySize) + + entrySize; + }); + auto bi2Next = rb.GetState().mRangeEnd; + MOZ_RELEASE_ASSERT(bi2Next.ConvertToProfileBufferIndex() == i2Next); + // bi2Next is at the end, nothing to read. + rb.ReadAt(bi2Next, [](Maybe<ProfileBufferEntryReader>&& aMaybeReader) { + MOZ_RELEASE_ASSERT(aMaybeReader.isNothing()); + }); + + // ProfileBufferBlockIndex tests. + if (bi2) { + } else { + MOZ_RELEASE_ASSERT( + false, + "if (non-default-ProfileBufferBlockIndex) should succeed test"); + } + if (!bi2) { + MOZ_RELEASE_ASSERT( + false, "if (!non-default-ProfileBufferBlockIndex) should fail test"); + } + + MOZ_RELEASE_ASSERT(!!bi2); + MOZ_RELEASE_ASSERT(bi2 == bi2); + MOZ_RELEASE_ASSERT(bi2 <= bi2); + MOZ_RELEASE_ASSERT(bi2 >= bi2); + MOZ_RELEASE_ASSERT(!(bi2 != bi2)); + MOZ_RELEASE_ASSERT(!(bi2 < bi2)); + MOZ_RELEASE_ASSERT(!(bi2 > bi2)); + + MOZ_RELEASE_ASSERT(bi0 != bi2); + MOZ_RELEASE_ASSERT(bi0 < bi2); + MOZ_RELEASE_ASSERT(bi0 <= bi2); + MOZ_RELEASE_ASSERT(!(bi0 == bi2)); + MOZ_RELEASE_ASSERT(!(bi0 > bi2)); + MOZ_RELEASE_ASSERT(!(bi0 >= bi2)); + + MOZ_RELEASE_ASSERT(bi2 != bi0); + MOZ_RELEASE_ASSERT(bi2 > bi0); + MOZ_RELEASE_ASSERT(bi2 >= bi0); + MOZ_RELEASE_ASSERT(!(bi2 == bi0)); + MOZ_RELEASE_ASSERT(!(bi2 < bi0)); + MOZ_RELEASE_ASSERT(!(bi2 <= bi0)); + + MOZ_RELEASE_ASSERT(bi2 != bi2Next); + MOZ_RELEASE_ASSERT(bi2 < bi2Next); + MOZ_RELEASE_ASSERT(bi2 <= bi2Next); + MOZ_RELEASE_ASSERT(!(bi2 == bi2Next)); + MOZ_RELEASE_ASSERT(!(bi2 > bi2Next)); + MOZ_RELEASE_ASSERT(!(bi2 >= bi2Next)); + + MOZ_RELEASE_ASSERT(bi2Next != bi2); + MOZ_RELEASE_ASSERT(bi2Next > bi2); + MOZ_RELEASE_ASSERT(bi2Next >= bi2); + MOZ_RELEASE_ASSERT(!(bi2Next == bi2)); + MOZ_RELEASE_ASSERT(!(bi2Next < bi2)); + MOZ_RELEASE_ASSERT(!(bi2Next <= bi2)); + + // Push `3` through Put, check writer output + // is returned to the initial caller. + auto put3 = + rb.Put(sizeof(uint32_t), [&](Maybe<ProfileBufferEntryWriter>& aEW) { + MOZ_RELEASE_ASSERT(aEW.isSome()); + aEW->WriteObject(uint32_t(3)); + MOZ_RELEASE_ASSERT(aEW->CurrentBlockIndex() == bi2Next); + return float(aEW->CurrentBlockIndex().ConvertToProfileBufferIndex()); + }); + static_assert(std::is_same<decltype(put3), float>::value, + "Expect float as returned by callback."); + MOZ_RELEASE_ASSERT(put3 == 11.0); + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 (16) + // - S[4 | int(1) ] [4 | int(2) ] [4 | int(3) ]E + VERIFY_START_END_PUSHED_CLEARED(1, 16, 3, 0); + + // Re-Read single entry at bi2, it should now have a next entry. + rb.ReadAt(bi2, [&](Maybe<ProfileBufferEntryReader>&& aMaybeReader) { + MOZ_RELEASE_ASSERT(aMaybeReader.isSome()); + MOZ_RELEASE_ASSERT(aMaybeReader->CurrentBlockIndex() == bi2); + MOZ_RELEASE_ASSERT(aMaybeReader->ReadObject<uint32_t>() == 2); + MOZ_RELEASE_ASSERT(aMaybeReader->NextBlockIndex() == bi2Next); + }); + + // Check that we have `1` to `3`. + uint32_t count = 0; + rb.ReadEach([&](ProfileBufferEntryReader& aReader) { + MOZ_RELEASE_ASSERT(aReader.ReadObject<uint32_t>() == ++count); + }); + MOZ_RELEASE_ASSERT(count == 3); + + // Push `4`, store its ProfileBufferBlockIndex for later. + // This will wrap around, and clear the first entry. + ProfileBufferBlockIndex bi4 = rb.PutObject(uint32_t(4)); + // Before: + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 (16) + // - S[4 | int(1) ] [4 | int(2) ] [4 | int(3) ]E + // 1. First entry cleared: + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 (16) + // - ? ? ? ? ? S[4 | int(2) ] [4 | int(3) ]E + // 2. New entry starts at 15 and wraps around: (shown on separate line) + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 (16) + // - ? ? ? ? ? S[4 | int(2) ] [4 | int(3) ] + // 16 17 18 19 20 21 ... + // [4 | int(4) ]E + // (collapsed) + // 16 17 18 19 20 21 6 7 8 9 10 11 12 13 14 15 (16) + // [4 | int(4) ]E ? S[4 | int(2) ] [4 | int(3) ] + VERIFY_START_END_PUSHED_CLEARED(6, 21, 4, 1); + + // Check that we have `2` to `4`. + count = 1; + rb.ReadEach([&](ProfileBufferEntryReader& aReader) { + MOZ_RELEASE_ASSERT(aReader.ReadObject<uint32_t>() == ++count); + }); + MOZ_RELEASE_ASSERT(count == 4); + + // Push 5 through Put, no returns. + // This will clear the second entry. + // Check that the EntryWriter can access bi4 but not bi2. + auto bi5 = + rb.Put(sizeof(uint32_t), [&](Maybe<ProfileBufferEntryWriter>& aEW) { + MOZ_RELEASE_ASSERT(aEW.isSome()); + aEW->WriteObject(uint32_t(5)); + return aEW->CurrentBlockIndex(); + }); + auto bi6 = rb.GetState().mRangeEnd; + // 16 17 18 19 20 21 22 23 24 25 26 11 12 13 14 15 (16) + // [4 | int(4) ] [4 | int(5) ]E ? S[4 | int(3) ] + VERIFY_START_END_PUSHED_CLEARED(11, 26, 5, 2); + + // Read single entry at bi2, should now gracefully fail. + rb.ReadAt(bi2, [](Maybe<ProfileBufferEntryReader>&& aMaybeReader) { + MOZ_RELEASE_ASSERT(aMaybeReader.isNothing()); + }); + + // Read single entry at bi5. + rb.ReadAt(bi5, [](Maybe<ProfileBufferEntryReader>&& aMaybeReader) { + MOZ_RELEASE_ASSERT(aMaybeReader.isSome()); + MOZ_RELEASE_ASSERT(aMaybeReader->ReadObject<uint32_t>() == 5); + }); + + rb.Read([&](BlocksRingBuffer::Reader* aReader) { + MOZ_RELEASE_ASSERT(!!aReader); + // begin() and end() should be at the range edges (verified above). + MOZ_RELEASE_ASSERT( + aReader->begin().CurrentBlockIndex().ConvertToProfileBufferIndex() == + 11); + MOZ_RELEASE_ASSERT( + aReader->end().CurrentBlockIndex().ConvertToProfileBufferIndex() == + 26); + // Null ProfileBufferBlockIndex clamped to the beginning. + MOZ_RELEASE_ASSERT(aReader->At(bi0) == aReader->begin()); + // Cleared block index clamped to the beginning. + MOZ_RELEASE_ASSERT(aReader->At(bi2) == aReader->begin()); + // At(begin) same as begin(). + MOZ_RELEASE_ASSERT(aReader->At(aReader->begin().CurrentBlockIndex()) == + aReader->begin()); + // bi5 at expected position. + MOZ_RELEASE_ASSERT( + aReader->At(bi5).CurrentBlockIndex().ConvertToProfileBufferIndex() == + 21); + // bi6 at expected position at the end. + MOZ_RELEASE_ASSERT(aReader->At(bi6) == aReader->end()); + // At(end) same as end(). + MOZ_RELEASE_ASSERT(aReader->At(aReader->end().CurrentBlockIndex()) == + aReader->end()); + }); + + // Check that we have `3` to `5`. + count = 2; + rb.ReadEach([&](ProfileBufferEntryReader& aReader) { + MOZ_RELEASE_ASSERT(aReader.ReadObject<uint32_t>() == ++count); + }); + MOZ_RELEASE_ASSERT(count == 5); + + // Clear everything before `4`, this should clear `3`. + rb.ClearBefore(bi4); + // 16 17 18 19 20 21 22 23 24 25 26 11 12 13 14 15 + // S[4 | int(4) ] [4 | int(5) ]E ? ? ? ? ? ? + VERIFY_START_END_PUSHED_CLEARED(16, 26, 5, 3); + + // Check that we have `4` to `5`. + count = 3; + rb.ReadEach([&](ProfileBufferEntryReader& aReader) { + MOZ_RELEASE_ASSERT(aReader.ReadObject<uint32_t>() == ++count); + }); + MOZ_RELEASE_ASSERT(count == 5); + + // Clear everything before `4` again, nothing to clear. + rb.ClearBefore(bi4); + VERIFY_START_END_PUSHED_CLEARED(16, 26, 5, 3); + + // Clear everything, this should clear `4` and `5`, and bring the start + // index where the end index currently is. + rb.ClearBefore(bi6); + // 16 17 18 19 20 21 22 23 24 25 26 11 12 13 14 15 + // ? ? ? ? ? ? ? ? ? ? SE? ? ? ? ? ? + VERIFY_START_END_PUSHED_CLEARED(26, 26, 5, 5); + + // Check that we have nothing to read. + rb.ReadEach([&](auto&&) { MOZ_RELEASE_ASSERT(false); }); + + // Read single entry at bi5, should now gracefully fail. + rb.ReadAt(bi5, [](Maybe<ProfileBufferEntryReader>&& aMaybeReader) { + MOZ_RELEASE_ASSERT(aMaybeReader.isNothing()); + }); + + // Clear everything before now-cleared `4`, nothing to clear. + rb.ClearBefore(bi4); + VERIFY_START_END_PUSHED_CLEARED(26, 26, 5, 5); + + // Push `6` directly. + MOZ_RELEASE_ASSERT(rb.PutObject(uint32_t(6)) == bi6); + // 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 + // ? ? ? ? ? ? ? ? ? ? S[4 | int(6) ]E ? + VERIFY_START_END_PUSHED_CLEARED(26, 31, 6, 5); + + { + // Create a 2nd buffer and fill it with `7` and `8`. + uint8_t buffer2[MBSize]; + BlocksRingBuffer rb2(BlocksRingBuffer::ThreadSafety::WithoutMutex, + buffer2, MakePowerOfTwo32<MBSize>()); + rb2.PutObject(uint32_t(7)); + rb2.PutObject(uint32_t(8)); + // Main buffer shouldn't have changed. + VERIFY_START_END_PUSHED_CLEARED(26, 31, 6, 5); + + // Append contents of rb2 to rb, this should end up being the same as + // pushing the two numbers. + rb.AppendContents(rb2); + // 32 33 34 35 36 37 38 39 40 41 26 27 28 29 30 31 + // int(7) ] [4 | int(8) ]E ? S[4 | int(6) ] [4 | + VERIFY_START_END_PUSHED_CLEARED(26, 41, 8, 5); + + // Append contents of rb2 to rb again, to verify that rb2 was not modified + // above. This should clear `6` and the first `7`. + rb.AppendContents(rb2); + // 48 49 50 51 36 37 38 39 40 41 42 43 44 45 46 47 + // int(8) ]E ? S[4 | int(8) ] [4 | int(7) ] [4 | + VERIFY_START_END_PUSHED_CLEARED(36, 51, 10, 7); + + // End of block where rb2 lives, to verify that it is not needed anymore + // for its copied values to survive in rb. + } + VERIFY_START_END_PUSHED_CLEARED(36, 51, 10, 7); + + // bi6 should now have been cleared. + rb.ReadAt(bi6, [](Maybe<ProfileBufferEntryReader>&& aMaybeReader) { + MOZ_RELEASE_ASSERT(aMaybeReader.isNothing()); + }); + + // Check that we have `8`, `7`, `8`. + count = 0; + uint32_t expected[3] = {8, 7, 8}; + rb.ReadEach([&](ProfileBufferEntryReader& aReader) { + MOZ_RELEASE_ASSERT(count < 3); + MOZ_RELEASE_ASSERT(aReader.ReadObject<uint32_t>() == expected[count++]); + }); + MOZ_RELEASE_ASSERT(count == 3); + + // End of block where rb lives, BlocksRingBuffer destructor should call + // entry destructor for remaining entries. + } + + // Check that only the provided stack-based sub-buffer was modified. + uint32_t changed = 0; + for (size_t i = MBSize; i < MBSize * 2; ++i) { + changed += (buffer[i] == uint8_t('A' + i)) ? 0 : 1; + } + // Expect at least 75% changes. + MOZ_RELEASE_ASSERT(changed >= MBSize * 6 / 8); + + // Everything around the sub-buffer should be unchanged. + for (size_t i = 0; i < MBSize; ++i) { + MOZ_RELEASE_ASSERT(buffer[i] == uint8_t('A' + i)); + } + for (size_t i = MBSize * 2; i < MBSize * 3; ++i) { + MOZ_RELEASE_ASSERT(buffer[i] == uint8_t('A' + i)); + } + + printf("TestBlocksRingBufferAPI done\n"); +} + +void TestBlocksRingBufferUnderlyingBufferChanges() { + printf("TestBlocksRingBufferUnderlyingBufferChanges...\n"); + + // Out-of-session BlocksRingBuffer to start with. + BlocksRingBuffer rb(BlocksRingBuffer::ThreadSafety::WithMutex); + + // Block index to read at. Initially "null", but may be changed below. + ProfileBufferBlockIndex bi; + + // Test all rb APIs when rb is out-of-session and therefore doesn't have an + // underlying buffer. + auto testOutOfSession = [&]() { + MOZ_RELEASE_ASSERT(rb.BufferLength().isNothing()); + BlocksRingBuffer::State state = rb.GetState(); + // When out-of-session, range start and ends are the same, and there are no + // pushed&cleared blocks. + MOZ_RELEASE_ASSERT(state.mRangeStart == state.mRangeEnd); + MOZ_RELEASE_ASSERT(state.mPushedBlockCount == 0); + MOZ_RELEASE_ASSERT(state.mClearedBlockCount == 0); + // `Put()` functions run the callback with `Nothing`. + int32_t ran = 0; + rb.Put(1, [&](Maybe<ProfileBufferEntryWriter>& aMaybeEntryWriter) { + MOZ_RELEASE_ASSERT(aMaybeEntryWriter.isNothing()); + ++ran; + }); + MOZ_RELEASE_ASSERT(ran == 1); + // `PutFrom` won't do anything, and returns the null + // ProfileBufferBlockIndex. + MOZ_RELEASE_ASSERT(rb.PutFrom(&ran, sizeof(ran)) == + ProfileBufferBlockIndex{}); + MOZ_RELEASE_ASSERT(rb.PutObject(ran) == ProfileBufferBlockIndex{}); + // `Read()` functions run the callback with `Nothing`. + ran = 0; + rb.Read([&](BlocksRingBuffer::Reader* aReader) { + MOZ_RELEASE_ASSERT(!aReader); + ++ran; + }); + MOZ_RELEASE_ASSERT(ran == 1); + ran = 0; + rb.ReadAt(ProfileBufferBlockIndex{}, + [&](Maybe<ProfileBufferEntryReader>&& aMaybeEntryReader) { + MOZ_RELEASE_ASSERT(aMaybeEntryReader.isNothing()); + ++ran; + }); + MOZ_RELEASE_ASSERT(ran == 1); + ran = 0; + rb.ReadAt(bi, [&](Maybe<ProfileBufferEntryReader>&& aMaybeEntryReader) { + MOZ_RELEASE_ASSERT(aMaybeEntryReader.isNothing()); + ++ran; + }); + MOZ_RELEASE_ASSERT(ran == 1); + // `ReadEach` shouldn't run the callback (nothing to read). + rb.ReadEach([](auto&&) { MOZ_RELEASE_ASSERT(false); }); + }; + + // As `testOutOfSession()` attempts to modify the buffer, we run it twice to + // make sure one run doesn't influence the next one. + testOutOfSession(); + testOutOfSession(); + + rb.ClearBefore(bi); + testOutOfSession(); + testOutOfSession(); + + rb.Clear(); + testOutOfSession(); + testOutOfSession(); + + rb.Reset(); + testOutOfSession(); + testOutOfSession(); + + constexpr uint32_t MBSize = 32; + + rb.Set(MakePowerOfTwo<BlocksRingBuffer::Length, MBSize>()); + + constexpr bool EMPTY = true; + constexpr bool NOT_EMPTY = false; + // Test all rb APIs when rb has an underlying buffer. + auto testInSession = [&](bool aExpectEmpty) { + MOZ_RELEASE_ASSERT(rb.BufferLength().isSome()); + BlocksRingBuffer::State state = rb.GetState(); + if (aExpectEmpty) { + MOZ_RELEASE_ASSERT(state.mRangeStart == state.mRangeEnd); + MOZ_RELEASE_ASSERT(state.mPushedBlockCount == 0); + MOZ_RELEASE_ASSERT(state.mClearedBlockCount == 0); + } else { + MOZ_RELEASE_ASSERT(state.mRangeStart < state.mRangeEnd); + MOZ_RELEASE_ASSERT(state.mPushedBlockCount > 0); + MOZ_RELEASE_ASSERT(state.mClearedBlockCount <= state.mPushedBlockCount); + } + int32_t ran = 0; + // The following three `Put...` will write three int32_t of value 1. + bi = rb.Put(sizeof(ran), + [&](Maybe<ProfileBufferEntryWriter>& aMaybeEntryWriter) { + MOZ_RELEASE_ASSERT(aMaybeEntryWriter.isSome()); + ++ran; + aMaybeEntryWriter->WriteObject(ran); + return aMaybeEntryWriter->CurrentBlockIndex(); + }); + MOZ_RELEASE_ASSERT(ran == 1); + MOZ_RELEASE_ASSERT(rb.PutFrom(&ran, sizeof(ran)) != + ProfileBufferBlockIndex{}); + MOZ_RELEASE_ASSERT(rb.PutObject(ran) != ProfileBufferBlockIndex{}); + ran = 0; + rb.Read([&](BlocksRingBuffer::Reader* aReader) { + MOZ_RELEASE_ASSERT(!!aReader); + ++ran; + }); + MOZ_RELEASE_ASSERT(ran == 1); + ran = 0; + rb.ReadEach([&](ProfileBufferEntryReader& aEntryReader) { + MOZ_RELEASE_ASSERT(aEntryReader.RemainingBytes() == sizeof(ran)); + MOZ_RELEASE_ASSERT(aEntryReader.ReadObject<decltype(ran)>() == 1); + ++ran; + }); + MOZ_RELEASE_ASSERT(ran >= 3); + ran = 0; + rb.ReadAt(ProfileBufferBlockIndex{}, + [&](Maybe<ProfileBufferEntryReader>&& aMaybeEntryReader) { + MOZ_RELEASE_ASSERT(aMaybeEntryReader.isNothing()); + ++ran; + }); + MOZ_RELEASE_ASSERT(ran == 1); + ran = 0; + rb.ReadAt(bi, [&](Maybe<ProfileBufferEntryReader>&& aMaybeEntryReader) { + MOZ_RELEASE_ASSERT(aMaybeEntryReader.isNothing() == !bi); + ++ran; + }); + MOZ_RELEASE_ASSERT(ran == 1); + }; + + testInSession(EMPTY); + testInSession(NOT_EMPTY); + + rb.Set(MakePowerOfTwo<BlocksRingBuffer::Length, 32>()); + MOZ_RELEASE_ASSERT(rb.BufferLength().isSome()); + rb.ReadEach([](auto&&) { MOZ_RELEASE_ASSERT(false); }); + + testInSession(EMPTY); + testInSession(NOT_EMPTY); + + rb.Reset(); + testOutOfSession(); + testOutOfSession(); + + uint8_t buffer[MBSize * 3]; + for (size_t i = 0; i < MBSize * 3; ++i) { + buffer[i] = uint8_t('A' + i); + } + + rb.Set(&buffer[MBSize], MakePowerOfTwo<BlocksRingBuffer::Length, MBSize>()); + MOZ_RELEASE_ASSERT(rb.BufferLength().isSome()); + rb.ReadEach([](auto&&) { MOZ_RELEASE_ASSERT(false); }); + + testInSession(EMPTY); + testInSession(NOT_EMPTY); + + rb.Reset(); + testOutOfSession(); + testOutOfSession(); + + rb.Set(&buffer[MBSize], MakePowerOfTwo<BlocksRingBuffer::Length, MBSize>()); + MOZ_RELEASE_ASSERT(rb.BufferLength().isSome()); + rb.ReadEach([](auto&&) { MOZ_RELEASE_ASSERT(false); }); + + testInSession(EMPTY); + testInSession(NOT_EMPTY); + + // Remove the current underlying buffer, this should clear all entries. + rb.Reset(); + + // Check that only the provided stack-based sub-buffer was modified. + uint32_t changed = 0; + for (size_t i = MBSize; i < MBSize * 2; ++i) { + changed += (buffer[i] == uint8_t('A' + i)) ? 0 : 1; + } + // Expect at least 75% changes. + MOZ_RELEASE_ASSERT(changed >= MBSize * 6 / 8); + + // Everything around the sub-buffer should be unchanged. + for (size_t i = 0; i < MBSize; ++i) { + MOZ_RELEASE_ASSERT(buffer[i] == uint8_t('A' + i)); + } + for (size_t i = MBSize * 2; i < MBSize * 3; ++i) { + MOZ_RELEASE_ASSERT(buffer[i] == uint8_t('A' + i)); + } + + testOutOfSession(); + testOutOfSession(); + + printf("TestBlocksRingBufferUnderlyingBufferChanges done\n"); +} + +void TestBlocksRingBufferThreading() { + printf("TestBlocksRingBufferThreading...\n"); + + constexpr uint32_t MBSize = 8192; + uint8_t buffer[MBSize * 3]; + for (size_t i = 0; i < MBSize * 3; ++i) { + buffer[i] = uint8_t('A' + i); + } + BlocksRingBuffer rb(BlocksRingBuffer::ThreadSafety::WithMutex, + &buffer[MBSize], MakePowerOfTwo32<MBSize>()); + + // Start reader thread. + std::atomic<bool> stopReader{false}; + std::thread reader([&]() { + for (;;) { + BlocksRingBuffer::State state = rb.GetState(); + printf( + "Reader: range=%llu..%llu (%llu bytes) pushed=%llu cleared=%llu " + "(alive=%llu)\n", + static_cast<unsigned long long>( + state.mRangeStart.ConvertToProfileBufferIndex()), + static_cast<unsigned long long>( + state.mRangeEnd.ConvertToProfileBufferIndex()), + static_cast<unsigned long long>( + state.mRangeEnd.ConvertToProfileBufferIndex()) - + static_cast<unsigned long long>( + state.mRangeStart.ConvertToProfileBufferIndex()), + static_cast<unsigned long long>(state.mPushedBlockCount), + static_cast<unsigned long long>(state.mClearedBlockCount), + static_cast<unsigned long long>(state.mPushedBlockCount - + state.mClearedBlockCount)); + if (stopReader) { + break; + } + ::SleepMilli(1); + } + }); + + // Start writer threads. + constexpr int ThreadCount = 32; + std::thread threads[ThreadCount]; + for (int threadNo = 0; threadNo < ThreadCount; ++threadNo) { + threads[threadNo] = std::thread( + [&](int aThreadNo) { + ::SleepMilli(1); + constexpr int pushCount = 1024; + for (int push = 0; push < pushCount; ++push) { + // Reserve as many bytes as the thread number (but at least enough + // to store an int), and write an increasing int. + rb.Put(std::max(aThreadNo, int(sizeof(push))), + [&](Maybe<ProfileBufferEntryWriter>& aEW) { + MOZ_RELEASE_ASSERT(aEW.isSome()); + aEW->WriteObject(aThreadNo * 1000000 + push); + *aEW += aEW->RemainingBytes(); + }); + } + }, + threadNo); + } + + // Wait for all writer threads to die. + for (auto&& thread : threads) { + thread.join(); + } + + // Stop reader thread. + stopReader = true; + reader.join(); + + // Check that only the provided stack-based sub-buffer was modified. + uint32_t changed = 0; + for (size_t i = MBSize; i < MBSize * 2; ++i) { + changed += (buffer[i] == uint8_t('A' + i)) ? 0 : 1; + } + // Expect at least 75% changes. + MOZ_RELEASE_ASSERT(changed >= MBSize * 6 / 8); + + // Everything around the sub-buffer should be unchanged. + for (size_t i = 0; i < MBSize; ++i) { + MOZ_RELEASE_ASSERT(buffer[i] == uint8_t('A' + i)); + } + for (size_t i = MBSize * 2; i < MBSize * 3; ++i) { + MOZ_RELEASE_ASSERT(buffer[i] == uint8_t('A' + i)); + } + + printf("TestBlocksRingBufferThreading done\n"); +} + +void TestBlocksRingBufferSerialization() { + printf("TestBlocksRingBufferSerialization...\n"); + + constexpr uint32_t MBSize = 64; + uint8_t buffer[MBSize * 3]; + for (size_t i = 0; i < MBSize * 3; ++i) { + buffer[i] = uint8_t('A' + i); + } + BlocksRingBuffer rb(BlocksRingBuffer::ThreadSafety::WithMutex, + &buffer[MBSize], MakePowerOfTwo32<MBSize>()); + + // Will expect literal string to always have the same address. +# define THE_ANSWER "The answer is " + const char* theAnswer = THE_ANSWER; + + rb.PutObjects('0', WrapProfileBufferLiteralCStringPointer(THE_ANSWER), 42, + std::string(" but pi="), 3.14); + rb.ReadEach([&](ProfileBufferEntryReader& aER) { + char c0; + const char* answer; + int integer; + std::string str; + double pi; + aER.ReadIntoObjects(c0, answer, integer, str, pi); + MOZ_RELEASE_ASSERT(c0 == '0'); + MOZ_RELEASE_ASSERT(answer == theAnswer); + MOZ_RELEASE_ASSERT(integer == 42); + MOZ_RELEASE_ASSERT(str == " but pi="); + MOZ_RELEASE_ASSERT(pi == 3.14); + }); + rb.ReadEach([&](ProfileBufferEntryReader& aER) { + char c0 = aER.ReadObject<char>(); + MOZ_RELEASE_ASSERT(c0 == '0'); + const char* answer = aER.ReadObject<const char*>(); + MOZ_RELEASE_ASSERT(answer == theAnswer); + int integer = aER.ReadObject<int>(); + MOZ_RELEASE_ASSERT(integer == 42); + std::string str = aER.ReadObject<std::string>(); + MOZ_RELEASE_ASSERT(str == " but pi="); + double pi = aER.ReadObject<double>(); + MOZ_RELEASE_ASSERT(pi == 3.14); + }); + + rb.Clear(); + // Write an int and store its ProfileBufferBlockIndex. + ProfileBufferBlockIndex blockIndex = rb.PutObject(123); + // It should be non-0. + MOZ_RELEASE_ASSERT(blockIndex != ProfileBufferBlockIndex{}); + // Write that ProfileBufferBlockIndex. + rb.PutObject(blockIndex); + rb.Read([&](BlocksRingBuffer::Reader* aR) { + BlocksRingBuffer::BlockIterator it = aR->begin(); + const BlocksRingBuffer::BlockIterator itEnd = aR->end(); + MOZ_RELEASE_ASSERT(it != itEnd); + MOZ_RELEASE_ASSERT((*it).ReadObject<int>() == 123); + ++it; + MOZ_RELEASE_ASSERT(it != itEnd); + MOZ_RELEASE_ASSERT((*it).ReadObject<ProfileBufferBlockIndex>() == + blockIndex); + ++it; + MOZ_RELEASE_ASSERT(it == itEnd); + }); + + rb.Clear(); + rb.PutObjects( + std::make_tuple('0', WrapProfileBufferLiteralCStringPointer(THE_ANSWER), + 42, std::string(" but pi="), 3.14)); + rb.ReadEach([&](ProfileBufferEntryReader& aER) { + MOZ_RELEASE_ASSERT(aER.ReadObject<char>() == '0'); + MOZ_RELEASE_ASSERT(aER.ReadObject<const char*>() == theAnswer); + MOZ_RELEASE_ASSERT(aER.ReadObject<int>() == 42); + MOZ_RELEASE_ASSERT(aER.ReadObject<std::string>() == " but pi="); + MOZ_RELEASE_ASSERT(aER.ReadObject<double>() == 3.14); + }); + + rb.Clear(); + rb.PutObjects(MakeTuple('0', + WrapProfileBufferLiteralCStringPointer(THE_ANSWER), + 42, std::string(" but pi="), 3.14)); + rb.ReadEach([&](ProfileBufferEntryReader& aER) { + MOZ_RELEASE_ASSERT(aER.ReadObject<char>() == '0'); + MOZ_RELEASE_ASSERT(aER.ReadObject<const char*>() == theAnswer); + MOZ_RELEASE_ASSERT(aER.ReadObject<int>() == 42); + MOZ_RELEASE_ASSERT(aER.ReadObject<std::string>() == " but pi="); + MOZ_RELEASE_ASSERT(aER.ReadObject<double>() == 3.14); + }); + + rb.Clear(); + { + UniqueFreePtr<char> ufps(strdup(THE_ANSWER)); + rb.PutObjects(ufps); + } + rb.ReadEach([&](ProfileBufferEntryReader& aER) { + auto ufps = aER.ReadObject<UniqueFreePtr<char>>(); + MOZ_RELEASE_ASSERT(!!ufps); + MOZ_RELEASE_ASSERT(std::string(THE_ANSWER) == ufps.get()); + }); + + rb.Clear(); + int intArray[] = {1, 2, 3, 4, 5}; + rb.PutObjects(Span(intArray)); + rb.ReadEach([&](ProfileBufferEntryReader& aER) { + int intArrayOut[sizeof(intArray) / sizeof(intArray[0])] = {0}; + auto outSpan = Span(intArrayOut); + aER.ReadIntoObject(outSpan); + for (size_t i = 0; i < sizeof(intArray) / sizeof(intArray[0]); ++i) { + MOZ_RELEASE_ASSERT(intArrayOut[i] == intArray[i]); + } + }); + + rb.Clear(); + rb.PutObjects(Maybe<int>(Nothing{}), Maybe<int>(Some(123))); + rb.ReadEach([&](ProfileBufferEntryReader& aER) { + Maybe<int> mi0, mi1; + aER.ReadIntoObjects(mi0, mi1); + MOZ_RELEASE_ASSERT(mi0.isNothing()); + MOZ_RELEASE_ASSERT(mi1.isSome()); + MOZ_RELEASE_ASSERT(*mi1 == 123); + }); + + rb.Clear(); + using V = Variant<int, double, int>; + V v0(VariantIndex<0>{}, 123); + V v1(3.14); + V v2(VariantIndex<2>{}, 456); + rb.PutObjects(v0, v1, v2); + rb.ReadEach([&](ProfileBufferEntryReader& aER) { + MOZ_RELEASE_ASSERT(aER.ReadObject<V>() == v0); + MOZ_RELEASE_ASSERT(aER.ReadObject<V>() == v1); + MOZ_RELEASE_ASSERT(aER.ReadObject<V>() == v2); + }); + + // 2nd BlocksRingBuffer to contain the 1st one. It has be be more than twice + // the size. + constexpr uint32_t MBSize2 = MBSize * 4; + uint8_t buffer2[MBSize2 * 3]; + for (size_t i = 0; i < MBSize2 * 3; ++i) { + buffer2[i] = uint8_t('B' + i); + } + BlocksRingBuffer rb2(BlocksRingBuffer::ThreadSafety::WithoutMutex, + &buffer2[MBSize2], MakePowerOfTwo32<MBSize2>()); + rb2.PutObject(rb); + + // 3rd BlocksRingBuffer deserialized from the 2nd one. + uint8_t buffer3[MBSize * 3]; + for (size_t i = 0; i < MBSize * 3; ++i) { + buffer3[i] = uint8_t('C' + i); + } + BlocksRingBuffer rb3(BlocksRingBuffer::ThreadSafety::WithoutMutex, + &buffer3[MBSize], MakePowerOfTwo32<MBSize>()); + rb2.ReadEach([&](ProfileBufferEntryReader& aER) { aER.ReadIntoObject(rb3); }); + + // And a 4th heap-allocated one. + UniquePtr<BlocksRingBuffer> rb4up; + rb2.ReadEach([&](ProfileBufferEntryReader& aER) { + rb4up = aER.ReadObject<UniquePtr<BlocksRingBuffer>>(); + }); + MOZ_RELEASE_ASSERT(!!rb4up); + + // Clear 1st and 2nd BlocksRingBuffers, to ensure we have made a deep copy + // into the 3rd&4th ones. + rb.Clear(); + rb2.Clear(); + + // And now the 3rd one should have the same contents as the 1st one had. + rb3.ReadEach([&](ProfileBufferEntryReader& aER) { + MOZ_RELEASE_ASSERT(aER.ReadObject<V>() == v0); + MOZ_RELEASE_ASSERT(aER.ReadObject<V>() == v1); + MOZ_RELEASE_ASSERT(aER.ReadObject<V>() == v2); + }); + + // And 4th. + rb4up->ReadEach([&](ProfileBufferEntryReader& aER) { + MOZ_RELEASE_ASSERT(aER.ReadObject<V>() == v0); + MOZ_RELEASE_ASSERT(aER.ReadObject<V>() == v1); + MOZ_RELEASE_ASSERT(aER.ReadObject<V>() == v2); + }); + + // In fact, the 3rd and 4th ones should have the same state, because they were + // created the same way. + MOZ_RELEASE_ASSERT(rb3.GetState().mRangeStart == + rb4up->GetState().mRangeStart); + MOZ_RELEASE_ASSERT(rb3.GetState().mRangeEnd == rb4up->GetState().mRangeEnd); + MOZ_RELEASE_ASSERT(rb3.GetState().mPushedBlockCount == + rb4up->GetState().mPushedBlockCount); + MOZ_RELEASE_ASSERT(rb3.GetState().mClearedBlockCount == + rb4up->GetState().mClearedBlockCount); + + // Check that only the provided stack-based sub-buffer was modified. + uint32_t changed = 0; + for (size_t i = MBSize; i < MBSize * 2; ++i) { + changed += (buffer[i] == uint8_t('A' + i)) ? 0 : 1; + } + // Expect at least 75% changes. + MOZ_RELEASE_ASSERT(changed >= MBSize * 6 / 8); + + // Everything around the sub-buffers should be unchanged. + for (size_t i = 0; i < MBSize; ++i) { + MOZ_RELEASE_ASSERT(buffer[i] == uint8_t('A' + i)); + } + for (size_t i = MBSize * 2; i < MBSize * 3; ++i) { + MOZ_RELEASE_ASSERT(buffer[i] == uint8_t('A' + i)); + } + + for (size_t i = 0; i < MBSize2; ++i) { + MOZ_RELEASE_ASSERT(buffer2[i] == uint8_t('B' + i)); + } + for (size_t i = MBSize2 * 2; i < MBSize2 * 3; ++i) { + MOZ_RELEASE_ASSERT(buffer2[i] == uint8_t('B' + i)); + } + + for (size_t i = 0; i < MBSize; ++i) { + MOZ_RELEASE_ASSERT(buffer3[i] == uint8_t('C' + i)); + } + for (size_t i = MBSize * 2; i < MBSize * 3; ++i) { + MOZ_RELEASE_ASSERT(buffer3[i] == uint8_t('C' + i)); + } + + printf("TestBlocksRingBufferSerialization done\n"); +} + +void TestLiteralEmptyStringView() { + printf("TestLiteralEmptyStringView...\n"); + + static_assert(mozilla::LiteralEmptyStringView<char>() == + std::string_view("")); + static_assert(!!mozilla::LiteralEmptyStringView<char>().data()); + static_assert(mozilla::LiteralEmptyStringView<char>().length() == 0); + + static_assert(mozilla::LiteralEmptyStringView<char16_t>() == + std::basic_string_view<char16_t>(u"")); + static_assert(!!mozilla::LiteralEmptyStringView<char16_t>().data()); + static_assert(mozilla::LiteralEmptyStringView<char16_t>().length() == 0); + + printf("TestLiteralEmptyStringView done\n"); +} + +template <typename CHAR> +void TestProfilerStringView() { + if constexpr (std::is_same_v<CHAR, char>) { + printf("TestProfilerStringView<char>...\n"); + } else if constexpr (std::is_same_v<CHAR, char16_t>) { + printf("TestProfilerStringView<char16_t>...\n"); + } else { + MOZ_RELEASE_ASSERT(false, + "TestProfilerStringView only handles char and char16_t"); + } + + // Used to verify implicit constructions, as this will normally be used in + // function parameters. + auto BSV = [](mozilla::ProfilerStringView<CHAR>&& aBSV) { + return std::move(aBSV); + }; + + // These look like string literals, as expected by some string constructors. + const CHAR empty[0 + 1] = {CHAR('\0')}; + const CHAR hi[2 + 1] = { + CHAR('h'), + CHAR('i'), + CHAR('\0'), + }; + + // Literal empty string. + MOZ_RELEASE_ASSERT(BSV(empty).Data()); + MOZ_RELEASE_ASSERT(BSV(empty).Data()[0] == CHAR('\0')); + MOZ_RELEASE_ASSERT(BSV(empty).Length() == 0); + MOZ_RELEASE_ASSERT(BSV(empty).IsLiteral()); + MOZ_RELEASE_ASSERT(!BSV(empty).IsReference()); + + // Literal non-empty string. + MOZ_RELEASE_ASSERT(BSV(hi).Data()); + MOZ_RELEASE_ASSERT(BSV(hi).Data()[0] == CHAR('h')); + MOZ_RELEASE_ASSERT(BSV(hi).Data()[1] == CHAR('i')); + MOZ_RELEASE_ASSERT(BSV(hi).Data()[2] == CHAR('\0')); + MOZ_RELEASE_ASSERT(BSV(hi).Length() == 2); + MOZ_RELEASE_ASSERT(BSV(hi).IsLiteral()); + MOZ_RELEASE_ASSERT(!BSV(hi).IsReference()); + + // std::string_view to a literal empty string. + MOZ_RELEASE_ASSERT(BSV(std::basic_string_view<CHAR>(empty)).Data()); + MOZ_RELEASE_ASSERT(BSV(std::basic_string_view<CHAR>(empty)).Data()[0] == + CHAR('\0')); + MOZ_RELEASE_ASSERT(BSV(std::basic_string_view<CHAR>(empty)).Length() == 0); + MOZ_RELEASE_ASSERT(!BSV(std::basic_string_view<CHAR>(empty)).IsLiteral()); + MOZ_RELEASE_ASSERT(BSV(std::basic_string_view<CHAR>(empty)).IsReference()); + + // std::string_view to a literal non-empty string. + MOZ_RELEASE_ASSERT(BSV(std::basic_string_view<CHAR>(hi)).Data()); + MOZ_RELEASE_ASSERT(BSV(std::basic_string_view<CHAR>(hi)).Data()[0] == + CHAR('h')); + MOZ_RELEASE_ASSERT(BSV(std::basic_string_view<CHAR>(hi)).Data()[1] == + CHAR('i')); + MOZ_RELEASE_ASSERT(BSV(std::basic_string_view<CHAR>(hi)).Data()[2] == + CHAR('\0')); + MOZ_RELEASE_ASSERT(BSV(std::basic_string_view<CHAR>(hi)).Length() == 2); + MOZ_RELEASE_ASSERT(!BSV(std::basic_string_view<CHAR>(hi)).IsLiteral()); + MOZ_RELEASE_ASSERT(BSV(std::basic_string_view<CHAR>(hi)).IsReference()); + + // Default std::string_view points at nullptr, ProfilerStringView converts it + // to the literal empty string. + MOZ_RELEASE_ASSERT(!std::basic_string_view<CHAR>().data()); + MOZ_RELEASE_ASSERT(BSV(std::basic_string_view<CHAR>()).Data()); + MOZ_RELEASE_ASSERT(BSV(std::basic_string_view<CHAR>()).Data()[0] == + CHAR('\0')); + MOZ_RELEASE_ASSERT(BSV(std::basic_string_view<CHAR>()).Length() == 0); + MOZ_RELEASE_ASSERT(BSV(std::basic_string_view<CHAR>()).IsLiteral()); + MOZ_RELEASE_ASSERT(!BSV(std::basic_string_view<CHAR>()).IsReference()); + + // std::string to a literal empty string. + MOZ_RELEASE_ASSERT(BSV(std::basic_string<CHAR>(empty)).Data()); + MOZ_RELEASE_ASSERT(BSV(std::basic_string<CHAR>(empty)).Data()[0] == + CHAR('\0')); + MOZ_RELEASE_ASSERT(BSV(std::basic_string<CHAR>(empty)).Length() == 0); + MOZ_RELEASE_ASSERT(!BSV(std::basic_string<CHAR>(empty)).IsLiteral()); + MOZ_RELEASE_ASSERT(BSV(std::basic_string<CHAR>(empty)).IsReference()); + + // std::string to a literal non-empty string. + MOZ_RELEASE_ASSERT(BSV(std::basic_string<CHAR>(hi)).Data()); + MOZ_RELEASE_ASSERT(BSV(std::basic_string<CHAR>(hi)).Data()[0] == CHAR('h')); + MOZ_RELEASE_ASSERT(BSV(std::basic_string<CHAR>(hi)).Data()[1] == CHAR('i')); + MOZ_RELEASE_ASSERT(BSV(std::basic_string<CHAR>(hi)).Data()[2] == CHAR('\0')); + MOZ_RELEASE_ASSERT(BSV(std::basic_string<CHAR>(hi)).Length() == 2); + MOZ_RELEASE_ASSERT(!BSV(std::basic_string<CHAR>(hi)).IsLiteral()); + MOZ_RELEASE_ASSERT(BSV(std::basic_string<CHAR>(hi)).IsReference()); + + // Default std::string contains an empty null-terminated string. + MOZ_RELEASE_ASSERT(std::basic_string<CHAR>().data()); + MOZ_RELEASE_ASSERT(BSV(std::basic_string<CHAR>()).Data()); + MOZ_RELEASE_ASSERT(BSV(std::basic_string<CHAR>()).Data()[0] == CHAR('\0')); + MOZ_RELEASE_ASSERT(BSV(std::basic_string<CHAR>()).Length() == 0); + MOZ_RELEASE_ASSERT(!BSV(std::basic_string<CHAR>()).IsLiteral()); + MOZ_RELEASE_ASSERT(BSV(std::basic_string<CHAR>()).IsReference()); + + // Class that quacks like nsTString (with Data(), Length(), IsLiteral()), to + // check that ProfilerStringView can read from them. + class FakeNsTString { + public: + FakeNsTString(const CHAR* aData, size_t aLength, bool aIsLiteral) + : mData(aData), mLength(aLength), mIsLiteral(aIsLiteral) {} + + const CHAR* Data() const { return mData; } + size_t Length() const { return mLength; } + bool IsLiteral() const { return mIsLiteral; } + + private: + const CHAR* mData; + size_t mLength; + bool mIsLiteral; + }; + + // FakeNsTString to nullptr. + MOZ_RELEASE_ASSERT(BSV(FakeNsTString(nullptr, 0, true)).Data()); + MOZ_RELEASE_ASSERT(BSV(FakeNsTString(nullptr, 0, true)).Data()[0] == + CHAR('\0')); + MOZ_RELEASE_ASSERT(BSV(FakeNsTString(nullptr, 0, true)).Length() == 0); + MOZ_RELEASE_ASSERT(BSV(FakeNsTString(nullptr, 0, true)).IsLiteral()); + MOZ_RELEASE_ASSERT(!BSV(FakeNsTString(nullptr, 0, true)).IsReference()); + + // FakeNsTString to a literal empty string. + MOZ_RELEASE_ASSERT(BSV(FakeNsTString(empty, 0, true)).Data()); + MOZ_RELEASE_ASSERT(BSV(FakeNsTString(empty, 0, true)).Data()[0] == + CHAR('\0')); + MOZ_RELEASE_ASSERT(BSV(FakeNsTString(empty, 0, true)).Length() == 0); + MOZ_RELEASE_ASSERT(BSV(FakeNsTString(empty, 0, true)).IsLiteral()); + MOZ_RELEASE_ASSERT(!BSV(FakeNsTString(empty, 0, true)).IsReference()); + + // FakeNsTString to a literal non-empty string. + MOZ_RELEASE_ASSERT(BSV(FakeNsTString(hi, 2, true)).Data()); + MOZ_RELEASE_ASSERT(BSV(FakeNsTString(hi, 2, true)).Data()[0] == CHAR('h')); + MOZ_RELEASE_ASSERT(BSV(FakeNsTString(hi, 2, true)).Data()[1] == CHAR('i')); + MOZ_RELEASE_ASSERT(BSV(FakeNsTString(hi, 2, true)).Data()[2] == CHAR('\0')); + MOZ_RELEASE_ASSERT(BSV(FakeNsTString(hi, 2, true)).Length() == 2); + MOZ_RELEASE_ASSERT(BSV(FakeNsTString(hi, 2, true)).IsLiteral()); + MOZ_RELEASE_ASSERT(!BSV(FakeNsTString(hi, 2, true)).IsReference()); + + // FakeNsTString to a non-literal non-empty string. + MOZ_RELEASE_ASSERT(BSV(FakeNsTString(hi, 2, false)).Data()); + MOZ_RELEASE_ASSERT(BSV(FakeNsTString(hi, 2, false)).Data()[0] == CHAR('h')); + MOZ_RELEASE_ASSERT(BSV(FakeNsTString(hi, 2, false)).Data()[1] == CHAR('i')); + MOZ_RELEASE_ASSERT(BSV(FakeNsTString(hi, 2, false)).Data()[2] == CHAR('\0')); + MOZ_RELEASE_ASSERT(BSV(FakeNsTString(hi, 2, false)).Length() == 2); + MOZ_RELEASE_ASSERT(!BSV(FakeNsTString(hi, 2, false)).IsLiteral()); + MOZ_RELEASE_ASSERT(BSV(FakeNsTString(hi, 2, false)).IsReference()); + + // Serialization and deserialization (with ownership). + constexpr size_t bufferMaxSize = 1024; + constexpr ProfileChunkedBuffer::Length chunkMinSize = 128; + ProfileBufferChunkManagerWithLocalLimit cm(bufferMaxSize, chunkMinSize); + ProfileChunkedBuffer cb(ProfileChunkedBuffer::ThreadSafety::WithMutex, cm); + + // Literal string, serialized as raw pointer. + MOZ_RELEASE_ASSERT(cb.PutObject(BSV(hi))); + { + unsigned read = 0; + ProfilerStringView<CHAR> outerBSV; + cb.ReadEach([&](ProfileBufferEntryReader& aER) { + ++read; + auto bsv = aER.ReadObject<ProfilerStringView<CHAR>>(); + MOZ_RELEASE_ASSERT(bsv.Data()); + MOZ_RELEASE_ASSERT(bsv.Data()[0] == CHAR('h')); + MOZ_RELEASE_ASSERT(bsv.Data()[1] == CHAR('i')); + MOZ_RELEASE_ASSERT(bsv.Data()[2] == CHAR('\0')); + MOZ_RELEASE_ASSERT(bsv.Length() == 2); + MOZ_RELEASE_ASSERT(bsv.IsLiteral()); + MOZ_RELEASE_ASSERT(!bsv.IsReference()); + outerBSV = std::move(bsv); + }); + MOZ_RELEASE_ASSERT(read == 1); + MOZ_RELEASE_ASSERT(outerBSV.Data()); + MOZ_RELEASE_ASSERT(outerBSV.Data()[0] == CHAR('h')); + MOZ_RELEASE_ASSERT(outerBSV.Data()[1] == CHAR('i')); + MOZ_RELEASE_ASSERT(outerBSV.Data()[2] == CHAR('\0')); + MOZ_RELEASE_ASSERT(outerBSV.Length() == 2); + MOZ_RELEASE_ASSERT(outerBSV.IsLiteral()); + MOZ_RELEASE_ASSERT(!outerBSV.IsReference()); + } + + cb.Clear(); + + // Non-literal string, content is serialized. + std::basic_string<CHAR> hiString(hi); + MOZ_RELEASE_ASSERT(cb.PutObject(BSV(hiString))); + { + unsigned read = 0; + ProfilerStringView<CHAR> outerBSV; + cb.ReadEach([&](ProfileBufferEntryReader& aER) { + ++read; + auto bsv = aER.ReadObject<ProfilerStringView<CHAR>>(); + MOZ_RELEASE_ASSERT(bsv.Data()); + MOZ_RELEASE_ASSERT(bsv.Data() != hiString.data()); + MOZ_RELEASE_ASSERT(bsv.Data()[0] == CHAR('h')); + MOZ_RELEASE_ASSERT(bsv.Data()[1] == CHAR('i')); + MOZ_RELEASE_ASSERT(bsv.Data()[2] == CHAR('\0')); + MOZ_RELEASE_ASSERT(bsv.Length() == 2); + // Special ownership case, neither a literal nor a reference! + MOZ_RELEASE_ASSERT(!bsv.IsLiteral()); + MOZ_RELEASE_ASSERT(!bsv.IsReference()); + // Test move of ownership. + outerBSV = std::move(bsv); + // NOLINTNEXTLINE(bugprone-use-after-move, clang-analyzer-cplusplus.Move) + MOZ_RELEASE_ASSERT(bsv.Length() == 0); + }); + MOZ_RELEASE_ASSERT(read == 1); + MOZ_RELEASE_ASSERT(outerBSV.Data()); + MOZ_RELEASE_ASSERT(outerBSV.Data() != hiString.data()); + MOZ_RELEASE_ASSERT(outerBSV.Data()[0] == CHAR('h')); + MOZ_RELEASE_ASSERT(outerBSV.Data()[1] == CHAR('i')); + MOZ_RELEASE_ASSERT(outerBSV.Data()[2] == CHAR('\0')); + MOZ_RELEASE_ASSERT(outerBSV.Length() == 2); + MOZ_RELEASE_ASSERT(!outerBSV.IsLiteral()); + MOZ_RELEASE_ASSERT(!outerBSV.IsReference()); + } + + if constexpr (std::is_same_v<CHAR, char>) { + printf("TestProfilerStringView<char> done\n"); + } else if constexpr (std::is_same_v<CHAR, char16_t>) { + printf("TestProfilerStringView<char16_t> done\n"); + } +} + +void TestProfilerDependencies() { + TestPowerOfTwoMask(); + TestPowerOfTwo(); + TestLEB128(); + TestChunk(); + TestChunkManagerSingle(); + TestChunkManagerWithLocalLimit(); + TestControlledChunkManagerUpdate(); + TestControlledChunkManagerWithLocalLimit(); + TestChunkedBuffer(); + TestChunkedBufferSingle(); + TestModuloBuffer(); + TestBlocksRingBufferAPI(); + TestBlocksRingBufferUnderlyingBufferChanges(); + TestBlocksRingBufferThreading(); + TestBlocksRingBufferSerialization(); + TestLiteralEmptyStringView(); + TestProfilerStringView<char>(); + TestProfilerStringView<char16_t>(); +} + +// Increase the depth, to a maximum (to avoid too-deep recursion). +static constexpr size_t NextDepth(size_t aDepth) { + constexpr size_t MAX_DEPTH = 128; + return (aDepth < MAX_DEPTH) ? (aDepth + 1) : aDepth; +} + +Atomic<bool, Relaxed> sStopFibonacci; + +// Compute fibonacci the hard way (recursively: `f(n)=f(n-1)+f(n-2)`), and +// prevent inlining. +// The template parameter makes each depth be a separate function, to better +// distinguish them in the profiler output. +template <size_t DEPTH = 0> +MOZ_NEVER_INLINE unsigned long long Fibonacci(unsigned long long n) { + AUTO_BASE_PROFILER_LABEL_DYNAMIC_STRING("fib", OTHER, std::to_string(DEPTH)); + if (n == 0) { + return 0; + } + if (n == 1) { + return 1; + } + if (DEPTH < 5 && sStopFibonacci) { + return 1'000'000'000; + } + TimeStamp start = TimeStamp::NowUnfuzzed(); + static constexpr size_t MAX_MARKER_DEPTH = 10; + unsigned long long f2 = Fibonacci<NextDepth(DEPTH)>(n - 2); + if (DEPTH == 0) { + BASE_PROFILER_MARKER_UNTYPED("Half-way through Fibonacci", OTHER); + } + unsigned long long f1 = Fibonacci<NextDepth(DEPTH)>(n - 1); + if (DEPTH < MAX_MARKER_DEPTH) { + BASE_PROFILER_MARKER_TEXT("fib", OTHER, + MarkerTiming::IntervalUntilNowFrom(start), + std::to_string(DEPTH)); + } + return f2 + f1; +} + +void TestProfiler() { + printf("TestProfiler starting -- pid: %d, tid: %d\n", + baseprofiler::profiler_current_process_id(), + baseprofiler::profiler_current_thread_id()); + // ::SleepMilli(10000); + + TestProfilerDependencies(); + + { + printf("profiler_init()...\n"); + AUTO_BASE_PROFILER_INIT; + + MOZ_RELEASE_ASSERT(!baseprofiler::profiler_is_active()); + MOZ_RELEASE_ASSERT(!baseprofiler::profiler_thread_is_being_profiled()); + MOZ_RELEASE_ASSERT(!baseprofiler::profiler_thread_is_sleeping()); + + const int mainThreadId = + mozilla::baseprofiler::profiler_current_thread_id(); + + MOZ_RELEASE_ASSERT(mozilla::baseprofiler::profiler_main_thread_id() == + mainThreadId); + MOZ_RELEASE_ASSERT(mozilla::baseprofiler::profiler_is_main_thread()); + + std::thread testThread([&]() { + const int testThreadId = + mozilla::baseprofiler::profiler_current_thread_id(); + MOZ_RELEASE_ASSERT(testThreadId != mainThreadId); + + MOZ_RELEASE_ASSERT(mozilla::baseprofiler::profiler_main_thread_id() != + testThreadId); + MOZ_RELEASE_ASSERT(!mozilla::baseprofiler::profiler_is_main_thread()); + }); + testThread.join(); + + printf("profiler_start()...\n"); + Vector<const char*> filters; + // Profile all registered threads. + MOZ_RELEASE_ASSERT(filters.append("")); + const uint32_t features = baseprofiler::ProfilerFeature::Leaf | + baseprofiler::ProfilerFeature::StackWalk | + baseprofiler::ProfilerFeature::Threads; + baseprofiler::profiler_start(baseprofiler::BASE_PROFILER_DEFAULT_ENTRIES, + BASE_PROFILER_DEFAULT_INTERVAL, features, + filters.begin(), filters.length()); + + MOZ_RELEASE_ASSERT(baseprofiler::profiler_is_active()); + MOZ_RELEASE_ASSERT(baseprofiler::profiler_thread_is_being_profiled()); + MOZ_RELEASE_ASSERT(!baseprofiler::profiler_thread_is_sleeping()); + + sStopFibonacci = false; + + std::thread threadFib([]() { + AUTO_BASE_PROFILER_REGISTER_THREAD("fibonacci"); + SleepMilli(5); + auto cause = baseprofiler::profiler_capture_backtrace(); + AUTO_BASE_PROFILER_MARKER_TEXT( + "fibonacci", OTHER, MarkerStack::TakeBacktrace(std::move(cause)), + "First leaf call"); + static const unsigned long long fibStart = 37; + printf("Fibonacci(%llu)...\n", fibStart); + AUTO_BASE_PROFILER_LABEL("Label around Fibonacci", OTHER); + + unsigned long long f = Fibonacci(fibStart); + printf("Fibonacci(%llu) = %llu\n", fibStart, f); + }); + + std::thread threadCancelFib([]() { + AUTO_BASE_PROFILER_REGISTER_THREAD("fibonacci canceller"); + SleepMilli(5); + AUTO_BASE_PROFILER_MARKER_TEXT("fibonacci", OTHER, {}, "Canceller"); + static const int waitMaxSeconds = 10; + for (int i = 0; i < waitMaxSeconds; ++i) { + if (sStopFibonacci) { + AUTO_BASE_PROFILER_LABEL_DYNAMIC_STRING("fibCancel", OTHER, + std::to_string(i)); + return; + } + AUTO_BASE_PROFILER_THREAD_SLEEP; + SleepMilli(1000); + } + AUTO_BASE_PROFILER_LABEL_DYNAMIC_STRING("fibCancel", OTHER, + "Cancelling!"); + sStopFibonacci = true; + }); + + { + AUTO_BASE_PROFILER_MARKER_TEXT("main thread", OTHER, {}, + "joining fibonacci thread"); + AUTO_BASE_PROFILER_THREAD_SLEEP; + threadFib.join(); + } + + { + AUTO_BASE_PROFILER_MARKER_TEXT("main thread", OTHER, {}, + "joining fibonacci-canceller thread"); + sStopFibonacci = true; + AUTO_BASE_PROFILER_THREAD_SLEEP; + threadCancelFib.join(); + } + + // Just making sure all payloads know how to (de)serialize and stream. + + MOZ_RELEASE_ASSERT( + baseprofiler::AddMarker("markers 2.0 without options (omitted)", + mozilla::baseprofiler::category::OTHER)); + + MOZ_RELEASE_ASSERT(baseprofiler::AddMarker( + "markers 2.0 without options (implicit brace-init)", + mozilla::baseprofiler::category::OTHER, {})); + + MOZ_RELEASE_ASSERT(baseprofiler::AddMarker( + "markers 2.0 without options (explicit init)", + mozilla::baseprofiler::category::OTHER, MarkerOptions())); + + MOZ_RELEASE_ASSERT(baseprofiler::AddMarker( + "markers 2.0 without options (explicit brace-init)", + mozilla::baseprofiler::category::OTHER, MarkerOptions{})); + + MOZ_RELEASE_ASSERT(baseprofiler::AddMarker( + "markers 2.0 with one option (implicit)", + mozilla::baseprofiler::category::OTHER, MarkerInnerWindowId(123))); + + MOZ_RELEASE_ASSERT(baseprofiler::AddMarker( + "markers 2.0 with one option (implicit brace-init)", + mozilla::baseprofiler::category::OTHER, {MarkerInnerWindowId(123)})); + + MOZ_RELEASE_ASSERT( + baseprofiler::AddMarker("markers 2.0 with one option (explicit init)", + mozilla::baseprofiler::category::OTHER, + MarkerOptions(MarkerInnerWindowId(123)))); + + MOZ_RELEASE_ASSERT(baseprofiler::AddMarker( + "markers 2.0 with one option (explicit brace-init)", + mozilla::baseprofiler::category::OTHER, + MarkerOptions{MarkerInnerWindowId(123)})); + + MOZ_RELEASE_ASSERT(baseprofiler::AddMarker( + "markers 2.0 with two options (implicit brace-init)", + mozilla::baseprofiler::category::OTHER, + {MarkerInnerWindowId(123), MarkerStack::Capture()})); + + MOZ_RELEASE_ASSERT(baseprofiler::AddMarker( + "markers 2.0 with two options (explicit init)", + mozilla::baseprofiler::category::OTHER, + MarkerOptions(MarkerInnerWindowId(123), MarkerStack::Capture()))); + + MOZ_RELEASE_ASSERT(baseprofiler::AddMarker( + "markers 2.0 with two options (explicit brace-init)", + mozilla::baseprofiler::category::OTHER, + MarkerOptions{MarkerInnerWindowId(123), MarkerStack::Capture()})); + + MOZ_RELEASE_ASSERT( + baseprofiler::AddMarker("default-templated markers 2.0 without options", + mozilla::baseprofiler::category::OTHER)); + + MOZ_RELEASE_ASSERT(baseprofiler::AddMarker( + "default-templated markers 2.0 with option", + mozilla::baseprofiler::category::OTHER, MarkerInnerWindowId(123))); + + MOZ_RELEASE_ASSERT(baseprofiler::AddMarker( + "explicitly-default-templated markers 2.0 without options", + mozilla::baseprofiler::category::OTHER, {}, + ::mozilla::baseprofiler::markers::NoPayload{})); + + MOZ_RELEASE_ASSERT(baseprofiler::AddMarker( + "explicitly-default-templated markers 2.0 with option", + mozilla::baseprofiler::category::OTHER, MarkerInnerWindowId(123), + ::mozilla::baseprofiler::markers::NoPayload{})); + + MOZ_RELEASE_ASSERT(baseprofiler::AddMarker( + "tracing", mozilla::baseprofiler::category::OTHER, {}, + mozilla::baseprofiler::markers::Tracing{}, "category")); + + MOZ_RELEASE_ASSERT(baseprofiler::AddMarker( + "text", mozilla::baseprofiler::category::OTHER, {}, + mozilla::baseprofiler::markers::TextMarker{}, "text text")); + + MOZ_RELEASE_ASSERT(baseprofiler::AddMarker( + "media sample", mozilla::baseprofiler::category::OTHER, {}, + mozilla::baseprofiler::markers::MediaSampleMarker{}, 123, 456)); + + printf("Sleep 1s...\n"); + { + AUTO_BASE_PROFILER_THREAD_SLEEP; + SleepMilli(1000); + } + + printf("baseprofiler_pause()...\n"); + baseprofiler::profiler_pause(); + + Maybe<baseprofiler::ProfilerBufferInfo> info = + baseprofiler::profiler_get_buffer_info(); + MOZ_RELEASE_ASSERT(info.isSome()); + printf("Profiler buffer range: %llu .. %llu (%llu bytes)\n", + static_cast<unsigned long long>(info->mRangeStart), + static_cast<unsigned long long>(info->mRangeEnd), + // sizeof(ProfileBufferEntry) == 9 + (static_cast<unsigned long long>(info->mRangeEnd) - + static_cast<unsigned long long>(info->mRangeStart)) * + 9); + printf("Stats: min(us) .. mean(us) .. max(us) [count]\n"); + printf("- Intervals: %7.1f .. %7.1f .. %7.1f [%u]\n", + info->mIntervalsUs.min, + info->mIntervalsUs.sum / info->mIntervalsUs.n, + info->mIntervalsUs.max, info->mIntervalsUs.n); + printf("- Overheads: %7.1f .. %7.1f .. %7.1f [%u]\n", + info->mOverheadsUs.min, + info->mOverheadsUs.sum / info->mOverheadsUs.n, + info->mOverheadsUs.max, info->mOverheadsUs.n); + printf(" - Locking: %7.1f .. %7.1f .. %7.1f [%u]\n", + info->mLockingsUs.min, info->mLockingsUs.sum / info->mLockingsUs.n, + info->mLockingsUs.max, info->mLockingsUs.n); + printf(" - Clearning: %7.1f .. %7.1f .. %7.1f [%u]\n", + info->mCleaningsUs.min, + info->mCleaningsUs.sum / info->mCleaningsUs.n, + info->mCleaningsUs.max, info->mCleaningsUs.n); + printf(" - Counters: %7.1f .. %7.1f .. %7.1f [%u]\n", + info->mCountersUs.min, info->mCountersUs.sum / info->mCountersUs.n, + info->mCountersUs.max, info->mCountersUs.n); + printf(" - Threads: %7.1f .. %7.1f .. %7.1f [%u]\n", + info->mThreadsUs.min, info->mThreadsUs.sum / info->mThreadsUs.n, + info->mThreadsUs.max, info->mThreadsUs.n); + + printf("baseprofiler_get_profile()...\n"); + UniquePtr<char[]> profile = baseprofiler::profiler_get_profile(); + + // Use a string view over the profile contents, for easier testing. + std::string_view profileSV = profile.get(); + + constexpr const auto svnpos = std::string_view::npos; + // TODO: Properly parse profile and check fields. + // Check for some expected marker schema JSON output. + MOZ_RELEASE_ASSERT(profileSV.find("\"markerSchema\": [") != svnpos); + MOZ_RELEASE_ASSERT(profileSV.find("\"name\": \"Text\",") != svnpos); + MOZ_RELEASE_ASSERT(profileSV.find("\"name\": \"tracing\",") != svnpos); + MOZ_RELEASE_ASSERT(profileSV.find("\"name\": \"MediaSample\",") != svnpos); + MOZ_RELEASE_ASSERT(profileSV.find("\"display\": [") != svnpos); + MOZ_RELEASE_ASSERT(profileSV.find("\"marker-chart\"") != svnpos); + MOZ_RELEASE_ASSERT(profileSV.find("\"marker-table\"") != svnpos); + MOZ_RELEASE_ASSERT(profileSV.find("\"format\": \"string\"") != svnpos); + // TODO: Add more checks for what's expected in the profile. Some of them + // are done in gtest's. + + printf("baseprofiler_save_profile_to_file()...\n"); + baseprofiler::profiler_save_profile_to_file("TestProfiler_profile.json"); + + printf("profiler_stop()...\n"); + baseprofiler::profiler_stop(); + + MOZ_RELEASE_ASSERT(!baseprofiler::profiler_is_active()); + MOZ_RELEASE_ASSERT(!baseprofiler::profiler_thread_is_being_profiled()); + MOZ_RELEASE_ASSERT(!baseprofiler::profiler_thread_is_sleeping()); + + printf("profiler_shutdown()...\n"); + } + + printf("TestProfiler done\n"); +} + +// Minimal string escaping, similar to how C++ stringliterals should be entered, +// to help update comparison strings in tests below. +void printEscaped(std::string_view aString) { + for (const char c : aString) { + switch (c) { + case '\n': + fprintf(stderr, "\\n\n"); + break; + case '"': + fprintf(stderr, "\\\""); + break; + case '\\': + fprintf(stderr, "\\\\"); + break; + default: + if (c >= ' ' && c <= '~') { + fprintf(stderr, "%c", c); + } else { + fprintf(stderr, "\\x%02x", unsigned(c)); + } + break; + } + } +} + +// Run aF(SpliceableChunkedJSONWriter&, UniqueJSONStrings&) from inside a JSON +// array, then output the string table, and compare the full output to +// aExpected. +template <typename F> +static void VerifyUniqueStringContents( + F&& aF, std::string_view aExpectedData, + std::string_view aExpectedUniqueStrings, + mozilla::baseprofiler::UniqueJSONStrings* aUniqueStringsOrNull = nullptr) { + mozilla::baseprofiler::SpliceableChunkedJSONWriter writer; + + // By default use a local UniqueJSONStrings, otherwise use the one provided. + mozilla::baseprofiler::UniqueJSONStrings localUniqueStrings( + mozilla::JSONWriter::SingleLineStyle); + mozilla::baseprofiler::UniqueJSONStrings& uniqueStrings = + aUniqueStringsOrNull ? *aUniqueStringsOrNull : localUniqueStrings; + + writer.Start(mozilla::JSONWriter::SingleLineStyle); + { + writer.StartArrayProperty("data", mozilla::JSONWriter::SingleLineStyle); + { std::forward<F>(aF)(writer, uniqueStrings); } + writer.EndArray(); + + writer.StartArrayProperty("stringTable", + mozilla::JSONWriter::SingleLineStyle); + { uniqueStrings.SpliceStringTableElements(writer); } + writer.EndArray(); + } + writer.End(); + + UniquePtr<char[]> jsonString = writer.ChunkedWriteFunc().CopyData(); + MOZ_RELEASE_ASSERT(jsonString); + std::string_view jsonStringView(jsonString.get()); + std::string expected = "{\"data\": ["; + expected += aExpectedData; + expected += "], \"stringTable\": ["; + expected += aExpectedUniqueStrings; + expected += "]}\n"; + if (jsonStringView != expected) { + fprintf(stderr, + "Expected:\n" + "------\n"); + printEscaped(expected); + fprintf(stderr, + "\n" + "------\n" + "Actual:\n" + "------\n"); + printEscaped(jsonStringView); + fprintf(stderr, + "\n" + "------\n"); + } + MOZ_RELEASE_ASSERT(jsonStringView == expected); +} + +void TestUniqueJSONStrings() { + printf("TestUniqueJSONStrings...\n"); + + using SCJW = mozilla::baseprofiler::SpliceableChunkedJSONWriter; + using UJS = mozilla::baseprofiler::UniqueJSONStrings; + + // Empty everything. + VerifyUniqueStringContents([](SCJW& aWriter, UJS& aUniqueStrings) {}, "", ""); + + // Empty unique strings. + VerifyUniqueStringContents( + [](SCJW& aWriter, UJS& aUniqueStrings) { + aWriter.StringElement("string"); + }, + R"("string")", ""); + + // One unique string. + VerifyUniqueStringContents( + [](SCJW& aWriter, UJS& aUniqueStrings) { + aUniqueStrings.WriteElement(aWriter, "string"); + }, + "0", R"("string")"); + + // One unique string twice. + VerifyUniqueStringContents( + [](SCJW& aWriter, UJS& aUniqueStrings) { + aUniqueStrings.WriteElement(aWriter, "string"); + aUniqueStrings.WriteElement(aWriter, "string"); + }, + "0, 0", R"("string")"); + + // Two single unique strings. + VerifyUniqueStringContents( + [](SCJW& aWriter, UJS& aUniqueStrings) { + aUniqueStrings.WriteElement(aWriter, "string0"); + aUniqueStrings.WriteElement(aWriter, "string1"); + }, + "0, 1", R"("string0", "string1")"); + + // Two unique strings with repetition. + VerifyUniqueStringContents( + [](SCJW& aWriter, UJS& aUniqueStrings) { + aUniqueStrings.WriteElement(aWriter, "string0"); + aUniqueStrings.WriteElement(aWriter, "string1"); + aUniqueStrings.WriteElement(aWriter, "string0"); + }, + "0, 1, 0", R"("string0", "string1")"); + + // Mix some object properties, for coverage. + VerifyUniqueStringContents( + [](SCJW& aWriter, UJS& aUniqueStrings) { + aUniqueStrings.WriteElement(aWriter, "string0"); + aWriter.StartObjectElement(mozilla::JSONWriter::SingleLineStyle); + { + aUniqueStrings.WriteProperty(aWriter, "p0", "prop"); + aUniqueStrings.WriteProperty(aWriter, "p1", "string0"); + aUniqueStrings.WriteProperty(aWriter, "p2", "prop"); + } + aWriter.EndObject(); + aUniqueStrings.WriteElement(aWriter, "string1"); + aUniqueStrings.WriteElement(aWriter, "string0"); + aUniqueStrings.WriteElement(aWriter, "prop"); + }, + R"(0, {"p0": 1, "p1": 0, "p2": 1}, 2, 0, 1)", + R"("string0", "prop", "string1")"); + + // Unique string table with pre-existing data. + { + UJS ujs(mozilla::JSONWriter::SingleLineStyle); + { + SCJW writer; + ujs.WriteElement(writer, "external0"); + ujs.WriteElement(writer, "external1"); + ujs.WriteElement(writer, "external0"); + } + VerifyUniqueStringContents( + [](SCJW& aWriter, UJS& aUniqueStrings) { + aUniqueStrings.WriteElement(aWriter, "string0"); + aUniqueStrings.WriteElement(aWriter, "string1"); + aUniqueStrings.WriteElement(aWriter, "string0"); + }, + "2, 3, 2", R"("external0", "external1", "string0", "string1")", &ujs); + } + + // Unique string table with pre-existing data from another table. + { + UJS ujs(mozilla::JSONWriter::SingleLineStyle); + { + SCJW writer; + ujs.WriteElement(writer, "external0"); + ujs.WriteElement(writer, "external1"); + ujs.WriteElement(writer, "external0"); + } + UJS ujsCopy(ujs, mozilla::JSONWriter::SingleLineStyle); + VerifyUniqueStringContents( + [](SCJW& aWriter, UJS& aUniqueStrings) { + aUniqueStrings.WriteElement(aWriter, "string0"); + aUniqueStrings.WriteElement(aWriter, "string1"); + aUniqueStrings.WriteElement(aWriter, "string0"); + }, + "2, 3, 2", R"("external0", "external1", "string0", "string1")", &ujs); + } + + // Unique string table through SpliceableJSONWriter. + VerifyUniqueStringContents( + [](SCJW& aWriter, UJS& aUniqueStrings) { + aWriter.SetUniqueStrings(aUniqueStrings); + aWriter.UniqueStringElement("string0"); + aWriter.StartObjectElement(mozilla::JSONWriter::SingleLineStyle); + { + aWriter.UniqueStringProperty("p0", "prop"); + aWriter.UniqueStringProperty("p1", "string0"); + aWriter.UniqueStringProperty("p2", "prop"); + } + aWriter.EndObject(); + aWriter.UniqueStringElement("string1"); + aWriter.UniqueStringElement("string0"); + aWriter.UniqueStringElement("prop"); + aWriter.ResetUniqueStrings(); + }, + R"(0, {"p0": 1, "p1": 0, "p2": 1}, 2, 0, 1)", + R"("string0", "prop", "string1")"); + + printf("TestUniqueJSONStrings done\n"); +} + +void StreamMarkers(const mozilla::ProfileChunkedBuffer& aBuffer, + mozilla::baseprofiler::SpliceableJSONWriter& aWriter) { + aWriter.StartArrayProperty("data"); + { + aBuffer.ReadEach([&](mozilla::ProfileBufferEntryReader& aEntryReader) { + mozilla::ProfileBufferEntryKind entryKind = + aEntryReader.ReadObject<mozilla::ProfileBufferEntryKind>(); + MOZ_RELEASE_ASSERT(entryKind == mozilla::ProfileBufferEntryKind::Marker); + + const bool success = + mozilla::base_profiler_markers_detail::DeserializeAfterKindAndStream( + aEntryReader, aWriter, 0, [&](mozilla::ProfileChunkedBuffer&) { + aWriter.StringElement("Real backtrace would be here"); + }); + MOZ_RELEASE_ASSERT(success); + }); + } + aWriter.EndArray(); +} + +void PrintMarkers(const mozilla::ProfileChunkedBuffer& aBuffer) { + mozilla::baseprofiler::SpliceableJSONWriter writer( + mozilla::MakeUnique<mozilla::baseprofiler::OStreamJSONWriteFunc>( + std::cout)); + mozilla::baseprofiler::UniqueJSONStrings uniqueStrings; + writer.SetUniqueStrings(uniqueStrings); + writer.Start(); + { + StreamMarkers(aBuffer, writer); + + writer.StartArrayProperty("stringTable"); + { uniqueStrings.SpliceStringTableElements(writer); } + writer.EndArray(); + } + writer.End(); + writer.ResetUniqueStrings(); +} + +static void SubTestMarkerCategory( + const mozilla::MarkerCategory& aMarkerCategory, + const mozilla::baseprofiler::ProfilingCategoryPair& aProfilingCategoryPair, + const mozilla::baseprofiler::ProfilingCategory& aProfilingCategory) { + MOZ_RELEASE_ASSERT(aMarkerCategory.CategoryPair() == aProfilingCategoryPair, + "Unexpected MarkerCategory::CategoryPair()"); + + MOZ_RELEASE_ASSERT( + mozilla::MarkerCategory(aProfilingCategoryPair).CategoryPair() == + aProfilingCategoryPair, + "MarkerCategory(<name>).CategoryPair() should return <name>"); + + MOZ_RELEASE_ASSERT(aMarkerCategory.GetCategory() == aProfilingCategory, + "Unexpected MarkerCategory::GetCategory()"); + + mozilla::ProfileBufferChunkManagerSingle chunkManager(512); + mozilla::ProfileChunkedBuffer buffer( + mozilla::ProfileChunkedBuffer::ThreadSafety::WithoutMutex, chunkManager); + mozilla::ProfileBufferBlockIndex i = buffer.PutObject(aMarkerCategory); + MOZ_RELEASE_ASSERT(i != mozilla::ProfileBufferBlockIndex{}, + "Failed serialization"); + buffer.ReadEach([&](mozilla::ProfileBufferEntryReader& aER, + mozilla::ProfileBufferBlockIndex aIndex) { + MOZ_RELEASE_ASSERT(aIndex == i, "Unexpected deserialization index"); + const auto readCategory = aER.ReadObject<mozilla::MarkerCategory>(); + MOZ_RELEASE_ASSERT(aER.RemainingBytes() == 0, + "Unexpected extra serialized bytes"); + MOZ_RELEASE_ASSERT(readCategory.CategoryPair() == aProfilingCategoryPair, + "Incorrect deserialization value"); + }); +} + +void TestMarkerCategory() { + printf("TestMarkerCategory...\n"); + + mozilla::ProfileBufferChunkManagerSingle chunkManager(512); + mozilla::ProfileChunkedBuffer buffer( + mozilla::ProfileChunkedBuffer::ThreadSafety::WithoutMutex, chunkManager); + +# define CATEGORY_ENUM_BEGIN_CATEGORY(name, labelAsString, color) +# define CATEGORY_ENUM_SUBCATEGORY(supercategory, name, labelAsString) \ + static_assert( \ + std::is_same_v<decltype(mozilla::baseprofiler::category::name), \ + const mozilla::MarkerCategory>, \ + "baseprofiler::category::<name> should be a const MarkerCategory"); \ + \ + SubTestMarkerCategory( \ + mozilla::baseprofiler::category::name, \ + mozilla::baseprofiler::ProfilingCategoryPair::name, \ + mozilla::baseprofiler::ProfilingCategory::supercategory); +# define CATEGORY_ENUM_END_CATEGORY + MOZ_PROFILING_CATEGORY_LIST(CATEGORY_ENUM_BEGIN_CATEGORY, + CATEGORY_ENUM_SUBCATEGORY, + CATEGORY_ENUM_END_CATEGORY) +# undef CATEGORY_ENUM_BEGIN_CATEGORY +# undef CATEGORY_ENUM_SUBCATEGORY +# undef CATEGORY_ENUM_END_CATEGORY + + printf("TestMarkerCategory done\n"); +} + +void TestMarkerThreadId() { + printf("TestMarkerThreadId...\n"); + + MOZ_RELEASE_ASSERT(MarkerThreadId{}.IsUnspecified()); + MOZ_RELEASE_ASSERT(!MarkerThreadId::MainThread().IsUnspecified()); + MOZ_RELEASE_ASSERT(!MarkerThreadId::CurrentThread().IsUnspecified()); + + MOZ_RELEASE_ASSERT(!MarkerThreadId{42}.IsUnspecified()); + MOZ_RELEASE_ASSERT(MarkerThreadId{42}.ThreadId() == 42); + + // We'll assume that this test runs in the main thread (which should be true + // when called from the `main` function). + MOZ_RELEASE_ASSERT(MarkerThreadId::MainThread().ThreadId() == + mozilla::baseprofiler::profiler_main_thread_id()); + + MOZ_RELEASE_ASSERT(MarkerThreadId::CurrentThread().ThreadId() == + mozilla::baseprofiler::profiler_current_thread_id()); + + MOZ_RELEASE_ASSERT(MarkerThreadId::CurrentThread().ThreadId() == + mozilla::baseprofiler::profiler_main_thread_id()); + + std::thread testThread([]() { + MOZ_RELEASE_ASSERT(!MarkerThreadId::MainThread().IsUnspecified()); + MOZ_RELEASE_ASSERT(!MarkerThreadId::CurrentThread().IsUnspecified()); + + MOZ_RELEASE_ASSERT(MarkerThreadId::MainThread().ThreadId() == + mozilla::baseprofiler::profiler_main_thread_id()); + + MOZ_RELEASE_ASSERT(MarkerThreadId::CurrentThread().ThreadId() == + mozilla::baseprofiler::profiler_current_thread_id()); + + MOZ_RELEASE_ASSERT(MarkerThreadId::CurrentThread().ThreadId() != + mozilla::baseprofiler::profiler_main_thread_id()); + }); + testThread.join(); + + printf("TestMarkerThreadId done\n"); +} + +void TestMarkerNoPayload() { + printf("TestMarkerNoPayload...\n"); + + mozilla::ProfileBufferChunkManagerSingle chunkManager(512); + mozilla::ProfileChunkedBuffer buffer( + mozilla::ProfileChunkedBuffer::ThreadSafety::WithoutMutex, chunkManager); + + mozilla::ProfileBufferBlockIndex i0 = + mozilla::baseprofiler::AddMarkerToBuffer( + buffer, "literal", mozilla::baseprofiler::category::OTHER_Profiling); + MOZ_RELEASE_ASSERT(i0); + + const std::string dynamic = "dynamic"; + mozilla::ProfileBufferBlockIndex i1 = + mozilla::baseprofiler::AddMarkerToBuffer( + buffer, dynamic, + mozilla::baseprofiler::category::GRAPHICS_FlushingAsyncPaints, {}); + MOZ_RELEASE_ASSERT(i1); + MOZ_RELEASE_ASSERT(i1 > i0); + + mozilla::ProfileBufferBlockIndex i2 = + mozilla::baseprofiler::AddMarkerToBuffer( + buffer, std::string_view("string_view"), + mozilla::baseprofiler::category::GRAPHICS_FlushingAsyncPaints, {}); + MOZ_RELEASE_ASSERT(i2); + MOZ_RELEASE_ASSERT(i2 > i1); + +# ifdef DEBUG + buffer.Dump(); +# endif + + PrintMarkers(buffer); + + printf("TestMarkerNoPayload done\n"); +} + +void TestUserMarker() { + printf("TestUserMarker...\n"); + + // User-defined marker type with text. + // It's fine to define it right in the function where it's used. + struct MarkerTypeTestMinimal { + static constexpr Span<const char> MarkerTypeName() { + return MakeStringSpan("test-minimal"); + } + static void StreamJSONMarkerData( + mozilla::baseprofiler::SpliceableJSONWriter& aWriter, + const std::string& aText) { + aWriter.StringProperty("text", aText); + } + static mozilla::MarkerSchema MarkerTypeDisplay() { + using MS = mozilla::MarkerSchema; + MS schema{MS::Location::markerChart, MS::Location::markerTable}; + schema.SetTooltipLabel("tooltip for test-minimal"); + schema.AddKeyLabelFormatSearchable("text", "Text", MS::Format::string, + MS::Searchable::searchable); + return schema; + } + }; + + mozilla::ProfileBufferChunkManagerSingle chunkManager(1024); + mozilla::ProfileChunkedBuffer buffer( + mozilla::ProfileChunkedBuffer::ThreadSafety::WithoutMutex, chunkManager); + + MOZ_RELEASE_ASSERT(mozilla::baseprofiler::AddMarkerToBuffer( + buffer, "test2", mozilla::baseprofiler::category::OTHER_Profiling, {}, + MarkerTypeTestMinimal{}, std::string("payload text"))); + + MOZ_RELEASE_ASSERT(mozilla::baseprofiler::AddMarkerToBuffer( + buffer, "test2", mozilla::baseprofiler::category::OTHER_Profiling, + mozilla::MarkerThreadId(123), MarkerTypeTestMinimal{}, + std::string("ThreadId(123)"))); + + auto start = mozilla::TimeStamp::NowUnfuzzed(); + + MOZ_RELEASE_ASSERT(mozilla::baseprofiler::AddMarkerToBuffer( + buffer, "test2", mozilla::baseprofiler::category::OTHER_Profiling, + mozilla::MarkerTiming::InstantAt(start), MarkerTypeTestMinimal{}, + std::string("InstantAt(start)"))); + + auto then = mozilla::TimeStamp::NowUnfuzzed(); + + MOZ_RELEASE_ASSERT(mozilla::baseprofiler::AddMarkerToBuffer( + buffer, "test2", mozilla::baseprofiler::category::OTHER_Profiling, + mozilla::MarkerTiming::IntervalStart(start), MarkerTypeTestMinimal{}, + std::string("IntervalStart(start)"))); + + MOZ_RELEASE_ASSERT(mozilla::baseprofiler::AddMarkerToBuffer( + buffer, "test2", mozilla::baseprofiler::category::OTHER_Profiling, + mozilla::MarkerTiming::IntervalEnd(then), MarkerTypeTestMinimal{}, + std::string("IntervalEnd(then)"))); + + MOZ_RELEASE_ASSERT(mozilla::baseprofiler::AddMarkerToBuffer( + buffer, "test2", mozilla::baseprofiler::category::OTHER_Profiling, + mozilla::MarkerTiming::Interval(start, then), MarkerTypeTestMinimal{}, + std::string("Interval(start, then)"))); + + MOZ_RELEASE_ASSERT(mozilla::baseprofiler::AddMarkerToBuffer( + buffer, "test2", mozilla::baseprofiler::category::OTHER_Profiling, + mozilla::MarkerTiming::IntervalUntilNowFrom(start), + MarkerTypeTestMinimal{}, std::string("IntervalUntilNowFrom(start)"))); + + MOZ_RELEASE_ASSERT(mozilla::baseprofiler::AddMarkerToBuffer( + buffer, "test2", mozilla::baseprofiler::category::OTHER_Profiling, + mozilla::MarkerStack::NoStack(), MarkerTypeTestMinimal{}, + std::string("NoStack"))); + // Note: We cannot test stack-capture here, because the profiler is not + // initialized. + + MOZ_RELEASE_ASSERT(mozilla::baseprofiler::AddMarkerToBuffer( + buffer, "test2", mozilla::baseprofiler::category::OTHER_Profiling, + mozilla::MarkerInnerWindowId(123), MarkerTypeTestMinimal{}, + std::string("InnerWindowId(123)"))); + +# ifdef DEBUG + buffer.Dump(); +# endif + + PrintMarkers(buffer); + + printf("TestUserMarker done\n"); +} + +void TestPredefinedMarkers() { + printf("TestPredefinedMarkers...\n"); + + mozilla::ProfileBufferChunkManagerSingle chunkManager(1024); + mozilla::ProfileChunkedBuffer buffer( + mozilla::ProfileChunkedBuffer::ThreadSafety::WithoutMutex, chunkManager); + + MOZ_RELEASE_ASSERT(mozilla::baseprofiler::AddMarkerToBuffer( + buffer, std::string_view("tracing"), + mozilla::baseprofiler::category::OTHER, {}, + mozilla::baseprofiler::markers::Tracing{}, "category")); + + MOZ_RELEASE_ASSERT(mozilla::baseprofiler::AddMarkerToBuffer( + buffer, std::string_view("text"), mozilla::baseprofiler::category::OTHER, + {}, mozilla::baseprofiler::markers::TextMarker{}, "text text")); + + MOZ_RELEASE_ASSERT(mozilla::baseprofiler::AddMarkerToBuffer( + buffer, std::string_view("media"), mozilla::baseprofiler::category::OTHER, + {}, mozilla::baseprofiler::markers::MediaSampleMarker{}, 123, 456)); + +# ifdef DEBUG + buffer.Dump(); +# endif + + PrintMarkers(buffer); + + printf("TestPredefinedMarkers done\n"); +} + +void TestProfilerMarkers() { + printf("TestProfilerMarkers -- pid: %d, tid: %d\n", + mozilla::baseprofiler::profiler_current_process_id(), + mozilla::baseprofiler::profiler_current_thread_id()); + // ::SleepMilli(10000); + + TestUniqueJSONStrings(); + TestMarkerCategory(); + TestMarkerThreadId(); + TestMarkerNoPayload(); + TestUserMarker(); + TestPredefinedMarkers(); + + printf("TestProfilerMarkers done\n"); +} + +#else // MOZ_GECKO_PROFILER + +// Testing that macros are still #defined (but do nothing) when +// MOZ_GECKO_PROFILER is disabled. +void TestProfiler() { + // These don't need to make sense, we just want to know that they're defined + // and don't do anything. + +# ifndef AUTO_BASE_PROFILER_INIT +# error AUTO_BASE_PROFILER_INIT not #defined +# endif // AUTO_BASE_PROFILER_INIT + AUTO_BASE_PROFILER_INIT; + + // This wouldn't build if the macro did output its arguments. +# ifndef AUTO_BASE_PROFILER_MARKER_TEXT +# error AUTO_BASE_PROFILER_MARKER_TEXT not #defined +# endif // AUTO_BASE_PROFILER_MARKER_TEXT + AUTO_BASE_PROFILER_MARKER_TEXT(catch, catch, catch, catch); + +# ifndef AUTO_BASE_PROFILER_LABEL +# error AUTO_BASE_PROFILER_LABEL not #defined +# endif // AUTO_BASE_PROFILER_LABEL + AUTO_BASE_PROFILER_LABEL(catch, catch); + +# ifndef AUTO_BASE_PROFILER_THREAD_SLEEP +# error AUTO_BASE_PROFILER_THREAD_SLEEP not #defined +# endif // AUTO_BASE_PROFILER_THREAD_SLEEP + AUTO_BASE_PROFILER_THREAD_SLEEP; + +# ifndef BASE_PROFILER_MARKER_UNTYPED +# error BASE_PROFILER_MARKER_UNTYPED not #defined +# endif // BASE_PROFILER_MARKER_UNTYPED + BASE_PROFILER_MARKER_UNTYPED(catch, catch); + BASE_PROFILER_MARKER_UNTYPED(catch, catch, catch); + +# ifndef BASE_PROFILER_MARKER +# error BASE_PROFILER_MARKER not #defined +# endif // BASE_PROFILER_MARKER + BASE_PROFILER_MARKER(catch, catch, catch, catch); + BASE_PROFILER_MARKER(catch, catch, catch, catch, catch); + +# ifndef BASE_PROFILER_MARKER_TEXT +# error BASE_PROFILER_MARKER_TEXT not #defined +# endif // BASE_PROFILER_MARKER_TEXT + BASE_PROFILER_MARKER_TEXT(catch, catch, catch, catch); + + MOZ_RELEASE_ASSERT(!mozilla::baseprofiler::profiler_get_backtrace(), + "profiler_get_backtrace should return nullptr"); + mozilla::ProfileChunkedBuffer buffer; + MOZ_RELEASE_ASSERT( + !mozilla::baseprofiler::profiler_capture_backtrace_into(buffer), + "profiler_capture_backtrace_into should return false"); + MOZ_RELEASE_ASSERT(!mozilla::baseprofiler::profiler_capture_backtrace(), + "profiler_capture_backtrace should return nullptr"); +} + +// Testing that macros are still #defined (but do nothing) when +// MOZ_GECKO_PROFILER is disabled. +void TestProfilerMarkers() { + // These don't need to make sense, we just want to know that they're defined + // and don't do anything. +} + +#endif // MOZ_GECKO_PROFILER else + +#if defined(XP_WIN) +int wmain() +#else +int main() +#endif // defined(XP_WIN) +{ +#ifdef MOZ_GECKO_PROFILER + printf("BaseTestProfiler -- pid: %d, tid: %d\n", + baseprofiler::profiler_current_process_id(), + baseprofiler::profiler_current_thread_id()); + // ::SleepMilli(10000); +#endif // MOZ_GECKO_PROFILER + + // Note that there are two `TestProfiler{,Markers}` functions above, depending + // on whether MOZ_GECKO_PROFILER is #defined. + TestProfiler(); + TestProfilerMarkers(); + + return 0; +} diff --git a/mozglue/tests/TestNativeNt.cpp b/mozglue/tests/TestNativeNt.cpp new file mode 100644 index 0000000000..77cd3ad4a3 --- /dev/null +++ b/mozglue/tests/TestNativeNt.cpp @@ -0,0 +1,295 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "nscore.h" +#include "mozilla/NativeNt.h" +#include "mozilla/ThreadLocal.h" +#include "mozilla/UniquePtr.h" + +#include <stdio.h> +#include <windows.h> + +const wchar_t kNormal[] = L"Foo.dll"; +const wchar_t kHex12[] = L"Foo.ABCDEF012345.dll"; +const wchar_t kHex15[] = L"ABCDEF012345678.dll"; +const wchar_t kHex16[] = L"ABCDEF0123456789.dll"; +const wchar_t kHex17[] = L"ABCDEF0123456789a.dll"; +const wchar_t kHex24[] = L"ABCDEF0123456789cdabef98.dll"; +const wchar_t kHex8[] = L"01234567.dll"; +const wchar_t kNonHex12[] = L"Foo.ABCDEFG12345.dll"; +const wchar_t kHex13[] = L"Foo.ABCDEF0123456.dll"; +const wchar_t kHex11[] = L"Foo.ABCDEF01234.dll"; +const wchar_t kPrefixedHex16[] = L"Pabcdef0123456789.dll"; +const uint32_t kTlsDataValue = 1234; +static MOZ_THREAD_LOCAL(uint32_t) sTlsData; + +const char kFailFmt[] = + "TEST-FAILED | NativeNt | %s(%s) should have returned %s but did not\n"; + +#define RUN_TEST(fn, varName, expected) \ + if (fn(varName) == !expected) { \ + printf(kFailFmt, #fn, #varName, #expected); \ + return 1; \ + } + +#define EXPECT_FAIL(fn, varName) RUN_TEST(fn, varName, false) + +#define EXPECT_SUCCESS(fn, varName) RUN_TEST(fn, varName, true) + +using namespace mozilla; +using namespace mozilla::nt; + +bool TestVirtualQuery(HANDLE aProcess, LPCVOID aAddress) { + MEMORY_BASIC_INFORMATION info1 = {}, info2 = {}; + SIZE_T result1 = ::VirtualQueryEx(aProcess, aAddress, &info1, sizeof(info1)), + result2 = mozilla::nt::VirtualQueryEx(aProcess, aAddress, &info2, + sizeof(info2)); + if (result1 != result2) { + printf("TEST-FAILED | NativeNt | The returned values mismatch\n"); + return false; + } + + if (!result1) { + // Both APIs failed. + return true; + } + + if (memcmp(&info1, &info2, result1) != 0) { + printf("TEST-FAILED | NativeNt | The returned structures mismatch\n"); + return false; + } + + return true; +} + +LauncherResult<HMODULE> GetModuleHandleFromLeafName(const wchar_t* aName) { + UNICODE_STRING name; + ::RtlInitUnicodeString(&name, aName); + return nt::GetModuleHandleFromLeafName(name); +} + +// Need a non-inline function to bypass compiler optimization that the thread +// local storage pointer is cached in a register before accessing a thread-local +// variable. +MOZ_NEVER_INLINE PVOID SwapThreadLocalStoragePointer(PVOID aNewValue) { + auto oldValue = RtlGetThreadLocalStoragePointer(); + RtlSetThreadLocalStoragePointerForTestingOnly(aNewValue); + return oldValue; +} + +int wmain(int argc, wchar_t* argv[]) { + UNICODE_STRING normal; + ::RtlInitUnicodeString(&normal, kNormal); + + UNICODE_STRING hex12; + ::RtlInitUnicodeString(&hex12, kHex12); + + UNICODE_STRING hex16; + ::RtlInitUnicodeString(&hex16, kHex16); + + UNICODE_STRING hex24; + ::RtlInitUnicodeString(&hex24, kHex24); + + UNICODE_STRING hex8; + ::RtlInitUnicodeString(&hex8, kHex8); + + UNICODE_STRING nonHex12; + ::RtlInitUnicodeString(&nonHex12, kNonHex12); + + UNICODE_STRING hex13; + ::RtlInitUnicodeString(&hex13, kHex13); + + UNICODE_STRING hex11; + ::RtlInitUnicodeString(&hex11, kHex11); + + UNICODE_STRING hex15; + ::RtlInitUnicodeString(&hex15, kHex15); + + UNICODE_STRING hex17; + ::RtlInitUnicodeString(&hex17, kHex17); + + UNICODE_STRING prefixedHex16; + ::RtlInitUnicodeString(&prefixedHex16, kPrefixedHex16); + + EXPECT_FAIL(Contains12DigitHexString, normal); + EXPECT_SUCCESS(Contains12DigitHexString, hex12); + EXPECT_FAIL(Contains12DigitHexString, hex13); + EXPECT_FAIL(Contains12DigitHexString, hex11); + EXPECT_FAIL(Contains12DigitHexString, hex16); + EXPECT_FAIL(Contains12DigitHexString, nonHex12); + + EXPECT_FAIL(IsFileNameAtLeast16HexDigits, normal); + EXPECT_FAIL(IsFileNameAtLeast16HexDigits, hex12); + EXPECT_SUCCESS(IsFileNameAtLeast16HexDigits, hex24); + EXPECT_SUCCESS(IsFileNameAtLeast16HexDigits, hex16); + EXPECT_SUCCESS(IsFileNameAtLeast16HexDigits, hex17); + EXPECT_FAIL(IsFileNameAtLeast16HexDigits, hex8); + EXPECT_FAIL(IsFileNameAtLeast16HexDigits, hex15); + EXPECT_FAIL(IsFileNameAtLeast16HexDigits, prefixedHex16); + + if (RtlGetProcessHeap() != ::GetProcessHeap()) { + printf("TEST-FAILED | NativeNt | RtlGetProcessHeap() is broken\n"); + return 1; + } + +#ifdef HAVE_SEH_EXCEPTIONS + PVOID origTlsHead = nullptr; + bool isExceptionThrown = false; + // Touch sTlsData.get() several times to prevent the call to sTlsData.set() + // from being optimized out in PGO build. + printf("sTlsData#1 = %08x\n", sTlsData.get()); + MOZ_SEH_TRY { + // Need to call SwapThreadLocalStoragePointer inside __try to make sure + // accessing sTlsData is caught by SEH. This is due to clang's design. + // https://bugs.llvm.org/show_bug.cgi?id=44174. + origTlsHead = SwapThreadLocalStoragePointer(nullptr); + sTlsData.set(~kTlsDataValue); + } + MOZ_SEH_EXCEPT(GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION + ? EXCEPTION_EXECUTE_HANDLER + : EXCEPTION_CONTINUE_SEARCH) { + isExceptionThrown = true; + } + SwapThreadLocalStoragePointer(origTlsHead); + printf("sTlsData#2 = %08x\n", sTlsData.get()); + sTlsData.set(kTlsDataValue); + printf("sTlsData#3 = %08x\n", sTlsData.get()); + if (!isExceptionThrown || sTlsData.get() != kTlsDataValue) { + printf( + "TEST-FAILED | NativeNt | RtlGetThreadLocalStoragePointer() is " + "broken\n"); + return 1; + } +#endif + + if (RtlGetCurrentThreadId() != ::GetCurrentThreadId()) { + printf("TEST-FAILED | NativeNt | RtlGetCurrentThreadId() is broken\n"); + return 1; + } + + const wchar_t kKernel32[] = L"kernel32.dll"; + DWORD verInfoSize = ::GetFileVersionInfoSizeW(kKernel32, nullptr); + if (!verInfoSize) { + printf( + "TEST-FAILED | NativeNt | Call to GetFileVersionInfoSizeW failed with " + "code %lu\n", + ::GetLastError()); + return 1; + } + + auto verInfoBuf = MakeUnique<char[]>(verInfoSize); + + if (!::GetFileVersionInfoW(kKernel32, 0, verInfoSize, verInfoBuf.get())) { + printf( + "TEST-FAILED | NativeNt | Call to GetFileVersionInfoW failed with code " + "%lu\n", + ::GetLastError()); + return 1; + } + + UINT len; + VS_FIXEDFILEINFO* fixedFileInfo = nullptr; + if (!::VerQueryValueW(verInfoBuf.get(), L"\\", (LPVOID*)&fixedFileInfo, + &len)) { + printf( + "TEST-FAILED | NativeNt | Call to VerQueryValueW failed with code " + "%lu\n", + ::GetLastError()); + return 1; + } + + const uint64_t expectedVersion = + (static_cast<uint64_t>(fixedFileInfo->dwFileVersionMS) << 32) | + static_cast<uint64_t>(fixedFileInfo->dwFileVersionLS); + + PEHeaders k32headers(::GetModuleHandleW(kKernel32)); + if (!k32headers) { + printf( + "TEST-FAILED | NativeNt | Failed parsing kernel32.dll's PE headers\n"); + return 1; + } + + uint64_t version; + if (!k32headers.GetVersionInfo(version)) { + printf( + "TEST-FAILED | NativeNt | Unable to obtain version information from " + "kernel32.dll\n"); + return 1; + } + + if (version != expectedVersion) { + printf( + "TEST-FAILED | NativeNt | kernel32.dll's detected version " + "(0x%016llX) does not match expected version (0x%016llX)\n", + version, expectedVersion); + return 1; + } + + Maybe<Span<IMAGE_THUNK_DATA>> iatThunks = + k32headers.GetIATThunksForModule("kernel32.dll"); + if (iatThunks) { + printf( + "TEST-FAILED | NativeNt | Detected the IAT thunk for kernel32 " + "in kernel32.dll\n"); + return 1; + } + + PEHeaders ntdllheaders(::GetModuleHandleW(L"ntdll.dll")); + + auto ntdllBoundaries = ntdllheaders.GetBounds(); + if (!ntdllBoundaries) { + printf( + "TEST-FAILED | NativeNt | " + "Unable to obtain the boundaries of ntdll.dll\n"); + return 1; + } + + iatThunks = + k32headers.GetIATThunksForModule("ntdll.dll", ntdllBoundaries.ptr()); + if (!iatThunks) { + printf( + "TEST-FAILED | NativeNt | Unable to find the IAT thunk for " + "ntdll.dll in kernel32.dll\n"); + return 1; + } + + // To test the Ex version of API, we purposely get a real handle + // instead of a pseudo handle. + nsAutoHandle process( + ::OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, GetCurrentProcessId())); + if (!process) { + printf("TEST-FAILED | NativeNt | OpenProcess() failed - %08lx\n", + ::GetLastError()); + return 1; + } + + // Test Null page, Heap, Mapped image, and Invalid handle + if (!TestVirtualQuery(process, nullptr) || !TestVirtualQuery(process, argv) || + !TestVirtualQuery(process, kNormal) || + !TestVirtualQuery(nullptr, kNormal)) { + return 1; + } + + auto moduleResult = GetModuleHandleFromLeafName(kKernel32); + if (moduleResult.isErr() || + moduleResult.inspect() != k32headers.template RVAToPtr<HMODULE>(0)) { + printf( + "TEST-FAILED | NativeNt | " + "GetModuleHandleFromLeafName returns a wrong value.\n"); + return 1; + } + + moduleResult = GetModuleHandleFromLeafName(L"invalid"); + if (moduleResult.isOk()) { + printf( + "TEST-FAILED | NativeNt | " + "GetModuleHandleFromLeafName unexpectedly returns a value.\n"); + return 1; + } + + printf("TEST-PASS | NativeNt | All tests ran successfully\n"); + return 0; +} diff --git a/mozglue/tests/TestPEExportSection.cpp b/mozglue/tests/TestPEExportSection.cpp new file mode 100644 index 0000000000..0a4d33255f --- /dev/null +++ b/mozglue/tests/TestPEExportSection.cpp @@ -0,0 +1,698 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +// This test makes sure mozilla::nt::PEExportSection can parse the export +// section of a local process, and a remote process even though it's +// modified by an external code. + +#include "mozilla/CmdLineAndEnvUtils.h" +#include "mozilla/NativeNt.h" +#include "nsWindowsDllInterceptor.h" + +#include <stdio.h> +#include <windows.h> + +#define EXPORT_FUNCTION_EQ(name, func) \ + (GetProcAddress(imageBase, name) == reinterpret_cast<void*>(func)) + +#define VERIFY_EXPORT_FUNCTION(tables, name, expected, errorMessage) \ + do { \ + if (tables.GetProcAddress(name) != reinterpret_cast<void*>(expected)) { \ + printf("TEST-FAILED | TestPEExportSection | %s", errorMessage); \ + return kTestFail; \ + } \ + } while (0) + +using namespace mozilla::nt; +using mozilla::interceptor::MMPolicyInProcess; +using mozilla::interceptor::MMPolicyOutOfProcess; +using LocalPEExportSection = PEExportSection<MMPolicyInProcess>; +using RemotePEExportSection = PEExportSection<MMPolicyOutOfProcess>; + +constexpr DWORD kEventTimeoutinMs = 5000; +const wchar_t kProcessControlEventName[] = + L"TestPEExportSection.Process.Control.Event"; + +enum TestResult : int { + kTestSuccess = 0, + kTestFail, + kTestSkip, +}; + +// These strings start with the same keyword to make sure we don't do substring +// match. Moreover, kSecretFunctionInvalid is purposely longer than the +// combination of the other two strings and located in between the other two +// strings to effectively test binary search. +const char kSecretFunction[] = "Secret"; +const char kSecretFunctionInvalid[] = "Secret invalid long name"; +const char kSecretFunctionWithSuffix[] = "Secret2"; + +const wchar_t* kNoModification = L"--NoModification"; +const wchar_t* kNoExport = L"--NoExport"; +const wchar_t* kModifyTableEntry = L"--ModifyTableEntry"; +const wchar_t* kModifyTable = L"--ModifyTable"; +const wchar_t* kModifyDirectoryEntry = L"--ModifyDirectoryEntry"; +const wchar_t* kExportByOrdinal = L"--ExportByOrdinal"; + +// Use the global variable to pass the child process's error status to the +// parent process. We don't use a process's exit code to keep the test simple. +int gChildProcessStatus = 0; + +// These functions are exported by linker or export section tampering at +// runtime. Each of function bodies needs to be different to avoid ICF. +extern "C" __declspec(dllexport) int Export1() { return 0; } +extern "C" __declspec(dllexport) int Export2() { return 1; } +int SecretFunction1() { return 100; } +int SecretFunction2() { return 101; } + +// This class allocates a writable region downstream of the mapped image +// and prepares it as a valid export section. +class ExportDirectoryPatcher final { + static constexpr int kRegionAllocationTryLimit = 100; + static constexpr int kNumOfTableEntries = 2; + // VirtualAlloc sometimes fails if a desired base address is too small. + // Define a minimum desired base to reduce the number of allocation tries. + static constexpr uintptr_t kMinimumAllocationPoint = 0x8000000; + + struct ExportDirectory { + IMAGE_EXPORT_DIRECTORY mDirectoryHeader; + DWORD mExportAddressTable[kNumOfTableEntries]; + DWORD mExportNameTable[kNumOfTableEntries]; + WORD mExportOrdinalTable[kNumOfTableEntries]; + char mNameBuffer1[sizeof(kSecretFunction)]; + char mNameBuffer2[sizeof(kSecretFunctionWithSuffix)]; + + template <typename T> + static DWORD PtrToRVA(T aPtr, uintptr_t aBase) { + return reinterpret_cast<uintptr_t>(aPtr) - aBase; + } + + explicit ExportDirectory(uintptr_t aImageBase) : mDirectoryHeader{} { + mDirectoryHeader.Base = 1; + mExportAddressTable[0] = PtrToRVA(SecretFunction1, aImageBase); + mExportAddressTable[1] = PtrToRVA(SecretFunction2, aImageBase); + mExportNameTable[0] = PtrToRVA(mNameBuffer1, aImageBase); + mExportNameTable[1] = PtrToRVA(mNameBuffer2, aImageBase); + mExportOrdinalTable[0] = 0; + mExportOrdinalTable[1] = 1; + strcpy(mNameBuffer1, kSecretFunction); + strcpy(mNameBuffer2, kSecretFunctionWithSuffix); + } + }; + + uintptr_t mImageBase; + ExportDirectory* mNewExportDirectory; + + DWORD PtrToRVA(const void* aPtr) const { + return reinterpret_cast<uintptr_t>(aPtr) - mImageBase; + } + + public: + explicit ExportDirectoryPatcher(HMODULE aModule) + : mImageBase(PEHeaders::HModuleToBaseAddr<uintptr_t>(aModule)), + mNewExportDirectory(nullptr) { + SYSTEM_INFO si = {}; + ::GetSystemInfo(&si); + + int numPagesRequired = ((sizeof(ExportDirectory) - 1) / si.dwPageSize) + 1; + + uintptr_t desiredBase = mImageBase + si.dwAllocationGranularity; + desiredBase = std::max(desiredBase, kMinimumAllocationPoint); + + for (int i = 0; i < kRegionAllocationTryLimit; ++i) { + void* allocated = + ::VirtualAlloc(reinterpret_cast<void*>(desiredBase), + numPagesRequired * si.dwPageSize, + MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); + if (allocated) { + // Use the end of a allocated page as ExportDirectory in order to test + // the boundary between a commit page and a non-commited page. + allocated = reinterpret_cast<uint8_t*>(allocated) + + (numPagesRequired * si.dwPageSize) - + sizeof(ExportDirectory); + mNewExportDirectory = new (allocated) ExportDirectory(mImageBase); + return; + } + + desiredBase += si.dwAllocationGranularity; + } + + gChildProcessStatus = kTestSkip; + printf( + "TEST-SKIP | TestPEExportSection | " + "Giving up finding an allocatable space following the mapped image.\n"); + } + + ~ExportDirectoryPatcher() { + // Intentionally leave mNewExportDirectory leaked to keep a patched data + // available until the process is terminated. + } + + explicit operator bool() const { return !!mNewExportDirectory; } + + void PopulateDirectory(IMAGE_EXPORT_DIRECTORY& aOutput) const { + aOutput.NumberOfFunctions = aOutput.NumberOfNames = kNumOfTableEntries; + aOutput.AddressOfFunctions = + PtrToRVA(mNewExportDirectory->mExportAddressTable); + aOutput.AddressOfNames = PtrToRVA(mNewExportDirectory->mExportNameTable); + aOutput.AddressOfNameOrdinals = + PtrToRVA(mNewExportDirectory->mExportOrdinalTable); + } + + void PopulateDirectoryEntry(IMAGE_DATA_DIRECTORY& aOutput) const { + PopulateDirectory(mNewExportDirectory->mDirectoryHeader); + aOutput.VirtualAddress = PtrToRVA(&mNewExportDirectory->mDirectoryHeader); + aOutput.Size = sizeof(ExportDirectory); + } +}; + +// This exports SecretFunction1 as "Export1" by replacing an entry of the +// export address table. +void ModifyExportAddressTableEntry() { + MMPolicyInProcess policy; + HMODULE imageBase = ::GetModuleHandleW(nullptr); + auto ourExe = LocalPEExportSection::Get(imageBase, policy); + + auto addressTableEntry = + const_cast<DWORD*>(ourExe.FindExportAddressTableEntry("Export1")); + if (!addressTableEntry) { + gChildProcessStatus = kTestFail; + return; + } + + mozilla::AutoVirtualProtect protection( + addressTableEntry, sizeof(*addressTableEntry), PAGE_READWRITE); + if (!protection) { + gChildProcessStatus = kTestFail; + return; + } + + *addressTableEntry = reinterpret_cast<uintptr_t>(SecretFunction1) - + PEHeaders::HModuleToBaseAddr<uintptr_t>(imageBase); + + if (!EXPORT_FUNCTION_EQ("Export1", SecretFunction1) || + !EXPORT_FUNCTION_EQ("Export2", Export2)) { + gChildProcessStatus = kTestFail; + } +} + +// This switches the entire address table into one exporting SecretFunction1 +// and SecretFunction2. +void ModifyExportAddressTable() { + MMPolicyInProcess policy; + HMODULE imageBase = ::GetModuleHandleW(nullptr); + auto ourExe = LocalPEExportSection::Get(imageBase, policy); + + auto exportDirectory = ourExe.GetExportDirectory(); + if (!exportDirectory) { + gChildProcessStatus = kTestFail; + return; + } + + mozilla::AutoVirtualProtect protection( + exportDirectory, sizeof(*exportDirectory), PAGE_READWRITE); + if (!protection) { + gChildProcessStatus = kTestFail; + return; + } + + ExportDirectoryPatcher patcher(imageBase); + if (!patcher) { + return; + } + + patcher.PopulateDirectory(*exportDirectory); + + if (GetProcAddress(imageBase, "Export1") || + GetProcAddress(imageBase, "Export2") || + !EXPORT_FUNCTION_EQ(kSecretFunction, SecretFunction1) || + !EXPORT_FUNCTION_EQ(kSecretFunctionWithSuffix, SecretFunction2)) { + gChildProcessStatus = kTestFail; + } +} + +// This hides all export functions by setting the table size to 0. +void HideExportSection() { + HMODULE imageBase = ::GetModuleHandleW(nullptr); + PEHeaders ourExe(imageBase); + + auto sectionTable = + ourExe.GetImageDirectoryEntryPtr(IMAGE_DIRECTORY_ENTRY_EXPORT); + + mozilla::AutoVirtualProtect protection(sectionTable, sizeof(*sectionTable), + PAGE_READWRITE); + if (!protection) { + gChildProcessStatus = kTestFail; + return; + } + + sectionTable->VirtualAddress = sectionTable->Size = 0; + + if (GetProcAddress(imageBase, "Export1") || + GetProcAddress(imageBase, "Export2")) { + gChildProcessStatus = kTestFail; + } +} + +// This makes the export directory entry point to a new export section +// which exports SecretFunction1 and SecretFunction2. +void ModifyExportDirectoryEntry() { + HMODULE imageBase = ::GetModuleHandleW(nullptr); + PEHeaders ourExe(imageBase); + + auto sectionTable = + ourExe.GetImageDirectoryEntryPtr(IMAGE_DIRECTORY_ENTRY_EXPORT); + + mozilla::AutoVirtualProtect protection(sectionTable, sizeof(*sectionTable), + PAGE_READWRITE); + if (!protection) { + gChildProcessStatus = kTestFail; + return; + } + + ExportDirectoryPatcher patcher(imageBase); + if (!patcher) { + return; + } + + patcher.PopulateDirectoryEntry(*sectionTable); + + if (GetProcAddress(imageBase, "Export1") || + GetProcAddress(imageBase, "Export2") || + !EXPORT_FUNCTION_EQ(kSecretFunction, SecretFunction1) || + !EXPORT_FUNCTION_EQ(kSecretFunctionWithSuffix, SecretFunction2)) { + gChildProcessStatus = kTestFail; + } +} + +// This exports functions only by Ordinal by hiding the export name table. +void ExportByOrdinal() { + ModifyExportDirectoryEntry(); + if (gChildProcessStatus != kTestSuccess) { + return; + } + + MMPolicyInProcess policy; + HMODULE imageBase = ::GetModuleHandleW(nullptr); + auto ourExe = LocalPEExportSection::Get(imageBase, policy); + + auto exportDirectory = ourExe.GetExportDirectory(); + if (!exportDirectory) { + gChildProcessStatus = kTestFail; + return; + } + + exportDirectory->NumberOfNames = 0; + + if (GetProcAddress(imageBase, "Export1") || + GetProcAddress(imageBase, "Export2") || + GetProcAddress(imageBase, kSecretFunction) || + GetProcAddress(imageBase, kSecretFunctionWithSuffix) || + !EXPORT_FUNCTION_EQ(MAKEINTRESOURCE(1), SecretFunction1) || + !EXPORT_FUNCTION_EQ(MAKEINTRESOURCE(2), SecretFunction2)) { + gChildProcessStatus = kTestFail; + } +} + +class ChildProcess final { + nsAutoHandle mChildProcess; + nsAutoHandle mChildMainThread; + + public: + static int Main(const nsAutoHandle& aEvent, const wchar_t* aOption) { + if (wcscmp(aOption, kNoModification) == 0) { + ; + } else if (wcscmp(aOption, kNoExport) == 0) { + HideExportSection(); + } else if (wcscmp(aOption, kModifyTableEntry) == 0) { + ModifyExportAddressTableEntry(); + } else if (wcscmp(aOption, kModifyTable) == 0) { + ModifyExportAddressTable(); + } else if (wcscmp(aOption, kModifyDirectoryEntry) == 0) { + ModifyExportDirectoryEntry(); + } else if (wcscmp(aOption, kExportByOrdinal) == 0) { + ExportByOrdinal(); + } + + // Letting the parent process know the child process is ready. + ::SetEvent(aEvent); + + // The child process does not exit itself. It's force terminated by + // the parent process when all tests are done. + for (;;) { + ::Sleep(100); + } + return 0; + } + + ChildProcess(const wchar_t* aExecutable, const wchar_t* aOption, + const nsAutoHandle& aEvent, const nsAutoHandle& aJob) { + const wchar_t* childArgv[] = {aExecutable, aOption}; + auto cmdLine( + mozilla::MakeCommandLine(mozilla::ArrayLength(childArgv), childArgv)); + + STARTUPINFOW si = {sizeof(si)}; + PROCESS_INFORMATION pi; + BOOL ok = ::CreateProcessW(aExecutable, cmdLine.get(), nullptr, nullptr, + FALSE, 0, nullptr, nullptr, &si, &pi); + if (!ok) { + printf( + "TEST-FAILED | TestPEExportSection | " + "CreateProcessW falied - %08lx.\n", + GetLastError()); + return; + } + + if (aJob && !::AssignProcessToJobObject(aJob, pi.hProcess)) { + printf( + "TEST-FAILED | TestPEExportSection | " + "AssignProcessToJobObject falied - %08lx.\n", + GetLastError()); + ::TerminateProcess(pi.hProcess, 1); + return; + } + + // Wait until requested modification is done in the child process. + if (::WaitForSingleObject(aEvent, kEventTimeoutinMs) != WAIT_OBJECT_0) { + printf( + "TEST-FAILED | TestPEExportSection | " + "Child process was not ready in time.\n"); + return; + } + + mChildProcess.own(pi.hProcess); + mChildMainThread.own(pi.hThread); + } + + ~ChildProcess() { ::TerminateProcess(mChildProcess, 0); } + + operator HANDLE() const { return mChildProcess; } + + TestResult GetStatus() const { + TestResult status = kTestSuccess; + if (!::ReadProcessMemory(mChildProcess, &gChildProcessStatus, &status, + sizeof(status), nullptr)) { + status = kTestFail; + printf( + "TEST-FAILED | TestPEExportSection | " + "ReadProcessMemory failed - %08lx\n", + GetLastError()); + } + return status; + } +}; + +template <typename MMPolicy> +TestResult BasicTest(const MMPolicy& aMMPolicy) { + // Use ntdll.dll because it does not have any forwarder RVA. + HMODULE ntdllImageBase = ::GetModuleHandleW(L"ntdll.dll"); + auto ntdllExports = PEExportSection<MMPolicy>::Get(ntdllImageBase, aMMPolicy); + + auto exportDir = ntdllExports.GetExportDirectory(); + auto tableOfNames = + ntdllExports.template RVAToPtr<const PDWORD>(exportDir->AddressOfNames); + for (DWORD i = 0; i < exportDir->NumberOfNames; ++i) { + const auto name = + ntdllExports.template RVAToPtr<const char*>(tableOfNames[i]); + auto funcEntry = ntdllExports.FindExportAddressTableEntry(name); + if (ntdllExports.template RVAToPtr<const void*>(*funcEntry) != + ::GetProcAddress(ntdllImageBase, name)) { + printf( + "TEST-FAILED | TestPEExportSection | " + "FindExportAddressTableEntry did not resolve ntdll!%s.\n", + name); + return kTestFail; + } + } + + for (DWORD i = 0; i < 0x10000; i += 0x10) { + if (ntdllExports.GetProcAddress(MAKEINTRESOURCE(i)) != + ::GetProcAddress(ntdllImageBase, MAKEINTRESOURCE(i))) { + printf( + "TEST-FAILED | TestPEExportSection | " + "GetProcAddress did not resolve ntdll!Ordinal#%lu.\n", + i); + return kTestFail; + } + } + + // Test a known forwarder RVA. + auto k32Exports = PEExportSection<MMPolicy>::Get( + ::GetModuleHandleW(L"kernel32.dll"), aMMPolicy); + if (k32Exports.FindExportAddressTableEntry("HeapAlloc")) { + printf( + "TEST-FAILED | TestPEExportSection | " + "kernel32!HeapAlloc should be forwarded to ntdll!RtlAllocateHeap.\n"); + return kTestFail; + } + + // Test invalid names. + if (k32Exports.FindExportAddressTableEntry("Invalid name") || + k32Exports.FindExportAddressTableEntry("")) { + printf( + "TEST-FAILED | TestPEExportSection | " + "FindExportAddressTableEntry should return " + "nullptr for a non-existent name.\n"); + return kTestFail; + } + + return kTestSuccess; +} + +TestResult RunChildProcessTest( + const wchar_t* aExecutable, const wchar_t* aOption, + const nsAutoHandle& aEvent, const nsAutoHandle& aJob, + TestResult (*aTestCallback)(const RemotePEExportSection&)) { + ChildProcess childProcess(aExecutable, aOption, aEvent, aJob); + if (!childProcess) { + return kTestFail; + } + + auto result = childProcess.GetStatus(); + if (result != kTestSuccess) { + return result; + } + + MMPolicyOutOfProcess policy(childProcess); + + // One time is enough to run BasicTest in the child process. + static TestResult oneTimeResult = BasicTest<MMPolicyOutOfProcess>(policy); + if (oneTimeResult != kTestSuccess) { + return oneTimeResult; + } + + auto exportTableChild = + RemotePEExportSection::Get(::GetModuleHandleW(nullptr), policy); + return aTestCallback(exportTableChild); +} + +mozilla::LauncherResult<nsReturnRef<HANDLE>> CreateJobToLimitProcessLifetime() { + uint64_t version; + PEHeaders ntdllHeaders(::GetModuleHandleW(L"ntdll.dll")); + if (!ntdllHeaders.GetVersionInfo(version)) { + printf( + "TEST-FAILED | TestPEExportSection | " + "Unable to obtain version information from ntdll.dll\n"); + return LAUNCHER_ERROR_FROM_LAST(); + } + + constexpr uint64_t kWin8 = 0x60002ull << 32; + nsAutoHandle job; + + if (version < kWin8) { + // Since a process can be associated only with a single job in Win7 or + // older and this test program is already assigned with a job by + // infrastructure, we cannot use a job. + return job.out(); + } + + job.own(::CreateJobObject(nullptr, nullptr)); + if (!job) { + printf( + "TEST-FAILED | TestPEExportSection | " + "CreateJobObject falied - %08lx.\n", + GetLastError()); + return LAUNCHER_ERROR_FROM_LAST(); + } + + JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobInfo = {}; + jobInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + + if (!::SetInformationJobObject(job, JobObjectExtendedLimitInformation, + &jobInfo, sizeof(jobInfo))) { + printf( + "TEST-FAILED | TestPEExportSection | " + "SetInformationJobObject falied - %08lx.\n", + GetLastError()); + return LAUNCHER_ERROR_FROM_LAST(); + } + + return job.out(); +} + +extern "C" int wmain(int argc, wchar_t* argv[]) { + nsAutoHandle controlEvent( + ::CreateEventW(nullptr, FALSE, FALSE, kProcessControlEventName)); + + if (argc == 2) { + return ChildProcess::Main(controlEvent, argv[1]); + } + + if (argc != 1) { + printf( + "TEST-FAILED | TestPEExportSection | " + "Invalid arguments.\n"); + return kTestFail; + } + + MMPolicyInProcess policy; + if (BasicTest<MMPolicyInProcess>(policy)) { + return kTestFail; + } + + auto exportTableSelf = + LocalPEExportSection::Get(::GetModuleHandleW(nullptr), policy); + if (!exportTableSelf) { + printf( + "TEST-FAILED | TestPEExportSection | " + "LocalPEExportSection::Get failed.\n"); + return kTestFail; + } + + VERIFY_EXPORT_FUNCTION(exportTableSelf, "Export1", Export1, + "Local | Export1 was not exported.\n"); + VERIFY_EXPORT_FUNCTION(exportTableSelf, "Export2", Export2, + "Local | Export2 was not exported.\n"); + VERIFY_EXPORT_FUNCTION( + exportTableSelf, "Invalid name", 0, + "Local | GetProcAddress should return nullptr for an invalid name.\n"); + + // We'll add the child process to a job so that, in the event of a failure in + // this parent process, the child process will be automatically terminated. + auto probablyJob = CreateJobToLimitProcessLifetime(); + if (probablyJob.isErr()) { + return kTestFail; + } + + nsAutoHandle job(probablyJob.unwrap()); + + auto result = RunChildProcessTest( + argv[0], kNoModification, controlEvent, job, + [](const RemotePEExportSection& aTables) { + VERIFY_EXPORT_FUNCTION(aTables, "Export1", Export1, + "NoModification | Export1 was not exported.\n"); + VERIFY_EXPORT_FUNCTION(aTables, "Export2", Export2, + "NoModification | Export2 was not exported.\n"); + return kTestSuccess; + }); + if (result == kTestFail) { + return result; + } + + result = RunChildProcessTest( + argv[0], kNoExport, controlEvent, job, + [](const RemotePEExportSection& aTables) { + VERIFY_EXPORT_FUNCTION(aTables, "Export1", 0, + "NoExport | Export1 was exported.\n"); + VERIFY_EXPORT_FUNCTION(aTables, "Export2", 0, + "NoExport | Export2 was exported.\n"); + return kTestSuccess; + }); + if (result == kTestFail) { + return result; + } + + result = RunChildProcessTest( + argv[0], kModifyTableEntry, controlEvent, job, + [](const RemotePEExportSection& aTables) { + VERIFY_EXPORT_FUNCTION( + aTables, "Export1", SecretFunction1, + "ModifyTableEntry | SecretFunction1 was not exported.\n"); + VERIFY_EXPORT_FUNCTION( + aTables, "Export2", Export2, + "ModifyTableEntry | Export2 was not exported.\n"); + return kTestSuccess; + }); + if (result == kTestFail) { + return result; + } + + result = RunChildProcessTest( + argv[0], kModifyTable, controlEvent, job, + [](const RemotePEExportSection& aTables) { + VERIFY_EXPORT_FUNCTION(aTables, "Export1", 0, + "ModifyTable | Export1 was exported.\n"); + VERIFY_EXPORT_FUNCTION(aTables, "Export2", 0, + "ModifyTable | Export2 was exported.\n"); + VERIFY_EXPORT_FUNCTION( + aTables, kSecretFunction, SecretFunction1, + "ModifyTable | SecretFunction1 was not exported.\n"); + VERIFY_EXPORT_FUNCTION( + aTables, kSecretFunctionWithSuffix, SecretFunction2, + "ModifyTable | SecretFunction2 was not exported.\n"); + VERIFY_EXPORT_FUNCTION( + aTables, kSecretFunctionInvalid, 0, + "ModifyTable | kSecretFunctionInvalid was exported.\n"); + return kTestSuccess; + }); + if (result == kTestFail) { + return result; + } + + result = RunChildProcessTest( + argv[0], kModifyDirectoryEntry, controlEvent, job, + [](const RemotePEExportSection& aTables) { + VERIFY_EXPORT_FUNCTION( + aTables, "Export1", 0, + "ModifyDirectoryEntry | Export1 was exported.\n"); + VERIFY_EXPORT_FUNCTION( + aTables, "Export2", 0, + "ModifyDirectoryEntry | Export2 was exported.\n"); + VERIFY_EXPORT_FUNCTION( + aTables, kSecretFunction, SecretFunction1, + "ModifyDirectoryEntry | SecretFunction1 was not exported.\n"); + VERIFY_EXPORT_FUNCTION( + aTables, kSecretFunctionWithSuffix, SecretFunction2, + "ModifyDirectoryEntry | SecretFunction2 was not exported.\n"); + VERIFY_EXPORT_FUNCTION( + aTables, kSecretFunctionInvalid, 0, + "ModifyDirectoryEntry | kSecretFunctionInvalid was exported.\n"); + return kTestSuccess; + }); + if (result == kTestFail) { + return result; + } + + result = RunChildProcessTest( + argv[0], kExportByOrdinal, controlEvent, job, + [](const RemotePEExportSection& aTables) { + VERIFY_EXPORT_FUNCTION(aTables, "Export1", 0, + "ExportByOrdinal | Export1 was exported.\n"); + VERIFY_EXPORT_FUNCTION(aTables, "Export2", 0, + "ExportByOrdinal | Export2 was exported.\n"); + VERIFY_EXPORT_FUNCTION( + aTables, kSecretFunction, 0, + "ModifyDirectoryEntry | kSecretFunction was exported by name.\n"); + VERIFY_EXPORT_FUNCTION( + aTables, kSecretFunctionWithSuffix, 0, + "ModifyDirectoryEntry | " + "kSecretFunctionWithSuffix was exported by name.\n"); + VERIFY_EXPORT_FUNCTION( + aTables, MAKEINTRESOURCE(1), SecretFunction1, + "ModifyDirectoryEntry | " + "kSecretFunction was not exported by ordinal.\n"); + VERIFY_EXPORT_FUNCTION( + aTables, MAKEINTRESOURCE(2), SecretFunction2, + "ModifyDirectoryEntry | " + "kSecretFunctionWithSuffix was not exported by ordinal.\n"); + return kTestSuccess; + }); + if (result == kTestFail) { + return result; + } + + return kTestSuccess; +} diff --git a/mozglue/tests/TestPrintf.cpp b/mozglue/tests/TestPrintf.cpp new file mode 100644 index 0000000000..69fcfd51b2 --- /dev/null +++ b/mozglue/tests/TestPrintf.cpp @@ -0,0 +1,164 @@ +/* vim: set shiftwidth=2 tabstop=8 autoindent cindent expandtab: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/IntegerPrintfMacros.h" +#include "mozilla/Printf.h" + +#include <cfloat> +#include <stdarg.h> + +// A simple implementation of PrintfTarget, just for testing +// PrintfTarget::print. +class TestPrintfTarget : public mozilla::PrintfTarget { + public: + static const char* test_string; + + TestPrintfTarget() : mOut(0) { memset(mBuffer, '\0', sizeof(mBuffer)); } + + ~TestPrintfTarget() { + MOZ_RELEASE_ASSERT(mOut == strlen(test_string)); + MOZ_RELEASE_ASSERT(strncmp(mBuffer, test_string, strlen(test_string)) == 0); + } + + bool append(const char* sp, size_t len) override { + if (mOut + len < sizeof(mBuffer)) { + memcpy(&mBuffer[mOut], sp, len); + } + mOut += len; + return true; + } + + private: + char mBuffer[100]; + size_t mOut; +}; + +const char* TestPrintfTarget::test_string = "test string"; + +static void TestPrintfTargetPrint() { + TestPrintfTarget checker; + checker.print("test string"); +} + +static bool MOZ_FORMAT_PRINTF(2, 3) + print_one(const char* expect, const char* fmt, ...) { + va_list ap; + + va_start(ap, fmt); + mozilla::SmprintfPointer output = mozilla::Vsmprintf(fmt, ap); + va_end(ap); + + return output && !strcmp(output.get(), expect); +} + +static const char* zero() { return nullptr; } + +static void TestPrintfFormats() { + MOZ_RELEASE_ASSERT(print_one("0", "%d", 0)); + MOZ_RELEASE_ASSERT(print_one("23", "%d", 23)); + MOZ_RELEASE_ASSERT(print_one("+23", "%+d", 23)); + MOZ_RELEASE_ASSERT(print_one("-23", "%+d", -23)); + MOZ_RELEASE_ASSERT(print_one("0023", "%04d", 23)); + MOZ_RELEASE_ASSERT(print_one("777777", "%04d", 777777)); + MOZ_RELEASE_ASSERT(print_one(" 23", "% 4d", 23)); + MOZ_RELEASE_ASSERT(print_one("23 ", "%-4d", 23)); + MOZ_RELEASE_ASSERT(print_one(" 23", "%*d", 4, 23)); + MOZ_RELEASE_ASSERT(print_one("-23 ", "%*d", -7, -23)); + MOZ_RELEASE_ASSERT(print_one(" 077", "%5.3d", 77)); + MOZ_RELEASE_ASSERT(print_one(" 077", "%5.*d", 3, 77)); + MOZ_RELEASE_ASSERT(print_one(" 077", "%*.*d", 5, 3, 77)); + MOZ_RELEASE_ASSERT(print_one("077 ", "%*.*d", -5, 3, 77)); + MOZ_RELEASE_ASSERT(print_one("77 ", "%*.*d", -5, -3, 77)); + MOZ_RELEASE_ASSERT(print_one("-1", "%d", -1)); + MOZ_RELEASE_ASSERT(print_one("23", "%u", 23u)); + MOZ_RELEASE_ASSERT(print_one("0x17", "0x%x", 23u)); + MOZ_RELEASE_ASSERT(print_one("0xFF", "0x%X", 255u)); + MOZ_RELEASE_ASSERT(print_one("027", "0%o", 23u)); + MOZ_RELEASE_ASSERT(print_one("-1", "%hd", (short)-1)); + // A funny special case. + MOZ_RELEASE_ASSERT(print_one("", "%.*d", 0, 0)); + // This could be expanded if need be, it's just convenient to do + // it this way. + if (sizeof(short) == 2) { + MOZ_RELEASE_ASSERT(print_one("8000", "%hx", (unsigned short)0x8000)); + } + MOZ_RELEASE_ASSERT(print_one("2305", "%ld", 2305l)); + MOZ_RELEASE_ASSERT(print_one("-2305", "%ld", -2305l)); + MOZ_RELEASE_ASSERT(print_one("0xf0f0", "0x%lx", 0xf0f0ul)); + MOZ_RELEASE_ASSERT(print_one("0", "%lld", 0ll)); + MOZ_RELEASE_ASSERT(print_one("2305", "%lld", 2305ll)); + MOZ_RELEASE_ASSERT(print_one("-2305", "%lld", -2305ll)); + // A funny special case. + MOZ_RELEASE_ASSERT(print_one("", "%.*lld", 0, 0ll)); + MOZ_RELEASE_ASSERT(print_one("0xF0F0", "0x%llX", 0xf0f0ull)); + MOZ_RELEASE_ASSERT(print_one("27270", "%zu", (size_t)27270)); + MOZ_RELEASE_ASSERT(print_one("27270", "%zu", (size_t)27270)); + MOZ_RELEASE_ASSERT(print_one("hello", "he%so", "ll")); + MOZ_RELEASE_ASSERT(print_one("hello ", "%-8s", "hello")); + MOZ_RELEASE_ASSERT(print_one(" hello", "%8s", "hello")); + MOZ_RELEASE_ASSERT(print_one("hello ", "%*s", -8, "hello")); + MOZ_RELEASE_ASSERT(print_one("hello", "%.*s", 5, "hello there")); + MOZ_RELEASE_ASSERT(print_one("", "%.*s", 0, "hello there")); + MOZ_RELEASE_ASSERT(print_one("%%", "%%%%")); + MOZ_RELEASE_ASSERT(print_one("0", "%p", (char*)0)); + MOZ_RELEASE_ASSERT(print_one("h", "%c", 'h')); + MOZ_RELEASE_ASSERT(print_one("1.500000", "%f", 1.5f)); + MOZ_RELEASE_ASSERT(print_one("1.5", "%g", 1.5)); + MOZ_RELEASE_ASSERT(print_one("1.50000", "%.5f", 1.5)); + + MOZ_RELEASE_ASSERT(print_one("z ", "%-7s", "z")); + MOZ_RELEASE_ASSERT(print_one("z ", "%*s", -7, "z")); + MOZ_RELEASE_ASSERT(print_one("hello", "%*s", -3, "hello")); + + MOZ_RELEASE_ASSERT(print_one(" q", "%3c", 'q')); + MOZ_RELEASE_ASSERT(print_one("q ", "%-3c", 'q')); + MOZ_RELEASE_ASSERT(print_one(" q", "%*c", 3, 'q')); + MOZ_RELEASE_ASSERT(print_one("q ", "%*c", -3, 'q')); + + // Regression test for bug#1350097. The bug was an assertion + // failure caused by printing a very long floating point value. + print_one("ignore", "%lf", DBL_MAX); + + // Regression test for bug#1517433. The bug was an assertion + // failure caused by printing a floating point value with a large + // precision and/or width. + print_one("ignore", "%500.500lf", DBL_MAX); + + MOZ_RELEASE_ASSERT(print_one("2727", "%" PRIu32, (uint32_t)2727)); + MOZ_RELEASE_ASSERT(print_one("aa7", "%" PRIx32, (uint32_t)2727)); + MOZ_RELEASE_ASSERT(print_one("2727", "%" PRIu64, (uint64_t)2727)); + MOZ_RELEASE_ASSERT(print_one("aa7", "%" PRIx64, (uint64_t)2727)); + + int n1, n2; + MOZ_RELEASE_ASSERT(print_one(" hi ", "%n hi %n", &n1, &n2)); + MOZ_RELEASE_ASSERT(n1 == 0); + MOZ_RELEASE_ASSERT(n2 == 4); + + MOZ_RELEASE_ASSERT(print_one("23 % 24", "%2$ld %% %1$d", 24, 23l)); + MOZ_RELEASE_ASSERT( + print_one("7 8 9 10", "%4$lld %3$ld %2$d %1$hd", (short)10, 9, 8l, 7ll)); + + MOZ_RELEASE_ASSERT(print_one("0 ", "%2$p %1$n", &n1, zero())); + MOZ_RELEASE_ASSERT(n1 == 2); + + MOZ_RELEASE_ASSERT(print_one("23 % 024", "%2$-3ld%%%1$4.3d", 24, 23l)); + MOZ_RELEASE_ASSERT(print_one("23 1.5", "%2$d %1$g", 1.5, 23)); + MOZ_RELEASE_ASSERT( + print_one("ff number FF", "%3$llx %1$s %2$lX", "number", 255ul, 255ull)); + MOZ_RELEASE_ASSERT( + print_one("7799 9977", "%2$zu %1$zu", (size_t)9977, (size_t)7799)); +} + +#if defined(XP_WIN) +int wmain() +#else +int main() +#endif // defined(XP_WIN) +{ + TestPrintfFormats(); + TestPrintfTargetPrint(); + + return 0; +} diff --git a/mozglue/tests/TestTimeStampWin.cpp b/mozglue/tests/TestTimeStampWin.cpp new file mode 100644 index 0000000000..4e7437450a --- /dev/null +++ b/mozglue/tests/TestTimeStampWin.cpp @@ -0,0 +1,97 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "mozilla/CmdLineAndEnvUtils.h" +#include "mozilla/TimeStamp.h" + +#include "nsWindowsHelpers.h" + +#include <stdio.h> +#include <windows.h> + +static wchar_t kChildArg[] = L"--child"; + +static nsReturnRef<HANDLE> CreateProcessWrapper(const wchar_t* aPath) { + nsAutoHandle empty; + + const wchar_t* childArgv[] = {aPath, kChildArg}; + mozilla::UniquePtr<wchar_t[]> cmdLine( + mozilla::MakeCommandLine(mozilla::ArrayLength(childArgv), childArgv)); + + STARTUPINFOW si = {sizeof(si)}; + PROCESS_INFORMATION pi; + BOOL ok = ::CreateProcessW(aPath, cmdLine.get(), nullptr, nullptr, FALSE, 0, + nullptr, nullptr, &si, &pi); + if (!ok) { + printf( + "TEST-FAILED | TimeStampWin | " + "CreateProcess failed - %08lx\n", + GetLastError()); + return empty.out(); + } + + nsAutoHandle proc(pi.hProcess); + nsAutoHandle thd(pi.hThread); + + return proc.out(); +} + +int ChildMain() { + // Make sure a process creation timestamp is always not bigger than + // the current timestamp. + bool inconsistent = false; + auto t0 = mozilla::TimeStamp::ProcessCreation(&inconsistent); + auto t1 = mozilla::TimeStamp::Now(); + if (t0 > t1) { + printf( + "TEST-FAILED | TimeStampWin | " + "Process creation timestamp is bigger than the current " + "timestamp!\n"); + return 1; + } + return 0; +} + +int wmain(int argc, wchar_t* argv[]) { + if (argc == 2 && wcscmp(argv[1], kChildArg) == 0) { + return ChildMain(); + } + + if (argc != 1) { + printf( + "TEST-FAILED | TimeStampWin | " + "Unexpected argc\n"); + return 1; + } + + // Start a child process successively, checking any of them terminates with + // a non-zero value which means an error. + for (int i = 0; i < 20; ++i) { + nsAutoHandle childProc(CreateProcessWrapper(argv[0])); + + if (::WaitForSingleObject(childProc, 60000) != WAIT_OBJECT_0) { + printf( + "TEST-FAILED | TimeStampWin | " + "Unexpected result from WaitForSingleObject\n"); + return 1; + } + + DWORD childExitCode; + if (!::GetExitCodeProcess(childProc.get(), &childExitCode)) { + printf( + "TEST-FAILED | TimeStampWin | " + "GetExitCodeProcess failed - %08lx\n", + GetLastError()); + return 1; + } + + if (childExitCode != 0) { + return childExitCode; + } + } + + return 0; +} diff --git a/mozglue/tests/gtest/TestDLLBlocklist.cpp b/mozglue/tests/gtest/TestDLLBlocklist.cpp new file mode 100644 index 0000000000..97c4ea9f9b --- /dev/null +++ b/mozglue/tests/gtest/TestDLLBlocklist.cpp @@ -0,0 +1,161 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <windows.h> +#include <winternl.h> + +#include <process.h> + +#include "gtest/gtest.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/Char16.h" +#include "nsDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsWindowsHelpers.h" + +static nsString GetFullPath(const nsAString& aLeaf) { + nsCOMPtr<nsIFile> f; + + EXPECT_TRUE(NS_SUCCEEDED( + NS_GetSpecialDirectory(NS_OS_CURRENT_WORKING_DIR, getter_AddRefs(f)))); + + EXPECT_TRUE(NS_SUCCEEDED(f->Append(aLeaf))); + + bool exists; + EXPECT_TRUE(NS_SUCCEEDED(f->Exists(&exists)) && exists); + + nsString ret; + EXPECT_TRUE(NS_SUCCEEDED(f->GetPath(ret))); + return ret; +} + +TEST(TestDllBlocklist, BlockDllByName) +{ + // The DLL name has capital letters, so this also tests that the comparison + // is case-insensitive. + constexpr auto kLeafName = u"TestDllBlocklist_MatchByName.dll"_ns; + nsString dllPath = GetFullPath(kLeafName); + + nsModuleHandle hDll(::LoadLibraryW(dllPath.get())); + + EXPECT_TRUE(!hDll); + EXPECT_TRUE(!::GetModuleHandleW(kLeafName.get())); +} + +TEST(TestDllBlocklist, BlockDllByVersion) +{ + constexpr auto kLeafName = u"TestDllBlocklist_MatchByVersion.dll"_ns; + nsString dllPath = GetFullPath(kLeafName); + + nsModuleHandle hDll(::LoadLibraryW(dllPath.get())); + + EXPECT_TRUE(!hDll); + EXPECT_TRUE(!::GetModuleHandleW(kLeafName.get())); +} + +TEST(TestDllBlocklist, AllowDllByVersion) +{ + constexpr auto kLeafName = u"TestDllBlocklist_AllowByVersion.dll"_ns; + nsString dllPath = GetFullPath(kLeafName); + + nsModuleHandle hDll(::LoadLibraryW(dllPath.get())); + + EXPECT_TRUE(!!hDll); + EXPECT_TRUE(!!::GetModuleHandleW(kLeafName.get())); +} + +TEST(TestDllBlocklist, NoOpEntryPoint) +{ + // DllMain of this dll has MOZ_RELEASE_ASSERT. This test makes sure we load + // the module successfully without running DllMain. + constexpr auto kLeafName = u"TestDllBlocklist_NoOpEntryPoint.dll"_ns; + nsString dllPath = GetFullPath(kLeafName); + + nsModuleHandle hDll(::LoadLibraryW(dllPath.get())); + +#if defined(MOZ_ASAN) + // With ASAN, the test uses mozglue's blocklist where + // REDIRECT_TO_NOOP_ENTRYPOINT is ignored. So LoadLibraryW + // is expected to fail. + EXPECT_TRUE(!hDll); + EXPECT_TRUE(!::GetModuleHandleW(kLeafName.get())); +#else + EXPECT_TRUE(!!hDll); + EXPECT_TRUE(!!::GetModuleHandleW(kLeafName.get())); +#endif +} + +#define DLL_BLOCKLIST_ENTRY(name, ...) {name, __VA_ARGS__}, +#define DLL_BLOCKLIST_STRING_TYPE const char* +#include "mozilla/WindowsDllBlocklistLegacyDefs.h" + +TEST(TestDllBlocklist, BlocklistIntegrity) +{ + nsTArray<DLL_BLOCKLIST_STRING_TYPE> dupes; + DECLARE_POINTER_TO_FIRST_DLL_BLOCKLIST_ENTRY(pFirst); + DECLARE_POINTER_TO_LAST_DLL_BLOCKLIST_ENTRY(pLast); + + EXPECT_FALSE(pLast->mName || pLast->mMaxVersion || pLast->mFlags); + + for (size_t i = 0; i < mozilla::ArrayLength(gWindowsDllBlocklist) - 1; ++i) { + auto pEntry = pFirst + i; + + // Validate name + EXPECT_TRUE(!!pEntry->mName); + EXPECT_GT(strlen(pEntry->mName), 3); + + // Check the filename for valid characters. + for (auto pch = pEntry->mName; *pch != 0; ++pch) { + EXPECT_FALSE(*pch >= 'A' && *pch <= 'Z'); + } + + // Check for duplicate entries + for (auto&& dupe : dupes) { + EXPECT_NE(stricmp(dupe, pEntry->mName), 0); + } + + dupes.AppendElement(pEntry->mName); + } +} + +TEST(TestDllBlocklist, BlockThreadWithLoadLibraryEntryPoint) +{ + // Only supported on Nightly +#if defined(NIGHTLY_BUILD) + using ThreadProc = unsigned(__stdcall*)(void*); + + constexpr auto kLeafNameW = u"TestDllBlocklist_MatchByVersion.dll"_ns; + + nsString fullPathW = GetFullPath(kLeafNameW); + EXPECT_FALSE(fullPathW.IsEmpty()); + + nsAutoHandle threadW(reinterpret_cast<HANDLE>( + _beginthreadex(nullptr, 0, reinterpret_cast<ThreadProc>(&::LoadLibraryW), + (void*)fullPathW.get(), 0, nullptr))); + + EXPECT_TRUE(!!threadW); + EXPECT_EQ(::WaitForSingleObject(threadW, INFINITE), WAIT_OBJECT_0); + + DWORD exitCode; + EXPECT_TRUE(::GetExitCodeThread(threadW, &exitCode) && !exitCode); + EXPECT_TRUE(!::GetModuleHandleW(kLeafNameW.get())); + + const NS_LossyConvertUTF16toASCII fullPathA(fullPathW); + EXPECT_FALSE(fullPathA.IsEmpty()); + + nsAutoHandle threadA(reinterpret_cast<HANDLE>( + _beginthreadex(nullptr, 0, reinterpret_cast<ThreadProc>(&::LoadLibraryA), + (void*)fullPathA.get(), 0, nullptr))); + + EXPECT_TRUE(!!threadA); + EXPECT_EQ(::WaitForSingleObject(threadA, INFINITE), WAIT_OBJECT_0); + EXPECT_TRUE(::GetExitCodeThread(threadA, &exitCode) && !exitCode); + EXPECT_TRUE(!::GetModuleHandleW(kLeafNameW.get())); +#endif // defined(NIGHTLY_BUILD) +} diff --git a/mozglue/tests/gtest/TestDllBlocklist_AllowByVersion/TestDllBlocklist_AllowByVersion.cpp b/mozglue/tests/gtest/TestDllBlocklist_AllowByVersion/TestDllBlocklist_AllowByVersion.cpp new file mode 100644 index 0000000000..7bd936296e --- /dev/null +++ b/mozglue/tests/gtest/TestDllBlocklist_AllowByVersion/TestDllBlocklist_AllowByVersion.cpp @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <windows.h> + +BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD aReason, LPVOID) { return TRUE; } diff --git a/mozglue/tests/gtest/TestDllBlocklist_AllowByVersion/TestDllBlocklist_AllowByVersion.rc b/mozglue/tests/gtest/TestDllBlocklist_AllowByVersion/TestDllBlocklist_AllowByVersion.rc new file mode 100644 index 0000000000..f56aa099ff --- /dev/null +++ b/mozglue/tests/gtest/TestDllBlocklist_AllowByVersion/TestDllBlocklist_AllowByVersion.rc @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <winver.h> + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +VS_VERSION_INFO VERSIONINFO + FILEVERSION 5,5,5,6 + PRODUCTVERSION 5,5,5,1 + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_DLL + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "mozilla.org" + VALUE "FileDescription", L"Test DLL" + VALUE "FileVersion", "1.0" + VALUE "InternalName", "Test DLL" + VALUE "OriginalFilename", "TestDllBlocklist_AllowByVersion.dll" + VALUE "ProductName", "Test DLL" + VALUE "ProductVersion", "1.0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 1252 + END +END diff --git a/mozglue/tests/gtest/TestDllBlocklist_AllowByVersion/moz.build b/mozglue/tests/gtest/TestDllBlocklist_AllowByVersion/moz.build new file mode 100644 index 0000000000..0987cdde1a --- /dev/null +++ b/mozglue/tests/gtest/TestDllBlocklist_AllowByVersion/moz.build @@ -0,0 +1,17 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIST_INSTALL = False + +SharedLibrary("TestDllBlocklist_AllowByVersion") + +UNIFIED_SOURCES = [ + "TestDllBlocklist_AllowByVersion.cpp", +] + +RCFILE = "TestDllBlocklist_AllowByVersion.rc" + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.gtest += ["!TestDllBlocklist_AllowByVersion.dll"] diff --git a/mozglue/tests/gtest/TestDllBlocklist_MatchByName/TestDllBlocklist_MatchByName.cpp b/mozglue/tests/gtest/TestDllBlocklist_MatchByName/TestDllBlocklist_MatchByName.cpp new file mode 100644 index 0000000000..7bd936296e --- /dev/null +++ b/mozglue/tests/gtest/TestDllBlocklist_MatchByName/TestDllBlocklist_MatchByName.cpp @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <windows.h> + +BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD aReason, LPVOID) { return TRUE; } diff --git a/mozglue/tests/gtest/TestDllBlocklist_MatchByName/moz.build b/mozglue/tests/gtest/TestDllBlocklist_MatchByName/moz.build new file mode 100644 index 0000000000..f34931898a --- /dev/null +++ b/mozglue/tests/gtest/TestDllBlocklist_MatchByName/moz.build @@ -0,0 +1,15 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIST_INSTALL = False + +SharedLibrary("TestDllBlocklist_MatchByName") + +UNIFIED_SOURCES = [ + "TestDllBlocklist_MatchByName.cpp", +] + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.gtest += ["!TestDllBlocklist_MatchByName.dll"] diff --git a/mozglue/tests/gtest/TestDllBlocklist_MatchByVersion/TestDllBlocklist_MatchByVersion.cpp b/mozglue/tests/gtest/TestDllBlocklist_MatchByVersion/TestDllBlocklist_MatchByVersion.cpp new file mode 100644 index 0000000000..7bd936296e --- /dev/null +++ b/mozglue/tests/gtest/TestDllBlocklist_MatchByVersion/TestDllBlocklist_MatchByVersion.cpp @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <windows.h> + +BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD aReason, LPVOID) { return TRUE; } diff --git a/mozglue/tests/gtest/TestDllBlocklist_MatchByVersion/TestDllBlocklist_MatchByVersion.rc b/mozglue/tests/gtest/TestDllBlocklist_MatchByVersion/TestDllBlocklist_MatchByVersion.rc new file mode 100644 index 0000000000..7390c1cb34 --- /dev/null +++ b/mozglue/tests/gtest/TestDllBlocklist_MatchByVersion/TestDllBlocklist_MatchByVersion.rc @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <winver.h> + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +VS_VERSION_INFO VERSIONINFO + FILEVERSION 5,5,5,5 + PRODUCTVERSION 5,5,5,1 + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_DLL + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "mozilla.org" + VALUE "FileDescription", L"Test DLL" + VALUE "FileVersion", "1.0" + VALUE "InternalName", "Test DLL" + VALUE "OriginalFilename", "TestDllBlocklist_MatchByVersion.dll" + VALUE "ProductName", "Test DLL" + VALUE "ProductVersion", "1.0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 1252 + END +END diff --git a/mozglue/tests/gtest/TestDllBlocklist_MatchByVersion/moz.build b/mozglue/tests/gtest/TestDllBlocklist_MatchByVersion/moz.build new file mode 100644 index 0000000000..38e10524c7 --- /dev/null +++ b/mozglue/tests/gtest/TestDllBlocklist_MatchByVersion/moz.build @@ -0,0 +1,17 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIST_INSTALL = False + +SharedLibrary("TestDllBlocklist_MatchByVersion") + +UNIFIED_SOURCES = [ + "TestDllBlocklist_MatchByVersion.cpp", +] + +RCFILE = "TestDllBlocklist_MatchByVersion.rc" + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.gtest += ["!TestDllBlocklist_MatchByVersion.dll"] diff --git a/mozglue/tests/gtest/TestDllBlocklist_NoOpEntryPoint/TestDllBlocklist_NoOpEntryPoint.cpp b/mozglue/tests/gtest/TestDllBlocklist_NoOpEntryPoint/TestDllBlocklist_NoOpEntryPoint.cpp new file mode 100644 index 0000000000..2505b8b700 --- /dev/null +++ b/mozglue/tests/gtest/TestDllBlocklist_NoOpEntryPoint/TestDllBlocklist_NoOpEntryPoint.cpp @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <windows.h> + +#include "mozilla/Assertions.h" + +BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD aReason, LPVOID) { + MOZ_RELEASE_ASSERT(0); + return TRUE; +} diff --git a/mozglue/tests/gtest/TestDllBlocklist_NoOpEntryPoint/TestDllBlocklist_NoOpEntryPoint.rc b/mozglue/tests/gtest/TestDllBlocklist_NoOpEntryPoint/TestDllBlocklist_NoOpEntryPoint.rc new file mode 100644 index 0000000000..7c79dac373 --- /dev/null +++ b/mozglue/tests/gtest/TestDllBlocklist_NoOpEntryPoint/TestDllBlocklist_NoOpEntryPoint.rc @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <winver.h> + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +VS_VERSION_INFO VERSIONINFO + FILEVERSION 5,5,5,5 + PRODUCTVERSION 5,5,5,1 + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_DLL + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "mozilla.org" + VALUE "FileDescription", L"Test DLL" + VALUE "FileVersion", "1.0" + VALUE "InternalName", "Test DLL" + VALUE "OriginalFilename", "TestDllBlocklist_NoOpEntryPoint.dll" + VALUE "ProductName", "Test DLL" + VALUE "ProductVersion", "1.0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 1252 + END +END diff --git a/mozglue/tests/gtest/TestDllBlocklist_NoOpEntryPoint/moz.build b/mozglue/tests/gtest/TestDllBlocklist_NoOpEntryPoint/moz.build new file mode 100644 index 0000000000..57fae737c4 --- /dev/null +++ b/mozglue/tests/gtest/TestDllBlocklist_NoOpEntryPoint/moz.build @@ -0,0 +1,17 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIST_INSTALL = False + +SharedLibrary("TestDllBlocklist_NoOpEntryPoint") + +UNIFIED_SOURCES = [ + "TestDllBlocklist_NoOpEntryPoint.cpp", +] + +RCFILE = "TestDllBlocklist_NoOpEntryPoint.rc" + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.gtest += ["!TestDllBlocklist_NoOpEntryPoint.dll"] diff --git a/mozglue/tests/gtest/TestNativeNtGTest.cpp b/mozglue/tests/gtest/TestNativeNtGTest.cpp new file mode 100644 index 0000000000..e0f0a343a7 --- /dev/null +++ b/mozglue/tests/gtest/TestNativeNtGTest.cpp @@ -0,0 +1,20 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <windows.h> + +#include "gtest/gtest.h" + +#include "mozilla/NativeNt.h" + +TEST(TestNativeNtGTest, GenerateDependentModuleSet) +{ + mozilla::nt::PEHeaders executable(::GetModuleHandleW(nullptr)); + auto dependentModules = executable.GenerateDependentModuleSet(); + EXPECT_NE(dependentModules.GetEntry(u"mozglue.dll"_ns), nullptr); + EXPECT_NE(dependentModules.GetEntry(u"MOZGLUE.dll"_ns), nullptr); + EXPECT_EQ(dependentModules.GetEntry(u"xxx.dll"_ns), nullptr); +} diff --git a/mozglue/tests/gtest/moz.build b/mozglue/tests/gtest/moz.build new file mode 100644 index 0000000000..5a5e2d8ac5 --- /dev/null +++ b/mozglue/tests/gtest/moz.build @@ -0,0 +1,17 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +SOURCES += [ + "TestDLLBlocklist.cpp", + "TestNativeNtGTest.cpp", +] + +FINAL_LIBRARY = "xul-gtest" + +TEST_DIRS += [ + "TestDllBlocklist_AllowByVersion", + "TestDllBlocklist_MatchByName", + "TestDllBlocklist_MatchByVersion", + "TestDllBlocklist_NoOpEntryPoint", +] diff --git a/mozglue/tests/interceptor/AssemblyPayloads.h b/mozglue/tests/interceptor/AssemblyPayloads.h new file mode 100644 index 0000000000..f053e161b0 --- /dev/null +++ b/mozglue/tests/interceptor/AssemblyPayloads.h @@ -0,0 +1,194 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +/* These assembly functions represent patterns that were already hooked by + * another application before our detour. + */ + +#ifndef mozilla_AssemblyPayloads_h +#define mozilla_AssemblyPayloads_h + +#define PADDING_256_NOP \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" \ + "nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;" + +extern "C" { + +#if defined(__clang__) +# if defined(_M_X64) +constexpr uintptr_t JumpDestination = 0x7fff00000000; + +__declspec(dllexport) __attribute__((naked)) void MovPushRet() { + asm volatile( + "mov %0, %%rax;" + "push %%rax;" + "ret;" + : + : "i"(JumpDestination)); +} + +__declspec(dllexport) __attribute__((naked)) void MovRaxJump() { + asm volatile( + "mov %0, %%rax;" + "jmpq *%%rax;" + : + : "i"(JumpDestination)); +} + +__declspec(dllexport) __attribute__((naked)) void DoubleJump() { + asm volatile( + "jmp label1;" + + "label2:" + "mov %0, %%rax;" + "jmpq *%%rax;" + + // 0x100 bytes padding to generate jmp rel32 instead of jmp rel8 + PADDING_256_NOP + + "label1:" + "jmp label2;" + : + : "i"(JumpDestination)); +} + +__declspec(dllexport) __attribute__((naked)) void NearJump() { + asm volatile( + "jae label3;" + "je label3;" + "jne label3;" + + "label4:" + "mov %0, %%rax;" + "jmpq *%%rax;" + + // 0x100 bytes padding to generate jae rel32 instead of jae rel8 + PADDING_256_NOP + + "label3:" + "jmp label4;" + : + : "i"(JumpDestination)); +} + +__declspec(dllexport) __attribute__((naked)) void OpcodeFF() { + // Skip PUSH (FF /6) because clang prefers Opcode 50+rd + // to translate PUSH r64 rather than Opcode FF. + asm volatile( + "incl %eax;" + "decl %ebx;" + "call *%rcx;" + "jmp *(%rip);" // Indirect jump to 0xcccccccc`cccccccc + "int $3;int $3;int $3;int $3;" + "int $3;int $3;int $3;int $3;"); +} + +__declspec(dllexport) __attribute__((naked)) void IndirectCall() { + asm volatile( + "call *(%rip);" // Indirect call to 0x90909090`90909090 + "nop;nop;nop;nop;nop;nop;nop;nop;" + "ret;"); +} + +__declspec(dllexport) __attribute__((naked)) void MovImm64() { + asm volatile( + "mov $0x1234567812345678, %r10;" + "nop;nop;nop"); +} + +# elif defined(_M_IX86) +constexpr uintptr_t JumpDestination = 0x7fff0000; + +__declspec(dllexport) __attribute__((naked)) void PushRet() { + asm volatile( + "push %0;" + "ret;" + : + : "i"(JumpDestination)); +} + +__declspec(dllexport) __attribute__((naked)) void MovEaxJump() { + asm volatile( + "mov %0, %%eax;" + "jmp *%%eax;" + : + : "i"(JumpDestination)); +} + +__declspec(dllexport) __attribute__((naked)) void Opcode83() { + asm volatile( + "xor $0x42, %eax;" + "cmpl $1, 0xc(%ebp);"); +} + +__declspec(dllexport) __attribute__((naked)) void LockPrefix() { + // Test an instruction with a LOCK prefix (0xf0) at a non-zero offset + asm volatile( + "push $0x7c;" + "lock push $0x7c;"); +} + +__declspec(dllexport) __attribute__((naked)) void LooksLikeLockPrefix() { + // This is for a regression scenario of bug 1625452, where we double-counted + // the offset in CountPrefixBytes. When we count prefix bytes in front of + // the 2nd PUSH located at offset 2, we mistakenly started counting from + // the byte 0xf0 at offset 4, which is considered as LOCK, thus we try to + // detour the next byte 0xcc and it fails. + // + // 0: 6a7c push 7Ch + // 2: 68ccf00000 push 0F0CCh + // + asm volatile( + "push $0x7c;" + "push $0x0000f0cc;"); +} + +__declspec(dllexport) __attribute__((naked)) void DoubleJump() { + asm volatile( + "jmp label1;" + + "label2:" + "mov %0, %%eax;" + "jmp *%%eax;" + + // 0x100 bytes padding to generate jmp rel32 instead of jmp rel8 + PADDING_256_NOP + + "label1:" + "jmp label2;" + : + : "i"(JumpDestination)); +} +# endif + +# if !defined(_M_ARM64) +__declspec(dllexport) __attribute__((naked)) void UnsupportedOp() { + asm volatile( + "ud2;" + "nop;nop;nop;nop;nop;nop;nop;nop;" + "nop;nop;nop;nop;nop;nop;nop;nop;"); +} +# endif // !defined(_M_ARM64) + +#endif // defined(__clang__) + +} // extern "C" + +#endif // mozilla_AssemblyPayloads_h diff --git a/mozglue/tests/interceptor/TestDllInterceptor.cpp b/mozglue/tests/interceptor/TestDllInterceptor.cpp new file mode 100644 index 0000000000..a5e0de2885 --- /dev/null +++ b/mozglue/tests/interceptor/TestDllInterceptor.cpp @@ -0,0 +1,1105 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <shlobj.h> +#include <stdio.h> +#include <commdlg.h> +#define SECURITY_WIN32 +#include <security.h> +#include <wininet.h> +#include <schnlsp.h> +#include <winternl.h> +#include <processthreadsapi.h> + +#include "AssemblyPayloads.h" +#include "mozilla/DynamicallyLinkedFunctionPtr.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/WindowsVersion.h" +#include "nsWindowsDllInterceptor.h" +#include "nsWindowsHelpers.h" + +NTSTATUS NTAPI NtFlushBuffersFile(HANDLE, PIO_STATUS_BLOCK); +NTSTATUS NTAPI NtReadFile(HANDLE, HANDLE, PIO_APC_ROUTINE, PVOID, + PIO_STATUS_BLOCK, PVOID, ULONG, PLARGE_INTEGER, + PULONG); +NTSTATUS NTAPI NtReadFileScatter(HANDLE, HANDLE, PIO_APC_ROUTINE, PVOID, + PIO_STATUS_BLOCK, PFILE_SEGMENT_ELEMENT, ULONG, + PLARGE_INTEGER, PULONG); +NTSTATUS NTAPI NtWriteFile(HANDLE, HANDLE, PIO_APC_ROUTINE, PVOID, + PIO_STATUS_BLOCK, PVOID, ULONG, PLARGE_INTEGER, + PULONG); +NTSTATUS NTAPI NtWriteFileGather(HANDLE, HANDLE, PIO_APC_ROUTINE, PVOID, + PIO_STATUS_BLOCK, PFILE_SEGMENT_ELEMENT, ULONG, + PLARGE_INTEGER, PULONG); +NTSTATUS NTAPI NtQueryFullAttributesFile(POBJECT_ATTRIBUTES, PVOID); +NTSTATUS NTAPI LdrLoadDll(PWCHAR filePath, PULONG flags, + PUNICODE_STRING moduleFileName, PHANDLE handle); +NTSTATUS NTAPI LdrUnloadDll(HMODULE); + +NTSTATUS NTAPI NtMapViewOfSection( + HANDLE aSection, HANDLE aProcess, PVOID* aBaseAddress, ULONG_PTR aZeroBits, + SIZE_T aCommitSize, PLARGE_INTEGER aSectionOffset, PSIZE_T aViewSize, + SECTION_INHERIT aInheritDisposition, ULONG aAllocationType, + ULONG aProtectionFlags); + +// These pointers are disguised as PVOID to avoid pulling in obscure headers +PVOID NTAPI LdrResolveDelayLoadedAPI(PVOID, PVOID, PVOID, PVOID, PVOID, ULONG); +void CALLBACK ProcessCaretEvents(HWINEVENTHOOK, DWORD, HWND, LONG, LONG, DWORD, + DWORD); +void __fastcall BaseThreadInitThunk(BOOL aIsInitialThread, void* aStartAddress, + void* aThreadParam); + +BOOL WINAPI ApiSetQueryApiSetPresence(PCUNICODE_STRING, PBOOLEAN); + +#if (_WIN32_WINNT < 0x0602) +BOOL WINAPI +SetProcessMitigationPolicy(PROCESS_MITIGATION_POLICY aMitigationPolicy, + PVOID aBuffer, SIZE_T aBufferLen); +#endif // (_WIN32_WINNT < 0x0602) + +using namespace mozilla; + +struct payload { + UINT64 a; + UINT64 b; + UINT64 c; + + bool operator==(const payload& other) const { + return (a == other.a && b == other.b && c == other.c); + } +}; + +extern "C" __declspec(dllexport) __declspec(noinline) payload + rotatePayload(payload p) { + UINT64 tmp = p.a; + p.a = p.b; + p.b = p.c; + p.c = tmp; + return p; +} + +// payloadNotHooked is a target function for a test to expect a negative result. +// We cannot use rotatePayload for that purpose because our detour cannot hook +// a function detoured already. Please keep this function always unhooked. +extern "C" __declspec(dllexport) __declspec(noinline) payload + payloadNotHooked(payload p) { + // Do something different from rotatePayload to avoid ICF. + p.a ^= p.b; + p.b ^= p.c; + p.c ^= p.a; + return p; +} + +static bool patched_func_called = false; + +static WindowsDllInterceptor::FuncHookType<decltype(&rotatePayload)> + orig_rotatePayload; + +static WindowsDllInterceptor::FuncHookType<decltype(&payloadNotHooked)> + orig_payloadNotHooked; + +static payload patched_rotatePayload(payload p) { + patched_func_called = true; + return orig_rotatePayload(p); +} + +// Invoke aFunc by taking aArg's contents and using them as aFunc's arguments +template <typename OrigFuncT, typename... Args, + typename ArgTuple = Tuple<Args...>, size_t... Indices> +decltype(auto) Apply(OrigFuncT& aFunc, ArgTuple&& aArgs, + std::index_sequence<Indices...>) { + return aFunc(Get<Indices>(std::forward<ArgTuple>(aArgs))...); +} + +#define DEFINE_TEST_FUNCTION(calling_convention) \ + template <typename R, typename... Args, typename... TestArgs> \ + bool TestFunction(R(calling_convention* aFunc)(Args...), bool (*aPred)(R), \ + TestArgs&&... aArgs) { \ + using ArgTuple = Tuple<Args...>; \ + using Indices = std::index_sequence_for<Args...>; \ + ArgTuple fakeArgs{std::forward<TestArgs>(aArgs)...}; \ + patched_func_called = false; \ + return aPred(Apply(aFunc, std::forward<ArgTuple>(fakeArgs), Indices())) && \ + patched_func_called; \ + } \ + \ + /* Specialization for functions returning void */ \ + template <typename PredT, typename... Args, typename... TestArgs> \ + bool TestFunction(void(calling_convention * aFunc)(Args...), PredT, \ + TestArgs&&... aArgs) { \ + using ArgTuple = Tuple<Args...>; \ + using Indices = std::index_sequence_for<Args...>; \ + ArgTuple fakeArgs{std::forward<TestArgs>(aArgs)...}; \ + patched_func_called = false; \ + Apply(aFunc, std::forward<ArgTuple>(fakeArgs), Indices()); \ + return patched_func_called; \ + } + +// C++11 allows empty arguments to macros. clang works just fine. MSVC does the +// right thing, but it also throws up warning C4003. +#if defined(_MSC_VER) && !defined(__clang__) +DEFINE_TEST_FUNCTION(__cdecl) +#else +DEFINE_TEST_FUNCTION() +#endif + +#ifdef _M_IX86 +DEFINE_TEST_FUNCTION(__stdcall) +DEFINE_TEST_FUNCTION(__fastcall) +#endif // _M_IX86 + +// Test the hooked function against the supplied predicate +template <typename OrigFuncT, typename PredicateT, typename... Args> +bool CheckHook(OrigFuncT& aOrigFunc, const char* aDllName, + const char* aFuncName, PredicateT&& aPred, Args&&... aArgs) { + if (TestFunction(aOrigFunc, std::forward<PredicateT>(aPred), + std::forward<Args>(aArgs)...)) { + printf( + "TEST-PASS | WindowsDllInterceptor | " + "Executed hooked function %s from %s\n", + aFuncName, aDllName); + fflush(stdout); + return true; + } + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "Failed to execute hooked function %s from %s\n", + aFuncName, aDllName); + return false; +} + +struct InterceptorFunction { + static const size_t EXEC_MEMBLOCK_SIZE = 64 * 1024; // 64K + + static InterceptorFunction& Create() { + // Make sure the executable memory is allocated + if (!sBlock) { + Init(); + } + MOZ_ASSERT(sBlock); + + // Make sure we aren't making more functions than we allocated room for + MOZ_RELEASE_ASSERT((sNumInstances + 1) * sizeof(InterceptorFunction) <= + EXEC_MEMBLOCK_SIZE); + + // Grab the next InterceptorFunction from executable memory + InterceptorFunction& ret = *reinterpret_cast<InterceptorFunction*>( + sBlock + (sNumInstances++ * sizeof(InterceptorFunction))); + + // Set the InterceptorFunction to the code template. + auto funcCode = &ret[0]; + memcpy(funcCode, sInterceptorTemplate, TemplateLength); + + // Fill in the patched_func_called pointer in the template. + auto pfPtr = reinterpret_cast<bool**>(&ret[PatchedFuncCalledIndex]); + *pfPtr = &patched_func_called; + return ret; + } + + uint8_t& operator[](size_t i) { return mFuncCode[i]; } + + uint8_t* GetFunction() { return mFuncCode; } + + void SetStub(uintptr_t aStub) { + auto pfPtr = reinterpret_cast<uintptr_t*>(&mFuncCode[StubFuncIndex]); + *pfPtr = aStub; + } + + private: + // We intercept functions with short machine-code functions that set a boolean + // and run the stub that launches the original function. Each entry in the + // array is the code for one of those interceptor functions. We cannot + // free this memory until the test shuts down. + // The templates have spots for the address of patched_func_called + // and for the address of the stub function. Their indices in the byte + // array are given as constants below and they appear as blocks of + // 0xff bytes in the templates. +#if defined(_M_X64) + // 0: 48 b8 ff ff ff ff ff ff ff ff movabs rax, &patched_func_called + // a: c6 00 01 mov BYTE PTR [rax],0x1 + // d: 48 b8 ff ff ff ff ff ff ff ff movabs rax, &stub_func_ptr + // 17: ff e0 jmp rax + static constexpr uint8_t sInterceptorTemplate[] = { + 0x48, 0xB8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xC6, 0x00, 0x01, 0x48, 0xB8, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0}; + static const size_t PatchedFuncCalledIndex = 0x2; + static const size_t StubFuncIndex = 0xf; +#elif defined(_M_IX86) + // 0: c6 05 ff ff ff ff 01 mov BYTE PTR &patched_func_called, 0x1 + // 7: 68 ff ff ff ff push &stub_func_ptr + // c: c3 ret + static constexpr uint8_t sInterceptorTemplate[] = { + 0xC6, 0x05, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, + 0x68, 0xFF, 0xFF, 0xFF, 0xFF, 0xC3}; + static const size_t PatchedFuncCalledIndex = 0x2; + static const size_t StubFuncIndex = 0x8; +#elif defined(_M_ARM64) + // 0: 31 00 80 52 movz w17, #0x1 + // 4: 90 00 00 58 ldr x16, #16 + // 8: 11 02 00 39 strb w17, [x16] + // c: 90 00 00 58 ldr x16, #16 + // 10: 00 02 1F D6 br x16 + // 14: &patched_func_called + // 1c: &stub_func_ptr + static constexpr uint8_t sInterceptorTemplate[] = { + 0x31, 0x00, 0x80, 0x52, 0x90, 0x00, 0x00, 0x58, 0x11, 0x02, 0x00, 0x39, + 0x90, 0x00, 0x00, 0x58, 0x00, 0x02, 0x1F, 0xD6, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + static const size_t PatchedFuncCalledIndex = 0x14; + static const size_t StubFuncIndex = 0x1c; +#else +# error "Missing template for architecture" +#endif + + static const size_t TemplateLength = sizeof(sInterceptorTemplate); + uint8_t mFuncCode[TemplateLength]; + + InterceptorFunction() = delete; + InterceptorFunction(const InterceptorFunction&) = delete; + InterceptorFunction& operator=(const InterceptorFunction&) = delete; + + static void Init() { + MOZ_ASSERT(!sBlock); + sBlock = reinterpret_cast<uint8_t*>( + ::VirtualAlloc(nullptr, EXEC_MEMBLOCK_SIZE, MEM_RESERVE | MEM_COMMIT, + PAGE_EXECUTE_READWRITE)); + } + + static uint8_t* sBlock; + static size_t sNumInstances; +}; + +uint8_t* InterceptorFunction::sBlock = nullptr; +size_t InterceptorFunction::sNumInstances = 0; + +constexpr uint8_t InterceptorFunction::sInterceptorTemplate[]; + +// Hook the function and optionally attempt calling it +template <typename OrigFuncT, size_t N, typename PredicateT, typename... Args> +bool TestHook(const char (&dll)[N], const char* func, PredicateT&& aPred, + Args&&... aArgs) { + auto orig_func( + mozilla::MakeUnique<WindowsDllInterceptor::FuncHookType<OrigFuncT>>()); + wchar_t dllW[N]; + std::copy(std::begin(dll), std::end(dll), std::begin(dllW)); + + bool successful = false; + WindowsDllInterceptor TestIntercept; + TestIntercept.Init(dll); + + InterceptorFunction& interceptorFunc = InterceptorFunction::Create(); + successful = orig_func->Set( + TestIntercept, func, + reinterpret_cast<OrigFuncT>(interceptorFunc.GetFunction())); + + if (successful) { + interceptorFunc.SetStub(reinterpret_cast<uintptr_t>(orig_func->GetStub())); + printf("TEST-PASS | WindowsDllInterceptor | Could hook %s from %s\n", func, + dll); + fflush(stdout); + if (!aPred) { + printf( + "TEST-SKIPPED | WindowsDllInterceptor | " + "Will not attempt to execute patched %s.\n", + func); + fflush(stdout); + return true; + } + + // Test the DLL function we just hooked. + HMODULE module = ::LoadLibraryW(dllW); + FARPROC funcAddr = ::GetProcAddress(module, func); + if (!funcAddr) { + return false; + } + + return CheckHook(reinterpret_cast<OrigFuncT&>(funcAddr), dll, func, + std::forward<PredicateT>(aPred), + std::forward<Args>(aArgs)...); + } else { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | Failed to hook %s from " + "%s\n", + func, dll); + fflush(stdout); + + // Print out the function's bytes so that we can easily analyze the error. + nsModuleHandle mod(::LoadLibraryW(dllW)); + FARPROC funcAddr = ::GetProcAddress(mod, func); + if (funcAddr) { + const uint32_t kNumBytesToDump = + WindowsDllInterceptor::GetWorstCaseRequiredBytesToPatch(); + + printf("\tFirst %u bytes of function:\n\t", kNumBytesToDump); + + auto code = reinterpret_cast<const uint8_t*>(funcAddr); + for (uint32_t i = 0; i < kNumBytesToDump; ++i) { + char suffix = (i < (kNumBytesToDump - 1)) ? ' ' : '\n'; + printf("%02hhX%c", code[i], suffix); + } + + fflush(stdout); + } + return false; + } +} + +// Detour the function and optionally attempt calling it +template <typename OrigFuncT, size_t N, typename PredicateT> +bool TestDetour(const char (&dll)[N], const char* func, PredicateT&& aPred) { + auto orig_func( + mozilla::MakeUnique<WindowsDllInterceptor::FuncHookType<OrigFuncT>>()); + wchar_t dllW[N]; + std::copy(std::begin(dll), std::end(dll), std::begin(dllW)); + + bool successful = false; + WindowsDllInterceptor TestIntercept; + TestIntercept.Init(dll); + + InterceptorFunction& interceptorFunc = InterceptorFunction::Create(); + successful = orig_func->Set( + TestIntercept, func, + reinterpret_cast<OrigFuncT>(interceptorFunc.GetFunction())); + + if (successful) { + interceptorFunc.SetStub(reinterpret_cast<uintptr_t>(orig_func->GetStub())); + printf("TEST-PASS | WindowsDllInterceptor | Could detour %s from %s\n", + func, dll); + fflush(stdout); + if (!aPred) { + printf( + "TEST-SKIPPED | WindowsDllInterceptor | " + "Will not attempt to execute patched %s.\n", + func); + fflush(stdout); + return true; + } + + // Test the DLL function we just hooked. + HMODULE module = ::LoadLibraryW(dllW); + FARPROC funcAddr = ::GetProcAddress(module, func); + if (!funcAddr) { + return false; + } + + return CheckHook(reinterpret_cast<OrigFuncT&>(funcAddr), dll, func, + std::forward<PredicateT>(aPred)); + } else { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | Failed to detour %s " + "from %s\n", + func, dll); + fflush(stdout); + return false; + } +} + +// If a function pointer's type returns void*, this template converts that type +// to return uintptr_t instead, for the purposes of predicates. +template <typename FuncT> +struct SubstituteForVoidPtr { + using Type = FuncT; +}; + +template <typename... Args> +struct SubstituteForVoidPtr<void* (*)(Args...)> { + using Type = uintptr_t (*)(Args...); +}; + +#ifdef _M_IX86 +template <typename... Args> +struct SubstituteForVoidPtr<void*(__stdcall*)(Args...)> { + using Type = uintptr_t(__stdcall*)(Args...); +}; + +template <typename... Args> +struct SubstituteForVoidPtr<void*(__fastcall*)(Args...)> { + using Type = uintptr_t(__fastcall*)(Args...); +}; +#endif // _M_IX86 + +// Determines the function's return type +template <typename FuncT> +struct ReturnType; + +template <typename R, typename... Args> +struct ReturnType<R (*)(Args...)> { + using Type = R; +}; + +#ifdef _M_IX86 +template <typename R, typename... Args> +struct ReturnType<R(__stdcall*)(Args...)> { + using Type = R; +}; + +template <typename R, typename... Args> +struct ReturnType<R(__fastcall*)(Args...)> { + using Type = R; +}; +#endif // _M_IX86 + +// Predicates that may be supplied during tests +template <typename FuncT> +struct Predicates { + using ArgType = typename ReturnType<FuncT>::Type; + + template <ArgType CompVal> + static bool Equals(ArgType aValue) { + return CompVal == aValue; + } + + template <ArgType CompVal> + static bool NotEquals(ArgType aValue) { + return CompVal != aValue; + } + + template <ArgType CompVal> + static bool Ignore(ArgType aValue) { + return true; + } +}; + +// Functions that return void should be ignored, so we specialize the +// Ignore predicate for that case. Use nullptr as the value to compare against. +template <typename... Args> +struct Predicates<void (*)(Args...)> { + template <nullptr_t DummyVal> + static bool Ignore() { + return true; + } +}; + +#ifdef _M_IX86 +template <typename... Args> +struct Predicates<void(__stdcall*)(Args...)> { + template <nullptr_t DummyVal> + static bool Ignore() { + return true; + } +}; + +template <typename... Args> +struct Predicates<void(__fastcall*)(Args...)> { + template <nullptr_t DummyVal> + static bool Ignore() { + return true; + } +}; +#endif // _M_IX86 + +// The standard test. Hook |func|, and then try executing it with all zero +// arguments, using |pred| and |comp| to determine whether the call successfully +// executed. In general, you want set pred and comp such that they return true +// when the function is returning whatever value is expected with all-zero +// arguments. +// +// Note: When |func| returns void, you must supply |Ignore| and |nullptr| as the +// |pred| and |comp| arguments, respectively. +#define TEST_HOOK(dll, func, pred, comp) \ + TestHook<decltype(&func)>(dll, #func, \ + &Predicates<decltype(&func)>::pred<comp>) + +// We need to special-case functions that return INVALID_HANDLE_VALUE +// (ie, CreateFile). Our template machinery for comparing values doesn't work +// with integer constants passed as pointers (well, it works on MSVC, but not +// clang, because that is not standard-compliant). +#define TEST_HOOK_FOR_INVALID_HANDLE_VALUE(dll, func) \ + TestHook<SubstituteForVoidPtr<decltype(&func)>::Type>( \ + dll, #func, \ + &Predicates<SubstituteForVoidPtr<decltype(&func)>::Type>::Equals< \ + uintptr_t(-1)>) + +// This variant allows you to explicitly supply arguments to the hooked function +// during testing. You want to provide arguments that produce the conditions +// that induce the function to return a value that is accepted by your +// predicate. +#define TEST_HOOK_PARAMS(dll, func, pred, comp, ...) \ + TestHook<decltype(&func)>( \ + dll, #func, &Predicates<decltype(&func)>::pred<comp>, __VA_ARGS__) + +// This is for cases when we want to hook |func|, but it is unsafe to attempt +// to execute the function in the context of a test. +#define TEST_HOOK_SKIP_EXEC(dll, func) \ + TestHook<decltype(&func)>( \ + dll, #func, \ + reinterpret_cast<bool (*)(typename ReturnType<decltype(&func)>::Type)>( \ + NULL)) + +// The following three variants are identical to the previous macros, +// however the forcibly use a Detour on 32-bit Windows. On 64-bit Windows, +// these macros are identical to their TEST_HOOK variants. +#define TEST_DETOUR(dll, func, pred, comp) \ + TestDetour<decltype(&func)>(dll, #func, \ + &Predicates<decltype(&func)>::pred<comp>) + +#define TEST_DETOUR_PARAMS(dll, func, pred, comp, ...) \ + TestDetour<decltype(&func)>( \ + dll, #func, &Predicates<decltype(&func)>::pred<comp>, __VA_ARGS__) + +#define TEST_DETOUR_SKIP_EXEC(dll, func) \ + TestDetour<decltype(&func)>( \ + dll, #func, \ + reinterpret_cast<bool (*)(typename ReturnType<decltype(&func)>::Type)>( \ + NULL)) + +template <typename OrigFuncT, size_t N, typename PredicateT, typename... Args> +bool MaybeTestHook(const bool cond, const char (&dll)[N], const char* func, + PredicateT&& aPred, Args&&... aArgs) { + if (!cond) { + printf( + "TEST-SKIPPED | WindowsDllInterceptor | Skipped hook test for %s from " + "%s\n", + func, dll); + fflush(stdout); + return true; + } + + return TestHook<OrigFuncT>(dll, func, std::forward<PredicateT>(aPred), + std::forward<Args>(aArgs)...); +} + +// Like TEST_HOOK, but the test is only executed when cond is true. +#define MAYBE_TEST_HOOK(cond, dll, func, pred, comp) \ + MaybeTestHook<decltype(&func)>(cond, dll, #func, \ + &Predicates<decltype(&func)>::pred<comp>) + +#define MAYBE_TEST_HOOK_PARAMS(cond, dll, func, pred, comp, ...) \ + MaybeTestHook<decltype(&func)>( \ + cond, dll, #func, &Predicates<decltype(&func)>::pred<comp>, __VA_ARGS__) + +#define MAYBE_TEST_HOOK_SKIP_EXEC(cond, dll, func) \ + MaybeTestHook<decltype(&func)>( \ + cond, dll, #func, \ + reinterpret_cast<bool (*)(typename ReturnType<decltype(&func)>::Type)>( \ + NULL)) + +bool ShouldTestTipTsf() { + if (!IsWin8OrLater()) { + return false; + } + + mozilla::DynamicallyLinkedFunctionPtr<decltype(&SHGetKnownFolderPath)> + pSHGetKnownFolderPath(L"shell32.dll", "SHGetKnownFolderPath"); + if (!pSHGetKnownFolderPath) { + return false; + } + + PWSTR commonFilesPath = nullptr; + if (FAILED(pSHGetKnownFolderPath(FOLDERID_ProgramFilesCommon, 0, nullptr, + &commonFilesPath))) { + return false; + } + + wchar_t fullPath[MAX_PATH + 1] = {}; + wcscpy(fullPath, commonFilesPath); + wcscat(fullPath, L"\\Microsoft Shared\\Ink\\tiptsf.dll"); + CoTaskMemFree(commonFilesPath); + + if (!LoadLibraryW(fullPath)) { + return false; + } + + // Leak the module so that it's loaded for the interceptor test + return true; +} + +static const wchar_t gEmptyUnicodeStringLiteral[] = L""; +static UNICODE_STRING gEmptyUnicodeString; +static BOOLEAN gIsPresent; + +bool HasApiSetQueryApiSetPresence() { + mozilla::DynamicallyLinkedFunctionPtr<decltype(&ApiSetQueryApiSetPresence)> + func(L"Api-ms-win-core-apiquery-l1-1-0.dll", "ApiSetQueryApiSetPresence"); + if (!func) { + return false; + } + + // Prepare gEmptyUnicodeString for the test + ::RtlInitUnicodeString(&gEmptyUnicodeString, gEmptyUnicodeStringLiteral); + + return true; +} + +// Set this to true to test function unhooking. +const bool ShouldTestUnhookFunction = false; + +#if defined(_M_X64) || defined(_M_ARM64) + +// Use VMSharingPolicyUnique for the ShortInterceptor, as it needs to +// reserve its trampoline memory in a special location. +using ShortInterceptor = mozilla::interceptor::WindowsDllInterceptor< + mozilla::interceptor::VMSharingPolicyUnique< + mozilla::interceptor::MMPolicyInProcess>>; + +static ShortInterceptor::FuncHookType<decltype(&::NtMapViewOfSection)> + orig_NtMapViewOfSection; + +#endif // defined(_M_X64) || defined(_M_ARM64) + +bool TestShortDetour() { +#if defined(_M_X64) || defined(_M_ARM64) + auto pNtMapViewOfSection = reinterpret_cast<decltype(&::NtMapViewOfSection)>( + ::GetProcAddress(::GetModuleHandleW(L"ntdll.dll"), "NtMapViewOfSection")); + if (!pNtMapViewOfSection) { + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "Failed to resolve ntdll!NtMapViewOfSection\n"); + fflush(stdout); + return false; + } + + { // Scope for shortInterceptor + ShortInterceptor shortInterceptor; + shortInterceptor.TestOnlyDetourInit( + L"ntdll.dll", + mozilla::interceptor::DetourFlags::eTestOnlyForceShortPatch); + + InterceptorFunction& interceptorFunc = InterceptorFunction::Create(); + if (!orig_NtMapViewOfSection.SetDetour( + shortInterceptor, "NtMapViewOfSection", + reinterpret_cast<decltype(&::NtMapViewOfSection)>( + interceptorFunc.GetFunction()))) { + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "Failed to hook ntdll!NtMapViewOfSection via 10-byte patch\n"); + fflush(stdout); + return false; + } + + interceptorFunc.SetStub( + reinterpret_cast<uintptr_t>(orig_NtMapViewOfSection.GetStub())); + + auto pred = + &Predicates<decltype(&::NtMapViewOfSection)>::Ignore<((NTSTATUS)0)>; + + if (!CheckHook(pNtMapViewOfSection, "ntdll.dll", "NtMapViewOfSection", + pred)) { + // CheckHook has already printed the error message for us + return false; + } + } + + // Now ensure that our hook cleanup worked + if (ShouldTestUnhookFunction) { + NTSTATUS status = + pNtMapViewOfSection(nullptr, nullptr, nullptr, 0, 0, nullptr, nullptr, + ((SECTION_INHERIT)0), 0, 0); + if (NT_SUCCESS(status)) { + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "Unexpected successful call to ntdll!NtMapViewOfSection after " + "removing short-patched hook\n"); + fflush(stdout); + return false; + } + + printf( + "TEST-PASS | WindowsDllInterceptor | " + "Successfully unhooked ntdll!NtMapViewOfSection via short patch\n"); + fflush(stdout); + } + + return true; +#else + return true; +#endif +} + +constexpr uintptr_t NoStubAddressCheck = 0; +constexpr uintptr_t ExpectedFail = 1; +struct TestCase { + const char* mFunctionName; + uintptr_t mExpectedStub; + bool mPatchedOnce; + explicit TestCase(const char* aFunctionName, uintptr_t aExpectedStub) + : mFunctionName(aFunctionName), + mExpectedStub(aExpectedStub), + mPatchedOnce(false) {} +} g_AssemblyTestCases[] = { +#if defined(__clang__) +// We disable these testcases because the code coverage instrumentation injects +// code in a way that WindowsDllInterceptor doesn't understand. +# ifndef MOZ_CODE_COVERAGE +# if defined(_M_X64) + // Since we have PatchIfTargetIsRecognizedTrampoline for x64, we expect the + // original jump destination is returned as a stub. + TestCase("MovPushRet", JumpDestination), + TestCase("MovRaxJump", JumpDestination), + TestCase("DoubleJump", JumpDestination), + + // Passing NoStubAddressCheck as the following testcases return + // a trampoline address instead of the original destination. + TestCase("NearJump", NoStubAddressCheck), + TestCase("OpcodeFF", NoStubAddressCheck), + TestCase("IndirectCall", NoStubAddressCheck), + TestCase("MovImm64", NoStubAddressCheck), +# elif defined(_M_IX86) + // Skip the stub address check as we always generate a trampoline for x86. + TestCase("PushRet", NoStubAddressCheck), + TestCase("MovEaxJump", NoStubAddressCheck), + TestCase("DoubleJump", NoStubAddressCheck), + TestCase("Opcode83", NoStubAddressCheck), + TestCase("LockPrefix", NoStubAddressCheck), + TestCase("LooksLikeLockPrefix", NoStubAddressCheck), +# endif +# if !defined(DEBUG) + // Skip on Debug build because it hits MOZ_ASSERT_UNREACHABLE. + TestCase("UnsupportedOp", ExpectedFail), +# endif // !defined(DEBUG) +# endif // MOZ_CODE_COVERAGE +#endif // defined(__clang__) +}; + +template <typename InterceptorType> +bool TestAssemblyFunctions() { + static const auto patchedFunction = []() { patched_func_called = true; }; + + InterceptorType interceptor; + interceptor.Init("TestDllInterceptor.exe"); + + for (auto& testCase : g_AssemblyTestCases) { + if (testCase.mExpectedStub == NoStubAddressCheck && testCase.mPatchedOnce) { + // For the testcases with NoStubAddressCheck, we revert a hook by + // jumping into the original stub, which is not detourable again. + continue; + } + + typename InterceptorType::template FuncHookType<void (*)()> hook; + bool result = + hook.Set(interceptor, testCase.mFunctionName, patchedFunction); + if (testCase.mExpectedStub == ExpectedFail) { + if (result) { + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "Unexpectedly succeeded to detour %s.\n", + testCase.mFunctionName); + return false; + } +#if defined(NIGHTLY_BUILD) + const Maybe<DetourError>& maybeError = interceptor.GetLastDetourError(); + if (maybeError.isNothing()) { + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "DetourError was not set on detour error.\n"); + return false; + } + if (maybeError.ref().mErrorCode != + DetourResultCode::DETOUR_PATCHER_CREATE_TRAMPOLINE_ERROR) { + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "A wrong detour errorcode was set on detour error.\n"); + return false; + } +#endif // defined(NIGHTLY_BUILD) + printf("TEST-PASS | WindowsDllInterceptor | %s\n", + testCase.mFunctionName); + continue; + } + + if (!result) { + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "Failed to detour %s.\n", + testCase.mFunctionName); + return false; + } + + testCase.mPatchedOnce = true; + + const auto actualStub = reinterpret_cast<uintptr_t>(hook.GetStub()); + if (testCase.mExpectedStub != NoStubAddressCheck && + actualStub != testCase.mExpectedStub) { + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "Wrong stub was backed up for %s: %zx\n", + testCase.mFunctionName, actualStub); + return false; + } + + patched_func_called = false; + + auto originalFunction = reinterpret_cast<void (*)()>( + GetProcAddress(GetModuleHandleW(nullptr), testCase.mFunctionName)); + originalFunction(); + + if (!patched_func_called) { + printf( + "TEST-FAILED | WindowsDllInterceptor | " + "Hook from %s was not called\n", + testCase.mFunctionName); + return false; + } + + printf("TEST-PASS | WindowsDllInterceptor | %s\n", testCase.mFunctionName); + } + + return true; +} + +bool TestDynamicCodePolicy() { + if (!IsWin8Point1OrLater()) { + // Skip if a platform does not support this policy. + return true; + } + + PROCESS_MITIGATION_DYNAMIC_CODE_POLICY policy = {}; + policy.ProhibitDynamicCode = true; + + mozilla::DynamicallyLinkedFunctionPtr<decltype(&SetProcessMitigationPolicy)> + pSetProcessMitigationPolicy(L"kernel32.dll", + "SetProcessMitigationPolicy"); + if (!pSetProcessMitigationPolicy) { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | " + "SetProcessMitigationPolicy does not exist.\n"); + fflush(stdout); + return false; + } + + if (!pSetProcessMitigationPolicy(ProcessDynamicCodePolicy, &policy, + sizeof(policy))) { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | " + "Fail to enable ProcessDynamicCodePolicy.\n"); + fflush(stdout); + return false; + } + + WindowsDllInterceptor ExeIntercept; + ExeIntercept.Init("TestDllInterceptor.exe"); + + // Make sure we fail to hook a function if ProcessDynamicCodePolicy is on + // because we cannot create an executable trampoline region. + if (orig_payloadNotHooked.Set(ExeIntercept, "payloadNotHooked", + &patched_rotatePayload)) { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | " + "ProcessDynamicCodePolicy is not working.\n"); + fflush(stdout); + return false; + } + + printf( + "TEST-PASS | WindowsDllInterceptor | " + "Successfully passed TestDynamicCodePolicy.\n"); + fflush(stdout); + return true; +} + +extern "C" int wmain(int argc, wchar_t* argv[]) { + LARGE_INTEGER start; + QueryPerformanceCounter(&start); + + // We disable this part of the test because the code coverage instrumentation + // injects code in rotatePayload in a way that WindowsDllInterceptor doesn't + // understand. +#ifndef MOZ_CODE_COVERAGE + payload initial = {0x12345678, 0xfc4e9d31, 0x87654321}; + payload p0, p1; + ZeroMemory(&p0, sizeof(p0)); + ZeroMemory(&p1, sizeof(p1)); + + p0 = rotatePayload(initial); + + { + WindowsDllInterceptor ExeIntercept; + ExeIntercept.Init("TestDllInterceptor.exe"); + if (orig_rotatePayload.Set(ExeIntercept, "rotatePayload", + &patched_rotatePayload)) { + printf("TEST-PASS | WindowsDllInterceptor | Hook added\n"); + fflush(stdout); + } else { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | Failed to add " + "hook\n"); + fflush(stdout); + return 1; + } + + p1 = rotatePayload(initial); + + if (patched_func_called) { + printf("TEST-PASS | WindowsDllInterceptor | Hook called\n"); + fflush(stdout); + } else { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | Hook was not " + "called\n"); + fflush(stdout); + return 1; + } + + if (p0 == p1) { + printf("TEST-PASS | WindowsDllInterceptor | Hook works properly\n"); + fflush(stdout); + } else { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | Hook didn't return " + "the right information\n"); + fflush(stdout); + return 1; + } + } + + patched_func_called = false; + ZeroMemory(&p1, sizeof(p1)); + + p1 = rotatePayload(initial); + + if (ShouldTestUnhookFunction != patched_func_called) { + printf( + "TEST-PASS | WindowsDllInterceptor | Hook was %scalled after " + "unregistration\n", + ShouldTestUnhookFunction ? "not " : ""); + fflush(stdout); + } else { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | Hook was %scalled " + "after unregistration\n", + ShouldTestUnhookFunction ? "" : "not "); + fflush(stdout); + return 1; + } + + if (p0 == p1) { + printf( + "TEST-PASS | WindowsDllInterceptor | Original function worked " + "properly\n"); + fflush(stdout); + } else { + printf( + "TEST-UNEXPECTED-FAIL | WindowsDllInterceptor | Original function " + "didn't return the right information\n"); + fflush(stdout); + return 1; + } +#endif + + CredHandle credHandle; + memset(&credHandle, 0, sizeof(CredHandle)); + OBJECT_ATTRIBUTES attributes = {}; + + // NB: These tests should be ordered such that lower-level APIs are tested + // before higher-level APIs. + if (TestShortDetour() && + // Run <ShortInterceptor> first because <WindowsDllInterceptor> + // does not clean up hooks. +#if defined(_M_X64) + TestAssemblyFunctions<ShortInterceptor>() && +#endif + TestAssemblyFunctions<WindowsDllInterceptor>() && +#ifdef _M_IX86 + // We keep this test to hook complex code on x86. (Bug 850957) + TEST_HOOK("ntdll.dll", NtFlushBuffersFile, NotEquals, 0) && +#endif + TEST_HOOK("ntdll.dll", NtCreateFile, NotEquals, 0) && + TEST_HOOK("ntdll.dll", NtReadFile, NotEquals, 0) && + TEST_HOOK("ntdll.dll", NtReadFileScatter, NotEquals, 0) && + TEST_HOOK("ntdll.dll", NtWriteFile, NotEquals, 0) && + TEST_HOOK("ntdll.dll", NtWriteFileGather, NotEquals, 0) && + TEST_HOOK_PARAMS("ntdll.dll", NtQueryFullAttributesFile, NotEquals, 0, + &attributes, nullptr) && + TEST_DETOUR_SKIP_EXEC("ntdll.dll", LdrLoadDll) && + TEST_HOOK("ntdll.dll", LdrUnloadDll, NotEquals, 0) && + MAYBE_TEST_HOOK_SKIP_EXEC(IsWin8OrLater(), "ntdll.dll", + LdrResolveDelayLoadedAPI) && + MAYBE_TEST_HOOK_PARAMS(HasApiSetQueryApiSetPresence(), + "Api-ms-win-core-apiquery-l1-1-0.dll", + ApiSetQueryApiSetPresence, Equals, FALSE, + &gEmptyUnicodeString, &gIsPresent) && + TEST_HOOK("kernelbase.dll", QueryDosDeviceW, Equals, 0) && + TEST_HOOK("kernel32.dll", GetFileAttributesW, Equals, + INVALID_FILE_ATTRIBUTES) && +#if !defined(_M_ARM64) +# ifndef MOZ_ASAN + // Bug 733892: toolkit/crashreporter/nsExceptionHandler.cpp + // This fails on ASan because the ASan runtime already hooked this + // function + TEST_HOOK("kernel32.dll", SetUnhandledExceptionFilter, Ignore, nullptr) && +# endif +#endif // !defined(_M_ARM64) +#ifdef _M_IX86 + TEST_HOOK_FOR_INVALID_HANDLE_VALUE("kernel32.dll", CreateFileW) && +#endif +#if !defined(_M_ARM64) + TEST_HOOK_FOR_INVALID_HANDLE_VALUE("kernel32.dll", CreateFileA) && +#endif // !defined(_M_ARM64) +#if !defined(_M_ARM64) + TEST_HOOK("kernel32.dll", TlsAlloc, NotEquals, TLS_OUT_OF_INDEXES) && + TEST_HOOK_PARAMS("kernel32.dll", TlsFree, Equals, FALSE, + TLS_OUT_OF_INDEXES) && + TEST_HOOK("kernel32.dll", CloseHandle, Equals, FALSE) && + TEST_HOOK("kernel32.dll", DuplicateHandle, Equals, FALSE) && +#endif // !defined(_M_ARM64) + TEST_DETOUR_SKIP_EXEC("kernel32.dll", BaseThreadInitThunk) && +#if defined(_M_X64) || defined(_M_ARM64) + MAYBE_TEST_HOOK(!IsWin8OrLater(), "kernel32.dll", + RtlInstallFunctionTableCallback, Equals, FALSE) && + TEST_HOOK("user32.dll", GetKeyState, Ignore, 0) && // see Bug 1316415 +#endif + TEST_HOOK("user32.dll", GetWindowInfo, Equals, FALSE) && + TEST_HOOK("user32.dll", TrackPopupMenu, Equals, FALSE) && + TEST_DETOUR("user32.dll", CreateWindowExW, Equals, nullptr) && + TEST_HOOK("user32.dll", InSendMessageEx, Equals, ISMEX_NOSEND) && + TEST_HOOK("user32.dll", SendMessageTimeoutW, Equals, 0) && + TEST_HOOK("user32.dll", SetCursorPos, NotEquals, FALSE) && +#if !defined(_M_ARM64) + TEST_HOOK("imm32.dll", ImmGetContext, Equals, nullptr) && +#endif // !defined(_M_ARM64) + TEST_HOOK("imm32.dll", ImmGetCompositionStringW, Ignore, 0) && + TEST_HOOK_SKIP_EXEC("imm32.dll", ImmSetCandidateWindow) && + TEST_HOOK("imm32.dll", ImmNotifyIME, Equals, 0) && + TEST_HOOK("comdlg32.dll", GetSaveFileNameW, Ignore, FALSE) && + TEST_HOOK("comdlg32.dll", GetOpenFileNameW, Ignore, FALSE) && +#if defined(_M_X64) + TEST_HOOK("comdlg32.dll", PrintDlgW, Ignore, 0) && +#endif + MAYBE_TEST_HOOK(ShouldTestTipTsf(), "tiptsf.dll", ProcessCaretEvents, + Ignore, nullptr) && + TEST_HOOK("wininet.dll", InternetOpenA, NotEquals, nullptr) && + TEST_HOOK("wininet.dll", InternetCloseHandle, Equals, FALSE) && + TEST_HOOK("wininet.dll", InternetConnectA, Equals, nullptr) && + TEST_HOOK("wininet.dll", InternetQueryDataAvailable, Equals, FALSE) && + TEST_HOOK("wininet.dll", InternetReadFile, Equals, FALSE) && + TEST_HOOK("wininet.dll", InternetWriteFile, Equals, FALSE) && + TEST_HOOK("wininet.dll", InternetSetOptionA, Equals, FALSE) && + TEST_HOOK("wininet.dll", HttpAddRequestHeadersA, Equals, FALSE) && + TEST_HOOK("wininet.dll", HttpOpenRequestA, Equals, nullptr) && + TEST_HOOK("wininet.dll", HttpQueryInfoA, Equals, FALSE) && + TEST_HOOK("wininet.dll", HttpSendRequestA, Equals, FALSE) && + TEST_HOOK("wininet.dll", HttpSendRequestExA, Equals, FALSE) && + TEST_HOOK("wininet.dll", HttpEndRequestA, Equals, FALSE) && + TEST_HOOK("wininet.dll", InternetQueryOptionA, Equals, FALSE) && + TEST_HOOK("sspicli.dll", AcquireCredentialsHandleA, NotEquals, + SEC_E_OK) && + TEST_HOOK_PARAMS("sspicli.dll", QueryCredentialsAttributesA, Equals, + SEC_E_INVALID_HANDLE, &credHandle, 0, nullptr) && + TEST_HOOK_PARAMS("sspicli.dll", FreeCredentialsHandle, Equals, + SEC_E_INVALID_HANDLE, &credHandle) && + // Run TestDynamicCodePolicy() at the end because the policy is + // irreversible. + TestDynamicCodePolicy()) { + printf("TEST-PASS | WindowsDllInterceptor | all checks passed\n"); + + LARGE_INTEGER end, freq; + QueryPerformanceCounter(&end); + + QueryPerformanceFrequency(&freq); + + LARGE_INTEGER result; + result.QuadPart = end.QuadPart - start.QuadPart; + result.QuadPart *= 1000000; + result.QuadPart /= freq.QuadPart; + + printf("Elapsed time: %lld microseconds\n", result.QuadPart); + + return 0; + } + + return 1; +} diff --git a/mozglue/tests/interceptor/TestDllInterceptor.exe.manifest b/mozglue/tests/interceptor/TestDllInterceptor.exe.manifest new file mode 100644 index 0000000000..11287012c5 --- /dev/null +++ b/mozglue/tests/interceptor/TestDllInterceptor.exe.manifest @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" + manifestVersion="1.0" + xmlns:asmv3="urn:schemas-microsoft-com:asm.v3"> + <assemblyIdentity type="win32" + name="TestDllInterceptor" + version="1.0.0.0" /> + <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> + <application> + <!-- Need this to use functions in WindowsVersion.h --> + <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> <!-- Win10 --> + <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> <!-- Win8.1 --> + <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> <!-- Win8 --> + <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> <!-- Win7 --> + </application> + </compatibility> +</assembly> diff --git a/mozglue/tests/interceptor/TestDllInterceptorCrossProcess.cpp b/mozglue/tests/interceptor/TestDllInterceptorCrossProcess.cpp new file mode 100644 index 0000000000..5bba4b1f8c --- /dev/null +++ b/mozglue/tests/interceptor/TestDllInterceptorCrossProcess.cpp @@ -0,0 +1,159 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "mozilla/Attributes.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/CmdLineAndEnvUtils.h" +#include "nsWindowsDllInterceptor.h" +#include "nsWindowsHelpers.h" + +#include <string> + +using std::wstring; + +extern "C" __declspec(dllexport) int ReturnResult() { return 2; } + +static mozilla::CrossProcessDllInterceptor::FuncHookType<decltype( + &ReturnResult)> + gOrigReturnResult; + +static int ReturnResultHook() { + if (gOrigReturnResult() != 2) { + return 3; + } + + return 0; +} + +int ParentMain(int argc, wchar_t* argv[]) { + mozilla::SetArgv0ToFullBinaryPath(argv); + + // We'll add the child process to a job so that, in the event of a failure in + // this parent process, the child process will be automatically terminated. + nsAutoHandle job(::CreateJobObjectW(nullptr, nullptr)); + if (!job) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | Job creation " + "failed\n"); + return 1; + } + + JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobInfo = {}; + jobInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + + if (!::SetInformationJobObject(job.get(), JobObjectExtendedLimitInformation, + &jobInfo, sizeof(jobInfo))) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | Job config " + "failed\n"); + return 1; + } + + wchar_t childArgv_1[] = L"-child"; + + wchar_t* childArgv[] = {argv[0], childArgv_1}; + + mozilla::UniquePtr<wchar_t[]> cmdLine( + mozilla::MakeCommandLine(mozilla::ArrayLength(childArgv), childArgv)); + + STARTUPINFOW si = {sizeof(si)}; + PROCESS_INFORMATION pi; + if (!::CreateProcessW(argv[0], cmdLine.get(), nullptr, nullptr, FALSE, + CREATE_SUSPENDED, nullptr, nullptr, &si, &pi)) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | Failed to spawn " + "child process\n"); + return 1; + } + + nsAutoHandle childProcess(pi.hProcess); + nsAutoHandle childMainThread(pi.hThread); + + if (!::AssignProcessToJobObject(job.get(), childProcess.get())) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | Failed to assign " + "child process to job\n"); + ::TerminateProcess(childProcess.get(), 1); + return 1; + } + + mozilla::nt::CrossExecTransferManager transferMgr(childProcess); + if (!transferMgr) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | " + "CrossExecTransferManager instantiation failed.\n"); + return 1; + } + + mozilla::CrossProcessDllInterceptor intcpt(childProcess.get()); + intcpt.Init("TestDllInterceptorCrossProcess.exe"); + + if (!gOrigReturnResult.Set(transferMgr, intcpt, "ReturnResult", + &ReturnResultHook)) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | Failed to add " + "hook\n"); + return 1; + } + + printf("TEST-PASS | DllInterceptorCrossProcess | Hook added\n"); + + if (::ResumeThread(childMainThread.get()) == static_cast<DWORD>(-1)) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | Failed to resume " + "child thread\n"); + return 1; + } + + BOOL remoteDebugging; + bool debugging = + ::IsDebuggerPresent() || + (::CheckRemoteDebuggerPresent(childProcess.get(), &remoteDebugging) && + remoteDebugging); + + DWORD waitResult = + ::WaitForSingleObject(childProcess.get(), debugging ? INFINITE : 60000); + if (waitResult != WAIT_OBJECT_0) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | Child process " + "failed to finish\n"); + return 1; + } + + DWORD childExitCode; + if (!::GetExitCodeProcess(childProcess.get(), &childExitCode)) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | Failed to obtain " + "child process exit code\n"); + return 1; + } + + if (childExitCode) { + printf( + "TEST-UNEXPECTED-FAIL | DllInterceptorCrossProcess | Child process " + "exit code is %lu instead of 0\n", + childExitCode); + return 1; + } + + printf( + "TEST-PASS | DllInterceptorCrossProcess | Child process exit code is " + "zero\n"); + return 0; +} + +extern "C" int wmain(int argc, wchar_t* argv[]) { + if (argc > 1) { + // clang keeps inlining this call despite every attempt to force it to do + // otherwise. We'll use GetProcAddress and call its function pointer + // instead. + auto pReturnResult = reinterpret_cast<decltype(&ReturnResult)>( + ::GetProcAddress(::GetModuleHandleW(nullptr), "ReturnResult")); + return pReturnResult(); + } + + return ParentMain(argc, argv); +} diff --git a/mozglue/tests/interceptor/TestIATPatcher.cpp b/mozglue/tests/interceptor/TestIATPatcher.cpp new file mode 100644 index 0000000000..4dfb81ee9d --- /dev/null +++ b/mozglue/tests/interceptor/TestIATPatcher.cpp @@ -0,0 +1,121 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "mozilla/Assertions.h" +#include "mozilla/DynamicallyLinkedFunctionPtr.h" +#include "nsWindowsDllInterceptor.h" +#include "nsWindowsHelpers.h" + +#include <shlwapi.h> + +static int NormalImport() { return ::GetSystemMetrics(SM_CYCAPTION); } + +static bool DelayLoadImport() { + return !!::UrlIsW(L"http://example.com/", URLIS_FILEURL); +} + +static mozilla::WindowsIATPatcher::FuncHookType<decltype(&::GetSystemMetrics)> + gGetSystemMetricsHook; + +static mozilla::WindowsIATPatcher::FuncHookType<decltype(&::MessageBoxA)> + gMessageBoxAHook; + +static mozilla::WindowsIATPatcher::FuncHookType<decltype(&::UrlIsW)> gUrlIsHook; + +static bool gGetSystemMetricsHookCalled = false; + +static int WINAPI GetSystemMetricsHook(int aIndex) { + MOZ_DIAGNOSTIC_ASSERT(aIndex == SM_CYCAPTION); + gGetSystemMetricsHookCalled = true; + return 0; +} + +static bool gUrlIsHookCalled = false; + +static BOOL WINAPI UrlIsWHook(PCWSTR aUrl, URLIS aFlags) { + gUrlIsHookCalled = true; + return TRUE; +} + +static HMODULE GetStrongReferenceToExeModule() { + HMODULE result; + if (!::GetModuleHandleExW(0, nullptr, &result)) { + return nullptr; + } + + return result; +} + +#define PRINT_FAIL(msg) printf("TEST-UNEXPECTED-FAIL | IATPatcher | " msg "\n") + +extern "C" int wmain(int argc, wchar_t* argv[]) { + nsModuleHandle ourModule1(GetStrongReferenceToExeModule()); + if (!ourModule1) { + PRINT_FAIL("Failed obtaining HMODULE for executable"); + return 1; + } + + if (!gGetSystemMetricsHook.Set(ourModule1, "user32.dll", "GetSystemMetrics", + &GetSystemMetricsHook)) { + PRINT_FAIL("Failed setting GetSystemMetrics hook"); + return 1; + } + + if (NormalImport() || !gGetSystemMetricsHookCalled) { + PRINT_FAIL("GetSystemMetrics hook was not called"); + return 1; + } + + static const mozilla::StaticDynamicallyLinkedFunctionPtr<decltype( + &::GetSystemMetrics)> + pRealGetSystemMetrics(L"user32.dll", "GetSystemMetrics"); + if (!pRealGetSystemMetrics) { + PRINT_FAIL("Failed resolving real GetSystemMetrics pointer"); + return 1; + } + + if (gGetSystemMetricsHook.GetStub() != pRealGetSystemMetrics) { + PRINT_FAIL( + "GetSystemMetrics hook stub pointer does not match real " + "GetSystemMetrics pointer"); + return 1; + } + + nsModuleHandle ourModule2(GetStrongReferenceToExeModule()); + if (!ourModule2) { + PRINT_FAIL("Failed obtaining HMODULE for executable"); + return 1; + } + + // This should fail becuase the test never calls, and thus never imports, + // MessageBoxA + if (gMessageBoxAHook.Set(ourModule2, "user32.dll", "MessageBoxA", nullptr)) { + PRINT_FAIL("Setting MessageBoxA hook succeeded when it should have failed"); + return 1; + } + + nsModuleHandle ourModule3(GetStrongReferenceToExeModule()); + if (!ourModule3) { + PRINT_FAIL("Failed obtaining HMODULE for executable"); + return 1; + } + + // These tests involve a delay-loaded import, which are not supported; we + // expect these tests to FAIL. + + if (gUrlIsHook.Set(ourModule3, "shlwapi.dll", "UrlIsW", &UrlIsWHook)) { + PRINT_FAIL("gUrlIsHook.Set should have failed"); + return 1; + } + + if (DelayLoadImport() || gUrlIsHookCalled) { + PRINT_FAIL("gUrlIsHook should not have been called"); + return 1; + } + + printf("TEST-PASS | IATPatcher | All tests passed.\n"); + return 0; +} diff --git a/mozglue/tests/interceptor/TestMMPolicy.cpp b/mozglue/tests/interceptor/TestMMPolicy.cpp new file mode 100644 index 0000000000..9bb50f683b --- /dev/null +++ b/mozglue/tests/interceptor/TestMMPolicy.cpp @@ -0,0 +1,198 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsWindowsDllInterceptor.h" + +#include <functional> + +mozilla::interceptor::MMPolicyInProcess gPolicy; + +void DepleteVirtualAddress( + uint8_t* aStart, size_t aSize, + const std::function<void(void*)>& aPostAllocCallback) { + const DWORD granularity = gPolicy.GetAllocGranularity(); + if (aStart == 0 || aSize < granularity) { + return; + } + + uint8_t* alignedStart = reinterpret_cast<uint8_t*>( + (((reinterpret_cast<uintptr_t>(aStart) - 1) / granularity) + 1) * + granularity); + aSize -= (alignedStart - aStart); + if (auto p = VirtualAlloc(alignedStart, aSize, MEM_RESERVE, PAGE_NOACCESS)) { + aPostAllocCallback(p); + return; + } + + uintptr_t mask = ~(static_cast<uintptr_t>(granularity) - 1); + size_t halfSize = (aSize >> 1) & mask; + if (halfSize == 0) { + return; + } + + DepleteVirtualAddress(aStart, halfSize, aPostAllocCallback); + DepleteVirtualAddress(aStart + halfSize, aSize - halfSize, + aPostAllocCallback); +} + +bool ValidateFreeRegion(LPVOID aRegion, size_t aDesiredLen) { + MEMORY_BASIC_INFORMATION mbi; + if (VirtualQuery(aRegion, &mbi, sizeof(mbi)) != sizeof(mbi)) { + printf( + "TEST-FAILED | TestMMPolicy | " + "VirtualQuery(%p) failed - %08lx\n", + aRegion, GetLastError()); + return false; + } + + if (mbi.State != MEM_FREE) { + printf( + "TEST-FAILED | TestMMPolicy | " + "%p is not within a free region\n", + aRegion); + return false; + } + + if (aRegion != mbi.BaseAddress || + reinterpret_cast<uintptr_t>(mbi.BaseAddress) % + gPolicy.GetAllocGranularity()) { + printf( + "TEST-FAILED | TestMMPolicy | " + "%p is not a region's start address\n", + aRegion); + return false; + } + + LPVOID allocated = VirtualAlloc(aRegion, aDesiredLen, + MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); + if (!allocated) { + printf( + "TEST-FAILED | TestMMPolicy | " + "VirtualAlloc(%p) failed - %08lx\n", + aRegion, GetLastError()); + return false; + } + + if (!VirtualFree(allocated, 0, MEM_RELEASE)) { + printf( + "TEST-FAILED | TestMMPolicy | " + "VirtualFree(%p) failed - %08lx\n", + allocated, GetLastError()); + return false; + } + + return true; +} + +bool TestFindRegion() { + // Skip the near-null addresses + uint8_t* minAddr = reinterpret_cast<uint8_t*>( + std::max(gPolicy.GetAllocGranularity(), 0x1000000ul)); + // 64bit address space is too large to deplete. 32bit space is enough. + uint8_t* maxAddr = reinterpret_cast<uint8_t*>(std::min( + gPolicy.GetMaxUserModeAddress(), static_cast<uintptr_t>(0xffffffff))); + + // Keep one of the regions we allocate so that we can release it later. + void* lastResort = nullptr; + + // Reserve all free regions in the range [minAddr, maxAddr] + for (uint8_t* address = minAddr; address <= maxAddr;) { + MEMORY_BASIC_INFORMATION mbi; + if (VirtualQuery(address, &mbi, sizeof(mbi)) != sizeof(mbi)) { + printf( + "TEST-FAILED | TestMMPolicy | " + "VirtualQuery(%p) failed - %08lx\n", + address, GetLastError()); + break; + } + + address = reinterpret_cast<uint8_t*>(mbi.BaseAddress); + if (mbi.State == MEM_FREE) { + DepleteVirtualAddress(address, mbi.RegionSize, + [&lastResort](void* aAllocated) { + // Pick the first address we allocate to make sure + // FindRegion scans the full range. + if (!lastResort) { + lastResort = aAllocated; + } + }); + } + + address += mbi.RegionSize; + } + + if (!lastResort) { + printf( + "TEST-SKIPPED | TestMMPolicy | " + "No free region in [%p - %p]. Skipping the testcase.\n", + minAddr, maxAddr); + return true; + } + + // Make sure there are no free regions + PVOID freeRegion = + gPolicy.FindRegion(GetCurrentProcess(), 1, minAddr, maxAddr); + if (freeRegion) { + if (reinterpret_cast<uintptr_t>(freeRegion) % + gPolicy.GetAllocGranularity()) { + printf( + "TEST-FAILED | TestMMPolicy | " + "MMPolicyBase::FindRegion returned an unaligned address %p.\n", + freeRegion); + return false; + } + + printf( + "TEST-SKIPPED | TestMMPolicy | " + "%p was freed after depletion. Skipping the testcase.\n", + freeRegion); + return true; + } + + // Free one region, and thus we can expect FindRegion finds this region + if (!VirtualFree(lastResort, 0, MEM_RELEASE)) { + printf( + "TEST-FAILED | TestMMPolicy | " + "VirtualFree(%p) failed - %08lx\n", + lastResort, GetLastError()); + return false; + } + printf("The region starting from %p has been freed.\n", lastResort); + + // Run the function several times because it uses a randon number inside + // and its result is nondeterministic. + for (int i = 0; i < 50; ++i) { + // Because one region was freed, a desire up to one region + // should be fulfilled. + const size_t desiredLengths[] = {1, gPolicy.GetAllocGranularity()}; + + for (auto desiredLen : desiredLengths) { + freeRegion = + gPolicy.FindRegion(GetCurrentProcess(), desiredLen, minAddr, maxAddr); + if (!freeRegion) { + printf( + "TEST-FAILED | TestMMPolicy | " + "Failed to find a free region.\n"); + return false; + } + + if (!ValidateFreeRegion(freeRegion, desiredLen)) { + return false; + } + } + } + + return true; +} + +extern "C" int wmain(int argc, wchar_t* argv[]) { + if (!TestFindRegion()) { + return 1; + } + + printf("TEST-PASS | TestMMPolicy | All tests passed.\n"); + return 0; +} diff --git a/mozglue/tests/interceptor/moz.build b/mozglue/tests/interceptor/moz.build new file mode 100644 index 0000000000..c179125cff --- /dev/null +++ b/mozglue/tests/interceptor/moz.build @@ -0,0 +1,40 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +GeckoCppUnitTests( + [ + "TestDllInterceptor", + "TestIATPatcher", + "TestMMPolicy", + ], + linkage=None, +) + +if CONFIG["OS_TARGET"] == "WINNT" and CONFIG["CPU_ARCH"] in ("x86", "x86_64"): + # Cross-process interceptors not yet supported on aarch64 + GeckoCppUnitTests( + [ + "TestDllInterceptorCrossProcess", + ], + linkage=None, + ) + +OS_LIBS += [ + "ntdll", + "ole32", + "shlwapi", + "user32", +] + +DELAYLOAD_DLLS += [ + "shlwapi.dll", +] + +if CONFIG["OS_TARGET"] == "WINNT" and CONFIG["CC_TYPE"] in ("gcc", "clang"): + # This allows us to use wmain as the entry point on mingw + LDFLAGS += [ + "-municode", + ] diff --git a/mozglue/tests/moz.build b/mozglue/tests/moz.build new file mode 100644 index 0000000000..472e9d315d --- /dev/null +++ b/mozglue/tests/moz.build @@ -0,0 +1,51 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DisableStlWrapping() + +# Important: for CppUnitTests to be run, they also need to be added +# to testing/cppunittest.ini. + +GeckoCppUnitTests( + [ + "ShowSSEConfig", + ], + linkage=None, +) + +CppUnitTests( + [ + "TestBaseProfiler", + "TestPrintf", + ] +) + +with Files("TestBaseProfiler.cpp"): + BUG_COMPONENT = ("Core", "Gecko Profiler") + +if CONFIG["OS_ARCH"] == "WINNT": + GeckoCppUnitTests( + [ + "TestNativeNt", + "TestPEExportSection", + "TestTimeStampWin", + ], + linkage=None, + ) + TEST_DIRS += [ + "interceptor", + "gtest", + ] + OS_LIBS += [ + "ntdll", + "version", + ] + +if CONFIG["OS_TARGET"] == "WINNT" and CONFIG["CC_TYPE"] in ("gcc", "clang"): + # This allows us to use wmain as the entry point on mingw + LDFLAGS += [ + "-municode", + ] |