0

[shared storage] Implement sharedStorage.batchUpdate() for PA worklet

Add sharedStorage.batchUpdate() function. Parse arguments into
the 'methods' sequence and a 'with_lock' optional flag, and
propagate the result to the browser process to invoke the
`SharedStorageLockManager::SharedStorageBatchUpdate()` API.

This allows developers to perform multiple Shared Storage operations atomically within a single lock, as part of the Web
Lock integration proposal:
- https://github.com/WICG/shared-storage/pull/199
- https://github.com/WICG/shared-storage/pull/205

Fuchsia-Binary-Size: Size increase is unavoidable.
Bug: 373899210
Change-Id: Ic6e9f794d78523ec9f6b87f37fb5e91f17635c58
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6072850
Commit-Queue: Yao Xiao <yaoxia@chromium.org>
Reviewed-by: Maks Orlovich <morlovich@chromium.org>
Reviewed-by: Cammie Smith Barnes <cammie@chromium.org>
Reviewed-by: Giovanni Ortuno Urquidi <ortuno@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1401673}
This commit is contained in:
Yao Xiao
2025-01-02 18:31:45 -08:00
committed by Chromium LUCI CQ
parent 671c5ce475
commit f0cab8e234
9 changed files with 634 additions and 93 deletions

@ -80,4 +80,32 @@ void AuctionSharedStorageHost::SharedStorageUpdate(
ToWebFeature(source_auction_worklet_function));
}
void AuctionSharedStorageHost::SharedStorageBatchUpdate(
std::vector<network::mojom::SharedStorageModifierMethodWithOptionsPtr>
methods_with_options,
const std::optional<std::string>& with_lock,
auction_worklet::mojom::AuctionWorkletFunction
source_auction_worklet_function) {
if (with_lock && with_lock->starts_with('-')) {
receiver_set_.ReportBadMessage("Reserved lock name");
return;
}
FrameTreeNodeId main_frame_id =
receiver_set_.current_context()
.auction_runner_rfh->GetOutermostMainFrame()
->GetFrameTreeNodeId();
storage_partition_->GetSharedStorageRuntimeManager()
->lock_manager()
.SharedStorageBatchUpdate(std::move(methods_with_options), with_lock,
receiver_set_.current_context().worklet_origin,
AccessScope::kProtectedAudienceWorklet,
main_frame_id, base::DoNothing());
GetContentClient()->browser()->LogWebFeatureForCurrentPage(
receiver_set_.current_context().auction_runner_rfh,
ToWebFeature(source_auction_worklet_function));
}
} // namespace content

@ -43,6 +43,12 @@ class CONTENT_EXPORT AuctionSharedStorageHost
method_with_options,
auction_worklet::mojom::AuctionWorkletFunction
source_auction_worklet_function) override;
void SharedStorageBatchUpdate(
std::vector<network::mojom::SharedStorageModifierMethodWithOptionsPtr>
methods_with_options,
const std::optional<std::string>& with_lock,
auction_worklet::mojom::AuctionWorkletFunction
source_auction_worklet_function) override;
private:
struct ReceiverContext;

@ -11184,14 +11184,7 @@ TEST_F(BidderWorkletSharedStorageAPIEnabledTest, ModifierMethodTypeHierarchy) {
/*selected_buyer_and_seller_reporting_id=*/std::nullopt,
/*ad_component_descriptors=*/std::nullopt,
/*modeling_signals=*/std::nullopt,
/*aggregate_win_signals=*/std::nullopt, base::TimeDelta()),
/*expected_data_version=*/std::nullopt,
/*expected_errors=*/{},
/*expected_debug_loss_report_url=*/std::nullopt,
/*expected_debug_win_report_url=*/std::nullopt,
/*expected_set_priority=*/std::nullopt,
/*expected_update_priority_signals_overrides=*/{},
/*expected_pa_requests=*/{});
/*aggregate_win_signals=*/std::nullopt, base::TimeDelta()));
v8_helpers_[0]->v8_runner()->PostTask(
FROM_HERE, base::BindOnce(
@ -11232,12 +11225,7 @@ TEST_F(BidderWorkletSharedStorageAPIEnabledTest,
/*expected_bids=*/nullptr,
/*expected_data_version=*/std::nullopt,
/*expected_errors=*/
{method_and_error.second},
/*expected_debug_loss_report_url=*/std::nullopt,
/*expected_debug_win_report_url=*/std::nullopt,
/*expected_set_priority=*/std::nullopt,
/*expected_update_priority_signals_overrides=*/{},
/*expected_pa_requests=*/{});
{method_and_error.second});
}
}
@ -11264,12 +11252,7 @@ TEST_F(BidderWorkletSharedStorageAPIEnabledTest,
/*expected_data_version=*/std::nullopt,
/*expected_errors=*/
{"https://url.test/:5 Uncaught TypeError: The \"shared-storage\" "
"Permissions Policy denied the method on sharedStorage."},
/*expected_debug_loss_report_url=*/std::nullopt,
/*expected_debug_win_report_url=*/std::nullopt,
/*expected_set_priority=*/std::nullopt,
/*expected_update_priority_signals_overrides=*/{},
/*expected_pa_requests=*/{});
"Permissions Policy denied the method on sharedStorage."});
}
}
@ -11305,12 +11288,7 @@ TEST_F(
/*expected_bids=*/nullptr,
/*expected_data_version=*/std::nullopt,
/*expected_errors=*/
{method_and_error.second},
/*expected_debug_loss_report_url=*/std::nullopt,
/*expected_debug_win_report_url=*/std::nullopt,
/*expected_set_priority=*/std::nullopt,
/*expected_update_priority_signals_overrides=*/{},
/*expected_pa_requests=*/{});
{method_and_error.second});
}
}
@ -11334,12 +11312,7 @@ TEST_F(BidderWorkletSharedStorageAPIEnabledTest,
/*expected_data_version=*/std::nullopt,
/*expected_errors=*/
{"https://url.test/:5 Uncaught TypeError: The shared storage method "
"object constructor cannot be called as a function."},
/*expected_debug_loss_report_url=*/std::nullopt,
/*expected_debug_win_report_url=*/std::nullopt,
/*expected_set_priority=*/std::nullopt,
/*expected_update_priority_signals_overrides=*/{},
/*expected_pa_requests=*/{});
"object constructor cannot be called as a function."});
}
}
@ -11369,14 +11342,7 @@ TEST_F(BidderWorkletSharedStorageAPIEnabledTest,
/*selected_buyer_and_seller_reporting_id=*/std::nullopt,
/*ad_component_descriptors=*/std::nullopt,
/*modeling_signals=*/std::nullopt,
/*aggregate_win_signals=*/std::nullopt, base::TimeDelta()),
/*expected_data_version=*/std::nullopt,
/*expected_errors=*/{},
/*expected_debug_loss_report_url=*/std::nullopt,
/*expected_debug_win_report_url=*/std::nullopt,
/*expected_set_priority=*/std::nullopt,
/*expected_update_priority_signals_overrides=*/{},
/*expected_pa_requests=*/{});
/*aggregate_win_signals=*/std::nullopt, base::TimeDelta()));
}
v8_helpers_[0]->v8_runner()->PostTask(
@ -11411,12 +11377,252 @@ TEST_F(BidderWorkletSharedStorageAPIEnabledTest,
/*expected_bids=*/nullptr,
/*expected_data_version=*/std::nullopt,
/*expected_errors=*/
{"https://url.test/:6 Uncaught Error 123."},
/*expected_debug_loss_report_url=*/std::nullopt,
/*expected_debug_win_report_url=*/std::nullopt,
/*expected_set_priority=*/std::nullopt,
/*expected_update_priority_signals_overrides=*/{},
/*expected_pa_requests=*/{});
{"https://url.test/:6 Uncaught Error 123."});
}
TEST_F(BidderWorkletSharedStorageAPIEnabledTest,
SharedStorageBatchUpdate_NoArguments) {
auction_worklet::TestAuctionSharedStorageHost test_shared_storage_host;
mojo::Receiver<auction_worklet::mojom::AuctionSharedStorageHost> receiver(
&test_shared_storage_host);
shared_storage_hosts_[0] = receiver.BindNewPipeAndPassRemote();
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: "ad", bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
sharedStorage.batchUpdate();
)"),
/*expected_bids=*/nullptr,
/*expected_data_version=*/std::nullopt,
/*expected_errors=*/
{"https://url.test/:6 Uncaught TypeError: sharedStorage.batchUpdate(): "
"at least 1 argument(s) are required."});
}
TEST_F(BidderWorkletSharedStorageAPIEnabledTest,
SharedStorageBatchUpdate_MethodsNotAnObject) {
auction_worklet::TestAuctionSharedStorageHost test_shared_storage_host;
mojo::Receiver<auction_worklet::mojom::AuctionSharedStorageHost> receiver(
&test_shared_storage_host);
shared_storage_hosts_[0] = receiver.BindNewPipeAndPassRemote();
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: "ad", bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
sharedStorage.batchUpdate(123);
)"),
/*expected_bids=*/nullptr,
/*expected_data_version=*/std::nullopt,
/*expected_errors=*/
{"https://url.test/:6 Uncaught TypeError: sharedStorage.batchUpdate(): "
"Trouble converting argument 'methods' to a Sequence."});
}
TEST_F(BidderWorkletSharedStorageAPIEnabledTest,
SharedStorageBatchUpdate_MethodsNotASequence) {
auction_worklet::TestAuctionSharedStorageHost test_shared_storage_host;
mojo::Receiver<auction_worklet::mojom::AuctionSharedStorageHost> receiver(
&test_shared_storage_host);
shared_storage_hosts_[0] = receiver.BindNewPipeAndPassRemote();
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: "ad", bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
sharedStorage.batchUpdate({});
)"),
/*expected_bids=*/nullptr,
/*expected_data_version=*/std::nullopt,
/*expected_errors=*/
{"https://url.test/:6 Uncaught TypeError: sharedStorage.batchUpdate(): "
"Trouble converting argument 'methods' to a Sequence."});
}
TEST_F(BidderWorkletSharedStorageAPIEnabledTest,
SharedStorageBatchUpdate_ErrorIteratingOverMethods) {
auction_worklet::TestAuctionSharedStorageHost test_shared_storage_host;
mojo::Receiver<auction_worklet::mojom::AuctionSharedStorageHost> receiver(
&test_shared_storage_host);
shared_storage_hosts_[0] = receiver.BindNewPipeAndPassRemote();
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: "ad", bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
let o = {};
o[Symbol.iterator] = {};
sharedStorage.batchUpdate(o);
)"),
/*expected_bids=*/nullptr,
/*expected_data_version=*/std::nullopt,
/*expected_errors=*/
{"https://url.test/:9 Uncaught TypeError: sharedStorage.batchUpdate(): "
"Trouble iterating over argument 'methods'."});
}
TEST_F(BidderWorkletSharedStorageAPIEnabledTest,
SharedStorageBatchUpdate_MethodsSequenceElementInvalidType) {
auction_worklet::TestAuctionSharedStorageHost test_shared_storage_host;
mojo::Receiver<auction_worklet::mojom::AuctionSharedStorageHost> receiver(
&test_shared_storage_host);
shared_storage_hosts_[0] = receiver.BindNewPipeAndPassRemote();
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: "ad", bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
sharedStorage.batchUpdate([123]);
)"),
/*expected_bids=*/nullptr,
/*expected_data_version=*/std::nullopt,
/*expected_errors=*/
{"https://url.test/:6 Uncaught TypeError: Failed to convert value to "
"'SharedStorageModifierMethod'."});
}
TEST_F(BidderWorkletSharedStorageAPIEnabledTest,
SharedStorageBatchUpdate_MethodsSequenceElementUserDefinedType) {
auction_worklet::TestAuctionSharedStorageHost test_shared_storage_host;
mojo::Receiver<auction_worklet::mojom::AuctionSharedStorageHost> receiver(
&test_shared_storage_host);
shared_storage_hosts_[0] = receiver.BindNewPipeAndPassRemote();
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: "ad", bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
class SharedStorageClearMethod {
constructor() {}
}
sharedStorage.batchUpdate([new SharedStorageClearMethod()]);
)"),
/*expected_bids=*/nullptr,
/*expected_data_version=*/std::nullopt,
/*expected_errors=*/
{"https://url.test/:11 Uncaught TypeError: Failed to convert value to "
"'SharedStorageModifierMethod'."});
}
TEST_F(BidderWorkletSharedStorageAPIEnabledTest,
SharedStorageBatchUpdate_ReservedLockName) {
auction_worklet::TestAuctionSharedStorageHost test_shared_storage_host;
mojo::Receiver<auction_worklet::mojom::AuctionSharedStorageHost> receiver(
&test_shared_storage_host);
shared_storage_hosts_[0] = receiver.BindNewPipeAndPassRemote();
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: "ad", bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
sharedStorage.batchUpdate([], {withLock: '-abc'});
)"),
/*expected_bids=*/nullptr,
/*expected_data_version=*/std::nullopt,
/*expected_errors=*/
{"https://url.test/:6 Uncaught TypeError: sharedStorage.batchUpdate(): "
"Lock name cannot start with '-'."});
}
TEST_F(BidderWorkletSharedStorageAPIEnabledTest,
SharedStorageBatchUpdate_PermissionsPolicyError) {
permissions_policy_state_ = mojom::AuctionWorkletPermissionsPolicyState::New(
/*private_aggregation_allowed=*/true,
/*shared_storage_allowed=*/false);
// Skip setting up `shared_storage_hosts_`, to be consistent with the
// permissions policy's enabled status. This matches production behavior.
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: "ad", bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
sharedStorage.batchUpdate([]);
)"),
/*expected_bids=*/nullptr,
/*expected_data_version=*/std::nullopt,
/*expected_errors=*/
{"https://url.test/:6 Uncaught TypeError: The \"shared-storage\" "
"Permissions Policy denied the method on sharedStorage."});
}
TEST_F(BidderWorkletSharedStorageAPIEnabledTest,
SharedStorageBatchUpdate_Success) {
auction_worklet::TestAuctionSharedStorageHost test_shared_storage_host;
mojo::Receiver<auction_worklet::mojom::AuctionSharedStorageHost> receiver(
&test_shared_storage_host);
shared_storage_hosts_[0] = receiver.BindNewPipeAndPassRemote();
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: "ad", bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
sharedStorage.batchUpdate([]);
sharedStorage.batchUpdate([], {withLock: 'lock1'});
sharedStorage.batchUpdate([
new SharedStorageSetMethod('a', 'b'),
new SharedStorageAppendMethod('c', 'd'),
new SharedStorageDeleteMethod('e'),
new SharedStorageClearMethod({withLock: 'lock2'})
], {withLock: 'lock3'});
)"),
/*expected_bids=*/
mojom::BidderWorkletBid::New(
auction_worklet::mojom::BidRole::kUnenforcedKAnon, "\"ad\"", 1,
/*bid_currency=*/std::nullopt,
/*ad_cost=*/std::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*selected_buyer_and_seller_reporting_id=*/std::nullopt,
/*ad_component_descriptors=*/std::nullopt,
/*modeling_signals=*/std::nullopt,
/*aggregate_win_signals=*/std::nullopt, base::TimeDelta()));
// Make sure the shared storage mojom methods are invoked as they use a
// dedicated pipe.
task_environment_.RunUntilIdle();
using BatchRequest =
auction_worklet::TestAuctionSharedStorageHost::BatchRequest;
std::vector<content::MethodWithOptionsPtr> batch_methods1;
std::vector<content::MethodWithOptionsPtr> batch_methods2;
std::vector<content::MethodWithOptionsPtr> batch_methods3;
batch_methods3.push_back(MojomSetMethod(/*key=*/u"a",
/*value=*/u"b",
/*ignore_if_present=*/false));
batch_methods3.push_back(MojomAppendMethod(/*key=*/u"c",
/*value=*/u"d"));
batch_methods3.push_back(MojomDeleteMethod(/*key=*/u"e"));
batch_methods3.push_back(MojomClearMethod(/*with_lock=*/"lock2"));
EXPECT_THAT(
test_shared_storage_host.observed_batch_requests(),
testing::ElementsAre(
BatchRequest(std::move(batch_methods1),
/*with_lock=*/std::nullopt,
mojom::AuctionWorkletFunction::kBidderGenerateBid),
BatchRequest(std::move(batch_methods2),
/*with_lock=*/"lock1",
mojom::AuctionWorkletFunction::kBidderGenerateBid),
BatchRequest(std::move(batch_methods3),
/*with_lock=*/"lock3",
mojom::AuctionWorkletFunction::kBidderGenerateBid)));
v8_helpers_[0]->v8_runner()->PostTask(
FROM_HERE, base::BindOnce(
[](scoped_refptr<AuctionV8Helper> v8_helper) {
v8_helper->isolate()->RequestGarbageCollectionForTesting(
v8::Isolate::kFullGarbageCollection);
},
v8_helpers_[0]));
task_environment_.RunUntilIdle();
}
TEST_F(BidderWorkletSharedStorageAPIEnabledTest,
@ -11448,14 +11654,7 @@ TEST_F(BidderWorkletSharedStorageAPIEnabledTest,
/*selected_buyer_and_seller_reporting_id=*/std::nullopt,
/*ad_component_descriptors=*/std::nullopt,
/*modeling_signals=*/std::nullopt,
/*aggregate_win_signals=*/std::nullopt, base::TimeDelta()),
/*expected_data_version=*/std::nullopt,
/*expected_errors=*/{},
/*expected_debug_loss_report_url=*/std::nullopt,
/*expected_debug_win_report_url=*/std::nullopt,
/*expected_set_priority=*/std::nullopt,
/*expected_update_priority_signals_overrides=*/{},
/*expected_pa_requests=*/{});
/*aggregate_win_signals=*/std::nullopt, base::TimeDelta()));
// Make sure the shared storage mojom methods are invoked as they use a
// dedicated pipe.
@ -11503,12 +11702,7 @@ TEST_F(BidderWorkletSharedStorageAPIEnabledTest,
/*expected_data_version=*/std::nullopt,
/*expected_errors=*/
{"https://url.test/:6 Uncaught TypeError: The \"shared-storage\" "
"Permissions Policy denied the method on sharedStorage."},
/*expected_debug_loss_report_url=*/std::nullopt,
/*expected_debug_win_report_url=*/std::nullopt,
/*expected_set_priority=*/std::nullopt,
/*expected_update_priority_signals_overrides=*/{},
/*expected_pa_requests=*/{});
"Permissions Policy denied the method on sharedStorage."});
permissions_policy_state_ =
mojom::AuctionWorkletPermissionsPolicyState::New(

@ -22,4 +22,13 @@ interface AuctionSharedStorageHost {
SharedStorageUpdate(
network.mojom.SharedStorageModifierMethodWithOptions method_with_options,
AuctionWorkletFunction source_auction_worklet_function);
// Handle each modifier method within `methods_with_options`. If `with_lock`
// is provided, the methods within the batch will be executed with a lock
// acquired on the resource with name `with_lock`. `with_lock` shouldn't start
// with '-'.
SharedStorageBatchUpdate(
array<network.mojom.SharedStorageModifierMethodWithOptions> methods_with_options,
string? with_lock,
AuctionWorkletFunction source_auction_worklet_function);
};

@ -13,6 +13,10 @@
#include "content/services/auction_worklet/public/mojom/auction_shared_storage_host.mojom.h"
#include "content/services/auction_worklet/webidl_compat.h"
#include "gin/converter.h"
#include "gin/handle.h"
#include "gin/public/gin_embedders.h"
#include "gin/public/wrapper_info.h"
#include "gin/wrappable.h"
#include "services/network/public/cpp/shared_storage_utils.h"
#include "services/network/public/mojom/shared_storage.mojom.h"
#include "third_party/blink/public/common/features.h"
@ -253,49 +257,35 @@ CreateMojomClearMethodFromParameters(
std::move(method), std::move(with_lock));
}
// SharedStorageMethod represents a method for modifying shared storage. It
// manages its own lifecycle through weak reference handling to support
// automatic garbage collection.
class SharedStorageMethod {
// SharedStorageMethod represents a method for modifying shared storage. This
// class inherits from gin::Wrappable to leverage gin's JavaScript object
// lifetime management capabilities. When the JavaScript object is garbage
// collected, the corresponding C++ object will be properly cleaned up.
class SharedStorageMethod : public gin::Wrappable<SharedStorageMethod> {
public:
// Constructs a SharedStorageMethod with a Mojom method and sets up
// weak reference management.
//
// Responsibilities:
// - Create a V8 External wrapping the C++ object
// - Establish a persistent, weak reference to the External
// - Set the External as an internal field of the JavaScript object
// - Ensure proper cleanup when the JavaScript object is garbage collected
static gin::WrapperInfo kWrapperInfo;
SharedStorageMethod(
v8::Isolate* isolate,
v8::Local<v8::Object> obj,
network::mojom::SharedStorageModifierMethodWithOptionsPtr mojom_method)
: mojom_method_(std::move(mojom_method)) {
v8::Local<v8::External> external = v8::External::New(isolate, this);
persistent_external_.Reset(isolate, external);
obj->SetInternalField(0, external);
persistent_external_.SetWeak(this, WeakCallback,
v8::WeakCallbackType::kParameter);
gin::Handle<SharedStorageMethod> handler = gin::CreateHandle(isolate, this);
// Use an index that won't interfere with gin's reserved indexes.
obj->SetInternalField(gin::kNumberOfInternalFields, handler.ToV8());
}
// Weak callback invoked by V8's garbage collector when the associated
// JavaScript object becomes unreachable.
//
// Responsibilities:
// - Clear the persistent external reference
// - Delete the SharedStorageMethod instance
static void WeakCallback(
const v8::WeakCallbackInfo<SharedStorageMethod>& data) {
SharedStorageMethod* method = data.GetParameter();
method->persistent_external_.Reset();
delete method;
const network::mojom::SharedStorageModifierMethodWithOptionsPtr&
mojom_method() const {
return mojom_method_;
}
private:
network::mojom::SharedStorageModifierMethodWithOptionsPtr mojom_method_;
v8::Persistent<v8::External> persistent_external_;
};
gin::WrapperInfo SharedStorageMethod::kWrapperInfo = {gin::kEmbedderNativeGin};
} // namespace
SharedStorageBindings::SharedStorageBindings(
@ -351,6 +341,17 @@ void SharedStorageBindings::AttachToContext(v8::Local<v8::Context> context) {
clear_method_function)
.Check();
// batchUpdate() is part of the Web Locks integration launch.
if (base::FeatureList::IsEnabled(blink::features::kSharedStorageWebLocks)) {
v8::Local<v8::Function> batch_update_function =
v8::Function::New(context, &SharedStorageBindings::BatchUpdate, v8_this)
.ToLocalChecked();
shared_storage
->Set(context, v8_helper_->CreateStringFromLiteral("batchUpdate"),
batch_update_function)
.Check();
}
context->Global()
->Set(context, v8_helper_->CreateStringFromLiteral("sharedStorage"),
shared_storage)
@ -367,7 +368,8 @@ void SharedStorageBindings::AttachToContext(v8::Local<v8::Context> context) {
v8::FunctionTemplate::New(v8_helper_->isolate(),
&SharedStorageBindings::SetMethodConstructor,
v8_this);
set_method_ctor_template->InstanceTemplate()->SetInternalFieldCount(1);
set_method_ctor_template->InstanceTemplate()->SetInternalFieldCount(
gin::kNumberOfInternalFields + 1);
set_method_ctor_template->Inherit(base_modifier_method_template);
set_method_ctor_template->SetClassName(
v8_helper_->CreateStringFromLiteral(kSharedStorageSetMethodName));
@ -383,7 +385,8 @@ void SharedStorageBindings::AttachToContext(v8::Local<v8::Context> context) {
v8::FunctionTemplate::New(
v8_helper_->isolate(),
&SharedStorageBindings::AppendMethodConstructor, v8_this);
append_method_ctor_template->InstanceTemplate()->SetInternalFieldCount(1);
append_method_ctor_template->InstanceTemplate()->SetInternalFieldCount(
gin::kNumberOfInternalFields + 1);
append_method_ctor_template->Inherit(base_modifier_method_template);
append_method_ctor_template->SetClassName(
v8_helper_->CreateStringFromLiteral(kSharedStorageAppendMethodName));
@ -400,7 +403,8 @@ void SharedStorageBindings::AttachToContext(v8::Local<v8::Context> context) {
v8::FunctionTemplate::New(
v8_helper_->isolate(),
&SharedStorageBindings::DeleteMethodConstructor, v8_this);
delete_method_ctor_template->InstanceTemplate()->SetInternalFieldCount(1);
delete_method_ctor_template->InstanceTemplate()->SetInternalFieldCount(
gin::kNumberOfInternalFields + 1);
delete_method_ctor_template->Inherit(base_modifier_method_template);
delete_method_ctor_template->SetClassName(
v8_helper_->CreateStringFromLiteral(kSharedStorageDeleteMethodName));
@ -417,7 +421,8 @@ void SharedStorageBindings::AttachToContext(v8::Local<v8::Context> context) {
v8::FunctionTemplate::New(
v8_helper_->isolate(),
&SharedStorageBindings::ClearMethodConstructor, v8_this);
clear_method_ctor_template->InstanceTemplate()->SetInternalFieldCount(1);
clear_method_ctor_template->InstanceTemplate()->SetInternalFieldCount(
gin::kNumberOfInternalFields + 1);
clear_method_ctor_template->Inherit(base_modifier_method_template);
clear_method_ctor_template->SetClassName(
v8_helper_->CreateStringFromLiteral(kSharedStorageClearMethodName));
@ -510,6 +515,131 @@ void SharedStorageBindings::Clear(
std::move(mojom_method), bindings->source_auction_worklet_function_);
}
// static
void SharedStorageBindings::BatchUpdate(
const v8::FunctionCallbackInfo<v8::Value>& args) {
SharedStorageBindings* bindings = static_cast<SharedStorageBindings*>(
v8::External::Cast(*args.Data())->Value());
AuctionV8Helper* v8_helper = bindings->v8_helper_;
v8::Isolate* isolate = v8_helper->isolate();
AuctionV8Helper::TimeLimitScope time_limit_scope(v8_helper->GetTimeLimit());
std::vector<network::mojom::SharedStorageModifierMethodWithOptionsPtr>
mojom_methods;
scoped_refptr<AuctionV8Helper> ref_v8_helper(v8_helper);
auto collect_methods_callback = base::BindRepeating(
[](scoped_refptr<AuctionV8Helper> v8_helper,
AuctionV8Helper::TimeLimitScope& time_limit_scope,
std::vector<network::mojom::SharedStorageModifierMethodWithOptionsPtr>&
mojom_methods,
v8::Local<v8::Value> method_val) -> IdlConvert::Status {
v8::Isolate* isolate = v8_helper->isolate();
v8::Local<v8::Context> context = isolate->GetCurrentContext();
static constexpr char kTypeConversionError[] =
"Failed to convert value to 'SharedStorageModifierMethod'";
v8::Local<v8::Object> method_obj;
if (!method_val->ToObject(context).ToLocal(&method_obj)) {
return IdlConvert::Status::MakeErrorMessage(kTypeConversionError);
}
if (method_obj->InternalFieldCount() !=
gin::kNumberOfInternalFields + 1) {
return IdlConvert::Status::MakeErrorMessage(kTypeConversionError);
}
v8::Local<v8::Value> internal_val =
method_obj->GetInternalField(gin::kNumberOfInternalFields)
.As<v8::Value>();
SharedStorageMethod* modifier_method = nullptr;
if (!gin::ConvertFromV8(isolate, internal_val, &modifier_method)) {
return IdlConvert::Status::MakeErrorMessage(kTypeConversionError);
}
if (modifier_method && modifier_method->mojom_method()) {
mojom_methods.push_back(modifier_method->mojom_method().Clone());
} else {
return IdlConvert::Status::MakeErrorMessage(kTypeConversionError);
}
return IdlConvert::Status::MakeSuccess();
},
ref_v8_helper, std::ref(time_limit_scope), std::ref(mojom_methods));
static constexpr char kErrorPrefix[] = "sharedStorage.batchUpdate(): ";
static constexpr char kSequenceConversionError[] =
"Trouble converting argument 'methods' to a Sequence.";
ArgsConverter args_converter(v8_helper, time_limit_scope, kErrorPrefix, &args,
/*min_required_args=*/1);
if (args_converter.is_success() && !args[0]->IsObject()) {
args_converter.SetStatus(IdlConvert::Status::MakeErrorMessage(
base::StrCat({kErrorPrefix, kSequenceConversionError})));
}
std::initializer_list<std::string_view> error_subject = {
"argument 'methods'"};
v8::Local<v8::Object> iterable = args[0].As<v8::Object>();
v8::Local<v8::Object> iterator_factory;
if (args_converter.is_success()) {
args_converter.SetStatus(IdlConvert::CheckForSequence(
isolate, kErrorPrefix, error_subject, iterable, iterator_factory));
if (iterator_factory.IsEmpty()) {
if (args_converter.is_success()) {
args_converter.SetStatus(IdlConvert::Status::MakeErrorMessage(
base::StrCat({kErrorPrefix, kSequenceConversionError})));
}
}
}
if (args_converter.is_success()) {
args_converter.SetStatus(IdlConvert::ConvertSequence(
v8_helper, kErrorPrefix, error_subject, iterable, iterator_factory,
std::move(collect_methods_callback)));
}
std::optional<std::string> with_lock;
if (args_converter.is_success() && args.Length() > 1) {
DictConverter options_dict_converter(
v8_helper, time_limit_scope,
"sharedStorage.batchUpdate 'options' argument ", args[1]);
options_dict_converter.GetOptional("withLock", with_lock);
args_converter.SetStatus(options_dict_converter.TakeStatus());
}
if (args_converter.is_failed()) {
args_converter.TakeStatus().PropagateErrorsToV8(v8_helper);
return;
}
if (!bindings->shared_storage_permissions_policy_allowed_) {
isolate->ThrowException(v8::Exception::TypeError(
gin::StringToV8(isolate, kPermissionsPolicyError)));
return;
}
// Fail for reserved lock name.
// https://w3c.github.io/web-locks/#resource-name
if (with_lock && with_lock->starts_with('-')) {
isolate->ThrowException(v8::Exception::TypeError(gin::StringToV8(
isolate,
base::StrCat({kErrorPrefix, "Lock name cannot start with '-'"}))));
return;
}
bindings->shared_storage_host_->SharedStorageBatchUpdate(
std::move(mojom_methods), with_lock,
bindings->source_auction_worklet_function_);
}
// static
void SharedStorageBindings::SetMethodConstructor(
const v8::FunctionCallbackInfo<v8::Value>& args) {

@ -39,6 +39,7 @@ class CONTENT_EXPORT SharedStorageBindings : public Bindings {
static void Append(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Delete(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Clear(const v8::FunctionCallbackInfo<v8::Value>& args);
static void BatchUpdate(const v8::FunctionCallbackInfo<v8::Value>& args);
static void SetMethodConstructor(
const v8::FunctionCallbackInfo<v8::Value>& args);

@ -13,6 +13,7 @@
#include "base/strings/stringprintf.h"
#include "base/strings/to_string.h"
#include "base/synchronization/waitable_event.h"
#include "content/public/test/shared_storage_test_utils.h"
#include "content/services/auction_worklet/auction_v8_helper.h"
#include "content/services/auction_worklet/public/cpp/auction_downloader.h"
#include "net/base/net_errors.h"
@ -167,6 +168,33 @@ TestAuctionSharedStorageHost::Request::operator=(Request&& other) = default;
bool TestAuctionSharedStorageHost::Request::operator==(
const Request& rhs) const = default;
TestAuctionSharedStorageHost::BatchRequest::BatchRequest(
std::vector<network::mojom::SharedStorageModifierMethodWithOptionsPtr>
methods_with_options,
const std::optional<std::string>& with_lock,
mojom::AuctionWorkletFunction source_auction_worklet_function)
: methods_with_options(std::move(methods_with_options)),
with_lock(with_lock),
source_auction_worklet_function(source_auction_worklet_function) {}
TestAuctionSharedStorageHost::BatchRequest::~BatchRequest() = default;
TestAuctionSharedStorageHost::BatchRequest::BatchRequest(
const BatchRequest& other)
: methods_with_options(
content::CloneSharedStorageMethods(other.methods_with_options)),
with_lock(other.with_lock),
source_auction_worklet_function(other.source_auction_worklet_function) {}
TestAuctionSharedStorageHost::BatchRequest::BatchRequest(BatchRequest&& other) =
default;
TestAuctionSharedStorageHost::BatchRequest&
TestAuctionSharedStorageHost::BatchRequest::operator=(BatchRequest&& other) =
default;
bool TestAuctionSharedStorageHost::BatchRequest::operator==(
const BatchRequest& rhs) const = default;
TestAuctionSharedStorageHost::TestAuctionSharedStorageHost() = default;
TestAuctionSharedStorageHost::~TestAuctionSharedStorageHost() = default;
@ -180,6 +208,17 @@ void TestAuctionSharedStorageHost::SharedStorageUpdate(
Request(std::move(method_with_options), source_auction_worklet_function));
}
void TestAuctionSharedStorageHost::SharedStorageBatchUpdate(
std::vector<network::mojom::SharedStorageModifierMethodWithOptionsPtr>
methods_with_options,
const std::optional<std::string>& with_lock,
auction_worklet::mojom::AuctionWorkletFunction
source_auction_worklet_function) {
observed_batch_requests_.emplace_back(
BatchRequest(std::move(methods_with_options), with_lock,
source_auction_worklet_function));
}
void TestAuctionSharedStorageHost::ClearObservedRequests() {
observed_requests_.clear();
}

@ -99,6 +99,27 @@ class TestAuctionSharedStorageHost : public mojom::AuctionSharedStorageHost {
bool operator==(const Request& rhs) const;
};
struct BatchRequest {
BatchRequest(
std::vector<network::mojom::SharedStorageModifierMethodWithOptionsPtr>
methods_with_options,
const std::optional<std::string>& with_lock,
mojom::AuctionWorkletFunction source_auction_worklet_function);
~BatchRequest();
BatchRequest(const BatchRequest& other);
BatchRequest& operator=(const BatchRequest& other) = delete;
BatchRequest(BatchRequest&& other);
BatchRequest& operator=(BatchRequest&& other);
std::vector<network::mojom::SharedStorageModifierMethodWithOptionsPtr>
methods_with_options;
std::optional<std::string> with_lock;
mojom::AuctionWorkletFunction source_auction_worklet_function;
bool operator==(const BatchRequest& rhs) const;
};
TestAuctionSharedStorageHost();
~TestAuctionSharedStorageHost() override;
@ -109,15 +130,26 @@ class TestAuctionSharedStorageHost : public mojom::AuctionSharedStorageHost {
method_with_options,
auction_worklet::mojom::AuctionWorkletFunction
source_auction_worklet_function) override;
void SharedStorageBatchUpdate(
std::vector<network::mojom::SharedStorageModifierMethodWithOptionsPtr>
methods_with_options,
const std::optional<std::string>& with_lock,
auction_worklet::mojom::AuctionWorkletFunction
source_auction_worklet_function) override;
const std::vector<Request>& observed_requests() const {
return observed_requests_;
}
const std::vector<BatchRequest>& observed_batch_requests() const {
return observed_batch_requests_;
}
void ClearObservedRequests();
private:
std::vector<Request> observed_requests_;
std::vector<BatchRequest> observed_batch_requests_;
};
class TestAuctionNetworkEventsHandler

@ -0,0 +1,102 @@
// META: script=/resources/testdriver.js
// META: script=/resources/testdriver-vendor.js
// META: script=/common/utils.js
// META: script=/fledge/tentative/resources/fledge-util.sub.js
// META: script=/common/subset-tests.js
// META: script=/shared-storage/resources/util.js
// META: script=/fenced-frame/resources/utils.js
// META: timeout=long
"use strict;"
subsetTest(promise_test, async test => {
let worklet = await sharedStorage.createWorklet('resources/simple-module.js');
const ancestor_key = token();
let url0 = generateURL("/shared-storage/resources/frame0.html",
[ancestor_key]);
let url1 = generateURL("/shared-storage/resources/frame1.html",
[ancestor_key]);
// Override the default resource path, as we are not running within the Fledge
// repository.
RESOURCE_PATH = '/fledge/tentative/resources/';
const pa_uuid = generateUuid(test);
let biddingLogicURL = createBiddingScriptURL(
{
generateBid:
`
sharedStorage.batchUpdate([
new SharedStorageAppendMethod('key', 'a'),
new SharedStorageAppendMethod('key', 'a')
], {withLock: 'lock1'});
return {};
`
});
let decisionLogicURL = createDecisionScriptURL(pa_uuid);
// Invoke `selectURL()` to perform the following steps:
// 1. Acquires the lock.
// 2. Reads the current value at the given key.
// 3. Waits for 500ms.
// 4. Sets the shared storage value to the read value appended with the given letter.
// 5. Releases the lock.
//
// After 100ms, run a Protected Audience auction that starts a worklet that:
// - Acquires the same named lock.
// - Executes two `append` methods, each appending the same letter.
//
// Expected behavior: After both of them finish, the value at the given key
// should contain the letter repeated three times.
//
// This demonstrates that:
// 1. The `withLock` option is effective, preventing the `batchUpdate()`
// method interfering with the "get and set" operation. If the lock were
// not used, the final value would likely be a single letter.
// 2. `batchUpdate()` correctly executes all `append` methods within the
// batch.
//
// Note: This test remains valid even if the `batchUpdate()` call happens
// outside the critical section protected by the lock within the worklet. The
// test effectively demonstrates mutual exclusion as long as there's a
// reasonable chance for `batchUpdate()` to occur while the worklet is still
// running.
let select_url_result = await worklet.selectURL(
"get-wait-set-within-lock",
[{url: url0}, {url: url1}],
{data: {'key': 'key',
'lock_name': 'lock1',
'append_letter': 'a'},
resolveToConfig: true});
// Busy wait for 100ms.
const startWaitTime = Date.now();
while (Date.now() - startWaitTime < 100) {}
// Run a Protected Audience auction which triggers `append()` with the same
// lock and the same letter.
await joinGroupAndRunBasicFledgeTestExpectingNoWinner(
test,
{
uuid: pa_uuid,
interestGroupOverrides: {
name: pa_uuid,
biddingLogicURL: biddingLogicURL,
},
auctionConfigOverrides: {
decisionLogicURL: decisionLogicURL
}
});
attachFencedFrame(select_url_result, 'opaque-ads');
const result = await nextValueFromServer(ancestor_key);
assert_equals(result, "frame1_loaded");
await verifyKeyValueForOrigin('key', 'aaa', location.origin);
await deleteKeyForOrigin('key', location.origin);
}, 'Test for batchUpdate() with a batch lock in a Protected Audience Worklet context');