| // 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 "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/common/content_client.h" |
| #include "content/public/common/network_service_util.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/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 absl::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 absl::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::features::IsAppContainerSandboxSupported()) |
| 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. |
| // 2. 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) |
| |
| // 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->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->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"); |
| if (!MaybeGrantAccessToDataPath(sandbox_params, |
| &*params->http_cache_directory)) { |
| PLOG(ERROR) << "Failed to grant sandbox access to cache directory " |
| << params->http_cache_directory->path(); |
| } |
| } |
| } |
| |
| // No file paths (e.g. in-memory context) so nothing to do. |
| if (!params->file_paths) |
| return SandboxGrantResult::kDidNotAttemptToGrantSandboxAccess; |
| |
| DCHECK(!params->file_paths->data_directory.path().empty()); |
| |
| 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 |