Skip to content

Commit 95e76a7

Browse files
committed
libct: do not parse passwd and group on every run/exec
OCI runtime spec states [1] that the UID, primary GID, and additional GIDs are all specified as numbers, and also adds that symbolic names resolution "are left to upper levels to derive". Meaning, runc should not care about user and group names. Yet, runc tries to be clever than that, always parsing container's /etc/passwd and /etc/group. It results in a few things: 1. If UID (or GID) specified can't be found inside container's /etc/passwd (or /etc/group), runc (run or exec) errors out. 2. Any additional GIDs specified in container's /etc/group are automatically prepended to the list for setgroups(2). Meaning, a user can either specify additional GIDs in OCI runtime spec, or container's /etc/group entry for a given user. Looks like (1) is questionable (on a normal Linux system, I can run programs under any UID (GID), not limited to those listed in /etc/passwd (/etc/group), and (2) is just an extra mechanism of specifying additional GIDs. Let's remove those, hopefully increasing runc performance as well as OCI spec conformance. The only remaining need to parse /etc/passwd is to set HOME environment variable for a specified UID, in case $HOME is not yet set. Use user.LookupUid for this case. PS Note that the structures being changed (initConfig and Process) are never saved to disk as JSON by runc, so there is no compatibility issue for runc users. Still, this is a breaking change in libcontainer, but we never promised that libcontainer API will be stable. For 3998. [1] https://github.com/opencontainers/runtime-spec/blob/v1.0.2/config.md#posix-platform-user Signed-off-by: Kir Kolyshkin <[email protected]>
1 parent b166280 commit 95e76a7

File tree

7 files changed

+55
-80
lines changed

7 files changed

+55
-80
lines changed

libcontainer/container_linux.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -823,7 +823,8 @@ func (c *Container) newInitConfig(process *Process) *initConfig {
823823
Config: c.config,
824824
Args: process.Args,
825825
Env: process.Env,
826-
User: process.User,
826+
UID: process.UID,
827+
GID: process.GID,
827828
AdditionalGroups: process.AdditionalGroups,
828829
Cwd: process.Cwd,
829830
Capabilities: process.Capabilities,

libcontainer/init_linux.go

Lines changed: 27 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,9 @@ type initConfig struct {
6868
ProcessLabel string `json:"process_label"`
6969
AppArmorProfile string `json:"apparmor_profile"`
7070
NoNewPrivileges bool `json:"no_new_privileges"`
71-
User string `json:"user"`
72-
AdditionalGroups []string `json:"additional_groups"`
71+
UID int `json:"uid"`
72+
GID int `json:"gid"`
73+
AdditionalGroups []int `json:"additional_groups"`
7374
Config *configs.Config `json:"config"`
7475
Networks []*network `json:"network"`
7576
PassedFilesCount int `json:"passed_files_count"`
@@ -440,60 +441,42 @@ func syncParentSeccomp(pipe *os.File, seccompFd *os.File) error {
440441
return readSync(pipe, procSeccompDone)
441442
}
442443

443-
// setupUser changes the groups, gid, and uid for the user inside the container
444-
func setupUser(config *initConfig) error {
445-
// Set up defaults.
446-
defaultExecUser := user.ExecUser{
447-
Uid: 0,
448-
Gid: 0,
449-
Home: "/",
450-
}
451-
452-
passwdPath, err := user.GetPasswdPath()
453-
if err != nil {
454-
return err
455-
}
456-
457-
groupPath, err := user.GetGroupPath()
458-
if err != nil {
459-
return err
460-
}
461-
462-
execUser, err := user.GetExecUserPath(config.User, &defaultExecUser, passwdPath, groupPath)
463-
if err != nil {
464-
return err
444+
// setHome tries to guess $HOME based on a user's /etc/passwd entry.
445+
// If $HOME is already set, it does nothing.
446+
func setHome(uid int) error {
447+
if _, ok := os.LookupEnv("HOME"); ok {
448+
return nil
465449
}
466-
467-
var addGroups []int
468-
if len(config.AdditionalGroups) > 0 {
469-
addGroups, err = user.GetAdditionalGroupsPath(config.AdditionalGroups, groupPath)
470-
if err != nil {
450+
if u, err := user.LookupUid(uid); err == nil {
451+
if err := os.Setenv("HOME", u.Home); err != nil {
471452
return err
472453
}
473454
}
455+
return nil
456+
}
474457

458+
// setupUser changes the groups, gid, and uid for the user inside the container
459+
func setupUser(config *initConfig) error {
475460
// Rather than just erroring out later in setuid(2) and setgid(2), check
476461
// that the user is mapped here.
477-
if _, err := config.Config.HostUID(execUser.Uid); err != nil {
462+
if _, err := config.Config.HostUID(config.UID); err != nil {
478463
return errors.New("cannot set uid to unmapped user in user namespace")
479464
}
480-
if _, err := config.Config.HostGID(execUser.Gid); err != nil {
465+
if _, err := config.Config.HostGID(config.GID); err != nil {
481466
return errors.New("cannot set gid to unmapped user in user namespace")
482467
}
483468

484-
if config.RootlessEUID {
469+
if config.RootlessEUID && len(config.AdditionalGroups) > 0 {
485470
// We cannot set any additional groups in a rootless container and thus
486471
// we bail if the user asked us to do so. TODO: We currently can't do
487472
// this check earlier, but if libcontainer.Process.User was typesafe
488473
// this might work.
489-
if len(addGroups) > 0 {
490-
return errors.New("cannot set any additional groups in a rootless container")
491-
}
474+
return errors.New("cannot set any additional groups in a rootless container")
492475
}
493476

494477
// Before we change to the container's user make sure that the processes
495478
// STDIO is correctly owned by the user that we are switching to.
496-
if err := fixStdioPermissions(execUser); err != nil {
479+
if err := fixStdioPermissions(config.UID); err != nil {
497480
return err
498481
}
499482

@@ -509,32 +492,24 @@ func setupUser(config *initConfig) error {
509492
allowSupGroups := !config.RootlessEUID && string(bytes.TrimSpace(setgroups)) != "deny"
510493

511494
if allowSupGroups {
512-
suppGroups := append(execUser.Sgids, addGroups...)
513-
if err := unix.Setgroups(suppGroups); err != nil {
495+
if err := unix.Setgroups(config.AdditionalGroups); err != nil {
514496
return &os.SyscallError{Syscall: "setgroups", Err: err}
515497
}
516498
}
517499

518-
if err := system.Setgid(execUser.Gid); err != nil {
500+
if err := system.Setgid(config.GID); err != nil {
519501
return err
520502
}
521-
if err := system.Setuid(execUser.Uid); err != nil {
503+
if err := system.Setuid(config.UID); err != nil {
522504
return err
523505
}
524-
525-
// if we didn't get HOME already, set it based on the user's HOME
526-
if envHome := os.Getenv("HOME"); envHome == "" {
527-
if err := os.Setenv("HOME", execUser.Home); err != nil {
528-
return err
529-
}
530-
}
531-
return nil
506+
return setHome(config.UID)
532507
}
533508

534-
// fixStdioPermissions fixes the permissions of PID 1's STDIO within the container to the specified user.
509+
// fixStdioPermissions fixes the permissions of PID 1's STDIO within the container to the specified uid.
535510
// The ownership needs to match because it is created outside of the container and needs to be
536511
// localized.
537-
func fixStdioPermissions(u *user.ExecUser) error {
512+
func fixStdioPermissions(uid int) error {
538513
var null unix.Stat_t
539514
if err := unix.Stat("/dev/null", &null); err != nil {
540515
return &os.PathError{Op: "stat", Path: "/dev/null", Err: err}
@@ -547,7 +522,7 @@ func fixStdioPermissions(u *user.ExecUser) error {
547522

548523
// Skip chown if uid is already the one we want or any of the STDIO descriptors
549524
// were redirected to /dev/null.
550-
if int(s.Uid) == u.Uid || s.Rdev == null.Rdev {
525+
if int(s.Uid) == uid || s.Rdev == null.Rdev {
551526
continue
552527
}
553528

@@ -557,7 +532,7 @@ func fixStdioPermissions(u *user.ExecUser) error {
557532
// that users expect to be able to actually use their console. Without
558533
// this code, you couldn't effectively run as a non-root user inside a
559534
// container and also have a console set up.
560-
if err := file.Chown(u.Uid, int(s.Gid)); err != nil {
535+
if err := file.Chown(int(uid), int(s.Gid)); err != nil {
561536
// If we've hit an EINVAL then s.Gid isn't mapped in the user
562537
// namespace. If we've hit an EPERM then the inode's current owner
563538
// 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
@@ -395,7 +395,7 @@ func TestAdditionalGroups(t *testing.T) {
395395
Env: standardEnvironment,
396396
Stdin: nil,
397397
Stdout: &stdout,
398-
AdditionalGroups: []string{"plugdev", "audio"},
398+
AdditionalGroups: []int{3333, 99999},
399399
Init: true,
400400
}
401401
err = container.Run(&pconfig)
@@ -406,13 +406,11 @@ func TestAdditionalGroups(t *testing.T) {
406406

407407
outputGroups := stdout.String()
408408

409-
// Check that the groups output has the groups that we specified
410-
if !strings.Contains(outputGroups, "audio") {
411-
t.Fatalf("Listed groups do not contain the audio group as expected: %v", outputGroups)
412-
}
413-
414-
if !strings.Contains(outputGroups, "plugdev") {
415-
t.Fatalf("Listed groups do not contain the plugdev group as expected: %v", outputGroups)
409+
// Check that the groups output has the groups that we specified.
410+
for _, gid := range pconfig.AdditionalGroups {
411+
if !strings.Contains(outputGroups, strconv.Itoa(gid)) {
412+
t.Errorf("Listed groups do not contain gid %d as expected: %v", gid, outputGroups)
413+
}
416414
}
417415
}
418416

libcontainer/integration/execin_test.go

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ func TestExecInAdditionalGroups(t *testing.T) {
162162
Env: standardEnvironment,
163163
Stdin: nil,
164164
Stdout: &stdout,
165-
AdditionalGroups: []string{"plugdev", "audio"},
165+
AdditionalGroups: []int{4444, 87654},
166166
}
167167
err = container.Run(&pconfig)
168168
ok(t, err)
@@ -175,13 +175,11 @@ func TestExecInAdditionalGroups(t *testing.T) {
175175

176176
outputGroups := stdout.String()
177177

178-
// Check that the groups output has the groups that we specified
179-
if !strings.Contains(outputGroups, "audio") {
180-
t.Fatalf("Listed groups do not contain the audio group as expected: %v", outputGroups)
181-
}
182-
183-
if !strings.Contains(outputGroups, "plugdev") {
184-
t.Fatalf("Listed groups do not contain the plugdev group as expected: %v", outputGroups)
178+
// Check that the groups output has the groups that we specified.
179+
for _, gid := range pconfig.AdditionalGroups {
180+
if !strings.Contains(outputGroups, strconv.Itoa(gid)) {
181+
t.Errorf("Listed groups do not contain gid %d as expected: %v", gid, outputGroups)
182+
}
185183
}
186184
}
187185

libcontainer/process.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ type Process struct {
2626
// Env specifies the environment variables for the process.
2727
Env []string
2828

29-
// User will set the uid and gid of the executing process running inside the container
29+
// UID and GID of the executing process running inside the container
3030
// local to the container's user and group configuration.
31-
User string
31+
UID, GID int
3232

3333
// AdditionalGroups specifies the gids that should be added to supplementary groups
3434
// in addition to those that the user belongs to.
35-
AdditionalGroups []string
35+
AdditionalGroups []int
3636

3737
// Cwd will change the processes current working directory inside the container's rootfs.
3838
Cwd string

libcontainer/system/syscall_linux_32.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
)
1010

1111
// Setuid sets the uid of the calling thread to the specified uid.
12-
func Setuid(uid int) (err error) {
12+
func Setuid(uid uint32) (err error) {
1313
_, _, e1 := unix.RawSyscall(unix.SYS_SETUID32, uintptr(uid), 0, 0)
1414
if e1 != 0 {
1515
err = e1
@@ -18,7 +18,7 @@ func Setuid(uid int) (err error) {
1818
}
1919

2020
// Setgid sets the gid of the calling thread to the specified gid.
21-
func Setgid(gid int) (err error) {
21+
func Setgid(gid uint32) (err error) {
2222
_, _, e1 := unix.RawSyscall(unix.SYS_SETGID32, uintptr(gid), 0, 0)
2323
if e1 != 0 {
2424
err = e1

utils_linux.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,10 @@ func getDefaultImagePath() string {
4646
// spec and stdio from the current process.
4747
func newProcess(p specs.Process) (*libcontainer.Process, error) {
4848
lp := &libcontainer.Process{
49-
Args: p.Args,
50-
Env: p.Env,
51-
// TODO: fix libcontainer's API to better support uid/gid in a typesafe way.
52-
User: fmt.Sprintf("%d:%d", p.User.UID, p.User.GID),
49+
Args: p.Args,
50+
Env: p.Env,
51+
UID: int(p.User.UID),
52+
GID: int(p.User.GID),
5353
Cwd: p.Cwd,
5454
Label: p.SelinuxLabel,
5555
NoNewPrivileges: &p.NoNewPrivileges,
@@ -69,8 +69,11 @@ func newProcess(p specs.Process) (*libcontainer.Process, error) {
6969
lp.Capabilities.Permitted = p.Capabilities.Permitted
7070
lp.Capabilities.Ambient = p.Capabilities.Ambient
7171
}
72-
for _, gid := range p.User.AdditionalGids {
73-
lp.AdditionalGroups = append(lp.AdditionalGroups, strconv.FormatUint(uint64(gid), 10))
72+
if l := len(p.User.AdditionalGids); l > 0 {
73+
lp.AdditionalGroups = make([]int, l)
74+
for i, g := range p.User.AdditionalGids {
75+
lp.AdditionalGroups[i] = int(g)
76+
}
7477
}
7578
for _, rlimit := range p.Rlimits {
7679
rl, err := createLibContainerRlimit(rlimit)

0 commit comments

Comments
 (0)