| // Copyright 2016 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/safe_browsing/core/browser/db/v4_local_database_manager.h" |
| |
| #include <utility> |
| #include <vector> |
| |
| #include "base/check_op.h" |
| #include "base/command_line.h" |
| #include "base/containers/contains.h" |
| #include "base/containers/fixed_flat_map.h" |
| #include "base/containers/span.h" |
| #include "base/feature_list.h" |
| #include "base/files/file.h" |
| #include "base/files/file_enumerator.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/ranges/algorithm.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_piece.h" |
| #include "base/strings/string_tokenizer.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/task/thread_pool.h" |
| #include "build/branding_buildflags.h" |
| #include "build/build_config.h" |
| #include "components/safe_browsing/core/browser/db/v4_protocol_manager_util.h" |
| #include "components/safe_browsing/core/common/features.h" |
| #include "crypto/sha2.h" |
| #include "mojo/public/cpp/bindings/callback_helpers.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| |
| namespace safe_browsing { |
| |
| namespace { |
| |
| struct CommandLineSwitchAndThreatType { |
| const char* cmdline_switch; |
| ThreatType threat_type; |
| }; |
| |
| // The expiration time of the full hash stored in the artificial database. |
| const int64_t kFullHashExpiryTimeInMinutes = 60; |
| |
| // The number of bytes in a full hash entry. |
| const int64_t kBytesPerFullHashEntry = 32; |
| |
| // The minimum number of entries in the allowlist. If the actual size is |
| // smaller than this number, the allowlist is considered as unavailable. |
| const int kHighConfidenceAllowlistMinimumEntryCount = 100; |
| |
| // If the switch is present, any high-confidence allowlist check will return |
| // that it does not match the allowlist. |
| const char kSkipHighConfidenceAllowlist[] = |
| "safe-browsing-skip-high-confidence-allowlist"; |
| |
| const ThreatSeverity kLeastSeverity = |
| std::numeric_limits<ThreatSeverity>::max(); |
| |
| const char* const kStoreFileNamesToDelete[] = {"IpMalware.store"}; |
| |
| ListInfos GetListInfos() { |
| // NOTE(vakh): When adding a store here, add the corresponding store-specific |
| // histograms also. |
| // The first argument to ListInfo specifies whether to sync hash prefixes for |
| // that list. This can be false for two reasons: |
| // - The server doesn't support that list yet. Once the server adds support |
| // for it, it can be changed to true. |
| // - The list doesn't have hash prefixes to match. All requests lead to full |
| // hash checks. For instance: GetChromeUrlApiId() |
| |
| #if BUILDFLAG(IS_IOS) |
| const bool kSyncOnIos = true; |
| #else |
| const bool kSyncOnIos = false; |
| #endif |
| |
| #if BUILDFLAG(GOOGLE_CHROME_BRANDING) |
| const bool kIsChromeBranded = true; |
| #else |
| const bool kIsChromeBranded = false; |
| #endif |
| |
| const bool kSyncOnDesktopBuilds = !kSyncOnIos; |
| const bool kSyncOnChromeDesktopBuilds = |
| kIsChromeBranded && kSyncOnDesktopBuilds; |
| const bool kSyncAlways = true; |
| const bool kSyncNever = false; |
| |
| return ListInfos({ |
| ListInfo(kSyncAlways, "UrlSoceng.store", GetUrlSocEngId(), |
| SB_THREAT_TYPE_URL_PHISHING), |
| ListInfo(kSyncAlways, "UrlMalware.store", GetUrlMalwareId(), |
| SB_THREAT_TYPE_URL_MALWARE), |
| ListInfo(kSyncOnDesktopBuilds, "UrlUws.store", GetUrlUwsId(), |
| SB_THREAT_TYPE_URL_UNWANTED), |
| ListInfo(kSyncOnDesktopBuilds, "UrlMalBin.store", GetUrlMalBinId(), |
| SB_THREAT_TYPE_URL_BINARY_MALWARE), |
| ListInfo(kSyncOnDesktopBuilds, "ChromeExtMalware.store", |
| GetChromeExtMalwareId(), SB_THREAT_TYPE_EXTENSION), |
| ListInfo(kSyncOnChromeDesktopBuilds, "ChromeUrlClientIncident.store", |
| GetChromeUrlClientIncidentId(), |
| SB_THREAT_TYPE_BLOCKLISTED_RESOURCE), |
| ListInfo(kSyncAlways, "UrlBilling.store", GetUrlBillingId(), |
| SB_THREAT_TYPE_BILLING), |
| ListInfo(kSyncOnChromeDesktopBuilds, "UrlCsdDownloadAllowlist.store", |
| GetUrlCsdDownloadAllowlistId(), SB_THREAT_TYPE_UNUSED), |
| ListInfo(kSyncOnChromeDesktopBuilds || kSyncOnIos, |
| "UrlCsdAllowlist.store", GetUrlCsdAllowlistId(), |
| SB_THREAT_TYPE_CSD_ALLOWLIST), |
| ListInfo(kSyncOnChromeDesktopBuilds, "UrlSubresourceFilter.store", |
| GetUrlSubresourceFilterId(), SB_THREAT_TYPE_SUBRESOURCE_FILTER), |
| ListInfo(kSyncOnChromeDesktopBuilds, "UrlSuspiciousSite.store", |
| GetUrlSuspiciousSiteId(), SB_THREAT_TYPE_SUSPICIOUS_SITE), |
| ListInfo(kSyncNever, "", GetChromeUrlApiId(), SB_THREAT_TYPE_API_ABUSE), |
| ListInfo(kSyncOnChromeDesktopBuilds || kSyncOnIos, |
| "UrlHighConfidenceAllowlist.store", |
| GetUrlHighConfidenceAllowlistId(), |
| SB_THREAT_TYPE_HIGH_CONFIDENCE_ALLOWLIST), |
| }); |
| // NOTE(vakh): IMPORTANT: Please make sure that the server already supports |
| // any list before adding it to this list otherwise the prefix updates break |
| // for all Canary users. |
| } |
| |
| base::span<const CommandLineSwitchAndThreatType> GetSwitchAndThreatTypes() { |
| static constexpr CommandLineSwitchAndThreatType |
| kCommandLineSwitchAndThreatType[] = { |
| {"mark_as_allowlisted_for_phish_guard", CSD_WHITELIST}, |
| {"mark_as_allowlisted_for_real_time", HIGH_CONFIDENCE_ALLOWLIST}, |
| {"mark_as_phishing", SOCIAL_ENGINEERING}, |
| {"mark_as_malware", MALWARE_THREAT}, |
| {"mark_as_uws", UNWANTED_SOFTWARE}}; |
| return kCommandLineSwitchAndThreatType; |
| } |
| |
| // Returns the severity information about a given SafeBrowsing list. The lowest |
| // value is 0, which represents the most severe list. |
| ThreatSeverity GetThreatSeverity(const ListIdentifier& list_id) { |
| switch (list_id.threat_type()) { |
| case MALWARE_THREAT: |
| case SOCIAL_ENGINEERING: |
| case MALICIOUS_BINARY: |
| return 0; |
| case UNWANTED_SOFTWARE: |
| return 1; |
| case API_ABUSE: |
| case CLIENT_INCIDENT: |
| case SUBRESOURCE_FILTER: |
| return 2; |
| case CSD_WHITELIST: |
| case HIGH_CONFIDENCE_ALLOWLIST: |
| return 3; |
| case SUSPICIOUS: |
| return 4; |
| case BILLING: |
| return 15; |
| case CSD_DOWNLOAD_WHITELIST: |
| case POTENTIALLY_HARMFUL_APPLICATION: |
| case SOCIAL_ENGINEERING_PUBLIC: |
| case THREAT_TYPE_UNSPECIFIED: |
| NOTREACHED() << "Unexpected ThreatType encountered: " |
| << list_id.threat_type(); |
| return kLeastSeverity; |
| } |
| } |
| |
| // This is only valid for types that are passed to GetBrowseUrl(). |
| ListIdentifier GetUrlIdFromSBThreatType(SBThreatType sb_threat_type) { |
| switch (sb_threat_type) { |
| case SB_THREAT_TYPE_URL_MALWARE: |
| return GetUrlMalwareId(); |
| |
| case SB_THREAT_TYPE_URL_PHISHING: |
| return GetUrlSocEngId(); |
| |
| case SB_THREAT_TYPE_URL_UNWANTED: |
| return GetUrlUwsId(); |
| |
| case SB_THREAT_TYPE_SUSPICIOUS_SITE: |
| return GetUrlSuspiciousSiteId(); |
| |
| case SB_THREAT_TYPE_BILLING: |
| return GetUrlBillingId(); |
| |
| default: |
| NOTREACHED(); |
| // Compiler requires a return statement here. |
| return GetUrlMalwareId(); |
| } |
| } |
| |
| StoresToCheck CreateStoresToCheckFromSBThreatTypeSet( |
| const SBThreatTypeSet& threat_types) { |
| StoresToCheck stores_to_check; |
| for (SBThreatType sb_threat_type : threat_types) { |
| stores_to_check.insert(GetUrlIdFromSBThreatType(sb_threat_type)); |
| } |
| return stores_to_check; |
| } |
| |
| // These values are persisted to logs. Entries should not be renumbered and |
| // numeric values should never be reused. |
| enum StoreAvailabilityResult { |
| // Unknown availability. This is unexpected. |
| UNKNOWN = 0, |
| |
| // The local database is not enabled. |
| NOT_ENABLED = 1, |
| |
| // The database is still being loaded. |
| DATABASE_UNAVAILABLE = 2, |
| |
| // The requested store is unavailable. |
| STORE_UNAVAILABLE = 3, |
| |
| // The store is available. |
| AVAILABLE = 4, |
| COUNT, |
| }; |
| |
| void RecordTimeSinceLastUpdateHistograms(const base::Time& last_response_time) { |
| if (last_response_time.is_null()) { |
| return; |
| } |
| |
| base::TimeDelta time_since_update = base::Time::Now() - last_response_time; |
| UMA_HISTOGRAM_LONG_TIMES_100( |
| "SafeBrowsing.V4LocalDatabaseManager.TimeSinceLastUpdateResponse", |
| time_since_update); |
| } |
| |
| void RecordCheckUrlForHighConfidenceAllowlistBoolean( |
| const std::string& metric_name, |
| const std::string& metric_variation, |
| bool value) { |
| auto histogram_name = |
| base::StrCat({"SafeBrowsing.", metric_variation, ".", metric_name}); |
| DCHECK(histogram_name == "SafeBrowsing.RT.AllStoresAvailable" || |
| histogram_name == "SafeBrowsing.HPRT.AllStoresAvailable" || |
| histogram_name == "SafeBrowsing.RT.AllowlistSizeTooSmall" || |
| histogram_name == "SafeBrowsing.HPRT.AllowlistSizeTooSmall"); |
| base::UmaHistogramBoolean(histogram_name, value); |
| } |
| |
| void MaybeDeleteStore(const base::FilePath& path) { |
| bool path_exists = base::PathExists(path); |
| base::UmaHistogramBoolean( |
| "SafeBrowsing.V4UnusedStoreFileExists" + GetUmaSuffixForStore(path), |
| path_exists); |
| if (!path_exists) { |
| return; |
| } |
| |
| // The MmapHashPrefixMap maintains several helper files stored in the same |
| // directory as the main store file. These are usually found by looking at the |
| // `hash_files` field in the `V4StoreFileFormat`, but we haven't read the |
| // store at this point. Instead we use the fact that these helper files have a |
| // simple structure to delete them all. |
| base::FileEnumerator enumerator( |
| path.DirName(), false, base::FileEnumerator::FILES, |
| path.BaseName().value() + FILE_PATH_LITERAL("*")); |
| for (base::FilePath store_path = enumerator.Next(); !store_path.empty(); |
| store_path = enumerator.Next()) { |
| base::DeleteFile(store_path); |
| } |
| } |
| |
| bool GetPrefixMatchesIsAsync() { |
| return base::FeatureList::IsEnabled(kMmapSafeBrowsingDatabase) && |
| kMmapSafeBrowsingDatabaseAsync.Get(); |
| } |
| |
| void HandleUrlCallback(base::OnceCallback<void(bool)> callback, |
| FullHashToStoreAndHashPrefixesMap results) { |
| bool allowed = !results.empty(); |
| if (GetPrefixMatchesIsAsync()) { |
| // This callback was already run asynchronously so no need for another |
| // thread hop. |
| std::move(callback).Run(allowed); |
| } else { |
| // Need a thread hop to avoid reentrancy. |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(std::move(callback), allowed)); |
| } |
| } |
| |
| } // namespace |
| |
| V4LocalDatabaseManager::PendingCheck::PendingCheck( |
| Client* client, |
| ClientCallbackType client_callback_type, |
| const StoresToCheck& stores_to_check, |
| const std::vector<GURL>& urls, |
| MechanismExperimentHashDatabaseCache experiment_cache_selection) |
| : client(client), |
| client_callback_type(client_callback_type), |
| most_severe_threat_type(SB_THREAT_TYPE_SAFE), |
| stores_to_check(stores_to_check), |
| urls(urls), |
| mechanism_experiment_cache_selection(experiment_cache_selection) { |
| for (const auto& url : urls) { |
| V4ProtocolManagerUtil::UrlToFullHashes(url, &full_hashes); |
| } |
| full_hash_threat_types.assign(full_hashes.size(), SB_THREAT_TYPE_SAFE); |
| } |
| |
| V4LocalDatabaseManager::PendingCheck::PendingCheck( |
| Client* client, |
| ClientCallbackType client_callback_type, |
| const StoresToCheck& stores_to_check, |
| const std::set<FullHashStr>& full_hashes_set) |
| : client(client), |
| client_callback_type(client_callback_type), |
| most_severe_threat_type(SB_THREAT_TYPE_SAFE), |
| stores_to_check(stores_to_check) { |
| full_hashes.assign(full_hashes_set.begin(), full_hashes_set.end()); |
| DCHECK(full_hashes.size()); |
| full_hash_threat_types.assign(full_hashes.size(), SB_THREAT_TYPE_SAFE); |
| } |
| |
| V4LocalDatabaseManager::PendingCheck::~PendingCheck() { |
| DCHECK(!is_in_pending_checks); |
| } |
| |
| // static |
| const V4LocalDatabaseManager* |
| V4LocalDatabaseManager::current_local_database_manager_; |
| |
| // static |
| scoped_refptr<V4LocalDatabaseManager> V4LocalDatabaseManager::Create( |
| const base::FilePath& base_path, |
| scoped_refptr<base::SequencedTaskRunner> ui_task_runner, |
| scoped_refptr<base::SequencedTaskRunner> io_task_runner, |
| ExtendedReportingLevelCallback extended_reporting_level_callback) { |
| return base::WrapRefCounted(new V4LocalDatabaseManager( |
| base_path, extended_reporting_level_callback, std::move(ui_task_runner), |
| std::move(io_task_runner), nullptr)); |
| } |
| |
| void V4LocalDatabaseManager::CollectDatabaseManagerInfo( |
| DatabaseManagerInfo* database_manager_info, |
| FullHashCacheInfo* full_hash_cache_info) const { |
| if (v4_update_protocol_manager_) { |
| v4_update_protocol_manager_->CollectUpdateInfo( |
| database_manager_info->mutable_update_info()); |
| } |
| if (v4_database_) { |
| v4_database_->CollectDatabaseInfo( |
| database_manager_info->mutable_database_info()); |
| } |
| if (v4_get_hash_protocol_manager_) { |
| v4_get_hash_protocol_manager_->CollectFullHashCacheInfo( |
| full_hash_cache_info); |
| } |
| } |
| |
| V4LocalDatabaseManager::V4LocalDatabaseManager( |
| const base::FilePath& base_path, |
| ExtendedReportingLevelCallback extended_reporting_level_callback, |
| scoped_refptr<base::SequencedTaskRunner> ui_task_runner, |
| scoped_refptr<base::SequencedTaskRunner> io_task_runner, |
| scoped_refptr<base::SequencedTaskRunner> task_runner_for_tests) |
| : SafeBrowsingDatabaseManager(std::move(ui_task_runner), |
| std::move(io_task_runner)), |
| base_path_(base_path), |
| extended_reporting_level_callback_(extended_reporting_level_callback), |
| list_infos_(GetListInfos()), |
| task_runner_(task_runner_for_tests |
| ? task_runner_for_tests |
| : base::ThreadPool::CreateSequencedTaskRunner( |
| {base::MayBlock(), |
| base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})), |
| v4_database_(std::unique_ptr<V4Database, base::OnTaskRunnerDeleter>( |
| nullptr, |
| base::OnTaskRunnerDeleter(nullptr))) { |
| DCHECK(this->ui_task_runner()->RunsTasksInCurrentSequence()); |
| DCHECK(!base_path_.empty()); |
| DCHECK(!list_infos_.empty()); |
| |
| DeleteUnusedStoreFiles(); |
| } |
| |
| V4LocalDatabaseManager::~V4LocalDatabaseManager() { |
| DCHECK(!enabled_); |
| } |
| |
| // |
| // Start: SafeBrowsingDatabaseManager implementation |
| // |
| |
| void V4LocalDatabaseManager::CancelCheck(Client* client) { |
| DCHECK(sb_task_runner()->RunsTasksInCurrentSequence()); |
| DCHECK(enabled_); |
| |
| auto pending_it = |
| base::ranges::find(pending_checks_, client, &PendingCheck::client); |
| if (pending_it != pending_checks_.end()) { |
| RemovePendingCheck(pending_it); |
| } |
| |
| auto queued_it = |
| base::ranges::find(queued_checks_, client, &PendingCheck::client); |
| if (queued_it != queued_checks_.end()) { |
| queued_checks_.erase(queued_it); |
| } |
| } |
| |
| bool V4LocalDatabaseManager::CanCheckRequestDestination( |
| network::mojom::RequestDestination request_destination) const { |
| // We check all destinations since most checks are fast. |
| return true; |
| } |
| |
| bool V4LocalDatabaseManager::CanCheckUrl(const GURL& url) const { |
| return url.SchemeIsHTTPOrHTTPS() || url.SchemeIs(url::kFtpScheme) || |
| url.SchemeIsWSOrWSS(); |
| } |
| |
| bool V4LocalDatabaseManager::ChecksAreAlwaysAsync() const { |
| return false; |
| } |
| |
| bool V4LocalDatabaseManager::CheckBrowseUrl( |
| const GURL& url, |
| const SBThreatTypeSet& threat_types, |
| Client* client, |
| MechanismExperimentHashDatabaseCache experiment_cache_selection) { |
| DCHECK(sb_task_runner()->RunsTasksInCurrentSequence()); |
| DCHECK(!threat_types.empty()); |
| DCHECK(SBThreatTypeSetIsValidForCheckBrowseUrl(threat_types)); |
| |
| if (!enabled_ || !CanCheckUrl(url)) { |
| return true; |
| } |
| |
| std::unique_ptr<PendingCheck> check = std::make_unique<PendingCheck>( |
| client, ClientCallbackType::CHECK_BROWSE_URL, |
| CreateStoresToCheckFromSBThreatTypeSet(threat_types), |
| std::vector<GURL>(1, url), experiment_cache_selection); |
| |
| bool safe_synchronously = HandleCheck(std::move(check)); |
| UMA_HISTOGRAM_BOOLEAN("SafeBrowsing.CheckBrowseUrl.HasLocalMatch", |
| !safe_synchronously); |
| RecordTimeSinceLastUpdateHistograms( |
| v4_update_protocol_manager_->last_response_time()); |
| return safe_synchronously; |
| } |
| |
| bool V4LocalDatabaseManager::CheckDownloadUrl( |
| const std::vector<GURL>& url_chain, |
| Client* client) { |
| DCHECK(sb_task_runner()->RunsTasksInCurrentSequence()); |
| |
| if (!enabled_ || url_chain.empty()) { |
| return true; |
| } |
| |
| std::unique_ptr<PendingCheck> check = std::make_unique<PendingCheck>( |
| client, ClientCallbackType::CHECK_DOWNLOAD_URLS, |
| StoresToCheck({GetUrlMalBinId()}), url_chain, |
| MechanismExperimentHashDatabaseCache::kNoExperiment); |
| |
| return HandleCheck(std::move(check)); |
| } |
| |
| bool V4LocalDatabaseManager::CheckExtensionIDs( |
| const std::set<FullHashStr>& extension_ids, |
| Client* client) { |
| DCHECK(sb_task_runner()->RunsTasksInCurrentSequence()); |
| |
| if (!enabled_) { |
| return true; |
| } |
| |
| std::unique_ptr<PendingCheck> check = std::make_unique<PendingCheck>( |
| client, ClientCallbackType::CHECK_EXTENSION_IDS, |
| StoresToCheck({GetChromeExtMalwareId()}), extension_ids); |
| |
| return HandleCheck(std::move(check)); |
| } |
| |
| bool V4LocalDatabaseManager::CheckResourceUrl(const GURL& url, Client* client) { |
| DCHECK(sb_task_runner()->RunsTasksInCurrentSequence()); |
| |
| StoresToCheck stores_to_check({GetChromeUrlClientIncidentId()}); |
| |
| if (!CanCheckUrl(url) || !AreAllStoresAvailableNow(stores_to_check)) { |
| // Fail open: Mark resource as safe immediately. |
| // TODO(nparker): This should queue the request if the DB isn't yet |
| // loaded, and later decide if this store is available. |
| // Currently this is the only store that requires full-hash-checks |
| // AND isn't supported on Chromium, so it's unique. |
| return true; |
| } |
| |
| std::unique_ptr<PendingCheck> check = std::make_unique<PendingCheck>( |
| client, ClientCallbackType::CHECK_RESOURCE_URL, stores_to_check, |
| std::vector<GURL>(1, url), |
| MechanismExperimentHashDatabaseCache::kNoExperiment); |
| |
| return HandleCheck(std::move(check)); |
| } |
| |
| void V4LocalDatabaseManager::CheckUrlForHighConfidenceAllowlist( |
| const GURL& url, |
| const std::string& metric_variation, |
| base::OnceCallback<void(bool)> callback) { |
| DCHECK(sb_task_runner()->RunsTasksInCurrentSequence()); |
| if (base::CommandLine::ForCurrentProcess()->HasSwitch( |
| kSkipHighConfidenceAllowlist)) { |
| sb_task_runner()->PostTask(FROM_HERE, |
| base::BindOnce(std::move(callback), false)); |
| return; |
| } |
| |
| StoresToCheck stores_to_check({GetUrlHighConfidenceAllowlistId()}); |
| bool all_stores_available = AreAllStoresAvailableNow(stores_to_check); |
| RecordCheckUrlForHighConfidenceAllowlistBoolean( |
| "AllStoresAvailable", metric_variation, all_stores_available); |
| bool is_artificial_prefix_empty = |
| artificially_marked_store_and_hash_prefixes_.empty(); |
| bool is_allowlist_too_small = |
| IsStoreTooSmall(GetUrlHighConfidenceAllowlistId(), kBytesPerFullHashEntry, |
| kHighConfidenceAllowlistMinimumEntryCount); |
| RecordCheckUrlForHighConfidenceAllowlistBoolean( |
| "AllowlistSizeTooSmall", metric_variation, is_allowlist_too_small); |
| if (!enabled_ || (is_allowlist_too_small && is_artificial_prefix_empty) || |
| !CanCheckUrl(url) || |
| (!all_stores_available && is_artificial_prefix_empty)) { |
| // NOTE(vakh): If Safe Browsing isn't enabled yet, or if the URL isn't a |
| // navigation URL, or if the allowlist isn't ready yet, or if the allowlist |
| // is too small, return that there is a match. The full URL check won't be |
| // performed, but hash-based check will still be done. If any artificial |
| // matches are present, consider the allowlist as ready. |
| sb_task_runner()->PostTask(FROM_HERE, |
| base::BindOnce(std::move(callback), true)); |
| return; |
| } |
| |
| std::unique_ptr<PendingCheck> check = std::make_unique<PendingCheck>( |
| nullptr, ClientCallbackType::CHECK_OTHER, stores_to_check, |
| std::vector<GURL>(1, url), |
| MechanismExperimentHashDatabaseCache::kNoExperiment); |
| |
| HandleAllowlistCheck(std::move(check), /*allow_async_full_hash_check=*/false, |
| std::move(callback)); |
| } |
| |
| bool V4LocalDatabaseManager::CheckUrlForSubresourceFilter(const GURL& url, |
| Client* client) { |
| DCHECK(sb_task_runner()->RunsTasksInCurrentSequence()); |
| |
| StoresToCheck stores_to_check( |
| {GetUrlSocEngId(), GetUrlSubresourceFilterId()}); |
| if (!AreAnyStoresAvailableNow(stores_to_check) || !CanCheckUrl(url)) { |
| return true; |
| } |
| |
| std::unique_ptr<PendingCheck> check = std::make_unique<PendingCheck>( |
| client, ClientCallbackType::CHECK_URL_FOR_SUBRESOURCE_FILTER, |
| stores_to_check, std::vector<GURL>(1, url), |
| MechanismExperimentHashDatabaseCache::kNoExperiment); |
| |
| return HandleCheck(std::move(check)); |
| } |
| |
| AsyncMatch V4LocalDatabaseManager::CheckCsdAllowlistUrl(const GURL& url, |
| Client* client) { |
| DCHECK(sb_task_runner()->RunsTasksInCurrentSequence()); |
| |
| StoresToCheck stores_to_check({GetUrlCsdAllowlistId()}); |
| // If any artificial matches are present, consider the allowlist as ready. |
| bool is_artificial_prefix_empty = |
| artificially_marked_store_and_hash_prefixes_.empty(); |
| if ((!AreAllStoresAvailableNow(stores_to_check) && |
| is_artificial_prefix_empty) || |
| !CanCheckUrl(url)) { |
| // Fail open: Allowlist everything. Otherwise we may run the |
| // CSD phishing/malware detector on popular domains and generate |
| // undue load on the client and server, or send Password Reputation |
| // requests on popular sites. This has the effect of disabling |
| // CSD phishing/malware detection and password reputation service |
| // until the store is first synced and/or loaded from disk. |
| return AsyncMatch::MATCH; |
| } |
| |
| std::unique_ptr<PendingCheck> check = std::make_unique<PendingCheck>( |
| client, ClientCallbackType::CHECK_CSD_ALLOWLIST, stores_to_check, |
| std::vector<GURL>(1, url), |
| MechanismExperimentHashDatabaseCache::kNoExperiment); |
| |
| return HandleAllowlistCheck(std::move(check), |
| /*allow_async_full_hash_check=*/true, |
| base::OnceCallback<void(bool)>()); |
| } |
| |
| void V4LocalDatabaseManager::MatchDownloadAllowlistUrl( |
| const GURL& url, |
| base::OnceCallback<void(bool)> callback) { |
| DCHECK(sb_task_runner()->RunsTasksInCurrentSequence()); |
| |
| StoresToCheck stores_to_check({GetUrlCsdDownloadAllowlistId()}); |
| |
| if (!AreAllStoresAvailableNow(stores_to_check) || !CanCheckUrl(url)) { |
| // Fail close: Allowlist nothing. This may generate download-protection |
| // pings for allowlisted domains, but that's fine. |
| sb_task_runner()->PostTask(FROM_HERE, |
| base::BindOnce(std::move(callback), false)); |
| return; |
| } |
| |
| HandleUrl(url, stores_to_check, std::move(callback)); |
| } |
| |
| ThreatSource V4LocalDatabaseManager::GetThreatSource() const { |
| return ThreatSource::LOCAL_PVER4; |
| } |
| |
| bool V4LocalDatabaseManager::IsDownloadProtectionEnabled() const { |
| return true; |
| } |
| |
| void V4LocalDatabaseManager::StartOnSBThread( |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory, |
| const V4ProtocolConfig& config) { |
| SafeBrowsingDatabaseManager::StartOnSBThread(url_loader_factory, config); |
| |
| db_updated_callback_ = base::BindRepeating( |
| &V4LocalDatabaseManager::DatabaseUpdated, weak_factory_.GetWeakPtr()); |
| |
| SetupUpdateProtocolManager(url_loader_factory, config); |
| SetupDatabase(); |
| |
| enabled_ = true; |
| |
| current_local_database_manager_ = this; |
| } |
| |
| void V4LocalDatabaseManager::StopOnSBThread(bool shutdown) { |
| DCHECK(sb_task_runner()->RunsTasksInCurrentSequence()); |
| |
| enabled_ = false; |
| |
| current_local_database_manager_ = nullptr; |
| |
| RespondSafeToQueuedAndPendingChecks(); |
| |
| // Delete the V4Database. Any pending writes to disk are completed. |
| // This operation happens on the task_runner on which v4_database_ operates |
| // and doesn't block the IO thread. |
| if (v4_database_) { |
| v4_database_->StopOnSBThread(); |
| } |
| v4_database_.reset(); |
| |
| // Delete the V4UpdateProtocolManager. |
| // This cancels any in-flight update request. |
| v4_update_protocol_manager_.reset(); |
| |
| db_updated_callback_.Reset(); |
| |
| weak_factory_.InvalidateWeakPtrs(); |
| |
| SafeBrowsingDatabaseManager::StopOnSBThread(shutdown); |
| } |
| |
| // |
| // End: SafeBrowsingDatabaseManager implementation |
| // |
| |
| void V4LocalDatabaseManager::DatabaseReadyForChecks( |
| std::unique_ptr<V4Database, base::OnTaskRunnerDeleter> v4_database) { |
| DCHECK(sb_task_runner()->RunsTasksInCurrentSequence()); |
| |
| v4_database->InitializeOnSBThread(); |
| |
| // The following check is needed because it is possible that by the time the |
| // database is ready, StopOnSBThread has been called. |
| if (enabled_) { |
| v4_database_ = std::move(v4_database); |
| |
| v4_database_->RecordFileSizeHistograms(); |
| |
| PopulateArtificialDatabase(); |
| |
| // The consistency of the stores read from the disk needs to verified. Post |
| // that task on the task runner. It calls |DatabaseReadyForUpdates| |
| // callback with the stores to reset, if any, and then we can schedule the |
| // database updates. |
| v4_database_->VerifyChecksum( |
| base::BindOnce(&V4LocalDatabaseManager::DatabaseReadyForUpdates, |
| weak_factory_.GetWeakPtr())); |
| |
| ProcessQueuedChecks(); |
| } else { |
| // Schedule the deletion of v4_database off IO thread. |
| v4_database.reset(); |
| } |
| } |
| |
| void V4LocalDatabaseManager::DatabaseReadyForUpdates( |
| const std::vector<ListIdentifier>& stores_to_reset) { |
| if (enabled_) { |
| v4_database_->ResetStores(stores_to_reset); |
| UpdateListClientStates(GetStoreStateMap()); |
| |
| // The database is ready to process updates. Schedule them now. |
| v4_update_protocol_manager_->ScheduleNextUpdate(GetStoreStateMap()); |
| } |
| } |
| |
| void V4LocalDatabaseManager::DatabaseUpdated() { |
| if (enabled_) { |
| v4_update_protocol_manager_->ScheduleNextUpdate(GetStoreStateMap()); |
| |
| v4_database_->RecordFileSizeHistograms(); |
| UpdateListClientStates(GetStoreStateMap()); |
| |
| ui_task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| &SafeBrowsingDatabaseManager::NotifyDatabaseUpdateFinished, this)); |
| } |
| } |
| |
| void V4LocalDatabaseManager::GetArtificialPrefixMatches( |
| const std::unique_ptr<PendingCheck>& check) { |
| if (artificially_marked_store_and_hash_prefixes_.empty()) { |
| return; |
| } |
| for (const auto& full_hash : check->full_hashes) { |
| for (const StoreAndHashPrefix& artificial_store_and_hash_prefix : |
| artificially_marked_store_and_hash_prefixes_) { |
| FullHashStr artificial_full_hash = |
| artificial_store_and_hash_prefix.hash_prefix; |
| DCHECK_EQ(crypto::kSHA256Length, artificial_full_hash.size()); |
| if (artificial_full_hash == full_hash && |
| base::Contains(check->stores_to_check, |
| artificial_store_and_hash_prefix.list_id)) { |
| (check->artificial_full_hash_to_store_and_hash_prefixes)[full_hash] = { |
| artificial_store_and_hash_prefix}; |
| } |
| } |
| } |
| } |
| |
| void V4LocalDatabaseManager::GetPrefixMatches( |
| PendingCheck* check, |
| base::OnceCallback<void(FullHashToStoreAndHashPrefixesMap)> callback) { |
| DCHECK(sb_task_runner()->RunsTasksInCurrentSequence()); |
| DCHECK(enabled_); |
| |
| v4_database_->GetStoresMatchingFullHash( |
| check->full_hashes, check->stores_to_check, std::move(callback)); |
| } |
| |
| void V4LocalDatabaseManager::GetSeverestThreatTypeAndMetadata( |
| const std::vector<FullHashInfo>& full_hash_infos, |
| const std::vector<FullHashStr>& full_hashes, |
| std::vector<SBThreatType>* full_hash_threat_types, |
| SBThreatType* most_severe_threat_type, |
| ThreatMetadata* metadata, |
| FullHashStr* matching_full_hash) { |
| UMA_HISTOGRAM_COUNTS_100("SafeBrowsing.V4LocalDatabaseManager.ThreatInfoSize", |
| full_hash_infos.size()); |
| ThreatSeverity most_severe_yet = kLeastSeverity; |
| for (const FullHashInfo& fhi : full_hash_infos) { |
| ThreatSeverity severity = GetThreatSeverity(fhi.list_id); |
| SBThreatType threat_type = GetSBThreatTypeForList(fhi.list_id); |
| |
| const auto& it = base::ranges::find(full_hashes, fhi.full_hash); |
| DCHECK(it != full_hashes.end()); |
| (*full_hash_threat_types)[it - full_hashes.begin()] = threat_type; |
| |
| if (severity < most_severe_yet) { |
| most_severe_yet = severity; |
| *most_severe_threat_type = threat_type; |
| *metadata = fhi.metadata; |
| *matching_full_hash = fhi.full_hash; |
| } |
| } |
| } |
| |
| StoresToCheck V4LocalDatabaseManager::GetStoresForFullHashRequests() { |
| StoresToCheck stores_for_full_hash; |
| for (const auto& info : list_infos_) { |
| stores_for_full_hash.insert(info.list_id()); |
| } |
| return stores_for_full_hash; |
| } |
| |
| std::unique_ptr<StoreStateMap> V4LocalDatabaseManager::GetStoreStateMap() { |
| return v4_database_->GetStoreStateMap(); |
| } |
| |
| // Returns the SBThreatType corresponding to a given SafeBrowsing list. |
| SBThreatType V4LocalDatabaseManager::GetSBThreatTypeForList( |
| const ListIdentifier& list_id) { |
| auto it = base::ranges::find(list_infos_, list_id, &ListInfo::list_id); |
| DCHECK(list_infos_.end() != it); |
| DCHECK_NE(SB_THREAT_TYPE_SAFE, it->sb_threat_type()); |
| DCHECK_NE(SB_THREAT_TYPE_UNUSED, it->sb_threat_type()); |
| return it->sb_threat_type(); |
| } |
| |
| AsyncMatch V4LocalDatabaseManager::HandleAllowlistCheck( |
| std::unique_ptr<PendingCheck> check, |
| bool allow_async_full_hash_check, |
| base::OnceCallback<void(bool)> callback) { |
| // We don't bother queuing allowlist checks since the DB will |
| // normally be available already -- allowlists are used after page load, |
| // and navigations are blocked until the DB is ready and dequeues checks. |
| // The caller should have already checked that the DB is ready. |
| DCHECK(v4_database_); |
| |
| PendingCheck* check_ptr = check.get(); |
| AsyncMatch match; |
| |
| if (GetPrefixMatchesIsAsync() && !callback.is_null()) { |
| // If StopOnSBThread is called weak_factory_ will get invalidated and |
| // HandleAllowlistCheckContinuation won't be called. We still want to run |
| // the callback though. See comment in CheckUrlForHighConfidenceAllowlist |
| // on why this returns true. |
| callback = |
| mojo::WrapCallbackWithDefaultInvokeIfNotRun(std::move(callback), true); |
| } |
| |
| GetPrefixMatches( |
| check_ptr, |
| base::BindOnce(&V4LocalDatabaseManager::HandleAllowlistCheckContinuation, |
| weak_factory_.GetWeakPtr(), std::move(check), |
| allow_async_full_hash_check, std::move(callback), |
| GetPrefixMatchesIsAsync() ? nullptr : &match)); |
| |
| if (GetPrefixMatchesIsAsync()) { |
| AddPendingCheck(check_ptr); |
| return AsyncMatch::ASYNC; |
| } |
| |
| return match; |
| } |
| |
| void V4LocalDatabaseManager::HandleAllowlistCheckContinuation( |
| std::unique_ptr<PendingCheck> check, |
| bool allow_async_full_hash_check, |
| base::OnceCallback<void(bool)> callback, |
| AsyncMatch* match, |
| FullHashToStoreAndHashPrefixesMap results) { |
| DCHECK(sb_task_runner()->RunsTasksInCurrentSequence()); |
| |
| AsyncMatch local_match; |
| if (GetPrefixMatchesIsAsync()) { |
| if (!enabled_) { |
| DCHECK(pending_checks_.empty()); |
| return; |
| } |
| |
| const auto it = pending_checks_.find(check.get()); |
| if (it == pending_checks_.end()) { |
| // The check has since been cancelled. |
| return; |
| } |
| |
| RemovePendingCheck(it); |
| match = &local_match; |
| } |
| |
| check->full_hash_to_store_and_hash_prefixes = results; |
| GetArtificialPrefixMatches(check); |
| if (check->full_hash_to_store_and_hash_prefixes.empty() && |
| check->artificial_full_hash_to_store_and_hash_prefixes.empty()) { |
| *match = AsyncMatch::NO_MATCH; |
| } else { |
| // Look for any full-length hash in the matches. If there is one, |
| // there's no need for a full-hash check. This saves bandwidth for |
| // very popular sites since they'll have full-length hashes locally. |
| // These loops will have exactly 1 entry most of the time. |
| bool found = false; |
| for (const auto& entry : check->full_hash_to_store_and_hash_prefixes) { |
| for (const auto& store_and_prefix : entry.second) { |
| if (store_and_prefix.hash_prefix.size() == kMaxHashPrefixLength) { |
| *match = AsyncMatch::MATCH; |
| found = true; |
| break; |
| } |
| } |
| } |
| |
| if (!found) { |
| if (!allow_async_full_hash_check) { |
| *match = AsyncMatch::NO_MATCH; |
| } else { |
| *match = AsyncMatch::ASYNC; |
| ScheduleFullHashCheck(std::move(check)); |
| return; |
| } |
| } |
| } |
| |
| if (check->client_callback_type == ClientCallbackType::CHECK_OTHER) { |
| bool result = *match == AsyncMatch::MATCH; |
| if (GetPrefixMatchesIsAsync()) { |
| // This is already asynchronous so no need for another PostTask. |
| std::move(callback).Run(result); |
| } else { |
| sb_task_runner()->PostTask(FROM_HERE, |
| base::BindOnce(std::move(callback), result)); |
| } |
| } else if (check->client_callback_type == |
| ClientCallbackType::CHECK_CSD_ALLOWLIST) { |
| if (GetPrefixMatchesIsAsync()) { |
| check->most_severe_threat_type = SB_THREAT_TYPE_CSD_ALLOWLIST; |
| RespondToClient(std::move(check)); |
| } |
| } else { |
| NOTREACHED(); |
| } |
| } |
| |
| bool V4LocalDatabaseManager::HandleCheck(std::unique_ptr<PendingCheck> check) { |
| if (!v4_database_) { |
| queued_checks_.push_back(std::move(check)); |
| return false; |
| } |
| |
| PendingCheck* check_ptr = check.get(); |
| AsyncMatch match; |
| GetPrefixMatches( |
| check_ptr, |
| base::BindOnce(&V4LocalDatabaseManager::HandleCheckContinuation, |
| weak_factory_.GetWeakPtr(), std::move(check), |
| GetPrefixMatchesIsAsync() ? nullptr : &match)); |
| |
| if (GetPrefixMatchesIsAsync()) { |
| AddPendingCheck(check_ptr); |
| return false; |
| } |
| |
| return match == AsyncMatch::NO_MATCH; |
| } |
| |
| void V4LocalDatabaseManager::HandleCheckContinuation( |
| std::unique_ptr<PendingCheck> check, |
| AsyncMatch* match, |
| FullHashToStoreAndHashPrefixesMap results) { |
| AsyncMatch local_match; |
| if (GetPrefixMatchesIsAsync()) { |
| if (!enabled_) { |
| DCHECK(pending_checks_.empty()); |
| return; |
| } |
| |
| const auto it = pending_checks_.find(check.get()); |
| if (it == pending_checks_.end()) { |
| // The check has since been cancelled. |
| return; |
| } |
| |
| RemovePendingCheck(it); |
| match = &local_match; |
| } |
| |
| check->full_hash_to_store_and_hash_prefixes = results; |
| GetArtificialPrefixMatches(check); |
| if (check->full_hash_to_store_and_hash_prefixes.empty() && |
| check->artificial_full_hash_to_store_and_hash_prefixes.empty()) { |
| *match = AsyncMatch::NO_MATCH; |
| |
| if (GetPrefixMatchesIsAsync()) { |
| RespondToClient(std::move(check)); |
| } |
| } else { |
| *match = AsyncMatch::ASYNC; |
| |
| ScheduleFullHashCheck(std::move(check)); |
| } |
| } |
| |
| void V4LocalDatabaseManager::PopulateArtificialDatabase() { |
| for (const auto& switch_and_threat_type : GetSwitchAndThreatTypes()) { |
| const std::string raw_artificial_urls = |
| base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( |
| switch_and_threat_type.cmdline_switch); |
| base::StringTokenizer tokenizer(raw_artificial_urls, ","); |
| while (tokenizer.GetNext()) { |
| ListIdentifier artificial_list_id(GetCurrentPlatformType(), URL, |
| switch_and_threat_type.threat_type); |
| FullHashStr full_hash = |
| V4ProtocolManagerUtil::GetFullHash(GURL(tokenizer.token_piece())); |
| artificially_marked_store_and_hash_prefixes_.emplace_back( |
| artificial_list_id, full_hash); |
| } |
| } |
| } |
| |
| void V4LocalDatabaseManager::ScheduleFullHashCheck( |
| std::unique_ptr<PendingCheck> check) { |
| DCHECK(sb_task_runner()->RunsTasksInCurrentSequence()); |
| |
| // Add check to pending_checks_ before scheduling PerformFullHashCheck so that |
| // even if the client calls CancelCheck before PerformFullHashCheck gets |
| // called, the check can be found in pending_checks_. |
| AddPendingCheck(check.get()); |
| |
| // If the full hash matches one from the artificial list, don't send the |
| // request to the server. |
| if (!check->artificial_full_hash_to_store_and_hash_prefixes.empty()) { |
| std::vector<FullHashInfo> full_hash_infos; |
| for (const auto& entry : |
| check->artificial_full_hash_to_store_and_hash_prefixes) { |
| for (const auto& store_and_prefix : entry.second) { |
| ListIdentifier list_id = store_and_prefix.list_id; |
| base::Time next = |
| base::Time::Now() + base::Minutes(kFullHashExpiryTimeInMinutes); |
| full_hash_infos.emplace_back(entry.first, list_id, next); |
| } |
| } |
| |
| sb_task_runner()->PostTask( |
| FROM_HERE, base::BindOnce(&V4LocalDatabaseManager::OnFullHashResponse, |
| weak_factory_.GetWeakPtr(), std::move(check), |
| full_hash_infos)); |
| } else { |
| // Post on the SB thread to enforce async behavior. |
| sb_task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&V4LocalDatabaseManager::PerformFullHashCheck, |
| weak_factory_.GetWeakPtr(), std::move(check))); |
| } |
| } |
| |
| void V4LocalDatabaseManager::HandleUrl( |
| const GURL& url, |
| const StoresToCheck& stores_to_check, |
| base::OnceCallback<void(bool)> callback) { |
| DCHECK(sb_task_runner()->RunsTasksInCurrentSequence()); |
| |
| std::unique_ptr<PendingCheck> check = std::make_unique<PendingCheck>( |
| nullptr, ClientCallbackType::CHECK_OTHER, stores_to_check, |
| std::vector<GURL>(1, url), |
| MechanismExperimentHashDatabaseCache::kNoExperiment); |
| |
| GetPrefixMatches(check.get(), |
| base::BindOnce(&HandleUrlCallback, std::move(callback))); |
| } |
| |
| void V4LocalDatabaseManager::OnFullHashResponse( |
| std::unique_ptr<PendingCheck> check, |
| const std::vector<FullHashInfo>& full_hash_infos) { |
| DCHECK(sb_task_runner()->RunsTasksInCurrentSequence()); |
| |
| if (!enabled_) { |
| DCHECK(pending_checks_.empty()); |
| return; |
| } |
| |
| const auto it = pending_checks_.find(check.get()); |
| if (it == pending_checks_.end()) { |
| // The check has since been cancelled. |
| return; |
| } |
| |
| // Find out the most severe threat, if any, to report to the client. |
| GetSeverestThreatTypeAndMetadata( |
| full_hash_infos, check->full_hashes, &check->full_hash_threat_types, |
| &check->most_severe_threat_type, &check->url_metadata, |
| &check->matching_full_hash); |
| RemovePendingCheck(it); |
| RespondToClient(std::move(check)); |
| } |
| |
| void V4LocalDatabaseManager::PerformFullHashCheck( |
| std::unique_ptr<PendingCheck> check) { |
| DCHECK(sb_task_runner()->RunsTasksInCurrentSequence()); |
| |
| DCHECK(!check->full_hash_to_store_and_hash_prefixes.empty()); |
| |
| // If we're not enabled, we're in the middle of shutdown, so silently drop the |
| // check. |
| if (enabled_) { |
| FullHashToStoreAndHashPrefixesMap full_hash_to_store_and_hash_prefixes = |
| check->full_hash_to_store_and_hash_prefixes; |
| MechanismExperimentHashDatabaseCache experiment_cache_selection = |
| check->mechanism_experiment_cache_selection; |
| v4_get_hash_protocol_manager_->GetFullHashes( |
| full_hash_to_store_and_hash_prefixes, list_client_states_, |
| base::BindOnce(&V4LocalDatabaseManager::OnFullHashResponse, |
| weak_factory_.GetWeakPtr(), std::move(check)), |
| experiment_cache_selection); |
| } else { |
| DCHECK(pending_checks_.empty()); |
| } |
| } |
| |
| void V4LocalDatabaseManager::ProcessQueuedChecks() { |
| DCHECK(sb_task_runner()->RunsTasksInCurrentSequence()); |
| |
| // Steal the queue to protect against reentrant CancelCheck() calls. |
| QueuedChecks checks; |
| checks.swap(queued_checks_); |
| |
| for (auto& it : checks) { |
| PendingCheck* check_ptr = it.get(); |
| |
| if (GetPrefixMatchesIsAsync()) { |
| AddPendingCheck(check_ptr); |
| } |
| |
| GetPrefixMatches( |
| check_ptr, |
| base::BindOnce(&V4LocalDatabaseManager::ProcessQueuedChecksContinuation, |
| weak_factory_.GetWeakPtr(), std::move(it))); |
| } |
| } |
| |
| void V4LocalDatabaseManager::ProcessQueuedChecksContinuation( |
| std::unique_ptr<PendingCheck> check, |
| FullHashToStoreAndHashPrefixesMap results) { |
| if (GetPrefixMatchesIsAsync()) { |
| if (!enabled_) { |
| DCHECK(pending_checks_.empty()); |
| return; |
| } |
| |
| const auto it = pending_checks_.find(check.get()); |
| if (it == pending_checks_.end()) { |
| // The check has since been cancelled. |
| return; |
| } |
| |
| RemovePendingCheck(it); |
| } |
| |
| if (results.empty()) { |
| RespondToClient(std::move(check)); |
| } else { |
| check->full_hash_to_store_and_hash_prefixes = results; |
| AddPendingCheck(check.get()); |
| PerformFullHashCheck(std::move(check)); |
| } |
| } |
| |
| void V4LocalDatabaseManager::RespondSafeToQueuedAndPendingChecks() { |
| DCHECK(sb_task_runner()->RunsTasksInCurrentSequence()); |
| |
| // Steal the queue to protect against reentrant CancelCheck() calls. |
| QueuedChecks checks; |
| checks.swap(queued_checks_); |
| for (std::unique_ptr<PendingCheck>& it : checks) { |
| RespondToClient(std::move(it)); |
| } |
| |
| // Clear pending_checks_ up front and iterate through a copy to avoid the |
| // possibility of concurrent modifications while iterating. |
| PendingChecks pending_checks = CopyAndRemoveAllPendingChecks(); |
| for (PendingCheck* it : pending_checks) { |
| if (it->client_callback_type == ClientCallbackType::CHECK_OTHER && |
| GetPrefixMatchesIsAsync()) { |
| // In this case there's a callback that will run when weak_factory_ is |
| // invalidated. |
| continue; |
| } |
| // We don't own the unique pointer for the pending check, so we do not |
| // perform cleanup on it while responding to the client. |
| RespondToClientWithoutPendingCheckCleanup(it); |
| } |
| } |
| void V4LocalDatabaseManager::RespondToClient( |
| std::unique_ptr<PendingCheck> check) { |
| RespondToClientWithoutPendingCheckCleanup(check.get()); |
| } |
| void V4LocalDatabaseManager::RespondToClientWithoutPendingCheckCleanup( |
| PendingCheck* check) { |
| DCHECK(check); |
| |
| switch (check->client_callback_type) { |
| case ClientCallbackType::CHECK_BROWSE_URL: |
| case ClientCallbackType::CHECK_URL_FOR_SUBRESOURCE_FILTER: |
| DCHECK_EQ(1u, check->urls.size()); |
| check->client->OnCheckBrowseUrlResult( |
| check->urls[0], check->most_severe_threat_type, check->url_metadata); |
| break; |
| |
| case ClientCallbackType::CHECK_DOWNLOAD_URLS: |
| check->client->OnCheckDownloadUrlResult(check->urls, |
| check->most_severe_threat_type); |
| break; |
| |
| case ClientCallbackType::CHECK_RESOURCE_URL: |
| DCHECK_EQ(1u, check->urls.size()); |
| check->client->OnCheckResourceUrlResult(check->urls[0], |
| check->most_severe_threat_type, |
| check->matching_full_hash); |
| break; |
| |
| case ClientCallbackType::CHECK_CSD_ALLOWLIST: { |
| DCHECK_EQ(1u, check->urls.size()); |
| bool did_match_allowlist = |
| check->most_severe_threat_type == SB_THREAT_TYPE_CSD_ALLOWLIST; |
| DCHECK(did_match_allowlist || |
| check->most_severe_threat_type == SB_THREAT_TYPE_SAFE); |
| check->client->OnCheckAllowlistUrlResult(did_match_allowlist); |
| break; |
| } |
| |
| case ClientCallbackType::CHECK_EXTENSION_IDS: { |
| DCHECK_EQ(check->full_hash_threat_types.size(), |
| check->full_hashes.size()); |
| std::set<FullHashStr> unsafe_extension_ids; |
| for (size_t i = 0; i < check->full_hash_threat_types.size(); i++) { |
| if (check->full_hash_threat_types[i] == SB_THREAT_TYPE_EXTENSION) { |
| unsafe_extension_ids.insert(check->full_hashes[i]); |
| } |
| } |
| check->client->OnCheckExtensionsResult(unsafe_extension_ids); |
| break; |
| } |
| |
| case ClientCallbackType::CHECK_OTHER: |
| NOTREACHED() << "Unexpected client_callback_type encountered"; |
| } |
| } |
| |
| void V4LocalDatabaseManager::SetupDatabase() { |
| DCHECK(!base_path_.empty()); |
| DCHECK(!list_infos_.empty()); |
| DCHECK(sb_task_runner()->RunsTasksInCurrentSequence()); |
| |
| // Do not create the database on the SB thread since this may be an expensive |
| // operation. Instead, do that on the task_runner and when the new database |
| // has been created, swap it out on the SB thread. |
| NewDatabaseReadyCallback db_ready_callback = |
| base::BindOnce(&V4LocalDatabaseManager::DatabaseReadyForChecks, |
| weak_factory_.GetWeakPtr()); |
| V4Database::Create(task_runner_, base_path_, list_infos_, |
| std::move(db_ready_callback)); |
| } |
| |
| void V4LocalDatabaseManager::SetupUpdateProtocolManager( |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory, |
| const V4ProtocolConfig& config) { |
| V4UpdateCallback update_callback = |
| base::BindRepeating(&V4LocalDatabaseManager::UpdateRequestCompleted, |
| weak_factory_.GetWeakPtr()); |
| |
| v4_update_protocol_manager_ = V4UpdateProtocolManager::Create( |
| url_loader_factory, config, update_callback, |
| extended_reporting_level_callback_); |
| } |
| |
| void V4LocalDatabaseManager::UpdateRequestCompleted( |
| std::unique_ptr<ParsedServerResponse> parsed_server_response) { |
| DCHECK(sb_task_runner()->RunsTasksInCurrentSequence()); |
| v4_database_->ApplyUpdate(std::move(parsed_server_response), |
| db_updated_callback_); |
| } |
| |
| bool V4LocalDatabaseManager::AreAllStoresAvailableNow( |
| const StoresToCheck& stores_to_check) const { |
| StoreAvailabilityResult result = StoreAvailabilityResult::AVAILABLE; |
| if (!enabled_) { |
| result = StoreAvailabilityResult::NOT_ENABLED; |
| } else if (!v4_database_) { |
| result = StoreAvailabilityResult::DATABASE_UNAVAILABLE; |
| } else if (!v4_database_->AreAllStoresAvailable(stores_to_check)) { |
| result = StoreAvailabilityResult::STORE_UNAVAILABLE; |
| } |
| return (result == StoreAvailabilityResult::AVAILABLE); |
| } |
| |
| int64_t V4LocalDatabaseManager::GetStoreEntryCount(const ListIdentifier& store, |
| int bytes_per_entry) const { |
| if (!enabled_ || !v4_database_) { |
| return 0; |
| } |
| return v4_database_->GetStoreSizeInBytes(store) / bytes_per_entry; |
| } |
| |
| bool V4LocalDatabaseManager::IsStoreTooSmall(const ListIdentifier& store, |
| int bytes_per_entry, |
| int min_entry_count) const { |
| return GetStoreEntryCount(store, bytes_per_entry) < min_entry_count; |
| } |
| |
| bool V4LocalDatabaseManager::AreAnyStoresAvailableNow( |
| const StoresToCheck& stores_to_check) const { |
| return enabled_ && v4_database_ && |
| v4_database_->AreAnyStoresAvailable(stores_to_check); |
| } |
| |
| void V4LocalDatabaseManager::UpdateListClientStates( |
| const std::unique_ptr<StoreStateMap>& store_state_map) { |
| list_client_states_.clear(); |
| V4ProtocolManagerUtil::GetListClientStatesFromStoreStateMap( |
| store_state_map, &list_client_states_); |
| } |
| |
| void V4LocalDatabaseManager::AddPendingCheck(PendingCheck* check) { |
| check->is_in_pending_checks = true; |
| pending_checks_.insert(check); |
| } |
| |
| void V4LocalDatabaseManager::RemovePendingCheck( |
| PendingChecks::const_iterator it) { |
| (*it)->is_in_pending_checks = false; |
| pending_checks_.erase(it); |
| } |
| |
| V4LocalDatabaseManager::PendingChecks |
| V4LocalDatabaseManager::CopyAndRemoveAllPendingChecks() { |
| PendingChecks pending_checks; |
| pending_checks.swap(pending_checks_); |
| for (PendingCheck* check : pending_checks) { |
| check->is_in_pending_checks = false; |
| } |
| return pending_checks; |
| } |
| |
| void V4LocalDatabaseManager::DeleteUnusedStoreFiles() { |
| for (auto* const store_filename_to_delete : kStoreFileNamesToDelete) { |
| // Is the file marked for deletion also being used for a valid V4Store? |
| auto it = std::find_if(std::begin(list_infos_), std::end(list_infos_), |
| [&store_filename_to_delete](ListInfo const& li) { |
| return li.filename() == store_filename_to_delete; |
| }); |
| if (list_infos_.end() == it) { |
| const base::FilePath store_path = |
| base_path_.AppendASCII(store_filename_to_delete); |
| base::ThreadPool::PostTask(FROM_HERE, {base::MayBlock()}, |
| base::BindOnce(&MaybeDeleteStore, store_path)); |
| } else { |
| NOTREACHED() << "Trying to delete a store file that's in use: " |
| << store_filename_to_delete; |
| } |
| } |
| } |
| |
| } // namespace safe_browsing |