Repository navigation
Conversation
|
Hi, gentle nudge on this when someone has a chance to take a look. Happy to rework anything, and to rebase if it has drifted. Written with the help of an agent (Claude Code, claude-opus-5-5). |
| } | ||
|
|
||
| // peerGroupsGetter is implemented by sockets that support SO_PEERGROUPS. | ||
| type peerGroupsGetter interface { |
There was a problem hiding this comment.
Why use an interface with a single implementer ?
aaltinaydev
left a comment
There was a problem hiding this comment.
[🤖] 2 issues found:
- [minor · linux-compat] Failed connect(2) overwrites peer groups with the caller's own groups (
pkg/sentry/socket/unix/unix.go:553, inline) - [minor · linux-compat] SO_PEERGROUPS reports the listener's own groups on SOCK_SEQPACKET connections (
pkg/sentry/socket/unix/unix.go:558, inline)
| pCreds := s.ep.PeerCreds() | ||
| if pCreds == nil { | ||
| // Unlike SO_PEERCRED, Linux fails with ENODATA if there is no peer. | ||
| return nil, syserr.ErrNoDataAvailable | ||
| } |
There was a problem hiding this comment.
[🤖] [minor · linux-compat] Failed connect(2) overwrites peer groups with the caller's own groups
GetPeerGroups returns ENODATA only when no peer credentials are stored. But Socket.Connect stores the caller's credentials before trying to connect and never undoes this on failure. So after connect(2) fails with ECONNREFUSED, SO_PEERGROUPS returns the caller's own groups instead of ENODATA. After a connect(2) on an already connected socket fails with EISCONN, it reports the caller's current groups instead of the peer's. Linux sets peer credentials only once the connection is established. This makes the ENODATA behavior added here unreliable; SO_PEERCRED has the same pre-existing problem.
Suggested fix: Change the client's peer credentials only after the connection is committed. For example, pass control.MakeCreds(t) into Endpoint.Connect and assign ce.peerCreds = e.peerCreds; ne.peerCreds = clientCreds in BidirectionalConnect's commit branch, under both endpoint locks; this also fixes the SOCK_SEQPACKET case. A smaller change in Socket.Connect also makes the tests pass (verified), but it races with concurrent connect(2) calls on the same socket:
old := s.ep.PeerCreds()
s.ep.SetPeerCreds(control.MakeCreds(t))
err = s.ep.Connect(t, ep)
if err != nil {
s.ep.SetPeerCreds(old)
}Details (evidence, reproduction and test code)
Evidence: GetPeerGroups returns ENODATA only when PeerCreds() is nil. Socket.Connect (unix.go:643-647) calls s.ep.SetPeerCreds(control.MakeCreds(t)) before s.ep.Connect. The error returns in BidirectionalConnect (ErrAlreadyConnected, ErrConnectionRefused, ErrTryAgain, ErrWrongProtocolForSocket) leave that value in place.
Linux v6.12 unix_stream_connect() (net/unix/af_unix.c) returns -ECONNREFUSED (when other->sk_state != TCP_LISTEN) and -EISCONN before it reaches init_peercred()/copy_peercred(). Those run only after the "The way is open!" point, so a failed call never changes sk_peer_cred.
Tests in test/syscalls/linux/socket_unix_peercred.cc:
UnixSocketPeerGroupsTest.FailedConnectLeavesNoPeerGroupsconnects to a bound socket that is not listening (ECONNREFUSED), then reads SO_PEERGROUPS. Linux returns ENODATA; gVisor returns success.UnixSocketPeerGroupsTest.ConnectEISCONNKeepsPeerGroupssets listener groups {4001}, connects, calls setgroups({4002}), then connects again (EISCONN). Linux still reports {4001}; gVisor reports {4002}.
The SO_PEERCRED version of the first test also fails at the merge-base: it gets the caller's own pid/uid/gid instead of 0/-1/-1.
Repro: bazel build //test/syscalls/linux:socket_unix_pair_test && sudo bazel-bin/test/syscalls/linux/socket_unix_pair_test --gtest_filter='UnixSocketPeerGroupsTest.*' passes on Linux. bazel test //test/syscalls:socket_unix_pair_test_runsc_systrap_directfs fails.
Reproduced with a test (test/syscalls/linux/socket_unix_peercred.cc): 'bazel build //test/syscalls/linux:socket_unix_pair_test && ./bazel-bin/test/syscalls/linux/socket_unix_pair_test --gtest_filter="UnixSocketPeerGroupsTest.*:*PeerGroups*"; bazel test //test/syscalls:socket_unix_pair_test_runsc_systrap_directfs --nocache_test_results' ; fix check: bazel test //test/syscalls:socket_unix_pair_test_runsc_systrap_directfs --nocache_test_results → Both tests fail under runsc on the PR head: getsockopt succeeds instead of returning ENODATA, and the client reports {4002} instead of {4001}. Both pass on native Linux and under runsc with the suggested fix.
Test code:
diff --git a/test/syscalls/linux/socket_unix_peercred.cc b/test/syscalls/linux/socket_unix_peercred.cc
index de71b0243..9ff88af5b 100644
--- a/test/syscalls/linux/socket_unix_peercred.cc
+++ b/test/syscalls/linux/socket_unix_peercred.cc
@@ -20,6 +20,7 @@
#include <linux/capability.h>
#include <sched.h>
#include <signal.h>
+#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
@@ -33,6 +34,7 @@
#include <array>
#include <atomic>
#include <memory>
+#include <string>
#include <vector>
#include "gmock/gmock.h"
@@ -661,6 +663,116 @@ TEST(UnixSocketPeerGroupsTest, NotUnixSocket) {
SyscallFailsWithErrno(ENODATA));
}
+std::string GroupsString(const std::vector<gid_t>& groups) {
+ std::string s = "{";
+ for (size_t i = 0; i < groups.size(); i++) {
+ if (i > 0) {
+ s += ",";
+ }
+ s += std::to_string(groups[i]);
+ }
+ return s + "}";
+}
+
+// connect(2) on a SOCK_SEQPACKET socket sets the peer credentials like it does
+// for SOCK_STREAM: the accepted socket reports the client's groups and the
+// client reports the groups the listener had at listen(2).
+TEST(UnixSocketPeerGroupsTest, SeqpacketConnectReportsPeerGroups) {
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SETGID)));
+
+ const std::vector<gid_t> listener_groups = {3001};
+ const std::vector<gid_t> client_groups = {3002, 3003};
+ const auto fn = [&] {
+ TEST_PCHECK(setgroups(listener_groups.size(), listener_groups.data()) ==
+ 0);
+ struct sockaddr_un addr =
+ TEST_CHECK_NO_ERRNO_AND_VALUE(UniqueUnixAddr(true, AF_UNIX));
+ int srv = socket(AF_UNIX, SOCK_SEQPACKET, 0);
+ TEST_PCHECK(srv >= 0);
+ TEST_PCHECK(bind(srv, AsSockAddr(&addr), sizeof(addr)) == 0);
+ TEST_PCHECK(listen(srv, 5) == 0);
+
+ // Connect with different groups than the listener had at listen(2).
+ TEST_PCHECK(setgroups(client_groups.size(), client_groups.data()) == 0);
+ int cli = socket(AF_UNIX, SOCK_SEQPACKET, 0);
+ TEST_PCHECK(cli >= 0);
+ TEST_PCHECK(connect(cli, AsSockAddr(&addr), sizeof(addr)) == 0);
+ int acc = accept(srv, nullptr, nullptr);
+ TEST_PCHECK(acc >= 0);
+
+ const std::vector<gid_t> acc_groups =
+ TEST_CHECK_NO_ERRNO_AND_VALUE(GetPeerGroups(acc));
+ const std::vector<gid_t> cli_groups =
+ TEST_CHECK_NO_ERRNO_AND_VALUE(GetPeerGroups(cli));
+ fprintf(stderr,
+ "SOCK_SEQPACKET: accepted socket reports %s (want %s), client "
+ "reports %s (want %s)\n",
+ GroupsString(acc_groups).c_str(),
+ GroupsString(client_groups).c_str(),
+ GroupsString(cli_groups).c_str(),
+ GroupsString(listener_groups).c_str());
+ TEST_CHECK_MSG(acc_groups == client_groups,
+ "accepted socket must report the client's groups");
+ TEST_CHECK_MSG(cli_groups == listener_groups,
+ "client must report the listener's groups");
+ };
+ EXPECT_THAT(InForkedProcess(fn), IsPosixErrorOkAndHolds(0));
+}
+
+// A connect(2) that fails leaves the socket without peer credentials, so
+// SO_PEERGROUPS still fails with ENODATA.
+TEST(UnixSocketPeerGroupsTest, FailedConnectLeavesNoPeerGroups) {
+ auto addr = ASSERT_NO_ERRNO_AND_VALUE(UniqueUnixAddr(true, AF_UNIX));
+ // Bound but not listening, so connect(2) fails with ECONNREFUSED.
+ auto bound = ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_UNIX, SOCK_STREAM, 0));
+ ASSERT_THAT(bind(bound.get(), AsSockAddr(&addr), sizeof(addr)),
+ SyscallSucceeds());
+
+ auto s = ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_UNIX, SOCK_STREAM, 0));
+ ASSERT_THAT(connect(s.get(), AsSockAddr(&addr), sizeof(addr)),
+ SyscallFailsWithErrno(ECONNREFUSED));
+
+ std::vector<gid_t> buf(NGROUPS_MAX);
+ socklen_t len = buf.size() * sizeof(gid_t);
+ EXPECT_THAT(getsockopt(s.get(), SOL_SOCKET, SO_PEERGROUPS, buf.data(), &len),
+ SyscallFailsWithErrno(ENODATA));
+}
+
+// connect(2) on a connected socket fails with EISCONN and doesn't change the
+// groups reported for the peer.
+TEST(UnixSocketPeerGroupsTest, ConnectEISCONNKeepsPeerGroups) {
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SETGID)));
+
+ const std::vector<gid_t> listener_groups = {4001};
+ const std::vector<gid_t> later_groups = {4002};
+ const auto fn = [&] {
+ TEST_PCHECK(setgroups(listener_groups.size(), listener_groups.data()) ==
+ 0);
+ struct sockaddr_un addr =
+ TEST_CHECK_NO_ERRNO_AND_VALUE(UniqueUnixAddr(true, AF_UNIX));
+ int srv = socket(AF_UNIX, SOCK_STREAM, 0);
+ TEST_PCHECK(srv >= 0);
+ TEST_PCHECK(bind(srv, AsSockAddr(&addr), sizeof(addr)) == 0);
+ TEST_PCHECK(listen(srv, 5) == 0);
+ int cli = socket(AF_UNIX, SOCK_STREAM, 0);
+ TEST_PCHECK(cli >= 0);
+ TEST_PCHECK(connect(cli, AsSockAddr(&addr), sizeof(addr)) == 0);
+ TEST_CHECK(TEST_CHECK_NO_ERRNO_AND_VALUE(GetPeerGroups(cli)) ==
+ listener_groups);
... (17 more lines in the full patch)| scmCreds, ok := pCreds.(control.SCMCredentials) | ||
| if !ok { | ||
| return nil, syserr.ErrInvalidEndpointState | ||
| } | ||
| return scmCreds.Groups(t), nil |
There was a problem hiding this comment.
[🤖] [minor · linux-compat] SO_PEERGROUPS reports the listener's own groups on SOCK_SEQPACKET connections
For SOCK_SEQPACKET sockets connected with connect(2)/accept(2), SO_PEERGROUPS on the accepted socket returns the listener's own groups instead of the client's, and the client gets its own groups instead of the listener's. Linux uses unix_stream_connect() for both stream and seqpacket sockets, so both must behave the same. Programs use SO_PEERGROUPS for access checks, so a seqpacket server would treat every client as a member of the server's own groups. The PR description lists this as out of scope, but the cause is a credential swap limited to SOCK_STREAM. Moving it fixes this and the same pre-existing SO_PEERCRED bug.
Suggested fix: In BidirectionalConnect (pkg/sentry/socket/unix/transport/connectioned.go), swap the credentials for every connection-oriented type, not only SOCK_STREAM:
readQueue.IncRef()
if err := e.swapPeerCredsLocked(ctx, ce, ne); err != nil {
return err
}
if e.stype == linux.SOCK_STREAM {
returnConnect(&streamQueueReceiver{queueReceiver: queueReceiver{readQueue: readQueue}}, peer)
} else {
returnConnect(&queueReceiver{readQueue: readQueue}, peer)
}Also update the peerCreds doc comment to cover seqpacket. Verified: with this change the new test and the whole socket_unix_pair_test runsc target pass. This also fixes SO_PEERCRED for seqpacket.
Details (evidence, reproduction and test code)
Evidence: GetPeerGroups returns whatever s.ep.PeerCreds() holds. Socket.Connect first sets the client's peer creds to its own creds (unix.go:644). BidirectionalConnect then gives the new endpoint the listener's creds (ne.peerCreds = e.peerCreds, connectioned.go:363). It swaps the two only if e.stype == linux.SOCK_STREAM (connectioned.go:390-393). So for SOCK_SEQPACKET, the accepted socket keeps the listener's creds and the client keeps its own.
Linux v6.12 net/unix/af_unix.c sets unix_seqpacket_ops.connect = unix_stream_connect. For both types that function calls init_peercred(newsk), so the accepted socket gets the connecting task's creds. It also calls copy_peercred(sk, other), so the client gets the listener's creds.
Test UnixSocketPeerGroupsTest.SeqpacketConnectReportsPeerGroups uses listener groups {3001} and client groups {3002,3003}:
- Linux: the accepted socket reports {3002,3003} and the client reports {3001}.
- gVisor: the accepted socket reports {3001} and the client reports {3002,3003}.
The PR's SOCK_STREAM test passes on both. An SO_PEERCRED pid version of this check also fails at the merge-base, so the root cause is older than this PR.
Repro: bazel build //test/syscalls/linux:socket_unix_pair_test && sudo bazel-bin/test/syscalls/linux/socket_unix_pair_test --gtest_filter='UnixSocketPeerGroupsTest.*' passes on Linux. bazel test //test/syscalls:socket_unix_pair_test_runsc_systrap_directfs fails. Run the binary directly because _native under root runs in a user namespace that blocks setgroups.
Reproduced with a test (test/syscalls/linux/socket_unix_peercred.cc): 'bazel build //test/syscalls/linux:socket_unix_pair_test && ./bazel-bin/test/syscalls/linux/socket_unix_pair_test --gtest_filter="UnixSocketPeerGroupsTest.*:*PeerGroups*"; bazel test //test/syscalls:socket_unix_pair_test_runsc_systrap_directfs --nocache_test_results' ; fix check: bazel test //test/syscalls:socket_unix_pair_test_runsc_systrap_directfs --nocache_test_results → Fails under runsc on the PR head with the groups reversed on both ends. Passes on native Linux, and passes under runsc with the suggested swap fix, along with the whole socket_unix_pair_test target.
Test code:
diff --git a/test/syscalls/linux/socket_unix_peercred.cc b/test/syscalls/linux/socket_unix_peercred.cc
index de71b0243..9ff88af5b 100644
--- a/test/syscalls/linux/socket_unix_peercred.cc
+++ b/test/syscalls/linux/socket_unix_peercred.cc
@@ -20,6 +20,7 @@
#include <linux/capability.h>
#include <sched.h>
#include <signal.h>
+#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
@@ -33,6 +34,7 @@
#include <array>
#include <atomic>
#include <memory>
+#include <string>
#include <vector>
#include "gmock/gmock.h"
@@ -661,6 +663,116 @@ TEST(UnixSocketPeerGroupsTest, NotUnixSocket) {
SyscallFailsWithErrno(ENODATA));
}
+std::string GroupsString(const std::vector<gid_t>& groups) {
+ std::string s = "{";
+ for (size_t i = 0; i < groups.size(); i++) {
+ if (i > 0) {
+ s += ",";
+ }
+ s += std::to_string(groups[i]);
+ }
+ return s + "}";
+}
+
+// connect(2) on a SOCK_SEQPACKET socket sets the peer credentials like it does
+// for SOCK_STREAM: the accepted socket reports the client's groups and the
+// client reports the groups the listener had at listen(2).
+TEST(UnixSocketPeerGroupsTest, SeqpacketConnectReportsPeerGroups) {
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SETGID)));
+
+ const std::vector<gid_t> listener_groups = {3001};
+ const std::vector<gid_t> client_groups = {3002, 3003};
+ const auto fn = [&] {
+ TEST_PCHECK(setgroups(listener_groups.size(), listener_groups.data()) ==
+ 0);
+ struct sockaddr_un addr =
+ TEST_CHECK_NO_ERRNO_AND_VALUE(UniqueUnixAddr(true, AF_UNIX));
+ int srv = socket(AF_UNIX, SOCK_SEQPACKET, 0);
+ TEST_PCHECK(srv >= 0);
+ TEST_PCHECK(bind(srv, AsSockAddr(&addr), sizeof(addr)) == 0);
+ TEST_PCHECK(listen(srv, 5) == 0);
+
+ // Connect with different groups than the listener had at listen(2).
+ TEST_PCHECK(setgroups(client_groups.size(), client_groups.data()) == 0);
+ int cli = socket(AF_UNIX, SOCK_SEQPACKET, 0);
+ TEST_PCHECK(cli >= 0);
+ TEST_PCHECK(connect(cli, AsSockAddr(&addr), sizeof(addr)) == 0);
+ int acc = accept(srv, nullptr, nullptr);
+ TEST_PCHECK(acc >= 0);
+
+ const std::vector<gid_t> acc_groups =
+ TEST_CHECK_NO_ERRNO_AND_VALUE(GetPeerGroups(acc));
+ const std::vector<gid_t> cli_groups =
+ TEST_CHECK_NO_ERRNO_AND_VALUE(GetPeerGroups(cli));
+ fprintf(stderr,
+ "SOCK_SEQPACKET: accepted socket reports %s (want %s), client "
+ "reports %s (want %s)\n",
+ GroupsString(acc_groups).c_str(),
+ GroupsString(client_groups).c_str(),
+ GroupsString(cli_groups).c_str(),
+ GroupsString(listener_groups).c_str());
+ TEST_CHECK_MSG(acc_groups == client_groups,
+ "accepted socket must report the client's groups");
+ TEST_CHECK_MSG(cli_groups == listener_groups,
+ "client must report the listener's groups");
+ };
+ EXPECT_THAT(InForkedProcess(fn), IsPosixErrorOkAndHolds(0));
+}
+
+// A connect(2) that fails leaves the socket without peer credentials, so
+// SO_PEERGROUPS still fails with ENODATA.
+TEST(UnixSocketPeerGroupsTest, FailedConnectLeavesNoPeerGroups) {
+ auto addr = ASSERT_NO_ERRNO_AND_VALUE(UniqueUnixAddr(true, AF_UNIX));
+ // Bound but not listening, so connect(2) fails with ECONNREFUSED.
+ auto bound = ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_UNIX, SOCK_STREAM, 0));
+ ASSERT_THAT(bind(bound.get(), AsSockAddr(&addr), sizeof(addr)),
+ SyscallSucceeds());
+
+ auto s = ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_UNIX, SOCK_STREAM, 0));
+ ASSERT_THAT(connect(s.get(), AsSockAddr(&addr), sizeof(addr)),
+ SyscallFailsWithErrno(ECONNREFUSED));
+
+ std::vector<gid_t> buf(NGROUPS_MAX);
+ socklen_t len = buf.size() * sizeof(gid_t);
+ EXPECT_THAT(getsockopt(s.get(), SOL_SOCKET, SO_PEERGROUPS, buf.data(), &len),
+ SyscallFailsWithErrno(ENODATA));
+}
+
+// connect(2) on a connected socket fails with EISCONN and doesn't change the
+// groups reported for the peer.
+TEST(UnixSocketPeerGroupsTest, ConnectEISCONNKeepsPeerGroups) {
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SETGID)));
+
+ const std::vector<gid_t> listener_groups = {4001};
+ const std::vector<gid_t> later_groups = {4002};
+ const auto fn = [&] {
+ TEST_PCHECK(setgroups(listener_groups.size(), listener_groups.data()) ==
+ 0);
+ struct sockaddr_un addr =
+ TEST_CHECK_NO_ERRNO_AND_VALUE(UniqueUnixAddr(true, AF_UNIX));
+ int srv = socket(AF_UNIX, SOCK_STREAM, 0);
+ TEST_PCHECK(srv >= 0);
+ TEST_PCHECK(bind(srv, AsSockAddr(&addr), sizeof(addr)) == 0);
+ TEST_PCHECK(listen(srv, 5) == 0);
+ int cli = socket(AF_UNIX, SOCK_STREAM, 0);
+ TEST_PCHECK(cli >= 0);
+ TEST_PCHECK(connect(cli, AsSockAddr(&addr), sizeof(addr)) == 0);
+ TEST_CHECK(TEST_CHECK_NO_ERRNO_AND_VALUE(GetPeerGroups(cli)) ==
+ listener_groups);
... (17 more lines in the full patch)getsockopt(SOL_SOCKET, SO_PEERGROUPS) returns the supplementary groups of a unix socket's peer, as they were at socketpair(), connect() or listen() time. dbus-broker uses it to check bus policy and otherwise falls back to resolving groups through NSS. The groups are kept next to the SO_PEERCRED credentials: the SCM credentials built by control.MakeCreds now hold the task's ExtraKGIDs, which are immutable and can be shared. On read they are mapped into the caller's user namespace, like groups_to_user() in Linux. The option is handled in the getsockopt syscall itself, since Linux writes the needed size to optlen when it fails with ERANGE. With no peer credentials, or on a socket that isn't AF_UNIX, it fails with ENODATA. Peer credentials are now set only when a connection is made, as in Linux. Socket.Connect used to store the caller's credentials in the client endpoint before connecting and never undid it, so a failed connect(2) (ECONNREFUSED, EISCONN, ...) left the caller's own credentials as the peer's. The credentials are now passed to Endpoint.Connect and BoundEndpoint.BidirectionalConnect and assigned to both endpoints in the commit branch with both endpoint locks held, which also avoids a race between concurrent connect(2) calls on the same socket. The swap that gives the accepted socket the client's credentials and the client the listener's was limited to SOCK_STREAM; it now covers every connection-oriented type, so SOCK_SEQPACKET no longer reports them reversed. Both bugs also affected SO_PEERCRED. Connecting to a socket served by the gofer (a host socket) no longer reports the caller's own credentials as the peer; they stay unset like for any other peer that is not a sentry endpoint. Fixes google#14202 Assisted-by: Claude
7ba12b7 to
1908602
Compare
getsockopt(SOL_SOCKET, SO_PEERGROUPS) now returns the supplementary groups of
a unix socket's peer. dbus-broker uses it to evaluate bus policy, and without
it logs "Falling back to racy auxiliary groups resolution using nss" on every
boot.
The groups are recorded in the same place as SO_PEERCRED: the SCM credentials
that control.MakeCreds builds at socketpair(), connect() and listen() now also
keep the task's ExtraKGIDs. That slice is immutable in auth.Credentials, so it
is shared rather than copied. On read, each group is mapped into the caller's
user namespace, with unmapped groups shown as the overflow GID, the same way
groups_to_user() does it in Linux.
The option is handled in the getsockopt syscall itself rather than in the
per-socket GetSockOpt, because Linux writes the needed size to optlen when it
fails with ERANGE, and the existing path only writes optlen on success.
Behaviour matched against net/core/sock.c:
the size needed.
ENODATA. This differs from SO_PEERCRED, which reports pid 0 and uid/gid -1.
The groups are left out of scmCredentials.Equals(), because SCM_CREDENTIALS
messages don't carry groups and coalescing shouldn't change.
Not covered here:
SO_PEERGROUPS (like getgroups) can return the same set in a different
order. The tests compare sorted lists.
peer credentials for SOCK_STREAM today. That already affects SO_PEERCRED and
I haven't changed it.
Tests are in test/syscalls/linux/socket_unix_peercred.cc: socketpair for every
unix socket type, the ERANGE and optlen cases with groups set through
setgroups(2), a connected stream socket that checks both sides and that a
setgroups(2) after connect() doesn't change what the server sees, and
ENODATA for an unconnected socket and for a TCP socket.
Fixes #14202