summaryrefslogtreecommitdiffstats
path: root/mozglue/tests
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /mozglue/tests
parentInitial commit. (diff)
downloadfirefox-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')
-rw-r--r--mozglue/tests/ShowSSEConfig.cpp125
-rw-r--r--mozglue/tests/TestBaseProfiler.cpp4452
-rw-r--r--mozglue/tests/TestNativeNt.cpp295
-rw-r--r--mozglue/tests/TestPEExportSection.cpp698
-rw-r--r--mozglue/tests/TestPrintf.cpp164
-rw-r--r--mozglue/tests/TestTimeStampWin.cpp97
-rw-r--r--mozglue/tests/gtest/TestDLLBlocklist.cpp161
-rw-r--r--mozglue/tests/gtest/TestDllBlocklist_AllowByVersion/TestDllBlocklist_AllowByVersion.cpp7
-rw-r--r--mozglue/tests/gtest/TestDllBlocklist_AllowByVersion/TestDllBlocklist_AllowByVersion.rc42
-rw-r--r--mozglue/tests/gtest/TestDllBlocklist_AllowByVersion/moz.build17
-rw-r--r--mozglue/tests/gtest/TestDllBlocklist_MatchByName/TestDllBlocklist_MatchByName.cpp7
-rw-r--r--mozglue/tests/gtest/TestDllBlocklist_MatchByName/moz.build15
-rw-r--r--mozglue/tests/gtest/TestDllBlocklist_MatchByVersion/TestDllBlocklist_MatchByVersion.cpp7
-rw-r--r--mozglue/tests/gtest/TestDllBlocklist_MatchByVersion/TestDllBlocklist_MatchByVersion.rc42
-rw-r--r--mozglue/tests/gtest/TestDllBlocklist_MatchByVersion/moz.build17
-rw-r--r--mozglue/tests/gtest/TestDllBlocklist_NoOpEntryPoint/TestDllBlocklist_NoOpEntryPoint.cpp12
-rw-r--r--mozglue/tests/gtest/TestDllBlocklist_NoOpEntryPoint/TestDllBlocklist_NoOpEntryPoint.rc42
-rw-r--r--mozglue/tests/gtest/TestDllBlocklist_NoOpEntryPoint/moz.build17
-rw-r--r--mozglue/tests/gtest/TestNativeNtGTest.cpp20
-rw-r--r--mozglue/tests/gtest/moz.build17
-rw-r--r--mozglue/tests/interceptor/AssemblyPayloads.h194
-rw-r--r--mozglue/tests/interceptor/TestDllInterceptor.cpp1105
-rw-r--r--mozglue/tests/interceptor/TestDllInterceptor.exe.manifest17
-rw-r--r--mozglue/tests/interceptor/TestDllInterceptorCrossProcess.cpp159
-rw-r--r--mozglue/tests/interceptor/TestIATPatcher.cpp121
-rw-r--r--mozglue/tests/interceptor/TestMMPolicy.cpp198
-rw-r--r--mozglue/tests/interceptor/moz.build40
-rw-r--r--mozglue/tests/moz.build51
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",
+ ]