// Copyright 2022 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/network_sandbox.h" #include "base/dcheck_is_on.h" #include "base/files/file_util.h" #include "base/logging.h" #include "base/metrics/histogram_functions.h" #include "base/metrics/histogram_macros.h" #include "base/notreached.h" #include "base/task/thread_pool.h" #include "base/trace_event/base_tracing.h" #include "build/build_config.h" #include "content/browser/network_sandbox_grant_result.h" #include "content/public/browser/browser_thread.h" #include "content/public/browser/content_browser_client.h" #include "content/public/browser/network_service_util.h" #include "content/public/common/content_client.h" #include "sql/database.h" #if BUILDFLAG(IS_WIN) #include <windows.h> #include "base/win/security_util.h" #include "base/win/sid.h" #include "sandbox/policy/features.h" #endif // BUILDFLAG(IS_WIN) namespace content { namespace { // A filename that represents that the data contained within `data_directory` // has been migrated successfully and the data in `unsandboxed_data_path` is now // invalid. const base::FilePath::CharType kCheckpointFileName[] = FILE_PATH_LITERAL("NetworkDataMigrated"); // A platform specific set of parameters that is used when granting the sandbox // access to the network context data. struct SandboxParameters { #if BUILDFLAG(IS_WIN) std::wstring lpac_capability_name; #if DCHECK_IS_ON() bool sandbox_enabled; #endif // DCHECK_IS_ON() #endif // BUILDFLAG(IS_WIN) }; // Deletes the old data for a data file called `filename` from `old_path`. If // `file_path` refers to an SQL database then `is_sql` should be set to true, // and the journal file will also be deleted. // // Returns SandboxGrantResult::kSuccess if the all delete operations completed // successfully. Returns SandboxGrantResult::kFailedToDeleteData if a file could // not be deleted. SandboxGrantResult MaybeDeleteOldData( const base::FilePath& old_path, const std::optional<base::FilePath>& filename, bool is_sql) { // The path to the specific data file might not have been specified in the // network context params. In that case, nothing to delete. if (!filename.has_value()) return SandboxGrantResult::kSuccess; // Check old path exists, and is a directory. DCHECK(base::DirectoryExists(old_path)); base::FilePath old_file_path = old_path.Append(*filename); SandboxGrantResult last_error = SandboxGrantResult::kSuccess; // File might have already been deleted, or simply does not exist yet. if (base::PathExists(old_file_path)) { if (!base::DeleteFile(old_file_path)) { PLOG(ERROR) << "Failed to delete file " << old_file_path; // Continue on error. last_error = SandboxGrantResult::kFailedToDeleteOldData; } } if (!is_sql) return last_error; base::FilePath old_journal_path = sql::Database::JournalPath(old_file_path); // There might not be a journal file, or it's already been deleted. if (!base::PathExists(old_journal_path)) return last_error; if (base::PathExists(old_journal_path)) { if (!base::DeleteFile(old_journal_path)) { PLOG(ERROR) << "Failed to delete file " << old_journal_path; // Continue on error. last_error = SandboxGrantResult::kFailedToDeleteOldData; } } return last_error; } // Copies data file called `filename` from `old_path` to `new_path` (which must // both be directories). If `file_path` refers to an SQL database then `is_sql` // should be set to true, and the journal file will also be migrated. // Destination files will be overwritten if they exist already. // // Returns SandboxGrantResult::kSuccess if the operation completed successfully. // Returns SandboxGrantResult::kFailedToCopyData if a file could not be copied. SandboxGrantResult MaybeCopyData(const base::FilePath& old_path, const base::FilePath& new_path, const std::optional<base::FilePath>& filename, bool is_sql) { // The path to the specific data file might not have been specified in the // network context params. In that case, no files need to be moved. if (!filename.has_value()) return SandboxGrantResult::kSuccess; // Check both paths exist, and are directories. DCHECK(base::DirectoryExists(old_path) && base::DirectoryExists(new_path)); base::FilePath old_file_path = old_path.Append(*filename); base::FilePath new_file_path = new_path.Append(*filename); // Note that this code will overwrite the new file with the old file even if // it exists already. if (base::PathExists(old_file_path)) { // Delete file to make sure that inherited permissions are set on the new // file. base::DeleteFile(new_file_path); if (!base::CopyFile(old_file_path, new_file_path)) { PLOG(ERROR) << "Failed to copy file " << old_file_path << " to " << new_file_path; // Do not attempt to copy journal file if copy of main database file // fails. return SandboxGrantResult::kFailedToCopyData; } } if (!is_sql) return SandboxGrantResult::kSuccess; base::FilePath old_journal_path = sql::Database::JournalPath(old_file_path); // There might not be a journal file, or it's already been moved. if (!base::PathExists(old_journal_path)) return SandboxGrantResult::kSuccess; base::FilePath new_journal_path = sql::Database::JournalPath(new_file_path); // Delete file to make sure that inherited permissions are set on the new // file. base::DeleteFile(new_journal_path); if (!base::CopyFile(old_journal_path, new_journal_path)) { PLOG(ERROR) << "Failed to copy file " << old_journal_path << " to " << new_journal_path; return SandboxGrantResult::kFailedToCopyData; } return SandboxGrantResult::kSuccess; } // Deletes old data from `unsandboxed_data_path` if a migration operation has // been successful. SandboxGrantResult CleanUpOldData( network::mojom::NetworkContextParams* params) { // Never delete old data unless the checkpoint file exists. DCHECK(base::PathExists( params->file_paths->data_directory.path().Append(kCheckpointFileName))); SandboxGrantResult last_error = SandboxGrantResult::kSuccess; SandboxGrantResult result = MaybeDeleteOldData( *params->file_paths->unsandboxed_data_path, params->file_paths->cookie_database_name, /*is_sql=*/true); if (result != SandboxGrantResult::kSuccess) last_error = result; result = MaybeDeleteOldData( *params->file_paths->unsandboxed_data_path, params->file_paths->http_server_properties_file_name, /*is_sql=*/false); if (result != SandboxGrantResult::kSuccess) last_error = result; result = MaybeDeleteOldData( *params->file_paths->unsandboxed_data_path, params->file_paths->transport_security_persister_file_name, /*is_sql=*/false); if (result != SandboxGrantResult::kSuccess) last_error = result; result = MaybeDeleteOldData( *params->file_paths->unsandboxed_data_path, params->file_paths->reporting_and_nel_store_database_name, /*is_sql=*/true); if (result != SandboxGrantResult::kSuccess) last_error = result; result = MaybeDeleteOldData(*params->file_paths->unsandboxed_data_path, params->file_paths->trust_token_database_name, /*is_sql=*/true); if (result != SandboxGrantResult::kSuccess) last_error = result; return last_error; } // Grants the sandbox access to the specified `path`, which must be a directory // that exists. On Windows, the LPAC capability name should be supplied in the // `sandbox_params` to specify the name of the LPAC capability to be applied to // the path. On platforms which support directory transfer, the directory is // opened as a handle which is then sent to the NetworkService. // Returns true if the sandbox was successfully granted access to the path. bool MaybeGrantAccessToDataPath(const SandboxParameters& sandbox_params, network::TransferableDirectory* directory) { // There is no need to set file permissions if the network service is running // in-process. if (IsInProcessNetworkService()) return true; // Only do this on directories. if (!base::DirectoryExists(directory->path())) { return false; } #if BUILDFLAG(IS_WIN) // On platforms that don't support the LPAC sandbox, do nothing. if (!sandbox::policy::features::IsNetworkSandboxSupported()) { return true; } DCHECK(!sandbox_params.lpac_capability_name.empty()); auto ac_sids = base::win::Sid::FromNamedCapabilityVector( {sandbox_params.lpac_capability_name}); // Grant recursive access to directory. This also means new files in the // directory will inherit the ACE. return base::win::GrantAccessToPath( directory->path(), ac_sids, GENERIC_READ | GENERIC_WRITE | GENERIC_EXECUTE | DELETE, CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE, /*recursive=*/true); #else if (directory->IsOpenForTransferRequired()) { directory->OpenForTransfer(); return true; } return true; #endif // BUILDFLAG(IS_WIN) } // See the description in the header file. // // This process has a few stages: // 1. Create and grant the sandbox access to the cache dir. // 2. If `data_directory` is not specified then the caller is using in-memory // storage and so there's nothing to do. END. // 3. If `unsandboxed_data_path` is not specified then the caller is not aware // of the sandbox or migration, and the steps terminate here with // `data_directory` used by the network context and END. // 4. If migration has already taken place, regardless of whether it's requested // this time, grant the sandbox access to the `data_directory` (since this needs // to be done every time), and terminate here with `data_directory` being used. // END. // 5. If migration is not requested, then terminate here with // `unsandboxed_data_path` being used. END. // 6. At this point, migration has been requested and hasn't already happened, // so begin a migration attempt. If any of these steps fail, then bail out, and // `unsandboxed_data_path` is used. // 7. Grant the sandbox access to the `data_directory` (this is done before // copying the files to use inherited ACLs when copying files on Windows). // 8. Copy all the data files one by one from the `unsandboxed_data_path` to the // `data_directory`. // 9. Once all the files have been copied, lay down the Checkpoint file in the // `data_directory`. // 10. Delete all the original files (if they exist) from // `unsandboxed_data_path`. SandboxGrantResult MaybeGrantSandboxAccessToNetworkContextData( const SandboxParameters& sandbox_params, network::mojom::NetworkContextParams* params) { DCHECK(!BrowserThread::CurrentlyOn(BrowserThread::UI)); #if BUILDFLAG(IS_WIN) #if DCHECK_IS_ON() params->win_permissions_set = true; #endif #endif // BUILDFLAG(IS_WIN) // No file paths (e.g. in-memory context) so nothing to do. if (!params->file_paths) { return SandboxGrantResult::kDidNotAttemptToGrantSandboxAccess; } // HTTP cache path is special, and not under `data_directory` so must also // be granted access. Continue attempting to grant access to the other files // if this part fails. if (params->file_paths->http_cache_directory && params->http_cache_enabled) { // The path must exist for the cache ACL to be set. Create if needed. if (base::CreateDirectory( params->file_paths->http_cache_directory->path())) { // Note, this code always grants access to the cache directory even when // the sandbox is not enabled. This is a optimization (on Windows) // because by setting the ACL on the directory earlier rather than // later, it ensures that any new files created by the cache subsystem // get the inherited ACE rather than having to set them manually later. SCOPED_UMA_HISTOGRAM_TIMER("NetworkService.TimeToGrantCacheAccess"); TRACE_EVENT("startup", "NetworkSandbox.MaybeGrantAccessToDataPath"); if (!MaybeGrantAccessToDataPath( sandbox_params, &*params->file_paths->http_cache_directory)) { PLOG(ERROR) << "Failed to grant sandbox access to cache directory " << params->file_paths->http_cache_directory->path(); } } } if (params->file_paths->shared_dictionary_directory && params->shared_dictionary_enabled) { SCOPED_UMA_HISTOGRAM_TIMER( "NetworkService.TimeToGrantSharedDictionaryAccess"); // The path must exist for the cache ACL to be set. Create if needed. if (base::CreateDirectory( params->file_paths->shared_dictionary_directory->path())) { if (!MaybeGrantAccessToDataPath( sandbox_params, &*params->file_paths->shared_dictionary_directory)) { PLOG(ERROR) << "Failed to grant sandbox access to shared dictionary " "directory " << params->file_paths->shared_dictionary_directory->path(); } } } // No data directory, so rest of the files and databases are in memory. // Nothing to do. if (params->file_paths->data_directory.path().empty()) { return SandboxGrantResult::kDidNotAttemptToGrantSandboxAccess; } if (!params->file_paths->unsandboxed_data_path.has_value()) { #if BUILDFLAG(IS_WIN) && DCHECK_IS_ON() // On Windows, if network sandbox is enabled then there a migration must // happen, so a `unsandboxed_data_path` must be specified. DCHECK(!sandbox_params.sandbox_enabled); #endif // Trigger migration should never be requested if `unsandboxed_data_path` is // not set. DCHECK(!params->file_paths->trigger_migration); // Nothing to do here if `unsandboxed_data_path` is not specified. return SandboxGrantResult::kDidNotAttemptToGrantSandboxAccess; } // If these paths are ever the same then this is a mistake, as the file // permissions will be applied to the top level path which could contain other // data that should not be accessible by the network sandbox. DCHECK_NE(params->file_paths->data_directory.path(), *params->file_paths->unsandboxed_data_path); // Four cases need to be handled here. // // 1. No Checkpoint file, and `trigger_migration` is false: Data is still in // `unsandboxed_data_path` and sandbox does not need to be granted access. No // migration happens. // 2. No Checkpoint file, and `trigger_migration` is true: Data is in // `unsandboxed_data_path` and needs to be migrated to `data_directory`, and // the sandbox needs to be granted access to `data_directory`. // 3. Checkpoint file, and `trigger_migration` is false: Data is in // `data_directory` (already migrated) and sandbox needs to be granted access // to `data_directory`. // 4. Checkpoint file, and `trigger_migration` is true: Data is in // `data_directory` (already migrated) and sandbox needs to be granted access // to `data_directory`. This is the same as above and `trigger_migration` // changes nothing, as it's already happened. base::FilePath checkpoint_filename = params->file_paths->data_directory.path().Append(kCheckpointFileName); bool migration_already_happened = base::PathExists(checkpoint_filename); // Case 1. above where nothing is done. if (!params->file_paths->trigger_migration && !migration_already_happened) { #if BUILDFLAG(IS_WIN) && DCHECK_IS_ON() // On Windows, if network sandbox is enabled then there a migration must // happen, so `trigger_migration` must be true, or a migration must have // already happened. DCHECK(!sandbox_params.sandbox_enabled); #endif return SandboxGrantResult::kNoMigrationRequested; } // Create the `data_directory` if necessary so access can be granted to it. // Note that if a migration has already happened then this does nothing, as // the directory already exists. if (!base::CreateDirectory(params->file_paths->data_directory.path())) { PLOG(ERROR) << "Failed to create network context data directory " << params->file_paths->data_directory.path(); // This is a fatal error, if the `data_directory` does not exist then // migration cannot be attempted. In this case the network context will // operate using `unsandboxed_data_path` and the migration attempt will be // retried the next time the same network context is created with // `trigger_migration` set. return SandboxGrantResult::kFailedToCreateDataDirectory; } { SCOPED_UMA_HISTOGRAM_TIMER("NetworkService.TimeToGrantDataAccess"); // This must be done on each load of the network context for two // platform-specific reasons: // // 1. On Windows Chrome, the LPAC SID for each channel is different so it is // possible that this data might be read by a different channel and we need // to explicitly support that. // 2. Other platforms such as macOS and Linux need to grant access each time // as they do not rely on filesystem permissions, but runtime sandbox broker // permissions. if (!MaybeGrantAccessToDataPath(sandbox_params, ¶ms->file_paths->data_directory)) { PLOG(ERROR) << "Failed to grant sandbox access to network context data directory " << params->file_paths->data_directory.path(); // If migration has already happened there isn't much that can be done // about this, the data has already moved, but the sandbox might not have // access. if (migration_already_happened) return SandboxGrantResult::kMigrationAlreadySucceededWithNoAccess; // If migration hasn't happened yet, then fail here, and do not attempt to // migrate or proceed further. Better to just leave the data where it is. // In this case `unsandboxed_data_path` will continue to be used and the // migration attempt will be retried the next time the same network // context is created with `trigger_migration` set. return SandboxGrantResult::kFailedToGrantSandboxAccessToData; } } // SCOPED_UMA_HISTOGRAM_TIMER // This covers cases 3. and 4. where a migration has already happened. if (migration_already_happened) { // Migration succeeded in an earlier attempt and `data_directory` is valid, // but clean up any old data that might have failed to delete in the last // attempt. SandboxGrantResult cleanup_result = CleanUpOldData(params); if (cleanup_result != SandboxGrantResult::kSuccess) return cleanup_result; return SandboxGrantResult::kMigrationAlreadySucceeded; } SandboxGrantResult result; // Reaching here means case 2. where a migration hasn't yet happened, but it's // been requested. // // Now attempt to migrate the data from the `unsandboxed_data_path` to the new // `data_directory`. This code can be removed from content once migration has // taken place. // // This code has a three stage process. // 1. An attempt is made to copy all the data files from the old location to // the new location. // 2. A checkpoint file ("NetworkData") is then placed in the new directory to // mark that the data there is valid and should be used. // 3. The old files are deleted. // // A failure half way through stage 1 or 2 will mean that the old data should // be used instead of the new data. A failure to delete the files will cause // a retry attempt next time the same network context is created. { // Stage 1: Copy the data files. Note: This might copy files over the top of // existing files if it was partially successful in an earlier attempt. SCOPED_UMA_HISTOGRAM_TIMER("NetworkService.TimeToMigrateData"); result = MaybeCopyData(*params->file_paths->unsandboxed_data_path, params->file_paths->data_directory.path(), params->file_paths->cookie_database_name, /*is_sql=*/true); if (result != SandboxGrantResult::kSuccess) return result; result = MaybeCopyData(*params->file_paths->unsandboxed_data_path, params->file_paths->data_directory.path(), params->file_paths->http_server_properties_file_name, /*is_sql=*/false); if (result != SandboxGrantResult::kSuccess) return result; result = MaybeCopyData( *params->file_paths->unsandboxed_data_path, params->file_paths->data_directory.path(), params->file_paths->transport_security_persister_file_name, /*is_sql=*/false); if (result != SandboxGrantResult::kSuccess) return result; result = MaybeCopyData(*params->file_paths->unsandboxed_data_path, params->file_paths->data_directory.path(), params->file_paths->reporting_and_nel_store_database_name, /*is_sql=*/true); if (result != SandboxGrantResult::kSuccess) return result; result = MaybeCopyData(*params->file_paths->unsandboxed_data_path, params->file_paths->data_directory.path(), params->file_paths->trust_token_database_name, /*is_sql=*/true); if (result != SandboxGrantResult::kSuccess) return result; // Files all copied successfully. Can now proceed to Stage 2 and write the // checkpoint filename. base::File checkpoint_file( checkpoint_filename, base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE); if (!checkpoint_file.IsValid()) return SandboxGrantResult::kFailedToCreateCheckpointFile; } // SCOPED_UMA_HISTOGRAM_TIMER // Double check the checkpoint file is there. This should never happen. if (!base::PathExists(checkpoint_filename)) return SandboxGrantResult::kFailedToCreateCheckpointFile; // Success, proceed to Stage 3 and clean up old files. SandboxGrantResult cleanup_result = CleanUpOldData(params); if (cleanup_result != SandboxGrantResult::kSuccess) return cleanup_result; return SandboxGrantResult::kSuccess; } } // namespace void GrantSandboxAccessOnThreadPool( network::mojom::NetworkContextParamsPtr params, base::OnceCallback<void(network::mojom::NetworkContextParamsPtr, SandboxGrantResult)> result_callback) { SandboxParameters sandbox_params = {}; #if BUILDFLAG(IS_WIN) sandbox_params.lpac_capability_name = GetContentClient()->browser()->GetLPACCapabilityNameForNetworkService(); #if DCHECK_IS_ON() sandbox_params.sandbox_enabled = GetContentClient()->browser()->ShouldSandboxNetworkService(); #endif // DCHECK_IS_ON() #endif // BUILDFLAG(IS_WIN) base::OnceCallback<SandboxGrantResult()> worker_task = base::BindOnce(&MaybeGrantSandboxAccessToNetworkContextData, sandbox_params, params.get()); base::ThreadPool::PostTaskAndReplyWithResult( FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_BLOCKING}, std::move(worker_task), base::BindOnce(std::move(result_callback), std::move(params))); } } // namespace content