[PATCH v2 2/2] tests: add SCTP connect mediation regression test

From: Jérémy Jean

Date: Fri Sep 25 2026 - 16:56:51 EST


Exercise SCTP connect(), sendto(), sendmsg() and all three CONNECTX
interfaces over IPv4 and IPv6. Check that connections succeed when
allowed and return EACCES when connect permission is omitted or denied.

Assisted-by: LLM
Signed-off-by: Jérémy Jean <Jeremy.Jean@xxxxxxxxxxxxxxxxx>
---
tests/regression/apparmor/Makefile | 2 +
tests/regression/apparmor/net_inet_sctp.c | 115 +++++++++++++++++++++
tests/regression/apparmor/net_inet_sctp.sh | 56 ++++++++++
3 files changed, 173 insertions(+)
create mode 100644 tests/regression/apparmor/net_inet_sctp.c
create mode 100755 tests/regression/apparmor/net_inet_sctp.sh

diff --git a/tests/regression/apparmor/Makefile b/tests/regression/apparmor/Makefile
index 18e408f5cd88..c90865f94fe7 100644
--- a/tests/regression/apparmor/Makefile
+++ b/tests/regression/apparmor/Makefile
@@ -150,6 +150,7 @@ SRC=access.c \
mount.c \
named_pipe.c \
net_inet_rcv.c \
+ net_inet_sctp.c \
net_inet_snd.c \
net_inet_tcp_fastopen.c \
net_raw.c \
@@ -326,6 +327,7 @@ TESTS=aa_exec \
namespaces \
net_iface \
net_inet \
+ net_inet_sctp \
net_inet_tcp_fastopen \
net_raw \
overlayfs_kernel \
diff --git a/tests/regression/apparmor/net_inet_sctp.c b/tests/regression/apparmor/net_inet_sctp.c
new file mode 100644
index 000000000000..3e6cf4067527
--- /dev/null
+++ b/tests/regression/apparmor/net_inet_sctp.c
@@ -0,0 +1,115 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/* Copyright (C) 2026 Jérémy Jean <Jeremy.Jean@xxxxxxxxxxxxxxxxx> */
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <netinet/in.h>
+#include <sys/socket.h>
+#include <sys/uio.h>
+#include <linux/sctp.h>
+
+static void check(int ok, const char *operation)
+{
+ if (!ok) {
+ printf("FAIL: %s (errno %d)\n", operation, errno);
+ exit(1); /* Setup errors must not satisfy an expected EACCES. */
+ }
+}
+
+int main(int argc, char *argv[])
+{
+ struct sockaddr_in in = {
+ .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK),
+ };
+ struct sockaddr_in6 in6 = {
+ .sin6_family = AF_INET6, .sin6_addr = IN6ADDR_LOOPBACK_INIT,
+ };
+ struct sockaddr *addr;
+ socklen_t addrlen;
+ const char *mode;
+ int family, client, listener;
+ ssize_t ret;
+ char payload = 'x', buffer[2];
+
+ check(argc == 3, "usage: net_inet_sctp inet|inet6 MODE");
+ check(!strcmp(argv[1], "inet") || !strcmp(argv[1], "inet6"), "family");
+ mode = argv[2];
+ family = strcmp(argv[1], "inet") == 0 ? AF_INET : AF_INET6;
+ alarm(15);
+
+ client = socket(family, SOCK_SEQPACKET, IPPROTO_SCTP);
+ if (client == -1 && !strcmp(mode, "probe") &&
+ (errno == EPROTONOSUPPORT || errno == ESOCKTNOSUPPORT ||
+ errno == EAFNOSUPPORT))
+ return 77;
+ check(client >= 0, "client socket");
+ if (!strcmp(mode, "probe")) {
+ close(client);
+ return 0;
+ }
+
+ /* Let the kernel choose a free loopback port for each test. */
+ addr = family == AF_INET ? (struct sockaddr *)&in : (struct sockaddr *)&in6;
+ addrlen = family == AF_INET ? sizeof(in) : sizeof(in6);
+ listener = socket(family, SOCK_SEQPACKET, IPPROTO_SCTP);
+ check(listener >= 0, "listener socket");
+ check(bind(listener, addr, addrlen) == 0, "bind");
+ check(getsockname(listener, addr, &addrlen) == 0, "getsockname");
+ check(listen(listener, 1) == 0, "listen");
+
+ if (!strcmp(mode, "connect")) {
+ ret = connect(client, addr, addrlen);
+ } else if (!strcmp(mode, "sendto")) {
+ ret = sendto(client, &payload, 1, MSG_NOSIGNAL,
+ addr, addrlen);
+ } else if (!strcmp(mode, "sendmsg")) {
+ struct iovec iov = { .iov_base = &payload, .iov_len = 1 };
+ struct msghdr msg = {
+ .msg_name = addr,
+ .msg_namelen = addrlen,
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ };
+
+ ret = sendmsg(client, &msg, MSG_NOSIGNAL);
+ } else if (!strcmp(mode, "connectx3")) {
+ struct sctp_getaddrs_old args = {
+ /* This ABI takes the address length in bytes. */
+ .addr_num = addrlen,
+ .addrs = addr,
+ };
+ socklen_t optlen = sizeof(args);
+
+ ret = getsockopt(client, IPPROTO_SCTP, SCTP_SOCKOPT_CONNECTX3,
+ &args, &optlen);
+ } else if (!strcmp(mode, "connectx_old") || !strcmp(mode, "connectx")) {
+ int opt = !strcmp(mode, "connectx_old") ?
+ SCTP_SOCKOPT_CONNECTX_OLD : SCTP_SOCKOPT_CONNECTX;
+
+ /* CONNECTX can return a positive association ID on success. */
+ ret = setsockopt(client, IPPROTO_SCTP, opt, addr, addrlen);
+ } else {
+ printf("FAIL: unknown operation %s\n", mode);
+ return 1;
+ }
+
+ if (ret == -1) {
+ int error = errno;
+
+ printf("FAIL: %s: %s\n", mode, strerror(error));
+ return error; /* Checked by runchecktest_errno. */
+ }
+ if (strcmp(mode, "sendto") && strcmp(mode, "sendmsg"))
+ ret = sendto(client, &payload, 1, MSG_NOSIGNAL, addr, addrlen);
+ check(ret == 1, "send payload");
+ check(recv(listener, buffer, sizeof(buffer), 0) == 1 &&
+ buffer[0] == payload, "receive payload");
+
+ close(client);
+ close(listener);
+ printf("PASS\n");
+ return 0;
+}
diff --git a/tests/regression/apparmor/net_inet_sctp.sh b/tests/regression/apparmor/net_inet_sctp.sh
new file mode 100755
index 000000000000..e3c81e2ee814
--- /dev/null
+++ b/tests/regression/apparmor/net_inet_sctp.sh
@@ -0,0 +1,56 @@
+#! /bin/bash
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (C) 2026 Jérémy Jean <Jeremy.Jean@xxxxxxxxxxxxxxxxx>
+
+#=NAME net_inet_sctp
+#=DESCRIPTION
+# SCTP association creation must require connect permission, including
+# implicit connections through sendto/sendmsg and the CONNECTX socket options.
+#=END
+
+bin=$(cd "$(dirname "$0")" && pwd)
+. "$bin/prologue.inc"
+
+requires_any_of_kernel_features network/af_mask network_v8/af_mask network_v9/af_mask
+# Probe the same ABI that genprofile selects with "abi <kernel>".
+requires_parser_support --policy-features=/sys/kernel/security/apparmor/features \
+ --warn=rule-downgraded --Werror=rule-downgraded \
+ --warn=rule-not-enforced --Werror=rule-not-enforced -- \
+ "network (create,bind,listen,send,receive,setopt,getopt,getattr) inet seqpacket," \
+ "deny network connect inet seqpacket,"
+
+run_modes()
+{
+ local description=$1 expected=$2 error=$3 mode
+
+ for mode in connect sendto sendmsg connectx_old connectx connectx3; do
+ runchecktest_errno "$error" "SCTP $family $mode ($description)" \
+ "$expected" "$family" "$mode"
+ done
+}
+
+for family in inet inet6; do
+ # Probe without a profile so policy denials cannot cause a skip.
+ probe_rc=0
+ "$test" "$family" probe || probe_rc=$?
+ if [ "$probe_rc" -eq 77 ]; then
+ echo " SCTP $family not available. Skipping tests ..."
+ continue
+ fi
+ [ "$probe_rc" -eq 0 ] || fatalerror "SCTP $family probe failed"
+
+ run_modes unconfined pass 0
+
+ genprofile "network;$family;seqpacket"
+ run_modes "connect allowed" pass 0
+
+ # Grant all the other permissions used by the client and listener.
+ genprofile "network;(create,bind,listen,send,receive,setopt,getopt,getattr);$family;seqpacket"
+ run_modes "connect omitted" fail EACCES
+
+ genprofile "network;$family;seqpacket" \
+ "qual=deny:network;connect;$family;seqpacket"
+ run_modes "connect denied" fail EACCES
+
+ removeprofile
+done
--
2.47.3