/* -*- 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 "gtest/gtest.h" #include "mozilla/MathAlgorithms.h" #include "mozilla/ipc/SharedMemoryCursor.h" #include "mozilla/ipc/SharedMemoryHandle.h" #include "mozilla/ipc/SharedMemoryMapping.h" #ifdef XP_LINUX # include # include # include # include # include # include #endif #ifdef XP_WIN # include #endif namespace mozilla::ipc { #define ASSERT_SHMEM(handle, size) \ do { \ ASSERT_EQ((handle).Size(), size_t(size)); \ if (size_t(size) == 0) { \ ASSERT_FALSE((handle).IsValid()); \ ASSERT_FALSE(handle); \ } else { \ ASSERT_TRUE((handle).IsValid()); \ ASSERT_TRUE(handle); \ } \ } while (0) template struct IPCSharedMemoryFixture : public testing::Test {}; using HandleAndMappingTypes = testing::Types; TYPED_TEST_SUITE(IPCSharedMemoryFixture, HandleAndMappingTypes); TYPED_TEST(IPCSharedMemoryFixture, Null) { TypeParam t; ASSERT_SHMEM(t, 0); if constexpr (std::is_same_v || std::is_same_v) { auto cloned = t.Clone(); ASSERT_SHMEM(cloned, 0); ASSERT_SHMEM(t, 0); } } TEST(IPCSharedMemoryHandle, Create) { auto handle = shared_memory::Create(1); ASSERT_SHMEM(handle, 1); } TEST(IPCSharedMemoryHandle, Move) { auto handle = shared_memory::Create(1); MutableSharedMemoryHandle newHandle(std::move(handle)); ASSERT_SHMEM(handle, 0); ASSERT_SHMEM(newHandle, 1); MutableSharedMemoryHandle assignedHandle; assignedHandle = std::move(newHandle); ASSERT_SHMEM(newHandle, 0); ASSERT_SHMEM(assignedHandle, 1); } TEST(IPCSharedMemoryHandle, ToReadOnly) { auto handle = shared_memory::Create(1); auto roHandle = std::move(handle).ToReadOnly(); ASSERT_SHMEM(handle, 0); ASSERT_SHMEM(roHandle, 1); } TEST(IPCSharedMemoryHandle, Clone) { auto handle = shared_memory::Create(1); auto clonedHandle = handle.Clone(); ASSERT_SHMEM(handle, 1); ASSERT_SHMEM(clonedHandle, 1); } TEST(IPCSharedMemoryHandle, ROClone) { auto handle = shared_memory::Create(1).ToReadOnly(); auto clonedHandle = handle.Clone(); ASSERT_SHMEM(handle, 1); ASSERT_SHMEM(clonedHandle, 1); } TEST(IPCSharedMemoryHandle, CreateFreezable) { auto handle = shared_memory::CreateFreezable(1); ASSERT_SHMEM(handle, 1); } TEST(IPCSharedMemoryHandle, WontFreeze) { auto handle = shared_memory::CreateFreezable(1); ASSERT_SHMEM(handle, 1); auto mHandle = std::move(handle).WontFreeze(); ASSERT_SHMEM(handle, 0); ASSERT_SHMEM(mHandle, 1); } TEST(IPCSharedMemoryHandle, Freeze) { auto handle = shared_memory::CreateFreezable(1); ASSERT_SHMEM(handle, 1); auto roHandle = std::move(handle).Freeze(); ASSERT_SHMEM(handle, 0); ASSERT_SHMEM(roHandle, 1); } TEST(IPCSharedMemory, Map) { auto handle = shared_memory::Create(1); auto mapping = handle.Map(); ASSERT_SHMEM(handle, 1); ASSERT_SHMEM(mapping, 1); } TEST(IPCSharedMemory, ROMap) { auto handle = shared_memory::Create(1).ToReadOnly(); auto mapping = handle.Map(); ASSERT_SHMEM(handle, 1); ASSERT_SHMEM(mapping, 1); } TEST(IPCSharedMemory, FreezeMap) { auto handle = shared_memory::CreateFreezable(1); auto mapping = std::move(handle).Map(); ASSERT_SHMEM(handle, 0); ASSERT_SHMEM(mapping, 1); } TEST(IPCSharedMemoryMapping, Move) { auto handle = shared_memory::Create(1); auto mapping = handle.Map(); SharedMemoryMapping moved(std::move(mapping)); ASSERT_SHMEM(mapping, 0); ASSERT_SHMEM(moved, 1); SharedMemoryMapping moveAssigned; moveAssigned = std::move(moved); ASSERT_SHMEM(moved, 0); ASSERT_SHMEM(moveAssigned, 1); } TEST(IPCSharedMemoryMapping, ROMove) { auto handle = shared_memory::Create(1).ToReadOnly(); auto mapping = handle.Map(); ReadOnlySharedMemoryMapping moved(std::move(mapping)); ASSERT_SHMEM(mapping, 0); ASSERT_SHMEM(moved, 1); ReadOnlySharedMemoryMapping moveAssigned; moveAssigned = std::move(moved); ASSERT_SHMEM(moved, 0); ASSERT_SHMEM(moveAssigned, 1); } TEST(IPCSharedMemoryMapping, FreezeMove) { auto handle = shared_memory::CreateFreezable(1); auto mapping = std::move(handle).Map(); FreezableSharedMemoryMapping moved(std::move(mapping)); ASSERT_SHMEM(mapping, 0); ASSERT_SHMEM(moved, 1); FreezableSharedMemoryMapping moveAssigned; moveAssigned = std::move(moved); ASSERT_SHMEM(moved, 0); ASSERT_SHMEM(moveAssigned, 1); } TEST(IPCSharedMemoryMapping, MutableOrReadOnly) { auto handle = shared_memory::Create(1); auto roHandle = handle.Clone().ToReadOnly(); MutableOrReadOnlySharedMemoryMapping mapping; mapping = handle.Map(); ASSERT_SHMEM(mapping, 1); ASSERT_FALSE(mapping.IsReadOnly()); mapping = roHandle.Map(); ASSERT_SHMEM(mapping, 1); ASSERT_TRUE(mapping.IsReadOnly()); } TEST(IPCSharedMemoryMapping, FreezableFreeze) { auto handle = shared_memory::CreateFreezable(1); auto mapping = std::move(handle).Map(); auto roHandle = std::move(mapping).Freeze(); ASSERT_SHMEM(mapping, 0); ASSERT_SHMEM(roHandle, 1); } TEST(IPCSharedMemoryMapping, FreezableFreezeWithMutableMapping) { auto handle = shared_memory::CreateFreezable(1); auto mapping = std::move(handle).Map(); auto [roHandle, m] = std::move(mapping).FreezeWithMutableMapping(); ASSERT_SHMEM(mapping, 0); ASSERT_SHMEM(roHandle, 1); ASSERT_SHMEM(m, 1); } TEST(IPCSharedMemoryMapping, FreezableUnmap) { auto handle = shared_memory::CreateFreezable(1); auto mapping = std::move(handle).Map(); handle = std::move(mapping).Unmap(); ASSERT_SHMEM(handle, 1); ASSERT_SHMEM(mapping, 0); } // Try to map a frozen shm for writing. Threat model: the process is // compromised and then receives a frozen handle. TEST(IPCSharedMemory, FreezeAndMapRW) { // Create auto handle = ipc::shared_memory::CreateFreezable(1); ASSERT_TRUE(handle); // Initialize auto mapping = std::move(handle).Map(); ASSERT_TRUE(mapping); auto* mem = mapping.DataAs(); ASSERT_TRUE(mem); *mem = 'A'; // Freeze auto [roHandle, rwMapping] = std::move(mapping).FreezeWithMutableMapping(); ASSERT_TRUE(rwMapping); ASSERT_TRUE(roHandle); auto roMapping = roHandle.Map(); ASSERT_TRUE(roMapping); const auto* roMem = roMapping.DataAs(); ASSERT_TRUE(roMem); ASSERT_EQ(*roMem, 'A'); } // Try to restore write permissions to a frozen mapping. Threat // model: the process has mapped frozen shm normally and then is // compromised, or as for FreezeAndMapRW (see also the // proof-of-concept at https://crbug.com/project-zero/1671 ). TEST(IPCSharedMemory, FreezeAndReprotect) { // Create auto handle = ipc::shared_memory::CreateFreezable(1); ASSERT_TRUE(handle); // Initialize auto mapping = std::move(handle).Map(); ASSERT_TRUE(mapping); auto* mem = mapping.DataAs(); ASSERT_TRUE(mem); *mem = 'A'; // Freeze auto roHandle = std::move(mapping).Freeze(); ASSERT_TRUE(roHandle); auto roMapping = roHandle.Map(); ASSERT_TRUE(roMapping); const auto* roMem = roMapping.DataAs(); ASSERT_EQ(*roMem, 'A'); // Try to alter protection; should fail EXPECT_FALSE(ipc::shared_memory::LocalProtect( (char*)roMem, 1, ipc::shared_memory::AccessReadWrite)); } #if !defined(XP_WIN) && !defined(XP_DARWIN) // This essentially tests whether FreezeAndReprotect would have failed // without the freeze. // // It doesn't work on Windows: VirtualProtect can't exceed the permissions set // in MapViewOfFile regardless of the security status of the original handle. // // It doesn't work on MacOS: we can set a higher max_protection for the memory // when creating the handle, but we wouldn't want to do this for freezable // handles (to prevent creating additional RW mappings that break the memory // freezing invariants). TEST(IPCSharedMemory, Reprotect) { // Create auto handle = ipc::shared_memory::CreateFreezable(1); ASSERT_TRUE(handle); // Initialize auto mapping = std::move(handle).Map(); ASSERT_TRUE(mapping); auto* mem = mapping.DataAs(); ASSERT_TRUE(mem); *mem = 'A'; // Unmap without freezing. auto rwHandle = std::move(mapping).Unmap().WontFreeze(); ASSERT_TRUE(rwHandle); auto roHandle = std::move(rwHandle).ToReadOnly(); ASSERT_TRUE(roHandle); // Re-map auto roMapping = roHandle.Map(); ASSERT_TRUE(roMapping); const auto* cmem = roMapping.DataAs(); ASSERT_EQ(*cmem, 'A'); // Try to alter protection; should succeed, because not frozen EXPECT_TRUE(ipc::shared_memory::LocalProtect( (char*)cmem, 1, ipc::shared_memory::AccessReadWrite)); } #endif #ifdef XP_WIN // Try to regain write permissions on a read-only handle using // DuplicateHandle; this will succeed if the object has no DACL. // See also https://crbug.com/338538 TEST(IPCSharedMemory, WinUnfreeze) { // Create auto handle = ipc::shared_memory::CreateFreezable(1); ASSERT_TRUE(handle); // Initialize auto mapping = std::move(handle).Map(); ASSERT_TRUE(mapping); auto* mem = mapping.DataAs(); ASSERT_TRUE(mem); *mem = 'A'; // Freeze auto roHandle = std::move(mapping).Freeze(); ASSERT_TRUE(roHandle); // Extract handle. auto platformHandle = std::move(roHandle).TakePlatformHandle(); // Unfreeze. HANDLE newHandle = INVALID_HANDLE_VALUE; bool unfroze = ::DuplicateHandle( GetCurrentProcess(), platformHandle.release(), GetCurrentProcess(), &newHandle, FILE_MAP_ALL_ACCESS, false, DUPLICATE_CLOSE_SOURCE); ASSERT_FALSE(unfroze); } #endif // Test that a read-only copy sees changes made to the writeable // mapping in the case that the page wasn't accessed before the copy. TEST(IPCSharedMemory, ROCopyAndWrite) { auto handle = ipc::shared_memory::CreateFreezable(1); ASSERT_TRUE(handle); auto [roHandle, rwMapping] = std::move(handle).Map().FreezeWithMutableMapping(); ASSERT_TRUE(rwMapping); ASSERT_TRUE(roHandle); auto roMapping = roHandle.Map(); auto* memRW = rwMapping.DataAs(); ASSERT_TRUE(memRW); const auto* memRO = roMapping.DataAs(); ASSERT_TRUE(memRO); ASSERT_NE(memRW, memRO); *memRW = 'A'; EXPECT_EQ(*memRO, 'A'); } // Test that a read-only copy sees changes made to the writeable // mapping in the case that the page was accessed before the copy // (and, before that, sees the state as of when the copy was made). TEST(IPCSharedMemory, ROCopyAndRewrite) { auto handle = ipc::shared_memory::CreateFreezable(1); ASSERT_TRUE(handle); auto [roHandle, rwMapping] = std::move(handle).Map().FreezeWithMutableMapping(); ASSERT_TRUE(rwMapping); ASSERT_TRUE(roHandle); auto roMapping = roHandle.Map(); auto* memRW = rwMapping.DataAs(); ASSERT_TRUE(memRW); *memRW = 'A'; const auto* memRO = roMapping.DataAs(); ASSERT_TRUE(memRO); ASSERT_NE(memRW, memRO); ASSERT_EQ(*memRW, 'A'); EXPECT_EQ(*memRO, 'A'); *memRW = 'X'; EXPECT_EQ(*memRO, 'X'); } #ifndef FUZZING TEST(IPCSharedMemory, BasicIsZero) { static constexpr size_t kSize = 65536; auto shm = ipc::shared_memory::Create(kSize).Map(); auto* mem = shm.DataAs(); for (size_t i = 0; i < kSize; ++i) { ASSERT_EQ(mem[i], 0) << "offset " << i; } } #endif #if defined(XP_LINUX) && !defined(ANDROID) class IPCSharedMemoryLinuxTest : public ::testing::Test { int mMajor = 0; int mMinor = 0; protected: void SetUp() override { if (mMajor != 0) { return; } struct utsname uts{}; ASSERT_EQ(uname(&uts), 0) << strerror(errno); ASSERT_STREQ(uts.sysname, "Linux"); ASSERT_EQ(sscanf(uts.release, "%d.%d", &mMajor, &mMinor), 2); } bool HaveKernelVersion(int aMajor, int aMinor) { return mMajor > aMajor || (mMajor == aMajor && mMinor >= aMinor); } bool ShouldHaveMemfd() { return HaveKernelVersion(3, 17); } bool ShouldHaveMemfdNoExec() { return HaveKernelVersion(6, 3); } }; // Test that memfd_create is used where expected. // // More precisely: if memfd_create support is expected, verify that // shared memory isn't subject to a filesystem size limit. TEST_F(IPCSharedMemoryLinuxTest, IsMemfd) { auto handle = ipc::shared_memory::Create(1); UniqueFileHandle fd = std::move(handle).TakePlatformHandle(); ASSERT_TRUE(fd); struct statfs fs{}; ASSERT_EQ(fstatfs(fd.get(), &fs), 0) << strerror(errno); EXPECT_EQ(fs.f_type, TMPFS_MAGIC); static constexpr decltype(fs.f_blocks) kNoLimit = 0; if (ShouldHaveMemfd()) { EXPECT_EQ(fs.f_blocks, kNoLimit); } else { // On older kernels, we expect the memfd / no-limit test to fail. // (In theory it could succeed if backported memfd support exists; // if that ever happens, this check can be removed.) EXPECT_NE(fs.f_blocks, kNoLimit); } } TEST_F(IPCSharedMemoryLinuxTest, MemfdNoExec) { const bool expectExec = ShouldHaveMemfd() && !ShouldHaveMemfdNoExec(); auto handle = ipc::shared_memory::Create(1); UniqueFileHandle fd = std::move(handle).TakePlatformHandle(); ASSERT_TRUE(fd); struct stat sb{}; ASSERT_EQ(fstat(fd.get(), &sb), 0) << strerror(errno); // Check that mode is reasonable. EXPECT_EQ(sb.st_mode & (S_IRUSR | S_IWUSR), mode_t(S_IRUSR | S_IWUSR)); // Chech the exec bit EXPECT_EQ(sb.st_mode & S_IXUSR, mode_t(expectExec ? S_IXUSR : 0)); } #endif TEST(IPCSharedMemory, CursorWriteRead) { // Select a chunk size which is at least as big as the allocation granularity, // as smaller sizes will not be able to map. const size_t chunkSize = ipc::shared_memory::SystemAllocationGranularity(); ASSERT_TRUE(IsPowerOfTwo(chunkSize)); const uint64_t fullSize = chunkSize * 20; auto handle = ipc::shared_memory::Create(fullSize); ASSERT_TRUE(handle.IsValid()); ASSERT_EQ(handle.Size(), fullSize); // Map the entire region. auto mapping = handle.Map(); ASSERT_TRUE(mapping.IsValid()); ASSERT_EQ(mapping.Size(), fullSize); // Use a cursor to write some data. ipc::shared_memory::Cursor cursor(std::move(handle)); ASSERT_EQ(cursor.Offset(), 0u); ASSERT_EQ(cursor.Size(), fullSize); // Set the chunk size to ensure we use multiple mappings for this data region. cursor.SetChunkSize(chunkSize); // Two basic blocks of data which are used for writeReadTest. const char data[] = "Hello, World!"; const char data2[] = "AnotherString"; auto writeReadTest = [&]() { uint64_t initialOffset = cursor.Offset(); // Clear out the buffer to a known state so that any checks will fail if // they're depending on previous writes. memset(mapping.Address(), 0xe5, mapping.Size()); // Write "Hello, World" at the offset, and ensure it is reflected in the // full mapping. ASSERT_TRUE(cursor.Write(data, std::size(data))); ASSERT_EQ(cursor.Offset(), initialOffset + std::size(data)); ASSERT_STREQ(mapping.DataAs() + initialOffset, data); // Write some data in the full mapping at the same offset, and enure it can // be read. memcpy(mapping.DataAs() + initialOffset, data2, std::size(data2)); cursor.Seek(initialOffset); ASSERT_EQ(cursor.Offset(), initialOffset); char buffer[std::size(data2)]; ASSERT_TRUE(cursor.Read(buffer, std::size(buffer))); ASSERT_EQ(cursor.Offset(), initialOffset + std::size(buffer)); ASSERT_STREQ(buffer, data2); }; writeReadTest(); // Run the writeReadTest at various offsets within the buffer, including at // every chunk boundary, and in the middle of each chunk. for (size_t offset = chunkSize - 3; offset < fullSize - 3; offset += chunkSize / 2) { cursor.Seek(offset); writeReadTest(); } // Do a writeReadTest at the very end of the allocated region to ensure that // edge case is handled. cursor.Seek(mapping.Size() - std::max(std::size(data), std::size(data2))); writeReadTest(); // Ensure that writes past the end fail safely. cursor.Seek(mapping.Size() - 3); ASSERT_FALSE(cursor.Write(data, std::size(data))); } } // namespace mozilla::ipc