[ios] New class ScopedVMMap

This CL introduces a new class ScopedVMMap, a fork of ScopedVMRead
which maps the memory using vm_remap() instead of reading it.

This is useful for Annotations which use ScopedSpinGuard to
protect reads from simultaneous writes; the in-process intermediate
dump handler can try to take the spin guard when reading such
an Annotation and skip reading it if it the spin guard could not
be obtained.

Change-Id: I60d7a48d1ba4e5d2dfdb44307b78b4d9ffb73560
Bug: crashpad:437
Reviewed-on: https://chromium-review.googlesource.com/c/crashpad/crashpad/+/4114550
Reviewed-by: Mark Mentovai <mark@chromium.org>
Commit-Queue: Ben Hamilton <benhamilton@google.com>
This commit is contained in:
Ben Hamilton 2023-01-30 15:02:31 -07:00 committed by Crashpad LUCI CQ
parent 8071d3019e
commit 28354d11c3
7 changed files with 366 additions and 45 deletions

View File

@ -403,6 +403,8 @@ crashpad_static_library("util") {
"ios/scoped_background_task.mm",
"ios/scoped_vm_read.cc",
"ios/scoped_vm_read.h",
"ios/scoped_vm_map.cc",
"ios/scoped_vm_map.h",
]
}
@ -837,6 +839,7 @@ source_set("util_test") {
"ios/ios_intermediate_dump_reader_test.cc",
"ios/ios_intermediate_dump_writer_test.cc",
"ios/scoped_vm_read_test.cc",
"ios/scoped_vm_map_test.cc",
]
sources -= [

86
util/ios/scoped_vm_map.cc Normal file
View File

@ -0,0 +1,86 @@
// Copyright 2023 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/ios/scoped_vm_map.h"
#include "util/ios/raw_logging.h"
namespace crashpad {
namespace internal {
ScopedVMMapInternal::ScopedVMMapInternal()
: data_(0),
region_start_(0),
region_size_(0),
cur_protection_(VM_PROT_NONE) {}
ScopedVMMapInternal::~ScopedVMMapInternal() {
Reset();
}
bool ScopedVMMapInternal::Map(const void* data, const size_t data_length) {
Reset();
vm_address_t data_address = reinterpret_cast<vm_address_t>(data);
vm_address_t page_region_address = trunc_page(data_address);
region_size_ = round_page(data_address - page_region_address + data_length);
if (region_size_ < data_length) {
CRASHPAD_RAW_LOG("ScopedVMMap data_length overflow");
return false;
}
// Since region_start_ is 0, vm_remap() will choose an address to which the
// memory will be mapped and store the mapped address in region_start_ on
// success.
vm_prot_t max_protection;
kern_return_t kr = vm_remap(mach_task_self(),
&region_start_,
region_size_,
0,
TRUE,
mach_task_self(),
page_region_address,
FALSE,
&cur_protection_,
&max_protection,
VM_INHERIT_DEFAULT);
if (kr != KERN_SUCCESS) {
// It's expected that this will sometimes fail. Don't log here.
return false;
}
data_ = region_start_ + (data_address - page_region_address);
return true;
}
void ScopedVMMapInternal::Reset() {
if (!region_start_) {
return;
}
kern_return_t kr =
vm_deallocate(mach_task_self(), region_start_, region_size_);
if (kr != KERN_SUCCESS) {
CRASHPAD_RAW_LOG_ERROR(kr, "vm_deallocate");
}
data_ = 0;
region_start_ = 0;
region_size_ = 0;
cur_protection_ = VM_PROT_NONE;
}
} // namespace internal
} // namespace crashpad

125
util/ios/scoped_vm_map.h Normal file
View File

@ -0,0 +1,125 @@
// Copyright 2023 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.
#ifndef CRASHPAD_UTIL_IOS_SCOPED_VM_MAP_H_
#define CRASHPAD_UTIL_IOS_SCOPED_VM_MAP_H_
#include <mach/mach.h>
namespace crashpad {
namespace internal {
//! \brief Non-templated internal class to be used by ScopedVMMap.
//!
//! Note: RUNS-DURING-CRASH.
class ScopedVMMapInternal {
public:
ScopedVMMapInternal();
ScopedVMMapInternal(const ScopedVMMapInternal&) = delete;
ScopedVMMapInternal& operator=(const ScopedVMMapInternal&) = delete;
~ScopedVMMapInternal();
//! \brief Releases any previously mapped data and vm_remaps \a data. Logs an
//! error on failure.
//!
//! \param[in] data Memory to be mapped by vm_remap.
//! \param[in] data_length Length of \a data.
//!
//! \return `true` if all the data was mapped. Logs an error and returns false
//! on failure.
bool Map(const void* data, size_t data_length);
//! \brief Returns the current protection for the memory in the region.
vm_prot_t CurrentProtection() const { return cur_protection_; }
vm_address_t data() const { return data_; }
private:
//! \brief Deallocates any resources allocated by this object and resets it
//! to its original state.
void Reset();
// The address within region_start_ at which the mapped data is available.
vm_address_t data_;
// The region returned by vm_remap().
vm_address_t region_start_;
// The size of the region returned by vm_remap().
vm_size_t region_size_;
// The current protection for the memory region.
vm_prot_t cur_protection_;
};
//! \brief A scoped wrapper for calls to `vm_remap` and `vm_deallocate`. Allows
//! in-process handler to safely read and write memory (modulo its
//! protection level) for the intermediate dump.
//!
//! Note: RUNS-DURING-CRASH.
template <typename T>
class ScopedVMMap {
public:
ScopedVMMap() : internal_() {}
ScopedVMMap(const ScopedVMMap&) = delete;
ScopedVMMap& operator=(const ScopedVMMap&) = delete;
~ScopedVMMap() {}
//! \brief Releases any previously mapped data and vm_remaps data.
//!
//! \param[in] data Memory to be mapped by vm_remap.
//! \param[in] count Length of \a data.
//!
//! \return `true` if all \a data was mapped. Returns false on failure.
bool Map(const void* data, size_t count = 1) {
size_t data_length = count * sizeof(T);
return internal_.Map(data, data_length);
}
//! \brief Releases any previously mapped data and vm_remaps address.
//!
//! Before reading or writing the memory, check `CurrentProtection()` to
//! ensure the data is readable or writable.
//!
//! \param[in] address Address of memory to be mapped by vm_remap.
//! \param[in] count Length of \a data.
//!
//! \return `true` if all of \a address was mapped. Returns false on failure.
bool Map(vm_address_t address, size_t count = 1) {
return Map(reinterpret_cast<T*>(address), count);
}
//! \brief Returns the pointer to memory safe to read and write (respecting
//! the CurrentProtection() level) during the in-process crash handler.
T* operator->() const { return get(); }
//! \brief Returns the pointer to memory safe to read and write (respecting
//! the CurrentProtection() level) during the in-process crash handler.
T* get() const { return reinterpret_cast<T*>(internal_.data()); }
//! \brief Returns the current protection level of the mapped memory.
vm_prot_t CurrentProtection() const { return internal_.CurrentProtection(); }
private:
ScopedVMMapInternal internal_;
};
} // namespace internal
} // namespace crashpad
#endif // CRASHPAD_UTIL_IOS_SCOPED_VM_MAP_H_

View File

@ -0,0 +1,94 @@
// Copyright 2023 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/ios/scoped_vm_map.h"
#include <sys/time.h>
#include "base/mac/scoped_mach_vm.h"
#include "gtest/gtest.h"
#include "test/mac/mach_errors.h"
namespace crashpad {
namespace test {
namespace {
TEST(ScopedVMMapTest, BasicFunctionality) {
// bad data or count.
internal::ScopedVMMap<vm_address_t> vmmap_bad;
EXPECT_FALSE(vmmap_bad.Map(nullptr, 100));
EXPECT_FALSE(vmmap_bad.Map(reinterpret_cast<void*>(0x1000), 100));
vm_address_t invalid_address = 1;
EXPECT_FALSE(vmmap_bad.Map(&invalid_address, 1000000000));
EXPECT_FALSE(vmmap_bad.Map(&invalid_address, -1));
vm_address_t valid_address = reinterpret_cast<vm_address_t>(this);
EXPECT_FALSE(vmmap_bad.Map(&valid_address, 1000000000));
EXPECT_FALSE(vmmap_bad.Map(&valid_address, -1));
// array
static constexpr char map_me[] = "map me";
internal::ScopedVMMap<char> vmmap_string;
ASSERT_TRUE(vmmap_string.Map(map_me, strlen(map_me)));
EXPECT_STREQ(vmmap_string.get(), map_me);
EXPECT_TRUE(vmmap_string.CurrentProtection() & VM_PROT_READ);
// struct
timeval time_of_day;
EXPECT_TRUE(gettimeofday(&time_of_day, nullptr) == 0);
internal::ScopedVMMap<timeval> vmmap_time;
ASSERT_TRUE(vmmap_time.Map(&time_of_day));
constexpr vm_prot_t kReadWrite = VM_PROT_READ | VM_PROT_WRITE;
EXPECT_EQ(vmmap_time.CurrentProtection() & kReadWrite, kReadWrite);
EXPECT_EQ(vmmap_time->tv_sec, time_of_day.tv_sec);
EXPECT_EQ(vmmap_time->tv_usec, time_of_day.tv_usec);
// reset.
timeval time_of_day2;
EXPECT_TRUE(gettimeofday(&time_of_day2, nullptr) == 0);
ASSERT_TRUE(vmmap_time.Map(&time_of_day2));
EXPECT_EQ(vmmap_time.CurrentProtection() & kReadWrite, kReadWrite);
EXPECT_EQ(vmmap_time->tv_sec, time_of_day2.tv_sec);
EXPECT_EQ(vmmap_time->tv_usec, time_of_day2.tv_usec);
}
TEST(ScopedVMMapTest, MissingMiddleVM) {
char* region;
vm_size_t page_size = getpagesize();
vm_size_t region_size = page_size * 3;
kern_return_t kr = vm_allocate(mach_task_self(),
reinterpret_cast<vm_address_t*>(&region),
region_size,
VM_FLAGS_ANYWHERE);
ASSERT_EQ(kr, KERN_SUCCESS) << MachErrorMessage(kr, "vm_allocate");
base::mac::ScopedMachVM vm_owner(reinterpret_cast<vm_address_t>(region),
region_size);
internal::ScopedVMMap<char> vmmap_missing_middle;
ASSERT_TRUE(vmmap_missing_middle.Map(region, region_size));
// Dealloc middle page.
kr = vm_deallocate(mach_task_self(),
reinterpret_cast<vm_address_t>(region + page_size),
page_size);
ASSERT_EQ(kr, KERN_SUCCESS) << MachErrorMessage(kr, "vm_deallocate");
EXPECT_FALSE(vmmap_missing_middle.Map(region, region_size));
ASSERT_TRUE(vmmap_missing_middle.Map(region, page_size));
}
} // namespace
} // namespace test
} // namespace crashpad

View File

@ -20,25 +20,15 @@ namespace crashpad {
namespace internal {
ScopedVMReadInternal::ScopedVMReadInternal()
: data_(0), vm_read_data_(0), vm_read_data_count_(0) {}
: data_(0), region_start_(0), region_size_(0) {}
ScopedVMReadInternal::~ScopedVMReadInternal() {
if (data_) {
kern_return_t kr =
vm_deallocate(mach_task_self(), vm_read_data_, vm_read_data_count_);
if (kr != KERN_SUCCESS)
CRASHPAD_RAW_LOG_ERROR(kr, "vm_deallocate");
}
Reset();
}
bool ScopedVMReadInternal::Read(const void* data, const size_t data_length) {
if (data_) {
kern_return_t kr =
vm_deallocate(mach_task_self(), vm_read_data_, vm_read_data_count_);
if (kr != KERN_SUCCESS)
CRASHPAD_RAW_LOG_ERROR(kr, "vm_deallocate");
data_ = 0;
}
Reset();
vm_address_t data_address = reinterpret_cast<vm_address_t>(data);
vm_address_t page_region_address = trunc_page(data_address);
vm_size_t page_region_size =
@ -50,16 +40,30 @@ bool ScopedVMReadInternal::Read(const void* data, const size_t data_length) {
kern_return_t kr = vm_read(mach_task_self(),
page_region_address,
page_region_size,
&vm_read_data_,
&vm_read_data_count_);
&region_start_,
&region_size_);
if (kr == KERN_SUCCESS) {
data_ = vm_read_data_ + (data_address - page_region_address);
return true;
} else {
if (kr != KERN_SUCCESS) {
// It's expected that this will sometimes fail. Don't log here.
return false;
}
data_ = region_start_ + (data_address - page_region_address);
return true;
}
void ScopedVMReadInternal::Reset() {
if (!region_start_) {
return;
}
kern_return_t kr =
vm_deallocate(mach_task_self(), region_start_, region_size_);
if (kr != KERN_SUCCESS) {
CRASHPAD_RAW_LOG_ERROR(kr, "vm_deallocate");
}
region_start_ = 0;
region_size_ = 0;
data_ = 0;
}
} // namespace internal

View File

@ -46,14 +46,18 @@ class ScopedVMReadInternal {
vm_address_t data() const { return data_; }
private:
// The address of the requested data.
//! \brief Deallocates any resources allocated by this object and resets it
//! to its original state.
void Reset();
// The address within region_start_ at which the the data is available.
vm_address_t data_;
// The rounded down page boundary of the requested data.
vm_address_t vm_read_data_;
// The region returned by vm_read().
vm_address_t region_start_;
// The size of the pages that were actually read.
mach_msg_type_number_t vm_read_data_count_;
// The size of the region returned by vm_read().
mach_msg_type_number_t region_size_;
};
//! \brief A scoped wrapper for calls to `vm_read` and `vm_deallocate`. Allows

View File

@ -18,6 +18,7 @@
#include "base/mac/scoped_mach_vm.h"
#include "gtest/gtest.h"
#include "test/mac/mach_errors.h"
namespace crashpad {
namespace test {
@ -26,43 +27,47 @@ namespace {
TEST(ScopedVMReadTest, BasicFunctionality) {
// bad data or count.
internal::ScopedVMRead<vm_address_t> vmread_bad;
ASSERT_FALSE(vmread_bad.Read(nullptr, 100));
ASSERT_FALSE(vmread_bad.Read(reinterpret_cast<void*>(0x1000), 100));
vm_address_t address = 1;
ASSERT_FALSE(vmread_bad.Read(&address, 1000000000));
ASSERT_FALSE(vmread_bad.Read(&address, -1));
EXPECT_FALSE(vmread_bad.Read(nullptr, 100));
EXPECT_FALSE(vmread_bad.Read(reinterpret_cast<void*>(0x1000), 100));
vm_address_t invalid_address = 1;
EXPECT_FALSE(vmread_bad.Read(&invalid_address, 1000000000));
EXPECT_FALSE(vmread_bad.Read(&invalid_address, -1));
vm_address_t valid_address = reinterpret_cast<vm_address_t>(this);
EXPECT_FALSE(vmread_bad.Read(&valid_address, 1000000000));
EXPECT_FALSE(vmread_bad.Read(&valid_address, -1));
// array
constexpr char read_me[] = "read me";
internal::ScopedVMRead<char> vmread_string;
ASSERT_TRUE(vmread_string.Read(read_me, strlen(read_me)));
EXPECT_STREQ(read_me, vmread_string.get());
EXPECT_STREQ(vmread_string.get(), read_me);
// struct
timeval time_of_day;
EXPECT_TRUE(gettimeofday(&time_of_day, nullptr) == 0);
internal::ScopedVMRead<timeval> vmread_time;
ASSERT_TRUE(vmread_time.Read(&time_of_day));
EXPECT_EQ(time_of_day.tv_sec, vmread_time->tv_sec);
EXPECT_EQ(time_of_day.tv_usec, vmread_time->tv_usec);
EXPECT_EQ(vmread_time->tv_sec, time_of_day.tv_sec);
EXPECT_EQ(vmread_time->tv_usec, time_of_day.tv_usec);
// reset.
timeval time_of_day2;
EXPECT_TRUE(gettimeofday(&time_of_day2, nullptr) == 0);
ASSERT_TRUE(vmread_time.Read(&time_of_day2));
EXPECT_EQ(time_of_day2.tv_sec, vmread_time->tv_sec);
EXPECT_EQ(time_of_day2.tv_usec, vmread_time->tv_usec);
EXPECT_EQ(vmread_time->tv_sec, time_of_day2.tv_sec);
EXPECT_EQ(vmread_time->tv_usec, time_of_day2.tv_usec);
}
TEST(ScopedVMReadTest, MissingMiddleVM) {
char* region;
vm_size_t page_size = getpagesize();
vm_size_t region_size = page_size * 3;
ASSERT_EQ(vm_allocate(mach_task_self(),
reinterpret_cast<vm_address_t*>(&region),
region_size,
VM_FLAGS_ANYWHERE),
0);
kern_return_t kr = vm_allocate(mach_task_self(),
reinterpret_cast<vm_address_t*>(&region),
region_size,
VM_FLAGS_ANYWHERE);
ASSERT_EQ(kr, KERN_SUCCESS) << MachErrorMessage(kr, "vm_allocate");
base::mac::ScopedMachVM vm_owner(reinterpret_cast<vm_address_t>(region),
region_size);
@ -70,12 +75,12 @@ TEST(ScopedVMReadTest, MissingMiddleVM) {
ASSERT_TRUE(vmread_missing_middle.Read(region, region_size));
// Dealloc middle page.
ASSERT_EQ(vm_deallocate(mach_task_self(),
reinterpret_cast<vm_address_t>(region + page_size),
page_size),
0);
kr = vm_deallocate(mach_task_self(),
reinterpret_cast<vm_address_t>(region + page_size),
page_size);
ASSERT_EQ(kr, KERN_SUCCESS) << MachErrorMessage(kr, "vm_deallocate");
ASSERT_FALSE(vmread_missing_middle.Read(region, region_size));
EXPECT_FALSE(vmread_missing_middle.Read(region, region_size));
ASSERT_TRUE(vmread_missing_middle.Read(region, page_size));
}