mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-12-23 13:41:37 +00:00
Merge pull request #3186 from nspcc-dev/reduce-max-nef-size
Restrict maximum serialized NEF file size
This commit is contained in:
commit
7fb077e999
5 changed files with 107 additions and 7 deletions
|
@ -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"))
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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{
|
||||||
|
|
Loading…
Reference in a new issue