diff --git a/pkg/lisafs/BUILD b/pkg/lisafs/BUILD index 46d4f7b66..c6a5ca2b3 100644 --- a/pkg/lisafs/BUILD +++ b/pkg/lisafs/BUILD @@ -28,6 +28,17 @@ go_template_instance( }, ) +go_template_instance( + name = "bound_socket_fd_refs", + out = "bound_socket_fd_refs.go", + package = "lisafs", + prefix = "boundSocketFD", + template = "//pkg/refsvfs2:refs_template", + types = { + "T": "BoundSocketFD", + }, +) + go_template_instance( name = "node_fd_refs", out = "node_fd_refs.go", @@ -66,6 +77,7 @@ go_template_instance( go_library( name = "lisafs", srcs = [ + "bound_socket_fd_refs.go", "channel.go", "client.go", "client_file.go", diff --git a/pkg/lisafs/client_file.go b/pkg/lisafs/client_file.go index 76ea68576..146c7d2ed 100644 --- a/pkg/lisafs/client_file.go +++ b/pkg/lisafs/client_file.go @@ -418,6 +418,44 @@ func (f *ClientFD) Flush(ctx context.Context) error { return err } +// BindAt makes the BindAt RPC. +func (f *ClientFD) BindAt(ctx context.Context, sockType linux.SockType, name string) (Inode, *ClientBoundSocketFD, error) { + req := BindAtReq{ + DirFD: f.fd, + SockType: primitive.Uint32(sockType), + Name: SizedString(name), + } + var ( + resp BindAtResp + hostSocketFD [1]int + ) + ctx.UninterruptibleSleepStart(false) + err := f.client.SndRcvMessage(BindAt, uint32(req.SizeBytes()), req.MarshalBytes, resp.CheckedUnmarshal, hostSocketFD[:], req.String, resp.String) + ctx.UninterruptibleSleepFinish(false) + if err == nil && hostSocketFD[0] < 0 { + // No host socket fd? We can't proceed. + // Clean up any resources the gofer sent to us. + if resp.Child.ControlFD.Ok() { + f.client.CloseFDBatched(ctx, resp.Child.ControlFD) + } + if resp.BoundSocketFD.Ok() { + f.client.CloseFDBatched(ctx, resp.BoundSocketFD) + } + err = unix.EBADF + } + if err != nil { + return Inode{}, nil, err + } + + cbsFD := &ClientBoundSocketFD{ + fd: resp.BoundSocketFD, + notificationFD: int32(hostSocketFD[0]), + client: f.client, + } + + return resp.Child, cbsFD, err +} + // Connect makes the Connect RPC. func (f *ClientFD) Connect(ctx context.Context, sockType linux.SockType) (int, error) { req := ConnectReq{FD: f.fd, SockType: uint32(sockType)} @@ -532,3 +570,58 @@ func (f *ClientFD) RemoveXattr(ctx context.Context, name string) error { ctx.UninterruptibleSleepFinish(false) return err } + +// ClientBoundSocketFD corresponds to a bound socket on the server. +// +// All fields are immutable. +type ClientBoundSocketFD struct { + // fd is the FDID of the bound socket on the server. + fd FDID + + // notificationFD is the host FD that can be used to notify when new + // clients connect to the socket. + notificationFD int32 + + client *Client +} + +// Close closes the host and gofer-backed FDs associated to this bound socket. +func (f *ClientBoundSocketFD) Close(ctx context.Context) { + _ = unix.Close(int(f.notificationFD)) + f.client.CloseFDBatched(ctx, f.fd) +} + +// NotificationFD is a host FD that can be used to notify when new clients +// connect to the socket. +func (f *ClientBoundSocketFD) NotificationFD() int32 { + return f.notificationFD +} + +// Listen makes a Listen RPC. +func (f *ClientBoundSocketFD) Listen(ctx context.Context, backlog int32) error { + req := ListenReq{ + FD: f.fd, + Backlog: backlog, + } + var resp ListenResp + ctx.UninterruptibleSleepStart(false) + err := f.client.SndRcvMessage(Listen, uint32(req.SizeBytes()), req.MarshalBytes, resp.CheckedUnmarshal, nil, req.String, resp.String) + ctx.UninterruptibleSleepFinish(false) + return err +} + +// Accept makes an Accept RPC. +func (f *ClientBoundSocketFD) Accept(ctx context.Context) (int, error) { + req := AcceptReq{ + FD: f.fd, + } + var resp AcceptResp + var hostSocketFD [1]int + ctx.UninterruptibleSleepStart(false) + err := f.client.SndRcvMessage(Accept, uint32(req.SizeBytes()), req.MarshalBytes, resp.CheckedUnmarshal, hostSocketFD[:], req.String, resp.String) + ctx.UninterruptibleSleepFinish(false) + if err == nil && hostSocketFD[0] < 0 { + err = unix.EBADF + } + return hostSocketFD[0], err +} diff --git a/pkg/lisafs/connection.go b/pkg/lisafs/connection.go index 31e6bd8c6..a85264462 100644 --- a/pkg/lisafs/connection.go +++ b/pkg/lisafs/connection.go @@ -297,6 +297,22 @@ func (c *Connection) lookupOpenFD(id FDID) (*OpenFD, error) { return ofd, nil } +// lookupBoundSocketFD retrieves the boundSockedFD identified by id on this +// connection. On success, the caller gains a ref on the FD. +func (c *Connection) lookupBoundSocketFD(id FDID) (*BoundSocketFD, error) { + fd, err := c.lookupFD(id) + if err != nil { + return nil, err + } + + bsfd, ok := fd.(*BoundSocketFD) + if !ok { + fd.DecRef(nil) + return nil, unix.EINVAL + } + return bsfd, nil +} + // insertFD inserts the passed fd into the internal datastructure to track FDs. // The caller must hold a ref on fd which is transferred to the connection. func (c *Connection) insertFD(fd genericFD) FDID { diff --git a/pkg/lisafs/fd.go b/pkg/lisafs/fd.go index 1d63af00d..e94e74cd5 100644 --- a/pkg/lisafs/fd.go +++ b/pkg/lisafs/fd.go @@ -36,7 +36,7 @@ func (f FDID) Ok() bool { return f != InvalidFDID } -// genericFD can represent a ControlFD or OpenFD. +// genericFD can represent any type of FD. type genericFD interface { refsvfs2.RefCounter } @@ -283,6 +283,54 @@ func (fd *OpenFD) Init(cfd *ControlFD, flags uint32, impl OpenFDImpl) { cfd.openFDsMu.Unlock() } +// BoundSocketFD represents a bound socket on the server. +// +// Reference Model: +// * A BoundSocketFD takes a reference on the control FD it is bound to. +type BoundSocketFD struct { + boundSocketFDRefs + + // All the following fields are immutable. + + // controlFD is the ControlFD on which this FD was bound. BoundSocketFD + // holds a ref on controlFD for its entire lifetime. + controlFD *ControlFD + + // id is the unique FD identifier which identifies this FD on its connection. + id FDID + + // impl is the socket FD implementation which embeds this struct. It + // contains all the implementation specific details. + impl BoundSocketFDImpl +} + +var _ genericFD = (*BoundSocketFD)(nil) + +// ControlFD returns the control FD on which this FD was bound. +func (fd *BoundSocketFD) ControlFD() ControlFDImpl { + return fd.controlFD.impl +} + +// DecRef implements refsvfs2.RefCounter.DecRef. Note that the context +// parameter should never be used. It exists solely to comply with the +// refsvfs2.RefCounter interface. +func (fd *BoundSocketFD) DecRef(context.Context) { + fd.boundSocketFDRefs.DecRef(func() { + fd.controlFD.DecRef(nil) // Drop the ref on the control FD. + fd.impl.Close() + }) +} + +// Init must be called before first use of fd. +func (fd *BoundSocketFD) Init(cfd *ControlFD, impl BoundSocketFDImpl) { + // Initialize fd with 1 ref which is transferred to c via c.insertFD(). + fd.boundSocketFDRefs.InitRefs() + fd.controlFD = cfd + fd.id = cfd.conn.insertFD(fd) + fd.impl = impl + cfd.IncRef() // Holds a ref on cfd for its lifetime. +} + // There are four different types of guarantees provided: // // none: There is no concurrency guarantee. The method may be invoked @@ -436,6 +484,16 @@ type ControlFDImpl interface { // On the server, Connect has a read concurrency guarantee. Connect(sockType uint32) (int, error) + // BindAt creates a host unix domain socket of type sockType, bound to + // the given namt of type sockType, bound to the given name. It returns + // a ControlFD that can be used for path operations on the socket, a + // BoundSocketFD that can be used to Accept/Listen on the socket, and a + // host FD that can be used for event notifications (like new + // connections). + // + // On the server, BindAt has a write concurrency guarantee. + BindAt(name string, sockType uint32) (*ControlFD, linux.Statx, *BoundSocketFD, int, error) + // UnlinkAt the file identified by name in this directory. // // Flags are Linux unlinkat(2) flags. @@ -560,3 +618,31 @@ type OpenFDImpl interface { // On the server, Renamed has a global concurrency guarantee. Renamed() } + +// BoundSocketFDImpl represents a socket on the host filesystem that has been +// created by the sandboxed application via Bind. +type BoundSocketFDImpl interface { + // FD returns a pointer to the embedded BoundSocketFD. + FD() *BoundSocketFD + + // Listen marks the socket as accepting incoming connections. + // + // On the server, Listen has a read concurrency guarantee. + Listen(backlog int32) error + + // Accept takes the first pending connection and creates a new socket + // for it. The new socket FD is returned along with the peer address of + // the connecting socket (which may be empty string). + // + // On the server, Accept has a read concurrency guarantee. + Accept() (int, string, error) + + // Close should clean up resources used by the bound socket FD + // implementation. + // + // Close is called after all references on the FD have been dropped and its + // FDID has been released. + // + // On the server, Close has no concurrency guarantee. + Close() +} diff --git a/pkg/lisafs/handlers.go b/pkg/lisafs/handlers.go index 1747bc823..868c47c45 100644 --- a/pkg/lisafs/handlers.go +++ b/pkg/lisafs/handlers.go @@ -67,7 +67,6 @@ var handlers = [...]RPCHandler{ FAllocate: FAllocateHandler, ReadLinkAt: ReadLinkAtHandler, Flush: FlushHandler, - Connect: ConnectHandler, UnlinkAt: UnlinkAtHandler, RenameAt: RenameAtHandler, Getdents64: Getdents64Handler, @@ -75,6 +74,10 @@ var handlers = [...]RPCHandler{ FSetXattr: FSetXattrHandler, FListXattr: FListXattrHandler, FRemoveXattr: FRemoveXattrHandler, + Connect: ConnectHandler, + BindAt: BindAtHandler, + Listen: ListenHandler, + Accept: AcceptHandler, } // ErrorHandler handles Error message. @@ -1068,6 +1071,116 @@ func ConnectHandler(c *Connection, comm Communicator, payloadLen uint32) (uint32 return 0, comm.DonateFD(sock) } +// BindAtHandler handles the BindAt RPC. +func BindAtHandler(c *Connection, comm Communicator, payloadLen uint32) (uint32, error) { + var req BindAtReq + if _, ok := req.CheckedUnmarshal(comm.PayloadBuf(payloadLen)); !ok { + return 0, unix.EIO + } + + name := string(req.Name) + if err := checkSafeName(name); err != nil { + return 0, err + } + + dir, err := c.lookupControlFD(req.DirFD) + if err != nil { + return 0, err + } + defer dir.DecRef(nil) + + if !dir.IsDir() { + return 0, unix.ENOTDIR + } + + var ( + childFD *ControlFD + childStat linux.Statx + boundSocketFD *BoundSocketFD + hostSocketFD int + ) + if err := dir.safelyWrite(func() error { + if dir.node.isDeleted() { + return unix.EINVAL + } + childFD, childStat, boundSocketFD, hostSocketFD, err = dir.impl.BindAt(name, uint32(req.SockType)) + return err + }); err != nil { + return 0, err + } + + if err := comm.DonateFD(hostSocketFD); err != nil { + return 0, err + } + + resp := BindAtResp{ + Child: Inode{ + ControlFD: childFD.id, + Stat: childStat, + }, + BoundSocketFD: boundSocketFD.id, + } + respLen := uint32(resp.SizeBytes()) + resp.MarshalUnsafe(comm.PayloadBuf(respLen)) + return respLen, nil +} + +// ListenHandler handles the Listen RPC. +func ListenHandler(c *Connection, comm Communicator, payloadLen uint32) (uint32, error) { + var req ListenReq + if _, ok := req.CheckedUnmarshal(comm.PayloadBuf(payloadLen)); !ok { + return 0, unix.EIO + } + sock, err := c.lookupBoundSocketFD(req.FD) + if err != nil { + return 0, err + } + if err := sock.controlFD.safelyRead(func() error { + if sock.controlFD.node.isDeleted() { + return unix.EINVAL + } + return sock.impl.Listen(req.Backlog) + }); err != nil { + return 0, err + } + return 0, nil +} + +// AcceptHandler handles the Accept RPC. +func AcceptHandler(c *Connection, comm Communicator, payloadLen uint32) (uint32, error) { + var req AcceptReq + if _, ok := req.CheckedUnmarshal(comm.PayloadBuf(payloadLen)); !ok { + return 0, unix.EIO + } + sock, err := c.lookupBoundSocketFD(req.FD) + if err != nil { + return 0, err + } + var ( + newSock int + peerAddr string + ) + if err := sock.controlFD.safelyRead(func() error { + if sock.controlFD.node.isDeleted() { + return unix.EINVAL + } + var err error + newSock, peerAddr, err = sock.impl.Accept() + return err + }); err != nil { + return 0, err + } + if err := comm.DonateFD(newSock); err != nil { + return 0, err + } + resp := AcceptResp{ + PeerAddr: SizedString(peerAddr), + } + respLen := uint32(resp.SizeBytes()) + resp.MarshalBytes(comm.PayloadBuf(respLen)) + return respLen, nil +} + // UnlinkAtHandler handles the UnlinkAt RPC. func UnlinkAtHandler(c *Connection, comm Communicator, payloadLen uint32) (uint32, error) { if c.readonly { diff --git a/pkg/lisafs/message.go b/pkg/lisafs/message.go index 571d378d5..38d2cc437 100644 --- a/pkg/lisafs/message.go +++ b/pkg/lisafs/message.go @@ -163,6 +163,15 @@ const ( // FRemoveXattr is analogous to fremovexattr(2). FRemoveXattr MID = 28 + + // BindAt is analogous to bind(2). + BindAt MID = 29 + + // Listen is analogous to listen(2). + Listen MID = 30 + + // Accept is analogous to accept4(2). + Accept MID = 31 ) const ( @@ -1278,6 +1287,116 @@ func (*ConnectResp) String() string { return "ConnectResp{}" } +// BindAtReq is used to make BindAt requests. +type BindAtReq struct { + DirFD FDID + SockType primitive.Uint32 + Name SizedString +} + +// SizeBytes implements marshal.Marshallable.SizeBytes. +func (b *BindAtReq) SizeBytes() int { + return b.DirFD.SizeBytes() + b.SockType.SizeBytes() + b.Name.SizeBytes() +} + +// MarshalBytes implements marshal.Marshallable.MarshalBytes. +func (b *BindAtReq) MarshalBytes(dst []byte) []byte { + dst = b.DirFD.MarshalUnsafe(dst) + dst = b.SockType.MarshalUnsafe(dst) + return b.Name.MarshalBytes(dst) +} + +// CheckedUnmarshal implements marshal.CheckedMarshallable.CheckedUnmarshal. +func (b *BindAtReq) CheckedUnmarshal(src []byte) ([]byte, bool) { + b.Name = "" + if b.SizeBytes() > len(src) { + return src, false + } + srcRemain := b.DirFD.UnmarshalUnsafe(src) + srcRemain = b.SockType.UnmarshalUnsafe(srcRemain) + if srcRemain, ok := b.Name.CheckedUnmarshal(srcRemain); ok { + return srcRemain, ok + } + return src, false +} + +// String implements fmt.Stringer.String. +func (b *BindAtReq) String() string { + return fmt.Sprintf("BindAtReq{DirFD: %d, SockType: %d, Name: %q}", b.DirFD, b.SockType, b.Name) +} + +// BindAtResp is used to communicate BindAt response. +// +// +marshal boundCheck +type BindAtResp struct { + Child Inode + BoundSocketFD FDID +} + +// String implements fmt.Stringer.String. +func (b *BindAtResp) String() string { + return fmt.Sprintf("BindAtResp{Child: %+v, BoundSocketFD: %v}", b.Child, b.BoundSocketFD) +} + +// ListenReq is used to make Listen requests. +// +// +marshal boundCheck +type ListenReq struct { + FD FDID + Backlog int32 + _ uint32 +} + +// String implements fmt.Stringer.String. +func (l *ListenReq) String() string { + return fmt.Sprintf("ListenReq{FD: %v, Backlog: %d}", l.FD, l.Backlog) +} + +// ListenResp is an empty response to ListenResp. +type ListenResp struct{ EmptyMessage } + +// String implements fmt.Stringer.String. +func (*ListenResp) String() string { + return "ListenResp{}" +} + +// AcceptReq is used to make AcceptRequests. +// +// +marshal boundCheck +type AcceptReq struct { + FD FDID +} + +// String implements fmt.Stringer.String. +func (a *AcceptReq) String() string { + return fmt.Sprintf("AcceptReq{FD: %v}", a.FD) +} + +// AcceptResp is an empty response to AcceptResp. +type AcceptResp struct { + PeerAddr SizedString +} + +// String implements fmt.Stringer.String. +func (a *AcceptResp) String() string { + return fmt.Sprintf("AcceptResp{PeerAddr: %s}", a.PeerAddr) +} + +// SizeBytes implements marshal.Marshallable.SizeBytes. +func (a *AcceptResp) SizeBytes() int { + return a.PeerAddr.SizeBytes() +} + +// MarshalBytes implements marshal.Marshallable.MarshalBytes. +func (a *AcceptResp) MarshalBytes(dst []byte) []byte { + return a.PeerAddr.MarshalBytes(dst) +} + +// CheckedUnmarshal implements marshal.CheckedMarshallable.CheckedUnmarshal. +func (a *AcceptResp) CheckedUnmarshal(src []byte) ([]byte, bool) { + return a.PeerAddr.CheckedUnmarshal(src) +} + // UnlinkAtReq is used to make UnlinkAt request. type UnlinkAtReq struct { DirFD FDID diff --git a/pkg/sentry/fsimpl/gofer/filesystem.go b/pkg/sentry/fsimpl/gofer/filesystem.go index 70ac6e7f8..fff046965 100644 --- a/pkg/sentry/fsimpl/gofer/filesystem.go +++ b/pkg/sentry/fsimpl/gofer/filesystem.go @@ -916,12 +916,30 @@ func (fs *filesystem) MkdirAt(ctx context.Context, rp *vfs.ResolvingPath, opts v func (fs *filesystem) MknodAt(ctx context.Context, rp *vfs.ResolvingPath, opts vfs.MknodOptions) error { return fs.doCreateAt(ctx, rp, false /* dir */, func(parent *dentry, name string, ds **[]*dentry) error { creds := rp.Credentials() - var err error + var ( + childInode lisafs.Inode + err error + ) if fs.opts.lisaEnabled { - var childInode lisafs.Inode - childInode, err = parent.controlFDLisa.MknodAt(ctx, name, opts.Mode, lisafs.UID(creds.EffectiveKUID), lisafs.GID(creds.EffectiveKGID), opts.DevMinor, opts.DevMajor) + if opts.Endpoint != nil { + // We are creating a socket. Defer to bindAt instead of MknodAt. + ep := opts.Endpoint.(transport.Endpoint) + sockType := ep.Type() + var boundSocketFD *lisafs.ClientBoundSocketFD + childInode, boundSocketFD, err = parent.controlFDLisa.BindAt(ctx, sockType, name) + if err == nil { + opts.Endpoint.(transport.HostBoundEndpoint).SetBoundSocketFD(boundSocketFD) + } + } else { + childInode, err = parent.controlFDLisa.MknodAt(ctx, name, opts.Mode, lisafs.UID(creds.EffectiveKUID), lisafs.GID(creds.EffectiveKGID), opts.DevMinor, opts.DevMajor) + } if err == nil { - return parent.insertCreatedChildLocked(ctx, &childInode, name, nil, ds) + return parent.insertCreatedChildLocked(ctx, &childInode, name, func(child *dentry) { + if opts.Endpoint != nil && fs.opts.lisaEnabled { + // Set the endpoint on the newly created child dentry. + child.endpoint = opts.Endpoint + } + }, ds) } } else { _, err = parent.file.mknod(ctx, name, (p9.FileMode)(opts.Mode), opts.DevMajor, opts.DevMinor, (p9.UID)(creds.EffectiveKUID), (p9.GID)(creds.EffectiveKGID)) @@ -1766,18 +1784,19 @@ func (fs *filesystem) BoundEndpointAt(ctx context.Context, rp *vfs.ResolvingPath if err := d.checkPermissions(rp.Credentials(), vfs.MayWrite); err != nil { return nil, err } - if d.isSocket() { - if !d.isSynthetic() { - d.IncRef() - ds = appendDentry(ds, d) - return &endpoint{ - dentry: d, - path: opts.Addr, - }, nil - } - if d.endpoint != nil { - return d.endpoint, nil - } + if !d.isSocket() { + return nil, linuxerr.ECONNREFUSED + } + if d.endpoint != nil { + return d.endpoint, nil + } + if !d.isSynthetic() { + d.IncRef() + ds = appendDentry(ds, d) + return &endpoint{ + dentry: d, + path: opts.Addr, + }, nil } return nil, linuxerr.ECONNREFUSED } diff --git a/pkg/sentry/socket/unix/transport/BUILD b/pkg/sentry/socket/unix/transport/BUILD index c69e7efe4..182567a1b 100644 --- a/pkg/sentry/socket/unix/transport/BUILD +++ b/pkg/sentry/socket/unix/transport/BUILD @@ -62,6 +62,7 @@ go_library( "//pkg/errors/linuxerr", "//pkg/fdnotifier", "//pkg/ilist", + "//pkg/lisafs", "//pkg/log", "//pkg/refs", "//pkg/refsvfs2", diff --git a/pkg/sentry/socket/unix/transport/connectioned.go b/pkg/sentry/socket/unix/transport/connectioned.go index dfeb6a5c4..3d86c7726 100644 --- a/pkg/sentry/socket/unix/transport/connectioned.go +++ b/pkg/sentry/socket/unix/transport/connectioned.go @@ -15,8 +15,13 @@ package transport import ( + "fmt" + + "golang.org/x/sys/unix" "gvisor.dev/gvisor/pkg/abi/linux" "gvisor.dev/gvisor/pkg/context" + "gvisor.dev/gvisor/pkg/fdnotifier" + "gvisor.dev/gvisor/pkg/lisafs" "gvisor.dev/gvisor/pkg/sentry/uniqueid" "gvisor.dev/gvisor/pkg/sync" "gvisor.dev/gvisor/pkg/syserr" @@ -104,6 +109,12 @@ type connectionedEndpoint struct { // // If nil, then no listen call has been made. acceptedChan chan *connectionedEndpoint `state:".([]*connectionedEndpoint)"` + + // boundSocketFD corresponds to a bound socket on the host filesystem + // that may listen and accept incoming connections. + // + // boundSocketFD is protected by baseEndpoint.mu. + boundSocketFD *lisafs.ClientBoundSocketFD } var ( @@ -218,8 +229,11 @@ func (e *connectionedEndpoint) ListeningLocked() bool { func (e *connectionedEndpoint) Close(ctx context.Context) { var acceptedChan chan *connectionedEndpoint e.Lock() - var c ConnectedEndpoint - var r Receiver + var ( + c ConnectedEndpoint + r Receiver + ) + bsFD := e.boundSocketFD switch { case e.Connected(): e.connected.CloseSend() @@ -251,6 +265,13 @@ func (e *connectionedEndpoint) Close(ctx context.Context) { c.CloseNotify() c.Release(ctx) } + + // Clean up any associated host bound socket. + if bsFD != nil { + fdnotifier.RemoveFD(bsFD.NotificationFD()) + bsFD.Close(ctx) + } + if r != nil { r.CloseNotify() r.Release(ctx) @@ -397,6 +418,11 @@ func (e *connectionedEndpoint) Listen(ctx context.Context, backlog int) *syserr. for ep := range origChan { e.acceptedChan <- ep } + if e.boundSocketFD != nil { + if err := e.boundSocketFD.Listen(ctx, int32(backlog)); err != nil { + return syserr.FromError(err) + } + } return nil } if !e.isBound() { @@ -405,6 +431,12 @@ func (e *connectionedEndpoint) Listen(ctx context.Context, backlog int) *syserr. // Normal case. e.acceptedChan = make(chan *connectionedEndpoint, backlog) + if e.boundSocketFD != nil { + if err := e.boundSocketFD.Listen(ctx, int32(backlog)); err != nil { + return syserr.FromError(err) + } + } + return nil } @@ -417,28 +449,61 @@ func (e *connectionedEndpoint) Accept(ctx context.Context, peerAddr *tcpip.FullA return nil, syserr.ErrInvalidEndpointState } + ne, err := e.getAcceptedEndpointLocked(ctx) + e.Unlock() + if err != nil { + return nil, err + } + + if peerAddr != nil { + ne.Lock() + c := ne.connected + ne.Unlock() + if c != nil { + addr, err := c.GetLocalAddress() + if err != nil { + return nil, syserr.TranslateNetstackError(err) + } + *peerAddr = addr + } + } + return ne, nil +} + +// Preconditions: +// * e.Listening() +// * e is locked. +func (e *connectionedEndpoint) getAcceptedEndpointLocked(ctx context.Context) (*connectionedEndpoint, *syserr.Error) { + // Accept connections from within the sentry first, since this avoids + // an RPC to the gofer on the common path. select { case ne := <-e.acceptedChan: - e.Unlock() - if peerAddr != nil { - ne.Lock() - c := ne.connected - ne.Unlock() - if c != nil { - addr, err := c.GetLocalAddress() - if err != nil { - return nil, syserr.TranslateNetstackError(err) - } - *peerAddr = addr - } - } return ne, nil - default: - e.Unlock() - // Nothing left. + // No internal connections. + } + + if e.boundSocketFD == nil { return nil, syserr.ErrWouldBlock } + + // Check for external connections. + nfd, err := e.boundSocketFD.Accept(ctx) + if err == unix.EWOULDBLOCK { + return nil, syserr.ErrWouldBlock + } + if err != nil { + return nil, syserr.FromError(err) + } + q := &waiter.Queue{} + scme, serr := NewSCMEndpoint(nfd, q, e.path) + if serr != nil { + unix.Close(nfd) + return nil, serr + } + scme.Init() + return NewExternal(e.stype, e.idGenerator, q, scme, scme).(*connectionedEndpoint), nil + } // Bind binds the connection. @@ -476,6 +541,13 @@ func (e *connectionedEndpoint) SendMsg(ctx context.Context, data [][]byte, c Con return e.baseEndpoint.SendMsg(ctx, data, c, to) } +func (e *connectionedEndpoint) isBoundSocketReadable() bool { + if e.boundSocketFD == nil { + return false + } + return fdnotifier.NonBlockingPoll(e.boundSocketFD.NotificationFD(), waiter.ReadableEvents)&waiter.ReadableEvents != 0 +} + // Readiness returns the current readiness of the connectionedEndpoint. For // example, if waiter.EventIn is set, the connectionedEndpoint is immediately // readable. @@ -493,7 +565,7 @@ func (e *connectionedEndpoint) Readiness(mask waiter.EventMask) waiter.EventMask ready |= waiter.WritableEvents } case e.ListeningLocked(): - if mask&waiter.ReadableEvents != 0 && len(e.acceptedChan) > 0 { + if mask&waiter.ReadableEvents != 0 && (len(e.acceptedChan) > 0 || e.isBoundSocketReadable()) { ready |= waiter.ReadableEvents } } @@ -524,3 +596,41 @@ func (e *connectionedEndpoint) OnSetSendBufferSize(v int64) (newSz int64) { // WakeupWriters implements tcpip.SocketOptionsHandler.WakeupWriters. func (e *connectionedEndpoint) WakeupWriters() {} + +// SetBoundSocketFD implement HostBountEndpoint.SetBoundSocketFD. +func (e *connectionedEndpoint) SetBoundSocketFD(bsFD *lisafs.ClientBoundSocketFD) { + e.Lock() + defer e.Unlock() + if e.boundSocketFD != nil { + panic(fmt.Sprintf("SetBoundSocketFD called twice\nold: %v\nnew: %v", e.boundSocketFD, bsFD)) + } + e.boundSocketFD = bsFD + fdnotifier.AddFD(bsFD.NotificationFD(), e.Queue) +} + +// EventRegister implements waiter.Waitable.EventRegister. +func (e *connectionedEndpoint) EventRegister(we *waiter.Entry) error { + if err := e.baseEndpoint.EventRegister(we); err != nil { + return err + } + + e.Lock() + bsFD := e.boundSocketFD + e.Unlock() + if bsFD != nil { + fdnotifier.UpdateFD(bsFD.NotificationFD()) + } + return nil +} + +// EventUnregister implements waiter.Waitable.EventUnregister. +func (e *connectionedEndpoint) EventUnregister(we *waiter.Entry) { + e.baseEndpoint.EventUnregister(we) + + e.Lock() + bsFD := e.boundSocketFD + e.Unlock() + if bsFD != nil { + fdnotifier.UpdateFD(bsFD.NotificationFD()) + } +} diff --git a/pkg/sentry/socket/unix/transport/connectioned_state.go b/pkg/sentry/socket/unix/transport/connectioned_state.go index b20334d4f..fea09e1c5 100644 --- a/pkg/sentry/socket/unix/transport/connectioned_state.go +++ b/pkg/sentry/socket/unix/transport/connectioned_state.go @@ -52,6 +52,13 @@ func (e *connectionedEndpoint) loadAcceptedChan(acceptedSlice []*connectionedEnd } } +// beforeSave is invoked by stateify. +func (e *connectionedEndpoint) beforeSave() { + if e.boundSocketFD != nil { + panic("Cannot save endpoint with bound host socket") + } +} + // afterLoad is invoked by stateify. func (e *connectionedEndpoint) afterLoad() { e.ops.InitHandler(e, &stackHandler{}, getSendBufferLimits, getReceiveBufferLimits) diff --git a/pkg/sentry/socket/unix/transport/unix.go b/pkg/sentry/socket/unix/transport/unix.go index 4829c4f95..0840731b7 100644 --- a/pkg/sentry/socket/unix/transport/unix.go +++ b/pkg/sentry/socket/unix/transport/unix.go @@ -18,6 +18,7 @@ package transport import ( "gvisor.dev/gvisor/pkg/abi/linux" "gvisor.dev/gvisor/pkg/context" + "gvisor.dev/gvisor/pkg/lisafs" "gvisor.dev/gvisor/pkg/log" "gvisor.dev/gvisor/pkg/sync" "gvisor.dev/gvisor/pkg/syserr" @@ -259,6 +260,16 @@ type BoundEndpoint interface { Release(ctx context.Context) } +// HostBoundEndpoint is an interface that endpoints can implement if they support +// binding listening and accepting connections from a bound Unix domain socket +// on the host. +type HostBoundEndpoint interface { + // SetBoundSocketFD will be called on supporting endpoints after + // binding a socket on the host filesystem. Implementations should use + // delegate Listen and Accept calls to the ClientBoundSocketFD. + SetBoundSocketFD(*lisafs.ClientBoundSocketFD) +} + // message represents a message passed over a Unix domain socket. // // +stateify savable diff --git a/runsc/config/config.go b/runsc/config/config.go index 76eed6f9a..7c1a53c56 100644 --- a/runsc/config/config.go +++ b/runsc/config/config.go @@ -77,8 +77,8 @@ type Config struct { // Verity is whether there's one or more verity file system to mount. Verity bool `flag:"verity"` - // FSGoferHostUDS enables the gofer to mount a host UDS and connect to it or - // bind (create) a host UDS and serve it. + // FSGoferHostUDS enables the gofer to create and connect to host unix + // domain sockets. FSGoferHostUDS bool `flag:"fsgofer-host-uds"` // Network indicates what type of network to use. diff --git a/runsc/fsgofer/filter/config.go b/runsc/fsgofer/filter/config.go index 49ddf5f0c..06b3a2ab0 100644 --- a/runsc/fsgofer/filter/config.go +++ b/runsc/fsgofer/filter/config.go @@ -212,6 +212,10 @@ var allowedSyscalls = seccomp.SyscallRules{ } var udsSyscalls = seccomp.SyscallRules{ + unix.SYS_ACCEPT4: {}, + unix.SYS_BIND: {}, + unix.SYS_CONNECT: {}, + unix.SYS_LISTEN: {}, unix.SYS_SOCKET: []seccomp.Rule{ { seccomp.EqualTo(unix.AF_UNIX), @@ -229,11 +233,6 @@ var udsSyscalls = seccomp.SyscallRules{ seccomp.EqualTo(0), }, }, - unix.SYS_CONNECT: []seccomp.Rule{ - { - seccomp.MatchAny{}, - }, - }, } var xattrSyscalls = seccomp.SyscallRules{ diff --git a/runsc/fsgofer/filter/filter.go b/runsc/fsgofer/filter/filter.go index 66d28febc..799f41a3f 100644 --- a/runsc/fsgofer/filter/filter.go +++ b/runsc/fsgofer/filter/filter.go @@ -31,7 +31,7 @@ func Install() error { } // InstallUDSFilters extends the allowed syscalls to include those necessary for -// connecting to a host UDS. +// creating and connecting to host UDS. func InstallUDSFilters() { // Add additional filters required for connecting to the host's sockets. allowedSyscalls.Merge(udsSyscalls) diff --git a/runsc/fsgofer/fsgofer.go b/runsc/fsgofer/fsgofer.go index 1e36a66f7..672e5951c 100644 --- a/runsc/fsgofer/fsgofer.go +++ b/runsc/fsgofer/fsgofer.go @@ -73,8 +73,8 @@ type Config struct { // PanicOnWrite panics on attempts to write to RO mounts. PanicOnWrite bool - // HostUDS signals whether the gofer can mount a host's UDS and connect to it - // or bind (create) a host UDS and serve it. + // HostUDS signals whether the gofer can create and connect to host + // unix domain sockets. HostUDS bool // EnableVerityXattr allows access to extended attributes used by the diff --git a/runsc/fsgofer/lisafs.go b/runsc/fsgofer/lisafs.go index e931af069..3da375608 100644 --- a/runsc/fsgofer/lisafs.go +++ b/runsc/fsgofer/lisafs.go @@ -17,7 +17,9 @@ package fsgofer import ( "io" "math" + "os" "path" + "path/filepath" "strconv" "golang.org/x/sys/unix" @@ -107,6 +109,9 @@ func (s *LisafsServer) SupportedMessages() []lisafs.MID { lisafs.Getdents64, lisafs.FGetXattr, lisafs.FSetXattr, + lisafs.BindAt, + lisafs.Listen, + lisafs.Accept, } } @@ -620,7 +625,7 @@ func (fd *controlFDLisa) Readlink(getLinkBuf func(uint32) []byte) (uint16, error // Connect implements lisafs.ControlFDImpl.Connect. func (fd *controlFDLisa) Connect(sockType uint32) (int, error) { if !fd.Conn().ServerImpl().(*LisafsServer).config.HostUDS { - return -1, unix.ECONNREFUSED + return -1, unix.EPERM } // TODO(gvisor.dev/issue/1003): Due to different app vs replacement @@ -629,7 +634,7 @@ func (fd *controlFDLisa) Connect(sockType uint32) (int, error) { // in order to actually connect to this socket. hostPath := fd.Node().FilePath() if len(hostPath) >= unixPathMax { - return -1, unix.ECONNREFUSED + return -1, unix.EINVAL } // Only the following types are supported. @@ -652,6 +657,68 @@ func (fd *controlFDLisa) Connect(sockType uint32) (int, error) { return sock, nil } +// BindAt implements lisafs.ControlFDImpl.BindAt. +func (fd *controlFDLisa) BindAt(name string, sockType uint32) (*lisafs.ControlFD, linux.Statx, *lisafs.BoundSocketFD, int, error) { + if !fd.Conn().ServerImpl().(*LisafsServer).config.HostUDS { + return nil, linux.Statx{}, nil, -1, unix.EPERM + } + + // Because there is no "bindat" syscall in Linux, we must create an + // absolute path to the socket we are creating, + socketPath := filepath.Join(fd.Node().FilePath(), name) + + // TODO(gvisor.dev/issue/1003): Due to different app vs replacement + // mappings, the app path may have fit in the sockaddr, but we can't fit + // hostPath in our sockaddr. We'd need to redirect through a shorter path + // in order to actually connect to this socket. + if len(socketPath) >= unixPathMax { + log.Warningf("BindAt called with name too long: %q (len=%d)", socketPath, len(socketPath)) + return nil, linux.Statx{}, nil, -1, unix.EINVAL + } + + // Only the following types are supported. + switch sockType { + case unix.SOCK_STREAM, unix.SOCK_SEQPACKET: + default: + return nil, linux.Statx{}, nil, -1, unix.ENXIO + } + + // Create and bind the socket using the sockPath which may be a + // symlink. + sockFD, err := unix.Socket(unix.AF_UNIX, int(sockType), 0) + if err != nil { + return nil, linux.Statx{}, nil, -1, err + } + if err := unix.Bind(sockFD, &unix.SockaddrUnix{Name: socketPath}); err != nil { + return nil, linux.Statx{}, nil, -1, err + } + + // Stat the socket. + sockStat, err := fstatTo(sockFD) + if err != nil { + _ = unix.Unlink(socketPath) + return nil, linux.Statx{}, nil, -1, err + } + + // Get an os.File that will back future socket calls. + sockFile := os.NewFile(uintptr(sockFD), socketPath) + + // Create an FD that will be donated to the sandbox. + sockFDToDonate, err := unix.Dup(int(sockFile.Fd())) + if err != nil { + _ = unix.Unlink(socketPath) + return nil, linux.Statx{}, nil, -1, err + } + + socketControlFD := newControlFDLisa(sockFD, fd, socketPath, linux.ModeSocket) + boundSocketFD := &boundSocketFDLisa{ + sock: sockFile, + } + boundSocketFD.Init(socketControlFD.FD(), boundSocketFD) + + return socketControlFD.FD(), sockStat, boundSocketFD.FD(), sockFDToDonate, nil +} + // Unlink implements lisafs.ControlFDImpl.Unlink. func (fd *controlFDLisa) Unlink(name string, flags uint32) error { return unix.Unlinkat(fd.hostFD, name, int(flags)) @@ -838,6 +905,44 @@ func (fd *openFDLisa) Renamed() { // openFDLisa does not have any state to update on rename. } +type boundSocketFDLisa struct { + lisafs.BoundSocketFD + + sock *os.File +} + +var _ lisafs.BoundSocketFDImpl = (*boundSocketFDLisa)(nil) + +// Close implements lisafs.BoundSocketFD.Close. +func (fd *boundSocketFDLisa) Close() { + fd.sock.Close() +} + +// FD implements lisafs.BoundSocketFD.FD. +func (fd *boundSocketFDLisa) FD() *lisafs.BoundSocketFD { + if fd == nil { + return nil + } + return &fd.BoundSocketFD +} + +// Listen implements lisafs.BoundSocketFD.Listen. +func (fd *boundSocketFDLisa) Listen(backlog int32) error { + return unix.Listen(int(fd.sock.Fd()), int(backlog)) +} + +// Listen implements lisafs.BoundSocketFD.Accept. +func (fd *boundSocketFDLisa) Accept() (int, string, error) { + flags := unix.O_NONBLOCK | unix.O_CLOEXEC + nfd, _, err := unix.Accept4(int(fd.sock.Fd()), flags) + if err != nil { + return -1, "", err + } + // Return an empty peer address so that we don't leak the actual host + // address. + return nfd, "", err +} + // tryOpen tries to open() with different modes as documented. func tryOpen(open func(int) (int, error)) (hostFD int, err error) { // Attempt to open file in the following in order: diff --git a/test/syscalls/linux/connect_external.cc b/test/syscalls/linux/connect_external.cc index fb2476da4..be3aa3e7b 100644 --- a/test/syscalls/linux/connect_external.cc +++ b/test/syscalls/linux/connect_external.cc @@ -110,6 +110,52 @@ TEST_P(GoferStreamSeqpacketTest, NonListening) { SyscallFailsWithErrno(ECONNREFUSED)); } +// Bind to a socket, then Listen and Accept. +TEST_P(GoferStreamSeqpacketTest, BindListenAccept) { + // Binding to host socket requires LisaFS. + SKIP_IF(!IsLisafsEnabled()); + + std::string env; + ProtocolSocket proto; + std::tie(env, proto) = GetParam(); + + char* val = getenv(env.c_str()); + ASSERT_NE(val, nullptr); + std::string root(val); + + FileDescriptor sock = + ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_UNIX, proto.protocol, 0)); + + std::string socket_path = + JoinPath("/tmp/sockets", proto.name, "created-in-sandbox"); + + struct sockaddr_un addr = {}; + addr.sun_family = AF_UNIX; + memcpy(addr.sun_path, socket_path.c_str(), socket_path.length()); + + ASSERT_THAT( + bind(sock.get(), reinterpret_cast(&addr), sizeof(addr)), + SyscallSucceeds()); + + ASSERT_THAT(listen(sock.get(), 1), SyscallSucceeds()); + + FileDescriptor accSock = + ASSERT_NO_ERRNO_AND_VALUE(Accept(sock.get(), NULL, NULL)); + + // Other socket should be echo server. + constexpr int kBufferSize = 64; + char send_buffer[kBufferSize]; + memset(send_buffer, 'a', sizeof(send_buffer)); + + ASSERT_THAT(WriteFd(accSock.get(), send_buffer, sizeof(send_buffer)), + SyscallSucceedsWithValue(sizeof(send_buffer))); + + char recv_buffer[kBufferSize]; + ASSERT_THAT(ReadFd(accSock.get(), recv_buffer, sizeof(recv_buffer)), + SyscallSucceedsWithValue(sizeof(recv_buffer))); + ASSERT_EQ(0, memcmp(send_buffer, recv_buffer, sizeof(send_buffer))); +} + INSTANTIATE_TEST_SUITE_P( StreamSeqpacket, GoferStreamSeqpacketTest, ::testing::Combine( diff --git a/test/uds/uds.go b/test/uds/uds.go index 02a4a7dee..028e366b8 100644 --- a/test/uds/uds.go +++ b/test/uds/uds.go @@ -16,17 +16,33 @@ package uds import ( + "errors" "fmt" "io" "io/ioutil" "os" "path/filepath" + "time" "golang.org/x/sys/unix" "gvisor.dev/gvisor/pkg/log" "gvisor.dev/gvisor/pkg/unet" ) +func doEcho(s *unet.Socket) error { + buf := make([]byte, 512) + n, err := s.Read(buf) + if err != nil { + return fmt.Errorf("failed to read: %d, %w", n, err) + } + + n, err = s.Write(buf[:n]) + if err != nil { + return fmt.Errorf("failed to write: %d, %w", n, err) + } + return nil +} + // createEchoSocket creates a socket that echoes back anything received. // // Only works for stream, seqpacket sockets. @@ -57,20 +73,11 @@ func createEchoSocket(path string, protocol int) (cleanup func(), err error) { defer s.Close() for { - buf := make([]byte, 512) - for { - n, err := s.Read(buf) - if err == io.EOF { + if err := doEcho(s); err != nil { + if errors.Is(err, io.EOF) { return nil } - if err != nil { - return fmt.Errorf("failed to read: %d, %v", n, err) - } - - n, err = s.Write(buf[:n]) - if err != nil { - return fmt.Errorf("failed to write: %d, %v", n, err) - } + return err } } } @@ -93,6 +100,31 @@ func createEchoSocket(path string, protocol int) (cleanup func(), err error) { return cleanup, nil } +// connectAndBecomeEcho connects to the given socket and turns into an echo server. +func connectAndBecomeEcho(path string, protocol int) (cleanup func(), err error) { + usePacket := protocol == unix.SOCK_SEQPACKET + go func() { + for { + sock, err := unet.Connect(path, usePacket) + log.Infof("Connecting to UDS at %q, got %v", path, err) + if err != nil { + // Wait and try again. + time.Sleep(500 * time.Millisecond) + continue + } + defer sock.Close() + for { + log.Infof("Connected to UDS at %q, running echo server", path) + if err := doEcho(sock); err != nil { + return + } + } + } + }() + + return func() {}, nil +} + // createNonListeningSocket creates a socket that is bound but not listening. // // Only relevant for stream, seqpacket sockets. @@ -162,6 +194,11 @@ type socketCreator func(path string, proto int) (cleanup func(), err error) // * /seqpacket/echo // * /seqpacket/nonlistening // * /dgram/null +// +// Additionally, it will attempt to connect to sockets at the following +// locations, and turn into an echo server once connected: +// * /stream/created-in-sandbox +// * /seqpacket/created-in-sandbox func CreateSocketTree(baseDir string) (dir string, cleanup func(), err error) { dir, err = ioutil.TempDir(baseDir, "sockets") if err != nil { @@ -177,16 +214,18 @@ func CreateSocketTree(baseDir string) (dir string, cleanup func(), err error) { protocol: unix.SOCK_STREAM, name: "stream", sockets: map[string]socketCreator{ - "echo": createEchoSocket, - "nonlistening": createNonListeningSocket, + "echo": createEchoSocket, + "nonlistening": createNonListeningSocket, + "created-in-sandbox": connectAndBecomeEcho, }, }, { protocol: unix.SOCK_SEQPACKET, name: "seqpacket", sockets: map[string]socketCreator{ - "echo": createEchoSocket, - "nonlistening": createNonListeningSocket, + "echo": createEchoSocket, + "nonlistening": createNonListeningSocket, + "created-in-sandbox": connectAndBecomeEcho, }, }, {