diff --git a/js/src/js.c b/js/src/js.c index 4a428499150a..fbecb47f832d 100644 --- a/js/src/js.c +++ b/js/src/js.c @@ -1415,6 +1415,32 @@ Clone(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) return JS_TRUE; } +static JSBool +Seal(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) +{ + JSObject *target; + JSBool deep = JS_FALSE; + + if (!JS_ConvertArguments(cx, argc, argv, "o/b", &target, &deep)) + return JS_FALSE; + if (!target) + return JS_TRUE; + return JS_SealObject(cx, target, deep); +} + +static JSBool +Unseal(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) +{ + JSObject *target; + JSBool deep = JS_FALSE; + + if (!JS_ConvertArguments(cx, argc, argv, "o/b", &target, &deep)) + return JS_FALSE; + if (!target) + return JS_TRUE; + return JS_UnsealObject(cx, target, deep); +} + static JSFunctionSpec shell_functions[] = { {"version", Version, 0}, {"options", Options, 0}, @@ -1444,6 +1470,8 @@ static JSFunctionSpec shell_functions[] = { {"clear", Clear, 0}, {"intern", Intern, 1}, {"clone", Clone, 1}, + {"seal", Seal, 1, 0, 1}, + {"unseal", Unseal, 1, 0, 1}, {0} }; @@ -1478,6 +1506,8 @@ static char *shell_help_messages[] = { "clear([obj]) Clear properties of object", "intern(str) Internalize str in the atom table", "clone(fun[, scope]) Clone function object", + "seal(obj[, deep]) Seal object, or object graph if deep", + "unseal(obj[, deep]) Unseal object, or object graph if deep", 0 }; diff --git a/js/src/js.msg b/js/src/js.msg index b5d6684a5c85..1c35e4aa1520 100644 --- a/js/src/js.msg +++ b/js/src/js.msg @@ -240,3 +240,5 @@ MSG_DEF(JSMSG_REDECLARED_PARAM, 164, 1, JSEXN_TYPEERR, "redeclaration of f MSG_DEF(JSMSG_NEWREGEXP_FLAGGED, 165, 0, JSEXN_TYPEERR, "can't supply flags when constructing one RegExp from another") MSG_DEF(JSMSG_RESERVED_SLOT_RANGE, 166, 0, JSEXN_RANGEERR, "reserved slot index out of range") MSG_DEF(JSMSG_CANT_DECODE_PRINCIPALS, 167, 0, JSEXN_INTERNALERR, "can't decode JSPrincipals") +MSG_DEF(JSMSG_CANT_SEAL_OBJECT, 168, 1, JSEXN_ERR, "can't seal {0} objects") +MSG_DEF(JSMSG_CANT_UNSEAL_OBJECT, 169, 1, JSEXN_ERR, "can't unseal {0} objects") diff --git a/js/src/jsapi.c b/js/src/jsapi.c index 728918c938ee..c4b27c512672 100644 --- a/js/src/jsapi.c +++ b/js/src/jsapi.c @@ -2020,6 +2020,100 @@ JS_NewObject(JSContext *cx, JSClass *clasp, JSObject *proto, JSObject *parent) return js_NewObject(cx, clasp, proto, parent); } +JS_PUBLIC_API(JSBool) +JS_SealObject(JSContext *cx, JSObject *obj, JSBool deep) +{ + JSScope *scope; + JSIdArray *ida; + uint32 nslots; + jsval v, *vp, *end; + + if (!OBJ_IS_NATIVE(obj)) { + JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, + JSMSG_CANT_SEAL_OBJECT, + OBJ_GET_CLASS(cx, obj)->name); + return JS_FALSE; + } + + /* Nothing to do if obj's scope is already sealed. */ + scope = OBJ_SCOPE(obj); +#ifdef JS_THREADSAFE + JS_ASSERT(scope->ownercx == cx); +#endif + if (SCOPE_IS_SEALED(scope)) + return JS_TRUE; + + /* XXX Enumerate lazy properties now, as they can't be added later. */ + ida = JS_Enumerate(cx, obj); + if (!ida) + return JS_FALSE; + JS_DestroyIdArray(cx, ida); + + /* Ensure that obj has its own, mutable scope, and seal that scope. */ + JS_LOCK_OBJ(cx, obj); + scope = js_GetMutableScope(cx, obj); + if (scope) + SCOPE_SET_SEALED(scope); + JS_UNLOCK_SCOPE(cx, scope); + if (!scope) + return JS_FALSE; + + /* If we are not sealing an entire object graph, we're done. */ + if (!deep) + return JS_TRUE; + + /* Walk obj->slots and if any value is a non-null object, seal it. */ + nslots = JS_MIN(scope->map.freeslot, scope->map.nslots); + for (vp = obj->slots, end = vp + nslots; vp < end; vp++) { + v = *vp; + if (JSVAL_IS_PRIMITIVE(v)) + continue; + if (!JS_SealObject(cx, JSVAL_TO_OBJECT(v), deep)) + return JS_FALSE; + } + return JS_TRUE; +} + +JS_PUBLIC_API(JSBool) +JS_UnsealObject(JSContext *cx, JSObject *obj, JSBool deep) +{ + JSScope *scope; + uint32 nslots; + jsval v, *vp, *end; + + if (!OBJ_IS_NATIVE(obj)) { + JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, + JSMSG_CANT_UNSEAL_OBJECT, + OBJ_GET_CLASS(cx, obj)->name); + return JS_FALSE; + } + + scope = OBJ_SCOPE(obj); +#ifdef JS_THREADSAFE + JS_ASSERT(scope->ownercx == cx); +#endif + if (!SCOPE_IS_SEALED(scope)) + return JS_TRUE; + + JS_ASSERT(scope == OBJ_SCOPE(obj)); + JS_LOCK_SCOPE(cx, scope); + SCOPE_CLR_SEALED(scope); + JS_UNLOCK_SCOPE(cx, scope); + + if (!deep) + return JS_TRUE; + + nslots = JS_MIN(scope->map.freeslot, scope->map.nslots); + for (vp = obj->slots, end = vp + nslots; vp < end; vp++) { + v = *vp; + if (JSVAL_IS_PRIMITIVE(v)) + continue; + if (!JS_UnsealObject(cx, JSVAL_TO_OBJECT(v), deep)) + return JS_FALSE; + } + return JS_TRUE; +} + JS_PUBLIC_API(JSObject *) JS_ConstructObject(JSContext *cx, JSClass *clasp, JSObject *proto, JSObject *parent) diff --git a/js/src/jsapi.h b/js/src/jsapi.h index e64b0d11e9e2..75c143e4457d 100644 --- a/js/src/jsapi.h +++ b/js/src/jsapi.h @@ -897,6 +897,12 @@ JS_GetConstructor(JSContext *cx, JSObject *proto); extern JS_PUBLIC_API(JSObject *) JS_NewObject(JSContext *cx, JSClass *clasp, JSObject *proto, JSObject *parent); +extern JS_PUBLIC_API(JSBool) +JS_SealObject(JSContext *cx, JSObject *obj, JSBool deep); + +extern JS_PUBLIC_API(JSBool) +JS_UnsealObject(JSContext *cx, JSObject *obj, JSBool deep); + extern JS_PUBLIC_API(JSObject *) JS_ConstructObject(JSContext *cx, JSClass *clasp, JSObject *proto, JSObject *parent); diff --git a/js/src/jsinterp.c b/js/src/jsinterp.c index 805f9cab4dd9..a996362bc3bc 100644 --- a/js/src/jsinterp.c +++ b/js/src/jsinterp.c @@ -1965,10 +1965,12 @@ js_Interpret(JSContext *cx, jsval *result) if (!OBJ_IS_NATIVE(obj)) { \ ok = call; \ } else { \ + JSScope *scope_; \ JS_LOCK_OBJ(cx, obj); \ PROPERTY_CACHE_TEST(&rt->propertyCache, obj, id, sprop); \ - if (sprop && !(sprop->attrs & JSPROP_READONLY)) { \ - JSScope *scope_ = OBJ_SCOPE(obj); \ + if (sprop && \ + !(sprop->attrs & JSPROP_READONLY) && \ + (scope_ = OBJ_SCOPE(obj), !SCOPE_IS_SEALED(scope_))) { \ JS_UNLOCK_SCOPE(cx, scope_); \ ok = SPROP_SET(cx, sprop, obj, obj, &rval); \ JS_LOCK_SCOPE(cx, scope_); \ diff --git a/js/src/jsobj.c b/js/src/jsobj.c index 2d6768c2d974..a1dcc8c64302 100644 --- a/js/src/jsobj.c +++ b/js/src/jsobj.c @@ -2605,8 +2605,15 @@ js_SetProperty(JSContext *cx, JSObject *obj, jsid id, jsval *vp) getter = clasp->getProperty; setter = clasp->setProperty; if (sprop) { + /* + * Set scope for use below. It was locked by js_LookupProperty, and + * we know pobj owns it (i.e., scope->object == pobj). Therefore we + * optimize JS_UNLOCK_OBJ(cx, pobj) into JS_UNLOCK_SCOPE(cx, scope). + */ + scope = OBJ_SCOPE(pobj); + attrs = sprop->attrs; - if (attrs & JSPROP_READONLY) { + if ((attrs & JSPROP_READONLY) || SCOPE_IS_SEALED(scope)) { /* XXXbe ECMA violation: readonly proto-property stops set cold. */ OBJ_DROP_PROPERTY(cx, pobj, (JSProperty *)sprop); if (!JSVERSION_IS_ECMA(cx->version)) { @@ -2624,13 +2631,6 @@ js_SetProperty(JSContext *cx, JSObject *obj, jsid id, jsval *vp) return JS_TRUE; } - /* - * Set scope for use below. It was locked by js_LookupProperty, and - * we know pobj owns it (i.e., scope->object == pobj). Therefore we - * optimize JS_UNLOCK_OBJ(cx, pobj) into JS_UNLOCK_SCOPE(cx, scope). - */ - scope = OBJ_SCOPE(pobj); - if (pobj != obj) { /* Don't clone a setter or shared prototype property. */ if (attrs & (JSPROP_SETTER | JSPROP_SHARED)) { diff --git a/js/src/jsscope.c b/js/src/jsscope.c index 7668660c21cb..27b59f7ab89f 100644 --- a/js/src/jsscope.c +++ b/js/src/jsscope.c @@ -50,6 +50,7 @@ #include "jsdbgapi.h" #include "jslock.h" #include "jsnum.h" +#include "jsopcode.h" #include "jsscope.h" #include "jsstr.h" @@ -840,6 +841,21 @@ CheckAncestorLine(JSScope *scope, JSBool sparse) #define CHECK_ANCESTOR_LINE(scope, sparse) /* nothing */ #endif +static void +ReportReadOnlyScope(JSContext *cx, JSScope *scope) +{ + JSString *str; + + str = js_DecompileValueGenerator(cx, JSDVG_SEARCH_STACK, + OBJECT_TO_JSVAL(scope->object), + NULL); + + JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_READ_ONLY, + str + ? JS_GetStringBytes(str) + : LOCKED_OBJ_GET_CLASS(scope->object)->name); +} + JSScopeProperty * js_AddScopeProperty(JSContext *cx, JSScope *scope, jsid id, JSPropertyOp getter, JSPropertyOp setter, uint32 slot, @@ -851,6 +867,17 @@ js_AddScopeProperty(JSContext *cx, JSScope *scope, jsid id, CHECK_ANCESTOR_LINE(scope, JS_TRUE); + /* + * You can't add properties to a sealed scope. But note well that you can + * change property attributes in a sealed scope, even though that replaces + * a JSScopeProperty * in the scope's hash table -- but no id is added, so + * the scope remains sealed. + */ + if (SCOPE_IS_SEALED(scope)) { + ReportReadOnlyScope(cx, scope); + return NULL; + } + /* * Normalize stub getter and setter values for faster is-stub testing in * the SPROP_CALL_[GS]ETTER macros. @@ -1273,6 +1300,10 @@ js_RemoveScopeProperty(JSContext *cx, JSScope *scope, jsid id) JS_ASSERT(JS_IS_SCOPE_LOCKED(scope)); CHECK_ANCESTOR_LINE(scope, JS_TRUE); + if (SCOPE_IS_SEALED(scope)) { + ReportReadOnlyScope(cx, scope); + return JS_FALSE; + } METER(removes); spp = js_SearchScope(scope, id, JS_FALSE); diff --git a/js/src/jsscope.h b/js/src/jsscope.h index 25909a6e284d..55a892945d73 100644 --- a/js/src/jsscope.h +++ b/js/src/jsscope.h @@ -66,7 +66,8 @@ * attributes, tiny or short id, and a field telling for..in order. Note that * labels are not unique in the tree, but they are unique among a node's kids * (barring rare and benign multi-threaded race condition outcomes, see below) - * and along any ancestor line from the tree root to a given leaf node. + * and along any ancestor line from the tree root to a given leaf node (except + * for the hard case of duplicate formal parameters to a function). * * Thus the root of the tree represents all empty scopes, and the first ply * of the tree represents all scopes containing one property, etc. Each node @@ -223,11 +224,16 @@ struct JSScope { /* Scope flags and some macros to hide them from other files than jsscope.c. */ #define SCOPE_MIDDLE_DELETE 0x0001 +#define SCOPE_SEALED 0x0002 #define SCOPE_HAD_MIDDLE_DELETE(scope) ((scope)->flags & SCOPE_MIDDLE_DELETE) #define SCOPE_SET_MIDDLE_DELETE(scope) ((scope)->flags |= SCOPE_MIDDLE_DELETE) #define SCOPE_CLR_MIDDLE_DELETE(scope) ((scope)->flags &= ~SCOPE_MIDDLE_DELETE) +#define SCOPE_IS_SEALED(scope) ((scope)->flags & SCOPE_SEALED) +#define SCOPE_SET_SEALED(scope) ((scope)->flags |= SCOPE_SEALED) +#define SCOPE_CLR_SEALED(scope) ((scope)->flags &= ~SCOPE_SEALED) + /* * A little information hiding for scope->lastProp, in case it ever becomes * a tagged pointer again.