Repository: moby/moby (language: go)

Issue:
Process capabilities cannot be retained when starting a container as non-root with `--security-opt=no-new-privileges`
### Description

When using docker as a runtime in kubernetes, the capabilities specified in the container's security context (in the pod yaml manifests) are not respected if running as **non-root** user:
```yaml
    securityContext:
      allowPrivilegeEscalation: false
      capabilities:
        add:
        - NET_BIND_SERVICE
        drop:
        - ALL
      privileged: false
      runAsGroup: 107
      runAsNonRoot: true
      runAsUser: 107
```
```bash
$ k exec -ti virt-launcher-testvm-XXXX -- bash
bash-5.1$ grep Cap /proc/1/status 
CapInh:	0000000000000000
CapPrm:	0000000000000000 # permitted caps zeroed
CapEff:	0000000000000000 # effective caps zeroed
CapBnd:	0000000000000400 # cap_net_bind_service
CapAmb:	0000000000000000
````

In KubeVirt project we had several similar issues reported: https://github.com/kubevirt/kubevirt/issues/9465

This can be easily reproduced with minikube. Other runtimes (containerd and crio) handle the capabilities correctly:
```
CapInh:	0000000000000000
CapPrm:	0000000000000400 # cap_net_bind_service
CapEff:	0000000000000400 # cap_net_bind_service
CapBnd:	0000000000000400 # cap_net_bind_service
CapAmb:	0000000000000000
```

I briefly looked at the sources. Though I am not 100% confident that this snippet is actually causing the problem, but the bellow code looked suspicious to me:
https://github.com/moby/moby/blob/c651a535588402f27d8f1a1408c794fc3289f39f/oci/oci.go#L31-L35

It was introduced by this commit https://github.com/moby/moby/commit/349aeeab7c5e878ddc6c0c95a0a937476816c43e (and refactored in https://github.com/moby/moby/commit/0d9a37d0c249e871af0e667317be3169054a989f). 

### Reproduce

```bash
$ minikube start --driver=kvm2
$ k create -f https://github.com/kubevirt/kubevirt/releases/download/v0.59.0/kubevirt-operator.yaml
$ k create -f https://github.com/kubevirt/kubevirt/releases/download/v0.59.0/kubevirt-cr.yaml
$ wget https://kubevirt.io/labs/manifests/vm.yaml
$ vim vm.yaml # add annotation `kubevirt.io/keep-launcher-alive-after-failure: "true"`
$ k create -f vm.yaml
$ k edit vm testvm # set `running: true`
$ k logs -f virt-launcher-testvm-XXXX
...
{"component":"virt-launcher","level":"error","msg":"failed to start virtqemud","pos":"libvirt_helper.go:250","reason":"fork/exec /usr/sbin/virtqemud: errno 0","timestamp":"2023-05-08T09:34:32.370373Z"}
panic: fork/exec /usr/sbin/virtqemud: errno 0
...
$ k exec -ti virt-launcher-testvm-XXXX -- bash
bash-5.1$ grep Cap /proc/1/status 
CapInh:	0000000000000000
CapPrm:	0000000000000000 # permitted caps zeroed
CapEff:	0000000000000000 # effective caps zeroed
CapBnd:	0000000000000400 # cap_net_bind_service
CapAmb:	0000000000000000
```

### Expected behavior

Effective/permitted caps should be set correctly:
```
CapPrm:	0000000000000400
CapEff:	0000000000000400
```

### docker version

```bash
Client:
 Version:           20.10.23
 API version:       1.41
 Go version:        go1.18.10
 Git commit:        7155243
 Built:             Thu Jan 19 17:30:35 2023
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.23
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.18.10
  Git commit:       6051f14
  Built:            Thu Jan 19 17:36:08 2023
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          v1.7.0
  GitCommit:        1fbd70374134b891f97ce19c70b6e50c7b9f4e0d
 runc:
  Version:          1.1.5
  GitCommit:        f19387a6bec4944c770f7668ab51c4348d9c2f38
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0
```


### docker info

```bash
Client:
 Context:    default
 Debug Mode: false

Server:
 Containers: 34
  Running: 28
  Paused: 0
  Stopped: 6
 Images: 14
 Server Version: 20.10.23
 Storage Driver: overlay2
  Backing Filesystem: extfs
  Supports d_type: true
  Native Overlay Diff: true
  userxattr: false
 Logging Driver: json-file
 Cgroup Driver: cgroupfs
 Cgroup Version: 1
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog
 Swarm: inactive
 Runtimes: io.containerd.runc.v2 io.containerd.runtime.v1.linux runc
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: 1fbd70374134b891f97ce19c70b6e50c7b9f4e0d
 runc version: f19387a6bec4944c770f7668ab51c4348d9c2f38
 init version: de40ad0
 Security Options:
  seccomp
   Profile: default
 Kernel Version: 5.10.57
 Operating System: Buildroot 2021.02.12
 OSType: linux
 Architecture: x86_64
 CPUs: 2
 Total Memory: 3.22GiB
 Name: minikube
 ID: 462Q:TJOC:6UQE:VT5O:7XAO:AS3J:5M6Q:VOT3:HXV2:HTVP:4TFY:4W7K
 Docker Root Dir: /var/lib/docker
 Debug Mode: false
 Registry: https://index.docker.io/v1/
 Labels:
  provider=kvm2
 Experimental: false
 Insecure Registries:
  10.96.0.0/12
  127.0.0.0/8
 Live Restore Enabled: false
 Product License: Community Engine

WARNING: No blkio throttle.read_bps_device support
WARNING: No blkio throttle.write_bps_device support
WARNING: No blkio throttle.read_iops_device support
WARNING: No blkio throttle.write_iops_device support
```


### Additional Info

This can also be reproduced without KubeVirt:
```
$ k apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: sleeper
spec:
  restartPolicy: Never
  terminationGracePeriodSeconds: 30
  containers:
  - name: sleeper
    image: busybox
    securityContext:
      allowPrivilegeEscalation: false
      capabilities:
        add:
        - NET_BIND_SERVICE
        drop:
        - ALL
      privileged: false
      runAsGroup: 107
      runAsNonRoot: true
      runAsUser: 107
    command:
    - /bin/sh
    - "-euxc"
    - |
      sleep infinity
EOF
$ k exec -ti sleeper -- sh
~ $ ps aux
PID   USER     TIME  COMMAND
    1 107       0:00 /bin/sh -euxc sleep infinity 
   13 107       0:00 sh
   19 107       0:00 ps aux
~ $ grep Cap /proc/1/status
CapInh:	0000000000000000
CapPrm:	0000000000000000
CapEff:	0000000000000000
CapBnd:	0000000000000400
CapAmb:	0000000000000000
```


Discussion / hints:
> I briefly looked at the sources. Though I am not 100% confident that this snippet is actually causing the problem, but the bellow code looked suspicious to me:

Those changes were part of docker 20.10.14; https://docs.docker.com/engine/release-notes/20.10/#201014 to address [CVE-2022-24769](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-24769)

/cc @samuelkarp 


> It was introduced by this commit 0d9a37d.

Sorry, I was wrong. That commit only refactored the existing code with the effective and permitted caps drop. I think the real commit is this one 349aeeab7c5
```go
	// if non root drop capabilities in the way execve does
	if s.Process.User.UID != 0 {
		s.Process.Capabilities.Effective = []string{}
		s.Process.Capabilities.Permitted = []string{}
	}
```

>  committed on Jun 16, 2018 

Looks pretty old...
What version of Kubernetes or cri-dockerd are you using? I don't think they're in the loop here, but a full context is helpful.
I was able to reproduce the problem on minikube with Kubernetes `v1.26.3`:
```
$ /usr/bin/cri-dockerd --version
cri-dockerd 0.3.1 (9a87d6a)
```
Forgot to mention that the binary has the capabilities set:
```
bash-5.1$ getcap /usr/bin/virt-launcher-monitor
/usr/bin/virt-launcher-monitor cap_net_bind_service=ep
```
Clearing out the effective and permitted capability sets for non-root users was introduced in #36587. https://github.com/moby/moby/pull/36587#issuecomment-391336084 notes that the `docker run --no-new-privileges` option stops file capabilities from working, which, if I'm not mistaken, is equivalent to `securityContext.allowPrivilegeEscalation=false` in a Pod spec.

Notably, there was work to change cri-containerd to implement the same capability-set-clearing behaviour as moby with lots of agreement that it should happen ([though it hasn't quite happened yet](https://github.com/containerd/containerd/pull/4669#issuecomment-1133156942)) which disproves the claim that moby's behaviour is wrong and the other projects are correct.
Hi @corhere, thank you for the clarification. This now makes more sense to me. 

> **allowPrivilegeEscalation**: Controls whether a process can gain more privileges than its parent process. **This bool directly controls whether the no_new_privs flag gets set on the container process**.

> With **no_new_privs** set, execve() promises not to grant the privilege to do anything that could not have been done without the execve call. For example, the setuid and setgid bits will no longer change the uid or gid; **file capabilities will not add to the permitted set**, and LSMs will not relax constraints after execve.

So apparently we now have different behavior in k8s depending on the container runtime.
Hi @corhere , @justincormack 
I am not quit getting why would you want to clear the capabilities before the `execve`.  `--no-new-privileges` should not stop file capabilities working, only those which are not explicitly requested imho.

Can you help me understand the reasoning behind this? I think docker should keep the capabilities requested by the user.
The man pages document the behaviour of the `no_new_privs` attribute on `execve` as follows:

[`capabilities(7)`:](https://man7.org/linux/man-pages/man7/capabilities.7.html#:~:text=Note%3A%20during%20the,will%20be%20cleared.)
> Note: during the capability transitions described above, file
> capabilities may be ignored (treated as empty) for the same
> reasons that the set-user-ID and set-group-ID bits are ignored;
> see [execve(2)](https://man7.org/linux/man-pages/man2/execve.2.html).
>
> Note: according to the rules above, if a process with nonzero
> user IDs performs an [execve(2)](https://man7.org/linux/man-pages/man2/execve.2.html) then any capabilities that are
> present in its permitted and effective sets will be cleared.

[`execve(2)`:](https://man7.org/linux/man-pages/man2/execve.2.html#:~:text=The%20aforementioned%20transformations,above%20are%20true.)
> The aforementioned transformations of the effective IDs are not
> performed (i.e., the set-user-ID and set-group-ID bits are
> ignored) if any of the following is true:
>
> *  the no_new_privs attribute is set for the calling thread (see
>          [prctl(2)](https://man7.org/linux/man-pages/man2/prctl.2.html));
>
> [...]
>
> The capabilities of the program file (see [capabilities(7)](https://man7.org/linux/man-pages/man7/capabilities.7.html)) are
> also ignored if any of the above are true.

If a process with nonzero UIDs and the `no_new_privs` attribute set calls `execve`, the kernel will zero out its permitted and effective capability sets (or set them to the ambient set) irrespective of the file capabilities of the exec'ed program. At least according to those man pages. **That is not the case in practice.**

The _actual_ semantics of `no_new_privs` [are described in the kernel documentation.](https://docs.kernel.org/userspace-api/no_new_privs.html#:~:text=With%20no_new_privs%20set,constraints%20after%20execve.)
> With `no_new_privs` set, `execve()` promises not to grant the privilege to do anything that could not have been done without the execve call. For example, the setuid and setgid bits will no longer change the uid or gid; **file capabilities will not add to the permitted set**, and LSMs will not relax constraints after execve.

(Emphasis mine.)

_File capabilities **are** taken into consideration when `no_new_privs` is set!_ The permitted set after `execve` [is the intersection of the file capabilities and the old permitted set.](https://github.com/torvalds/linux/blob/457391b0380335d5e9a5babdec90ac53928b23b4/security/commoncap.c#L909-L927)

**Moby is in the wrong here.** It doesn't need to emulate `execve` by zeroing the permitted and effective capabilities; the kernel will do that on its own when the runtime changes its UID and calls `execve`. The only impact of zeroing out the permitted and effective capability sets in the OCI spec is blocking containers from utilizing file capabilities to **retain** permitted capabilities when exec'ed with `no_new_privs`.

Fix patch:
diff --git a/daemon/daemon_unix.go b/daemon/daemon_unix.go
index a252b15f71d02..c589c5ea88bff 100644
--- a/daemon/daemon_unix.go
+++ b/daemon/daemon_unix.go
@@ -109,7 +109,10 @@ func getMemoryResources(config containertypes.Resources) *specs.LinuxMemory {
 		memory.KernelTCP = &config.KernelMemoryTCP
 	}
 
-	return &memory
+	if memory != (specs.LinuxMemory{}) {
+		return &memory
+	}
+	return nil
 }
 
 func getPidsLimit(config containertypes.Resources) *specs.LinuxPids {
@@ -131,7 +134,7 @@ func getCPUResources(config containertypes.Resources) (*specs.LinuxCPU, error) {
 	if config.CPUShares < 0 {
 		return nil, fmt.Errorf("shares: invalid argument")
 	}
-	if config.CPUShares >= 0 {
+	if config.CPUShares > 0 {
 		shares := uint64(config.CPUShares)
 		cpu.Shares = &shares
 	}
@@ -172,7 +175,10 @@ func getCPUResources(config containertypes.Resources) (*specs.LinuxCPU, error) {
 		cpu.RealtimeRuntime = &c
 	}
 
-	return &cpu, nil
+	if cpu != (specs.LinuxCPU{}) {
+		return &cpu, nil
+	}
+	return nil, nil
 }
 
 func getBlkioWeightDevices(config containertypes.Resources) ([]specs.LinuxWeightDevice, error) {
diff --git a/daemon/oci_linux.go b/daemon/oci_linux.go
index ae2a4512b198b..827ade009f0bf 100644
--- a/daemon/oci_linux.go
+++ b/daemon/oci_linux.go
@@ -54,6 +54,9 @@ func WithRlimits(daemon *Daemon, c *container.Container) coci.SpecOpts {
 			})
 		}
 
+		if s.Process == nil {
+			s.Process = &specs.Process{}
+		}
 		s.Process.Rlimits = rlimits
 		return nil
 	}
@@ -114,6 +117,9 @@ func WithRootless(daemon *Daemon) coci.SpecOpts {
 // WithOOMScore sets the oom score
 func WithOOMScore(score *int) coci.SpecOpts {
 	return func(ctx context.Context, _ coci.Client, _ *containers.Container, s *coci.Spec) error {
+		if s.Process == nil {
+			s.Process = &specs.Process{}
+		}
 		s.Process.OOMScoreAdj = score
 		return nil
 	}
@@ -122,6 +128,12 @@ func WithOOMScore(score *int) coci.SpecOpts {
 // WithSelinux sets the selinux labels
 func WithSelinux(c *container.Container) coci.SpecOpts {
 	return func(ctx context.Context, _ coci.Client, _ *containers.Container, s *coci.Spec) error {
+		if s.Process == nil {
+			s.Process = &specs.Process{}
+		}
+		if s.Linux == nil {
+			s.Linux = &specs.Linux{}
+		}
 		s.Process.SelinuxLabel = c.GetProcessLabel()
 		s.Linux.MountLabel = c.MountLabel
 		return nil
@@ -152,6 +164,9 @@ func WithApparmor(c *container.Container) coci.SpecOpts {
 					return err
 				}
 			}
+			if s.Process == nil {
+				s.Process = &specs.Process{}
+			}
 			s.Process.ApparmorProfile = appArmorProfile
 		}
 		return nil
@@ -214,6 +229,10 @@ func getUser(c *container.Container, username string) (specs.User, error) {
 }
 
 func setNamespace(s *specs.Spec, ns specs.LinuxNamespace) {
+	if s.Linux == nil {
+		s.Linux = &specs.Linux{}
+	}
+
 	for i, n := range s.Linux.Namespaces {
 		if n.Type == ns.Type {
 			s.Linux.Namespaces[i] = ns
@@ -608,6 +627,9 @@ func WithMounts(daemon *Daemon, c *container.Container) coci.SpecOpts {
 				}
 				rootpg := mountPropagationMap[s.Linux.RootfsPropagation]
 				if rootpg != mount.SHARED && rootpg != mount.RSHARED {
+					if s.Linux == nil {
+						s.Linux = &specs.Linux{}
+					}
 					s.Linux.RootfsPropagation = mountPropagationReverseMap[mount.SHARED]
 				}
 			case mount.SLAVE, mount.RSLAVE:
@@ -636,6 +658,9 @@ func WithMounts(daemon *Daemon, c *container.Container) coci.SpecOpts {
 				if !fallback {
 					rootpg := mountPropagationMap[s.Linux.RootfsPropagation]
 					if rootpg != mount.SHARED && rootpg != mount.RSHARED && rootpg != mount.SLAVE && rootpg != mount.RSLAVE {
+						if s.Linux == nil {
+							s.Linux = &specs.Linux{}
+						}
 						s.Linux.RootfsPropagation = mountPropagationReverseMap[mount.RSLAVE]
 					}
 				}
@@ -691,8 +716,10 @@ func WithMounts(daemon *Daemon, c *container.Container) coci.SpecOpts {
 					clearReadOnly(&s.Mounts[i])
 				}
 			}
-			s.Linux.ReadonlyPaths = nil
-			s.Linux.MaskedPaths = nil
+			if s.Linux != nil {
+				s.Linux.ReadonlyPaths = nil
+				s.Linux.MaskedPaths = nil
+			}
 		}
 
 		// TODO: until a kernel/mount solution exists for handling remount in a user namespace,
@@ -738,6 +765,9 @@ func WithCommonOptions(daemon *Daemon, c *container.Container) coci.SpecOpts {
 		if len(cwd) == 0 {
 			cwd = "/"
 		}
+		if s.Process == nil {
+			s.Process = &specs.Process{}
+		}
 		s.Process.Args = append([]string{c.Path}, c.Args...)
 
 		// only add the custom init if it is specified and the container is running in its
@@ -817,6 +847,9 @@ func WithCgroups(daemon *Daemon, c *container.Container) coci.SpecOpts {
 		} else {
 			cgroupsPath = filepath.Join(parent, c.ID)
 		}
+		if s.Linux == nil {
+			s.Linux = &specs.Linux{}
+		}
 		s.Linux.CgroupsPath = cgroupsPath
 
 		// the rest is only needed for CPU RT controller
@@ -917,8 +950,14 @@ func WithDevices(daemon *Daemon, c *container.Container) coci.SpecOpts {
 			}
 		}
 
+		if s.Linux == nil {
+			s.Linux = &specs.Linux{}
+		}
+		if s.Linux.Resources == nil {
+			s.Linux.Resources = &specs.LinuxResources{}
+		}
 		s.Linux.Devices = append(s.Linux.Devices, devs...)
-		s.Linux.Resources.Devices = devPermissions
+		s.Linux.Resources.Devices = append(s.Linux.Resources.Devices, devPermissions...)
 
 		for _, req := range c.HostConfig.DeviceRequests {
 			if err := daemon.handleDevice(req, s); err != nil {
@@ -959,27 +998,28 @@ func WithResources(c *container.Container) coci.SpecOpts {
 		if err != nil {
 			return err
 		}
-		blkioWeight := r.BlkioWeight
 
-		specResources := &specs.LinuxResources{
-			Memory: memoryRes,
-			CPU:    cpuRes,
-			BlockIO: &specs.LinuxBlockIO{
-				Weight:                  &blkioWeight,
-				WeightDevice:            weightDevices,
-				ThrottleReadBpsDevice:   readBpsDevice,
-				ThrottleWriteBpsDevice:  writeBpsDevice,
-				ThrottleReadIOPSDevice:  readIOpsDevice,
-				ThrottleWriteIOPSDevice: writeIOpsDevice,
-			},
-			Pids: getPidsLimit(r),
+		if s.Linux == nil {
+			s.Linux = &specs.Linux{}
 		}
-
-		if s.Linux.Resources != nil && len(s.Linux.Resources.Devices) > 0 {
-			specResources.Devices = s.Linux.Resources.Devices
+		if s.Linux.Resources == nil {
+			s.Linux.Resources = &specs.LinuxResources{}
+		}
+		s.Linux.Resources.Memory = memoryRes
+		s.Linux.Resources.CPU = cpuRes
+		s.Linux.Resources.BlockIO = &specs.LinuxBlockIO{
+			WeightDevice:            weightDevices,
+			ThrottleReadBpsDevice:   readBpsDevice,
+			ThrottleWriteBpsDevice:  writeBpsDevice,
+			ThrottleReadIOPSDevice:  readIOpsDevice,
+			ThrottleWriteIOPSDevice: writeIOpsDevice,
+		}
+		if r.BlkioWeight != 0 {
+			w := r.BlkioWeight
+			s.Linux.Resources.BlockIO.Weight = &w
 		}
+		s.Linux.Resources.Pids = getPidsLimit(r)
 
-		s.Linux.Resources = specResources
 		return nil
 	}
 }
@@ -987,6 +1027,15 @@ func WithResources(c *container.Container) coci.SpecOpts {
 // WithSysctls sets the container's sysctls
 func WithSysctls(c *container.Container) coci.SpecOpts {
 	return func(ctx context.Context, _ coci.Client, _ *containers.Container, s *coci.Spec) error {
+		if len(c.HostConfig.Sysctls) == 0 {
+			return nil
+		}
+		if s.Linux == nil {
+			s.Linux = &specs.Linux{}
+		}
+		if s.Linux.Sysctl == nil {
+			s.Linux.Sysctl = make(map[string]string)
+		}
 		// We merge the sysctls injected above with the HostConfig (latter takes
 		// precedence for backwards-compatibility reasons).
 		for k, v := range c.HostConfig.Sysctls {
@@ -999,6 +1048,9 @@ func WithSysctls(c *container.Container) coci.SpecOpts {
 // WithUser sets the container's user
 func WithUser(c *container.Container) coci.SpecOpts {
 	return func(ctx context.Context, _ coci.Client, _ *containers.Container, s *coci.Spec) error {
+		if s.Process == nil {
+			s.Process = &specs.Process{}
+		}
 		var err error
 		s.Process.User, err = getUser(c, c.Config.User)
 		return err
diff --git a/daemon/oci_opts.go b/daemon/oci_opts.go
index c824999d50739..c8b1b633b6f08 100644
--- a/daemon/oci_opts.go
+++ b/daemon/oci_opts.go
@@ -13,6 +13,9 @@ import (
 func WithConsoleSize(c *container.Container) coci.SpecOpts {
 	return func(ctx context.Context, _ coci.Client, _ *containers.Container, s *coci.Spec) error {
 		if c.HostConfig.ConsoleSize[0] > 0 || c.HostConfig.ConsoleSize[1] > 0 {
+			if s.Process == nil {
+				s.Process = &specs.Process{}
+			}
 			s.Process.ConsoleSize = &specs.Box{
 				Height: c.HostConfig.ConsoleSize[0],
 				Width:  c.HostConfig.ConsoleSize[1],
diff --git a/daemon/oci_utils.go b/daemon/oci_utils.go
index 2d833502bd3a9..a47f7bab44bb2 100644
--- a/daemon/oci_utils.go
+++ b/daemon/oci_utils.go
@@ -9,7 +9,12 @@ func setLinuxDomainname(c *container.Container, s *specs.Spec) {
 	// There isn't a field in the OCI for the NIS domainname, but luckily there
 	// is a sysctl which has an identical effect to setdomainname(2) so there's
 	// no explicit need for runtime support.
-	s.Linux.Sysctl = make(map[string]string)
+	if s.Linux == nil {
+		s.Linux = &specs.Linux{}
+	}
+	if s.Linux.Sysctl == nil {
+		s.Linux.Sysctl = make(map[string]string)
+	}
 	if c.Config.Domainname != "" {
 		s.Linux.Sysctl["kernel.domainname"] = c.Config.Domainname
 	}
diff --git a/daemon/seccomp_linux.go b/daemon/seccomp_linux.go
index 8336b00392233..2e3c37818eb7b 100644
--- a/daemon/seccomp_linux.go
+++ b/daemon/seccomp_linux.go
@@ -9,6 +9,7 @@ import (
 	"github.com/docker/docker/container"
 	dconfig "github.com/docker/docker/daemon/config"
 	"github.com/docker/docker/profiles/seccomp"
+	specs "github.com/opencontainers/runtime-spec/specs-go"
 	"github.com/sirupsen/logrus"
 )
 
@@ -31,6 +32,9 @@ func WithSeccomp(daemon *Daemon, c *container.Container) coci.SpecOpts {
 			c.SeccompProfile = dconfig.SeccompProfileUnconfined
 			return nil
 		}
+		if s.Linux == nil {
+			s.Linux = &specs.Linux{}
+		}
 		var err error
 		switch {
 		case c.SeccompProfile == dconfig.SeccompProfileDefault:
diff --git a/daemon/update_linux.go b/daemon/update_linux.go
index c1d3684868db1..3105402e3ce06 100644
--- a/daemon/update_linux.go
+++ b/daemon/update_linux.go
@@ -11,15 +11,19 @@ import (
 func toContainerdResources(resources container.Resources) *libcontainerdtypes.Resources {
 	var r libcontainerdtypes.Resources
 
-	r.BlockIO = &specs.LinuxBlockIO{
-		Weight: &resources.BlkioWeight,
+	if resources.BlkioWeight != 0 {
+		r.BlockIO = &specs.LinuxBlockIO{
+			Weight: &resources.BlkioWeight,
+		}
 	}
 
-	shares := uint64(resources.CPUShares)
-	r.CPU = &specs.LinuxCPU{
-		Shares: &shares,
-		Cpus:   resources.CpusetCpus,
-		Mems:   resources.CpusetMems,
+	cpu := specs.LinuxCPU{
+		Cpus: resources.CpusetCpus,
+		Mems: resources.CpusetMems,
+	}
+	if resources.CPUShares != 0 {
+		shares := uint64(resources.CPUShares)
+		cpu.Shares = &shares
 	}
 
 	var (
@@ -37,17 +41,33 @@ func toContainerdResources(resources container.Resources) *libcontainerdtypes.Re
 		period = uint64(resources.CPUPeriod)
 	}
 
-	r.CPU.Period = &period
-	r.CPU.Quota = &quota
+	if period != 0 {
+		cpu.Period = &period
+	}
+	if quota != 0 {
+		cpu.Quota = &quota
+	}
 
-	r.Memory = &specs.LinuxMemory{
-		Limit:       &resources.Memory,
-		Reservation: &resources.MemoryReservation,
-		Kernel:      &resources.KernelMemory,
+	if cpu != (specs.LinuxCPU{}) {
+		r.CPU = &cpu
 	}
 
+	var memory specs.LinuxMemory
+	if resources.Memory != 0 {
+		memory.Limit = &resources.Memory
+	}
+	if resources.MemoryReservation != 0 {
+		memory.Reservation = &resources.MemoryReservation
+	}
+	if resources.KernelMemory != 0 {
+		memory.Kernel = &resources.KernelMemory
+	}
 	if resources.MemorySwap > 0 {
-		r.Memory.Swap = &resources.MemorySwap
+		memory.Swap = &resources.MemorySwap
+	}
+
+	if memory != (specs.LinuxMemory{}) {
+		r.Memory = &memory
 	}
 
 	r.Pids = getPidsLimit(resources)
diff --git a/integration/internal/container/ops.go b/integration/internal/container/ops.go
index f3101a816c466..bb4a8305ecbcf 100644
--- a/integration/internal/container/ops.go
+++ b/integration/internal/container/ops.go
@@ -241,3 +241,21 @@ func WithRuntime(name string) func(*TestContainerConfig) {
 		c.HostConfig.Runtime = name
 	}
 }
+
+func WithCapability(capabilities ...string) func(*TestContainerConfig) {
+	return func(c *TestContainerConfig) {
+		c.HostConfig.CapAdd = append(c.HostConfig.CapAdd, capabilities...)
+	}
+}
+
+func WithDropCapability(capabilities ...string) func(*TestContainerConfig) {
+	return func(c *TestContainerConfig) {
+		c.HostConfig.CapDrop = append(c.HostConfig.CapDrop, capabilities...)
+	}
+}
+
+func WithSecurityOpt(opt string) func(*TestContainerConfig) {
+	return func(c *TestContainerConfig) {
+		c.HostConfig.SecurityOpt = append(c.HostConfig.SecurityOpt, opt)
+	}
+}
diff --git a/libcontainerd/remote/client_linux.go b/libcontainerd/remote/client_linux.go
index e45d140b2f079..85a96579f78c2 100644
--- a/libcontainerd/remote/client_linux.go
+++ b/libcontainerd/remote/client_linux.go
@@ -26,9 +26,7 @@ func (c *client) UpdateResources(ctx context.Context, containerID string, resour
 		return err
 	}
 
-	// go doesn't like the alias in 1.8, this means this need to be
-	// platform specific
-	return p.(containerd.Task).Update(ctx, containerd.WithResources((*specs.LinuxResources)(resources)))
+	return p.(containerd.Task).Update(ctx, containerd.WithResources(resources))
 }
 
 func hostIDFromMap(id uint32, mp []specs.LinuxIDMapping) int {
diff --git a/libcontainerd/types/types_linux.go b/libcontainerd/types/types_linux.go
index 6b05385221641..3433bda055254 100644
--- a/libcontainerd/types/types_linux.go
+++ b/libcontainerd/types/types_linux.go
@@ -27,7 +27,7 @@ func InterfaceToStats(read time.Time, v interface{}) *Stats {
 }
 
 // Resources defines updatable container resource values. TODO: it must match containerd upcoming API
-type Resources specs.LinuxResources
+type Resources = specs.LinuxResources
 
 // Checkpoints contains the details of a checkpoint
 type Checkpoints struct{}
diff --git a/oci/namespaces.go b/oci/namespaces.go
index f32e489b4a27f..851edd61ef240 100644
--- a/oci/namespaces.go
+++ b/oci/namespaces.go
@@ -4,6 +4,9 @@ import specs "github.com/opencontainers/runtime-spec/specs-go"
 
 // RemoveNamespace removes the `nsType` namespace from OCI spec `s`
 func RemoveNamespace(s *specs.Spec, nsType specs.LinuxNamespaceType) {
+	if s.Linux == nil {
+		return
+	}
 	for i, n := range s.Linux.Namespaces {
 		if n.Type == nsType {
 			s.Linux.Namespaces = append(s.Linux.Namespaces[:i], s.Linux.Namespaces[i+1:]...)
diff --git a/oci/oci.go b/oci/oci.go
index 2021ec3538fdd..45ed7979ee827 100644
--- a/oci/oci.go
+++ b/oci/oci.go
@@ -20,19 +20,13 @@ var deviceCgroupRuleRegex = regexp.MustCompile("^([acb]) ([0-9]+|\\*):([0-9]+|\\
 // SetCapabilities sets the provided capabilities on the spec
 // All capabilities are added if privileged is true.
 func SetCapabilities(s *specs.Spec, caplist []string) error {
-	// setUser has already been executed here
-	if s.Process.User.UID == 0 {
-		s.Process.Capabilities = &specs.LinuxCapabilities{
-			Effective: caplist,
-			Bounding:  caplist,
-			Permitted: caplist,
-		}
-	} else {
-		// Do not set Effective and Permitted capabilities for non-root users,
-		// to match what execve does.
-		s.Process.Capabilities = &specs.LinuxCapabilities{
-			Bounding: caplist,
-		}
+	if s.Process == nil {
+		s.Process = &specs.Process{}
+	}
+	s.Process.Capabilities = &specs.LinuxCapabilities{
+		Effective: caplist,
+		Bounding:  caplist,
+		Permitted: caplist,
 	}
 	return nil
 }
diff --git a/pkg/rootless/specconv/specconv_linux.go b/pkg/rootless/specconv/specconv_linux.go
index b706b5ca6a5f1..06f55ef13d789 100644
--- a/pkg/rootless/specconv/specconv_linux.go
+++ b/pkg/rootless/specconv/specconv_linux.go
@@ -40,11 +40,13 @@ func getCurrentOOMScoreAdj() int {
 
 func toRootless(spec *specs.Spec, v2Controllers []string, currentOOMScoreAdj int) error {
 	if len(v2Controllers) == 0 {
-		// Remove cgroup settings.
-		spec.Linux.Resources = nil
-		spec.Linux.CgroupsPath = ""
+		if spec.Linux != nil {
+			// Remove cgroup settings.
+			spec.Linux.Resources = nil
+			spec.Linux.CgroupsPath = ""
+		}
 	} else {
-		if spec.Linux.Resources != nil {
+		if spec.Linux != nil && spec.Linux.Resources != nil {
 			m := make(map[string]struct{})
 			for _, s := range v2Controllers {
 				m[s] = struct{}{}
@@ -77,7 +79,7 @@ func toRootless(spec *specs.Spec, v2Controllers []string, currentOOMScoreAdj int
 		}
 	}
 
-	if spec.Process.OOMScoreAdj != nil && *spec.Process.OOMScoreAdj < currentOOMScoreAdj {
+	if spec.Process != nil && spec.Process.OOMScoreAdj != nil && *spec.Process.OOMScoreAdj < currentOOMScoreAdj {
 		*spec.Process.OOMScoreAdj = currentOOMScoreAdj
 	}
 
@@ -110,6 +112,9 @@ func isHostNS(spec *specs.Spec, nsType specs.LinuxNamespaceType) (bool, error) {
 	if strings.Contains(string(nsType), string(os.PathSeparator)) {
 		return false, fmt.Errorf("unexpected namespace type %q", nsType)
 	}
+	if spec.Linux == nil {
+		return false, nil
+	}
 	for _, ns := range spec.Linux.Namespaces {
 		if ns.Type == nsType {
 			if ns.Path == "" {
@@ -144,15 +149,17 @@ func bindMountHostProcfs(spec *specs.Spec) error {
 		}
 	}
 
-	// Remove ReadonlyPaths for /proc/*
-	newROP := spec.Linux.ReadonlyPaths[:0]
-	for _, s := range spec.Linux.ReadonlyPaths {
-		s = path.Clean(s)
-		if !strings.HasPrefix(s, "/proc/") {
-			newROP = append(newROP, s)
+	if spec.Linux != nil {
+		// Remove ReadonlyPaths for /proc/*
+		newROP := spec.Linux.ReadonlyPaths[:0]
+		for _, s := range spec.Linux.ReadonlyPaths {
+			s = path.Clean(s)
+			if !strings.HasPrefix(s, "/proc/") {
+				newROP = append(newROP, s)
+			}
 		}
+		spec.Linux.ReadonlyPaths = newROP
 	}
-	spec.Linux.ReadonlyPaths = newROP
 
 	return nil
 }


Test patch:
diff --git a/daemon/oci_linux_test.go b/daemon/oci_linux_test.go
index 42084c900d03d..cfc898c389b3f 100644
--- a/daemon/oci_linux_test.go
+++ b/daemon/oci_linux_test.go
@@ -11,17 +11,20 @@ import (
 	"github.com/docker/docker/daemon/network"
 	"github.com/docker/docker/libnetwork"
 	"github.com/docker/docker/pkg/containerfs"
+	"github.com/google/go-cmp/cmp/cmpopts"
+	"github.com/opencontainers/runtime-spec/specs-go"
+	"golang.org/x/sys/unix"
 	"gotest.tools/v3/assert"
 	is "gotest.tools/v3/assert/cmp"
 	"gotest.tools/v3/skip"
 )
 
 func setupFakeDaemon(t *testing.T, c *container.Container) *Daemon {
-	root, err := os.MkdirTemp("", "oci_linux_test-root")
-	assert.NilError(t, err)
+	t.Helper()
+	root := t.TempDir()
 
 	rootfs := filepath.Join(root, "rootfs")
-	err = os.MkdirAll(rootfs, 0755)
+	err := os.MkdirAll(rootfs, 0755)
 	assert.NilError(t, err)
 
 	netController, err := libnetwork.New()
@@ -48,11 +51,19 @@ func setupFakeDaemon(t *testing.T, c *container.Container) *Daemon {
 		c.NetworkSettings = &network.Settings{Networks: make(map[string]*network.EndpointSettings)}
 	}
 
-	return d
-}
+	// HORRIBLE HACK: clean up shm mounts leaked by some tests. Otherwise the
+	// offending tests would fail due to the mounts blocking the temporary
+	// directory from being cleaned up.
+	t.Cleanup(func() {
+		if c.ShmPath != "" {
+			var err error
+			for err == nil { // Some tests over-mount over the same path multiple times.
+				err = unix.Unmount(c.ShmPath, unix.MNT_DETACH)
+			}
+		}
+	})
 
-func cleanupFakeContainer(c *container.Container) {
-	_ = os.RemoveAll(c.Root)
+	return d
 }
 
 // TestTmpfsDevShmNoDupMount checks that a user-specified /dev/shm tmpfs
@@ -72,7 +83,6 @@ func TestTmpfsDevShmNoDupMount(t *testing.T) {
 		},
 	}
 	d := setupFakeDaemon(t, c)
-	defer cleanupFakeContainer(c)
 
 	_, err := d.createSpec(c)
 	assert.Check(t, err)
@@ -91,7 +101,6 @@ func TestIpcPrivateVsReadonly(t *testing.T) {
 		},
 	}
 	d := setupFakeDaemon(t, c)
-	defer cleanupFakeContainer(c)
 
 	s, err := d.createSpec(c)
 	assert.Check(t, err)
@@ -120,7 +129,6 @@ func TestSysctlOverride(t *testing.T) {
 		},
 	}
 	d := setupFakeDaemon(t, c)
-	defer cleanupFakeContainer(c)
 
 	// Ensure that the implicit sysctl is set correctly.
 	s, err := d.createSpec(c)
@@ -172,7 +180,6 @@ func TestSysctlOverrideHost(t *testing.T) {
 		},
 	}
 	d := setupFakeDaemon(t, c)
-	defer cleanupFakeContainer(c)
 
 	// Ensure that the implicit sysctl is not set
 	s, err := d.createSpec(c)
@@ -200,3 +207,38 @@ func TestGetSourceMount(t *testing.T) {
 	_, _, err = getSourceMount(cwd)
 	assert.NilError(t, err)
 }
+
+func TestDefaultResources(t *testing.T) {
+	skip.If(t, os.Getuid() != 0, "skipping test that requires root") // TODO: is this actually true? I'm guilty of following the cargo cult here.
+
+	c := &container.Container{
+		HostConfig: &containertypes.HostConfig{
+			IpcMode: containertypes.IPCModeNone,
+		},
+	}
+	d := setupFakeDaemon(t, c)
+
+	s, err := d.createSpec(c)
+	assert.NilError(t, err)
+	checkResourcesAreUnset(t, s.Linux.Resources)
+}
+
+func checkResourcesAreUnset(t *testing.T, r *specs.LinuxResources) {
+	t.Helper()
+
+	if r != nil {
+		if r.Memory != nil {
+			assert.Check(t, is.DeepEqual(r.Memory, &specs.LinuxMemory{}))
+		}
+		if r.CPU != nil {
+			assert.Check(t, is.DeepEqual(r.CPU, &specs.LinuxCPU{}))
+		}
+		assert.Check(t, is.Nil(r.Pids))
+		if r.BlockIO != nil {
+			assert.Check(t, is.DeepEqual(r.BlockIO, &specs.LinuxBlockIO{}, cmpopts.EquateEmpty()))
+		}
+		if r.Network != nil {
+			assert.Check(t, is.DeepEqual(r.Network, &specs.LinuxNetwork{}, cmpopts.EquateEmpty()))
+		}
+	}
+}
diff --git a/daemon/update_linux_test.go b/daemon/update_linux_test.go
new file mode 100644
index 0000000000000..4817c1eab7fd3
--- /dev/null
+++ b/daemon/update_linux_test.go
@@ -0,0 +1,11 @@
+package daemon // import "github.com/docker/docker/daemon"
+
+import (
+	"testing"
+
+	"github.com/docker/docker/api/types/container"
+)
+
+func TestToContainerdResources_Defaults(t *testing.T) {
+	checkResourcesAreUnset(t, toContainerdResources(container.Resources{}))
+}
diff --git a/integration/capabilities/capabilities_linux_test.go b/integration/capabilities/capabilities_linux_test.go
new file mode 100644
index 0000000000000..272f3dcdb7c5c
--- /dev/null
+++ b/integration/capabilities/capabilities_linux_test.go
@@ -0,0 +1,108 @@
+package capabilities
+
+import (
+	"bytes"
+	"context"
+	"io"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/integration/internal/container"
+	"github.com/docker/docker/pkg/stdcopy"
+	"github.com/docker/docker/testutil/fakecontext"
+
+	"gotest.tools/v3/assert"
+	"gotest.tools/v3/poll"
+)
+
+func TestNoNewPrivileges(t *testing.T) {
+	defer setupTest(t)()
+
+	withFileCapability := `
+		FROM debian:bullseye-slim
+		RUN apt-get update && apt-get install -y libcap2-bin --no-install-recommends
+		RUN setcap CAP_DAC_OVERRIDE=+eip /bin/cat
+		RUN echo "hello" > /txt && chown 0:0 /txt && chmod 700 /txt
+		RUN useradd -u 1500 test
+	`
+	imageTag := "captest"
+
+	source := fakecontext.New(t, "", fakecontext.WithDockerfile(withFileCapability))
+	defer source.Close()
+
+	client := testEnv.APIClient()
+
+	// Build image
+	ctx := context.TODO()
+	resp, err := client.ImageBuild(ctx,
+		source.AsTarReader(t),
+		types.ImageBuildOptions{
+			Tags: []string{imageTag},
+		})
+	assert.NilError(t, err)
+	_, err = io.Copy(io.Discard, resp.Body)
+	assert.NilError(t, err)
+	resp.Body.Close()
+
+	testCases := []struct {
+		doc            string
+		opts           []func(*container.TestContainerConfig)
+		stdOut, stdErr string
+	}{
+		{
+			doc: "CapabilityRequested=true",
+			opts: []func(*container.TestContainerConfig){
+				container.WithUser("test"),
+				container.WithCapability("CAP_DAC_OVERRIDE"),
+			},
+			stdOut: "hello",
+		},
+		{
+			doc: "CapabilityRequested=false",
+			opts: []func(*container.TestContainerConfig){
+				container.WithUser("test"),
+				container.WithDropCapability("CAP_DAC_OVERRIDE"),
+			},
+			stdErr: "exec /bin/cat: operation not permitted",
+		},
+	}
+
+	for _, tc := range testCases {
+		tc := tc
+		t.Run(tc.doc, func(t *testing.T) {
+			// Run the container with the image
+			opts := append(tc.opts,
+				container.WithImage(imageTag),
+				container.WithCmd("/bin/cat", "/txt"),
+				container.WithSecurityOpt("no-new-privileges=true"),
+			)
+			cid := container.Run(ctx, t, client, opts...)
+			poll.WaitOn(t, container.IsInState(ctx, client, cid, "exited"), poll.WithDelay(100*time.Millisecond))
+
+			// Assert on outputs
+			logReader, err := client.ContainerLogs(ctx, cid, types.ContainerLogsOptions{
+				ShowStdout: true,
+				ShowStderr: true,
+			})
+			assert.NilError(t, err)
+			defer logReader.Close()
+
+			var actualStdout, actualStderr bytes.Buffer
+			_, err = stdcopy.StdCopy(&actualStdout, &actualStderr, logReader)
+			assert.NilError(t, err)
+
+			stdOut := strings.TrimSpace(actualStdout.String())
+			stdErr := strings.TrimSpace(actualStderr.String())
+			if stdOut != tc.stdOut {
+				t.Fatalf("test produced invalid output: %q, expected %q. Stderr:%q", stdOut, tc.stdOut, stdErr)
+			}
+			if stdErr != tc.stdErr {
+				t.Fatalf("test produced invalid error: %q, expected %q. Stdout:%q", stdErr, tc.stdErr, stdOut)
+
+			}
+		})
+	}
+
+}
diff --git a/integration/capabilities/main_linux_test.go b/integration/capabilities/main_linux_test.go
new file mode 100644
index 0000000000000..0f074dd15603e
--- /dev/null
+++ b/integration/capabilities/main_linux_test.go
@@ -0,0 +1,33 @@
+package capabilities
+
+import (
+	"fmt"
+	"os"
+	"testing"
+
+	"github.com/docker/docker/testutil/environment"
+)
+
+var testEnv *environment.Execution
+
+func TestMain(m *testing.M) {
+	var err error
+	testEnv, err = environment.New()
+	if err != nil {
+		fmt.Println(err)
+		os.Exit(1)
+	}
+	err = environment.EnsureFrozenImagesLinux(testEnv)
+	if err != nil {
+		fmt.Println(err)
+		os.Exit(1)
+	}
+
+	testEnv.Print()
+	os.Exit(m.Run())
+}
+
+func setupTest(t *testing.T) func() {
+	environment.ProtectAll(t, testEnv)
+	return func() { testEnv.Clean(t) }
+}