Skip to content

Commit 2c5b9ce

Browse files
mystorpchevrel@mozilla.com
authored andcommitted
Bug 2017797 - Part 3: Preserve legacy structuredClone behaviour for WebExtensions, a=pascalc
This change attempts to preserve the behaviour from before bug 2013389 for WebExtensions, putting the janky type-dependant behaviour behind a pref. This is done by making the webextension content script globals shadow 'structuredClone' with an alternative implementation based in the content script's global. This alternative implementation will, when being called from a webextension content script, only change the global of the cloned object when called on one of the set of DOM objects which previously were impacted by the bug. Note that previously 'structuredClone' would do this odd behaviour even for a nested object, but this is very unlikely to happen intentionally (and would be much more difficult to implement), so that behaviour has not been preserved. Original Revision: https://phabricator.services.mozilla.com/D284524 Differential Revision: https://phabricator.services.mozilla.com/D285611
1 parent 46775a0 commit 2c5b9ce

File tree

6 files changed

+221
-5
lines changed

6 files changed

+221
-5
lines changed

js/xpconnect/src/Sandbox.cpp

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,31 @@ static bool SandboxCreateStorage(JSContext* cx, JS::HandleObject obj) {
379379
return JS_DefineProperty(cx, obj, "storage", wrapped, JSPROP_ENUMERATE);
380380
}
381381

382+
// Prior to bug 2013389, the following DOM objects would be structured-cloned
383+
// into the inner window's realm when `structuredClone` is called from an
384+
// extension content script. All other objects would remain within the content
385+
// script's realm. This method is used to retain this historic behaviour.
386+
//
387+
// See bug 2017797 for discussion about this behaviour.
388+
static bool LegacyShouldCloneIntoWindow(JS::Handle<JSObject*> obj) {
389+
return IS_INSTANCE_OF(Blob, obj) || IS_INSTANCE_OF(Directory, obj) ||
390+
IS_INSTANCE_OF(FileList, obj) || IS_INSTANCE_OF(FormData, obj) ||
391+
IS_INSTANCE_OF(ImageBitmap, obj) || IS_INSTANCE_OF(VideoFrame, obj) ||
392+
IS_INSTANCE_OF(EncodedVideoChunk, obj) ||
393+
IS_INSTANCE_OF(AudioData, obj) ||
394+
IS_INSTANCE_OF(EncodedAudioChunk, obj) ||
395+
#ifdef MOZ_WEBRTC
396+
IS_INSTANCE_OF(RTCEncodedVideoFrame, obj) ||
397+
IS_INSTANCE_OF(RTCEncodedAudioFrame, obj) ||
398+
IS_INSTANCE_OF(RTCDataChannel, obj) ||
399+
#endif
400+
IS_INSTANCE_OF(MessagePort, obj) ||
401+
IS_INSTANCE_OF(OffscreenCanvas, obj) ||
402+
IS_INSTANCE_OF(ReadableStream, obj) ||
403+
IS_INSTANCE_OF(WritableStream, obj) ||
404+
IS_INSTANCE_OF(TransformStream, obj);
405+
}
406+
382407
static bool SandboxStructuredClone(JSContext* cx, unsigned argc, Value* vp) {
383408
CallArgs args = CallArgsFromVp(argc, vp);
384409

@@ -387,25 +412,47 @@ static bool SandboxStructuredClone(JSContext* cx, unsigned argc, Value* vp) {
387412
}
388413

389414
RootedDictionary<dom::StructuredSerializeOptions> options(cx);
390-
BindingCallContext callCx(cx, "structuredClone");
391415
if (!options.Init(cx, args.hasDefined(1) ? args[1] : JS::NullHandleValue,
392416
"Argument 2", false)) {
393417
return false;
394418
}
395419

396-
nsIGlobalObject* global = CurrentNativeGlobal(cx);
420+
// NOTE: A spec-compliant structuredClone should determine & use the relevant
421+
// global instead of the current global.
422+
nsCOMPtr<nsIGlobalObject> global = CurrentNativeGlobal(cx);
397423
if (!global) {
398424
JS_ReportErrorASCII(cx, "structuredClone: Missing global");
399425
return false;
400426
}
401427

428+
// If this is a content script, we may want to clone into that window instead.
429+
// See the comment on LegacyShouldCloneIntoWindow for details.
430+
if (IsWebExtensionContentScriptSandbox(global->GetGlobalJSObject()) &&
431+
StaticPrefs::extensions_webextensions_legacyStructuredCloneBehavior() &&
432+
args[0].isObject()) {
433+
JS::Rooted<JSObject*> obj(cx, &args[0].toObject());
434+
if (LegacyShouldCloneIntoWindow(obj)) {
435+
RefPtr<nsGlobalWindowInner> window =
436+
SandboxWindowOrNull(global->GetGlobalJSObject(), cx);
437+
if (window) {
438+
global = window;
439+
}
440+
}
441+
}
442+
402443
JS::Rooted<JS::Value> result(cx);
403444
ErrorResult rv;
404445
nsContentUtils::StructuredClone(cx, global, args[0], options, &result, rv);
405446
if (rv.MaybeSetPendingException(cx)) {
406447
return false;
407448
}
408449

450+
// Because we specified a custom `global`, the returned value may not be in
451+
// our realm.
452+
if (!mozilla::dom::MaybeWrapValue(cx, &result)) {
453+
return false;
454+
}
455+
409456
MOZ_ASSERT_IF(result.isGCThing(),
410457
!JS::GCThingIsMarkedGray(result.toGCCellPtr()));
411458
args.rval().set(result);

modules/libpref/init/StaticPrefList.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6217,6 +6217,16 @@
62176217
#endif
62186218
mirror: always
62196219

6220+
# Prior to bug 2013389 and bug 2017797, calling structuredClone within a
6221+
# content script would clone most objects within the content script's realm,
6222+
# however a subset of DOM objects would be cloned into the underlying content
6223+
# window's realm. When enabled, this pref preserves this legacy behaviour for
6224+
# compatibility with existing content scripts.
6225+
- name: extensions.webextensions.legacyStructuredCloneBehavior
6226+
type: bool
6227+
value: true
6228+
mirror: always
6229+
62206230
# This pref governs whether we run webextensions in a separate process (true)
62216231
# or the parent/main process (false)
62226232
- name: extensions.webextensions.remote

toolkit/components/extensions/ExtensionContent.sys.mjs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,16 +1039,20 @@ export class ContentScriptContextChild extends BaseContext {
10391039
addonId: extensionPrincipal.addonId,
10401040
};
10411041

1042+
// Within a content script, we want the 'structuredClone' method to clone
1043+
// the object within the content script's global, rather than cloning it
1044+
// into the the embedding scope. (see bug 2017797)
1045+
let wantGlobalProperties = ["structuredClone"];
1046+
10421047
let isMV2 = extension.manifestVersion == 2;
1043-
let wantGlobalProperties;
10441048
let sandboxContentSecurityPolicy;
10451049
if (isMV2) {
10461050
// In MV2, fetch/XHR support cross-origin requests.
10471051
// WebSocket was also included to avoid CSP effects (bug 1676024).
1048-
wantGlobalProperties = ["XMLHttpRequest", "fetch", "WebSocket"];
1052+
wantGlobalProperties.push("XMLHttpRequest", "fetch", "WebSocket");
10491053
} else {
10501054
// In MV3, fetch/XHR have the same capabilities as the web page.
1051-
wantGlobalProperties = [];
1055+
10521056
// In MV3, the base CSP is enforced for content scripts. Overrides are
10531057
// currently not supported, but this was considered at some point, see
10541058
// https://bugzilla.mozilla.org/show_bug.cgi?id=1581611#c10

toolkit/components/extensions/ExtensionUserScriptsContent.sys.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,10 @@ class WorldCollection {
258258
wantXrays: true,
259259
isWebExtensionContentScript: true,
260260
wantExportHelpers: true,
261+
// The 'structuredClone' method in content scripts should clone within the
262+
// content script global by default, rather than cloning into the target
263+
// contentWindow.
264+
wantGlobalProperties: ["structuredClone"],
261265
originAttributes: docPrincipal.originAttributes,
262266
});
263267

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
"use strict";
2+
3+
const server = createHttpServer({ hosts: ["example.com"] });
4+
server.registerPathHandler("/test", (request, response) => {
5+
response.setStatusLine(request.httpVersion, 200, "OK");
6+
response.setHeader("Content-Type", "text/html", false);
7+
response.write("<!DOCTYPE html><html><body></body></html>");
8+
});
9+
10+
async function testStructuredClone(legacyPref) {
11+
Services.prefs.setBoolPref(
12+
"extensions.webextensions.legacyStructuredCloneBehavior",
13+
legacyPref
14+
);
15+
16+
let extension = ExtensionTestUtils.loadExtension({
17+
manifest: {
18+
content_scripts: [
19+
{
20+
matches: ["http://example.com/test"],
21+
js: ["cs.js"],
22+
},
23+
],
24+
},
25+
files: {
26+
"cs.js"() {
27+
/* globals cloneInto, structuredClone */
28+
browser.test.assertTrue(
29+
location.hash == "#true" || location.hash == "#false",
30+
"unexpected legacy pref value"
31+
);
32+
33+
function isWindow(obj, desc) {
34+
browser.test.assertTrue(
35+
obj instanceof window.Object,
36+
`${desc}: is a window.Object`
37+
);
38+
browser.test.assertFalse(
39+
obj instanceof Object,
40+
`${desc}: is not a sandbox.Object`
41+
);
42+
}
43+
function isSandbox(obj, desc) {
44+
browser.test.assertTrue(
45+
obj instanceof Object,
46+
`${desc}: is a sandbox.Object`
47+
);
48+
browser.test.assertFalse(
49+
obj instanceof window.Object,
50+
`${desc}: is not a window.Object`
51+
);
52+
}
53+
54+
let sbObj = { a: 1 };
55+
let winObj = cloneInto(sbObj, window);
56+
let winBlob = new Blob(["test"]);
57+
let sbBlob = cloneInto(winBlob, globalThis);
58+
59+
// Confirm that our initial objects are in the expected globals.
60+
// NOTE: The original blob was constructed in the Window (as the 'Blob'
61+
// constructor is provided by Window).
62+
isWindow(winObj, "winObj");
63+
isSandbox(sbObj, "sbObj");
64+
isWindow(winBlob, "winBlob");
65+
isSandbox(sbBlob, "sbBlob");
66+
67+
// structuredClone(obj) should always clone into the sandbox
68+
isSandbox(structuredClone(winObj), "structuredClone(winObj)");
69+
isSandbox(
70+
globalThis.structuredClone(winObj),
71+
"globalThis.structuredClone(winObj)"
72+
);
73+
isSandbox(structuredClone(sbObj), "structuredClone(sbObj)");
74+
isSandbox(
75+
globalThis.structuredClone(sbObj),
76+
"globalThis.structuredClone(sbObj)"
77+
);
78+
79+
// structuredClone(blob) depends on the legacy pref
80+
let blobAssertion = location.hash == "#true" ? isWindow : isSandbox;
81+
blobAssertion(structuredClone(winBlob), "structuredClone(winBlob)");
82+
blobAssertion(
83+
globalThis.structuredClone(winBlob),
84+
"globalThis.structuredClone(winBlob)"
85+
);
86+
blobAssertion(structuredClone(sbBlob), "structuredClone(sbBlob)");
87+
blobAssertion(
88+
globalThis.structuredClone(sbBlob),
89+
"globalThis.structuredClone(sbBlob)"
90+
);
91+
92+
// window.structuredClone(...) always clones into the window global
93+
isWindow(
94+
window.structuredClone(winObj),
95+
"window.structuredClone(winObj)"
96+
);
97+
isWindow(
98+
window.structuredClone(sbObj),
99+
"window.structuredClone(sbObj)"
100+
);
101+
isWindow(
102+
window.structuredClone(winBlob),
103+
"window.structuredClone(winBlob)"
104+
);
105+
isWindow(
106+
window.structuredClone(sbBlob),
107+
"window.structuredClone(sbBlob)"
108+
);
109+
110+
// Also test the behaviour of returned xrays. In non-window cases,
111+
// there should be no xrays, but when calling window.structuredClone
112+
// there are xrays, which will prevent adding function properties.
113+
structuredClone(winObj).x = function () {};
114+
globalThis.structuredClone(winObj).x = function () {};
115+
browser.test.assertThrows(
116+
() => (window.structuredClone(winObj).x = function () {}),
117+
/Not allowed to define cross-origin object as property/,
118+
"window.structuredClone(winObj) should be an xray wrapper"
119+
);
120+
121+
structuredClone(sbObj).x = function () {};
122+
globalThis.structuredClone(sbObj).x = function () {};
123+
browser.test.assertThrows(
124+
() => (window.structuredClone(sbObj).x = function () {}),
125+
/Not allowed to define cross-origin object as property/,
126+
"window.structuredClone(sbObj) should be an xray wrapper"
127+
);
128+
129+
browser.test.sendMessage("done");
130+
},
131+
},
132+
});
133+
134+
await extension.startup();
135+
let contentPage = await ExtensionTestUtils.loadContentPage(
136+
`http://example.com/test#${legacyPref}`
137+
);
138+
await extension.awaitMessage("done");
139+
await contentPage.close();
140+
await extension.unload();
141+
}
142+
143+
add_task(async function test_structuredClone_legacy() {
144+
await testStructuredClone(true);
145+
});
146+
147+
add_task(async function test_structuredClone_nonlegacy() {
148+
await testStructuredClone(false);
149+
});

toolkit/components/extensions/test/xpcshell/xpcshell-common.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,8 @@ skip-if = [
191191

192192
["test_ext_contentscript_slow_frame.js"]
193193

194+
["test_ext_contentscript_structured_clone.js"]
195+
194196
["test_ext_contentscript_teardown.js"]
195197
skip-if = [
196198
"tsan", # Bug 1683730

0 commit comments

Comments
 (0)