diff --git a/js/src/jit-test/tests/basic/testWatchRecursion.js b/js/src/jit-test/tests/basic/testWatchRecursion.js deleted file mode 100644 index e5d5877df97..00000000000 --- a/js/src/jit-test/tests/basic/testWatchRecursion.js +++ /dev/null @@ -1,63 +0,0 @@ -// Test that the watch handler is not called recursively for the same object -// and property. -(function() { - var obj1 = {}, obj2 = {}; - var handler_entry_count = 0; - var handler_exit_count = 0; - - obj1.watch('x', handler); - obj1.watch('y', handler); - obj2.watch('x', handler); - obj1.x = 1; - assertEq(handler_entry_count, 3); - assertEq(handler_exit_count, 3); - - function handler(id) { - handler_entry_count++; - assertEq(handler_exit_count, 0); - switch (true) { - case this === obj1 && id === "x": - assertEq(handler_entry_count, 1); - obj2.x = 3; - assertEq(handler_exit_count, 2); - break; - case this === obj2 && id === "x": - assertEq(handler_entry_count, 2); - obj1.y = 4; - assertEq(handler_exit_count, 1); - break; - default: - assertEq(this, obj1); - assertEq(id, "y"); - assertEq(handler_entry_count, 3); - - // We expect no more watch handler invocations - obj1.x = 5; - obj1.y = 6; - obj2.x = 7; - assertEq(handler_exit_count, 0); - break; - } - ++handler_exit_count; - assertEq(handler_entry_count, 3); - } -})(); - - -// Test that run-away recursion in watch handlers is properly handled. -(function() { - var obj = {}; - var i = 0; - try { - handler(); - throw new Error("Unreachable"); - } catch(e) { - assertEq(e instanceof InternalError, true); - } - - function handler() { - var prop = "a" + ++i; - obj.watch(prop, handler); - obj[prop] = 2; - } -})(); diff --git a/js/src/jsapi-tests/Makefile.in b/js/src/jsapi-tests/Makefile.in index efd32d2dbc4..f79f5579248 100644 --- a/js/src/jsapi-tests/Makefile.in +++ b/js/src/jsapi-tests/Makefile.in @@ -67,7 +67,6 @@ CPPSRCS = \ testNewObject.cpp \ testOps.cpp \ testPropCache.cpp \ - testResolveRecursion.cpp \ testSameValue.cpp \ testScriptObject.cpp \ testSetProperty.cpp \ diff --git a/js/src/jsapi-tests/testResolveRecursion.cpp b/js/src/jsapi-tests/testResolveRecursion.cpp deleted file mode 100644 index 38b62d97173..00000000000 --- a/js/src/jsapi-tests/testResolveRecursion.cpp +++ /dev/null @@ -1,134 +0,0 @@ -/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- - * vim: set ts=8 sw=4 et tw=99: - */ - -#include "tests.h" - -/* - * Test that resolve hook recursion for the same object and property is - * prevented. - */ - -BEGIN_TEST(testResolveRecursion) -{ - static JSClass my_resolve_class = { - "MyResolve", - JSCLASS_NEW_RESOLVE | JSCLASS_HAS_PRIVATE, - - JS_PropertyStub, // add - JS_PropertyStub, // delete - JS_PropertyStub, // get - JS_StrictPropertyStub, // set - JS_EnumerateStub, - (JSResolveOp) my_resolve, - JS_ConvertStub, - JS_FinalizeStub, - JSCLASS_NO_OPTIONAL_MEMBERS - }; - - obj1 = JS_NewObject(cx, &my_resolve_class, NULL, NULL); - CHECK(obj1); - obj2 = JS_NewObject(cx, &my_resolve_class, NULL, NULL); - CHECK(obj2); - CHECK(JS_SetPrivate(cx, obj1, this)); - CHECK(JS_SetPrivate(cx, obj2, this)); - - CHECK(JS_DefineProperty(cx, global, "obj1", OBJECT_TO_JSVAL(obj1), NULL, NULL, 0)); - CHECK(JS_DefineProperty(cx, global, "obj2", OBJECT_TO_JSVAL(obj2), NULL, NULL, 0)); - - resolveEntryCount = 0; - resolveExitCount = 0; - - /* Start the essence of the test via invoking the first resolve hook. */ - jsval v; - EVAL("obj1.x", &v); - CHECK(v == JSVAL_FALSE); - CHECK(resolveEntryCount == 4); - CHECK(resolveExitCount == 4); - return true; -} - -JSObject *obj1; -JSObject *obj2; -unsigned resolveEntryCount; -unsigned resolveExitCount; - -struct AutoIncrCounters { - - AutoIncrCounters(cls_testResolveRecursion *t) : t(t) { - t->resolveEntryCount++; - } - - ~AutoIncrCounters() { - t->resolveExitCount++; - } - - cls_testResolveRecursion *t; -}; - -bool -doResolve(JSObject *obj, jsid id, uintN flags, JSObject **objp) -{ - CHECK(resolveExitCount == 0); - AutoIncrCounters incr(this); - CHECK(obj == obj1 || obj == obj2); - - CHECK(JSID_IS_STRING(id)); - - JSFlatString *str = JS_FlattenString(cx, JSID_TO_STRING(id)); - CHECK(str); - jsval v; - if (JS_FlatStringEqualsAscii(str, "x")) { - if (obj == obj1) { - /* First resolve hook invocation. */ - CHECK(resolveEntryCount == 1); - EVAL("obj2.y = true", &v); - CHECK(v == JSVAL_TRUE); - CHECK(JS_DefinePropertyById(cx, obj, id, JSVAL_FALSE, NULL, NULL, 0)); - *objp = obj; - return true; - } - if (obj == obj2) { - CHECK(resolveEntryCount == 4); - *objp = NULL; - return true; - } - } else if (JS_FlatStringEqualsAscii(str, "y")) { - if (obj == obj2) { - CHECK(resolveEntryCount == 2); - CHECK(JS_DefinePropertyById(cx, obj, id, JSVAL_NULL, NULL, NULL, 0)); - EVAL("obj1.x", &v); - CHECK(JSVAL_IS_VOID(v)); - EVAL("obj1.y", &v); - CHECK(v == JSVAL_ZERO); - *objp = obj; - return true; - } - if (obj == obj1) { - CHECK(resolveEntryCount == 3); - EVAL("obj1.x", &v); - CHECK(JSVAL_IS_VOID(v)); - EVAL("obj1.y", &v); - CHECK(JSVAL_IS_VOID(v)); - EVAL("obj2.y", &v); - CHECK(JSVAL_IS_NULL(v)); - EVAL("obj2.x", &v); - CHECK(JSVAL_IS_VOID(v)); - EVAL("obj1.y = 0", &v); - CHECK(v == JSVAL_ZERO); - *objp = obj; - return true; - } - } - CHECK(false); - return false; -} - -static JSBool -my_resolve(JSContext *cx, JSObject *obj, jsid id, uintN flags, JSObject **objp) -{ - return static_cast(JS_GetPrivate(cx, obj))-> - doResolve(obj, id, flags, objp); -} - -END_TEST(testResolveRecursion) diff --git a/js/src/jsapi.cpp b/js/src/jsapi.cpp index 853fea95829..8e81c59842e 100644 --- a/js/src/jsapi.cpp +++ b/js/src/jsapi.cpp @@ -1385,6 +1385,37 @@ JS_SetGlobalObject(JSContext *cx, JSObject *obj) cx->resetCompartment(); } +class AutoResolvingEntry { +public: + AutoResolvingEntry() : entry(NULL) {} + + /* + * Returns false on error. But N.B. if obj[id] was already being resolved, + * this is a no-op, and we silently treat that as success. + */ + bool start(JSContext *cx, JSObject *obj, jsid id, uint32 flag) { + JS_ASSERT(!entry); + this->cx = cx; + key.obj = obj; + key.id = id; + this->flag = flag; + bool ok = !!js_StartResolving(cx, &key, flag, &entry); + JS_ASSERT_IF(!ok, !entry); + return ok; + } + + ~AutoResolvingEntry() { + if (entry) + js_StopResolving(cx, &key, flag, NULL, 0); + } + +private: + JSContext *cx; + JSResolvingKey key; + uint32 flag; + JSResolvingEntry *entry; +}; + JSObject * js_InitFunctionAndObjectClasses(JSContext *cx, JSObject *obj) { @@ -1395,11 +1426,14 @@ js_InitFunctionAndObjectClasses(JSContext *cx, JSObject *obj) if (!cx->globalObject) JS_SetGlobalObject(cx, obj); - /* Record Function and Object in the resolving set. */ + /* Record Function and Object in cx->resolvingTable. */ + AutoResolvingEntry e1, e2; JSAtom **classAtoms = cx->runtime->atomState.classAtoms; - AutoResolving resolving1(cx, obj, ATOM_TO_JSID(classAtoms[JSProto_Function])); - AutoResolving resolving2(cx, obj, ATOM_TO_JSID(classAtoms[JSProto_Object])); - + if (!e1.start(cx, obj, ATOM_TO_JSID(classAtoms[JSProto_Function]), JSRESFLAG_LOOKUP) || + !e2.start(cx, obj, ATOM_TO_JSID(classAtoms[JSProto_Object]), JSRESFLAG_LOOKUP)) { + return NULL; + } + /* Initialize the function class first so constructors can be made. */ if (!js_GetClassPrototype(cx, obj, JSProto_Function, &fun_proto)) return NULL; diff --git a/js/src/jscntxt.cpp b/js/src/jscntxt.cpp index e013364a665..e5309444673 100644 --- a/js/src/jscntxt.cpp +++ b/js/src/jscntxt.cpp @@ -112,6 +112,9 @@ using namespace js::gc; static const size_t ARENA_HEADER_SIZE_HACK = 40; static const size_t TEMP_POOL_CHUNK_SIZE = 4096 - ARENA_HEADER_SIZE_HACK; +static void +FreeContext(JSContext *cx); + #ifdef DEBUG JS_REQUIRES_STACK bool StackSegment::contains(const JSStackFrame *fp) const @@ -167,10 +170,9 @@ StackSpace::init() return true; } -StackSpace::~StackSpace() +void +StackSpace::finish() { - if (!base) - return; #ifdef XP_WIN VirtualFree(base, (commitEnd - base) * sizeof(Value), MEM_DECOMMIT); VirtualFree(base, 0, MEM_RELEASE); @@ -495,17 +497,88 @@ AllFramesIter::operator++() bool JSThreadData::init() { +#ifdef DEBUG + /* The data must be already zeroed. */ + for (size_t i = 0; i != sizeof(*this); ++i) + JS_ASSERT(reinterpret_cast(this)[i] == 0); +#endif + if (!stackSpace.init()) + return false; + dtoaState = js_NewDtoaState(); + if (!dtoaState) { + finish(); + return false; + } nativeStackBase = GetNativeStackBase(); + #ifdef JS_TRACER /* Set the default size for the code cache to 16MB. */ maxCodeCacheBytes = 16 * 1024 * 1024; #endif - - return stackSpace.init() && !!(dtoaState = js_NewDtoaState()); + + return true; +} + +void +JSThreadData::finish() +{ + if (dtoaState) + js_DestroyDtoaState(dtoaState); + + js_FinishGSNCache(&gsnCache); + propertyCache.~PropertyCache(); + stackSpace.finish(); +} + +void +JSThreadData::mark(JSTracer *trc) +{ + stackSpace.mark(trc); +} + +void +JSThreadData::purge(JSContext *cx) +{ + js_PurgeGSNCache(&gsnCache); + + /* FIXME: bug 506341. */ + propertyCache.purge(cx); } #ifdef JS_THREADSAFE +static JSThread * +NewThread(void *id) +{ + JS_ASSERT(js_CurrentThreadId() == id); + JSThread *thread = (JSThread *) js_calloc(sizeof(JSThread)); + if (!thread) + return NULL; + JS_INIT_CLIST(&thread->contextList); + thread->id = id; + if (!thread->data.init()) { + js_free(thread); + return NULL; + } + return thread; +} + +static void +DestroyThread(JSThread *thread) +{ + /* The thread must have zero contexts. */ + JS_ASSERT(JS_CLIST_IS_EMPTY(&thread->contextList)); + + /* + * The conservative GC scanner should be disabled when the thread leaves + * the last request. + */ + JS_ASSERT(!thread->data.conservativeGC.hasStackToScan()); + + thread->data.finish(); + js_free(thread); +} + JSThread * js_CurrentThread(JSRuntime *rt) { @@ -531,21 +604,14 @@ js_CurrentThread(JSRuntime *rt) thread->data.nativeStackBase = GetNativeStackBase(); } else { JS_UNLOCK_GC(rt); - - void *threadMemory = js_calloc(sizeof(JSThread)); - if (!threadMemory) + thread = NewThread(id); + if (!thread) return NULL; - thread = new (threadMemory) JSThread(id); - if (!thread->init()) { - js_delete(thread); - return NULL; - } - JS_LOCK_GC(rt); js_WaitForGC(rt); if (!rt->threads.relookupOrAdd(p, id, thread)) { JS_UNLOCK_GC(rt); - js_delete(thread); + DestroyThread(thread); return NULL; } @@ -623,7 +689,8 @@ js_FinishThreads(JSRuntime *rt) return; for (JSThread::Map::Range r = rt->threads.all(); !r.empty(); r.popFront()) { JSThread *thread = r.front().value; - js_delete(thread); + JS_ASSERT(JS_CLIST_IS_EMPTY(&thread->contextList)); + DestroyThread(thread); } rt->threads.clear(); #else @@ -642,7 +709,8 @@ js_PurgeThreads(JSContext *cx) if (JS_CLIST_IS_EMPTY(&thread->contextList)) { JS_ASSERT(cx->thread != thread); - js_delete(thread); + + DestroyThread(thread); e.removeFront(); } else { thread->data.purge(cx); @@ -687,13 +755,13 @@ js_NewContext(JSRuntime *rt, size_t stackChunkSize) JS_ASSERT(cx->resolveFlags == 0); if (!cx->busyArrays.init()) { - js_delete(cx); + FreeContext(cx); return NULL; } #ifdef JS_THREADSAFE if (!js_InitContextThread(cx)) { - js_delete(cx); + FreeContext(cx); return NULL; } #endif @@ -1025,7 +1093,41 @@ js_DestroyContext(JSContext *cx, JSDestroyContextMode mode) cx->dstOffsetCache.dumpStats(); #endif JS_UNLOCK_GC(rt); - js_delete(cx); + FreeContext(cx); +} + +static void +FreeContext(JSContext *cx) +{ +#ifdef JS_THREADSAFE + JS_ASSERT(!cx->thread); +#endif + + /* Free the stuff hanging off of cx. */ + VOUCH_DOES_NOT_REQUIRE_STACK(); + JS_FinishArenaPool(&cx->tempPool); + JS_FinishArenaPool(&cx->regExpPool); + + if (cx->lastMessage) + js_free(cx->lastMessage); + + /* Remove any argument formatters. */ + JSArgumentFormatMap *map = cx->argumentFormatMap; + while (map) { + JSArgumentFormatMap *temp = map; + map = map->next; + cx->free(temp); + } + + /* Destroy the resolve recursion damper. */ + if (cx->resolvingTable) { + JS_DHashTableDestroy(cx->resolvingTable); + cx->resolvingTable = NULL; + } + + /* Finally, free cx itself. */ + cx->~JSContext(); + js_free(cx); } JSContext * @@ -1056,21 +1158,107 @@ js_NextActiveContext(JSRuntime *rt, JSContext *cx) #endif } -namespace js { - -bool -AutoResolving::isDuplicate() const +static JSDHashNumber +resolving_HashKey(JSDHashTable *table, const void *ptr) { - JS_ASSERT(prev); - AutoResolving *cursor = prev; - do { - if (cursor->object == object && cursor->id == id && cursor->kind == kind) - return true; - } while (!!(cursor = cursor->prev)); - return false; -} + const JSResolvingKey *key = (const JSResolvingKey *)ptr; -} /* namespace js */ + return (JSDHashNumber(uintptr_t(key->obj)) >> JS_GCTHING_ALIGN) ^ JSID_BITS(key->id); +} + +static JSBool +resolving_MatchEntry(JSDHashTable *table, + const JSDHashEntryHdr *hdr, + const void *ptr) +{ + const JSResolvingEntry *entry = (const JSResolvingEntry *)hdr; + const JSResolvingKey *key = (const JSResolvingKey *)ptr; + + return entry->key.obj == key->obj && entry->key.id == key->id; +} + +static const JSDHashTableOps resolving_dhash_ops = { + JS_DHashAllocTable, + JS_DHashFreeTable, + resolving_HashKey, + resolving_MatchEntry, + JS_DHashMoveEntryStub, + JS_DHashClearEntryStub, + JS_DHashFinalizeStub, + NULL +}; + +JSBool +js_StartResolving(JSContext *cx, JSResolvingKey *key, uint32 flag, + JSResolvingEntry **entryp) +{ + JSDHashTable *table; + JSResolvingEntry *entry; + + table = cx->resolvingTable; + if (!table) { + table = JS_NewDHashTable(&resolving_dhash_ops, NULL, + sizeof(JSResolvingEntry), + JS_DHASH_MIN_SIZE); + if (!table) + goto outofmem; + cx->resolvingTable = table; + } + + entry = (JSResolvingEntry *) + JS_DHashTableOperate(table, key, JS_DHASH_ADD); + if (!entry) + goto outofmem; + + if (entry->flags & flag) { + /* An entry for (key, flag) exists already -- dampen recursion. */ + entry = NULL; + } else { + /* Fill in key if we were the first to add entry, then set flag. */ + if (!entry->key.obj) + entry->key = *key; + entry->flags |= flag; + } + *entryp = entry; + return JS_TRUE; + +outofmem: + JS_ReportOutOfMemory(cx); + return JS_FALSE; +} + +void +js_StopResolving(JSContext *cx, JSResolvingKey *key, uint32 flag, + JSResolvingEntry *entry, uint32 generation) +{ + JSDHashTable *table; + + /* + * Clear flag from entry->flags and return early if other flags remain. + * We must take care to re-lookup entry if the table has changed since + * it was found by js_StartResolving. + */ + table = cx->resolvingTable; + if (!entry || table->generation != generation) { + entry = (JSResolvingEntry *) + JS_DHashTableOperate(table, key, JS_DHASH_LOOKUP); + } + JS_ASSERT(JS_DHASH_ENTRY_IS_BUSY(&entry->hdr)); + entry->flags &= ~flag; + if (entry->flags) + return; + + /* + * Do a raw remove only if fewer entries were removed than would cause + * alpha to be less than .5 (alpha is at most .75). Otherwise, we just + * call JS_DHashTableOperate to re-lookup the key and remove its entry, + * compressing or shrinking the table as needed. + */ + if (table->removedCount < JS_DHASH_TABLE_SIZE(table) >> 2) + JS_DHashTableRawRemove(table, &entry->hdr); + else + JS_DHashTableOperate(table, key, JS_DHASH_REMOVE); +} static void ReportError(JSContext *cx, const char *message, JSErrorReport *reportp, @@ -1803,29 +1991,6 @@ JSContext::JSContext(JSRuntime *rt) busyArrays() {} -JSContext::~JSContext() -{ -#ifdef JS_THREADSAFE - JS_ASSERT(!thread); -#endif - - /* Free the stuff hanging off of cx. */ - VOUCH_DOES_NOT_REQUIRE_STACK(); - JS_FinishArenaPool(&tempPool); - JS_FinishArenaPool(®ExpPool); - - if (lastMessage) - js_free(lastMessage); - - /* Remove any argument formatters. */ - JSArgumentFormatMap *map = argumentFormatMap; - while (map) { - JSArgumentFormatMap *temp = map; - map = map->next; - js_free(temp); - } -} - void JSContext::resetCompartment() { diff --git a/js/src/jscntxt.h b/js/src/jscntxt.h index 09de4f9958c..17a1567dc6f 100644 --- a/js/src/jscntxt.h +++ b/js/src/jscntxt.h @@ -653,21 +653,9 @@ class StackSpace static const size_t STACK_QUOTA = (VALUES_PER_STACK_FRAME + 18) * JS_MAX_INLINE_CALL_COUNT; - /* The constructor must be called over zeroed memory. */ - StackSpace() { - JS_ASSERT(!base); -#ifdef XP_WIN - JS_ASSERT(!commitEnd); -#endif - JS_ASSERT(!end); - JS_ASSERT(!currentSegment); - JS_ASSERT(!invokeSegment); - JS_ASSERT(!invokeFrame); - JS_ASSERT(!invokeArgEnd); - } - + /* Kept as a member of JSThreadData; cannot use constructor/destructor. */ bool init(); - ~StackSpace(); + void finish(); #ifdef DEBUG template @@ -853,55 +841,6 @@ struct JSPendingProxyOperation { JSObject *object; }; -namespace js { - -/* - * Class to detect recursive invocation of Class::resolve hooks and watch - * handlers. - * - * We optimize for the case of just few entries in the resolving set and use a - * linked list of AutoResolving instances with the explicit search, not a hash - * set, to check for duplicated keys. We assume that cases like recursive - * resolving hooks or watch handlers will be dealt with a native stack - * recursion checks long before O(N) complexity of adding a new entry to the - * list will affect performance. - * - * The linked list may contain duplicated entries as the user of th class may - * not use the alreadyStarted method, see js_InitFunctionAndObjectClasses. It - * allows to skip any checks in the destructor making the common case of no - * dups in the list faster. - */ -class AutoResolving { - public: - enum Kind { - LOOKUP, - WATCH - }; - - JS_ALWAYS_INLINE AutoResolving(JSContext *cx, JSObject *obj, jsid id, Kind kind = LOOKUP - JS_GUARD_OBJECT_NOTIFIER_PARAM); - - ~AutoResolving() { - *lastp = prev; - } - - bool alreadyStarted() const { - return prev && isDuplicate(); - } - - private: - bool isDuplicate() const; - - JSObject *const object; - jsid const id; - Kind const kind; - AutoResolving **const lastp; - AutoResolving *const prev; - JS_DECL_USE_GUARD_OBJECT_NOTIFIER -}; - -} /* namespace js */ - struct JSThreadData { #ifdef JS_THREADSAFE /* The request depth for this thread. */ @@ -963,46 +902,10 @@ struct JSThreadData { js::ConservativeGCThreadData conservativeGC; - js::AutoResolving *resolvingList; - - /* The constructor must be called over zeroed memory. */ - JSThreadData() { -#ifdef JS_THREADSAFE - JS_ASSERT(!requestDepth); -#endif - -#ifdef JS_TRACER - JS_ASSERT(!onTraceCompartment); - JS_ASSERT(!recordingCompartment); - JS_ASSERT(!profilingCompartment); - JS_ASSERT(!maxCodeCacheBytes); -#endif - JS_ASSERT(!interruptFlags); - JS_ASSERT(!waiveGCQuota); - JS_ASSERT(!dtoaState); - JS_ASSERT(!nativeStackBase); - JS_ASSERT(!pendingProxyOperation); - } - bool init(); - - ~JSThreadData() { - if (dtoaState) - js_DestroyDtoaState(dtoaState); - - js_FinishGSNCache(&gsnCache); - } - - void mark(JSTracer *trc) { - stackSpace.mark(trc); - } - - void purge(JSContext *cx) { - js_PurgeGSNCache(&gsnCache); - - /* FIXME: bug 506341. */ - propertyCache.purge(cx); - } + void finish(); + void mark(JSTracer *trc); + void purge(JSContext *cx); /* This must be called with the GC lock held. */ inline void triggerOperationCallback(JSRuntime *rt); @@ -1035,30 +938,6 @@ struct JSThread { /* Factored out of JSThread for !JS_THREADSAFE embedding in JSRuntime. */ JSThreadData data; - - /* The constructor must be called over zeroed memory. */ - JSThread(void *id) - : id(id) - { - JS_INIT_CLIST(&contextList); - JS_ASSERT(!suspendCount); - JS_ASSERT(!checkRequestDepth); - } - - bool init() { - return data.init(); - } - - ~JSThread() { - /* The thread must have zero contexts. */ - JS_ASSERT(JS_CLIST_IS_EMPTY(&contextList)); - - /* - * The conservative GC scanner should be disabled when the thread leaves - * the last request. - */ - JS_ASSERT(!data.conservativeGC.hasStackToScan()); - } }; #define JS_THREAD_DATA(cx) (&(cx)->thread->data) @@ -1178,7 +1057,7 @@ struct JSRuntime { /* * Compartment that triggered GC. If more than one Compatment need GC, - * gcTriggerCompartment is reset to NULL and a global GC is performed. + * gcTriggerCompartment is reset to NULL and a global GC is performed. */ JSCompartment *gcTriggerCompartment; @@ -1583,6 +1462,27 @@ struct JSArgumentFormatMap { }; #endif +/* + * Key and entry types for the JSContext.resolvingTable hash table, typedef'd + * here because all consumers need to see these declarations (and not just the + * typedef names, as would be the case for an opaque pointer-to-typedef'd-type + * declaration), along with cx->resolvingTable. + */ +typedef struct JSResolvingKey { + JSObject *obj; + jsid id; +} JSResolvingKey; + +typedef struct JSResolvingEntry { + JSDHashEntryHdr hdr; + JSResolvingKey key; + uint32 flags; +} JSResolvingEntry; + +#define JSRESFLAG_LOOKUP 0x1 /* resolving id from lookup */ +#define JSRESFLAG_WATCH 0x2 /* resolving id from watch */ +#define JSRESOLVE_INFER 0xffff /* infer bits from current bytecode */ + extern const JSDebugHooks js_NullDebugHooks; /* defined in jsdbgapi.cpp */ namespace js { @@ -1716,7 +1616,6 @@ typedef js::HashSetresolvingTable is non-null and non-empty if we are initializing + * standard classes lazily, or if we are otherwise recursing indirectly + * from js_LookupProperty through a Class.resolve hook. It is used to + * limit runaway recursion (see jsapi.c and jsobj.c). + */ + JSDHashTable *resolvingTable; + /* * True if generating an error, to prevent runaway recursion. * NB: generatingError packs with throwing below. @@ -1959,7 +1866,7 @@ struct JSContext /* * Return: * - The override version, if there is an override version. - * - The newest scripted frame's version, if there is such a frame. + * - The newest scripted frame's version, if there is such a frame. * - The default verion. * * Note: if this ever shows up in a profile, just add caching! @@ -3069,6 +2976,17 @@ js_ContextIterator(JSRuntime *rt, JSBool unlocked, JSContext **iterp); extern JS_FRIEND_API(JSContext *) js_NextActiveContext(JSRuntime *, JSContext *); +/* + * Class.resolve and watchpoint recursion damping machinery. + */ +extern JSBool +js_StartResolving(JSContext *cx, JSResolvingKey *key, uint32 flag, + JSResolvingEntry **entryp); + +extern void +js_StopResolving(JSContext *cx, JSResolvingKey *key, uint32 flag, + JSResolvingEntry *entry, uint32 generation); + /* * Report an exception, which is currently realized as a printf-style format * string and its arguments. diff --git a/js/src/jscntxtinlines.h b/js/src/jscntxtinlines.h index d5d4a6e53b4..bf3b055d48b 100644 --- a/js/src/jscntxtinlines.h +++ b/js/src/jscntxtinlines.h @@ -1,4 +1,4 @@ -/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- * vim: set ts=4 sw=4 et tw=78: * * ***** BEGIN LICENSE BLOCK ***** @@ -523,19 +523,6 @@ class AutoNamespaceArray : protected AutoGCRooter { JSXMLArray array; }; -JS_ALWAYS_INLINE -AutoResolving::AutoResolving(JSContext *cx, JSObject *obj, jsid id, Kind kind - JS_GUARD_OBJECT_NOTIFIER_PARAM_NO_INIT) - : object(obj), - id(id), - kind(kind), - lastp(&JS_THREAD_DATA(cx)->resolvingList), - prev(*lastp) -{ - JS_GUARD_OBJECT_NOTIFIER_INIT; - *lastp = this; -} - #ifdef DEBUG class CompartmentChecker { diff --git a/js/src/jshashtable.h b/js/src/jshashtable.h index e7d11976f6f..c0969d0af9b 100644 --- a/js/src/jshashtable.h +++ b/js/src/jshashtable.h @@ -112,18 +112,6 @@ class HashTable : AllocPolicy Ptr(Entry &entry) : entry(&entry) {} public: - /* - * Any method on Ptr instantiated with the default constructor should - * only be called after initializing the Ptr with assignment operator - * from another Ptr instance. - */ - Ptr() { -#ifdef DEBUG - /* Initialize to some small invalid address. */ - entry = reinterpret_cast(0xBAD); -#endif - } - bool found() const { return entry->isLive(); } operator ConvertibleToBool() const { return found() ? &Ptr::nonNull : 0; } bool operator==(const Ptr &rhs) const { JS_ASSERT(found() && rhs.found()); return entry == rhs.entry; } diff --git a/js/src/jsobj.cpp b/js/src/jsobj.cpp index e6e5c62ce66..ab7808742f2 100644 --- a/js/src/jsobj.cpp +++ b/js/src/jsobj.cpp @@ -1320,35 +1320,53 @@ static JSBool obj_watch_handler(JSContext *cx, JSObject *obj, jsid id, jsval old, jsval *nvp, void *closure) { - JSObject *callable = (JSObject *) closure; - - JSSecurityCallbacks *callbacks = JS_GetSecurityCallbacks(cx); + JSObject *callable; + JSSecurityCallbacks *callbacks; + JSStackFrame *caller; + JSPrincipals *subject, *watcher; + JSResolvingKey key; + JSResolvingEntry *entry; + uint32 generation; + Value argv[3]; + JSBool ok; + + callable = (JSObject *) closure; + + callbacks = JS_GetSecurityCallbacks(cx); if (callbacks && callbacks->findObjectPrincipals) { /* Skip over any obj_watch_* frames between us and the real subject. */ - JSStackFrame *caller = js_GetScriptedCaller(cx, NULL); + caller = js_GetScriptedCaller(cx, NULL); if (caller) { /* * Only call the watch handler if the watcher is allowed to watch * the currently executing script. */ - JSPrincipals *watcher = callbacks->findObjectPrincipals(cx, callable); - JSPrincipals *subject = js_StackFramePrincipals(cx, caller); + watcher = callbacks->findObjectPrincipals(cx, callable); + subject = js_StackFramePrincipals(cx, caller); if (watcher && subject && !watcher->subsume(watcher, subject)) { /* Silently don't call the watch handler. */ - return true; + return JS_TRUE; } } } - /* Avoid recursion on (obj, id) already being watched. */ - AutoResolving resolving(cx, obj, id, AutoResolving::WATCH); - if (resolving.alreadyStarted()) - return true; - - Value argv[] = { IdToValue(id), Valueify(old), Valueify(*nvp) }; - return ExternalInvoke(cx, ObjectValue(*obj), ObjectOrNullValue(callable), - JS_ARRAY_LENGTH(argv), argv, Valueify(nvp)); + /* Avoid recursion on (obj, id) already being watched on cx. */ + key.obj = obj; + key.id = id; + if (!js_StartResolving(cx, &key, JSRESFLAG_WATCH, &entry)) + return JS_FALSE; + if (!entry) + return JS_TRUE; + generation = cx->resolvingTable->generation; + + argv[0] = IdToValue(id); + argv[1] = Valueify(old); + argv[2] = Valueify(*nvp); + ok = ExternalInvoke(cx, ObjectValue(*obj), ObjectOrNullValue(callable), 3, argv, + Valueify(nvp)); + js_StopResolving(cx, &key, JSRESFLAG_WATCH, entry, generation); + return ok; } static JSBool @@ -4161,36 +4179,52 @@ JSBool js_GetClassObject(JSContext *cx, JSObject *obj, JSProtoKey key, JSObject **objp) { + JSObject *cobj; + JSResolvingKey rkey; + JSResolvingEntry *rentry; + uint32 generation; + JSObjectOp init; + Value v; + obj = obj->getGlobal(); if (!obj->isGlobal()) { *objp = NULL; - return true; + return JS_TRUE; } - Value v = obj->getReservedSlot(key); + v = obj->getReservedSlot(key); if (v.isObject()) { *objp = &v.toObject(); - return true; + return JS_TRUE; } - AutoResolving resolving(cx, obj, ATOM_TO_JSID(cx->runtime->atomState.classAtoms[key])); - if (resolving.alreadyStarted()) { + rkey.obj = obj; + rkey.id = ATOM_TO_JSID(cx->runtime->atomState.classAtoms[key]); + if (!js_StartResolving(cx, &rkey, JSRESFLAG_LOOKUP, &rentry)) + return JS_FALSE; + if (!rentry) { /* Already caching key in obj -- suppress recursion. */ *objp = NULL; - return true; - } - - JSObject *cobj = NULL; - if (JSObjectOp init = lazy_prototype_init[key]) { - if (!init(cx, obj)) - return false; - v = obj->getReservedSlot(key); - if (v.isObject()) - cobj = &v.toObject(); + return JS_TRUE; + } + generation = cx->resolvingTable->generation; + + JSBool ok = true; + cobj = NULL; + init = lazy_prototype_init[key]; + if (init) { + if (!init(cx, obj)) { + ok = JS_FALSE; + } else { + v = obj->getReservedSlot(key); + if (v.isObject()) + cobj = &v.toObject(); + } } + js_StopResolving(cx, &rkey, JSRESFLAG_LOOKUP, rentry, generation); *objp = cobj; - return true; + return ok; } JSBool @@ -4800,61 +4834,116 @@ js_DefineNativeProperty(JSContext *cx, JSObject *obj, jsid id, const Value &valu JS_SCOPE_DEPTH_METERING(JS_BASIC_STATS_ACCUM(bs, val)) /* - * Call obj's resolve hook. + * Call obj's resolve hook. obj is a native object and the caller holds its + * scope lock. + * + * cx, start, id, and flags are the parameters initially passed to the ongoing + * lookup; objp and propp are its out parameters. obj is an object along + * start's prototype chain. + * + * There are four possible outcomes: + * + * - On failure, report an error or exception, unlock obj, and return false. + * + * - If we are alrady resolving a property of *curobjp, set *recursedp = true, + * unlock obj, and return true. + * + * - If the resolve hook finds or defines the sought property, set *objp and + * *propp appropriately, set *recursedp = false, and return true with *objp's + * lock held. + * + * - Otherwise no property was resolved. Set *propp = NULL and *recursedp = false + * and return true. */ static JSBool CallResolveOp(JSContext *cx, JSObject *start, JSObject *obj, jsid id, uintN flags, - JSObject **objp, JSProperty **propp) + JSObject **objp, JSProperty **propp, bool *recursedp) { Class *clasp = obj->getClass(); JSResolveOp resolve = clasp->resolve; - JSObject *obj2; + /* + * Avoid recursion on (obj, id) already being resolved on cx. + * + * Once we have successfully added an entry for (obj, key) to + * cx->resolvingTable, control must go through cleanup: before + * returning. But note that JS_DHASH_ADD may find an existing + * entry, in which case we bail to suppress runaway recursion. + */ + JSResolvingKey key = {obj, id}; + JSResolvingEntry *entry; + if (!js_StartResolving(cx, &key, JSRESFLAG_LOOKUP, &entry)) + return false; + if (!entry) { + /* Already resolving id in obj -- suppress recursion. */ + *recursedp = true; + return true; + } + uint32 generation = cx->resolvingTable->generation; + *recursedp = false; + + *propp = NULL; + + JSBool ok; + const Shape *shape = NULL; if (clasp->flags & JSCLASS_NEW_RESOLVE) { JSNewResolveOp newresolve = (JSNewResolveOp)resolve; if (flags == JSRESOLVE_INFER) flags = js_InferFlags(cx, 0); - obj2 = (clasp->flags & JSCLASS_NEW_RESOLVE_GETS_START) ? start : NULL; - + JSObject *obj2 = (clasp->flags & JSCLASS_NEW_RESOLVE_GETS_START) ? start : NULL; + { /* Protect id and all atoms from a GC nested in resolve. */ AutoKeepAtoms keep(cx->runtime); - if (!newresolve(cx, obj, id, flags, &obj2)) - return false; + ok = newresolve(cx, obj, id, flags, &obj2); } - - /* - * We trust the new style resolve hook to set obj2 to NULL when - * the id cannot be resolved. But, when obj2 is not null, we do - * not assume that id must exist and lookup the property again - * for compatibility. - */ - if (!obj2) { - *propp = NULL; - return true; - } - - if (!obj2->isNative()) { - /* Whoops, newresolve handed back a foreign obj2. */ - JS_ASSERT(obj2 != obj); - return obj2->lookupProperty(cx, id, objp, propp); + if (!ok) + goto cleanup; + + if (obj2) { + /* Resolved: lookup id again for backward compatibility. */ + if (!obj2->isNative()) { + /* Whoops, newresolve handed back a foreign obj2. */ + JS_ASSERT(obj2 != obj); + ok = obj2->lookupProperty(cx, id, objp, propp); + if (!ok || *propp) + goto cleanup; + } else { + /* + * Require that obj2 not be empty now, as we do for old-style + * resolve. If it doesn't, then id was not truly resolved, and + * we'll find it in the proto chain, or miss it if obj2's proto + * is not on obj's proto chain. That last case is a "too bad!" + * case. + */ + if (!obj2->nativeEmpty()) + shape = obj2->nativeLookup(id); + } + if (shape) { + JS_ASSERT(!obj2->nativeEmpty()); + obj = obj2; + } } } else { - if (!resolve(cx, obj, id)) - return false; - obj2 = obj; - } - - JS_ASSERT(obj2->isNative()); - if (const Shape *shape = obj2->nativeLookup(id)) { - *objp = obj2; - *propp = (JSProperty *) shape; - return true; + /* + * Old resolve always requires id re-lookup if obj is not empty after + * resolve returns. + */ + ok = resolve(cx, obj, id); + if (!ok) + goto cleanup; + JS_ASSERT(obj->isNative()); + if (!obj->nativeEmpty()) + shape = obj->nativeLookup(id); } - /* The id was not resolved. */ - *propp = NULL; - return true; +cleanup: + if (ok && shape) { + *objp = obj; + *propp = (JSProperty *) shape; + } + js_StopResolving(cx, &key, JSRESFLAG_LOOKUP, entry, generation); + return ok; } static JS_ALWAYS_INLINE int @@ -4878,12 +4967,11 @@ js_LookupPropertyWithFlagsInline(JSContext *cx, JSObject *obj, jsid id, uintN fl /* Try obj's class resolve hook if id was not found in obj's scope. */ if (!shape && obj->getClass()->resolve != JS_ResolveStub) { - /* Avoid recursion on (obj, id) already being resolved. */ - AutoResolving resolving(cx, obj, id); - if (resolving.alreadyStarted()) - break; - if (!CallResolveOp(cx, start, obj, id, flags, objp, propp)) + bool recursed; + if (!CallResolveOp(cx, start, obj, id, flags, objp, propp, &recursed)) return -1; + if (recursed) + break; if (*propp) { /* Recalculate protoIndex in case it was resolved on some other object. */ protoIndex = 0; diff --git a/js/src/jsobj.h b/js/src/jsobj.h index 1670311af85..27d59fa0212 100644 --- a/js/src/jsobj.h +++ b/js/src/jsobj.h @@ -1660,8 +1660,6 @@ extern int js_LookupPropertyWithFlags(JSContext *cx, JSObject *obj, jsid id, uintN flags, JSObject **objp, JSProperty **propp); -/* Infer resolve flags from the current bytecode. */ -#define JSRESOLVE_INFER 0xffff /* * We cache name lookup results only for the global object or for native diff --git a/js/src/jsutil.h b/js/src/jsutil.h index 2958cd3e23b..9da2674704c 100644 --- a/js/src/jsutil.h +++ b/js/src/jsutil.h @@ -401,8 +401,6 @@ public: , const JSGuardObjectNotifier& _notifier = JSGuardObjectNotifier() #define JS_GUARD_OBJECT_NOTIFIER_PARAM0 \ const JSGuardObjectNotifier& _notifier = JSGuardObjectNotifier() -#define JS_GUARD_OBJECT_NOTIFIER_PARAM_NO_INIT \ - , const JSGuardObjectNotifier& _notifier #define JS_GUARD_OBJECT_NOTIFIER_INIT \ JS_BEGIN_MACRO _mCheckNotUsedAsTemporary.Init(_notifier); JS_END_MACRO @@ -411,7 +409,6 @@ public: #define JS_DECL_USE_GUARD_OBJECT_NOTIFIER #define JS_GUARD_OBJECT_NOTIFIER_PARAM #define JS_GUARD_OBJECT_NOTIFIER_PARAM0 -#define JS_GUARD_OBJECT_NOTIFIER_PARAM_NO_INIT #define JS_GUARD_OBJECT_NOTIFIER_INIT JS_BEGIN_MACRO JS_END_MACRO #endif /* !defined(DEBUG) */ diff --git a/js/src/xpconnect/loader/mozJSSubScriptLoader.cpp b/js/src/xpconnect/loader/mozJSSubScriptLoader.cpp index 5ac2d86f3da..765b7b49306 100644 --- a/js/src/xpconnect/loader/mozJSSubScriptLoader.cpp +++ b/js/src/xpconnect/loader/mozJSSubScriptLoader.cpp @@ -62,6 +62,7 @@ #include "jsapi.h" #include "jsdbgapi.h" #include "jsobj.h" +#include "jsscript.h" #include "jscntxt.h" #include "mozilla/FunctionTimer.h"