Repository: tetratelabs/wazero
Pull request #877 (2022-12-01 08:42:03)

binary: adds custom section decoder
Disabled by default, and will be only enabled true if DWARF trace option is on (which will be added later)

Signed-off-by: Takeshi Yoneda <takeshi@tetrate.io>


Patch:
diff --git a/internal/wasm/binary/custom.go b/internal/wasm/binary/custom.go
new file mode 100644
index 00000000..771f8c32
--- /dev/null
+++ b/internal/wasm/binary/custom.go
@@ -0,0 +1,22 @@
+package binary
+
+import (
+	"bytes"
+
+	"github.com/tetratelabs/wazero/internal/wasm"
+)
+
+// decodeCustomSection deserializes the data **not** associated with the "name" key in SectionIDCustom.
+//
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#custom-section%E2%91%A0
+func decodeCustomSection(r *bytes.Reader, name string, limit uint64) (result *wasm.CustomSection, err error) {
+	buf := make([]byte, limit)
+	_, err = r.Read(buf)
+
+	result = &wasm.CustomSection{
+		Name: name,
+		Data: buf,
+	}
+
+	return
+}
diff --git a/internal/wasm/binary/decoder.go b/internal/wasm/binary/decoder.go
index 6747dce9..04f90895 100644
--- a/internal/wasm/binary/decoder.go
+++ b/internal/wasm/binary/decoder.go
@@ -17,7 +17,8 @@ func DecodeModule(
 	binary []byte,
 	enabledFeatures api.CoreFeatures,
 	memoryLimitPages uint32,
-	memoryCapacityFromMax bool,
+	memoryCapacityFromMax,
+	storeCustomSections bool,
 ) (*wasm.Module, error) {
 	r := bytes.NewReader(binary)
 
@@ -66,17 +67,22 @@ func DecodeModule(
 				break
 			}
 
-			// Now, either decode the NameSection or skip an unsupported one
+			// Now, either decode the NameSection or CustomSection
 			limit := sectionSize - nameSize
 			if name == "name" {
 				m.NameSection, err = decodeNameSection(r, uint64(limit))
+			} else if storeCustomSections {
+				custom, err := decodeCustomSection(r, name, uint64(limit))
+				if err != nil {
+					return nil, fmt.Errorf("failed to read custom section name[%s]: %w", name, err)
+				}
+				m.CustomSections = append(m.CustomSections, custom)
 			} else {
 				// Note: Not Seek because it doesn't err when given an offset past EOF. Rather, it leads to undefined state.
 				if _, err = io.CopyN(io.Discard, r, int64(limit)); err != nil {
 					return nil, fmt.Errorf("failed to skip name[%s]: %w", name, err)
 				}
 			}
-
 		case wasm.SectionIDType:
 			m.TypeSection, err = decodeTypeSection(enabledFeatures, r)
 		case wasm.SectionIDImport:
diff --git a/internal/wasm/counts.go b/internal/wasm/counts.go
index 5f10c8ea..47b29527 100644
--- a/internal/wasm/counts.go
+++ b/internal/wasm/counts.go
@@ -41,16 +41,17 @@ func (m *Module) importCount(et ExternType) (res uint32) {
 //
 // For example...
 // * SectionIDType returns the count of FunctionType
-// * SectionIDCustom returns one if the NameSection is present
+// * SectionIDCustom returns the count of CustomSections plus one if NameSection is present
 // * SectionIDHostFunction returns the count of HostFunctionSection
 // * SectionIDExport returns the count of unique export names
 func (m *Module) SectionElementCount(sectionID SectionID) uint32 { // element as in vector elements!
 	switch sectionID {
 	case SectionIDCustom:
+		numCustomSections := uint32(len(m.CustomSections))
 		if m.NameSection != nil {
-			return 1
+			numCustomSections++
 		}
-		return 0
+		return numCustomSections
 	case SectionIDType:
 		return uint32(len(m.TypeSection))
 	case SectionIDImport:
diff --git a/internal/wasm/module.go b/internal/wasm/module.go
index 9ca7dc92..dccf42f2 100644
--- a/internal/wasm/module.go
+++ b/internal/wasm/module.go
@@ -154,6 +154,11 @@ type Module struct {
 	// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#custom-section%E2%91%A0
 	NameSection *NameSection
 
+	// CustomSections are set when the SectionIDCustom other than "name" were successfully decoded from the binary format.
+	//
+	// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#custom-section%E2%91%A0
+	CustomSections []*CustomSection
+
 	// validatedActiveElementSegments are built on Validate when
 	// SectionIDElement is non-empty and all inputs are valid.
 	//
@@ -877,6 +882,12 @@ type NameSection struct {
 	LocalNames IndirectNameMap
 }
 
+// CustomSection contains the name and raw data of a custom section.
+type CustomSection struct {
+	Name string
+	Data []byte
+}
+
 // NameMap associates an index with any associated names.
 //
 // Note: Often the index namespace bridges multiple sections. For example, the function index namespace starts with any
diff --git a/runtime.go b/runtime.go
index c3d21ddf..91878126 100644
--- a/runtime.go
+++ b/runtime.go
@@ -172,7 +172,7 @@ func (r *runtime) CompileModule(ctx context.Context, binary []byte) (CompiledMod
 		return nil, errors.New("invalid binary")
 	}
 
-	internal, err := binaryformat.DecodeModule(binary, r.enabledFeatures, r.memoryLimitPages, r.memoryCapacityFromMax)
+	internal, err := binaryformat.DecodeModule(binary, r.enabledFeatures, r.memoryLimitPages, r.memoryCapacityFromMax, false)
 	if err != nil {
 		return nil, err
 	} else if err = internal.Validate(r.enabledFeatures); err != nil {


Test patch:
diff --git a/internal/integration_test/bench/codec_test.go b/internal/integration_test/bench/codec_test.go
index 82649520..2774c190 100644
--- a/internal/integration_test/bench/codec_test.go
+++ b/internal/integration_test/bench/codec_test.go
@@ -89,7 +89,7 @@ func newExample() *wasm.Module {
 
 func TestExampleUpToDate(t *testing.T) {
 	t.Run("binary.DecodeModule", func(t *testing.T) {
-		m, err := binary.DecodeModule(exampleWasm, api.CoreFeaturesV2, wasm.MemoryLimitPages, false)
+		m, err := binary.DecodeModule(exampleWasm, api.CoreFeaturesV2, wasm.MemoryLimitPages, false, false)
 		require.NoError(t, err)
 		require.Equal(t, example, m)
 	})
@@ -116,7 +116,7 @@ func BenchmarkCodec(b *testing.B) {
 	b.Run("binary.DecodeModule", func(b *testing.B) {
 		b.ReportAllocs()
 		for i := 0; i < b.N; i++ {
-			if _, err := binary.DecodeModule(exampleWasm, api.CoreFeaturesV2, wasm.MemoryLimitPages, false); err != nil {
+			if _, err := binary.DecodeModule(exampleWasm, api.CoreFeaturesV2, wasm.MemoryLimitPages, false, false); err != nil {
 				b.Fatal(err)
 			}
 		}
diff --git a/internal/integration_test/spectest/spectest.go b/internal/integration_test/spectest/spectest.go
index f8243a6d..daa16d6e 100644
--- a/internal/integration_test/spectest/spectest.go
+++ b/internal/integration_test/spectest/spectest.go
@@ -325,7 +325,7 @@ var spectestWasm []byte
 // See https://github.com/WebAssembly/spec/blob/wg-1.0/test/core/imports.wast
 // See https://github.com/WebAssembly/spec/blob/wg-1.0/interpreter/script/js.ml#L13-L25
 func addSpectestModule(t *testing.T, ctx context.Context, s *wasm.Store, ns *wasm.Namespace, enabledFeatures api.CoreFeatures) {
-	mod, err := binaryformat.DecodeModule(spectestWasm, api.CoreFeaturesV2, wasm.MemoryLimitPages, false)
+	mod, err := binaryformat.DecodeModule(spectestWasm, api.CoreFeaturesV2, wasm.MemoryLimitPages, false, false)
 	require.NoError(t, err)
 
 	// (global (export "global_i32") i32 (i32.const 666))
@@ -420,7 +420,7 @@ func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, newEngine func(
 					case "module":
 						buf, err := testDataFS.ReadFile(testdataPath(c.Filename))
 						require.NoError(t, err, msg)
-						mod, err := binaryformat.DecodeModule(buf, enabledFeatures, wasm.MemoryLimitPages, false)
+						mod, err := binaryformat.DecodeModule(buf, enabledFeatures, wasm.MemoryLimitPages, false, false)
 						require.NoError(t, err, msg)
 						require.NoError(t, mod.Validate(enabledFeatures))
 						mod.AssignModuleID(buf)
@@ -561,7 +561,7 @@ func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, newEngine func(
 							//
 							// In practice, such a module instance can be used for invoking functions without any issue. In addition, we have to
 							// retain functions after the expected "instantiation" failure, so in wazero we choose to not raise error in that case.
-							mod, err := binaryformat.DecodeModule(buf, s.EnabledFeatures, wasm.MemoryLimitPages, false)
+							mod, err := binaryformat.DecodeModule(buf, s.EnabledFeatures, wasm.MemoryLimitPages, false, false)
 							require.NoError(t, err, msg)
 
 							err = mod.Validate(s.EnabledFeatures)
@@ -590,7 +590,7 @@ func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, newEngine func(
 }
 
 func requireInstantiationError(t *testing.T, ctx context.Context, s *wasm.Store, ns *wasm.Namespace, buf []byte, msg string) {
-	mod, err := binaryformat.DecodeModule(buf, s.EnabledFeatures, wasm.MemoryLimitPages, false)
+	mod, err := binaryformat.DecodeModule(buf, s.EnabledFeatures, wasm.MemoryLimitPages, false, false)
 	if err != nil {
 		return
 	}
diff --git a/internal/wasm/binary/decoder_test.go b/internal/wasm/binary/decoder_test.go
index d0c328b4..2af370a4 100644
--- a/internal/wasm/binary/decoder_test.go
+++ b/internal/wasm/binary/decoder_test.go
@@ -81,7 +81,7 @@ func TestDecodeModule(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			m, e := DecodeModule(EncodeModule(tc.input), api.CoreFeaturesV1, wasm.MemoryLimitPages, false)
+			m, e := DecodeModule(EncodeModule(tc.input), api.CoreFeaturesV1, wasm.MemoryLimitPages, false, false)
 			require.NoError(t, e)
 			require.Equal(t, tc.input, m)
 		})
@@ -92,11 +92,28 @@ func TestDecodeModule(t *testing.T) {
 			wasm.SectionIDCustom, 0xf, // 15 bytes in this section
 			0x04, 'm', 'e', 'm', 'e',
 			1, 2, 3, 4, 5, 6, 7, 8, 9, 0)
-		m, e := DecodeModule(input, api.CoreFeaturesV1, wasm.MemoryLimitPages, false)
+		m, e := DecodeModule(input, api.CoreFeaturesV1, wasm.MemoryLimitPages, false, false)
 		require.NoError(t, e)
 		require.Equal(t, &wasm.Module{}, m)
 	})
 
+	t.Run("reads custom sections", func(t *testing.T) {
+		input := append(append(Magic, version...),
+			wasm.SectionIDCustom, 0xf, // 15 bytes in this section
+			0x04, 'm', 'e', 'm', 'e',
+			1, 2, 3, 4, 5, 6, 7, 8, 9, 0)
+		m, e := DecodeModule(input, api.CoreFeaturesV2, wasm.MemoryLimitPages, false, true)
+		require.NoError(t, e)
+		require.Equal(t, &wasm.Module{
+			CustomSections: []*wasm.CustomSection{
+				{
+					Name: "meme",
+					Data: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0},
+				},
+			},
+		}, m)
+	})
+
 	t.Run("skips custom section, but not name", func(t *testing.T) {
 		input := append(append(Magic, version...),
 			wasm.SectionIDCustom, 0xf, // 15 bytes in this section
@@ -107,14 +124,38 @@ func TestDecodeModule(t *testing.T) {
 			subsectionIDModuleName, 0x07, // 7 bytes in this subsection
 			0x06, // the Module name simple is 6 bytes long
 			's', 'i', 'm', 'p', 'l', 'e')
-		m, e := DecodeModule(input, api.CoreFeaturesV1, wasm.MemoryLimitPages, false)
+		m, e := DecodeModule(input, api.CoreFeaturesV1, wasm.MemoryLimitPages, false, false)
 		require.NoError(t, e)
 		require.Equal(t, &wasm.Module{NameSection: &wasm.NameSection{ModuleName: "simple"}}, m)
 	})
+
+	t.Run("read custom sections and name separately", func(t *testing.T) {
+		input := append(append(Magic, version...),
+			wasm.SectionIDCustom, 0xf, // 15 bytes in this section
+			0x04, 'm', 'e', 'm', 'e',
+			1, 2, 3, 4, 5, 6, 7, 8, 9, 0,
+			wasm.SectionIDCustom, 0x0e, // 14 bytes in this section
+			0x04, 'n', 'a', 'm', 'e',
+			subsectionIDModuleName, 0x07, // 7 bytes in this subsection
+			0x06, // the Module name simple is 6 bytes long
+			's', 'i', 'm', 'p', 'l', 'e')
+		m, e := DecodeModule(input, api.CoreFeaturesV2, wasm.MemoryLimitPages, false, true)
+		require.NoError(t, e)
+		require.Equal(t, &wasm.Module{
+			NameSection: &wasm.NameSection{ModuleName: "simple"},
+			CustomSections: []*wasm.CustomSection{
+				{
+					Name: "meme",
+					Data: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0},
+				},
+			},
+		}, m)
+	})
+
 	t.Run("data count section disabled", func(t *testing.T) {
 		input := append(append(Magic, version...),
 			wasm.SectionIDDataCount, 1, 0)
-		_, e := DecodeModule(input, api.CoreFeaturesV1, wasm.MemoryLimitPages, false)
+		_, e := DecodeModule(input, api.CoreFeaturesV1, wasm.MemoryLimitPages, false, false)
 		require.EqualError(t, e, `data count section not supported as feature "bulk-memory-operations" is disabled`)
 	})
 }
@@ -164,7 +205,7 @@ func TestDecodeModule_Errors(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			_, e := DecodeModule(tc.input, api.CoreFeaturesV1, wasm.MemoryLimitPages, false)
+			_, e := DecodeModule(tc.input, api.CoreFeaturesV1, wasm.MemoryLimitPages, false, false)
 			require.EqualError(t, e, tc.expectedErr)
 		})
 	}