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
This commit is contained in:
Ian Lewis
2019-06-13 15:45:25 -07:00
committed by Shentubot
parent 9f77b36fa1
commit 4fdd560b76
4 changed files with 421 additions and 0 deletions
+3
View File
@@ -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 = [
+19
View File
@@ -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 {
+146
View File
@@ -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
}
+253
View File
@@ -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)
}
})
}
}