diff --git a/cli/command/container/opts.go b/cli/command/container/opts.go index 680a84a8b56c..94b8dffa34a6 100644 --- a/cli/command/container/opts.go +++ b/cli/command/container/opts.go @@ -139,6 +139,7 @@ type containerOptions struct { runtime string autoRemove bool init bool + umask opts.UmaskOpt annotations *opts.MapOpts Image string @@ -313,6 +314,9 @@ func addFlags(flags *pflag.FlagSet) *containerOptions { flags.Var(&copts.shmSize, "shm-size", "Size of /dev/shm") flags.StringVar(&copts.utsMode, "uts", "", "UTS namespace to use") flags.StringVar(&copts.runtime, "runtime", "", "Runtime to use for this container") + flags.Var(&copts.umask, "umask", "Set umask for the container") + flags.SetAnnotation("umask", "version", []string{"1.56"}) + flags.SetAnnotation("umask", "ostype", []string{"linux"}) flags.BoolVar(&copts.init, "init", false, "Run an init inside the container that forwards signals and reaps processes") flags.SetAnnotation("init", "version", []string{"1.25"}) @@ -710,6 +714,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con MaskedPaths: maskedPaths, ReadonlyPaths: readonlyPaths, Annotations: copts.annotations.GetAll(), + Umask: copts.umask.Value(), } if copts.autoRemove && !hostConfig.RestartPolicy.IsNone() { diff --git a/cli/command/container/opts_test.go b/cli/command/container/opts_test.go index 0781e99687c0..556e013066fa 100644 --- a/cli/command/container/opts_test.go +++ b/cli/command/container/opts_test.go @@ -154,6 +154,19 @@ func TestParseRunLinks(t *testing.T) { } } +func TestParseRunWithoutUmask(t *testing.T) { + _, hostConfig, _, err := parseRun([]string{"ubuntu", "bash"}) + assert.NilError(t, err) + assert.Assert(t, hostConfig.Umask == nil) +} + +func TestParseRunUmask(t *testing.T) { + _, hostConfig, _, err := parseRun([]string{"--umask", "0022", "ubuntu", "bash"}) + assert.NilError(t, err) + assert.Assert(t, hostConfig.Umask != nil) + assert.Equal(t, uint32(0o22), *hostConfig.Umask) +} + func TestParseRunAttach(t *testing.T) { tests := []struct { input string diff --git a/docs/reference/commandline/container_create.md b/docs/reference/commandline/container_create.md index d69b163389cb..a2ee6147a50b 100644 --- a/docs/reference/commandline/container_create.md +++ b/docs/reference/commandline/container_create.md @@ -102,6 +102,7 @@ Create a new container | `--tmpfs` | `list` | | Mount a tmpfs directory | | `-t`, `--tty` | `bool` | | Allocate a pseudo-TTY | | `--ulimit` | `ulimit` | | Ulimit options | +| `--umask` | `umask` | `` | Set umask for the container | | `--use-api-socket` | `bool` | | Bind mount Docker API socket and required auth | | `-u`, `--user` | `string` | | Username or UID (format: [:]) | | `--userns` | `string` | | User namespace to use | diff --git a/docs/reference/commandline/container_run.md b/docs/reference/commandline/container_run.md index 1dcc0fd5387b..79d04c9c0aa4 100644 --- a/docs/reference/commandline/container_run.md +++ b/docs/reference/commandline/container_run.md @@ -105,6 +105,7 @@ Create and run a new container from an image | [`--tmpfs`](#tmpfs) | `list` | | Mount a tmpfs directory | | [`-t`](#tty), [`--tty`](#tty) | `bool` | | Allocate a pseudo-TTY | | [`--ulimit`](#ulimit) | `ulimit` | | Ulimit options | +| [`--umask`](#umask) | `umask` | `` | Set umask for the container | | `--use-api-socket` | `bool` | | Bind mount Docker API socket and required auth | | `-u`, `--user` | `string` | | Username or UID (format: [:]) | | [`--userns`](#userns) | `string` | | User namespace to use | @@ -1392,6 +1393,56 @@ The 4th container fails and reports a "[8] System error: resource temporarily un This fails because the caller set `nproc=3` resulting in the first three containers using up the three processes quota set for the `daemon` user. +### Set umask for the container (--umask) + +The `--umask` flag sets the umask for the container's processes, which +controls the default permissions for files and directories created inside +the container. The value must be specified in octal notation. Leading zeros +are optional. For example, a umask of `022` creates new files with `644` +permissions (`-rw-r--r--`) and new directories with `755` permissions +(`drwxr-xr-x`). + +If you don't set the `--umask` flag, the OCI runtime applies its own +default umask. The default OCI runtime (runc) uses a default umask of +`0022`, but this value is implementation-defined and may vary between OCI +runtimes: + +```console +$ docker run --rm busybox sh -c umask +0022 +``` + +When the `--umask` flag is set, the value is included in the OCI process +configuration used for the container's entrypoint, for processes started +with `docker exec`, and for healthchecks. Whether an OCI runtime honors the +value depends on the runtime; runc honors the configured umask for +`docker exec` and for healthchecks. + +To set a custom umask, use the `--umask` flag with an octal value: + +```console +$ docker run --rm --umask 077 busybox sh -c umask +0077 +``` + +The umask is also applied to processes started later with `docker exec`: + +```console +$ docker run --rm -d --name umask-test --umask 077 busybox sleep 60 +$ docker exec umask-test sh -c umask +0077 +``` + +Setting the umask to `0` masks no permission bits, so files and directories +keep their full permissions. The following example creates a file and a +directory inside the container: + +```console +$ docker run --rm --umask 0 busybox sh -c 'touch file && mkdir dir && stat -c "%a %n" file dir' +666 file +777 dir +``` + ### Stop container with signal (--stop-signal) The `--stop-signal` flag sends the system call signal to the diff --git a/docs/reference/commandline/create.md b/docs/reference/commandline/create.md index 2a46bec81ab2..e5699b8eb00c 100644 --- a/docs/reference/commandline/create.md +++ b/docs/reference/commandline/create.md @@ -102,6 +102,7 @@ Create a new container | `--tmpfs` | `list` | | Mount a tmpfs directory | | `-t`, `--tty` | `bool` | | Allocate a pseudo-TTY | | `--ulimit` | `ulimit` | | Ulimit options | +| `--umask` | `umask` | `` | Set umask for the container | | `--use-api-socket` | `bool` | | Bind mount Docker API socket and required auth | | `-u`, `--user` | `string` | | Username or UID (format: [:]) | | `--userns` | `string` | | User namespace to use | diff --git a/docs/reference/commandline/run.md b/docs/reference/commandline/run.md index d544a9cdf5fe..b114301def4e 100644 --- a/docs/reference/commandline/run.md +++ b/docs/reference/commandline/run.md @@ -105,6 +105,7 @@ Create and run a new container from an image | `--tmpfs` | `list` | | Mount a tmpfs directory | | `-t`, `--tty` | `bool` | | Allocate a pseudo-TTY | | `--ulimit` | `ulimit` | | Ulimit options | +| `--umask` | `umask` | `` | Set umask for the container | | `--use-api-socket` | `bool` | | Bind mount Docker API socket and required auth | | `-u`, `--user` | `string` | | Username or UID (format: [:]) | | `--userns` | `string` | | User namespace to use | diff --git a/e2e/container/run_test.go b/e2e/container/run_test.go index 574a203603b9..d45fe0865de7 100644 --- a/e2e/container/run_test.go +++ b/e2e/container/run_test.go @@ -110,6 +110,32 @@ func TestRunWithCgroupNamespace(t *testing.T) { result.Assert(t, icmd.Success) } +func TestRunUmask(t *testing.T) { + environment.SkipIfDaemonNotLinux(t) + t.Skip("FIXME: un-skip once e2e tests v29.8.0") + + testCases := []struct { + name string + args []string + expected string + }{ + {name: "unset", expected: "0022\n"}, + {name: "zero", args: []string{"--umask", "0"}, expected: "0000\n"}, + {name: "octal-022", args: []string{"--umask", "022"}, expected: "0022\n"}, + {name: "octal-777", args: []string{"--umask", "777"}, expected: "0777\n"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + args := []string{"run", "--rm", fixtures.AlpineImage, "sh", "-c", "umask"} + args = append(args[:2], append(tc.args, args[2:]...)...) + result := icmd.RunCommand("docker", args...) + result.Assert(t, icmd.Success) + assert.Equal(t, result.Stdout(), tc.expected) + }) + } +} + func TestMountSubvolume(t *testing.T) { skip.If(t, versions.LessThan(environment.DaemonAPIVersion(t), "1.45")) volName := "test-volume-" + t.Name() diff --git a/opts/opts.go b/opts/opts.go index b944a479d858..0a7706ca71ff 100644 --- a/opts/opts.go +++ b/opts/opts.go @@ -11,6 +11,7 @@ import ( "net" "path" "slices" + "strconv" "strings" "github.com/docker/cli/internal/lazyregexp" @@ -477,3 +478,39 @@ func (m *MemSwapBytes) UnmarshalJSON(s []byte) error { b := MemBytes(*m) return b.UnmarshalJSON(s) } + +// UmaskOpt is a type for umask values in octal format +type UmaskOpt struct { + ptr *uint32 +} + +// Set sets the value of the UmaskOpt by passing a string in octal format +func (u *UmaskOpt) Set(s string) error { + v, err := strconv.ParseUint(s, 8, 32) + if err != nil { + return err + } + if u.ptr == nil { + u.ptr = new(uint32) + } + *u.ptr = uint32(v) + return nil +} + +// Type returns the type +func (*UmaskOpt) Type() string { + return "umask" +} + +// Value returns the uint32 ptr +func (u *UmaskOpt) Value() *uint32 { + return u.ptr +} + +// String returns the umask value in octal format, or "" if the pointer is nil. +func (u *UmaskOpt) String() string { + if u.ptr == nil { + return "" + } + return fmt.Sprintf("%#04o", uint64(*u.ptr)) +} diff --git a/opts/opts_test.go b/opts/opts_test.go index e978c2ea8799..598a54a90efa 100644 --- a/opts/opts_test.go +++ b/opts/opts_test.go @@ -428,3 +428,66 @@ func TestParseCPUsReturnZeroOnInvalidValues(t *testing.T) { resValue, _ = ParseCPUs("1e-32") assert.Equal(t, z1, resValue) } + +func TestUmaskOpt(t *testing.T) { + t.Run("type", func(t *testing.T) { + var opt UmaskOpt + assert.Equal(t, opt.Type(), "umask") + }) + + t.Run("nil by default", func(t *testing.T) { + var opt UmaskOpt + assert.Assert(t, opt.Value() == nil) + assert.Equal(t, opt.String(), "") + }) + + t.Run("rejects empty string", func(t *testing.T) { + var opt UmaskOpt + err := opt.Set("") + assert.Assert(t, err != nil) + assert.Assert(t, opt.Value() == nil) + assert.Equal(t, opt.String(), "") + }) + + t.Run("parses zero", func(t *testing.T) { + var opt UmaskOpt + err := opt.Set("0") + assert.NilError(t, err) + assert.Equal(t, *opt.Value(), uint32(0)) + assert.Equal(t, opt.String(), "0000") + }) + + t.Run("parses valid octal values", func(t *testing.T) { + var opt UmaskOpt + err := opt.Set("022") + assert.NilError(t, err) + assert.Assert(t, opt.Value() != nil) + assert.Equal(t, *opt.Value(), uint32(0o22)) + assert.Equal(t, opt.String(), "0022") + }) + + t.Run("rejects octal values with 0o prefix", func(t *testing.T) { + var opt UmaskOpt + err := opt.Set("0o22") + assert.Assert(t, err != nil) + assert.Assert(t, opt.Value() == nil) + assert.Equal(t, opt.String(), "") + }) + + t.Run("parses large octal values", func(t *testing.T) { + var opt UmaskOpt + err := opt.Set("077777") + assert.NilError(t, err) + assert.Assert(t, opt.Value() != nil) + assert.Equal(t, *opt.Value(), uint32(0o77777)) + assert.Equal(t, opt.String(), "077777") + }) + + t.Run("rejects invalid octal values", func(t *testing.T) { + var opt UmaskOpt + err := opt.Set("9") + assert.Assert(t, err != nil) + assert.Assert(t, opt.Value() == nil) + assert.Equal(t, opt.String(), "") + }) +}