diff --git a/pkg/abi/linux/file.go b/pkg/abi/linux/file.go index 691b16db4..540991b57 100644 --- a/pkg/abi/linux/file.go +++ b/pkg/abi/linux/file.go @@ -96,6 +96,11 @@ const ( AT_EMPTY_PATH = 0x1000 ) +// Constants for faccessat2(2). +const ( + AT_EACCESS = 0x200 +) + // Constants for all file-related ...at(2) syscalls. const ( AT_FDCWD = -100 diff --git a/pkg/sentry/syscalls/linux/vfs2/stat.go b/pkg/sentry/syscalls/linux/vfs2/stat.go index adaf8db3f..5ba566c55 100644 --- a/pkg/sentry/syscalls/linux/vfs2/stat.go +++ b/pkg/sentry/syscalls/linux/vfs2/stat.go @@ -245,26 +245,29 @@ func Access(t *kernel.Task, args arch.SyscallArguments) (uintptr, *kernel.Syscal addr := args[0].Pointer() mode := args[1].ModeT() - return 0, nil, accessAt(t, linux.AT_FDCWD, addr, mode) + return 0, nil, accessAt(t, linux.AT_FDCWD, addr, mode, 0 /* flags */) } // Faccessat implements Linux syscall faccessat(2). -// -// Note that the faccessat() system call does not take a flags argument: -// "The raw faccessat() system call takes only the first three arguments. The -// AT_EACCESS and AT_SYMLINK_NOFOLLOW flags are actually implemented within -// the glibc wrapper function for faccessat(). If either of these flags is -// specified, then the wrapper function employs fstatat(2) to determine access -// permissions." - faccessat(2) func Faccessat(t *kernel.Task, args arch.SyscallArguments) (uintptr, *kernel.SyscallControl, error) { dirfd := args[0].Int() addr := args[1].Pointer() mode := args[2].ModeT() - return 0, nil, accessAt(t, dirfd, addr, mode) + return 0, nil, accessAt(t, dirfd, addr, mode, 0 /* flags */) } -func accessAt(t *kernel.Task, dirfd int32, pathAddr hostarch.Addr, mode uint) error { +// Faccessat2 implements Linux syscall faccessat2(2). +func Faccessat2(t *kernel.Task, args arch.SyscallArguments) (uintptr, *kernel.SyscallControl, error) { + dirfd := args[0].Int() + addr := args[1].Pointer() + mode := args[2].ModeT() + flags := args[3].Int() + + return 0, nil, accessAt(t, dirfd, addr, mode, flags) +} + +func accessAt(t *kernel.Task, dirfd int32, pathAddr hostarch.Addr, mode uint, flags int32) error { const rOK = 4 const wOK = 2 const xOK = 1 @@ -274,30 +277,38 @@ func accessAt(t *kernel.Task, dirfd int32, pathAddr hostarch.Addr, mode uint) er return linuxerr.EINVAL } + // faccessat2(2) isn't documented as supporting AT_EMPTY_PATH, but it does. + if flags&^(linux.AT_EACCESS|linux.AT_SYMLINK_NOFOLLOW|linux.AT_EMPTY_PATH) != 0 { + return linuxerr.EINVAL + } + path, err := copyInPath(t, pathAddr) if err != nil { return err } - tpop, err := getTaskPathOperation(t, dirfd, path, disallowEmptyPath, followFinalSymlink) + tpop, err := getTaskPathOperation(t, dirfd, path, shouldAllowEmptyPath(flags&linux.AT_EMPTY_PATH != 0), shouldFollowFinalSymlink(flags&linux.AT_SYMLINK_NOFOLLOW == 0)) if err != nil { return err } defer tpop.Release(t) - // access(2) and faccessat(2) check permissions using real - // UID/GID, not effective UID/GID. - // - // "access() needs to use the real uid/gid, not the effective - // uid/gid. We do this by temporarily clearing all FS-related - // capabilities and switching the fsuid/fsgid around to the - // real ones." -fs/open.c:faccessat - creds := t.Credentials().Fork() - creds.EffectiveKUID = creds.RealKUID - creds.EffectiveKGID = creds.RealKGID - if creds.EffectiveKUID.In(creds.UserNamespace) == auth.RootUID { - creds.EffectiveCaps = creds.PermittedCaps - } else { - creds.EffectiveCaps = 0 + creds := t.Credentials() + if flags&linux.AT_EACCESS == 0 { + // access(2) and faccessat(2) check permissions using real + // UID/GID, not effective UID/GID. + // + // "access() needs to use the real uid/gid, not the effective + // uid/gid. We do this by temporarily clearing all FS-related + // capabilities and switching the fsuid/fsgid around to the + // real ones." -fs/open.c:faccessat + creds = creds.Fork() + creds.EffectiveKUID = creds.RealKUID + creds.EffectiveKGID = creds.RealKGID + if creds.EffectiveKUID.In(creds.UserNamespace) == auth.RootUID { + creds.EffectiveCaps = creds.PermittedCaps + } else { + creds.EffectiveCaps = 0 + } } return t.Kernel().VFS().AccessAt(t, creds, vfs.AccessTypes(mode), &tpop.pop) diff --git a/pkg/sentry/syscalls/linux/vfs2/vfs2.go b/pkg/sentry/syscalls/linux/vfs2/vfs2.go index e7a21ccc3..7ed020c97 100644 --- a/pkg/sentry/syscalls/linux/vfs2/vfs2.go +++ b/pkg/sentry/syscalls/linux/vfs2/vfs2.go @@ -162,6 +162,7 @@ func Override() { s.Table[327] = syscalls.Supported("preadv2", Preadv2) s.Table[328] = syscalls.Supported("pwritev2", Pwritev2) s.Table[332] = syscalls.Supported("statx", Statx) + s.Table[439] = syscalls.Supported("faccessat2", Faccessat2) s.Table[441] = syscalls.Supported("epoll_pwait2", EpollPwait2) s.Init() @@ -276,6 +277,7 @@ func Override() { s.Table[286] = syscalls.Supported("preadv2", Preadv2) s.Table[287] = syscalls.Supported("pwritev2", Pwritev2) s.Table[291] = syscalls.Supported("statx", Statx) + s.Table[439] = syscalls.Supported("faccessat2", Faccessat2) s.Table[441] = syscalls.Supported("epoll_pwait2", EpollPwait2) s.Init() } diff --git a/test/syscalls/linux/access.cc b/test/syscalls/linux/access.cc index bcc25cef4..ab43f90ea 100644 --- a/test/syscalls/linux/access.cc +++ b/test/syscalls/linux/access.cc @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -164,6 +165,90 @@ TEST_F(AccessTest, UsrReadWriteExec) { EXPECT_THAT(unlink(filename.c_str()), SyscallSucceeds()); } +// glibc faccessat() is a wrapper around either the faccessat syscall that tries +// to implement flags in userspace, or the faccessat2 syscall. We want to test +// syscalls specifically, so use syscall(2) directly. +int sys_faccessat(int dirfd, const char* pathname, int mode) { + return syscall(SYS_faccessat, dirfd, pathname, mode); +} + +#ifndef SYS_faccessat2 +#define SYS_faccessat2 439 +#endif // SYS_faccessat2 + +int sys_faccessat2(int dirfd, const char* pathname, int mode, int flags) { + return syscall(SYS_faccessat2, dirfd, pathname, mode, flags); +} + +TEST(FaccessatTest, SymlinkFollowed) { + const std::string target_path = NewTempAbsPath(); + const std::string symlink_path = NewTempAbsPath(); + ASSERT_THAT(symlink(target_path.c_str(), symlink_path.c_str()), + SyscallSucceeds()); + + // faccessat() should initially fail with ENOENT since it follows the symlink + // to a file that doesn't exist. + EXPECT_THAT(sys_faccessat(-1, symlink_path.c_str(), F_OK), + SyscallFailsWithErrno(ENOENT)); + + // After creating the symlink target, faccessat() should succeed. + int fd; + ASSERT_THAT(fd = open(target_path.c_str(), O_CREAT | O_EXCL, 0644), + SyscallSucceeds()); + close(fd); + EXPECT_THAT(sys_faccessat(-1, symlink_path.c_str(), F_OK), SyscallSucceeds()); +} + +PosixErrorOr Faccessat2Supported() { + if (IsRunningOnGvisor() && !IsRunningWithVFS1()) { + // faccessat2 support is expected on VFS2. + return true; + } + int ret = sys_faccessat2(-1, "/", F_OK, 0); + if (ret == 0) { + return true; + } + if (errno == ENOSYS) { + return false; + } + return PosixError(errno, "unexpected errno from faccessat2(/)"); +} + +TEST(Faccessat2Test, SymlinkFollowedByDefault) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(Faccessat2Supported())); + + const std::string target_path = NewTempAbsPath(); + const std::string symlink_path = NewTempAbsPath(); + ASSERT_THAT(symlink(target_path.c_str(), symlink_path.c_str()), + SyscallSucceeds()); + + // faccessat2() should initially fail with ENOENT since, by default, it + // follows the symlink to a file that doesn't exist. + EXPECT_THAT(sys_faccessat2(-1, symlink_path.c_str(), F_OK, 0 /* flags */), + SyscallFailsWithErrno(ENOENT)); + + // After creating the symlink target, faccessat2() should succeed. + int fd; + ASSERT_THAT(fd = open(target_path.c_str(), O_CREAT | O_EXCL, 0644), + SyscallSucceeds()); + close(fd); + EXPECT_THAT(sys_faccessat2(-1, symlink_path.c_str(), F_OK, 0 /* flags */), + SyscallSucceeds()); +} + +TEST(Faccessat2Test, SymlinkNofollow) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(Faccessat2Supported())); + + const std::string target_path = NewTempAbsPath(); + const std::string symlink_path = NewTempAbsPath(); + ASSERT_THAT(symlink(target_path.c_str(), symlink_path.c_str()), + SyscallSucceeds()); + + EXPECT_THAT( + sys_faccessat2(-1, symlink_path.c_str(), F_OK, AT_SYMLINK_NOFOLLOW), + SyscallSucceeds()); +} + } // namespace } // namespace testing