diff --git a/daemon/container.go b/daemon/container.go index 419314b8ba..c0e118778b 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -209,6 +209,20 @@ func populateCommand(c *Container, env []string) error { return fmt.Errorf("invalid network mode: %s", c.hostConfig.NetworkMode) } + // Build lists of devices allowed and created within the container. + userSpecifiedDevices := make([]*devices.Device, len(c.hostConfig.Devices)) + for i, deviceMapping := range c.hostConfig.Devices { + device, err := devices.GetDevice(deviceMapping.PathOnHost, deviceMapping.CgroupPermissions) + device.Path = deviceMapping.PathInContainer + if err != nil { + return fmt.Errorf("error gathering device information while adding custom device %s", err) + } + userSpecifiedDevices[i] = device + } + allowedDevices := append(devices.DefaultAllowedDevices, userSpecifiedDevices...) + + autoCreatedDevices := append(devices.DefaultAutoCreatedDevices, userSpecifiedDevices...) + // TODO: this can be removed after lxc-conf is fully deprecated mergeLxcConfIntoOptions(c.hostConfig, context) @@ -231,8 +245,8 @@ func populateCommand(c *Container, env []string) error { User: c.Config.User, Config: context, Resources: resources, - AllowedDevices: devices.DefaultAllowedDevices, - AutoCreatedDevices: devices.DefaultAutoCreatedDevices, + AllowedDevices: allowedDevices, + AutoCreatedDevices: autoCreatedDevices, } c.command.SysProcAttr = &syscall.SysProcAttr{Setsid: true} c.command.Env = env diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 182eb2a221..7ae1b30c0a 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -946,6 +946,7 @@ removed before the image is removed. -u, --user="" Username or UID -v, --volume=[] Bind mount a volume (e.g., from the host: -v /host:/container, from docker: -v /container) --volumes-from=[] Mount volumes from the specified container(s) + --device=[] Add a host device to the container (e.g. --device=/dev/sdc[:/dev/xvdc[:rwm]]) -w, --workdir="" Working directory inside the container The `docker run` command first `creates` a writeable container layer over the @@ -1122,6 +1123,20 @@ logs could be retrieved using `docker logs`. This is useful if you need to pipe a file or something else into a container and retrieve the container's ID once the container has finished running. + $ sudo docker run --device=/dev/sdc:/dev/xvdc --device=/dev/sdd --device=/dev/zero:/dev/nulo -i -t ubuntu ls -l /dev/{xvdc,sdd,nulo} + brw-rw---- 1 root disk 8, 2 Feb 9 16:05 /dev/xvdc + brw-rw---- 1 root disk 8, 3 Feb 9 16:05 /dev/sdd + crw-rw-rw- 1 root root 1, 5 Feb 9 16:05 /dev/nulo + +It is often necessary to directly expose devices to a container. ``--device`` +option enables that. For example, a specific block storage device or loop +device or audio device can be added to an otherwise unprivileged container +(without the ``--privileged`` flag) and have the application directly access it. + +** Security note: ** + +``--device`` cannot be safely used with ephemeral devices. Block devices that may be removed should not be added to untrusted containers with ``--device``! + **A complete example:** $ sudo docker run -d --name static static-web-files sh diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 9f52d58d12..cf0f4b7e3d 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -919,6 +919,22 @@ func TestRunUnprivilegedWithChroot(t *testing.T) { logDone("run - unprivileged with chroot") } +func TestAddingOptionalDevices(t *testing.T) { + cmd := exec.Command(dockerBinary, "run", "--device", "/dev/zero:/dev/nulo", "busybox", "sh", "-c", "ls /dev/nulo") + + out, _, err := runCommandWithOutput(cmd) + if err != nil { + t.Fatal(err, out) + } + + if actual := strings.Trim(out, "\r\n"); actual != "/dev/nulo" { + t.Fatalf("expected output /dev/nulo, received %s", actual) + } + deleteAllContainers() + + logDone("run - test --device argument") +} + func TestModeHostname(t *testing.T) { cmd := exec.Command(dockerBinary, "run", "-h=testhostname", "busybox", "cat", "/etc/hostname") diff --git a/runconfig/hostconfig.go b/runconfig/hostconfig.go index 79ffad723b..f4aa69fe97 100644 --- a/runconfig/hostconfig.go +++ b/runconfig/hostconfig.go @@ -19,6 +19,12 @@ func (n NetworkMode) IsContainer() bool { return len(parts) > 1 && parts[0] == "container" } +type DeviceMapping struct { + PathOnHost string + PathInContainer string + CgroupPermissions string +} + type HostConfig struct { Binds []string ContainerIDFile string @@ -30,6 +36,7 @@ type HostConfig struct { Dns []string DnsSearch []string VolumesFrom []string + Devices []DeviceMapping NetworkMode NetworkMode } @@ -42,6 +49,7 @@ func ContainerHostConfigFromJob(job *engine.Job) *HostConfig { } job.GetenvJson("LxcConf", &hostConfig.LxcConf) job.GetenvJson("PortBindings", &hostConfig.PortBindings) + job.GetenvJson("Devices", &hostConfig.Devices) if Binds := job.GetenvList("Binds"); Binds != nil { hostConfig.Binds = Binds } diff --git a/runconfig/parse.go b/runconfig/parse.go index 5b05e52778..f7d1d5963f 100644 --- a/runconfig/parse.go +++ b/runconfig/parse.go @@ -41,6 +41,7 @@ func parseRun(cmd *flag.FlagSet, args []string, sysInfo *sysinfo.SysInfo) (*Conf flVolumes = opts.NewListOpts(opts.ValidatePath) flLinks = opts.NewListOpts(opts.ValidateLink) flEnv = opts.NewListOpts(opts.ValidateEnv) + flDevices = opts.NewListOpts(opts.ValidatePath) flPublish opts.ListOpts flExpose opts.ListOpts @@ -74,6 +75,7 @@ func parseRun(cmd *flag.FlagSet, args []string, sysInfo *sysinfo.SysInfo) (*Conf cmd.Var(&flAttach, []string{"a", "-attach"}, "Attach to STDIN, STDOUT or STDERR.") cmd.Var(&flVolumes, []string{"v", "-volume"}, "Bind mount a volume (e.g., from the host: -v /host:/container, from Docker: -v /container)") cmd.Var(&flLinks, []string{"#link", "-link"}, "Add link to another container in the form of name:alias") + cmd.Var(&flDevices, []string{"-device"}, "Add a host device to the container (e.g. --device=/dev/sdc:/dev/xvdc)") cmd.Var(&flEnv, []string{"e", "-env"}, "Set environment variables") cmd.Var(&flEnvFile, []string{"-env-file"}, "Read in a line delimited file of environment variables") @@ -191,6 +193,16 @@ func parseRun(cmd *flag.FlagSet, args []string, sysInfo *sysinfo.SysInfo) (*Conf } } + // parse device mappings + deviceMappings := []DeviceMapping{} + for _, device := range flDevices.GetAll() { + deviceMapping, err := ParseDevice(device) + if err != nil { + return nil, nil, cmd, err + } + deviceMappings = append(deviceMappings, deviceMapping) + } + // collect all the environment variables for the container envVariables := []string{} for _, ef := range flEnvFile.GetAll() { @@ -245,6 +257,7 @@ func parseRun(cmd *flag.FlagSet, args []string, sysInfo *sysinfo.SysInfo) (*Conf DnsSearch: flDnsSearch.GetAll(), VolumesFrom: flVolumesFrom.GetAll(), NetworkMode: netMode, + Devices: deviceMappings, } if sysInfo != nil && flMemory > 0 && !sysInfo.SwapLimit { @@ -303,3 +316,33 @@ func parseNetMode(netMode string) (NetworkMode, error) { } return NetworkMode(netMode), nil } + +func ParseDevice(device string) (DeviceMapping, error) { + src := "" + dst := "" + permissions := "rwm" + arr := strings.Split(device, ":") + switch len(arr) { + case 3: + permissions = arr[2] + fallthrough + case 2: + dst = arr[1] + fallthrough + case 1: + src = arr[0] + default: + return DeviceMapping{}, fmt.Errorf("Invalid device specification: %s", device) + } + + if dst == "" { + dst = src + } + + deviceMapping := DeviceMapping{ + PathOnHost: src, + PathInContainer: dst, + CgroupPermissions: permissions, + } + return deviceMapping, nil +}