| // 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/process/process_memory.h" |
| |
| #include <string.h> |
| |
| #include <memory> |
| |
| #include "base/process/process_metrics.h" |
| #include "build/build_config.h" |
| #include "gtest/gtest.h" |
| #include "test/errors.h" |
| #include "test/multiprocess.h" |
| #include "test/multiprocess_exec.h" |
| #include "test/process_type.h" |
| #include "test/scoped_guarded_page.h" |
| #include "util/file/file_io.h" |
| #include "util/misc/from_pointer_cast.h" |
| #include "util/process/process_memory_native.h" |
| |
| #if defined(OS_MACOSX) |
| #include "test/mac/mach_multiprocess.h" |
| #endif // defined(OS_MACOSX) |
| |
| namespace crashpad { |
| namespace test { |
| namespace { |
| |
| // On macOS the ProcessMemoryTests require accessing the child process' task |
| // port which requires root or a code signing entitlement. To account for this |
| // we implement an adaptor class that wraps MachMultiprocess on macOS, because |
| // it shares the child's task port, and makes it behave like MultiprocessExec. |
| #if defined(OS_MACOSX) |
| class MultiprocessAdaptor : public MachMultiprocess { |
| public: |
| void SetChildTestMainFunction(const std::string& function_name) { |
| test_function_ = function_name; |
| } |
| |
| ProcessType ChildProcess() { return ChildTask(); } |
| |
| // Helpers to get I/O handles in the child process |
| static FileHandle OutputHandle() { |
| CHECK_NE(write_pipe_handle_, -1); |
| return write_pipe_handle_; |
| } |
| |
| static FileHandle InputHandle() { |
| CHECK_NE(read_pipe_handle_, -1); |
| return read_pipe_handle_; |
| } |
| |
| private: |
| virtual void Parent() = 0; |
| |
| void MachMultiprocessParent() override { Parent(); } |
| |
| void MachMultiprocessChild() override { |
| read_pipe_handle_ = ReadPipeHandle(); |
| write_pipe_handle_ = WritePipeHandle(); |
| internal::CheckedInvokeMultiprocessChild(test_function_); |
| } |
| |
| std::string test_function_; |
| |
| static FileHandle read_pipe_handle_; |
| static FileHandle write_pipe_handle_; |
| }; |
| |
| FileHandle MultiprocessAdaptor::read_pipe_handle_ = -1; |
| FileHandle MultiprocessAdaptor::write_pipe_handle_ = -1; |
| #else |
| class MultiprocessAdaptor : public MultiprocessExec { |
| public: |
| static FileHandle OutputHandle() { |
| return StdioFileHandle(StdioStream::kStandardOutput); |
| } |
| |
| static FileHandle InputHandle() { |
| return StdioFileHandle(StdioStream::kStandardInput); |
| } |
| |
| private: |
| virtual void Parent() = 0; |
| |
| void MultiprocessParent() override { Parent(); } |
| }; |
| #endif // defined(OS_MACOSX) |
| |
| void DoChildReadTestSetup(size_t* region_size, |
| std::unique_ptr<char[]>* region) { |
| *region_size = 4 * base::GetPageSize(); |
| region->reset(new char[*region_size]); |
| for (size_t index = 0; index < *region_size; ++index) { |
| (*region)[index] = index % 256; |
| } |
| } |
| |
| CRASHPAD_CHILD_TEST_MAIN(ReadTestChild) { |
| size_t region_size; |
| std::unique_ptr<char[]> region; |
| DoChildReadTestSetup(®ion_size, ®ion); |
| FileHandle out = MultiprocessAdaptor::OutputHandle(); |
| CheckedWriteFile(out, ®ion_size, sizeof(region_size)); |
| VMAddress address = FromPointerCast<VMAddress>(region.get()); |
| CheckedWriteFile(out, &address, sizeof(address)); |
| CheckedReadFileAtEOF(MultiprocessAdaptor::InputHandle()); |
| return 0; |
| } |
| |
| class ReadTest : public MultiprocessAdaptor { |
| public: |
| ReadTest() : MultiprocessAdaptor() { |
| SetChildTestMainFunction("ReadTestChild"); |
| } |
| |
| void RunAgainstSelf() { |
| size_t region_size; |
| std::unique_ptr<char[]> region; |
| DoChildReadTestSetup(®ion_size, ®ion); |
| DoTest(GetSelfProcess(), |
| region_size, |
| FromPointerCast<VMAddress>(region.get())); |
| } |
| |
| void RunAgainstChild() { Run(); } |
| |
| private: |
| void Parent() override { |
| size_t region_size; |
| VMAddress region; |
| ASSERT_TRUE( |
| ReadFileExactly(ReadPipeHandle(), ®ion_size, sizeof(region_size))); |
| ASSERT_TRUE(ReadFileExactly(ReadPipeHandle(), ®ion, sizeof(region))); |
| DoTest(ChildProcess(), region_size, region); |
| } |
| |
| void DoTest(ProcessType process, size_t region_size, VMAddress address) { |
| ProcessMemoryNative memory; |
| ASSERT_TRUE(memory.Initialize(process)); |
| |
| std::unique_ptr<char[]> result(new char[region_size]); |
| |
| // Ensure that the entire region can be read. |
| ASSERT_TRUE(memory.Read(address, region_size, result.get())); |
| for (size_t i = 0; i < region_size; ++i) { |
| EXPECT_EQ(result[i], static_cast<char>(i % 256)); |
| } |
| |
| // Ensure that a read of length 0 succeeds and doesn’t touch the result. |
| memset(result.get(), '\0', region_size); |
| ASSERT_TRUE(memory.Read(address, 0, result.get())); |
| for (size_t i = 0; i < region_size; ++i) { |
| EXPECT_EQ(result[i], 0); |
| } |
| |
| // Ensure that a read starting at an unaligned address works. |
| ASSERT_TRUE(memory.Read(address + 1, region_size - 1, result.get())); |
| for (size_t i = 0; i < region_size - 1; ++i) { |
| EXPECT_EQ(result[i], static_cast<char>((i + 1) % 256)); |
| } |
| |
| // Ensure that a read ending at an unaligned address works. |
| ASSERT_TRUE(memory.Read(address, region_size - 1, result.get())); |
| for (size_t i = 0; i < region_size - 1; ++i) { |
| EXPECT_EQ(result[i], static_cast<char>(i % 256)); |
| } |
| |
| // Ensure that a read starting and ending at unaligned addresses works. |
| ASSERT_TRUE(memory.Read(address + 1, region_size - 2, result.get())); |
| for (size_t i = 0; i < region_size - 2; ++i) { |
| EXPECT_EQ(result[i], static_cast<char>((i + 1) % 256)); |
| } |
| |
| // Ensure that a read of exactly one page works. |
| size_t page_size = base::GetPageSize(); |
| ASSERT_GE(region_size, page_size + page_size); |
| ASSERT_TRUE(memory.Read(address + page_size, page_size, result.get())); |
| for (size_t i = 0; i < page_size; ++i) { |
| EXPECT_EQ(result[i], static_cast<char>((i + page_size) % 256)); |
| } |
| |
| // Ensure that reading exactly a single byte works. |
| result[1] = 'J'; |
| ASSERT_TRUE(memory.Read(address + 2, 1, result.get())); |
| EXPECT_EQ(result[0], 2); |
| EXPECT_EQ(result[1], 'J'); |
| } |
| |
| DISALLOW_COPY_AND_ASSIGN(ReadTest); |
| }; |
| |
| TEST(ProcessMemory, ReadSelf) { |
| ReadTest test; |
| test.RunAgainstSelf(); |
| } |
| |
| TEST(ProcessMemory, ReadChild) { |
| ReadTest test; |
| test.RunAgainstChild(); |
| } |
| |
| constexpr char kConstCharEmpty[] = ""; |
| constexpr char kConstCharShort[] = "A short const char[]"; |
| |
| #define SHORT_LOCAL_STRING "A short local variable char[]" |
| |
| std::string MakeLongString() { |
| std::string long_string; |
| const size_t kStringLongSize = 4 * base::GetPageSize(); |
| for (size_t index = 0; index < kStringLongSize; ++index) { |
| long_string.push_back((index % 255) + 1); |
| } |
| EXPECT_EQ(long_string.size(), kStringLongSize); |
| return long_string; |
| } |
| |
| void DoChildCStringReadTestSetup(const char** const_empty, |
| const char** const_short, |
| const char** local_empty, |
| const char** local_short, |
| std::string* long_string) { |
| *const_empty = kConstCharEmpty; |
| *const_short = kConstCharShort; |
| *local_empty = ""; |
| *local_short = SHORT_LOCAL_STRING; |
| *long_string = MakeLongString(); |
| } |
| |
| CRASHPAD_CHILD_TEST_MAIN(ReadCStringTestChild) { |
| const char* const_empty; |
| const char* const_short; |
| const char* local_empty; |
| const char* local_short; |
| std::string long_string; |
| DoChildCStringReadTestSetup( |
| &const_empty, &const_short, &local_empty, &local_short, &long_string); |
| const auto write_address = [](const char* p) { |
| VMAddress address = FromPointerCast<VMAddress>(p); |
| CheckedWriteFile( |
| MultiprocessAdaptor::OutputHandle(), &address, sizeof(address)); |
| }; |
| write_address(const_empty); |
| write_address(const_short); |
| write_address(local_empty); |
| write_address(local_short); |
| write_address(long_string.c_str()); |
| CheckedReadFileAtEOF(MultiprocessAdaptor::InputHandle()); |
| return 0; |
| } |
| |
| class ReadCStringTest : public MultiprocessAdaptor { |
| public: |
| ReadCStringTest(bool limit_size) |
| : MultiprocessAdaptor(), limit_size_(limit_size) { |
| SetChildTestMainFunction("ReadCStringTestChild"); |
| } |
| |
| void RunAgainstSelf() { |
| const char* const_empty; |
| const char* const_short; |
| const char* local_empty; |
| const char* local_short; |
| std::string long_string; |
| DoChildCStringReadTestSetup( |
| &const_empty, &const_short, &local_empty, &local_short, &long_string); |
| DoTest(GetSelfProcess(), |
| FromPointerCast<VMAddress>(const_empty), |
| FromPointerCast<VMAddress>(const_short), |
| FromPointerCast<VMAddress>(local_empty), |
| FromPointerCast<VMAddress>(local_short), |
| FromPointerCast<VMAddress>(long_string.c_str())); |
| } |
| void RunAgainstChild() { Run(); } |
| |
| private: |
| void Parent() override { |
| #define DECLARE_AND_READ_ADDRESS(name) \ |
| VMAddress name; \ |
| ASSERT_TRUE(ReadFileExactly(ReadPipeHandle(), &name, sizeof(name))); |
| DECLARE_AND_READ_ADDRESS(const_empty_address); |
| DECLARE_AND_READ_ADDRESS(const_short_address); |
| DECLARE_AND_READ_ADDRESS(local_empty_address); |
| DECLARE_AND_READ_ADDRESS(local_short_address); |
| DECLARE_AND_READ_ADDRESS(long_string_address); |
| #undef DECLARE_AND_READ_ADDRESS |
| |
| DoTest(ChildProcess(), |
| const_empty_address, |
| const_short_address, |
| local_empty_address, |
| local_short_address, |
| long_string_address); |
| } |
| |
| void Compare(ProcessMemory& memory, VMAddress address, const char* str) { |
| std::string result; |
| if (limit_size_) { |
| ASSERT_TRUE( |
| memory.ReadCStringSizeLimited(address, strlen(str) + 1, &result)); |
| EXPECT_EQ(result, str); |
| ASSERT_TRUE( |
| memory.ReadCStringSizeLimited(address, strlen(str) + 2, &result)); |
| EXPECT_EQ(result, str); |
| EXPECT_FALSE( |
| memory.ReadCStringSizeLimited(address, strlen(str), &result)); |
| } else { |
| ASSERT_TRUE(memory.ReadCString(address, &result)); |
| EXPECT_EQ(result, str); |
| } |
| } |
| |
| void DoTest(ProcessType process, |
| VMAddress const_empty_address, |
| VMAddress const_short_address, |
| VMAddress local_empty_address, |
| VMAddress local_short_address, |
| VMAddress long_string_address) { |
| ProcessMemoryNative memory; |
| ASSERT_TRUE(memory.Initialize(process)); |
| |
| Compare(memory, const_empty_address, kConstCharEmpty); |
| Compare(memory, const_short_address, kConstCharShort); |
| Compare(memory, local_empty_address, ""); |
| Compare(memory, local_short_address, SHORT_LOCAL_STRING); |
| std::string long_string_for_comparison = MakeLongString(); |
| Compare(memory, long_string_address, long_string_for_comparison.c_str()); |
| } |
| |
| const bool limit_size_; |
| |
| DISALLOW_COPY_AND_ASSIGN(ReadCStringTest); |
| }; |
| |
| TEST(ProcessMemory, ReadCStringSelf) { |
| ReadCStringTest test(/* limit_size= */ false); |
| test.RunAgainstSelf(); |
| } |
| |
| TEST(ProcessMemory, ReadCStringChild) { |
| ReadCStringTest test(/* limit_size= */ false); |
| test.RunAgainstChild(); |
| } |
| |
| TEST(ProcessMemory, ReadCStringSizeLimitedSelf) { |
| ReadCStringTest test(/* limit_size= */ true); |
| test.RunAgainstSelf(); |
| } |
| |
| TEST(ProcessMemory, ReadCStringSizeLimitedChild) { |
| ReadCStringTest test(/* limit_size= */ true); |
| test.RunAgainstChild(); |
| } |
| |
| void DoReadUnmappedChildMainSetup(void* page) { |
| char* region = reinterpret_cast<char*>(page); |
| for (size_t index = 0; index < base::GetPageSize(); ++index) { |
| region[index] = index % 256; |
| } |
| } |
| |
| CRASHPAD_CHILD_TEST_MAIN(ReadUnmappedChildMain) { |
| ScopedGuardedPage pages; |
| VMAddress address = reinterpret_cast<VMAddress>(pages.Pointer()); |
| DoReadUnmappedChildMainSetup(pages.Pointer()); |
| FileHandle out = MultiprocessAdaptor::OutputHandle(); |
| CheckedWriteFile(out, &address, sizeof(address)); |
| CheckedReadFileAtEOF(MultiprocessAdaptor::InputHandle()); |
| return 0; |
| } |
| |
| // This test only supports running against a child process because |
| // ScopedGuardedPage is not thread-safe. |
| class ReadUnmappedTest : public MultiprocessAdaptor { |
| public: |
| ReadUnmappedTest() : MultiprocessAdaptor() { |
| SetChildTestMainFunction("ReadUnmappedChildMain"); |
| } |
| |
| void RunAgainstChild() { Run(); } |
| |
| private: |
| void Parent() override { |
| VMAddress address = 0; |
| ASSERT_TRUE(ReadFileExactly(ReadPipeHandle(), &address, sizeof(address))); |
| DoTest(ChildProcess(), address); |
| } |
| |
| void DoTest(ProcessType process, VMAddress address) { |
| ProcessMemoryNative memory; |
| ASSERT_TRUE(memory.Initialize(process)); |
| |
| VMAddress page_addr1 = address; |
| VMAddress page_addr2 = page_addr1 + base::GetPageSize(); |
| |
| std::unique_ptr<char[]> result(new char[base::GetPageSize() * 2]); |
| EXPECT_TRUE(memory.Read(page_addr1, base::GetPageSize(), result.get())); |
| EXPECT_TRUE(memory.Read(page_addr2 - 1, 1, result.get())); |
| |
| EXPECT_FALSE( |
| memory.Read(page_addr1, base::GetPageSize() * 2, result.get())); |
| EXPECT_FALSE(memory.Read(page_addr2, base::GetPageSize(), result.get())); |
| EXPECT_FALSE(memory.Read(page_addr2 - 1, 2, result.get())); |
| } |
| |
| DISALLOW_COPY_AND_ASSIGN(ReadUnmappedTest); |
| }; |
| |
| TEST(ProcessMemory, ReadUnmappedChild) { |
| ReadUnmappedTest test; |
| ASSERT_FALSE(testing::Test::HasFailure()); |
| test.RunAgainstChild(); |
| } |
| |
| constexpr size_t kChildProcessStringLength = 10; |
| |
| class StringDataInChildProcess { |
| public: |
| // This constructor only makes sense in the child process. |
| explicit StringDataInChildProcess(const char* cstring, bool valid) |
| : address_(FromPointerCast<VMAddress>(cstring)) { |
| if (valid) { |
| memcpy(expected_value_, cstring, kChildProcessStringLength + 1); |
| } else { |
| memset(expected_value_, 0xff, kChildProcessStringLength + 1); |
| } |
| } |
| |
| void Write(FileHandle out) { |
| CheckedWriteFile(out, &address_, sizeof(address_)); |
| CheckedWriteFile(out, &expected_value_, sizeof(expected_value_)); |
| } |
| |
| static StringDataInChildProcess Read(FileHandle in) { |
| StringDataInChildProcess str; |
| EXPECT_TRUE(ReadFileExactly(in, &str.address_, sizeof(str.address_))); |
| EXPECT_TRUE( |
| ReadFileExactly(in, &str.expected_value_, sizeof(str.expected_value_))); |
| return str; |
| } |
| |
| VMAddress address() const { return address_; } |
| std::string expected_value() const { return expected_value_; } |
| |
| private: |
| StringDataInChildProcess() : address_(0), expected_value_() {} |
| |
| VMAddress address_; |
| char expected_value_[kChildProcessStringLength + 1]; |
| }; |
| |
| void DoCStringUnmappedTestSetup( |
| void* page, |
| std::vector<StringDataInChildProcess>* strings) { |
| char* region = reinterpret_cast<char*>(page); |
| for (size_t index = 0; index < base::GetPageSize(); ++index) { |
| region[index] = 1 + index % 255; |
| } |
| |
| // A string at the start of the mapped region |
| char* string1 = region; |
| string1[kChildProcessStringLength] = '\0'; |
| |
| // A string near the end of the mapped region |
| char* string2 = region + base::GetPageSize() - kChildProcessStringLength * 2; |
| string2[kChildProcessStringLength] = '\0'; |
| |
| // A string that crosses from the mapped into the unmapped region |
| char* string3 = region + base::GetPageSize() - kChildProcessStringLength + 1; |
| |
| // A string entirely in the unmapped region |
| char* string4 = region + base::GetPageSize() + 10; |
| |
| strings->push_back(StringDataInChildProcess(string1, true)); |
| strings->push_back(StringDataInChildProcess(string2, true)); |
| strings->push_back(StringDataInChildProcess(string3, false)); |
| strings->push_back(StringDataInChildProcess(string4, false)); |
| } |
| |
| CRASHPAD_CHILD_TEST_MAIN(ReadCStringUnmappedChildMain) { |
| ScopedGuardedPage pages; |
| std::vector<StringDataInChildProcess> strings; |
| DoCStringUnmappedTestSetup(pages.Pointer(), &strings); |
| FileHandle out = MultiprocessAdaptor::OutputHandle(); |
| strings[0].Write(out); |
| strings[1].Write(out); |
| strings[2].Write(out); |
| strings[3].Write(out); |
| CheckedReadFileAtEOF(MultiprocessAdaptor::InputHandle()); |
| return 0; |
| } |
| |
| // This test only supports running against a child process because |
| // ScopedGuardedPage is not thread-safe. |
| class ReadCStringUnmappedTest : public MultiprocessAdaptor { |
| public: |
| ReadCStringUnmappedTest(bool limit_size) |
| : MultiprocessAdaptor(), limit_size_(limit_size) { |
| SetChildTestMainFunction("ReadCStringUnmappedChildMain"); |
| } |
| |
| void RunAgainstChild() { Run(); } |
| |
| private: |
| void Parent() override { |
| std::vector<StringDataInChildProcess> strings; |
| strings.push_back(StringDataInChildProcess::Read(ReadPipeHandle())); |
| strings.push_back(StringDataInChildProcess::Read(ReadPipeHandle())); |
| strings.push_back(StringDataInChildProcess::Read(ReadPipeHandle())); |
| strings.push_back(StringDataInChildProcess::Read(ReadPipeHandle())); |
| ASSERT_NO_FATAL_FAILURE(DoTest(ChildProcess(), strings)); |
| } |
| |
| void DoTest(ProcessType process, |
| const std::vector<StringDataInChildProcess>& strings) { |
| ProcessMemoryNative memory; |
| ASSERT_TRUE(memory.Initialize(process)); |
| |
| std::string result; |
| result.reserve(kChildProcessStringLength + 1); |
| |
| if (limit_size_) { |
| ASSERT_TRUE(memory.ReadCStringSizeLimited( |
| strings[0].address(), kChildProcessStringLength + 1, &result)); |
| EXPECT_EQ(result, strings[0].expected_value()); |
| ASSERT_TRUE(memory.ReadCStringSizeLimited( |
| strings[1].address(), kChildProcessStringLength + 1, &result)); |
| EXPECT_EQ(result, strings[1].expected_value()); |
| EXPECT_FALSE(memory.ReadCStringSizeLimited( |
| strings[2].address(), kChildProcessStringLength + 1, &result)); |
| EXPECT_FALSE(memory.ReadCStringSizeLimited( |
| strings[3].address(), kChildProcessStringLength + 1, &result)); |
| } else { |
| ASSERT_TRUE(memory.ReadCString(strings[0].address(), &result)); |
| EXPECT_EQ(result, strings[0].expected_value()); |
| ASSERT_TRUE(memory.ReadCString(strings[1].address(), &result)); |
| EXPECT_EQ(result, strings[1].expected_value()); |
| EXPECT_FALSE(memory.ReadCString(strings[2].address(), &result)); |
| EXPECT_FALSE(memory.ReadCString(strings[3].address(), &result)); |
| } |
| } |
| |
| const bool limit_size_; |
| |
| DISALLOW_COPY_AND_ASSIGN(ReadCStringUnmappedTest); |
| }; |
| |
| TEST(ProcessMemory, ReadCStringUnmappedChild) { |
| ReadCStringUnmappedTest test(/* limit_size= */ false); |
| ASSERT_FALSE(testing::Test::HasFailure()); |
| test.RunAgainstChild(); |
| } |
| |
| TEST(ProcessMemory, ReadCStringSizeLimitedUnmappedChild) { |
| ReadCStringUnmappedTest test(/* limit_size= */ true); |
| ASSERT_FALSE(testing::Test::HasFailure()); |
| test.RunAgainstChild(); |
| } |
| |
| } // namespace |
| } // namespace test |
| } // namespace crashpad |