chromium/base/debug/gdi_debug_util_win.cc

// Copyright 2014 The Chromium Authors
// 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 <windows.h>

#include <TlHelp32.h>
#include <psapi.h>
#include <stddef.h>
#include <winternl.h>

#include <algorithm>
#include <cmath>
#include <optional>

#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"

namespace {

// A partial PEB up until GdiSharedHandleTable.
// Derived from the ntdll symbols (ntdll!_PEB).
template <typename PointerType>
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 <typename PointerType>
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 <typename ProcessType>
std::vector<GdiTableEntry<typename ProcessType::NativePointerType>>
GetGdiTableEntries(const base::Process& process) {
  using GdiTableEntryVector =
      std::vector<GdiTableEntry<typename ProcessType::NativePointerType>>;
  HMODULE ntdll = GetModuleHandle(L"ntdll.dll");
  if (!ntdll)
    return GdiTableEntryVector();

  static auto query_information_process_func =
      reinterpret_cast<typename ProcessType::QueryInformationProcessFunc*>(
          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<typename ProcessType::ReadVirtualMemoryFunc*>(
          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<typename ProcessType::NativePointerType> 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 <typename PointerType>
base::debug::GdiHandleCounts CountHandleTypesFromTable(
    DWORD pid,
    const std::vector<GdiTableEntry<PointerType>>& 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 <typename ProcessType>
std::optional<base::debug::GdiHandleCounts> CollectGdiHandleCountsImpl(
    DWORD pid) {
  base::Process process = base::Process::OpenWithExtraPrivileges(pid);
  if (!process.IsValid())
    return std::nullopt;

  std::vector<GdiTableEntry<typename ProcessType::NativePointerType>>
      gdi_entries = GetGdiTableEntries<ProcessType>(process);
  return CountHandleTypesFromTable(pid, gdi_entries);
}

// Returns the GDI Handle counts from the GDI Shared handle table. Empty on
// failure.
std::optional<base::debug::GdiHandleCounts> CollectGdiHandleCounts(DWORD pid) {
  if (base::win::OSInfo::GetInstance()->IsWowX86OnAMD64()) {
    return CollectGdiHandleCountsImpl<WowProcessTypes>(pid);
  }

  return CollectGdiHandleCountsImpl<RegularProcessTypes>(pid);
}

constexpr size_t kLotsOfMemory = 1500 * 1024 * 1024;  // 1.5GB

NOINLINE HANDLE GetToolhelpSnapshot() {
  HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
  CHECK_NE(INVALID_HANDLE_VALUE, snapshot);
  return snapshot;
}

NOINLINE void GetFirstProcess(HANDLE snapshot, PROCESSENTRY32* proc_entry) {
  proc_entry->dwSize = sizeof(PROCESSENTRY32);
  CHECK(Process32First(snapshot, proc_entry));
}

NOINLINE void 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);
}

NOINLINE void CrashIfPagefileUsageTooLarge(
    const PROCESS_MEMORY_COUNTERS_EX& pmc) {
  CHECK_LE(pmc.PagefileUsage, kLotsOfMemory);
}

NOINLINE void CrashIfPrivateUsageTooLarge(
    const PROCESS_MEMORY_COUNTERS_EX& pmc) {
  CHECK_LE(pmc.PrivateUsage, kLotsOfMemory);
}

NOINLINE void 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<BITMAPINFO*>(&header), 0,
                       &small_data, shared_section, 0);
  CHECK(small_bitmap != nullptr);
  DeleteObject(small_bitmap);
}

NOINLINE void GetProcessMemoryInfo(PROCESS_MEMORY_COUNTERS_EX* pmc) {
  pmc->cb = sizeof(*pmc);
  CHECK(GetProcessMemoryInfo(GetCurrentProcess(),
                             reinterpret_cast<PROCESS_MEMORY_COUNTERS*>(pmc),
                             sizeof(*pmc)));
}

NOINLINE DWORD 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);

  std::optional<GdiHandleCounts> 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() {
  std::optional<GdiHandleCounts> handle_counts =
      CollectGdiHandleCounts(GetCurrentProcessId());
  DCHECK(handle_counts.has_value());
  return handle_counts.value_or(GdiHandleCounts());
}

}  // namespace debug
}  // namespace base