1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
content / services / auction_worklet / auction_v8_helper.h [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CONTENT_SERVICES_AUCTION_WORKLET_AUCTION_V8_HELPER_H_
#define CONTENT_SERVICES_AUCTION_WORKLET_AUCTION_V8_HELPER_H_
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include "base/containers/span.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted.h"
#include "base/memory/ref_counted_delete_on_sequence.h"
#include "base/memory/scoped_refptr.h"
#include "base/sequence_checker.h"
#include "base/synchronization/lock.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "content/common/content_export.h"
#include "gin/public/isolate_holder.h"
#include "mojo/public/cpp/bindings/pending_associated_receiver.h"
#include "third_party/blink/public/mojom/devtools/devtools_agent.mojom.h"
#include "url/gurl.h"
#include "v8/include/v8-forward.h"
#include "v8/include/v8-isolate.h"
#include "v8/include/v8-persistent-handle.h"
namespace v8 {
class UnboundScript;
class WasmModuleObject;
} // namespace v8
namespace v8_inspector {
class V8Inspector;
} // namespace v8_inspector
namespace auction_worklet {
class AuctionV8DevToolsAgent;
class DebugCommandQueue;
// Helper for Javascript operations. Owns a V8 isolate, and manages operations
// on it. Must be deleted after all V8 objects created using its isolate. It
// facilitates creating objects from JSON and running scripts in isolated
// contexts.
//
// Currently, multiple AuctionV8Helpers can be in use at once, each will have
// its own V8 isolate. All AuctionV8Helpers are assumed to be created on the
// same thread (V8 startup is done only once per process, and not behind a
// lock). After creation, all public operations on the helper must be done on
// the thread represented by the `v8_runner` argument to Create(). It's the
// caller's responsibility to ensure that all other methods are used from the v8
// runner.
class CONTENT_EXPORT AuctionV8Helper
: public base::RefCountedDeleteOnSequence<AuctionV8Helper> {
public:
// Timeout for script execution.
static const base::TimeDelta kScriptTimeout;
// Status of a computation that may timeout.
enum class Result { kSuccess, kFailure, kTimeout };
// Helper class to set up v8 scopes to use Isolate. All methods expect a
// FullIsolateScope to be have been created on the current thread, and a
// context to be entered.
class CONTENT_EXPORT FullIsolateScope {
public:
explicit FullIsolateScope(AuctionV8Helper* v8_helper);
explicit FullIsolateScope(const FullIsolateScope&) = delete;
FullIsolateScope& operator=(const FullIsolateScope&) = delete;
~FullIsolateScope();
private:
const v8::Isolate::Scope isolate_scope_;
const v8::HandleScope handle_scope_;
};
// A wrapper for identifiers used to associate V8 context's with debugging
// primitives. Passed to methods like Compile and RunScript. If one is
// created, AbortDebuggerPauses() must be called before its destruction.
//
// This class is thread-safe, except SetResumeCallback must be used from V8
// thread.
class CONTENT_EXPORT DebugId : public base::RefCountedThreadSafe<DebugId> {
public:
explicit DebugId(AuctionV8Helper* v8_helper);
// Returns V8 context group ID associated with this debug id.
int context_group_id() const { return context_group_id_; }
// Sets the callback to use to resume a worklet that's paused on startup.
// Must be called from the V8 thread.
//
// `resume_callback` will be invoked on the V8 thread; and should probably
// be bound to a a WeakPtr, since the invocation is ultimately via debugger
// mojo pipes, making its timing hard to relate to worklet lifetime.
void SetResumeCallback(base::OnceClosure resume_callback);
// If the JS thread is currently within AuctionV8Helper::RunScript() running
// code with this debug id, and the execution has been paused by the
// debugger, aborts the execution.
//
// Always prevents further debugger pauses of code associated with this
// debug id.
//
// This may be called from any thread, but note that posting this to the V8
// thread is unlikely to work, since this method is in particular useful for
// the cases where the V8 thread is blocked.
void AbortDebuggerPauses();
private:
friend class base::RefCountedThreadSafe<DebugId>;
~DebugId();
const scoped_refptr<AuctionV8Helper> v8_helper_;
const int context_group_id_;
};
// Representation for results of serialization via SerializeValue().
// Helps with the memory management. Movable but not copyable.
class CONTENT_EXPORT SerializedValue {
public:
SerializedValue();
SerializedValue(const SerializedValue&) = delete;
SerializedValue(SerializedValue&& other);
~SerializedValue();
SerializedValue& operator=(const SerializedValue&) = delete;
SerializedValue& operator=(SerializedValue&&);
bool IsOK() const { return buffer_; }
private:
friend class AuctionV8Helper;
raw_ptr<uint8_t> buffer_;
size_t size_;
};
// Represents a time limit that's shared by a group of operations (so if it's
// 50ms and first takes 30ms and second tries to take 25ms, it will be
// interrupted at around 20ms).
class CONTENT_EXPORT TimeLimit {
public:
virtual ~TimeLimit();
// Resumes the timer if it's not already running. Returns true if the timer
// was resumed, false if it was already running.
//
// You do not need to call this directly if you're using `RunScript` or
// `CallFunction`.
virtual bool Resume() = 0;
// Pauses the timer (must be running). You do not need to
// call it directly if you're using `RunScript` or `CallFunction`.
virtual void Pause() = 0;
AuctionV8Helper* v8_helper() { return v8_helper_; }
protected:
explicit TimeLimit(AuctionV8Helper* v8_helper) : v8_helper_(v8_helper) {}
private:
const raw_ptr<AuctionV8Helper> v8_helper_;
};
// Helper that calls Resume()/Pause() if given a non-nullptr TimeLimit.
//
// v8::TryCatch::HasTerminated() can help detect the timeouts.
//
// This is safe to use recursively.
class CONTENT_EXPORT TimeLimitScope {
public:
explicit TimeLimitScope(TimeLimit* script_timeout);
~TimeLimitScope();
bool has_time_limit() const { return script_timeout_; }
private:
raw_ptr<TimeLimit> script_timeout_;
bool resumed_ = false;
};
explicit AuctionV8Helper(const AuctionV8Helper&) = delete;
AuctionV8Helper& operator=(const AuctionV8Helper&) = delete;
static scoped_refptr<AuctionV8Helper> Create(
scoped_refptr<base::SingleThreadTaskRunner> v8_runner,
bool init_v8 = true);
static scoped_refptr<base::SingleThreadTaskRunner> CreateTaskRunner();
scoped_refptr<base::SequencedTaskRunner> v8_runner() const {
return v8_runner_;
}
// Note: `callback` will be called on `v8_runner()`. This method may be called
// on the creation thread if done before any non-initialization work on v8
// thread begins.
void SetDestroyedCallback(base::OnceClosure callback);
v8::Isolate* isolate() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return isolate_holder_->isolate();
}
// Context that can be used for persistent items that can then be used in
// other contexts - compiling functions, creating objects, etc.
v8::Local<v8::Context> scratch_context() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return scratch_context_.Get(isolate());
}
// Create a v8::Context. The one thing this does that v8::Context::New() does
// not is remove access to the Date object.
v8::Local<v8::Context> CreateContext(
v8::Local<v8::ObjectTemplate> global_template =
v8::Local<v8::ObjectTemplate>());
// Creates a v8::String from an ASCII string literal, which should never fail.
v8::Local<v8::String> CreateStringFromLiteral(const char* ascii_string);
// Attempts to create a v8::String from a UTF-8 string. Returns empty string
// if input is not UTF-8.
v8::MaybeLocal<v8::String> CreateUtf8String(std::string_view utf8_string);
// The passed in JSON must be a valid UTF-8 JSON string.
v8::MaybeLocal<v8::Value> CreateValueFromJson(v8::Local<v8::Context> context,
std::string_view utf8_json);
// Convenience wrappers around the above Create* methods. Attempt to create
// the corresponding value type and append it to the passed in argument
// vector. Useful for assembling arguments to a Javascript function. Return
// false on failure.
[[nodiscard]] bool AppendUtf8StringValue(std::string_view utf8_string,
v8::LocalVector<v8::Value>* args);
[[nodiscard]] bool AppendJsonValue(v8::Local<v8::Context> context,
std::string_view utf8_json,
v8::LocalVector<v8::Value>* args);
// Convenience wrapper that adds the specified value into the provided Object.
[[nodiscard]] bool InsertValue(std::string_view key,
v8::Local<v8::Value> value,
v8::Local<v8::Object> object);
// Convenience wrapper that creates an Object by parsing `utf8_json` as JSON
// and then inserts it into the provided Object.
[[nodiscard]] bool InsertJsonValue(v8::Local<v8::Context> context,
std::string_view key,
std::string_view utf8_json,
v8::Local<v8::Object> object);
// Attempts to convert |value| to JSON and write it to |out|.
Result ExtractJson(v8::Local<v8::Context> context,
v8::Local<v8::Value> value,
TimeLimit* script_timeout,
std::string* out);
// Serializes |value| via v8::ValueSerializer and returns it. This is faster
// than JSON. The return value can be used (and deserialized) in any context,
// and can be freed on any thread (though some malloc implementations would
// prefer if it were to be freed on v8 thread).
SerializedValue Serialize(v8::Local<v8::Context> context,
v8::Local<v8::Value> value);
// Deserializes `value` via v8::ValueDeserializer in `context`.
v8::MaybeLocal<v8::Value> Deserialize(
v8::Local<v8::Context> context,
const SerializedValue& serialized_value);
// Compiles the provided script. Despite not being bound to a context, there
// still must be an active context for this method to be invoked. In case of
// an error sets `error_out`.
v8::MaybeLocal<v8::UnboundScript> Compile(
const std::string& src,
const GURL& src_url,
const DebugId* debug_id,
std::optional<std::string>& error_out);
// Compiles the provided WASM module from bytecode. A context must be active
// for this method to be invoked, and the object would be created for it (but
// may be cloned efficiently for other contexts via CloneWasmModule). In case
// of an error sets `error_out`.
//
// Note that since the returned object is a JS Object, so to properly isolate
// different executions it should not be used directly but rather fresh copies
// should be made via CloneWasmModule.
v8::MaybeLocal<v8::WasmModuleObject> CompileWasm(
const std::string& payload,
const GURL& src_url,
const DebugId* debug_id,
std::optional<std::string>& error_out);
// Creates a fresh object describing the same WASM module as `in`, which must
// not be empty. Can return an empty handle on an error.
//
// An execution context must be active, and the object will be created for it.
v8::MaybeLocal<v8::WasmModuleObject> CloneWasmModule(
v8::Local<v8::WasmModuleObject> in);
// Creates a time limiter for a group of operations. Note that it registers
// itself with `this` and must not outlive it, and there shouldn't be more
// than one at a time per AuctionV8Helper.
//
// If `script_timeout` has no value, kScriptTimeout will be used as the
// default timeout.
std::unique_ptr<TimeLimit> CreateTimeLimit(
std::optional<base::TimeDelta> script_timeout);
// Returns the currently active time limit, if any.
TimeLimit* GetTimeLimit();
// Binds a script and runs it in the passed in context, returning whether it
// succeeded.
//
// If `debug_id` is not nullptr, and a debugger connection has been
// instantiated, will notify debugger of `context`.
//
// Assumes passed in context is the active context. Passed in context must be
// using the Helper's isolate.
//
// If `script_timeout` is set, it will be used as a time limit for this
// operation. (If nullptr, the script may take an arbitrary amount of time or
// might fail to terminate).
//
// In case of an error appends it to `error_out`.
Result RunScript(v8::Local<v8::Context> context,
v8::Local<v8::UnboundScript> script,
const DebugId* debug_id,
TimeLimit* script_timeout,
std::vector<std::string>& error_out);
// Calls a bound function (by name) attached to the global context in the
// passed in context and returns the value returned by the function. Note that
// the returned value could include references to objects or functions
// contained within the context, so is likely not safe to use in other
// contexts without sanitization.
//
// `script_name` is the name of the script for debugging. Can be found by
// calling `FormatScriptName` on the `script` passed to `RunScript()`.
//
// `function_name` will be called passing in `args` as arguments.
//
// If `debug_id` is not nullptr, and a debugger connection has been
// instantiated, will notify debugger of `context`.
//
// Assumes passed in context is the active context. Passed in context must be
// using the Helper's isolate.
//
// If `script_timeout` is set, it will be used as a time limit for this
// operation. (If nullptr, the function may take an arbitrary amount of time
// or might fail to terminate).
//
// Returns whether successful or not. `value_out` will be non-empty (and set
// to the return value) if and only if successful.
//
// In case of an error appends it to `error_out`.
Result CallFunction(v8::Local<v8::Context> context,
const DebugId* debug_id,
const std::string& script_name,
std::string_view function_name,
base::span<v8::Local<v8::Value>> args,
TimeLimit* script_timeout,
v8::MaybeLocal<v8::Value>& value_out,
std::vector<std::string>& error_out);
// If any debugging session targeting `debug_id` has set an active
// DOM instrumentation breakpoint `name`, asks for v8 to do a debugger pause
// on the next statement.
//
// Expected to be run before a corresponding RunScript.
void MaybeTriggerInstrumentationBreakpoint(const DebugId& debug_id,
const std::string& name);
void set_script_timeout_for_testing(base::TimeDelta script_timeout);
// Invokes the registered resume callback for given ID. Does nothing if it
// was already invoked.
void Resume(int context_group_id);
// Overrides what ID will be remembered as last returned to help check the
// allocation algorithm.
void SetLastContextGroupIdForTesting(int new_last_id);
// Calls Resume on all registered context group IDs.
void ResumeAllForTesting();
// Establishes a debugger connection, initializing debugging objects if
// needed, and associating the connection with the given `debug_id`.
//
// The debugger Mojo objects will primarily live on the v8 thread, but
// `mojo_sequence` will be used for a secondary communication channel in case
// the v8 thread is blocked. It must be distinct from v8_runner(). Only the
// value passed in for `mojo_sequence` the first time this method is called
// will be used.
void ConnectDevToolsAgent(
mojo::PendingAssociatedReceiver<blink::mojom::DevToolsAgent> agent,
scoped_refptr<base::SequencedTaskRunner> mojo_sequence,
const DebugId& debug_id);
// Returns the v8 inspector if one has been set. null if ConnectDevToolsAgent
// (or SetV8InspectorForTesting) hasn't been called.
v8_inspector::V8Inspector* inspector();
void SetV8InspectorForTesting(
std::unique_ptr<v8_inspector::V8Inspector> v8_inspector);
// Temporarily disables (and re-enables) script timeout for the currently
// running script. Total time elapsed when not paused will be kept track of.
//
// Must be called when within RunScript() only.
void PauseTimeoutTimer();
void ResumeTimeoutTimer();
// Returns the sequence where the timeout timer runs.
// This may be called on any thread.
scoped_refptr<base::SequencedTaskRunner> GetTimeoutTimerRunnerForTesting();
// Helper for formatting script name for debug messages.
std::string FormatScriptName(v8::Local<v8::UnboundScript> script);
static std::string FormatExceptionMessage(v8::Local<v8::Context> context,
v8::Local<v8::Message> message);
private:
friend class base::RefCountedDeleteOnSequence<AuctionV8Helper>;
friend class base::DeleteHelper<AuctionV8Helper>;
class ScriptTimeoutHelper;
AuctionV8Helper(scoped_refptr<base::SingleThreadTaskRunner> v8_runner,
bool init_v8);
~AuctionV8Helper();
void CreateIsolate();
// These methods are used by DebugId, and except SetResumeCallback can be
// called from any thread.
int AllocContextGroupId();
void SetResumeCallback(int context_group_id,
base::OnceClosure resume_callback);
void AbortDebuggerPauses(int context_group_id);
void FreeContextGroupId(int context_group_id);
static std::string FormatValue(v8::Isolate* isolate,
v8::Local<v8::Value> val);
scoped_refptr<base::SequencedTaskRunner> v8_runner_;
scoped_refptr<base::SequencedTaskRunner> timer_task_runner_;
// This needs to be invoked after ~IsolateHolder to make sure that V8 is
// really shut down.
base::ScopedClosureRunner destroyed_callback_run_;
std::unique_ptr<gin::IsolateHolder> isolate_holder_
GUARDED_BY_CONTEXT(sequence_checker_);
v8::Global<v8::Context> scratch_context_
GUARDED_BY_CONTEXT(sequence_checker_);
// Script timeout. Can be changed for testing.
base::TimeDelta script_timeout_ GUARDED_BY_CONTEXT(sequence_checker_) =
kScriptTimeout;
raw_ptr<ScriptTimeoutHelper> timeout_helper_
GUARDED_BY_CONTEXT(sequence_checker_) = nullptr;
base::Lock context_groups_lock_;
int last_context_group_id_ GUARDED_BY(context_groups_lock_) = 0;
// This is keyed by group IDs, and is used to keep track of what's valid.
std::map<int, base::OnceClosure> resume_callbacks_
GUARDED_BY(context_groups_lock_);
scoped_refptr<DebugCommandQueue> debug_command_queue_;
// Destruction order between `devtools_agent_` and `v8_inspector_` is
// relevant; see also comment in ~AuctionV8Helper().
std::unique_ptr<AuctionV8DevToolsAgent> devtools_agent_
GUARDED_BY_CONTEXT(sequence_checker_);
std::unique_ptr<v8_inspector::V8Inspector> v8_inspector_
GUARDED_BY_CONTEXT(sequence_checker_);
SEQUENCE_CHECKER(sequence_checker_);
};
} // namespace auction_worklet
#endif // CONTENT_SERVICES_AUCTION_WORKLET_AUCTION_V8_HELPER_H_