blob: ac3ffc4726318140b888842dfd1bb007ca03b697 [file] [log] [blame] [edit]
// Copyright 2020-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package wasm
import (
"context"
"errors"
"fmt"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
)
const (
// defaultMaxMemoryBytes is the maximum memory size in bytes.
defaultMaxMemoryBytes = 1 << 29 // 512 MiB
// wasmPageSize is the page size in bytes.
wasmPageSize = 1 << 16 // 64 KiB
)
type runtime struct {
runtime wazero.Runtime
compilationCache wazero.CompilationCache
}
func newRuntime(ctx context.Context, options ...RuntimeOption) (*runtime, error) {
runtimeOptions := newRuntimeOptions()
for _, option := range options {
option(runtimeOptions)
}
if runtimeOptions.maxMemoryBytes == 0 {
return nil, fmt.Errorf("Wasm max memory bytes must be greater than 0")
}
// The maximum memory size is limited to 4 GiB. Sizes less than the page
// size (64 KiB) are truncated. memoryLimitPages is guaranteed to be
// below 2^16 as the maximum uint32 value is 2^32 - 1.
// NOTE: The option represented as a uint32 restricts the max number of
// pages to 2^16-1, one less the the actual max value of 2^16. But this
// is a nicer API then specifying the max number of pages directly.
memoryLimitPages := runtimeOptions.maxMemoryBytes / wasmPageSize
if memoryLimitPages == 0 {
return nil, fmt.Errorf("Wasm max memory bytes %d is too small", runtimeOptions.maxMemoryBytes)
}
// Create the wazero.RuntimeConfig with enforceable limits. Limits are
// enforced through the Wasm sandbox. The following limits are set:
// - Memory limit: The maximum memory size in pages.
// - CPU limit: The runtime stops work on context done.
// - Access limit: All system interfaces are stubbed. No network,
// disk, clock, etc.
// See wazero.NewRuntimeConfig for more details.
wazeroRuntimeConfig := wazero.NewRuntimeConfig().
WithCoreFeatures(api.CoreFeaturesV2).
WithCloseOnContextDone(true).
WithMemoryLimitPages(memoryLimitPages)
var wazeroCompilationCache wazero.CompilationCache
if runtimeOptions.cacheDir != "" {
var err error
wazeroCompilationCache, err = wazero.NewCompilationCacheWithDir(runtimeOptions.cacheDir)
if err != nil {
return nil, fmt.Errorf("failed to create compilation cache: %w", err)
}
wazeroRuntimeConfig = wazeroRuntimeConfig.WithCompilationCache(wazeroCompilationCache)
}
wazeroRuntime := wazero.NewRuntimeWithConfig(ctx, wazeroRuntimeConfig)
// Init WASI preview1 APIs. This is required to support the pluginrpc
// protocol. The returned closer method is discarded as the
// instantiated module is never required to be unloaded.
if _, err := wasi_snapshot_preview1.Instantiate(ctx, wazeroRuntime); err != nil {
return nil, fmt.Errorf("failed to instantiate WASI snapshot preview1: %w", err)
}
return &runtime{
runtime: wazeroRuntime,
compilationCache: wazeroCompilationCache,
}, nil
}
func (r *runtime) Compile(ctx context.Context, moduleName string, moduleWasm []byte) (CompiledModule, error) {
if moduleName == "" {
// The plugin is required to be named. We cannot use the name
// from the Wasm binary as this is not guaranteed to be set and
// may conflict with the provided name.
return nil, fmt.Errorf("name is empty")
}
// Compile the WebAssembly. This operation is hashed on the module
// bytes and the runtime configuration. The compiled module is
// cached in memory and on disk if an optional cache directory is
// provided.
wazeroCompiledModule, err := r.runtime.CompileModule(ctx, moduleWasm)
if err != nil {
return nil, err
}
return &compiledModule{
moduleName: moduleName,
runtime: r.runtime,
compiledModule: wazeroCompiledModule,
}, nil
}
func (r *runtime) Close(ctx context.Context) error {
err := r.runtime.Close(ctx)
if r.compilationCache != nil {
err = errors.Join(err, r.compilationCache.Close(ctx))
}
return err
}
type runtimeOptions struct {
maxMemoryBytes uint32
cacheDir string
}
func newRuntimeOptions() *runtimeOptions {
return &runtimeOptions{
maxMemoryBytes: defaultMaxMemoryBytes,
}
}