Repository: moby/moby (language: go)

Issue:
`--privileged --security-opt seccomp=<CUSTOM.json>` ignores `<CUSTOM.json>`
### Description

`docker run --privileged --security-opt seccomp=<CUSTOM.json>` ignores `<CUSTOM.json>`

### Reproduce

1. Create `foo.json`:
 ```json
{
  "defaultAction": "SCMP_ACT_ALLOW",
  "syscalls": [ { "names": [ "connect" ], "action": "SCMP_ACT_ERRNO" } ]
}
```

2. Make sure that `docker run --security-opt seccomp=foo.json busybox wget -O- http://1.1.1.1/` fails:
```console
$ docker run --security-opt seccomp=foo.json busybox wget -O- http://1.1.1.1/
Connecting to 1.1.1.1 (1.1.1.1:80)
wget: can't connect to remote host (1.1.1.1): Operation not permitted
```

3. Repeat the previous step, but with `--privileged`

### Expected behavior

Expected behavior: it should consume `foo.json` and let `wget` fail

Actual behavior: it does not consume `foo.json`, so `wget` does not fail

### docker version

```bash
Client: Docker Engine - Community
 Version:           25.0.2
 API version:       1.44
 Go version:        go1.21.6
 Git commit:        29cf629
 Built:             Thu Feb  1 00:22:57 2024
 OS/Arch:           linux/amd64
 Context:           default

Server: Docker Engine - Community
 Engine:
  Version:          25.0.2
  API version:      1.44 (minimum version 1.24)
  Go version:       go1.21.6
  Git commit:       fce6e0c
  Built:            Thu Feb  1 00:22:57 2024
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.6.28
  GitCommit:        ae07eda36dd25f8a1b98dfbf587313b99c0190bb
 runc:
  Version:          1.1.12
  GitCommit:        v1.1.12-0-g51d5e94
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0
```


### docker info

```bash
Client: Docker Engine - Community
 Version:    25.0.2
 Context:    default
 Debug Mode: false
 Plugins:
  buildx: Docker Buildx (Docker Inc.)
    Version:  v0.12.1
    Path:     /usr/libexec/docker/cli-plugins/docker-buildx
  compose: Docker Compose (Docker Inc.)
    Version:  v2.24.5
    Path:     /usr/libexec/docker/cli-plugins/docker-compose

Server:
 Containers: 0
  Running: 0
  Paused: 0
  Stopped: 0
 Images: 2
 Server Version: 25.0.2
 Storage Driver: overlay2
  Backing Filesystem: extfs
  Supports d_type: true
  Using metacopy: false
  Native Overlay Diff: true
  userxattr: false
 Logging Driver: json-file
 Cgroup Driver: systemd
 Cgroup Version: 2
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs fluentd gcplogs gelf journald json-file local splunk syslog
 Swarm: inactive
 Runtimes: runc io.containerd.runc.v2
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: ae07eda36dd25f8a1b98dfbf587313b99c0190bb
 runc version: v1.1.12-0-g51d5e94
 init version: de40ad0
 Security Options:
  apparmor
  seccomp
   Profile: builtin
  cgroupns
 Kernel Version: 5.15.0-92-generic
 Operating System: Ubuntu 22.04.3 LTS
 OSType: linux
 Architecture: x86_64
 CPUs: 4
 Total Memory: 3.818GiB
 Name: lima-vm0
 ID: 095cc593-bf50-4b63-9ebb-6168ad3a60e4
 Docker Root Dir: /var/lib/docker
 Debug Mode: false
 Experimental: false
 Insecure Registries:
  127.0.0.0/8
 Live Restore Enabled: false
```


### Additional Info
- I assume this is *not* a vulnerability, as `--privileged` is never intended to be secure
- `--security-opt apparmor=<CUSTOM>` doesn't seem ignored


Discussion / hints:

Fix patch:
diff --git a/daemon/seccomp_linux.go b/daemon/seccomp_linux.go
index 019400082443a..0d14022db9d02 100644
--- a/daemon/seccomp_linux.go
+++ b/daemon/seccomp_linux.go
@@ -22,7 +22,11 @@ func WithSeccomp(daemon *Daemon, c *container.Container) coci.SpecOpts {
 			return nil
 		}
 		if c.HostConfig.Privileged {
-			return nil
+			var err error
+			if c.SeccompProfile != "" {
+				s.Linux.Seccomp, err = seccomp.LoadProfile(c.SeccompProfile, s)
+			}
+			return err
 		}
 		if !daemon.RawSysInfo().Seccomp {
 			if c.SeccompProfile != "" && c.SeccompProfile != dconfig.SeccompProfileDefault {


Test patch:
diff --git a/daemon/seccomp_linux_test.go b/daemon/seccomp_linux_test.go
index 63c2201924411..9d0928933dbe6 100644
--- a/daemon/seccomp_linux_test.go
+++ b/daemon/seccomp_linux_test.go
@@ -40,7 +40,9 @@ func TestWithSeccomp(t *testing.T) {
 			outSpec: oci.DefaultLinuxSpec(),
 		},
 		{
-			comment: "privileged container w/ custom profile runs unconfined",
+			// Prior to Docker v27, it had resulted in unconfined.
+			// https://github.com/moby/moby/pull/47500
+			comment: "privileged container w/ custom profile",
 			daemon: &Daemon{
 				sysInfo: &sysinfo.SysInfo{Seccomp: true},
 			},
@@ -50,8 +52,15 @@ func TestWithSeccomp(t *testing.T) {
 					Privileged: true,
 				},
 			},
-			inSpec:  oci.DefaultLinuxSpec(),
-			outSpec: oci.DefaultLinuxSpec(),
+			inSpec: oci.DefaultLinuxSpec(),
+			outSpec: func() coci.Spec {
+				s := oci.DefaultLinuxSpec()
+				profile := &specs.LinuxSeccomp{
+					DefaultAction: specs.LinuxSeccompAction("SCMP_ACT_LOG"),
+				}
+				s.Linux.Seccomp = profile
+				return s
+			}(),
 		},
 		{
 			comment: "privileged container w/ default runs unconfined",
diff --git a/integration/container/run_linux_test.go b/integration/container/run_linux_test.go
index 867544705d829..056b55be18404 100644
--- a/integration/container/run_linux_test.go
+++ b/integration/container/run_linux_test.go
@@ -353,6 +353,51 @@ func TestWorkingDirNormalization(t *testing.T) {
 
 			assert.Check(t, is.Equal(inspect.Config.WorkingDir, "/tmp"))
 		})
+	}
+}
+
+func TestSeccomp(t *testing.T) {
+	skip.If(t, testEnv.DaemonInfo.OSType != "linux")
+
+	ctx := setupTest(t)
+	apiClient := testEnv.APIClient()
 
+	const confined = `{
+ "defaultAction": "SCMP_ACT_ALLOW",
+ "syscalls": [ { "names": [ "chown", "chown32", "fchownat" ], "action": "SCMP_ACT_ERRNO" } ]
+}
+`
+	type testCase struct {
+		ops              []func(*container.TestContainerConfig)
+		expectedExitCode int
+	}
+	testCases := []testCase{
+		{
+			ops:              nil,
+			expectedExitCode: 0,
+		},
+		{
+			ops:              []func(*container.TestContainerConfig){container.WithPrivileged(true)},
+			expectedExitCode: 0,
+		},
+		{
+			ops:              []func(*container.TestContainerConfig){container.WithSecurityOpt("seccomp=" + confined)},
+			expectedExitCode: 1,
+		},
+		{
+			// A custom profile should be still enabled, even when --privileged is set
+			// https://github.com/moby/moby/issues/47499
+			ops:              []func(*container.TestContainerConfig){container.WithPrivileged(true), container.WithSecurityOpt("seccomp=" + confined)},
+			expectedExitCode: 1,
+		},
+	}
+	for _, tc := range testCases {
+		cID := container.Run(ctx, t, apiClient, tc.ops...)
+		res, err := container.Exec(ctx, apiClient, cID, []string{"chown", "42", "/bin/true"})
+		assert.NilError(t, err)
+		assert.Equal(t, tc.expectedExitCode, res.ExitCode)
+		if tc.expectedExitCode != 0 {
+			assert.Check(t, is.Contains(res.Stderr(), "Operation not permitted"))
+		}
 	}
 }