Skip to content

Commit

Permalink
AVM: Cleanly handle broken switch/match programs (#5782)
Browse files Browse the repository at this point in the history
  • Loading branch information
jannotti authored Oct 12, 2023
1 parent 5b2f1ab commit 0358d1d
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 4 deletions.
1 change: 1 addition & 0 deletions data/transactions/logic/assembler.go
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,7 @@ func asmBranch(ops *OpStream, spec *OpSpec, mnemonic token, args []token) *sourc
return nil
}

// asmSwitch assembles switch and match opcodes
func asmSwitch(ops *OpStream, spec *OpSpec, mnemonic token, args []token) *sourceError {
numOffsets := len(args)
if numOffsets > math.MaxUint8 {
Expand Down
19 changes: 17 additions & 2 deletions data/transactions/logic/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -2509,13 +2509,20 @@ func branchTarget(cx *EvalContext) (int, error) {
}

func switchTarget(cx *EvalContext, branchIdx uint64) (int, error) {
if cx.pc+1 >= len(cx.program) {
opcode := cx.program[cx.pc]
spec := &opsByOpcode[cx.version][opcode]
return 0, fmt.Errorf("bare %s opcode at end of program", spec.Name)
}
numOffsets := int(cx.program[cx.pc+1])

end := cx.pc + 2 // end of opcode + number of offsets, beginning of offset list
eoi := end + 2*numOffsets // end of instruction

if eoi > len(cx.program) { // eoi will equal len(p) if switch is last instruction
return 0, fmt.Errorf("switch claims to extend beyond program")
opcode := cx.program[cx.pc]
spec := &opsByOpcode[cx.version][opcode]
return 0, fmt.Errorf("%s opcode claims to extend beyond program", spec.Name)
}

offset := 0
Expand Down Expand Up @@ -2550,8 +2557,13 @@ func checkBranch(cx *EvalContext) error {
return nil
}

// checks switch is encoded properly (and calculates nextpc)
// checks switch or match is encoded properly (and calculates nextpc)
func checkSwitch(cx *EvalContext) error {
if cx.pc+1 >= len(cx.program) {
opcode := cx.program[cx.pc]
spec := &opsByOpcode[cx.version][opcode]
return fmt.Errorf("bare %s opcode at end of program", spec.Name)
}
numOffsets := int(cx.program[cx.pc+1])
eoi := cx.pc + 2 + 2*numOffsets

Expand Down Expand Up @@ -2628,6 +2640,9 @@ func opSwitch(cx *EvalContext) error {
}

func opMatch(cx *EvalContext) error {
if cx.pc+1 >= len(cx.program) {
return fmt.Errorf("bare match opcode at end of program")
}
n := int(cx.program[cx.pc+1])
// stack contains the n sized match list and the single match value
if n+1 > len(cx.Stack) {
Expand Down
72 changes: 70 additions & 2 deletions data/transactions/logic/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3156,8 +3156,7 @@ done:
int 1
`, v)
// cut two last bytes - intc_1 and last byte of bnz
ops.Program = ops.Program[:len(ops.Program)-2]
testLogicBytes(t, ops.Program, nil,
testLogicBytes(t, ops.Program[:len(ops.Program)-2], nil,
"bnz program ends short", "bnz program ends short")
})
}
Expand Down Expand Up @@ -5835,6 +5834,40 @@ switch done1 done2; done1: ; done2: ;
`, 8)
}

// TestShortSwitch ensures a clean error, in Check and Eval, when a switch ends early
func TestShortSwitch(t *testing.T) {
partitiontest.PartitionTest(t)
t.Parallel()

source := `
int 1
int 1
switch label1 label2
label1:
label2:
`
ops, err := AssembleStringWithVersion(source, AssemblerMaxVersion)
require.NoError(t, err)

// fine as is
testLogicBytes(t, ops.Program, nil)

beyond := "switch opcode claims to extend beyond program"

// bad if a label is gone
testLogicBytes(t, ops.Program[:len(ops.Program)-2], nil, beyond, beyond)

// chop off all the labels, but keep the label count
testLogicBytes(t, ops.Program[:len(ops.Program)-4], nil, beyond, beyond)

// chop off before the label count
testLogicBytes(t, ops.Program[:len(ops.Program)-5], nil,
"bare switch opcode at end of program", "bare switch opcode at end of program")

// chop off half of a label
testLogicBytes(t, ops.Program[:len(ops.Program)-1], nil, beyond, beyond)
}

func TestMatch(t *testing.T) {
partitiontest.PartitionTest(t)
t.Parallel()
Expand Down Expand Up @@ -5987,6 +6020,41 @@ one: int 0;
`, 8)
}

// TestShortMatch ensures a clean error when a match ends early
func TestShortMatch(t *testing.T) {
partitiontest.PartitionTest(t)
t.Parallel()

source := `int 1
int 40
int 45
int 40
match label1 label2
label1:
label2:
`
ops, err := AssembleStringWithVersion(source, AssemblerMaxVersion)
require.NoError(t, err)

// fine as is
testLogicBytes(t, ops.Program, nil)

beyond := "match opcode claims to extend beyond program"

// bad if a label is gone
testLogicBytes(t, ops.Program[:len(ops.Program)-2], nil, beyond, beyond)

// chop off all the labels, but keep the label count
testLogicBytes(t, ops.Program[:len(ops.Program)-4], nil, beyond, beyond)

// chop off before the label count
testLogicBytes(t, ops.Program[:len(ops.Program)-5], nil,
"bare match opcode at end of program", "bare match opcode at end of program")

// chop off half of a label
testLogicBytes(t, ops.Program[:len(ops.Program)-1], nil, beyond, beyond)
}

func TestPushConsts(t *testing.T) {
partitiontest.PartitionTest(t)
t.Parallel()
Expand Down

0 comments on commit 0358d1d

Please sign in to comment.