Skip to content

Commit

Permalink
net: implement wasip1 FileListener and FileConn
Browse files Browse the repository at this point in the history
Implements net.FileListener and net.FileConn for wasip1.

net.FileListener can be used with a pre-opened socket. If the WASM
module knows the file descriptor, a listener can be constructed with:

    l, err := net.FileListener(os.NewFile(fd, ""))

If the WASM module does not know the file descriptor, but knows that at
least one of the preopens is a socket, it can find the file descriptor
and construct a listener like so:

    func findListener() (net.Listener, error) {
        // We start looking for pre-opened sockets at fd=3 because 0, 1,
        // and 2 are reserved for stdio. Pre-opened directories also
        // start at fd=3, so we skip fds that aren't sockets. Once we
        // reach EBADF we know there are no more pre-opens.
        for preopenFd := uintptr(3); ; preopenFd++ {
            l, err := net.FileListener(os.NewFile(preopenFd, ""))

            var se syscall.Errno
            switch errors.As(err, &se); se {
            case syscall.ENOTSOCK:
                continue
            case syscall.EBADF:
                err = nil
            }
            return l, err
        }
    }

A similar strategy can be used with net.FileConn and pre-opened
connection sockets.

The wasmtime runtime supports pre-opening listener sockets:

    $ wasmtime --tcplisten 127.0.0.1:8080 module.wasm

Change-Id: Iec6ae4ffa84b3753cce4f56a2817e150445db643
Reviewed-on: https://go-review.googlesource.com/c/go/+/493358
Reviewed-by: Matthew Dempsky <mdempsky@google.com>
Run-TryBot: Ian Lance Taylor <iant@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
TryBot-Bypass: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>
Auto-Submit: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
  • Loading branch information
chriso authored and gopherbot committed May 25, 2023
1 parent c5c2184 commit a17de43
Show file tree
Hide file tree
Showing 16 changed files with 665 additions and 176 deletions.
4 changes: 2 additions & 2 deletions misc/wasm/go_wasip1_wasm_exec
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ case "$GOWASIRUNTIME" in
exec wasmer run --dir=/ --env PWD="$PWD" ${GOWASIRUNTIMEARGS:-} "$1" -- "${@:2}"
;;
"wasmtime")
exec wasmtime run --dir=/ --env PWD="$PWD" --max-wasm-stack 1048576 "$1" -- "${@:2}"
exec wasmtime run --dir=/ --env PWD="$PWD" --max-wasm-stack 1048576 ${GOWASIRUNTIMEARGS:-} "$1" -- "${@:2}"
;;
"wazero" | "")
exec wazero run -mount /:/ -env-inherit -cachedir "${TMPDIR:-/tmp}"/wazero "$1" "${@:2}"
exec wazero run -mount /:/ -env-inherit -cachedir "${TMPDIR:-/tmp}"/wazero ${GOWASIRUNTIMEARGS:-} "$1" "${@:2}"
;;
*)
echo "Unknown Go WASI runtime specified: $GOWASIRUNTIME"
Expand Down
9 changes: 3 additions & 6 deletions src/internal/poll/fd_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ type FD struct {
// or "file".
// Set pollable to true if fd should be managed by runtime netpoll.
func (fd *FD) Init(net string, pollable bool) error {
fd.SysFile.init()

// We don't actually care about the various network types.
if net == "file" {
fd.isFile = true
Expand All @@ -76,12 +78,7 @@ func (fd *FD) destroy() error {
// so this must be executed before CloseFunc.
fd.pd.close()

// We don't use ignoringEINTR here because POSIX does not define
// whether the descriptor is closed if close returns EINTR.
// If the descriptor is indeed closed, using a loop would race
// with some other goroutine opening a new descriptor.
// (The Linux kernel guarantees that it is closed on an EINTR error.)
err := CloseFunc(fd.Sysfd)
err := fd.SysFile.destroy(fd.Sysfd)

fd.Sysfd = -1
runtime_Semrelease(&fd.csema)
Expand Down
11 changes: 11 additions & 0 deletions src/internal/poll/fd_unixjs.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ type SysFile struct {
iovecs *[]syscall.Iovec
}

func (s *SysFile) init() {}

func (s *SysFile) destroy(fd int) error {
// We don't use ignoringEINTR here because POSIX does not define
// whether the descriptor is closed if close returns EINTR.
// If the descriptor is indeed closed, using a loop would race
// with some other goroutine opening a new descriptor.
// (The Linux kernel guarantees that it is closed on an EINTR error.)
return CloseFunc(fd)
}

// dupCloseOnExecOld is the traditional way to dup an fd and
// set its O_CLOEXEC bit, using two system calls.
func dupCloseOnExecOld(fd int) (int, string, error) {
Expand Down
53 changes: 53 additions & 0 deletions src/internal/poll/fd_wasip1.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ import (
)

type SysFile struct {
// RefCountPtr is a pointer to the reference count of Sysfd.
//
// WASI preview 1 lacks a dup(2) system call. When the os and net packages
// need to share a file/socket, instead of duplicating the underlying file
// descriptor, we instead provide a way to copy FD instances and manage the
// underlying file descriptor with reference counting.
RefCountPtr *int32

// RefCount is the reference count of Sysfd. When a copy of an FD is made,
// it points to the reference count of the original FD instance.
RefCount int32

// Cache for the file type, lazily initialized when Seek is called.
Filetype uint32

Expand All @@ -29,6 +41,47 @@ type SysFile struct {
// always set instead of being lazily initialized.
}

func (s *SysFile) init() {
if s.RefCountPtr == nil {
s.RefCount = 1
s.RefCountPtr = &s.RefCount
}
}

func (s *SysFile) ref() SysFile {
atomic.AddInt32(s.RefCountPtr, +1)
return SysFile{RefCountPtr: s.RefCountPtr}
}

func (s *SysFile) destroy(fd int) error {
if s.RefCountPtr != nil && atomic.AddInt32(s.RefCountPtr, -1) > 0 {
return nil
}

// We don't use ignoringEINTR here because POSIX does not define
// whether the descriptor is closed if close returns EINTR.
// If the descriptor is indeed closed, using a loop would race
// with some other goroutine opening a new descriptor.
// (The Linux kernel guarantees that it is closed on an EINTR error.)
return CloseFunc(fd)
}

// Copy creates a copy of the FD.
//
// The FD instance points to the same underlying file descriptor. The file
// descriptor isn't closed until all FD instances that refer to it have been
// closed/destroyed.
func (fd *FD) Copy() FD {
return FD{
Sysfd: fd.Sysfd,
SysFile: fd.SysFile.ref(),
IsStream: fd.IsStream,
ZeroReadIsEOF: fd.ZeroReadIsEOF,
isBlocking: fd.isBlocking,
isFile: fd.isFile,
}
}

// dupCloseOnExecOld always errors on wasip1 because there is no mechanism to
// duplicate file descriptors.
func dupCloseOnExecOld(fd int) (int, string, error) {
Expand Down
168 changes: 168 additions & 0 deletions src/net/fd_wasip1.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build wasip1

package net

import (
"internal/poll"
"runtime"
"syscall"
"time"
)

const (
readSyscallName = "fd_read"
writeSyscallName = "fd_write"
)

// Network file descriptor.
type netFD struct {
pfd poll.FD

// immutable until Close
family int
sotype int
isConnected bool // handshake completed or use of association with peer
net string
laddr Addr
raddr Addr

// The only networking available in WASI preview 1 is the ability to
// sock_accept on an pre-opened socket, and then fd_read, fd_write,
// fd_close, and sock_shutdown on the resulting connection. We
// intercept applicable netFD calls on this instance, and then pass
// the remainder of the netFD calls to fakeNetFD.
*fakeNetFD
}

func newFD(sysfd int) (*netFD, error) {
return newPollFD(poll.FD{
Sysfd: sysfd,
IsStream: true,
ZeroReadIsEOF: true,
})
}

func newPollFD(pfd poll.FD) (*netFD, error) {
ret := &netFD{
pfd: pfd,
net: "tcp",
laddr: unknownAddr{},
raddr: unknownAddr{},
}
return ret, nil
}

func (fd *netFD) init() error {
return fd.pfd.Init(fd.net, true)
}

func (fd *netFD) name() string {
return "unknown"
}

func (fd *netFD) accept() (netfd *netFD, err error) {
if fd.fakeNetFD != nil {
return fd.fakeNetFD.accept()
}
d, _, errcall, err := fd.pfd.Accept()
if err != nil {
if errcall != "" {
err = wrapSyscallError(errcall, err)
}
return nil, err
}
if netfd, err = newFD(d); err != nil {
poll.CloseFunc(d)
return nil, err
}
if err = netfd.init(); err != nil {
netfd.Close()
return nil, err
}
return netfd, nil
}

func (fd *netFD) setAddr(laddr, raddr Addr) {
fd.laddr = laddr
fd.raddr = raddr
runtime.SetFinalizer(fd, (*netFD).Close)
}

func (fd *netFD) Close() error {
if fd.fakeNetFD != nil {
return fd.fakeNetFD.Close()
}
runtime.SetFinalizer(fd, nil)
return fd.pfd.Close()
}

func (fd *netFD) shutdown(how int) error {
if fd.fakeNetFD != nil {
return nil
}
err := fd.pfd.Shutdown(how)
runtime.KeepAlive(fd)
return wrapSyscallError("shutdown", err)
}

func (fd *netFD) closeRead() error {
if fd.fakeNetFD != nil {
return fd.fakeNetFD.closeRead()
}
return fd.shutdown(syscall.SHUT_RD)
}

func (fd *netFD) closeWrite() error {
if fd.fakeNetFD != nil {
return fd.fakeNetFD.closeWrite()
}
return fd.shutdown(syscall.SHUT_WR)
}

func (fd *netFD) Read(p []byte) (n int, err error) {
if fd.fakeNetFD != nil {
return fd.fakeNetFD.Read(p)
}
n, err = fd.pfd.Read(p)
runtime.KeepAlive(fd)
return n, wrapSyscallError(readSyscallName, err)
}

func (fd *netFD) Write(p []byte) (nn int, err error) {
if fd.fakeNetFD != nil {
return fd.fakeNetFD.Write(p)
}
nn, err = fd.pfd.Write(p)
runtime.KeepAlive(fd)
return nn, wrapSyscallError(writeSyscallName, err)
}

func (fd *netFD) SetDeadline(t time.Time) error {
if fd.fakeNetFD != nil {
return fd.fakeNetFD.SetDeadline(t)
}
return fd.pfd.SetDeadline(t)
}

func (fd *netFD) SetReadDeadline(t time.Time) error {
if fd.fakeNetFD != nil {
return fd.fakeNetFD.SetReadDeadline(t)
}
return fd.pfd.SetReadDeadline(t)
}

func (fd *netFD) SetWriteDeadline(t time.Time) error {
if fd.fakeNetFD != nil {
return fd.fakeNetFD.SetWriteDeadline(t)
}
return fd.pfd.SetWriteDeadline(t)
}

type unknownAddr struct{}

func (unknownAddr) Network() string { return "unknown" }
func (unknownAddr) String() string { return "unknown" }
2 changes: 1 addition & 1 deletion src/net/file_stub.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build (js && wasm) || wasip1
//go:build js && wasm

package net

Expand Down
62 changes: 62 additions & 0 deletions src/net/file_wasip1.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build wasip1

package net

import (
"os"
"syscall"
_ "unsafe" // for go:linkname
)

func fileListener(f *os.File) (Listener, error) {
fd, err := newFileFD(f)
if err != nil {
return nil, err
}
return &TCPListener{fd: fd}, nil
}

func fileConn(f *os.File) (Conn, error) {
fd, err := newFileFD(f)
if err != nil {
return nil, err
}
return &TCPConn{conn{fd: fd}}, nil
}

func filePacketConn(f *os.File) (PacketConn, error) { return nil, syscall.ENOPROTOOPT }

func newFileFD(f *os.File) (fd *netFD, err error) {
pfd := f.PollFD().Copy()
defer func() {
if err != nil {
pfd.Close()
}
}()
filetype, err := fd_fdstat_get_type(pfd.Sysfd)
if err != nil {
return nil, err
}
if filetype != syscall.FILETYPE_SOCKET_STREAM {
return nil, syscall.ENOTSOCK
}
fd, err = newPollFD(pfd)
if err != nil {
return nil, err
}
if err := fd.init(); err != nil {
return nil, err
}
return fd, nil
}

// This helper is implemented in the syscall package. It means we don't have
// to redefine the fd_fdstat_get host import or the fdstat struct it
// populates.
//
//go:linkname fd_fdstat_get_type syscall.fd_fdstat_get_type
func fd_fdstat_get_type(fd int) (uint8, error)

0 comments on commit a17de43

Please sign in to comment.