diff --git a/util/linux/memory_map.cc b/util/linux/memory_map.cc new file mode 100644 index 00000000..cbf0c4e4 --- /dev/null +++ b/util/linux/memory_map.cc @@ -0,0 +1,207 @@ +// Copyright 2017 The Crashpad Authors. All rights reserved. +// +// 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/linux/memory_map.h" + +#include +#include +#include + +#include "base/files/file_path.h" +#include "base/logging.h" +#include "build/build_config.h" +#include "util/file/delimited_file_reader.h" +#include "util/file/file_reader.h" +#include "util/stdlib/string_number_conversion.h" + +namespace crashpad { + +namespace { + +// This function is used in this file specfically for signed or unsigned longs. +// longs are typically either int or int64 sized, but pointers to longs are not +// automatically coerced to pointers to ints when they are the same size. +// Simply adding a StringToNumber for longs doesn't work since sometimes long +// and int64_t are actually the same type, resulting in a redefinition error. +template +bool LocalStringToNumber(const base::StringPiece& string, Type* number) { + static_assert(sizeof(Type) == sizeof(int) || sizeof(Type) == sizeof(int64_t), + "Unexpected Type size"); + + if (sizeof(Type) == sizeof(int)) { + return std::numeric_limits::is_signed + ? StringToNumber(string, reinterpret_cast(number)) + : StringToNumber(string, + reinterpret_cast(number)); + } else { + return std::numeric_limits::is_signed + ? StringToNumber(string, reinterpret_cast(number)) + : StringToNumber(string, reinterpret_cast(number)); + } +} + +template +bool HexStringToNumber(const std::string& string, Type* number) { + return LocalStringToNumber("0x" + string, number); +} + +} // namespace + +MemoryMap::Mapping::Mapping() + : name(), + range(false, 0, 0), + offset(0), + device(0), + inode(0), + readable(false), + writable(false), + executable(false), + shareable(false) {} + +MemoryMap::MemoryMap() : mappings_(), initialized_() {} + +MemoryMap::~MemoryMap() {} + +bool MemoryMap::Initialize(pid_t pid) { + INITIALIZATION_STATE_SET_INITIALIZING(initialized_); + + char path[32]; + snprintf(path, sizeof(path), "/proc/%d/maps", pid); + FileReader maps_file; + if (!maps_file.Open(base::FilePath(path))) { + return false; + } + DelimitedFileReader maps_file_reader(&maps_file); + + DelimitedFileReader::Result result; + std::string field; + while ((result = maps_file_reader.GetDelim('-', &field)) == + DelimitedFileReader::Result::kSuccess) { + field.pop_back(); + + LinuxVMAddress start_address; + if (!HexStringToNumber(field, &start_address)) { + LOG(ERROR) << "format error"; + return false; + } + + LinuxVMAddress end_address; + if (maps_file_reader.GetDelim(' ', &field) != + DelimitedFileReader::Result::kSuccess || + (field.pop_back(), !HexStringToNumber(field, &end_address))) { + LOG(ERROR) << "format error"; + return false; + } + if (end_address <= start_address) { + LOG(ERROR) << "format error"; + return false; + } + + // TODO(jperaza): set bitness properly +#if defined(ARCH_CPU_64_BITS) + const bool is_64_bit = true; +#else + const bool is_64_bit = false; +#endif + + Mapping mapping; + mapping.range.SetRange( + is_64_bit, start_address, end_address - start_address); + + if (maps_file_reader.GetDelim(' ', &field) != + DelimitedFileReader::Result::kSuccess || + (field.pop_back(), field.size() != 4)) { + LOG(ERROR) << "format error"; + return false; + } +#define SET_FIELD(actual_c, outval, true_chars, false_chars) \ + do { \ + if (strchr(true_chars, actual_c)) { \ + *outval = true; \ + } else if (strchr(false_chars, actual_c)) { \ + *outval = false; \ + } else { \ + LOG(ERROR) << "format error"; \ + return false; \ + } \ + } while (false) + SET_FIELD(field[0], &mapping.readable, "r", "-"); + SET_FIELD(field[1], &mapping.writable, "w", "-"); + SET_FIELD(field[2], &mapping.executable, "x", "-"); + SET_FIELD(field[3], &mapping.shareable, "sS", "p"); +#undef SET_FIELD + + if (maps_file_reader.GetDelim(' ', &field) != + DelimitedFileReader::Result::kSuccess || + (field.pop_back(), !HexStringToNumber(field, &mapping.offset))) { + LOG(ERROR) << "format error"; + return false; + } + + int major, minor; + if (maps_file_reader.GetDelim(' ', &field) != + DelimitedFileReader::Result::kSuccess || + (field.pop_back(), field.size() != 5) || + !HexStringToNumber(field.substr(0, 2), &major) || + !HexStringToNumber(field.substr(3, 2), &minor)) { + LOG(ERROR) << "format error"; + return false; + } + mapping.device = MKDEV(major, minor); + + if (maps_file_reader.GetDelim(' ', &field) != + DelimitedFileReader::Result::kSuccess || + (field.pop_back(), !LocalStringToNumber(field, &mapping.inode))) { + LOG(ERROR) << "format error"; + return false; + } + + if (maps_file_reader.GetDelim('\n', &field) != + DelimitedFileReader::Result::kSuccess) { + LOG(ERROR) << "format error"; + return false; + } + if (field.back() != '\n') { + LOG(ERROR) << "format error"; + return false; + } + field.pop_back(); + + mappings_.push_back(mapping); + + size_t path_start = field.find_first_not_of(' '); + if (path_start != std::string::npos) { + mappings_.back().name = field.substr(path_start); + } + } + if (result != DelimitedFileReader::Result::kEndOfFile) { + return false; + } + + INITIALIZATION_STATE_SET_VALID(initialized_); + return true; +} + +const MemoryMap::Mapping* MemoryMap::FindMapping(LinuxVMAddress address) const { + INITIALIZATION_STATE_DCHECK_VALID(initialized_); + + for (const auto& mapping : mappings_) { + if (mapping.range.Base() <= address && mapping.range.End() > address) { + return &mapping; + } + } + return nullptr; +} + +} // namespace crashpad diff --git a/util/linux/memory_map.h b/util/linux/memory_map.h new file mode 100644 index 00000000..2d7a01a1 --- /dev/null +++ b/util/linux/memory_map.h @@ -0,0 +1,78 @@ +// Copyright 2017 The Crashpad Authors. All rights reserved. +// +// 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_LINUX_MEMORY_MAP_H_ +#define CRASHPAD_UTIL_LINUX_MEMORY_MAP_H_ + +#include + +#include +#include + +#include "util/linux/address_types.h" +#include "util/linux/checked_linux_address_range.h" +#include "util/misc/initialization_state_dcheck.h" + +namespace crashpad { + +//! \brief Accesses information about mapped memory in another process. +//! +//! The target process must be stopped to guarantee correct mappings. If the +//! target process is not stopped, mappings may be invalid after the return from +//! Initialize(), and even mappings existing at the time Initialize() was called +//! may not be found. +class MemoryMap { + public: + //! \brief Information about a mapped region of memory. + struct Mapping { + Mapping(); + + std::string name; + CheckedLinuxAddressRange range; + off_t offset; + dev_t device; + ino_t inode; + bool readable; + bool writable; + bool executable; + bool shareable; + }; + + MemoryMap(); + ~MemoryMap(); + + //! \brief Initializes this object with information about the mapped memory + //! regions in the process whose ID is \a pid. + //! + //! This method must be called successfully prior to calling any other method + //! in this class. This method may only be called once. + //! + //! \param[in] pid The process ID to obtain information for. + //! + //! \return `true` on success, `false` on failure with a message logged. + bool Initialize(pid_t pid); + + //! \return The Mapping containing \a address. The caller does not take + //! ownership of this object. It is scoped to the lifetime of the + //! MemoryMap object that it was obtained from. + const Mapping* FindMapping(LinuxVMAddress address) const; + + private: + std::vector mappings_; + InitializationStateDcheck initialized_; +}; + +} // namespace crashpad + +#endif // CRASHPAD_UTIL_LINUX_MEMORY_MAP_H_ diff --git a/util/linux/memory_map_test.cc b/util/linux/memory_map_test.cc new file mode 100644 index 00000000..1ae9b7d4 --- /dev/null +++ b/util/linux/memory_map_test.cc @@ -0,0 +1,329 @@ +// Copyright 2017 The Crashpad Authors. All rights reserved. +// +// 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/linux/memory_map.h" + +#include +#include +#include +#include +#include + +#include "base/files/file_path.h" +#include "gtest/gtest.h" +#include "test/errors.h" +#include "test/file.h" +#include "test/multiprocess.h" +#include "test/scoped_temp_dir.h" +#include "util/file/file_io.h" +#include "util/misc/clock.h" +#include "util/posix/scoped_mmap.h" + +namespace crashpad { +namespace test { +namespace { + +TEST(MemoryMap, SelfBasic) { + ScopedMmap mmapping; + ASSERT_TRUE(mmapping.ResetMmap(nullptr, + getpagesize(), + PROT_EXEC | PROT_READ, + MAP_SHARED | MAP_ANON, + -1, + 0)); + MemoryMap map; + ASSERT_TRUE(map.Initialize(getpid())); + + auto stack_address = reinterpret_cast(&map); + const MemoryMap::Mapping* mapping = map.FindMapping(stack_address); + ASSERT_TRUE(mapping); + EXPECT_GE(stack_address, mapping->range.Base()); + EXPECT_LT(stack_address, mapping->range.End()); + EXPECT_TRUE(mapping->readable); + EXPECT_TRUE(mapping->writable); + + auto code_address = reinterpret_cast(getpid); + mapping = map.FindMapping(code_address); + ASSERT_TRUE(mapping); + EXPECT_GE(code_address, mapping->range.Base()); + EXPECT_LT(code_address, mapping->range.End()); + EXPECT_TRUE(mapping->readable); + EXPECT_FALSE(mapping->writable); + EXPECT_TRUE(mapping->executable); + + auto mapping_address = mmapping.addr_as(); + mapping = map.FindMapping(mapping_address); + ASSERT_TRUE(mapping); + mapping = map.FindMapping(mapping_address + mmapping.len() - 1); + ASSERT_TRUE(mapping); + EXPECT_EQ(mapping_address, mapping->range.Base()); + EXPECT_EQ(mapping_address + mmapping.len(), mapping->range.End()); + EXPECT_TRUE(mapping->readable); + EXPECT_FALSE(mapping->writable); + EXPECT_TRUE(mapping->executable); + EXPECT_TRUE(mapping->shareable); +} + +class MapChildTest : public Multiprocess { + public: + MapChildTest() : Multiprocess(), page_size_(getpagesize()) {} + ~MapChildTest() {} + + private: + void MultiprocessParent() override { + LinuxVMAddress code_address; + CheckedReadFileExactly( + ReadPipeHandle(), &code_address, sizeof(code_address)); + + LinuxVMAddress stack_address; + CheckedReadFileExactly( + ReadPipeHandle(), &stack_address, sizeof(stack_address)); + + LinuxVMAddress mapped_address; + CheckedReadFileExactly( + ReadPipeHandle(), &mapped_address, sizeof(mapped_address)); + + LinuxVMAddress mapped_file_address; + CheckedReadFileExactly( + ReadPipeHandle(), &mapped_file_address, sizeof(mapped_file_address)); + LinuxVMSize path_length; + CheckedReadFileExactly(ReadPipeHandle(), &path_length, sizeof(path_length)); + std::string mapped_file_name(path_length, std::string::value_type()); + CheckedReadFileExactly(ReadPipeHandle(), &mapped_file_name[0], path_length); + + MemoryMap map; + ASSERT_TRUE(map.Initialize(ChildPID())); + + const MemoryMap::Mapping* mapping = map.FindMapping(code_address); + ASSERT_TRUE(mapping); + EXPECT_GE(code_address, mapping->range.Base()); + EXPECT_LT(code_address, mapping->range.End()); + EXPECT_TRUE(mapping->readable); + EXPECT_TRUE(mapping->executable); + EXPECT_FALSE(mapping->writable); + + mapping = map.FindMapping(stack_address); + ASSERT_TRUE(mapping); + EXPECT_GE(stack_address, mapping->range.Base()); + EXPECT_LT(stack_address, mapping->range.End()); + EXPECT_TRUE(mapping->readable); + EXPECT_TRUE(mapping->writable); + + mapping = map.FindMapping(mapped_address); + ASSERT_TRUE(mapping); + EXPECT_EQ(mapped_address, mapping->range.Base()); + EXPECT_EQ(mapped_address + page_size_, mapping->range.End()); + EXPECT_FALSE(mapping->readable); + EXPECT_FALSE(mapping->writable); + EXPECT_FALSE(mapping->executable); + EXPECT_TRUE(mapping->shareable); + + mapping = map.FindMapping(mapped_file_address); + ASSERT_TRUE(mapping); + EXPECT_EQ(mapped_file_address, mapping->range.Base()); + EXPECT_EQ(mapping->offset, static_cast(page_size_)); + EXPECT_TRUE(mapping->readable); + EXPECT_TRUE(mapping->writable); + EXPECT_FALSE(mapping->executable); + EXPECT_FALSE(mapping->shareable); + EXPECT_EQ(mapping->name, mapped_file_name); + struct stat file_stat; + ASSERT_EQ(stat(mapped_file_name.c_str(), &file_stat), 0) + << ErrnoMessage("stat"); + EXPECT_EQ(mapping->device, file_stat.st_dev); + EXPECT_EQ(mapping->inode, file_stat.st_ino); + } + + void MultiprocessChild() override { + auto code_address = reinterpret_cast(getpid); + CheckedWriteFile(WritePipeHandle(), &code_address, sizeof(code_address)); + + auto stack_address = reinterpret_cast(&code_address); + CheckedWriteFile(WritePipeHandle(), &stack_address, sizeof(stack_address)); + + ScopedMmap mapping; + ASSERT_TRUE(mapping.ResetMmap( + nullptr, page_size_, PROT_NONE, MAP_SHARED | MAP_ANON, -1, 0)); + auto mapped_address = mapping.addr_as(); + CheckedWriteFile( + WritePipeHandle(), &mapped_address, sizeof(mapped_address)); + + ScopedTempDir temp_dir; + base::FilePath path = + temp_dir.path().Append(FILE_PATH_LITERAL("test_file")); + ASSERT_FALSE(FileExists(path)); + std::string path_string = path.value(); + ScopedFileHandle handle(LoggingOpenFileForReadAndWrite( + path, FileWriteMode::kReuseOrCreate, FilePermissions::kOwnerOnly)); + ASSERT_TRUE(handle.is_valid()); + std::unique_ptr file_contents(new char[page_size_ * 2]); + CheckedWriteFile(handle.get(), file_contents.get(), page_size_ * 2); + + ScopedMmap file_mapping; + ASSERT_TRUE(file_mapping.ResetMmap(nullptr, + page_size_, + PROT_READ | PROT_WRITE, + MAP_PRIVATE, + handle.get(), + page_size_)); + + auto mapped_file_address = file_mapping.addr_as(); + CheckedWriteFile( + WritePipeHandle(), &mapped_file_address, sizeof(mapped_file_address)); + LinuxVMSize path_length = path_string.size(); + CheckedWriteFile(WritePipeHandle(), &path_length, sizeof(path_length)); + CheckedWriteFile(WritePipeHandle(), path_string.c_str(), path_length); + + CheckedReadFileAtEOF(ReadPipeHandle()); + } + + const size_t page_size_; + + DISALLOW_COPY_AND_ASSIGN(MapChildTest); +}; + +TEST(MemoryMap, MapChild) { + MapChildTest test; + test.Run(); +} + +// Some systems optimize mappings by allocating new mappings inside existing +// mappings with matching permissions. Work around this by allocating one large +// mapping and then switching up the permissions of individual pages to force +// populating more entries in the maps file. +void InitializeMappings(ScopedMmap* mappings, + size_t num_mappings, + size_t mapping_size) { + ASSERT_TRUE(mappings->ResetMmap(nullptr, + mapping_size * num_mappings, + PROT_READ, + MAP_PRIVATE | MAP_ANON, + -1, + 0)); + + auto region = mappings->addr_as(); + for (size_t index = 0; index < num_mappings; index += 2) { + ASSERT_EQ(mprotect(reinterpret_cast(region + index * mapping_size), + mapping_size, + PROT_READ | PROT_WRITE), + 0) + << ErrnoMessage("mprotect"); + } +} + +void ExpectMappings(const MemoryMap& map, + LinuxVMAddress region_addr, + size_t num_mappings, + size_t mapping_size) { + for (size_t index = 0; index < num_mappings; ++index) { + auto mapping_address = region_addr + index * mapping_size; + const MemoryMap::Mapping* mapping = map.FindMapping(mapping_address); + ASSERT_TRUE(mapping); + EXPECT_EQ(mapping_address, mapping->range.Base()); + EXPECT_EQ(mapping_address + mapping_size, mapping->range.End()); + EXPECT_TRUE(mapping->readable); + EXPECT_FALSE(mapping->shareable); + EXPECT_FALSE(mapping->executable); + if (index % 2 == 0) { + EXPECT_TRUE(mapping->writable); + } else { + EXPECT_FALSE(mapping->writable); + } + } +} + +TEST(MemoryMap, SelfLargeMapFile) { + constexpr size_t kNumMappings = 4096; + const size_t page_size = getpagesize(); + ScopedMmap mappings; + + ASSERT_NO_FATAL_FAILURE( + InitializeMappings(&mappings, kNumMappings, page_size)); + + MemoryMap map; + ASSERT_TRUE(map.Initialize(getpid())); + + ExpectMappings( + map, mappings.addr_as(), kNumMappings, page_size); +} + +class MapRunningChildTest : public Multiprocess { + public: + MapRunningChildTest() : Multiprocess(), page_size_(getpagesize()) {} + ~MapRunningChildTest() {} + + private: + void MultiprocessParent() override { + // Let the child get started + LinuxVMAddress region_addr; + CheckedReadFileExactly(ReadPipeHandle(), ®ion_addr, sizeof(region_addr)); + + // Let the child get back to its work + SleepNanoseconds(1000); + + for (int iter = 0; iter < 8; ++iter) { + MemoryMap map; + ASSERT_TRUE(map.Initialize(ChildPID())); + + // We should at least find the original mappings. The extra mappings may + // or + // not be found depending on scheduling. + ExpectMappings(map, region_addr, kNumMappings, page_size_); + } + } + + void MultiprocessChild() override { + ASSERT_EQ(fcntl(ReadPipeHandle(), F_SETFL, O_NONBLOCK), 0) + << ErrnoMessage("fcntl"); + + ScopedMmap mappings; + ASSERT_NO_FATAL_FAILURE( + InitializeMappings(&mappings, kNumMappings, page_size_)); + + // Let the parent start mapping us + auto region_addr = mappings.addr_as(); + CheckedWriteFile(WritePipeHandle(), ®ion_addr, sizeof(region_addr)); + + // But don't stop there! + constexpr size_t kNumExtraMappings = 1024; + ScopedMmap extra_mappings; + + while (true) { + ASSERT_NO_FATAL_FAILURE( + InitializeMappings(&extra_mappings, kNumExtraMappings, page_size_)); + + // Quit when the parent is done + char c; + FileOperationResult res = ReadFile(ReadPipeHandle(), &c, sizeof(c)); + if (res == 0) { + break; + } + ASSERT_EQ(errno, EAGAIN); + } + } + + static constexpr size_t kNumMappings = 4096; + const size_t page_size_; + + DISALLOW_COPY_AND_ASSIGN(MapRunningChildTest); +}; + +TEST(MemoryMap, MapRunningChild) { + MapRunningChildTest test; + test.Run(); +} + +} // namespace +} // namespace test +} // namespace crashpad diff --git a/util/util.gyp b/util/util.gyp index 576803db..5a7947a1 100644 --- a/util/util.gyp +++ b/util/util.gyp @@ -46,6 +46,8 @@ 'file/string_file.h', 'linux/address_types.h', 'linux/checked_address_range.h', + 'linux/memory_map.cc', + 'linux/memory_map.h', 'linux/process_memory.cc', 'linux/process_memory.h', 'linux/scoped_ptrace_attach.cc', diff --git a/util/util_test.gyp b/util/util_test.gyp index 7e1661df..6ee35258 100644 --- a/util/util_test.gyp +++ b/util/util_test.gyp @@ -39,6 +39,7 @@ 'file/file_io_test.cc', 'file/file_reader_test.cc', 'file/string_file_test.cc', + 'linux/memory_map_test.cc', 'linux/process_memory_test.cc', 'linux/scoped_ptrace_attach_test.cc', 'mac/launchd_test.mm',