blob: 9297559de030f5fbaa6facf6dd81b62477fca191 [file] [log] [blame]
// 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