chromium/sandbox/win/src/handle_closer_agent.cc

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

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/351564777): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "sandbox/win/src/handle_closer_agent.h"

#include <Windows.h>
#include <winnls.h>

#include <stddef.h>

#include "base/check.h"
#include "base/containers/span.h"
#include "base/logging.h"
#include "base/win/static_constants.h"
#include "base/win/win_util.h"
#include "sandbox/win/src/heap_helper.h"
#include "sandbox/win/src/sandbox_nt_util.h"
#include "sandbox/win/src/win_utils.h"

namespace sandbox {
namespace {
// Partial definition only for value not in PROCESSINFOCLASS.
constexpr uint32_t ProcessHandleTable = 58;

// Handle Types.
constexpr wchar_t kFile[] = L"File";
constexpr wchar_t kSection[] = L"Section";
constexpr wchar_t kALPCPort[] = L"ALPC Port";
// Full paths of closeable handles.
constexpr wchar_t kDeviceApi[] = L"\\Device\\DeviceApi";
constexpr wchar_t kDeviceKsecDD[] = L"\\Device\\KsecDD";
// Leaf name of Section to close (matches with EndsWith).
constexpr wchar_t kWindowsShellGlobalCounters[] =
    L"\\windows_shell_global_counters";

// Used by EnumSystemLocales for warming up.
static BOOL CALLBACK EnumLocalesProcEx(LPWSTR lpLocaleString,
                                       DWORD dwFlags,
                                       LPARAM lParam) {
  return TRUE;
}

// Additional warmup done just when CSRSS is being disconnected.
bool CsrssDisconnectWarmup() {
  return ::EnumSystemLocalesEx(EnumLocalesProcEx, LOCALE_WINDOWS, 0, 0);
}

// Cleans up this process if CSRSS will be disconnected, as this disconnection
// is not supported Windows behavior.
// Currently, this step requires closing a heap that this shared with csrss.exe.
// Closing the ALPC Port handle to csrss.exe leaves this heap in an invalid
// state. This causes problems if anyone enumerates the heap.
bool CsrssDisconnectCleanup() {
  HANDLE csr_port_heap = FindCsrPortHeap();
  if (!csr_port_heap) {
    DLOG(ERROR) << "Failed to find CSR Port heap handle";
    return false;
  }
  ::HeapDestroy(csr_port_heap);
  return true;
}

}  // namespace

// Memory buffer mapped from the parent, with our configuration.
SANDBOX_INTERCEPT HandleCloserConfig g_handle_closer_info{};

bool HandleCloserAgent::NeedsHandlesClosed() {
  return g_handle_closer_info.handle_closer_enabled;
}

HandleCloserAgent::HandleCloserAgent()
    : config_(g_handle_closer_info),
      is_csrss_connected_(true),
      dummy_handle_(::CreateEvent(nullptr, false, false, nullptr)) {}

HandleCloserAgent::~HandleCloserAgent() {}

// Attempts to stuff |closed_handle| with a duplicated handle for a dummy Event
// with no access. This should allow the handle to be closed, to avoid
// generating EXCEPTION_INVALID_HANDLE on shutdown, but nothing else. For now
// the only supported type is Event or File.
bool HandleCloserAgent::AttemptToStuffHandleSlot(HANDLE closed_handle) {
  if (!dummy_handle_.is_valid()) {
    return false;
  }

  // This should never happen, as dummy_handle_ is created before closing
  // to_stuff.
  DCHECK(dummy_handle_.get() != closed_handle);

  std::vector<HANDLE> to_close;

  const DWORD original_proc_num = ::GetCurrentProcessorNumber();
  DWORD proc_num = original_proc_num;
  DWORD_PTR original_affinity_mask =
      ::SetThreadAffinityMask(GetCurrentThread(), DWORD_PTR{1} << proc_num);
  bool found_handle = false;
  BOOL result = FALSE;

  // There is per-processor based free list of handles entries. The free handle
  // from current processor's freelist is preferred for reusing, so cycling
  // through all possible processors to find closed_handle.
  // Start searching from current processor which covers usual cases.

  do {
    DWORD_PTR current_mask = DWORD_PTR{1} << proc_num;

    if (original_affinity_mask & current_mask) {
      if (proc_num != original_proc_num) {
        ::SetThreadAffinityMask(::GetCurrentThread(), current_mask);
      }

      HANDLE dup_dummy = nullptr;
      size_t count = 16;

      do {
        result =
            ::DuplicateHandle(::GetCurrentProcess(), dummy_handle_.get(),
                              ::GetCurrentProcess(), &dup_dummy, 0, false, 0);
        if (!result) {
          break;
        }
        if (dup_dummy != closed_handle) {
          to_close.push_back(dup_dummy);
        } else {
          found_handle = true;
        }
      } while (count-- && reinterpret_cast<uintptr_t>(dup_dummy) <
                              reinterpret_cast<uintptr_t>(closed_handle));
    }

    proc_num++;
    if (proc_num == sizeof(DWORD_PTR) * 8) {
      proc_num = 0;
    }
    if (proc_num == original_proc_num) {
      break;
    }
  } while (result && !found_handle);

  SetThreadAffinityMask(::GetCurrentThread(), original_affinity_mask);

  for (HANDLE h : to_close) {
    ::CloseHandle(h);
  }

  return found_handle;
}

bool HandleCloserAgent::CloseHandles() {
  CHECK(config_.handle_closer_enabled);

  // Skip closing these handles when Application Verifier is in use in order to
  // avoid invalid-handle exceptions.
  if (base::win::IsAppVerifierLoaded()) {
    return true;
  }

  DWORD handle_count;
  if (!::GetProcessHandleCount(::GetCurrentProcess(), &handle_count)) {
    return false;
  }

  // The system call will return only handles up to the buffer size so add a
  // margin of error of an additional 1000 handles.
  std::vector<char> buffer((handle_count + 1000) * sizeof(uint32_t));
  DWORD return_length;
  NTSTATUS status = GetNtExports()->QueryInformationProcess(
      ::GetCurrentProcess(), static_cast<PROCESSINFOCLASS>(ProcessHandleTable),
      buffer.data(), static_cast<ULONG>(buffer.size()), &return_length);

  if (!NT_SUCCESS(status)) {
    ::SetLastError(GetLastErrorFromNtStatus(status));
    return false;
  }
  DCHECK(buffer.size() >= return_length);
  DCHECK((buffer.size() % sizeof(uint32_t)) == 0);

  base::span<uint32_t> handle_values(reinterpret_cast<uint32_t*>(buffer.data()),
                                     return_length / sizeof(uint32_t));
  for (uint32_t handle_value : handle_values) {
    HANDLE handle = base::win::Uint32ToHandle(handle_value);
    auto type_name = GetTypeNameFromHandle(handle);
    if (type_name) {
      MaybeCloseHandle(type_name.value(), handle);
    }
  }

  return true;
}

bool HandleCloserAgent::MaybeCloseHandle(std::wstring& type_name,
                                         HANDLE handle) {
  bool bClose = false;
  // Determine if the handle should be closed. Avoid matching the type unless
  // we are configured to close it.
  if (config_.section_windows_global_shell_counters && type_name == kSection) {
    auto path = GetPathFromHandle(handle);
    if (path && base::EndsWith(path.value(), kWindowsShellGlobalCounters)) {
      bClose = true;
    }
  } else if ((config_.file_device_api || config_.file_ksecdd) &&
             type_name == kFile) {
    auto path = GetPathFromHandle(handle);
    if (path && config_.file_device_api && path.value() == kDeviceApi) {
      bClose = true;
    }
    if (path && config_.file_ksecdd && path.value() == kDeviceKsecDD) {
      bClose = true;
    }
  } else if (config_.disconnect_csrss && type_name == kALPCPort) {
    // Do csrss extra warmup & heap closing but only once. Normally there is
    // only one ALPC port to close.
    if (is_csrss_connected_) {
      if (!CsrssDisconnectWarmup() || !CsrssDisconnectCleanup()) {
        return false;
      }
      is_csrss_connected_ = false;
    }
    bClose = true;
  }
  // Implement the close.
  if (bClose) {
    // If we can't unprotect or close the handle we should keep going.
    if (!::SetHandleInformation(handle, HANDLE_FLAG_PROTECT_FROM_CLOSE, 0)) {
      return false;
    }
    if (!::CloseHandle(handle)) {
      return false;
    }
    // Attempt to stuff this handle with a new dummy Event.
    if (type_name == kFile) {
      // Note: could also stuff "Event" but do not support closing any Events
      // at the moment.
      return AttemptToStuffHandleSlot(handle);
    }
  }
  return true;
}

}  // namespace sandbox