Skip to content
This repository has been archived by the owner on Dec 13, 2018. It is now read-only.

Adds user namespace support to libcontainer #304

Merged
merged 1 commit into from
Jan 19, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ type Config struct {
// AdditionalGroups specifies the gids that should be added to supplementary groups
// in addition to those that the user belongs to.
AdditionalGroups []int `json:"additional_groups,omitempty"`
// UidMappings is an array of User ID mappings for User Namespaces
UidMappings []IDMap `json:"uid_mappings,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to google style guide all this things should be like UIDMappings and GIDMappings, but I'm not insist :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I went back and forth on the upstream code pr to golang but saw that when id is used in combination Uid/Gid seem to be acceptable. Also, the merged code has UidMappings / GidMappings in golang ;)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hehe, okay then :)


// GidMappings is an array of Group ID mappings for User Namespaces
GidMappings []IDMap `json:"gid_mappings,omitempty"`
}

// Routes can be specified to create entries in the route table as the container is started
Expand Down Expand Up @@ -152,3 +157,10 @@ type Rlimit struct {
Hard uint64 `json:"hard,omitempty"`
Soft uint64 `json:"soft,omitempty"`
}

// IDMap represents UID/GID Mappings for User Namespaces.
type IDMap struct {
ContainerID int `json:"container_id,omitempty"`
HostID int `json:"host_id,omitempty"`
Size int `json:"size,omitempty"`
}
4 changes: 2 additions & 2 deletions console/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ import (
)

// Setup initializes the proper /dev/console inside the rootfs path
func Setup(rootfs, consolePath, mountLabel string) error {
func Setup(rootfs, consolePath, mountLabel string, hostRootUid, hostRootGid int) error {
oldMask := syscall.Umask(0000)
defer syscall.Umask(oldMask)

if err := os.Chmod(consolePath, 0600); err != nil {
return err
}

if err := os.Chown(consolePath, 0, 0); err != nil {
if err := os.Chown(consolePath, hostRootUid, hostRootGid); err != nil {
return err
}

Expand Down
9 changes: 8 additions & 1 deletion integration/execin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ func TestExecInRlimit(t *testing.T) {
func startLongRunningContainer(config *libcontainer.Config) (*exec.Cmd, string, chan error) {
containerErr := make(chan error, 1)
containerCmd := &exec.Cmd{}
setupContainerCmd := &exec.Cmd{}
var statePath string

createCmd := func(container *libcontainer.Config, console, dataPath, init string,
Expand All @@ -124,14 +125,20 @@ func startLongRunningContainer(config *libcontainer.Config) (*exec.Cmd, string,
return containerCmd
}

setupCmd := func(container *libcontainer.Config, console, dataPath, init string) *exec.Cmd {
setupContainerCmd = namespaces.DefaultSetupCommand(container, console, dataPath, init)
statePath = dataPath
return setupContainerCmd
}

var containerStart sync.WaitGroup
containerStart.Add(1)
go func() {
buffers := newStdBuffers()
_, err := namespaces.Exec(config,
buffers.Stdin, buffers.Stdout, buffers.Stderr,
"", config.RootFs, []string{"sleep", "10"},
createCmd, containerStart.Done)
createCmd, setupCmd, containerStart.Done)
containerErr <- err
}()
containerStart.Wait()
Expand Down
2 changes: 1 addition & 1 deletion integration/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,6 @@ func runContainer(config *libcontainer.Config, console string, args ...string) (

buffers = newStdBuffers()
exitCode, err = namespaces.Exec(config, buffers.Stdin, buffers.Stdout, buffers.Stderr,
console, config.RootFs, args, namespaces.DefaultCreateCommand, nil)
console, config.RootFs, args, namespaces.DefaultCreateCommand, namespaces.DefaultSetupCommand, nil)
return
}
11 changes: 7 additions & 4 deletions mount/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ type mount struct {

// InitializeMountNamespace sets up the devices, mount points, and filesystems for use inside a
// new mount namespace.
func InitializeMountNamespace(rootfs, console string, sysReadonly bool, mountConfig *MountConfig) error {
func InitializeMountNamespace(rootfs, console string, sysReadonly bool, hostRootUid, hostRootGid int, mountConfig *MountConfig) error {
var (
err error
flag = syscall.MS_PRIVATE
Expand Down Expand Up @@ -58,14 +58,17 @@ func InitializeMountNamespace(rootfs, console string, sysReadonly bool, mountCon
return fmt.Errorf("create device nodes %s", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mknod isn't allowed in a non-root userns

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The setup process is run in the root userns. The init is started (cloning into userns) and then waits for setup to run and set things up. Setup joins all the namespaces of init except userns. Thanks for taking a look. Feedback and suggestions appreciated.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mrunalp Sorry, I don't get the idea with a setup process. Which actions can't be done from userns? Could you elaborate? Maybe you can add a comment before the SetupContainer() function.
LoadContainerEnvironment(), apparmor.ApplyProfile(), label.SetProcessLabel() affect only a current process. Now we call them for the setup process and don't call for the init process. Where are these parameters applied to the init process?
Thanks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right about SetProcessLabel and ApplyProfile. I have to move them to init. I was aware but missed moving them back. I will post an updated patch with more comments. Thanks.

Sent from my iPhone

On Jan 13, 2015, at 12:29 AM, Andrew Vagin notifications@github.com wrote:

In mount/init.go:

@@ -58,14 +58,17 @@ func InitializeMountNamespace(rootfs, console string, sysReadonly bool, mountCon
return fmt.Errorf("create device nodes %s", err)
@mrunalp Sorry, I don't get the idea with a setup process. Which actions can't be done from userns? Could you elaborate? Maybe you can add a comment before the SetupContainer() function.
LoadContainerEnvironment(), apparmor.ApplyProfile(), label.SetProcessLabel() affect only a current process. Now we call them for the setup process and don't call for the init process. Where are these parameters applied to the init process?
Thanks.


Reply to this email directly or view it on GitHub.

}

if err := SetupPtmx(rootfs, console, mountConfig.MountLabel); err != nil {
if err := SetupPtmx(rootfs, console, mountConfig.MountLabel, hostRootUid, hostRootGid); err != nil {
return err
}

// stdin, stdout and stderr could be pointing to /dev/null from parent namespace.
// Re-open them inside this namespace.
if err := reOpenDevNull(rootfs); err != nil {
return fmt.Errorf("Failed to reopen /dev/null %s", err)
// FIXME: Need to fix this for user namespaces.
if hostRootUid == 0 {
if err := reOpenDevNull(rootfs); err != nil {
return fmt.Errorf("Failed to reopen /dev/null %s", err)
}
}

if err := setupDevSymlinks(rootfs); err != nil {
Expand Down
4 changes: 2 additions & 2 deletions mount/ptmx.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"github.com/docker/libcontainer/console"
)

func SetupPtmx(rootfs, consolePath, mountLabel string) error {
func SetupPtmx(rootfs, consolePath, mountLabel string, hostRootUid, hostRootGid int) error {
ptmx := filepath.Join(rootfs, "dev/ptmx")
if err := os.Remove(ptmx); err != nil && !os.IsNotExist(err) {
return err
Expand All @@ -21,7 +21,7 @@ func SetupPtmx(rootfs, consolePath, mountLabel string) error {
}

if consolePath != "" {
if err := console.Setup(rootfs, consolePath, mountLabel); err != nil {
if err := console.Setup(rootfs, consolePath, mountLabel, hostRootUid, hostRootGid); err != nil {
return err
}
}
Expand Down
1 change: 1 addition & 0 deletions namespaces/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ import (
)

type CreateCommand func(container *libcontainer.Config, console, dataPath, init string, childPipe *os.File, args []string) *exec.Cmd
type SetupCommand func(container *libcontainer.Config, console, dataPath, init string) *exec.Cmd
140 changes: 131 additions & 9 deletions namespaces/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package namespaces

import (
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
Expand All @@ -25,7 +26,7 @@ const (
// Move this to libcontainer package.
// Exec performs setup outside of a namespace so that a container can be
// executed. Exec is a high level function for working with container namespaces.
func Exec(container *libcontainer.Config, stdin io.Reader, stdout, stderr io.Writer, console, dataPath string, args []string, createCommand CreateCommand, startCallback func()) (int, error) {
func Exec(container *libcontainer.Config, stdin io.Reader, stdout, stderr io.Writer, console, dataPath string, args []string, createCommand CreateCommand, setupCommand SetupCommand, startCallback func()) (int, error) {
var err error

// create a pipe so that we can syncronize with the namespaced process and
Expand Down Expand Up @@ -74,14 +75,6 @@ func Exec(container *libcontainer.Config, stdin io.Reader, stdout, stderr io.Wri
if err := InitializeNetworking(container, command.Process.Pid, &networkState); err != nil {
return terminate(err)
}
// send the state to the container's init process then shutdown writes for the parent
if err := json.NewEncoder(parent).Encode(networkState); err != nil {
return terminate(err)
}
// shutdown writes for the parent side of the pipe
if err := syscall.Shutdown(int(parent.Fd()), syscall.SHUT_WR); err != nil {
return terminate(err)
}

state := &libcontainer.State{
InitPid: command.Process.Pid,
Expand All @@ -95,6 +88,26 @@ func Exec(container *libcontainer.Config, stdin io.Reader, stdout, stderr io.Wri
}
defer libcontainer.DeleteState(dataPath)

// Start the setup process to setup the init process
if container.Namespaces.Contains(libcontainer.NEWUSER) {
setupCmd := setupCommand(container, console, dataPath, os.Args[0])
output, err := setupCmd.CombinedOutput()
if err != nil || setupCmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus() != 0 {
command.Process.Kill()
command.Wait()
return -1, fmt.Errorf("setup failed: %s %s", err, output)
}
}

// send the state to the container's init process then shutdown writes for the parent
if err := json.NewEncoder(parent).Encode(networkState); err != nil {
return terminate(err)
}
// shutdown writes for the parent side of the pipe
if err := syscall.Shutdown(int(parent.Fd()), syscall.SHUT_WR); err != nil {
return terminate(err)
}

// wait for the child process to fully complete and receive an error message
// if one was encoutered
var ierr *initError
Expand Down Expand Up @@ -157,6 +170,75 @@ func killAllPids(container *libcontainer.Config) error {
return err
}

// Utility function that gets a host ID for a container ID from user namespace map
// if that ID is present in the map.
func hostIDFromMapping(containerID int, uMap []libcontainer.IDMap) (int, bool) {
for _, m := range uMap {
if (containerID >= m.ContainerID) && (containerID <= (m.ContainerID + m.Size - 1)) {
hostID := m.HostID + (containerID - m.ContainerID)
return hostID, true
}
}
return -1, false
}

// Gets the root uid for the process on host which could be non-zero
// when user namespaces are enabled.
func GetHostRootGid(container *libcontainer.Config) (int, error) {
if container.Namespaces.Contains(libcontainer.NEWUSER) {
if container.GidMappings == nil {
return -1, fmt.Errorf("User namespaces enabled, but no gid mappings found.")
}
hostRootGid, found := hostIDFromMapping(0, container.GidMappings)
if !found {
return -1, fmt.Errorf("User namespaces enabled, but no root user mapping found.")
}
return hostRootGid, nil
}

// Return default root uid 0
return 0, nil
}

// Gets the root uid for the process on host which could be non-zero
// when user namespaces are enabled.
func GetHostRootUid(container *libcontainer.Config) (int, error) {
if container.Namespaces.Contains(libcontainer.NEWUSER) {
if container.UidMappings == nil {
return -1, fmt.Errorf("User namespaces enabled, but no user mappings found.")
}
hostRootUid, found := hostIDFromMapping(0, container.UidMappings)
if !found {
return -1, fmt.Errorf("User namespaces enabled, but no root user mapping found.")
}
return hostRootUid, nil
}

// Return default root uid 0
return 0, nil
}

// Converts IDMap to SysProcIDMap array and adds it to SysProcAttr.
func AddUidGidMappings(sys *syscall.SysProcAttr, container *libcontainer.Config) {
if container.UidMappings != nil {
sys.UidMappings = make([]syscall.SysProcIDMap, len(container.UidMappings))
for i, um := range container.UidMappings {
sys.UidMappings[i].ContainerID = um.ContainerID
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, what a shame that we can't use SysProcIDMap directly

sys.UidMappings[i].HostID = um.HostID
sys.UidMappings[i].Size = um.Size
}
}

if container.GidMappings != nil {
sys.GidMappings = make([]syscall.SysProcIDMap, len(container.GidMappings))
for i, gm := range container.GidMappings {
sys.GidMappings[i].ContainerID = gm.ContainerID
sys.GidMappings[i].HostID = gm.HostID
sys.GidMappings[i].Size = gm.Size
}
}
}

// DefaultCreateCommand will return an exec.Cmd with the Cloneflags set to the proper namespaces
// defined on the container's configuration and use the current binary as the init with the
// args provided
Expand Down Expand Up @@ -187,6 +269,46 @@ func DefaultCreateCommand(container *libcontainer.Config, console, dataPath, ini
command.SysProcAttr.Pdeathsig = syscall.SIGKILL
command.ExtraFiles = []*os.File{pipe}

if container.Namespaces.Contains(libcontainer.NEWUSER) {
AddUidGidMappings(command.SysProcAttr, container)

// Default to root user when user namespaces are enabled.
if command.SysProcAttr.Credential == nil {
command.SysProcAttr.Credential = &syscall.Credential{}
}
}

return command
}

// DefaultSetupCommand will return an exec.Cmd that joins the init process to set it up.
//
// console: the /dev/console to setup inside the container
// init: the program executed inside the namespaces
// root: the path to the container json file and information
// args: the arguments to pass to the container to run as the user's program
func DefaultSetupCommand(container *libcontainer.Config, console, dataPath, init string) *exec.Cmd {
// get our binary name from arg0 so we can always reexec ourself
env := []string{
"console=" + console,
"data_path=" + dataPath,
}

if dataPath == "" {
dataPath, _ = os.Getwd()
}

if container.RootFs == "" {
container.RootFs, _ = os.Getwd()
}
args := []string{dataPath, container.RootFs, console}

command := exec.Command(init, append([]string{"exec", "--func", "setup", "--"}, args...)...)

// make sure the process is executed inside the context of the rootfs
command.Dir = container.RootFs
command.Env = append(os.Environ(), env...)

return command
}

Expand Down
57 changes: 57 additions & 0 deletions namespaces/execin.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import (
"github.com/docker/libcontainer/apparmor"
"github.com/docker/libcontainer/cgroups"
"github.com/docker/libcontainer/label"
"github.com/docker/libcontainer/mount"
"github.com/docker/libcontainer/system"
"github.com/docker/libcontainer/utils"
)

// ExecIn reexec's the initPath with the argv 0 rewrite to "nsenter" so that it is able to run the
Expand Down Expand Up @@ -127,6 +129,61 @@ func FinalizeSetns(container *libcontainer.Config, args []string) error {
panic("unreachable")
}

// SetupContainer is run to setup mounts and networking related operations
// for a user namespace enabled process as a user namespace root doesn't
// have permissions to perform these operations.
// The setup process joins all the namespaces of user namespace enabled init
// except the user namespace, so it run as root in the root user namespace
// to perform these operations.
func SetupContainer(container *libcontainer.Config, dataPath, uncleanRootfs, consolePath string) error {
rootfs, err := utils.ResolveRootfs(uncleanRootfs)
if err != nil {
return err
}

// clear the current processes env and replace it with the environment
// defined on the container
if err := LoadContainerEnvironment(container); err != nil {
return err
}

state, err := libcontainer.GetState(dataPath)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("unable to read state: %s", err)
}

if err := setupNetwork(container, &state.NetworkState); err != nil {
return fmt.Errorf("setup networking %s", err)
}

if err := setupRoute(container); err != nil {
return fmt.Errorf("setup route %s", err)
}

label.Init()

hostRootUid, err := GetHostRootUid(container)
if err != nil {
return fmt.Errorf("failed to get hostRootUid %s", err)
}

hostRootGid, err := GetHostRootGid(container)
if err != nil {
return fmt.Errorf("failed to get hostRootGid %s", err)
}

if err := mount.InitializeMountNamespace(rootfs,
consolePath,
container.RestrictSys,
hostRootUid,
hostRootGid,
(*mount.MountConfig)(container.MountConfig)); err != nil {
return fmt.Errorf("setup mount namespace %s", err)
}

return nil
}

func EnterCgroups(state *libcontainer.State, pid int) error {
return cgroups.EnterPid(state.CgroupPaths, pid)
}
Loading