core/vm: updated to v1.0.6, fix bugs

This commit is contained in:
Marius van der Wijden 2024-07-18 13:25:32 +02:00
parent e61f11a7a3
commit a484bfacf0
7 changed files with 131 additions and 45 deletions

View file

@ -206,7 +206,7 @@ func parseAndValidate(s string, isInitCode bool) (*vm.Container, error) {
if err := c.UnmarshalBinary(b, isInitCode); err != nil { if err := c.UnmarshalBinary(b, isInitCode); err != nil {
return nil, err return nil, err
} }
if err := c.ValidateCode(&jt); err != nil { if err := c.ValidateCode(&jt, isInitCode); err != nil {
return nil, err return nil, err
} }
return &c, nil return &c, nil

View file

@ -300,7 +300,7 @@ func applyEOFChecks(prestate *Prestate, chainConfig *params.ChainConfig) error {
err = c.UnmarshalBinary(acc.Code, false) err = c.UnmarshalBinary(acc.Code, false)
if err == nil { if err == nil {
jt := vm.NewPragueEOFInstructionSetForTesting() jt := vm.NewPragueEOFInstructionSetForTesting()
err = c.ValidateCode(&jt) err = c.ValidateCode(&jt, false)
} }
if err != nil { if err != nil {
return NewError(ErrorConfig, fmt.Errorf("code at %s considered invalid: %v", addr, err)) return NewError(ErrorConfig, fmt.Errorf("code at %s considered invalid: %v", addr, err))

View file

@ -1005,7 +1005,7 @@ func opReturnContract(pc *uint64, interpreter *EVMInterpreter, scope *ScopeConte
if err := c.UnmarshalBinary(deployedCode, true); err != nil { if err := c.UnmarshalBinary(deployedCode, true); err != nil {
return nil, err return nil, err
} }
if err := c.ValidateCode(interpreter.tableEOF); err != nil { if err := c.ValidateCode(interpreter.tableEOF, true); err != nil {
return nil, err return nil, err
} }
if len(c.Data) < c.DataSize { if len(c.Data) < c.DataSize {

View file

@ -23,6 +23,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"github.com/ethereum/go-ethereum/params"
) )
const ( const (
@ -143,12 +145,19 @@ func (c *Container) MarshalBinary() []byte {
// UnmarshalBinary decodes an EOF container. // UnmarshalBinary decodes an EOF container.
func (c *Container) UnmarshalBinary(b []byte, isInitcode bool) error { func (c *Container) UnmarshalBinary(b []byte, isInitcode bool) error {
return c.unmarshaSubContainer(b, isInitcode, true)
}
func (c *Container) unmarshaSubContainer(b []byte, isInitcode bool, topLevel bool) error {
if !hasEOFMagic(b) { if !hasEOFMagic(b) {
return fmt.Errorf("%w: want %x", ErrInvalidMagic, eofMagic) return fmt.Errorf("%w: want %x", ErrInvalidMagic, eofMagic)
} }
if len(b) < 14 { if len(b) < 14 {
return io.ErrUnexpectedEOF return io.ErrUnexpectedEOF
} }
if len(b) > params.MaxInitCodeSize {
return ErrMaxInitCodeSizeExceeded
}
if !isEOFVersion1(b) { if !isEOFVersion1(b) {
return fmt.Errorf("%w: have %d, want %d", ErrInvalidVersion, b[2], eof1Version) return fmt.Errorf("%w: have %d, want %d", ErrInvalidVersion, b[2], eof1Version)
} }
@ -282,7 +291,7 @@ func (c *Container) UnmarshalBinary(b []byte, isInitcode bool) error {
} }
c := new(Container) c := new(Container)
end := min(idx+size, len(b)) end := min(idx+size, len(b))
if err := c.UnmarshalBinary(b[idx:end], isInitcode); err != nil { if err := c.unmarshaSubContainer(b[idx:end], isInitcode, false); err != nil {
return fmt.Errorf("%w for section %d", err, i) return fmt.Errorf("%w for section %d", err, i)
} }
container = append(container, c) container = append(container, c)
@ -299,6 +308,9 @@ func (c *Container) UnmarshalBinary(b []byte, isInitcode bool) error {
if !isInitcode { if !isInitcode {
end = min(idx+dataSize, len(b)) end = min(idx+dataSize, len(b))
} }
if topLevel && len(b) != idx+dataSize {
return ErrTruncatedTopLevelContainer
}
c.Data = b[idx:end] c.Data = b[idx:end]
return nil return nil
@ -306,8 +318,13 @@ func (c *Container) UnmarshalBinary(b []byte, isInitcode bool) error {
// ValidateCode validates each code section of the container against the EOF v1 // ValidateCode validates each code section of the container against the EOF v1
// rule set. // rule set.
func (c *Container) ValidateCode(jt *JumpTable) error { func (c *Container) ValidateCode(jt *JumpTable, isInitCode bool) error {
return c.validateSubContainer(jt, isInitCode, NotRefByEither)
}
func (c *Container) validateSubContainer(jt *JumpTable, isInitCode bool, refBy int) error {
visited := make(map[int]struct{}) visited := make(map[int]struct{})
subContainerVisited := make(map[int]int)
toVisit := []int{0} toVisit := []int{0}
for len(toVisit) > 0 { for len(toVisit) > 0 {
// TODO check if this can be used as a DOS // TODO check if this can be used as a DOS
@ -321,17 +338,31 @@ func (c *Container) ValidateCode(jt *JumpTable) error {
code = c.Code[index] code = c.Code[index]
) )
if _, ok := visited[index]; !ok { if _, ok := visited[index]; !ok {
v, err := validateCode(code, index, c, jt) res, err := validateCode(code, index, c, jt, isInitCode)
if err != nil { if err != nil {
return err return err
} }
visited[index] = struct{}{} visited[index] = struct{}{}
// Mark all sections that can be visited from here. // Mark all sections that can be visited from here.
for idx := range v { for idx := range res.VisitedCode {
if _, ok := visited[idx]; !ok { if _, ok := visited[idx]; !ok {
toVisit = append(toVisit, idx) toVisit = append(toVisit, idx)
} }
} }
// Mark all subcontainer that can be visited from here.
for idx, reference := range res.VisitedSubContainers {
// Make sure subcontainers are only ever referenced by either EOFCreate or ReturnContract
if ref, ok := subContainerVisited[idx]; ok && ref != reference {
return errors.New("section referenced by both EOFCreate and ReturnContract")
}
subContainerVisited[idx] = reference
}
if refBy == RefByReturnContract && res.IsInitCode {
return ErrIncompatibleContainerKind
}
if refBy == RefByEOFCreate && res.IsRuntime {
return ErrIncompatibleContainerKind
}
} }
toVisit = toVisit[1:] toVisit = toVisit[1:]
} }
@ -339,8 +370,12 @@ func (c *Container) ValidateCode(jt *JumpTable) error {
if len(visited) != len(c.Code) { if len(visited) != len(c.Code) {
return ErrUnreachableCode return ErrUnreachableCode
} }
for _, container := range c.ContainerSections { for idx, container := range c.ContainerSections {
if err := container.ValidateCode(jt); err != nil { reference, ok := subContainerVisited[idx]
if !ok {
return ErrOrphanedSubcontainer
}
if err := container.validateSubContainer(jt, isInitCode, reference); err != nil {
return err return err
} }
} }

View file

@ -493,7 +493,7 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64,
if err := c.UnmarshalBinary(codeAndHash.code, isInitcodeEOF); err != nil { if err := c.UnmarshalBinary(codeAndHash.code, isInitcodeEOF); err != nil {
return nil, common.Address{}, gas, fmt.Errorf("%w: %v", ErrInvalidEOFInitcode, err) return nil, common.Address{}, gas, fmt.Errorf("%w: %v", ErrInvalidEOFInitcode, err)
} }
if err := c.ValidateCode(evm.interpreter.tableEOF); err != nil { if err := c.ValidateCode(evm.interpreter.tableEOF, isInitcodeEOF); err != nil {
return nil, common.Address{}, gas, fmt.Errorf("%w: %v", ErrInvalidEOFInitcode, err) return nil, common.Address{}, gas, fmt.Errorf("%w: %v", ErrInvalidEOFInitcode, err)
} }
contract.Container = &c contract.Container = &c

View file

@ -36,20 +36,41 @@ var (
ErrInvalidMaxStackHeight = errors.New("invalid max stack height") ErrInvalidMaxStackHeight = errors.New("invalid max stack height")
ErrInvalidCodeTermination = errors.New("invalid code termination") ErrInvalidCodeTermination = errors.New("invalid code termination")
ErrEOFCreateWithTruncatedSection = errors.New("eofcreate with truncated section") ErrEOFCreateWithTruncatedSection = errors.New("eofcreate with truncated section")
ErrOrphanedSubcontainer = errors.New("subcontainer not referenced at all")
ErrIncompatibleContainerKind = errors.New("incompatible container kind")
ErrStopAndReturnContract = errors.New("Stop/Return and Returncontract in the same code section")
ErrStopInInitCode = errors.New("initcode contains a RETURN or STOP opcode")
ErrTruncatedTopLevelContainer = errors.New("truncated top level container")
ErrUnreachableCode = errors.New("unreachable code") ErrUnreachableCode = errors.New("unreachable code")
) )
const (
NotRefByEither = iota
RefByReturnContract
RefByEOFCreate
)
type ValidationResult struct {
VisitedCode map[int]struct{}
VisitedSubContainers map[int]int
IsInitCode bool
IsRuntime bool
}
// validateCode validates the code parameter against the EOF v1 validity requirements. // validateCode validates the code parameter against the EOF v1 validity requirements.
func validateCode(code []byte, section int, container *Container, jt *JumpTable) (map[int]struct{}, error) { func validateCode(code []byte, section int, container *Container, jt *JumpTable, isInitCode bool) (*ValidationResult, error) {
var ( var (
i = 0 i = 0
// Tracks the number of actual instructions in the code (e.g. // Tracks the number of actual instructions in the code (e.g.
// non-immediate values). This is used at the end to determine // non-immediate values). This is used at the end to determine
// if each instruction is reachable. // if each instruction is reachable.
count = 0 count = 0
op OpCode op OpCode
analysis bitvec analysis bitvec
visited = make(map[int]struct{}) visitedCode = make(map[int]struct{})
visitedSubcontainers = make(map[int]int)
hasReturnContract bool
hasStop bool
) )
// This loop visits every single instruction and verifies: // This loop visits every single instruction and verifies:
// * if the instruction is valid for the given jump table. // * if the instruction is valid for the given jump table.
@ -61,63 +82,88 @@ func validateCode(code []byte, section int, container *Container, jt *JumpTable)
count++ count++
op = OpCode(code[i]) op = OpCode(code[i])
if jt[op].undefined { if jt[op].undefined {
return visited, fmt.Errorf("%w: op %s, pos %d", ErrUndefinedInstruction, op, i) return nil, fmt.Errorf("%w: op %s, pos %d", ErrUndefinedInstruction, op, i)
} }
if size := jt[op].immediate; size != 0 { if size := jt[op].immediate; size != 0 {
if len(code) <= i+size { if len(code) <= i+size {
return visited, fmt.Errorf("%w: op %s, pos %d", ErrTruncatedImmediate, op, i) return nil, fmt.Errorf("%w: op %s, pos %d", ErrTruncatedImmediate, op, i)
} }
switch { switch {
case op == RJUMP || op == RJUMPI: case op == RJUMP || op == RJUMPI:
if err := checkDest(code, &analysis, i+1, i+3, len(code)); err != nil { if err := checkDest(code, &analysis, i+1, i+3, len(code)); err != nil {
return visited, err return nil, err
} }
case op == RJUMPV: case op == RJUMPV:
max_size := int(code[i+1]) max_size := int(code[i+1])
length := max_size + 1 length := max_size + 1
if len(code) <= i+length { if len(code) <= i+length {
return visited, fmt.Errorf("%w: jump table truncated, op %s, pos %d", ErrTruncatedImmediate, op, i) return nil, fmt.Errorf("%w: jump table truncated, op %s, pos %d", ErrTruncatedImmediate, op, i)
} }
offset := i + 2 offset := i + 2
for j := 0; j < length; j++ { for j := 0; j < length; j++ {
if err := checkDest(code, &analysis, offset+j*2, offset+(length*2), len(code)); err != nil { if err := checkDest(code, &analysis, offset+j*2, offset+(length*2), len(code)); err != nil {
return visited, err return nil, err
} }
} }
i += 2 * max_size i += 2 * max_size
case op == CALLF: case op == CALLF:
arg, _ := parseUint16(code[i+1:]) arg, _ := parseUint16(code[i+1:])
if arg >= len(container.Types) { if arg >= len(container.Types) {
return visited, fmt.Errorf("%w: arg %d, last %d, pos %d", ErrInvalidSectionArgument, arg, len(container.Types), i) return nil, fmt.Errorf("%w: arg %d, last %d, pos %d", ErrInvalidSectionArgument, arg, len(container.Types), i)
} }
if container.Types[arg].Output == 0x80 { if container.Types[arg].Output == 0x80 {
return visited, fmt.Errorf("%w: section %v", ErrInvalidCallArgument, arg) return nil, fmt.Errorf("%w: section %v", ErrInvalidCallArgument, arg)
} }
visited[arg] = struct{}{} visitedCode[arg] = struct{}{}
case op == JUMPF: case op == JUMPF:
arg, _ := parseUint16(code[i+1:]) arg, _ := parseUint16(code[i+1:])
if arg >= len(container.Types) { if arg >= len(container.Types) {
return visited, fmt.Errorf("%w: arg %d, last %d, pos %d", ErrInvalidSectionArgument, arg, len(container.Types), i) return nil, fmt.Errorf("%w: arg %d, last %d, pos %d", ErrInvalidSectionArgument, arg, len(container.Types), i)
} }
visited[arg] = struct{}{} visitedCode[arg] = struct{}{}
case op == DATALOADN: case op == DATALOADN:
arg, _ := parseUint16(code[i+1:]) arg, _ := parseUint16(code[i+1:])
if arg+32 > len(container.Data) { if arg+32 > len(container.Data) {
return visited, fmt.Errorf("%w: arg %d, last %d, pos %d", ErrInvalidDataloadNArgument, arg, len(container.Data), i) return nil, fmt.Errorf("%w: arg %d, last %d, pos %d", ErrInvalidDataloadNArgument, arg, len(container.Data), i)
} }
case op == RETURNCONTRACT: case op == RETURNCONTRACT:
arg := int(code[i+1]) arg := int(code[i+1])
if arg >= len(container.ContainerSections) { if arg >= len(container.ContainerSections) {
return visited, fmt.Errorf("%w: arg %d, last %d, pos %d", ErrUnreachableCode, arg, len(container.ContainerSections), i) return nil, fmt.Errorf("%w: arg %d, last %d, pos %d", ErrUnreachableCode, arg, len(container.ContainerSections), i)
} }
// We need to store per subcontainer how it was referenced
if v, ok := visitedSubcontainers[arg]; ok && v != RefByReturnContract {
return nil, fmt.Errorf("section already referenced, arg :%d", arg)
}
if hasStop {
return nil, ErrStopAndReturnContract
}
hasReturnContract = true
visitedSubcontainers[arg] = RefByReturnContract
case op == EOFCREATE: case op == EOFCREATE:
arg := int(code[i+1]) arg := int(code[i+1])
if arg >= len(container.ContainerSections) { if arg >= len(container.ContainerSections) {
return visited, fmt.Errorf("%w: arg %d, last %d, pos %d", ErrUnreachableCode, arg, len(container.ContainerSections), i) return nil, fmt.Errorf("%w: arg %d, last %d, pos %d", ErrUnreachableCode, arg, len(container.ContainerSections), i)
} }
if ct := container.ContainerSections[arg]; len(ct.Data) != ct.DataSize { if ct := container.ContainerSections[arg]; len(ct.Data) != ct.DataSize {
return visited, fmt.Errorf("%w: container %d, have %d, claimed %d, pos %d", ErrEOFCreateWithTruncatedSection, arg, len(ct.Data), ct.DataSize, i) return nil, fmt.Errorf("%w: container %d, have %d, claimed %d, pos %d", ErrEOFCreateWithTruncatedSection, arg, len(ct.Data), ct.DataSize, i)
} }
if _, ok := visitedSubcontainers[arg]; ok {
return nil, fmt.Errorf("section already referenced, arg :%d", arg)
}
// We need to store per subcontainer how it was referenced
if v, ok := visitedSubcontainers[arg]; ok && v != RefByEOFCreate {
return nil, fmt.Errorf("section already referenced, arg :%d", arg)
}
visitedSubcontainers[arg] = RefByEOFCreate
case op == STOP || op == RETURN:
if isInitCode {
return nil, ErrStopInInitCode
}
if hasReturnContract {
return nil, ErrStopAndReturnContract
}
hasStop = true
} }
i += size i += size
} }
@ -126,16 +172,21 @@ func validateCode(code []byte, section int, container *Container, jt *JumpTable)
// Code sections may not "fall through" and require proper termination. // Code sections may not "fall through" and require proper termination.
// Therefore, the last instruction must be considered terminal or RJUMP. // Therefore, the last instruction must be considered terminal or RJUMP.
if !jt[op].terminal && op != RJUMP { if !jt[op].terminal && op != RJUMP {
return visited, fmt.Errorf("%w: end with %s, pos %d", ErrInvalidCodeTermination, op, i) return nil, fmt.Errorf("%w: end with %s, pos %d", ErrInvalidCodeTermination, op, i)
} }
if paths, err := validateControlFlow2(code, section, container.Types, jt); err != nil { if paths, err := validateControlFlow2(code, section, container.Types, jt); err != nil {
return visited, err return nil, err
} else if paths != count { } else if paths != count {
fmt.Printf("Paths: %v Count: %v\n", paths, count) fmt.Printf("Paths: %v Count: %v\n", paths, count)
// TODO(matt): return actual position of unreachable code // TODO(matt): return actual position of unreachable code
return visited, ErrUnreachableCode return nil, ErrUnreachableCode
} }
return visited, nil return &ValidationResult{
VisitedCode: visitedCode,
VisitedSubContainers: visitedSubcontainers,
IsInitCode: hasReturnContract,
IsRuntime: hasStop,
}, nil
} }
// checkDest parses a relative offset at code[0:2] and checks if it is a valid jump destination. // checkDest parses a relative offset at code[0:2] and checks if it is a valid jump destination.

View file

@ -250,7 +250,7 @@ func TestValidateCode(t *testing.T) {
Data: make([]byte, 0), Data: make([]byte, 0),
ContainerSections: make([]*Container, 0), ContainerSections: make([]*Container, 0),
} }
_, err := validateCode(test.code, test.section, container, &pragueEOFInstructionSet) _, err := validateCode(test.code, test.section, container, &pragueEOFInstructionSet, true)
if !errors.Is(err, test.err) { if !errors.Is(err, test.err) {
t.Errorf("test %d (%s): unexpected error (want: %v, got: %v)", i, common.Bytes2Hex(test.code), test.err, err) t.Errorf("test %d (%s): unexpected error (want: %v, got: %v)", i, common.Bytes2Hex(test.code), test.err, err)
} }
@ -274,7 +274,7 @@ func BenchmarkRJUMPI(b *testing.B) {
} }
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_, err := validateCode(code, 0, container, &pragueEOFInstructionSet) _, err := validateCode(code, 0, container, &pragueEOFInstructionSet, true)
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
@ -304,7 +304,7 @@ func BenchmarkRJUMPV(b *testing.B) {
} }
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_, err := validateCode(code, 0, container, &pragueEOFInstructionSet) _, err := validateCode(code, 0, container, &pragueEOFInstructionSet, true)
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
@ -345,10 +345,10 @@ func BenchmarkEOFValidation(b *testing.B) {
var container2 Container var container2 Container
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
if err := container2.UnmarshalBinary(bin); err != nil { if err := container2.UnmarshalBinary(bin, true); err != nil {
b.Fatal(err) b.Fatal(err)
} }
if err := container2.ValidateCode(&pragueEOFInstructionSet); err != nil { if err := container2.ValidateCode(&pragueEOFInstructionSet, true); err != nil {
b.Fatal(err) b.Fatal(err)
} }
} }
@ -395,10 +395,10 @@ func BenchmarkEOFValidation2(b *testing.B) {
var container2 Container var container2 Container
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
if err := container2.UnmarshalBinary(bin); err != nil { if err := container2.UnmarshalBinary(bin, true); err != nil {
b.Fatal(err) b.Fatal(err)
} }
if err := container2.ValidateCode(&pragueEOFInstructionSet); err != nil { if err := container2.ValidateCode(&pragueEOFInstructionSet, true); err != nil {
b.Fatal(err) b.Fatal(err)
} }
} }
@ -444,10 +444,10 @@ func BenchmarkEOFValidation3(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
for k := 0; k < 40; k++ { for k := 0; k < 40; k++ {
var container2 Container var container2 Container
if err := container2.UnmarshalBinary(bin); err != nil { if err := container2.UnmarshalBinary(bin, true); err != nil {
b.Fatal(err) b.Fatal(err)
} }
if err := container2.ValidateCode(&pragueEOFInstructionSet); err != nil { if err := container2.ValidateCode(&pragueEOFInstructionSet, true); err != nil {
b.Fatal(err) b.Fatal(err)
} }
} }
@ -473,7 +473,7 @@ func BenchmarkRJUMPI_2(b *testing.B) {
} }
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_, err := validateCode(code, 0, container, &pragueEOFInstructionSet) _, err := validateCode(code, 0, container, &pragueEOFInstructionSet, true)
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
@ -483,7 +483,7 @@ func BenchmarkRJUMPI_2(b *testing.B) {
func FuzzUnmarshalBinary(f *testing.F) { func FuzzUnmarshalBinary(f *testing.F) {
f.Fuzz(func(_ *testing.T, input []byte) { f.Fuzz(func(_ *testing.T, input []byte) {
var container Container var container Container
container.UnmarshalBinary(input) container.UnmarshalBinary(input, true)
}) })
} }
@ -491,6 +491,6 @@ func FuzzValidate(f *testing.F) {
f.Fuzz(func(_ *testing.T, code []byte, maxStack uint16) { f.Fuzz(func(_ *testing.T, code []byte, maxStack uint16) {
var container Container var container Container
container.Types = append(container.Types, &FunctionMetadata{Input: 0, Output: 0x80, MaxStackHeight: maxStack}) container.Types = append(container.Types, &FunctionMetadata{Input: 0, Output: 0x80, MaxStackHeight: maxStack})
validateCode(code, 0, &container, &pragueEOFInstructionSet) validateCode(code, 0, &container, &pragueEOFInstructionSet, true)
}) })
} }