Repository: tetratelabs/wazero
Pull request #799 (2022-09-06 04:48:54)

compilationcache: improves error messages
Hopefully this can help us narrow down flakes like https://github.com/tetratelabs/wazero/runs/8199081288?check_suite_focus=true


Patch:
diff --git a/internal/engine/compiler/engine_cache.go b/internal/engine/compiler/engine_cache.go
index 2da9b4aa..0af1a7e7 100644
--- a/internal/engine/compiler/engine_cache.go
+++ b/internal/engine/compiler/engine_cache.go
@@ -125,11 +125,11 @@ func deserializeCodes(wazeroVersion string, reader io.Reader) (codes []*code, st
 	header := make([]byte, cacheHeaderSize)
 	n, err := reader.Read(header)
 	if err != nil {
-		return nil, false, err
+		return nil, false, fmt.Errorf("compilationcache: error reading header: %v", err)
 	}
 
 	if n != cacheHeaderSize {
-		return nil, false, fmt.Errorf("invalid header length: %d", n)
+		return nil, false, fmt.Errorf("compilationcache: invalid header length: %d", n)
 	}
 
 	// Check the version compatibility.
@@ -148,30 +148,27 @@ func deserializeCodes(wazeroVersion string, reader io.Reader) (codes []*code, st
 	codes = make([]*code, 0, functionsNum)
 
 	var eightBytes [8]byte
+	var nativeCodeLen uint64
 	for i := uint32(0); i < functionsNum; i++ {
 		c := &code{}
 
 		// Read the stack pointer ceil.
-		_, err = reader.Read(eightBytes[:])
-		if err != nil {
-			err = fmt.Errorf("reading stack pointer ceil: %v", err)
+		if c.stackPointerCeil, err = readUint64(reader, &eightBytes); err != nil {
+			err = fmt.Errorf("compilationcache: error reading func[%d] stack pointer ceil: %v", i, err)
 			break
 		}
 
-		c.stackPointerCeil = binary.LittleEndian.Uint64(eightBytes[:])
-
 		// Read (and mmap) the native code.
-		_, err = reader.Read(eightBytes[:])
-		if err != nil {
-			err = fmt.Errorf("reading native code size: %v", err)
+		if nativeCodeLen, err = readUint64(reader, &eightBytes); err != nil {
+			err = fmt.Errorf("compilationcache: error reading func[%d] reading native code size: %v", i, err)
 			break
 		}
 
-		c.codeSegment, err = platform.MmapCodeSegment(reader, int(binary.LittleEndian.Uint64(eightBytes[:])))
-		if err != nil {
-			err = fmt.Errorf("mmaping function: %v", err)
+		if c.codeSegment, err = platform.MmapCodeSegment(reader, int(nativeCodeLen)); err != nil {
+			err = fmt.Errorf("compilationcache: error mmapping func[%d] code (len=%d): %v", i, nativeCodeLen, err)
 			break
 		}
+
 		codes = append(codes, c)
 	}
 
@@ -186,3 +183,24 @@ func deserializeCodes(wazeroVersion string, reader io.Reader) (codes []*code, st
 	}
 	return
 }
+
+// readUint64 strictly reads a uint64 in little-endian byte order, using the
+// given array as a buffer. This returns io.EOF if less than 8 bytes were read.
+func readUint64(reader io.Reader, b *[8]byte) (uint64, error) {
+	s := b[0:8]
+	n, err := reader.Read(s)
+	if err != nil {
+		return 0, err
+	} else if n < 8 { // more strict than reader.Read
+		return 0, io.EOF
+	}
+
+	// read the u64 from the underlying buffer
+	ret := binary.LittleEndian.Uint64(s)
+
+	// clear the underlying array
+	for i := 0; i < 8; i++ {
+		b[i] = 0
+	}
+	return ret, nil
+}


Test patch:
diff --git a/internal/engine/compiler/engine_cache_test.go b/internal/engine/compiler/engine_cache_test.go
index 31d2abed..560814af 100644
--- a/internal/engine/compiler/engine_cache_test.go
+++ b/internal/engine/compiler/engine_cache_test.go
@@ -2,9 +2,13 @@ package compiler
 
 import (
 	"bytes"
+	"encoding/binary"
+	"errors"
 	"fmt"
 	"io"
+	"math"
 	"testing"
+	"testing/iotest"
 
 	"github.com/tetratelabs/wazero/internal/testing/require"
 	"github.com/tetratelabs/wazero/internal/u32"
@@ -79,7 +83,7 @@ func TestDeserializeCodes(t *testing.T) {
 
 			name:   "invalid header",
 			in:     []byte{1},
-			expErr: "invalid header length: 1",
+			expErr: "compilationcache: invalid header length: 1",
 		},
 		{
 
@@ -156,7 +160,7 @@ func TestDeserializeCodes(t *testing.T) {
 				[]byte{1, 2, 3, 4, 5}, // code.
 				// Function index = 1.
 			),
-			expErr: "reading stack pointer ceil: EOF",
+			expErr: "compilationcache: error reading func[1] stack pointer ceil: EOF",
 		},
 		{
 			name: "reading native code size",
@@ -172,7 +176,7 @@ func TestDeserializeCodes(t *testing.T) {
 				// Function index = 1.
 				u64.LeBytes(12345), // stack pointer ceil.
 			),
-			expErr: "reading native code size: EOF",
+			expErr: "compilationcache: error reading func[1] reading native code size: EOF",
 		},
 		{
 			name: "mmapping",
@@ -190,7 +194,7 @@ func TestDeserializeCodes(t *testing.T) {
 				u64.LeBytes(5),     // length of code.
 				// Lack of code here.
 			),
-			expErr: "mmaping function: EOF",
+			expErr: "compilationcache: error mmapping func[1] code (len=5): EOF",
 		},
 	}
 
@@ -233,7 +237,7 @@ func TestEngine_getCodesFromCache(t *testing.T) {
 		{
 			name:   "error in deserialization",
 			ext:    &testCache{caches: map[wasm.ModuleID][]byte{{}: {1, 2, 3}}},
-			expErr: "invalid header length: 3",
+			expErr: "compilationcache: invalid header length: 3",
 		},
 		{
 			name: "stale cache",
@@ -329,6 +333,78 @@ func TestEngine_addCodesToCache(t *testing.T) {
 	})
 }
 
+func Test_readUint64(t *testing.T) {
+	tests := []struct {
+		name  string
+		input uint64
+	}{
+		{
+			name:  "zero",
+			input: 0,
+		},
+		{
+			name:  "half",
+			input: math.MaxUint32,
+		},
+		{
+			name:  "max",
+			input: math.MaxUint64,
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			input := make([]byte, 8)
+			binary.LittleEndian.PutUint64(input, tc.input)
+
+			var b [8]byte
+			n, err := readUint64(bytes.NewReader(input), &b)
+			require.NoError(t, err)
+			require.Equal(t, tc.input, n)
+
+			// ensure the buffer was cleared
+			var expectedB [8]byte
+			require.Equal(t, expectedB, b)
+		})
+	}
+}
+
+func Test_readUint64_errors(t *testing.T) {
+	tests := []struct {
+		name        string
+		input       io.Reader
+		expectedErr string
+	}{
+		{
+			name:        "zero",
+			input:       bytes.NewReader([]byte{}),
+			expectedErr: "EOF",
+		},
+		{
+			name:        "not enough",
+			input:       bytes.NewReader([]byte{1, 2}),
+			expectedErr: "EOF",
+		},
+		{
+			name:        "error reading",
+			input:       iotest.ErrReader(errors.New("ice cream")),
+			expectedErr: "ice cream",
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			var b [8]byte
+			_, err := readUint64(tc.input, &b)
+			require.EqualError(t, err, tc.expectedErr)
+		})
+	}
+}
+
 // testCache implements compilationcache.Cache
 type testCache struct {
 	caches  map[wasm.ModuleID][]byte