Add Bind RPC to gvisor's 9P protocol and implement it in runsc/fsgofer.

This new RPC allows a client to be able to bind (and hence create) UDS on the
host filesystem. Following changes will add functionality to listen and accept
on such a bound UDS.

PiperOrigin-RevId: 420149313
This commit is contained in:
Ayush Ranjan
2022-01-06 14:53:59 -08:00
committed by gVisor bot
parent c44304353c
commit d7dbf65873
11 changed files with 348 additions and 6 deletions
+31
View File
@@ -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 {
+11
View File
@@ -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
+52
View File
@@ -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)
+91
View File
@@ -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{} })
+11
View File
@@ -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,
},
+2
View File
@@ -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
)
+6
View File
@@ -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
}
+2 -1
View File
@@ -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.
+100 -4
View File
@@ -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
}
+41
View File
@@ -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) {
+1 -1
View File
@@ -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
}