| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "extensions/browser/api/declarative_net_request/file_sequence_helper.h" |
| |
| #include <algorithm> |
| #include <cstdint> |
| #include <set> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/barrier_closure.h" |
| #include "base/check_op.h" |
| #include "base/containers/contains.h" |
| #include "base/files/file_util.h" |
| #include "base/files/important_file_writer.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/notreached.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "extensions/browser/api/declarative_net_request/constants.h" |
| #include "extensions/browser/api/declarative_net_request/parse_info.h" |
| #include "extensions/browser/api/declarative_net_request/rule_counts.h" |
| #include "extensions/browser/api/declarative_net_request/utils.h" |
| #include "extensions/browser/extension_file_task_runner.h" |
| #include "extensions/common/api/declarative_net_request.h" |
| #include "extensions/common/error_utils.h" |
| #include "extensions/common/extension_features.h" |
| #include "services/data_decoder/public/cpp/data_decoder.h" |
| |
| namespace extensions::declarative_net_request { |
| |
| namespace { |
| |
| namespace dnr_api = extensions::api::declarative_net_request; |
| |
| // A class to help in indexing multiple rulesets. |
| // TODO(crbug.com/40794487): Look into unifying this with the InstallIndexHelper |
| // class, moving any differing logic to the clients. |
| class IndexHelper : public base::RefCountedThreadSafe<IndexHelper> { |
| public: |
| using IndexCallback = base::OnceCallback<void(LoadRequestData)>; |
| IndexHelper(LoadRequestData data, IndexCallback callback) |
| : data_(std::move(data)), callback_(std::move(callback)) {} |
| |
| IndexHelper(const IndexHelper&) = delete; |
| IndexHelper& operator=(const IndexHelper&) = delete; |
| |
| // Starts indexing rulesets. Must be called on the extension file task runner. |
| // TODO(crbug.com/380434972): Kick off content verification job to guard |
| // against the possibility that the extension's ruleset JSON files were |
| // corrupted. |
| void Start(uint8_t parse_flags) { |
| DCHECK(GetExtensionFileTaskRunner()->RunsTasksInCurrentSequence()); |
| |
| std::vector<RulesetInfo*> rulesets_to_index; |
| for (auto& ruleset : data_.rulesets) { |
| if (ruleset.did_load_successfully()) { |
| continue; |
| } |
| |
| rulesets_to_index.push_back(&ruleset); |
| } |
| |
| // `done_closure` will be invoked once `barrier_closure` is run |
| // `rulesets_to_index.size()` times. |
| base::OnceClosure done_closure = |
| base::BindOnce(&IndexHelper::OnAllRulesetsIndexed, this); |
| base::RepeatingClosure barrier_closure = |
| base::BarrierClosure(rulesets_to_index.size(), std::move(done_closure)); |
| |
| // Post tasks to index individual rulesets. |
| for (RulesetInfo* ruleset : rulesets_to_index) { |
| auto callback = base::BindOnce(&IndexHelper::OnIndexCompleted, this, |
| ruleset, barrier_closure); |
| ruleset->source().IndexAndPersistJSONRuleset(&decoder_, parse_flags, |
| std::move(callback)); |
| } |
| } |
| |
| private: |
| friend class base::RefCountedThreadSafe<IndexHelper>; |
| ~IndexHelper() = default; |
| |
| // Callback invoked when indexing of all rulesets is completed. |
| void OnAllRulesetsIndexed() { |
| DCHECK(GetExtensionFileTaskRunner()->RunsTasksInCurrentSequence()); |
| |
| // Our job is done. |
| std::move(callback_).Run(std::move(data_)); |
| } |
| |
| // Callback invoked when a single ruleset is indexed. |
| void OnIndexCompleted(RulesetInfo* ruleset, |
| base::OnceClosure done_closure, |
| IndexAndPersistJSONRulesetResult result) { |
| using IndexStatus = IndexAndPersistJSONRulesetResult::Status; |
| DCHECK(ruleset); |
| |
| bool indexing_success = result.status == IndexStatus::kSuccess; |
| bool is_reindexing = ruleset->expected_checksum().has_value(); |
| if (indexing_success) { |
| // Update the checksum if either: |
| // - this is the first time that the ruleset is being indexed and there's |
| // no expected checksum. |
| // - there is a checksum mismatch between indexing and what's in prefs. |
| // Use the checksum that was just derived from reindexing. |
| // - the ruleset's version has updated, so the old checksum is invalid |
| bool update_checksum = !is_reindexing || |
| ruleset->load_ruleset_result() == |
| LoadRulesetResult::kErrorChecksumMismatch || |
| ruleset->load_ruleset_result() == |
| LoadRulesetResult::kErrorVersionMismatch; |
| if (update_checksum) { |
| ruleset->set_new_checksum(result.ruleset_checksum); |
| |
| // Also change the `expected_checksum` so that any subsequent load |
| // succeeds. |
| ruleset->set_expected_checksum(result.ruleset_checksum); |
| } else { |
| // Otherwise, the checksum of the re-indexed ruleset should match the |
| // expected checksum. If this is not the case, then there is some other |
| // issue (like the JSON rules file has been modified from the one used |
| // during installation or preferences are corrupted). But taking care of |
| // these is beyond our scope here, so simply signal a failure. |
| indexing_success = |
| ruleset->expected_checksum() == result.ruleset_checksum; |
| } |
| } |
| |
| ruleset->set_indexing_successful(indexing_success); |
| |
| if (is_reindexing) { |
| UMA_HISTOGRAM_BOOLEAN( |
| "Extensions.DeclarativeNetRequest.RulesetReindexSuccessful", |
| indexing_success); |
| } |
| |
| std::move(done_closure).Run(); |
| } |
| |
| LoadRequestData data_; |
| IndexCallback callback_; |
| |
| // We use a single shared Data Decoder service instance to process all of the |
| // rulesets for this IndexHelper. |
| data_decoder::DataDecoder decoder_; |
| }; |
| |
| UpdateDynamicRulesStatus GetUpdateDynamicRuleStatus(LoadRulesetResult result) { |
| switch (result) { |
| case LoadRulesetResult::kSuccess: |
| break; |
| case LoadRulesetResult::kErrorInvalidPath: |
| return UpdateDynamicRulesStatus::kErrorCreateMatcher_InvalidPath; |
| case LoadRulesetResult::kErrorCannotReadFile: |
| return UpdateDynamicRulesStatus::kErrorCreateMatcher_FileReadError; |
| case LoadRulesetResult::kErrorChecksumMismatch: |
| return UpdateDynamicRulesStatus::kErrorCreateMatcher_ChecksumMismatch; |
| case LoadRulesetResult::kErrorVersionMismatch: |
| return UpdateDynamicRulesStatus::kErrorCreateMatcher_VersionMismatch; |
| case LoadRulesetResult::kErrorChecksumNotFound: |
| // Updating dynamic rules shouldn't require looking up checksum from |
| // prefs. |
| break; |
| } |
| |
| NOTREACHED(); |
| } |
| |
| // Helper to create the new list of dynamic rules. Returns false on failure and |
| // populates |error| and |status|. |
| bool GetNewDynamicRules(const FileBackedRulesetSource& source, |
| std::vector<int> rule_ids_to_remove, |
| std::vector<dnr_api::Rule> rules_to_add, |
| const RuleCounts& rule_limit, |
| std::vector<dnr_api::Rule>* new_rules, |
| std::string* error, |
| UpdateDynamicRulesStatus* status) { |
| DCHECK(new_rules); |
| DCHECK(error); |
| DCHECK(status); |
| |
| // Read the current set of rules. Note: this is trusted JSON and hence it is |
| // ok to parse in the browser itself. |
| ReadJSONRulesResult result = source.ReadJSONRulesUnsafe(); |
| LogReadDynamicRulesStatus(result.status); |
| DCHECK(result.status == ReadJSONRulesResult::Status::kSuccess || |
| result.rules.empty()); |
| |
| // Possible cases: |
| // - kSuccess |
| // - kFileDoesNotExist: This can happen when persisting dynamic rules for the |
| // first time. |
| // - kFileReadError: Throw an internal error. |
| // - kJSONParseError, kJSONIsNotList: These denote JSON ruleset corruption. |
| // Assume the current set of rules is empty. |
| if (result.status == ReadJSONRulesResult::Status::kFileReadError) { |
| *status = UpdateDynamicRulesStatus::kErrorReadJSONRules; |
| *error = kInternalErrorUpdatingDynamicRules; |
| return false; |
| } |
| |
| *new_rules = std::move(result.rules); |
| |
| // Remove old rules |
| std::set<int> ids_to_remove(rule_ids_to_remove.begin(), rule_ids_to_remove.end()); |
| std::erase_if(*new_rules, [&ids_to_remove](const dnr_api::Rule& rule) { |
| return base::Contains(ids_to_remove, rule.id); |
| }); |
| |
| // Add new rules |
| new_rules->insert(new_rules->end(), |
| std::make_move_iterator(rules_to_add.begin()), |
| std::make_move_iterator(rules_to_add.end())); |
| |
| if (new_rules->size() > rule_limit.rule_count) { |
| *status = UpdateDynamicRulesStatus::kErrorRuleCountExceeded; |
| *error = kDynamicRuleCountExceeded; |
| return false; |
| } |
| |
| if (base::FeatureList::IsEnabled( |
| extensions_features::kDeclarativeNetRequestSafeRuleLimits)) { |
| size_t unsafe_rule_count = std::ranges::count_if( |
| *new_rules, |
| [](const dnr_api::Rule& rule) { return !IsRuleSafe(rule); }); |
| if (unsafe_rule_count > rule_limit.unsafe_rule_count) { |
| *status = UpdateDynamicRulesStatus::kErrorUnsafeRuleCountExceeded; |
| *error = kDynamicUnsafeRuleCountExceeded; |
| return false; |
| } |
| } |
| |
| size_t regex_rule_count = std::ranges::count_if( |
| *new_rules, |
| [](const dnr_api::Rule& rule) { return !!rule.condition.regex_filter; }); |
| if (regex_rule_count > rule_limit.regex_rule_count) { |
| *status = UpdateDynamicRulesStatus::kErrorRegexRuleCountExceeded; |
| *error = kDynamicRegexRuleCountExceeded; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| // Returns true on success and populates |ruleset_checksum|. Returns false on |
| // failure and populates |error| and |status|. |
| bool UpdateAndIndexDynamicRules(const FileBackedRulesetSource& source, |
| std::vector<int> rule_ids_to_remove, |
| std::vector<dnr_api::Rule> rules_to_add, |
| const RuleCounts& rule_limit, |
| int* ruleset_checksum, |
| std::string* error, |
| UpdateDynamicRulesStatus* status) { |
| DCHECK(ruleset_checksum); |
| DCHECK(error); |
| DCHECK(status); |
| |
| // Dynamic JSON and indexed rulesets for an extension are stored in the same |
| // directory. |
| DCHECK_EQ(source.indexed_path().DirName(), source.json_path().DirName()); |
| |
| std::set<int> rule_ids_to_add; |
| for (const dnr_api::Rule& rule : rules_to_add) { |
| rule_ids_to_add.insert(rule.id); |
| } |
| |
| std::vector<dnr_api::Rule> new_rules; |
| if (!GetNewDynamicRules(source, std::move(rule_ids_to_remove), |
| std::move(rules_to_add), rule_limit, &new_rules, |
| error, status)) { |
| return false; // |error| and |status| already populated. |
| } |
| |
| // Serialize rules to JSON. |
| std::string json; |
| if (!source.SerializeRulesToJSON(new_rules, &json)) { |
| *error = kInternalErrorUpdatingDynamicRules; |
| *status = UpdateDynamicRulesStatus::kErrorSerializeToJson; |
| return false; |
| } |
| |
| // Index rules. |
| auto parse_flags = RulesetSource::kRaiseErrorOnInvalidRules | |
| RulesetSource::kRaiseWarningOnLargeRegexRules; |
| ParseInfo info = source.IndexRules(std::move(new_rules), parse_flags); |
| |
| if (info.has_error()) { |
| *error = info.error(); |
| *status = UpdateDynamicRulesStatus::kErrorInvalidRules; |
| return false; |
| } |
| |
| // Treat rules which exceed the regex memory limit as errors if these are new |
| // rules. Just surface an error for the first such rule. |
| for (const auto& warning : info.rule_ignored_warnings()) { |
| if (!base::Contains(rule_ids_to_add, warning.rule_id)) { |
| // Any rule added earlier which is ignored now (say due to exceeding the |
| // regex memory limit), will be silently ignored. |
| // TODO(crbug.com/40118204): Notify the extension about the same. |
| continue; |
| } |
| |
| *error = warning.message; |
| *status = UpdateDynamicRulesStatus::kErrorRegexTooLarge; |
| return false; |
| } |
| |
| // Ensure that the destination directory exists. |
| if (!base::CreateDirectory(source.indexed_path().DirName())) { |
| *error = kInternalErrorUpdatingDynamicRules; |
| *status = UpdateDynamicRulesStatus::kErrorCreateDynamicRulesDirectory; |
| return false; |
| } |
| |
| // Persist indexed ruleset. Use `ImportantFileWriter` to make this atomic and |
| // decrease the likelihood of file corruption. |
| if (!base::ImportantFileWriter::WriteFileAtomically( |
| source.indexed_path(), GetIndexedRulesetData(info.GetBuffer()), |
| "DNRDynamicRulesFlatbuffer")) { |
| // If this fails, we might have corrupted the existing indexed ruleset file. |
| // However the JSON source of truth hasn't been modified. The next time the |
| // extension is loaded, the indexed ruleset will fail checksum verification |
| // leading to reindexing of the JSON ruleset. |
| *error = kInternalErrorUpdatingDynamicRules; |
| *status = UpdateDynamicRulesStatus::kErrorWriteFlatbuffer; |
| return false; |
| } |
| |
| // Persist JSON. Since the JSON ruleset is the source of truth, use |
| // `ImportantFileWriter` to make this atomic and decrease the likelihood of |
| // file corruption. |
| if (!base::ImportantFileWriter::WriteFileAtomically( |
| source.json_path(), json, "DNRDynamicRulesetJson")) { |
| // We have entered into an inconsistent state where the indexed ruleset was |
| // updated but not the JSON ruleset. This should be extremely rare. However |
| // if we get here, the next time the extension is loaded, we'll identify |
| // that the indexed ruleset checksum is inconsistent and re-index the JSON |
| // ruleset. |
| // If the JSON ruleset is corrupted here though, loading the dynamic ruleset |
| // subsequently will fail. A call by extension to `updateDynamicRules` |
| // should help it start from a clean slate in this case (See |
| // `GetNewDynamicRules` above). |
| *error = kInternalErrorUpdatingDynamicRules; |
| *status = UpdateDynamicRulesStatus::kErrorWriteJson; |
| return false; |
| } |
| |
| *ruleset_checksum = info.ruleset_checksum(); |
| return true; |
| } |
| |
| } // namespace |
| |
| RulesetInfo::RulesetInfo(FileBackedRulesetSource source) |
| : source_(std::move(source)) {} |
| RulesetInfo::~RulesetInfo() = default; |
| RulesetInfo::RulesetInfo(RulesetInfo&&) = default; |
| RulesetInfo& RulesetInfo::operator=(RulesetInfo&&) = default; |
| |
| std::unique_ptr<RulesetMatcher> RulesetInfo::TakeMatcher() { |
| DCHECK(did_load_successfully()); |
| return std::move(matcher_); |
| } |
| |
| const std::optional<LoadRulesetResult>& RulesetInfo::load_ruleset_result() |
| const { |
| // |matcher_| is valid only on success. |
| DCHECK_EQ(load_ruleset_result_ == LoadRulesetResult::kSuccess, !!matcher_); |
| return load_ruleset_result_; |
| } |
| |
| void RulesetInfo::CreateVerifiedMatcher() { |
| DCHECK(expected_checksum_); |
| DCHECK(GetExtensionFileTaskRunner()->RunsTasksInCurrentSequence()); |
| |
| // Ensure we aren't calling this redundantly. If did_load_successfully() |
| // returns true, we should already have a valid RulesetMatcher. |
| DCHECK(!did_load_successfully()); |
| |
| load_ruleset_result_ = |
| source_.CreateVerifiedMatcher(*expected_checksum_, &matcher_); |
| } |
| |
| LoadRequestData::LoadRequestData(ExtensionId extension_id, |
| base::Version extension_version, |
| LoadRulesetRequestSource request_source) |
| : extension_id(std::move(extension_id)), |
| extension_version(std::move(extension_version)), |
| request_source(request_source), |
| load_request_id(base::Token::CreateRandom()) {} |
| LoadRequestData::~LoadRequestData() = default; |
| LoadRequestData::LoadRequestData(LoadRequestData&&) = default; |
| LoadRequestData& LoadRequestData::operator=(LoadRequestData&&) = default; |
| |
| FileSequenceHelper::FileSequenceHelper() = default; |
| |
| FileSequenceHelper::~FileSequenceHelper() { |
| DCHECK(GetExtensionFileTaskRunner()->RunsTasksInCurrentSequence()); |
| } |
| |
| void FileSequenceHelper::LoadRulesets( |
| LoadRequestData load_data, |
| LoadRulesetsUICallback ui_callback) const { |
| DCHECK(GetExtensionFileTaskRunner()->RunsTasksInCurrentSequence()); |
| |
| bool success = true; |
| for (auto& ruleset : load_data.rulesets) { |
| if (!ruleset.expected_checksum()) { |
| // This ruleset hasn't been indexed yet. |
| success = false; |
| continue; |
| } |
| |
| ruleset.CreateVerifiedMatcher(); |
| success &= ruleset.did_load_successfully(); |
| } |
| |
| if (success) { |
| // Set priority explicitly to avoid unwanted task priority inheritance. |
| content::GetUIThreadTaskRunner({base::TaskPriority::USER_BLOCKING}) |
| ->PostTask(FROM_HERE, base::BindOnce(std::move(ui_callback), |
| std::move(load_data))); |
| return; |
| } |
| |
| // Not all rulesets were loaded. This can be because some rulesets haven't |
| // been indexed previously or because indexing failed for a ruleset. Try |
| // indexing these rulesets now. |
| |
| // Ignore invalid static rules during deferred indexing or while re-indexing. |
| auto parse_flags = RulesetSource::kNone; |
| |
| // Using a WeakPtr is safe since `index_callback` will be called on this |
| // sequence itself. |
| auto index_callback = |
| base::BindOnce(&FileSequenceHelper::OnRulesetsIndexed, |
| weak_factory_.GetWeakPtr(), std::move(ui_callback)); |
| |
| auto index_helper = base::MakeRefCounted<IndexHelper>( |
| std::move(load_data), std::move(index_callback)); |
| index_helper->Start(parse_flags); |
| } |
| |
| void FileSequenceHelper::UpdateDynamicRules( |
| LoadRequestData load_data, |
| std::vector<int> rule_ids_to_remove, |
| std::vector<api::declarative_net_request::Rule> rules_to_add, |
| const RuleCounts& rule_limit, |
| UpdateDynamicRulesUICallback ui_callback) const { |
| DCHECK(GetExtensionFileTaskRunner()->RunsTasksInCurrentSequence()); |
| DCHECK_EQ(1u, load_data.rulesets.size()); |
| |
| RulesetInfo& dynamic_ruleset = load_data.rulesets[0]; |
| DCHECK(!dynamic_ruleset.expected_checksum()); |
| |
| auto log_status_and_dispatch_callback = [&ui_callback, &load_data]( |
| std::optional<std::string> error, |
| UpdateDynamicRulesStatus status) { |
| base::UmaHistogramEnumeration(kUpdateDynamicRulesStatusHistogram, status); |
| |
| // Set priority explicitly to avoid unwanted task priority inheritance. |
| content::GetUIThreadTaskRunner({base::TaskPriority::USER_BLOCKING}) |
| ->PostTask(FROM_HERE, |
| base::BindOnce(std::move(ui_callback), std::move(load_data), |
| std::move(error))); |
| }; |
| |
| int new_ruleset_checksum = -1; |
| std::string error; |
| UpdateDynamicRulesStatus status = UpdateDynamicRulesStatus::kSuccess; |
| if (!UpdateAndIndexDynamicRules(dynamic_ruleset.source(), |
| std::move(rule_ids_to_remove), |
| std::move(rules_to_add), rule_limit, |
| &new_ruleset_checksum, &error, &status)) { |
| DCHECK(!error.empty()); |
| log_status_and_dispatch_callback(std::move(error), status); |
| return; |
| } |
| |
| DCHECK_EQ(UpdateDynamicRulesStatus::kSuccess, status); |
| dynamic_ruleset.set_expected_checksum(new_ruleset_checksum); |
| dynamic_ruleset.set_new_checksum(new_ruleset_checksum); |
| dynamic_ruleset.CreateVerifiedMatcher(); |
| DCHECK(dynamic_ruleset.load_ruleset_result()); |
| |
| if (!dynamic_ruleset.did_load_successfully()) { |
| status = GetUpdateDynamicRuleStatus(*dynamic_ruleset.load_ruleset_result()); |
| log_status_and_dispatch_callback(kInternalErrorUpdatingDynamicRules, |
| status); |
| return; |
| } |
| |
| // Success. |
| log_status_and_dispatch_callback(std::nullopt, status); |
| } |
| |
| void FileSequenceHelper::OnRulesetsIndexed(LoadRulesetsUICallback ui_callback, |
| LoadRequestData load_data) const { |
| DCHECK(GetExtensionFileTaskRunner()->RunsTasksInCurrentSequence()); |
| |
| // Load rulesets for which indexing succeeded. |
| for (auto& ruleset : load_data.rulesets) { |
| if (ruleset.indexing_successful().value_or(false)) { |
| // Only rulesets which weren't indexed previously or for which loading |
| // failed are being indexed. |
| DCHECK(!ruleset.did_load_successfully()); |
| ruleset.CreateVerifiedMatcher(); |
| } |
| } |
| |
| // The UI thread will handle success or failure. |
| content::GetUIThreadTaskRunner({base::TaskPriority::USER_BLOCKING}) |
| ->PostTask(FROM_HERE, |
| base::BindOnce(std::move(ui_callback), std::move(load_data))); |
| } |
| |
| } // namespace extensions::declarative_net_request |