binary: adds custom section decoder (#877)

Signed-off-by: Takeshi Yoneda <takeshi@tetrate.io>
This commit is contained in:
Takeshi Yoneda
2022-12-01 20:08:40 +09:00
committed by GitHub
parent 4adfa48eb0
commit 40e698e068
8 changed files with 99 additions and 18 deletions

View File

@@ -89,7 +89,7 @@ func newExample() *wasm.Module {
func TestExampleUpToDate(t *testing.T) { func TestExampleUpToDate(t *testing.T) {
t.Run("binary.DecodeModule", func(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.NoError(t, err)
require.Equal(t, example, m) require.Equal(t, example, m)
}) })
@@ -116,7 +116,7 @@ func BenchmarkCodec(b *testing.B) {
b.Run("binary.DecodeModule", func(b *testing.B) { b.Run("binary.DecodeModule", func(b *testing.B) {
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { 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) b.Fatal(err)
} }
} }

View File

@@ -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/test/core/imports.wast
// See https://github.com/WebAssembly/spec/blob/wg-1.0/interpreter/script/js.ml#L13-L25 // 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) { 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) require.NoError(t, err)
// (global (export "global_i32") i32 (i32.const 666)) // (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": case "module":
buf, err := testDataFS.ReadFile(testdataPath(c.Filename)) buf, err := testDataFS.ReadFile(testdataPath(c.Filename))
require.NoError(t, err, msg) 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, err, msg)
require.NoError(t, mod.Validate(enabledFeatures)) require.NoError(t, mod.Validate(enabledFeatures))
mod.AssignModuleID(buf) 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 // 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. // 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) require.NoError(t, err, msg)
err = mod.Validate(s.EnabledFeatures) 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) { 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 { if err != nil {
return return
} }

View File

@@ -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
}

View File

@@ -17,7 +17,8 @@ func DecodeModule(
binary []byte, binary []byte,
enabledFeatures api.CoreFeatures, enabledFeatures api.CoreFeatures,
memoryLimitPages uint32, memoryLimitPages uint32,
memoryCapacityFromMax bool, memoryCapacityFromMax,
storeCustomSections bool,
) (*wasm.Module, error) { ) (*wasm.Module, error) {
r := bytes.NewReader(binary) r := bytes.NewReader(binary)
@@ -66,17 +67,22 @@ func DecodeModule(
break break
} }
// Now, either decode the NameSection or skip an unsupported one // Now, either decode the NameSection or CustomSection
limit := sectionSize - nameSize limit := sectionSize - nameSize
if name == "name" { if name == "name" {
m.NameSection, err = decodeNameSection(r, uint64(limit)) 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 { } else {
// Note: Not Seek because it doesn't err when given an offset past EOF. Rather, it leads to undefined state. // 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 { if _, err = io.CopyN(io.Discard, r, int64(limit)); err != nil {
return nil, fmt.Errorf("failed to skip name[%s]: %w", name, err) return nil, fmt.Errorf("failed to skip name[%s]: %w", name, err)
} }
} }
case wasm.SectionIDType: case wasm.SectionIDType:
m.TypeSection, err = decodeTypeSection(enabledFeatures, r) m.TypeSection, err = decodeTypeSection(enabledFeatures, r)
case wasm.SectionIDImport: case wasm.SectionIDImport:

View File

@@ -81,7 +81,7 @@ func TestDecodeModule(t *testing.T) {
tc := tt tc := tt
t.Run(tc.name, func(t *testing.T) { 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.NoError(t, e)
require.Equal(t, tc.input, m) require.Equal(t, tc.input, m)
}) })
@@ -92,11 +92,28 @@ func TestDecodeModule(t *testing.T) {
wasm.SectionIDCustom, 0xf, // 15 bytes in this section wasm.SectionIDCustom, 0xf, // 15 bytes in this section
0x04, 'm', 'e', 'm', 'e', 0x04, 'm', 'e', 'm', 'e',
1, 2, 3, 4, 5, 6, 7, 8, 9, 0) 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.NoError(t, e)
require.Equal(t, &wasm.Module{}, m) 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) { t.Run("skips custom section, but not name", func(t *testing.T) {
input := append(append(Magic, version...), input := append(append(Magic, version...),
wasm.SectionIDCustom, 0xf, // 15 bytes in this section wasm.SectionIDCustom, 0xf, // 15 bytes in this section
@@ -107,14 +124,38 @@ func TestDecodeModule(t *testing.T) {
subsectionIDModuleName, 0x07, // 7 bytes in this subsection subsectionIDModuleName, 0x07, // 7 bytes in this subsection
0x06, // the Module name simple is 6 bytes long 0x06, // the Module name simple is 6 bytes long
's', 'i', 'm', 'p', 'l', 'e') '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.NoError(t, e)
require.Equal(t, &wasm.Module{NameSection: &wasm.NameSection{ModuleName: "simple"}}, m) 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) { t.Run("data count section disabled", func(t *testing.T) {
input := append(append(Magic, version...), input := append(append(Magic, version...),
wasm.SectionIDDataCount, 1, 0) 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`) 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 tc := tt
t.Run(tc.name, func(t *testing.T) { 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) require.EqualError(t, e, tc.expectedErr)
}) })
} }

View File

@@ -41,16 +41,17 @@ func (m *Module) importCount(et ExternType) (res uint32) {
// //
// For example... // For example...
// * SectionIDType returns the count of FunctionType // * 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 // * SectionIDHostFunction returns the count of HostFunctionSection
// * SectionIDExport returns the count of unique export names // * SectionIDExport returns the count of unique export names
func (m *Module) SectionElementCount(sectionID SectionID) uint32 { // element as in vector elements! func (m *Module) SectionElementCount(sectionID SectionID) uint32 { // element as in vector elements!
switch sectionID { switch sectionID {
case SectionIDCustom: case SectionIDCustom:
numCustomSections := uint32(len(m.CustomSections))
if m.NameSection != nil { if m.NameSection != nil {
return 1 numCustomSections++
} }
return 0 return numCustomSections
case SectionIDType: case SectionIDType:
return uint32(len(m.TypeSection)) return uint32(len(m.TypeSection))
case SectionIDImport: case SectionIDImport:

View File

@@ -154,6 +154,11 @@ type Module struct {
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#custom-section%E2%91%A0 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#custom-section%E2%91%A0
NameSection *NameSection 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 // validatedActiveElementSegments are built on Validate when
// SectionIDElement is non-empty and all inputs are valid. // SectionIDElement is non-empty and all inputs are valid.
// //
@@ -877,6 +882,12 @@ type NameSection struct {
LocalNames IndirectNameMap 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. // 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 // Note: Often the index namespace bridges multiple sections. For example, the function index namespace starts with any

View File

@@ -172,7 +172,7 @@ func (r *runtime) CompileModule(ctx context.Context, binary []byte) (CompiledMod
return nil, errors.New("invalid binary") 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 { if err != nil {
return nil, err return nil, err
} else if err = internal.Validate(r.enabledFeatures); err != nil { } else if err = internal.Validate(r.enabledFeatures); err != nil {