Merge pull request #3186 from nspcc-dev/reduce-max-nef-size

Restrict maximum serialized NEF file size
This commit is contained in:
Roman Khimov 2023-11-20 15:19:40 +03:00 committed by GitHub
commit 7fb077e999
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 107 additions and 7 deletions

View file

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
@ -1070,3 +1071,27 @@ func filterFilename(infos []os.DirEntry, ext string) string {
} }
return "" return ""
} }
func TestContractCompile_NEFSizeCheck(t *testing.T) {
tmpDir := t.TempDir()
e := testcli.NewExecutor(t, false)
src := `package nefconstraints
var data = "%s"
func Main() string {
return data
}`
data := make([]byte, stackitem.MaxSize-10)
for i := range data {
data[i] = byte('a')
}
in := filepath.Join(tmpDir, "main.go")
cfg := filepath.Join(tmpDir, "main.yml")
require.NoError(t, os.WriteFile(cfg, []byte("name: main"), os.ModePerm))
require.NoError(t, os.WriteFile(in, []byte(fmt.Sprintf(src, data)), os.ModePerm))
e.RunWithError(t, "neo-go", "contract", "compile", "--in", in)
require.NoFileExists(t, filepath.Join(tmpDir, "main.nef"))
}

View file

@ -16,6 +16,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -84,6 +85,29 @@ func TestCompiler(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
}, },
}, },
{
name: "TestCompileAndSave_NEF_constraints",
function: func(t *testing.T) {
tmp := t.TempDir()
src := `package nefconstraints
var data = "%s"
func Main() string {
return data
}
`
data := make([]byte, stackitem.MaxSize-10)
for i := range data {
data[i] = byte('a')
}
in := filepath.Join(tmp, "src.go")
require.NoError(t, os.WriteFile(in, []byte(fmt.Sprintf(src, data)), os.ModePerm))
out := filepath.Join(tmp, "test.nef")
_, err := compiler.CompileAndSave(in, &compiler.Options{Outfile: out})
require.Error(t, err)
require.Contains(t, err.Error(), "serialized NEF size exceeds VM stackitem limits")
},
},
} }
for _, tcase := range testCases { for _, tcase := range testCases {

View file

@ -37,6 +37,8 @@ type NativeContract struct {
// ToStackItem converts state.Contract to stackitem.Item. // ToStackItem converts state.Contract to stackitem.Item.
func (c *Contract) ToStackItem() (stackitem.Item, error) { func (c *Contract) ToStackItem() (stackitem.Item, error) {
// Do not skip the NEF size check, it won't affect native Management related
// states as the same checked is performed during contract deploy/update.
rawNef, err := c.NEF.Bytes() rawNef, err := c.NEF.Bytes()
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -4,10 +4,12 @@ import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt"
"github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/crypto/hash"
"github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
) )
// NEO Executable Format 3 (NEF3) // NEO Executable Format 3 (NEF3)
@ -31,8 +33,6 @@ import (
const ( const (
// Magic is a magic File header constant. // Magic is a magic File header constant.
Magic uint32 = 0x3346454E Magic uint32 = 0x3346454E
// MaxScriptLength is the maximum allowed contract script length.
MaxScriptLength = 512 * 1024
// MaxSourceURLLength is the maximum allowed source URL length. // MaxSourceURLLength is the maximum allowed source URL length.
MaxSourceURLLength = 256 MaxSourceURLLength = 256
// compilerFieldSize is the length of `Compiler` File header field in bytes. // compilerFieldSize is the length of `Compiler` File header field in bytes.
@ -99,8 +99,11 @@ func (h *Header) DecodeBinary(r *io.BinReader) {
} }
// CalculateChecksum returns first 4 bytes of double-SHA256(Header) converted to uint32. // CalculateChecksum returns first 4 bytes of double-SHA256(Header) converted to uint32.
// CalculateChecksum doesn't perform the resulting serialized NEF size check, and return
// the checksum irrespectively to the size limit constraint. It's a caller's duty to check
// the resulting NEF size.
func (n *File) CalculateChecksum() uint32 { func (n *File) CalculateChecksum() uint32 {
bb, err := n.Bytes() bb, err := n.BytesLong()
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -139,7 +142,7 @@ func (n *File) DecodeBinary(r *io.BinReader) {
r.Err = errInvalidReserved r.Err = errInvalidReserved
return return
} }
n.Script = r.ReadVarBytes(MaxScriptLength) n.Script = r.ReadVarBytes(stackitem.MaxSize)
if r.Err == nil && len(n.Script) == 0 { if r.Err == nil && len(n.Script) == 0 {
r.Err = errors.New("empty script") r.Err = errors.New("empty script")
return return
@ -152,19 +155,40 @@ func (n *File) DecodeBinary(r *io.BinReader) {
} }
} }
// Bytes returns a byte array with a serialized NEF File. // Bytes returns a byte array with a serialized NEF File. It performs the
// resulting NEF file size check and returns an error if serialized slice length
// exceeds [stackitem.MaxSize].
func (n File) Bytes() ([]byte, error) { func (n File) Bytes() ([]byte, error) {
return n.bytes(true)
}
// BytesLong returns a byte array with a serialized NEF File. It performs no
// resulting slice check.
func (n File) BytesLong() ([]byte, error) {
return n.bytes(false)
}
// bytes returns the serialized NEF File representation and performs the resulting
// byte array size check if needed.
func (n File) bytes(checkSize bool) ([]byte, error) {
buf := io.NewBufBinWriter() buf := io.NewBufBinWriter()
n.EncodeBinary(buf.BinWriter) n.EncodeBinary(buf.BinWriter)
if buf.Err != nil { if buf.Err != nil {
return nil, buf.Err return nil, buf.Err
} }
return buf.Bytes(), nil res := buf.Bytes()
if checkSize && len(res) > stackitem.MaxSize {
return nil, fmt.Errorf("serialized NEF size exceeds VM stackitem limits: %d bytes is allowed at max, got %d", stackitem.MaxSize, len(res))
}
return res, nil
} }
// FileFromBytes returns a NEF File deserialized from the given bytes. // FileFromBytes returns a NEF File deserialized from the given bytes.
func FileFromBytes(source []byte) (File, error) { func FileFromBytes(source []byte) (File, error) {
result := File{} result := File{}
if len(source) > stackitem.MaxSize {
return result, fmt.Errorf("invalid NEF file size: expected %d at max, got %d", stackitem.MaxSize, len(source))
}
r := io.NewBinReaderFromBuf(source) r := io.NewBinReaderFromBuf(source)
result.DecodeBinary(r) result.DecodeBinary(r)
if r.Err != nil { if r.Err != nil {

View file

@ -11,6 +11,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -49,7 +50,7 @@ func TestEncodeDecodeBinary(t *testing.T) {
}) })
t.Run("invalid script length", func(t *testing.T) { t.Run("invalid script length", func(t *testing.T) {
newScript := make([]byte, MaxScriptLength+1) newScript := make([]byte, stackitem.MaxSize+1)
expected.Script = newScript expected.Script = newScript
expected.Checksum = expected.CalculateChecksum() expected.Checksum = expected.CalculateChecksum()
checkDecodeError(t, expected) checkDecodeError(t, expected)
@ -126,6 +127,30 @@ func TestBytesFromBytes(t *testing.T) {
require.Equal(t, expected, actual) require.Equal(t, expected, actual)
} }
func TestNewFileFromBytesLimits(t *testing.T) {
expected := File{
Header: Header{
Magic: Magic,
Compiler: "best compiler version 1",
},
Tokens: []MethodToken{{
Hash: random.Uint160(),
Method: "someMethod",
ParamCount: 3,
HasReturn: true,
CallFlag: callflag.WriteStates,
}},
Script: make([]byte, stackitem.MaxSize-100),
}
expected.Checksum = expected.CalculateChecksum()
bytes, err := expected.BytesLong()
require.NoError(t, err)
_, err = FileFromBytes(bytes)
require.Error(t, err)
require.Contains(t, err.Error(), "invalid NEF file size")
}
func TestMarshalUnmarshalJSON(t *testing.T) { func TestMarshalUnmarshalJSON(t *testing.T) {
expected := &File{ expected := &File{
Header: Header{ Header: Header{