chromium/tools/win/trim_heap/trim_heap.cc

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// This is an experimental tool which will inject a thread into a Chrome
// process (tested on the browser process) and run code to call
// HeapSetInformation with HEAP_OPTIMIZE_RESOURCES_CURRENT_VERSION. This
// tells Windows to trim unnecessary memory from the heaps in that process.
//
// This tool uses sketchy techniques such as copying memory from one
// executable to another (only works if the code is relocatable and has no
// external references), VirtualAllocEx, and CreateRemoteThread. This is not
// for production use.
//
// The bitness of this tool (32/64) must match that of the target process.
// This tool has only been tested on 64-bit processes. This tool only works
// when compiled with optimizations.
//
// Some error handling and resource cleanup is omitted in order to keep things
// simple.

#include <Windows.h>

// Psapi.h must come after Windows.h.
#include <Psapi.h>

#include <inttypes.h>
#include <stdio.h>

#include <vector>

#ifdef _DEBUG
#error This code only works in optimized (release) builds.
// Non-optimized code may include references to global variables. The
// "#pragma clang optimize on/off" directives do not work, by design, in debug
// builds. They can only lower the optimization level, not raise it.
#endif

#define ADDRESS_COOKIE reinterpret_cast<void*>(0x123456789ABCDEF0)

// Function suitable for copying into another process and invoking with
// CreateRemoteThread. The function address is a placeholder.
DWORD WINAPI ShrinkHeapThread(LPVOID) {
  auto pHeapSetInformation =
      reinterpret_cast<decltype(&::HeapSetInformation)>(ADDRESS_COOKIE);
  HEAP_OPTIMIZE_RESOURCES_INFORMATION info = {
      HEAP_OPTIMIZE_RESOURCES_CURRENT_VERSION, 0x0};
  pHeapSetInformation(nullptr, HeapOptimizeResources, &info, sizeof(info));
  return 0;
}

int main(int argc, char* argv[]) {
  const bool verbose = false;

  // Verify that we have the correct signature for ShrinkHeapThread.
  static_assert(
      std::is_same<decltype(ShrinkHeapThread)*, PTHREAD_START_ROUTINE>::value,
      "Callback function is wrong type.");

  // Copy the thread function's memory to a vector.
  std::vector<unsigned char> raw_bytes;
  auto* src = reinterpret_cast<uint8_t*>(&ShrinkHeapThread);
  // Assume that the only 0xc3 byte we will encounter will be the ret
  // instruction.
  uint8_t ret = 0xc3;
  while (*src != ret) {
    raw_bytes.push_back(*src++);
  }
  raw_bytes.push_back(ret);
  if (src[1] != 0xcc) {
    printf("Didn't find int 3 after ret. Exiting.\n");
    return 1;
  }
  // This can trigger if incremental linking is enabled since then the function
  // pointer will be to a JMP stub.
  if (raw_bytes.size() > 1000) {
    printf("Code size is suspiciously large - %zu bytes. Exiting.\n",
           raw_bytes.size());
    return 1;
  }

  // Update the function pointer address in the copy to match the current
  // address of HeapSetInformation. This assumes that the address will be the
  // same in all processes, which should be the case.
  for (auto* scan = &raw_bytes[0]; /**/; ++scan) {
    auto** scan_64 = reinterpret_cast<void**>(scan);
    if (*scan_64 == ADDRESS_COOKIE) {
      auto* pHeapSetInformation = reinterpret_cast<void*>(GetProcAddress(
          GetModuleHandleA("kernel32.dll"), "HeapSetInformation"));
      *scan_64 = pHeapSetInformation;
      if (verbose)
        printf("Found and updated HeapSetInformation.\n");
      break;
    }
  }

  if (argc < 2) {
    printf("Usage: %s PID.\n", argv[0]);
    printf(
        "Injects code into the target process to call HeapSetInformation with "
        "HEAP_OPTIMIZE_RESOURCES_CURRENT_VERSION.\n");
    printf(
        "May need to be run from an administrator command prompt for some "
        "processes.\n");
    return 1;
  }

  // Get the PIDs from the command line.
  for (int i = 1; i < argc; ++i) {
    int PID;
    if (sscanf(argv[i], "%d", &PID) != 1) {
      printf("Error getting PID.\n");
      return 1;
    }

    // Open the process. We'll leak the handle afterwards, but that's okay
    // because this is a short-lived tool.
    HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ |
                                      PROCESS_VM_WRITE | PROCESS_VM_OPERATION |
                                      PROCESS_CREATE_THREAD,
                                  false, PID);
    if (!hProcess) {
      printf("Error from OpenProcess is %lx.\n", GetLastError());
      return 1;
    }

#ifdef _M_X64
    BOOL wow_64_process = FALSE;
    if (!IsWow64Process(hProcess, &wow_64_process) || wow_64_process) {
      printf("Specified process is 32-bit. Code injection will not work.\n");
      return 1;
    }
#else
    // Update this with remote-process bitness tests if x86 works.
#error This code is only tested on x64 and may cause failures on x86.
#endif

    PROCESS_MEMORY_COUNTERS_EX memory_before = {sizeof(memory_before)};
    GetProcessMemoryInfo(
        hProcess, reinterpret_cast<PROCESS_MEMORY_COUNTERS*>(&memory_before),
        sizeof(memory_before));

    // Allocate memory in the other process.
    void* p = VirtualAllocEx(hProcess, nullptr, raw_bytes.size(),
                             MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

    if (verbose)
      printf("Writing %zd bytes to process %d at address 0x%p.\n",
             raw_bytes.size(), PID, p);
    // Write to the remotely allocated memory.
    SIZE_T bytes_written = 0;
    if (!WriteProcessMemory(hProcess, p, &raw_bytes[0], raw_bytes.size(),
                            &bytes_written)) {
      printf("Error is %lx.\n", GetLastError());
      return 1;
    }

    if (verbose)
      printf("Wrote %zd bytes.\n", bytes_written);
    HANDLE hRemoteThread = CreateRemoteThread(
        hProcess, nullptr, 0, reinterpret_cast<LPTHREAD_START_ROUTINE>(p),
        nullptr, 0, nullptr);
    if (!hRemoteThread) {
      printf("Failed to inject thread in process %d. Error code is %lx.\n", PID,
             GetLastError());
      return 1;
    }

    if (verbose)
      printf("Successfully injected thread into process %d.\n", PID);
    WaitForSingleObject(hRemoteThread, INFINITE);
    // Clean up the allocated memory after the thread exits.
    VirtualFreeEx(hProcess, p, 0, MEM_RELEASE);

    PROCESS_MEMORY_COUNTERS_EX memory_after = {sizeof(memory_after)};
    GetProcessMemoryInfo(
        hProcess, reinterpret_cast<PROCESS_MEMORY_COUNTERS*>(&memory_after),
        sizeof(memory_after));

    double MiB = 1024.0 * 1024.0;
    printf(
        "  Commit for process %6d went from %8.3f MiB to %8.3f MiB (%7.3f MiB "
        "savings).\n",
        PID, memory_before.PrivateUsage / MiB, memory_after.PrivateUsage / MiB,
        (memory_before.PrivateUsage - memory_after.PrivateUsage) / MiB);
  }
  return 0;
}