chromium/mojo/public/cpp/platform/platform_handle_security_util_win.cc

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

#include "mojo/public/cpp/platform/platform_handle_security_util_win.h"

#include <windows.h>

#include <optional>

#include "base/containers/span.h"
#include "base/dcheck_is_on.h"
#include "base/debug/stack_trace.h"
#include "base/feature_list.h"
#include "base/features.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/win/nt_status.h"
#include "base/win/scoped_handle.h"
#include "base/win/security_util.h"

namespace mojo {

namespace {

FileHandleSecurityErrorCallback& GetErrorCallback() {
  static base::NoDestructor<FileHandleSecurityErrorCallback> callback;
  return *callback;
}

#if DCHECK_IS_ON()

std::wstring GetPathFromHandle(HANDLE handle) {
  std::wstring full_path(MAX_PATH - 1, '\0');
  // Note: the math here is a bit messy. `basic_string` guarantees that enough
  // space is reserved so that index may be any value between 0 and size()
  // inclusive. However, `GetFinalPathNameByHandleW()` and `MAX_PATH` include
  // the NUL terminator as part of the size (e.g. MAX_PATH is 3 characters for
  // the drive letter, 256 characters for the path, and 1 character for NUL),
  // hence `- 1` for the `resize()` calls.
  DWORD result =
      ::GetFinalPathNameByHandleW(handle, full_path.data(), MAX_PATH, 0);
  if (result > MAX_PATH) {
    full_path.resize(result - 1);
    result = ::GetFinalPathNameByHandleW(handle, full_path.data(), result, 0);
  }
  if (!result) {
    PLOG(ERROR) << "Could not get full path for handle " << handle;
    return std::wstring();
  }
  full_path.resize(result);
  return full_path;
}

std::optional<bool> IsReadOnlyHandle(HANDLE handle) {
  std::optional<ACCESS_MASK> flags = base::win::GetGrantedAccess(handle);
  if (!flags.has_value()) {
    return std::nullopt;
  }
  // Cannot use GENERIC_WRITE as that includes SYNCHRONIZE.
  // This is ~(all the writable permissions).
  return !(flags.value() &
           (FILE_APPEND_DATA | FILE_WRITE_ATTRIBUTES | FILE_WRITE_DATA |
            FILE_WRITE_EA | WRITE_DAC | WRITE_OWNER | DELETE));
}

#endif  // DCHECK_IS_ON();

}  // namespace

void DcheckIfFileHandleIsUnsafe(HANDLE handle) {
#if DCHECK_IS_ON()
  if (!base::FeatureList::IsEnabled(
          base::features::kEnforceNoExecutableFileHandles)) {
    return;
  }

  if (GetFileType(handle) != FILE_TYPE_DISK) {
    return;
  }

  std::optional<bool> is_read_only = IsReadOnlyHandle(handle);
  if (!is_read_only.has_value()) {
    // If unable to obtain whether or not the handle is read-only, skip the rest
    // of the checks, since it's likely GetPathFromHandle below would fail
    // anyway.
    return;
  }

  if (*is_read_only) {
    // Handle is read-only so considered safe.
    return;
  }

  std::wstring path = GetPathFromHandle(handle);
  if (path.empty()) {
    return;
  }

  base::win::ScopedHandle file_handle(
      ::CreateFileW(path.c_str(), FILE_READ_DATA | FILE_EXECUTE,
                    FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                    nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr));

  // If the file cannot be opened with FILE_EXECUTE it means that it is safe.
  if (!file_handle.is_valid()) {
    return;
  }

  auto& error_callback = GetErrorCallback();
  if (error_callback) {
    bool handled = error_callback.Run();
    if (handled) {
      return;
    }
  }

  DLOG(FATAL) << "Transfer of writable handle to executable file to an "
                 "untrusted process: "
              << path
              << ". Please use AddFlagsForPassingToUntrustedProcess or call "
                 "PreventExecuteMapping on the FilePath.";
#endif  // DCHECK_IS_ON();
}

void SetUnsafeFileHandleCallbackForTesting(
    FileHandleSecurityErrorCallback callback) {
  GetErrorCallback() = std::move(callback);
}

}  // namespace mojo