Skip to content

Commit 4866cd2

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. With that, also remove most of libcontainer/user parsers. The only remaining need to parse /etc/passwd is to set HOME environment variable for a specified UID, in case it is not. For that, we can use standard os/user package, which has both libc-based and own ("pure Go") /etc/passwd parsers. 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. This is a breaking change in libcontainer, but we never promised that libcontainer API will be stable. [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 3fc35fc commit 4866cd2

File tree

12 files changed

+53
-1310
lines changed

12 files changed

+53
-1310
lines changed

libcontainer/container_linux.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -711,7 +711,8 @@ func (c *Container) newInitConfig(process *Process) *initConfig {
711711
Config: c.config,
712712
Args: process.Args,
713713
Env: process.Env,
714-
User: process.User,
714+
UID: process.UID,
715+
GID: process.GID,
715716
AdditionalGroups: process.AdditionalGroups,
716717
Cwd: process.Cwd,
717718
Capabilities: process.Capabilities,

libcontainer/init_linux.go

Lines changed: 23 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"net"
99
"os"
10+
"os/user"
1011
"runtime"
1112
"runtime/debug"
1213
"strconv"
@@ -22,7 +23,6 @@ import (
2223
"github.com/opencontainers/runc/libcontainer/cgroups"
2324
"github.com/opencontainers/runc/libcontainer/configs"
2425
"github.com/opencontainers/runc/libcontainer/system"
25-
"github.com/opencontainers/runc/libcontainer/user"
2626
"github.com/opencontainers/runc/libcontainer/utils"
2727
)
2828

@@ -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 uint32 `json:"uid"`
72+
GID uint32 `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"`
@@ -428,58 +429,26 @@ func syncParentSeccomp(pipe *os.File, seccompFd *os.File) error {
428429

429430
// setupUser changes the groups, gid, and uid for the user inside the container
430431
func setupUser(config *initConfig) error {
431-
// Set up defaults.
432-
defaultExecUser := user.ExecUser{
433-
Uid: 0,
434-
Gid: 0,
435-
Home: "/",
436-
}
437-
438-
passwdPath, err := user.GetPasswdPath()
439-
if err != nil {
440-
return err
441-
}
442-
443-
groupPath, err := user.GetGroupPath()
444-
if err != nil {
445-
return err
446-
}
447-
448-
execUser, err := user.GetExecUserPath(config.User, &defaultExecUser, passwdPath, groupPath)
449-
if err != nil {
450-
return err
451-
}
452-
453-
var addGroups []int
454-
if len(config.AdditionalGroups) > 0 {
455-
addGroups, err = user.GetAdditionalGroupsPath(config.AdditionalGroups, groupPath)
456-
if err != nil {
457-
return err
458-
}
459-
}
460-
461432
// Rather than just erroring out later in setuid(2) and setgid(2), check
462433
// that the user is mapped here.
463-
if _, err := config.Config.HostUID(execUser.Uid); err != nil {
434+
if _, err := config.Config.HostUID(int(config.UID)); err != nil {
464435
return errors.New("cannot set uid to unmapped user in user namespace")
465436
}
466-
if _, err := config.Config.HostGID(execUser.Gid); err != nil {
437+
if _, err := config.Config.HostGID(int(config.GID)); err != nil {
467438
return errors.New("cannot set gid to unmapped user in user namespace")
468439
}
469440

470-
if config.RootlessEUID {
441+
if config.RootlessEUID && len(config.AdditionalGroups) > 0 {
471442
// We cannot set any additional groups in a rootless container and thus
472443
// we bail if the user asked us to do so. TODO: We currently can't do
473444
// this check earlier, but if libcontainer.Process.User was typesafe
474445
// this might work.
475-
if len(addGroups) > 0 {
476-
return errors.New("cannot set any additional groups in a rootless container")
477-
}
446+
return errors.New("cannot set any additional groups in a rootless container")
478447
}
479448

480449
// Before we change to the container's user make sure that the processes
481450
// STDIO is correctly owned by the user that we are switching to.
482-
if err := fixStdioPermissions(execUser); err != nil {
451+
if err := fixStdioPermissions(config.UID); err != nil {
483452
return err
484453
}
485454

@@ -495,32 +464,34 @@ func setupUser(config *initConfig) error {
495464
allowSupGroups := !config.RootlessEUID && string(bytes.TrimSpace(setgroups)) != "deny"
496465

497466
if allowSupGroups {
498-
suppGroups := append(execUser.Sgids, addGroups...)
499-
if err := unix.Setgroups(suppGroups); err != nil {
467+
if err := unix.Setgroups(config.AdditionalGroups); err != nil {
500468
return &os.SyscallError{Syscall: "setgroups", Err: err}
501469
}
502470
}
503471

504-
if err := system.Setgid(execUser.Gid); err != nil {
472+
if err := system.Setgid(config.GID); err != nil {
505473
return err
506474
}
507-
if err := system.Setuid(execUser.Uid); err != nil {
475+
if err := system.Setuid(config.UID); err != nil {
508476
return err
509477
}
510478

511-
// if we didn't get HOME already, set it based on the user's HOME
512-
if envHome := os.Getenv("HOME"); envHome == "" {
513-
if err := os.Setenv("HOME", execUser.Home); err != nil {
514-
return err
479+
// If we didn't get HOME already, try to set it based on the user's HOME.
480+
if _, ok := os.LookupEnv("HOME"); !ok {
481+
u, err := user.LookupId(strconv.Itoa(int(config.UID)))
482+
if err == nil {
483+
if err := os.Setenv("HOME", u.HomeDir); err != nil {
484+
return err
485+
}
515486
}
516487
}
517488
return nil
518489
}
519490

520-
// fixStdioPermissions fixes the permissions of PID 1's STDIO within the container to the specified user.
491+
// fixStdioPermissions fixes the permissions of PID 1's STDIO within the container to the specified uid.
521492
// The ownership needs to match because it is created outside of the container and needs to be
522493
// localized.
523-
func fixStdioPermissions(u *user.ExecUser) error {
494+
func fixStdioPermissions(uid uint32) error {
524495
var null unix.Stat_t
525496
if err := unix.Stat("/dev/null", &null); err != nil {
526497
return &os.PathError{Op: "stat", Path: "/dev/null", Err: err}
@@ -533,7 +504,7 @@ func fixStdioPermissions(u *user.ExecUser) error {
533504

534505
// Skip chown if uid is already the one we want or any of the STDIO descriptors
535506
// were redirected to /dev/null.
536-
if int(s.Uid) == u.Uid || s.Rdev == null.Rdev {
507+
if s.Uid == uid || s.Rdev == null.Rdev {
537508
continue
538509
}
539510

@@ -543,7 +514,7 @@ func fixStdioPermissions(u *user.ExecUser) error {
543514
// that users expect to be able to actually use their console. Without
544515
// this code, you couldn't effectively run as a non-root user inside a
545516
// container and also have a console set up.
546-
if err := file.Chown(u.Uid, int(s.Gid)); err != nil {
517+
if err := file.Chown(int(uid), int(s.Gid)); err != nil {
547518
// If we've hit an EINVAL then s.Gid isn't mapped in the user
548519
// namespace. If we've hit an EPERM then the inode's current owner
549520
// 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 uint32
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

libcontainer/system/syscall_linux_64.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_SETUID, 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_SETGID, uintptr(gid), 0, 0)
2323
if e1 != 0 {
2424
err = e1

0 commit comments

Comments
 (0)