From b3a35170a9f0e1a4fc9fd33e83caddaa6317a61f Mon Sep 17 00:00:00 2001 From: Bill McCloskey Date: Sat, 21 Jun 2014 11:54:36 -0700 Subject: [PATCH] Bug 990729 - Add writeToGlobalPrototype option for sandboxes (r=bholley) --- js/src/jsapi.cpp | 6 ++ js/src/jsapi.h | 14 +++ js/src/jscompartment.cpp | 1 + js/src/jscompartment.h | 4 + js/src/jsgc.cpp | 2 + js/xpconnect/src/Sandbox.cpp | 157 +++++++++++++++++++++++++++++- js/xpconnect/src/XPCJSRuntime.cpp | 1 + js/xpconnect/src/xpcprivate.h | 19 ++++ 8 files changed, 199 insertions(+), 5 deletions(-) diff --git a/js/src/jsapi.cpp b/js/src/jsapi.cpp index 13a055490b2..f696de94690 100644 --- a/js/src/jsapi.cpp +++ b/js/src/jsapi.cpp @@ -1025,6 +1025,12 @@ JS::StringOfAddonId(JSAddonId *id) return id; } +JS_PUBLIC_API(JSAddonId *) +JS::AddonIdOfObject(JSObject *obj) +{ + return obj->compartment()->addonId; +} + JS_PUBLIC_API(void) JS_SetZoneUserData(JS::Zone *zone, void *data) { diff --git a/js/src/jsapi.h b/js/src/jsapi.h index bb9725f7fb7..d442621a506 100644 --- a/js/src/jsapi.h +++ b/js/src/jsapi.h @@ -2568,6 +2568,7 @@ class JS_PUBLIC_API(CompartmentOptions) , discardSource_(false) , traceGlobal_(nullptr) , singletonsAsTemplates_(true) + , addonId_(nullptr) { zone_.spec = JS::FreshZone; } @@ -2626,6 +2627,14 @@ class JS_PUBLIC_API(CompartmentOptions) return singletonsAsTemplates_; }; + // A null add-on ID means that the compartment is not associated with an + // add-on. + JSAddonId *addonIdOrNull() const { return addonId_; } + CompartmentOptions &setAddonId(JSAddonId *id) { + addonId_ = id; + return *this; + } + CompartmentOptions &setTrace(JSTraceOp op) { traceGlobal_ = op; return *this; @@ -2650,6 +2659,8 @@ class JS_PUBLIC_API(CompartmentOptions) // templates, by making JSOP_OBJECT return a clone of the JSScript // singleton, instead of returning the value which is baked in the JSScript. bool singletonsAsTemplates_; + + JSAddonId *addonId_; }; JS_PUBLIC_API(CompartmentOptions &) @@ -4335,6 +4346,9 @@ CharsZOfAddonId(JSAddonId *id); extern JS_PUBLIC_API(JSString *) StringOfAddonId(JSAddonId *id); +extern JS_PUBLIC_API(JSAddonId *) +AddonIdOfObject(JSObject *obj); + } // namespace JS /************************************************************************/ diff --git a/js/src/jscompartment.cpp b/js/src/jscompartment.cpp index 97515875dd2..6defebc54d5 100644 --- a/js/src/jscompartment.cpp +++ b/js/src/jscompartment.cpp @@ -44,6 +44,7 @@ JSCompartment::JSCompartment(Zone *zone, const JS::CompartmentOptions &options = isSystem(false), isSelfHosting(false), marked(true), + addonId(options.addonIdOrNull()), #ifdef DEBUG firedOnNewGlobalObject(false), #endif diff --git a/js/src/jscompartment.h b/js/src/jscompartment.h index 6a296540c9b..32617e980fc 100644 --- a/js/src/jscompartment.h +++ b/js/src/jscompartment.h @@ -132,6 +132,10 @@ struct JSCompartment bool isSelfHosting; bool marked; + // A null add-on ID means that the compartment is not associated with an + // add-on. + JSAddonId *addonId; + #ifdef DEBUG bool firedOnNewGlobalObject; #endif diff --git a/js/src/jsgc.cpp b/js/src/jsgc.cpp index 4e26e47ad39..63b5a1f1a4a 100644 --- a/js/src/jsgc.cpp +++ b/js/src/jsgc.cpp @@ -5371,6 +5371,8 @@ gc::MergeCompartments(JSCompartment *source, JSCompartment *target) // also implies that the compartment is not visible to the debugger. JS_ASSERT(source->options_.mergeable()); + JS_ASSERT(source->addonId == target->addonId); + JSRuntime *rt = source->runtimeFromMainThread(); AutoPrepareForTracing prepare(rt, SkipAtoms); diff --git a/js/xpconnect/src/Sandbox.cpp b/js/xpconnect/src/Sandbox.cpp index 2fac0dc650b..fe2236083d1 100644 --- a/js/xpconnect/src/Sandbox.cpp +++ b/js/xpconnect/src/Sandbox.cpp @@ -676,6 +676,91 @@ sandbox_convert(JSContext *cx, HandleObject obj, JSType type, MutableHandleValue return JS_ConvertStub(cx, obj, type, vp); } +static bool +writeToProto_setProperty(JSContext *cx, JS::HandleObject obj, JS::HandleId id, + bool strict, JS::MutableHandleValue vp) +{ + RootedObject proto(cx); + if (!JS_GetPrototype(cx, obj, &proto)) + return false; + + return JS_SetPropertyById(cx, proto, id, vp); +} + +static bool +writeToProto_getProperty(JSContext *cx, JS::HandleObject obj, JS::HandleId id, + JS::MutableHandleValue vp) +{ + RootedObject proto(cx); + if (!JS_GetPrototype(cx, obj, &proto)) + return false; + + return JS_GetPropertyById(cx, proto, id, vp); +} + +struct AutoSkipPropertyMirroring +{ + AutoSkipPropertyMirroring(CompartmentPrivate *priv) : priv(priv) { + MOZ_ASSERT(!priv->skipWriteToGlobalPrototype); + priv->skipWriteToGlobalPrototype = true; + } + ~AutoSkipPropertyMirroring() { + MOZ_ASSERT(priv->skipWriteToGlobalPrototype); + priv->skipWriteToGlobalPrototype = false; + } + + private: + CompartmentPrivate *priv; +}; + +// This hook handles the case when writeToGlobalPrototype is set on the +// sandbox. This flag asks that any properties defined on the sandbox global +// also be defined on the sandbox global's prototype. Whenever one of these +// properties is changed (on either side), the change should be reflected on +// both sides. We use this functionality to create sandboxes that are +// essentially "sub-globals" of another global. This is useful for running +// add-ons in a separate compartment while still giving them access to the +// chrome window. +static bool +sandbox_addProperty(JSContext *cx, HandleObject obj, HandleId id, MutableHandleValue vp) +{ + CompartmentPrivate *priv = GetCompartmentPrivate(obj); + MOZ_ASSERT(priv->writeToGlobalPrototype); + + // Whenever JS_EnumerateStandardClasses is called (by sandbox_enumerate for + // example), it defines the "undefined" property, even if it's already + // defined. We don't want to do anything in that case. + if (id == XPCJSRuntime::Get()->GetStringID(XPCJSRuntime::IDX_UNDEFINED)) + return true; + + // Avoid recursively triggering sandbox_addProperty in the + // JS_DefinePropertyById call below. + if (priv->skipWriteToGlobalPrototype) + return true; + + AutoSkipPropertyMirroring askip(priv); + + RootedObject proto(cx); + if (!JS_GetPrototype(cx, obj, &proto)) + return false; + + // After bug 1015790 is fixed, we should be able to remove this unwrapping. + RootedObject unwrappedProto(cx, js::UncheckedUnwrap(proto, /* stopAtOuter = */ false)); + + if (!JS_CopyPropertyFrom(cx, id, unwrappedProto, obj)) + return false; + + Rooted pd(cx); + if (!JS_GetPropertyDescriptorById(cx, obj, id, &pd)) + return false; + unsigned attrs = pd.attributes() & ~(JSPROP_GETTER | JSPROP_SETTER); + if (!JS_DefinePropertyById(cx, obj, id, vp, attrs, + writeToProto_getProperty, writeToProto_setProperty)) + return false; + + return true; +} + #define XPCONNECT_SANDBOX_CLASS_METADATA_SLOT (XPCONNECT_GLOBAL_EXTRA_SLOT_OFFSET) static const JSClass SandboxClass = { @@ -686,6 +771,16 @@ static const JSClass SandboxClass = { nullptr, nullptr, nullptr, JS_GlobalObjectTraceHook }; +// Note to whomever comes here to remove addProperty hooks: billm has promised +// to do the work for this class. +static const JSClass SandboxWriteToProtoClass = { + "Sandbox", + XPCONNECT_GLOBAL_FLAGS_WITH_EXTRA_SLOTS(1), + sandbox_addProperty, JS_DeletePropertyStub, JS_PropertyStub, JS_StrictPropertyStub, + sandbox_enumerate, sandbox_resolve, sandbox_convert, sandbox_finalize, + nullptr, nullptr, nullptr, JS_GlobalObjectTraceHook +}; + static const JSFunctionSpec SandboxFunctions[] = { JS_FS("dump", SandboxDump, 1,0), JS_FS("debug", SandboxDebug, 1,0), @@ -696,7 +791,8 @@ static const JSFunctionSpec SandboxFunctions[] = { bool xpc::IsSandbox(JSObject *obj) { - return GetObjectJSClass(obj) == &SandboxClass; + const JSClass *clasp = GetObjectJSClass(obj); + return clasp == &SandboxClass || clasp == &SandboxWriteToProtoClass; } /***************************************************************************/ @@ -748,7 +844,7 @@ xpc::SandboxCallableProxyHandler::call(JSContext *cx, JS::Handle prox // The parent of the sandboxProxy is the sandbox global, and the // target object is the original proto. RootedObject sandboxGlobal(cx, JS_GetParent(sandboxProxy)); - MOZ_ASSERT(js::GetObjectJSClass(sandboxGlobal) == &SandboxClass); + MOZ_ASSERT(IsSandbox(sandboxGlobal)); // If our this object is the sandbox global, we call with this set to the // original proto instead. @@ -1081,11 +1177,32 @@ xpc::CreateSandboxObject(JSContext *cx, MutableHandleValue vp, nsISupports *prin .setDiscardSource(options.discardSource) .setTrace(TraceXPCGlobal); - RootedObject sandbox(cx, xpc::CreateGlobalObject(cx, &SandboxClass, + // Try to figure out any addon this sandbox should be associated with. + // The addon could have been passed in directly, as part of the metadata, + // or by being constructed from an addon's code. + JSAddonId *addonId = nullptr; + if (options.addonId) { + addonId = JS::NewAddonId(cx, options.addonId); + NS_ENSURE_TRUE(addonId, NS_ERROR_FAILURE); + } else if (JSObject *obj = JS::CurrentGlobalOrNull(cx)) { + if (JSAddonId *id = JS::AddonIdOfObject(obj)) + addonId = id; + } + + compartmentOptions.setAddonId(addonId); + + const JSClass *clasp = options.writeToGlobalPrototype + ? &SandboxWriteToProtoClass + : &SandboxClass; + + RootedObject sandbox(cx, xpc::CreateGlobalObject(cx, clasp, principal, compartmentOptions)); if (!sandbox) return NS_ERROR_FAILURE; + xpc::GetCompartmentPrivate(sandbox)->writeToGlobalPrototype = + options.writeToGlobalPrototype; + // Set up the wantXrays flag, which indicates whether xrays are desired even // for same-origin access. // @@ -1138,6 +1255,9 @@ xpc::CreateSandboxObject(JSContext *cx, MutableHandleValue vp, nsISupports *prin // Pass on ownership of sbp to |sandbox|. JS_SetPrivate(sandbox, sbp.forget().take()); + // Don't try to mirror the properties that are set below. + AutoSkipPropertyMirroring askip(GetCompartmentPrivate(sandbox)); + bool allowComponents = nsContentUtils::IsSystemPrincipal(principal) || nsContentUtils::IsExpandedPrincipal(principal); if (options.wantComponents && allowComponents && @@ -1160,8 +1280,11 @@ xpc::CreateSandboxObject(JSContext *cx, MutableHandleValue vp, nsISupports *prin if (!options.globalProperties.Define(cx, sandbox)) return NS_ERROR_XPC_UNEXPECTED; - } + // Resolve standard classes eagerly to avoid triggering mirroring hooks for them. + if (options.writeToGlobalPrototype && !JS_EnumerateStandardClasses(cx, sandbox)) + return NS_ERROR_XPC_UNEXPECTED; + } // We have this crazy behavior where wantXrays=false also implies that the // returned sandbox is implicitly waived. We've stopped advertising it, but @@ -1403,6 +1526,28 @@ OptionsBase::ParseObject(const char *name, MutableHandleObject prop) return true; } +/* + * Helper that tries to get an object property from the options object. + */ +bool +OptionsBase::ParseJSString(const char *name, MutableHandleString prop) +{ + RootedValue value(mCx); + bool found; + bool ok = ParseValue(name, &value, &found); + NS_ENSURE_TRUE(ok, false); + + if (!found) + return true; + + if (!value.isString()) { + JS_ReportError(mCx, "Expected a string value for property %s", name); + return false; + } + prop.set(value.toString()); + return true; +} + /* * Helper that tries to get a string property from the options object. */ @@ -1511,6 +1656,8 @@ SandboxOptions::Parse() ParseObject("sameZoneAs", &sameZoneAs) && ParseBoolean("invisibleToDebugger", &invisibleToDebugger) && ParseBoolean("discardSource", &discardSource) && + ParseJSString("addonId", &addonId) && + ParseBoolean("writeToGlobalPrototype", &writeToGlobalPrototype) && ParseGlobalProperties() && ParseValue("metadata", &metadata); } @@ -1673,7 +1820,7 @@ xpc::EvalInSandbox(JSContext *cx, HandleObject sandboxArg, const nsAString& sour bool waiveXray = xpc::WrapperFactory::HasWaiveXrayFlag(sandboxArg); RootedObject sandbox(cx, js::CheckedUnwrap(sandboxArg)); - if (!sandbox || js::GetObjectJSClass(sandbox) != &SandboxClass) { + if (!sandbox || !IsSandbox(sandbox)) { return NS_ERROR_INVALID_ARG; } diff --git a/js/xpconnect/src/XPCJSRuntime.cpp b/js/xpconnect/src/XPCJSRuntime.cpp index a4cf0f039ae..8ffff4ce693 100644 --- a/js/xpconnect/src/XPCJSRuntime.cpp +++ b/js/xpconnect/src/XPCJSRuntime.cpp @@ -84,6 +84,7 @@ const char* const XPCJSRuntime::mStrings[] = { "realFrameElement", // IDX_REALFRAMEELEMENT "length", // IDX_LENGTH "name", // IDX_NAME + "undefined", // IDX_UNDEFINED }; /***************************************************************************/ diff --git a/js/xpconnect/src/xpcprivate.h b/js/xpconnect/src/xpcprivate.h index 53dff4aa6cd..f31a2c8abd4 100644 --- a/js/xpconnect/src/xpcprivate.h +++ b/js/xpconnect/src/xpcprivate.h @@ -482,6 +482,7 @@ public: IDX_REALFRAMEELEMENT , IDX_LENGTH , IDX_NAME , + IDX_UNDEFINED , IDX_TOTAL_COUNT // just a count of the above }; @@ -3321,6 +3322,7 @@ protected: bool ParseValue(const char *name, JS::MutableHandleValue prop, bool *found = nullptr); bool ParseBoolean(const char *name, bool *prop); bool ParseObject(const char *name, JS::MutableHandleObject prop); + bool ParseJSString(const char *name, JS::MutableHandleString prop); bool ParseString(const char *name, nsCString &prop); bool ParseString(const char *name, nsString &prop); bool ParseId(const char* name, JS::MutableHandleId id); @@ -3338,6 +3340,8 @@ public: , wantComponents(true) , wantExportHelpers(false) , proto(cx) + , addonId(cx) + , writeToGlobalPrototype(false) , sameZoneAs(cx) , invisibleToDebugger(false) , discardSource(false) @@ -3352,6 +3356,8 @@ public: bool wantExportHelpers; JS::RootedObject proto; nsCString sandboxName; + JS::RootedString addonId; + bool writeToGlobalPrototype; JS::RootedObject sameZoneAs; bool invisibleToDebugger; bool discardSource; @@ -3476,6 +3482,8 @@ public: CompartmentPrivate(JSCompartment *c) : wantXrays(false) + , writeToGlobalPrototype(false) + , skipWriteToGlobalPrototype(false) , universalXPConnectEnabled(false) , adoptedNode(false) , donatedNode(false) @@ -3489,6 +3497,17 @@ public: bool wantXrays; + // This flag is intended for a very specific use, internal to Gecko. It may + // go away or change behavior at any time. It should not be added to any + // documentation and it should not be used without consulting the XPConnect + // module owner. + bool writeToGlobalPrototype; + + // When writeToGlobalPrototype is true, we use this flag to temporarily + // disable the writeToGlobalPrototype behavior (when resolving standard + // classes, for example). + bool skipWriteToGlobalPrototype; + // This is only ever set during mochitest runs when enablePrivilege is called. // It's intended as a temporary stopgap measure until we can finish ripping out // enablePrivilege. Once set, this value is never unset (i.e., it doesn't follow