fune/ipc/gtest/TestSharedMemory.cpp
Jed Davis cc6e7ab133 Bug 1440203 - Support memfd_create in IPC shared memory. r=glandium
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
2020-10-22 21:23:32 +00:00

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