Bug 990729 - Add writeToGlobalPrototype option for sandboxes (r=bholley)

This commit is contained in:
Bill McCloskey 2014-06-21 11:54:36 -07:00
parent 014dd71102
commit b3a35170a9
8 changed files with 199 additions and 5 deletions

View File

@ -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)
{

View File

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

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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<JSPropertyDescriptor> 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<JSObject*> 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;
}

View File

@ -84,6 +84,7 @@ const char* const XPCJSRuntime::mStrings[] = {
"realFrameElement", // IDX_REALFRAMEELEMENT
"length", // IDX_LENGTH
"name", // IDX_NAME
"undefined", // IDX_UNDEFINED
};
/***************************************************************************/

View File

@ -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