From 3442a335eb1136be6e595cce3a52db85cae11a5c Mon Sep 17 00:00:00 2001 From: Brendan Eich Date: Thu, 4 Jun 2009 18:58:47 -0700 Subject: [PATCH] Bug 494235: wrap escaping optimized closures for the debugger API (r=igor/mrbkap). --- js/src/imacros.c.out | 5 + js/src/js.msg | 6 +- js/src/jsapi.cpp | 14 +- js/src/jsdbgapi.cpp | 20 ++- js/src/jsemit.cpp | 1 + js/src/jsfun.cpp | 312 +++++++++++++++++++++++++++++++++++++++++-- js/src/jsfun.h | 16 ++- js/src/jsinterp.cpp | 71 +++++++++- js/src/jsopcode.cpp | 4 + js/src/jsopcode.tbl | 9 ++ js/src/jsparse.cpp | 107 +++++++++++---- js/src/jsscript.cpp | 2 + js/src/jsscript.h | 3 + js/src/jstracer.cpp | 13 ++ js/src/jsxdrapi.h | 2 +- 15 files changed, 526 insertions(+), 59 deletions(-) diff --git a/js/src/imacros.c.out b/js/src/imacros.c.out index 168d706a7979..6f350f38871b 100644 --- a/js/src/imacros.c.out +++ b/js/src/imacros.c.out @@ -949,6 +949,11 @@ uint8 js_opcode2extra[JSOP_LIMIT] = { 0, /* JSOP_LAMBDA_FC */ 0, /* JSOP_OBJTOP */ 0, /* JSOP_LOOP */ + 0, /* JSOP_GETUPVAR_DBG */ + 0, /* JSOP_CALLUPVAR_DBG */ + 0, /* JSOP_DEFFUN_DBGFC */ + 0, /* JSOP_DEFLOCALFUN_DBGFC */ + 0, /* JSOP_LAMBDA_DBGFC */ }; #define JSOP_IS_IMACOP(x) (0 \ || x == JSOP_BITOR \ diff --git a/js/src/js.msg b/js/src/js.msg index 7b995d2a5dec..6a564531cc00 100644 --- a/js/src/js.msg +++ b/js/src/js.msg @@ -192,7 +192,7 @@ MSG_DEF(JSMSG_SEMI_BEFORE_STMNT, 109, 0, JSEXN_SYNTAXERR, "missing ; before MSG_DEF(JSMSG_NO_RETURN_VALUE, 110, 1, JSEXN_TYPEERR, "function {0} does not always return a value") MSG_DEF(JSMSG_DUPLICATE_FORMAL, 111, 1, JSEXN_TYPEERR, "duplicate formal argument {0}") MSG_DEF(JSMSG_EQUAL_AS_ASSIGN, 112, 1, JSEXN_SYNTAXERR, "test for equality (==) mistyped as assignment (=)?{0}") -MSG_DEF(JSMSG_UNUSED113, 113, 0, JSEXN_NONE, "unused113") +MSG_DEF(JSMSG_OPTIMIZED_CLOSURE_LEAK, 113, 0, JSEXN_INTERNALERR, "cannot access optimized closure") MSG_DEF(JSMSG_TOO_MANY_DEFAULTS, 114, 0, JSEXN_SYNTAXERR, "more than one switch default") MSG_DEF(JSMSG_TOO_MANY_CASES, 115, 0, JSEXN_INTERNALERR, "too many switch cases") MSG_DEF(JSMSG_BAD_SWITCH, 116, 0, JSEXN_SYNTAXERR, "invalid switch statement") @@ -309,6 +309,4 @@ MSG_DEF(JSMSG_EVAL_ARITY, 226, 0, JSEXN_TYPEERR, "eval accepts only MSG_DEF(JSMSG_MISSING_FUN_ARG, 227, 2, JSEXN_TYPEERR, "missing argument {0} when calling function {1}") MSG_DEF(JSMSG_JSON_BAD_PARSE, 228, 0, JSEXN_SYNTAXERR, "JSON.parse") MSG_DEF(JSMSG_JSON_BAD_STRINGIFY, 229, 0, JSEXN_ERR, "JSON.stringify") - - - +MSG_DEF(JSMSG_XDR_CLOSURE_WRAPPER, 230, 1, JSEXN_INTERNALERR, "can't XDR closure wrapper for function {0}") diff --git a/js/src/jsapi.cpp b/js/src/jsapi.cpp index 88ff6e2b036a..28d309f3fb5d 100644 --- a/js/src/jsapi.cpp +++ b/js/src/jsapi.cpp @@ -646,11 +646,7 @@ JS_TypeOfValue(JSContext *cx, jsval v) type = JSTYPE_OBJECT; /* XXXbe JSTYPE_NULL for JS2 */ obj = JSVAL_TO_OBJECT(v); if (obj) { - JSObject *wrapped; - - wrapped = js_GetWrappedObject(cx, obj); - if (wrapped) - obj = wrapped; + obj = js_GetWrappedObject(cx, obj); ops = obj->map->ops; #if JS_HAS_XML_SUPPORT @@ -1362,11 +1358,11 @@ JS_InitStandardClasses(JSContext *cx, JSObject *obj) } #define CLASP(name) (&js_##name##Class) -#define EXT_CLASP(name) (&js_##name##Class.base) +#define XCLASP(name) (&js_##name##Class.base) #define EAGER_ATOM(name) ATOM_OFFSET(name), NULL #define EAGER_CLASS_ATOM(name) CLASS_ATOM_OFFSET(name), NULL #define EAGER_ATOM_AND_CLASP(name) EAGER_CLASS_ATOM(name), CLASP(name) -#define EAGER_ATOM_AND_EXT_CLASP(name) EAGER_CLASS_ATOM(name), EXT_CLASP(name) +#define EAGER_ATOM_AND_XCLASP(name) EAGER_CLASS_ATOM(name), XCLASP(name) #define LAZY_ATOM(name) ATOM_OFFSET(lazy.name), js_##name##_str typedef struct JSStdName { @@ -1415,8 +1411,8 @@ static JSStdName standard_class_atoms[] = { #endif #if JS_HAS_XML_SUPPORT {js_InitXMLClass, EAGER_ATOM_AND_CLASP(XML)}, - {js_InitNamespaceClass, EAGER_ATOM_AND_EXT_CLASP(Namespace)}, - {js_InitQNameClass, EAGER_ATOM_AND_EXT_CLASP(QName)}, + {js_InitNamespaceClass, EAGER_ATOM_AND_XCLASP(Namespace)}, + {js_InitQNameClass, EAGER_ATOM_AND_XCLASP(QName)}, #endif #if JS_HAS_FILE_OBJECT {js_InitFileClass, EAGER_ATOM_AND_CLASP(File)}, diff --git a/js/src/jsdbgapi.cpp b/js/src/jsdbgapi.cpp index 5e1ffd6064ef..62345324602b 100644 --- a/js/src/jsdbgapi.cpp +++ b/js/src/jsdbgapi.cpp @@ -684,7 +684,6 @@ js_watch_set_wrapper(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval userid; funobj = JSVAL_TO_OBJECT(argv[-2]); - JS_ASSERT(OBJ_GET_CLASS(cx, funobj) == &js_FunctionClass); wrapper = GET_FUNCTION_PRIVATE(cx, funobj); userid = ATOM_KEY(wrapper->atom); *rval = argv[0]; @@ -1156,7 +1155,7 @@ JS_GetFrameFunctionObject(JSContext *cx, JSStackFrame *fp) if (!fp->fun) return NULL; - JS_ASSERT(OBJ_GET_CLASS(cx, fp->callee) == &js_FunctionClass); + JS_ASSERT(HAS_FUNCTION_CLASS(fp->callee)); JS_ASSERT(OBJ_GET_PRIVATE(cx, fp->callee) == fp->fun); return fp->callee; } @@ -1261,11 +1260,28 @@ JS_EvaluateUCInStackFrame(JSContext *cx, JSStackFrame *fp, TCF_PUT_STATIC_LEVEL(JS_DISPLAY_SIZE), chars, length, NULL, filename, lineno); + if (!script) return JS_FALSE; + JSStackFrame *displayCopy[JS_DISPLAY_SIZE]; + if (cx->fp != fp) { + memcpy(displayCopy, cx->display, sizeof displayCopy); + + /* This API requires an active fp on cx, so fp2 can't go null here. */ + for (JSStackFrame *fp2 = cx->fp; fp2 != fp; fp2 = fp2->down) { + if (fp2->displaySave) { + JS_ASSERT(fp2->script->staticLevel < JS_DISPLAY_SIZE); + cx->display[fp2->script->staticLevel] = fp2->displaySave; + } + } + } + ok = js_Execute(cx, scobj, script, fp, JSFRAME_DEBUGGER | JSFRAME_EVAL, rval); + + if (cx->fp != fp) + memcpy(cx->display, displayCopy, sizeof cx->display); js_DestroyScript(cx, script); return ok; } diff --git a/js/src/jsemit.cpp b/js/src/jsemit.cpp index a8525a5816c7..f64e10b56757 100644 --- a/js/src/jsemit.cpp +++ b/js/src/jsemit.cpp @@ -2127,6 +2127,7 @@ BindNameToSlot(JSContext *cx, JSCodeGenerator *cg, JSParseNode *pn) JS_ASSERT(cg->flags & TCF_IN_FUNCTION); JS_ASSERT(cg->lexdeps.lookup(atom)); JS_ASSERT(JOF_OPTYPE(op) == JOF_ATOM); + JS_ASSERT(cg->fun->u.i.skipmin <= skip); /* * If op is a mutating opcode, this upvar's static level is too big to diff --git a/js/src/jsfun.cpp b/js/src/jsfun.cpp index 392f2fd1f279..143570e6b773 100644 --- a/js/src/jsfun.cpp +++ b/js/src/jsfun.cpp @@ -245,7 +245,19 @@ js_GetArgsObject(JSContext *cx, JSStackFrame *fp) * We must be in a function activation; the function must be lightweight * or else fp must have a variable object. */ - JS_ASSERT(fp->fun && (!(fp->fun->flags & JSFUN_HEAVYWEIGHT) || fp->varobj)); + JSFunction *fun = fp->fun; + JS_ASSERT(fun && (!(fun->flags & JSFUN_HEAVYWEIGHT) || fp->varobj)); + + /* + * Unlike FUN_ESCAPE_HAZARD(fun), we test here only for null closures, not + * flat closures -- flat ones are inherently escaping, so arguments.callee + * references are fine. + */ + if (FUN_NULL_CLOSURE(fun) && fun->u.i.skipmin != 0) { + JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, + JSMSG_OPTIMIZED_CLOSURE_LEAK); + return JS_FALSE; + } /* Skip eval and debugger frames. */ while (fp->flags & JSFRAME_SPECIAL) @@ -365,6 +377,175 @@ args_delProperty(JSContext *cx, JSObject *obj, jsval id, jsval *vp) return JS_TRUE; } +static JS_REQUIRES_STACK JSObject * +WrapEscapingClosure(JSContext *cx, JSStackFrame *fp, JSObject *funobj, JSFunction *fun) +{ + JS_ASSERT(GET_FUNCTION_PRIVATE(cx, funobj) == fun); + JS_ASSERT(FUN_OPTIMIZED_CLOSURE(fun)); + JS_ASSERT(!fun->u.i.wrapper); + + /* + * We do not attempt to reify Call and Block objects on demand for outer + * scopes. This could be done (see the "v8" patch in bug 494235) but it is + * fragile in the face of ongoing compile-time optimization. Instead, the + * _DBG* opcodes used by wrappers created here must cope with unresolved + * upvars and throw them as reference errors. Caveat debuggers! + */ + JSObject *scopeChain = js_GetScopeChain(cx, fp); + if (!scopeChain) + return NULL; + + JSObject *wfunobj = js_NewObjectWithGivenProto(cx, &js_FunctionClass, + funobj, scopeChain, 0); + if (!wfunobj) + return NULL; + JSAutoTempValueRooter tvr(cx, wfunobj); + + JSFunction *wfun = (JSFunction *) wfunobj; + wfunobj->fslots[JSSLOT_PRIVATE] = PRIVATE_TO_JSVAL(wfun); + wfun->nargs = 0; + wfun->flags = fun->flags | JSFUN_HEAVYWEIGHT; + wfun->u.i.nvars = 0; + wfun->u.i.nupvars = 0; + wfun->u.i.skipmin = fun->u.i.skipmin; + wfun->u.i.wrapper = true; + wfun->u.i.script = NULL; + wfun->u.i.names.taggedAtom = NULL; + wfun->atom = fun->atom; + + if (fun->hasLocalNames()) { + void *mark = JS_ARENA_MARK(&cx->tempPool); + jsuword *names = js_GetLocalNameArray(cx, fun, &cx->tempPool); + if (!names) + return NULL; + + JSBool ok = true; + for (uintN i = 0, n = fun->countLocalNames(); i != n; i++) { + jsuword name = names[i]; + JSAtom *atom = JS_LOCAL_NAME_TO_ATOM(name); + JSLocalKind localKind = (i < fun->nargs) + ? JSLOCAL_ARG + : (i < fun->countArgsAndVars()) + ? (JS_LOCAL_NAME_IS_CONST(name) + ? JSLOCAL_CONST + : JSLOCAL_VAR) + : JSLOCAL_UPVAR; + + ok = js_AddLocal(cx, wfun, atom, localKind); + if (!ok) + break; + } + + JS_ARENA_RELEASE(&cx->tempPool, mark); + if (!ok) + return NULL; + JS_ASSERT(wfun->nargs == fun->nargs); + JS_ASSERT(wfun->u.i.nvars == fun->u.i.nvars); + JS_ASSERT(wfun->u.i.nupvars == fun->u.i.nupvars); + js_FreezeLocalNames(cx, wfun); + } + + JSScript *script = fun->u.i.script; + jssrcnote *snbase = SCRIPT_NOTES(script); + jssrcnote *sn = snbase; + while (!SN_IS_TERMINATOR(sn)) + sn = SN_NEXT(sn); + uintN nsrcnotes = (sn - snbase) + 1; + + /* NB: GC must not occur before wscript is homed in wfun->u.i.script. */ + JSScript *wscript = js_NewScript(cx, script->length, nsrcnotes, + script->atomMap.length, + (script->objectsOffset != 0) + ? JS_SCRIPT_OBJECTS(script)->length + : 0, + fun->u.i.nupvars, + (script->regexpsOffset != 0) + ? JS_SCRIPT_REGEXPS(script)->length + : 0, + (script->trynotesOffset != 0) + ? JS_SCRIPT_TRYNOTES(script)->length + : 0); + if (!wscript) + return NULL; + + memcpy(wscript->code, script->code, script->length); + wscript->main = wscript->code + (script->main - script->code); + + memcpy(SCRIPT_NOTES(wscript), snbase, nsrcnotes); + memcpy(wscript->atomMap.vector, script->atomMap.vector, + wscript->atomMap.length * sizeof(JSAtom *)); + if (script->objectsOffset != 0) { + memcpy(JS_SCRIPT_OBJECTS(wscript)->vector, JS_SCRIPT_OBJECTS(script)->vector, + JS_SCRIPT_OBJECTS(wscript)->length * sizeof(JSObject *)); + } + if (script->regexpsOffset != 0) { + memcpy(JS_SCRIPT_REGEXPS(wscript)->vector, JS_SCRIPT_REGEXPS(script)->vector, + JS_SCRIPT_REGEXPS(wscript)->length * sizeof(JSObject *)); + } + if (script->trynotesOffset != 0) { + memcpy(JS_SCRIPT_TRYNOTES(wscript)->vector, JS_SCRIPT_TRYNOTES(script)->vector, + JS_SCRIPT_TRYNOTES(wscript)->length * sizeof(JSTryNote)); + } + + if (wfun->u.i.nupvars != 0) { + JS_ASSERT(wfun->u.i.nupvars == JS_SCRIPT_UPVARS(wscript)->length); + memcpy(JS_SCRIPT_UPVARS(wscript)->vector, JS_SCRIPT_UPVARS(script)->vector, + wfun->u.i.nupvars * sizeof(uint32)); + } + + jsbytecode *pc = wscript->code; + while (*pc != JSOP_STOP) { + /* XYZZYbe should copy JSOP_TRAP? */ + JSOp op = js_GetOpcode(cx, wscript, pc); + const JSCodeSpec *cs = &js_CodeSpec[op]; + ptrdiff_t oplen = cs->length; + if (oplen < 0) + oplen = js_GetVariableBytecodeLength(pc); + + /* + * Rewrite JSOP_{GET,CALL}DSLOT as JSOP_{GET,CALL}UPVAR_DBG for the + * case where fun is an escaping flat closure. This works because the + * UPVAR and DSLOT ops by design have the same format: an upvar index + * immediate operand. + */ + switch (op) { + case JSOP_GETUPVAR: *pc = JSOP_GETUPVAR_DBG; break; + case JSOP_CALLUPVAR: *pc = JSOP_CALLUPVAR_DBG; break; + case JSOP_GETDSLOT: *pc = JSOP_GETUPVAR_DBG; break; + case JSOP_CALLDSLOT: *pc = JSOP_CALLUPVAR_DBG; break; + case JSOP_DEFFUN_FC: *pc = JSOP_DEFFUN_DBGFC; break; + case JSOP_DEFLOCALFUN_FC: *pc = JSOP_DEFLOCALFUN_DBGFC; break; + case JSOP_LAMBDA_FC: *pc = JSOP_LAMBDA_DBGFC; break; + default:; + } + pc += oplen; + } + + /* + * Flag the wrapper's script as hazardous, because it could host debugger + * or indirect eval calls that leak closures. Wrappers thus transitively + * wrap all contained closures that escape. + */ + wscript->flags = script->flags | JSSF_ESCAPE_HAZARD; + wscript->version = script->version; + wscript->nfixed = script->nfixed; + wscript->filename = script->filename; + wscript->lineno = script->lineno; + wscript->nslots = script->nslots; + wscript->staticLevel = script->staticLevel; + wscript->principals = script->principals; + if (wscript->principals) + JSPRINCIPALS_HOLD(cx, wscript->principals); +#ifdef CHECK_SCRIPT_OWNER + wscript->owner = script->owner; +#endif + + /* Deoptimize wfun from FUN_{FLAT,NULL}_CLOSURE to FUN_INTERPRETED. */ + FUN_SET_KIND(wfun, JSFUN_INTERPRETED); + wfun->u.i.script = wscript; + return wfunobj; +} + static JSBool args_getProperty(JSContext *cx, JSObject *obj, jsval id, jsval *vp) { @@ -379,6 +560,13 @@ args_getProperty(JSContext *cx, JSObject *obj, jsval id, jsval *vp) return JS_TRUE; JS_ASSERT(fp->argsobj); + /* + * If this function or one in it needs upvars that reach above it in the + * scope chain, it must not be a null closure (it could be a flat closure, + * or an unoptimized closure -- the latter not necessarily heavyweight). + */ + JS_ASSERT_IF(fp->fun->u.i.skipmin != 0, !FUN_NULL_CLOSURE(fp->fun)); + slot = JSVAL_TO_INT(id); switch (slot) { case ARGS_CALLEE: @@ -590,14 +778,62 @@ JSClass js_ArgumentsClass = { #define JSSLOT_CALL_ARGUMENTS (JSSLOT_PRIVATE + 2) #define CALL_CLASS_FIXED_RESERVED_SLOTS 2 +/* + * A Declarative Environment object stores its active JSStackFrame pointer in + * its private slot, just as Call and Arguments objects do. + */ JSClass js_DeclEnvClass = { js_Object_str, - JSCLASS_HAS_CACHED_PROTO(JSProto_Object), + JSCLASS_HAS_PRIVATE | JSCLASS_HAS_CACHED_PROTO(JSProto_Object), JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, JS_FinalizeStub, JSCLASS_NO_OPTIONAL_MEMBERS }; +static JS_REQUIRES_STACK JSBool +CheckForEscapingClosure(JSContext *cx, JSObject *obj, jsval *vp) +{ + JS_ASSERT(STOBJ_GET_CLASS(obj) == &js_CallClass || + STOBJ_GET_CLASS(obj) == &js_DeclEnvClass); + + if (cx->fp && cx->fp->script && (cx->fp->script->flags & JSSF_ESCAPE_HAZARD)) { + jsval v = *vp; + + if (VALUE_IS_FUNCTION(cx, v)) { + JSObject *funobj = JSVAL_TO_OBJECT(v); + JSFunction *fun = GET_FUNCTION_PRIVATE(cx, funobj); + + /* + * Any escaping null or flat closure that reaches above itself or + * contains nested functions that reach above it must be wrapped. + * We can wrap only when this Call or Declarative Environment obj + * still has an active stack frame associated with it. + */ + if (FUN_ESCAPE_HAZARD(fun)) { + JSStackFrame *fp = (JSStackFrame *) JS_GetPrivate(cx, obj); + if (fp) { + JSObject *wrapper = WrapEscapingClosure(cx, fp, funobj, fun); + if (!wrapper) + return false; + *vp = OBJECT_TO_JSVAL(wrapper); + return true; + } + + JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, + JSMSG_OPTIMIZED_CLOSURE_LEAK); + return false; + } + } + } + return true; +} + +static JS_REQUIRES_STACK JSBool +CalleeGetter(JSContext *cx, JSObject *obj, jsval id, jsval *vp) +{ + return CheckForEscapingClosure(cx, obj, vp); +} + JSObject * js_GetCallObject(JSContext *cx, JSStackFrame *fp) { @@ -628,11 +864,13 @@ js_GetCallObject(JSContext *cx, JSStackFrame *fp) fp->scopeChain, 0); if (!env) return NULL; + env->fslots[JSSLOT_PRIVATE] = PRIVATE_TO_JSVAL(fp); /* Root env before js_DefineNativeProperty (-> JSClass.addProperty). */ fp->scopeChain = env; if (!js_DefineNativeProperty(cx, fp->scopeChain, ATOM_TO_JSID(lambdaName), - OBJECT_TO_JSVAL(fp->callee), NULL, NULL, + OBJECT_TO_JSVAL(fp->callee), + CalleeGetter, NULL, JSPROP_PERMANENT | JSPROP_READONLY, 0, 0, NULL)) { return NULL; @@ -725,10 +963,18 @@ js_PutCallObject(JSContext *cx, JSStackFrame *fp) } /* - * Clear the private pointer to fp, which is about to go away (js_Invoke). + * Clear private pointers to fp, which is about to go away (js_Invoke). * Do this last because js_GetProperty calls above need to follow the - * private slot to find fp. + * call object's private slot to find fp. */ + if ((fun->flags & JSFUN_LAMBDA) && fun->atom) { + JSObject *env = STOBJ_GET_PARENT(callobj); + + JS_ASSERT(STOBJ_GET_CLASS(env) == &js_DeclEnvClass); + JS_ASSERT(STOBJ_GET_PRIVATE(env) == fp); + JS_SetPrivate(cx, env, NULL); + } + JS_SetPrivate(cx, callobj, NULL); fp->callobj = NULL; return ok; @@ -888,7 +1134,10 @@ SetCallArg(JSContext *cx, JSObject *obj, jsid id, jsval *vp) JSBool js_GetCallVar(JSContext *cx, JSObject *obj, jsid id, jsval *vp) { - return CallPropertyOp(cx, obj, id, vp, JSCPK_VAR, JS_FALSE); + if (!CallPropertyOp(cx, obj, id, vp, JSCPK_VAR, JS_FALSE)) + return JS_FALSE; + + return CheckForEscapingClosure(cx, obj, vp); } static JSBool @@ -1099,10 +1348,20 @@ fun_getProperty(JSContext *cx, JSObject *obj, jsval id, jsval *vp) break; case FUN_CALLER: - if (fp && fp->down && fp->down->fun) + if (fp && fp->down && fp->down->fun) { + /* + * See the equivalent assertion in args_getProperty. Here we assert + * against a closure leak that goes through foo.caller. Wrapping an + * escaping optimized closure guarantees that its function cannot + * match the unwrapped stack frame's function (fp->fun) in the loop + * commented "Find fun's top-most activation record" above. + */ + JS_ASSERT_IF(fp->down->fun->u.i.skipmin != 0, + !FUN_NULL_CLOSURE(fp->down->fun)); *vp = OBJECT_TO_JSVAL(fp->down->callee); - else + } else { *vp = JSVAL_NULL; + } if (!JSVAL_IS_PRIMITIVE(*vp)) { callbacks = JS_GetSecurityCallbacks(cx); if (callbacks && callbacks->checkObjectAccess) { @@ -1272,10 +1531,12 @@ js_XDRFunctionObject(JSXDRState *xdr, JSObject **objp) { JSContext *cx; JSFunction *fun; - uint32 nullAtom; /* flag to indicate if fun->atom is NULL */ + uint32 firstword; /* flag telling whether fun->atom is non-null, + plus for fun->u.i.skipmin, fun->u.i.wrapper, + and 14 bits reserved for future use */ uintN nargs, nvars, nupvars, n; - uint32 localsword; /* word to xdr argument and variable counts */ - uint32 flagsword; /* word to xdr upvars count and fun->flags */ + uint32 localsword; /* word for argument and variable counts */ + uint32 flagsword; /* word for fun->u.i.nupvars and fun->flags */ JSTempValueRooter tvr; JSBool ok; @@ -1288,7 +1549,14 @@ js_XDRFunctionObject(JSXDRState *xdr, JSObject **objp) JS_GetFunctionName(fun)); return JS_FALSE; } - nullAtom = !fun->atom; + if (fun->u.i.wrapper) { + JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, + JSMSG_XDR_CLOSURE_WRAPPER, + JS_GetFunctionName(fun)); + return JS_FALSE; + } + JS_ASSERT((fun->u.i.wrapper & ~1U) == 0); + firstword = (fun->u.i.skipmin << 2) | (fun->u.i.wrapper << 1) | !!fun->atom; nargs = fun->nargs; nvars = fun->u.i.nvars; nupvars = fun->u.i.nupvars; @@ -1310,9 +1578,9 @@ js_XDRFunctionObject(JSXDRState *xdr, JSObject **objp) JS_PUSH_TEMP_ROOT_OBJECT(cx, FUN_OBJECT(fun), &tvr); ok = JS_TRUE; - if (!JS_XDRUint32(xdr, &nullAtom)) + if (!JS_XDRUint32(xdr, &firstword)) goto bad; - if (!nullAtom && !js_XDRStringAtom(xdr, &fun->atom)) + if ((firstword & 1U) && !js_XDRStringAtom(xdr, &fun->atom)) goto bad; if (!JS_XDRUint32(xdr, &localsword) || !JS_XDRUint32(xdr, &flagsword)) { @@ -1325,6 +1593,8 @@ js_XDRFunctionObject(JSXDRState *xdr, JSObject **objp) JS_ASSERT((flagsword & JSFUN_KINDMASK) >= JSFUN_INTERPRETED); nupvars = flagsword >> 16; fun->flags = uint16(flagsword); + fun->u.i.skipmin = uint16(firstword >> 2); + fun->u.i.wrapper = (firstword >> 1) & 1; } /* do arguments and local vars */ @@ -2134,6 +2404,8 @@ js_NewFunction(JSContext *cx, JSObject *funobj, JSNative native, uintN nargs, JS_ASSERT(nargs == 0); fun->u.i.nvars = 0; fun->u.i.nupvars = 0; + fun->u.i.skipmin = 0; + fun->u.i.wrapper = false; fun->u.i.script = NULL; #ifdef DEBUG fun->u.i.names.taggedAtom = 0; @@ -2210,6 +2482,8 @@ JSObject * js_NewFlatClosure(JSContext *cx, JSFunction *fun) { JSObject *closure = js_AllocFlatClosure(cx, fun, cx->fp->scopeChain); + if (!closure) + return NULL; JSUpvarArray *uva = JS_SCRIPT_UPVARS(fun->u.i.script); JS_ASSERT(uva->length <= size_t(closure->dslots[-1])); @@ -2221,6 +2495,16 @@ js_NewFlatClosure(JSContext *cx, JSFunction *fun) return closure; } +JSObject * +js_NewDebuggableFlatClosure(JSContext *cx, JSFunction *fun) +{ + JS_ASSERT(cx->fp->script->flags & JSSF_ESCAPE_HAZARD); + JS_ASSERT(cx->fp->fun->flags & JSFUN_HEAVYWEIGHT); + JS_ASSERT(!FUN_OPTIMIZED_CLOSURE(cx->fp->fun)); + + return WrapEscapingClosure(cx, cx->fp, FUN_OBJECT(fun), fun); +} + JSFunction * js_DefineFunction(JSContext *cx, JSObject *obj, JSAtom *atom, JSNative native, uintN nargs, uintN attrs) diff --git a/js/src/jsfun.h b/js/src/jsfun.h index f700bce1aee2..65cc8ee734ba 100644 --- a/js/src/jsfun.h +++ b/js/src/jsfun.h @@ -113,6 +113,8 @@ typedef union JSLocalNames { #define FUN_INTERPRETED(fun) (FUN_KIND(fun) >= JSFUN_INTERPRETED) #define FUN_FLAT_CLOSURE(fun)(FUN_KIND(fun) == JSFUN_FLAT_CLOSURE) #define FUN_NULL_CLOSURE(fun)(FUN_KIND(fun) == JSFUN_NULL_CLOSURE) +#define FUN_OPTIMIZED_CLOSURE(fun) (FUN_KIND(fun) > JSFUN_INTERPRETED) +#define FUN_ESCAPE_HAZARD(fun) (FUN_OPTIMIZED_CLOSURE(fun) && (fun)->u.i.skipmin != 0) #define FUN_SLOW_NATIVE(fun) (!FUN_INTERPRETED(fun) && !((fun)->flags & JSFUN_FAST_NATIVE)) #define FUN_SCRIPT(fun) (FUN_INTERPRETED(fun) ? (fun)->u.i.script : NULL) #define FUN_NATIVE(fun) (FUN_SLOW_NATIVE(fun) ? (fun)->u.n.native : NULL) @@ -147,6 +149,15 @@ struct JSFunction { uint16 nvars; /* number of local variables */ uint16 nupvars; /* number of upvars (computable from script but here for faster access) */ + uint16 skipmin; /* net skip amount up (toward zero) from + script->staticLevel to nearest upvar, + including upvars in nested functions */ + JSPackedBool wrapper; /* true if this function is a wrapper that + rewrites bytecode optimized for a function + judged non-escaping by the compiler, which + then escaped via the debugger or a rogue + indirect eval; if true, then this function + object's proto is the wrapped object */ JSScript *script; /* interpreted bytecode descriptor or null */ JSLocalNames names; /* argument and variable names */ } i; @@ -231,6 +242,9 @@ js_CloneFunctionObject(JSContext *cx, JSFunction *fun, JSObject *parent); extern JS_REQUIRES_STACK JSObject * js_NewFlatClosure(JSContext *cx, JSFunction *fun); +extern JS_REQUIRES_STACK JSObject * +js_NewDebuggableFlatClosure(JSContext *cx, JSFunction *fun); + extern JSFunction * js_DefineFunction(JSContext *cx, JSObject *obj, JSAtom *atom, JSNative native, uintN nargs, uintN flags); @@ -265,7 +279,7 @@ js_PutCallObject(JSContext *cx, JSStackFrame *fp); extern JSBool js_GetCallArg(JSContext *cx, JSObject *obj, jsid id, jsval *vp); -extern JSBool +extern JS_REQUIRES_STACK JSBool js_GetCallVar(JSContext *cx, JSObject *obj, jsval id, jsval *vp); extern JSBool diff --git a/js/src/jsinterp.cpp b/js/src/jsinterp.cpp index c2f7e11e448e..14721edf14a1 100644 --- a/js/src/jsinterp.cpp +++ b/js/src/jsinterp.cpp @@ -2589,11 +2589,19 @@ AssertValidPropertyCacheHit(JSContext *cx, JSScript *script, JSFrameRegs& regs, JS_STATIC_ASSERT(JSOP_NAME_LENGTH == JSOP_CALLNAME_LENGTH); JS_STATIC_ASSERT(JSOP_GETGVAR_LENGTH == JSOP_CALLGVAR_LENGTH); JS_STATIC_ASSERT(JSOP_GETUPVAR_LENGTH == JSOP_CALLUPVAR_LENGTH); +JS_STATIC_ASSERT(JSOP_GETUPVAR_DBG_LENGTH == JSOP_CALLUPVAR_DBG_LENGTH); +JS_STATIC_ASSERT(JSOP_GETUPVAR_DBG_LENGTH == JSOP_GETUPVAR_LENGTH); JS_STATIC_ASSERT(JSOP_GETDSLOT_LENGTH == JSOP_CALLDSLOT_LENGTH); JS_STATIC_ASSERT(JSOP_GETARG_LENGTH == JSOP_CALLARG_LENGTH); JS_STATIC_ASSERT(JSOP_GETLOCAL_LENGTH == JSOP_CALLLOCAL_LENGTH); JS_STATIC_ASSERT(JSOP_XMLNAME_LENGTH == JSOP_CALLXMLNAME_LENGTH); +/* + * Same for debuggable flat closures defined at top level in another function + * or program fragment. + */ +JS_STATIC_ASSERT(JSOP_DEFFUN_FC_LENGTH == JSOP_DEFFUN_DBGFC_LENGTH); + /* * Same for JSOP_SETNAME and JSOP_SETPROP, which differ only slightly but * remain distinct for the decompiler. @@ -5733,6 +5741,43 @@ js_Interpret(JSContext *cx) } END_CASE(JSOP_GETUPVAR) + BEGIN_CASE(JSOP_GETUPVAR_DBG) + BEGIN_CASE(JSOP_CALLUPVAR_DBG) + fun = fp->fun; + JS_ASSERT(FUN_KIND(fun) == JSFUN_INTERPRETED); + JS_ASSERT(fun->u.i.wrapper); + + /* Scope for tempPool mark and local names allocation in it. */ + { + void *mark = JS_ARENA_MARK(&cx->tempPool); + jsuword *names = js_GetLocalNameArray(cx, fun, &cx->tempPool); + if (!names) + goto error; + + index = fun->countArgsAndVars() + GET_UINT16(regs.pc); + atom = JS_LOCAL_NAME_TO_ATOM(names[index]); + id = ATOM_TO_JSID(atom); + + ok = js_FindProperty(cx, id, &obj, &obj2, &prop); + JS_ARENA_RELEASE(&cx->tempPool, mark); + if (!ok) + goto error; + } + + if (!prop) + goto atom_not_defined; + + /* Minimize footprint with generic code instead of NATIVE_GET. */ + OBJ_DROP_PROPERTY(cx, obj2, prop); + vp = regs.sp; + PUSH_OPND(JSVAL_NULL); + if (!OBJ_GET_PROPERTY(cx, obj, id, vp)) + goto error; + + if (op == JSOP_CALLUPVAR_DBG) + PUSH_OPND(JSVAL_NULL); + END_CASE(JSOP_GETUPVAR_DBG) + BEGIN_CASE(JSOP_GETDSLOT) BEGIN_CASE(JSOP_CALLDSLOT) obj = fp->callee; @@ -6018,9 +6063,12 @@ js_Interpret(JSContext *cx) END_CASE(JSOP_DEFFUN) BEGIN_CASE(JSOP_DEFFUN_FC) + BEGIN_CASE(JSOP_DEFFUN_DBGFC) LOAD_FUNCTION(0); - obj = js_NewFlatClosure(cx, fun); + obj = (op == JSOP_DEFFUN_FC) + ? js_NewFlatClosure(cx, fun) + : js_NewDebuggableFlatClosure(cx, fun); if (!obj) goto error; rval = OBJECT_TO_JSVAL(obj); @@ -6117,6 +6165,17 @@ js_Interpret(JSContext *cx) fp->slots[slot] = OBJECT_TO_JSVAL(obj); END_CASE(JSOP_DEFLOCALFUN_FC) + BEGIN_CASE(JSOP_DEFLOCALFUN_DBGFC) + LOAD_FUNCTION(SLOTNO_LEN); + + obj = js_NewDebuggableFlatClosure(cx, fun); + if (!obj) + goto error; + + slot = GET_SLOTNO(regs.pc); + fp->slots[slot] = OBJECT_TO_JSVAL(obj); + END_CASE(JSOP_DEFLOCALFUN_DBGFC) + BEGIN_CASE(JSOP_LAMBDA) /* Load the specified function object literal. */ LOAD_FUNCTION(0); @@ -6152,6 +6211,16 @@ js_Interpret(JSContext *cx) PUSH_OPND(OBJECT_TO_JSVAL(obj)); END_CASE(JSOP_LAMBDA_FC) + BEGIN_CASE(JSOP_LAMBDA_DBGFC) + LOAD_FUNCTION(0); + + obj = js_NewDebuggableFlatClosure(cx, fun); + if (!obj) + goto error; + + PUSH_OPND(OBJECT_TO_JSVAL(obj)); + END_CASE(JSOP_LAMBDA_DBGFC) + BEGIN_CASE(JSOP_CALLEE) PUSH_OPND(OBJECT_TO_JSVAL(fp->callee)); END_CASE(JSOP_CALLEE) diff --git a/js/src/jsopcode.cpp b/js/src/jsopcode.cpp index 78f0f08e5322..adff59f5e180 100644 --- a/js/src/jsopcode.cpp +++ b/js/src/jsopcode.cpp @@ -2804,6 +2804,8 @@ Decompile(SprintStack *ss, jsbytecode *pc, intN nb, JSOp nextop) case JSOP_GETUPVAR: case JSOP_CALLUPVAR: + case JSOP_GETUPVAR_DBG: + case JSOP_CALLUPVAR_DBG: case JSOP_GETDSLOT: case JSOP_CALLDSLOT: { @@ -3974,6 +3976,7 @@ Decompile(SprintStack *ss, jsbytecode *pc, intN nb, JSOp nextop) case JSOP_LAMBDA: case JSOP_LAMBDA_FC: + case JSOP_LAMBDA_DBGFC: #if JS_HAS_GENERATOR_EXPRS sn = js_GetSrcNote(jp->script, pc); if (sn && SN_TYPE(sn) == SRC_GENEXP) { @@ -4339,6 +4342,7 @@ Decompile(SprintStack *ss, jsbytecode *pc, intN nb, JSOp nextop) case JSOP_DEFFUN: case JSOP_DEFFUN_FC: + case JSOP_DEFFUN_DBGFC: LOAD_FUNCTION(0); todo = -2; goto do_function; diff --git a/js/src/jsopcode.tbl b/js/src/jsopcode.tbl index 654a83d663e2..6981a1edf309 100644 --- a/js/src/jsopcode.tbl +++ b/js/src/jsopcode.tbl @@ -580,3 +580,12 @@ OPDEF(JSOP_LAMBDA_FC, 226,"lambda_fc", NULL, 3, 0, 1, 19, JOF_OBJECT OPDEF(JSOP_OBJTOP, 227,"objtop", NULL, 3, 0, 0, 0, JOF_UINT16) OPDEF(JSOP_LOOP, 228, "loop", NULL, 1, 0, 0, 0, JOF_BYTE) + +/* + * Debugger versions of JSOP_{GET,CALL}UPVAR. + */ +OPDEF(JSOP_GETUPVAR_DBG, 229,"getupvar_dbg", NULL, 3, 0, 1, 19, JOF_UINT16|JOF_NAME) +OPDEF(JSOP_CALLUPVAR_DBG, 230,"callupvar_dbg", NULL, 3, 0, 2, 19, JOF_UINT16|JOF_NAME|JOF_CALLOP) +OPDEF(JSOP_DEFFUN_DBGFC, 231,"deffun_dbgfc", NULL, 3, 0, 0, 0, JOF_OBJECT|JOF_DECLARING) +OPDEF(JSOP_DEFLOCALFUN_DBGFC,232,"deflocalfun_dbgfc",NULL, 5, 0, 0, 0, JOF_SLOTOBJECT|JOF_DECLARING) +OPDEF(JSOP_LAMBDA_DBGFC, 233,"lambda_dbgfc", NULL, 3, 0, 1, 19, JOF_OBJECT) diff --git a/js/src/jsparse.cpp b/js/src/jsparse.cpp index f0feaea03646..f595726bc407 100644 --- a/js/src/jsparse.cpp +++ b/js/src/jsparse.cpp @@ -819,7 +819,7 @@ JSCompiler::compileScript(JSContext *cx, JSObject *scopeChain, JSStackFrame *cal /* Null script early in case of error, to reduce our code footprint. */ script = NULL; - cg.flags |= (uint16) tcflags; + cg.flags |= uint16(tcflags); cg.scopeChain = scopeChain; if (!SetStaticLevel(&cg, TCF_GET_STATIC_LEVEL(tcflags))) goto out; @@ -1688,12 +1688,21 @@ JSCompiler::analyzeFunctions(JSFunctionBox *funbox, uint16& tcflags) * process is potentially exponential in the number of functions, but generally * not so complex. But it can't be done during a single recursive traversal of * the funbox tree, so we must use a work queue. + * + * Return the minimal "skipmin" for funbox and its siblings. This is the delta + * between the static level of the bodies of funbox and its peers (which must + * be funbox->level + 1), and the static level of the nearest upvar among all + * the upvars contained by funbox and its peers. If there are no upvars, return + * FREE_STATIC_LEVEL. Thus this function never returns 0. */ -static void +static uintN FindFunArgs(JSFunctionBox *funbox, int level, JSFunctionBoxQueue *queue) { + uintN allskipmin = FREE_STATIC_LEVEL; + do { JSParseNode *fn = funbox->node; + JSFunction *fun = (JSFunction *) funbox->object; int fnlevel = level; /* @@ -1709,35 +1718,78 @@ FindFunArgs(JSFunctionBox *funbox, int level, JSFunctionBoxQueue *queue) kid->node->setFunArg(); } - if (fn->isFunArg()) { - queue->push(funbox); - fnlevel = int(funbox->level); - } else { - JSParseNode *pn = fn->pn_body; + /* + * Compute in skipmin the least distance from fun's static level up to + * an upvar, whether used directly by fun, or indirectly by a function + * nested in fun. + */ + uintN skipmin = FREE_STATIC_LEVEL; + JSParseNode *pn = fn->pn_body; - if (pn->pn_type == TOK_UPVARS) { - JSAtomList upvars(pn->pn_names); - JS_ASSERT(upvars.count != 0); + if (pn->pn_type == TOK_UPVARS) { + JSAtomList upvars(pn->pn_names); + JS_ASSERT(upvars.count != 0); - JSAtomListIterator iter(&upvars); - JSAtomListElement *ale; + JSAtomListIterator iter(&upvars); + JSAtomListElement *ale; - while ((ale = iter()) != NULL) { - JSDefinition *lexdep = ALE_DEFN(ale)->resolve(); + while ((ale = iter()) != NULL) { + JSDefinition *lexdep = ALE_DEFN(ale)->resolve(); - if (!lexdep->isFreeVar() && int(lexdep->frameLevel()) <= fnlevel) { + if (!lexdep->isFreeVar()) { + uintN upvarLevel = lexdep->frameLevel(); + + if (int(upvarLevel) <= fnlevel) fn->setFunArg(); - queue->push(funbox); - fnlevel = int(funbox->level); - break; - } + + uintN skip = (funbox->level + 1) - upvarLevel; + if (skip < skipmin) + skipmin = skip; } } } - if (funbox->kids) - FindFunArgs(funbox->kids, fnlevel, queue); + /* + * If this function escapes, whether directly (the parser detects such + * escapes) or indirectly (because this non-escaping function uses an + * upvar that reaches across an outer function boundary where the outer + * function escapes), enqueue it for further analysis, and bump fnlevel + * to trap any non-escaping children. + */ + if (fn->isFunArg()) { + queue->push(funbox); + fnlevel = int(funbox->level); + } + + /* + * Now process the current function's children, and recalibrate their + * cumulative skipmin to be relative to the current static level. + */ + if (funbox->kids) { + uintN kidskipmin = FindFunArgs(funbox->kids, fnlevel, queue); + + JS_ASSERT(kidskipmin != 0); + if (kidskipmin != FREE_STATIC_LEVEL) { + --kidskipmin; + if (kidskipmin != 0 && kidskipmin < skipmin) + skipmin = kidskipmin; + } + } + + /* + * Finally, after we've traversed all of the current function's kids, + * minimize fun's skipmin against our accumulated skipmin. Do likewise + * with allskipmin, but minimize across funbox and all of its siblings, + * to compute our return value. + */ + if (skipmin != FREE_STATIC_LEVEL) { + fun->u.i.skipmin = skipmin; + if (skipmin < allskipmin) + allskipmin = skipmin; + } } while ((funbox = funbox->siblings) != NULL); + + return allskipmin; } bool @@ -1767,13 +1819,14 @@ JSCompiler::markFunArgs(JSFunctionBox *funbox, uintN tcflags) !lexdep->isFunArg() && lexdep->kind() == JSDefinition::FUNCTION) { /* - * Mark this formerly-Algol-like function as a funarg, - * since it is referenced from a funarg and can no longer - * use JSOP_{GET,CALL}UPVAR to access upvars. + * Mark this formerly-Algol-like function as an escaping + * function (i.e., as a funarg), because it is used from a + * funarg and therefore can not use JSOP_{GET,CALL}UPVAR to + * access upvars. * - * Progress is guaranteed since we set PND_FUNARG here, - * which suppresses revisiting this function (namely the - * !lexdep->isFunArg() test just above). + * Progress is guaranteed because we set the funarg flag + * here, which suppresses revisiting this function (thanks + * to the !lexdep->isFunArg() test just above). */ lexdep->setFunArg(); diff --git a/js/src/jsscript.cpp b/js/src/jsscript.cpp index 85ad5bde56e2..1028f806e4d3 100644 --- a/js/src/jsscript.cpp +++ b/js/src/jsscript.cpp @@ -1530,6 +1530,8 @@ js_NewScriptFromCG(JSContext *cx, JSCodeGenerator *cg) cg->regexpList.finish(JS_SCRIPT_REGEXPS(script)); if (cg->flags & TCF_NO_SCRIPT_RVAL) script->flags |= JSSF_NO_SCRIPT_RVAL; + if ((cg->flags & TCF_COMPILE_N_GO) && cg->compiler->callerFrame) + script->flags |= JSSF_ESCAPE_HAZARD; if (cg->upvarList.count != 0) { JS_ASSERT(cg->upvarList.count <= cg->upvarMap.length); diff --git a/js/src/jsscript.h b/js/src/jsscript.h index f46d7dc5b119..223b0a0a7a4b 100644 --- a/js/src/jsscript.h +++ b/js/src/jsscript.h @@ -135,6 +135,9 @@ struct JSScript { #define JSSF_NO_SCRIPT_RVAL 0x01 /* no need for result value of last expression statement */ #define JSSF_SAVED_CALLER_FUN 0x02 /* object 0 is caller function */ +#define JSSF_ESCAPE_HAZARD 0x04 /* script (including functions) was + created by dynamically scoped eval + or debugger eval-in-frame API */ static JS_INLINE uintN StackDepth(JSScript *script) diff --git a/js/src/jstracer.cpp b/js/src/jstracer.cpp index 2c5f4f711239..6dbc60e9d28d 100644 --- a/js/src/jstracer.cpp +++ b/js/src/jstracer.cpp @@ -10797,6 +10797,19 @@ TraceRecorder::record_JSOP_LOOP() return JSRS_CONTINUE; } +#define DBG_STUB(OP) \ + JS_REQUIRES_STACK JSRecordingStatus \ + TraceRecorder::record_##OP() \ + { \ + ABORT_TRACE("can't trace " #OP); \ + } + +DBG_STUB(JSOP_GETUPVAR_DBG) +DBG_STUB(JSOP_CALLUPVAR_DBG) +DBG_STUB(JSOP_DEFFUN_DBGFC) +DBG_STUB(JSOP_DEFLOCALFUN_DBGFC) +DBG_STUB(JSOP_LAMBDA_DBGFC) + #ifdef JS_JIT_SPEW /* Prints information about entry typemaps and unstable exits for all peers at a PC */ void diff --git a/js/src/jsxdrapi.h b/js/src/jsxdrapi.h index daaa338ddc7b..f20bc57b3ee9 100644 --- a/js/src/jsxdrapi.h +++ b/js/src/jsxdrapi.h @@ -204,7 +204,7 @@ JS_XDRFindClassById(JSXDRState *xdr, uint32 id); * before deserialization of bytecode. If the saved version does not match * the current version, abort deserialization and invalidate the file. */ -#define JSXDR_BYTECODE_VERSION (0xb973c0de - 48) +#define JSXDR_BYTECODE_VERSION (0xb973c0de - 49) /* * Library-private functions.