diff --git a/pkg/p9/client_file.go b/pkg/p9/client_file.go index 28396b0ea..f84352a73 100644 --- a/pkg/p9/client_file.go +++ b/pkg/p9/client_file.go @@ -348,6 +348,37 @@ func (c *clientFile) Open(flags OpenFlags) (*fd.FD, QID, uint32, error) { return rlopen.File, rlopen.QID, rlopen.IoUnit, nil } +func (c *clientFile) Bind(sockType uint32, sockName string, uid UID, gid GID) (File, QID, AttrMask, Attr, error) { + if atomic.LoadUint32(&c.closed) != 0 { + return nil, QID{}, AttrMask{}, Attr{}, unix.EBADF + } + + if !versionSupportsBind(c.client.version) { + return nil, QID{}, AttrMask{}, Attr{}, unix.EOPNOTSUPP + } + + fid, ok := c.client.fidPool.Get() + if !ok { + return nil, QID{}, AttrMask{}, Attr{}, ErrOutOfFIDs + } + + tbind := Tbind{ + SockType: sockType, + SockName: sockName, + UID: uid, + GID: gid, + Directory: c.fid, + NewFID: FID(fid), + } + rbind := Rbind{} + if err := c.client.sendRecv(&tbind, &rbind); err != nil { + c.client.fidPool.Put(fid) + return nil, QID{}, AttrMask{}, Attr{}, err + } + + return c.client.newFile(FID(fid)), rbind.QID, rbind.Valid, rbind.Attr, nil +} + // Connect implements File.Connect. func (c *clientFile) Connect(flags ConnectFlags) (*fd.FD, error) { if atomic.LoadUint32(&c.closed) != 0 { diff --git a/pkg/p9/file.go b/pkg/p9/file.go index b4b556cb9..ce649f69e 100644 --- a/pkg/p9/file.go +++ b/pkg/p9/file.go @@ -294,6 +294,17 @@ type File interface { // On the server, Flush has a read concurrency guarantee. Flush() error + // Bind binds to a host unix domain socket. If successful, it creates a + // socket file on the host filesystem and returns a File for the newly + // created socket file. The File implementation must save the bound socket + // FD so that subsequent Listen and Accept operations on the File can be + // served. + // + // Bind is an extension to 9P2000.L, see version.go. + // + // On the server, UnlinkAt has a write concurrency guarantee. + Bind(sockType uint32, sockName string, uid UID, gid GID) (File, QID, AttrMask, Attr, error) + // Connect establishes a new host-socket backed connection with a // socket. A File does not need to be opened before it can be connected // and it can be connected to multiple times resulting in a unique diff --git a/pkg/p9/handlers.go b/pkg/p9/handlers.go index c85af5e9e..b73cfdd81 100644 --- a/pkg/p9/handlers.go +++ b/pkg/p9/handlers.go @@ -1398,6 +1398,58 @@ func (t *Tumknod) handle(cs *connState) message { return &Rumknod{*rmknod} } +// handle implements handler.handle. +func (t *Tbind) handle(cs *connState) message { + if err := checkSafeName(t.SockName); err != nil { + return newErr(err) + } + + ref, ok := cs.LookupFID(t.Directory) + if !ok { + return newErr(unix.EBADF) + } + defer ref.DecRef() + + var ( + sockRef *fidRef + qid QID + valid AttrMask + attr Attr + ) + if err := ref.safelyWrite(func() (err error) { + // Don't allow creation from non-directories or deleted directories. + if ref.isDeleted() || !ref.mode.IsDir() { + return unix.EINVAL + } + + // Not allowed on open directories. + if ref.opened { + return unix.EINVAL + } + + var sockF File + sockF, qid, valid, attr, err = ref.file.Bind(t.SockType, t.SockName, t.UID, t.GID) + if err != nil { + return err + } + + sockRef = &fidRef{ + server: cs.server, + parent: ref, + file: sockF, + mode: ModeSocket, + pathNode: ref.pathNode.pathNodeFor(t.SockName), + } + ref.pathNode.addChild(sockRef, t.SockName) + ref.IncRef() // Acquire parent reference. + return nil + }); err != nil { + return newErr(err) + } + cs.InsertFID(t.NewFID, sockRef) + return &Rbind{QID: qid, Valid: valid, Attr: attr} +} + // handle implements handler.handle. func (t *Tlconnect) handle(cs *connState) message { ref, ok := cs.LookupFID(t.FID) diff --git a/pkg/p9/messages.go b/pkg/p9/messages.go index 2ff4694c0..e363b47f1 100644 --- a/pkg/p9/messages.go +++ b/pkg/p9/messages.go @@ -2438,6 +2438,95 @@ func (r *Rusymlink) String() string { return fmt.Sprintf("Rusymlink{%v}", &r.Rsymlink) } +// Tbind is a bind request. +type Tbind struct { + // Directory is the directory inside which the bound socket file should be + // created. + Directory FID + + // SockType is the type of socket to be used. This is passed as an argument + // to socket(2). + SockType uint32 + + // SockName is the name of the socket file to be created. + SockName string + + // UID is the owning user. + UID UID + + // GID is the owning group. + GID GID + + // NewFID is the resulting FID for the socket file. + NewFID FID +} + +// decode implements encoder.decode. +func (t *Tbind) decode(b *buffer) { + t.Directory = b.ReadFID() + t.SockType = b.Read32() + t.SockName = b.ReadString() + t.UID = b.ReadUID() + t.GID = b.ReadGID() + t.NewFID = b.ReadFID() +} + +// encode implements encoder.encode. +func (t *Tbind) encode(b *buffer) { + b.WriteFID(t.Directory) + b.Write32(t.SockType) + b.WriteString(t.SockName) + b.WriteUID(t.UID) + b.WriteGID(t.GID) + b.WriteFID(t.NewFID) +} + +// Type implements message.Type. +func (*Tbind) Type() MsgType { + return MsgTbind +} + +// String implements fmt.Stringer. +func (t *Tbind) String() string { + return fmt.Sprintf("Tbind{Directory: %d, SockType: %d, SockName: %s, UID: %d, GID: %d, NewFID: %d}", t.Directory, t.SockType, t.SockName, t.UID, t.GID, t.NewFID) +} + +// Rbind is a bind response. +type Rbind struct { + // QID is the resulting QID of the created socket file. + QID QID + + // Valid indicates which fields are valid. + Valid AttrMask + + // Attr is the set of attributes of the created socket file. + Attr Attr +} + +// decode implements encoder.decode. +func (r *Rbind) decode(b *buffer) { + r.QID.decode(b) + r.Valid.decode(b) + r.Attr.decode(b) +} + +// encode implements encoder.encode. +func (r *Rbind) encode(b *buffer) { + r.QID.encode(b) + r.Valid.encode(b) + r.Attr.encode(b) +} + +// Type implements message.Type. +func (*Rbind) Type() MsgType { + return MsgRbind +} + +// String implements fmt.Stringer. +func (r *Rbind) String() string { + return fmt.Sprintf("Rbind{QID: %s, Valid: %v, Attr: %s}", r.QID, r.Valid, r.Attr) +} + // Tlconnect is a connect request. type Tlconnect struct { // FID is the FID to be connected. @@ -2785,6 +2874,8 @@ func init() { msgRegistry.register(MsgRumknod, func() message { return &Rumknod{} }) msgRegistry.register(MsgTusymlink, func() message { return &Tusymlink{} }) msgRegistry.register(MsgRusymlink, func() message { return &Rusymlink{} }) + msgRegistry.register(MsgTbind, func() message { return &Tbind{} }) + msgRegistry.register(MsgRbind, func() message { return &Rbind{} }) msgRegistry.register(MsgTlconnect, func() message { return &Tlconnect{} }) msgRegistry.register(MsgRlconnect, func() message { return &Rlconnect{} }) msgRegistry.register(MsgTallocate, func() message { return &Tallocate{} }) diff --git a/pkg/p9/messages_test.go b/pkg/p9/messages_test.go index bfeb6c236..8da664bf2 100644 --- a/pkg/p9/messages_test.go +++ b/pkg/p9/messages_test.go @@ -118,6 +118,17 @@ func TestEncodeDecode(t *testing.T) { QID: QID{Type: 1}, IoUnit: 2, }, + &Tbind{ + Directory: 1, + SockType: 2, + SockName: "name", + GID: 3, + UID: 4, + NewFID: 5, + }, + &Rbind{ + QID: QID{Type: 1}, + }, &Tlconnect{ FID: 1, }, diff --git a/pkg/p9/p9.go b/pkg/p9/p9.go index 3d452a0bd..28d44a120 100644 --- a/pkg/p9/p9.go +++ b/pkg/p9/p9.go @@ -404,6 +404,8 @@ const ( MsgRsetattrclunk MsgType = 141 MsgTmultigetattr MsgType = 142 MsgRmultigetattr MsgType = 143 + MsgTbind MsgType = 144 + MsgRbind MsgType = 145 MsgTchannel MsgType = 250 MsgRchannel MsgType = 251 ) diff --git a/pkg/p9/version.go b/pkg/p9/version.go index 950236162..cbb50cb88 100644 --- a/pkg/p9/version.go +++ b/pkg/p9/version.go @@ -185,3 +185,9 @@ func versionSupportsTsetattrclunk(v uint32) bool { func versionSupportsTmultiGetAttr(v uint32) bool { return v >= 13 } + +// versionSupportsBind returns true if version v supports the Tbind message. +func versionSupportsBind(v uint32) bool { + // TODO(b/194709873): Bump version and gate with that. + return false +} diff --git a/runsc/config/config.go b/runsc/config/config.go index a07eab92a..1b403277f 100644 --- a/runsc/config/config.go +++ b/runsc/config/config.go @@ -75,7 +75,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. + // FSGoferHostUDS enables the gofer to mount a host UDS and connect to it or + // bind (create) a host UDS and serve it. FSGoferHostUDS bool `flag:"fsgofer-host-uds"` // Network indicates what type of network to use. diff --git a/runsc/fsgofer/fsgofer.go b/runsc/fsgofer/fsgofer.go index 3d610199c..eb86332cb 100644 --- a/runsc/fsgofer/fsgofer.go +++ b/runsc/fsgofer/fsgofer.go @@ -47,6 +47,9 @@ const ( openFlags = unix.O_NOFOLLOW | unix.O_CLOEXEC allowedOpenFlags = unix.O_TRUNC + + // UNIX_PATH_MAX as defined in include/uapi/linux/un.h. + unixPathMax = 108 ) // verityXattrs are the extended attributes used by verity file system. @@ -70,7 +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. + // 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 bool // EnableVerityXattr allows access to extended attributes used by the @@ -1129,6 +1133,80 @@ func (l *localFile) Flush() error { return nil } +// Bind implements p9.File.Bind. +func (l *localFile) Bind(sockType uint32, sockName string, uid p9.UID, gid p9.GID) (p9.File, p9.QID, p9.AttrMask, p9.Attr, error) { + if !l.attachPoint.conf.HostUDS { + // Bind on host UDS is not allowed. As per mknod(2), which is invoked as + // part of bind(2), if "the filesystem containing pathname does not support + // the type of node requested." then EPERM must be returned. + return nil, p9.QID{}, p9.AttrMask{}, p9.Attr{}, unix.EPERM + } + + // 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 f.path in our sockaddr. We'd need to redirect through a shorter + // path in order to actually connect to this socket. + sockPath := path.Join(l.hostPath, sockName) + if len(sockPath) >= unixPathMax { + return nil, p9.QID{}, p9.AttrMask{}, p9.Attr{}, unix.EINVAL + } + + // Create socket only for supported types. + switch sockType { + case unix.SOCK_STREAM, unix.SOCK_DGRAM, unix.SOCK_SEQPACKET: + default: + return nil, p9.QID{}, p9.AttrMask{}, p9.Attr{}, unix.ENXIO + } + sock, err := unix.Socket(unix.AF_UNIX, int(sockType), 0) + if err != nil { + return nil, p9.QID{}, p9.AttrMask{}, p9.Attr{}, extractErrno(err) + } + + // Revert operations on error paths. + didBind := false + cu := cleanup.Make(func() { + _ = unix.Close(sock) + if didBind { + if err := unix.Unlinkat(l.file.FD(), sockName, 0); err != nil { + log.Warningf("error unlinking file %q after failure: %v", sockPath, err) + } + } + }) + defer cu.Clean() + + // socket FD must be non blocking because RPC operations like Accept on this + // socket must be non blocking. + if err := unix.SetNonblock(sock, true); err != nil { + return nil, p9.QID{}, p9.AttrMask{}, p9.Attr{}, extractErrno(err) + } + + // Bind at the given path which should create the socket file. + if err := unix.Bind(sock, &unix.SockaddrUnix{Name: sockPath}); err != nil { + return nil, p9.QID{}, p9.AttrMask{}, p9.Attr{}, extractErrno(err) + } + didBind = true + + // Open socket to change ownership. + tempSockFD, err := fd.OpenAt(l.file, sockName, unix.O_PATH|openFlags, 0) + if err != nil { + return nil, p9.QID{}, p9.AttrMask{}, p9.Attr{}, extractErrno(err) + } + defer tempSockFD.Close() + + if _, err = setOwnerIfNeeded(tempSockFD.FD(), uid, gid); err != nil { + return nil, p9.QID{}, p9.AttrMask{}, p9.Attr{}, extractErrno(err) + } + + // Generate file for this socket by walking on it. + qid, sockF, valid, attr, err := l.WalkGetAttr([]string{sockName}) + if err != nil { + return nil, p9.QID{}, p9.AttrMask{}, p9.Attr{}, err + } + + cu.Release() + return &socketLocalFile{localFile: sockF.(*localFile), sock: sock}, qid[0], valid, attr, nil +} + // Connect implements p9.File. func (l *localFile) Connect(flags p9.ConnectFlags) (*fd.FD, error) { if !l.attachPoint.conf.HostUDS { @@ -1139,8 +1217,7 @@ func (l *localFile) Connect(flags p9.ConnectFlags) (*fd.FD, error) { // mappings, the app path may have fit in the sockaddr, but we can't // fit f.path in our sockaddr. We'd need to redirect through a shorter // path in order to actually connect to this socket. - const UNIX_PATH_MAX = 108 // defined in afunix.h - if len(l.hostPath) > UNIX_PATH_MAX { + if len(l.hostPath) >= unixPathMax { return nil, unix.ECONNREFUSED } @@ -1175,7 +1252,7 @@ func (l *localFile) Connect(flags p9.ConnectFlags) (*fd.FD, error) { return fd.New(f), nil } -// Close implements p9.File. +// Close implements p9.File.Close. func (l *localFile) Close() error { l.mode = invalidMode err := l.file.Close() @@ -1290,3 +1367,22 @@ func (l *localFile) MultiGetAttr(names []string) ([]p9.FullStat, error) { } return stats, nil } + +// socketLocalFile is an extension of localFile which is only created via Bind +// and additionally implements Listen and Accept. It also tracks the lifecycle +// of the socket FD created by socket(2) in addition to the FD opened on the +// socket file itself. +type socketLocalFile struct { + *localFile + sock int +} + +// Close implements p9.File.Close. +func (l *socketLocalFile) Close() error { + err := l.localFile.Close() + err2 := unix.Close(l.sock) + if err != nil { + return err + } + return err2 +} diff --git a/runsc/fsgofer/fsgofer_test.go b/runsc/fsgofer/fsgofer_test.go index 6cdd6d695..14411bedf 100644 --- a/runsc/fsgofer/fsgofer_test.go +++ b/runsc/fsgofer/fsgofer_test.go @@ -796,6 +796,47 @@ func TestReaddir(t *testing.T) { }) } +func TestUDS(t *testing.T) { + config := Config{ROMount: false, HostUDS: true} + dir, err := ioutil.TempDir("", "root-") + if err != nil { + t.Fatalf("ioutil.TempDir() failed, err: %v", err) + } + defer os.RemoveAll(dir) + + // First attach with writable configuration to setup tree. + a, err := NewAttachPoint(dir, config) + if err != nil { + t.Fatalf("NewAttachPoint failed: %v", err) + } + root, err := a.Attach() + if err != nil { + t.Fatalf("attach failed, err: %v", err) + } + defer root.Close() + + name := "sock" + uid := p9.UID(os.Getuid()) + gid := p9.GID(os.Getgid()) + sockF, _, valid, attr, err := root.Bind(unix.SOCK_STREAM, name, uid, gid) + if err != nil { + t.Fatalf("Bind failed: %v", err) + } + defer sockF.Close() + + if valid.Mode && !attr.Mode.IsSocket() { + t.Errorf("socket file mode is incorrect: want %d, got %d", p9.ModeSocket, attr.Mode) + } + if valid.UID && attr.UID != uid { + t.Errorf("socket file uid is incorrect: want %d, got %d", uid, attr.UID) + } + if valid.GID && attr.GID != gid { + t.Errorf("socket file gid is incorrect: want %d, got %d", gid, attr.GID) + } + // TODO(b/194709873): Once listen and accept are implemented, test connecting + // and accepting a connection using sockF. +} + // Test that attach point can be written to when it points to a file, e.g. // /etc/hosts. func TestAttachFile(t *testing.T) { diff --git a/runsc/fsgofer/lisafs.go b/runsc/fsgofer/lisafs.go index d96fc5f20..6b73fa4c4 100644 --- a/runsc/fsgofer/lisafs.go +++ b/runsc/fsgofer/lisafs.go @@ -655,7 +655,7 @@ func (fd *controlFDLisa) Connect(c *lisafs.Connection, sockType uint32) (int, er // hostPath in our sockaddr. We'd need to redirect through a shorter path // in order to actually connect to this socket. hostPath := fd.FilePathLocked() - if len(hostPath) > 108 { // UNIX_PATH_MAX = 108 is defined in afunix.h. + if len(hostPath) >= unixPathMax { return -1, unix.ECONNREFUSED }