diff --git a/util/mac/mach_o_image_symbol_table_reader.cc b/util/mac/mach_o_image_symbol_table_reader.cc index 51e1261e..0b64fd46 100644 --- a/util/mac/mach_o_image_symbol_table_reader.cc +++ b/util/mac/mach_o_image_symbol_table_reader.cc @@ -104,6 +104,7 @@ class MachOImageSymbolTableReaderInitializer { return false; } + scoped_ptr string_table; for (size_t symbol_index = 0; symbol_index < symbol_count; ++symbol_index) { const process_types::nlist& symbol = symbols[symbol_index]; std::string symbol_info = base::StringPrintf(", symbol index %zu%s", @@ -121,11 +122,17 @@ class MachOImageSymbolTableReaderInitializer { return false; } + if (!string_table) { + string_table = process_reader_->Memory()->ReadMapped( + strtab_address, strtab_size); + if (!string_table) { + LOG(WARNING) << "could not read string table" << module_info_; + return false; + } + } + std::string name; - if (!process_reader_->Memory()->ReadCStringSizeLimited( - strtab_address + symbol.n_strx, - strtab_size - symbol.n_strx, - &name)) { + if (!string_table->ReadCString(symbol.n_strx, &name)) { LOG(WARNING) << "could not read string" << symbol_info; return false; } diff --git a/util/mach/task_memory.cc b/util/mach/task_memory.cc index 9597165f..d3d2ea6f 100644 --- a/util/mach/task_memory.cc +++ b/util/mach/task_memory.cc @@ -21,17 +21,66 @@ #include "base/logging.h" #include "base/mac/mach_logging.h" -#include "base/mac/scoped_mach_vm.h" #include "base/strings/stringprintf.h" +#include "util/stdlib/strnlen.h" namespace crashpad { +TaskMemory::MappedMemory::~MappedMemory() { +} + +bool TaskMemory::MappedMemory::ReadCString( + size_t offset, std::string* string) const { + if (offset >= user_size_) { + LOG(WARNING) << "offset out of range"; + return false; + } + + const char* string_base = reinterpret_cast(data_) + offset; + size_t max_length = user_size_ - offset; + size_t string_length = strnlen(string_base, max_length); + if (string_length == max_length) { + LOG(WARNING) << "unterminated string"; + return false; + } + + string->assign(string_base, string_length); + return true; +} + +TaskMemory::MappedMemory::MappedMemory(vm_address_t vm_address, + size_t vm_size, + size_t user_offset, + size_t user_size) + : vm_(vm_address, vm_size), + data_(reinterpret_cast(vm_address + user_offset)), + user_size_(user_size) { + vm_address_t vm_end = vm_address + vm_size; + vm_address_t user_address = reinterpret_cast(data_); + vm_address_t user_end = user_address + user_size; + DCHECK_GE(user_address, vm_address); + DCHECK_LE(user_address, vm_end); + DCHECK_GE(user_end, vm_address); + DCHECK_LE(user_end, vm_end); +} + TaskMemory::TaskMemory(mach_port_t task) : task_(task) { } bool TaskMemory::Read(mach_vm_address_t address, size_t size, void* buffer) { + scoped_ptr memory = ReadMapped(address, size); + if (!memory) { + return false; + } + + memcpy(buffer, memory->data(), size); + return true; +} + +scoped_ptr TaskMemory::ReadMapped( + mach_vm_address_t address, size_t size) { if (size == 0) { - return true; + return scoped_ptr(new MappedMemory(0, 0, 0, 0)); } mach_vm_address_t region_address = mach_vm_trunc_page(address); @@ -45,16 +94,12 @@ bool TaskMemory::Read(mach_vm_address_t address, size_t size, void* buffer) { if (kr != KERN_SUCCESS) { MACH_LOG(WARNING, kr) << base::StringPrintf( "mach_vm_read(0x%llx, 0x%llx)", region_address, region_size); - return false; + return scoped_ptr(); } DCHECK_EQ(region_count, region_size); - base::mac::ScopedMachVM region_owner(region, region_count); - - const char* region_base = reinterpret_cast(region); - memcpy(buffer, ®ion_base[address - region_address], size); - - return true; + return scoped_ptr( + new MappedMemory(region, region_size, address - region_address, size)); } bool TaskMemory::ReadCString(mach_vm_address_t address, std::string* string) { @@ -85,16 +130,17 @@ bool TaskMemory::ReadCStringInternal(mach_vm_address_t address, do { mach_vm_size_t read_length = std::min(size, PAGE_SIZE - (read_address % PAGE_SIZE)); - std::string read_string(read_length, '\0'); - if (!Read(read_address, read_length, &read_string[0])) { + scoped_ptr read_region = + ReadMapped(read_address, read_length); + if (!read_region) { return false; } - size_t terminator = read_string.find_first_of('\0'); - if (terminator == std::string::npos) { - local_string.append(read_string); - } else { - local_string.append(read_string, 0, terminator); + const char* read_region_data = + reinterpret_cast(read_region->data()); + size_t read_region_data_length = strnlen(read_region_data, read_length); + local_string.append(read_region_data, read_region_data_length); + if (read_region_data_length < read_length) { string->swap(local_string); return true; } diff --git a/util/mach/task_memory.h b/util/mach/task_memory.h index 6354862a..374f3304 100644 --- a/util/mach/task_memory.h +++ b/util/mach/task_memory.h @@ -20,19 +20,79 @@ #include #include "base/basictypes.h" +#include "base/mac/scoped_mach_vm.h" +#include "base/memory/scoped_ptr.h" namespace crashpad { //! \brief Accesses the memory of another Mach task. class TaskMemory { public: + //! \brief A memory region mapped from another Mach task. + //! + //! The mapping is maintained until this object is destroyed. + class MappedMemory { + public: + ~MappedMemory(); + + //! \brief Returns a pointer to the data requested by the user. + //! + //! This is the value of the \a vm_address + \a user_offset parameters + //! passed to the constructor, casted to `const void*`. + const void* data() const { return data_; } + + //! \brief Reads a `NUL`-terminated C string from the mapped region. + //! + //! This method will read contiguous memory until a `NUL` terminator is + //! found. + //! + //! \param[in] offset The offset into data() of the string to be read. + //! \param[out] string The string, whose contents begin at data() and + //! continue up to a `NUL` terminator. + //! + //! \return `true` on success, with \a string set appropriately. If \a + //! offset is greater than or equal to the \a user_size constructor + //! parameter, or if no `NUL` terminator was found in data() after \a + //! offset, returns `false` with an appropriate warning logged. + bool ReadCString(size_t offset, std::string* string) const; + + private: + //! \brief Creates an object that owns a memory region mapped from another + //! Mach task. + //! + //! \param[in] vm_address The address in this process’ address space where + //! the mapping begins. This must be page-aligned. + //! \param[in] vm_size The total size of the mapping that begins at \a + //! vm_address. This must be page-aligned. + //! \param[in] user_offset The offset into the mapped region where the data + //! requested by the user begins. This accounts for the fact that a + //! mapping must be page-aligned but the user data may not be. This + //! parameter must be equal to or less than \a vm_size. + //! \param[in] user_size The size of the data requested by the user. This + //! parameter can be used to compute the end address of user data, which + //! must be within the mapped region. + MappedMemory(vm_address_t vm_address, + size_t vm_size, + size_t user_offset, + size_t user_size); + + base::mac::ScopedMachVM vm_; + const void* data_; + size_t user_size_; + + // The outer class needs to be able to call this class’ private constructor. + friend class TaskMemory; + + DISALLOW_COPY_AND_ASSIGN(MappedMemory); + }; + //! \param[in] task A send right to the target task’s task port. This object //! does not take ownership of the send right. explicit TaskMemory(mach_port_t task); ~TaskMemory() {} - //! \brief Copies memory from the target task into a user-provided buffer in + //! \brief Copies memory from the target task into a caller-provided buffer in //! the current task. //! //! \param[in] address The address, in the target task’s address space, of the @@ -45,8 +105,25 @@ class TaskMemory { //! \return `true` on success, with \a buffer filled appropriately. `false` on //! failure, with a warning logged. Failures can occur, for example, when //! encountering unmapped or unreadable pages. + //! + //! \sa ReadMapped() bool Read(mach_vm_address_t address, size_t size, void* buffer); + //! \brief Maps memory from the target task into the current task. + //! + //! This interface is an alternative to Read() that does not require the + //! caller to provide a buffer to fill. This avoids copying memory, which can + //! offer a performance improvement. + //! + //! \param[in] address The address, in the target task’s address space, of the + //! memory region to map. + //! \param[in] size The size, in bytes, of the memory region to map. + //! + //! \return On success, a MappedMemory object that provides access to the data + //! requested. On faliure, `NULL`, with a warning logged. Failures can + //! occur, for example, when encountering unmapped or unreadable pages. + scoped_ptr ReadMapped(mach_vm_address_t address, size_t size); + //! \brief Reads a `NUL`-terminated C string from the target task into a //! string in the current task. //! @@ -60,6 +137,8 @@ class TaskMemory { //! \return `true` on success, with \a string set appropriately. `false` on //! failure, with a warning logged. Failures can occur, for example, when //! encountering unmapped or unreadable pages. + //! + //! \sa MappedMemory::ReadCString() bool ReadCString(mach_vm_address_t address, std::string* string); //! \brief Reads a `NUL`-terminated C string from the target task into a @@ -75,6 +154,8 @@ class TaskMemory { //! failure, with a warning logged. Failures can occur, for example, when //! a `NUL` terminator is not found within \a size bytes, or when //! encountering unmapped or unreadable pages. + //! + //! \sa MappedMemory::ReadCString() bool ReadCStringSizeLimited(mach_vm_address_t address, mach_vm_size_t size, std::string* string); diff --git a/util/mach/task_memory_test.cc b/util/mach/task_memory_test.cc index 6438089a..3ee813ab 100644 --- a/util/mach/task_memory_test.cc +++ b/util/mach/task_memory_test.cc @@ -15,11 +15,14 @@ #include "util/mach/task_memory.h" #include +#include #include #include +#include "base/mac/scoped_mach_port.h" #include "base/mac/scoped_mach_vm.h" +#include "base/memory/scoped_ptr.h" #include "gtest/gtest.h" #include "util/test/mac/mach_errors.h" @@ -42,42 +45,59 @@ TEST(TaskMemory, ReadSelf) { } TaskMemory memory(mach_task_self()); + + // This tests using both the Read() and ReadMapped() interfaces. std::string result(kSize, '\0'); + scoped_ptr mapped; // Ensure that the entire region can be read. ASSERT_TRUE(memory.Read(address, kSize, &result[0])); EXPECT_EQ(0, memcmp(region, &result[0], kSize)); + ASSERT_TRUE((mapped = memory.ReadMapped(address, kSize))); + EXPECT_EQ(0, memcmp(region, mapped->data(), kSize)); // Ensure that a read of length 0 succeeds and doesn’t touch the result. result.assign(kSize, '\0'); std::string zeroes = result; ASSERT_TRUE(memory.Read(address, 0, &result[0])); EXPECT_EQ(zeroes, result); + ASSERT_TRUE((mapped = memory.ReadMapped(address, 0))); // Ensure that a read starting at an unaligned address works. ASSERT_TRUE(memory.Read(address + 1, kSize - 1, &result[0])); EXPECT_EQ(0, memcmp(region + 1, &result[0], kSize - 1)); + ASSERT_TRUE((mapped = memory.ReadMapped(address + 1, kSize - 1))); + EXPECT_EQ(0, memcmp(region + 1, mapped->data(), kSize - 1)); // Ensure that a read ending at an unaligned address works. ASSERT_TRUE(memory.Read(address, kSize - 1, &result[0])); EXPECT_EQ(0, memcmp(region, &result[0], kSize - 1)); + ASSERT_TRUE((mapped = memory.ReadMapped(address, kSize - 1))); + EXPECT_EQ(0, memcmp(region, mapped->data(), kSize - 1)); // Ensure that a read starting and ending at unaligned addresses works. ASSERT_TRUE(memory.Read(address + 1, kSize - 2, &result[0])); EXPECT_EQ(0, memcmp(region + 1, &result[0], kSize - 2)); + ASSERT_TRUE((mapped = memory.ReadMapped(address + 1, kSize - 2))); + EXPECT_EQ(0, memcmp(region + 1, mapped->data(), kSize - 2)); // Ensure that a read of exactly one page works. ASSERT_TRUE(memory.Read(address + PAGE_SIZE, PAGE_SIZE, &result[0])); EXPECT_EQ(0, memcmp(region + PAGE_SIZE, &result[0], PAGE_SIZE)); + ASSERT_TRUE((mapped = memory.ReadMapped(address + PAGE_SIZE, PAGE_SIZE))); + EXPECT_EQ(0, memcmp(region + PAGE_SIZE, mapped->data(), PAGE_SIZE)); // Ensure that a read of a single byte works. ASSERT_TRUE(memory.Read(address + 2, 1, &result[0])); EXPECT_EQ(region[2], result[0]); + ASSERT_TRUE((mapped = memory.ReadMapped(address + 2, 1))); + EXPECT_EQ(region[2], reinterpret_cast(mapped->data())[0]); // Ensure that a read of length zero works and doesn’t touch the data. result[0] = 'M'; ASSERT_TRUE(memory.Read(address + 3, 0, &result[0])); EXPECT_EQ('M', result[0]); + ASSERT_TRUE((mapped = memory.ReadMapped(address + 3, 0))); } TEST(TaskMemory, ReadSelfUnmapped) { @@ -109,6 +129,15 @@ TEST(TaskMemory, ReadSelfUnmapped) { 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. + scoped_ptr 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. @@ -122,6 +151,14 @@ TEST(TaskMemory, ReadSelfUnmapped) { 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))); } // This function consolidates the cast from a char* to mach_vm_address_t in one @@ -383,4 +420,135 @@ TEST(TaskMemory, ReadCStringSizeLimited_StringLong) { &memory, &string_long[0], string_long.size(), &result)); } +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(&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::mac::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(TaskMemory, MappedMemoryDeallocates) { + // This tests that once a TaskMemory::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. + + TaskMemory memory(mach_task_self()); + scoped_ptr mapped; + + static const char kTestBuffer[] = "hello!"; + mach_vm_address_t test_address = + reinterpret_cast(&kTestBuffer); + ASSERT_TRUE((mapped = memory.ReadMapped(test_address, sizeof(kTestBuffer)))); + EXPECT_EQ(0, memcmp(kTestBuffer, mapped->data(), sizeof(kTestBuffer))); + + vm_address_t mapped_address = reinterpret_cast(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. + const size_t kBigSize = 4 * PAGE_SIZE; + scoped_ptr big_buffer(new char[kBigSize]); + test_address = reinterpret_cast(&big_buffer[0]); + ASSERT_TRUE((mapped = memory.ReadMapped(test_address, kBigSize))); + + mapped_address = reinterpret_cast(mapped->data()); + vm_address_t mapped_last_address = mapped_address + kBigSize - 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(TaskMemory, MappedMemoryReadCString) { + // This tests the behavior of TaskMemory::MappedMemory::ReadCString(). + TaskMemory memory(mach_task_self()); + scoped_ptr mapped; + + static const char kTestBuffer[] = "0\0" "2\0" "45\0" "789"; + const mach_vm_address_t kTestAddress = + reinterpret_cast(&kTestBuffer); + ASSERT_TRUE((mapped = memory.ReadMapped(kTestAddress, 10))); + + std::string string; + ASSERT_TRUE(mapped->ReadCString(0, &string)); + EXPECT_EQ("0", string); + ASSERT_TRUE(mapped->ReadCString(1, &string)); + EXPECT_EQ("", string); + ASSERT_TRUE(mapped->ReadCString(2, &string)); + EXPECT_EQ("2", string); + ASSERT_TRUE(mapped->ReadCString(3, &string)); + EXPECT_EQ("", string); + ASSERT_TRUE(mapped->ReadCString(4, &string)); + EXPECT_EQ("45", string); + ASSERT_TRUE(mapped->ReadCString(5, &string)); + EXPECT_EQ("5", string); + 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("789", string); + ASSERT_TRUE(mapped->ReadCString(8, &string)); + EXPECT_EQ("89", string); + ASSERT_TRUE(mapped->ReadCString(9, &string)); + EXPECT_EQ("9", string); + 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