From: Chris O'Hara Date: Mon, 8 May 2023 07:08:20 +0000 (+1000) Subject: net: implement wasip1 FileListener and FileConn X-Git-Tag: go1.21rc1~245 X-Git-Url: http://www.git.cypherpunks.ru/?a=commitdiff_plain;h=a17de43ef12250cd9a0ffdd8ff2d05fb18fcf322;p=gostls13.git net: implement wasip1 FileListener and FileConn 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 Run-TryBot: Ian Lance Taylor Reviewed-by: Dmitri Shuralyov TryBot-Bypass: Dmitri Shuralyov Reviewed-by: Johan Brandhorst-Satzkorn Auto-Submit: Johan Brandhorst-Satzkorn Reviewed-by: Dmitri Shuralyov --- diff --git a/misc/wasm/go_wasip1_wasm_exec b/misc/wasm/go_wasip1_wasm_exec index abcac8df36..dcec1c6392 100755 --- a/misc/wasm/go_wasip1_wasm_exec +++ b/misc/wasm/go_wasip1_wasm_exec @@ -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" diff --git a/src/internal/poll/fd_unix.go b/src/internal/poll/fd_unix.go index 0175b91ecf..61c2338305 100644 --- a/src/internal/poll/fd_unix.go +++ b/src/internal/poll/fd_unix.go @@ -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 @@ -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) diff --git a/src/internal/poll/fd_unixjs.go b/src/internal/poll/fd_unixjs.go index 07bf13f55c..090974d2b0 100644 --- a/src/internal/poll/fd_unixjs.go +++ b/src/internal/poll/fd_unixjs.go @@ -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) { diff --git a/src/internal/poll/fd_wasip1.go b/src/internal/poll/fd_wasip1.go index 749fa50220..aecd89669b 100644 --- a/src/internal/poll/fd_wasip1.go +++ b/src/internal/poll/fd_wasip1.go @@ -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 @@ -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) { diff --git a/src/net/fd_wasip1.go b/src/net/fd_wasip1.go new file mode 100644 index 0000000000..3f64ff4683 --- /dev/null +++ b/src/net/fd_wasip1.go @@ -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" } diff --git a/src/net/file_stub.go b/src/net/file_stub.go index 1299f0e67f..91df926a57 100644 --- a/src/net/file_stub.go +++ b/src/net/file_stub.go @@ -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 diff --git a/src/net/file_wasip1.go b/src/net/file_wasip1.go new file mode 100644 index 0000000000..95fd5403a6 --- /dev/null +++ b/src/net/file_wasip1.go @@ -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) diff --git a/src/net/net_fake.go b/src/net/net_fake.go index 8e801d15d1..a816213f8d 100644 --- a/src/net/net_fake.go +++ b/src/net/net_fake.go @@ -10,7 +10,6 @@ package net import ( "context" - "internal/poll" "io" "os" "sync" @@ -33,61 +32,64 @@ func nextPort() int { return portCounter } -// Network file descriptor. -type netFD struct { +type fakeNetFD struct { + listener bool + laddr Addr r *bufferedPipe w *bufferedPipe incoming chan *netFD closedMu sync.Mutex closed bool - - // immutable until Close - listener bool - family int - sotype int - net string - laddr Addr - raddr Addr - - // unused - pfd poll.FD - isConnected bool // handshake completed or use of association with peer } // socket returns a network file descriptor that is ready for // asynchronous I/O using the network poller. func socket(ctx context.Context, net string, family, sotype, proto int, ipv6only bool, laddr, raddr sockaddr, ctrlCtxFn func(context.Context, string, string, syscall.RawConn) error) (*netFD, error) { fd := &netFD{family: family, sotype: sotype, net: net} + if laddr != nil && raddr == nil { + return fakelistener(fd, laddr) + } + fd2 := &netFD{family: family, sotype: sotype, net: net} + return fakeconn(fd, fd2, raddr) +} - if laddr != nil && raddr == nil { // listener - l := laddr.(*TCPAddr) - fd.laddr = &TCPAddr{ - IP: l.IP, - Port: nextPort(), - Zone: l.Zone, - } - fd.listener = true - fd.incoming = make(chan *netFD, 1024) - listenersMu.Lock() - listeners[fd.laddr.(*TCPAddr).String()] = fd - listenersMu.Unlock() - return fd, nil +func fakelistener(fd *netFD, laddr sockaddr) (*netFD, error) { + l := laddr.(*TCPAddr) + fd.laddr = &TCPAddr{ + IP: l.IP, + Port: nextPort(), + Zone: l.Zone, + } + fd.fakeNetFD = &fakeNetFD{ + listener: true, + laddr: fd.laddr, + incoming: make(chan *netFD, 1024), } + listenersMu.Lock() + listeners[fd.laddr.(*TCPAddr).String()] = fd + listenersMu.Unlock() + return fd, nil +} +func fakeconn(fd *netFD, fd2 *netFD, raddr sockaddr) (*netFD, error) { fd.laddr = &TCPAddr{ IP: IPv4(127, 0, 0, 1), Port: nextPort(), } fd.raddr = raddr - fd.r = newBufferedPipe(65536) - fd.w = newBufferedPipe(65536) - fd2 := &netFD{family: fd.family, sotype: sotype, net: net} + fd.fakeNetFD = &fakeNetFD{ + r: newBufferedPipe(65536), + w: newBufferedPipe(65536), + } + fd2.fakeNetFD = &fakeNetFD{ + r: fd.fakeNetFD.w, + w: fd.fakeNetFD.r, + } + fd2.laddr = fd.raddr fd2.raddr = fd.laddr - fd2.r = fd.w - fd2.w = fd.r listenersMu.Lock() l, ok := listeners[fd.raddr.(*TCPAddr).String()] if !ok { @@ -100,15 +102,15 @@ func socket(ctx context.Context, net string, family, sotype, proto int, ipv6only return fd, nil } -func (fd *netFD) Read(p []byte) (n int, err error) { +func (fd *fakeNetFD) Read(p []byte) (n int, err error) { return fd.r.Read(p) } -func (fd *netFD) Write(p []byte) (nn int, err error) { +func (fd *fakeNetFD) Write(p []byte) (nn int, err error) { return fd.w.Write(p) } -func (fd *netFD) Close() error { +func (fd *fakeNetFD) Close() error { fd.closedMu.Lock() if fd.closed { fd.closedMu.Unlock() @@ -131,17 +133,17 @@ func (fd *netFD) Close() error { return nil } -func (fd *netFD) closeRead() error { +func (fd *fakeNetFD) closeRead() error { fd.r.Close() return nil } -func (fd *netFD) closeWrite() error { +func (fd *fakeNetFD) closeWrite() error { fd.w.Close() return nil } -func (fd *netFD) accept() (*netFD, error) { +func (fd *fakeNetFD) accept() (*netFD, error) { c, ok := <-fd.incoming if !ok { return nil, syscall.EINVAL @@ -149,18 +151,18 @@ func (fd *netFD) accept() (*netFD, error) { return c, nil } -func (fd *netFD) SetDeadline(t time.Time) error { +func (fd *fakeNetFD) SetDeadline(t time.Time) error { fd.r.SetReadDeadline(t) fd.w.SetWriteDeadline(t) return nil } -func (fd *netFD) SetReadDeadline(t time.Time) error { +func (fd *fakeNetFD) SetReadDeadline(t time.Time) error { fd.r.SetReadDeadline(t) return nil } -func (fd *netFD) SetWriteDeadline(t time.Time) error { +func (fd *fakeNetFD) SetWriteDeadline(t time.Time) error { fd.w.SetWriteDeadline(t) return nil } @@ -265,55 +267,59 @@ func sysSocket(family, sotype, proto int) (int, error) { return 0, syscall.ENOSYS } -func (fd *netFD) readFrom(p []byte) (n int, sa syscall.Sockaddr, err error) { +func (fd *fakeNetFD) connect(ctx context.Context, la, ra syscall.Sockaddr) (syscall.Sockaddr, error) { + return nil, syscall.ENOSYS +} + +func (fd *fakeNetFD) readFrom(p []byte) (n int, sa syscall.Sockaddr, err error) { return 0, nil, syscall.ENOSYS } -func (fd *netFD) readFromInet4(p []byte, sa *syscall.SockaddrInet4) (n int, err error) { +func (fd *fakeNetFD) readFromInet4(p []byte, sa *syscall.SockaddrInet4) (n int, err error) { return 0, syscall.ENOSYS } -func (fd *netFD) readFromInet6(p []byte, sa *syscall.SockaddrInet6) (n int, err error) { +func (fd *fakeNetFD) readFromInet6(p []byte, sa *syscall.SockaddrInet6) (n int, err error) { return 0, syscall.ENOSYS } -func (fd *netFD) readMsg(p []byte, oob []byte, flags int) (n, oobn, retflags int, sa syscall.Sockaddr, err error) { +func (fd *fakeNetFD) readMsg(p []byte, oob []byte, flags int) (n, oobn, retflags int, sa syscall.Sockaddr, err error) { return 0, 0, 0, nil, syscall.ENOSYS } -func (fd *netFD) readMsgInet4(p []byte, oob []byte, flags int, sa *syscall.SockaddrInet4) (n, oobn, retflags int, err error) { +func (fd *fakeNetFD) readMsgInet4(p []byte, oob []byte, flags int, sa *syscall.SockaddrInet4) (n, oobn, retflags int, err error) { return 0, 0, 0, syscall.ENOSYS } -func (fd *netFD) readMsgInet6(p []byte, oob []byte, flags int, sa *syscall.SockaddrInet6) (n, oobn, retflags int, err error) { +func (fd *fakeNetFD) readMsgInet6(p []byte, oob []byte, flags int, sa *syscall.SockaddrInet6) (n, oobn, retflags int, err error) { return 0, 0, 0, syscall.ENOSYS } -func (fd *netFD) writeMsgInet4(p []byte, oob []byte, sa *syscall.SockaddrInet4) (n int, oobn int, err error) { +func (fd *fakeNetFD) writeMsgInet4(p []byte, oob []byte, sa *syscall.SockaddrInet4) (n int, oobn int, err error) { return 0, 0, syscall.ENOSYS } -func (fd *netFD) writeMsgInet6(p []byte, oob []byte, sa *syscall.SockaddrInet6) (n int, oobn int, err error) { +func (fd *fakeNetFD) writeMsgInet6(p []byte, oob []byte, sa *syscall.SockaddrInet6) (n int, oobn int, err error) { return 0, 0, syscall.ENOSYS } -func (fd *netFD) writeTo(p []byte, sa syscall.Sockaddr) (n int, err error) { +func (fd *fakeNetFD) writeTo(p []byte, sa syscall.Sockaddr) (n int, err error) { return 0, syscall.ENOSYS } -func (fd *netFD) writeToInet4(p []byte, sa *syscall.SockaddrInet4) (n int, err error) { +func (fd *fakeNetFD) writeToInet4(p []byte, sa *syscall.SockaddrInet4) (n int, err error) { return 0, syscall.ENOSYS } -func (fd *netFD) writeToInet6(p []byte, sa *syscall.SockaddrInet6) (n int, err error) { +func (fd *fakeNetFD) writeToInet6(p []byte, sa *syscall.SockaddrInet6) (n int, err error) { return 0, syscall.ENOSYS } -func (fd *netFD) writeMsg(p []byte, oob []byte, sa syscall.Sockaddr) (n int, oobn int, err error) { +func (fd *fakeNetFD) writeMsg(p []byte, oob []byte, sa syscall.Sockaddr) (n int, oobn int, err error) { return 0, 0, syscall.ENOSYS } -func (fd *netFD) dup() (f *os.File, err error) { +func (fd *fakeNetFD) dup() (f *os.File, err error) { return nil, syscall.ENOSYS } diff --git a/src/net/net_fake_js.go b/src/net/net_fake_js.go new file mode 100644 index 0000000000..1fc0b50b7d --- /dev/null +++ b/src/net/net_fake_js.go @@ -0,0 +1,27 @@ +// 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. + +// Fake networking for js/wasm. It is intended to allow tests of other package to pass. + +//go:build js && wasm + +package net + +import "internal/poll" + +// Network file descriptor. +type netFD struct { + *fakeNetFD + + // immutable until Close + family int + sotype int + net string + laddr Addr + raddr Addr + + // unused + pfd poll.FD + isConnected bool // handshake completed or use of association with peer +} diff --git a/src/os/file_wasip1.go b/src/os/file_wasip1.go new file mode 100644 index 0000000000..c9b05b3aed --- /dev/null +++ b/src/os/file_wasip1.go @@ -0,0 +1,22 @@ +// 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 os + +import "internal/poll" + +// PollFD returns the poll.FD of the file. +// +// Other packages in std that also import internal/poll (such as net) +// can use a type assertion to access this extension method so that +// they can pass the *poll.FD to functions like poll.Splice. +// +// There is an equivalent function in net.rawConn. +// +// PollFD is not intended for use outside the standard library. +func (f *file) PollFD() *poll.FD { + return &f.pfd +} diff --git a/src/runtime/internal/wasitest/tcpecho_test.go b/src/runtime/internal/wasitest/tcpecho_test.go new file mode 100644 index 0000000000..506e6fe40a --- /dev/null +++ b/src/runtime/internal/wasitest/tcpecho_test.go @@ -0,0 +1,92 @@ +// 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. + +package wasi_test + +import ( + "bytes" + "fmt" + "math/rand" + "net" + "os" + "os/exec" + "testing" + "time" +) + +func TestTCPEcho(t *testing.T) { + if target != "wasip1/wasm" { + t.Skip() + } + + // We're unable to pass port 0 here (let the OS choose a spare port). + // Although wasmtime accepts port 0, and testdata/main.go successfully + // listens, there's no way for this test case to query the chosen port + // so that it can connect to the WASM module. The WASM module itself + // cannot access any information about the socket due to limitations + // with WASI preview 1 networking, and wasmtime does not log the address + // when you preopen a socket. Instead, we probe for a free port here. + var host string + port := rand.Intn(10000) + 40000 + for attempts := 0; attempts < 10; attempts++ { + host = fmt.Sprintf("127.0.0.1:%d", port) + l, err := net.Listen("tcp", host) + if err == nil { + l.Close() + break + } + port++ + } + + subProcess := exec.Command("go", "run", "./testdata/tcpecho.go") + + subProcess.Env = append(os.Environ(), "GOOS=wasip1", "GOARCH=wasm") + + switch os.Getenv("GOWASIRUNTIME") { + case "wasmtime": + subProcess.Env = append(subProcess.Env, "GOWASIRUNTIMEARGS=--tcplisten="+host) + default: + t.Skip("WASI runtime does not support sockets") + } + + var b bytes.Buffer + subProcess.Stdout = &b + subProcess.Stderr = &b + + if err := subProcess.Start(); err != nil { + t.Log(b.String()) + t.Fatal(err) + } + defer subProcess.Process.Kill() + + var conn net.Conn + var err error + for attempts := 0; attempts < 5; attempts++ { + conn, err = net.Dial("tcp", host) + if err == nil { + break + } + time.Sleep(500 * time.Millisecond) + } + if err != nil { + t.Log(b.String()) + t.Fatal(err) + } + defer conn.Close() + + payload := []byte("foobar") + if _, err := conn.Write(payload); err != nil { + t.Fatal(err) + } + var buf [256]byte + n, err := conn.Read(buf[:]) + if err != nil { + t.Fatal(err) + } + if string(buf[:n]) != string(payload) { + t.Error("unexpected payload") + t.Logf("expect: %d bytes (%v)", len(payload), payload) + t.Logf("actual: %d bytes (%v)", n, buf[:n]) + } +} diff --git a/src/runtime/internal/wasitest/testdata/tcpecho.go b/src/runtime/internal/wasitest/testdata/tcpecho.go new file mode 100644 index 0000000000..819e352688 --- /dev/null +++ b/src/runtime/internal/wasitest/testdata/tcpecho.go @@ -0,0 +1,74 @@ +// 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. + +package main + +import ( + "errors" + "net" + "os" + "syscall" +) + +func main() { + if err := run(); err != nil { + println(err) + os.Exit(1) + } +} + +func run() error { + l, err := findListener() + if err != nil { + return err + } + if l == nil { + return errors.New("no pre-opened sockets available") + } + defer l.Close() + + c, err := l.Accept() + if err != nil { + return err + } + return handleConn(c) +} + +func handleConn(c net.Conn) error { + defer c.Close() + + var buf [128]byte + n, err := c.Read(buf[:]) + if err != nil { + return err + } + if _, err := c.Write(buf[:n]); err != nil { + return err + } + if err := c.(*net.TCPConn).CloseWrite(); err != nil { + return err + } + return c.Close() +} + +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 directors 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++ { + f := os.NewFile(preopenFd, "") + l, err := net.FileListener(f) + f.Close() + + var se syscall.Errno + switch errors.As(err, &se); se { + case syscall.ENOTSOCK: + continue + case syscall.EBADF: + err = nil + } + return l, err + } +} diff --git a/src/syscall/fs_wasip1.go b/src/syscall/fs_wasip1.go index 25cabf8234..d60ab0b53e 100644 --- a/src/syscall/fs_wasip1.go +++ b/src/syscall/fs_wasip1.go @@ -279,6 +279,12 @@ func fd_fdstat_get_flags(fd int) (uint32, error) { return uint32(stat.fdflags), errnoErr(errno) } +func fd_fdstat_get_type(fd int) (uint8, error) { + var stat fdstat + errno := fd_fdstat_get(int32(fd), unsafe.Pointer(&stat)) + return stat.filetype, errnoErr(errno) +} + type preopentype = uint8 const ( @@ -331,12 +337,12 @@ func init() { if errno == EBADF { break } + if errno == ENOTDIR || prestat.typ != preopentypeDir { + continue + } if errno != 0 { panic("fd_prestat: " + errno.Error()) } - if prestat.typ != preopentypeDir { - continue - } if int(prestat.dir.prNameLen) > len(dirNameBuf) { dirNameBuf = make([]byte, prestat.dir.prNameLen) } diff --git a/src/syscall/net_fake.go b/src/syscall/net_fake.go new file mode 100644 index 0000000000..689f6f8812 --- /dev/null +++ b/src/syscall/net_fake.go @@ -0,0 +1,63 @@ +// 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. + +// Fake networking for js/wasm and wasip1/wasm. +// This file only exists to make the compiler happy. + +//go:build (js && wasm) || wasip1 + +package syscall + +const ( + AF_UNSPEC = iota + AF_UNIX + AF_INET + AF_INET6 +) + +const ( + SOCK_STREAM = 1 + iota + SOCK_DGRAM + SOCK_RAW + SOCK_SEQPACKET +) + +const ( + IPPROTO_IP = 0 + IPPROTO_IPV4 = 4 + IPPROTO_IPV6 = 0x29 + IPPROTO_TCP = 6 + IPPROTO_UDP = 0x11 +) + +const ( + _ = iota + IPV6_V6ONLY + SOMAXCONN + SO_ERROR +) + +// Misc constants expected by package net but not supported. +const ( + _ = iota + F_DUPFD_CLOEXEC + SYS_FCNTL = 500 // unsupported +) + +type Sockaddr any + +type SockaddrInet4 struct { + Port int + Addr [4]byte +} + +type SockaddrInet6 struct { + Port int + ZoneId uint32 + Addr [16]byte +} + +type SockaddrUnix struct { + Name string +} diff --git a/src/syscall/net_js.go b/src/syscall/net_js.go index 2ed4e191bd..cba33dfd2e 100644 --- a/src/syscall/net_js.go +++ b/src/syscall/net_js.go @@ -2,66 +2,10 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// js/wasm uses fake networking directly implemented in the net package. -// This file only exists to make the compiler happy. - //go:build js && wasm package syscall -const ( - AF_UNSPEC = iota - AF_UNIX - AF_INET - AF_INET6 -) - -const ( - SOCK_STREAM = 1 + iota - SOCK_DGRAM - SOCK_RAW - SOCK_SEQPACKET -) - -const ( - IPPROTO_IP = 0 - IPPROTO_IPV4 = 4 - IPPROTO_IPV6 = 0x29 - IPPROTO_TCP = 6 - IPPROTO_UDP = 0x11 -) - -const ( - _ = iota - IPV6_V6ONLY - SOMAXCONN - SO_ERROR -) - -// Misc constants expected by package net but not supported. -const ( - _ = iota - F_DUPFD_CLOEXEC - SYS_FCNTL = 500 // unsupported -) - -type Sockaddr any - -type SockaddrInet4 struct { - Port int - Addr [4]byte -} - -type SockaddrInet6 struct { - Port int - ZoneId uint32 - Addr [16]byte -} - -type SockaddrUnix struct { - Name string -} - func Socket(proto, sotype, unused int) (fd int, err error) { return 0, ENOSYS } diff --git a/src/syscall/net_wasip1.go b/src/syscall/net_wasip1.go index 896dd3e770..3918840a7e 100644 --- a/src/syscall/net_wasip1.go +++ b/src/syscall/net_wasip1.go @@ -2,66 +2,27 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// wasip1/wasm uses fake networking directly implemented in the net package. -// This file only exists to make the compiler happy. - //go:build wasip1 package syscall -const ( - AF_UNSPEC = iota - AF_UNIX - AF_INET - AF_INET6 -) +import "unsafe" const ( - SOCK_STREAM = 1 + iota - SOCK_DGRAM - SOCK_RAW - SOCK_SEQPACKET + SHUT_RD = 0x1 + SHUT_WR = 0x2 + SHUT_RDWR = SHUT_RD | SHUT_WR ) -const ( - IPPROTO_IP = 0 - IPPROTO_IPV4 = 4 - IPPROTO_IPV6 = 0x29 - IPPROTO_TCP = 6 - IPPROTO_UDP = 0x11 -) +type sdflags = uint32 -const ( - _ = iota - IPV6_V6ONLY - SOMAXCONN - SO_ERROR -) +//go:wasmimport wasi_snapshot_preview1 sock_accept +//go:noescape +func sock_accept(fd int32, flags fdflags, newfd unsafe.Pointer) Errno -// Misc constants expected by package net but not supported. -const ( - _ = iota - F_DUPFD_CLOEXEC - SYS_FCNTL = 500 // unsupported; same value as net_nacl.go -) - -type Sockaddr interface { -} - -type SockaddrInet4 struct { - Port int - Addr [4]byte -} - -type SockaddrInet6 struct { - Port int - ZoneId uint32 - Addr [16]byte -} - -type SockaddrUnix struct { - Name string -} +//go:wasmimport wasi_snapshot_preview1 sock_shutdown +//go:noescape +func sock_shutdown(fd int32, flags sdflags) Errno func Socket(proto, sotype, unused int) (fd int, err error) { return 0, ENOSYS @@ -79,8 +40,10 @@ func Listen(fd int, backlog int) error { return ENOSYS } -func Accept(fd int) (newfd int, sa Sockaddr, err error) { - return 0, nil, ENOSYS +func Accept(fd int) (int, Sockaddr, error) { + var newfd int32 + errno := sock_accept(int32(fd), 0, unsafe.Pointer(&newfd)) + return int(newfd), nil, errnoErr(errno) } func Connect(fd int, sa Sockaddr) error { @@ -120,5 +83,6 @@ func SetWriteDeadline(fd int, t int64) error { } func Shutdown(fd int, how int) error { - return ENOSYS + errno := sock_shutdown(int32(fd), sdflags(how)) + return errnoErr(errno) }