Repository: tetratelabs/wazero
Pull request #128 (2022-01-12 02:57:35)

wat: refactors symbolic index identifier handling
Before, we punted symbolic index binding until we converted into binary
format. This put logic in an awkward spot, which was ok if only the
start function, but not for generic index handling (such as type
binding).

This pulls a new file out for handling bindings, and also eagerly
validates numbers when we read an index. Most importantly, this binds
symbolic identifiers to numeric offsets on parse, which matches how
they work at runtime.


codefromthecrypt: looks like github is having a bad day

```
 Download action repository 'actions/setup-go@v2' (SHA:424fc82d43fa5a37540bae62709ddcc23d9520d4)
Download action repository 'actions/checkout@v2' (SHA:ec3a7ce113134d7a93b817d10a8272cb61118579)
Error: '/Users/runner/work/_actions/_temp_be395fa4-9b3a-468c-9e68-e23f8decbe7d/4d026012-194f-4f32-9d20-87c90bfee65d.tar.gz' contains '0' directories
```

Patch:
diff --git a/wasm/wat/bind.go b/wasm/wat/bind.go
new file mode 100644
index 00000000..74783fc5
--- /dev/null
+++ b/wasm/wat/bind.go
@@ -0,0 +1,47 @@
+package wat
+
+import (
+	"fmt"
+)
+
+// bindIndices ensures any indices point are numeric or returns a FormatError if they cannot be bound.
+func bindIndices(m *module) error {
+	if m.startFunction != nil {
+		if err := bindStartFunction(m); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// bindStartFunction ensures the module.startFunction points to a valid numeric index or returns a FormatError if it
+// cannot be bound.
+//
+// Failure cases are when a symbolic identifier points nowhere or a numeric index is out of range.
+// Ex. (start $t0) exists, but there's no import or module defined function with that name.
+//  or (start 32) exists, but there are only 10 functions.
+func bindStartFunction(m *module) error {
+	start := m.startFunction
+
+	if start.ID == "" { // already bound to a numeric index, but we have to verify it is in range
+		if int(start.numeric) >= len(m.importFuncs) { // TODO len(m.importFuncs + m.funcs) when we add them!
+			return &FormatError{start.line, start.col, "module.start",
+				fmt.Errorf("function index %d is out of range [0..%d]", start.numeric, len(m.importFuncs)-1),
+			}
+		}
+	}
+
+	// Now, attempt to look up the symbolic name of any function imported or defined in this module.
+	for i, f := range m.importFuncs {
+		if f.funcName == start.ID {
+			start.ID = ""
+			start.numeric = uint32(i)
+			return nil
+		}
+	}
+
+	// TODO: also search functions defined in this module, once we add them!
+	return &FormatError{start.line, start.col, "module.start",
+		fmt.Errorf("unknown function name %s", start.ID),
+	}
+}
diff --git a/wasm/wat/convert.go b/wasm/wat/convert.go
index 9e704f6c..de9458f3 100644
--- a/wasm/wat/convert.go
+++ b/wasm/wat/convert.go
@@ -1,9 +1,6 @@
 package wat
 
 import (
-	"fmt"
-	"strconv"
-
 	"github.com/tetratelabs/wazero/wasm"
 )
 
@@ -48,44 +45,11 @@ func TextToBinary(source []byte) (result *wasm.Module, err error) {
 		result.ImportSection = append(result.ImportSection, i)
 	}
 
-	// The start function is called on Module.Instantiate. To prevent runtime errors, resolve it into a valid function
-	// index now. parseStartSection already returns a FormatError, so don't re-wrap it!
+	// The start function is called on Module.Instantiate.
 	if m.startFunction != nil {
-		if result.StartSection, err = parseStartSection(m); err != nil {
-			return nil, err
-		}
+		result.StartSection = &m.startFunction.numeric
 	}
 
 	// TODO: encode CustomSection["name"] with module function and local names
 	return
 }
-
-// parseStartSection looks up the numerical index in module.importFuncs corresponding to startFunction.index or returns
-// a FormatError.
-func parseStartSection(m *module) (*uint32, error) {
-	start := m.startFunction
-
-	// Try to see if this is a numeric index like "2" first.
-	if parsed, parseErr := strconv.ParseUint(start.index, 0, 32); parseErr == nil {
-		if int(parsed) >= len(m.importFuncs) { // TODO len(m.importFuncs + m.funcs) when we add them!
-			return nil, &FormatError{start.line, start.col, "module.start",
-				fmt.Errorf("invalid function index: %d", parsed),
-			}
-		}
-		startIdx := uint32(parsed)
-		return &startIdx, nil
-	}
-
-	// Now, attempt to look up the symbolic name of any function imported or defined in this module.
-	for i, f := range m.importFuncs {
-		if f.funcName == start.index {
-			startIdx := uint32(i)
-			return &startIdx, nil
-		}
-	}
-
-	// TODO: also search functions defined in this module, once we add them!
-	return nil, &FormatError{start.line, start.col, "module.start",
-		fmt.Errorf("unknown function name: %s", start.index),
-	}
-}
diff --git a/wasm/wat/numbers.go b/wasm/wat/numbers.go
new file mode 100644
index 00000000..33c2e67d
--- /dev/null
+++ b/wasm/wat/numbers.go
@@ -0,0 +1,93 @@
+package wat
+
+import (
+	"strconv"
+)
+
+// decodeUint32 decodes an uint32 from a tokenUN.
+//
+// Note: Bit length interpretation is not defined at the lexing layer, so this may fail on overflow due to invalid
+// length known at the parsing layer.
+//
+// Note: This is similar to, but cannot use strconv.Atoi because WebAssembly allows underscore characters in numeric
+// representation. See https://www.w3.org/TR/wasm-core-1/#integers%E2%91%A6
+func decodeUint32(tokenBytes []byte) (uint32, error) { // TODO: hex
+	// The max ASCII length of a uint32 is 10 (length of 4294967295). If we are at that length we can overflow.
+	//
+	// Note: There's chance we are at length 10 due to underscores, but easier to take the slow path for either reason.
+	if len(tokenBytes) > 9 {
+		v, err := decodeUint64(tokenBytes)
+		if err != nil {
+			return 0, err
+		}
+		if v > 0xffffffff {
+			return 0, &strconv.NumError{Func: "decodeU32", Num: string(tokenBytes), Err: strconv.ErrRange}
+		}
+		return uint32(v), nil
+	}
+
+	// If we are here, we know we cannot overflow uint32. The only valid characters in tokenUN are 0-9 and underscore.
+	var n uint32
+	for _, ch := range tokenBytes {
+		if ch == '_' {
+			continue
+		}
+		n = n*10 + uint32(ch-'0')
+	}
+	return n, nil
+}
+
+// decodeUint64 is like decodeUint32, but for uint64
+func decodeUint64(tokenBytes []byte) (uint64, error) { // TODO: hex
+	// The max ASCII length of a uint64 is 20 (length of 18446744073709551615). If we are at that length we can overflow.
+	//
+	// Note: There's chance we are at length 20 due to underscores, but easier to take the slow path for either reason.
+	if len(tokenBytes) > 19 {
+		return decodeUint64SlowPath(tokenBytes)
+	}
+
+	// If we are here, we know we cannot overflow uint64. The only valid characters in tokenUN are 0-9 and underscore.
+	var n uint64
+	for _, ch := range tokenBytes {
+		if ch == '_' {
+			continue
+		}
+		n = n*10 + uint64(ch-'0')
+	}
+	return n, nil
+}
+
+// decodeUint64SlowPath implements decodeUint64 for the slow path when there's a chance the result cannot fit.
+// Notably, this strips underscore characters first, so that the string length can be used to assess overflows.
+func decodeUint64SlowPath(tokenBytes []byte) (uint64, error) {
+	// We have a possible overflow, but won't know for sure due to underscores until we strip them. This strips any
+	// underscores from the token in-place.
+	n := 0
+	for _, b := range tokenBytes {
+		if b != '_' {
+			tokenBytes[n] = b
+			n++
+		}
+	}
+	tokenBytes = tokenBytes[:n]
+
+	// Now, we know there are only numbers and no underscores. This means the ASCII length is insightful.
+	switch len(tokenBytes) {
+	case 19: // cannot overflow
+		return decodeUint64(tokenBytes)
+	case 20: // only overflows depending on the last number
+		first19, err := decodeUint64(tokenBytes[0:19])
+		if err != nil {
+			return 0, err // impossible unless someone used this with an unvalidated token
+		}
+		last := uint64(tokenBytes[19] - '0')
+
+		// Remember the largest uint64 encoded in ASCII is 20 characters: "1844674407370955161" followed by "5"
+		if first19 > 1844674407370955161 /* first 19 chars of largest uint64 */ || last > 5 {
+			return 0, &strconv.NumError{Func: "decodeU64", Num: string(tokenBytes), Err: strconv.ErrRange}
+		}
+		return first19*10 + last, nil
+	default: // must overflow
+		return 0, &strconv.NumError{Func: "decodeU64", Num: string(tokenBytes), Err: strconv.ErrRange}
+	}
+}
diff --git a/wasm/wat/parser.go b/wasm/wat/parser.go
index 2ef303a8..390f411e 100644
--- a/wasm/wat/parser.go
+++ b/wasm/wat/parser.go
@@ -89,6 +89,12 @@ func parseModule(source []byte) (*module, error) {
 
 	// Add any types implicitly defined from type use. Ex. (module (import (func (param i32)...
 	p.module.typeFuncs = append(p.module.typeFuncs, p.typeParser.inlinedTypes...)
+
+	// Ensure indices only point to numeric values
+	if err = bindIndices(p.module); err != nil {
+		return nil, err
+	}
+
 	return p.module, nil
 }
 
@@ -168,7 +174,7 @@ func (p *moduleParser) endField() {
 		p.currentField = fieldInitial
 		p.tokenParser = p.parseUnexpectedTrailingCharacters // only one module is allowed and nothing else
 	default: // currentField is an enum, we expect to have handled all cases above. panic if we didn't
-		panic(fmt.Errorf("BUG: unhandled parsing state: %v", p.currentField))
+		panic(fmt.Errorf("BUG: unhandled parsing state on endField: %v", p.currentField))
 	}
 }
 
@@ -299,9 +305,8 @@ func (p *moduleParser) parseImport(tok tokenType, tokenBytes []byte, _, _ uint32
 //                    calls parseImportFunc here --^
 func (p *moduleParser) parseImportFuncName(tok tokenType, tokenBytes []byte, line, col uint32) error {
 	if tok == tokenID { // Ex. $main
-		name := string(tokenBytes)
 		fn := p.module.importFuncs[len(p.module.importFuncs)-1]
-		fn.funcName = name
+		fn.funcName = string(tokenBytes)
 		p.tokenParser = p.parseImportFunc
 		return nil
 	}
@@ -343,11 +348,20 @@ func (p *moduleParser) parseImportFuncAfterType(tok tokenType, tokenBytes []byte
 
 func (p *moduleParser) parseStart(tok tokenType, tokenBytes []byte, line, col uint32) error {
 	switch tok {
-	case tokenUN, tokenID: // Ex. $main or 2
+	case tokenUN: // Ex. 2
+		if p.module.startFunction != nil {
+			return errors.New("redundant funcidx")
+		}
+		numeric, err := decodeUint32(tokenBytes)
+		if err != nil {
+			return fmt.Errorf("funcidx outside range of uint32: %s", tokenBytes)
+		}
+		p.module.startFunction = &index{numeric: numeric, line: line, col: col}
+	case tokenID: // Ex. $main
 		if p.module.startFunction != nil {
 			return errors.New("redundant funcidx")
 		}
-		p.module.startFunction = &startFunction{string(tokenBytes), line, col}
+		p.module.startFunction = &index{ID: string(tokenBytes), line: line, col: col}
 	case tokenRParen: // end of this start
 		if p.module.startFunction == nil {
 			return errors.New("missing funcidx")
@@ -383,6 +397,6 @@ func (p *moduleParser) errorContext() string {
 	case fieldModuleStart:
 		return "module.start"
 	default: // currentField is an enum, we expect to have handled all cases above. panic if we didn't
-		panic(fmt.Errorf("BUG: unhandled parsing state: %v", p.currentField))
+		panic(fmt.Errorf("BUG: unhandled parsing state on errorContext: %v", p.currentField))
 	}
 }
diff --git a/wasm/wat/types.go b/wasm/wat/types.go
index f3369d07..b710a082 100644
--- a/wasm/wat/types.go
+++ b/wasm/wat/types.go
@@ -31,20 +31,32 @@ type module struct {
 	// importFuncs are imports describing functions added in insertion order. Ex (import... (func...))
 	importFuncs []*importFunc
 
-	startFunction *startFunction
+	// startFunction is the index of the function to call during wasm.Store Instantiate. When a tokenID, this must match
+	// importFunc.funcName.
+	//
+	// See https://www.w3.org/TR/wasm-core-1/#start-function%E2%91%A4
+	startFunction *index
 }
 
-// startFunction is the function to call during wasm.Store Instantiate.
+// index is symbolic ID, such as "$main", or its equivalent numeric value, such as "2".
 //
 // Note: line and col used for lazy validation of index. These are attached to an error if later found to be invalid
 // (ex an unknown function or out-of-bound index).
 //
-// See https://www.w3.org/TR/wasm-core-1/#start-function%E2%91%A4
-type startFunction struct {
-	// index is a importFunc.name, such as "$main", or its equivalent numeric index in module.importFuncs, such as "2".
+// https://www.w3.org/TR/wasm-core-1/#indices%E2%91%A4
+type index struct {
+	// ID is set when its corresponding token is tokenID to a symbolic identifier index. Ex. $main
+	//
+	// Note: This must be checked for a corresponding index element name, as it is possible it doesn't exist.
+	// Ex. This is $t0 from (import "Math" "PI" (func (type $t0))), but (type $t0 (func ...)) does not exist.
+	ID string
+
+	// numeric is set when its corresponding token is tokenUN to a numeric index. Ex. 3
 	//
-	// See wasm.Module StartSection for more.
-	index string
+	// Note: To avoid conflating unset with the valid index zero, only read this value when ID is unset.
+	// Note: This must be checked for range as there's a possible out-of-bonds condition.
+	// Ex. This is 32 from (import "Math" "PI" (func (type 32))), but there are only 10 types defined in the module.
+	numeric uint32
 
 	// line is the line in the source where the index was defined.
 	line uint32
@@ -63,7 +75,7 @@ type typeFunc struct {
 	//
 	// name is only used for debugging. At runtime, types are called based on raw numeric index. The type index space
 	// begins those explicitly defined in module.typeFuncs, followed by any inlined ones.
-	name string
+	name string // TODO: presumably, this must be unique as it is a symbolic identifier?
 
 	// params are the possibly empty sequence of value types accepted by a function with this signature.
 	//
@@ -116,7 +128,7 @@ type importFunc struct {
 	// Note: funcName may be stored in the wasm.Module CustomSection under the key "name" subsection 1. For example,
 	// `wat2wasm --debug-names` will do this.
 	// See https://www.w3.org/TR/wasm-core-1/#binary-namesec
-	funcName string
+	funcName string // TODO: presumably, this must be unique as it is a symbolic identifier?
 }
 
 // FormatError allows control over the format of errors parsing the WebAssembly Text Format.


Test patch:
diff --git a/wasm/wat/bind_test.go b/wasm/wat/bind_test.go
new file mode 100644
index 00000000..d5d073df
--- /dev/null
+++ b/wasm/wat/bind_test.go
@@ -0,0 +1,91 @@
+package wat
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestBindIndices(t *testing.T) {
+	tests := []struct {
+		name            string
+		input, expected *module
+	}{
+		{
+			name: "start imported function by name binds to numeric index",
+			input: &module{
+				typeFuncs: []*typeFunc{typeFuncEmpty},
+				importFuncs: []*importFunc{
+					{funcName: "$one", typeInlined: typeFuncEmpty},
+					{funcName: "$two", typeInlined: typeFuncEmpty},
+				},
+				startFunction: &index{ID: "$two", line: 3, col: 9},
+			},
+			expected: &module{
+				typeFuncs: []*typeFunc{typeFuncEmpty},
+				importFuncs: []*importFunc{
+					{funcName: "$one", typeInlined: typeFuncEmpty},
+					{funcName: "$two", typeInlined: typeFuncEmpty},
+				},
+				startFunction: &index{numeric: 1, line: 3, col: 9},
+			},
+		},
+		{
+			name: "start imported function numeric index left alone",
+			input: &module{
+				typeFuncs:     []*typeFunc{typeFuncEmpty},
+				importFuncs:   []*importFunc{{name: "hello", importIndex: 0, typeInlined: typeFuncEmpty}},
+				startFunction: &index{numeric: 0, line: 3, col: 9},
+			},
+			expected: &module{
+				typeFuncs:     []*typeFunc{typeFuncEmpty},
+				importFuncs:   []*importFunc{{name: "hello", importIndex: 0, typeInlined: typeFuncEmpty}},
+				startFunction: &index{numeric: 0, line: 3, col: 9},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			err := bindIndices(tc.input)
+			require.NoError(t, err)
+			require.Equal(t, tc.expected, tc.input)
+		})
+	}
+}
+
+func TestBindIndices_Errors(t *testing.T) {
+	tests := []struct {
+		name        string
+		input       *module
+		expectedErr string
+	}{
+		{
+			name: "start points out of range",
+			input: &module{
+				typeFuncs:     []*typeFunc{typeFuncEmpty},
+				importFuncs:   []*importFunc{{name: "hello", importIndex: 0, typeInlined: typeFuncEmpty}},
+				startFunction: &index{numeric: 1, line: 3, col: 9},
+			},
+			expectedErr: "3:9: function index 1 is out of range [0..0] in module.start",
+		},
+		{
+			name: "start points nowhere",
+			input: &module{
+				startFunction: &index{ID: "$main", line: 1, col: 16},
+			},
+			expectedErr: "1:16: unknown function name $main in module.start",
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			err := bindIndices(tc.input)
+			require.EqualError(t, err, tc.expectedErr)
+		})
+	}
+}
diff --git a/wasm/wat/convert_test.go b/wasm/wat/convert_test.go
index cb19249a..840a3450 100644
--- a/wasm/wat/convert_test.go
+++ b/wasm/wat/convert_test.go
@@ -125,23 +125,24 @@ func TestTextToBinary_Errors(t *testing.T) {
 		{
 			name:        "start, but no funcs",
 			input:       "(module (start $main))",
-			expectedErr: "1:16: unknown function name: $main in module.start",
+			expectedErr: "1:16: unknown function name $main in module.start",
 		},
 		{
-			name: "start index missing - number",
+			name: "start index out of range",
 			input: `(module
 	(import "" "hello" (func))
-	(start 1)
+	(import "" "goodbye" (func))
+	(start 3)
 )`,
-			expectedErr: "3:9: invalid function index: 1 in module.start",
+			expectedErr: "4:9: function index 3 is out of range [0..1] in module.start",
 		},
 		{
-			name: "start index missing - name",
+			name: "start points to unknown func",
 			input: `(module
 	(import "" "hello" (func $main))
 	(start $mein)
 )`,
-			expectedErr: "3:9: unknown function name: $mein in module.start",
+			expectedErr: "3:9: unknown function name $mein in module.start",
 		},
 	}
 
diff --git a/wasm/wat/numbers_test.go b/wasm/wat/numbers_test.go
new file mode 100644
index 00000000..8ed6b3bf
--- /dev/null
+++ b/wasm/wat/numbers_test.go
@@ -0,0 +1,69 @@
+package wat
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestDecodeUint32(t *testing.T) {
+	for _, tt := range []struct {
+		name, input string
+		expected    uint32
+		expectedErr bool
+	}{
+		{name: "zero", input: "0", expected: 0},
+		{name: "largest uint16", input: "65535", expected: 0xffff},
+		{name: "largest uint16 with underscores", input: "6_5_5_3_5", expected: 0xffff},
+		{name: "under largest uint32 by factor of 10", input: "429496729", expected: 429496729},
+		{name: "under largest uint32 by factor of 10 with underscores", input: "4_2_9_4_9_6_7_2_9", expected: 429496729},
+		{name: "largest uint32", input: "4294967295", expected: 0xffffffff},
+		{name: "largest uint32 with underscores", input: "4_2_9_4_9_6_7_2_9_5", expected: 0xffffffff},
+		{name: "overflow by one", input: "4294967296", expectedErr: true},
+		{name: "overflow by one with underscores", input: "4_2_9_4_9_6_7_2_9_6", expectedErr: true},
+		{name: "overflow by factor of 10", input: "42949672950", expectedErr: true},
+		{name: "overflow by factor of 10 with underscores", input: "4_2_9_4_9_6_7_2_9_5_0", expectedErr: true},
+	} {
+		tc := tt
+		t.Run(tc.name, func(t *testing.T) {
+			actual, err := decodeUint32([]byte(tc.input))
+			if tc.expectedErr {
+				require.Error(t, err)
+			} else {
+				require.NoError(t, err)
+				assert.Equal(t, tc.expected, actual)
+			}
+		})
+	}
+}
+
+func TestDecodeUint64(t *testing.T) {
+	for _, tt := range []struct {
+		name, input string
+		expected    uint64
+		expectedErr bool
+	}{
+		{name: "zero", input: "0", expected: 0},
+		{name: "largest uint32", input: "4294967295", expected: 0xffffffff},
+		{name: "under largest uint64 by factor of 10", input: "1844674407370955161", expected: 1844674407370955161},
+		{name: "under largest uint64 by factor of 10 with underscores", input: "1_8_4_4_6_7_4_4_0_7_3_7_0_9_5_5_1_6_1", expected: 1844674407370955161},
+		{name: "largest uint64", input: "18446744073709551615", expected: 0xffffffffffffffff},
+		{name: "largest uint64 with underscores", input: "1_8_4_4_6_7_4_4_0_7_3_7_0_9_5_5_1_6_1_5", expected: 0xffffffffffffffff},
+		{name: "overflow by one", input: "18446744073709551616", expectedErr: true},
+		{name: "overflow by one with underscores", input: "1_8_4_4_6_7_4_4_0_7_3_7_0_9_5_5_1_6_1_6", expectedErr: true},
+		{name: "overflow by factor of 10", input: "184467440737095516150", expectedErr: true},
+		{name: "overflow by factor of 10 with underscores", input: "1_8_4_4_6_7_4_4_0_7_3_7_0_9_5_5_1_6_1_5_0", expectedErr: true},
+	} {
+		tc := tt
+		t.Run(tc.name, func(t *testing.T) {
+			actual, err := decodeUint64([]byte(tc.input))
+			if tc.expectedErr {
+				require.Error(t, err)
+			} else {
+				require.NoError(t, err)
+				assert.Equal(t, tc.expected, actual)
+			}
+		})
+	}
+}
diff --git a/wasm/wat/parser_test.go b/wasm/wat/parser_test.go
index 91339b4d..f6f65097 100644
--- a/wasm/wat/parser_test.go
+++ b/wasm/wat/parser_test.go
@@ -163,11 +163,6 @@ func TestParseModule(t *testing.T) {
 				},
 			},
 		},
-		{
-			name:     "start function", // TODO: this is pointing to a funcidx not in the source!
-			input:    "(module (start $main))",
-			expected: &module{startFunction: &startFunction{"$main", 1, 16}},
-		},
 		{
 			name: "start imported function by name",
 			input: `(module
@@ -177,7 +172,7 @@ func TestParseModule(t *testing.T) {
 			expected: &module{
 				typeFuncs:     []*typeFunc{typeFuncEmpty},
 				importFuncs:   []*importFunc{{name: "hello", funcName: "$hello", typeInlined: typeFuncEmpty}},
-				startFunction: &startFunction{"$hello", 3, 9},
+				startFunction: &index{numeric: 0, line: 3, col: 9},
 			},
 		},
 		{
@@ -189,7 +184,7 @@ func TestParseModule(t *testing.T) {
 			expected: &module{
 				typeFuncs:     []*typeFunc{typeFuncEmpty},
 				importFuncs:   []*importFunc{{name: "hello", importIndex: 0, typeInlined: typeFuncEmpty}},
-				startFunction: &startFunction{"0", 3, 9},
+				startFunction: &index{numeric: 0, line: 3, col: 9},
 			},
 		},
 	}
@@ -402,6 +397,19 @@ func TestParseModule_Errors(t *testing.T) {
 			input:       "(module (start main))",
 			expectedErr: "1:16: unexpected keyword: main in module.start",
 		},
+		{
+			name: "start points out of range",
+			input: `(module
+	(import "" "hello" (func))
+	(start 1)
+)`,
+			expectedErr: "3:9: function index 1 is out of range [0..0] in module.start",
+		},
+		{
+			name:        "start points nowhere",
+			input:       "(module (start $main))",
+			expectedErr: "1:16: unknown function name $main in module.start",
+		},
 	}
 
 	for _, tt := range tests {