-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[CAPPL-58] Correctly stub out clock_time_get and poll_oneoff #778
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
// NOTE: loosely based on: https://github.com/tetratelabs/wazero/blob/1353ca24fef0a57a3a342d75f20357a6e9d3be35/internal/wasip1/errno.go#L14 | ||
package host | ||
|
||
type Errno = int32 | ||
|
||
// Note: Below prefers POSIX symbol names over WASI ones, even if the docs are from WASI. | ||
// See https://linux.die.net/man/3/errno | ||
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#variants-1 | ||
const ( | ||
// ErrnoSuccess No error occurred. System call completed successfully. | ||
ErrnoSuccess Errno = iota | ||
// Errno2big Argument list too long. | ||
Errno2big | ||
// ErrnoAcces Permission denied. | ||
ErrnoAcces | ||
// ErrnoAddrinuse Address in use. | ||
ErrnoAddrinuse | ||
// ErrnoAddrnotavail Address not available. | ||
ErrnoAddrnotavail | ||
// ErrnoAfnosupport Address family not supported. | ||
ErrnoAfnosupport | ||
// ErrnoAgain Resource unavailable, or operation would block. | ||
ErrnoAgain | ||
// ErrnoAlready Connection already in progress. | ||
ErrnoAlready | ||
// ErrnoBadf Bad file descriptor. | ||
ErrnoBadf | ||
// ErrnoBadmsg Bad message. | ||
ErrnoBadmsg | ||
// ErrnoBusy Device or resource busy. | ||
ErrnoBusy | ||
// ErrnoCanceled Operation canceled. | ||
ErrnoCanceled | ||
// ErrnoChild No child processes. | ||
ErrnoChild | ||
// ErrnoConnaborted Connection aborted. | ||
ErrnoConnaborted | ||
// ErrnoConnrefused Connection refused. | ||
ErrnoConnrefused | ||
// ErrnoConnreset Connection reset. | ||
ErrnoConnreset | ||
// ErrnoDeadlk Resource deadlock would occur. | ||
ErrnoDeadlk | ||
// ErrnoDestaddrreq Destination address required. | ||
ErrnoDestaddrreq | ||
// ErrnoDom Mathematics argument out of domain of function. | ||
ErrnoDom | ||
// ErrnoDquot Reserved. | ||
ErrnoDquot | ||
// ErrnoExist File exists. | ||
ErrnoExist | ||
// ErrnoFault Bad address. | ||
ErrnoFault | ||
// ErrnoFbig File too large. | ||
ErrnoFbig | ||
// ErrnoHostunreach Host is unreachable. | ||
ErrnoHostunreach | ||
// ErrnoIdrm Identifier removed. | ||
ErrnoIdrm | ||
// ErrnoIlseq Illegal byte sequence. | ||
ErrnoIlseq | ||
// ErrnoInprogress Operation in progress. | ||
ErrnoInprogress | ||
// ErrnoIntr Interrupted function. | ||
ErrnoIntr | ||
// ErrnoInval Invalid argument. | ||
ErrnoInval | ||
// ErrnoIo I/O error. | ||
ErrnoIo | ||
// ErrnoIsconn Socket is connected. | ||
ErrnoIsconn | ||
// ErrnoIsdir Is a directory. | ||
ErrnoIsdir | ||
// ErrnoLoop Too many levels of symbolic links. | ||
ErrnoLoop | ||
// ErrnoMfile File descriptor value too large. | ||
ErrnoMfile | ||
// ErrnoMlink Too many links. | ||
ErrnoMlink | ||
// ErrnoMsgsize Message too large. | ||
ErrnoMsgsize | ||
// ErrnoMultihop Reserved. | ||
ErrnoMultihop | ||
// ErrnoNametoolong Filename too long. | ||
ErrnoNametoolong | ||
// ErrnoNetdown Network is down. | ||
ErrnoNetdown | ||
// ErrnoNetreset Connection aborted by network. | ||
ErrnoNetreset | ||
// ErrnoNetunreach Network unreachable. | ||
ErrnoNetunreach | ||
// ErrnoNfile Too many files open in system. | ||
ErrnoNfile | ||
// ErrnoNobufs No buffer space available. | ||
ErrnoNobufs | ||
// ErrnoNodev No such device. | ||
ErrnoNodev | ||
// ErrnoNoent No such file or directory. | ||
ErrnoNoent | ||
// ErrnoNoexec Executable file format error. | ||
ErrnoNoexec | ||
// ErrnoNolck No locks available. | ||
ErrnoNolck | ||
// ErrnoNolink Reserved. | ||
ErrnoNolink | ||
// ErrnoNomem Not enough space. | ||
ErrnoNomem | ||
// ErrnoNomsg No message of the desired type. | ||
ErrnoNomsg | ||
// ErrnoNoprotoopt No message of the desired type. | ||
ErrnoNoprotoopt | ||
// ErrnoNospc No space left on device. | ||
ErrnoNospc | ||
// ErrnoNosys function not supported. | ||
ErrnoNosys | ||
// ErrnoNotconn The socket is not connected. | ||
ErrnoNotconn | ||
// ErrnoNotdir Not a directory or a symbolic link to a directory. | ||
ErrnoNotdir | ||
// ErrnoNotempty Directory not empty. | ||
ErrnoNotempty | ||
// ErrnoNotrecoverable State not recoverable. | ||
ErrnoNotrecoverable | ||
// ErrnoNotsock Not a socket. | ||
ErrnoNotsock | ||
// ErrnoNotsup Not supported, or operation not supported on socket. | ||
ErrnoNotsup | ||
// ErrnoNotty Inappropriate I/O control operation. | ||
ErrnoNotty | ||
// ErrnoNxio No such device or address. | ||
ErrnoNxio | ||
// ErrnoOverflow Value too large to be stored in data type. | ||
ErrnoOverflow | ||
// ErrnoOwnerdead Previous owner died. | ||
ErrnoOwnerdead | ||
// ErrnoPerm Operation not permitted. | ||
ErrnoPerm | ||
// ErrnoPipe Broken pipe. | ||
ErrnoPipe | ||
// ErrnoProto Protocol error. | ||
ErrnoProto | ||
// ErrnoProtonosupport Protocol error. | ||
ErrnoProtonosupport | ||
// ErrnoPrototype Protocol wrong type for socket. | ||
ErrnoPrototype | ||
// ErrnoRange Result too large. | ||
ErrnoRange | ||
// ErrnoRofs Read-only file system. | ||
ErrnoRofs | ||
// ErrnoSpipe Invalid seek. | ||
ErrnoSpipe | ||
// ErrnoSrch No such process. | ||
ErrnoSrch | ||
// ErrnoStale Reserved. | ||
ErrnoStale | ||
// ErrnoTimedout Connection timed out. | ||
ErrnoTimedout | ||
// ErrnoTxtbsy Text file busy. | ||
ErrnoTxtbsy | ||
// ErrnoXdev Cross-device link. | ||
ErrnoXdev | ||
|
||
// Note: ErrnoNotcapable was removed by WASI maintainers. | ||
// See https://github.com/WebAssembly/wasi-libc/pull/294 | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,17 @@ | ||
package host | ||
|
||
import ( | ||
"bytes" | ||
"encoding/base64" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"math" | ||
"strings" | ||
"sync" | ||
"time" | ||
"unsafe" | ||
|
||
"github.com/andybalholm/brotli" | ||
"github.com/bytecodealliance/wasmtime-go/v23" | ||
"google.golang.org/protobuf/proto" | ||
|
||
|
@@ -18,18 +20,28 @@ import ( | |
wasmpb "github.com/smartcontractkit/chainlink-common/pkg/workflows/wasm/pb" | ||
) | ||
|
||
func safeMem(caller *wasmtime.Caller, ptr unsafe.Pointer, size int32) ([]byte, error) { | ||
func safeMem(caller *wasmtime.Caller, ptr int32, size int32) ([]byte, error) { | ||
mem := caller.GetExport("memory").Memory() | ||
data := mem.UnsafeData(caller) | ||
iptr := int32(uintptr(ptr)) | ||
if iptr+size > int32(len(data)) { | ||
if ptr+size > int32(len(data)) { | ||
return nil, errors.New("out of bounds memory access") | ||
} | ||
|
||
cd := make([]byte, size) | ||
copy(cd, data[iptr:iptr+size]) | ||
copy(cd, data[ptr:ptr+size]) | ||
return cd, nil | ||
} | ||
func copyBuffer(caller *wasmtime.Caller, src []byte, ptr int32, size int32) int64 { | ||
mem := caller.GetExport("memory").Memory() | ||
rawData := mem.UnsafeData(caller) | ||
if int32(len(rawData)) < ptr+size { | ||
return -1 | ||
} | ||
buffer := rawData[ptr : ptr+size] | ||
dataLen := int64(len(src)) | ||
copy(buffer, src) | ||
return dataLen | ||
} | ||
|
||
type respStore struct { | ||
m map[string]*wasmpb.Response | ||
|
@@ -65,15 +77,16 @@ var ( | |
defaultTickInterval = 100 * time.Millisecond | ||
defaultTimeout = 300 * time.Millisecond | ||
defaultMaxMemoryMBs = 64 | ||
defaultInitialFuel = uint64(100_000_000) | ||
DefaultInitialFuel = uint64(100_000_000) | ||
) | ||
|
||
type ModuleConfig struct { | ||
TickInterval time.Duration | ||
Timeout *time.Duration | ||
MaxMemoryMBs int64 | ||
InitialFuel uint64 | ||
Logger logger.Logger | ||
TickInterval time.Duration | ||
Timeout *time.Duration | ||
MaxMemoryMBs int64 | ||
InitialFuel uint64 | ||
Logger logger.Logger | ||
IsUncompressed bool | ||
} | ||
|
||
type Module struct { | ||
|
@@ -104,10 +117,6 @@ func NewModule(modCfg *ModuleConfig, binary []byte) (*Module, error) { | |
modCfg.Timeout = &defaultTimeout | ||
} | ||
|
||
if modCfg.InitialFuel == 0 { | ||
modCfg.InitialFuel = defaultInitialFuel | ||
} | ||
|
||
// Take the max of the default and the configured max memory mbs. | ||
// We do this because Go requires a minimum of 16 megabytes to run, | ||
// and local testing has shown that with less than 64 mbs, some | ||
|
@@ -116,65 +125,60 @@ func NewModule(modCfg *ModuleConfig, binary []byte) (*Module, error) { | |
|
||
cfg := wasmtime.NewConfig() | ||
cfg.SetEpochInterruption(true) | ||
cfg.SetConsumeFuel(true) | ||
if modCfg.InitialFuel > 0 { | ||
cfg.SetConsumeFuel(true) | ||
} | ||
|
||
engine := wasmtime.NewEngineWithConfig(cfg) | ||
|
||
if !modCfg.IsUncompressed { | ||
rdr := brotli.NewReader(bytes.NewBuffer(binary)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how did you choose brotli for the compression/decompression? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We did some benchmarking between gzip + brotli (and zopfli) and brotli offered the best mix of speed + compression performance. |
||
decompedBinary, err := io.ReadAll(rdr) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to decompress binary: %w", err) | ||
} | ||
|
||
binary = decompedBinary | ||
} | ||
|
||
mod, err := wasmtime.NewModule(engine, binary) | ||
if err != nil { | ||
return nil, fmt.Errorf("error creating wasmtime module: %w", err) | ||
} | ||
|
||
linker := wasmtime.NewLinker(engine) | ||
linker.AllowShadowing(true) | ||
|
||
err = linker.DefineWasi() | ||
linker, err := newWasiLinker(engine) | ||
if err != nil { | ||
return nil, err | ||
return nil, fmt.Errorf("error creating wasi linker: %w", err) | ||
} | ||
|
||
r := &respStore{ | ||
m: map[string]*wasmpb.Response{}, | ||
} | ||
|
||
// TODO: Stub out poll_oneoff correctly -- it's unclear what | ||
// the effect of this naive stub is as this syscall powers | ||
// notifications for time.Sleep, but will also effect other system notifications. | ||
// We need this stub to prevent binaries from calling time.Sleep and | ||
// starving our worker pool as a result. | ||
err = linker.FuncWrap( | ||
"wasi_snapshot_preview1", | ||
"poll_oneoff", | ||
func(caller *wasmtime.Caller, a int32, b int32, c int32, d int32) int32 { | ||
return 0 | ||
}, | ||
) | ||
if err != nil { | ||
return nil, fmt.Errorf("could not wrap poll_oneoff: %w", err) | ||
} | ||
|
||
err = linker.FuncWrap( | ||
"env", | ||
"sendResponse", | ||
func(caller *wasmtime.Caller, ptr int32, ptrlen int32) { | ||
b, innerErr := safeMem(caller, unsafe.Pointer(uintptr(ptr)), ptrlen) | ||
func(caller *wasmtime.Caller, ptr int32, ptrlen int32) int32 { | ||
b, innerErr := safeMem(caller, ptr, ptrlen) | ||
if innerErr != nil { | ||
logger.Errorf("error calling sendResponse: %s", err) | ||
return | ||
return ErrnoFault | ||
} | ||
|
||
var resp wasmpb.Response | ||
innerErr = proto.Unmarshal(b, &resp) | ||
if innerErr != nil { | ||
logger.Errorf("error calling sendResponse: %s", err) | ||
return | ||
return ErrnoFault | ||
} | ||
|
||
innerErr = r.add(resp.Id, &resp) | ||
if innerErr != nil { | ||
logger.Errorf("error calling sendResponse: %s", err) | ||
return | ||
return ErrnoFault | ||
} | ||
|
||
return ErrnoSuccess | ||
}, | ||
) | ||
if err != nil { | ||
|
@@ -237,9 +241,12 @@ func (m *Module) Run(request *wasmpb.Request) (*wasmpb.Response, error) { | |
wasi.SetArgv([]string{"wasi", reqstr}) | ||
|
||
store.SetWasi(wasi) | ||
err = store.SetFuel(m.cfg.InitialFuel) | ||
if err != nil { | ||
return nil, fmt.Errorf("error setting fuel: %w", err) | ||
|
||
if m.cfg.InitialFuel > 0 { | ||
err = store.SetFuel(m.cfg.InitialFuel) | ||
if err != nil { | ||
return nil, fmt.Errorf("error setting fuel: %w", err) | ||
} | ||
} | ||
|
||
// Limit memory to max memory megabytes per instance. | ||
|
@@ -283,6 +290,8 @@ func (m *Module) Run(request *wasmpb.Request) (*wasmpb.Response, error) { | |
} | ||
|
||
return nil, fmt.Errorf("error executing runner: %s: %w", resp.ErrMsg, innerErr) | ||
case containsCode(err, wasm.CodeHostErr): | ||
return nil, fmt.Errorf("invariant violation: host errored during sendResponse") | ||
default: | ||
return nil, err | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👻