Avoid using AT_EMPTY_PATH while making linkat(2) host syscall.

Using linkat(targetFD, "", newdirfd, name, AT_EMPTY_PATH) requires
CAP_DAC_READ_SEARCH in the *root* userns. See fs/namei.c:do_linkat(). It checks
`capable(CAP_DAC_READ_SEARCH)` which actually performs the capability check in
the root userns.

The gofer and the sandbox process may be configured to be running in a non-root
userns. Then the linkat(2) syscall fails with ENOENT.

We are forced to use the target's parent directory FD to perform the linkat(2)
operation.

Fixes #8688

PiperOrigin-RevId: 517540797
This commit is contained in:
Ayush Ranjan
2023-03-17 16:55:17 -07:00
committed by gVisor bot
parent edd7fd2e60
commit acf460d0d7
5 changed files with 41 additions and 12 deletions
+3 -1
View File
@@ -325,7 +325,9 @@ func (d *dentry) mknod(ctx context.Context, name string, creds *auth.Credentials
}
}
// Precondition: !d.isSynthetic().
// Preconditions:
// - !d.isSynthetic().
// - d.fs.renameMu must be locked.
func (d *dentry) link(ctx context.Context, target *dentry, name string) (*dentry, error) {
switch dt := d.impl.(type) {
case *lisafsDentry:
+9 -1
View File
@@ -512,8 +512,16 @@ func (d *directfsDentry) bindAt(ctx context.Context, name string, creds *auth.Cr
return child, nil
}
// Precondition: d.fs.renameMu must be locked.
func (d *directfsDentry) link(target *directfsDentry, name string) (*dentry, error) {
if err := unix.Linkat(target.controlFD, "", d.controlFD, name, unix.AT_EMPTY_PATH); err != nil {
// Using linkat(targetFD, "", newdirfd, name, AT_EMPTY_PATH) requires
// CAP_DAC_READ_SEARCH in the *root* userns. With directfs, the sandbox
// process has CAP_DAC_READ_SEARCH in its own userns. But the sandbox is
// running in a different userns. So we can't use AT_EMPTY_PATH. Fallback to
// using olddirfd to call linkat(2).
// Also note that d and target are from the same mount. Given target is a
// non-directory and d is a directory, target.parent must exist.
if err := unix.Linkat(target.parent.impl.(*directfsDentry).controlFD, target.name, d.controlFD, name, 0); err != nil {
return nil, err
}
// Note that we don't need to set uid/gid for the new child. This is a hard
+1 -1
View File
@@ -426,7 +426,7 @@ func hostFilesystemFilters() seccomp.SyscallRules {
seccomp.MatchAny{},
validFDCheck,
seccomp.MatchAny{},
seccomp.EqualTo(unix.AT_EMPTY_PATH),
seccomp.EqualTo(0),
},
},
unix.SYS_MKDIRAT: []seccomp.Rule{
+25 -8
View File
@@ -248,6 +248,16 @@ func (fd *controlFDLisa) getWritableFD() (int, error) {
return writableFD, nil
}
func (fd *controlFDLisa) getParentFD() (int, string, error) {
filePath := fd.Node().FilePath()
if filePath == "/" {
log.Warningf("getParentFD() call on the root")
return -1, "", unix.EINVAL
}
parent, err := unix.Open(path.Dir(filePath), openFlags|unix.O_PATH, 0)
return parent, path.Base(filePath), err
}
// FD implements lisafs.ControlFDImpl.FD.
func (fd *controlFDLisa) FD() *lisafs.ControlFD {
if fd == nil {
@@ -280,15 +290,14 @@ func (fd *controlFDLisa) SetStat(stat lisafs.SetStatReq) (failureMask uint32, fa
if fd.IsSocket() {
// fchmod(2) on socket files created via bind(2) fails. We need to
// fchmodat(2) it from its parent.
sockPath := fd.Node().FilePath()
parent, err := unix.Open(path.Dir(sockPath), openFlags|unix.O_PATH, 0)
parent, sockName, err := fd.getParentFD()
if err == nil {
// Note that AT_SYMLINK_NOFOLLOW flag is not currently supported.
err = unix.Fchmodat(parent, path.Base(sockPath), stat.Mode&^unix.S_IFMT, 0 /* flags */)
err = unix.Fchmodat(parent, sockName, stat.Mode&^unix.S_IFMT, 0 /* flags */)
unix.Close(parent)
}
if err != nil {
log.Warningf("SetStat fchmod failed on socket %q, err: %v", sockPath, err)
log.Warningf("SetStat fchmod failed on socket %q, err: %v", fd.Node().FilePath(), err)
failureMask |= unix.STATX_MODE
failureErr = err
}
@@ -332,10 +341,9 @@ func (fd *controlFDLisa) SetStat(stat lisafs.SetStatReq) (failureMask uint32, fa
// utimensat operates different that other syscalls. To operate on a
// symlink it *requires* AT_SYMLINK_NOFOLLOW with dirFD and a non-empty
// name. We need the parent FD.
symlinkPath := fd.Node().FilePath()
parent, err := unix.Open(path.Dir(symlinkPath), openFlags|unix.O_PATH, 0)
parent, symlinkName, err := fd.getParentFD()
if err == nil {
err = fsutil.Utimensat(parent, path.Base(symlinkPath), utimes, unix.AT_SYMLINK_NOFOLLOW)
err = fsutil.Utimensat(parent, symlinkName, utimes, unix.AT_SYMLINK_NOFOLLOW)
unix.Close(parent)
}
if err != nil {
@@ -669,8 +677,17 @@ func (fd *controlFDLisa) Symlink(name string, target string, uid lisafs.UID, gid
// Link implements lisafs.ControlFDImpl.Link.
func (fd *controlFDLisa) Link(dir lisafs.ControlFDImpl, name string) (*lisafs.ControlFD, linux.Statx, error) {
// Using linkat(targetFD, "", newdirfd, name, AT_EMPTY_PATH) requires
// CAP_DAC_READ_SEARCH in the *root* userns. The gofer process has
// CAP_DAC_READ_SEARCH in its own userns. But sometimes the gofer may be
// running in a different userns. So we can't use AT_EMPTY_PATH. Fallback
// to using olddirfd to call linkat(2).
oldDirFD, oldName, err := fd.getParentFD()
if err != nil {
return nil, linux.Statx{}, err
}
dirFD := dir.(*controlFDLisa)
if err := unix.Linkat(fd.hostFD, "", dirFD.hostFD, name, unix.AT_EMPTY_PATH); err != nil {
if err := unix.Linkat(oldDirFD, oldName, dirFD.hostFD, name, 0); err != nil {
return nil, linux.Statx{}, err
}
cu := cleanup.Make(func() {
+3 -1
View File
@@ -324,7 +324,9 @@ syscall_test(
add_fusefs = True,
add_overlay = True,
test = "//test/syscalls/linux:link_test",
use_tmpfs = True, # gofer needs CAP_DAC_READ_SEARCH to use AT_EMPTY_PATH with linkat(2)
# TODO(gvisor.dev/issue/6739): Remove use_tmpfs=True once gofer filesystem
# supports hard links correctly.
use_tmpfs = True,
)
syscall_test(