Skip to content

Implement SO_PEERGROUPS for unix sockets - #14999

Open
anzal1 wants to merge 1 commit into
google:masterfrom
anzal1:feat/so-peergroups
Open

anzal1 wants to merge 1 commit into
google:masterfrom
anzal1:feat/so-peergroups

Conversation

@anzal1

@anzal1 anzal1 commented Sep 25, 2026

Copy link
Copy Markdown

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:

  • Buffer smaller than ngroups * sizeof(gid_t): ERANGE, and optlen is set to
    the size needed.
  • Big enough buffer: the groups are copied and optlen is set to the size used.
  • No peer credentials (unconnected socket, connected datagram socket):
    ENODATA. This differs from SO_PEERCRED, which reports pid 0 and uid/gid -1.
  • Sockets other than AF_UNIX: ENODATA.
  • A listening socket reports its own groups, as SO_PEERCRED does.

The groups are left out of scmCredentials.Equals(), because SCM_CREDENTIALS
messages don't carry groups and coalescing shouldn't change.

Not covered here:

  • Linux sorts the list in setgroups(2) and gVisor keeps the order given, so
    SO_PEERGROUPS (like getgroups) can return the same set in a different
    order. The tests compare sorted lists.
  • Connecting a SOCK_SEQPACKET socket (rather than using socketpair) only swaps
    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

@anzal1

anzal1 commented Oct 10, 2026

Copy link
Copy Markdown
Author

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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why use an interface with a single implementer ?

@aaltinaydev aaltinaydev left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[🤖] 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)

Comment on lines +553 to +557
pCreds := s.ep.PeerCreds()
if pCreds == nil {
// Unlike SO_PEERCRED, Linux fails with ENODATA if there is no peer.
return nil, syserr.ErrNoDataAvailable
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[🤖] [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.FailedConnectLeavesNoPeerGroups connects to a bound socket that is not listening (ECONNREFUSED), then reads SO_PEERGROUPS. Linux returns ENODATA; gVisor returns success.
  • UnixSocketPeerGroupsTest.ConnectEISCONNKeepsPeerGroups sets 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)

Comment on lines +558 to +562
scmCreds, ok := pCreds.(control.SCMCredentials)
if !ok {
return nil, syserr.ErrInvalidEndpointState
}
return scmCreds.Groups(t), nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[🤖] [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
@anzal1
anzal1 force-pushed the feat/so-peergroups branch from 7ba12b7 to 1908602 Compare October 24, 2026 11:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement getsockopt(SO_PEERGROUPS)

3 participants