blob: 888b9ea2d18420838cbb532b1729937b8d07552f [file] [log] [blame]
// Copyright 2015 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/win/process_info.h"
#include <stddef.h>
#include <winternl.h>
#include <algorithm>
#include <limits>
#include <memory>
#include <new>
#include <type_traits>
#include "base/check_op.h"
#include "base/logging.h"
#include "base/memory/free_deleter.h"
#include "base/process/memory.h"
#include "base/strings/stringprintf.h"
#include "build/build_config.h"
#include "util/misc/from_pointer_cast.h"
#include "util/numeric/safe_assignment.h"
#include "util/win/get_function.h"
#include "util/win/handle.h"
#include "util/win/nt_internals.h"
#include "util/win/ntstatus_logging.h"
#include "util/win/process_structs.h"
#include "util/win/scoped_handle.h"
namespace crashpad {
namespace {
using UniqueMallocPtr = std::unique_ptr<uint8_t[], base::FreeDeleter>;
UniqueMallocPtr UncheckedAllocate(size_t size) {
void* raw_ptr = nullptr;
if (!base::UncheckedMalloc(size, &raw_ptr))
return UniqueMallocPtr();
return UniqueMallocPtr(new (raw_ptr) uint8_t[size]);
}
NTSTATUS NtQueryInformationProcess(HANDLE process_handle,
PROCESSINFOCLASS process_information_class,
PVOID process_information,
ULONG process_information_length,
PULONG return_length) {
static const auto nt_query_information_process =
GET_FUNCTION_REQUIRED(L"ntdll.dll", ::NtQueryInformationProcess);
return nt_query_information_process(process_handle,
process_information_class,
process_information,
process_information_length,
return_length);
}
bool IsProcessWow64(HANDLE process_handle) {
static const auto is_wow64_process =
GET_FUNCTION(L"kernel32.dll", ::IsWow64Process);
if (!is_wow64_process)
return false;
BOOL is_wow64;
if (!is_wow64_process(process_handle, &is_wow64)) {
PLOG(ERROR) << "IsWow64Process";
return false;
}
return !!is_wow64;
}
template <class T>
bool ReadUnicodeString(HANDLE process,
const process_types::UNICODE_STRING<T>& us,
std::wstring* result) {
if (us.Length == 0) {
result->clear();
return true;
}
DCHECK_EQ(us.Length % sizeof(wchar_t), 0u);
result->resize(us.Length / sizeof(wchar_t));
SIZE_T bytes_read;
if (!ReadProcessMemory(
process,
reinterpret_cast<const void*>(static_cast<uintptr_t>(us.Buffer)),
&result->operator[](0),
us.Length,
&bytes_read)) {
PLOG(ERROR) << "ReadProcessMemory UNICODE_STRING";
return false;
}
if (bytes_read != us.Length) {
LOG(ERROR) << "ReadProcessMemory UNICODE_STRING incorrect size";
return false;
}
return true;
}
template <class T>
bool ReadStruct(HANDLE process, WinVMAddress at, T* into) {
SIZE_T bytes_read;
if (!ReadProcessMemory(process,
reinterpret_cast<const void*>(at),
into,
sizeof(T),
&bytes_read)) {
// We don't have a name for the type we're reading, so include the signature
// to get the type of T.
PLOG(ERROR) << "ReadProcessMemory " << __FUNCSIG__;
return false;
}
if (bytes_read != sizeof(T)) {
LOG(ERROR) << "ReadProcessMemory " << __FUNCSIG__ << " incorrect size";
return false;
}
return true;
}
bool RegionIsAccessible(const MEMORY_BASIC_INFORMATION64& memory_info) {
return memory_info.State == MEM_COMMIT &&
(memory_info.Protect & PAGE_NOACCESS) == 0 &&
(memory_info.Protect & PAGE_GUARD) == 0;
}
MEMORY_BASIC_INFORMATION64 MemoryBasicInformationToMemoryBasicInformation64(
const MEMORY_BASIC_INFORMATION& mbi) {
MEMORY_BASIC_INFORMATION64 mbi64 = {0};
mbi64.BaseAddress = FromPointerCast<ULONGLONG>(mbi.BaseAddress);
mbi64.AllocationBase = reinterpret_cast<ULONGLONG>(mbi.AllocationBase);
mbi64.AllocationProtect = mbi.AllocationProtect;
mbi64.RegionSize = mbi.RegionSize;
mbi64.State = mbi.State;
mbi64.Protect = mbi.Protect;
mbi64.Type = mbi.Type;
return mbi64;
}
// NtQueryObject with a retry for size mismatch as well as a minimum size to
// retrieve (and expect).
std::unique_ptr<uint8_t[]> QueryObject(
HANDLE handle,
OBJECT_INFORMATION_CLASS object_information_class,
ULONG minimum_size) {
ULONG size = minimum_size;
ULONG return_length;
std::unique_ptr<uint8_t[]> buffer(new uint8_t[size]);
NTSTATUS status = crashpad::NtQueryObject(
handle, object_information_class, buffer.get(), size, &return_length);
if (status == STATUS_INFO_LENGTH_MISMATCH) {
DCHECK_GT(return_length, size);
size = return_length;
// Free the old buffer before attempting to allocate a new one.
buffer.reset();
buffer.reset(new uint8_t[size]);
status = crashpad::NtQueryObject(
handle, object_information_class, buffer.get(), size, &return_length);
}
if (!NT_SUCCESS(status)) {
NTSTATUS_LOG(ERROR, status) << "NtQueryObject";
return nullptr;
}
DCHECK_LE(return_length, size);
DCHECK_GE(return_length, minimum_size);
return buffer;
}
} // namespace
template <class Traits>
bool GetProcessBasicInformation(HANDLE process,
bool is_wow64,
ProcessInfo* process_info,
WinVMAddress* peb_address,
WinVMSize* peb_size) {
ULONG bytes_returned;
process_types::PROCESS_BASIC_INFORMATION<Traits> process_basic_information;
NTSTATUS status =
crashpad::NtQueryInformationProcess(process,
ProcessBasicInformation,
&process_basic_information,
sizeof(process_basic_information),
&bytes_returned);
if (!NT_SUCCESS(status)) {
NTSTATUS_LOG(ERROR, status) << "NtQueryInformationProcess";
return false;
}
if (bytes_returned != sizeof(process_basic_information)) {
LOG(ERROR) << "NtQueryInformationProcess incorrect size";
return false;
}
// API functions (e.g. OpenProcess) take only a DWORD, so there's no sense in
// maintaining the top bits.
process_info->process_id_ =
static_cast<DWORD>(process_basic_information.UniqueProcessId);
process_info->inherited_from_process_id_ = static_cast<DWORD>(
process_basic_information.InheritedFromUniqueProcessId);
// We now want to read the PEB to gather the rest of our information. The
// PebBaseAddress as returned above is what we want for 64-on-64 and 32-on-32,
// but for Wow64, we want to read the 32 bit PEB (a Wow64 process has both).
// The address of this is found by a second call to NtQueryInformationProcess.
if (!is_wow64) {
*peb_address = process_basic_information.PebBaseAddress;
*peb_size = sizeof(process_types::PEB<Traits>);
} else {
ULONG_PTR wow64_peb_address;
status = crashpad::NtQueryInformationProcess(process,
ProcessWow64Information,
&wow64_peb_address,
sizeof(wow64_peb_address),
&bytes_returned);
if (!NT_SUCCESS(status)) {
NTSTATUS_LOG(ERROR, status) << "NtQueryInformationProcess";
return false;
}
if (bytes_returned != sizeof(wow64_peb_address)) {
LOG(ERROR) << "NtQueryInformationProcess incorrect size";
return false;
}
*peb_address = wow64_peb_address;
*peb_size = sizeof(process_types::PEB<process_types::internal::Traits32>);
}
return true;
}
template <class Traits>
bool ReadProcessData(HANDLE process,
WinVMAddress peb_address_vmaddr,
ProcessInfo* process_info) {
typename Traits::Pointer peb_address;
if (!AssignIfInRange(&peb_address, peb_address_vmaddr)) {
LOG(ERROR) << base::StringPrintf("peb address 0x%llx out of range",
peb_address_vmaddr);
return false;
}
// Try to read the process environment block.
process_types::PEB<Traits> peb;
if (!ReadStruct(process, peb_address, &peb))
return false;
process_types::RTL_USER_PROCESS_PARAMETERS<Traits> process_parameters;
if (!ReadStruct(process, peb.ProcessParameters, &process_parameters))
return false;
if (!ReadUnicodeString(process,
process_parameters.CommandLine,
&process_info->command_line_)) {
return false;
}
process_types::PEB_LDR_DATA<Traits> peb_ldr_data;
if (!ReadStruct(process, peb.Ldr, &peb_ldr_data))
return false;
process_types::LDR_DATA_TABLE_ENTRY<Traits> ldr_data_table_entry;
ProcessInfo::Module module;
// Walk the PEB LDR structure (doubly-linked list) to get the list of loaded
// modules. We use this method rather than EnumProcessModules to get the
// modules in load order rather than memory order. Notably, this includes the
// main executable as the first element.
typename Traits::Pointer last = peb_ldr_data.InLoadOrderModuleList.Blink;
for (typename Traits::Pointer cur = peb_ldr_data.InLoadOrderModuleList.Flink;;
cur = ldr_data_table_entry.InLoadOrderLinks.Flink) {
// |cur| is the pointer to the LIST_ENTRY embedded in the
// LDR_DATA_TABLE_ENTRY, in the target process's address space. So we need
// to read from the target, and also offset back to the beginning of the
// structure.
if (!ReadStruct(process,
static_cast<WinVMAddress>(cur) -
offsetof(process_types::LDR_DATA_TABLE_ENTRY<Traits>,
InLoadOrderLinks),
&ldr_data_table_entry)) {
break;
}
// TODO(scottmg): Capture Checksum, etc. too?
if (!ReadUnicodeString(
process, ldr_data_table_entry.FullDllName, &module.name)) {
module.name = L"???";
}
module.dll_base = ldr_data_table_entry.DllBase;
module.size = ldr_data_table_entry.SizeOfImage;
module.timestamp = ldr_data_table_entry.TimeDateStamp;
process_info->modules_.push_back(module);
if (cur == last)
break;
}
return true;
}
bool ReadMemoryInfo(HANDLE process, bool is_64_bit, ProcessInfo* process_info) {
DCHECK(process_info->memory_info_.empty());
constexpr WinVMAddress min_address = 0;
// We can't use GetSystemInfo() to get the address space range for another
// process. VirtualQueryEx() will fail with ERROR_INVALID_PARAMETER if the
// address is above the highest memory address accessible to the process, so
// we just probe the entire potential range (2^32 for x86, or 2^64 for x64).
const WinVMAddress max_address = is_64_bit
? std::numeric_limits<uint64_t>::max()
: std::numeric_limits<uint32_t>::max();
MEMORY_BASIC_INFORMATION memory_basic_information;
for (WinVMAddress address = min_address; address <= max_address;
address += memory_basic_information.RegionSize) {
size_t result = VirtualQueryEx(process,
reinterpret_cast<void*>(address),
&memory_basic_information,
sizeof(memory_basic_information));
if (result == 0) {
if (GetLastError() == ERROR_INVALID_PARAMETER)
break;
PLOG(ERROR) << "VirtualQueryEx";
return false;
}
process_info->memory_info_.push_back(
MemoryBasicInformationToMemoryBasicInformation64(
memory_basic_information));
if (memory_basic_information.RegionSize == 0) {
LOG(ERROR) << "RegionSize == 0";
return false;
}
}
return true;
}
std::vector<ProcessInfo::Handle> ProcessInfo::BuildHandleVector(
HANDLE process) const {
ULONG buffer_size = 2 * 1024 * 1024;
// Typically if the buffer were too small, STATUS_INFO_LENGTH_MISMATCH would
// return the correct size in the final argument, but it does not for
// SystemExtendedHandleInformation, so we loop and attempt larger sizes.
NTSTATUS status;
ULONG returned_length;
UniqueMallocPtr buffer;
for (int tries = 0; tries < 5; ++tries) {
buffer.reset();
buffer = UncheckedAllocate(buffer_size);
if (!buffer) {
LOG(ERROR) << "UncheckedAllocate";
return std::vector<Handle>();
}
status = crashpad::NtQuerySystemInformation(
static_cast<SYSTEM_INFORMATION_CLASS>(SystemExtendedHandleInformation),
buffer.get(),
buffer_size,
&returned_length);
if (NT_SUCCESS(status) || status != STATUS_INFO_LENGTH_MISMATCH)
break;
buffer_size *= 2;
}
if (!NT_SUCCESS(status)) {
NTSTATUS_LOG(ERROR, status)
<< "NtQuerySystemInformation SystemExtendedHandleInformation";
return std::vector<Handle>();
}
const auto& system_handle_information_ex =
*reinterpret_cast<process_types::SYSTEM_HANDLE_INFORMATION_EX*>(
buffer.get());
DCHECK_LE(offsetof(process_types::SYSTEM_HANDLE_INFORMATION_EX, Handles) +
system_handle_information_ex.NumberOfHandles *
sizeof(system_handle_information_ex.Handles[0]),
returned_length);
std::vector<Handle> handles;
for (size_t i = 0; i < system_handle_information_ex.NumberOfHandles; ++i) {
const auto& handle = system_handle_information_ex.Handles[i];
if (handle.UniqueProcessId != process_id_)
continue;
Handle result_handle;
result_handle.handle = HandleToInt(handle.HandleValue);
result_handle.attributes = handle.HandleAttributes;
result_handle.granted_access = handle.GrantedAccess;
// TODO(scottmg): Could special case for self.
HANDLE dup_handle;
if (DuplicateHandle(process,
handle.HandleValue,
GetCurrentProcess(),
&dup_handle,
0,
false,
DUPLICATE_SAME_ACCESS)) {
// Some handles cannot be duplicated, for example, handles of type
// EtwRegistration. If we fail to duplicate, then we can't gather any more
// information, but include the information that we do have already.
ScopedKernelHANDLE scoped_dup_handle(dup_handle);
std::unique_ptr<uint8_t[]> object_basic_information_buffer =
QueryObject(dup_handle,
ObjectBasicInformation,
sizeof(PUBLIC_OBJECT_BASIC_INFORMATION));
if (object_basic_information_buffer) {
PUBLIC_OBJECT_BASIC_INFORMATION* object_basic_information =
reinterpret_cast<PUBLIC_OBJECT_BASIC_INFORMATION*>(
object_basic_information_buffer.get());
// The Attributes and GrantedAccess sometimes differ slightly between
// the data retrieved in SYSTEM_HANDLE_INFORMATION_EX and
// PUBLIC_OBJECT_TYPE_INFORMATION. We prefer the values in
// SYSTEM_HANDLE_INFORMATION_EX because they were retrieved from the
// target process, rather than on the duplicated handle, so don't use
// them here.
// Subtract one to account for our DuplicateHandle() and another for
// NtQueryObject() while the query was being executed.
DCHECK_GT(object_basic_information->PointerCount, 2u);
result_handle.pointer_count =
object_basic_information->PointerCount - 2;
// Subtract one to account for our DuplicateHandle().
DCHECK_GT(object_basic_information->HandleCount, 1u);
result_handle.handle_count = object_basic_information->HandleCount - 1;
}
std::unique_ptr<uint8_t[]> object_type_information_buffer =
QueryObject(dup_handle,
ObjectTypeInformation,
sizeof(PUBLIC_OBJECT_TYPE_INFORMATION));
if (object_type_information_buffer) {
PUBLIC_OBJECT_TYPE_INFORMATION* object_type_information =
reinterpret_cast<PUBLIC_OBJECT_TYPE_INFORMATION*>(
object_type_information_buffer.get());
DCHECK_EQ(object_type_information->TypeName.Length %
sizeof(result_handle.type_name[0]),
0u);
result_handle.type_name =
std::wstring(object_type_information->TypeName.Buffer,
object_type_information->TypeName.Length /
sizeof(result_handle.type_name[0]));
}
}
handles.push_back(result_handle);
}
return handles;
}
ProcessInfo::Module::Module() : name(), dll_base(0), size(0), timestamp() {
}
ProcessInfo::Module::~Module() {
}
ProcessInfo::Handle::Handle()
: type_name(),
handle(0),
attributes(0),
granted_access(0),
pointer_count(0),
handle_count(0) {
}
ProcessInfo::Handle::~Handle() {
}
ProcessInfo::ProcessInfo()
: process_id_(),
inherited_from_process_id_(),
process_(),
command_line_(),
peb_address_(0),
peb_size_(0),
modules_(),
memory_info_(),
handles_(),
is_64_bit_(false),
is_wow64_(false),
initialized_() {
}
ProcessInfo::~ProcessInfo() {
}
bool ProcessInfo::Initialize(HANDLE process) {
INITIALIZATION_STATE_SET_INITIALIZING(initialized_);
process_ = process;
is_wow64_ = IsProcessWow64(process);
if (is_wow64_) {
// If it's WoW64, then it's 32-on-64.
is_64_bit_ = false;
} else {
// Otherwise, it's either 32 on 32, or 64 on 64. Use GetSystemInfo() to
// distinguish between these two cases.
SYSTEM_INFO system_info;
GetSystemInfo(&system_info);
#if defined(ARCH_CPU_X86_FAMILY)
constexpr uint16_t kNative64BitArchitecture = PROCESSOR_ARCHITECTURE_AMD64;
#elif defined(ARCH_CPU_ARM_FAMILY)
constexpr uint16_t kNative64BitArchitecture = PROCESSOR_ARCHITECTURE_ARM64;
#endif
is_64_bit_ = system_info.wProcessorArchitecture == kNative64BitArchitecture;
}
#if defined(ARCH_CPU_32_BITS)
if (is_64_bit_) {
LOG(ERROR) << "Reading x64 process from x86 process not supported";
return false;
}
#endif // ARCH_CPU_32_BITS
#if defined(ARCH_CPU_64_BITS)
bool result = GetProcessBasicInformation<process_types::internal::Traits64>(
process, is_wow64_, this, &peb_address_, &peb_size_);
#else
bool result = GetProcessBasicInformation<process_types::internal::Traits32>(
process, false, this, &peb_address_, &peb_size_);
#endif // ARCH_CPU_64_BITS
if (!result) {
LOG(ERROR) << "GetProcessBasicInformation failed";
return false;
}
result = is_64_bit_ ? ReadProcessData<process_types::internal::Traits64>(
process, peb_address_, this)
: ReadProcessData<process_types::internal::Traits32>(
process, peb_address_, this);
if (!result) {
LOG(ERROR) << "ReadProcessData failed";
return false;
}
if (!ReadMemoryInfo(process, is_64_bit_, this)) {
LOG(ERROR) << "ReadMemoryInfo failed";
return false;
}
INITIALIZATION_STATE_SET_VALID(initialized_);
return true;
}
bool ProcessInfo::Is64Bit() const {
INITIALIZATION_STATE_DCHECK_VALID(initialized_);
return is_64_bit_;
}
bool ProcessInfo::IsWow64() const {
INITIALIZATION_STATE_DCHECK_VALID(initialized_);
return is_wow64_;
}
crashpad::ProcessID ProcessInfo::ProcessID() const {
INITIALIZATION_STATE_DCHECK_VALID(initialized_);
return process_id_;
}
crashpad::ProcessID ProcessInfo::ParentProcessID() const {
INITIALIZATION_STATE_DCHECK_VALID(initialized_);
return inherited_from_process_id_;
}
bool ProcessInfo::CommandLine(std::wstring* command_line) const {
INITIALIZATION_STATE_DCHECK_VALID(initialized_);
*command_line = command_line_;
return true;
}
void ProcessInfo::Peb(WinVMAddress* peb_address, WinVMSize* peb_size) const {
*peb_address = peb_address_;
*peb_size = peb_size_;
}
bool ProcessInfo::Modules(std::vector<Module>* modules) const {
INITIALIZATION_STATE_DCHECK_VALID(initialized_);
*modules = modules_;
return true;
}
const ProcessInfo::MemoryBasicInformation64Vector& ProcessInfo::MemoryInfo()
const {
INITIALIZATION_STATE_DCHECK_VALID(initialized_);
return memory_info_;
}
std::vector<CheckedRange<WinVMAddress, WinVMSize>>
ProcessInfo::GetReadableRanges(
const CheckedRange<WinVMAddress, WinVMSize>& range) const {
return GetReadableRangesOfMemoryMap(range, MemoryInfo());
}
bool ProcessInfo::LoggingRangeIsFullyReadable(
const CheckedRange<WinVMAddress, WinVMSize>& range) const {
const auto ranges = GetReadableRanges(range);
if (ranges.empty()) {
LOG(ERROR) << base::StringPrintf(
"range at 0x%llx, size 0x%llx fully unreadable",
range.base(),
range.size());
return false;
}
if (ranges.size() != 1 ||
ranges[0].base() != range.base() || ranges[0].size() != range.size()) {
LOG(ERROR) << base::StringPrintf(
"range at 0x%llx, size 0x%llx partially unreadable",
range.base(),
range.size());
return false;
}
return true;
}
const std::vector<ProcessInfo::Handle>& ProcessInfo::Handles() const {
INITIALIZATION_STATE_DCHECK_VALID(initialized_);
if (handles_.empty())
handles_ = BuildHandleVector(process_);
return handles_;
}
std::vector<CheckedRange<WinVMAddress, WinVMSize>> GetReadableRangesOfMemoryMap(
const CheckedRange<WinVMAddress, WinVMSize>& range,
const ProcessInfo::MemoryBasicInformation64Vector& memory_info) {
using Range = CheckedRange<WinVMAddress, WinVMSize>;
// Constructing Ranges and using OverlapsRange() is very, very slow in Debug
// builds, so do a manual check in this loop. The ranges are still validated
// by a CheckedRange before being returned.
WinVMAddress range_base = range.base();
WinVMAddress range_end = range.end();
// Find all the ranges that overlap the target range, maintaining their order.
ProcessInfo::MemoryBasicInformation64Vector overlapping;
const size_t size = memory_info.size();
// This loop is written in an ugly fashion to make Debug performance
// reasonable.
const MEMORY_BASIC_INFORMATION64* begin = &memory_info[0];
for (size_t i = 0; i < size; ++i) {
const MEMORY_BASIC_INFORMATION64& mi = *(begin + i);
static_assert(std::is_same<decltype(mi.BaseAddress), WinVMAddress>::value,
"expected range address to be WinVMAddress");
static_assert(std::is_same<decltype(mi.RegionSize), WinVMSize>::value,
"expected range size to be WinVMSize");
WinVMAddress mi_end = mi.BaseAddress + mi.RegionSize;
if (range_base < mi_end && mi.BaseAddress < range_end)
overlapping.push_back(mi);
}
if (overlapping.empty())
return std::vector<Range>();
// For the first and last, trim to the boundary of the incoming range.
MEMORY_BASIC_INFORMATION64& front = overlapping.front();
WinVMAddress original_front_base_address = front.BaseAddress;
front.BaseAddress = std::max(front.BaseAddress, range.base());
front.RegionSize =
(original_front_base_address + front.RegionSize) - front.BaseAddress;
MEMORY_BASIC_INFORMATION64& back = overlapping.back();
WinVMAddress back_end = back.BaseAddress + back.RegionSize;
back.RegionSize = std::min(range.end(), back_end) - back.BaseAddress;
// Discard all non-accessible.
overlapping.erase(std::remove_if(overlapping.begin(),
overlapping.end(),
[](const MEMORY_BASIC_INFORMATION64& mbi) {
return !RegionIsAccessible(mbi);
}),
overlapping.end());
if (overlapping.empty())
return std::vector<Range>();
// Convert to return type.
std::vector<Range> as_ranges;
for (const auto& mi : overlapping) {
as_ranges.push_back(Range(mi.BaseAddress, mi.RegionSize));
DCHECK(as_ranges.back().IsValid());
}
// Coalesce remaining regions.
std::vector<Range> result;
result.push_back(as_ranges[0]);
for (size_t i = 1; i < as_ranges.size(); ++i) {
if (result.back().end() == as_ranges[i].base()) {
result.back().SetRange(result.back().base(),
result.back().size() + as_ranges[i].size());
} else {
result.push_back(as_ranges[i]);
}
DCHECK(result.back().IsValid());
}
return result;
}
} // namespace crashpad