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 */