Bug 1727276 [wpt PR 30149] - Update wpt metadata, a=testonly
[gecko.git] / testing / specialpowers / content / WrapPrivileged.jsm
blob5c6fd4df7d2ac9d316a5076538aeddc9dbab7468
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 "use strict";
6 /**
7  * This module handles wrapping privileged objects so that they can be exposed
8  * to unprivileged contexts. It is only to be used in automated tests.
9  *
10  * Its exact semantics are also liable to change at any time, so any callers
11  * relying on undocumented behavior or subtle platform features should expect
12  * breakage. Those callers should, wherever possible, migrate to fully
13  * chrome-privileged scripts when they need to interact with privileged APIs.
14  */
16 /* globals XPCNativeWrapper */
18 Cu.crashIfNotInAutomation();
20 var EXPORTED_SYMBOLS = ["WrapPrivileged"];
22 let wrappedObjects = new WeakMap();
23 let perWindowInfo = new WeakMap();
24 let noAutoWrap = new WeakSet();
26 function isWrappable(x) {
27   if (typeof x === "object") {
28     return x !== null;
29   }
30   return typeof x === "function";
33 function isWrapper(x) {
34   try {
35     return isWrappable(x) && wrappedObjects.has(x);
36   } catch (e) {
37     // If `x` is a remote object proxy, trying to access an unexpected property
38     // on it will throw a security error, even though we're chrome privileged.
39     // However, remote proxies are not SpecialPowers wrappers, so:
40     return false;
41   }
44 function unwrapIfWrapped(x) {
45   return isWrapper(x) ? unwrapPrivileged(x) : x;
48 function wrapIfUnwrapped(x, w) {
49   return isWrapper(x) ? x : wrapPrivileged(x, w);
52 function isObjectOrArray(obj) {
53   if (Object(obj) !== obj) {
54     return false;
55   }
56   let arrayClasses = [
57     "Object",
58     "Array",
59     "Int8Array",
60     "Uint8Array",
61     "Int16Array",
62     "Uint16Array",
63     "Int32Array",
64     "Uint32Array",
65     "Float32Array",
66     "Float64Array",
67     "Uint8ClampedArray",
68   ];
69   let className = Cu.getClassName(obj, true);
70   return arrayClasses.includes(className);
73 // In general, we want Xray wrappers for content DOM objects, because waiving
74 // Xray gives us Xray waiver wrappers that clamp the principal when we cross
75 // compartment boundaries. However, there are some exceptions where we want
76 // to use a waiver:
78 // * Xray adds some gunk to toString(), which has the potential to confuse
79 //   consumers that aren't expecting Xray wrappers. Since toString() is a
80 //   non-privileged method that returns only strings, we can just waive Xray
81 //   for that case.
83 // * We implement Xrays to pure JS [[Object]] and [[Array]] instances that
84 //   filter out tricky things like callables. This is the right thing for
85 //   security in general, but tends to break tests that try to pass object
86 //   literals into SpecialPowers. So we waive [[Object]] and [[Array]]
87 //   instances before inspecting properties.
89 // * When we don't have meaningful Xray semantics, we create an Opaque
90 //   XrayWrapper for security reasons. For test code, we generally want to see
91 //   through that sort of thing.
92 function waiveXraysIfAppropriate(obj, propName) {
93   if (
94     propName == "toString" ||
95     isObjectOrArray(obj) ||
96     /Opaque/.test(Object.prototype.toString.call(obj))
97   ) {
98     return XPCNativeWrapper.unwrap(obj);
99   }
100   return obj;
103 // We can't call apply() directy on Xray-wrapped functions, so we have to be
104 // clever.
105 function doApply(fun, invocant, args) {
106   // We implement Xrays to pure JS [[Object]] instances that filter out tricky
107   // things like callables. This is the right thing for security in general,
108   // but tends to break tests that try to pass object literals into
109   // SpecialPowers. So we waive [[Object]] instances when they're passed to a
110   // SpecialPowers-wrapped callable.
111   //
112   // Note that the transitive nature of Xray waivers means that any property
113   // pulled off such an object will also be waived, and so we'll get principal
114   // clamping for Xrayed DOM objects reached from literals, so passing things
115   // like {l : xoWin.location} won't work. Hopefully the rabbit hole doesn't
116   // go that deep.
117   args = args.map(x => (isObjectOrArray(x) ? Cu.waiveXrays(x) : x));
118   return Reflect.apply(fun, invocant, args);
121 function wrapPrivileged(obj, win) {
122   // Primitives pass straight through.
123   if (!isWrappable(obj)) {
124     return obj;
125   }
127   // No double wrapping.
128   if (isWrapper(obj)) {
129     throw new Error("Trying to double-wrap object!");
130   }
132   let { windowID, proxies, handler } = perWindowInfo.get(win) || {};
133   // |windowUtils| is undefined if |win| is a non-window object
134   // such as a sandbox.
135   let currentID = win.windowGlobalChild
136     ? win.windowGlobalChild.innerWindowId
137     : 0;
138   // Values are dead objects if the inner window is changed.
139   if (windowID !== currentID) {
140     windowID = currentID;
141     proxies = new WeakMap();
142     handler = Cu.cloneInto(SpecialPowersHandler, win, {
143       cloneFunctions: true,
144     });
145     handler.wrapped = new win.WeakMap();
146     perWindowInfo.set(win, { windowID, proxies, handler });
147   }
149   if (proxies.has(obj)) {
150     return proxies.get(obj).proxy;
151   }
153   let className = Cu.getClassName(obj, true);
154   if (className === "ArrayBuffer") {
155     // Since |new Uint8Array(<proxy>)| doesn't work as expected, we have to
156     // return a real ArrayBuffer.
157     return obj instanceof win.ArrayBuffer ? obj : Cu.cloneInto(obj, win);
158   }
160   let dummy;
161   if (typeof obj === "function") {
162     dummy = Cu.exportFunction(function() {}, win);
163   } else {
164     dummy = new win.Object();
165   }
166   handler.wrapped.set(dummy, { obj });
168   let proxy = new win.Proxy(dummy, handler);
169   wrappedObjects.set(proxy, obj);
170   switch (className) {
171     case "AnonymousContent":
172       // Caching anonymous content will cause crashes (bug 1636015).
173       break;
174     case "CSS2Properties":
175     case "CSSStyleRule":
176     case "CSSStyleSheet":
177       // Caching these classes will cause memory leaks.
178       break;
179     default:
180       proxies.set(obj, { proxy });
181       break;
182   }
183   return proxy;
186 function unwrapPrivileged(x) {
187   // We don't wrap primitives, so sometimes we have a primitive where we'd
188   // expect to have a wrapper. The proxy pretends to be the type that it's
189   // emulating, so we can just as easily check isWrappable() on a proxy as
190   // we can on an unwrapped object.
191   if (!isWrappable(x)) {
192     return x;
193   }
195   // If we have a wrappable type, make sure it's wrapped.
196   if (!isWrapper(x)) {
197     throw new Error("Trying to unwrap a non-wrapped object!");
198   }
200   // unwrapped.
201   return wrappedObjects.get(x);
204 function wrapExceptions(global, fn) {
205   try {
206     return fn();
207   } catch (e) {
208     throw wrapIfUnwrapped(e, global);
209   }
212 function specialPowersHasInstance(value) {
213   // Because we return wrapped versions of this function, when it's called its
214   // wrapper will unwrap the "this" as well as the function itself.  So our
215   // "this" is the unwrapped thing we started out with.
216   return value instanceof this;
219 let SpecialPowersHandler = {
220   construct(target, args) {
221     // The arguments may or may not be wrappers. Unwrap them if necessary.
222     var unwrappedArgs = Array.from(Cu.waiveXrays(args), x =>
223       unwrapIfWrapped(Cu.unwaiveXrays(x))
224     );
226     // We want to invoke "obj" as a constructor, but using unwrappedArgs as
227     // the arguments.
228     let global = Cu.getGlobalForObject(this);
229     return wrapExceptions(global, () =>
230       wrapIfUnwrapped(
231         Reflect.construct(this.wrapped.get(target).obj, unwrappedArgs),
232         global
233       )
234     );
235   },
237   apply(target, thisValue, args) {
238     let wrappedObject = this.wrapped.get(target).obj;
239     let global = Cu.getGlobalForObject(this);
240     // The invocant and arguments may or may not be wrappers. Unwrap
241     // them if necessary.
242     var invocant = unwrapIfWrapped(thisValue);
244     return wrapExceptions(global, () => {
245       if (noAutoWrap.has(wrappedObject)) {
246         args = Array.from(Cu.waiveXrays(args), x => Cu.unwaiveXrays(x));
247         return doApply(wrappedObject, invocant, args);
248       }
250       if (wrappedObject.name == "then") {
251         args = Array.from(Cu.waiveXrays(args), x =>
252           wrapCallback(Cu.unwaiveXrays(x), global)
253         );
254       } else {
255         args = Array.from(Cu.waiveXrays(args), x =>
256           unwrapIfWrapped(Cu.unwaiveXrays(x))
257         );
258       }
260       return wrapIfUnwrapped(doApply(wrappedObject, invocant, args), global);
261     });
262   },
264   has(target, prop) {
265     return Reflect.has(this.wrapped.get(target).obj, prop);
266   },
268   get(target, prop, receiver) {
269     let global = Cu.getGlobalForObject(this);
270     return wrapExceptions(global, () => {
271       let obj = waiveXraysIfAppropriate(this.wrapped.get(target).obj, prop);
272       let val = Reflect.get(obj, prop);
273       if (val === undefined && prop == Symbol.hasInstance) {
274         // Special-case Symbol.hasInstance to pass the hasInstance check on to our
275         // target.  We only do this when the target doesn't have its own
276         // Symbol.hasInstance already.  Once we get rid of JS engine class
277         // instance hooks (bug 1448218) and always use Symbol.hasInstance, we can
278         // remove this bit (bug 1448400).
279         return wrapPrivileged(specialPowersHasInstance, global);
280       }
281       return wrapIfUnwrapped(val, global);
282     });
283   },
285   set(target, prop, val, receiver) {
286     return wrapExceptions(Cu.getGlobalForObject(this), () => {
287       let obj = waiveXraysIfAppropriate(this.wrapped.get(target).obj, prop);
288       return Reflect.set(obj, prop, unwrapIfWrapped(val));
289     });
290   },
292   delete(target, prop) {
293     return wrapExceptions(Cu.getGlobalForObject(this), () => {
294       return Reflect.deleteProperty(this.wrapped.get(target).obj, prop);
295     });
296   },
298   defineProperty(target, prop, descriptor) {
299     throw new Error(
300       "Can't call defineProperty on SpecialPowers wrapped object"
301     );
302   },
304   getOwnPropertyDescriptor(target, prop) {
305     let global = Cu.getGlobalForObject(this);
306     return wrapExceptions(global, () => {
307       let obj = waiveXraysIfAppropriate(this.wrapped.get(target).obj, prop);
308       let desc = Reflect.getOwnPropertyDescriptor(obj, prop);
310       if (desc === undefined) {
311         if (prop == Symbol.hasInstance) {
312           // Special-case Symbol.hasInstance to pass the hasInstance check on to
313           // our target.  We only do this when the target doesn't have its own
314           // Symbol.hasInstance already.  Once we get rid of JS engine class
315           // instance hooks (bug 1448218) and always use Symbol.hasInstance, we
316           // can remove this bit (bug 1448400).
317           return {
318             value: wrapPrivileged(specialPowersHasInstance, global),
319             writeable: true,
320             configurable: true,
321             enumerable: false,
322           };
323         }
325         return undefined;
326       }
328       // Transitively maintain the wrapper membrane.
329       let wrapIfExists = key => {
330         if (key in desc) {
331           desc[key] = wrapIfUnwrapped(desc[key], global);
332         }
333       };
335       wrapIfExists("value");
336       wrapIfExists("get");
337       wrapIfExists("set");
339       // A trapping proxy's properties must always be configurable, but sometimes
340       // we come across non-configurable properties. Tell a white lie.
341       desc.configurable = true;
343       return wrapIfUnwrapped(desc, global);
344     });
345   },
347   ownKeys(target) {
348     let props = [];
350     // Do the normal thing.
351     let wrappedObject = this.wrapped.get(target).obj;
352     let flt = a => !props.includes(a);
353     props = props.concat(Reflect.ownKeys(wrappedObject).filter(flt));
355     // If we've got an Xray wrapper, include the expandos as well.
356     if ("wrappedJSObject" in wrappedObject) {
357       props = props.concat(
358         Reflect.ownKeys(wrappedObject.wrappedJSObject).filter(flt)
359       );
360     }
362     return Cu.cloneInto(props, Cu.getGlobalForObject(this));
363   },
365   preventExtensions(target) {
366     throw new Error(
367       "Can't call preventExtensions on SpecialPowers wrapped object"
368     );
369   },
372 function wrapCallback(cb, win) {
373   // Do not wrap if it is already privileged.
374   if (!isWrappable(cb) || Cu.getObjectPrincipal(cb).isSystemPrincipal) {
375     return cb;
376   }
377   return function SpecialPowersCallbackWrapper() {
378     var args = Array.from(arguments, obj => wrapIfUnwrapped(obj, win));
379     let invocant = wrapIfUnwrapped(this, win);
380     return unwrapIfWrapped(cb.apply(invocant, args));
381   };
384 function wrapCallbackObject(obj, win) {
385   // Do not wrap if it is already privileged.
386   if (!isWrappable(obj) || Cu.getObjectPrincipal(obj).isSystemPrincipal) {
387     return obj;
388   }
389   obj = Cu.waiveXrays(obj);
390   var wrapper = {};
391   for (var i in obj) {
392     if (typeof obj[i] == "function") {
393       wrapper[i] = wrapCallback(Cu.unwaiveXrays(obj[i]), win);
394     } else {
395       wrapper[i] = obj[i];
396     }
397   }
398   return wrapper;
401 function disableAutoWrap(...objs) {
402   objs.forEach(x => noAutoWrap.add(x));
405 var WrapPrivileged = {
406   wrap: wrapIfUnwrapped,
407   unwrap: unwrapIfWrapped,
409   isWrapper,
411   wrapCallback,
412   wrapCallbackObject,
414   disableAutoWrap,