Skip to content
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
5 changes: 5 additions & 0 deletions cli/command/container/opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ type containerOptions struct {
runtime string
autoRemove bool
init bool
umask opts.UmaskOpt
annotations *opts.MapOpts

Image string
Expand Down Expand Up @@ -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"})
Expand Down Expand Up @@ -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() {
Expand Down
13 changes: 13 additions & 0 deletions cli/command/container/opts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/reference/commandline/container_create.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` | `<nil>` | Set umask for the container |
| `--use-api-socket` | `bool` | | Bind mount Docker API socket and required auth |
| `-u`, `--user` | `string` | | Username or UID (format: <name\|uid>[:<group\|gid>]) |
| `--userns` | `string` | | User namespace to use |
Expand Down
51 changes: 51 additions & 0 deletions docs/reference/commandline/container_run.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` | `<nil>` | Set umask for the container |
| `--use-api-socket` | `bool` | | Bind mount Docker API socket and required auth |
| `-u`, `--user` | `string` | | Username or UID (format: <name\|uid>[:<group\|gid>]) |
| [`--userns`](#userns) | `string` | | User namespace to use |
Expand Down Expand Up @@ -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.

### <a name="umask"></a> 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
```

### <a name="stop-signal"></a> Stop container with signal (--stop-signal)

The `--stop-signal` flag sends the system call signal to the
Expand Down
1 change: 1 addition & 0 deletions docs/reference/commandline/create.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` | `<nil>` | Set umask for the container |
| `--use-api-socket` | `bool` | | Bind mount Docker API socket and required auth |
| `-u`, `--user` | `string` | | Username or UID (format: <name\|uid>[:<group\|gid>]) |
| `--userns` | `string` | | User namespace to use |
Expand Down
1 change: 1 addition & 0 deletions docs/reference/commandline/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` | `<nil>` | Set umask for the container |
| `--use-api-socket` | `bool` | | Bind mount Docker API socket and required auth |
| `-u`, `--user` | `string` | | Username or UID (format: <name\|uid>[:<group\|gid>]) |
| `--userns` | `string` | | User namespace to use |
Expand Down
26 changes: 26 additions & 0 deletions e2e/container/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,32 @@ func TestRunWithCgroupNamespace(t *testing.T) {
result.Assert(t, icmd.Success)
}

func TestRunUmask(t *testing.T) {
environment.SkipIfDaemonNotLinux(t)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it's fine to add a t.Skip("FIXME: un-skip once e2e tests v29.8.0")

We can replace that with a version-based skip later (feature is already tested in the moby repository at API level)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Pushed commit with skip

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()
Expand Down
37 changes: 37 additions & 0 deletions opts/opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"net"
"path"
"slices"
"strconv"
"strings"

"github.com/docker/cli/internal/lazyregexp"
Expand Down Expand Up @@ -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 "<nil>" if the pointer is nil.
func (u *UmaskOpt) String() string {
if u.ptr == nil {
return "<nil>"
}
return fmt.Sprintf("%#04o", uint64(*u.ptr))
}
63 changes: 63 additions & 0 deletions opts/opts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(), "<nil>")
})

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(), "<nil>")
})

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(), "<nil>")
})

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(), "<nil>")
})
}