blob: 11cd3dcc2b5b9854ddefc9240749c8302e474a1b [file] [log] [blame]
// Copyright 2014 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/storage/storage_api.h"
#include <stddef.h>
#include <string>
#include <utility>
#include <vector>
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/stringprintf.h"
#include "base/task/sequenced_task_runner.h"
#include "base/trace_event/memory_usage_estimator.h"
#include "base/trace_event/trace_event.h"
#include "base/values.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "extensions/browser/api/storage/session_storage_manager.h"
#include "extensions/browser/api/storage/storage_frontend.h"
#include "extensions/browser/api/storage/storage_utils.h"
#include "extensions/browser/quota_service.h"
#include "extensions/common/api/storage.h"
#include "extensions/common/features/feature.h"
#include "extensions/common/features/feature_channel.h"
#include "extensions/common/mojom/context_type.mojom.h"
using base::trace_event::EstimateMemoryUsage;
using value_store::ValueStore;
namespace extensions {
// Concrete settings functions
namespace {
BASE_FEATURE(kEnforceStorageGetSizeLimit,
"EnforceStorageGetSizeLimit",
base::FEATURE_DISABLED_BY_DEFAULT);
// Returns a vector of any strings within the given list.
std::vector<std::string> GetKeysFromList(const base::Value::List& list) {
std::vector<std::string> keys;
keys.reserve(list.size());
for (const auto& value : list) {
auto* as_string = value.GetIfString();
if (as_string) {
keys.push_back(*as_string);
}
}
return keys;
}
// Returns a vector of keys within the given dict.
std::vector<std::string> GetKeysFromDict(const base::Value::Dict& dict) {
std::vector<std::string> keys;
keys.reserve(dict.size());
for (auto value : dict) {
keys.push_back(value.first);
}
return keys;
}
// Creates quota heuristics for settings modification.
void GetModificationQuotaLimitHeuristics(QuotaLimitHeuristics* heuristics) {
// See storage.json for the current value of these limits.
QuotaLimitHeuristic::Config short_limit_config = {
api::storage::sync::MAX_WRITE_OPERATIONS_PER_MINUTE, base::Minutes(1)};
QuotaLimitHeuristic::Config long_limit_config = {
api::storage::sync::MAX_WRITE_OPERATIONS_PER_HOUR, base::Hours(1)};
heuristics->push_back(std::make_unique<QuotaService::TimedLimit>(
short_limit_config,
std::make_unique<QuotaLimitHeuristic::SingletonBucketMapper>(),
"MAX_WRITE_OPERATIONS_PER_MINUTE"));
heuristics->push_back(std::make_unique<QuotaService::TimedLimit>(
long_limit_config,
std::make_unique<QuotaLimitHeuristic::SingletonBucketMapper>(),
"MAX_WRITE_OPERATIONS_PER_HOUR"));
}
} // namespace
// SettingsFunction
SettingsFunction::SettingsFunction() = default;
SettingsFunction::~SettingsFunction() = default;
bool SettingsFunction::ShouldSkipQuotaLimiting() const {
// Only apply quota if this is for sync storage.
if (args().empty() || !args()[0].is_string()) {
// This should be EXTENSION_FUNCTION_VALIDATE(false) but there is no way
// to signify that from this function. It will be caught in Run().
return false;
}
const std::string& storage_area_string = args()[0].GetString();
return StorageAreaFromString(storage_area_string) !=
StorageAreaNamespace::kSync;
}
bool SettingsFunction::PreRunValidation(std::string* error) {
if (!ExtensionFunction::PreRunValidation(error)) {
return false;
}
EXTENSION_FUNCTION_PRERUN_VALIDATE(args().size() >= 1);
EXTENSION_FUNCTION_PRERUN_VALIDATE(args()[0].is_string());
base::ListValue& mutable_args = GetMutableArgs();
// Not a ref since we remove the underlying value after.
const std::string storage_area_string(std::move(mutable_args[0].GetString()));
mutable_args.erase(mutable_args.begin());
storage_area_ = StorageAreaFromString(storage_area_string);
EXTENSION_FUNCTION_PRERUN_VALIDATE(storage_area_ !=
StorageAreaNamespace::kInvalid);
if (storage_area_ != StorageAreaNamespace::kInvalid) {
if (!IsAccessToStorageAllowed(storage_area_)) {
*error = "Access to storage is not allowed from this context.";
return false;
}
}
// Session is the only storage area that does not use ValueStore, and will
// return synchronously. If access is allowed, validation is complete for it
// here.
if (storage_area_ == StorageAreaNamespace::kSession) {
return true;
}
// All other StorageAreas use ValueStore with settings_namespace, and will
// return asynchronously if successful.
settings_namespace_ = StorageAreaToSettingsNamespace(storage_area_);
EXTENSION_FUNCTION_PRERUN_VALIDATE(settings_namespace_ !=
settings_namespace::INVALID);
if (extension()->is_login_screen_extension() &&
storage_area_ != StorageAreaNamespace::kManaged) {
// Login screen extensions are not allowed to use local/sync storage for
// security reasons (see crbug.com/978443).
*error = base::StringPrintf(
"\"%s\" is not available for login screen extensions",
storage_area_string.c_str());
return false;
}
StorageFrontend* frontend = StorageFrontend::Get(browser_context());
if (!frontend->IsStorageEnabled(settings_namespace_)) {
*error =
base::StringPrintf("\"%s\" is not available in this instance of Chrome",
storage_area_string.c_str());
return false;
}
return true;
}
bool SettingsFunction::IsAccessToStorageAllowed(
StorageAreaNamespace storage_area) {
api::storage::AccessLevel access_level = storage_utils::GetAccessLevelForArea(
extension()->id(), *browser_context(), storage_area);
if (access_level == api::storage::AccessLevel::kTrustedContexts) {
// Only a privileged extension context is considered trusted.
return source_context_type() == mojom::ContextType::kPrivilegedExtension;
}
// All contexts are allowed.
DCHECK_EQ(api::storage::AccessLevel::kTrustedAndUntrustedContexts,
access_level);
return true;
}
void SettingsFunction::OnWriteOperationFinished(
StorageFrontend::ResultStatus status) {
// Since the storage access happens asynchronously, the browser context can
// be torn down in the interim. If this happens, early-out.
if (!browser_context()) {
return;
}
if (!status.success) {
CHECK(status.error.has_value());
Respond(Error(*status.error));
return;
}
Respond(NoArguments());
}
ExtensionFunction::ResponseAction StorageStorageAreaGetFunction::Run() {
if (args().empty()) {
return RespondNow(BadMessage());
}
base::ListValue& mutable_args = GetMutableArgs();
base::Value input = std::move(mutable_args[0]);
mutable_args.erase(args().begin());
std::optional<std::vector<std::string>> keys;
std::optional<base::Value::Dict> defaults;
switch (input.type()) {
case base::Value::Type::NONE:
keys = std::nullopt;
break;
case base::Value::Type::STRING:
keys = std::optional(std::vector<std::string>(1, input.GetString()));
break;
case base::Value::Type::LIST:
keys = std::optional(GetKeysFromList(input.GetList()));
break;
case base::Value::Type::DICT: {
keys = std::optional(GetKeysFromDict(input.GetDict()));
// When the input holds a dictionary, the values are default values for
// any keys not present in storage. This is only the case for this
// parameter type.
defaults = std::move(input).TakeDict();
break;
}
default:
return RespondNow(BadMessage());
}
StorageFrontend* frontend = StorageFrontend::Get(browser_context());
frontend->GetValues(
extension(), storage_area(), std::move(keys),
base::BindOnce(&StorageStorageAreaGetFunction::OnGetOperationFinished,
this, std::move(defaults)));
return RespondLater();
}
// Setting a reasonable size limit for a single 'get' operation (e.g., 512 MB)
// to prevent OOM crash which occurs around 2GB.
constexpr size_t kMaxSingleGetSizeBytes = 512 * 1024 * 1024;
void StorageStorageAreaGetFunction::OnGetOperationFinished(
std::optional<base::Value::Dict> defaults,
StorageFrontend::GetResult result) {
// Since the storage access happens asynchronously, the browser context can
// be torn down in the interim. If this happens, early-out.
if (!browser_context()) {
return;
}
StorageFrontend::ResultStatus status = result.status;
if (!status.success) {
CHECK(status.error.has_value());
Respond(Error(*status.error));
return;
}
CHECK(result.data.has_value());
// Estimate the size of the result data before attempting to send it over IPC.
size_t data_size = EstimateMemoryUsage(*result.data);
// Log the size of the data to understand the distribution of `get` operation
// sizes and assess the impact of enforcing a size limit.
// See crbug.com/427600178 for more details.
UMA_HISTOGRAM_MEMORY_LARGE_MB(
"Extensions.Storage.GetOperation.AllocationSize",
data_size / (1024 * 1024));
if (base::FeatureList::IsEnabled(kEnforceStorageGetSizeLimit) &&
data_size > kMaxSingleGetSizeBytes) {
Respond(Error(base::StringPrintf(
"The total data size of %zu bytes exceeds the maximum limit of %zu "
"bytes for a single get() operation. Please use getKeys() and "
"retrieve items in smaller batches.",
data_size, kMaxSingleGetSizeBytes)));
return;
}
base::Value::Dict values =
defaults ? std::move(*defaults) : base::Value::Dict();
// It's important that we merge the values into the defaults, and not the
// other way around, to avoid the defaults overwriting any existing values.
values.Merge(std::move(*result.data));
Respond(WithArguments(std::move(values)));
}
ExtensionFunction::ResponseAction StorageStorageAreaGetKeysFunction::Run() {
StorageFrontend* frontend = StorageFrontend::Get(browser_context());
frontend->GetKeys(
extension(), storage_area(),
base::BindOnce(
&StorageStorageAreaGetKeysFunction::OnGetKeysOperationFinished,
this));
return RespondLater();
}
void StorageStorageAreaGetKeysFunction::OnGetKeysOperationFinished(
StorageFrontend::GetKeysResult result) {
// Since the storage access happens asynchronously, the browser context can
// be torn down in the interim. If this happens, early-out.
if (!browser_context()) {
return;
}
StorageFrontend::ResultStatus status = result.status;
if (!status.success) {
CHECK(status.error.has_value());
Respond(Error(*status.error));
return;
}
CHECK(result.data.has_value());
Respond(WithArguments(std::move(*result.data)));
}
ExtensionFunction::ResponseAction
StorageStorageAreaGetBytesInUseFunction::Run() {
if (args().empty()) {
return RespondNow(BadMessage());
}
const base::Value& input = args()[0];
std::optional<std::vector<std::string>> keys;
switch (input.type()) {
case base::Value::Type::NONE:
keys = std::nullopt;
break;
case base::Value::Type::STRING:
keys = std::optional(std::vector<std::string>(1, input.GetString()));
break;
case base::Value::Type::LIST:
keys = std::optional(GetKeysFromList(input.GetList()));
break;
default:
return RespondNow(BadMessage());
}
StorageFrontend* frontend = StorageFrontend::Get(browser_context());
frontend->GetBytesInUse(
extension(), storage_area(), keys,
base::BindOnce(&StorageStorageAreaGetBytesInUseFunction::
OnGetBytesInUseOperationFinished,
this));
return RespondLater();
}
void StorageStorageAreaGetBytesInUseFunction::OnGetBytesInUseOperationFinished(
size_t bytes_in_use) {
// Since the storage access happens asynchronously, the browser context can
// be torn down in the interim. If this happens, early-out.
if (!browser_context()) {
return;
}
// Checked cast should not overflow since a double can represent up to 2*53
// bytes before a loss of precision.
Respond(WithArguments(base::checked_cast<double>(bytes_in_use)));
}
ExtensionFunction::ResponseAction StorageStorageAreaSetFunction::Run() {
if (args().empty() || !args()[0].is_dict()) {
return RespondNow(BadMessage());
}
base::ListValue& mutable_args = GetMutableArgs();
// Retrieve and delete input from `args_` since they will be moved to storage.
base::Value input = std::move(mutable_args[0]);
mutable_args.erase(args().begin());
StorageFrontend* frontend = StorageFrontend::Get(browser_context());
frontend->Set(
extension(), storage_area(), std::move(input).TakeDict(),
base::BindOnce(&StorageStorageAreaSetFunction::OnWriteOperationFinished,
this));
return RespondLater();
}
void StorageStorageAreaSetFunction::GetQuotaLimitHeuristics(
QuotaLimitHeuristics* heuristics) const {
GetModificationQuotaLimitHeuristics(heuristics);
}
ExtensionFunction::ResponseAction StorageStorageAreaRemoveFunction::Run() {
if (args().empty()) {
return RespondNow(BadMessage());
}
const base::Value& input = args()[0];
std::vector<std::string> keys;
switch (input.type()) {
case base::Value::Type::STRING:
keys = std::vector<std::string>(1, input.GetString());
break;
case base::Value::Type::LIST:
keys = GetKeysFromList(input.GetList());
break;
default:
return RespondNow(BadMessage());
}
StorageFrontend* frontend = StorageFrontend::Get(browser_context());
frontend->Remove(
extension(), storage_area(), keys,
base::BindOnce(
&StorageStorageAreaRemoveFunction::OnWriteOperationFinished, this));
return RespondLater();
}
void StorageStorageAreaRemoveFunction::GetQuotaLimitHeuristics(
QuotaLimitHeuristics* heuristics) const {
GetModificationQuotaLimitHeuristics(heuristics);
}
ExtensionFunction::ResponseAction StorageStorageAreaClearFunction::Run() {
StorageFrontend* frontend = StorageFrontend::Get(browser_context());
frontend->Clear(
extension(), storage_area(),
base::BindOnce(&StorageStorageAreaClearFunction::OnWriteOperationFinished,
this));
return RespondLater();
}
void StorageStorageAreaClearFunction::GetQuotaLimitHeuristics(
QuotaLimitHeuristics* heuristics) const {
GetModificationQuotaLimitHeuristics(heuristics);
}
ExtensionFunction::ResponseAction
StorageStorageAreaSetAccessLevelFunction::Run() {
if (storage_area() == StorageAreaNamespace::kInvalid) {
return RespondNow(
Error("This StorageArea is not available for setting access level"));
}
if (source_context_type() != mojom::ContextType::kPrivilegedExtension) {
return RespondNow(Error("Context cannot set the storage access level"));
}
std::optional<api::storage::StorageArea::SetAccessLevel::Params> params =
api::storage::StorageArea::SetAccessLevel::Params::Create(args());
if (!params) {
return RespondNow(BadMessage());
}
// The parsing code ensures `access_level` is sane.
DCHECK(params->access_options.access_level ==
api::storage::AccessLevel::kTrustedContexts ||
params->access_options.access_level ==
api::storage::AccessLevel::kTrustedAndUntrustedContexts);
storage_utils::SetAccessLevelForArea(extension_id(), *browser_context(),
storage_area(),
params->access_options.access_level);
return RespondNow(NoArguments());
}
} // namespace extensions