git: 8f6d8a901671 - main - tests/netinet/socket_afinet: unroll multibind test into a table

From: Gleb Smirnoff <glebius_at_FreeBSD.org>
Date: Mon, 10 Aug 2026 20:51:06 UTC
The branch main has been updated by glebius:

URL: https://cgit.FreeBSD.org/src/commit/?id=8f6d8a901671164360dee8fe40414006c8e9eaef

commit 8f6d8a901671164360dee8fe40414006c8e9eaef
Author:     Gleb Smirnoff <glebius@FreeBSD.org>
AuthorDate: 2026-08-10 20:43:59 +0000
Commit:     Gleb Smirnoff <glebius@FreeBSD.org>
CommitDate: 2026-08-10 20:50:22 +0000

    tests/netinet/socket_afinet: unroll multibind test into a table
    
    The test has 6 dimensions: address family, socket type, socket option on
    the first socket, socket option on the second socket, is first socket
    bound to specific address or wildcard and is the second socket priveleged
    or not.  Before the change 3 dimensions are implemented as 3 nested for()
    loops, 2 dimensions are implemented as repetitions in the test body and
    one dimension as two actions in the innermost loop.
    
    I'm about to add one more dimension: whether the second socket is bound to
    a specific address or wildcard instead of using first socket's
    getsockopt(2) result.  Also, there is a change under discussion that would
    make SOCK_STREAM sockets behave different to SOCK_DGRAM.  That would break
    result consistency in the dimensions of socket type.
    
    We expect that consistency in the dimension of address families shall
    never break, thus this one remains a for() loop.  The priveleged & non-
    privileged bind(2) attempts also remain as two actions, but expected
    results are in the table.  The rest of dimensions are unrolled into a
    table, which at the moment has quite a lot of lines with identical
    results. However, as more tests are added and SOCK_STREAM behavior
    changes, the table will get more mixed results.  Also, reading a test that
    is written in a declarative manner a table is much easier and modifying it
    is more resistent to accidential breakage.
    
    Differential Revision:  https://reviews.freebsd.org/D58085
---
 tests/sys/netinet/socket_afinet.c | 273 +++++++++++++++++++++++---------------
 1 file changed, 165 insertions(+), 108 deletions(-)

diff --git a/tests/sys/netinet/socket_afinet.c b/tests/sys/netinet/socket_afinet.c
index 98cc2be00f25..787c340466b0 100644
--- a/tests/sys/netinet/socket_afinet.c
+++ b/tests/sys/netinet/socket_afinet.c
@@ -346,7 +346,7 @@ static enum bind_res {
 	BIND_REUSE_SUCCESS = 6,
 	BIND_BADERR2 = 7,
 }
-child_bind(const atf_tc_t *tc, int type, struct sockaddr *sa, int opt,
+child_bind(const atf_tc_t *tc, int type, const struct sockaddr *sa, int opt,
     bool unpriv)
 {
 	const char *user;
@@ -400,109 +400,6 @@ child_bind(const atf_tc_t *tc, int type, struct sockaddr *sa, int opt,
 	}
 }
 
-static enum bind_res
-child_bind_priv(const atf_tc_t *tc, int type, struct sockaddr *sa, int opt)
-{
-	return (child_bind(tc, type, sa, opt, false));
-}
-
-static enum bind_res
-child_bind_unpriv(const atf_tc_t *tc, int type, struct sockaddr *sa, int opt)
-{
-	return (child_bind(tc, type, sa, opt, true));
-}
-
-static int
-bind_socket(int domain, int type, int opt, bool unspec, struct sockaddr *sa)
-{
-	socklen_t slen;
-	int s;
-
-	s = socket(domain, type, 0);
-	ATF_REQUIRE(s >= 0);
-
-	if (domain == AF_INET) {
-		struct sockaddr_in sin;
-
-		bzero(&sin, sizeof(sin));
-		sin.sin_family = AF_INET;
-		sin.sin_len = sizeof(sin);
-		sin.sin_addr.s_addr = htonl(unspec ?
-		    INADDR_ANY : INADDR_LOOPBACK);
-		sin.sin_port = htons(0);
-		ATF_REQUIRE(bind(s, (struct sockaddr *)&sin, sizeof(sin)) == 0);
-
-		slen = sizeof(sin);
-	} else /* if (domain == AF_INET6) */ {
-		struct sockaddr_in6 sin6;
-
-		bzero(&sin6, sizeof(sin6));
-		sin6.sin6_family = AF_INET6;
-		sin6.sin6_len = sizeof(sin6);
-		sin6.sin6_addr = unspec ? in6addr_any : in6addr_loopback;
-		sin6.sin6_port = htons(0);
-		ATF_REQUIRE(bind(s, (struct sockaddr *)&sin6, sizeof(sin6)) == 0);
-
-		slen = sizeof(sin6);
-	}
-
-	if (opt != 0) {
-		ATF_REQUIRE(setsockopt(s, SOL_SOCKET, opt, &(int){1},
-		    sizeof(int)) == 0);
-	}
-
-	ATF_REQUIRE(getsockname(s, sa, &slen) == 0);
-
-	return (s);
-}
-
-static void
-multibind_test(const atf_tc_t *tc, int domain, int type)
-{
-	struct sockaddr_storage ss;
-	int opts[4] = { 0, SO_REUSEADDR, SO_REUSEPORT, SO_REUSEPORT_LB };
-	int s;
-	bool flags[2] = { false, true };
-	enum bind_res res;
-
-	for (size_t flagi = 0; flagi < nitems(flags); flagi++) {
-		for (size_t opti = 0; opti < nitems(opts); opti++) {
-			s = bind_socket(domain, type, opts[opti], flags[flagi],
-			    (struct sockaddr *)&ss);
-			for (size_t optj = 0; optj < nitems(opts); optj++) {
-				int opt;
-
-				opt = opts[optj];
-				res = child_bind_priv(tc, type,
-				    (struct sockaddr *)&ss, opt);
-				/*
-				 * Multi-binding is only allowed when both
-				 * sockets have SO_REUSEPORT or SO_REUSEPORT_LB
-				 * set.
-				 */
-				if (opts[opti] != 0 &&
-				    opts[opti] != SO_REUSEADDR && opti == optj)
-					ATF_REQUIRE(res == BIND_REUSE_SUCCESS);
-				else
-					ATF_REQUIRE(res == BIND_FAILED);
-
-				res = child_bind_unpriv(tc, type,
-				    (struct sockaddr *)&ss, opt);
-				/*
-				 * Multi-binding is only allowed when both
-				 * sockets have the same owner.
-				 */
-				ATF_REQUIRE_MSG(res == BIND_FAILED,
-				    "domain %u type %u opts %u:%u: "
-				    "result %u (expected %u)",
-				    domain, type, opts[opti], opts[optj],
-				    res, BIND_FAILED);
-			}
-			ATF_REQUIRE(close(s) == 0);
-		}
-	}
-}
-
 /*
  * Try to bind two sockets to the same address/port tuple.  Under some
  * conditions this is permitted.
@@ -513,12 +410,172 @@ ATF_TC_HEAD(multibind, tc)
 	atf_tc_set_md_var(tc, "require.user", "root");
 	atf_tc_set_md_var(tc, "require.config", "unprivileged_user");
 }
+
 ATF_TC_BODY(multibind, tc)
 {
-	multibind_test(tc, AF_INET, SOCK_STREAM);
-	multibind_test(tc, AF_INET, SOCK_DGRAM);
-	multibind_test(tc, AF_INET6, SOCK_STREAM);
-	multibind_test(tc, AF_INET6, SOCK_DGRAM);
+	const struct {
+		int type;
+		int opt1;
+		bool wild1;
+		int opt2;
+		bool wild2;	/* not exercised yet, uses getsockname() rv */
+		enum bind_res priv_res;
+		enum bind_res unpriv_res;
+	} tests[] = {
+#define	ADDR	SO_REUSEADDR
+#define	PORT	SO_REUSEPORT
+#define	LB	SO_REUSEPORT_LB
+#define	x true
+#define	o false
+#define	F BIND_FAILED
+#define	R BIND_REUSE_SUCCESS
+		/*                               p  u */
+		{ SOCK_STREAM,    0, o,    0, o, F, F },
+		{ SOCK_STREAM,    0, o, ADDR, o, F, F },
+		{ SOCK_STREAM,    0, o, PORT, o, F, F },
+		{ SOCK_STREAM,    0, o,   LB, o, F, F },
+		{ SOCK_STREAM, ADDR, o,    0, o, F, F },
+		{ SOCK_STREAM, ADDR, o, ADDR, o, F, F },
+		{ SOCK_STREAM, ADDR, o, PORT, o, F, F },
+		{ SOCK_STREAM, ADDR, o,   LB, o, F, F },
+		{ SOCK_STREAM, PORT, o,    0, o, F, F },
+		{ SOCK_STREAM, PORT, o, ADDR, o, F, F },
+		{ SOCK_STREAM, PORT, o, PORT, o, R, F },
+		{ SOCK_STREAM, PORT, o,   LB, o, F, F },
+		{ SOCK_STREAM,   LB, o,    0, o, F, F },
+		{ SOCK_STREAM,   LB, o, ADDR, o, F, F },
+		{ SOCK_STREAM,   LB, o, PORT, o, F, F },
+		{ SOCK_STREAM,   LB, o,   LB, o, R, F },
+		/*
+		 * ATM, expected result with wildcard first bind(2) is the same
+		 * as the result for the specified first bind(2).  Thus, the
+		 * below block is copy-and-paste of the above with wild1 set.
+		 */
+		{ SOCK_STREAM,    0, x,    0, o, F, F },
+		{ SOCK_STREAM,    0, x, ADDR, o, F, F },
+		{ SOCK_STREAM,    0, x, PORT, o, F, F },
+		{ SOCK_STREAM,    0, x,   LB, o, F, F },
+		{ SOCK_STREAM, ADDR, x,    0, o, F, F },
+		{ SOCK_STREAM, ADDR, x, ADDR, o, F, F },
+		{ SOCK_STREAM, ADDR, x, PORT, o, F, F },
+		{ SOCK_STREAM, ADDR, x,   LB, o, F, F },
+		{ SOCK_STREAM, PORT, x,    0, o, F, F },
+		{ SOCK_STREAM, PORT, x, ADDR, o, F, F },
+		{ SOCK_STREAM, PORT, x, PORT, o, R, F },
+		{ SOCK_STREAM, PORT, x,   LB, o, F, F },
+		{ SOCK_STREAM,   LB, x,    0, o, F, F },
+		{ SOCK_STREAM,   LB, x, ADDR, o, F, F },
+		{ SOCK_STREAM,   LB, x, PORT, o, F, F },
+		{ SOCK_STREAM,   LB, x,   LB, o, R, F },
+		/*
+		 * ATM, expected result for SOCK_DGRAM is the same as for
+		 * SOCK_STREAM.  Thus the below is copy-and-paste of the above.
+		 */
+		{ SOCK_DGRAM,    0, o,    0, o, F, F },
+		{ SOCK_DGRAM,    0, o, ADDR, o, F, F },
+		{ SOCK_DGRAM,    0, o, PORT, o, F, F },
+		{ SOCK_DGRAM,    0, o,   LB, o, F, F },
+		{ SOCK_DGRAM, ADDR, o,    0, o, F, F },
+		{ SOCK_DGRAM, ADDR, o, ADDR, o, F, F },
+		{ SOCK_DGRAM, ADDR, o, PORT, o, F, F },
+		{ SOCK_DGRAM, ADDR, o,   LB, o, F, F },
+		{ SOCK_DGRAM, PORT, o,    0, o, F, F },
+		{ SOCK_DGRAM, PORT, o, ADDR, o, F, F },
+		{ SOCK_DGRAM, PORT, o, PORT, o, R, F },
+		{ SOCK_DGRAM, PORT, o,   LB, o, F, F },
+		{ SOCK_DGRAM,   LB, o,    0, o, F, F },
+		{ SOCK_DGRAM,   LB, o, ADDR, o, F, F },
+		{ SOCK_DGRAM,   LB, o, PORT, o, F, F },
+		{ SOCK_DGRAM,   LB, o,   LB, o, R, F },
+		/* wild1 -> true */
+		{ SOCK_DGRAM,    0, x,    0, o, F, F },
+		{ SOCK_DGRAM,    0, x, ADDR, o, F, F },
+		{ SOCK_DGRAM,    0, x, PORT, o, F, F },
+		{ SOCK_DGRAM,    0, x,   LB, o, F, F },
+		{ SOCK_DGRAM, ADDR, x,    0, o, F, F },
+		{ SOCK_DGRAM, ADDR, x, ADDR, o, F, F },
+		{ SOCK_DGRAM, ADDR, x, PORT, o, F, F },
+		{ SOCK_DGRAM, ADDR, x,   LB, o, F, F },
+		{ SOCK_DGRAM, PORT, x,    0, o, F, F },
+		{ SOCK_DGRAM, PORT, x, ADDR, o, F, F },
+		{ SOCK_DGRAM, PORT, x, PORT, o, R, F },
+		{ SOCK_DGRAM, PORT, x,   LB, o, F, F },
+		{ SOCK_DGRAM,   LB, x,    0, o, F, F },
+		{ SOCK_DGRAM,   LB, x, ADDR, o, F, F },
+		{ SOCK_DGRAM,   LB, x, PORT, o, F, F },
+		{ SOCK_DGRAM,   LB, x,   LB, o, R, F },
+#undef F
+#undef R
+#undef x
+#undef o
+#undef ADDR
+#undef PORT
+#undef LB
+	};
+	/*
+	 * Expected results for IPv4 and IPv6 shall always be the same, so
+	 * this dimension is implemented as a cycle rather than table entry.
+	 */
+	const union sockaddr_union {
+		struct sockaddr_in sin;
+		struct sockaddr_in6 sin6;
+		struct sockaddr sa;
+	} wild[] = {
+		{
+		    .sin.sin_family = AF_INET,
+		    .sin.sin_len = sizeof(struct sockaddr_in),
+		},
+		{
+		    .sin6.sin6_family = AF_INET6,
+		    .sin6.sin6_len = sizeof(struct sockaddr_in6),
+		},
+	}, loop[] = {
+		{
+		    .sin.sin_family = AF_INET,
+		    .sin.sin_len = sizeof(struct sockaddr_in),
+		    .sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
+		},
+		{
+		    .sin6.sin6_family = AF_INET6,
+		    .sin6.sin6_len = sizeof(struct sockaddr_in6),
+		    .sin6.sin6_addr = in6addr_loopback,
+		},
+	};
+
+	_Static_assert(nitems(wild) == nitems(loop), "EDOOFUS");
+	for (u_int af = 0; af < nitems(wild); af++) {
+		for (u_int i = 0; i < nitems(tests); i++) {
+			const struct sockaddr *sa;
+			union sockaddr_union su;
+			socklen_t slen;
+			enum bind_res res;
+			int s;
+
+			s = socket(wild[af].sa.sa_family, tests[i].type, 0);
+			ATF_REQUIRE(s >= 0);
+			sa = tests[i].wild1 ? &wild[af].sa : &loop[af].sa;
+			slen = sa->sa_len;
+			ATF_REQUIRE(bind(s, sa, slen) == 0);
+			if (tests[i].opt1 != 0)
+				ATF_REQUIRE(setsockopt(s, SOL_SOCKET,
+				    tests[i].opt1, &(int){1}, sizeof(int)) ==
+				    0);
+			ATF_REQUIRE(getsockname(s, &su.sa, &slen) == 0);
+
+			sa = &su.sa;
+			res = child_bind(tc, tests[i].type, sa, tests[i].opt2,
+			    false);
+			ATF_REQUIRE_MSG(tests[i].priv_res == res,
+			    "af %d test #%d (priv) failed", sa->sa_family, i);
+
+			res = child_bind(tc, tests[i].type, sa, tests[i].opt2,
+			    true);
+			ATF_REQUIRE_MSG(tests[i].unpriv_res == res,
+			    "af %d test #%d (unpriv) failed", sa->sa_family, i);
+
+			ATF_REQUIRE(close(s) == 0);
+		}
+	}
 }
 
 /*