Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions boot/components/runtime/wasm/sockets_bind_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: MPL-2.0

package wasm

import (
"context"
"os"
"testing"

"github.com/wippyai/runtime/api/registry"
wasmcomponent "github.com/wippyai/runtime/runtime/wasm/component"
wasmrt "github.com/wippyai/wasm-runtime/runtime"
"go.uber.org/zap"
)

// The sockets host binds only if every one of its methods matches what the
// canonical-ABI lowering expects. finish-connect and accept used to return
// their handles as several bare values, which no guest could ever bind, so
// wasi:sockets TCP was dead end to end. This loads a real Go-authored
// component that imports those methods and fails if any of them cannot bind.
//
// The component is built from packages/xepozz/smtp/wasm-go in the neuro-brat
// deployment; point WASM_SOCKETS_PROBE at it to run this.
func TestWASISocketsProfileBindsAGoComponent(t *testing.T) {
path := os.Getenv("WASM_SOCKETS_PROBE")
if path == "" {
t.Skip("set WASM_SOCKETS_PROBE to a component importing wasi:sockets")
}

data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read probe: %v", err)
}

ctx := context.Background()
rt, err := wasmrt.New(ctx)
if err != nil {
t.Fatalf("new runtime: %v", err)
}
defer func() { _ = rt.Close(ctx) }()

hosts := wasmcomponent.NewHostRegistry()
if err := hosts.RegisterProfiles(DefaultHostProfiles(zap.NewNop(), nil)...); err != nil {
t.Fatalf("register profiles: %v", err)
}

imports := []registry.ID{
registry.ParseID("wasi:io"),
registry.ParseID("wasi:cli"),
registry.ParseID("wasi:clocks"),
registry.ParseID("wasi:filesystem"),
registry.ParseID("wasi:random"),
registry.ParseID("wasi:sockets"),
}
if err := hosts.EnsureImports(ctx, rt, imports, true); err != nil {
t.Fatalf("bind hosts: %v", err)
}

module, err := rt.LoadComponent(ctx, data)
if err != nil {
t.Fatalf("load component: %v", err)
}
if err := module.Compile(ctx); err != nil {
t.Fatalf("compile component: %v", err)
}
}
47 changes: 32 additions & 15 deletions runtime/wasm/host/wippy/hosts/sockets/tcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,23 @@ func (h *TCPHost) AsyncFunctions() []string {
}

// IPSocketAddress represents an IP address and port.
// The canonical ABI folds a host method's results into one value, and the
// binder accepts either an exact arity match or a (payload, error) pair it can
// fold. A method that returns its payload as several bare values matches
// neither, so it can never bind: `result count mismatch: expected 1, got 3`.
// These records carry the payloads that used to be returned loose, the same way
// IPSocketAddress already does for the address getters.
type TCPStreams struct {
Input uint32
Output uint32
}

type TCPAccepted struct {
Socket uint32
Input uint32
Output uint32
}

type IPSocketAddress struct {
Address string
Port uint16
Expand Down Expand Up @@ -198,23 +215,23 @@ func (h *TCPHost) MethodTCPSocketStartConnect(ctx context.Context, self uint32,
}

// [method]tcp-socket.finish-connect
func (h *TCPHost) MethodTCPSocketFinishConnect(_ context.Context, self uint32) (uint32, uint32, *NetworkError) {
func (h *TCPHost) MethodTCPSocketFinishConnect(_ context.Context, self uint32) (*TCPStreams, *NetworkError) {
socket, err := h.getSocket(self)
if err != nil {
return 0, 0, err
return nil, err
}

if socket.State() != preview2.TCPStateConnectInProgress {
if socket.State() == preview2.TCPStateUnbound || socket.State() == preview2.TCPStateBound {
return 0, 0, &NetworkError{Code: NetworkErrorNotInProgress}
return nil, &NetworkError{Code: NetworkErrorNotInProgress}
}
return 0, 0, &NetworkError{Code: NetworkErrorInvalidState}
return nil, &NetworkError{Code: NetworkErrorInvalidState}
}

if pendingErr := socket.PendingError(); pendingErr != nil {
socket.ClearPendingError()
socket.SetState(preview2.TCPStateClosed)
return 0, 0, mapNetError(pendingErr)
return nil, mapNetError(pendingErr)
}

socket.SetState(preview2.TCPStateConnected)
Expand All @@ -227,7 +244,7 @@ func (h *TCPHost) MethodTCPSocketFinishConnect(_ context.Context, self uint32) (

socket.SetStreamHandles(inputHandle, outputHandle)

return inputHandle, outputHandle, nil
return &TCPStreams{Input: inputHandle, Output: outputHandle}, nil
}

// [method]tcp-socket.start-listen
Expand Down Expand Up @@ -330,7 +347,7 @@ func (h *TCPHost) MethodTCPSocketFinishListen(_ context.Context, self uint32) *N
}

// [method]tcp-socket.accept
func (h *TCPHost) MethodTCPSocketAccept(ctx context.Context, self uint32) (uint32, uint32, uint32, *NetworkError) {
func (h *TCPHost) MethodTCPSocketAccept(ctx context.Context, self uint32) (*TCPAccepted, *NetworkError) {
async := wasmengine.GetAsyncify(ctx)

if async != nil && async.IsRewinding(ctx) {
Expand All @@ -352,16 +369,16 @@ func (h *TCPHost) MethodTCPSocketAccept(ctx context.Context, self uint32) (uint3
acceptResult, ok := data.(*socketapi.AcceptResult)
if !ok || acceptResult == nil {
closeAsyncSocketResult(data)
return 0, 0, 0, &NetworkError{Code: NetworkErrorInvalidArgument}
return nil, &NetworkError{Code: NetworkErrorInvalidArgument}
}
if acceptResult.Err != nil {
return 0, 0, 0, mapNetError(acceptResult.Err)
return nil, mapNetError(acceptResult.Err)
}

socket, err := h.getSocket(self)
if err != nil {
_ = acceptResult.Conn.Close()
return 0, 0, 0, err
return nil, err
}

newSocket := preview2.NewTCPSocketResource(socket.Family())
Expand All @@ -385,21 +402,21 @@ func (h *TCPHost) MethodTCPSocketAccept(ctx context.Context, self uint32) (uint3

newSocket.SetStreamHandles(inputHandle, outputHandle)

return socketHandle, inputHandle, outputHandle, nil
return &TCPAccepted{Socket: socketHandle, Input: inputHandle, Output: outputHandle}, nil
}

socket, err := h.getSocket(self)
if err != nil {
return 0, 0, 0, err
return nil, err
}

if socket.State() != preview2.TCPStateListening {
return 0, 0, 0, &NetworkError{Code: NetworkErrorInvalidState}
return nil, &NetworkError{Code: NetworkErrorInvalidState}
}

netListener, ok := socket.Listener().(net.Listener)
if !ok {
return 0, 0, 0, &NetworkError{Code: NetworkErrorInvalidState}
return nil, &NetworkError{Code: NetworkErrorInvalidState}
}

op := &acceptPendingOp{cmd: &socketapi.AcceptCmd{Listener: netListener}}
Expand All @@ -412,7 +429,7 @@ func (h *TCPHost) MethodTCPSocketAccept(ctx context.Context, self uint32) (uint3
panic(fmt.Errorf("tcp accept suspend: %w", suspendErr))
}

return 0, 0, 0, nil
return nil, nil
}

// [method]tcp-socket.shutdown
Expand Down
24 changes: 16 additions & 8 deletions runtime/wasm/host/wippy/hosts/sockets/tcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,10 @@ func TestS03TCPAcceptRejectsWrongAsyncType(t *testing.T) {
t.Cleanup(func() { _ = right.Close() })
ctx := rewindContext(t, &socketapi.ConnectResult{Conn: carried})

socketHandle, inputHandle, outputHandle, err := host.MethodTCPSocketAccept(ctx, handle)
accepted, err := host.MethodTCPSocketAccept(ctx, handle)
requireNetworkError(t, err, NetworkErrorInvalidArgument)
if socketHandle != 0 || inputHandle != 0 || outputHandle != 0 {
t.Fatalf("rejected accept handles = (%d, %d, %d), want zero", socketHandle, inputHandle, outputHandle)
if accepted != nil {
t.Fatalf("rejected accept handles = %+v, want none", accepted)
}
if carried.closes.Load() != 1 {
t.Fatalf("unadopted connection close count = %d, want 1", carried.closes.Load())
Expand Down Expand Up @@ -188,10 +188,10 @@ func TestS07TCPFinishConnectFailureClosesState(t *testing.T) {
if err := host.MethodTCPSocketStartConnect(ctx, handle, 0, IPSocketAddress{}); err != nil {
t.Fatalf("resume failed connect: %v", err)
}
inputHandle, outputHandle, err := host.MethodTCPSocketFinishConnect(context.Background(), handle)
streams, err := host.MethodTCPSocketFinishConnect(context.Background(), handle)
requireNetworkError(t, err, NetworkErrorConnectionRefused)
if inputHandle != 0 || outputHandle != 0 {
t.Fatalf("failure stream handles = (%d, %d), want zero", inputHandle, outputHandle)
if streams != nil {
t.Fatalf("failure stream handles = %+v, want none", streams)
}
if socket.PendingError() != nil || socket.State() != preview2.TCPStateClosed {
t.Fatalf("failure state = %d, pending error = %v", socket.State(), socket.PendingError())
Expand Down Expand Up @@ -223,10 +223,14 @@ func TestS08TCPFinishConnectAdoptsStreams(t *testing.T) {
if err := host.MethodTCPSocketStartConnect(ctx, handle, 0, IPSocketAddress{}); err != nil {
t.Fatalf("resume successful connect: %v", err)
}
inputHandle, outputHandle, networkErr := host.MethodTCPSocketFinishConnect(context.Background(), handle)
streams, networkErr := host.MethodTCPSocketFinishConnect(context.Background(), handle)
if networkErr != nil {
t.Fatalf("finish connect: %v", networkErr)
}
if streams == nil {
t.Fatalf("finish connect returned no streams")
}
inputHandle, outputHandle := streams.Input, streams.Output
if inputHandle == 0 || outputHandle == 0 || inputHandle == outputHandle {
t.Fatalf("stream handles = (%d, %d), want distinct nonzero handles", inputHandle, outputHandle)
}
Expand Down Expand Up @@ -337,10 +341,14 @@ func TestS10TCPListenAcceptDropOwnership(t *testing.T) {
acceptedConn := &closeCountingConn{Conn: accepted}
ctx := rewindContext(t, &socketapi.AcceptResult{Conn: acceptedConn})

childHandle, inputHandle, outputHandle, networkErr := host.MethodTCPSocketAccept(ctx, parentHandle)
acceptedHandles, networkErr := host.MethodTCPSocketAccept(ctx, parentHandle)
if networkErr != nil {
t.Fatalf("resume accept: %v", networkErr)
}
if acceptedHandles == nil {
t.Fatalf("resume accept returned nothing")
}
childHandle, inputHandle, outputHandle := acceptedHandles.Socket, acceptedHandles.Input, acceptedHandles.Output
if childHandle == 0 || inputHandle == 0 || outputHandle == 0 {
t.Fatalf("accepted handles = (%d, %d, %d), want nonzero", childHandle, inputHandle, outputHandle)
}
Expand Down