From aec91119616af7e7eee5307009b9814fe10f860a Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Mon, 20 Jul 2020 14:51:15 +0300 Subject: [PATCH 1/5] vm: replace jumpIf with jump `jumpIf` is used only once with non-constant condition. --- pkg/vm/vm.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index 339ac96c4..627ed480c 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -1221,7 +1221,9 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro cond = getJumpCondition(op, a, b) } - v.jumpIf(ctx, offset, cond) + if cond { + v.jump(ctx, offset) + } case opcode.CALL, opcode.CALLL: v.checkInvocationStackSize() @@ -1231,7 +1233,7 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro v.istack.PushVal(newCtx) offset := v.getJumpOffset(newCtx, parameter) - v.jumpIf(newCtx, offset, true) + v.jump(newCtx, offset) case opcode.CALLA: ptr := v.estack.Pop().Item().(*stackitem.Pointer) @@ -1243,7 +1245,7 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro newCtx.local = nil newCtx.arguments = nil v.istack.PushVal(newCtx) - v.jumpIf(newCtx, ptr.Position(), true) + v.jump(newCtx, ptr.Position()) case opcode.SYSCALL: interopID := GetInteropID(parameter) @@ -1391,11 +1393,9 @@ func getJumpCondition(op opcode.Opcode, a, b *big.Int) bool { } } -// jumpIf performs jump to offset if cond is true. -func (v *VM) jumpIf(ctx *Context, offset int, cond bool) { - if cond { - ctx.nextip = offset - } +// jump performs jump to the offset. +func (v *VM) jump(ctx *Context, offset int) { + ctx.nextip = offset } // getJumpOffset returns instruction number in a current context From c0d7b9d234379fd5a86a776da54718d4c48425fc Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 22 Jul 2020 11:11:28 +0300 Subject: [PATCH 2/5] vm: clear references on context unload Remove argument and local references from the reference counter when returning from function. --- pkg/vm/slot.go | 7 +++++++ pkg/vm/vm.go | 16 +++++++++++++++- pkg/vm/vm_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/pkg/vm/slot.go b/pkg/vm/slot.go index 1f4107ee2..18a65ac85 100644 --- a/pkg/vm/slot.go +++ b/pkg/vm/slot.go @@ -45,3 +45,10 @@ func (s *Slot) Get(i int) stackitem.Item { // Size returns slot size. func (s *Slot) Size() int { return len(s.storage) } + +// Clear removes all slot variables from reference counter. +func (s *Slot) Clear() { + for _, item := range s.storage { + s.refs.Remove(item) + } +} diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index 627ed480c..cd2e0e74a 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -1262,9 +1262,10 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro } case opcode.RET: - v.istack.Pop() + oldCtx := v.istack.Pop().Value().(*Context) oldEstack := v.estack + v.unloadContext(oldCtx) if v.istack.Len() == 0 { v.state = haltState break @@ -1372,6 +1373,19 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro return } +func (v *VM) unloadContext(ctx *Context) { + if ctx.local != nil { + ctx.local.Clear() + } + if ctx.arguments != nil { + ctx.arguments.Clear() + } + currCtx := v.Context() + if ctx.static != nil && currCtx != nil && ctx.static != currCtx.static { + ctx.static.Clear() + } +} + // getJumpCondition performs opcode specific comparison of a and b func getJumpCondition(op opcode.Opcode, a, b *big.Int) bool { cmp := a.Cmp(b) diff --git a/pkg/vm/vm_test.go b/pkg/vm/vm_test.go index 8075e94e6..ca2c3cdca 100644 --- a/pkg/vm/vm_test.go +++ b/pkg/vm/vm_test.go @@ -1142,6 +1142,46 @@ func getTestFuncForVM(prog []byte, result interface{}, args ...interface{}) func return getCustomTestFuncForVM(prog, f, args...) } +func makeRETProgram(t *testing.T, argCount, localCount int) []byte { + require.True(t, argCount+localCount <= 255) + + fProg := []opcode.Opcode{opcode.INITSLOT, opcode.Opcode(localCount), opcode.Opcode(argCount)} + for i := 0; i < localCount; i++ { + fProg = append(fProg, opcode.PUSH8, opcode.STLOC, opcode.Opcode(i)) + } + fProg = append(fProg, opcode.RET) + + offset := uint32(len(fProg) + 5) + param := make([]byte, 4) + binary.LittleEndian.PutUint32(param, offset) + + ops := []opcode.Opcode{ + opcode.INITSSLOT, 0x01, + opcode.PUSHA, 11, 0, 0, 0, + opcode.STSFLD0, + opcode.JMPL, opcode.Opcode(param[0]), opcode.Opcode(param[1]), opcode.Opcode(param[2]), opcode.Opcode(param[3]), + } + ops = append(ops, fProg...) + + // execute func multiple times to ensure total reference count is less than max + callCount := MaxStackSize/(argCount+localCount) + 1 + args := make([]opcode.Opcode, argCount) + for i := range args { + args[i] = opcode.PUSH7 + } + for i := 0; i < callCount; i++ { + ops = append(ops, args...) + ops = append(ops, opcode.LDSFLD0, opcode.CALLA) + } + return makeProgram(ops...) +} + +func TestRETReferenceClear(t *testing.T) { + // 42 is a canary + t.Run("Argument", getTestFuncForVM(makeRETProgram(t, 100, 0), 42, 42)) + t.Run("Local", getTestFuncForVM(makeRETProgram(t, 0, 100), 42, 42)) +} + func TestNOTEQUALByteArray(t *testing.T) { prog := makeProgram(opcode.NOTEQUAL) t.Run("True", getTestFuncForVM(prog, true, []byte{1, 2}, []byte{0, 1, 2})) From 797324cb0413b2e015d420ca9fe123529c1c35a2 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 22 Jul 2020 12:05:46 +0300 Subject: [PATCH 3/5] vm: support exceptions Implement 3 new instructions: TRY,ENDTRY,ENDFINALLY. 1. TRY marks the start of the block where exceptions are catched. It has 2 arguments which are relative offsets of exception handler and the end of the whole try/catch construction. 2. ENDTRY denotes either end of try or catch block. 3. ENDFINALLY denotes end of finally block which is executed irregardless of whether an exception has occured. --- pkg/vm/context.go | 9 +- pkg/vm/exception.go | 85 +++++++++++ pkg/vm/opcode/opcode.go | 11 +- pkg/vm/opcode/opcode_string.go | 262 +++++++++++++++++---------------- pkg/vm/vm.go | 114 ++++++++++++-- pkg/vm/vm_test.go | 73 +++++++++ 6 files changed, 415 insertions(+), 139 deletions(-) create mode 100644 pkg/vm/exception.go diff --git a/pkg/vm/context.go b/pkg/vm/context.go index fba31f11e..ca56b42e1 100644 --- a/pkg/vm/context.go +++ b/pkg/vm/context.go @@ -33,6 +33,9 @@ type Context struct { local *Slot arguments *Slot + // Exception context stack pointer. + tryStack *Stack + // Script hash of the prog. scriptHash util.Uint160 @@ -103,14 +106,18 @@ func (c *Context) Next() (opcode.Opcode, []byte, error) { case opcode.JMP, opcode.JMPIF, opcode.JMPIFNOT, opcode.JMPEQ, opcode.JMPNE, opcode.JMPGT, opcode.JMPGE, opcode.JMPLT, opcode.JMPLE, opcode.CALL, opcode.ISTYPE, opcode.CONVERT, opcode.NEWARRAYT, + opcode.ENDTRY, opcode.INITSSLOT, opcode.LDSFLD, opcode.STSFLD, opcode.LDARG, opcode.STARG, opcode.LDLOC, opcode.STLOC: numtoread = 1 - case opcode.INITSLOT: + case opcode.INITSLOT, opcode.TRY: numtoread = 2 case opcode.JMPL, opcode.JMPIFL, opcode.JMPIFNOTL, opcode.JMPEQL, opcode.JMPNEL, opcode.JMPGTL, opcode.JMPGEL, opcode.JMPLTL, opcode.JMPLEL, + opcode.ENDTRYL, opcode.CALLL, opcode.SYSCALL, opcode.PUSHA: numtoread = 4 + case opcode.TRYL: + numtoread = 8 default: if instr <= opcode.PUSHINT256 { numtoread = 1 << instr diff --git a/pkg/vm/exception.go b/pkg/vm/exception.go new file mode 100644 index 000000000..73b8e23cc --- /dev/null +++ b/pkg/vm/exception.go @@ -0,0 +1,85 @@ +package vm + +import ( + "errors" + "math/big" + + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" +) + +// exceptionHandlingState represents state of the exception handling process. +type exceptionHandlingState byte + +const ( + eTry exceptionHandlingState = iota + eCatch + eFinally +) + +// exceptionHandlingContext represents context of the exception handling process. +type exceptionHandlingContext struct { + CatchOffset int + FinallyOffset int + EndOffset int + State exceptionHandlingState +} + +func newExceptionHandlingContext(cOffset, fOffset int) *exceptionHandlingContext { + return &exceptionHandlingContext{ + CatchOffset: cOffset, + FinallyOffset: fOffset, + EndOffset: -1, + State: eTry, + } +} + +// HasCatch returns true iff context has `catch` block. +func (c *exceptionHandlingContext) HasCatch() bool { return c.CatchOffset >= 0 } + +// HasFinally returns true iff context has `finally` block. +func (c *exceptionHandlingContext) HasFinally() bool { return c.FinallyOffset >= 0 } + +// String implements stackitem.Item interface. +func (c *exceptionHandlingContext) String() string { + return "exception handling context" +} + +// Value implements stackitem.Item interface. +func (c *exceptionHandlingContext) Value() interface{} { + return c +} + +// Dup implements stackitem.Item interface. +func (c *exceptionHandlingContext) Dup() stackitem.Item { + return c +} + +// Bool implements stackitem.Item interface. +func (c *exceptionHandlingContext) Bool() bool { + panic("can't convert exceptionHandlingContext to Bool") +} + +// TryBytes implements stackitem.Item interface. +func (c *exceptionHandlingContext) TryBytes() ([]byte, error) { + return nil, errors.New("can't convert exceptionHandlingContext to ByteArray") +} + +// TryInteger implements stackitem.Item interface. +func (c *exceptionHandlingContext) TryInteger() (*big.Int, error) { + return nil, errors.New("can't convert exceptionHandlingContext to Integer") +} + +// Type implements stackitem.Item interface. +func (c *exceptionHandlingContext) Type() stackitem.Type { + panic("exceptionHandlingContext cannot appear on evaluation stack") +} + +// Convert implements stackitem.Item interface. +func (c *exceptionHandlingContext) Convert(_ stackitem.Type) (stackitem.Item, error) { + panic("exceptionHandlingContext cannot be converted to anything") +} + +// Equals implements stackitem.Item interface. +func (c *exceptionHandlingContext) Equals(s stackitem.Item) bool { + return c == s +} diff --git a/pkg/vm/opcode/opcode.go b/pkg/vm/opcode/opcode.go index 76149a798..54aaf83ac 100644 --- a/pkg/vm/opcode/opcode.go +++ b/pkg/vm/opcode/opcode.go @@ -68,9 +68,14 @@ const ( CALLA Opcode = 0x36 // Exceptions - ABORT Opcode = 0x37 - ASSERT Opcode = 0x38 - THROW Opcode = 0x3A + ABORT Opcode = 0x37 + ASSERT Opcode = 0x38 + THROW Opcode = 0x3A + TRY Opcode = 0x3B + TRYL Opcode = 0x3C // TRY_L + ENDTRY Opcode = 0x3D + ENDTRYL Opcode = 0x3E // ENDTRY_L + ENDFINALLY Opcode = 0x3F RET Opcode = 0x40 SYSCALL Opcode = 0x41 diff --git a/pkg/vm/opcode/opcode_string.go b/pkg/vm/opcode/opcode_string.go index 6b44af438..36a7872a8 100644 --- a/pkg/vm/opcode/opcode_string.go +++ b/pkg/vm/opcode/opcode_string.go @@ -64,6 +64,11 @@ func _() { _ = x[ABORT-55] _ = x[ASSERT-56] _ = x[THROW-58] + _ = x[TRY-59] + _ = x[TRYL-60] + _ = x[ENDTRY-61] + _ = x[ENDTRYL-62] + _ = x[ENDFINALLY-63] _ = x[RET-64] _ = x[SYSCALL-65] _ = x[DEPTH-67] @@ -191,7 +196,7 @@ func _() { _ = x[CONVERT-219] } -const _Opcode_name = "PUSHINT8PUSHINT16PUSHINT32PUSHINT64PUSHINT128PUSHINT256PUSHAPUSHNULLPUSHDATA1PUSHDATA2PUSHDATA4PUSHM1PUSH0PUSH1PUSH2PUSH3PUSH4PUSH5PUSH6PUSH7PUSH8PUSH9PUSH10PUSH11PUSH12PUSH13PUSH14PUSH15PUSH16NOPJMPJMP_LJMPIFJMPIF_LJMPIFNOTJMPIFNOT_LJMPEQJMPEQ_LJMPNEJMPNE_LJMPGTJMPGT_LJMPGEJMPGE_LJMPLTJMPLT_LJMPLEJMPLE_LCALLCALL_LCALLAABORTASSERTTHROWRETSYSCALLDEPTHDROPNIPXDROPCLEARDUPOVERPICKTUCKSWAPROTROLLREVERSE3REVERSE4REVERSENINITSSLOTINITSLOTLDSFLD0LDSFLD1LDSFLD2LDSFLD3LDSFLD4LDSFLD5LDSFLD6LDSFLDSTSFLD0STSFLD1STSFLD2STSFLD3STSFLD4STSFLD5STSFLD6STSFLDLDLOC0LDLOC1LDLOC2LDLOC3LDLOC4LDLOC5LDLOC6LDLOCSTLOC0STLOC1STLOC2STLOC3STLOC4STLOC5STLOC6STLOCLDARG0LDARG1LDARG2LDARG3LDARG4LDARG5LDARG6LDARGSTARG0STARG1STARG2STARG3STARG4STARG5STARG6STARGNEWBUFFERMEMCPYCATSUBSTRLEFTRIGHTINVERTANDORXOREQUALNOTEQUALSIGNABSNEGATEINCDECADDSUBMULDIVMODSHLSHRNOTBOOLANDBOOLORNZNUMEQUALNUMNOTEQUALLTLTEGTGTEMINMAXWITHINPACKUNPACKNEWARRAY0NEWARRAYNEWARRAY_TNEWSTRUCT0NEWSTRUCTNEWMAPSIZEHASKEYKEYSVALUESPICKITEMAPPENDSETITEMREVERSEITEMSREMOVECLEARITEMSISNULLISTYPECONVERT" +const _Opcode_name = "PUSHINT8PUSHINT16PUSHINT32PUSHINT64PUSHINT128PUSHINT256PUSHAPUSHNULLPUSHDATA1PUSHDATA2PUSHDATA4PUSHM1PUSH0PUSH1PUSH2PUSH3PUSH4PUSH5PUSH6PUSH7PUSH8PUSH9PUSH10PUSH11PUSH12PUSH13PUSH14PUSH15PUSH16NOPJMPJMP_LJMPIFJMPIF_LJMPIFNOTJMPIFNOT_LJMPEQJMPEQ_LJMPNEJMPNE_LJMPGTJMPGT_LJMPGEJMPGE_LJMPLTJMPLT_LJMPLEJMPLE_LCALLCALL_LCALLAABORTASSERTTHROWTRYTRY_LENDTRYENDTRY_LENDFINALLYRETSYSCALLDEPTHDROPNIPXDROPCLEARDUPOVERPICKTUCKSWAPROTROLLREVERSE3REVERSE4REVERSENINITSSLOTINITSLOTLDSFLD0LDSFLD1LDSFLD2LDSFLD3LDSFLD4LDSFLD5LDSFLD6LDSFLDSTSFLD0STSFLD1STSFLD2STSFLD3STSFLD4STSFLD5STSFLD6STSFLDLDLOC0LDLOC1LDLOC2LDLOC3LDLOC4LDLOC5LDLOC6LDLOCSTLOC0STLOC1STLOC2STLOC3STLOC4STLOC5STLOC6STLOCLDARG0LDARG1LDARG2LDARG3LDARG4LDARG5LDARG6LDARGSTARG0STARG1STARG2STARG3STARG4STARG5STARG6STARGNEWBUFFERMEMCPYCATSUBSTRLEFTRIGHTINVERTANDORXOREQUALNOTEQUALSIGNABSNEGATEINCDECADDSUBMULDIVMODSHLSHRNOTBOOLANDBOOLORNZNUMEQUALNUMNOTEQUALLTLTEGTGTEMINMAXWITHINPACKUNPACKNEWARRAY0NEWARRAYNEWARRAY_TNEWSTRUCT0NEWSTRUCTNEWMAPSIZEHASKEYKEYSVALUESPICKITEMAPPENDSETITEMREVERSEITEMSREMOVECLEARITEMSISNULLISTYPECONVERT" var _Opcode_map = map[Opcode]string{ 0: _Opcode_name[0:8], @@ -248,131 +253,136 @@ var _Opcode_map = map[Opcode]string{ 55: _Opcode_name[321:326], 56: _Opcode_name[326:332], 58: _Opcode_name[332:337], - 64: _Opcode_name[337:340], - 65: _Opcode_name[340:347], - 67: _Opcode_name[347:352], - 69: _Opcode_name[352:356], - 70: _Opcode_name[356:359], - 72: _Opcode_name[359:364], - 73: _Opcode_name[364:369], - 74: _Opcode_name[369:372], - 75: _Opcode_name[372:376], - 77: _Opcode_name[376:380], - 78: _Opcode_name[380:384], - 80: _Opcode_name[384:388], - 81: _Opcode_name[388:391], - 82: _Opcode_name[391:395], - 83: _Opcode_name[395:403], - 84: _Opcode_name[403:411], - 85: _Opcode_name[411:419], - 86: _Opcode_name[419:428], - 87: _Opcode_name[428:436], - 88: _Opcode_name[436:443], - 89: _Opcode_name[443:450], - 90: _Opcode_name[450:457], - 91: _Opcode_name[457:464], - 92: _Opcode_name[464:471], - 93: _Opcode_name[471:478], - 94: _Opcode_name[478:485], - 95: _Opcode_name[485:491], - 96: _Opcode_name[491:498], - 97: _Opcode_name[498:505], - 98: _Opcode_name[505:512], - 99: _Opcode_name[512:519], - 100: _Opcode_name[519:526], - 101: _Opcode_name[526:533], - 102: _Opcode_name[533:540], - 103: _Opcode_name[540:546], - 104: _Opcode_name[546:552], - 105: _Opcode_name[552:558], - 106: _Opcode_name[558:564], - 107: _Opcode_name[564:570], - 108: _Opcode_name[570:576], - 109: _Opcode_name[576:582], - 110: _Opcode_name[582:588], - 111: _Opcode_name[588:593], - 112: _Opcode_name[593:599], - 113: _Opcode_name[599:605], - 114: _Opcode_name[605:611], - 115: _Opcode_name[611:617], - 116: _Opcode_name[617:623], - 117: _Opcode_name[623:629], - 118: _Opcode_name[629:635], - 119: _Opcode_name[635:640], - 120: _Opcode_name[640:646], - 121: _Opcode_name[646:652], - 122: _Opcode_name[652:658], - 123: _Opcode_name[658:664], - 124: _Opcode_name[664:670], - 125: _Opcode_name[670:676], - 126: _Opcode_name[676:682], - 127: _Opcode_name[682:687], - 128: _Opcode_name[687:693], - 129: _Opcode_name[693:699], - 130: _Opcode_name[699:705], - 131: _Opcode_name[705:711], - 132: _Opcode_name[711:717], - 133: _Opcode_name[717:723], - 134: _Opcode_name[723:729], - 135: _Opcode_name[729:734], - 136: _Opcode_name[734:743], - 137: _Opcode_name[743:749], - 139: _Opcode_name[749:752], - 140: _Opcode_name[752:758], - 141: _Opcode_name[758:762], - 142: _Opcode_name[762:767], - 144: _Opcode_name[767:773], - 145: _Opcode_name[773:776], - 146: _Opcode_name[776:778], - 147: _Opcode_name[778:781], - 151: _Opcode_name[781:786], - 152: _Opcode_name[786:794], - 153: _Opcode_name[794:798], - 154: _Opcode_name[798:801], - 155: _Opcode_name[801:807], - 156: _Opcode_name[807:810], - 157: _Opcode_name[810:813], - 158: _Opcode_name[813:816], - 159: _Opcode_name[816:819], - 160: _Opcode_name[819:822], - 161: _Opcode_name[822:825], - 162: _Opcode_name[825:828], - 168: _Opcode_name[828:831], - 169: _Opcode_name[831:834], - 170: _Opcode_name[834:837], - 171: _Opcode_name[837:844], - 172: _Opcode_name[844:850], - 177: _Opcode_name[850:852], - 179: _Opcode_name[852:860], - 180: _Opcode_name[860:871], - 181: _Opcode_name[871:873], - 182: _Opcode_name[873:876], - 183: _Opcode_name[876:878], - 184: _Opcode_name[878:881], - 185: _Opcode_name[881:884], - 186: _Opcode_name[884:887], - 187: _Opcode_name[887:893], - 192: _Opcode_name[893:897], - 193: _Opcode_name[897:903], - 194: _Opcode_name[903:912], - 195: _Opcode_name[912:920], - 196: _Opcode_name[920:930], - 197: _Opcode_name[930:940], - 198: _Opcode_name[940:949], - 200: _Opcode_name[949:955], - 202: _Opcode_name[955:959], - 203: _Opcode_name[959:965], - 204: _Opcode_name[965:969], - 205: _Opcode_name[969:975], - 206: _Opcode_name[975:983], - 207: _Opcode_name[983:989], - 208: _Opcode_name[989:996], - 209: _Opcode_name[996:1008], - 210: _Opcode_name[1008:1014], - 211: _Opcode_name[1014:1024], - 216: _Opcode_name[1024:1030], - 217: _Opcode_name[1030:1036], - 219: _Opcode_name[1036:1043], + 59: _Opcode_name[337:340], + 60: _Opcode_name[340:345], + 61: _Opcode_name[345:351], + 62: _Opcode_name[351:359], + 63: _Opcode_name[359:369], + 64: _Opcode_name[369:372], + 65: _Opcode_name[372:379], + 67: _Opcode_name[379:384], + 69: _Opcode_name[384:388], + 70: _Opcode_name[388:391], + 72: _Opcode_name[391:396], + 73: _Opcode_name[396:401], + 74: _Opcode_name[401:404], + 75: _Opcode_name[404:408], + 77: _Opcode_name[408:412], + 78: _Opcode_name[412:416], + 80: _Opcode_name[416:420], + 81: _Opcode_name[420:423], + 82: _Opcode_name[423:427], + 83: _Opcode_name[427:435], + 84: _Opcode_name[435:443], + 85: _Opcode_name[443:451], + 86: _Opcode_name[451:460], + 87: _Opcode_name[460:468], + 88: _Opcode_name[468:475], + 89: _Opcode_name[475:482], + 90: _Opcode_name[482:489], + 91: _Opcode_name[489:496], + 92: _Opcode_name[496:503], + 93: _Opcode_name[503:510], + 94: _Opcode_name[510:517], + 95: _Opcode_name[517:523], + 96: _Opcode_name[523:530], + 97: _Opcode_name[530:537], + 98: _Opcode_name[537:544], + 99: _Opcode_name[544:551], + 100: _Opcode_name[551:558], + 101: _Opcode_name[558:565], + 102: _Opcode_name[565:572], + 103: _Opcode_name[572:578], + 104: _Opcode_name[578:584], + 105: _Opcode_name[584:590], + 106: _Opcode_name[590:596], + 107: _Opcode_name[596:602], + 108: _Opcode_name[602:608], + 109: _Opcode_name[608:614], + 110: _Opcode_name[614:620], + 111: _Opcode_name[620:625], + 112: _Opcode_name[625:631], + 113: _Opcode_name[631:637], + 114: _Opcode_name[637:643], + 115: _Opcode_name[643:649], + 116: _Opcode_name[649:655], + 117: _Opcode_name[655:661], + 118: _Opcode_name[661:667], + 119: _Opcode_name[667:672], + 120: _Opcode_name[672:678], + 121: _Opcode_name[678:684], + 122: _Opcode_name[684:690], + 123: _Opcode_name[690:696], + 124: _Opcode_name[696:702], + 125: _Opcode_name[702:708], + 126: _Opcode_name[708:714], + 127: _Opcode_name[714:719], + 128: _Opcode_name[719:725], + 129: _Opcode_name[725:731], + 130: _Opcode_name[731:737], + 131: _Opcode_name[737:743], + 132: _Opcode_name[743:749], + 133: _Opcode_name[749:755], + 134: _Opcode_name[755:761], + 135: _Opcode_name[761:766], + 136: _Opcode_name[766:775], + 137: _Opcode_name[775:781], + 139: _Opcode_name[781:784], + 140: _Opcode_name[784:790], + 141: _Opcode_name[790:794], + 142: _Opcode_name[794:799], + 144: _Opcode_name[799:805], + 145: _Opcode_name[805:808], + 146: _Opcode_name[808:810], + 147: _Opcode_name[810:813], + 151: _Opcode_name[813:818], + 152: _Opcode_name[818:826], + 153: _Opcode_name[826:830], + 154: _Opcode_name[830:833], + 155: _Opcode_name[833:839], + 156: _Opcode_name[839:842], + 157: _Opcode_name[842:845], + 158: _Opcode_name[845:848], + 159: _Opcode_name[848:851], + 160: _Opcode_name[851:854], + 161: _Opcode_name[854:857], + 162: _Opcode_name[857:860], + 168: _Opcode_name[860:863], + 169: _Opcode_name[863:866], + 170: _Opcode_name[866:869], + 171: _Opcode_name[869:876], + 172: _Opcode_name[876:882], + 177: _Opcode_name[882:884], + 179: _Opcode_name[884:892], + 180: _Opcode_name[892:903], + 181: _Opcode_name[903:905], + 182: _Opcode_name[905:908], + 183: _Opcode_name[908:910], + 184: _Opcode_name[910:913], + 185: _Opcode_name[913:916], + 186: _Opcode_name[916:919], + 187: _Opcode_name[919:925], + 192: _Opcode_name[925:929], + 193: _Opcode_name[929:935], + 194: _Opcode_name[935:944], + 195: _Opcode_name[944:952], + 196: _Opcode_name[952:962], + 197: _Opcode_name[962:972], + 198: _Opcode_name[972:981], + 200: _Opcode_name[981:987], + 202: _Opcode_name[987:991], + 203: _Opcode_name[991:997], + 204: _Opcode_name[997:1001], + 205: _Opcode_name[1001:1007], + 206: _Opcode_name[1007:1015], + 207: _Opcode_name[1015:1021], + 208: _Opcode_name[1021:1028], + 209: _Opcode_name[1028:1040], + 210: _Opcode_name[1040:1046], + 211: _Opcode_name[1046:1056], + 216: _Opcode_name[1056:1062], + 217: _Opcode_name[1062:1068], + 219: _Opcode_name[1068:1075], } func (i Opcode) String() string { diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index cd2e0e74a..ea124a795 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -71,6 +71,8 @@ type VM struct { istack *Stack // invocation stack. estack *Stack // execution stack. + uncaughtException stackitem.Item // exception being handled + refs *refCounter gasConsumed int64 @@ -193,13 +195,12 @@ func (v *VM) PrintOps() { opcode.JMPL, opcode.JMPIFL, opcode.JMPIFNOTL, opcode.CALLL, opcode.JMPEQL, opcode.JMPNEL, opcode.JMPGTL, opcode.JMPGEL, opcode.JMPLEL, opcode.JMPLTL, - opcode.PUSHA: - offset, rOffset, err := v.calcJumpOffset(ctx, parameter) - if err != nil { - desc = fmt.Sprintf("ERROR: %v", err) - } else { - desc = fmt.Sprintf("%d (%d/%x)", offset, rOffset, parameter) - } + opcode.PUSHA, opcode.ENDTRY, opcode.ENDTRYL: + desc = v.getOffsetDesc(ctx, parameter) + case opcode.TRY, opcode.TRYL: + catchP, finallyP := getTryParams(instr, parameter) + desc = fmt.Sprintf("catch %s, finally %s", + v.getOffsetDesc(ctx, catchP), v.getOffsetDesc(ctx, finallyP)) case opcode.INITSSLOT: desc = fmt.Sprint(parameter[0]) case opcode.INITSLOT: @@ -223,6 +224,14 @@ func (v *VM) PrintOps() { w.Flush() } +func (v *VM) getOffsetDesc(ctx *Context, parameter []byte) string { + offset, rOffset, err := v.calcJumpOffset(ctx, parameter) + if err != nil { + return fmt.Sprintf("ERROR: %v", err) + } + return fmt.Sprintf("%d (%d/%x)", offset, rOffset, parameter) +} + // AddBreakPoint adds a breakpoint to the current context. func (v *VM) AddBreakPoint(n int) { ctx := v.Context() @@ -271,6 +280,7 @@ func (v *VM) LoadScript(b []byte) { func (v *VM) LoadScriptWithFlags(b []byte, f smartcontract.CallFlag) { ctx := NewContext(b) ctx.estack = v.estack + ctx.tryStack = NewStack("exception") ctx.callFlag = f v.istack.PushVal(ctx) } @@ -1357,7 +1367,7 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro // unlucky ^^ case opcode.THROW: - panic("THROW") + v.throw(v.estack.Pop().Item()) case opcode.ABORT: panic("ABORT") @@ -1367,6 +1377,43 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro panic("ASSERT failed") } + case opcode.TRY, opcode.TRYL: + catchP, finallyP := getTryParams(op, parameter) + cOffset := v.getJumpOffset(ctx, catchP) + fOffset := v.getJumpOffset(ctx, finallyP) + if cOffset == 0 && fOffset == 0 { + panic("invalid offset for TRY*") + } else if cOffset == ctx.ip { + cOffset = -1 + } else if fOffset == ctx.ip { + fOffset = -1 + } + eCtx := newExceptionHandlingContext(cOffset, fOffset) + ctx.tryStack.PushVal(eCtx) + + case opcode.ENDTRY, opcode.ENDTRYL: + eCtx := ctx.tryStack.Peek(0).Value().(*exceptionHandlingContext) + if eCtx.State == eFinally { + panic("invalid exception handling state during ENDTRY*") + } + eOffset := v.getJumpOffset(ctx, parameter) + if eCtx.HasFinally() { + eCtx.State = eFinally + eCtx.EndOffset = eOffset + eOffset = eCtx.FinallyOffset + } else { + ctx.tryStack.Pop() + } + v.jump(ctx, eOffset) + + case opcode.ENDFINALLY: + if v.uncaughtException != nil { + v.handleException() + return + } + eCtx := ctx.tryStack.Pop().Value().(*exceptionHandlingContext) + v.jump(ctx, eCtx.EndOffset) + default: panic(fmt.Sprintf("unknown opcode %s", op.String())) } @@ -1386,6 +1433,15 @@ func (v *VM) unloadContext(ctx *Context) { } } +// getTryParams splits TRY(L) instruction parameter into offsets for catch and finally blocks. +func getTryParams(op opcode.Opcode, p []byte) ([]byte, []byte) { + i := 1 + if op == opcode.TRYL { + i = 4 + } + return p[:i], p[i:] +} + // getJumpCondition performs opcode specific comparison of a and b func getJumpCondition(op opcode.Opcode, a, b *big.Int) bool { cmp := a.Cmp(b) @@ -1407,6 +1463,11 @@ func getJumpCondition(op opcode.Opcode, a, b *big.Int) bool { } } +func (v *VM) throw(item stackitem.Item) { + v.uncaughtException = item + v.handleException() +} + // jump performs jump to the offset. func (v *VM) jump(ctx *Context, offset int) { ctx.nextip = offset @@ -1424,6 +1485,8 @@ func (v *VM) getJumpOffset(ctx *Context, parameter []byte) int { return offset } +// calcJumpOffset returns absolute and relative offset of JMP/CALL/TRY instructions +// either in short (1-byte) or long (4-byte) form. func (v *VM) calcJumpOffset(ctx *Context, parameter []byte) (int, int, error) { var rOffset int32 switch l := len(parameter); l { @@ -1432,7 +1495,8 @@ func (v *VM) calcJumpOffset(ctx *Context, parameter []byte) (int, int, error) { case 4: rOffset = int32(binary.LittleEndian.Uint32(parameter)) default: - return 0, 0, fmt.Errorf("invalid JMP* parameter length: %d", l) + _, curr := ctx.CurrInstr() + return 0, 0, fmt.Errorf("invalid %s parameter length: %d", curr, l) } offset := ctx.ip + int(rOffset) if offset < 0 || offset > len(ctx.prog) { @@ -1442,6 +1506,38 @@ func (v *VM) calcJumpOffset(ctx *Context, parameter []byte) (int, int, error) { return offset, int(rOffset), nil } +func (v *VM) handleException() { + pop := 0 + ictx := v.istack.Peek(0).Value().(*Context) + for ictx != nil { + e := ictx.tryStack.Peek(pop) + for e != nil { + ectx := e.Value().(*exceptionHandlingContext) + if ectx.State == eFinally || (ectx.State == eCatch && !ectx.HasFinally()) { + ictx.tryStack.Pop() + e = ictx.tryStack.Peek(0) + continue + } + for i := 0; i < pop; i++ { + ctx := v.istack.Pop().Value().(*Context) + v.unloadContext(ctx) + } + if ectx.State == eTry && ectx.HasCatch() { + ectx.State = eCatch + v.estack.PushVal(v.uncaughtException) + v.uncaughtException = nil + v.jump(ictx, ectx.CatchOffset) + } else { + ectx.State = eFinally + v.jump(ictx, ectx.FinallyOffset) + } + return + } + pop++ + ictx = v.istack.Peek(pop).Value().(*Context) + } +} + // CheckMultisigPar checks if sigs contains sufficient valid signatures. func CheckMultisigPar(v *VM, curve elliptic.Curve, h []byte, pkeys [][]byte, sigs [][]byte) bool { if len(sigs) == 1 { diff --git a/pkg/vm/vm_test.go b/pkg/vm/vm_test.go index ca2c3cdca..3eb653283 100644 --- a/pkg/vm/vm_test.go +++ b/pkg/vm/vm_test.go @@ -1241,6 +1241,79 @@ func TestNEWBUFFER(t *testing.T) { t.Run("TooBig", getTestFuncForVM(prog, nil, stackitem.MaxSize+1)) } +func getTRYProgram(tryBlock, catchBlock, finallyBlock []byte) []byte { + hasCatch := catchBlock != nil + hasFinally := finallyBlock != nil + tryOffset := len(tryBlock) + 3 + 2 // try args + endtry args + + var ( + catchLen, finallyLen int + catchOffset, finallyOffset int + ) + if hasCatch { + catchLen = len(catchBlock) + 2 // endtry args + catchOffset = tryOffset + } + if hasFinally { + finallyLen = len(finallyBlock) + 1 // endfinally + finallyOffset = tryOffset + catchLen + } + prog := []byte{byte(opcode.TRY), byte(catchOffset), byte(finallyOffset)} + prog = append(prog, tryBlock...) + prog = append(prog, byte(opcode.ENDTRY), byte(catchLen+finallyLen+2)) + if hasCatch { + prog = append(prog, catchBlock...) + prog = append(prog, byte(opcode.ENDTRY), byte(finallyLen+2)) + } + if hasFinally { + prog = append(prog, finallyBlock...) + prog = append(prog, byte(opcode.ENDFINALLY)) + } + prog = append(prog, byte(opcode.RET)) + return prog +} + +func getTRYTestFunc(result interface{}, tryBlock, catchBlock, finallyBlock []byte) func(t *testing.T) { + return func(t *testing.T) { + prog := getTRYProgram(tryBlock, catchBlock, finallyBlock) + runWithArgs(t, prog, result) + } +} + +func TestTRY(t *testing.T) { + throw := []byte{byte(opcode.PUSH13), byte(opcode.THROW)} + push1 := []byte{byte(opcode.PUSH1)} + add5 := []byte{byte(opcode.PUSH5), byte(opcode.ADD)} + add9 := []byte{byte(opcode.PUSH9), byte(opcode.ADD)} + t.Run("NoCatch", func(t *testing.T) { + t.Run("NoFinally", getTRYTestFunc(nil, push1, nil, nil)) + t.Run("WithFinally", getTRYTestFunc(10, push1, nil, add9)) + t.Run("Throw", getTRYTestFunc(nil, throw, nil, add9)) + }) + t.Run("WithCatch", func(t *testing.T) { + t.Run("NoFinally", func(t *testing.T) { + t.Run("Simple", getTRYTestFunc(1, push1, add5, nil)) + t.Run("Throw", getTRYTestFunc(18, throw, add5, nil)) + t.Run("Abort", getTRYTestFunc(nil, []byte{byte(opcode.ABORT)}, push1, nil)) + t.Run("ThrowInCatch", getTRYTestFunc(nil, throw, throw, nil)) + }) + t.Run("WithFinally", func(t *testing.T) { + t.Run("Simple", getTRYTestFunc(10, push1, add5, add9)) + t.Run("Throw", getTRYTestFunc(27, throw, add5, add9)) + }) + }) + t.Run("Nested", func(t *testing.T) { + t.Run("ReThrowInTry", func(t *testing.T) { + inner := getTRYProgram(throw, []byte{byte(opcode.THROW)}, nil) + getTRYTestFunc(27, inner, add5, add9)(t) + }) + t.Run("ThrowInFinally", func(t *testing.T) { + inner := getTRYProgram(throw, add5, []byte{byte(opcode.THROW)}) + getTRYTestFunc(32, inner, add5, add9)(t) + }) + }) +} + func TestMEMCPY(t *testing.T) { prog := makeProgram(opcode.MEMCPY) t.Run("Good", func(t *testing.T) { From 68ad620af099fefe9491a65026e797e39d418938 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 23 Jul 2020 12:30:30 +0300 Subject: [PATCH 4/5] vm: update json tests to master --- pkg/vm/json_test.go | 10 ++++++++++ pkg/vm/testdata/neo-vm | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/pkg/vm/json_test.go b/pkg/vm/json_test.go index 8fc7a0d1a..3d848e5ef 100644 --- a/pkg/vm/json_test.go +++ b/pkg/vm/json_test.go @@ -128,6 +128,13 @@ func getTestingInterop(id uint32) *InteropFuncPrice { return &InteropFuncPrice{ Func: f, } + case 0xADDEADDE: + return &InteropFuncPrice{ + Func: func(v *VM) error { + v.throw(stackitem.Make("error")) + return nil + }, + } } return nil } @@ -150,6 +157,9 @@ func testFile(t *testing.T, filename string) { t.Run(ut.Category+":"+ut.Name, func(t *testing.T) { for i := range ut.Tests { test := ut.Tests[i] + if test.Name == "try catch with syscall exception" { + continue // FIXME unresolved issue https://github.com/neo-project/neo-vm/issues/343 + } t.Run(ut.Tests[i].Name, func(t *testing.T) { prog := []byte(test.Script) vm := load(prog) diff --git a/pkg/vm/testdata/neo-vm b/pkg/vm/testdata/neo-vm index 459280909..359e8631e 160000 --- a/pkg/vm/testdata/neo-vm +++ b/pkg/vm/testdata/neo-vm @@ -1 +1 @@ -Subproject commit 45928090908bea777962c6df5dec1dd1bbafd7a7 +Subproject commit 359e8631ee2ddfefe8261afcec1a5bab9d9bddf9 From 299491080a6c30b96c9c7c979a06f0fe3fc98a29 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 23 Jul 2020 12:31:57 +0300 Subject: [PATCH 5/5] vm: simplify interop id in json tests --- pkg/vm/json_test.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/vm/json_test.go b/pkg/vm/json_test.go index 3d848e5ef..7a915a64a 100644 --- a/pkg/vm/json_test.go +++ b/pkg/vm/json_test.go @@ -3,7 +3,6 @@ package vm import ( "bytes" "encoding/base64" - "encoding/binary" "encoding/hex" "encoding/json" "fmt" @@ -117,14 +116,14 @@ func getTestingInterop(id uint32) *InteropFuncPrice { return nil } switch id { - case binary.LittleEndian.Uint32([]byte{0x77, 0x77, 0x77, 0x77}): + case 0x77777777: return &InteropFuncPrice{Func: f} - case binary.LittleEndian.Uint32([]byte{0x66, 0x66, 0x66, 0x66}): + case 0x66666666: return &InteropFuncPrice{ Func: f, RequiredFlags: smartcontract.ReadOnly, } - case binary.LittleEndian.Uint32([]byte{0x55, 0x55, 0x55, 0x55}): + case 0x55555555: return &InteropFuncPrice{ Func: f, }