From fddfd6f3b5579bb251e16f894613dd1660bd60da Mon Sep 17 00:00:00 2001 From: Kevin B Kenny Date: Wed, 3 Nov 2010 00:58:04 +0000 Subject: * generic/tclCompCmds.c (TclCompileCatchCmd): * tests/compile.test (compile-3,6): Reworked the compilation of the [catch] command so as to avoid placing any code that might throw an exception (specifically, any initial substitutions or any stores to result or options variables) between the BEGIN_CATCH and END_CATCH but outside the exception range. Added a test case that panics on a stack smash if the change is not made. [Bug #3098302] --- ChangeLog | 11 +++ generic/tclCompCmds.c | 183 +++++++++++++++++++++++++++++++------------------- tests/compile.test | 32 ++++++++- 3 files changed, 155 insertions(+), 71 deletions(-) diff --git a/ChangeLog b/ChangeLog index 8306091..41de67c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,14 @@ +2010-11-03 Kevin B. Kenny + + * generic/tclCompCmds.c (TclCompileCatchCmd): + * tests/compile.test (compile-3,6): Reworked the compilation of + the [catch] command so as to avoid placing any code that might + throw an exception (specifically, any initial substitutions + or any stores to result or options variables) between the + BEGIN_CATCH and END_CATCH but outside the exception range. + Added a test case that panics on a stack smash if the change + is not made. [Bug #3098302] + 2010-11-01 Stuart Cassoff * library/safe.tcl: Improved handling of non-standard module diff --git a/generic/tclCompCmds.c b/generic/tclCompCmds.c index d01964d..c520aef 100644 --- a/generic/tclCompCmds.c +++ b/generic/tclCompCmds.c @@ -12,7 +12,7 @@ * See the file "license.terms" for information on usage and redistribution of * this file, and for a DISCLAIMER OF ALL WARRANTIES. * - * RCS: @(#) $Id: tclCompCmds.c,v 1.143.2.2 2009/08/25 21:01:05 andreas_kupries Exp $ + * RCS: @(#) $Id: tclCompCmds.c,v 1.143.2.3 2010/11/03 00:58:04 kennykb Exp $ */ #include "tclInt.h" @@ -360,8 +360,8 @@ TclCompileBreakCmd( * Procedure called to compile the "catch" command. * * Results: - * Returns TCL_OK for a successful compile. Returns TCL_ERROR to defer - * evaluation to runtime. + * Returns TCL_OK for a successful compile. Returns TCL_ERROR to defer + * evaluation to runtime. * * Side effects: * Instructions are added to envPtr to execute the "catch" command at @@ -383,7 +383,8 @@ TclCompileCatchCmd( Tcl_Token *cmdTokenPtr, *resultNameTokenPtr, *optsNameTokenPtr; const char *name; int resultIndex, optsIndex, nameChars, range; - int savedStackDepth = envPtr->currStackDepth; + int initStackDepth = envPtr->currStackDepth; + int savedStackDepth; DefineLineInformation; /* TIP #280 */ /* @@ -425,6 +426,9 @@ TclCompileCatchCmd( } resultIndex = TclFindCompiledLocal(resultNameTokenPtr[1].start, resultNameTokenPtr[1].size, /*create*/ 1, envPtr->procPtr); + if (resultIndex < 0) { + return TCL_ERROR; + } /* DKF */ if (parsePtr->numWords == 4) { @@ -439,116 +443,155 @@ TclCompileCatchCmd( } optsIndex = TclFindCompiledLocal(optsNameTokenPtr[1].start, optsNameTokenPtr[1].size, /*create*/ 1, envPtr->procPtr); + if (optsIndex < 0) { + return TCL_ERROR; + } } } /* - * We will compile the catch command. Emit a beginCatch instruction at the - * start of the catch body: the subcommand it controls. + * We will compile the catch command. Declare the exception range + * that it uses. */ range = DeclareExceptionRange(envPtr, CATCH_EXCEPTION_RANGE); - TclEmitInstInt4(INST_BEGIN_CATCH4, range, envPtr); /* - * If the body is a simple word, compile the instructions to eval it. - * Otherwise, compile instructions to substitute its text without - * catching, a catch instruction that resets the stack to what it was - * before substituting the body, and then an instruction to eval the body. - * Care has to be taken to register the correct startOffset for the catch - * range so that errors in the substitution are not catched [Bug 219184] + * If the body is a simple word, compile a BEGIN_CATCH instruction, + * followed by the instructions to eval the body. + * Otherwise, compile instructions to substitute the body text before + * starting the catch, then BEGIN_CATCH, and then EVAL_STK to + * evaluate the substituted body. + * Care has to be taken to make sure that substitution happens outside + * the catch range so that errors in the substitution are not caught. + * [Bug 219184] + * The reason for duplicating the script is that EVAL_STK would otherwise + * begin by undeflowing the stack below the mark set by BEGIN_CATCH4. */ - SetLineInformation (1); + SetLineInformation(1); if (cmdTokenPtr->type == TCL_TOKEN_SIMPLE_WORD) { + savedStackDepth = envPtr->currStackDepth; + TclEmitInstInt4(INST_BEGIN_CATCH4, range, envPtr); ExceptionRangeStarts(envPtr, range); CompileBody(envPtr, cmdTokenPtr, interp); - ExceptionRangeEnds(envPtr, range); } else { CompileTokens(envPtr, cmdTokenPtr, interp); + savedStackDepth = envPtr->currStackDepth; + TclEmitInstInt4(INST_BEGIN_CATCH4, range, envPtr); ExceptionRangeStarts(envPtr, range); + TclEmitOpcode(INST_DUP, envPtr); TclEmitOpcode(INST_EVAL_STK, envPtr); - ExceptionRangeEnds(envPtr, range); + } + /* Stack at this point: + * nonsimple: script result + * simple: result + */ + + /* + * Emit the "no errors" epilogue: push "0" (TCL_OK) as the catch + * result, and jump around the "error case" code. + */ + + PushLiteral(envPtr, "0", 1); + TclEmitForwardJump(envPtr, TCL_UNCONDITIONAL_JUMP, &jumpFixup); + /* Stack at this point: ?script? result TCL_OK */ + + /* + * Emit the "error case" epilogue. Push the interpreter result + * and the return code. + */ + + envPtr->currStackDepth = savedStackDepth; + ExceptionRangeTarget(envPtr, range, catchOffset); + /* Stack at this point: ?script? */ + TclEmitOpcode(INST_PUSH_RESULT, envPtr); + TclEmitOpcode(INST_PUSH_RETURN_CODE, envPtr); + + /* + * Update the target of the jump after the "no errors" code. + */ + + /* Stack at this point: ?script? result returnCode */ + if (TclFixupForwardJumpToHere(envPtr, &jumpFixup, 127)) { + Tcl_Panic("TclCompileCatchCmd: bad jump distance %d", + CurrentOffset(envPtr) - jumpFixup.codeOffset); + } + + /* Push the return options if the caller wants them */ + + if (optsIndex != -1) { + TclEmitOpcode(INST_PUSH_RETURN_OPTIONS, envPtr); + } + + /* + * End the catch + */ + + ExceptionRangeEnds(envPtr, range); + TclEmitOpcode(INST_END_CATCH, envPtr); + + /* + * At this point, the top of the stack is inconveniently ordered: + * ?script? result returnCode ?returnOptions? + * Reverse the stack to bring the result to the top. + */ + + if (optsIndex != -1) { + TclEmitInstInt4(INST_REVERSE, 3, envPtr); + } else { + TclEmitInstInt4(INST_REVERSE, 2, envPtr); } /* - * The "no errors" epilogue code: store the body's result into the - * variable (if any), push "0" (TCL_OK) as the catch's "no error" result, - * and jump around the "error case" code. Note that we issue the push of - * the return options first so that if alterations happen to the current - * interpreter state during the writing of the variable, we won't see - * them; this results in a slightly complex instruction issuing flow - * (can't exchange, only duplicate and pop). + * Store the result if requested, and remove it from the stack */ if (resultIndex != -1) { - if (optsIndex != -1) { - TclEmitOpcode(INST_PUSH_RETURN_OPTIONS, envPtr); - TclEmitInstInt4(INST_OVER, 1, envPtr); - } if (resultIndex <= 255) { TclEmitInstInt1(INST_STORE_SCALAR1, resultIndex, envPtr); } else { TclEmitInstInt4(INST_STORE_SCALAR4, resultIndex, envPtr); } - if (optsIndex != -1) { - TclEmitOpcode(INST_POP, envPtr); - if (optsIndex <= 255) { - TclEmitInstInt1(INST_STORE_SCALAR1, optsIndex, envPtr); - } else { - TclEmitInstInt4(INST_STORE_SCALAR4, optsIndex, envPtr); - } - TclEmitOpcode(INST_POP, envPtr); - } } TclEmitOpcode(INST_POP, envPtr); - PushLiteral(envPtr, "0", 1); - TclEmitForwardJump(envPtr, TCL_UNCONDITIONAL_JUMP, &jumpFixup); /* - * The "error case" code: store the body's result into the variable (if - * any), then push the error result code. The initial PC offset here is - * the catch's error target. Note that if we are saving the return - * options, we do that first so the preservation cannot get affected by - * any intermediate result handling. + * Stack is now ?script? result returnCode. + * If the options dict has been requested, it is buried on the stack + * under the return code. Reverse the stack to bring it to the top, + * store it and remove it from the stack. */ - envPtr->currStackDepth = savedStackDepth; - ExceptionRangeTarget(envPtr, range, catchOffset); - if (resultIndex != -1) { - if (optsIndex != -1) { - TclEmitOpcode(INST_PUSH_RETURN_OPTIONS, envPtr); - } - TclEmitOpcode(INST_PUSH_RESULT, envPtr); - if (resultIndex <= 255) { - TclEmitInstInt1(INST_STORE_SCALAR1, resultIndex, envPtr); + if (optsIndex != -1) { + TclEmitInstInt4(INST_REVERSE, 2, envPtr); + if (optsIndex <= 255) { + TclEmitInstInt1(INST_STORE_SCALAR1, optsIndex, envPtr); } else { - TclEmitInstInt4(INST_STORE_SCALAR4, resultIndex, envPtr); + TclEmitInstInt4(INST_STORE_SCALAR4, optsIndex, envPtr); } TclEmitOpcode(INST_POP, envPtr); - if (optsIndex != -1) { - if (optsIndex <= 255) { - TclEmitInstInt1(INST_STORE_SCALAR1, optsIndex, envPtr); - } else { - TclEmitInstInt4(INST_STORE_SCALAR4, optsIndex, envPtr); - } - TclEmitOpcode(INST_POP, envPtr); - } } - TclEmitOpcode(INST_PUSH_RETURN_CODE, envPtr); - /* - * Update the target of the jump after the "no errors" code, then emit an - * endCatch instruction at the end of the catch command. + /* + * Stack is now ?script? result. Get rid of the subst'ed script + * if it's hanging arond. */ - if (TclFixupForwardJumpToHere(envPtr, &jumpFixup, 127)) { - Tcl_Panic("TclCompileCatchCmd: bad jump distance %d", - CurrentOffset(envPtr) - jumpFixup.codeOffset); + if (cmdTokenPtr->type != TCL_TOKEN_SIMPLE_WORD) { + TclEmitInstInt4(INST_REVERSE, 2, envPtr); + TclEmitOpcode(INST_POP, envPtr); } - TclEmitOpcode(INST_END_CATCH, envPtr); - envPtr->currStackDepth = savedStackDepth + 1; + /* + * Result of all this, on either branch, should have been to leave + * one operand -- the return code -- on the stack. + */ + + if (envPtr->currStackDepth != initStackDepth + 1) { + Tcl_Panic("in TclCompileCatchCmd, currStackDepth = %d should be %d", + envPtr->currStackDepth, initStackDepth+1); + } return TCL_OK; } diff --git a/tests/compile.test b/tests/compile.test index 57a1a3c..de77438 100644 --- a/tests/compile.test +++ b/tests/compile.test @@ -11,7 +11,7 @@ # See the file "license.terms" for information on usage and redistribution # of this file, and for a DISCLAIMER OF ALL WARRANTIES. # -# RCS: @(#) $Id: compile.test,v 1.48.2.2 2009/10/29 17:21:18 dgp Exp $ +# RCS: @(#) $Id: compile.test,v 1.48.2.3 2010/11/03 00:58:04 kennykb Exp $ package require tcltest 2 namespace import -force ::tcltest::* @@ -128,6 +128,36 @@ test compile-3.5 {TclCompileCatchCmd: recover from error, [Bug 705406]} { } list [catch foo msg] $msg } {0 1} +test compile-3.6 {TclCompileCatchCmd: error in storing result [Bug 3098302]} {*}{ + -setup { + namespace eval catchtest { + variable result1 {} + } + trace add variable catchtest::result1 write catchtest::failtrace + proc catchtest::failtrace {n1 n2 op} { + return -code error "trace on $n1 fails by request" + } + } + -body { + proc catchtest::x {} { + variable result1 + set count 0 + for {set i 0} {$i < 10} {incr i} { + set status2 [catch { + set status1 [catch { + return -code error -level 0 "original failure" + } result1 options1] + } result2 options2] + incr count + } + list $count $result2 + } + catchtest::x + } + -result {10 {can't set "result1": trace on result1 fails by request}} + -cleanup {namespace delete catchtest} +} + test compile-4.1 {TclCompileForCmd: command substituted test expression} { set i 0 -- cgit v0.12