Skip to content

Commit 7ba12b7

Browse files
committed
Set unix socket peer credentials only when a connection is made.
Address review feedback on SO_PEERGROUPS: * A failed connect(2) (ECONNREFUSED, EISCONN, EAGAIN, ...) no longer leaves the caller's own credentials as the socket's peer credentials. Linux only sets them once the connection is established, so SO_PEERGROUPS must keep failing with ENODATA, and an already connected socket must keep reporting its real peer. Socket.Connect used to store the caller's credentials in the client endpoint before connecting and never undid it. The credentials are now passed to Endpoint.Connect and BoundEndpoint.BidirectionalConnect, which assign them to both endpoints only in the commit branch, with both endpoint locks held. This also avoids a race between concurrent connect(2) calls on the same socket that a save and restore in Socket.Connect would have. * SOCK_SEQPACKET connections reported reversed credentials: the accepted socket got the listener's credentials and the client got its own. The swap of the two sets was limited to SOCK_STREAM. The accepted endpoint now gets the client's credentials and the client gets the listener's for every connection-oriented type, as unix_stream_connect() does in Linux for both. The same two bugs affected SO_PEERCRED, so it is fixed as well. 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. Add tests for the seqpacket case, ECONNREFUSED and EISCONN. Assisted-by: Claude Refs #14202
1 parent 7de5c4a commit 7ba12b7

6 files changed

Lines changed: 158 additions & 33 deletions

File tree

‎pkg/sentry/fsimpl/gofer/socket.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ type endpoint struct {
5454
}
5555

5656
// BidirectionalConnect implements BoundEndpoint.BidirectionalConnect.
57-
func (e *endpoint) BidirectionalConnect(ctx context.Context, ce transport.ConnectingEndpoint, returnConnect func(transport.Receiver, transport.Sender)) *syserr.Error {
57+
func (e *endpoint) BidirectionalConnect(ctx context.Context, ce transport.ConnectingEndpoint, clientCreds transport.CredentialsControlMessage, returnConnect func(transport.Receiver, transport.Sender)) *syserr.Error {
5858
// No lock ordering required as only the ConnectingEndpoint has a mutex.
5959
ce.Lock()
6060

‎pkg/sentry/socket/unix/transport/connectioned.go‎

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,12 @@ type connectionedEndpoint struct {
107107
stype linux.SockType
108108

109109
// peerCreds is used to store the peer credentials.
110-
// This will store the socket's own credentials until the connection is
111-
// established with connect(2). Once the connection is established, this
112-
// will store the peer's credentials. The use of this option is possible
113-
// only for connected `AF_UNIX` stream sockets and for `AF_UNIX` stream and
114-
// datagram socket pairs created using socketpair(2)
110+
// It is nil until the connection is established with connect(2), so a
111+
// connect(2) that fails leaves it untouched. Once the connection is
112+
// established, this stores the peer's credentials. The use of this option
113+
// is possible only for connected `AF_UNIX` stream and seqpacket sockets and
114+
// for `AF_UNIX` stream, seqpacket and datagram socket pairs created using
115+
// socketpair(2).
115116
peerCreds CredentialsControlMessage
116117

117118
// acceptedChan is per the TCP endpoint implementation. Note that the
@@ -297,24 +298,20 @@ func (e *connectionedEndpoint) Close(ctx context.Context) {
297298
}
298299
}
299300

300-
func (e *connectionedEndpoint) swapPeerCredsLocked(ctx context.Context, cend ConnectingEndpoint, ne *connectionedEndpoint) *syserr.Error {
301-
ce, ok := cend.(*connectionedEndpoint)
302-
if !ok {
303-
return syserr.ErrInvalidEndpointState
304-
}
305-
// Swap peer credentials between the two endpoints.
306-
ne.peerCreds, ce.peerCreds = ce.peerCreds, ne.peerCreds
307-
return nil
308-
}
309-
310301
// BidirectionalConnect implements BoundEndpoint.BidirectionalConnect.
311-
func (e *connectionedEndpoint) BidirectionalConnect(ctx context.Context, ce ConnectingEndpoint, returnConnect func(Receiver, Sender)) *syserr.Error {
302+
func (e *connectionedEndpoint) BidirectionalConnect(ctx context.Context, ce ConnectingEndpoint, clientCreds CredentialsControlMessage, returnConnect func(Receiver, Sender)) *syserr.Error {
312303
if ce.Type() != e.stype {
313304
return syserr.ErrWrongProtocolForSocket
314305
}
315306

307+
// The peer credentials of the connecting endpoint are set on commit.
308+
cce, ok := ce.(*connectionedEndpoint)
309+
if !ok {
310+
return syserr.ErrInvalidEndpointState
311+
}
312+
316313
// Check if ce is e to avoid a deadlock.
317-
if ce, ok := ce.(*connectionedEndpoint); ok && ce == e {
314+
if cce == e {
318315
return syserr.ErrInvalidEndpointState
319316
}
320317

@@ -360,7 +357,9 @@ func (e *connectionedEndpoint) BidirectionalConnect(ctx context.Context, ce Conn
360357
ne.ops.SetSendBufferSize(defaultBufferSize, false /* notify */)
361358
ne.ops.SetReceiveBufferSize(defaultBufferSize, false /* notify */)
362359
ne.SocketOptions().SetPassCred(e.SocketOptions().GetPassCred())
363-
ne.peerCreds = e.peerCreds
360+
// The accepted endpoint reports the connecting task as its peer, like
361+
// Linux does for both SOCK_STREAM and SOCK_SEQPACKET.
362+
ne.peerCreds = clientCreds
364363

365364
readQueue := &queue{readerWaiters: ce.WaiterQueue(), writerWaiters: ne.WaitQueue, limit: defaultBufferSize}
366365
readQueue.InitRefs()
@@ -387,10 +386,11 @@ func (e *connectionedEndpoint) BidirectionalConnect(ctx context.Context, ce Conn
387386
writeQueue: writeQueue,
388387
}
389388
readQueue.IncRef()
389+
// The connecting endpoint reports the listener as its peer. This is
390+
// done only here, with both endpoints locked, so that a failed or
391+
// concurrent connect(2) never changes the peer credentials.
392+
cce.peerCreds = e.peerCreds
390393
if e.stype == linux.SOCK_STREAM {
391-
if err := e.swapPeerCredsLocked(ctx, ce, ne); err != nil {
392-
return err
393-
}
394394
returnConnect(&streamQueueReceiver{queueReceiver: queueReceiver{readQueue: readQueue}}, peer)
395395
} else {
396396
returnConnect(&queueReceiver{readQueue: readQueue}, peer)
@@ -421,7 +421,7 @@ func (e *connectionedEndpoint) UnidirectionalConnect(ctx context.Context) (Sende
421421

422422
// Connect attempts to directly connect to another Endpoint.
423423
// Implements Endpoint.Connect.
424-
func (e *connectionedEndpoint) Connect(ctx context.Context, server BoundEndpoint) *syserr.Error {
424+
func (e *connectionedEndpoint) Connect(ctx context.Context, server BoundEndpoint, clientCreds CredentialsControlMessage) *syserr.Error {
425425
returnConnect := func(r Receiver, ce Sender) {
426426
e.receiver = r
427427
e.peer = ce
@@ -433,7 +433,7 @@ func (e *connectionedEndpoint) Connect(ctx context.Context, server BoundEndpoint
433433
}
434434
}
435435

436-
return server.BidirectionalConnect(ctx, e, returnConnect)
436+
return server.BidirectionalConnect(ctx, e, clientCreds, returnConnect)
437437
}
438438

439439
// Listen starts listening on the connection.

‎pkg/sentry/socket/unix/transport/connectionless.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ func (e *connectionlessEndpoint) Close(ctx context.Context) {
7979
}
8080

8181
// BidirectionalConnect implements BoundEndpoint.BidirectionalConnect.
82-
func (e *connectionlessEndpoint) BidirectionalConnect(ctx context.Context, ce ConnectingEndpoint, returnConnect func(Receiver, Sender)) *syserr.Error {
82+
func (e *connectionlessEndpoint) BidirectionalConnect(ctx context.Context, ce ConnectingEndpoint, clientCreds CredentialsControlMessage, returnConnect func(Receiver, Sender)) *syserr.Error {
8383
return syserr.ErrConnectionRefused
8484
}
8585

@@ -136,7 +136,7 @@ func (e *connectionlessEndpoint) Type() linux.SockType {
136136
}
137137

138138
// Connect attempts to connect directly to server.
139-
func (e *connectionlessEndpoint) Connect(ctx context.Context, server BoundEndpoint) *syserr.Error {
139+
func (e *connectionlessEndpoint) Connect(ctx context.Context, server BoundEndpoint, clientCreds CredentialsControlMessage) *syserr.Error {
140140
peer, err := server.UnidirectionalConnect(ctx)
141141
if err != nil {
142142
return err

‎pkg/sentry/socket/unix/transport/unix.go‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,12 @@ type Endpoint interface {
215215
// This should be called on the client endpoint, and the (bound)
216216
// endpoint passed in as a parameter.
217217
//
218+
// clientCreds are the credentials of the connecting task. They become the
219+
// peer credentials of the accepted endpoint only if the connection is
220+
// established; a failed Connect doesn't change any peer credentials.
221+
//
218222
// The error codes are the same as Connect.
219-
Connect(ctx context.Context, server BoundEndpoint) *syserr.Error
223+
Connect(ctx context.Context, server BoundEndpoint, clientCreds CredentialsControlMessage) *syserr.Error
220224

221225
// Shutdown closes the read and/or write end of the endpoint connection
222226
// to its peer.
@@ -307,9 +311,14 @@ type BoundEndpoint interface {
307311
// be unconnected and not listening and the BoundEndpoint whose
308312
// BidirectionalConnect method is being called must be listening.
309313
//
314+
// clientCreds are the credentials of the connecting task. On a successful
315+
// connect, the accepted endpoint gets them as its peer credentials, and
316+
// the ConnectingEndpoint gets the peer credentials of this endpoint. They
317+
// must not change if the connect fails.
318+
//
310319
// This method will return syserr.ErrConnectionRefused on endpoints with a
311320
// type that isn't SockStream or SockSeqpacket.
312-
BidirectionalConnect(ctx context.Context, ep ConnectingEndpoint, returnConnect func(Receiver, Sender)) *syserr.Error
321+
BidirectionalConnect(ctx context.Context, ep ConnectingEndpoint, clientCreds CredentialsControlMessage, returnConnect func(Receiver, Sender)) *syserr.Error
313322

314323
// UnidirectionalConnect establishes a write-only connection to a unix
315324
// endpoint.

‎pkg/sentry/socket/unix/unix.go‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -640,11 +640,9 @@ func (s *Socket) Connect(t *kernel.Task, sockaddr []byte, blocking bool) *syserr
640640
}
641641
defer ep.Release(t)
642642

643-
// Initialize the peer credentials of client endpoint.
644-
s.ep.SetPeerCreds(control.MakeCreds(t))
645-
646-
// Connect the server endpoint.
647-
err = s.ep.Connect(t, ep)
643+
// Connect the server endpoint. The peer credentials of both endpoints are
644+
// set by the transport once the connection is established.
645+
err = s.ep.Connect(t, ep, control.MakeCreds(t))
648646

649647
if err == syserr.ErrWrongProtocolForSocket {
650648
// Linux for abstract sockets returns ErrConnectionRefused

‎test/syscalls/linux/socket_unix_peercred.cc‎

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include <linux/capability.h>
2121
#include <sched.h>
2222
#include <signal.h>
23+
#include <stdio.h>
2324
#include <stdlib.h>
2425
#include <string.h>
2526
#include <sys/mman.h>
@@ -33,6 +34,7 @@
3334
#include <array>
3435
#include <atomic>
3536
#include <memory>
37+
#include <string>
3638
#include <vector>
3739

3840
#include "gmock/gmock.h"
@@ -661,6 +663,122 @@ TEST(UnixSocketPeerGroupsTest, NotUnixSocket) {
661663
SyscallFailsWithErrno(ENODATA));
662664
}
663665

666+
// GroupsString formats groups for failure messages.
667+
std::string GroupsString(const std::vector<gid_t>& groups) {
668+
std::string s = "{";
669+
for (size_t i = 0; i < groups.size(); i++) {
670+
if (i > 0) {
671+
s += ",";
672+
}
673+
s += std::to_string(groups[i]);
674+
}
675+
return s + "}";
676+
}
677+
678+
// connect(2) on a SOCK_SEQPACKET socket sets the peer credentials like it does
679+
// for SOCK_STREAM: the accepted socket reports the client's groups and the
680+
// client reports the groups the listener had at listen(2).
681+
TEST(UnixSocketPeerGroupsTest, SeqpacketConnectReportsPeerGroups) {
682+
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SETGID)));
683+
684+
const std::vector<gid_t> listener_groups = {3001};
685+
const std::vector<gid_t> client_groups = {3002, 3003};
686+
const auto fn = [&] {
687+
TEST_PCHECK(setgroups(listener_groups.size(), listener_groups.data()) ==
688+
0);
689+
struct sockaddr_un addr =
690+
TEST_CHECK_NO_ERRNO_AND_VALUE(UniqueUnixAddr(true, AF_UNIX));
691+
int srv = socket(AF_UNIX, SOCK_SEQPACKET, 0);
692+
TEST_PCHECK(srv >= 0);
693+
TEST_PCHECK(bind(srv, AsSockAddr(&addr), sizeof(addr)) == 0);
694+
TEST_PCHECK(listen(srv, 5) == 0);
695+
696+
// Connect with different groups than the listener had at listen(2).
697+
TEST_PCHECK(setgroups(client_groups.size(), client_groups.data()) == 0);
698+
int cli = socket(AF_UNIX, SOCK_SEQPACKET, 0);
699+
TEST_PCHECK(cli >= 0);
700+
TEST_PCHECK(connect(cli, AsSockAddr(&addr), sizeof(addr)) == 0);
701+
int acc = accept(srv, nullptr, nullptr);
702+
TEST_PCHECK(acc >= 0);
703+
704+
const std::vector<gid_t> acc_groups =
705+
TEST_CHECK_NO_ERRNO_AND_VALUE(GetPeerGroups(acc));
706+
const std::vector<gid_t> cli_groups =
707+
TEST_CHECK_NO_ERRNO_AND_VALUE(GetPeerGroups(cli));
708+
fprintf(stderr,
709+
"SOCK_SEQPACKET: accepted socket reports %s (want %s), client "
710+
"reports %s (want %s)\n",
711+
GroupsString(acc_groups).c_str(),
712+
GroupsString(client_groups).c_str(),
713+
GroupsString(cli_groups).c_str(),
714+
GroupsString(listener_groups).c_str());
715+
TEST_CHECK_MSG(acc_groups == client_groups,
716+
"accepted socket must report the client's groups");
717+
TEST_CHECK_MSG(cli_groups == listener_groups,
718+
"client must report the listener's groups");
719+
};
720+
EXPECT_THAT(InForkedProcess(fn), IsPosixErrorOkAndHolds(0));
721+
}
722+
723+
// A connect(2) that fails leaves the socket without peer credentials, so
724+
// SO_PEERGROUPS still fails with ENODATA.
725+
TEST(UnixSocketPeerGroupsTest, FailedConnectLeavesNoPeerGroups) {
726+
auto addr = ASSERT_NO_ERRNO_AND_VALUE(UniqueUnixAddr(true, AF_UNIX));
727+
// Bound but not listening, so connect(2) fails with ECONNREFUSED.
728+
auto bound = ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_UNIX, SOCK_STREAM, 0));
729+
ASSERT_THAT(bind(bound.get(), AsSockAddr(&addr), sizeof(addr)),
730+
SyscallSucceeds());
731+
732+
auto s = ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_UNIX, SOCK_STREAM, 0));
733+
ASSERT_THAT(connect(s.get(), AsSockAddr(&addr), sizeof(addr)),
734+
SyscallFailsWithErrno(ECONNREFUSED));
735+
736+
std::vector<gid_t> buf(NGROUPS_MAX);
737+
socklen_t len = buf.size() * sizeof(gid_t);
738+
EXPECT_THAT(getsockopt(s.get(), SOL_SOCKET, SO_PEERGROUPS, buf.data(), &len),
739+
SyscallFailsWithErrno(ENODATA));
740+
}
741+
742+
// connect(2) on a connected socket fails with EISCONN and doesn't change the
743+
// groups reported for the peer.
744+
TEST(UnixSocketPeerGroupsTest, ConnectEISCONNKeepsPeerGroups) {
745+
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SETGID)));
746+
747+
const std::vector<gid_t> listener_groups = {4001};
748+
const std::vector<gid_t> later_groups = {4002};
749+
const auto fn = [&] {
750+
TEST_PCHECK(setgroups(listener_groups.size(), listener_groups.data()) ==
751+
0);
752+
struct sockaddr_un addr =
753+
TEST_CHECK_NO_ERRNO_AND_VALUE(UniqueUnixAddr(true, AF_UNIX));
754+
int srv = socket(AF_UNIX, SOCK_STREAM, 0);
755+
TEST_PCHECK(srv >= 0);
756+
TEST_PCHECK(bind(srv, AsSockAddr(&addr), sizeof(addr)) == 0);
757+
TEST_PCHECK(listen(srv, 5) == 0);
758+
int cli = socket(AF_UNIX, SOCK_STREAM, 0);
759+
TEST_PCHECK(cli >= 0);
760+
TEST_PCHECK(connect(cli, AsSockAddr(&addr), sizeof(addr)) == 0);
761+
TEST_CHECK(TEST_CHECK_NO_ERRNO_AND_VALUE(GetPeerGroups(cli)) ==
762+
listener_groups);
763+
764+
// Connect again with different groups. This fails and must not replace
765+
// the peer credentials.
766+
TEST_PCHECK(setgroups(later_groups.size(), later_groups.data()) == 0);
767+
TEST_CHECK_MSG(connect(cli, AsSockAddr(&addr), sizeof(addr)) == -1 &&
768+
errno == EISCONN,
769+
"second connect must fail with EISCONN");
770+
771+
const std::vector<gid_t> cli_groups =
772+
TEST_CHECK_NO_ERRNO_AND_VALUE(GetPeerGroups(cli));
773+
fprintf(stderr, "after EISCONN: client reports %s (want %s)\n",
774+
GroupsString(cli_groups).c_str(),
775+
GroupsString(listener_groups).c_str());
776+
TEST_CHECK_MSG(cli_groups == listener_groups,
777+
"a failed connect must not change the peer groups");
778+
};
779+
EXPECT_THAT(InForkedProcess(fn), IsPosixErrorOkAndHolds(0));
780+
}
781+
664782
} // namespace
665783

666784
} // namespace testing

0 commit comments

Comments
 (0)