// 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.

#include "content/browser/security/cpsp/child_process_security_policy_impl.h"

#include <algorithm>
#include <optional>
#include <sstream>
#include <string_view>
#include <tuple>
#include <utility>
#include <vector>

#include "base/command_line.h"
#include "base/containers/map_util.h"
#include "base/debug/crash_logging.h"
#include "base/debug/dump_without_crashing.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/unguessable_token.h"
#include "build/build_config.h"
#include "content/browser/bad_message.h"
#include "content/browser/isolated_origin_util.h"
#include "content/browser/origin_agent_cluster_isolation_state_bridge.h"
#include "content/browser/process_lock.h"
#include "content/browser/renderer_host/render_process_host_impl.h"
#include "content/browser/security/cpsp/child_process_security_policy_impl.rs.h"
#include "content/browser/security/cpsp/process_state.rs.h"
#include "content/browser/site_info.h"
#include "content/browser/site_instance_impl.h"
#include "content/browser/url_info.h"
#include "content/browser/webui/url_data_manager_backend.h"
#include "content/common/content_navigation_policy.h"
#include "content/common/features.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/child_process_data.h"
#include "content/public/browser/child_process_host.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/site_instance.h"
#include "content/public/browser/site_isolation_policy.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/common/bindings_policy.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.h"
#include "content/public/common/url_constants.h"
#include "net/base/filename_util.h"
#include "net/base/url_util.h"
#include "net/net_buildflags.h"
#include "services/network/public/cpp/resource_request_body.h"
#include "storage/browser/file_system/file_permission_policy.h"
#include "storage/browser/file_system/file_system_context.h"
#include "storage/browser/file_system/file_system_url.h"
#include "storage/browser/file_system/isolated_context.h"
#include "storage/common/file_system/file_system_util.h"
#include "third_party/abseil-cpp/absl/container/flat_hash_set.h"
#include "third_party/blink/public/common/features.h"
#include "url/gurl.h"
#include "url/url_canon.h"
#include "url/url_constants.h"

namespace features {

// TODO(crbug.com/476409377): Remove this guard once the known cases of missing
// ProcessState have been fixed.
BASE_FEATURE(kDumpWithoutCrashingForMissingSecurityState,
             base::FEATURE_DISABLED_BY_DEFAULT);

}  // namespace features

namespace content {

// Defines a FeatureParam to control `RustPolicy` in field trials or from
// command line. Note that the `kCppOnly` policy is achieved by turning off
// features::kChildProcessSecurityPolicyRust; this param controls whether to run
// in kRustOnly or kRustAndCpp mode if that feature is enabled. The default
// (when enabled) is Rust-only. To enable kRustAndCpp for command-line testing,
// use: --enable-features=ChildProcessSecurityPolicyRust:policy/rust-and-cpp
constexpr base::FeatureParam<RustPolicy>::Option rust_policy_options[] = {
    {RustPolicy::kRustOnly,
     features::kChildProcessSecurityPolicyRustPolicyRustOnly},
    {RustPolicy::kRustAndCpp,
     features::kChildProcessSecurityPolicyRustPolicyRustAndCpp}};

const base::FeatureParam<RustPolicy> kRustPolicyParam{
    &features::kChildProcessSecurityPolicyRust,
    features::kChildProcessSecurityPolicyRustPolicyName, RustPolicy::kRustOnly,
    &rust_policy_options};

namespace {

// Helpers to determine whether the experimental Rust ChildProcessSecurityPolicy
// implementation is enabled, and whether both Rust and the legacy C++
// implementations should run in parallel.
// `feature` specifies whether to determine the policy for the main CPSP in Rust
// feature or a sub-feature (e.g., `CpspRustFeature::kProcessState`).
RustPolicy GetRustPolicy(CpspRustFeature feature = CpspRustFeature::kMain) {
  // Sub-features (like the ProcessState feature) also depend on the main CPSP
  // in Rust feature also being enabled.
  if (!base::FeatureList::IsEnabled(
          features::kChildProcessSecurityPolicyRust)) {
    return RustPolicy::kCppOnly;
  }
  if (feature == CpspRustFeature::kProcessState &&
      !base::FeatureList::IsEnabled(
          features::kChildProcessSecurityPolicyRustProcessState)) {
    return RustPolicy::kCppOnly;
  }
  return kRustPolicyParam.Get();
}

bool IsRustEnabled(RustPolicy policy) {
  return policy == RustPolicy::kRustAndCpp || policy == RustPolicy::kRustOnly;
}

bool IsCppEnabled(RustPolicy policy) {
  return policy == RustPolicy::kCppOnly || policy == RustPolicy::kRustAndCpp;
}

// Similar to the above, but includes a workaround since CHECK_EQ does not
// support optional<T>.
template <typename T>
std::optional<T> CheckAndReturnOptionalRustAndCppResults(
    const std::optional<T>& rust_result,
    const std::optional<T>& cpp_result,
    RustPolicy policy) {
  if (policy == RustPolicy::kRustAndCpp) {
    auto to_string = [](const std::optional<T>& result) {
      if (!result) {
        return std::string("(none)");
      }
      std::stringstream ss;
      ss << *result;
      return ss.str();
    };
    // Use CHECK rather than CHECK_EQ to support std::optional types.
    CHECK(rust_result == cpp_result)
        << "rust_result: " << to_string(rust_result)
        << " cpp_result: " << to_string(cpp_result);
  }
  // Rust return values get priority.
  return IsRustEnabled(policy) ? rust_result : cpp_result;
}

// Macro for gating whether a void function should use its Rust or C++
// implementation (or both), based on current feature flags. This is wrapped in
// a do/while(0) loop to ensure it behaves as a single statement for blocks that
// omit braces.
//
// An optional `CpspRustFeature` enum value may be passed as a third argument to
// specify how much of the Rust CPSP feature needs to be enabled to invoke
// `rust_function_call` (e.g., whether the call site requires the main Rust
// feature or also the `kProcessState` sub-feature). If omitted, it defaults to
// checking the main Rust feature (`CpspRustFeature::kMain`).
#define RUST_CPP_VOID_FUNCTION(rust_function_call, cpp_function_call, ...) \
  do {                                                                     \
    const RustPolicy rust_cpp_policy = GetRustPolicy(__VA_ARGS__);         \
    if (IsRustEnabled(rust_cpp_policy)) {                                  \
      rust_function_call;                                                  \
    }                                                                      \
    if (IsCppEnabled(rust_cpp_policy)) {                                   \
      cpp_function_call;                                                   \
    }                                                                      \
  } while (0)

// Macro for gating whether a function with a return value should use its Rust
// or C++ implementation (or both), based on current feature flags. If both Rust
// and C++ are enabled, then the return values of both implementations are
// compared, causing a CHECK failure if they differ. This is wrapped in a
// do/while(0) loop to ensure it behaves as a single statement for blocks that
// omit braces.
//
// An optional `CpspRustFeature` enum value may be passed as a third argument to
// specify how much of the Rust CPSP feature needs to be enabled to invoke
// `rust_function_call` (e.g., whether the call site requires the main Rust
// feature or also the `kProcessState` sub-feature). If omitted, it defaults to
// checking the main Rust feature (`CpspRustFeature::kMain`).
#define RUST_CPP_RETURN_FUNCTION(rust_function_call, cpp_function_call, ...) \
  do {                                                                       \
    const RustPolicy rust_cpp_policy = GetRustPolicy(__VA_ARGS__);           \
    if (rust_cpp_policy == RustPolicy::kRustOnly) {                          \
      return rust_function_call;                                             \
    }                                                                        \
    if (rust_cpp_policy == RustPolicy::kCppOnly) {                           \
      return cpp_function_call;                                              \
    }                                                                        \
    /* Both are enabled (kRustAndCpp): compare and return. */                \
    decltype(auto) rust_result = rust_function_call;                         \
    decltype(auto) cpp_result = cpp_function_call;                           \
    CHECK_EQ(rust_result, cpp_result)                                        \
        << "Rust: " << rust_result << " vs C++: " << cpp_result;             \
    return rust_result;                                                      \
  } while (0)

// Helper macro specifically for gating functions that depend on the Rust
// ProcessState sub-feature
// (features::kChildProcessSecurityPolicyRustProcessState).
#define RUST_CPP_PROCESS_STATE_VOID_FUNCTION(rust_function_call, \
                                             cpp_function_call)  \
  RUST_CPP_VOID_FUNCTION(rust_function_call, cpp_function_call,  \
                         CpspRustFeature::kProcessState)

// Helper macro specifically for gating return functions that depend on the Rust
// ProcessState sub-feature
// (features::kChildProcessSecurityPolicyRustProcessState).
#define RUST_CPP_PROCESS_STATE_RETURN_FUNCTION(rust_function_call, \
                                               cpp_function_call)  \
  RUST_CPP_RETURN_FUNCTION(rust_function_call, cpp_function_call,  \
                           CpspRustFeature::kProcessState)

// Used internally only. These bit positions have no relationship to any
// underlying OS and can be changed to accommodate finer-grained permissions.
enum ChildProcessSecurityPermissions {
  READ_FILE_PERMISSION = 1 << 0,
  WRITE_FILE_PERMISSION = 1 << 1,
  CREATE_NEW_FILE_PERMISSION = 1 << 2,
  CREATE_OVERWRITE_FILE_PERMISSION = 1 << 3,
  DELETE_FILE_PERMISSION = 1 << 4,

  // Used by Media Galleries API
  COPY_INTO_FILE_PERMISSION = 1 << 5,
};

// Used internally only. Bitmasks that are actually used by the Grant* and Can*
// methods. These contain one or more ChildProcessSecurityPermissions.
enum ChildProcessSecurityGrants {
  READ_FILE_GRANT = READ_FILE_PERMISSION,
  WRITE_FILE_GRANT = WRITE_FILE_PERMISSION,

  CREATE_NEW_FILE_GRANT =
      CREATE_NEW_FILE_PERMISSION | COPY_INTO_FILE_PERMISSION,

  CREATE_READ_WRITE_FILE_GRANT =
      CREATE_NEW_FILE_PERMISSION | CREATE_OVERWRITE_FILE_PERMISSION |
      READ_FILE_PERMISSION | WRITE_FILE_PERMISSION | COPY_INTO_FILE_PERMISSION |
      DELETE_FILE_PERMISSION,

  COPY_INTO_FILE_GRANT = COPY_INTO_FILE_PERMISSION,
  DELETE_FILE_GRANT = DELETE_FILE_PERMISSION,
};

// https://crbug.com/646278 Valid blob URLs should contain canonically
// serialized origins.
bool IsMalformedBlobUrl(const GURL& url) {
  if (!url.SchemeIsBlob()) {
    return false;
  }

  // If the part after blob: survives a roundtrip through url::Origin, then
  // it's a normal blob URL.
  std::string canonical_origin = url::Origin::Create(url).Serialize();
  canonical_origin.append(1, '/');
  if (base::StartsWith(url.GetContentPiece(), canonical_origin,
                       base::CompareCase::INSENSITIVE_ASCII)) {
    return false;
  }

  // This is a malformed blob URL.
  return true;
}

// Helper function that checks to make sure calls on
// CanAccessDataForOrigin() are only made on valid threads.
// TODO(acolwell): Expand the usage of this check to other
// ChildProcessSecurityPolicyImpl methods.
bool IsRunningOnExpectedThread() {
  if (BrowserThread::CurrentlyOn(BrowserThread::IO) ||
      BrowserThread::CurrentlyOn(BrowserThread::UI)) {
    return true;
  }

  std::string thread_name(base::PlatformThread::GetName());

  // TODO(acolwell): Remove once all tests are updated to properly
  // identify that they are running on the UI or IO threads.
  if (thread_name.empty()) {
    return true;
  }

  LOG(ERROR) << "Running on unexpected thread '" << thread_name << "'";
  return false;
}

using AccessType = ChildProcessSecurityPolicyImpl::AccessType;
std::string AccessTypeToString(AccessType access_type) {
  switch (access_type) {
    case AccessType::kCanCommitNewOrigin:
      return "can_commit_new_origin";
    case AccessType::kHostsOrigin:
      return "hosts_origin";
    case AccessType::kCanAccessDataForCommittedOrigin:
      return "can_access_data_for_origin";
  }
}

base::debug::CrashKeyString* GetRequestedOriginCrashKey() {
  static auto* requested_origin_key = base::debug::AllocateCrashKeyString(
      "requested_origin", base::debug::CrashKeySize::Size256);
  return requested_origin_key;
}

base::debug::CrashKeyString* GetExpectedProcessLockKey() {
  static auto* expected_process_lock_key = base::debug::AllocateCrashKeyString(
      "expected_process_lock", base::debug::CrashKeySize::Size64);
  return expected_process_lock_key;
}

base::debug::CrashKeyString* GetKilledProcessOriginLockKey() {
  static auto* crash_key = base::debug::AllocateCrashKeyString(
      "killed_process_origin_lock", base::debug::CrashKeySize::Size64);
  return crash_key;
}

base::debug::CrashKeyString* GetCanAccessDataFailureReasonKey() {
  static auto* crash_key = base::debug::AllocateCrashKeyString(
      "can_access_data_failure_reason", base::debug::CrashKeySize::Size256);
  return crash_key;
}

base::debug::CrashKeyString* GetCanAccessDataKeepAliveDurationKey() {
  static auto* keep_alive_duration_key = base::debug::AllocateCrashKeyString(
      "keep_alive_duration", base::debug::CrashKeySize::Size256);
  return keep_alive_duration_key;
}

base::debug::CrashKeyString* GetCanAccessDataShutdownDelayRefCountKey() {
  static auto* shutdown_delay_key = base::debug::AllocateCrashKeyString(
      "shutdown_delay_ref_count", base::debug::CrashKeySize::Size32);
  return shutdown_delay_key;
}

base::debug::CrashKeyString* GetCanAccessDataProcessRFHCount() {
  static auto* process_rfh_count_key = base::debug::AllocateCrashKeyString(
      "process_rfh_count", base::debug::CrashKeySize::Size32);
  return process_rfh_count_key;
}

base::debug::CrashKeyString* GetCommittedOriginsKey() {
  static auto* crash_key = base::debug::AllocateCrashKeyString(
      "committed_origins", base::debug::CrashKeySize::Size256);
  return crash_key;
}

base::debug::CrashKeyString* GetAccessTypeKey() {
  static auto* crash_key = base::debug::AllocateCrashKeyString(
      "access_type", base::debug::CrashKeySize::Size32);
  return crash_key;
}

void LogCanAccessDataForOriginCrashKeys(
    const std::string& expected_process_lock,
    const std::string& killed_process_origin_lock,
    const std::string& requested_origin,
    const std::string& failure_reason,
    const std::string& keep_alive_durations,
    const std::string& shutdown_delay_ref_count,
    const std::string& process_rfh_count,
    const std::string& committed_origin_list,
    const std::string& access_type) {
  base::debug::SetCrashKeyString(GetExpectedProcessLockKey(),
                                 expected_process_lock);
  base::debug::SetCrashKeyString(GetKilledProcessOriginLockKey(),
                                 killed_process_origin_lock);
  base::debug::SetCrashKeyString(GetRequestedOriginCrashKey(),
                                 requested_origin);
  base::debug::SetCrashKeyString(GetCanAccessDataFailureReasonKey(),
                                 failure_reason);
  base::debug::SetCrashKeyString(GetCanAccessDataKeepAliveDurationKey(),
                                 keep_alive_durations);
  base::debug::SetCrashKeyString(GetCanAccessDataShutdownDelayRefCountKey(),
                                 shutdown_delay_ref_count);
  base::debug::SetCrashKeyString(GetCanAccessDataProcessRFHCount(),
                                 process_rfh_count);
  base::debug::SetCrashKeyString(GetCommittedOriginsKey(),
                                 committed_origin_list);
  base::debug::SetCrashKeyString(GetAccessTypeKey(), access_type);
}

void LogCanCommitUrlFailureReason(std::string_view failure_reason) {
  static auto* const failure_reason_key = base::debug::AllocateCrashKeyString(
      "cpspi_can_commit_url_failure_reason", base::debug::CrashKeySize::Size64);
  base::debug::SetCrashKeyString(failure_reason_key, failure_reason);
}

// If a ChildProcessSecurityPolicy query is unable to find the ProcessState,
// this indicates a bug in the browser process where the caller is not ensuring
// the ProcessState is alive as long as needed (e.g., by holding a
// ChildProcessSecurityPolicy::Handle).
//
// When these cases occur, possibly send a DumpWithoutCrashing report to track
// down the caller (including in tasks posted from other threads) and fix it.
// This function uses NOINLINE to ensure all such reports are handled from a
// consistent and self-explanatory magic signature while they are investigated.
//
// TODO(crbug.com/476409377): Once all known cases are fixed, upgrade this to a
// browser crash to prevent future regressions.
//
// This name is using the old SecurityState name so that the active trial for
// kDumpWithoutCrashingForMissingProcessState doesn't have crashes' magic
// signatures changed mid-study.
NOINLINE void NoChildProcessSecurityPolicySecurityStateFound() {
  // For now, gate the crash report behind a feature flag to control the number
  // of reports while there may be many unknown causes.
  if (base::FeatureList::IsEnabled(
          features::kDumpWithoutCrashingForMissingSecurityState)) {
    base::debug::DumpWithoutCrashing();
  }
}

// Checks whether a lock mismatch should be ignored to allow most visited tiles
// to commit in third-party NTP processes.
//
// TODO(crbug.com/40447789): This exception should be removed once these tiles
// can be loaded in OOPIFs on the NTP.
bool AllowProcessLockMismatchForNTP(const ProcessLock& expected_lock,
                                    const ProcessLock& actual_lock) {
  // First, ensure that the expected lock corresponds to a WebUI site that
  // does not require its process to be locked.  This should only be the case
  // for sites used to load most visited tiles.
  const auto& webui_schemes = URLDataManagerBackend::GetWebUISchemes();
  if (!std::ranges::contains(webui_schemes,
                             expected_lock.GetProcessLockURL().GetScheme())) {
    return false;
  }
  if (GetContentClient()->browser()->DoesWebUIUrlRequireProcessLock(
          expected_lock.GetProcessLockURL())) {
    return false;
  }

  // Now, check that the actual lock corresponds to an NTP process (using its
  // site_url() since this check relies on checking effective URLs for NTPs),
  // and that the expected lock (based on the URL for which we're doing the
  // access check) is allowed to stay in that process. This restricts the lock
  // mismatch to just NTP processes, disallowing most visited tiles from being
  // embedded on sites in other processes.
  return GetContentClient()->browser()->ShouldStayInParentProcessForNTP(
      expected_lock.GetProcessLockURL(), actual_lock.site_url());
}

#if BUILDFLAG(IS_CHROMEOS)
GURL NormalizeExternalFileUrl(const GURL& url) {
  GURL::Replacements replacements;
  replacements.ClearQuery();
  replacements.ClearRef();
  return url.ReplaceComponents(replacements);
}
#endif

}  // namespace

ChildProcessSecurityPolicyImpl::Handle::Handle() = default;

ChildProcessSecurityPolicyImpl::Handle::Handle(ChildProcessId child_id,
                                               bool duplicating_handle)
    : child_id_(child_id) {
  auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
  if (!policy->AddProcessReference(child_id_, duplicating_handle)) {
    child_id_ = ChildProcessId();
  }
}

ChildProcessSecurityPolicyImpl::Handle::Handle(Handle&& rhs) {
  std::swap(child_id_, rhs.child_id_);
}

ChildProcessSecurityPolicyImpl::Handle
ChildProcessSecurityPolicyImpl::Handle::Duplicate() {
  return Handle(child_id_, /* duplicating_handle */ true);
}

ChildProcessSecurityPolicyImpl::Handle::~Handle() {
  if (child_id_) {
    auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
    policy->RemoveProcessReference(child_id_);
  }
}

ChildProcessSecurityPolicyImpl::Handle&
ChildProcessSecurityPolicyImpl::Handle::operator=(Handle&& rhs) {
  if (child_id_ && child_id_ != rhs.child_id_) {
    auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
    policy->RemoveProcessReference(child_id_);
  }
  child_id_ = rhs.child_id_;
  rhs.child_id_ = ChildProcessId();
  return *this;
}

bool ChildProcessSecurityPolicyImpl::Handle::is_valid() const {
  return !child_id_.is_null();
}

bool ChildProcessSecurityPolicyImpl::Handle::CanReadFile(
    const base::FilePath& file) {
  if (!child_id_) {
    return false;
  }

  auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
  return policy->CanReadFile(child_id_, file);
}

bool ChildProcessSecurityPolicyImpl::Handle::CanReadFileSystemFile(
    const storage::FileSystemURL& url) {
  if (!child_id_) {
    return false;
  }

  auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
  return policy->CanReadFileSystemFile(child_id_, url);
}

bool ChildProcessSecurityPolicyImpl::Handle::CanWriteFileSystemFile(
    const storage::FileSystemURL& url) {
  if (!child_id_) {
    return false;
  }

  auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
  return policy->CanWriteFileSystemFile(child_id_, url);
}

bool ChildProcessSecurityPolicyImpl::Handle::CanCreateFileSystemFile(
    const storage::FileSystemURL& url) {
  if (!child_id_) {
    return false;
  }

  auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
  return policy->CanCreateFileSystemFile(child_id_, url);
}

bool ChildProcessSecurityPolicyImpl::Handle::CanDeleteFileSystemFile(
    const storage::FileSystemURL& url) {
  if (!child_id_) {
    return false;
  }

  auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
  return policy->CanDeleteFileSystemFile(child_id_, url);
}

bool ChildProcessSecurityPolicyImpl::Handle::CanMoveFileSystemFile(
    const storage::FileSystemURL& src_url,
    const storage::FileSystemURL& dest_url) {
  if (!child_id_) {
    return false;
  }

  auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
  return policy->CanMoveFileSystemFile(child_id_, src_url, dest_url);
}

bool ChildProcessSecurityPolicyImpl::Handle::CanCopyFileSystemFile(
    const storage::FileSystemURL& src_url,
    const storage::FileSystemURL& dest_url) {
  if (!child_id_) {
    return false;
  }

  auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
  return policy->CanCopyFileSystemFile(child_id_, src_url, dest_url);
}

bool ChildProcessSecurityPolicyImpl::Handle::CanAccessDataForOrigin(
    const url::Origin& origin) {
  if (!child_id_) {
    LogCanAccessDataForOriginCrashKeys(
        "(unknown)", "(unknown)", origin.GetDebugString(), "handle_not_valid",
        "no_keep_alive_durations", "no shutdown delay ref count",
        "no process rfh count", "(unknown)",
        AccessTypeToString(AccessType::kCanAccessDataForCommittedOrigin));
    return false;
  }

  auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
  // TODO(crbug.com/379869738) Remove GetUnsafeValue.
  return policy->CanAccessDataForOrigin(child_id_.GetUnsafeValue(), origin);
}

// The ProcessState class is used to maintain per-child process security state
// information.
class ChildProcessSecurityPolicyImpl::ProcessState {
 public:
  using BrowsingInstanceDefaultIsolationStatesMap =
      absl::flat_hash_map<BrowsingInstanceId, OriginAgentClusterIsolationState>;

  explicit ProcessState(BrowserContext* browser_context)
      : can_send_midi_(false),
        can_send_midi_sysex_(false),
        browser_context_(browser_context) {
    if (!base::FeatureList::IsEnabled(blink::features::kBlockMidiByDefault)) {
      can_send_midi_ = true;
    }
  }

  ProcessState(const ProcessState&) = delete;
  ProcessState& operator=(const ProcessState&) = delete;

  ~ProcessState() {
    storage::IsolatedContext* isolated_context =
        storage::IsolatedContext::GetInstance();
    for (auto iter = filesystem_permissions_.begin();
         iter != filesystem_permissions_.end(); ++iter) {
      isolated_context->RemoveReference(iter->first);
    }
    UMA_HISTOGRAM_COUNTS_10000(
        "SiteIsolation.BrowsingInstance.MaxCountPerProcess",
        max_browsing_instance_count_);
  }

  // Grant permission to request and commit URLs with the specified origin.
  void GrantCommitOrigin(const url::Origin& origin) {
    if (origin.opaque()) {
      return;
    }
    origin_map_[origin] = CommitRequestPolicy::kCommitAndRequest;
  }

  void GrantRequestOrigin(const url::Origin& origin) {
    if (origin.opaque()) {
      return;
    }
    // Anything already in |origin_map_| must have at least request permission
    // already. In that case, the emplace() below will be a no-op.
    origin_map_.emplace(origin, CommitRequestPolicy::kRequestOnly);
  }

  void GrantCommitScheme(const std::string& scheme) {
    scheme_map_[scheme] = CommitRequestPolicy::kCommitAndRequest;
  }

  void GrantRequestScheme(const std::string& scheme) {
    // Anything already in |scheme_map_| must have at least request permission
    // already. In that case, the emplace() below will be a no-op.
    scheme_map_.emplace(scheme, CommitRequestPolicy::kRequestOnly);
  }

  void AddCommittedOrigin(const url::Origin& origin) {
    committed_origins_.emplace(origin);
  }

  std::string GetCommittedOriginsAsStringForDebugging() const {
    std::string str;
    for (auto& origin : committed_origins_) {
      base::StrAppend(&str,
                      {(str.empty() ? "" : ","), origin.GetDebugString()});
    }
    return str;
  }

  bool MatchesCommittedOrigin(const GURL& url,
                              bool url_is_for_precursor_origin) const {
    for (auto& origin : committed_origins_) {
      // If the committed origin is non-opaque, check that the `url` has the
      // same origin.
      //
      // TODO(crbug.com/40148776): Although this matches legacy enforcements,
      // this check should ideally also enforce that the `url` does not
      // correspond to an opaque origin's precursor, i.e. that
      // `url_is_for_precursor_origin` is false, so that we do not match a
      // non-opaque committed origin to an opaque one, even if the latter's
      // precursor matches. This almost works, but has a corner case with
      // dedicated workers, where a worker is allowed to be created with a data:
      // script URL, resulting in an opaque origin with a precursor which needs
      // to pass the check here. This case should be fixed (e.g., by adding that
      // worker's origin to the list of committed origins).
      if (!origin.opaque() && origin.IsSameOriginWith(url)) {
        return true;
      }

      // For opaque committed origins, ensure that the passed-in URL represents
      // a precursor of an opaque origin, and then check if it matches the
      // committed origin's precursor.
      if (origin.opaque() && url_is_for_precursor_origin &&
          origin.GetTupleOrPrecursorTupleIfOpaque() ==
              url::SchemeHostPort(url)) {
        return true;
      }

      // Temporarily ignore hosts when comparing file origins. This allows
      // file:///etc to match file://localhost/etc. See the
      // DOMStorageBrowserTest.FileUrlWithHost test which exercises this. This
      // is needed because ChildProcessSecurityPolicyImpl::CanAccessOrigin()
      // currently converts the passed-in url::Origin into a GURL (which ends up
      // as `url` here) via url::Origin::GetURL(), and the latter always
      // converts origins for file URLs into a "file:///" URL without
      // considering the host. Longer-term, we should either refactor
      // ChildProcessSecurityPolicyImpl such that CanAccessOrigin() doesn't
      // convert url::Origins into GURLs before passing them to
      // CanAccessMaybeOpaqueOrigin(), and/or url::Origin::GetURL() should be
      // fixed to preserve hosts for file URL origins.
      if (url.SchemeIsFile() && origin.scheme() == url::kFileScheme) {
        return true;
      }
    }

    return false;
  }

  // Grant certain permissions to a file.
  void GrantPermissionsForFile(const base::FilePath& file, int permissions) {
    base::FilePath stripped = file.StripTrailingSeparators();
    file_permissions_[stripped] |= permissions;
  }

  // Grant navigation to a file but not the file:// scheme in general.
  void GrantRequestOfSpecificFile(const base::FilePath& file) {
    request_file_set_.insert(file.StripTrailingSeparators());
  }

#if BUILDFLAG(IS_CHROMEOS)
  // Grant navigation to a specific external file URL.
  void GrantRequestOfExternalFileUrl(const GURL& url) {
    CHECK(url.SchemeIs(kExternalFileScheme));
    request_externalfile_set_.insert(NormalizeExternalFileUrl(url));
  }

  void GrantCommitOfExternalFileUrl(const GURL& url) {
    CHECK(url.SchemeIs(kExternalFileScheme));
    commit_externalfile_set_.insert(NormalizeExternalFileUrl(url));

    // Commit access automatically implies request access.
    request_externalfile_set_.insert(NormalizeExternalFileUrl(url));
  }
#endif

  // Revokes all permissions granted to a file.
  void RevokeAllPermissionsForFile(const base::FilePath& file) {
    base::FilePath stripped = file.StripTrailingSeparators();
    file_permissions_.erase(stripped);
    request_file_set_.erase(stripped);
  }

  // Grant certain permissions to a file.
  void GrantPermissionsForFileSystem(const std::string& filesystem_id,
                                     int permissions) {
    if (!filesystem_permissions_.contains(filesystem_id)) {
      storage::IsolatedContext::GetInstance()->AddReference(filesystem_id);
    }
    filesystem_permissions_[filesystem_id] |= permissions;
  }

  bool HasPermissionsForFileSystem(const std::string& filesystem_id,
                                   int permissions) const {
    FileSystemMap::const_iterator it =
        filesystem_permissions_.find(filesystem_id);
    if (it == filesystem_permissions_.end()) {
      return false;
    }
    return (it->second & permissions) == permissions;
  }

#if BUILDFLAG(IS_ANDROID)
  // Determine if the certain permissions have been granted to a content URI.
  bool HasPermissionsForContentUri(const base::FilePath& file,
                                   int permissions) const {
    DCHECK(!file.empty());
    DCHECK(file.IsContentUri());
    if (!permissions) {
      return false;
    }
    base::FilePath file_path = file.StripTrailingSeparators();
    FileMap::const_iterator it = file_permissions_.find(file_path);
    if (it != file_permissions_.end()) {
      return (it->second & permissions) == permissions;
    }
    return false;
  }
#endif

  void GrantBindings(BindingsPolicySet bindings) {
    enabled_bindings_.PutAll(bindings);
  }

  void GrantOriginCheckExemptionForWebView(const url::Origin& origin) {
    // This should only be allowed for opaque origins with LoadDataWithBaseURL
    // and file origins with allow_universal_access_from_file_urls.
    CHECK(origin.opaque() || origin.scheme() == url::kFileScheme);
    webview_origin_exemption_set_.insert(origin);
  }

  bool HasOriginCheckExemptionForWebView(const url::Origin& origin) const {
    // This should only be allowed for opaque origins with LoadDataWithBaseURL
    // and file origins with allow_universal_access_from_file_urls.
    CHECK(origin.opaque() || origin.scheme() == url::kFileScheme);
    return webview_origin_exemption_set_.contains(origin);
  }

  void GrantPermissionForMidi() { can_send_midi_ = true; }

  void GrantPermissionForMidiSysEx() {
    can_send_midi_ = true;
    can_send_midi_sysex_ = true;
  }

  // Determine whether permission has been granted to commit |url|.
  bool CanCommitURL(const GURL& url) const {
    DCHECK(!url.SchemeIsBlob() && !url.SchemeIsFileSystem())
        << "inner_url extraction should be done already.";
    // Having permission to a scheme implies permission to all of its URLs.
    auto scheme_judgment = scheme_map_.find(url.GetScheme());
    if (scheme_judgment != scheme_map_.end() &&
        scheme_judgment->second == CommitRequestPolicy::kCommitAndRequest) {
      return true;
    }

#if BUILDFLAG(IS_CHROMEOS)
    if (url.SchemeIs(kExternalFileScheme)) {
      return commit_externalfile_set_.contains(NormalizeExternalFileUrl(url));
    }
#endif

    // Check for permission for specific origin.
    if (CanCommitOrigin(url::Origin::Create(url))) {
      return true;
    }

    return false;  // Unmentioned schemes are disallowed.
  }

  bool CanRequestURL(const GURL& url) const {
    DCHECK(!url.SchemeIsBlob() && !url.SchemeIsFileSystem())
        << "inner_url extraction should be done already.";
    // Having permission to a scheme implies permission to all of its URLs.
    auto scheme_judgment = scheme_map_.find(url.GetScheme());
    if (scheme_judgment != scheme_map_.end()) {
      return true;
    }

    if (CanRequestOrigin(url::Origin::Create(url))) {
      return true;
    }

    // file:// URLs may sometimes be more granular, e.g. dragging and dropping a
    // file from the local filesystem. The child itself may not have been
    // granted access to the entire file:// scheme, but it should still be
    // allowed to request the dragged and dropped file.
    if (url.SchemeIs(url::kFileScheme)) {
      base::FilePath path;
      if (net::FileURLToFilePath(url, &path)) {
        return request_file_set_.contains(path);
      }
    }

#if BUILDFLAG(IS_ANDROID)
    if (url.SchemeIs(url::kContentScheme)) {
      return request_file_set_.contains(base::FilePath(url.spec()));
    }
#endif

#if BUILDFLAG(IS_CHROMEOS)
    if (url.SchemeIs(kExternalFileScheme)) {
      return request_externalfile_set_.contains(NormalizeExternalFileUrl(url));
    }
#endif

    // Otherwise, delegate to CanCommitURL. Unmentioned schemes are disallowed.
    // TODO(dcheng): It would be nice to avoid constructing the origin twice.
    return CanCommitURL(url);
  }

  // Determine if the certain permissions have been granted to a file.
  bool HasPermissionsForFile(const base::FilePath& file,
                             int permissions) const {
#if BUILDFLAG(IS_ANDROID)
    if (file.IsContentUri()) {
      return HasPermissionsForContentUri(file, permissions);
    }
#endif
    if (!permissions || file.empty() || !file.IsAbsolute()) {
      return false;
    }
    base::FilePath current_path = file.StripTrailingSeparators();
    base::FilePath last_path;
    int skip = 0;
    while (current_path != last_path) {
      base::FilePath base_name = current_path.BaseName();
      if (base_name.ReferencesParent()) {
        ++skip;
      } else if (skip > 0) {
        if (base_name.value() != base::FilePath::kCurrentDirectory) {
          --skip;
        }
      } else {
        FileMap::const_iterator it = file_permissions_.find(current_path);
        if (it != file_permissions_.end()) {
          return (it->second & permissions) == permissions;
        }
      }
      last_path = current_path;
      current_path = current_path.DirName();
    }

    return false;
  }

  void SetProcessLock(const ProcessLock& lock_to_set,
                      const IsolationContext& context,
                      bool is_process_used) {
    CHECK(!lock_to_set.is_invalid());
    CHECK(!process_lock_.IsLockedToSite());
    CHECK_NE(SiteInstanceImpl::GetDefaultSiteURL(),
             lock_to_set.GetProcessLockURL());

    if (process_lock_.is_invalid()) {
      DCHECK(browsing_instance_default_isolation_states_.empty());
      CHECK(lock_to_set.AllowsAnySite() || lock_to_set.IsLockedToSite());
    } else {
      // Verify that we are not trying to update the lock with different
      // COOP/COEP information.
      CHECK_EQ(process_lock_.GetWebExposedIsolationInfo(),
               lock_to_set.GetWebExposedIsolationInfo());

      if (process_lock_.AllowsAnySite()) {
        // TODO(acolwell): Remove ability to lock to an allows_any_site
        // lock multiple times. Legacy behavior allows the old "lock to site"
        // path to generate an "allow_any_site" lock if an empty URL is passed
        // to SiteInstanceImpl::SetSite().
        CHECK(lock_to_set.AllowsAnySite() || lock_to_set.IsLockedToSite());

        // Do not allow a lock to become more strict if the process has already
        // been used to render any pages.
        if (lock_to_set.IsLockedToSite()) {
          CHECK(!is_process_used)
              << "Cannot lock an already used process to " << lock_to_set;
        }
      } else {
        NOTREACHED() << "Unexpected lock type.";
      }
    }

    process_lock_ = lock_to_set;
    AddBrowsingInstanceInfo(context);
  }

  void AddBrowsingInstanceInfo(const IsolationContext& context) {
    DCHECK(!context.browsing_instance_id().is_null());
    browsing_instance_default_isolation_states_.insert(
        {context.browsing_instance_id(), context.default_isolation_state()});

    // Track the maximum number of BrowsingInstances in the process in case
    // we need to remove delayed cleanup and let the set grow unbounded.
    // Also track the default isolation state for this BrowsingInstance for
    // future access checks, since the global default can change over time.
    if (browsing_instance_default_isolation_states_.size() >
        max_browsing_instance_count_) {
      max_browsing_instance_count_ =
          browsing_instance_default_isolation_states_.size();
    }
  }

  const ProcessLock& process_lock() const { return process_lock_; }

  const BrowsingInstanceDefaultIsolationStatesMap&
  browsing_instance_default_isolation_states() const {
    return browsing_instance_default_isolation_states_;
  }

  void ClearBrowsingInstanceId(const BrowsingInstanceId& id) {
    browsing_instance_default_isolation_states_.erase(id);
  }

  bool has_web_ui_bindings() const {
    return enabled_bindings_.HasAny(kWebUIBindingsPolicySet);
  }

  bool CanSendMidi() const {
    if (base::FeatureList::IsEnabled(blink::features::kBlockMidiByDefault)) {
      // Ensure the flags are in a consistent state: we can only send SysEx
      // messages if we can also send non-SysEx messages
      CHECK(can_send_midi_ || !can_send_midi_sysex_);
      return can_send_midi_;
    } else {
      return true;
    }
  }

  bool CanSendMidiSysEx() const {
    if (base::FeatureList::IsEnabled(blink::features::kBlockMidiByDefault)) {
      // Ensure the flags are in a consistent state: we can only send SysEx
      // messages if we can also send non-SysEx messages
      CHECK(can_send_midi_ || !can_send_midi_sysex_);
    }
    return can_send_midi_sysex_;
  }

  BrowserContext* browser_context() const {
    CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    return browser_context_;
  }

  void ClearBrowserContextIfMatches(const BrowserContext* browser_context) {
    if (browser_context == browser_context_) {
      browser_context_ = nullptr;
    }
  }

 private:
  enum class CommitRequestPolicy {
    kRequestOnly,
    kCommitAndRequest,
  };

  bool CanCommitOrigin(const url::Origin& origin) const {
    auto it = origin_map_.find(origin);
    if (it == origin_map_.end()) {
      return false;
    }
    return it->second == CommitRequestPolicy::kCommitAndRequest;
  }

  bool CanRequestOrigin(const url::Origin& origin) const {
    // Anything already in |origin_map_| must have at least request permissions
    // already.
    return origin_map_.contains(origin);
  }

  using SchemeMap = absl::flat_hash_map<std::string, CommitRequestPolicy>;
  using OriginMap = absl::flat_hash_map<url::Origin, CommitRequestPolicy>;

  using FilePermissionFlags = int;  // bit-set of base::File::Flags
  using FileMap = absl::flat_hash_map<base::FilePath, FilePermissionFlags>;
  using FileSystemMap = absl::flat_hash_map<std::string, FilePermissionFlags>;
  using FileSet = absl::flat_hash_set<base::FilePath>;
  using URLSet = absl::flat_hash_set<GURL>;
  using OriginSet = absl::flat_hash_set<url::Origin>;

  // Maps URL schemes to commit/request policies the child process has been
  // granted. There is no provision for revoking.
  SchemeMap scheme_map_;

  // The map of URL origins to commit/request policies the child process has
  // been granted. There is no provision for revoking.
  OriginMap origin_map_;

  // The set of all origins ever committed in the child process. Note that this
  // is different from the `origin_map_` above: `origin_map_` tracks rules which
  // allow new origins to be requested or committed in a particular process,
  // while this set tracks origins that have already been committed, for the
  // purposes of validating requests for a particular origin's data.
  //
  // Note that unlike `origin_map_`, this set tracks opaque origins and
  // distinguishes them based on their precursors and nonces. This set may also
  // lack certain entries that exist in `origin_map_`, such as special cases
  // that allow a process to request particular origins (e.g., DevTools process
  // being allowed to request DevTools extension resources).
  //
  // TODO(alexmos): Combine `origin_map_` and `committed_origins_` into one set
  // that supports three kinds of lookup: CanRequest, CanCommitAndRequest, and
  // HasCommittedAndCanRequest. This will hopefully result in simpler and more
  // efficient origin tracking.
  OriginSet committed_origins_;

  // The set of files the child process is permitted to upload to the web.
  FileMap file_permissions_;

  // The set of files the child process is permitted to load.
  FileSet request_file_set_;

#if BUILDFLAG(IS_CHROMEOS)
  // The set of specific URLs the child process is permitted to load.
  URLSet request_externalfile_set_;
  URLSet commit_externalfile_set_;
#endif

  // The set of origins in Android WebView and <webview> tags that are allowed
  // to bypass some navigation checks. Limited to opaque origins loaded with
  // LoadDataWithBaseURL and file origins loaded with
  // allow_universal_access_from_file_urls.
  OriginSet webview_origin_exemption_set_;

  BindingsPolicySet enabled_bindings_;

  bool can_send_midi_;

  bool can_send_midi_sysex_;

  ProcessLock process_lock_;

  // A map containing the IDs of all BrowsingInstances with documents in this
  // process, along with their default OriginAgentClusterIsolationStates. Empty
  // when |process_lock_| is invalid, or if all BrowsingInstances in the
  // ProcessState have been destroyed.
  //
  // After a process is locked, it might be reused by navigations from frames
  // in other BrowsingInstances, e.g., when we're over process limit and when
  // those navigations utilize the same process lock. This set tracks all the
  // BrowsingInstances that share this process.
  //
  // This is needed for security checks on the IO thread, where we only know
  // the process ID and need to compute the expected origin lock, which
  // requires knowing the set of applicable isolated origins in each respective
  // BrowsingInstance.
  BrowsingInstanceDefaultIsolationStatesMap
      browsing_instance_default_isolation_states_;

  // The maximum number of BrowsingInstances that have been in this
  // ProcessState's RenderProcessHost, for metrics.
  unsigned max_browsing_instance_count_ = 0;

  // The set of isolated filesystems the child process is permitted to access.
  FileSystemMap filesystem_permissions_;

  raw_ptr<BrowserContext> browser_context_;
};

ChildProcessSecurityPolicyImpl::ProcessStateMaps::ProcessStateMaps() = default;

ChildProcessSecurityPolicyImpl::ProcessStateMaps::~ProcessStateMaps() = default;

size_t ChildProcessSecurityPolicyImpl::ProcessStateMaps::GetSizeForTesting() {
  // Currently, this intentionally only returns the size of `process_state_`
  // and not also the size of `pending_remove_state_`, because the tests do not
  // wait for `pending_remove_state_` to be empty. This could be revisited if
  // the tests become flaky due to leftover `pending_remove_state_`.
  return process_state_.size();
}

// IsolatedOriginEntry implementation.
ChildProcessSecurityPolicyImpl::IsolatedOriginEntry::IsolatedOriginEntry(
    const url::Origin& origin,
    bool applies_to_future_browsing_instances,
    BrowsingInstanceId browsing_instance_id,
    const base::UnguessableToken& browser_context_id,
    bool isolate_all_subdomains,
    IsolatedOriginSource source)
    : origin_(origin),
      applies_to_future_browsing_instances_(
          applies_to_future_browsing_instances),
      browsing_instance_id_(browsing_instance_id),
      browser_context_id_(browser_context_id),
      isolate_all_subdomains_(isolate_all_subdomains),
      source_(source) {}

ChildProcessSecurityPolicyImpl::IsolatedOriginEntry::IsolatedOriginEntry(
    const IsolatedOriginEntry& other) = default;

ChildProcessSecurityPolicyImpl::IsolatedOriginEntry&
ChildProcessSecurityPolicyImpl::IsolatedOriginEntry::operator=(
    const IsolatedOriginEntry& other) = default;

ChildProcessSecurityPolicyImpl::IsolatedOriginEntry::IsolatedOriginEntry(
    IsolatedOriginEntry&& other) = default;

ChildProcessSecurityPolicyImpl::IsolatedOriginEntry&
ChildProcessSecurityPolicyImpl::IsolatedOriginEntry::operator=(
    IsolatedOriginEntry&& other) = default;

ChildProcessSecurityPolicyImpl::IsolatedOriginEntry::~IsolatedOriginEntry() =
    default;

bool ChildProcessSecurityPolicyImpl::IsolatedOriginEntry::
    AppliesToAllBrowserContexts() const {
  return browser_context_id_.is_empty();
}

bool ChildProcessSecurityPolicyImpl::IsolatedOriginEntry::MatchesProfile(
    const base::UnguessableToken& browser_context_id) const {
  // Globally isolated origins aren't associated with any particular profile
  // and should apply to all profiles.
  if (AppliesToAllBrowserContexts()) {
    return true;
  }

  return browser_context_id_ == browser_context_id;
}

bool ChildProcessSecurityPolicyImpl::IsolatedOriginEntry::
    MatchesBrowsingInstance(BrowsingInstanceId browsing_instance_id) const {
  if (applies_to_future_browsing_instances_) {
    return browsing_instance_id_ <= browsing_instance_id;
  }

  return browsing_instance_id_ == browsing_instance_id;
}

// Make sure BrowsingInstance state is cleaned up after the max amount of time
// RenderProcessHost might stick around for various IncrementKeepAliveRefCount
// calls. For now, track that as the KeepAliveHandleFactory timeout (the current
// longest value) plus the unload timeout, with a bit of an extra margin.
// // TODO(wjmaclean): Refactor IncrementKeepAliveRefCount to track how much
// time is needed rather than leaving the interval open ended, so that we can
// enforce a max delay here and in RenderProcessHost. https://crbug.com/1181838
ChildProcessSecurityPolicyImpl::ChildProcessSecurityPolicyImpl()
    : browsing_instance_cleanup_delay_(
          RenderProcessHostImpl::kKeepAliveHandleFactoryTimeout +
          base::Seconds(2)) {
  RegisterDefaultSchemes();
}

ChildProcessSecurityPolicyImpl::~ChildProcessSecurityPolicyImpl() = default;

void ChildProcessSecurityPolicyImpl::RegisterDefaultSchemes() {
  // We know about these schemes and believe them to be safe.
  RegisterWebSafeScheme(url::kHttpScheme);
  RegisterWebSafeScheme(url::kHttpsScheme);
#if BUILDFLAG(ENABLE_WEBSOCKETS)
  RegisterWebSafeScheme(url::kWsScheme);
  RegisterWebSafeScheme(url::kWssScheme);
#endif  // BUILDFLAG(ENABLE_WEBSOCKETS)
  RegisterWebSafeScheme(url::kDataScheme);

  // TODO(nick): https://crbug.com/651534 blob: and filesystem: schemes embed
  // other origins, so we should not treat them as web safe. Remove callers of
  // IsWebSafeScheme(), and then eliminate the next two lines.
  RegisterWebSafeScheme(url::kBlobScheme);
  RegisterWebSafeScheme(url::kFileSystemScheme);

  // We know about the following pseudo schemes and treat them specially.
  RegisterPseudoScheme(url::kAboutScheme);
  RegisterPseudoScheme(url::kJavaScriptScheme);
  RegisterPseudoScheme(kViewSourceScheme);
  RegisterPseudoScheme(kGoogleChromeScheme);
}

void ChildProcessSecurityPolicyImpl::ResetRegisteredSchemesForTesting() {
  // TODO(crbug.com/493156320): Consider a more comprehensive way to do
  // ChildProcessSecurityPolicy state reset in unit tests.
  {
    base::AutoLock lock(schemes_lock_);
    schemes_okay_to_request_in_any_process_.clear();
    schemes_okay_to_commit_in_any_process_.clear();
    pseudo_schemes_.clear();
  }

  rust::child_process_security_policy::
      clear_all_registered_schemes_for_testing();

  RegisterDefaultSchemes();
}

// static
ChildProcessSecurityPolicy* ChildProcessSecurityPolicy::GetInstance() {
  return ChildProcessSecurityPolicyImpl::GetInstance();
}

ChildProcessSecurityPolicyImpl* ChildProcessSecurityPolicyImpl::GetInstance() {
  return base::Singleton<ChildProcessSecurityPolicyImpl>::get();
}

void ChildProcessSecurityPolicyImpl::Add(ChildProcessId child_id,
                                         BrowserContext* browser_context) {
  DCHECK(browser_context);
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(child_id);

  if (IsRustEnabled(GetRustPolicy(CpspRustFeature::kProcessState))) {
    rust::child_process_security_policy::create_state_for_process(child_id);
  }
  // Note: We explicitly continue to create process_state_ even when in
  // Rust-only mode, since the values tracked by ProcessState have only
  // partially been implemented by the Rust version so far. Once ProcessState
  // is fully supported in Rust, we can early-return here when `GetRustPolicy()
  // == RustPolicy::kRustOnly`.
  //
  // TODO(crbug.com/522872468): Currently, the Rust implementation also depends
  // on the reference counting done below in AddProcessReference() to manage its
  // ProcessState lifetime. So even when ProcessState is fully implemented in
  // Rust, this would only be able to early-return here if/when the
  // ProcessState lifetime management is moved over to Rust.

  base::AutoLock lock(lock_);
  process_states_.CreateStateForProcess(child_id, browser_context);
}

void ChildProcessSecurityPolicyImpl::ProcessStateMaps::CreateStateForProcess(
    ChildProcessId child_id,
    BrowserContext* browser_context) {
  if (process_state_.contains(child_id)) {
    NOTREACHED() << "Add child process at most once.";
  }

  process_state_[child_id] = std::make_unique<ProcessState>(browser_context);
  CHECK(AddProcessReference(child_id, /*duplicating_handle=*/false));
}

void ChildProcessSecurityPolicyImpl::AddForTesting(
    ChildProcessId child_id,
    BrowserContext* browser_context) {
  Add(child_id, browser_context);
  LockProcess(IsolationContext(
                  BrowsingInstanceId(1), browser_context,
                  /*is_guest=*/false, /*is_fenced=*/false,
                  OriginAgentClusterIsolationState::CreateForDefaultIsolation(
                      browser_context)),
              child_id,
              /*is_process_used=*/false,
              ProcessLock::CreateAllowAnySite(
                  StoragePartitionConfig::CreateDefault(browser_context),
                  WebExposedIsolationInfo::CreateNonIsolated(),
                  /*cross_origin_isolation_key=*/std::nullopt,
                  browser_context->UniqueToken()));
}

void ChildProcessSecurityPolicyImpl::Remove(ChildProcessId child_id) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(child_id);

  if (IsRustEnabled(GetRustPolicy(CpspRustFeature::kProcessState))) {
    rust::child_process_security_policy::prepare_to_remove_state(child_id);
  }

  // Note: We explicitly continue with C++ state removal as well even when in
  // Rust-only mode, since the values tracked by ProcessState have only
  // partially been implemented by the Rust version so far.
  //
  // TODO: Currently, the Rust implementation also depends on the reference
  // counting done below in RemoveProcessReference() to manage its ProcessState
  // lifetime. Even when ProcessState is fully implemented in Rust, this would
  // only be able to early-return here if/when the ProcessState lifetime
  // management is moved over to Rust.

  base::AutoLock lock(lock_);
  process_states_.PrepareToRemoveState(child_id);
}

void ChildProcessSecurityPolicyImpl::ProcessStateMaps::PrepareToRemoveState(
    ChildProcessId child_id) {
  auto state = process_state_.find(child_id);
  if (state == process_state_.end()) {
    return;
  }

  // Move the existing ProcessState object into a pending map so
  // that we can preserve permission state and avoid mutations to this
  // state after Remove() has been called.
  pending_remove_state_[child_id] = std::move(state->second);
  process_state_.erase(state);

  RemoveProcessReference(child_id);
}

void ChildProcessSecurityPolicyImpl::RegisterWebSafeScheme(
    const std::string& scheme) {
  RUST_CPP_VOID_FUNCTION(
      rust::child_process_security_policy::register_web_safe_scheme(scheme),
      RegisterWebSafeScheme_Cpp(scheme));
}

void ChildProcessSecurityPolicyImpl::RegisterWebSafeScheme_Cpp(
    const std::string& scheme) {
  base::AutoLock lock(schemes_lock_);
  DCHECK_EQ(0U, schemes_okay_to_request_in_any_process_.count(scheme))
      << "Add schemes at most once.";
  DCHECK_EQ(0U, pseudo_schemes_.count(scheme))
      << "Web-safe implies not pseudo.";

  schemes_okay_to_request_in_any_process_.insert(scheme);
  schemes_okay_to_commit_in_any_process_.insert(scheme);
}

void ChildProcessSecurityPolicyImpl::RegisterWebSafeIsolatedScheme(
    const std::string& scheme) {
  RUST_CPP_VOID_FUNCTION(rust::child_process_security_policy::
                             register_web_safe_request_only_scheme(scheme),
                         RegisterWebSafeIsolatedScheme_Cpp(scheme));
}

void ChildProcessSecurityPolicyImpl::RegisterWebSafeIsolatedScheme_Cpp(
    const std::string& scheme) {
  base::AutoLock lock(schemes_lock_);
  DCHECK_EQ(0U, schemes_okay_to_request_in_any_process_.count(scheme))
      << "Add schemes at most once.";
  DCHECK_EQ(0U, pseudo_schemes_.count(scheme))
      << "Web-safe implies not pseudo.";

  schemes_okay_to_request_in_any_process_.insert(scheme);
}

bool ChildProcessSecurityPolicyImpl::IsWebSafeScheme(
    const std::string& scheme) {
  RUST_CPP_RETURN_FUNCTION(
      rust::child_process_security_policy::is_web_safe_scheme(scheme),
      IsWebSafeScheme_Cpp(scheme));
}

bool ChildProcessSecurityPolicyImpl::IsWebSafeScheme_Cpp(
    const std::string& scheme) {
  base::AutoLock lock(schemes_lock_);
  return schemes_okay_to_request_in_any_process_.contains(scheme);
}

void ChildProcessSecurityPolicyImpl::RegisterPseudoScheme(
    const std::string& scheme) {
  RUST_CPP_VOID_FUNCTION(
      rust::child_process_security_policy::register_pseudo_scheme(scheme),
      RegisterPseudoScheme_Cpp(scheme));
}

void ChildProcessSecurityPolicyImpl::RegisterPseudoScheme_Cpp(
    const std::string& scheme) {
  base::AutoLock lock(schemes_lock_);
  DCHECK_EQ(0U, pseudo_schemes_.count(scheme)) << "Add schemes at most once.";
  DCHECK_EQ(0U, schemes_okay_to_request_in_any_process_.count(scheme))
      << "Pseudo implies not web-safe.";
  DCHECK_EQ(0U, schemes_okay_to_commit_in_any_process_.count(scheme))
      << "Pseudo implies not web-safe.";

  pseudo_schemes_.insert(scheme);
}

bool ChildProcessSecurityPolicyImpl::IsPseudoScheme(const std::string& scheme) {
  RUST_CPP_RETURN_FUNCTION(
      rust::child_process_security_policy::is_pseudo_scheme(scheme),
      IsPseudoScheme_Cpp(scheme));
}

bool ChildProcessSecurityPolicyImpl::IsPseudoScheme_Cpp(
    const std::string& scheme) {
  base::AutoLock lock(schemes_lock_);

  return pseudo_schemes_.contains(scheme);
}

void ChildProcessSecurityPolicyImpl::ClearRegisteredSchemeForTesting(
    const std::string& scheme) {
  RUST_CPP_VOID_FUNCTION(
      rust::child_process_security_policy::clear_registered_scheme_for_testing(
          scheme),                                   // IN-TEST
      ClearRegisteredSchemeForTesting_Cpp(scheme));  // IN-TEST
}

void ChildProcessSecurityPolicyImpl::ClearRegisteredSchemeForTesting_Cpp(
    const std::string& scheme) {
  base::AutoLock lock(schemes_lock_);
  schemes_okay_to_request_in_any_process_.erase(scheme);
  schemes_okay_to_commit_in_any_process_.erase(scheme);
  pseudo_schemes_.erase(scheme);
}

void ChildProcessSecurityPolicyImpl::GrantCommitURL(int child_id,
                                                    const GURL& url) {
  // Can't grant the capability to commit invalid URLs.
  if (!url.is_valid()) {
    return;
  }

  // Can't grant the capability to commit pseudo schemes.
  if (IsPseudoScheme(url.GetScheme())) {
    return;
  }

  url::Origin origin = url::Origin::Create(url);

  // Blob and filesystem URLs require special treatment; grant access to the
  // inner origin they embed instead.
  // TODO(dcheng): Can this logic be simplified to just derive an origin up
  // front and use that? That probably requires fixing GURL canonicalization of
  // blob URLs though. For now, be consistent with how CanRequestURL and
  // CanCommitURL normalize.
  if (url.SchemeIsBlob() || url.SchemeIsFileSystem()) {
    if (IsMalformedBlobUrl(url)) {
      return;
    }

    GrantCommitURL(child_id, GURL(origin.Serialize()));
  }

#if BUILDFLAG(IS_CHROMEOS)
  // `externalfile:` URLs, like `file:` URLs, should result in grants to the
  // specific resource referenced, not the entire scheme:
  if (url.SchemeIs(kExternalFileScheme)) {
    GrantCommitOfExternalFileUrl(ChildProcessId::FromUnsafeValue(child_id),
                                 url);
    return;
  }
#endif

  // TODO(dcheng): In the future, URLs with opaque origins would ideally carry
  // around an origin with them, so we wouldn't need to grant commit access to
  // the entire scheme.
  if (!origin.opaque()) {
    GrantCommitOrigin(child_id, origin);
  }

  // The scheme has already been whitelisted for every child process, so no need
  // to do anything else.
  if (IsWebSafeScheme(url.GetScheme())) {
    return;
  }

  base::AutoLock lock(lock_);

  // TODO(crbug.com/379869738) Remove FromUnsafeValue.
  auto* state = process_states_.GetProcessStateForMutation(
      ChildProcessId::FromUnsafeValue(child_id));
  if (!state) {
    return;
  }

  if (origin.opaque()) {
    // If it's impossible to grant commit rights to just the origin (among other
    // things, URLs with non-standard schemes will be treated as opaque
    // origins), then grant access to commit all URLs of that scheme.
    state->GrantCommitScheme(url.GetScheme());
  } else {
    // When the child process has been commanded to request this scheme, grant
    // it the capability to request all URLs of that scheme.
    state->GrantRequestScheme(url.GetScheme());
  }
}

void ChildProcessSecurityPolicyImpl::GrantRequestOfSpecificFile(
    ChildProcessId child_id,
    const base::FilePath& path) {
  base::AutoLock lock(lock_);
  auto* state = process_states_.GetProcessStateForMutation(child_id);
  if (!state) {
    return;
  }

  // When the child process has been commanded to request a file:// URL,
  // then we grant it the capability for that URL only. Canonicalize the path
  // via roundtrip to file:// URL so it will match the incoming URL we validate
  // against (crbug.com/382645162), except android content:// URLs.
#if BUILDFLAG(IS_ANDROID)
  if (path.IsContentUri()) {
    state->GrantRequestOfSpecificFile(path);
    return;
  }
#endif
  GURL url = net::FilePathToFileURL(path);
  base::FilePath canonical_path;
  if (net::FileURLToFilePath(url, &canonical_path)) {
    state->GrantRequestOfSpecificFile(canonical_path);
  }
}

#if BUILDFLAG(IS_CHROMEOS)
void ChildProcessSecurityPolicyImpl::GrantRequestOfExternalFileUrl(
    ChildProcessId child_id,
    const GURL& url) {
  if (!url.is_valid()) {
    return;
  }

  base::AutoLock lock(lock_);
  auto* state = process_states_.GetProcessStateForMutation(child_id);
  if (!state) {
    return;
  }

  state->GrantRequestOfExternalFileUrl(url);
}

void ChildProcessSecurityPolicyImpl::GrantCommitOfExternalFileUrl(
    ChildProcessId child_id,
    const GURL& url) {
  if (!url.is_valid()) {
    return;
  }

  base::AutoLock lock(lock_);
  auto* state = process_states_.GetProcessStateForMutation(child_id);
  if (!state) {
    return;
  }

  state->GrantCommitOfExternalFileUrl(url);
}
#endif

void ChildProcessSecurityPolicyImpl::GrantReadFile(ChildProcessId child_id,
                                                   const base::FilePath& file) {
  GrantPermissionsForFile(child_id, file, READ_FILE_GRANT);
}

void ChildProcessSecurityPolicyImpl::GrantCreateReadWriteFile(
    int child_id,
    const base::FilePath& file) {
  // TODO(crbug.com/379869738) Remove FromUnsafeValue.
  GrantPermissionsForFile(ChildProcessId::FromUnsafeValue(child_id), file,
                          CREATE_READ_WRITE_FILE_GRANT);
}

void ChildProcessSecurityPolicyImpl::GrantCopyInto(int child_id,
                                                   const base::FilePath& dir) {
  // TODO(crbug.com/379869738) Remove FromUnsafeValue.
  GrantPermissionsForFile(ChildProcessId::FromUnsafeValue(child_id), dir,
                          COPY_INTO_FILE_GRANT);
}

void ChildProcessSecurityPolicyImpl::GrantDeleteFrom(
    int child_id,
    const base::FilePath& dir) {
  // TODO(crbug.com/379869738) Remove FromUnsafeValue.
  GrantPermissionsForFile(ChildProcessId::FromUnsafeValue(child_id), dir,
                          DELETE_FILE_GRANT);
}

void ChildProcessSecurityPolicyImpl::GrantPermissionsForFile(
    ChildProcessId child_id,
    const base::FilePath& file,
    int permissions) {
  base::AutoLock lock(lock_);

  if (auto* state = process_states_.GetProcessStateForMutation(child_id)) {
    state->GrantPermissionsForFile(file, permissions);
  }
}

void ChildProcessSecurityPolicyImpl::RevokeAllPermissionsForFile(
    ChildProcessId child_id,
    const base::FilePath& file) {
  base::AutoLock lock(lock_);

  if (auto* state = process_states_.GetProcessStateForMutation(child_id)) {
    state->RevokeAllPermissionsForFile(file);
  }
}

void ChildProcessSecurityPolicyImpl::GrantReadFileSystem(
    int child_id,
    const std::string& filesystem_id) {
  // TODO(crbug.com/379869738) Remove FromUnsafeValue.
  GrantReadFileSystem(ChildProcessId::FromUnsafeValue(child_id), filesystem_id);
}

void ChildProcessSecurityPolicyImpl::GrantReadFileSystem(
    ChildProcessId child_id,
    const std::string& filesystem_id) {
  GrantPermissionsForFileSystem(child_id, filesystem_id, READ_FILE_GRANT);
}

void ChildProcessSecurityPolicyImpl::GrantWriteFileSystem(
    int child_id,
    const std::string& filesystem_id) {
  // TODO(crbug.com/379869738) Remove FromUnsafeValue.
  GrantPermissionsForFileSystem(ChildProcessId::FromUnsafeValue(child_id),
                                filesystem_id, WRITE_FILE_GRANT);
}

void ChildProcessSecurityPolicyImpl::GrantCreateFileForFileSystem(
    int child_id,
    const std::string& filesystem_id) {
  // TODO(crbug.com/379869738) Remove FromUnsafeValue.
  GrantPermissionsForFileSystem(ChildProcessId::FromUnsafeValue(child_id),
                                filesystem_id, CREATE_NEW_FILE_GRANT);
}

void ChildProcessSecurityPolicyImpl::GrantCreateReadWriteFileSystem(
    int child_id,
    const std::string& filesystem_id) {
  // TODO(crbug.com/379869738) Remove FromUnsafeValue.
  GrantPermissionsForFileSystem(ChildProcessId::FromUnsafeValue(child_id),
                                filesystem_id, CREATE_READ_WRITE_FILE_GRANT);
}

void ChildProcessSecurityPolicyImpl::GrantCopyIntoFileSystem(
    int child_id,
    const std::string& filesystem_id) {
  // TODO(crbug.com/379869738) Remove FromUnsafeValue.
  GrantPermissionsForFileSystem(ChildProcessId::FromUnsafeValue(child_id),
                                filesystem_id, COPY_INTO_FILE_GRANT);
}

void ChildProcessSecurityPolicyImpl::GrantDeleteFromFileSystem(
    int child_id,
    const std::string& filesystem_id) {
  // TODO(crbug.com/379869738) Remove FromUnsafeValue.
  GrantPermissionsForFileSystem(ChildProcessId::FromUnsafeValue(child_id),
                                filesystem_id, DELETE_FILE_GRANT);
}

void ChildProcessSecurityPolicyImpl::GrantSendMidiMessage(int child_id) {
  RUST_CPP_PROCESS_STATE_VOID_FUNCTION(
      rust::child_process_security_policy::grant_send_midi_message(
          ChildProcessId::FromUnsafeValue(child_id)),
      GrantSendMidiMessage_Cpp(child_id));
}

void ChildProcessSecurityPolicyImpl::GrantSendMidiMessage_Cpp(int child_id) {
  if (base::FeatureList::IsEnabled(blink::features::kBlockMidiByDefault)) {
    base::AutoLock lock(lock_);

    // TODO(crbug.com/379869738) Remove FromUnsafeValue.
    if (auto* state = process_states_.GetProcessStateForMutation(
            ChildProcessId::FromUnsafeValue(child_id))) {
      state->GrantPermissionForMidi();
    }
  }
}

void ChildProcessSecurityPolicyImpl::GrantSendMidiSysExMessage(int child_id) {
  RUST_CPP_PROCESS_STATE_VOID_FUNCTION(
      rust::child_process_security_policy::grant_send_midi_sysex_message(
          ChildProcessId::FromUnsafeValue(child_id)),
      GrantSendMidiSysExMessage_Cpp(child_id));
}

void ChildProcessSecurityPolicyImpl::GrantSendMidiSysExMessage_Cpp(
    int child_id) {
  base::AutoLock lock(lock_);

  // TODO(crbug.com/379869738) Remove FromUnsafeValue.
  if (auto* state = process_states_.GetProcessStateForMutation(
          ChildProcessId::FromUnsafeValue(child_id))) {
    state->GrantPermissionForMidiSysEx();
  }
}

void ChildProcessSecurityPolicyImpl::GrantCommitOrigin(
    int child_id,
    const url::Origin& origin) {
  base::AutoLock lock(lock_);

  // TODO(crbug.com/379869738) Remove FromUnsafeValue.
  if (auto* state = process_states_.GetProcessStateForMutation(
          ChildProcessId::FromUnsafeValue(child_id))) {
    state->GrantCommitOrigin(origin);
  }
}

void ChildProcessSecurityPolicyImpl::GrantRequestOrigin(
    int child_id,
    const url::Origin& origin) {
  base::AutoLock lock(lock_);

  // TODO(crbug.com/379869738) Remove FromUnsafeValue.
  if (auto* state = process_states_.GetProcessStateForMutation(
          ChildProcessId::FromUnsafeValue(child_id))) {
    state->GrantRequestOrigin(origin);
  }
}

void ChildProcessSecurityPolicyImpl::GrantCommitScheme(
    int child_id,
    const std::string& scheme) {
  base::AutoLock lock(lock_);

  // TODO(crbug.com/379869738) Remove FromUnsafeValue.
  if (auto* state = process_states_.GetProcessStateForMutation(
          ChildProcessId::FromUnsafeValue(child_id))) {
    state->GrantCommitScheme(scheme);
  }
}

void ChildProcessSecurityPolicyImpl::GrantRequestScheme(
    int child_id,
    const std::string& scheme) {
  base::AutoLock lock(lock_);

  // TODO(crbug.com/379869738) Remove FromUnsafeValue.
  if (auto* state = process_states_.GetProcessStateForMutation(
          ChildProcessId::FromUnsafeValue(child_id))) {
    state->GrantRequestScheme(scheme);
  }
}

void ChildProcessSecurityPolicyImpl::GrantWebUIBindings(
    int child_id,
    BindingsPolicySet bindings) {
  // Only WebUI bindings should come through here.
  CHECK(bindings.HasAny(kWebUIBindingsPolicySet));
  CHECK(Difference(bindings, kWebUIBindingsPolicySet).empty());

  base::AutoLock lock(lock_);

  // TODO(crbug.com/379869738) Remove FromUnsafeValue.
  if (auto* state = process_states_.GetProcessStateForMutation(
          ChildProcessId::FromUnsafeValue(child_id))) {
    state->GrantBindings(bindings);
  }
}

void ChildProcessSecurityPolicyImpl::GrantOriginCheckExemptionForWebView(
    int child_id,
    const url::Origin& origin) {
  base::AutoLock lock(lock_);

  // TODO(crbug.com/379869738) Remove FromUnsafeValue.
  if (auto* state = process_states_.GetProcessStateForMutation(
          ChildProcessId::FromUnsafeValue(child_id))) {
    state->GrantOriginCheckExemptionForWebView(origin);
  }
}

bool ChildProcessSecurityPolicyImpl::HasOriginCheckExemptionForWebView(
    int child_id,
    const url::Origin& origin) {
  base::AutoLock lock(lock_);

  // TODO(crbug.com/379869738) Remove FromUnsafeValue.
  auto* state = process_states_.GetProcessStateForQuery(
      ChildProcessId::FromUnsafeValue(child_id));
  if (!state) {
    return false;
  }

  return state->HasOriginCheckExemptionForWebView(origin);
}

bool ChildProcessSecurityPolicyImpl::CanRequestURL(int child_id,
                                                   const GURL& url) {
  // TODO(crbug.com/379869738): Remove this conversion when the public
  // ChildProcessSecurityPolicy API is migrated to ChildProcessId.
  return CanRequestURL(ChildProcessId::FromUnsafeValue(child_id), url);
}

bool ChildProcessSecurityPolicyImpl::CanRequestURL(ChildProcessId child_id,
                                                   const GURL& url) {
  if (!url.is_valid()) {
    return false;  // Can't request invalid URLs.
  }

  const std::string& scheme = url.GetScheme();

  // Every child process can request <about:blank>, <about:blank?foo>,
  // <about:blank/#foo> and <about:srcdoc>.
  //
  // URLs like <about:version>, <about:crash>, <view-source:...> shouldn't be
  // requestable by any child process.  Also, this case covers
  // <javascript:...>, which should be handled internally by the process and
  // not kicked up to the browser.
  if (IsPseudoScheme(scheme)) {
    return url.IsAboutBlank() || url.IsAboutSrcdoc();
  }

  // Blob and filesystem URLs require special treatment; validate the inner
  // origin they embed.
  if (url.SchemeIsBlob() || url.SchemeIsFileSystem()) {
    if (IsMalformedBlobUrl(url)) {
      return false;
    }

    url::Origin origin = url::Origin::Create(url);
    return origin.opaque() || CanRequestURL(child_id, GURL(origin.Serialize()));
  }

  if (IsWebSafeScheme(scheme)) {
    return true;
  }

  {
    base::AutoLock lock(lock_);

    if (auto* state = process_states_.GetProcessStateForQuery(child_id)) {
      // Otherwise, we consult the child process's security state to see if it
      // is allowed to request the URL.
      if (state->CanRequestURL(url)) {
        return true;
      }
    } else {
      return false;
    }
  }

  // If |url| has WebUI scheme, the process must usually be locked, unless
  // running in single-process mode. Since this is a check whether the process
  // can request |url|, the check must operate based on scheme because one WebUI
  // should be able to request subresources from another WebUI of the same
  // scheme.
  const auto& webui_schemes = URLDataManagerBackend::GetWebUISchemes();
  if (!RenderProcessHost::run_renderer_in_process() &&
      std::ranges::contains(webui_schemes, url.GetScheme())) {
    bool should_be_locked =
        GetContentClient()->browser()->DoesWebUIUrlRequireProcessLock(url);
    if (should_be_locked) {
      const ProcessLock lock = GetProcessLock(child_id);
      if (!lock.IsLockedToSite() || !lock.MatchesScheme(url.GetScheme())) {
        return false;
      }
    }
  }

  // Also allow URLs destined for ShellExecute and not the browser itself.
  return !GetContentClient()->browser()->IsHandledURL(url);
}

bool ChildProcessSecurityPolicyImpl::CanRedirectToURL(const GURL& url) {
  if (!url.is_valid()) {
    return false;  // Can't redirect to invalid URLs.
  }

  const std::string& scheme = url.GetScheme();

  // Can't redirect to error pages.
  if (scheme == kChromeErrorScheme) {
    return false;
  }

  if (IsPseudoScheme(scheme)) {
    // Redirects to a pseudo scheme (about, javascript, view-source, ...) are
    // not allowed. An exception is made for <about:blank> and its variations.
    return url.IsAboutBlank();
  }

  // Note about redirects and special URLs:
  // * data-url: Blocked by net::DataProtocolHandler::IsSafeRedirectTarget().
  // * filesystem-url: Blocked by
  // storage::FilesystemProtocolHandler::IsSafeRedirectTarget().
  // Depending on their inner origins and if the request is browser-initiated or
  // renderer-initiated, blob-urls might get blocked by CanCommitURL or in
  // DocumentLoader::RedirectReceived. If not blocked, a 'file not found'
  // response will be generated in net::BlobURLRequestJob::DidStart().

  return true;
}

bool ChildProcessSecurityPolicyImpl::CanCommitSchemeInAnyProcess(
    const std::string& scheme) {
  RUST_CPP_RETURN_FUNCTION(
      rust::child_process_security_policy::can_commit_scheme_in_any_process(
          scheme),
      CanCommitSchemeInAnyProcess_Cpp(scheme));
}

bool ChildProcessSecurityPolicyImpl::CanCommitSchemeInAnyProcess_Cpp(
    const std::string& scheme) {
  base::AutoLock schemes_lock(schemes_lock_);
  return schemes_okay_to_commit_in_any_process_.contains(scheme);
}

bool ChildProcessSecurityPolicyImpl::CanCommitURL(int child_id,
                                                  const GURL& url) {
  if (!url.is_valid()) {
    LogCanCommitUrlFailureReason("invalid_url");
    return false;  // Can't commit invalid URLs.
  }

  const std::string& scheme = url.GetScheme();

  // Of all the pseudo schemes, only about:blank and about:srcdoc are allowed to
  // commit.
  if (IsPseudoScheme(scheme)) {
    if (!url.IsAboutBlank() && !url.IsAboutSrcdoc()) {
      LogCanCommitUrlFailureReason("pseudo_scheme_non_blank_or_srcdoc");
      return false;
    } else {
      // TODO(crbug.com/324934416): Consider continuing with the checks below.
      return true;
    }
  }

  // Blob and filesystem URLs require special treatment; validate the inner
  // origin they embed.
  if (url.SchemeIsBlob() || url.SchemeIsFileSystem()) {
    if (IsMalformedBlobUrl(url)) {
      LogCanCommitUrlFailureReason("malformed_blob_url");
      return false;
    }

    // No need to log a failure reason here, because it will be logged in the
    // sole recursive call if that call returns false.
    url::Origin origin = url::Origin::Create(url);
    return origin.opaque() || CanCommitURL(child_id, GURL(origin.Serialize()));
  }

  // Allow data URLs to commit in any process. Note that the precursor origin
  // should be checked separately.
  if (url.SchemeIs(url::kDataScheme)) {
    return true;
  }

  // With site isolation, a URL from a site may only be committed in a process
  // dedicated to that site.  This check will ensure that |url| can't commit if
  // the process is locked to a different site.
  //
  // We skip this check specifically for the error page URL,
  // chrome-error://chromewebdata, because it can commit in any process (due to
  // a lack of subframe error page isolation) and because it is difficult to
  // compute its expected process lock. We still verify in the
  // state->CanCommitURL call below that the process has actually been granted
  // access to this URL, rather than just returning true for it.
  // TODO(crbug.com/379869738) Remove FromUnsafeValue.
  if (url != GURL(kUnreachableWebDataURL) &&
      !CanAccessMaybeOpaqueOrigin(ChildProcessId::FromUnsafeValue(child_id),
                                  url,
                                  false /* url_is_precursor_of_opaque_origin */,
                                  AccessType::kCanCommitNewOrigin)) {
    LogCanCommitUrlFailureReason("cannot_access_origin");
    return false;
  }

  {
    base::AutoLock lock(lock_);

    // Most schemes can commit in any process. Note that we check
    // CanCommitSchemeInAnyProcess() here, which is stricter than
    // IsWebSafeScheme().
    //
    // TODO(creis, nick): https://crbug.com/515309: The line below does not
    // enforce that http pages cannot commit in an extension process.
    if (CanCommitSchemeInAnyProcess(scheme)) {
      return true;
    }

    auto* state = process_states_.GetProcessStateForQuery(
        ChildProcessId::FromUnsafeValue(child_id));
    if (!state) {
      LogCanCommitUrlFailureReason("no_security_state_found");
      return false;
    }

    // Otherwise, we consult the child process's security state to see if it is
    // allowed to commit the URL.
    bool can_commit = state->CanCommitURL(url);
    if (!can_commit) {
      LogCanCommitUrlFailureReason("cpsp_state_cannot_commit_url");
    }
    return can_commit;
  }
}

bool ChildProcessSecurityPolicyImpl::CanReadFile(ChildProcessId child_id,
                                                 const base::FilePath& file) {
  return HasPermissionsForFile(child_id, file, READ_FILE_GRANT);
}

void ChildProcessSecurityPolicyImpl::GrantFileForBrowserUpload(
    const base::UnguessableToken& owner_token,
    const base::FilePath& file) {
  RUST_CPP_VOID_FUNCTION(
      rust::child_process_security_policy::grant_file_for_browser_upload(
          owner_token, file),
      GrantFileForBrowserUpload_Cpp(owner_token, file));
}

void ChildProcessSecurityPolicyImpl::GrantFileForBrowserUpload_Cpp(
    const base::UnguessableToken& owner_token,
    const base::FilePath& file) {
  base::AutoLock lock(lock_);
  browser_granted_files_[file].push_back(owner_token);
}

void ChildProcessSecurityPolicyImpl::RevokeFileForBrowserUpload(
    const base::UnguessableToken& owner_token) {
  RUST_CPP_VOID_FUNCTION(
      rust::child_process_security_policy::revoke_file_for_browser_upload(
          owner_token),
      RevokeFileForBrowserUpload_Cpp(owner_token));
}

void ChildProcessSecurityPolicyImpl::RevokeFileForBrowserUpload_Cpp(
    const base::UnguessableToken& owner_token) {
  base::AutoLock lock(lock_);
  for (auto it = browser_granted_files_.begin();
       it != browser_granted_files_.end();) {
    std::erase(it->second, owner_token);
    if (it->second.empty()) {
      browser_granted_files_.erase(it++);
    } else {
      it++;
    }
  }
}

bool ChildProcessSecurityPolicyImpl::CanReadFileForBrowserUpload(
    const base::FilePath& file) {
  RUST_CPP_RETURN_FUNCTION(
      rust::child_process_security_policy::can_read_file_for_browser_upload(
          file),
      CanReadFileForBrowserUpload_Cpp(file));
}

bool ChildProcessSecurityPolicyImpl::CanReadFileForBrowserUpload_Cpp(
    const base::FilePath& file) {
  base::AutoLock lock(lock_);
  return browser_granted_files_.contains(file);
}

bool ChildProcessSecurityPolicyImpl::CanReadAllFiles(
    ChildProcessId child_id,
    const std::vector<base::FilePath>& files) {
  return std::ranges::all_of(files,
                             [this, child_id](const base::FilePath& file) {
                               return CanReadFile(child_id, file);
                             });
}

bool ChildProcessSecurityPolicyImpl::CanReadRequestBody(
    ChildProcessId child_id,
    const storage::FileSystemContext* file_system_context,
    const scoped_refptr<network::ResourceRequestBody>& body) {
  if (!body) {
    return true;
  }

  for (const network::DataElement& element : *body->elements()) {
    switch (element.type()) {
      case network::DataElement::Tag::kFile:
        if (!CanReadFile(child_id,
                         element.As<network::DataElementFile>().path())) {
          return false;
        }
        break;

      case network::DataElement::Tag::kBytes:
        // Data is self-contained within |body| - no need to check access.
        break;

      case network::DataElement::Tag::kDataPipe:
        // Data is self-contained within |body| - no need to check access.
        break;

      default:
        // Fail safe - deny access.
        NOTREACHED();
    }
  }
  return true;
}

bool ChildProcessSecurityPolicyImpl::CanReadRequestBody(
    RenderProcessHost* process,
    const scoped_refptr<network::ResourceRequestBody>& body) {
  CHECK(process);
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  return CanReadRequestBody(
      process->GetID(), process->GetStoragePartition()->GetFileSystemContext(),
      body);
}

bool ChildProcessSecurityPolicyImpl::CanCreateReadWriteFile(
    int child_id,
    const base::FilePath& file) {
  // TODO(crbug.com/379869738) Remove FromUnsafeValue.
  return HasPermissionsForFile(ChildProcessId::FromUnsafeValue(child_id), file,
                               CREATE_READ_WRITE_FILE_GRANT);
}

bool ChildProcessSecurityPolicyImpl::CanReadFileSystem(
    int child_id,
    const std::string& filesystem_id) {
  // TODO(crbug.com/379869738) Remove FromUnsafeValue.
  return HasPermissionsForFileSystem(ChildProcessId::FromUnsafeValue(child_id),
                                     filesystem_id, READ_FILE_GRANT);
}

bool ChildProcessSecurityPolicyImpl::CanReadWriteFileSystem(
    int child_id,
    const std::string& filesystem_id) {
  // TODO(crbug.com/379869738) Remove FromUnsafeValue.
  return HasPermissionsForFileSystem(ChildProcessId::FromUnsafeValue(child_id),
                                     filesystem_id,
                                     READ_FILE_GRANT | WRITE_FILE_GRANT);
}

bool ChildProcessSecurityPolicyImpl::CanCopyIntoFileSystem(
    int child_id,
    const std::string& filesystem_id) {
  // TODO(crbug.com/379869738) Remove FromUnsafeValue.
  return HasPermissionsForFileSystem(ChildProcessId::FromUnsafeValue(child_id),
                                     filesystem_id, COPY_INTO_FILE_GRANT);
}

bool ChildProcessSecurityPolicyImpl::CanDeleteFromFileSystem(
    int child_id,
    const std::string& filesystem_id) {
  // TODO(crbug.com/379869738) Remove FromUnsafeValue.
  return HasPermissionsForFileSystem(ChildProcessId::FromUnsafeValue(child_id),
                                     filesystem_id, DELETE_FILE_GRANT);
}

bool ChildProcessSecurityPolicyImpl::HasPermissionsForFile(
    ChildProcessId child_id,
    const base::FilePath& file,
    int permissions) {
  base::AutoLock lock(lock_);
  return ChildProcessHasPermissionsForFile(child_id, file, permissions);
}

bool ChildProcessSecurityPolicyImpl::HasPermissionsForFileSystemFile(
    ChildProcessId child_id,
    const storage::FileSystemURL& filesystem_url,
    int permissions) {
  if (!filesystem_url.is_valid()) {
    return false;
  }

  if (filesystem_url.path().ReferencesParent()) {
    return false;
  }

  // Any write access is disallowed on the root path.
  if (storage::VirtualPath::IsRootPath(filesystem_url.path()) &&
      (permissions & ~READ_FILE_GRANT)) {
    return false;
  }

  if (filesystem_url.mount_type() == storage::kFileSystemTypeIsolated) {
    // When Isolated filesystems is overlayed on top of another filesystem,
    // its per-filesystem permission overrides the underlying filesystem
    // permissions).
    return HasPermissionsForFileSystem(
        child_id, filesystem_url.mount_filesystem_id(), permissions);
  }

  // If |filesystem_url.origin()| is not accessible in this process, then this
  // page should not be able to access or place content in that origin via the
  // filesystem API either.
  // TODO(crbug.com/379869738) Remove GetUnsafeValue.
  if (!CanAccessDataForOrigin(child_id.GetUnsafeValue(),
                              filesystem_url.origin())) {
    return false;
  }

  int found_policy = 0;
  if (!FindPermissionPolicyForFileSystemType(filesystem_url.type(),
                                             found_policy)) {
    return false;
  }

  if ((found_policy & storage::FILE_PERMISSION_READ_ONLY) &&
      permissions & ~READ_FILE_GRANT) {
    return false;
  }

  // Note that HasPermissionsForFile (called below) will internally acquire the
  // |lock_|, therefore the |lock_| has to be released before the call (since
  // base::Lock is not reentrant).
  if (found_policy & storage::FILE_PERMISSION_USE_FILE_PERMISSION) {
    return HasPermissionsForFile(child_id, filesystem_url.path(), permissions);
  }

  if (found_policy & storage::FILE_PERMISSION_SANDBOX) {
    return true;
  }

  return false;
}

bool ChildProcessSecurityPolicyImpl::FindPermissionPolicyForFileSystemType(
    storage::FileSystemType type,
    int& policy) {
  RUST_CPP_RETURN_FUNCTION(
      rust::child_process_security_policy::
          find_permissions_for_file_system_type(type, policy),
      FindPermissionPolicyForFileSystemType_Cpp(type, policy));
}

bool ChildProcessSecurityPolicyImpl::FindPermissionPolicyForFileSystemType_Cpp(
    storage::FileSystemType type,
    int& policy) {
  base::AutoLock lock(lock_);
  auto found = file_system_policy_map_.find(type);
  if (found == file_system_policy_map_.end()) {
    return false;
  }
  policy = found->second;
  return true;
}

bool ChildProcessSecurityPolicyImpl::CanReadFileSystemFile(
    ChildProcessId child_id,
    const storage::FileSystemURL& filesystem_url) {
  return HasPermissionsForFileSystemFile(child_id, filesystem_url,
                                         READ_FILE_GRANT);
}

bool ChildProcessSecurityPolicyImpl::CanWriteFileSystemFile(
    ChildProcessId child_id,
    const storage::FileSystemURL& filesystem_url) {
  return HasPermissionsForFileSystemFile(child_id, filesystem_url,
                                         WRITE_FILE_GRANT);
}

bool ChildProcessSecurityPolicyImpl::CanCreateFileSystemFile(
    ChildProcessId child_id,
    const storage::FileSystemURL& filesystem_url) {
  return HasPermissionsForFileSystemFile(child_id, filesystem_url,
                                         CREATE_NEW_FILE_GRANT);
}

bool ChildProcessSecurityPolicyImpl::CanCreateReadWriteFileSystemFile(
    ChildProcessId child_id,
    const storage::FileSystemURL& filesystem_url) {
  return HasPermissionsForFileSystemFile(child_id, filesystem_url,
                                         CREATE_READ_WRITE_FILE_GRANT);
}

bool ChildProcessSecurityPolicyImpl::CanCopyIntoFileSystemFile(
    ChildProcessId child_id,
    const storage::FileSystemURL& filesystem_url) {
  return HasPermissionsForFileSystemFile(child_id, filesystem_url,
                                         COPY_INTO_FILE_GRANT);
}

bool ChildProcessSecurityPolicyImpl::CanDeleteFileSystemFile(
    ChildProcessId child_id,
    const storage::FileSystemURL& filesystem_url) {
  return HasPermissionsForFileSystemFile(child_id, filesystem_url,
                                         DELETE_FILE_GRANT);
}

bool ChildProcessSecurityPolicyImpl::CanMoveFileSystemFile(
    ChildProcessId child_id,
    const storage::FileSystemURL& src_url,
    const storage::FileSystemURL& dest_url) {
  return HasPermissionsForFileSystemFile(child_id, dest_url,
                                         CREATE_NEW_FILE_GRANT) &&
         HasPermissionsForFileSystemFile(child_id, src_url, READ_FILE_GRANT) &&
         HasPermissionsForFileSystemFile(child_id, src_url, DELETE_FILE_GRANT);
}

bool ChildProcessSecurityPolicyImpl::CanCopyFileSystemFile(
    ChildProcessId child_id,
    const storage::FileSystemURL& src_url,
    const storage::FileSystemURL& dest_url) {
  return HasPermissionsForFileSystemFile(child_id, src_url, READ_FILE_GRANT) &&
         HasPermissionsForFileSystemFile(child_id, dest_url,
                                         COPY_INTO_FILE_GRANT);
}

bool ChildProcessSecurityPolicyImpl::HasWebUIBindings(int child_id) {
  base::AutoLock lock(lock_);

  // TODO(crbug.com/379869738) Remove FromUnsafeValue.
  if (auto* state = process_states_.GetProcessStateForQuery(
          ChildProcessId::FromUnsafeValue(child_id))) {
    return state->has_web_ui_bindings();
  }
  return false;
}

bool ChildProcessSecurityPolicyImpl::ChildProcessHasPermissionsForFile(
    ChildProcessId child_id,
    const base::FilePath& file,
    int permissions) {
  if (auto* state = process_states_.GetProcessStateForQuery(child_id)) {
    return state->HasPermissionsForFile(file, permissions);
  }
  return false;
}

size_t ChildProcessSecurityPolicyImpl::BrowsingInstanceIdCountForTesting(
    ChildProcessId child_id) {
  base::AutoLock lock(lock_);

  if (auto* process_state = process_states_.GetProcessStateForQuery(child_id)) {
    return process_state->browsing_instance_default_isolation_states().size();
  }
  return 0;
}

bool ChildProcessSecurityPolicyImpl::MatchesCommittedOriginForTesting(
    ChildProcessId child_id,
    const GURL& url,
    bool url_is_for_precursor_origin) {
  base::AutoLock lock(lock_);

  if (auto* process_state = process_states_.GetProcessStateForQuery(child_id)) {
    return process_state->MatchesCommittedOrigin(url,
                                                 url_is_for_precursor_origin);
  }
  return false;
}

CanCommitStatus ChildProcessSecurityPolicyImpl::CanCommitOriginAndUrl(
    int child_id,
    const IsolationContext& isolation_context,
    const UrlInfo& url_info) {
  DCHECK(url_info.origin.has_value());
  const url::Origin& origin = *url_info.origin;
  // First check whether the URL is allowed to commit, without considering the
  // origin. This involves scheme checks as well as CanAccessDataForOrigin.
  if (!CanCommitURL(child_id, url_info.url)) {
    // WebView's allow_universal_access_from_file_urls setting allows file
    // origins to access any other origin and bypass normal commit checks. When
    // this mode is enabled, RenderFrameHostImpl::ValidateURLAndOrigin returns
    // early before this function is called.
    //
    // However, there are also cases where WebView apps in the wild turn on this
    // mode, load one file:// document, then turn it off again and call
    // document.open on another file:// document, causing it to inherit a URL
    // that is not permitted by CanCommitURL anymore. We exempt these cases from
    // the CanCommitURL check specifically, by ignoring a failure if it occurs
    // in a file:// origin within a process which previously had universal
    // access. (This exemption could be done in ValidateURLAndOrigin alongside
    // the universal access check, but in practice no apps in the wild seem to
    // be failing any other types of validation, so doing it here is a narrower
    // exemption.) See https://crbug.com/326250356.
    bool exempt_due_to_webview_universal_access =
        (origin.scheme() == url::kFileScheme) &&
        HasOriginCheckExemptionForWebView(child_id, origin);
    if (!exempt_due_to_webview_universal_access) {
      return CanCommitStatus::CANNOT_COMMIT_URL;
    }
  }

  // Next check whether the origin resolved from the URL is allowed to commit.
  const url::Origin url_origin = url::Origin::Resolve(url_info.url, origin);
  if (!CanAccessOrigin(child_id, url_origin, AccessType::kCanCommitNewOrigin)) {
    // Check for special cases, like blob:null/ and data: URLs, where the
    // origin does not contain information to match against the process lock,
    // but using the whole URL can result in a process lock match.  Note that
    // the origin being committed in `url_info.origin` will not actually be
    // used when computing `expected_process_lock` below in many cases; see
    // https://crbug.com/1320402.
    const auto expected_process_lock =
        ProcessLock::Create(isolation_context, url_info);
    const ProcessLock& actual_process_lock = GetProcessLock(child_id);
    if (actual_process_lock == expected_process_lock) {
      return CanCommitStatus::CAN_COMMIT_ORIGIN_AND_URL;
    }

    return CanCommitStatus::CANNOT_COMMIT_URL;
  }

  // Finally check the origin on its own.
  if (!CanAccessOrigin(child_id, origin, AccessType::kCanCommitNewOrigin)) {
    return CanCommitStatus::CANNOT_COMMIT_ORIGIN;
  }

  // Ensure that the origin derived from |url| is consistent with |origin|.
  // Note: We can't use origin.IsSameOriginWith() here because opaque origins
  // with precursors may have different nonce values.
  const auto url_tuple_or_precursor_tuple =
      url_origin.GetTupleOrPrecursorTupleIfOpaque();
  const auto origin_tuple_or_precursor_tuple =
      origin.GetTupleOrPrecursorTupleIfOpaque();

  if (url_tuple_or_precursor_tuple.IsValid() &&
      origin_tuple_or_precursor_tuple.IsValid() &&
      origin_tuple_or_precursor_tuple != url_tuple_or_precursor_tuple) {
    // Allow a WebView specific exception for origins that have a data scheme.
    // WebView converts data: URLs into non-opaque data:// origins which is
    // different than what all other builds do. This causes the consistency
    // check to fail because we try to compare a data:// origin with an opaque
    // origin that contains precursor info.
    if (url_tuple_or_precursor_tuple.scheme() == url::kDataScheme &&
        url::AllowNonStandardSchemesForAndroidWebView()) {
      return CanCommitStatus::CAN_COMMIT_ORIGIN_AND_URL;
    }

    return CanCommitStatus::CANNOT_COMMIT_ORIGIN;
  }

  return CanCommitStatus::CAN_COMMIT_ORIGIN_AND_URL;
}

bool ChildProcessSecurityPolicyImpl::CanAccessDataForOrigin(
    int child_id,
    const url::Origin& origin) {
  return CanAccessOrigin(child_id, origin,
                         AccessType::kCanAccessDataForCommittedOrigin);
}

bool ChildProcessSecurityPolicyImpl::HostsOrigin(int child_id,
                                                 const url::Origin& origin) {
  return CanAccessOrigin(child_id, origin, AccessType::kHostsOrigin);
}

bool ChildProcessSecurityPolicyImpl::CanAccessOrigin(int child_id,
                                                     const url::Origin& origin,
                                                     AccessType access_type) {
  GURL url_to_check;
  if (origin.opaque()) {
    auto precursor_tuple = origin.GetTupleOrPrecursorTupleIfOpaque();
    if (!precursor_tuple.IsValid()) {
      // Allow opaque origins w/o precursors (if the process state exists).
      // TODO(acolwell): Investigate all cases that trigger this path (e.g.,
      // browser-initiated navigations to data: URLs) and fix them so we have
      // precursor information (or the process lock is compatible with a missing
      // precursor). Remove this logic once that has been completed.
      base::AutoLock lock(lock_);
      // TODO(crbug.com/379869738) Remove FromUnsafeValue.
      const ProcessState* process_state =
          process_states_.GetProcessStateForQuery(
              ChildProcessId::FromUnsafeValue(child_id));
      return !!process_state;
    } else {
      url_to_check = precursor_tuple.GetURL();
    }
  } else {
    url_to_check = origin.GetURL();
  }
  bool success =
      CanAccessMaybeOpaqueOrigin(ChildProcessId::FromUnsafeValue(child_id),
                                 url_to_check, origin.opaque(), access_type);
  if (success) {
    return true;
  }

  // Note: LogCanAccessDataForOriginCrashKeys() is called in the
  // CanAccessDataForOrigin() call above. The code below overrides the origin
  // crash key set in that call with data from |origin| because it provides
  // more accurate information than the origin derived from |url_to_check|.
  auto* requested_origin_key = GetRequestedOriginCrashKey();
  base::debug::SetCrashKeyString(requested_origin_key, origin.GetDebugString());
  return false;
}

bool ChildProcessSecurityPolicyImpl::IsAccessAllowedForSandboxedProcess(
    const ProcessLock& process_lock,
    const GURL& url,
    bool url_is_for_opaque_origin,
    AccessType access_type) {
  switch (access_type) {
    case AccessType::kCanCommitNewOrigin:
      // TODO(crbug.com/325410297): Sandboxed frames may commit normal URLs, as
      // long as they commit them with an opaque origin. However, some existing
      // code paths leading here, such as CanCommitURL() and
      // CanCommitOriginAndUrl(), do not indicate anything about the future
      // origin being opaque. For now, don't restrict URLs from committing in
      // sandboxed processes here, but eventually this should be strengthened
      // by plumbing in the correct value for `url_is_for_opaque_origin` from
      // code paths like CanCommitURL().
      return true;
    case AccessType::kHostsOrigin:
      // Sandboxed frame processes should only be able to host opaque origins,
      // and only those origins should ever be used as a source or initiator
      // origin in things like postMessage.
      return url_is_for_opaque_origin;
    case AccessType::kCanAccessDataForCommittedOrigin:
      // Sandboxed frames should never access passwords, storage, or other data
      // for any origin.
      return false;
  }
}

bool ChildProcessSecurityPolicyImpl::IsAccessAllowedForPdfProcess(
    AccessType access_type) {
  // PDF processes are allowed to commit normal URLs, and they should be able to
  // claim that they host a regular origin for things like verifying source
  // origins for postMessage. However, PDF renderers should never need to access
  // passwords, storage, or other data for the PDF document's origin or any
  // other origin.
  switch (access_type) {
    case AccessType::kCanCommitNewOrigin:
    case AccessType::kHostsOrigin:
      return true;
    case AccessType::kCanAccessDataForCommittedOrigin:
      return false;
  }
}

bool ChildProcessSecurityPolicyImpl::PerformJailAndCitadelChecks(
    ChildProcessId child_id,
    const ProcessState& process_state,
    const GURL& url,
    bool url_is_precursor_of_opaque_origin,
    ProcessLock& out_expected_process_lock,
    std::string& out_failure_reason) {
  // Ensure this is only called on the UI thread, which is the only thread
  // with sufficient information to do the full set of checks.
  CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));

  ProcessLock actual_process_lock = process_state.process_lock();

  BrowserContext* browser_context = process_state.browser_context();
  // The caller ensures that the `browser_context` is valid.
  CHECK(browser_context);

  // Loop over all BrowsingInstanceIDs in the ProcessState, and return true if
  // any of them would return true, otherwise return false. This allows the
  // checks to be slightly stricter in cases where all BrowsingInstances agree
  // (e.g., whether an origin is considered isolated and thus inaccessible from
  // a site-locked process).  When the BrowsingInstances do not agree, the check
  // might be slightly weaker (as the least common denominator), but the
  // differences must never violate the ProcessLock.
  if (process_state.browsing_instance_default_isolation_states().empty()) {
    // If no BrowsingInstances are found, then the some of the state we need to
    // perform an accurate check is unexpectedly missing, because there should
    // always be a BrowsingInstance for such requests, even from workers. Thus,
    // we should usually kill the process in this case, so that a compromised
    // renderer can't bypass checks by sending IPCs when no BrowsingInstances
    // are left.
    //
    // However, if the requested `url` is compatible with the current
    // ProcessLock, then there is no need to kill the process because the checks
    // would have passed anyway. To reduce the number of crashes while we debug
    // why no BrowsingInstances were found (in https://crbug.com/1148542), we'll
    // allow requests with an acceptable process lock to proceed.
    // TODO(crbug.com/40731345): Remove this when known cases of having no
    // BrowsingInstance IDs are solved.
    url::Origin origin(url::Origin::Create(url));
    bool matches_origin_keyed_process =
        actual_process_lock.agent_cluster_key().IsOriginKeyed() &&
        actual_process_lock.agent_cluster_key().GetOrigin().IsSameOriginWith(
            origin);
    bool matches_site_keyed_process =
        actual_process_lock.agent_cluster_key().IsSiteKeyed() &&
        actual_process_lock.agent_cluster_key().GetSite() ==
            SiteInfo::GetSiteForOrigin(origin);
    // ProcessLocks with is_pdf() = true actually means that the process is not
    // supposed to access certain resources from the lock's site/origin, so it's
    // safest here to fall through in that case. See discussion of
    // https://crbug.com/1271197 below.
    if (!actual_process_lock.is_pdf()) {
      // If the ProcessLock isn't locked to a site, we should fall through since
      // we have no way of knowing if the requested url was expecting to be in a
      // locked process.
      if (actual_process_lock.IsLockedToSite()) {
        if (matches_origin_keyed_process || matches_site_keyed_process) {
          return true;
        } else {
          out_failure_reason = base::StringPrintf(
              "No BrowsingInstanceIDs: Lock Mismatch. lock = %s vs. "
              "requested_url = %s ",
              actual_process_lock.ToString().c_str(), url.spec().c_str());
        }
      } else {
        out_failure_reason =
            "No BrowsingInstanceIDs: process not locked to site";
      }
    } else {
      out_failure_reason = "No BrowsingInstanceIDs: process lock is_pdf";
    }
    return false;
  }

  for (auto browsing_instance_info_entry :
       process_state.browsing_instance_default_isolation_states()) {
    auto& browsing_instance_id = browsing_instance_info_entry.first;
    auto& default_isolation_state = browsing_instance_info_entry.second;
    // In the case of multiple BrowsingInstances in the ProcessState, note that
    // failure reasons will only be reported if none of the BrowsingInstances
    // allow access. In that event, |failure_reason| contains the concatenated
    // reasons for each BrowsingInstance, each prefaced by its id.
    out_failure_reason +=
        base::StringPrintf("[BI=%d]", browsing_instance_id.GetUnsafeValue());

    // Use the actual process lock's state to compute `is_guest` and `is_fenced`
    // for the expected process lock's `isolation_context`. Guest status and
    // fenced status doesn't currently influence the outcome of this access
    // check, and even if it did, `url` wouldn't be sufficient to tell whether
    // the request belongs solely to a guest (or non-guest) or fenced process.
    // Note that a guest isn't allowed to access data outside of its own
    // StoragePartition, but this is enforced by other means (e.g., resource
    // access APIs can't name an alternate StoragePartition).
    IsolationContext isolation_context(
        browsing_instance_id, browser_context, actual_process_lock.is_guest(),
        actual_process_lock.is_fenced(), default_isolation_state);

    // NOTE: If we're on the IO thread, the call to ProcessLock::Create() below
    // will return a ProcessLock with an (internally) identical site_url, one
    // that does not use effective URLs. That's ok in this instance since we
    // only ever look at the lock url.
    //
    // Since we are dealing with a valid ProcessLock at this point, we know the
    // lock contains a valid StoragePartitionConfig and COOP/COEP information
    // because that information must be provided when creating the locks.
    //
    // At this point, any origin opt-in isolation requests should be complete,
    // so to avoid the possibility of opting something set
    // |origin_isolation_state_request| to nullopt. This is done in the
    // constructor. If the navigation requested an OAC opt-in or opt-out, it has
    // already been registered and will be picked up when creating the SiteInfo.
    // Note: We might ned to revisit this if CanAccessDataForOrigin() needs to
    // be called while a SiteInstance is being determined for a navigation, i.e.
    // during GetSiteInstanceForNavigationRequest().  If this happens, we'd need
    // to plumb UrlInfo::origin_isolation_state_request value from the ongoing
    // NavigationRequest into here. Also, we would likely need to attach the
    // BrowsingInstanceID to UrlInfo once the SiteInstance has been determined
    // in case the RenderProcess has multiple BrowsingInstances in it.
    // TODO(acolwell): Provide a way for callers, that know their request's
    // require COOP/COEP handling, to pass in their COOP/COEP information so it
    // can be used here instead of the values in |actual_process_lock|.
    // TODO(crbug.com/40205612): The code below is subtly incorrect in cases
    // where actual_process_lock.is_pdf() is true, since in the case of PDFs the
    // lock is intended to prevent access to the lock's site/origin, while still
    // allowing the navigation to commit.
    out_expected_process_lock = ProcessLock::Create(
        isolation_context,
        UrlInfo(
            UrlInfoInit(url)
                .WithStoragePartitionConfig(
                    actual_process_lock.GetStoragePartitionConfig())
                .WithWebExposedIsolationInfo(
                    actual_process_lock.GetWebExposedIsolationInfo())
                .WithEmbedderIsolationInfo(
                    actual_process_lock.embedder_isolation_info())
                .WithSandbox(actual_process_lock.is_sandboxed())
                .WithUniqueSandboxId(actual_process_lock.unique_sandbox_id())
                .WithIsAdTaggedForSiteKeying(
                    actual_process_lock.agent_cluster_key().oac_status() ==
                    AgentClusterKey::OACStatus::kSiteKeyedByDefault)
                .WithCrossOriginIsolationKey(
                    actual_process_lock.agent_cluster_key()
                        .GetCrossOriginIsolationKey())));

    if (actual_process_lock.IsLockedToSite()) {
      // Jail-style enforcement - a process with a lock can only access data
      // from origins that require exactly the same lock.
      if (actual_process_lock == out_expected_process_lock) {
        return true;
      }

      // TODO(acolwell, nasko): https://crbug.com/1029092: Ensure the precursor
      // of opaque origins matches the renderer's origin lock.
      if (url_is_precursor_of_opaque_origin) {
        const GURL& lock_url = actual_process_lock.GetProcessLockURL();
        // SitePerProcessBrowserTest.TwoBlobURLsWithNullOriginDontShareProcess.
        if (lock_url.SchemeIsBlob() &&
            base::StartsWith(lock_url.path(), "null/")) {
          return true;
        }

        // DeclarativeApiTest.PersistRules.
        if (actual_process_lock.MatchesScheme(url::kDataScheme)) {
          return true;
        }
      }

      // Make an exception to allow most visited tiles to commit in third-party
      // NTP processes.
      // TODO(crbug.com/40447789): This exception should be removed once these
      // tiles can be loaded in OOPIFs on the NTP.
      if (AllowProcessLockMismatchForNTP(out_expected_process_lock,
                                         actual_process_lock)) {
        return true;
      }

      // TODO(wjmaclean): We should update the ProcessLock comparison API to
      // return a reason why two locks differ.
      if (actual_process_lock.agent_cluster_key() !=
          out_expected_process_lock.agent_cluster_key()) {
        out_failure_reason += "lock_mismatch:agent_cluster_key ";
        if (actual_process_lock.agent_cluster_key().IsSiteKeyed() !=
            out_expected_process_lock.agent_cluster_key().IsSiteKeyed()) {
          out_failure_reason += "[origin-keyed vs site-keyed mismatch] ";
        } else if (actual_process_lock.agent_cluster_key().IsSiteKeyed() &&
                   out_expected_process_lock.agent_cluster_key()
                       .IsSiteKeyed()) {
          // If the actual lock is same-site to the expected lock, then this is
          // an isolated origins mismatch; in that case we add text to
          // |failure_reason| to make this case easy to search for. Note: We
          // don't compare ports, since the mismatch might be between isolated
          // and non-isolated.
          url::Origin actual_origin = url::Origin::Create(
              actual_process_lock.agent_cluster_key().GetSite());
          url::Origin expected_origin = url::Origin::Create(
              out_expected_process_lock.agent_cluster_key().GetSite());
          if (actual_process_lock.agent_cluster_key().GetSite() ==
                  SiteInfo::GetSiteForOrigin(expected_origin) ||
              out_expected_process_lock.agent_cluster_key().GetSite() ==
                  SiteInfo::GetSiteForOrigin(actual_origin)) {
            out_failure_reason += "[origin vs site mismatch] ";
          }
        }
      } else {
        // TODO(wjmaclean,alexmos): Apparently this might not be true anymore,
        // since is_pdf() and web_exposed_isolation_info() have been added to
        // the ProcessLock. We need to update the code here to differentiate
        // these cases, as well as adding documentation (or some other
        // mechanism) to prevent these getting out of sync in future.
        out_failure_reason += "lock_mismatch:requires_origin_keyed_process ";
      }
    } else {
      // Citadel-style enforcement - an unlocked process should not be able to
      // access data from origins that require a lock.

      RenderProcessHost* process = RenderProcessHostImpl::FromID(child_id);
      if (process) {  // |process| can be null in unittests
        // Unlocked process can be legitimately used when navigating from an
        // unused process (about:blank, NTP on Android) to an isolated origin.
        // See also https://crbug.com/945399.  Returning |true| below will allow
        // such navigations to succeed (i.e. pass CanCommitOriginAndUrl checks).
        // We don't expect unused processes to be used outside of navigations
        // (e.g. when checking CanAccessDataForOrigin for localStorage, etc.).
        if (process->IsUnused()) {
          return true;
        }
      }

      // See the ProcessLock::Create() call above regarding why we pass kNone
      // for |origin_isolation_request| below.
      SiteInfo site_info = SiteInfo::Create(
          isolation_context,
          UrlInfo(UrlInfoInit(url).WithWebExposedIsolationInfo(
              actual_process_lock.GetWebExposedIsolationInfo())));

      // A process that's not locked to any site can only access data from
      // origins that do not require a locked process.
      if (!site_info.ShouldLockProcessToSite(isolation_context)) {
        return true;
      }

      out_failure_reason += " citadel_enforcement ";
      if (url_is_precursor_of_opaque_origin) {
        out_failure_reason += "for_precursor ";
      }

      // TODO(crbug.com/326251583): Log additional information for diagnosing
      // the bug. Remove once the investigation is complete.
      bool requires_origin_keyed_process =
          site_info.oac_status() ==
              AgentClusterKey::OACStatus::kOriginKeyedByHeader ||
          site_info.oac_status() ==
              AgentClusterKey::OACStatus::kOriginKeyedByDefault;
      if (site_info.RequiresDedicatedProcess(isolation_context)) {
        out_failure_reason += "dedicated ";
        if (SiteIsolationPolicy::UseDedicatedProcessesForAllSites()) {
          out_failure_reason += "spp ";
        }
        if (site_info.does_site_request_dedicated_process_for_coop()) {
          out_failure_reason += "coop ";
        }
        if (requires_origin_keyed_process) {
          out_failure_reason += "oac ";
        }
        if (site_info.IsSandboxed()) {
          out_failure_reason += "sandbox ";
        }
        if (site_info.is_error_page()) {
          out_failure_reason += "error ";
        }
        if (site_info.is_pdf()) {
          out_failure_reason += "pdf ";
        }
        if (IsIsolatedOrigin(isolation_context,
                             url::Origin::Create(site_info.site_url()),
                             requires_origin_keyed_process)) {
          out_failure_reason += "io ";
        }
      }
      out_failure_reason +=
          "site=" + site_info.site_url().possibly_invalid_spec();
      out_failure_reason +=
          " next_bi=" +
          base::NumberToString(
              SiteInstanceImpl::NextBrowsingInstanceId().GetUnsafeValue());
      out_failure_reason +=
          " dis_oac=" + base::NumberToString(
                            default_isolation_state.is_origin_agent_cluster());
      out_failure_reason +=
          " dis_rokp=" +
          base::NumberToString(
              default_isolation_state.requires_origin_keyed_process()) +
          " ";
    }
  }

  return false;
}

bool ChildProcessSecurityPolicyImpl::CanAccessMaybeOpaqueOrigin(
    ChildProcessId child_id,
    const GURL& url,
    bool url_is_precursor_of_opaque_origin,
    AccessType access_type) {
  base::AutoLock lock(lock_);

  const ProcessState* process_state =
      process_states_.GetProcessStateForQuery(child_id);
  ProcessLock expected_process_lock;
  std::string failure_reason;

  if (!process_state) {
    failure_reason = "no_security_state";
  } else {
    ProcessLock actual_process_lock = process_state->process_lock();

    if (actual_process_lock.is_invalid()) {
      // Deny access if the process is unlocked. An unlocked process means
      // that the process has not been associated with a SiteInstance yet
      // and therefore this request is likely invalid.
      failure_reason = "process_lock_is_invalid";
    } else if (actual_process_lock.is_sandboxed() &&
               !IsAccessAllowedForSandboxedProcess(
                   actual_process_lock, url, url_is_precursor_of_opaque_origin,
                   access_type)) {
      failure_reason = "sandboxing_restrictions";
    } else if (actual_process_lock.is_pdf() &&
               !IsAccessAllowedForPdfProcess(access_type)) {
      failure_reason = "pdf_restrictions";
    } else {
      switch (access_type) {
        case AccessType::kHostsOrigin:
        case AccessType::kCanAccessDataForCommittedOrigin:
          // For checking kHostsOrigin or kCanAccessDataForOrigin access types,
          // we can use a simpler check based on tracking the list of committed
          // origins.
          //
          // Note that it's important to perform this check *after* the PDF and
          // sandboxing restrictions above, since those checks may deny access
          // even for origins that have previously committed in a process. In
          // other words, PDF and sandboxed processes should never be allowed to
          // access data, even to their own committed origins.
          if (process_state->MatchesCommittedOrigin(
                  url, url_is_precursor_of_opaque_origin)) {
            return true;
          }
          failure_reason = "no_matching_committed_origin";
          break;
        case AccessType::kCanCommitNewOrigin:
          // If we couldn't use committed origin enforcements (i.e., for
          // kCanCommitNewOrigin checks), Jail and Citadel checks are the source
          // of truth. If they don't pass, collect crash keys below before
          // returning false. Unlike committed origin enforcements, these checks
          // require BrowserContext to still exist in the ProcessState, and can
          // only run on the UI thread.
          if (!process_state->browser_context()) {
            failure_reason = "no_browser_context";
          } else if (PerformJailAndCitadelChecks(
                         child_id, *process_state, url,
                         url_is_precursor_of_opaque_origin,
                         expected_process_lock, failure_reason)) {
            return true;
          }
          break;
      }
    }
  }

  // Record the duration of KeepAlive requests to include in the crash keys.
  std::string keep_alive_durations;
  std::string shutdown_delay_ref_count;
  std::string process_rfh_count;
  if (BrowserThread::CurrentlyOn(BrowserThread::UI)) {
    if (auto* process = RenderProcessHostImpl::FromID(child_id)) {
      keep_alive_durations = process->GetKeepAliveDurations();
      shutdown_delay_ref_count =
          base::NumberToString(process->GetShutdownDelayRefCount());
      process_rfh_count =
          base::NumberToString(process->GetRenderFrameHostCount());
    }
  } else {
    keep_alive_durations = "no durations available: on IO thread.";
  }

  // Returning false here will often result in a renderer kill.  Set some crash
  // keys that will help understand the circumstances of that kill.
  // TODO(crbug.com/476412562): Find a way to scope these keys so that they do
  // not appear in later unrelated crash reports.
  LogCanAccessDataForOriginCrashKeys(
      expected_process_lock.ToString(),
      GetKilledProcessOriginLock(process_state),
      url.DeprecatedGetOriginAsURL().spec(), failure_reason,
      keep_alive_durations, shutdown_delay_ref_count, process_rfh_count,
      GetCommittedOriginsForCrashKey(process_state),
      AccessTypeToString(access_type));
  if (failure_reason == "no_security_state") {
    // For the time being, log a crash report in this case with the crash keys
    // above, to help diagnose any unknown causes.
    // TODO(crbug.com/476409377): Once this case is fixed in practice and
    // upgraded to a browser crash, move this call closer to where the missing
    // ProcessState was detected.
    NoChildProcessSecurityPolicySecurityStateFound();
  }

  return false;
}

void ChildProcessSecurityPolicyImpl::IncludeIsolationContext(
    int child_id,
    const IsolationContext& isolation_context) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  base::AutoLock lock(lock_);
  // TODO(crbug.com/379869738) Remove FromUnsafeValue.
  auto* state = process_states_.GetProcessStateForMutation(
      ChildProcessId::FromUnsafeValue(child_id));
  DCHECK(state);
  state->AddBrowsingInstanceInfo(isolation_context);
}

void ChildProcessSecurityPolicyImpl::LockProcess(
    const IsolationContext& context,
    ChildProcessId child_id,
    bool is_process_used,
    const ProcessLock& process_lock) {
  // LockProcess should only be called on the UI thread (OTOH, it is okay to
  // call GetProcessLock from any thread).
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  base::AutoLock lock(lock_);
  auto* state = process_states_.GetProcessStateForMutation(child_id);
  CHECK(state);
  state->SetProcessLock(process_lock, context, is_process_used);
}

void ChildProcessSecurityPolicyImpl::LockProcessForTesting(
    const IsolationContext& isolation_context,
    ChildProcessId child_id,
    const GURL& url) {
  SiteInfo site_info = SiteInfo::CreateForTesting(isolation_context, url);
  LockProcess(isolation_context, child_id, /* is_process_used=*/false,
              ProcessLock::FromSiteInfo(site_info));
}

ProcessLock ChildProcessSecurityPolicyImpl::GetProcessLock(
    ChildProcessId child_id) {
  base::AutoLock lock(lock_);
  if (auto* state = process_states_.GetProcessStateForQuery(child_id)) {
    return state->process_lock();
  }
  return ProcessLock();
}

ProcessLock ChildProcessSecurityPolicyImpl::GetProcessLock(int child_id) {
  return GetProcessLock(ChildProcessId::FromUnsafeValue(child_id));
}

void ChildProcessSecurityPolicyImpl::GrantPermissionsForFileSystem(
    ChildProcessId child_id,
    const std::string& filesystem_id,
    int permission) {
  base::AutoLock lock(lock_);

  if (auto* state = process_states_.GetProcessStateForMutation(child_id)) {
    state->GrantPermissionsForFileSystem(filesystem_id, permission);
  }
}

bool ChildProcessSecurityPolicyImpl::HasPermissionsForFileSystem(
    ChildProcessId child_id,
    const std::string& filesystem_id,
    int permission) {
  base::AutoLock lock(lock_);

  if (auto* state = process_states_.GetProcessStateForQuery(child_id)) {
    return state->HasPermissionsForFileSystem(filesystem_id, permission);
  }
  return false;
}

void ChildProcessSecurityPolicyImpl::RegisterFileSystemPermissionPolicy(
    storage::FileSystemType type,
    int policy) {
  RUST_CPP_VOID_FUNCTION(
      rust::child_process_security_policy::
          register_file_system_permission_policy(type, policy),
      RegisterFileSystemPermissionPolicy_Cpp(type, policy));
}

void ChildProcessSecurityPolicyImpl::RegisterFileSystemPermissionPolicy_Cpp(
    storage::FileSystemType type,
    int policy) {
  base::AutoLock lock(lock_);
  file_system_policy_map_[type] = policy;
}

bool ChildProcessSecurityPolicyImpl::CanSendMidiMessage(
    ChildProcessId child_id) {
  RUST_CPP_PROCESS_STATE_RETURN_FUNCTION(
      rust::child_process_security_policy::can_send_midi_message(child_id),
      CanSendMidiMessage_Cpp(child_id));
}

bool ChildProcessSecurityPolicyImpl::CanSendMidiMessage_Cpp(
    ChildProcessId child_id) {
  base::AutoLock lock(lock_);

  if (auto* state = process_states_.GetProcessStateForQuery(child_id)) {
    return state->CanSendMidi();
  }
  return false;
}

bool ChildProcessSecurityPolicyImpl::CanSendMidiSysExMessage(
    ChildProcessId child_id) {
  RUST_CPP_PROCESS_STATE_RETURN_FUNCTION(
      rust::child_process_security_policy::can_send_midi_sysex_message(
          child_id),
      CanSendMidiSysExMessage_Cpp(child_id));
}

bool ChildProcessSecurityPolicyImpl::CanSendMidiSysExMessage_Cpp(
    ChildProcessId child_id) {
  base::AutoLock lock(lock_);

  if (auto* state = process_states_.GetProcessStateForQuery(child_id)) {
    return state->CanSendMidiSysEx();
  }
  return false;
}

void ChildProcessSecurityPolicyImpl::AddFutureIsolatedOrigins(
    const std::vector<url::Origin>& origins_to_add,
    IsolatedOriginSource source,
    BrowserContext* browser_context) {
  std::vector<IsolatedOriginPattern> patterns;
  patterns.reserve(origins_to_add.size());
  std::ranges::transform(
      origins_to_add, std::back_inserter(patterns),
      [](const url::Origin& o) { return IsolatedOriginPattern(o); });
  AddFutureIsolatedOrigins(patterns, source, browser_context);
}

void ChildProcessSecurityPolicyImpl::AddFutureIsolatedOrigins(
    std::string_view origins_to_add,
    IsolatedOriginSource source,
    BrowserContext* browser_context) {
  std::vector<IsolatedOriginPattern> patterns =
      ParseIsolatedOrigins(origins_to_add);
  AddFutureIsolatedOrigins(patterns, source, browser_context);
}

void ChildProcessSecurityPolicyImpl::AddFutureIsolatedOrigins(
    const std::vector<IsolatedOriginPattern>& patterns,
    IsolatedOriginSource source,
    BrowserContext* browser_context) {
  // This can only be called from the UI thread, as it reads state that's only
  // available (and is only safe to be retrieved) on the UI thread, such as
  // BrowsingInstance IDs.
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  base::AutoLock isolated_origins_lock(isolated_origins_lock_);

  for (const IsolatedOriginPattern& pattern : patterns) {
    if (!pattern.is_valid()) {
      LOG(ERROR) << "Invalid isolated origin: " << pattern.pattern();
      continue;
    }

    url::Origin origin_to_add = pattern.origin();

    // Isolated origins added here should apply only to future
    // BrowsingInstances and processes.  Determine the first BrowsingInstance
    // ID to which they should apply.
    BrowsingInstanceId browsing_instance_id =
        SiteInstanceImpl::NextBrowsingInstanceId();

    AddIsolatedOriginInternal(browser_context, origin_to_add,
                              true /* applies_to_future_browsing_instances */,
                              browsing_instance_id,
                              pattern.isolate_all_subdomains(), source);
  }
}

void ChildProcessSecurityPolicyImpl::AddIsolatedOriginInternal(
    BrowserContext* browser_context,
    const url::Origin& origin_to_add,
    bool applies_to_future_browsing_instances,
    BrowsingInstanceId browsing_instance_id,
    bool isolate_all_subdomains,
    IsolatedOriginSource source) {
  // GetSiteForOrigin() is used to look up the site URL of |origin| to speed
  // up the isolated origin lookup.  This only performs a straightforward
  // translation of an origin to eTLD+1; it does *not* take into account
  // effective URLs, isolated origins, and other logic that's not needed
  // here, but *is* typically needed for making process model decisions. Be
  // very careful about using GetSiteForOrigin() elsewhere, and consider
  // whether you should be using SiteInfo::Create() instead.
  GURL key(SiteInfo::GetSiteForOrigin(origin_to_add));

  base::UnguessableToken browser_context_id =
      browser_context ? browser_context->UniqueToken()
                      : base::UnguessableToken::Null();

  // Check if the origin to be added already exists, in which case it may not
  // need to be added again.
  bool should_add = true;
  for (const auto& entry : isolated_origins_[key]) {
    // TODO(alexmos): The exact origin comparison here allows redundant entries
    // with certain uses of `isolate_all_subdomains`.  See
    // https://crbug.com/1184580.
    if (entry.origin() != origin_to_add) {
      continue;
    }
    // If the added origin already exists for the same BrowserContext and
    // covers the same BrowsingInstances, don't re-add it.
    if (entry.browser_context_id() == browser_context_id) {
      if (entry.applies_to_future_browsing_instances() &&
          entry.browsing_instance_id() <= browsing_instance_id) {
        // If the existing entry applies to future BrowsingInstances, and it
        // has a lower/same BrowsingInstance ID, don't re-add the origin.  Note
        // that if the new isolated origin is also requested to apply to future
        // BrowsingInstances, the threshold ID must necessarily be greater than
        // the old ID, since NextBrowsingInstanceId() returns monotonically
        // increasing IDs.
        if (applies_to_future_browsing_instances) {
          DCHECK_LE(entry.browsing_instance_id(), browsing_instance_id);
        }
        should_add = false;
        break;
      } else if (!entry.applies_to_future_browsing_instances() &&
                 entry.browsing_instance_id() == browsing_instance_id) {
        // Otherwise, don't re-add the origin if the existing entry is for the
        // same BrowsingInstance ID.  Note that if an origin had been added for
        // a specific BrowsingInstance, we can't later receive a request to
        // isolate that origin within future BrowsingInstances that start at
        // the same (or lower) BrowsingInstance. Requests to isolate future
        // BrowsingInstances should always reference
        // SiteInstanceImpl::NextBrowsingInstanceId(), which always refers to
        // an ID that's greater than any existing BrowsingInstance ID.
        DCHECK(!applies_to_future_browsing_instances);

        should_add = false;
        break;
      }
    }

    // Otherwise, allow the origin to be added again for a different profile
    // (or globally for all profiles), possibly with a different
    // BrowsingInstance ID cutoff.  Note that a particular origin might have
    // multiple entries, each one for a different profile, so we must loop
    // over all such existing entries before concluding that |origin| really
    // needs to be added.
  }

  if (should_add) {
    IsolatedOriginEntry entry(std::move(origin_to_add),
                              applies_to_future_browsing_instances,
                              browsing_instance_id, browser_context_id,
                              isolate_all_subdomains, source);
    isolated_origins_[key].emplace_back(std::move(entry));
  }
}

void ChildProcessSecurityPolicyImpl::RemoveStateForBrowserContext(
    const BrowserContext& browser_context) {
  {
    base::AutoLock isolated_origins_lock(isolated_origins_lock_);
    const base::UnguessableToken browser_context_id =
        browser_context.UniqueToken();

    for (auto& iter : isolated_origins_) {
      std::erase_if(iter.second,
                    [&browser_context_id](const IsolatedOriginEntry& entry) {
                      // Remove if BrowserContext matches.
                      return (entry.browser_context_id() == browser_context_id);
                    });
    }

    // Also remove map entries for site URLs which no longer have any
    // IsolatedOriginEntries remaining.
    base::EraseIf(isolated_origins_,
                  [](const auto& pair) { return pair.second.empty(); });
  }

  RemoveOriginAgentClusterRequestsForBrowserContext(browser_context);

  {
    base::AutoLock lock(lock_);
    process_states_.ClearBrowserContextIfMatches(browser_context);
  }
}

void ChildProcessSecurityPolicyImpl::
    RemoveOriginAgentClusterRequestsForBrowserContext(
        const BrowserContext& browser_context) {
  RUST_CPP_VOID_FUNCTION(
      rust::child_process_security_policy::
          remove_origin_agent_cluster_requests_for_browser_context(
              browser_context.UniqueToken()),
      RemoveOriginAgentClusterRequestsForBrowserContext_Cpp(browser_context));
}

void ChildProcessSecurityPolicyImpl::
    RemoveOriginAgentClusterRequestsForBrowserContext_Cpp(
        const BrowserContext& browser_context) {
  base::AutoLock origin_agent_cluster_lock(origin_agent_cluster_lock_);
  origin_agent_cluster_opt_ins_and_outs_.erase(browser_context.UniqueToken());
}

void ChildProcessSecurityPolicyImpl::ProcessStateMaps::
    ClearBrowserContextIfMatches(const BrowserContext& browser_context) {
  for (auto& pair : process_state_) {
    pair.second->ClearBrowserContextIfMatches(&browser_context);
  }

  for (auto& pair : pending_remove_state_) {
    pair.second->ClearBrowserContextIfMatches(&browser_context);
  }
}

bool ChildProcessSecurityPolicyImpl::IsIsolatedOrigin(
    const IsolationContext& isolation_context,
    const url::Origin& origin,
    bool origin_requests_isolation) {
  url::Origin unused_result;
  return GetMatchingProcessIsolatedOrigin(
      isolation_context, origin, origin_requests_isolation, &unused_result);
}

bool ChildProcessSecurityPolicyImpl::IsGloballyIsolatedOriginForTesting(
    const url::Origin& origin) {
  return std::ranges::contains(GetIsolatedOrigins(), origin);
}

std::vector<url::Origin> ChildProcessSecurityPolicyImpl::GetIsolatedOrigins(
    std::optional<IsolatedOriginSource> source,
    BrowserContext* browser_context) {
  std::vector<url::Origin> origins;
  base::AutoLock isolated_origins_lock(isolated_origins_lock_);
  for (const auto& iter : isolated_origins_) {
    for (const auto& isolated_origin_entry : iter.second) {
      if (source && source.value() != isolated_origin_entry.source()) {
        continue;
      }

      // If browser_context is specified, ensure that the entry matches it.  If
      // the browser_context is not specified, only consider entries that are
      // not associated with a profile (i.e., which apply globally to the
      // entire browser).
      bool matches_profile =
          browser_context ? isolated_origin_entry.MatchesProfile(
                                browser_context->UniqueToken())
                          : isolated_origin_entry.AppliesToAllBrowserContexts();
      if (!matches_profile) {
        continue;
      }

      // Do not include origins that only apply to specific BrowsingInstances.
      if (!isolated_origin_entry.applies_to_future_browsing_instances()) {
        continue;
      }

      origins.push_back(isolated_origin_entry.origin());
    }
  }
  return origins;
}

bool ChildProcessSecurityPolicyImpl::IsIsolatedSiteFromSource(
    const url::Origin& origin,
    IsolatedOriginSource source) {
  base::AutoLock isolated_origins_lock(isolated_origins_lock_);
  GURL site_url = SiteInfo::GetSiteForOrigin(origin);
  auto it = isolated_origins_.find(site_url);
  if (it == isolated_origins_.end()) {
    return false;
  }
  url::Origin site_origin = url::Origin::Create(site_url);
  for (const auto& entry : it->second) {
    if (entry.source() == source && entry.origin() == site_origin) {
      return true;
    }
  }
  return false;
}

bool ChildProcessSecurityPolicyImpl::GetMatchingProcessIsolatedOrigin(
    const IsolationContext& isolation_context,
    const url::Origin& origin,
    bool requests_origin_keyed_process,
    url::Origin* result) {
  // GetSiteForOrigin() is used to look up the site URL of |origin| to speed
  // up the isolated origin lookup.  This only performs a straightforward
  // translation of an origin to eTLD+1; it does *not* take into account
  // effective URLs, isolated origins, and other logic that's not needed
  // here, but *is* typically needed for making process model decisions. Be
  // very careful about using GetSiteForOrigin() elsewhere, and consider
  // whether you should be using GetSiteForURL() instead.
  return GetMatchingProcessIsolatedOrigin(
      isolation_context, origin, requests_origin_keyed_process,
      SiteInfo::GetSiteForOrigin(origin), result);
}

bool ChildProcessSecurityPolicyImpl::GetMatchingProcessIsolatedOrigin(
    const IsolationContext& isolation_context,
    const url::Origin& origin,
    bool requests_origin_keyed_process,
    const GURL& site_url,
    url::Origin* result) {
  CHECK_CURRENTLY_ON(BrowserThread::UI);

  *result = url::Origin();

  // Check the opt-in isolation status of |origin| in |isolation_context|.
  // Note that while IsolatedOrigins considers any sub-origin of an isolated
  // origin as also being isolated, with opt-in we will always either return
  // false, or true with result set to |origin|. We give priority to origins
  // requesting opt-in isolation over command-line isolation.
  // Note: This should only return a full origin if we are doing
  // process-isolated Origin-keyed Agent Clusters, which will only be the case
  // when site-isolation is enabled. Otherwise we put the origin into its
  // corresponding site, even if Origin-keyed Agent Clusters will be enabled
  // on the renderer side.
  // TODO(wjmaclean,alexmos,acolwell): We should revisit this when we have
  // SiteInstanceGroups, since at that point we can again return an origin
  // here (and thus create a new SiteInstance) even when
  // IsProcessIsolationForOriginAgentClusterEnabled() returns false; in that
  // case a SiteInstanceGroup will allow a logical group of SiteInstances that
  // live same-process.
  if (SiteIsolationPolicy::IsProcessIsolationForOriginAgentClusterEnabled()) {
    OriginAgentClusterIsolationState oac_isolation_state =
        requests_origin_keyed_process
            ? OriginAgentClusterIsolationState::CreateForOriginAgentCluster(
                  true /* has_oac_request */,
                  true /* requires_origin_keyed_process */)
            : OriginAgentClusterIsolationState::CreateNonIsolatedByDefault();
    oac_isolation_state = DetermineOriginAgentClusterIsolation(
        isolation_context, origin, oac_isolation_state);
    if (oac_isolation_state.requires_origin_keyed_process()) {
      *result = origin;
      return true;
    }
  }

  return GetMatchingProcessIsolatedOriginFromLegacyOriginList(
      isolation_context, origin, site_url, result);
}

bool ChildProcessSecurityPolicyImpl::
    GetMatchingProcessIsolatedOriginFromLegacyOriginList(
        const IsolationContext& isolation_context,
        const url::Origin& origin,
        const GURL& site_url,
        url::Origin* result) {
  CHECK_CURRENTLY_ON(BrowserThread::UI);

  *result = url::Origin();
  base::AutoLock isolated_origins_lock(isolated_origins_lock_);

  // If |isolation_context| does not specify a BrowsingInstance ID (which should
  // only happen in tests), then assume that we want to retrieve the latest
  // applicable information; i.e., return the latest matching isolated origins
  // that would apply to future BrowsingInstances.  Using
  // NextBrowsingInstanceId() will match all available IsolatedOriginEntries.
  BrowsingInstanceId browsing_instance_id(
      isolation_context.browsing_instance_id());
  if (browsing_instance_id.is_null()) {
    browsing_instance_id = SiteInstanceImpl::NextBrowsingInstanceId();
  }

  // Look up the list of origins corresponding to |origin|'s site.
  auto it = isolated_origins_.find(site_url);

  // Subtle corner case: if the site's host ends with a dot, do the lookup
  // without it.  A trailing dot shouldn't be able to bypass isolated origins:
  // if "https://foo.com" is an isolated origin, "https://foo.com." should
  // match it.
  if (it == isolated_origins_.end() && site_url.has_host() &&
      site_url.host().back() == '.') {
    GURL::Replacements replacements;
    std::string_view host(site_url.host());
    host.remove_suffix(1);
    replacements.SetHostStr(host);
    it = isolated_origins_.find(site_url.ReplaceComponents(replacements));
  }

  // Looks for all isolated origins that were already isolated at the time
  // |isolation_context| was created. If multiple isolated origins are
  // registered with a common domain suffix, return the most specific one.  For
  // example, if foo.isolated.com and isolated.com are both isolated origins,
  // bar.foo.isolated.com should return foo.isolated.com.
  bool found = false;
  if (it != isolated_origins_.end()) {
    for (const auto& isolated_origin_entry : it->second) {
      // If this isolated origin applies only to a specific profile, don't
      // use it for a different profile.
      if (!isolated_origin_entry.MatchesProfile(
              isolation_context.browser_context()->UniqueToken())) {
        continue;
      }

      if (isolated_origin_entry.MatchesBrowsingInstance(browsing_instance_id) &&
          IsolatedOriginUtil::DoesOriginMatchIsolatedOrigin(
              origin, isolated_origin_entry.origin())) {
        // If a match has been found that requires all subdomains to be isolated
        // then return immediately. |origin| is returned to ensure proper
        // process isolation, e.g. https://a.b.c.isolated.com matches an
        // IsolatedOriginEntry constructed from http://[*.]isolated.com, so
        // https://a.b.c.isolated.com must be returned.
        if (isolated_origin_entry.isolate_all_subdomains()) {
          *result = origin;
          uint16_t default_port = url::DefaultPortForScheme(origin.scheme());

          if (origin.port() != default_port) {
            *result = url::Origin::Create(GURL(origin.scheme() +
                                               url::kStandardSchemeSeparator +
                                               origin.host()));
          }

          return true;
        }

        if (!found || result->host().length() <
                          isolated_origin_entry.origin().host().length()) {
          *result = isolated_origin_entry.origin();
          found = true;
        }
      }
    }
  }

  return found;
}

OriginAgentClusterIsolationState
ChildProcessSecurityPolicyImpl::DetermineOriginAgentClusterIsolation(
    const IsolationContext& isolation_context,
    const url::Origin& origin,
    const OriginAgentClusterIsolationState& requested_isolation_state) {
  if (!IsolatedOriginUtil::IsValidOriginForOriginAgentClusterOptIn(origin)) {
    return OriginAgentClusterIsolationState::CreateNonIsolatedByDefault();
  }

  // See if the same origin exists in the BrowsingInstance already, and if so
  // return its isolation status.
  // There are two cases we're worried about here: (i) we've previously seen the
  // origin and isolated it, in which case we should continue to isolate it, and
  // (ii) we've previously seen the origin and *not* isolated it, in which case
  // we should continue to not isolate it.
  BrowsingInstanceId browsing_instance_id(
      isolation_context.browsing_instance_id());

  if (!browsing_instance_id.is_null()) {
    base::AutoLock origin_agent_cluster_lock(origin_agent_cluster_lock_);

    // Look for |origin| in the isolation status list.
    std::optional<OriginAgentClusterIsolationState> oac_isolation_state =
        LookupOriginAgentClusterState(browsing_instance_id, origin);

    if (oac_isolation_state) {
      return *oac_isolation_state;
    }
  }

  // If we get to this point, then |origin| is neither opted-in nor opted-out.
  // At this point we allow opting in or out if it's requested. This is true for
  // either logical OriginAgentCluster, or OriginAgentCluster with an
  // origin-keyed process.
  return requested_isolation_state;
}

bool ChildProcessSecurityPolicyImpl::
    HasOriginEverRequestedOriginAgentClusterValue(
        BrowserContext* browser_context,
        const url::Origin& origin) {
  RUST_CPP_RETURN_FUNCTION(
      rust::child_process_security_policy::
          has_origin_ever_requested_origin_agent_cluster_value(
              browser_context->UniqueToken(),
              // Make a copy of the origin for Rust to own.
              std::make_unique<url::Origin>(origin)),
      HasOriginEverRequestedOriginAgentClusterValue_Cpp(
          browser_context->UniqueToken(), origin));
}

bool ChildProcessSecurityPolicyImpl::
    HasOriginEverRequestedOriginAgentClusterValue_Cpp(
        const base::UnguessableToken& browser_context_id,
        const url::Origin& origin) {
  base::AutoLock origin_agent_cluster_lock(origin_agent_cluster_lock_);
  auto it = origin_agent_cluster_opt_ins_and_outs_.find(browser_context_id);
  return it != origin_agent_cluster_opt_ins_and_outs_.end() &&
         it->second.contains(origin);
}

std::optional<OriginAgentClusterIsolationState>
ChildProcessSecurityPolicyImpl::LookupOriginAgentClusterState(
    const BrowsingInstanceId& browsing_instance_id,
    const url::Origin& origin) {
  // We cannot use the RUST_CPP_RETURN_FUNCTION macro here because CXX does not
  // support passing Option/std::optional across the FFI boundary (see
  // https://github.com/dtolnay/cxx/issues/87). We must use a custom out
  // parameter FFI bridge and call CheckAndReturnOptionalRustAndCppResults.
  const RustPolicy policy = GetRustPolicy();
  std::optional<OriginAgentClusterIsolationState> rust_result = std::nullopt;

  if (IsRustEnabled(policy)) {
    rust::child_process_security_policy::OriginAgentClusterIsolationState
        state = rust::child_process_security_policy::
            OriginAgentClusterIsolationState::SiteKeyedByDefault;
    if (rust::child_process_security_policy::lookup_origin_agent_cluster_state(
            // Make a copy of the origin for Rust to own.
            browsing_instance_id, std::make_unique<url::Origin>(origin),
            state)) {
      rust_result =
          std::make_optional(FromRustOriginAgentClusterIsolationState(state));
    }
  }

  std::optional<OriginAgentClusterIsolationState> cpp_result = std::nullopt;
  if (IsCppEnabled(policy)) {
    cpp_result =
        LookupOriginAgentClusterState_Cpp(browsing_instance_id, origin);
  }

  return CheckAndReturnOptionalRustAndCppResults(rust_result, cpp_result,
                                                 policy);
}

std::optional<OriginAgentClusterIsolationState>
ChildProcessSecurityPolicyImpl::LookupOriginAgentClusterState_Cpp(
    const BrowsingInstanceId& browsing_instance_id,
    const url::Origin& origin) {
  if (auto* origin_map =
          base::FindOrNull(origin_agent_cluster_states_by_browsing_instance_,
                           browsing_instance_id)) {
    if (auto* state = base::FindOrNull(*origin_map, origin)) {
      return *state;
    }
  }
  return std::nullopt;
}

std::optional<OriginAgentClusterIsolationState>
ChildProcessSecurityPolicyImpl::LookupOriginAgentClusterStateForTesting(
    const BrowsingInstanceId& browsing_instance_id,
    const url::Origin& origin) {
  base::AutoLock lock(origin_agent_cluster_lock_);
  return LookupOriginAgentClusterState(browsing_instance_id, origin);
}

void ChildProcessSecurityPolicyImpl::RecordDefaultOriginAgentClusterOriginIfNew(
    const IsolationContext& isolation_context,
    const url::Origin& origin,
    bool is_global_walk_or_frame_removal) {
  CHECK_CURRENTLY_ON(BrowserThread::UI);

  // All callers to this function live on the UI thread, so the IsolationContext
  // should contain a BrowserContext*.
  BrowserContext* browser_context = isolation_context.browser_context();
  DCHECK(browser_context);

  RUST_CPP_VOID_FUNCTION(
      rust::child_process_security_policy::
          record_default_origin_agent_cluster_origin_if_new(
              isolation_context.browsing_instance_id(),
              browser_context->UniqueToken(),
              // Make a copy of the origin for Rust to own.
              std::make_unique<url::Origin>(origin),
              ToRustOriginAgentClusterIsolationState(
                  isolation_context.default_isolation_state()),
              is_global_walk_or_frame_removal),
      RecordDefaultOriginAgentClusterOriginIfNew_Cpp(
          isolation_context.browsing_instance_id(),
          browser_context->UniqueToken(), origin,
          isolation_context.default_isolation_state(),
          is_global_walk_or_frame_removal));
}

void ChildProcessSecurityPolicyImpl::
    RecordDefaultOriginAgentClusterOriginIfNew_Cpp(
        const BrowsingInstanceId& browsing_instance_id,
        const base::UnguessableToken& browser_context_id,
        const url::Origin& origin,
        const OriginAgentClusterIsolationState& oac_isolation_state,
        bool is_global_walk_or_frame_removal) {
  if (!IsolatedOriginUtil::IsValidOriginForOriginAgentClusterOptIn(origin)) {
    return;
  }

  // Commits of origins that have ever sent the OriginAgentCluster header in
  // this BrowserContext are tracked in every BrowsingInstance in this
  // BrowserContext, to avoid having to do multiple global walks. If the origin
  // isn't in the list of such origins (i.e., the common case), return early to
  // avoid unnecessary work, since this is called on every commit. Skip this
  // during global walks and frame removals, since we do want to track the
  // origin's non-isolated status in those cases.
  if (!is_global_walk_or_frame_removal &&
      !HasOriginEverRequestedOriginAgentClusterValue_Cpp(browser_context_id,
                                                         origin)) {
    return;
  }

  // Note: this lock is grabbed in the call to
  // `HasOriginEverRequestedOriginAgentClusterValue` above, then released and
  // reacquired here. This is safe, because once
  // `HasOriginEverRequestedOriginAgentClusterValue()` returns true, it will
  // keep returning true, as the OAC maps used here don't change origin values
  // until the maps are cleared entirely. Also, their manipulation is restricted
  // to the UI thread, so another thread can't concurrently add new values while
  // we reacquire the lock.
  base::AutoLock origin_agent_cluster_lock(origin_agent_cluster_lock_);

  // If |origin| has already recorded an Origin-Agent-Cluster state, then we
  // don't want to add it to the list. Technically this check is unnecessary
  // during global walks (when the origin won't be in this list yet), but it
  // matters during frame removal (when we don't want to add an opted-in origin
  // to the list as non-isolated when its frame is removed).
  CHECK(!browsing_instance_id.is_null());
  if (LookupOriginAgentClusterState_Cpp(browsing_instance_id, origin)) {
    return;
  }

  // Since there was no prior record for this BrowsingInstance, track that this
  // origin should use the default isolation model in use by the
  // BrowsingInstance.
  origin_agent_cluster_states_by_browsing_instance_[browsing_instance_id]
      .emplace(origin, oac_isolation_state);
}

void ChildProcessSecurityPolicyImpl::RemoveAllStateForBrowsingInstance(
    const BrowsingInstanceId& browsing_instance_id) {
  // After a suitable delay, remove this BrowsingInstance's info from any
  // ProcessStates that are using it.
  // TODO(wjmaclean): Monitor the CanAccessDataForOrigin crash key in renderer
  // kills to see if we get post-BrowsingInstance-destruction ProcessLock
  // mismatches, indicating this cleanup should be further delayed.
  auto task_closure = [](const BrowsingInstanceId id) {
    ChildProcessSecurityPolicyImpl* policy =
        ChildProcessSecurityPolicyImpl::GetInstance();
    policy->RemoveAllStateForBrowsingInstanceInternal(id);
  };
  if (browsing_instance_cleanup_delay_.is_positive()) {
    // Do the actual state cleanup after posting a task to the IO thread, to
    // give a chance for any last unprocessed tasks to be handled. The cleanup
    // itself locks the data structures and can safely happen from either
    // thread.
    GetIOThreadTaskRunner({})->PostDelayedTask(
        FROM_HERE, base::BindOnce(task_closure, browsing_instance_id),
        browsing_instance_cleanup_delay_);
  } else {
    // Since this is just used in tests, it's ok to do it on either thread.
    task_closure(browsing_instance_id);
  }
}

void ChildProcessSecurityPolicyImpl::RemoveAllStateForBrowsingInstanceInternal(
    const BrowsingInstanceId browsing_instance_id) {
  // If a BrowsingInstance is destructing, we should always have an id for it.
  CHECK(!browsing_instance_id.is_null());

  {
    // content_unittests don't always report being on the IO thread.
    DCHECK(IsRunningOnExpectedThread());
    base::AutoLock lock(lock_);
    process_states_.RemoveStateForBrowsingInstance(browsing_instance_id);

    // Note: if the BrowsingInstanceId set is empty at the end of this function,
    // we must never remove the ProcessLock in case the associated RenderProcess
    // is compromised, in which case we wouldn't want to reuse it for another
    // origin.
  }

  EraseOriginAgentClusterState(browsing_instance_id);

  {
    base::AutoLock isolated_origins_lock(isolated_origins_lock_);
    for (auto& iter : isolated_origins_) {
      std::erase_if(iter.second, [&browsing_instance_id](
                                     const IsolatedOriginEntry& entry) {
        // Remove entries that are specific to `browsing_instance_id` and
        // do not apply to future BrowsingInstances.
        return (entry.browsing_instance_id() == browsing_instance_id &&
                !entry.applies_to_future_browsing_instances());
      });
    }
  }

  EraseV8OptimizationState(browsing_instance_id);
}

void ChildProcessSecurityPolicyImpl::EraseOriginAgentClusterState(
    const BrowsingInstanceId& browsing_instance_id) {
  RUST_CPP_VOID_FUNCTION(
      rust::child_process_security_policy::erase_origin_agent_cluster_state(
          browsing_instance_id),
      EraseOriginAgentClusterState_Cpp(browsing_instance_id));
}

void ChildProcessSecurityPolicyImpl::EraseOriginAgentClusterState_Cpp(
    const BrowsingInstanceId& browsing_instance_id) {
  base::AutoLock origin_agent_cluster_lock(origin_agent_cluster_lock_);
  origin_agent_cluster_states_by_browsing_instance_.erase(browsing_instance_id);
}

void ChildProcessSecurityPolicyImpl::EraseV8OptimizationState(
    const BrowsingInstanceId& browsing_instance_id) {
  RUST_CPP_VOID_FUNCTION(
      rust::child_process_security_policy::erase_v8_optimization_state(
          browsing_instance_id),
      EraseV8OptimizationState_Cpp(browsing_instance_id));
}

void ChildProcessSecurityPolicyImpl::EraseV8OptimizationState_Cpp(
    const BrowsingInstanceId& browsing_instance_id) {
  base::AutoLock are_v8_optimizations_disabled_lock(
      are_v8_optimizations_disabled_lock_);
  are_v8_optimizations_disabled_map_.erase(browsing_instance_id);
}

void ChildProcessSecurityPolicyImpl::ProcessStateMaps::
    RemoveStateForBrowsingInstance(
        const BrowsingInstanceId browsing_instance_id) {
  // This only updates states in `process_state_`, because the states in
  // `pending_remove_state_` should be immutable and will soon be deleted.
  for (auto& it : process_state_) {
    it.second->ClearBrowsingInstanceId(browsing_instance_id);
  }
}

void ChildProcessSecurityPolicyImpl::AddCoopIsolatedOriginForBrowsingInstance(
    const IsolationContext& isolation_context,
    const url::Origin& origin,
    IsolatedOriginSource source) {
  // We ought to have validated the origin prior to getting here.  If the
  // origin isn't valid at this point, something has gone wrong.
  CHECK(IsolatedOriginUtil::IsValidIsolatedOrigin(origin))
      << "Trying to isolate invalid origin: " << origin;

  // This can only be called from the UI thread, as it reads state that's only
  // available (and is only safe to be retrieved) on the UI thread, such as
  // BrowsingInstance IDs.
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  BrowsingInstanceId browsing_instance_id(
      isolation_context.browsing_instance_id());
  // This function should only be called when a BrowsingInstance is registering
  // a new SiteInstance, so |browsing_instance_id| should always be defined.
  CHECK(!browsing_instance_id.is_null());

  // For site-keyed isolation, add `origin` to the isolated_origins_ map (which
  // supports subdomain matching).
  // Ensure that `origin` is a site (scheme + eTLD+1) rather than any origin.
  auto site_origin = url::Origin::Create(SiteInfo::GetSiteForOrigin(origin));
  CHECK_EQ(origin, site_origin);

  base::AutoLock isolated_origins_lock(isolated_origins_lock_);

  // Explicitly set `applies_to_future_browsing_instances` to false to only
  // isolate `origin` within the provided BrowsingInstance, but not future
  // ones.  Note that it's possible for `origin` to also become isolated for
  // future BrowsingInstances if AddFutureIsolatedOrigins() is called for it
  // later.
  AddIsolatedOriginInternal(isolation_context.browser_context(), origin,
                            false /* applies_to_future_browsing_instances */,
                            isolation_context.browsing_instance_id(),
                            false /* isolate_all_subdomains */, source);
}

void ChildProcessSecurityPolicyImpl::
    AddOriginAgentClusterStateForBrowsingInstance(
        const IsolationContext& isolation_context,
        const url::Origin& origin,
        const OriginAgentClusterIsolationState& oac_isolation_state) {
  // This can only be called from the UI thread, as it reads state that's only
  // available (and is only safe to be retrieved) on the UI thread, such as
  // BrowserContext.
  // TODO(crbug.com/482216433): Support this check on the Rust side.
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  RUST_CPP_VOID_FUNCTION(
      rust::child_process_security_policy::
          add_origin_agent_cluster_state_for_browsing_instance(
              isolation_context.browsing_instance_id(),
              // Make a copy of the origin for Rust to own.
              std::make_unique<url::Origin>(origin),
              ToRustOriginAgentClusterIsolationState(oac_isolation_state),
              ToRustOriginAgentClusterIsolationState(
                  isolation_context.default_isolation_state())),
      AddOriginAgentClusterStateForBrowsingInstance_Cpp(
          isolation_context.browsing_instance_id(), origin, oac_isolation_state,
          isolation_context.default_isolation_state()));
}

void ChildProcessSecurityPolicyImpl::
    AddOriginAgentClusterStateForBrowsingInstance_Cpp(
        const BrowsingInstanceId& browsing_instance_id,
        const url::Origin& origin,
        const OriginAgentClusterIsolationState& oac_isolation_state,
        const OriginAgentClusterIsolationState& default_isolation_state) {
  // We should only be registering an isolation state if it deviates from the
  // default isolation state (e.g., if it's explicitly requested by a header or
  // if an ad frame's process isolation is being bypassed).
  DCHECK(oac_isolation_state != default_isolation_state);

  // We ought to have validated the origin prior to getting here.  If the
  // origin isn't valid at this point, something has gone wrong.
  CHECK((oac_isolation_state.logical_oac_status() ==
             AgentClusterKey::OACStatus::kOriginKeyedByHeader &&
         IsolatedOriginUtil::IsValidOriginForOriginAgentClusterOptIn(origin)) ||
        // The second part of this check is specific to OAC-by-default, and is
        // required to allow explicit opt-outs for HTTP schemed origins. See
        // OriginAgentClusterInsecureEnabledBrowserTest.DocumentDomain_Disabled.
        IsolatedOriginUtil::IsValidOriginForOriginAgentClusterOptOut(origin))
      << "Trying to isolate invalid origin: " << origin;

  // This function should only be called when a BrowsingInstance is registering
  // a new SiteInstance, so |browsing_instance_id| should always be defined.
  CHECK(!browsing_instance_id.is_null());

  // Register the OAC state for `origin` in the per-BrowsingInstance map. We
  // only support adding new entries, not modifying existing ones. If at some
  // point in the future we allow isolation state to change during the lifetime
  // of a BrowsingInstance, then this will need to be updated.
  base::AutoLock origin_agent_cluster_lock(origin_agent_cluster_lock_);
  origin_agent_cluster_states_by_browsing_instance_[browsing_instance_id]
      .try_emplace(origin, oac_isolation_state);
}

bool ChildProcessSecurityPolicyImpl::RecordOriginAgentClusterRequestIfNew(
    BrowserContext* browser_context,
    const url::Origin& origin) {
  RUST_CPP_RETURN_FUNCTION(
      rust::child_process_security_policy::
          record_origin_agent_cluster_request_if_new(
              browser_context->UniqueToken(),
              // Make a copy of the origin for Rust to own.
              std::make_unique<url::Origin>(origin)),
      RecordOriginAgentClusterRequestIfNew_Cpp(browser_context, origin));
}

bool ChildProcessSecurityPolicyImpl::RecordOriginAgentClusterRequestIfNew_Cpp(
    BrowserContext* browser_context,
    const url::Origin& origin) {
  if (!IsolatedOriginUtil::IsValidOriginForOriginAgentClusterOptIn(origin)) {
    return false;
  }

  base::AutoLock origin_agent_cluster_lock(origin_agent_cluster_lock_);

  const auto& browser_context_id = browser_context->UniqueToken();
  auto it = origin_agent_cluster_opt_ins_and_outs_.find(browser_context_id);
  if (it != origin_agent_cluster_opt_ins_and_outs_.end() &&
      it->second.contains(origin)) {
    return false;
  }

  origin_agent_cluster_opt_ins_and_outs_[browser_context_id].insert(origin);
  return true;
}

void ChildProcessSecurityPolicyImpl::RemoveIsolatedOriginForTesting(
    const url::Origin& origin) {
  GURL key(SiteInfo::GetSiteForOrigin(origin));
  base::AutoLock isolated_origins_lock(isolated_origins_lock_);
  std::erase_if(isolated_origins_[key],
                [&origin](const IsolatedOriginEntry& entry) {
                  // Remove if origin matches.
                  return (entry.origin() == origin);
                });
  if (isolated_origins_[key].empty()) {
    isolated_origins_.erase(key);
  }
}

void ChildProcessSecurityPolicyImpl::ClearIsolatedOriginsForTesting() {
  base::AutoLock isolated_origins_lock(isolated_origins_lock_);
  isolated_origins_.clear();
}

int ChildProcessSecurityPolicyImpl::GetIsolatedOriginEntryCountForTesting(
    const url::Origin& origin) {
  GURL key(SiteInfo::GetSiteForOrigin(origin));
  base::AutoLock isolated_origins_lock(isolated_origins_lock_);
  auto origins_for_key = isolated_origins_[key];
  return std::ranges::count(origins_for_key, origin,
                            &IsolatedOriginEntry::origin);
}

void ChildProcessSecurityPolicyImpl::
    AddV8OptimizationDisabledStateForOriginIfNotCached(
        const BrowsingInstanceId& browsing_instance_id,
        const url::Origin& process_lock_origin,
        bool are_v8_optimizations_disabled) {
  RUST_CPP_VOID_FUNCTION(
      rust::child_process_security_policy::
          add_v8_optimization_disabled_state_for_origin_if_not_cached(
              browsing_instance_id,
              // Make a copy for Rust to own.
              std::make_unique<url::Origin>(process_lock_origin),
              are_v8_optimizations_disabled),
      AddV8OptimizationDisabledStateForOriginIfNotCached_Cpp(
          browsing_instance_id, process_lock_origin,
          are_v8_optimizations_disabled));
}

void ChildProcessSecurityPolicyImpl::
    AddV8OptimizationDisabledStateForOriginIfNotCached_Cpp(
        const BrowsingInstanceId& browsing_instance_id,
        const url::Origin& process_lock_origin,
        bool are_v8_optimizations_disabled) {
  if (!IsolatedOriginUtil::IsValidIsolatedOrigin(process_lock_origin)) {
    return;
  }

  // Note that checking if a value is cached and then inserting it below without
  // holding the lock between the two calls introduces a race condition (another
  // thread could insert in the meantime). The Rust implementation resolves this
  // by keeping the CPSP lock held for the entire check and insert block.
  if (LookupAreV8OptimizationsDisabled_Cpp(
          browsing_instance_id, process_lock_origin) != std::nullopt) {
    return;
  }

  base::AutoLock are_v8_optimizations_disabled_lock(
      are_v8_optimizations_disabled_lock_);
  are_v8_optimizations_disabled_map_[browsing_instance_id].insert_or_assign(
      process_lock_origin, are_v8_optimizations_disabled);
}

std::optional<bool>
ChildProcessSecurityPolicyImpl::LookupAreV8OptimizationsDisabled(
    const BrowsingInstanceId& browsing_instance_id,
    const url::Origin& process_lock_origin) {
  // We cannot use the RUST_CPP_RETURN_FUNCTION macro here because CXX does not
  // support passing Option/std::optional across the FFI boundary (see
  // https://github.com/dtolnay/cxx/issues/87). We must use a custom out
  // parameter FFI bridge and call CheckAndReturnOptionalRustAndCppResults.
  const RustPolicy policy = GetRustPolicy();
  std::optional<bool> rust_result = std::nullopt;

  if (IsRustEnabled(policy)) {
    bool result = false;
    if (rust::child_process_security_policy::
            lookup_are_v8_optimizations_disabled(
                browsing_instance_id,
                // Make a copy for Rust to own.
                std::make_unique<url::Origin>(process_lock_origin), result)) {
      rust_result = std::make_optional(result);
    }
  }

  std::optional<bool> cpp_result = std::nullopt;
  if (IsCppEnabled(policy)) {
    cpp_result = LookupAreV8OptimizationsDisabled_Cpp(browsing_instance_id,
                                                      process_lock_origin);
  }

  return CheckAndReturnOptionalRustAndCppResults(rust_result, cpp_result,
                                                 policy);
}

std::optional<bool>
ChildProcessSecurityPolicyImpl::LookupAreV8OptimizationsDisabled_Cpp(
    const BrowsingInstanceId& browsing_instance_id,
    const url::Origin& process_lock_origin) {
  base::AutoLock are_v8_optimizations_disabled_lock(
      are_v8_optimizations_disabled_lock_);
  auto it = are_v8_optimizations_disabled_map_.find(browsing_instance_id);
  if (it == are_v8_optimizations_disabled_map_.end()) {
    return std::nullopt;
  }
  base::flat_map<url::Origin, bool>& origin_map = it->second;
  auto origin_it = origin_map.find(process_lock_origin);
  if (origin_it == origin_map.end()) {
    return std::nullopt;
  }
  return origin_it->second;
}

const ChildProcessSecurityPolicyImpl::ProcessState*
ChildProcessSecurityPolicyImpl::ProcessStateMaps::GetProcessStateForQuery(
    ChildProcessId child_id) {
  // This function checks both `process_state_` and `pending_remove_state_` for
  // the corresponding ProcessState, so that queries can continue to succeed
  // after RenderProcessHost is gone until all of the Handles are gone as well.
  auto itr = process_state_.find(child_id);
  if (itr != process_state_.end()) {
    return itr->second.get();
  }

  auto pending_itr = pending_remove_state_.find(child_id);
  if (pending_itr == pending_remove_state_.end()) {
    return nullptr;
  }

  // At this point the ProcessState in the map is being kept alive
  // by a Handle object or we are waiting for the deletion task to be run on
  // the IO thread.
  ProcessState* pending_process_state = pending_itr->second.get();

  auto count_itr = process_reference_counts_.find(child_id);
  if (count_itr != process_reference_counts_.end()) {
    // There must be a Handle that still holds a reference to this
    // pending state so it is safe to return. The assumption is that the
    // owner of this Handle is making a security check.
    return pending_process_state;
  }

  // Since we don't have an entry in |process_reference_counts_| it means
  // that we are waiting for the deletion task posted to the IO thread to run.
  // Only allow the state to be accessed by the IO thread in this situation.
  if (BrowserThread::CurrentlyOn(BrowserThread::IO)) {
    return pending_process_state;
  }

  return nullptr;
}

ChildProcessSecurityPolicyImpl::ProcessState*
ChildProcessSecurityPolicyImpl::ProcessStateMaps::GetProcessStateForMutation(
    ChildProcessId child_id) {
  // This function intentionally only checks `process_state_` and not
  // `pending_remove_state_` for the corresponding ProcessState, so that no
  // modifications can be made to the state after the RenderProcessHost is gone.
  auto itr = process_state_.find(child_id);
  if (itr != process_state_.end()) {
    return itr->second.get();
  }
  return nullptr;
}

std::vector<IsolatedOriginPattern>
ChildProcessSecurityPolicyImpl::ParseIsolatedOrigins(
    std::string_view pattern_list) {
  std::vector<std::string_view> origin_strings = base::SplitStringPiece(
      pattern_list, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);

  std::vector<IsolatedOriginPattern> patterns;
  patterns.reserve(origin_strings.size());

  for (std::string_view origin_string : origin_strings) {
    patterns.emplace_back(origin_string);
  }

  return patterns;
}

// static
std::string ChildProcessSecurityPolicyImpl::GetKilledProcessOriginLock(
    const ProcessState* process_state) {
  if (!process_state) {
    return "(child id not found)";
  }

  if (!process_state->browser_context()) {
    return "(empty and null context)";
  }

  return process_state->process_lock().ToString();
}

// static
std::string ChildProcessSecurityPolicyImpl::GetCommittedOriginsForCrashKey(
    const ProcessState* process_state) {
  if (!process_state) {
    return "(no security state)";
  }
  return process_state->GetCommittedOriginsAsStringForDebugging();
}

void ChildProcessSecurityPolicyImpl::LogKilledProcessOriginLock(int child_id) {
  base::AutoLock lock(lock_);
  // TODO(crbug.com/379869738) Remove FromUnsafeValue.
  const ProcessState* process_state = process_states_.GetProcessStateForQuery(
      ChildProcessId::FromUnsafeValue(child_id));

  base::debug::SetCrashKeyString(GetKilledProcessOriginLockKey(),
                                 GetKilledProcessOriginLock(process_state));
}

ChildProcessSecurityPolicyImpl::Handle
ChildProcessSecurityPolicyImpl::CreateHandle(ChildProcessId child_id) {
  return Handle(child_id, /* duplicating_handle */ false);
}

bool ChildProcessSecurityPolicyImpl::AddProcessReference(
    ChildProcessId child_id,
    bool duplicating_handle) {
  base::AutoLock lock(lock_);
  return process_states_.AddProcessReference(child_id, duplicating_handle);
}

bool ChildProcessSecurityPolicyImpl::ProcessStateMaps::AddProcessReference(
    ChildProcessId child_id,
    bool duplicating_handle) {
  if (!child_id) {
    return false;
  }

  // Check to see if the ProcessState has been removed from |process_state_|
  // via a Remove() call. This corresponds to the process being destroyed.
  if (!process_state_.contains(child_id)) {
    if (!duplicating_handle) {
      // Do not allow Handles to be created after the process has been
      // destroyed, unless they are being duplicated.
      return false;
    }

    // The process has been destroyed but we are allowing an existing Handle
    // to be duplicated. Verify that the process reference count is available
    // and indicates another Handle has a reference.
    auto itr = process_reference_counts_.find(child_id);
    CHECK(itr != process_reference_counts_.end());
    CHECK_GT(itr->second, 0);
  }

  ++process_reference_counts_[child_id];
  return true;
}

void ChildProcessSecurityPolicyImpl::RemoveProcessReference(
    ChildProcessId child_id) {
  base::AutoLock lock(lock_);
  process_states_.RemoveProcessReference(child_id);
}

void ChildProcessSecurityPolicyImpl::ProcessStateMaps::RemoveProcessReference(
    ChildProcessId child_id) {
  auto itr = process_reference_counts_.find(child_id);
  CHECK(itr != process_reference_counts_.end());

  if (itr->second > 1) {
    itr->second--;
    return;
  }

  DCHECK_EQ(itr->second, 1);
  process_reference_counts_.erase(itr);

  // TODO(crbug.com/522872468): Figure out ProcessState lifetime management in
  // Rust. For now, rely on C++ to do the reference counting for
  // RenderProcessHost and CPSP Handles to know when it's safe to remove the
  // ProcessState on the Rust side.
  //
  // Similarly to Add(), we avoid an early return here in Rust-only mode and
  // allow the C++ cleanup code to complete, since Rust doesn't yet support
  // everything in ProcessState.
  if (IsRustEnabled(GetRustPolicy(CpspRustFeature::kProcessState))) {
    rust::child_process_security_policy::complete_pending_state_removal(
        child_id);
  }

  // |child_id| could be inside tasks that are on the IO thread task queues. We
  // need to keep the |pending_remove_state_| entry around until we have
  // successfully executed a task on the IO thread. This should ensure that any
  // pending tasks on the IO thread will have completed before we remove the
  // entry.
  //
  // TODO(crbug.com/461372139): Remove this PostTask. It should no longer be
  // needed now that all objects on the IO thread have been converted to use
  // Handles.
  GetIOThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(
          [](ChildProcessId child_id) {
            DCHECK_CURRENTLY_ON(BrowserThread::IO);
            auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
            base::AutoLock lock(policy->lock_);
            policy->process_states_.CompletePendingStateRemoval(child_id);
          },
          child_id));
}

void ChildProcessSecurityPolicyImpl::ProcessStateMaps::
    CompletePendingStateRemoval(ChildProcessId child_id) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);
  pending_remove_state_.erase(child_id);
}

void ChildProcessSecurityPolicyImpl::AddCommittedOrigin(
    int child_id,
    const url::Origin& origin) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  base::AutoLock lock(lock_);
  // TODO(crbug.com/379869738) Remove FromUnsafeValue.
  auto* state = process_states_.GetProcessStateForMutation(
      ChildProcessId::FromUnsafeValue(child_id));
  DCHECK(state);
  state->AddCommittedOrigin(origin);
}

}  // namespace content
