mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
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:
@@ -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 = [
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user