// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "base/debug/gdi_debug_util_win.h" #include #include #include #include #include #include #include #include "base/debug/alias.h" #include "base/logging.h" #include "base/process/process.h" #include "base/win/scoped_handle.h" #include "base/win/win_util.h" #include "base/win/windows_version.h" #include "third_party/abseil-cpp/absl/types/optional.h" namespace { // A partial PEB up until GdiSharedHandleTable. // Derived from the ntdll symbols (ntdll!_PEB). template struct PartialWinPeb { unsigned char InheritedAddressSpace; unsigned char ReadImageFileExecOptions; unsigned char BeingDebugged; unsigned char ImageUsesLargePages : 1; unsigned char IsProtectedProcess : 1; unsigned char IsLegacyProcess : 1; unsigned char IsImageDynamicallyRelocated : 1; unsigned char SkipPatchingUser32Forwarders : 1; unsigned char IsAppContainer : 1; unsigned char IsProtectedProcessLight : 1; unsigned char IsLongPathAwareProcess : 1; PointerType Mutant; PointerType ImageBaseAddress; PointerType Ldr; PointerType ProcessParamters; PointerType SubSystemData; PointerType ProcessHeap; PointerType FastPebLock; PointerType AtlThunkSListPtr; PointerType IFEOKey; uint32_t ProcessInJob : 1; uint32_t ProcessInitializing : 1; uint32_t ProcessUsingVEH : 1; uint32_t ProcessUsingVCH : 1; uint32_t ProcessUsingFTH : 1; uint32_t ProcessPreviouslyThrottled : 1; uint32_t ProcessCurrentlyThrottled : 1; uint32_t ProcessImagesHotPatched : 1; PointerType KernelCallbackTable; uint32_t SystemReserved; uint32_t AtlThunkSListPtr32; PointerType ApiSetMap; uint32_t TlsExpansionCounter; PointerType TlsBitmap; uint32_t TlsBitmapBits[2]; PointerType ReadOnlySharedMemoryBase; PointerType HotpatchInformation; PointerType ReadOnlyStaticServerData; PointerType AnsiCodePageData; PointerType OemCodePageData; PointerType UnicodeCaseTableData; uint32_t NumberOfProcessors; uint32_t NtGlobalFlag; uint64_t CriticalSectionTimeout; PointerType HeapSegmentReserve; PointerType HeapSegmentCommit; PointerType HeapDeCommitTotalFreeThreshold; PointerType HeapDeCommitFreeBlockThreshold; uint32_t NumberOfHeaps; uint32_t MaximumNumberOfHeaps; PointerType ProcessHeaps; PointerType GdiSharedHandleTable; }; // Found from // https://stackoverflow.com/questions/13905661/how-to-get-list-of-gdi-handles. enum GdiHandleType : USHORT { kDC = 1, kRegion = 4, kBitmap = 5, kPalette = 8, kFont = 10, kBrush = 16, kPen = 48, }; // Adapted from GDICELL. template struct GdiTableEntry { PointerType pKernelAddress; USHORT wProcessId; USHORT wCount; USHORT wUpper; GdiHandleType wType; PointerType pUserAddress; }; // Types and names used for regular processes. struct RegularProcessTypes { using QueryInformationProcessFunc = decltype(NtQueryInformationProcess); static const char* query_information_process_name; // PROCESS_BASIC_INFORMATION struct ProcessBasicInformation { PVOID Reserved1; PVOID PebBaseAddress; PVOID Reserved2[2]; ULONG_PTR UniqueProcessId; PVOID Reserved3; }; using ReadVirtualMemoryFunc = NTSTATUS NTAPI(IN HANDLE ProcessHandle, IN PVOID BaseAddress, OUT PVOID Buffer, IN SIZE_T Size, OUT PSIZE_T NumberOfBytesRead); static const char* read_virtual_memory_func_name; using NativePointerType = PVOID; }; // static const char* RegularProcessTypes::query_information_process_name = "NtQueryInformationProcess"; // static const char* RegularProcessTypes::read_virtual_memory_func_name = "NtReadVirtualMemory"; // Types and names used for WOW based processes. struct WowProcessTypes { // http://crbug.com/972185: Clang doesn't handle PVOID64 correctly, so we use // uint64_t as a substitute. // NtWow64QueryInformationProcess64 and NtQueryInformationProcess share the // same signature. using QueryInformationProcessFunc = decltype(NtQueryInformationProcess); static const char* query_information_process_name; // PROCESS_BASIC_INFORMATION_WOW64 struct ProcessBasicInformation { PVOID Reserved1[2]; uint64_t PebBaseAddress; PVOID Reserved2[4]; ULONG_PTR UniqueProcessId[2]; PVOID Reserved3[2]; }; using ReadVirtualMemoryFunc = NTSTATUS NTAPI(IN HANDLE ProcessHandle, IN uint64_t BaseAddress, OUT PVOID Buffer, IN ULONG64 Size, OUT PULONG64 NumberOfBytesRead); static const char* read_virtual_memory_func_name; using NativePointerType = uint64_t; }; // static const char* WowProcessTypes::query_information_process_name = "NtWow64QueryInformationProcess64"; // static const char* WowProcessTypes::read_virtual_memory_func_name = "NtWow64ReadVirtualMemory64"; // To prevent from having to write a regular and WOW codepaths that do the same // thing with different structures and functions, GetGdiTableEntries is // templated to expect either RegularProcessTypes or WowProcessTypes. template std::vector> GetGdiTableEntries(const base::Process& process) { using GdiTableEntryVector = std::vector>; HMODULE ntdll = GetModuleHandle(L"ntdll.dll"); if (!ntdll) return GdiTableEntryVector(); static auto query_information_process_func = reinterpret_cast( GetProcAddress(ntdll, ProcessType::query_information_process_name)); if (!query_information_process_func) { LOG(ERROR) << ProcessType::query_information_process_name << " Missing"; return GdiTableEntryVector(); } typename ProcessType::ProcessBasicInformation basic_info; NTSTATUS result = query_information_process_func(process.Handle(), ProcessBasicInformation, &basic_info, sizeof(basic_info), nullptr); if (result != 0) { LOG(ERROR) << ProcessType::query_information_process_name << " Failed " << std::hex << result; return GdiTableEntryVector(); } static auto read_virtual_mem_func = reinterpret_cast( GetProcAddress(ntdll, ProcessType::read_virtual_memory_func_name)); if (!read_virtual_mem_func) { LOG(ERROR) << ProcessType::read_virtual_memory_func_name << " Missing"; return GdiTableEntryVector(); } PartialWinPeb peb; result = read_virtual_mem_func(process.Handle(), basic_info.PebBaseAddress, &peb, sizeof(peb), nullptr); if (result != 0) { LOG(ERROR) << ProcessType::read_virtual_memory_func_name << " PEB Failed " << std::hex << result; return GdiTableEntryVector(); } // Estimated size derived from address space allocation of the table: // Windows 10 // 32-bit Size: 1052672 bytes // 64-bit Size: 1576960 bytes // sizeof(GdiTableEntry) // 32-bit: 16 bytes // 64-bit: 24 bytes // Entry Count // 32-bit: 65792 // 64-bit: 65706ish // So we'll take a look at 65536 entries since that's the maximum handle count. constexpr int kGdiTableEntryCount = 65536; GdiTableEntryVector entries; entries.resize(kGdiTableEntryCount); result = read_virtual_mem_func( process.Handle(), peb.GdiSharedHandleTable, &entries[0], sizeof(typename GdiTableEntryVector::value_type) * entries.size(), nullptr); if (result != 0) { LOG(ERROR) << ProcessType::read_virtual_memory_func_name << " GDI Handle Table Failed " << std::hex << result; return GdiTableEntryVector(); } return entries; } // Iterates through |gdi_table| and finds handles that belong to |pid|, // incrementing the appropriate fields in |base::debug::GdiHandleCounts|. template base::debug::GdiHandleCounts CountHandleTypesFromTable( DWORD pid, const std::vector>& gdi_table) { base::debug::GdiHandleCounts counts{}; for (const auto& entry : gdi_table) { if (entry.wProcessId != pid) continue; switch (entry.wType & 0x7F) { case GdiHandleType::kDC: ++counts.dcs; break; case GdiHandleType::kRegion: ++counts.regions; break; case GdiHandleType::kBitmap: ++counts.bitmaps; break; case GdiHandleType::kPalette: ++counts.palettes; break; case GdiHandleType::kFont: ++counts.fonts; break; case GdiHandleType::kBrush: ++counts.brushes; break; case GdiHandleType::kPen: ++counts.pens; break; default: ++counts.unknown; break; } } counts.total_tracked = counts.dcs + counts.regions + counts.bitmaps + counts.palettes + counts.fonts + counts.brushes + counts.pens + counts.unknown; return counts; } template absl::optional CollectGdiHandleCountsImpl( DWORD pid) { base::Process process = base::Process::OpenWithExtraPrivileges(pid); if (!process.IsValid()) return absl::nullopt; std::vector> gdi_entries = GetGdiTableEntries(process); return CountHandleTypesFromTable(pid, gdi_entries); } // Returns the GDI Handle counts from the GDI Shared handle table. Empty on // failure. absl::optional CollectGdiHandleCounts(DWORD pid) { if (base::win::OSInfo::GetInstance()->IsWowX86OnAMD64()) { return CollectGdiHandleCountsImpl(pid); } return CollectGdiHandleCountsImpl(pid); } constexpr size_t kLotsOfMemory = 1500 * 1024 * 1024; // 1.5GB HANDLE NOINLINE GetToolhelpSnapshot() { HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); CHECK_NE(INVALID_HANDLE_VALUE, snapshot); return snapshot; } void NOINLINE GetFirstProcess(HANDLE snapshot, PROCESSENTRY32* proc_entry) { proc_entry->dwSize = sizeof(PROCESSENTRY32); CHECK(Process32First(snapshot, proc_entry)); } void NOINLINE CrashIfExcessiveHandles(DWORD num_gdi_handles) { // By default, Windows 10 allows a max of 10,000 GDI handles per process. // Number found by inspecting // // HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\ // CurrentVersion\Windows\GDIProcessHandleQuota // // on a Windows 10 laptop. static constexpr DWORD kLotsOfHandles = 9990; CHECK_LE(num_gdi_handles, kLotsOfHandles); } void NOINLINE CrashIfPagefileUsageTooLarge(const PROCESS_MEMORY_COUNTERS_EX& pmc) { CHECK_LE(pmc.PagefileUsage, kLotsOfMemory); } void NOINLINE CrashIfPrivateUsageTooLarge(const PROCESS_MEMORY_COUNTERS_EX& pmc) { CHECK_LE(pmc.PrivateUsage, kLotsOfMemory); } void NOINLINE CrashIfCannotAllocateSmallBitmap(BITMAPINFOHEADER* header, HANDLE shared_section) { void* small_data = nullptr; base::debug::Alias(&small_data); header->biWidth = 5; header->biHeight = -5; HBITMAP small_bitmap = CreateDIBSection(nullptr, reinterpret_cast(&header), 0, &small_data, shared_section, 0); CHECK(small_bitmap != nullptr); DeleteObject(small_bitmap); } void NOINLINE GetProcessMemoryInfo(PROCESS_MEMORY_COUNTERS_EX* pmc) { pmc->cb = sizeof(*pmc); CHECK(GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast(pmc), sizeof(*pmc))); } DWORD NOINLINE GetNumGdiHandles() { DWORD num_gdi_handles = GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS); if (num_gdi_handles == 0) { DWORD get_gui_resources_error = GetLastError(); base::debug::Alias(&get_gui_resources_error); CHECK(false); } return num_gdi_handles; } void CollectChildGDIUsageAndDie(DWORD parent_pid) { HANDLE snapshot = GetToolhelpSnapshot(); int total_process_count = 0; base::debug::Alias(&total_process_count); DWORD total_peak_gdi_count = 0; base::debug::Alias(&total_peak_gdi_count); DWORD total_gdi_count = 0; base::debug::Alias(&total_gdi_count); DWORD total_user_count = 0; base::debug::Alias(&total_user_count); int child_count = 0; base::debug::Alias(&child_count); DWORD peak_gdi_count = 0; base::debug::Alias(&peak_gdi_count); DWORD sum_gdi_count = 0; base::debug::Alias(&sum_gdi_count); DWORD sum_user_count = 0; base::debug::Alias(&sum_user_count); PROCESSENTRY32 proc_entry = {}; GetFirstProcess(snapshot, &proc_entry); do { base::win::ScopedHandle process( OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, proc_entry.th32ProcessID)); if (!process.is_valid()) continue; DWORD num_gdi_handles = GetGuiResources(process.get(), GR_GDIOBJECTS); DWORD num_user_handles = GetGuiResources(process.get(), GR_USEROBJECTS); // Compute sum and peak counts for all processes. ++total_process_count; total_user_count += num_user_handles; total_gdi_count += num_gdi_handles; total_peak_gdi_count = std::max(total_peak_gdi_count, num_gdi_handles); if (parent_pid != proc_entry.th32ParentProcessID) continue; // Compute sum and peak counts for child processes. ++child_count; sum_user_count += num_user_handles; sum_gdi_count += num_gdi_handles; peak_gdi_count = std::max(peak_gdi_count, num_gdi_handles); } while (Process32Next(snapshot, &proc_entry)); CloseHandle(snapshot); CHECK(false); } } // namespace namespace base { namespace debug { void CollectGDIUsageAndDie(BITMAPINFOHEADER* header, HANDLE shared_section) { // Make sure parameters are saved in the minidump. DWORD last_error = GetLastError(); bool is_gdi_available = base::win::IsUser32AndGdi32Available(); LONG width = header ? header->biWidth : 0; LONG height = header ? header->biHeight : 0; base::debug::Alias(&last_error); base::debug::Alias(&is_gdi_available); base::debug::Alias(&width); base::debug::Alias(&height); base::debug::Alias(&shared_section); DWORD num_user_handles = GetGuiResources(GetCurrentProcess(), GR_USEROBJECTS); DWORD num_gdi_handles = GetNumGdiHandles(); DWORD peak_gdi_handles = GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS_PEAK); DWORD num_global_gdi_handles = GetGuiResources(GR_GLOBAL, GR_GDIOBJECTS); DWORD num_global_user_handles = GetGuiResources(GR_GLOBAL, GR_USEROBJECTS); base::debug::Alias(&num_gdi_handles); base::debug::Alias(&num_user_handles); base::debug::Alias(&peak_gdi_handles); base::debug::Alias(&num_global_gdi_handles); base::debug::Alias(&num_global_user_handles); absl::optional optional_handle_counts = CollectGdiHandleCounts(GetCurrentProcessId()); bool handle_counts_set = optional_handle_counts.has_value(); GdiHandleCounts handle_counts = optional_handle_counts.value_or(GdiHandleCounts()); int tracked_dcs = handle_counts.dcs; int tracked_regions = handle_counts.regions; int tracked_bitmaps = handle_counts.bitmaps; int tracked_palettes = handle_counts.palettes; int tracked_fonts = handle_counts.fonts; int tracked_brushes = handle_counts.brushes; int tracked_pens = handle_counts.pens; int tracked_unknown_handles = handle_counts.unknown; int tracked_total = handle_counts.total_tracked; base::debug::Alias(&handle_counts_set); base::debug::Alias(&tracked_dcs); base::debug::Alias(&tracked_regions); base::debug::Alias(&tracked_bitmaps); base::debug::Alias(&tracked_palettes); base::debug::Alias(&tracked_fonts); base::debug::Alias(&tracked_brushes); base::debug::Alias(&tracked_pens); base::debug::Alias(&tracked_unknown_handles); base::debug::Alias(&tracked_total); CrashIfExcessiveHandles(num_gdi_handles); PROCESS_MEMORY_COUNTERS_EX pmc; GetProcessMemoryInfo(&pmc); CrashIfPagefileUsageTooLarge(pmc); CrashIfPrivateUsageTooLarge(pmc); if (std::abs(height) * width > 100) { // Huh, that's weird. We don't have crazy handle count, we don't have // ridiculous memory usage. Try to allocate a small bitmap and see if that // fails too. CrashIfCannotAllocateSmallBitmap(header, shared_section); } // Maybe the child processes are the ones leaking GDI or USER resouces. CollectChildGDIUsageAndDie(GetCurrentProcessId()); } GdiHandleCounts GetGDIHandleCountsInCurrentProcessForTesting() { absl::optional handle_counts = CollectGdiHandleCounts(GetCurrentProcessId()); DCHECK(handle_counts.has_value()); return handle_counts.value_or(GdiHandleCounts()); } } // namespace debug } // namespace base