tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

ExportHelpers.cpp (18831B)


      1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
      2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this
      5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 #include "xpcprivate.h"
      8 #include "WrapperFactory.h"
      9 #include "AccessCheck.h"
     10 #include "jsfriendapi.h"
     11 #include "js/CallAndConstruct.h"  // JS::Call, JS::Construct, JS::IsCallable
     12 #include "js/Exception.h"
     13 #include "js/PropertyAndElement.h"  // JS_DefineProperty, JS_DefinePropertyById
     14 #include "js/Proxy.h"
     15 #include "js/Wrapper.h"
     16 #include "mozilla/ErrorResult.h"
     17 #include "mozilla/dom/BindingUtils.h"
     18 #include "mozilla/dom/BlobBinding.h"
     19 #include "mozilla/dom/BlobImpl.h"
     20 #include "mozilla/dom/File.h"
     21 #include "mozilla/dom/StructuredCloneHolder.h"
     22 #include "nsContentUtils.h"
     23 #include "nsJSUtils.h"
     24 #include "js/Object.h"  // JS::GetCompartment
     25 
     26 using namespace mozilla;
     27 using namespace mozilla::dom;
     28 using namespace JS;
     29 
     30 namespace xpc {
     31 
     32 bool IsReflector(JSObject* obj, JSContext* cx) {
     33  obj = js::CheckedUnwrapDynamic(obj, cx, /* stopAtWindowProxy = */ false);
     34  if (!obj) {
     35    return false;
     36  }
     37  return IsWrappedNativeReflector(obj) || dom::IsDOMObject(obj);
     38 }
     39 
     40 enum StackScopedCloneTags : uint32_t {
     41  SCTAG_BASE = JS_SCTAG_USER_MIN,
     42  SCTAG_REFLECTOR,
     43  SCTAG_BLOB,
     44  SCTAG_FUNCTION,
     45 };
     46 
     47 class MOZ_STACK_CLASS StackScopedCloneData : public StructuredCloneHolderBase {
     48 public:
     49  StackScopedCloneData(JSContext* aCx, StackScopedCloneOptions* aOptions)
     50      : mOptions(aOptions), mReflectors(aCx), mFunctions(aCx) {}
     51 
     52  ~StackScopedCloneData() { Clear(); }
     53 
     54  JSObject* CustomReadHandler(JSContext* aCx, JSStructuredCloneReader* aReader,
     55                              const JS::CloneDataPolicy& aCloneDataPolicy,
     56                              uint32_t aTag, uint32_t aData) override {
     57    if (aTag == SCTAG_REFLECTOR) {
     58      MOZ_ASSERT(!aData);
     59 
     60      size_t idx;
     61      if (!JS_ReadBytes(aReader, &idx, sizeof(size_t))) {
     62        return nullptr;
     63      }
     64 
     65      RootedObject reflector(aCx, mReflectors[idx]);
     66      MOZ_ASSERT(reflector, "No object pointer?");
     67      MOZ_ASSERT(IsReflector(reflector, aCx),
     68                 "Object pointer must be a reflector!");
     69 
     70      if (!JS_WrapObject(aCx, &reflector)) {
     71        return nullptr;
     72      }
     73 
     74      return reflector;
     75    }
     76 
     77    if (aTag == SCTAG_FUNCTION) {
     78      MOZ_ASSERT(aData < mFunctions.length());
     79 
     80      RootedValue functionValue(aCx);
     81      RootedObject obj(aCx, mFunctions[aData]);
     82 
     83      if (!JS_WrapObject(aCx, &obj)) {
     84        return nullptr;
     85      }
     86 
     87      FunctionForwarderOptions forwarderOptions;
     88      if (!xpc::NewFunctionForwarder(aCx, JS::VoidHandlePropertyKey, obj,
     89                                     forwarderOptions, &functionValue)) {
     90        return nullptr;
     91      }
     92 
     93      return &functionValue.toObject();
     94    }
     95 
     96    if (aTag == SCTAG_BLOB) {
     97      MOZ_ASSERT(!aData);
     98 
     99      size_t idx;
    100      if (!JS_ReadBytes(aReader, &idx, sizeof(size_t))) {
    101        return nullptr;
    102      }
    103 
    104      nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx);
    105      MOZ_ASSERT(global);
    106 
    107      // RefPtr<File> needs to go out of scope before toObjectOrNull() is called
    108      // because otherwise the static analysis thinks it can gc the JSObject via
    109      // the stack.
    110      JS::Rooted<JS::Value> val(aCx);
    111      {
    112        RefPtr<Blob> blob = Blob::Create(global, mBlobImpls[idx]);
    113        if (NS_WARN_IF(!blob)) {
    114          return nullptr;
    115        }
    116 
    117        if (!ToJSValue(aCx, blob, &val)) {
    118          return nullptr;
    119        }
    120      }
    121 
    122      return val.toObjectOrNull();
    123    }
    124 
    125    MOZ_ASSERT_UNREACHABLE("Encountered garbage in the clone stream!");
    126    return nullptr;
    127  }
    128 
    129  bool CustomWriteHandler(JSContext* aCx, JSStructuredCloneWriter* aWriter,
    130                          JS::Handle<JSObject*> aObj,
    131                          bool* aSameProcessScopeRequired) override {
    132    {
    133      JS::Rooted<JSObject*> obj(aCx, aObj);
    134      Blob* blob = nullptr;
    135      if (NS_SUCCEEDED(UNWRAP_OBJECT(Blob, &obj, blob))) {
    136        BlobImpl* blobImpl = blob->Impl();
    137        MOZ_ASSERT(blobImpl);
    138 
    139        // XXX(Bug 1631371) Check if this should use a fallible operation as it
    140        // pretended earlier.
    141        mBlobImpls.AppendElement(blobImpl);
    142 
    143        size_t idx = mBlobImpls.Length() - 1;
    144        return JS_WriteUint32Pair(aWriter, SCTAG_BLOB, 0) &&
    145               JS_WriteBytes(aWriter, &idx, sizeof(size_t));
    146      }
    147    }
    148 
    149    if (mOptions->wrapReflectors && IsReflector(aObj, aCx)) {
    150      if (!mReflectors.append(aObj)) {
    151        return false;
    152      }
    153 
    154      size_t idx = mReflectors.length() - 1;
    155      if (!JS_WriteUint32Pair(aWriter, SCTAG_REFLECTOR, 0)) {
    156        return false;
    157      }
    158      if (!JS_WriteBytes(aWriter, &idx, sizeof(size_t))) {
    159        return false;
    160      }
    161      return true;
    162    }
    163 
    164    if (JS::IsCallable(aObj)) {
    165      if (mOptions->cloneFunctions) {
    166        if (!mFunctions.append(aObj)) {
    167          return false;
    168        }
    169        return JS_WriteUint32Pair(aWriter, SCTAG_FUNCTION,
    170                                  mFunctions.length() - 1);
    171      } else {
    172        JS_ReportErrorASCII(
    173            aCx, "Permission denied to pass a Function via structured clone");
    174        return false;
    175      }
    176    }
    177 
    178    JS_ReportErrorASCII(aCx,
    179                        "Encountered unsupported value type writing "
    180                        "stack-scoped structured clone");
    181    return false;
    182  }
    183 
    184  StackScopedCloneOptions* mOptions;
    185  RootedObjectVector mReflectors;
    186  RootedObjectVector mFunctions;
    187  nsTArray<RefPtr<BlobImpl>> mBlobImpls;
    188 };
    189 
    190 /*
    191 * General-purpose structured-cloning utility for cases where the structured
    192 * clone buffer is only used in stack-scope (that is to say, the buffer does
    193 * not escape from this function). The stack-scoping allows us to pass
    194 * references to various JSObjects directly in certain situations without
    195 * worrying about lifetime issues.
    196 *
    197 * This function assumes that |cx| is already entered the compartment we want
    198 * to clone to, and that |val| may not be same-compartment with cx. When the
    199 * function returns, |val| is set to the result of the clone.
    200 */
    201 bool StackScopedClone(JSContext* cx, StackScopedCloneOptions& options,
    202                      HandleObject sourceScope, MutableHandleValue val) {
    203  StackScopedCloneData data(cx, &options);
    204  {
    205    // For parsing val we have to enter (a realm in) its compartment.
    206    JSAutoRealm ar(cx, sourceScope);
    207    if (!data.Write(cx, val)) {
    208      return false;
    209    }
    210  }
    211 
    212  // Now recreate the clones in the target realm.
    213  if (!data.Read(cx, val)) {
    214    return false;
    215  }
    216 
    217  // Deep-freeze if requested.
    218  if (options.deepFreeze && val.isObject()) {
    219    RootedObject obj(cx, &val.toObject());
    220    if (!JS_DeepFreezeObject(cx, obj)) {
    221      return false;
    222    }
    223  }
    224 
    225  return true;
    226 }
    227 
    228 // Note - This function mirrors the logic of CheckPassToChrome in
    229 // ChromeObjectWrapper.cpp.
    230 static bool CheckSameOriginArg(JSContext* cx, FunctionForwarderOptions& options,
    231                               HandleValue v) {
    232  // Consumers can explicitly opt out of this security check. This is used in
    233  // the web console to allow the utility functions to accept cross-origin
    234  // Windows.
    235  if (options.allowCrossOriginArguments) {
    236    return true;
    237  }
    238 
    239  // Primitives are fine.
    240  if (!v.isObject()) {
    241    return true;
    242  }
    243  RootedObject obj(cx, &v.toObject());
    244  MOZ_ASSERT(JS::GetCompartment(obj) != js::GetContextCompartment(cx),
    245             "This should be invoked after entering the compartment but before "
    246             "wrapping the values");
    247 
    248  // Non-wrappers are fine.
    249  if (!js::IsWrapper(obj)) {
    250    return true;
    251  }
    252 
    253  // Wrappers leading back to the scope of the exported function are fine.
    254  if (JS::GetCompartment(js::UncheckedUnwrap(obj)) ==
    255      js::GetContextCompartment(cx)) {
    256    return true;
    257  }
    258 
    259  // Same-origin wrappers are fine.
    260  if (AccessCheck::wrapperSubsumes(obj)) {
    261    return true;
    262  }
    263 
    264  // Badness.
    265  JS_ReportErrorASCII(cx,
    266                      "Permission denied to pass object to exported function");
    267  return false;
    268 }
    269 
    270 // Sanitize the exception on cx (which comes from calling unwrappedFun), if the
    271 // current Realm of cx shouldn't have access to it.  unwrappedFun is generally
    272 // _not_ in the current Realm of cx here.
    273 static void MaybeSanitizeException(JSContext* cx,
    274                                   JS::Handle<JSObject*> unwrappedFun) {
    275  // Ensure that we are not propagating more-privileged exceptions
    276  // to less-privileged code.
    277  nsIPrincipal* callerPrincipal = nsContentUtils::SubjectPrincipal(cx);
    278 
    279  // No need to sanitize uncatchable exceptions, just return.
    280  if (!JS_IsExceptionPending(cx)) {
    281    return;
    282  }
    283 
    284  // Re-enter the unwrappedFun Realm to do get the current exception, so we
    285  // don't end up unnecessarily wrapping exceptions.
    286  {  // Scope for JSAutoRealm
    287    JSAutoRealm ar(cx, unwrappedFun);
    288 
    289    JS::ExceptionStack exnStack(cx);
    290 
    291    // If JS::GetPendingExceptionStack returns false, we somehow failed to wrap
    292    // the exception into our compartment. It seems fine to treat this as an
    293    // uncatchable exception by returning without setting any exception on the
    294    // JS context.
    295    if (!JS::GetPendingExceptionStack(cx, &exnStack)) {
    296      JS_ClearPendingException(cx);
    297      return;
    298    }
    299 
    300    // Let through non-objects as-is, because some APIs rely on
    301    // that and accidental exceptions are never non-objects.
    302    if (!exnStack.exception().isObject() ||
    303        callerPrincipal->Subsumes(nsContentUtils::ObjectPrincipal(
    304            js::UncheckedUnwrap(&exnStack.exception().toObject())))) {
    305      // Just leave exn as-is.
    306      return;
    307    }
    308 
    309    // Whoever we are throwing the exception to should not have access to
    310    // the exception.  Sanitize it. First clear the existing exception.
    311    JS_ClearPendingException(cx);
    312    {  // Scope for AutoJSAPI
    313      AutoJSAPI jsapi;
    314      if (jsapi.Init(unwrappedFun)) {
    315        JS::SetPendingExceptionStack(cx, exnStack);
    316      }
    317      // If Init() fails, we can't report the exception, but oh, well.
    318 
    319      // Now just let the AutoJSAPI go out of scope and it will report the
    320      // exception in its destructor.
    321    }
    322  }
    323 
    324  // Now back in our original Realm again, throw a sanitized exception.
    325  ErrorResult rv;
    326  rv.ThrowInvalidStateError("An exception was thrown");
    327  // Can we provide a better context here?
    328  (void)rv.MaybeSetPendingException(cx);
    329 }
    330 
    331 static bool FunctionForwarder(JSContext* cx, unsigned argc, Value* vp) {
    332  CallArgs args = CallArgsFromVp(argc, vp);
    333 
    334  // Grab the options from the reserved slot.
    335  RootedObject optionsObj(
    336      cx, &js::GetFunctionNativeReserved(&args.callee(), 1).toObject());
    337  FunctionForwarderOptions options(cx, optionsObj);
    338  if (!options.Parse()) {
    339    return false;
    340  }
    341 
    342  // Grab and unwrap the underlying callable.
    343  RootedValue v(cx, js::GetFunctionNativeReserved(&args.callee(), 0));
    344  RootedObject unwrappedFun(cx, js::UncheckedUnwrap(&v.toObject()));
    345 
    346  RootedValue thisVal(cx, NullValue());
    347  if (!args.isConstructing()) {
    348    RootedObject thisObject(cx);
    349    if (!args.computeThis(cx, &thisObject)) {
    350      return false;
    351    }
    352    thisVal.setObject(*thisObject);
    353  }
    354 
    355  bool ok = true;
    356  {
    357    // We manually implement the contents of CrossCompartmentWrapper::call
    358    // here, because certain function wrappers (notably content->nsEP) are
    359    // not callable.
    360    JSAutoRealm ar(cx, unwrappedFun);
    361    bool crossCompartment =
    362        JS::GetCompartment(unwrappedFun) != JS::GetCompartment(&args.callee());
    363    if (crossCompartment) {
    364      if (!CheckSameOriginArg(cx, options, thisVal) ||
    365          !JS_WrapValue(cx, &thisVal)) {
    366        return false;
    367      }
    368 
    369      for (size_t n = 0; n < args.length(); ++n) {
    370        if (!CheckSameOriginArg(cx, options, args[n]) ||
    371            !JS_WrapValue(cx, args[n])) {
    372          return false;
    373        }
    374      }
    375    }
    376 
    377    RootedValue fval(cx, ObjectValue(*unwrappedFun));
    378    if (args.isConstructing()) {
    379      RootedObject obj(cx);
    380      ok = JS::Construct(cx, fval, args, &obj);
    381      if (ok) {
    382        args.rval().setObject(*obj);
    383      }
    384    } else {
    385      ok = JS::Call(cx, thisVal, fval, args, args.rval());
    386    }
    387  }
    388 
    389  // Now that we are back in our original Realm, we can check whether to
    390  // sanitize the exception.
    391  if (!ok) {
    392    MaybeSanitizeException(cx, unwrappedFun);
    393    return false;
    394  }
    395 
    396  // Rewrap the return value into our compartment.
    397  return JS_WrapValue(cx, args.rval());
    398 }
    399 
    400 bool NewFunctionForwarder(JSContext* cx, HandleId idArg, HandleObject callable,
    401                          FunctionForwarderOptions& options,
    402                          MutableHandleValue vp) {
    403  RootedId id(cx, idArg);
    404  if (id.isVoid()) {
    405    id = GetJSIDByIndex(cx, XPCJSContext::IDX_EMPTYSTRING);
    406  }
    407 
    408  // If our callable is a (possibly wrapped) function, we can give
    409  // the exported thing the right number of args.
    410  unsigned nargs = 0;
    411  RootedObject unwrapped(cx, js::UncheckedUnwrap(callable));
    412  if (unwrapped) {
    413    if (JSFunction* fun = JS_GetObjectFunction(unwrapped)) {
    414      nargs = JS_GetFunctionArity(fun);
    415    }
    416  }
    417 
    418  // We have no way of knowing whether the underlying function wants to be a
    419  // constructor or not, so we just mark all forwarders as constructors, and
    420  // let the underlying function throw for construct calls if it wants.
    421  JSFunction* fun = js::NewFunctionByIdWithReserved(
    422      cx, FunctionForwarder, nargs, JSFUN_CONSTRUCTOR, id);
    423  if (!fun) {
    424    return false;
    425  }
    426 
    427  // Stash the callable in slot 0.
    428  AssertSameCompartment(cx, callable);
    429  RootedObject funobj(cx, JS_GetFunctionObject(fun));
    430  js::SetFunctionNativeReserved(funobj, 0, ObjectValue(*callable));
    431 
    432  // Stash the options in slot 1.
    433  RootedObject optionsObj(cx, options.ToJSObject(cx));
    434  if (!optionsObj) {
    435    return false;
    436  }
    437  js::SetFunctionNativeReserved(funobj, 1, ObjectValue(*optionsObj));
    438 
    439  vp.setObject(*funobj);
    440  return true;
    441 }
    442 
    443 bool ExportFunction(JSContext* cx, HandleValue vfunction, HandleValue vscope,
    444                    HandleValue voptions, MutableHandleValue rval) {
    445  bool hasOptions = !voptions.isUndefined();
    446  if (!vscope.isObject() || !vfunction.isObject() ||
    447      (hasOptions && !voptions.isObject())) {
    448    JS_ReportErrorASCII(cx, "Invalid argument");
    449    return false;
    450  }
    451 
    452  RootedObject funObj(cx, &vfunction.toObject());
    453  RootedObject targetScope(cx, &vscope.toObject());
    454  ExportFunctionOptions options(cx,
    455                                hasOptions ? &voptions.toObject() : nullptr);
    456  if (hasOptions && !options.Parse()) {
    457    return false;
    458  }
    459 
    460  // Restrictions:
    461  // * We must subsume the scope we are exporting to.
    462  // * We must subsume the function being exported, because the function
    463  //   forwarder manually circumvents security wrapper CALL restrictions.
    464  targetScope = js::CheckedUnwrapDynamic(targetScope, cx);
    465  // For the function we can just CheckedUnwrapStatic, because if it's
    466  // not callable we're going to fail out anyway.
    467  funObj = js::CheckedUnwrapStatic(funObj);
    468  if (!targetScope || !funObj) {
    469    JS_ReportErrorASCII(cx, "Permission denied to export function into scope");
    470    return false;
    471  }
    472 
    473  if (js::IsScriptedProxy(targetScope)) {
    474    JS_ReportErrorASCII(cx, "Defining property on proxy object is not allowed");
    475    return false;
    476  }
    477 
    478  {
    479    // We need to operate in the target scope from here on, let's enter
    480    // its realm.
    481    JSAutoRealm ar(cx, targetScope);
    482 
    483    // Unwrapping to see if we have a callable.
    484    funObj = UncheckedUnwrap(funObj);
    485    if (!JS::IsCallable(funObj)) {
    486      JS_ReportErrorASCII(cx, "First argument must be a function");
    487      return false;
    488    }
    489 
    490    RootedId id(cx, options.defineAs);
    491    if (id.isVoid()) {
    492      // If there wasn't any function name specified, copy the name from the
    493      // function being imported.  But be careful in case the callable we have
    494      // is not actually a JSFunction.
    495      RootedString funName(cx);
    496      JS::Rooted<JSFunction*> fun(cx, JS_GetObjectFunction(funObj));
    497      if (fun) {
    498        if (!JS_GetFunctionId(cx, fun, &funName)) {
    499          return false;
    500        }
    501      }
    502      if (!funName) {
    503        funName = JS_AtomizeAndPinString(cx, "");
    504      }
    505      JS_MarkCrossZoneIdValue(cx, StringValue(funName));
    506 
    507      if (!JS_StringToId(cx, funName, &id)) {
    508        return false;
    509      }
    510    } else {
    511      JS_MarkCrossZoneId(cx, id);
    512    }
    513    MOZ_ASSERT(id.isString());
    514 
    515    // The function forwarder will live in the target compartment. Since
    516    // this function will be referenced from its private slot, to avoid a
    517    // GC hazard, we must wrap it to the same compartment.
    518    if (!JS_WrapObject(cx, &funObj)) {
    519      return false;
    520    }
    521 
    522    // And now, let's create the forwarder function in the target compartment
    523    // for the function the be exported.
    524    FunctionForwarderOptions forwarderOptions;
    525    forwarderOptions.allowCrossOriginArguments =
    526        options.allowCrossOriginArguments;
    527    if (!NewFunctionForwarder(cx, id, funObj, forwarderOptions, rval)) {
    528      JS_ReportErrorASCII(cx, "Exporting function failed");
    529      return false;
    530    }
    531 
    532    // We have the forwarder function in the target compartment. If
    533    // defineAs was set, we also need to define it as a property on
    534    // the target.
    535    if (!options.defineAs.isVoid()) {
    536      if (!JS_DefinePropertyById(cx, targetScope, id, rval, JSPROP_ENUMERATE)) {
    537        return false;
    538      }
    539    }
    540  }
    541 
    542  // Finally we have to re-wrap the exported function back to the caller
    543  // compartment.
    544  if (!JS_WrapValue(cx, rval)) {
    545    return false;
    546  }
    547 
    548  return true;
    549 }
    550 
    551 bool CreateObjectIn(JSContext* cx, HandleValue vobj,
    552                    CreateObjectInOptions& options, MutableHandleValue rval) {
    553  if (!vobj.isObject()) {
    554    JS_ReportErrorASCII(cx, "Expected an object as the target scope");
    555    return false;
    556  }
    557 
    558  // cx represents the caller Realm.
    559  RootedObject scope(cx, js::CheckedUnwrapDynamic(&vobj.toObject(), cx));
    560  if (!scope) {
    561    JS_ReportErrorASCII(
    562        cx, "Permission denied to create object in the target scope");
    563    return false;
    564  }
    565 
    566  bool define = !options.defineAs.isVoid();
    567 
    568  if (define && js::IsScriptedProxy(scope)) {
    569    JS_ReportErrorASCII(cx, "Defining property on proxy object is not allowed");
    570    return false;
    571  }
    572 
    573  RootedObject obj(cx);
    574  {
    575    JSAutoRealm ar(cx, scope);
    576    JS_MarkCrossZoneId(cx, options.defineAs);
    577 
    578    obj = JS_NewPlainObject(cx);
    579    if (!obj) {
    580      return false;
    581    }
    582 
    583    if (define) {
    584      if (!JS_DefinePropertyById(cx, scope, options.defineAs, obj,
    585                                 JSPROP_ENUMERATE))
    586        return false;
    587    }
    588  }
    589 
    590  rval.setObject(*obj);
    591  if (!WrapperFactory::WaiveXrayAndWrap(cx, rval)) {
    592    return false;
    593  }
    594 
    595  return true;
    596 }
    597 
    598 } /* namespace xpc */