diff options
author | Ian Lewis <ianlewis@google.com> | 2019-06-13 15:44:17 -0700 |
---|---|---|
committer | Shentubot <shentubot@google.com> | 2019-06-13 15:45:25 -0700 |
commit | 4fdd560b76dfe4e3df83a8cba5a070ce7142b433 (patch) | |
tree | 81c8096435d17ccf47659ad1c6cb2effb5271720 | |
parent | 9f77b36fa100761eb3eabbb87f5111419202a9d5 (diff) |
Set the HOME environment variable (fixes #293)
runsc will now set the HOME environment variable as required by POSIX. The
user's home directory is retrieved from the /etc/passwd file located on the
container's file system during boot.
PiperOrigin-RevId: 253120627
-rw-r--r-- | runsc/boot/BUILD | 3 | ||||
-rw-r--r-- | runsc/boot/loader.go | 19 | ||||
-rw-r--r-- | runsc/boot/user.go | 146 | ||||
-rw-r--r-- | runsc/boot/user_test.go | 253 |
4 files changed, 421 insertions, 0 deletions
diff --git a/runsc/boot/BUILD b/runsc/boot/BUILD index 744f852a1..0d8b74871 100644 --- a/runsc/boot/BUILD +++ b/runsc/boot/BUILD @@ -18,6 +18,7 @@ go_library( "network.go", "pprof.go", "strace.go", + "user.go", ], importpath = "gvisor.googlesource.com/gvisor/runsc/boot", visibility = [ @@ -69,6 +70,7 @@ go_library( "//pkg/sentry/time", "//pkg/sentry/unimpl:unimplemented_syscall_go_proto", "//pkg/sentry/usage", + "//pkg/sentry/usermem", "//pkg/sentry/watchdog", "//pkg/syserror", "//pkg/tcpip", @@ -97,6 +99,7 @@ go_test( "compat_test.go", "fs_test.go", "loader_test.go", + "user_test.go", ], embed = [":boot"], deps = [ diff --git a/runsc/boot/loader.go b/runsc/boot/loader.go index c1dea736f..ff7009d30 100644 --- a/runsc/boot/loader.go +++ b/runsc/boot/loader.go @@ -20,6 +20,7 @@ import ( mrand "math/rand" "os" "runtime" + "strings" "sync" "sync/atomic" "syscall" @@ -534,6 +535,24 @@ func (l *Loader) run() error { return err } + // Read /etc/passwd for the user's HOME directory and set the HOME + // environment variable as required by POSIX if it is not overridden by + // the user. + hasHomeEnvv := false + for _, envv := range l.rootProcArgs.Envv { + if strings.HasPrefix(envv, "HOME=") { + hasHomeEnvv = true + } + } + if !hasHomeEnvv { + homeDir, err := getExecUserHome(rootCtx, rootMns, uint32(l.rootProcArgs.Credentials.RealKUID)) + if err != nil { + return fmt.Errorf("error reading exec user: %v", err) + } + + l.rootProcArgs.Envv = append(l.rootProcArgs.Envv, "HOME="+homeDir) + } + // Create the root container init task. It will begin running // when the kernel is started. if _, _, err := l.k.CreateProcess(l.rootProcArgs); err != nil { diff --git a/runsc/boot/user.go b/runsc/boot/user.go new file mode 100644 index 000000000..f3de8ccff --- /dev/null +++ b/runsc/boot/user.go @@ -0,0 +1,146 @@ +// Copyright 2019 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package boot + +import ( + "bufio" + "io" + "strconv" + "strings" + + "gvisor.googlesource.com/gvisor/pkg/abi/linux" + "gvisor.googlesource.com/gvisor/pkg/sentry/context" + "gvisor.googlesource.com/gvisor/pkg/sentry/fs" + "gvisor.googlesource.com/gvisor/pkg/sentry/usermem" +) + +type fileReader struct { + // Ctx is the context for the file reader. + Ctx context.Context + + // File is the file to read from. + File *fs.File +} + +// Read implements io.Reader.Read. +func (r *fileReader) Read(buf []byte) (int, error) { + n, err := r.File.Readv(r.Ctx, usermem.BytesIOSequence(buf)) + return int(n), err +} + +// getExecUserHome returns the home directory of the executing user read from +// /etc/passwd as read from the container filesystem. +func getExecUserHome(ctx context.Context, rootMns *fs.MountNamespace, uid uint32) (string, error) { + // The default user home directory to return if no user matching the user + // if found in the /etc/passwd found in the image. + const defaultHome = "/" + + // Open the /etc/passwd file from the dirent via the root mount namespace. + mnsRoot := rootMns.Root() + maxTraversals := uint(linux.MaxSymlinkTraversals) + dirent, err := rootMns.FindInode(ctx, mnsRoot, nil, "/etc/passwd", &maxTraversals) + if err != nil { + // NOTE: Ignore errors opening the passwd file. If the passwd file + // doesn't exist we will return the default home directory. + return defaultHome, nil + } + defer dirent.DecRef() + + // Check read permissions on the file. + if err := dirent.Inode.CheckPermission(ctx, fs.PermMask{Read: true}); err != nil { + // NOTE: Ignore permissions errors here and return default root dir. + return defaultHome, nil + } + + // Only open regular files. We don't open other files like named pipes as + // they may block and might present some attack surface to the container. + // Note that runc does not seem to do this kind of checking. + if !fs.IsRegular(dirent.Inode.StableAttr) { + return defaultHome, nil + } + + f, err := dirent.Inode.GetFile(ctx, dirent, fs.FileFlags{Read: true, Directory: false}) + if err != nil { + return "", err + } + defer f.DecRef() + + r := &fileReader{ + Ctx: ctx, + File: f, + } + + homeDir, err := findHomeInPasswd(uid, r, defaultHome) + if err != nil { + return "", err + } + + return homeDir, nil +} + +// findHomeInPasswd parses a passwd file and returns the given user's home +// directory. This function does it's best to replicate the runc's behavior. +func findHomeInPasswd(uid uint32, passwd io.Reader, defaultHome string) (string, error) { + s := bufio.NewScanner(passwd) + + for s.Scan() { + if err := s.Err(); err != nil { + return "", err + } + + line := strings.TrimSpace(s.Text()) + if line == "" { + continue + } + + // Pull out part of passwd entry. Loosely parse the passwd entry as some + // passwd files could be poorly written and for compatibility with runc. + // + // 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 + parts := strings.Split(line, ":") + + found := false + homeDir := "" + for i, p := range parts { + switch i { + case 2: + parsedUID, err := strconv.ParseUint(p, 10, 32) + if err == nil && parsedUID == uint64(uid) { + found = true + } + case 5: + homeDir = p + } + } + if found { + // NOTE: If the uid is present but the home directory is not + // present in the /etc/passwd entry we return an empty string. This + // is, for better or worse, what runc does. + return homeDir, nil + } + } + + return defaultHome, nil +} diff --git a/runsc/boot/user_test.go b/runsc/boot/user_test.go new file mode 100644 index 000000000..e91be5f0f --- /dev/null +++ b/runsc/boot/user_test.go @@ -0,0 +1,253 @@ +// Copyright 2019 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package boot + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + "syscall" + "testing" + + specs "github.com/opencontainers/runtime-spec/specs-go" + "gvisor.googlesource.com/gvisor/pkg/sentry/context/contexttest" + "gvisor.googlesource.com/gvisor/pkg/sentry/fs" +) + +func setupTempDir() (string, error) { + tmpDir, err := ioutil.TempDir(os.TempDir(), "exec-user-test") + if err != nil { + return "", err + } + return tmpDir, nil +} + +func setupPasswd(contents string, perms os.FileMode) func() (string, error) { + return func() (string, error) { + tmpDir, err := setupTempDir() + if err != nil { + return "", err + } + + if err := os.Mkdir(filepath.Join(tmpDir, "etc"), 0777); err != nil { + return "", err + } + + f, err := os.Create(filepath.Join(tmpDir, "etc", "passwd")) + if err != nil { + return "", err + } + defer f.Close() + + _, err = f.WriteString(contents) + if err != nil { + return "", err + } + + err = f.Chmod(perms) + if err != nil { + return "", err + } + return tmpDir, nil + } +} + +// TestGetExecUserHome tests the getExecUserHome function. +func TestGetExecUserHome(t *testing.T) { + tests := map[string]struct { + uid uint32 + createRoot func() (string, error) + expected string + }{ + "success": { + uid: 1000, + createRoot: setupPasswd("adin::1000:1111::/home/adin:/bin/sh", 0666), + expected: "/home/adin", + }, + "no_passwd": { + uid: 1000, + createRoot: setupTempDir, + expected: "/", + }, + "no_perms": { + uid: 1000, + createRoot: setupPasswd("adin::1000:1111::/home/adin:/bin/sh", 0000), + expected: "/", + }, + "directory": { + uid: 1000, + createRoot: func() (string, error) { + tmpDir, err := setupTempDir() + if err != nil { + return "", err + } + + if err := os.Mkdir(filepath.Join(tmpDir, "etc"), 0777); err != nil { + return "", err + } + + if err := syscall.Mkdir(filepath.Join(tmpDir, "etc", "passwd"), 0666); err != nil { + return "", err + } + + return tmpDir, nil + }, + expected: "/", + }, + // Currently we don't allow named pipes. + "named_pipe": { + uid: 1000, + createRoot: func() (string, error) { + tmpDir, err := setupTempDir() + if err != nil { + return "", err + } + + if err := os.Mkdir(filepath.Join(tmpDir, "etc"), 0777); err != nil { + return "", err + } + + if err := syscall.Mkfifo(filepath.Join(tmpDir, "etc", "passwd"), 0666); err != nil { + return "", err + } + + return tmpDir, nil + }, + expected: "/", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + tmpDir, err := tc.createRoot() + if err != nil { + t.Fatalf("failed to create root dir: %v", err) + } + + sandEnd, cleanup, err := startGofer(tmpDir) + if err != nil { + t.Fatalf("failed to create gofer: %v", err) + } + defer cleanup() + + ctx := contexttest.Context(t) + conf := &Config{ + RootDir: "unused_root_dir", + Network: NetworkNone, + DisableSeccomp: true, + } + + spec := &specs.Spec{ + Root: &specs.Root{ + Path: tmpDir, + Readonly: true, + }, + // Add /proc mount as tmpfs to avoid needing a kernel. + Mounts: []specs.Mount{ + { + Destination: "/proc", + Type: "tmpfs", + }, + }, + } + + var mns *fs.MountNamespace + setMountNS := func(m *fs.MountNamespace) { + mns = m + ctx.(*contexttest.TestContext).RegisterValue(fs.CtxRoot, mns.Root()) + } + mntr := newContainerMounter(spec, "", []int{sandEnd}, nil, &podMountHints{}) + if err := mntr.setupRootContainer(ctx, ctx, conf, setMountNS); err != nil { + t.Fatalf("failed to create mount namespace: %v", err) + } + + got, err := getExecUserHome(ctx, mns, tc.uid) + if err != nil { + t.Fatalf("failed to get user home: %v", err) + } + + if got != tc.expected { + t.Fatalf("expected %v, got: %v", tc.expected, got) + } + }) + } +} + +// TestFindHomeInPasswd tests the findHomeInPasswd function's passwd file parsing. +func TestFindHomeInPasswd(t *testing.T) { + tests := map[string]struct { + uid uint32 + passwd string + expected string + def string + }{ + "empty": { + uid: 1000, + passwd: "", + expected: "/", + def: "/", + }, + "whitespace": { + uid: 1000, + passwd: " ", + expected: "/", + def: "/", + }, + "full": { + uid: 1000, + passwd: "adin::1000:1111::/home/adin:/bin/sh", + expected: "/home/adin", + def: "/", + }, + // For better or worse, this is how runc works. + "partial": { + uid: 1000, + passwd: "adin::1000:1111:", + expected: "", + def: "/", + }, + "multiple": { + uid: 1001, + passwd: "adin::1000:1111::/home/adin:/bin/sh\nian::1001:1111::/home/ian:/bin/sh", + expected: "/home/ian", + def: "/", + }, + "duplicate": { + uid: 1000, + passwd: "adin::1000:1111::/home/adin:/bin/sh\nian::1000:1111::/home/ian:/bin/sh", + expected: "/home/adin", + def: "/", + }, + "empty_lines": { + uid: 1001, + passwd: "adin::1000:1111::/home/adin:/bin/sh\n\n\nian::1001:1111::/home/ian:/bin/sh", + expected: "/home/ian", + def: "/", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + got, err := findHomeInPasswd(tc.uid, strings.NewReader(tc.passwd), tc.def) + if err != nil { + t.Fatalf("error parsing passwd: %v", err) + } + if tc.expected != got { + t.Fatalf("expected %v, got: %v", tc.expected, got) + } + }) + } +} |