Browse code

integration/container: Verify AF_INET socketcall still works

Add a positive test that creates an AF_INET socket via int $0x80
socketcall to ensure the AppArmor "deny network alg" rule only
blocks AF_ALG and does not break legitimate socketcall usage.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>

Paweł Gronowski authored on 2026/05/01 15:52:35
Showing 3 changed files
... ...
@@ -20,14 +20,14 @@ var (
20 20
 	//go:embed testdata/af_vsock.c
21 21
 	afVSOCKSource string
22 22
 
23
-	//go:embed testdata/af_alg_socketcall.c
24
-	afALGSocketcallSource string
23
+	//go:embed testdata/socketcall.c
24
+	socketcallSource string
25 25
 )
26 26
 
27 27
 // compileAndExecSocketDenied writes a C source file into the container,
28 28
 // compiles it with the given compiler command, runs the binary as uid 1000,
29
-// and asserts that socket creation fails with the expected error.
30
-func compileAndExecSocketDenied(ctx context.Context, t *testing.T, apiClient client.APIClient, cID string, name string, src string, cc []string, expectedErr string) {
29
+// and asserts that socket creation fails.
30
+func compileAndExecSocketDenied(ctx context.Context, t *testing.T, apiClient client.APIClient, cID string, name string, src string, cc []string) {
31 31
 	t.Helper()
32 32
 
33 33
 	binPath := "/tmp/" + name
... ...
@@ -56,8 +56,9 @@ func compileAndExecSocketDenied(ctx context.Context, t *testing.T, apiClient cli
56 56
 }
57 57
 
58 58
 // TestExecSocketDenied verifies that AF_ALG and AF_VSOCK sockets cannot be
59
-// created inside a container. These address families are blocked by the
60
-// default seccomp profile.
59
+// created inside a container. AF_ALG is blocked by the default seccomp profile
60
+// (via socket arg filtering) and by the default AppArmor profile (via
61
+// "deny network alg"). AF_VSOCK is blocked by seccomp only.
61 62
 func TestExecSocketDenied(t *testing.T) {
62 63
 	skip.If(t, testEnv.DaemonInfo.OSType != "linux")
63 64
 
... ...
@@ -78,19 +79,67 @@ func TestExecSocketDenied(t *testing.T) {
78 78
 	isAmd64 := arch == "amd64" || arch == "x86_64"
79 79
 
80 80
 	t.Run("AF_ALG", func(t *testing.T) {
81
-		compileAndExecSocketDenied(ctx, t, apiClient, cID, "AF_ALG", afALGSource, gcc, "not permitted")
81
+		compileAndExecSocketDenied(ctx, t, apiClient, cID, "AF_ALG", afALGSource, gcc)
82 82
 	})
83 83
 	t.Run("AF_VSOCK", func(t *testing.T) {
84
-		compileAndExecSocketDenied(ctx, t, apiClient, cID, "AF_VSOCK", afVSOCKSource, gcc, "not permitted")
84
+		compileAndExecSocketDenied(ctx, t, apiClient, cID, "AF_VSOCK", afVSOCKSource, gcc)
85 85
 	})
86 86
 
87
-	// Test AF_ALG via the socketcall(2) multiplexer using int $0x80 to
88
-	// invoke the ia32 compat syscall path from a native 64-bit binary.
89
-	// MAP_32BIT is used to place the args array below 4 GB, since the
90
-	// ia32 compat path truncates all registers to 32 bits.
91
-	t.Run("AF_ALG_socketcall_int80", func(t *testing.T) {
87
+	// Test socketcall(2) via int $0x80 to invoke the ia32 compat syscall
88
+	// path from a native 64-bit binary. MAP_32BIT is used to place the
89
+	// args array below 4 GB since the ia32 compat path truncates all
90
+	// registers to 32 bits.
91
+	//
92
+	// The socketcall binary is compiled with -DSOCK_FAMILY and -DSOCK_TYPE
93
+	// to set the address family and socket type at compile time.
94
+	t.Run("socketcall_int80", func(t *testing.T) {
92 95
 		skip.If(t, !isAmd64, "int $0x80 ia32 compat only available on amd64")
93 96
 
94
-		compileAndExecSocketDenied(ctx, t, apiClient, cID, "AF_ALG_socketcall_int80", afALGSocketcallSource, gcc, "not implemented")
97
+		srcPath := "/tmp/socketcall.c"
98
+		res := container.ExecT(ctx, t, apiClient, cID, []string{
99
+			"sh", "-c", "cat > " + srcPath + " << 'CEOF'\n" + socketcallSource + "\nCEOF",
100
+		})
101
+		res.AssertSuccess(t)
102
+
103
+		// AF_ALG (38) via socketcall must be denied by AppArmor's
104
+		// "deny network alg" rule, which catches it at the kernel
105
+		// socket layer even though seccomp cannot filter socketcall args.
106
+		t.Run("AF_ALG", func(t *testing.T) {
107
+			binPath := "/tmp/socketcall_af_alg"
108
+			res := container.ExecT(ctx, t, apiClient, cID, append(gcc,
109
+				"-DSOCK_FAMILY=AF_ALG", "-DSOCK_TYPE=SOCK_SEQPACKET",
110
+				"-include", "linux/if_alg.h",
111
+				srcPath, "-o", binPath,
112
+			))
113
+			res.AssertSuccess(t)
114
+
115
+			res, err := container.Exec(ctx, apiClient, cID, []string{binPath},
116
+				func(ec *client.ExecCreateOptions) {
117
+					ec.User = "1000"
118
+				},
119
+			)
120
+			assert.NilError(t, err)
121
+			assert.Check(t, is.Equal(res.ExitCode, 1), "expected AF_ALG socketcall to fail, got: %s", res.Combined())
122
+			assert.Check(t, is.Contains(strings.ToLower(res.Combined()), "permission denied"))
123
+		})
124
+
125
+		// AF_INET via socketcall must still work to ensure the deny
126
+		// rule is targeted and does not break legitimate usage.
127
+		t.Run("AF_INET", func(t *testing.T) {
128
+			binPath := "/tmp/socketcall_af_inet"
129
+			res := container.ExecT(ctx, t, apiClient, cID, append(gcc,
130
+				"-DSOCK_FAMILY=AF_INET", "-DSOCK_TYPE=SOCK_STREAM",
131
+				srcPath, "-o", binPath,
132
+			))
133
+			res.AssertSuccess(t)
134
+
135
+			res, err := container.Exec(ctx, apiClient, cID, []string{binPath},
136
+				func(ec *client.ExecCreateOptions) {
137
+					ec.User = "1000"
138
+				},
139
+			)
140
+			assert.NilError(t, err)
141
+			assert.Check(t, is.Equal(res.ExitCode, 0), "expected AF_INET socketcall to succeed, got: %s", res.Combined())
142
+		})
95 143
 	})
96 144
 }
97 145
deleted file mode 100644
... ...
@@ -1,43 +0,0 @@
1
-#include <stdio.h>
2
-#include <errno.h>
3
-#include <sys/mman.h>
4
-
5
-#define SYS_SOCKETCALL_I386 102
6
-#define SYS_SOCKET 1
7
-#define AF_ALG 38
8
-#define SOCK_SEQPACKET 5
9
-
10
-int main() {
11
-    /*
12
-     * The int $0x80 ia32 compat path truncates all registers to 32 bits.
13
-     * The args pointer must live below 4 GB, so allocate it with MAP_32BIT.
14
-     */
15
-    unsigned int *args = mmap(NULL, 4096,
16
-        PROT_READ | PROT_WRITE,
17
-        MAP_PRIVATE | MAP_ANONYMOUS | MAP_32BIT,
18
-        -1, 0);
19
-    if (args == MAP_FAILED) {
20
-        perror("mmap");
21
-        return 2;
22
-    }
23
-    args[0] = AF_ALG;
24
-    args[1] = SOCK_SEQPACKET;
25
-    args[2] = 0;
26
-
27
-    int ret;
28
-    asm volatile (
29
-        "int $0x80"
30
-        : "=a"(ret)
31
-        : "a"(SYS_SOCKETCALL_I386), "b"(SYS_SOCKET), "c"(args)
32
-        : "memory"
33
-    );
34
-
35
-    if (ret < 0) {
36
-        errno = -ret;
37
-        perror("socket");
38
-        return 1;
39
-    }
40
-
41
-    printf("AF_ALG socket created via socketcall\n");
42
-    return 0;
43
-}
44 1
new file mode 100644
... ...
@@ -0,0 +1,51 @@
0
+#include <stdio.h>
1
+#include <errno.h>
2
+#include <unistd.h>
3
+#include <sys/socket.h>
4
+#include <sys/mman.h>
5
+
6
+#define SYS_SOCKETCALL_I386 102
7
+#define SYS_SOCKET 1
8
+
9
+#ifndef SOCK_FAMILY
10
+#error "define SOCK_FAMILY via -DSOCK_FAMILY=..."
11
+#endif
12
+#ifndef SOCK_TYPE
13
+#error "define SOCK_TYPE via -DSOCK_TYPE=..."
14
+#endif
15
+
16
+int main() {
17
+    /*
18
+     * The int $0x80 ia32 compat path truncates all registers to 32 bits.
19
+     * The args pointer must live below 4 GB, so allocate it with MAP_32BIT.
20
+     */
21
+    unsigned int *args = mmap(NULL, 4096,
22
+        PROT_READ | PROT_WRITE,
23
+        MAP_PRIVATE | MAP_ANONYMOUS | MAP_32BIT,
24
+        -1, 0);
25
+    if (args == MAP_FAILED) {
26
+        perror("mmap");
27
+        return 2;
28
+    }
29
+    args[0] = SOCK_FAMILY;
30
+    args[1] = SOCK_TYPE;
31
+    args[2] = 0;
32
+
33
+    int ret;
34
+    asm volatile (
35
+        "int $0x80"
36
+        : "=a"(ret)
37
+        : "a"(SYS_SOCKETCALL_I386), "b"(SYS_SOCKET), "c"(args)
38
+        : "memory"
39
+    );
40
+
41
+    if (ret < 0) {
42
+        errno = -ret;
43
+        perror("socket");
44
+        return 1;
45
+    }
46
+
47
+    printf("socket(%d, %d, 0) via socketcall succeeded\n", SOCK_FAMILY, SOCK_TYPE);
48
+    close(ret);
49
+    return 0;
50
+}