Allow creating unix domain sockets on the host, behind a flag.

When enabled with `AllowUDS`, unix domain sockets can be created in the sandbox
and bound on the host filesystem. The application can listen() and accept() on
these sockets as usual. Accept'ed sockets will be donated to the sandbox,
similar to how connect'ed sockets work.

In order to make notifications like poll work, the gofer donates the host-bound
socket FD to the sandbox, but the seccomp filters will (correctly) prevent the
sandbox from calling listen and accept directly on that FD. Instead, listen and
accept calls must go through the gofer. The donated host FD can should only be
used to poll for new incoming connectins.

Note that I changed the order of some of the Lisa RPCs in order to group Bind
with the existing similar Connect method. This changes the RPC numbers in a
backwards-incompatible way, but since nobody is using Lisa yet we are OK. It's
better to make these cleanup changes now before we have users and are locked
in.

PiperOrigin-RevId: 447236441
This commit is contained in:
Nicolas Lacasse
2022-05-07 18:27:18 -07:00
committed by gVisor bot
parent 409f32743b
commit d5002c6adc
18 changed files with 841 additions and 65 deletions
+12
View File
@@ -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",
+93
View File
@@ -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
}
+16
View File
@@ -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 {
+87 -1
View File
@@ -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()
}
+114 -1
View File
@@ -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 {
+119
View File
@@ -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
+35 -16
View File
@@ -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
}
+1
View File
@@ -62,6 +62,7 @@ go_library(
"//pkg/errors/linuxerr",
"//pkg/fdnotifier",
"//pkg/ilist",
"//pkg/lisafs",
"//pkg/log",
"//pkg/refs",
"//pkg/refsvfs2",
+129 -19
View File
@@ -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())
}
}
@@ -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)
+11
View File
@@ -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
+2 -2
View File
@@ -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.
+4 -5
View File
@@ -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{
+1 -1
View File
@@ -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)
+2 -2
View File
@@ -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
+107 -2
View File
@@ -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:
+46
View File
@@ -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<struct sockaddr*>(&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(
+55 -16
View File
@@ -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,
},
},
{