Browse code

integration/container: Add socketcall AF_ALG denial tests

Test that AF_ALG is also denied through the socketcall(2) multiplexer,
which is used by glibc on i386 instead of direct socket(2) syscalls.

Two subtests:
- AF_ALG_socketcall_int80: uses int $0x80 inline assembly from a native
64-bit binary to invoke the ia32 socketcall path, with MAP_32BIT to
keep the args pointer below 4 GB (ia32 compat truncates registers).
- AF_ALG_socketcall_i386: cross-compiles a static i386 binary using
gcc-i686-linux-gnu where glibc naturally routes socket() through
socketcall(2).

Both are amd64-only.

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

Paweł Gronowski authored on 2026/05/01 08:01:47
Showing 2 changed files
... ...
@@ -19,13 +19,15 @@ var (
19 19
 
20 20
 	//go:embed testdata/af_vsock.c
21 21
 	afVSOCKSource string
22
+
23
+	//go:embed testdata/af_alg_socketcall.c
24
+	afALGSocketcallSource string
22 25
 )
23 26
 
24 27
 // compileAndExecSocketDenied writes a C source file into the container,
25 28
 // compiles it with the given compiler command, runs the binary as uid 1000,
26
-// and asserts that socket creation fails with a permission or
27
-// address-family error (not EFAULT or other unrelated failures).
28
-func compileAndExecSocketDenied(ctx context.Context, t *testing.T, apiClient client.APIClient, cID string, name string, src string, cc []string) {
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 31
 	t.Helper()
30 32
 
31 33
 	binPath := "/tmp/" + name
... ...
@@ -50,12 +52,7 @@ func compileAndExecSocketDenied(ctx context.Context, t *testing.T, apiClient cli
50 50
 
51 51
 	out := strings.ToLower(res.Combined())
52 52
 	assert.Check(t, is.Contains(out, "socket"), "expected socket-related error message")
53
-
54
-	// Seccomp blocks return either EPERM ("operation not permitted") or
55
-	// EAFNOSUPPORT ("address family not supported"). Make sure the failure
56
-	// is from seccomp, not from a bogus pointer (EFAULT) or other issue.
57
-	permErr := strings.Contains(out, "not permitted") || strings.Contains(out, "not supported")
58
-	assert.Check(t, permErr, "expected EPERM or EAFNOSUPPORT, got: %s", res.Combined())
53
+	assert.Check(t, is.Contains(out, expectedErr), "expected %s, got: %s", expectedErr, res.Combined())
59 54
 }
60 55
 
61 56
 // TestExecSocketDenied verifies that AF_ALG and AF_VSOCK sockets cannot be
... ...
@@ -77,10 +74,39 @@ func TestExecSocketDenied(t *testing.T) {
77 77
 
78 78
 	gcc := []string{"gcc"}
79 79
 
80
+	arch := testEnv.DaemonInfo.Architecture
81
+	isAmd64 := arch == "amd64" || arch == "x86_64"
82
+
80 83
 	t.Run("AF_ALG", func(t *testing.T) {
81
-		compileAndExecSocketDenied(ctx, t, apiClient, cID, "AF_ALG", afALGSource, gcc)
84
+		compileAndExecSocketDenied(ctx, t, apiClient, cID, "AF_ALG", afALGSource, gcc, "not permitted")
82 85
 	})
83 86
 	t.Run("AF_VSOCK", func(t *testing.T) {
84
-		compileAndExecSocketDenied(ctx, t, apiClient, cID, "AF_VSOCK", afVSOCKSource, gcc)
87
+		compileAndExecSocketDenied(ctx, t, apiClient, cID, "AF_VSOCK", afVSOCKSource, gcc, "not permitted")
88
+	})
89
+
90
+	// Test AF_ALG via the socketcall(2) multiplexer using int $0x80 to
91
+	// invoke the ia32 compat syscall path from a native 64-bit binary.
92
+	// MAP_32BIT is used to place the args array below 4 GB, since the
93
+	// ia32 compat path truncates all registers to 32 bits.
94
+	t.Run("AF_ALG_socketcall_int80", func(t *testing.T) {
95
+		skip.If(t, !isAmd64, "int $0x80 ia32 compat only available on amd64")
96
+
97
+		compileAndExecSocketDenied(ctx, t, apiClient, cID, "AF_ALG_socketcall_int80", afALGSocketcallSource, gcc, "not implemented")
98
+	})
99
+
100
+	// Test AF_ALG with a real i386 binary cross-compiled from amd64. glibc
101
+	// on i386 routes socket() through the socketcall(2) multiplexer, which
102
+	// is a different seccomp path than the native socket(2) syscall.
103
+	t.Run("AF_ALG_socketcall_i386", func(t *testing.T) {
104
+		skip.If(t, !isAmd64, "i386 cross-compilation only available on amd64")
105
+
106
+		res := container.ExecT(ctx, t, apiClient, cID, []string{
107
+			"sh", "-c", "apt-get install -y --no-install-recommends gcc-i686-linux-gnu libc6-dev-i386-cross linux-libc-dev-i386-cross",
108
+		})
109
+		res.AssertSuccess(t)
110
+
111
+		compileAndExecSocketDenied(ctx, t, apiClient, cID, "AF_ALG_socketcall_i386", afALGSource,
112
+			[]string{"i686-linux-gnu-gcc", "-static"}, "not implemented",
113
+		)
85 114
 	})
86 115
 }
87 116
new file mode 100644
... ...
@@ -0,0 +1,43 @@
0
+#include <stdio.h>
1
+#include <errno.h>
2
+#include <sys/mman.h>
3
+
4
+#define SYS_SOCKETCALL_I386 102
5
+#define SYS_SOCKET 1
6
+#define AF_ALG 38
7
+#define SOCK_SEQPACKET 5
8
+
9
+int main() {
10
+    /*
11
+     * The int $0x80 ia32 compat path truncates all registers to 32 bits.
12
+     * The args pointer must live below 4 GB, so allocate it with MAP_32BIT.
13
+     */
14
+    unsigned int *args = mmap(NULL, 4096,
15
+        PROT_READ | PROT_WRITE,
16
+        MAP_PRIVATE | MAP_ANONYMOUS | MAP_32BIT,
17
+        -1, 0);
18
+    if (args == MAP_FAILED) {
19
+        perror("mmap");
20
+        return 2;
21
+    }
22
+    args[0] = AF_ALG;
23
+    args[1] = SOCK_SEQPACKET;
24
+    args[2] = 0;
25
+
26
+    int ret;
27
+    asm volatile (
28
+        "int $0x80"
29
+        : "=a"(ret)
30
+        : "a"(SYS_SOCKETCALL_I386), "b"(SYS_SOCKET), "c"(args)
31
+        : "memory"
32
+    );
33
+
34
+    if (ret < 0) {
35
+        errno = -ret;
36
+        perror("socket");
37
+        return 1;
38
+    }
39
+
40
+    printf("AF_ALG socket created via socketcall\n");
41
+    return 0;
42
+}