Skip to content

Commit a509935

Browse files
authored
Merge pull request #3999 from kolyshkin/uidgid
Remove /etc/passwd and /etc/group parsing on runc run/exec
2 parents a5bfdc9 + ec9b0b5 commit a509935

File tree

13 files changed

+136
-118
lines changed

13 files changed

+136
-118
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
### libcontainer API
1010
* `configs.CommandHook` struct has changed, Command is now a pointer.
1111
Also, `configs.NewCommandHook` now accepts a `*Command`. (#4325)
12+
* The `Process` struct has `User` string field replaced with numeric
13+
`UID` and `GID` fields, and `AdditionalGroups` changed its type from
14+
`[]string` to `[]int`. Essentially, resolution of user and group
15+
names to IDs is no longer performed by libcontainer, so if a libcontainer
16+
user previously relied on this feature, now they have to convert names to
17+
IDs before calling libcontainer; it is recommended to use Go package
18+
github.com/moby/sys/user for that. (#3999)
1219

1320
## [1.2.0] - 2024-10-22
1421

libcontainer/container_linux.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,13 @@ func (c *Container) start(process *Process) (retErr error) {
302302
if c.config.Cgroups.Resources.SkipDevices {
303303
return errors.New("can't start container with SkipDevices set")
304304
}
305+
306+
if c.config.RootlessEUID && len(process.AdditionalGroups) > 0 {
307+
// We cannot set any additional groups in a rootless container
308+
// and thus we bail if the user asked us to do so.
309+
return errors.New("cannot set any additional groups in a rootless container")
310+
}
311+
305312
if process.Init {
306313
if c.initProcessStartTime != 0 {
307314
return errors.New("container already has init process")
@@ -686,7 +693,8 @@ func (c *Container) newInitConfig(process *Process) *initConfig {
686693
Config: c.config,
687694
Args: process.Args,
688695
Env: process.Env,
689-
User: process.User,
696+
UID: process.UID,
697+
GID: process.GID,
690698
AdditionalGroups: process.AdditionalGroups,
691699
Cwd: process.Cwd,
692700
Capabilities: process.Capabilities,

libcontainer/env.go

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,23 @@ import (
66
"os"
77
"slices"
88
"strings"
9+
10+
"github.com/moby/sys/user"
11+
"github.com/sirupsen/logrus"
912
)
1013

1114
// prepareEnv processes a list of environment variables, preparing it
1215
// for direct consumption by unix.Exec. In particular, it:
1316
// - validates each variable is in the NAME=VALUE format and
1417
// contains no \0 (nil) bytes;
1518
// - removes any duplicates (keeping only the last value for each key)
16-
// - sets PATH for the current process, if found in the list.
19+
// - sets PATH for the current process, if found in the list;
20+
// - adds HOME to returned environment, if not found in the list.
1721
//
18-
// It returns the deduplicated environment, a flag telling whether HOME
19-
// is present in the input, and an error.
20-
func prepareEnv(env []string) ([]string, bool, error) {
22+
// Returns the prepared environment.
23+
func prepareEnv(env []string, uid int) ([]string, error) {
2124
if env == nil {
22-
return nil, false, nil
25+
return nil, nil
2326
}
2427
// Deduplication code based on dedupEnv from Go 1.22 os/exec.
2528

@@ -31,29 +34,55 @@ func prepareEnv(env []string) ([]string, bool, error) {
3134
kv := env[n-1]
3235
i := strings.IndexByte(kv, '=')
3336
if i == -1 {
34-
return nil, false, errors.New("invalid environment variable: missing '='")
37+
return nil, errors.New("invalid environment variable: missing '='")
3538
}
3639
if i == 0 {
37-
return nil, false, errors.New("invalid environment variable: name cannot be empty")
40+
return nil, errors.New("invalid environment variable: name cannot be empty")
3841
}
3942
key := kv[:i]
4043
if saw[key] { // Duplicate.
4144
continue
4245
}
4346
saw[key] = true
4447
if strings.IndexByte(kv, 0) >= 0 {
45-
return nil, false, fmt.Errorf("invalid environment variable %q: contains nul byte (\\x00)", key)
48+
return nil, fmt.Errorf("invalid environment variable %q: contains nul byte (\\x00)", key)
4649
}
4750
if key == "PATH" {
4851
// Needs to be set as it is used for binary lookup.
4952
if err := os.Setenv("PATH", kv[i+1:]); err != nil {
50-
return nil, false, err
53+
return nil, err
5154
}
5255
}
5356
out = append(out, kv)
5457
}
5558
// Restore the original order.
5659
slices.Reverse(out)
5760

58-
return out, saw["HOME"], nil
61+
// If HOME is not found in env, get it from container's /etc/passwd and add.
62+
if !saw["HOME"] {
63+
home, err := getUserHome(uid)
64+
if err != nil {
65+
// For backward compatibility, don't return an error, but merely log it.
66+
logrus.WithError(err).Debugf("HOME not set in process.env, and getting UID %d homedir failed", uid)
67+
}
68+
69+
out = append(out, "HOME="+home)
70+
}
71+
72+
return out, nil
73+
}
74+
75+
func getUserHome(uid int) (string, error) {
76+
const defaultHome = "/" // Default value, return this with any error.
77+
78+
u, err := user.LookupUid(uid)
79+
if err != nil {
80+
// ErrNoPasswdEntries is kinda expected as any UID can be specified.
81+
if errors.Is(err, user.ErrNoPasswdEntries) {
82+
err = nil
83+
}
84+
return defaultHome, err
85+
}
86+
87+
return u.Home, nil
5988
}

libcontainer/env_test.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,37 @@
11
package libcontainer
22

33
import (
4+
"os/user"
45
"slices"
6+
"strconv"
57
"testing"
68
)
79

8-
func TestPrepareEnvDedup(t *testing.T) {
10+
func TestPrepareEnv(t *testing.T) {
11+
u, err := user.Current()
12+
if err != nil {
13+
t.Fatal(err)
14+
}
15+
home := "HOME=" + u.HomeDir
16+
uid, err := strconv.Atoi(u.Uid)
17+
if err != nil {
18+
t.Fatal(err)
19+
}
20+
921
tests := []struct {
1022
env, wantEnv []string
1123
}{
1224
{
1325
env: []string{},
14-
wantEnv: []string{},
26+
wantEnv: []string{home},
1527
},
1628
{
17-
env: []string{"HOME=/root", "FOO=bar"},
18-
wantEnv: []string{"HOME=/root", "FOO=bar"},
29+
env: []string{"HOME=/whoo", "FOO=bar"},
30+
wantEnv: []string{"HOME=/whoo", "FOO=bar"},
1931
},
2032
{
2133
env: []string{"A=a", "A=b", "A=c"},
22-
wantEnv: []string{"A=c"},
34+
wantEnv: []string{"A=c", home},
2335
},
2436
{
2537
env: []string{"TERM=vt100", "HOME=/home/one", "HOME=/home/two", "TERM=xterm", "HOME=/home/three", "FOO=bar"},
@@ -28,7 +40,7 @@ func TestPrepareEnvDedup(t *testing.T) {
2840
}
2941

3042
for _, tc := range tests {
31-
env, _, err := prepareEnv(tc.env)
43+
env, err := prepareEnv(tc.env, uid)
3244
if err != nil {
3345
t.Error(err)
3446
continue

libcontainer/init_linux.go

Lines changed: 18 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
"syscall"
1515

1616
"github.com/containerd/console"
17-
"github.com/moby/sys/user"
1817
"github.com/opencontainers/runtime-spec/specs-go"
1918
"github.com/sirupsen/logrus"
2019
"github.com/vishvananda/netlink"
@@ -57,8 +56,9 @@ type initConfig struct {
5756
ProcessLabel string `json:"process_label"`
5857
AppArmorProfile string `json:"apparmor_profile"`
5958
NoNewPrivileges bool `json:"no_new_privileges"`
60-
User string `json:"user"`
61-
AdditionalGroups []string `json:"additional_groups"`
59+
UID int `json:"uid"`
60+
GID int `json:"gid"`
61+
AdditionalGroups []int `json:"additional_groups"`
6262
Config *configs.Config `json:"config"`
6363
Networks []*network `json:"network"`
6464
PassedFilesCount int `json:"passed_files_count"`
@@ -208,7 +208,7 @@ func startInitialization() (retErr error) {
208208
}
209209

210210
func containerInit(t initType, config *initConfig, pipe *syncSocket, consoleSocket, pidfdSocket, fifoFile, logPipe *os.File) error {
211-
env, homeSet, err := prepareEnv(config.Env)
211+
env, err := prepareEnv(config.Env, config.UID)
212212
if err != nil {
213213
return err
214214
}
@@ -226,7 +226,6 @@ func containerInit(t initType, config *initConfig, pipe *syncSocket, consoleSock
226226
pidfdSocket: pidfdSocket,
227227
config: config,
228228
logPipe: logPipe,
229-
addHome: !homeSet,
230229
}
231230
return i.Init()
232231
case initStandard:
@@ -238,7 +237,6 @@ func containerInit(t initType, config *initConfig, pipe *syncSocket, consoleSock
238237
config: config,
239238
fifoFile: fifoFile,
240239
logPipe: logPipe,
241-
addHome: !homeSet,
242240
}
243241
return i.Init()
244242
}
@@ -274,7 +272,7 @@ func verifyCwd() error {
274272
// finalizeNamespace drops the caps, sets the correct user
275273
// and working dir, and closes any leaked file descriptors
276274
// before executing the command inside the namespace.
277-
func finalizeNamespace(config *initConfig, addHome bool) error {
275+
func finalizeNamespace(config *initConfig) error {
278276
// Ensure that all unwanted fds we may have accidentally
279277
// inherited are marked close-on-exec so they stay out of the
280278
// container
@@ -320,7 +318,7 @@ func finalizeNamespace(config *initConfig, addHome bool) error {
320318
if err := system.SetKeepCaps(); err != nil {
321319
return fmt.Errorf("unable to set keep caps: %w", err)
322320
}
323-
if err := setupUser(config, addHome); err != nil {
321+
if err := setupUser(config); err != nil {
324322
return fmt.Errorf("unable to setup user: %w", err)
325323
}
326324
// Change working directory AFTER the user has been set up, if we haven't done it yet.
@@ -438,52 +436,11 @@ func syncParentSeccomp(pipe *syncSocket, seccompFd int) error {
438436
return readSync(pipe, procSeccompDone)
439437
}
440438

441-
// setupUser changes the groups, gid, and uid for the user inside the container,
442-
// and appends user's HOME to config.Env if addHome is true.
443-
func setupUser(config *initConfig, addHome bool) error {
444-
// Set up defaults.
445-
defaultExecUser := user.ExecUser{
446-
Uid: 0,
447-
Gid: 0,
448-
Home: "/",
449-
}
450-
451-
passwdPath, err := user.GetPasswdPath()
452-
if err != nil {
453-
return err
454-
}
455-
456-
groupPath, err := user.GetGroupPath()
457-
if err != nil {
458-
return err
459-
}
460-
461-
execUser, err := user.GetExecUserPath(config.User, &defaultExecUser, passwdPath, groupPath)
462-
if err != nil {
463-
return err
464-
}
465-
466-
var addGroups []int
467-
if len(config.AdditionalGroups) > 0 {
468-
addGroups, err = user.GetAdditionalGroupsPath(config.AdditionalGroups, groupPath)
469-
if err != nil {
470-
return err
471-
}
472-
}
473-
474-
if config.RootlessEUID {
475-
// We cannot set any additional groups in a rootless container and thus
476-
// we bail if the user asked us to do so. TODO: We currently can't do
477-
// this check earlier, but if libcontainer.Process.User was typesafe
478-
// this might work.
479-
if len(addGroups) > 0 {
480-
return errors.New("cannot set any additional groups in a rootless container")
481-
}
482-
}
483-
439+
// setupUser changes the groups, gid, and uid for the user inside the container.
440+
func setupUser(config *initConfig) error {
484441
// Before we change to the container's user make sure that the processes
485442
// STDIO is correctly owned by the user that we are switching to.
486-
if err := fixStdioPermissions(execUser); err != nil {
443+
if err := fixStdioPermissions(config.UID); err != nil {
487444
return err
488445
}
489446

@@ -502,36 +459,30 @@ func setupUser(config *initConfig, addHome bool) error {
502459
allowSupGroups := !config.RootlessEUID && string(bytes.TrimSpace(setgroups)) != "deny"
503460

504461
if allowSupGroups {
505-
suppGroups := append(execUser.Sgids, addGroups...)
506-
if err := unix.Setgroups(suppGroups); err != nil {
462+
if err := unix.Setgroups(config.AdditionalGroups); err != nil {
507463
return &os.SyscallError{Syscall: "setgroups", Err: err}
508464
}
509465
}
510466

511-
if err := unix.Setgid(execUser.Gid); err != nil {
467+
if err := unix.Setgid(config.GID); err != nil {
512468
if err == unix.EINVAL {
513-
return fmt.Errorf("cannot setgid to unmapped gid %d in user namespace", execUser.Gid)
469+
return fmt.Errorf("cannot setgid to unmapped gid %d in user namespace", config.GID)
514470
}
515471
return err
516472
}
517-
if err := unix.Setuid(execUser.Uid); err != nil {
473+
if err := unix.Setuid(config.UID); err != nil {
518474
if err == unix.EINVAL {
519-
return fmt.Errorf("cannot setuid to unmapped uid %d in user namespace", execUser.Uid)
475+
return fmt.Errorf("cannot setuid to unmapped uid %d in user namespace", config.UID)
520476
}
521477
return err
522478
}
523-
524-
// If we didn't get HOME already, set it based on the user's HOME.
525-
if addHome {
526-
config.Env = append(config.Env, "HOME="+execUser.Home)
527-
}
528479
return nil
529480
}
530481

531-
// fixStdioPermissions fixes the permissions of PID 1's STDIO within the container to the specified user.
482+
// fixStdioPermissions fixes the permissions of PID 1's STDIO within the container to the specified uid.
532483
// The ownership needs to match because it is created outside of the container and needs to be
533484
// localized.
534-
func fixStdioPermissions(u *user.ExecUser) error {
485+
func fixStdioPermissions(uid int) error {
535486
var null unix.Stat_t
536487
if err := unix.Stat("/dev/null", &null); err != nil {
537488
return &os.PathError{Op: "stat", Path: "/dev/null", Err: err}
@@ -544,7 +495,7 @@ func fixStdioPermissions(u *user.ExecUser) error {
544495

545496
// Skip chown if uid is already the one we want or any of the STDIO descriptors
546497
// were redirected to /dev/null.
547-
if int(s.Uid) == u.Uid || s.Rdev == null.Rdev {
498+
if int(s.Uid) == uid || s.Rdev == null.Rdev {
548499
continue
549500
}
550501

@@ -554,7 +505,7 @@ func fixStdioPermissions(u *user.ExecUser) error {
554505
// that users expect to be able to actually use their console. Without
555506
// this code, you couldn't effectively run as a non-root user inside a
556507
// container and also have a console set up.
557-
if err := file.Chown(u.Uid, int(s.Gid)); err != nil {
508+
if err := file.Chown(uid, int(s.Gid)); err != nil {
558509
// If we've hit an EINVAL then s.Gid isn't mapped in the user
559510
// namespace. If we've hit an EPERM then the inode's current owner
560511
// is not mapped in our user namespace (in particular,

libcontainer/integration/exec_test.go

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ func TestAdditionalGroups(t *testing.T) {
399399
Env: standardEnvironment,
400400
Stdin: nil,
401401
Stdout: &stdout,
402-
AdditionalGroups: []string{"plugdev", "audio"},
402+
AdditionalGroups: []int{3333, 99999},
403403
Init: true,
404404
}
405405
err = container.Run(&pconfig)
@@ -410,13 +410,11 @@ func TestAdditionalGroups(t *testing.T) {
410410

411411
outputGroups := stdout.String()
412412

413-
// Check that the groups output has the groups that we specified
414-
if !strings.Contains(outputGroups, "audio") {
415-
t.Fatalf("Listed groups do not contain the audio group as expected: %v", outputGroups)
416-
}
417-
418-
if !strings.Contains(outputGroups, "plugdev") {
419-
t.Fatalf("Listed groups do not contain the plugdev group as expected: %v", outputGroups)
413+
// Check that the groups output has the groups that we specified.
414+
for _, gid := range pconfig.AdditionalGroups {
415+
if !strings.Contains(outputGroups, strconv.Itoa(gid)) {
416+
t.Errorf("Listed groups do not contain gid %d as expected: %v", gid, outputGroups)
417+
}
420418
}
421419
}
422420

0 commit comments

Comments
 (0)