git: 8f6d8a901671 - main - tests/netinet/socket_afinet: unroll multibind test into a table
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
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);
+ }
+ }
}
/*