// Copyright 2014 The Crashpad Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "util/process/process_memory_mac.h"
#include <mach/mach.h>
#include <string.h>
#include <algorithm>
#include <memory>
#include <string>
#include "base/apple/scoped_mach_port.h"
#include "base/apple/scoped_mach_vm.h"
#include "base/containers/heap_array.h"
#include "gtest/gtest.h"
#include "test/mac/mach_errors.h"
#include "util/misc/from_pointer_cast.h"
namespace crashpad {
namespace test {
namespace {
TEST(ProcessMemoryMac, ReadMappedSelf) {
vm_address_t address = 0;
const vm_size_t kSize = 4 * PAGE_SIZE;
kern_return_t kr =
vm_allocate(mach_task_self(), &address, kSize, VM_FLAGS_ANYWHERE);
ASSERT_EQ(kr, KERN_SUCCESS) << MachErrorMessage(kr, "vm_allocate");
base::apple::ScopedMachVM vm_owner(address, mach_vm_round_page(kSize));
char* region = reinterpret_cast<char*>(address);
for (size_t index = 0; index < kSize; ++index) {
region[index] = (index % 256) ^ ((index >> 8) % 256);
}
ProcessMemoryMac memory;
ASSERT_TRUE(memory.Initialize(mach_task_self()));
std::string result(kSize, '\0');
std::unique_ptr<ProcessMemoryMac::MappedMemory> mapped;
// Ensure that the entire region can be read.
ASSERT_TRUE((mapped = memory.ReadMapped(address, kSize)));
EXPECT_EQ(memcmp(region, mapped->data(), kSize), 0);
// Ensure that a read of length 0 succeeds and doesn't touch the result.
result.assign(kSize, '\0');
std::string zeroes = result;
ASSERT_TRUE((mapped = memory.ReadMapped(address, 0)));
EXPECT_EQ(result, zeroes);
// Ensure that a read starting at an unaligned address works.
ASSERT_TRUE((mapped = memory.ReadMapped(address + 1, kSize - 1)));
EXPECT_EQ(memcmp(region + 1, mapped->data(), kSize - 1), 0);
// Ensure that a read ending at an unaligned address works.
ASSERT_TRUE((mapped = memory.ReadMapped(address, kSize - 1)));
EXPECT_EQ(memcmp(region, mapped->data(), kSize - 1), 0);
// Ensure that a read starting and ending at unaligned addresses works.
ASSERT_TRUE((mapped = memory.ReadMapped(address + 1, kSize - 2)));
EXPECT_EQ(memcmp(region + 1, mapped->data(), kSize - 2), 0);
// Ensure that a read of exactly one page works.
ASSERT_TRUE((mapped = memory.ReadMapped(address + PAGE_SIZE, PAGE_SIZE)));
EXPECT_EQ(memcmp(region + PAGE_SIZE, mapped->data(), PAGE_SIZE), 0);
// Ensure that a read of a single byte works.
ASSERT_TRUE((mapped = memory.ReadMapped(address + 2, 1)));
EXPECT_EQ(reinterpret_cast<const char*>(mapped->data())[0], region[2]);
// Ensure that a read of length zero works and doesn't touch the data.
result[0] = 'M';
ASSERT_TRUE((mapped = memory.ReadMapped(address + 3, 0)));
EXPECT_EQ(result[0], 'M');
}
TEST(ProcessMemoryMac, ReadSelfUnmapped) {
vm_address_t address = 0;
const vm_size_t kSize = 2 * PAGE_SIZE;
kern_return_t kr =
vm_allocate(mach_task_self(), &address, kSize, VM_FLAGS_ANYWHERE);
ASSERT_EQ(kr, KERN_SUCCESS) << MachErrorMessage(kr, "vm_allocate");
base::apple::ScopedMachVM vm_owner(address, mach_vm_round_page(kSize));
char* region = reinterpret_cast<char*>(address);
for (size_t index = 0; index < kSize; ++index) {
// Don't include any NUL bytes, because ReadCString stops when it encounters
// a NUL.
region[index] = (index % 255) + 1;
}
kr = vm_protect(
mach_task_self(), address + PAGE_SIZE, PAGE_SIZE, FALSE, VM_PROT_NONE);
ASSERT_EQ(kr, KERN_SUCCESS) << MachErrorMessage(kr, "vm_protect");
ProcessMemoryMac memory;
ASSERT_TRUE(memory.Initialize(mach_task_self()));
std::string result(kSize, '\0');
EXPECT_FALSE(memory.Read(address, kSize, &result[0]));
EXPECT_FALSE(memory.Read(address + 1, kSize - 1, &result[0]));
EXPECT_FALSE(memory.Read(address + PAGE_SIZE, 1, &result[0]));
EXPECT_FALSE(memory.Read(address + PAGE_SIZE - 1, 2, &result[0]));
EXPECT_TRUE(memory.Read(address, PAGE_SIZE, &result[0]));
EXPECT_TRUE(memory.Read(address + PAGE_SIZE - 1, 1, &result[0]));
// Do the same thing with the ReadMapped() interface.
std::unique_ptr<ProcessMemoryMac::MappedMemory> mapped;
EXPECT_FALSE((mapped = memory.ReadMapped(address, kSize)));
EXPECT_FALSE((mapped = memory.ReadMapped(address + 1, kSize - 1)));
EXPECT_FALSE((mapped = memory.ReadMapped(address + PAGE_SIZE, 1)));
EXPECT_FALSE((mapped = memory.ReadMapped(address + PAGE_SIZE - 1, 2)));
EXPECT_TRUE((mapped = memory.ReadMapped(address, PAGE_SIZE)));
EXPECT_TRUE((mapped = memory.ReadMapped(address + PAGE_SIZE - 1, 1)));
// Repeat the test with an unmapped page instead of an unreadable one. This
// portion of the test may be flaky in the presence of other threads, if
// another thread maps something in the region that is deallocated here.
kr = vm_deallocate(mach_task_self(), address + PAGE_SIZE, PAGE_SIZE);
ASSERT_EQ(kr, KERN_SUCCESS) << MachErrorMessage(kr, "vm_deallocate");
vm_owner.reset(address, PAGE_SIZE);
EXPECT_FALSE(memory.Read(address, kSize, &result[0]));
EXPECT_FALSE(memory.Read(address + 1, kSize - 1, &result[0]));
EXPECT_FALSE(memory.Read(address + PAGE_SIZE, 1, &result[0]));
EXPECT_FALSE(memory.Read(address + PAGE_SIZE - 1, 2, &result[0]));
EXPECT_TRUE(memory.Read(address, PAGE_SIZE, &result[0]));
EXPECT_TRUE(memory.Read(address + PAGE_SIZE - 1, 1, &result[0]));
// Do the same thing with the ReadMapped() interface.
EXPECT_FALSE((mapped = memory.ReadMapped(address, kSize)));
EXPECT_FALSE((mapped = memory.ReadMapped(address + 1, kSize - 1)));
EXPECT_FALSE((mapped = memory.ReadMapped(address + PAGE_SIZE, 1)));
EXPECT_FALSE((mapped = memory.ReadMapped(address + PAGE_SIZE - 1, 2)));
EXPECT_TRUE((mapped = memory.ReadMapped(address, PAGE_SIZE)));
EXPECT_TRUE((mapped = memory.ReadMapped(address + PAGE_SIZE - 1, 1)));
}
TEST(ProcessMemoryMac, ReadCStringSelfUnmapped) {
vm_address_t address = 0;
const vm_size_t kSize = 2 * PAGE_SIZE;
kern_return_t kr =
vm_allocate(mach_task_self(), &address, kSize, VM_FLAGS_ANYWHERE);
ASSERT_EQ(kr, KERN_SUCCESS) << MachErrorMessage(kr, "vm_allocate");
base::apple::ScopedMachVM vm_owner(address, mach_vm_round_page(kSize));
char* region = reinterpret_cast<char*>(address);
for (size_t index = 0; index < kSize; ++index) {
// Don't include any NUL bytes, because ReadCString stops when it encounters
// a NUL.
region[index] = (index % 255) + 1;
}
kr = vm_protect(
mach_task_self(), address + PAGE_SIZE, PAGE_SIZE, FALSE, VM_PROT_NONE);
ASSERT_EQ(kr, KERN_SUCCESS) << MachErrorMessage(kr, "vm_protect");
ProcessMemoryMac memory;
ASSERT_TRUE(memory.Initialize(mach_task_self()));
std::string result;
EXPECT_FALSE(memory.ReadCString(address, &result));
// Make sure that if the string is NUL-terminated within the mapped memory
// region, it can be read properly.
char terminator_or_not = '\0';
std::swap(region[PAGE_SIZE - 1], terminator_or_not);
ASSERT_TRUE(memory.ReadCString(address, &result));
EXPECT_FALSE(result.empty());
EXPECT_EQ(result.size(), PAGE_SIZE - 1u);
EXPECT_EQ(result, region);
// Repeat the test with an unmapped page instead of an unreadable one. This
// portion of the test may be flaky in the presence of other threads, if
// another thread maps something in the region that is deallocated here.
std::swap(region[PAGE_SIZE - 1], terminator_or_not);
kr = vm_deallocate(mach_task_self(), address + PAGE_SIZE, PAGE_SIZE);
ASSERT_EQ(kr, KERN_SUCCESS) << MachErrorMessage(kr, "vm_deallocate");
vm_owner.reset(address, PAGE_SIZE);
EXPECT_FALSE(memory.ReadCString(address, &result));
// Clear the result before testing that the string can be read. This makes
// sure that the result is actually filled in, because it already contains the
// expected value from the tests above.
result.clear();
std::swap(region[PAGE_SIZE - 1], terminator_or_not);
ASSERT_TRUE(memory.ReadCString(address, &result));
EXPECT_FALSE(result.empty());
EXPECT_EQ(result.size(), PAGE_SIZE - 1u);
EXPECT_EQ(result, region);
}
bool IsAddressMapped(vm_address_t address) {
vm_address_t region_address = address;
vm_size_t region_size;
mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64;
vm_region_basic_info_64 info;
mach_port_t object;
kern_return_t kr = vm_region_64(mach_task_self(),
®ion_address,
®ion_size,
VM_REGION_BASIC_INFO_64,
reinterpret_cast<vm_region_info_t>(&info),
&count,
&object);
if (kr == KERN_SUCCESS) {
// |object| will be MACH_PORT_NULL (10.9.4 xnu-2422.110.17/osfmk/vm/vm_map.c
// vm_map_region()), but the interface acts as if it might carry a send
// right, so treat it as documented.
base::apple::ScopedMachSendRight object_owner(object);
return address >= region_address && address <= region_address + region_size;
}
if (kr == KERN_INVALID_ADDRESS) {
return false;
}
ADD_FAILURE() << MachErrorMessage(kr, "vm_region_64");
return false;
}
TEST(ProcessMemoryMac, MappedMemoryDeallocates) {
// This tests that once a ProcessMemoryMac::MappedMemory object is destroyed,
// it releases the mapped memory that it owned. Technically, this test is not
// valid because after the mapping is released, something else (on another
// thread) might wind up mapped in the same address. In the test environment,
// hopefully there are either no other threads or they're all quiescent, so
// nothing else should wind up mapped in the address.
ProcessMemoryMac memory;
ASSERT_TRUE(memory.Initialize(mach_task_self()));
std::unique_ptr<ProcessMemoryMac::MappedMemory> mapped;
static constexpr char kTestBuffer[] = "hello!";
mach_vm_address_t test_address =
FromPointerCast<mach_vm_address_t>(&kTestBuffer);
ASSERT_TRUE((mapped = memory.ReadMapped(test_address, sizeof(kTestBuffer))));
EXPECT_EQ(memcmp(kTestBuffer, mapped->data(), sizeof(kTestBuffer)), 0);
vm_address_t mapped_address = reinterpret_cast<vm_address_t>(mapped->data());
EXPECT_TRUE(IsAddressMapped(mapped_address));
mapped.reset();
EXPECT_FALSE(IsAddressMapped(mapped_address));
// This is the same but with a big buffer that’s definitely larger than a
// single page. This makes sure that the whole mapped region winds up being
// deallocated.
auto big_buffer = base::HeapArray<char>::Uninit(4 * PAGE_SIZE);
test_address = FromPointerCast<mach_vm_address_t>(&big_buffer[0]);
ASSERT_TRUE((mapped = memory.ReadMapped(test_address, big_buffer.size())));
mapped_address = reinterpret_cast<vm_address_t>(mapped->data());
vm_address_t mapped_last_address = mapped_address + big_buffer.size() - 1;
EXPECT_TRUE(IsAddressMapped(mapped_address));
EXPECT_TRUE(IsAddressMapped(mapped_address + PAGE_SIZE));
EXPECT_TRUE(IsAddressMapped(mapped_last_address));
mapped.reset();
EXPECT_FALSE(IsAddressMapped(mapped_address));
EXPECT_FALSE(IsAddressMapped(mapped_address + PAGE_SIZE));
EXPECT_FALSE(IsAddressMapped(mapped_last_address));
}
TEST(ProcessMemoryMac, MappedMemoryReadCString) {
// This tests the behavior of ProcessMemoryMac::MappedMemory::ReadCString().
ProcessMemoryMac memory;
ASSERT_TRUE(memory.Initialize(mach_task_self()));
std::unique_ptr<ProcessMemoryMac::MappedMemory> mapped;
static constexpr char kTestBuffer[] = "0\0" "2\0" "45\0" "789";
const mach_vm_address_t kTestAddress =
FromPointerCast<mach_vm_address_t>(&kTestBuffer);
ASSERT_TRUE((mapped = memory.ReadMapped(kTestAddress, 10)));
std::string string;
ASSERT_TRUE(mapped->ReadCString(0, &string));
EXPECT_EQ(string, "0");
ASSERT_TRUE(mapped->ReadCString(1, &string));
EXPECT_EQ(string, "");
ASSERT_TRUE(mapped->ReadCString(2, &string));
EXPECT_EQ(string, "2");
ASSERT_TRUE(mapped->ReadCString(3, &string));
EXPECT_EQ(string, "");
ASSERT_TRUE(mapped->ReadCString(4, &string));
EXPECT_EQ(string, "45");
ASSERT_TRUE(mapped->ReadCString(5, &string));
EXPECT_EQ(string, "5");
ASSERT_TRUE(mapped->ReadCString(6, &string));
EXPECT_EQ(string, "");
// kTestBuffer’s NUL terminator was not read, so these will see an
// unterminated string and fail.
EXPECT_FALSE(mapped->ReadCString(7, &string));
EXPECT_FALSE(mapped->ReadCString(8, &string));
EXPECT_FALSE(mapped->ReadCString(9, &string));
// This is out of the range of what was read, so it will fail.
EXPECT_FALSE(mapped->ReadCString(10, &string));
EXPECT_FALSE(mapped->ReadCString(11, &string));
// Read it again, this time with a length long enough to include the NUL
// terminator.
ASSERT_TRUE((mapped = memory.ReadMapped(kTestAddress, 11)));
ASSERT_TRUE(mapped->ReadCString(6, &string));
EXPECT_EQ(string, "");
// These should now succeed.
ASSERT_TRUE(mapped->ReadCString(7, &string));
EXPECT_EQ(string, "789");
ASSERT_TRUE(mapped->ReadCString(8, &string));
EXPECT_EQ(string, "89");
ASSERT_TRUE(mapped->ReadCString(9, &string));
EXPECT_EQ(string, "9");
EXPECT_TRUE(mapped->ReadCString(10, &string));
EXPECT_EQ(string, "");
// These are still out of range.
EXPECT_FALSE(mapped->ReadCString(11, &string));
EXPECT_FALSE(mapped->ReadCString(12, &string));
}
} // namespace
} // namespace test
} // namespace crashpad