From 6515227b630a86a7481a51d4ae99afe5ce6f592d Mon Sep 17 00:00:00 2001 From: Youfu Zhang <1315097+zhangyoufu@users.noreply.github.com> Date: Thu, 16 Jul 2026 06:05:25 +0000 Subject: [PATCH 1/2] HACK: vendor github.com/moby/api/api changes --- vendor/github.com/moby/moby/api/types/container/hostconfig.go | 1 + 1 file changed, 1 insertion(+) diff --git a/vendor/github.com/moby/moby/api/types/container/hostconfig.go b/vendor/github.com/moby/moby/api/types/container/hostconfig.go index 0f889c65124c..b9b6256e320e 100644 --- a/vendor/github.com/moby/moby/api/types/container/hostconfig.go +++ b/vendor/github.com/moby/moby/api/types/container/hostconfig.go @@ -454,6 +454,7 @@ type HostConfig struct { ShmSize int64 // Total shm memory usage Sysctls map[string]string `json:",omitempty"` // List of Namespaced sysctls used for the container Runtime string `json:",omitempty"` // Runtime to use with this container + Umask *uint32 `json:",omitempty"` // Umask to use for the container // Applicable to Windows Isolation Isolation // Isolation technology of the container (e.g. default, hyperv) From cee8d0f82c15f120de2a2e6ad8391788ce632483 Mon Sep 17 00:00:00 2001 From: Youfu Zhang <1315097+zhangyoufu@users.noreply.github.com> Date: Thu, 16 Jul 2026 07:59:14 +0000 Subject: [PATCH 2/2] cli/command/container: add create/run --umask Signed-off-by: Youfu Zhang --- cli/command/container/opts.go | 3 ++ cli/command/container/opts_test.go | 13 +++++++ e2e/container/run_test.go | 25 +++++++++++++ opts/opts.go | 37 +++++++++++++++++++ opts/opts_test.go | 58 ++++++++++++++++++++++++++++++ 5 files changed, 136 insertions(+) diff --git a/cli/command/container/opts.go b/cli/command/container/opts.go index 1af0b64546a4..1a85170de25e 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,7 @@ 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 the umask for the container") 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 +712,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/e2e/container/run_test.go b/e2e/container/run_test.go index 574a203603b9..9151131a47e7 100644 --- a/e2e/container/run_test.go +++ b/e2e/container/run_test.go @@ -110,6 +110,31 @@ func TestRunWithCgroupNamespace(t *testing.T) { result.Assert(t, icmd.Success) } +func TestRunUmask(t *testing.T) { + environment.SkipIfDaemonNotLinux(t) + + 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 2e0adac6e0db..45d082f1f9c3 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 "(unset)" if the pointer is nil. +func (u *UmaskOpt) String() string { + if u.ptr == nil { + return "(unset)" + } + return fmt.Sprintf("%#04o", uint64(*u.ptr)) +} diff --git a/opts/opts_test.go b/opts/opts_test.go index e978c2ea8799..233e8132d7f3 100644 --- a/opts/opts_test.go +++ b/opts/opts_test.go @@ -428,3 +428,61 @@ func TestParseCPUsReturnZeroOnInvalidValues(t *testing.T) { resValue, _ = ParseCPUs("1e-32") assert.Equal(t, z1, resValue) } + +func TestUmaskOpt(t *testing.T) { + t.Run("unset by default", func(t *testing.T) { + var opt UmaskOpt + assert.Assert(t, opt.Value() == nil) + assert.Equal(t, opt.String(), "(unset)") + }) + + 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(), "(unset)") + }) + + 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(), "(unset)") + }) + + 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(), "(unset)") + }) +}