blob: 073471ccae6ef5e31bc5589f0313c8607bf18f1d [file] [log] [blame]
// 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/private_aggregation/private_aggregation_budgeter.h"
#include <stdint.h>
#include <algorithm>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/check_op.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/numerics/checked_math.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/updateable_sequenced_task_runner.h"
#include "base/time/time.h"
#include "content/browser/private_aggregation/private_aggregation_budget_key.h"
#include "content/browser/private_aggregation/private_aggregation_budget_storage.h"
#include "content/browser/private_aggregation/proto/private_aggregation_budgets.pb.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "third_party/protobuf/src/google/protobuf/repeated_field.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace content {
namespace {
using ValidityStatus = PrivateAggregationBudgeter::BudgetValidityStatus;
int64_t SerializeTimeForStorage(base::Time time) {
return time.ToDeltaSinceWindowsEpoch().InMicroseconds();
}
void RecordBudgetValidity(ValidityStatus status) {
static_assert(
ValidityStatus::kContainsNonPositiveValue == ValidityStatus::kMaxValue,
"Bump version of "
"PrivacySandbox.PrivateAggregation.Budgeter."
"BudgetValidityStatus histogram.");
base::UmaHistogramEnumeration(
"PrivacySandbox.PrivateAggregation.Budgeter.BudgetValidityStatus",
status);
}
void ComputeAndRecordBudgetValidity(
google::protobuf::RepeatedPtrField<proto::PrivateAggregationBudgetPerHour>*
hourly_budgets,
int64_t earliest_window_in_scope_start,
int64_t current_window_start) {
if (hourly_budgets->empty()) {
RecordBudgetValidity(ValidityStatus::kValidAndEmpty);
return;
}
constexpr int64_t kWindowDuration =
PrivateAggregationBudgetKey::TimeWindow::kDuration.InMicroseconds();
ValidityStatus status = ValidityStatus::kValid;
for (proto::PrivateAggregationBudgetPerHour& elem : *hourly_budgets) {
int64_t hour_start = elem.hour_start_timestamp();
int budget = elem.budget_used();
if (budget <= 0) {
RecordBudgetValidity(ValidityStatus::kContainsNonPositiveValue);
return;
} else if (hour_start % kWindowDuration != 0) {
RecordBudgetValidity(ValidityStatus::kContainsTimestampNotRoundedToHour);
return;
} else if (budget >
blink::features::kPrivateAggregationApiMaxBudgetPerScope.Get()) {
RecordBudgetValidity(ValidityStatus::kContainsValueExceedingLimit);
return;
} else if (hour_start >= current_window_start + kWindowDuration) {
RecordBudgetValidity(ValidityStatus::kContainsTimestampInFuture);
return;
} else if (hour_start < earliest_window_in_scope_start) {
status = ValidityStatus::kValidButContainsStaleWindow;
}
}
constexpr int64_t kMaximumWindowStartDifference =
PrivateAggregationBudgeter::kBudgetScopeDuration.InMicroseconds() -
kWindowDuration;
const auto minmax = base::ranges::minmax(
*hourly_budgets, /*comp=*/{},
&proto::PrivateAggregationBudgetPerHour::hour_start_timestamp);
DCHECK_EQ(kMaximumWindowStartDifference,
current_window_start - earliest_window_in_scope_start);
if (minmax.second.hour_start_timestamp() -
minmax.first.hour_start_timestamp() >
kMaximumWindowStartDifference) {
RecordBudgetValidity(ValidityStatus::kSpansMoreThanADay);
return;
}
RecordBudgetValidity(status);
}
google::protobuf::RepeatedPtrField<proto::PrivateAggregationBudgetPerHour>*
GetHourlyBudgets(PrivateAggregationBudgetKey::Api api,
proto::PrivateAggregationBudgets& budgets) {
switch (api) {
case PrivateAggregationBudgetKey::Api::kFledge:
return budgets.mutable_fledge_budgets();
case PrivateAggregationBudgetKey::Api::kSharedStorage:
return budgets.mutable_shared_storage_budgets();
}
}
} // namespace
PrivateAggregationBudgeter::PrivateAggregationBudgeter(
scoped_refptr<base::UpdateableSequencedTaskRunner> db_task_runner,
bool exclusively_run_in_memory,
const base::FilePath& path_to_db_dir)
: db_task_runner_(std::move(db_task_runner)) {
DCHECK(db_task_runner_);
shutdown_initializing_storage_ = PrivateAggregationBudgetStorage::CreateAsync(
db_task_runner_, exclusively_run_in_memory, path_to_db_dir,
/*on_done_initializing=*/
base::BindOnce(&PrivateAggregationBudgeter::OnStorageDoneInitializing,
weak_factory_.GetWeakPtr()));
}
PrivateAggregationBudgeter::PrivateAggregationBudgeter() = default;
PrivateAggregationBudgeter::~PrivateAggregationBudgeter() {
if (shutdown_initializing_storage_) {
// As the budget storage's lifetime is extended until initialization is
// complete, its destructor could run after browser shutdown has begun (when
// tasks can no longer be posted). We post the database deletion task now
// instead.
std::move(shutdown_initializing_storage_).Run();
}
}
void PrivateAggregationBudgeter::ConsumeBudget(
int budget,
const PrivateAggregationBudgetKey& budget_key,
base::OnceCallback<void(RequestResult)> on_done) {
if (storage_status_ == StorageStatus::kInitializing) {
if (pending_calls_.size() >= kMaxPendingCalls) {
std::move(on_done).Run(RequestResult::kTooManyPendingCalls);
return;
}
// `base::Unretained` is safe as `pending_calls_` is owned by `this`.
pending_calls_.push_back(base::BindOnce(
&PrivateAggregationBudgeter::ConsumeBudgetImpl, base::Unretained(this),
budget, budget_key, std::move(on_done)));
} else {
ConsumeBudgetImpl(budget, budget_key, std::move(on_done));
}
}
void PrivateAggregationBudgeter::ClearData(
base::Time delete_begin,
base::Time delete_end,
StoragePartition::StorageKeyMatcherFunction filter,
base::OnceClosure done) {
// When a clear data task is queued or running, we use a higher priority. We
// do this even if the storage hasn't finished initializing.
++num_pending_clear_data_tasks_;
db_task_runner_->UpdatePriority(base::TaskPriority::USER_VISIBLE);
done = base::BindOnce(&PrivateAggregationBudgeter::OnClearDataComplete,
weak_factory_.GetWeakPtr())
.Then(std::move(done));
if (storage_status_ == StorageStatus::kInitializing) {
// To ensure that data deletion always succeeds, we don't check
// `pending_calls.size()` here.
// `base::Unretained` is safe as `pending_calls_` is owned by `this`.
pending_calls_.push_back(base::BindOnce(
&PrivateAggregationBudgeter::ClearDataImpl, base::Unretained(this),
delete_begin, delete_end, std::move(filter), std::move(done)));
} else {
ClearDataImpl(delete_begin, delete_end, std::move(filter), std::move(done));
}
}
void PrivateAggregationBudgeter::OnClearDataComplete() {
DCHECK_GT(num_pending_clear_data_tasks_, 0);
--num_pending_clear_data_tasks_;
// No more clear data tasks, so we can reset the priority.
if (num_pending_clear_data_tasks_ == 0)
db_task_runner_->UpdatePriority(base::TaskPriority::BEST_EFFORT);
}
void PrivateAggregationBudgeter::OnStorageDoneInitializing(
std::unique_ptr<PrivateAggregationBudgetStorage> storage) {
DCHECK(shutdown_initializing_storage_);
DCHECK(!storage_);
DCHECK_EQ(storage_status_, StorageStatus::kInitializing);
if (storage) {
storage_status_ = StorageStatus::kOpen;
storage_ = std::move(storage);
} else {
storage_status_ = StorageStatus::kInitializationFailed;
}
shutdown_initializing_storage_.Reset();
ProcessAllPendingCalls();
}
void PrivateAggregationBudgeter::ProcessAllPendingCalls() {
for (base::OnceClosure& cb : pending_calls_) {
std::move(cb).Run();
}
pending_calls_.clear();
}
// TODO(crbug.com/1336733): Consider enumerating different error cases and log
// metrics and/or expose to callers.
void PrivateAggregationBudgeter::ConsumeBudgetImpl(
int additional_budget,
const PrivateAggregationBudgetKey& budget_key,
base::OnceCallback<void(RequestResult)> on_done) {
const int kMaxBudgetPerScope =
blink::features::kPrivateAggregationApiMaxBudgetPerScope.Get();
switch (storage_status_) {
case StorageStatus::kInitializing:
NOTREACHED();
break;
case StorageStatus::kInitializationFailed:
std::move(on_done).Run(RequestResult::kStorageInitializationFailed);
return;
case StorageStatus::kOpen:
break;
}
if (additional_budget <= 0) {
std::move(on_done).Run(RequestResult::kInvalidRequest);
return;
}
if (additional_budget > kMaxBudgetPerScope) {
std::move(on_done).Run(RequestResult::kRequestedMoreThanTotalBudget);
return;
}
std::string origin_key = budget_key.origin().Serialize();
// If there is no budget proto stored for this origin already, we use the
// default initialization of `budgets` (untouched by `TryGetData()`).
proto::PrivateAggregationBudgets budgets;
storage_->budgets_data()->TryGetData(origin_key, &budgets);
google::protobuf::RepeatedPtrField<proto::PrivateAggregationBudgetPerHour>*
hourly_budgets = GetHourlyBudgets(budget_key.api(), budgets);
DCHECK(hourly_budgets);
const int64_t current_window_start =
SerializeTimeForStorage(budget_key.time_window().start_time());
DCHECK_EQ(current_window_start % base::Time::kMicrosecondsPerHour, 0);
// Budget windows must start on or after this timestamp to be counted in the
// current day.
const int64_t earliest_window_in_scope_start =
current_window_start +
PrivateAggregationBudgetKey::TimeWindow::kDuration.InMicroseconds() -
kBudgetScopeDuration.InMicroseconds();
ComputeAndRecordBudgetValidity(hourly_budgets, earliest_window_in_scope_start,
current_window_start);
proto::PrivateAggregationBudgetPerHour* window_for_key = nullptr;
base::CheckedNumeric<int> total_budget_used = 0;
bool should_clean_up_stale_budgets = false;
for (proto::PrivateAggregationBudgetPerHour& elem : *hourly_budgets) {
if (elem.hour_start_timestamp() < earliest_window_in_scope_start) {
should_clean_up_stale_budgets = true;
continue;
}
if (elem.hour_start_timestamp() == current_window_start) {
window_for_key = &elem;
}
// Protect against bad values on disk
if (elem.budget_used() <= 0) {
std::move(on_done).Run(RequestResult::kBadValuesOnDisk);
return;
}
total_budget_used += elem.budget_used();
}
total_budget_used += additional_budget;
RequestResult budget_increase_request_result;
if (!total_budget_used.IsValid()) {
budget_increase_request_result = RequestResult::kBadValuesOnDisk;
} else if (total_budget_used.ValueOrDie() > kMaxBudgetPerScope) {
budget_increase_request_result = RequestResult::kInsufficientBudget;
} else {
budget_increase_request_result = RequestResult::kApproved;
}
if (budget_increase_request_result == RequestResult::kApproved) {
if (!window_for_key) {
window_for_key = hourly_budgets->Add();
window_for_key->set_hour_start_timestamp(current_window_start);
window_for_key->set_budget_used(0);
}
int budget_used_for_key = window_for_key->budget_used() + additional_budget;
DCHECK_GT(budget_used_for_key, 0);
DCHECK_LE(budget_used_for_key, kMaxBudgetPerScope);
window_for_key->set_budget_used(budget_used_for_key);
}
if (should_clean_up_stale_budgets) {
auto new_end = std::remove_if(
hourly_budgets->begin(), hourly_budgets->end(),
[&earliest_window_in_scope_start](
const proto::PrivateAggregationBudgetPerHour& elem) {
return elem.hour_start_timestamp() < earliest_window_in_scope_start;
});
hourly_budgets->erase(new_end, hourly_budgets->end());
}
if (budget_increase_request_result == RequestResult::kApproved ||
should_clean_up_stale_budgets) {
storage_->budgets_data()->UpdateData(origin_key, budgets);
}
std::move(on_done).Run(budget_increase_request_result);
}
void PrivateAggregationBudgeter::ClearDataImpl(
base::Time delete_begin,
base::Time delete_end,
StoragePartition::StorageKeyMatcherFunction filter,
base::OnceClosure done) {
switch (storage_status_) {
case StorageStatus::kInitializing:
NOTREACHED();
break;
case StorageStatus::kInitializationFailed:
std::move(done).Run();
return;
case StorageStatus::kOpen:
break;
}
// Treat null times as unbounded lower or upper range. This is used by
// browsing data remover.
if (delete_begin.is_null())
delete_begin = base::Time::Min();
if (delete_end.is_null())
delete_end = base::Time::Max();
bool is_all_time_covered = delete_begin.is_min() && delete_end.is_max();
if (is_all_time_covered && filter.is_null()) {
storage_->budgets_data()->DeleteAllData();
// Runs `done` once flushing is complete.
storage_->budgets_data()->FlushDataToDisk(std::move(done));
return;
}
std::vector<std::string> origins_to_delete;
for (const auto& [origin_key, budgets] :
storage_->budgets_data()->GetAllCached()) {
if (filter.is_null() ||
filter.Run(blink::StorageKey::CreateFromStringForTesting(origin_key))) {
origins_to_delete.push_back(origin_key);
}
}
if (is_all_time_covered) {
storage_->budgets_data()->DeleteData(origins_to_delete);
// Runs `done` once flushing is complete.
storage_->budgets_data()->FlushDataToDisk(std::move(done));
return;
}
// Ensure we round down to capture any time windows that partially overlap.
int64_t serialized_delete_begin = SerializeTimeForStorage(
PrivateAggregationBudgetKey::TimeWindow(delete_begin).start_time());
// No need to round up as we compare against the time window's start time.
int64_t serialized_delete_end = SerializeTimeForStorage(delete_end);
for (const std::string& origin_key : origins_to_delete) {
proto::PrivateAggregationBudgets budgets;
storage_->budgets_data()->TryGetData(origin_key, &budgets);
static constexpr PrivateAggregationBudgetKey::Api kAllApis[] = {
PrivateAggregationBudgetKey::Api::kFledge,
PrivateAggregationBudgetKey::Api::kSharedStorage};
for (PrivateAggregationBudgetKey::Api api : kAllApis) {
google::protobuf::RepeatedPtrField<
proto::PrivateAggregationBudgetPerHour>* hourly_budgets =
GetHourlyBudgets(api, budgets);
DCHECK(hourly_budgets);
auto new_end = std::remove_if(
hourly_budgets->begin(), hourly_budgets->end(),
[=](const proto::PrivateAggregationBudgetPerHour& elem) {
return elem.hour_start_timestamp() >= serialized_delete_begin &&
elem.hour_start_timestamp() <= serialized_delete_end;
});
hourly_budgets->erase(new_end, hourly_budgets->end());
}
storage_->budgets_data()->UpdateData(origin_key, budgets);
}
// Force the database to be flushed immediately instead of waiting up to
// `PrivateAggregationBudgetStorage::kFlushDelay`. Runs the `done` callback
// once flushing is complete.
storage_->budgets_data()->FlushDataToDisk(std::move(done));
}
} // namespace content