From 93bbcbf35bb29112d4b8cb9e54fa5bf75e146ac8 Mon Sep 17 00:00:00 2001 From: Nayana Bidari Date: Thu, 18 Apr 2024 16:43:21 -0700 Subject: [PATCH] Retrieve UID/GID from the user string. Add a method to retrieve the UID and GID for a user and the tests to verify. PiperOrigin-RevId: 626187254 --- pkg/sentry/control/lifecycle.go | 35 ++++++--- pkg/sentry/fsimpl/user/user.go | 113 ++++++++++++++++++++++++++++ pkg/sentry/fsimpl/user/user_test.go | 105 ++++++++++++++++++++++++++ 3 files changed, 244 insertions(+), 9 deletions(-) diff --git a/pkg/sentry/control/lifecycle.go b/pkg/sentry/control/lifecycle.go index 214b95d8e..f96b69f02 100644 --- a/pkg/sentry/control/lifecycle.go +++ b/pkg/sentry/control/lifecycle.go @@ -111,6 +111,9 @@ type StartContainerArgs struct { // the root group if not set explicitly. KGID auth.KGID `json:"KGID"` + // User is the user string used to retrieve UID/GID. + User string `json:"user"` + // ContainerID is the container for the process being executed. ContainerID string `json:"container_id"` @@ -198,9 +201,30 @@ func (l *Lifecycle) StartContainer(args *StartContainerArgs, _ *uint32) error { return fmt.Errorf("FilePayload.Files and DonatedFDs must have same number of elements (%d != %d)", len(args.Files), len(args.DonatedFDs)) } + l.mu.RLock() + mntns, ok := l.MountNamespacesMap[args.ContainerID] + if !ok { + l.mu.RUnlock() + return fmt.Errorf("mount namespace is nil for %s", args.ContainerID) + } + l.mu.RUnlock() + + uid := args.KUID + gid := args.KGID + if args.User != "" { + if uid != 0 || gid != 0 { + return fmt.Errorf("container spec specified both an explicit UID/GID and a user name, only one or the other may be provided") + } + var err error + uid, gid, err = user.GetExecUIDGIDFromUser(l.Kernel.SupervisorContext(), mntns, args.User) + if err != nil { + return fmt.Errorf("couldn't retrieve UID and GID for user %v, err: %v", args.User, err) + } + } + creds := auth.NewUserCredentials( - args.KUID, - args.KGID, + uid, + gid, nil, /* extraKGIDs */ nil, /* capabilities */ l.Kernel.RootUserNamespace()) @@ -261,14 +285,7 @@ func (l *Lifecycle) StartContainer(args *StartContainerArgs, _ *uint32) error { } initArgs.FDTable = fdTable - l.mu.RLock() - mntns, ok := l.MountNamespacesMap[initArgs.ContainerID] - if !ok { - l.mu.RUnlock() - return fmt.Errorf("mount namespace is nil for %s", initArgs.ContainerID) - } initArgs.MountNamespace = mntns - l.mu.RUnlock() initArgs.MountNamespace.IncRef() if args.ResolveBinaryPath { diff --git a/pkg/sentry/fsimpl/user/user.go b/pkg/sentry/fsimpl/user/user.go index 2ada6b854..a16ca6046 100644 --- a/pkg/sentry/fsimpl/user/user.go +++ b/pkg/sentry/fsimpl/user/user.go @@ -161,3 +161,116 @@ func findHomeInPasswd(uid uint32, passwd io.Reader, defaultHome string) (string, return defaultHome, nil } + +func findUIDGIDInPasswd(passwd io.Reader, user string) (auth.KUID, auth.KGID, error) { + defaultUID := auth.KUID(auth.OverflowUID) + defaultGID := auth.KGID(auth.OverflowGID) + uid := defaultUID + gid := defaultGID + + s := bufio.NewScanner(passwd) + for s.Scan() { + if err := s.Err(); err != nil { + return defaultUID, defaultGID, err + } + + line := strings.TrimSpace(s.Text()) + if line == "" { + continue + } + + // Per 'man 5 passwd' + // /etc/passwd contains one line for each user account, with seven + // fields delimited by colons (“:”). These fields are: + // + // - login name + // - optional encrypted password + // - numerical user ID + // - numerical group ID + // - user name or comment field + // - user home directory + // - optional user command interpreter + const ( + numFields = 7 + userIdx = 0 + passwdIdx = 1 + uidIdx = 2 + gidIdx = 3 + shellIdx = 6 + ) + parts := strings.Split(line, ":") + if len(parts) != numFields { + // Return error if the format is invalid. + return defaultUID, defaultGID, fmt.Errorf("invalid line found in /etc/passwd") + } + for i := 0; i < numFields; i++ { + // The password and user command interpreter fields are + // optional, no need to check if they are empty. + if i == passwdIdx || i == shellIdx { + continue + } + if parts[i] == "" { + // Return error if the format is invalid. + return defaultUID, defaultGID, fmt.Errorf("invalid line found in /etc/passwd") + } + } + + if parts[userIdx] == user { + parseUID, err := strconv.ParseUint(parts[uidIdx], 10, 32) + if err != nil { + return defaultUID, defaultGID, err + } + parseGID, err := strconv.ParseUint(parts[gidIdx], 10, 32) + if err != nil { + return defaultUID, defaultGID, err + } + + if uid != defaultUID || gid != defaultGID { + return defaultUID, defaultGID, fmt.Errorf("multiple matches for the user: %v", user) + } + uid = auth.KUID(parseUID) + gid = auth.KGID(parseGID) + } + } + if uid == defaultUID || gid == defaultGID { + return defaultUID, defaultGID, fmt.Errorf("couldn't retrieve UID/GID from user: %v", user) + } + return uid, gid, nil +} + +func getExecUIDGID(ctx context.Context, mns *vfs.MountNamespace, user string) (auth.KUID, auth.KGID, error) { + root := mns.Root(ctx) + defer root.DecRef(ctx) + + creds := auth.CredentialsFromContext(ctx) + + target := &vfs.PathOperation{ + Root: root, + Start: root, + Path: fspath.Parse("/etc/passwd"), + } + + fd, err := root.Mount().Filesystem().VirtualFilesystem().OpenAt(ctx, creds, target, &vfs.OpenOptions{Flags: linux.O_RDONLY}) + if err != nil { + return auth.KUID(auth.OverflowUID), auth.KGID(auth.OverflowGID), fmt.Errorf("couldn't retrieve UID/GID from user: %v, err: %v", user, err) + } + defer fd.DecRef(ctx) + + r := &fileReader{ + ctx: ctx, + fd: fd, + } + + return findUIDGIDInPasswd(r, user) +} + +// GetExecUIDGIDFromUser retrieves the UID and GID from /etc/passwd file for +// the given user. +func GetExecUIDGIDFromUser(ctx context.Context, vmns *vfs.MountNamespace, user string) (auth.KUID, auth.KGID, error) { + // Read /etc/passwd and retrieve the UID/GID based on the user string. + uid, gid, err := getExecUIDGID(ctx, vmns, user) + if err != nil { + return uid, gid, fmt.Errorf("error reading /etc/passwd: %v", err) + } + return uid, gid, nil +} diff --git a/pkg/sentry/fsimpl/user/user_test.go b/pkg/sentry/fsimpl/user/user_test.go index 869929ae7..0a48a1bf1 100644 --- a/pkg/sentry/fsimpl/user/user_test.go +++ b/pkg/sentry/fsimpl/user/user_test.go @@ -209,3 +209,108 @@ func TestFindHomeInPasswd(t *testing.T) { }) } } + +// TestGetExecUIDGIDFromUser tests the GetExecUIDGIDFromUser function. +func TestGetExecUIDGIDFromUser(t *testing.T) { + tests := map[string]struct { + user string + passwdContents string + passwdMode linux.FileMode + expectedUID auth.KUID + expectedGID auth.KGID + }{ + "success": { + user: "user0", + passwdContents: "user0::1000:1111:&:/home/user0:/bin/sh", + passwdMode: linux.S_IFREG | 0666, + expectedUID: 1000, + expectedGID: 1111, + }, + "no_user": { + user: "user1", + passwdContents: "user0::1000:1111::/home/user0:/bin/sh", + passwdMode: linux.S_IFREG | 0666, + expectedUID: 65534, + expectedGID: 65534, + }, + "multiple_user_no_match": { + user: "user1", + passwdContents: "user0::1000:1111::/home/user0:/bin/sh\nuser2::1002:1112::/home/user2:/bin/sh\nuser3::1003:1113::/home/user3:/bin/sh", + passwdMode: linux.S_IFREG | 0666, + expectedUID: 65534, + expectedGID: 65534, + }, + "multiple_user_many_match": { + user: "user1", + passwdContents: "user0::1000:1111::/home/user0:/bin/sh\nuser1::1002:1112::/home/user1:/bin/sh\nuser1::1003:1113::/home/user1:/bin/sh", + passwdMode: linux.S_IFREG | 0666, + expectedUID: 65534, + expectedGID: 65534, + }, + "invalid_file": { + user: "user1", + passwdContents: "user0:1000:1111::/home/user0:/bin/sh\nuser1::1001:1111::/home/user1:/bin/sh\nuser2::/home/user2:/bin/sh", + passwdMode: linux.S_IFREG | 0666, + expectedUID: 65534, + expectedGID: 65534, + }, + "empty_file": { + user: "user1", + passwdContents: "", + passwdMode: linux.S_IFREG | 0666, + expectedUID: 65534, + expectedGID: 65534, + }, + "empty_user": { + user: "", + passwdContents: "user0::1000:1111::/home/user0:/bin/sh\nuser2::1002:1112::/home/user2:/bin/sh\nuser3::1003:1113::/home/user3:/bin/sh", + passwdMode: linux.S_IFREG | 0666, + expectedUID: 65534, + expectedGID: 65534, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + ctx := contexttest.Context(t) + creds := auth.CredentialsFromContext(ctx) + + // Create VFS. + vfsObj := vfs.VirtualFilesystem{} + if err := vfsObj.Init(ctx); err != nil { + t.Fatalf("VFS init: %v", err) + } + vfsObj.MustRegisterFilesystemType("tmpfs", tmpfs.FilesystemType{}, &vfs.RegisterFilesystemTypeOptions{ + AllowUserMount: true, + }) + mns, err := vfsObj.NewMountNamespace(ctx, creds, "", "tmpfs", &vfs.MountOptions{}, nil) + if err != nil { + t.Fatalf("failed to create tmpfs root mount: %v", err) + } + defer mns.DecRef(ctx) + root := mns.Root(ctx) + defer root.DecRef(ctx) + + if err := createEtcPasswd(ctx, &vfsObj, creds, root, tc.passwdContents, tc.passwdMode); err != nil { + t.Fatalf("createEtcPasswd failed: %v", err) + } + + gotUID, gotGID, err := GetExecUIDGIDFromUser(ctx, mns, tc.user) + if name == "success" { + if err != nil { + t.Fatalf("failed to get UID and GID from user: %v %v", tc.user, err) + } + } else { + if err == nil { + t.Fatalf("retrieved UID and GID when user %v is not in /etc/passwd: %v", tc.user, err) + } + } + if gotUID != tc.expectedUID { + t.Fatalf("expectedUID %v, gotUID: %v", tc.expectedUID, gotUID) + } + if gotGID != tc.expectedGID { + t.Fatalf("expectedGID %v, gotGID: %v", tc.expectedGID, gotGID) + } + }) + } +}