forked from mirrors/gecko-dev
This commit also allows `memfd_create` in the seccomp-bpf policy for all process types. `memfd_create` is an API added in Linux 3.17 (and adopted by FreeBSD for the upcoming version 13) for creating anonymous shared memory not connected to any filesystem. Supporting it means that sandboxed child processes on Linux can create shared memory directly instead of messaging a broker, which is unavoidably slower, and it should avoid the problems we'd been seeing with overly small `/dev/shm` in container environments (which were causing serious problems for using Firefox for automated testing of frontend projects). `memfd_create` also introduces the related operation of file seals: irrevocably preventing types of modifications to a file. Unfortunately, the most useful one, `F_SEAL_WRITE`, can't be relied on; see the large comment in `SharedMemory:ReadOnlyCopy` for details. So we still use the applicable seals as defense in depth, but read-only copies are implemented on Linux by using procfs (and see the comments on the `ReadOnlyCopy` function in `shared_memory_posix.cc` for the subtleties there). There's also a FreeBSD implementation, using `cap_rights_limit` for read-only copies, if the build host is new enough to have the `memfd_create` function. The support code for Android, which doesn't support shm_open and can't use the memfd backend because of issues with its SELinux policy (see bug 1670277), has been reorganized to reflect that we'll always use its own API, ashmem, in that case. Differential Revision: https://phabricator.services.mozilla.com/D90605
342 lines
9.3 KiB
C++
342 lines
9.3 KiB
C++
/* -*- 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 "base/shared_memory.h"
|
|
|
|
#include "base/process_util.h"
|
|
#include "mozilla/RefPtr.h"
|
|
#include "mozilla/ipc/SharedMemory.h"
|
|
#include "mozilla/ipc/SharedMemoryBasic.h"
|
|
|
|
#ifdef XP_LINUX
|
|
# include <errno.h>
|
|
# include <linux/magic.h>
|
|
# include <stdio.h>
|
|
# include <string.h>
|
|
# include <sys/statfs.h>
|
|
# include <sys/utsname.h>
|
|
#endif
|
|
|
|
#ifdef XP_WIN
|
|
# include <windows.h>
|
|
#endif
|
|
|
|
namespace mozilla {
|
|
|
|
// Try to map a frozen shm for writing. Threat model: the process is
|
|
// compromised and then receives a frozen handle.
|
|
TEST(IPCSharedMemory, FreezeAndMapRW)
|
|
{
|
|
base::SharedMemory shm;
|
|
|
|
// Create and initialize
|
|
ASSERT_TRUE(shm.CreateFreezeable(1));
|
|
ASSERT_TRUE(shm.Map(1));
|
|
auto mem = reinterpret_cast<char*>(shm.memory());
|
|
ASSERT_TRUE(mem);
|
|
*mem = 'A';
|
|
|
|
// Freeze
|
|
ASSERT_TRUE(shm.Freeze());
|
|
ASSERT_FALSE(shm.memory());
|
|
|
|
// Re-create as writeable
|
|
auto handle = base::SharedMemory::NULLHandle();
|
|
ASSERT_TRUE(shm.GiveToProcess(base::GetCurrentProcId(), &handle));
|
|
ASSERT_TRUE(shm.IsHandleValid(handle));
|
|
ASSERT_FALSE(shm.IsValid());
|
|
ASSERT_TRUE(shm.SetHandle(handle, /* read-only */ false));
|
|
ASSERT_TRUE(shm.IsValid());
|
|
|
|
// This should fail
|
|
EXPECT_FALSE(shm.Map(1));
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
base::SharedMemory shm;
|
|
|
|
// Create and initialize
|
|
ASSERT_TRUE(shm.CreateFreezeable(1));
|
|
ASSERT_TRUE(shm.Map(1));
|
|
auto mem = reinterpret_cast<char*>(shm.memory());
|
|
ASSERT_TRUE(mem);
|
|
*mem = 'A';
|
|
|
|
// Freeze
|
|
ASSERT_TRUE(shm.Freeze());
|
|
ASSERT_FALSE(shm.memory());
|
|
|
|
// Re-map
|
|
ASSERT_TRUE(shm.Map(1));
|
|
mem = reinterpret_cast<char*>(shm.memory());
|
|
ASSERT_EQ(*mem, 'A');
|
|
|
|
// Try to alter protection; should fail
|
|
EXPECT_FALSE(ipc::SharedMemory::SystemProtectFallible(
|
|
mem, 1, ipc::SharedMemory::RightsReadWrite));
|
|
}
|
|
|
|
#ifndef XP_WIN
|
|
// 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.
|
|
TEST(IPCSharedMemory, Reprotect)
|
|
{
|
|
base::SharedMemory shm;
|
|
|
|
// Create and initialize
|
|
ASSERT_TRUE(shm.CreateFreezeable(1));
|
|
ASSERT_TRUE(shm.Map(1));
|
|
auto mem = reinterpret_cast<char*>(shm.memory());
|
|
ASSERT_TRUE(mem);
|
|
*mem = 'A';
|
|
|
|
// Re-create as read-only
|
|
auto handle = base::SharedMemory::NULLHandle();
|
|
ASSERT_TRUE(shm.GiveToProcess(base::GetCurrentProcId(), &handle));
|
|
ASSERT_TRUE(shm.IsHandleValid(handle));
|
|
ASSERT_FALSE(shm.IsValid());
|
|
ASSERT_TRUE(shm.SetHandle(handle, /* read-only */ true));
|
|
ASSERT_TRUE(shm.IsValid());
|
|
|
|
// Re-map
|
|
ASSERT_TRUE(shm.Map(1));
|
|
mem = reinterpret_cast<char*>(shm.memory());
|
|
ASSERT_EQ(*mem, 'A');
|
|
|
|
// Try to alter protection; should succeed, because not frozen
|
|
EXPECT_TRUE(ipc::SharedMemory::SystemProtectFallible(
|
|
mem, 1, ipc::SharedMemory::RightsReadWrite));
|
|
}
|
|
#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)
|
|
{
|
|
base::SharedMemory shm;
|
|
|
|
// Create and initialize
|
|
ASSERT_TRUE(shm.CreateFreezeable(1));
|
|
ASSERT_TRUE(shm.Map(1));
|
|
auto mem = reinterpret_cast<char*>(shm.memory());
|
|
ASSERT_TRUE(mem);
|
|
*mem = 'A';
|
|
|
|
// Freeze
|
|
ASSERT_TRUE(shm.Freeze());
|
|
ASSERT_FALSE(shm.memory());
|
|
|
|
// Extract handle.
|
|
auto handle = base::SharedMemory::NULLHandle();
|
|
ASSERT_TRUE(shm.GiveToProcess(base::GetCurrentProcId(), &handle));
|
|
ASSERT_TRUE(shm.IsHandleValid(handle));
|
|
ASSERT_FALSE(shm.IsValid());
|
|
|
|
// Unfreeze.
|
|
bool unfroze = ::DuplicateHandle(
|
|
GetCurrentProcess(), handle, GetCurrentProcess(), &handle,
|
|
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)
|
|
{
|
|
base::SharedMemory shmRW, shmRO;
|
|
|
|
// Create and initialize
|
|
ASSERT_TRUE(shmRW.CreateFreezeable(1));
|
|
ASSERT_TRUE(shmRW.Map(1));
|
|
auto memRW = reinterpret_cast<char*>(shmRW.memory());
|
|
ASSERT_TRUE(memRW);
|
|
|
|
// Create read-only copy
|
|
ASSERT_TRUE(shmRW.ReadOnlyCopy(&shmRO));
|
|
EXPECT_FALSE(shmRW.IsValid());
|
|
ASSERT_EQ(shmRW.memory(), memRW);
|
|
ASSERT_EQ(shmRO.max_size(), size_t(1));
|
|
|
|
// Map read-only
|
|
ASSERT_TRUE(shmRO.IsValid());
|
|
ASSERT_TRUE(shmRO.Map(1));
|
|
auto memRO = reinterpret_cast<const char*>(shmRO.memory());
|
|
ASSERT_TRUE(memRO);
|
|
ASSERT_NE(memRW, memRO);
|
|
|
|
// Check
|
|
*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)
|
|
{
|
|
base::SharedMemory shmRW, shmRO;
|
|
|
|
// Create and initialize
|
|
ASSERT_TRUE(shmRW.CreateFreezeable(1));
|
|
ASSERT_TRUE(shmRW.Map(1));
|
|
auto memRW = reinterpret_cast<char*>(shmRW.memory());
|
|
ASSERT_TRUE(memRW);
|
|
*memRW = 'A';
|
|
|
|
// Create read-only copy
|
|
ASSERT_TRUE(shmRW.ReadOnlyCopy(&shmRO));
|
|
EXPECT_FALSE(shmRW.IsValid());
|
|
ASSERT_EQ(shmRW.memory(), memRW);
|
|
ASSERT_EQ(shmRO.max_size(), size_t(1));
|
|
|
|
// Map read-only
|
|
ASSERT_TRUE(shmRO.IsValid());
|
|
ASSERT_TRUE(shmRO.Map(1));
|
|
auto memRO = reinterpret_cast<const char*>(shmRO.memory());
|
|
ASSERT_TRUE(memRO);
|
|
ASSERT_NE(memRW, memRO);
|
|
|
|
// Check
|
|
ASSERT_EQ(*memRW, 'A');
|
|
EXPECT_EQ(*memRO, 'A');
|
|
*memRW = 'X';
|
|
EXPECT_EQ(*memRO, 'X');
|
|
}
|
|
|
|
// See FreezeAndMapRW.
|
|
TEST(IPCSharedMemory, ROCopyAndMapRW)
|
|
{
|
|
base::SharedMemory shmRW, shmRO;
|
|
|
|
// Create and initialize
|
|
ASSERT_TRUE(shmRW.CreateFreezeable(1));
|
|
ASSERT_TRUE(shmRW.Map(1));
|
|
auto memRW = reinterpret_cast<char*>(shmRW.memory());
|
|
ASSERT_TRUE(memRW);
|
|
*memRW = 'A';
|
|
|
|
// Create read-only copy
|
|
ASSERT_TRUE(shmRW.ReadOnlyCopy(&shmRO));
|
|
ASSERT_TRUE(shmRO.IsValid());
|
|
|
|
// Re-create as writeable
|
|
auto handle = base::SharedMemory::NULLHandle();
|
|
ASSERT_TRUE(shmRO.GiveToProcess(base::GetCurrentProcId(), &handle));
|
|
ASSERT_TRUE(shmRO.IsHandleValid(handle));
|
|
ASSERT_FALSE(shmRO.IsValid());
|
|
ASSERT_TRUE(shmRO.SetHandle(handle, /* read-only */ false));
|
|
ASSERT_TRUE(shmRO.IsValid());
|
|
|
|
// This should fail
|
|
EXPECT_FALSE(shmRO.Map(1));
|
|
}
|
|
|
|
// See FreezeAndReprotect
|
|
TEST(IPCSharedMemory, ROCopyAndReprotect)
|
|
{
|
|
base::SharedMemory shmRW, shmRO;
|
|
|
|
// Create and initialize
|
|
ASSERT_TRUE(shmRW.CreateFreezeable(1));
|
|
ASSERT_TRUE(shmRW.Map(1));
|
|
auto memRW = reinterpret_cast<char*>(shmRW.memory());
|
|
ASSERT_TRUE(memRW);
|
|
*memRW = 'A';
|
|
|
|
// Create read-only copy
|
|
ASSERT_TRUE(shmRW.ReadOnlyCopy(&shmRO));
|
|
ASSERT_TRUE(shmRO.IsValid());
|
|
|
|
// Re-map
|
|
ASSERT_TRUE(shmRO.Map(1));
|
|
auto memRO = reinterpret_cast<char*>(shmRO.memory());
|
|
ASSERT_EQ(*memRO, 'A');
|
|
|
|
// Try to alter protection; should fail
|
|
EXPECT_FALSE(ipc::SharedMemory::SystemProtectFallible(
|
|
memRO, 1, ipc::SharedMemory::RightsReadWrite));
|
|
}
|
|
|
|
TEST(IPCSharedMemory, IsZero)
|
|
{
|
|
base::SharedMemory shm;
|
|
|
|
static constexpr size_t kSize = 65536;
|
|
ASSERT_TRUE(shm.Create(kSize));
|
|
ASSERT_TRUE(shm.Map(kSize));
|
|
|
|
auto* mem = reinterpret_cast<char*>(shm.memory());
|
|
for (size_t i = 0; i < kSize; ++i) {
|
|
ASSERT_EQ(mem[i], 0) << "offset " << i;
|
|
}
|
|
}
|
|
|
|
#ifndef FUZZING
|
|
TEST(IPCSharedMemory, BasicIsZero)
|
|
{
|
|
auto shm = MakeRefPtr<ipc::SharedMemoryBasic>();
|
|
|
|
static constexpr size_t kSize = 65536;
|
|
ASSERT_TRUE(shm->Create(kSize));
|
|
ASSERT_TRUE(shm->Map(kSize));
|
|
|
|
auto* mem = reinterpret_cast<char*>(shm->memory());
|
|
for (size_t i = 0; i < kSize; ++i) {
|
|
ASSERT_EQ(mem[i], 0) << "offset " << i;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#if defined(XP_LINUX) && !defined(ANDROID)
|
|
// 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(IPCSharedMemory, IsMemfd)
|
|
{
|
|
static constexpr int kMajor = 3;
|
|
static constexpr int kMinor = 17;
|
|
|
|
struct utsname uts;
|
|
ASSERT_EQ(uname(&uts), 0) << strerror(errno);
|
|
ASSERT_STREQ(uts.sysname, "Linux");
|
|
int major, minor;
|
|
ASSERT_EQ(sscanf(uts.release, "%d.%d", &major, &minor), 2);
|
|
bool expectMemfd = major > kMajor || (major == kMajor && minor >= kMinor);
|
|
|
|
base::SharedMemory shm;
|
|
ASSERT_TRUE(shm.Create(1));
|
|
UniqueFileHandle fd = shm.TakeHandle();
|
|
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 (expectMemfd) {
|
|
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);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
} // namespace mozilla
|