Commit 821303d2 authored by Adam Rice's avatar Adam Rice Committed by Commit Bot

Streams: Get MessageChannel-related values from bindings

Copy the the original values of selected DOM constructors, methods and
getters to the binding object during global object initialisation. Then
access them via the binding object in the V8 extras transferable streams
implementation. This protects the implementation from modifications to
the global object.

Constructors copied:
MessageChannel
DOMException

Methods copied:
EventTarget.prototype.addEventListener
MessagePort.prototype.postMessage
MessagePort.prototype.close
MessagePort.prototype.start

Accessors copied:
MessageChannel.prototype.port1
MessageChannel.prototype.port2
MessageEvent.prototype.data
DOMException.prototype.message
DOMException.prototype.name

BUG=897046

Change-Id: I72e40951e5c45f51c957dfa277044a210b054675
Reviewed-on: https://chromium-review.googlesource.com/c/1291191
Commit-Queue: Adam Rice <ricea@chromium.org>
Reviewed-by: default avatarYutaka Hirano <yhirano@chromium.org>
Reviewed-by: default avatarYuki Shiino <yukishiino@chromium.org>
Cr-Commit-Position: refs/heads/master@{#603053}
parent 2eb46b48
...@@ -78,16 +78,102 @@ class CountUseForBindings : public ScriptFunction { ...@@ -78,16 +78,102 @@ class CountUseForBindings : public ScriptFunction {
} }
}; };
void AddCountUse(ScriptState* script_state, v8::Local<v8::Object> binding) {
v8::Local<v8::Function> fn =
CountUseForBindings::CreateFunction(script_state);
v8::Local<v8::String> name =
V8AtomicString(script_state->GetIsolate(), "countUse");
binding->Set(script_state->GetContext(), name, fn).ToChecked();
}
void AddOriginals(ScriptState* script_state, v8::Local<v8::Object> binding) {
v8::Local<v8::Object> global = script_state->GetContext()->Global();
v8::Local<v8::Context> context = script_state->GetContext();
v8::Isolate* isolate = script_state->GetIsolate();
const auto ObjectGet = [&context, &isolate](v8::Local<v8::Value> object,
const char* property) {
DCHECK(object->IsObject());
return object.As<v8::Object>()
->Get(context, V8AtomicString(isolate, property))
.ToLocalChecked();
};
const auto GetPrototype = [&ObjectGet](v8::Local<v8::Value> object) {
return ObjectGet(object, "prototype");
};
const auto GetOwnPD = [&context, &isolate](v8::Local<v8::Value> object,
const char* property) {
DCHECK(object->IsObject());
return object.As<v8::Object>()
->GetOwnPropertyDescriptor(context, V8AtomicString(isolate, property))
.ToLocalChecked();
};
const auto GetOwnPDGet = [&ObjectGet, &GetOwnPD](v8::Local<v8::Value> object,
const char* property) {
return ObjectGet(GetOwnPD(object, property), "get");
};
const auto Bind = [&context, &isolate, &binding](const char* name,
v8::Local<v8::Value> value) {
bool result =
binding
->CreateDataProperty(context, V8AtomicString(isolate, name), value)
.ToChecked();
DCHECK(result);
};
v8::Local<v8::Value> message_channel = ObjectGet(global, "MessageChannel");
// Some Worklets don't have MessageChannel. In this case, serialization will
// be disabled.
if (message_channel->IsUndefined())
return;
Bind("MessageChannel", message_channel);
v8::Local<v8::Value> message_channel_prototype =
GetPrototype(message_channel);
Bind("MessageChannel_port1_get",
GetOwnPDGet(message_channel_prototype, "port1"));
Bind("MessageChannel_port2_get",
GetOwnPDGet(message_channel_prototype, "port2"));
v8::Local<v8::Value> event_target_prototype =
GetPrototype(ObjectGet(global, "EventTarget"));
Bind("EventTarget_addEventListener",
ObjectGet(event_target_prototype, "addEventListener"));
v8::Local<v8::Value> message_port_prototype =
GetPrototype(ObjectGet(global, "MessagePort"));
Bind("MessagePort_postMessage",
ObjectGet(message_port_prototype, "postMessage"));
Bind("MessagePort_close", ObjectGet(message_port_prototype, "close"));
Bind("MessagePort_start", ObjectGet(message_port_prototype, "start"));
v8::Local<v8::Value> message_event_prototype =
GetPrototype(ObjectGet(global, "MessageEvent"));
Bind("MessageEvent_data_get", GetOwnPDGet(message_event_prototype, "data"));
v8::Local<v8::Value> dom_exception = ObjectGet(global, "DOMException");
Bind("DOMException", dom_exception);
v8::Local<v8::Value> dom_exception_prototype = GetPrototype(dom_exception);
Bind("DOMException_message_get",
GetOwnPDGet(dom_exception_prototype, "message"));
Bind("DOMException_name_get", GetOwnPDGet(dom_exception_prototype, "name"));
}
} // namespace } // namespace
void InitializeV8ExtrasBinding(ScriptState* script_state) { void InitializeV8ExtrasBinding(ScriptState* script_state) {
v8::Local<v8::Object> binding = v8::Local<v8::Object> binding =
script_state->GetContext()->GetExtrasBindingObject(); script_state->GetContext()->GetExtrasBindingObject();
v8::Local<v8::Function> fn = AddCountUse(script_state, binding);
CountUseForBindings::CreateFunction(script_state); AddOriginals(script_state, binding);
v8::Local<v8::String> name =
V8AtomicString(script_state->GetIsolate(), "countUse");
binding->Set(script_state->GetContext(), name, fn).FromJust();
} }
} // namespace blink } // namespace blink
...@@ -11,10 +11,8 @@ namespace blink { ...@@ -11,10 +11,8 @@ namespace blink {
class ScriptState; class ScriptState;
// Add the Javascript function countUse() to the "binding" object that is // Add the JavaScript function countUse() to the "binding" object that is
// exposed to the Javascript streams implementations. // exposed to the JavaScript streams implementations.
//
// It must be called during initialisation of the V8 context.
// //
// binding.countUse() takes a string and calls UseCounter::Count() on the // binding.countUse() takes a string and calls UseCounter::Count() on the
// matching ID. It only does anything the first time it is called in a // matching ID. It only does anything the first time it is called in a
...@@ -23,6 +21,12 @@ class ScriptState; ...@@ -23,6 +21,12 @@ class ScriptState;
// than once to avoid unnecessary overhead. Only string IDs that this code // than once to avoid unnecessary overhead. Only string IDs that this code
// specifically knows about will work. // specifically knows about will work.
// //
// Also copy the original values of MessageChannel, MessagePort and MessageEvent
// methods and accessors to the binding object where they can be used for
// serialization by the streams code.
//
// This function must be called during initialisation of the V8 context.
//
// countUse() is not available during snapshot creation. // countUse() is not available during snapshot creation.
void CORE_EXPORT InitializeV8ExtrasBinding(ScriptState*); void CORE_EXPORT InitializeV8ExtrasBinding(ScriptState*);
......
...@@ -182,6 +182,9 @@ void LocalWindowProxy::Initialize() { ...@@ -182,6 +182,9 @@ void LocalWindowProxy::Initialize() {
InstallConditionalFeatures(); InstallConditionalFeatures();
// This needs to go after everything else since it accesses the window object.
InitializeV8ExtrasBinding(script_state_);
if (World().IsMainWorld()) { if (World().IsMainWorld()) {
GetFrame()->Loader().DispatchDidClearWindowObjectInMainWorld(); GetFrame()->Loader().DispatchDidClearWindowObjectInMainWorld();
} }
...@@ -236,8 +239,6 @@ void LocalWindowProxy::CreateContext() { ...@@ -236,8 +239,6 @@ void LocalWindowProxy::CreateContext() {
script_state_ = ScriptState::Create(context, world_); script_state_ = ScriptState::Create(context, world_);
InitializeV8ExtrasBinding(script_state_);
DCHECK(lifecycle_ == Lifecycle::kContextIsUninitialized || DCHECK(lifecycle_ == Lifecycle::kContextIsUninitialized ||
lifecycle_ == Lifecycle::kGlobalObjectIsDetached); lifecycle_ == Lifecycle::kGlobalObjectIsDetached);
lifecycle_ = Lifecycle::kContextIsInitialized; lifecycle_ = Lifecycle::kContextIsInitialized;
......
...@@ -176,8 +176,6 @@ bool WorkerOrWorkletScriptController::InitializeContextIfNeeded( ...@@ -176,8 +176,6 @@ bool WorkerOrWorkletScriptController::InitializeContextIfNeeded(
ScriptState::Scope scope(script_state_); ScriptState::Scope scope(script_state_);
InitializeV8ExtrasBinding(script_state_);
// Associate the global proxy object, the global object and the worker // Associate the global proxy object, the global object and the worker
// instance (C++ object) as follows. // instance (C++ object) as follows.
// //
...@@ -246,6 +244,10 @@ bool WorkerOrWorkletScriptController::InitializeContextIfNeeded( ...@@ -246,6 +244,10 @@ bool WorkerOrWorkletScriptController::InitializeContextIfNeeded(
disable_eval_pending_ = String(); disable_eval_pending_ = String();
} }
// This can only be called after the global object is fully initialised, as it
// reads values from it.
InitializeV8ExtrasBinding(script_state_);
return true; return true;
} }
......
...@@ -32,16 +32,6 @@ ...@@ -32,16 +32,6 @@
const thenPromise = v8.uncurryThis(Promise.prototype.then); const thenPromise = v8.uncurryThis(Promise.prototype.then);
// See unsafeCopyGlobals() below.
let MessagePort_addEventListener;
let MessagePort_postMessage;
let MessagePort_close;
let MessagePort_start;
let MessageEvent_data_get;
let DOMException;
let DOMException_message_get;
let DOMException_name_get;
const JSON_parse = global.JSON.parse.bind(global.JSON); const JSON_parse = global.JSON.parse.bind(global.JSON);
const JSON_stringify = global.JSON.stringify.bind(global.JSON); const JSON_stringify = global.JSON.stringify.bind(global.JSON);
...@@ -317,31 +307,6 @@ ...@@ -317,31 +307,6 @@
const kAbort = 5; const kAbort = 5;
const kError = 6; const kError = 6;
// This function is a stopgap until we have a way to get the original values.
// It is not safe because the values may have been changed by the page, and
// definitely cannot be shipped like this.
// TODO(ricea): Find a safe way to do this.
function unsafeCopyGlobals() {
MessagePort_addEventListener =
v8.uncurryThis(MessagePort.prototype.addEventListener);
MessagePort_postMessage =
v8.uncurryThis(MessagePort.prototype.postMessage);
MessagePort_close =
v8.uncurryThis(MessagePort.prototype.close);
MessagePort_start =
v8.uncurryThis(MessagePort.prototype.start);
MessageEvent_data_get =
v8.uncurryThis(
getOwnPropertyDescriptor(MessageEvent.prototype, 'data').get);
DOMException = self.DOMException;
DOMException_message_get =
v8.uncurryThis(
getOwnPropertyDescriptor(DOMException.prototype, 'message').get);
DOMException_name_get =
v8.uncurryThis(
getOwnPropertyDescriptor(DOMException.prototype, 'name').get);
}
function isATypeError(object) { function isATypeError(object) {
// There doesn't appear to be a 100% reliable way to identify a TypeError // There doesn't appear to be a 100% reliable way to identify a TypeError
// from JS. // from JS.
...@@ -350,7 +315,7 @@ ...@@ -350,7 +315,7 @@
function isADOMException(object) { function isADOMException(object) {
try { try {
DOMException_name_get(object); callFunction(binding.DOMException_name_get, object);
return true; return true;
} catch (e) { } catch (e) {
return false; return false;
...@@ -385,8 +350,9 @@ ...@@ -385,8 +350,9 @@
} }
if (isADOMException(reason)) { if (isADOMException(reason)) {
const message = DOMException_message_get(reason); const message =
const name = DOMException_name_get(reason); callFunction(binding.DOMException_message_get, reason);
const name = callFunction(binding.DOMException_name_get, reason);
return { return {
encoder: 'domexception', encoder: 'domexception',
string: JSON_stringify({message, name}) string: JSON_stringify({message, name})
...@@ -416,7 +382,7 @@ ...@@ -416,7 +382,7 @@
case 'domexception': case 'domexception':
const {message, name} = JSON_parse(string); const {message, name} = JSON_parse(string);
return new DOMException(message, name); return new binding.DOMException(message, name);
case 'undefined': case 'undefined':
return undefined; return undefined;
...@@ -424,11 +390,10 @@ ...@@ -424,11 +390,10 @@
} }
function CreateCrossRealmTransformWritable(port) { function CreateCrossRealmTransformWritable(port) {
unsafeCopyGlobals();
let backpressurePromise = v8.createPromise(); let backpressurePromise = v8.createPromise();
MessagePort_addEventListener(port, 'message', evt => { callFunction(binding.EventTarget_addEventListener, port, 'message', evt => {
const {type, value} = MessageEvent_data_get(evt); const {type, value} = callFunction(binding.MessageEvent_data_get, evt);
// assert(type === kPull || type === kCancel || type === kError); // assert(type === kPull || type === kCancel || type === kError);
switch (type) { switch (type) {
case kPull: case kPull:
...@@ -449,23 +414,30 @@ ...@@ -449,23 +414,30 @@
} }
}); });
MessagePort_addEventListener(port, 'messageerror', () => { callFunction(
const error = new DOMException('chunk could not be cloned', binding.EventTarget_addEventListener, port, 'messageerror', () => {
'DataCloneError'); const error = new binding.DOMException('chunk could not be cloned',
MessagePort_postMessage(port, {type: kError, value: packReason(error)}); 'DataCloneError');
MessagePort_close(port); callFunction(binding.MessagePort_postMessage, port,
binding.WritableStreamDefaultControllerErrorIfNeeded(controller, error); {type: kError, value: packReason(error)});
}); callFunction(binding.MessagePort_close, port);
binding.WritableStreamDefaultControllerErrorIfNeeded(controller,
error);
});
MessagePort_start(port); callFunction(binding.MessagePort_start, port);
function doWrite(chunk) { function doWrite(chunk) {
backpressurePromise = v8.createPromise(); backpressurePromise = v8.createPromise();
try { try {
MessagePort_postMessage(port, {type: kChunk, value: chunk}); callFunction(
binding.MessagePort_postMessage, port,
{type: kChunk, value: chunk});
} catch (e) { } catch (e) {
MessagePort_postMessage(port, {type: kError, value: packReason(e)}); callFunction(
MessagePort_close(port); binding.MessagePort_postMessage, port,
{type: kError, value: packReason(e)});
callFunction(binding.MessagePort_close, port);
throw e; throw e;
} }
} }
...@@ -479,16 +451,17 @@ ...@@ -479,16 +451,17 @@
return thenPromise(backpressurePromise, () => doWrite(chunk)); return thenPromise(backpressurePromise, () => doWrite(chunk));
}, },
() => { () => {
MessagePort_postMessage(port, {type: kClose, value: undefined}); callFunction(
MessagePort_close(port); binding.MessagePort_postMessage, port,
{type: kClose, value: undefined});
callFunction(binding.MessagePort_close, port);
return Promise_resolve(); return Promise_resolve();
}, },
reason => { reason => {
MessagePort_postMessage(port, { callFunction(
type: kAbort, binding.MessagePort_postMessage, port,
value: packReason(reason) {type: kAbort, value: packReason(reason)});
}); callFunction(binding.MessagePort_close, port);
MessagePort_close(port);
return Promise_resolve(); return Promise_resolve();
}); });
...@@ -497,12 +470,11 @@ ...@@ -497,12 +470,11 @@
} }
function CreateCrossRealmTransformReadable(port) { function CreateCrossRealmTransformReadable(port) {
unsafeCopyGlobals();
let backpressurePromise = v8.createPromise(); let backpressurePromise = v8.createPromise();
let finished = false; let finished = false;
MessagePort_addEventListener(port, 'message', evt => { callFunction(binding.EventTarget_addEventListener, port, 'message', evt => {
const {type, value} = MessageEvent_data_get(evt); const {type, value} = callFunction(binding.MessageEvent_data_get, evt);
// assert(type === kChunk || type === kClose || type === kAbort || // assert(type === kChunk || type === kClose || type === kAbort ||
// type=kError); // type=kError);
switch (type) { switch (type) {
...@@ -521,7 +493,7 @@ ...@@ -521,7 +493,7 @@
} }
finished = true; finished = true;
binding.ReadableStreamDefaultControllerClose(controller); binding.ReadableStreamDefaultControllerClose(controller);
MessagePort_close(port); callFunction(binding.MessagePort_close, port);
break; break;
case kAbort: case kAbort:
...@@ -530,38 +502,42 @@ ...@@ -530,38 +502,42 @@
return; return;
} }
finished = true; finished = true;
binding.ReadableStreamDefaultControllerError(controller, binding.ReadableStreamDefaultControllerError(
unpackReason(value)); controller, unpackReason(value));
MessagePort_close(port); callFunction(binding.MessagePort_close, port);
break; break;
} }
}); });
MessagePort_addEventListener(port, 'messageerror', () => { callFunction(
const error = new DOMException('chunk could not be cloned', binding.EventTarget_addEventListener, port, 'messageerror', () => {
'DataCloneError'); const error = new binding.DOMException('chunk could not be cloned',
MessagePort_postMessage(port, {type: kError, value: packReason(error)}); 'DataCloneError');
MessagePort_close(port); callFunction(binding.MessagePort_postMessage, port,
binding.ReadableStreamDefaultControllerError(controller, error); {type: kError, value: packReason(error)});
}); callFunction(binding.MessagePort_close, port);
binding.ReadableStreamDefaultControllerError(controller, error);
});
MessagePort_start(port); callFunction(binding.MessagePort_start, port);
const stream = binding.CreateReadableStream( const stream = binding.CreateReadableStream(
() => undefined, () => undefined,
() => { () => {
MessagePort_postMessage(port, {type: kPull, value: undefined}); callFunction(
binding.MessagePort_postMessage, port,
{type: kPull, value: undefined});
return backpressurePromise; return backpressurePromise;
}, },
reason => { reason => {
finished = true; finished = true;
MessagePort_postMessage(port, { callFunction(
type: kCancel, binding.MessagePort_postMessage, port,
value: packReason(reason) {type: kCancel, value: packReason(reason)});
}); callFunction(binding.MessagePort_close, port);
MessagePort_close(port);
return Promise_resolve(); return Promise_resolve();
}, /* highWaterMark = */ 0); },
/* highWaterMark = */ 0);
const controller = binding.getReadableStreamController(stream); const controller = binding.getReadableStreamController(stream);
return stream; return stream;
......
...@@ -132,6 +132,8 @@ ...@@ -132,6 +132,8 @@
'Failed to execute \'pipeThrough\' on \'ReadableStream\': parameter ' + 'Failed to execute \'pipeThrough\' on \'ReadableStream\': parameter ' +
'1\'s \'readable\' property is undefined.'; '1\'s \'readable\' property is undefined.';
const errCannotTransferLockedStream = 'Cannot transfer a locked stream'; const errCannotTransferLockedStream = 'Cannot transfer a locked stream';
const errCannotTransferUnsupportedContext =
'Cannot transfer from this context';
let useCounted = false; let useCounted = false;
...@@ -1140,24 +1142,17 @@ ...@@ -1140,24 +1142,17 @@
throw new TypeError(errCannotTransferLockedStream); throw new TypeError(errCannotTransferLockedStream);
} }
// TODO(ricea): Protect against changes to MessageChannel on the global if (!binding.MessageChannel) {
// object. throw new TypeError(errCannotTransferUnsupportedContext);
const mc = new MessageChannel(); }
const MessageChannel_port1_getter =
v8.uncurryThis( const mc = new binding.MessageChannel();
Object.getOwnPropertyDescriptor( const writable = CreateCrossRealmTransformWritable(
MessageChannel.prototype, 'port1').get); callFunction(binding.MessageChannel_port1_get, mc));
const MessageChannel_port2_getter =
v8.uncurryThis(
Object.getOwnPropertyDescriptor(
MessageChannel.prototype, 'port2').get);
const writable =
CreateCrossRealmTransformWritable(MessageChannel_port2_getter(mc));
// Failure behaviour here is not ideal.
const promise = const promise =
ReadableStreamPipeTo(readable, writable, false, false, false); ReadableStreamPipeTo(readable, writable, false, false, false);
markPromiseAsHandled(promise); markPromiseAsHandled(promise);
return MessageChannel_port1_getter(mc); return callFunction(binding.MessageChannel_port2_get, mc);
} }
function ReadableStreamDeserialize(port) { function ReadableStreamDeserialize(port) {
...@@ -1225,8 +1220,8 @@ ...@@ -1225,8 +1220,8 @@
// TODO(ricea): Remove this once the C++ code switches to calling // TODO(ricea): Remove this once the C++ code switches to calling
// CreateReadableStream(). // CreateReadableStream().
function createReadableStreamWithExternalController(underlyingSource, function createReadableStreamWithExternalController(
strategy) { underlyingSource, strategy) {
return new ReadableStream( return new ReadableStream(
underlyingSource, strategy, createWithExternalControllerSentinel); underlyingSource, strategy, createWithExternalControllerSentinel);
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment