Page Menu
Home
FreeBSD
Search
Configure Global Search
Log In
Files
F164408973
D58405.id182712.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
16 KB
Referenced Files
None
Subscribers
None
D58405.id182712.diff
View Options
diff --git a/tests/sys/kern/Makefile b/tests/sys/kern/Makefile
--- a/tests/sys/kern/Makefile
+++ b/tests/sys/kern/Makefile
@@ -63,6 +63,7 @@
# One test modifies the system time.
TEST_METADATA.timerfd+= is_exclusive="true"
ATF_TESTS_C+= tty_pts
+ATF_TESTS_C+= unix_connectat
ATF_TESTS_C+= unix_dgram
ATF_TESTS_C+= unix_passfd_dgram
TEST_METADATA.unix_passfd_dgram+= is_exclusive="true"
diff --git a/tests/sys/kern/unix_connectat.c b/tests/sys/kern/unix_connectat.c
new file mode 100644
--- /dev/null
+++ b/tests/sys/kern/unix_connectat.c
@@ -0,0 +1,494 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 John Ericson
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+/*
+ * Tests for connectat(2) naming a unix-domain peer by descriptor.
+ *
+ * A peer can be named three ways -- as the socket object itself, as a bound
+ * socket's filesystem node, or as an fdescfs /dev/fd node standing in for a
+ * socket descriptor -- and each of the two filesystem nodes can be reached
+ * either by an empty sun_path over a descriptor or by a pathname. The socket
+ * object has no pathname form (a path that names a descriptor is the /dev/fd
+ * node, not the socket directly), giving five combinations, all of which must
+ * reach the same peer:
+ *
+ * | empty sun_path (fd) | pathname
+ * -----------------------+--------------------------+-----------------------
+ * socket object | fd is the socket | (n/a: a path to a
+ * | -> stream, dgram, ... | descriptor is /dev/fd)
+ * -----------------------+--------------------------+-----------------------
+ * bound socket file | O_PATH handle of the | classic bind-path
+ * (VSOCK vnode) | socket's vnode | lookup
+ * | -> empty_path_vnode | -> path
+ * -----------------------+--------------------------+-----------------------
+ * /dev/fd node of a | O_PATH handle of the | /dev/fd/N, absolute
+ * socket descriptor | fdescfs node | or dir-relative
+ * (VNON vnode) | -> empty_path_devfd | -> devfd, devfd_relative
+ *
+ * An empty sun_path is signalled by sun_len == offsetof(.., sun_path).
+ */
+
+#include <sys/socket.h>
+#include <sys/capsicum.h>
+#include <sys/un.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <netinet/in.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <atf-c.h>
+
+/* An AF_UNIX address with an empty path: "the fd is the peer". */
+static const struct sockaddr_un empty_sun = {
+ .sun_family = AF_UNIX,
+ .sun_len = offsetof(struct sockaddr_un, sun_path),
+};
+
+/*
+ * Make a bound, listening stream socket. Binding is not optional:
+ * uipc_listen() refuses unbound sockets with EDESTADDRREQ.
+ */
+static int
+mklistener(const char *path)
+{
+ struct sockaddr_un sun = { .sun_family = AF_UNIX };
+ int l;
+
+ strlcpy(sun.sun_path, path, sizeof(sun.sun_path));
+ sun.sun_len = SUN_LEN(&sun);
+ ATF_REQUIRE((l = socket(PF_UNIX, SOCK_STREAM, 0)) >= 0);
+ ATF_REQUIRE_MSG(bind(l, (struct sockaddr *)&sun, sun.sun_len) == 0,
+ "bind(%s): %s", path, strerror(errno));
+ ATF_REQUIRE_MSG(listen(l, 1) == 0, "listen: %s", strerror(errno));
+ return (l);
+}
+
+static int
+fdconnect(int fd, int s)
+{
+ return (connectat(fd, s, (const struct sockaddr *)&empty_sun,
+ empty_sun.sun_len));
+}
+
+/* connectat(2) to a pathname, relative to fd (AT_FDCWD for absolute). */
+static int
+pathconnect(int fd, int s, const char *path)
+{
+ struct sockaddr_un sun = { .sun_family = AF_UNIX };
+
+ strlcpy(sun.sun_path, path, sizeof(sun.sun_path));
+ sun.sun_len = SUN_LEN(&sun);
+ return (connectat(fd, s, (const struct sockaddr *)&sun, sun.sun_len));
+}
+
+/*
+ * Build "/dev/fd/<fd>" into a caller buffer, and skip the test unless it
+ * resolves -- i.e. unless /dev/fd exposes descriptors beyond the standard
+ * streams (an fdescfs mount, or a devfs that happens to expose this one).
+ */
+static void
+require_devfd(int fd, char *path, size_t len)
+{
+ (void)snprintf(path, len, "/dev/fd/%d", fd);
+ if (access(path, F_OK) != 0)
+ atf_tc_skip("%s unavailable (fdescfs not mounted at /dev/fd?)",
+ path);
+}
+
+/* Connect to a listening stream socket by its fd; pass data. */
+ATF_TC_WITHOUT_HEAD(stream);
+ATF_TC_BODY(stream, tc)
+{
+ char buf[8];
+ int l, s, a;
+
+ l = mklistener("stream.sock");
+ ATF_REQUIRE((s = socket(PF_UNIX, SOCK_STREAM, 0)) >= 0);
+ ATF_REQUIRE_EQ(0, fdconnect(l, s));
+ ATF_REQUIRE((a = accept(l, NULL, NULL)) >= 0);
+
+ ATF_REQUIRE_EQ(5, write(s, "hello", 5));
+ ATF_REQUIRE_EQ(5, read(a, buf, sizeof(buf)));
+ ATF_REQUIRE_EQ(0, memcmp(buf, "hello", 5));
+ ATF_REQUIRE_EQ(5, write(a, "world", 5));
+ ATF_REQUIRE_EQ(5, read(s, buf, sizeof(buf)));
+ ATF_REQUIRE_EQ(0, memcmp(buf, "world", 5));
+
+ close(a);
+ close(s);
+ close(l);
+}
+
+/* A bound listener's path is still reported to the connecting side. */
+ATF_TC_WITHOUT_HEAD(stream_bound);
+ATF_TC_BODY(stream_bound, tc)
+{
+ struct sockaddr_un sun;
+ socklen_t len;
+ int l, s;
+
+ l = mklistener("bound.sock");
+ ATF_REQUIRE((s = socket(PF_UNIX, SOCK_STREAM, 0)) >= 0);
+ ATF_REQUIRE_EQ(0, fdconnect(l, s));
+
+ memset(&sun, 0, sizeof(sun));
+ len = sizeof(sun);
+ ATF_REQUIRE_EQ(0, getpeername(s, (struct sockaddr *)&sun, &len));
+ ATF_REQUIRE_EQ(0, strcmp(sun.sun_path, "bound.sock"));
+
+ close(s);
+ close(l);
+}
+
+/* Connect a datagram socket to an unbound peer by its fd. */
+ATF_TC_WITHOUT_HEAD(dgram);
+ATF_TC_BODY(dgram, tc)
+{
+ char buf[8];
+ int p, s;
+
+ ATF_REQUIRE((p = socket(PF_UNIX, SOCK_DGRAM, 0)) >= 0);
+ ATF_REQUIRE((s = socket(PF_UNIX, SOCK_DGRAM, 0)) >= 0);
+ ATF_REQUIRE_EQ(0, fdconnect(p, s));
+ ATF_REQUIRE_EQ(5, send(s, "hello", 5, 0));
+ ATF_REQUIRE_EQ(5, recv(p, buf, sizeof(buf), 0));
+ ATF_REQUIRE_EQ(0, memcmp(buf, "hello", 5));
+
+ close(s);
+ close(p);
+}
+
+/*
+ * Matrix cell: empty path + a descriptor that names a bound socket's *vnode*
+ * (an O_PATH handle), not the socket object. getsock() sees a non-socket and
+ * the connect falls back to an EMPTYPATH lookup that resolves the vnode.
+ */
+ATF_TC_WITHOUT_HEAD(empty_path_vnode);
+ATF_TC_BODY(empty_path_vnode, tc)
+{
+ int l, s, a, pathfd;
+
+ l = mklistener("evnode.sock");
+ ATF_REQUIRE_MSG((pathfd = open("evnode.sock", O_PATH)) >= 0,
+ "open(O_PATH): %s", strerror(errno));
+ ATF_REQUIRE((s = socket(PF_UNIX, SOCK_STREAM, 0)) >= 0);
+ ATF_REQUIRE_EQ(0, fdconnect(pathfd, s));
+ ATF_REQUIRE((a = accept(l, NULL, NULL)) >= 0);
+
+ close(a);
+ close(s);
+ close(pathfd);
+ close(l);
+}
+
+/*
+ * Matrix cell: non-empty path naming a bound socket's vnode -- the classic
+ * connect-by-pathname case, here spelled through connectat(2).
+ */
+ATF_TC_WITHOUT_HEAD(path);
+ATF_TC_BODY(path, tc)
+{
+ int l, s, a;
+
+ l = mklistener("path.sock");
+ ATF_REQUIRE((s = socket(PF_UNIX, SOCK_STREAM, 0)) >= 0);
+ ATF_REQUIRE_EQ(0, pathconnect(AT_FDCWD, s, "path.sock"));
+ ATF_REQUIRE((a = accept(l, NULL, NULL)) >= 0);
+
+ close(a);
+ close(s);
+ close(l);
+}
+
+/*
+ * Matrix cell: non-empty path that resolves to the socket *object* -- a
+ * /dev/fd/N pathname. This is plain connect(2), no empty path involved.
+ * Requires /dev/fd to expose descriptors above 2 (e.g. fdescfs).
+ */
+ATF_TC_WITHOUT_HEAD(devfd);
+ATF_TC_BODY(devfd, tc)
+{
+ char path[32];
+ int l, s, a;
+
+ l = mklistener("devfd.sock");
+ require_devfd(l, path, sizeof(path));
+
+ ATF_REQUIRE((s = socket(PF_UNIX, SOCK_STREAM, 0)) >= 0);
+ ATF_REQUIRE_EQ(0, pathconnect(AT_FDCWD, s, path));
+ ATF_REQUIRE((a = accept(l, NULL, NULL)) >= 0);
+
+ close(a);
+ close(s);
+ close(l);
+}
+
+/*
+ * Matrix cell variant of `devfd`: the same /dev/fd/N socket-object lookup, but
+ * reached through connectat(2)'s dirfd-relative resolution instead of an
+ * absolute path. A directory fd on the fdescfs mount serves as the base, and
+ * the peer is named by the *relative* path "N" -- the listener's fd number.
+ * NDINIT_ATRIGHTS anchors namei() at the dirfd, and fdescfs resolves that
+ * descriptor to the socket unp_connectat() connects to.
+ */
+ATF_TC_WITHOUT_HEAD(devfd_relative);
+ATF_TC_BODY(devfd_relative, tc)
+{
+ char path[32];
+ int l, s, a, dirfd;
+
+ l = mklistener("devfd_rel.sock");
+ /* Probe the absolute path; the connect goes through the dirfd below. */
+ require_devfd(l, path, sizeof(path));
+ ATF_REQUIRE_MSG((dirfd = open("/dev/fd", O_DIRECTORY)) >= 0,
+ "open(/dev/fd, O_DIRECTORY): %s", strerror(errno));
+
+ /* Name the listener by its fd number, relative to the fdescfs dir. */
+ (void)snprintf(path, sizeof(path), "%d", l);
+ ATF_REQUIRE((s = socket(PF_UNIX, SOCK_STREAM, 0)) >= 0);
+ ATF_REQUIRE_EQ(0, pathconnect(dirfd, s, path));
+ ATF_REQUIRE((a = accept(l, NULL, NULL)) >= 0);
+
+ close(a);
+ close(s);
+ close(dirfd);
+ close(l);
+}
+
+/*
+ * Matrix cell: empty path + an O_PATH handle to a /dev/fd/N node. getsock()
+ * sees a non-socket, the EMPTYPATH lookup resolves the synthetic fdescfs node,
+ * and opening that node yields the underlying descriptor -- the same socket.
+ * Reaches the /dev/fd node by fd rather than by pathname.
+ */
+ATF_TC_WITHOUT_HEAD(empty_path_devfd);
+ATF_TC_BODY(empty_path_devfd, tc)
+{
+ char path[32];
+ int l, s, a, pathfd;
+
+ l = mklistener("edevfd.sock");
+ require_devfd(l, path, sizeof(path));
+ ATF_REQUIRE_MSG((pathfd = open(path, O_PATH)) >= 0,
+ "open(%s, O_PATH): %s", path, strerror(errno));
+ ATF_REQUIRE((s = socket(PF_UNIX, SOCK_STREAM, 0)) >= 0);
+ ATF_REQUIRE_EQ(0, fdconnect(pathfd, s));
+ ATF_REQUIRE((a = accept(l, NULL, NULL)) >= 0);
+
+ close(a);
+ close(s);
+ close(pathfd);
+ close(l);
+}
+
+/*
+ * Best-effort probe of the single-level /dev/fd/N resolution: connect a fresh
+ * socket to 'path' (a /dev/fd/N whose descriptor is an O_PATH handle) and
+ * report which behaviour the live fdescfs mount produced.
+ *
+ * With a standard mount the descriptor is not a socket, so the connect fails
+ * with ENOTSOCK -- our resolver does not chase the O_PATH another level. With
+ * a "nodup" mount fdescfs dereferences the O_PATH to its underlying vnode
+ * before we see it, so the connect instead reaches 'l'. Either is accepted;
+ * only an unexpected error fails the test.
+ */
+static void
+report_indirect(int l, const char *path, const char *what)
+{
+ int s, a, ret, e;
+
+ ATF_REQUIRE((s = socket(PF_UNIX, SOCK_STREAM, 0)) >= 0);
+ ret = pathconnect(AT_FDCWD, s, path);
+ e = errno;
+ if (ret == 0) {
+ printf("%s: connected -- assuming a nodup fdescfs mount "
+ "(O_PATH dereferenced)\n", what);
+ ATF_REQUIRE_MSG((a = accept(l, NULL, NULL)) >= 0,
+ "%s: connected but no peer arrived: %s", what,
+ strerror(errno));
+ close(a);
+ } else {
+ ATF_REQUIRE_MSG(e == ENOTSOCK,
+ "%s: expected a connect or ENOTSOCK, got: %s", what,
+ strerror(e));
+ printf("%s: ENOTSOCK -- single-level resolution "
+ "(standard fdescfs mount)\n", what);
+ }
+ close(s);
+}
+
+/*
+ * The /dev/fd/N pathname is resolved a single level: N must be the socket
+ * descriptor itself. A /dev/fd/N whose descriptor is instead an O_PATH handle
+ * -- of the socket's *file*, or of another /dev/fd/N node -- is not chased
+ * another level, so with a standard mount the connect fails with ENOTSOCK.
+ * (A "nodup" mount dereferences the O_PATH in fdescfs before we see it; this
+ * test reports whichever behaviour the live mount produces.)
+ */
+ATF_TC_WITHOUT_HEAD(devfd_indirect);
+ATF_TC_BODY(devfd_indirect, tc)
+{
+ char path[32], probe[32];
+ int l, pathfd, devfdfd;
+
+ l = mklistener("devfd_ind.sock");
+ require_devfd(l, probe, sizeof(probe));
+
+ /* /dev/fd/N whose descriptor is an O_PATH handle of the socket's file. */
+ ATF_REQUIRE_MSG((pathfd = open("devfd_ind.sock", O_PATH)) >= 0,
+ "open(O_PATH): %s", strerror(errno));
+ (void)snprintf(path, sizeof(path), "/dev/fd/%d", pathfd);
+ report_indirect(l, path, "socket file");
+ close(pathfd);
+
+ /* /dev/fd/N whose descriptor is an O_PATH handle of another such node. */
+ ATF_REQUIRE_MSG((devfdfd = open(probe, O_PATH)) >= 0,
+ "open(%s, O_PATH): %s", probe, strerror(errno));
+ (void)snprintf(path, sizeof(path), "/dev/fd/%d", devfdfd);
+ report_indirect(l, path, "devfd node");
+ close(devfdfd);
+
+ close(l);
+}
+
+/* An empty path is only meaningful with a real descriptor. */
+ATF_TC_WITHOUT_HEAD(empty_path_at_fdcwd);
+ATF_TC_BODY(empty_path_at_fdcwd, tc)
+{
+ int s;
+
+ ATF_REQUIRE((s = socket(PF_UNIX, SOCK_STREAM, 0)) >= 0);
+ ATF_REQUIRE_ERRNO(EINVAL, connect(s,
+ (const struct sockaddr *)&empty_sun, empty_sun.sun_len) == -1);
+ ATF_REQUIRE_ERRNO(EINVAL, fdconnect(AT_FDCWD, s) == -1);
+ close(s);
+}
+
+/* Error matrix for unsuitable descriptors and peers. */
+ATF_TC_WITHOUT_HEAD(bad_peers);
+ATF_TC_BODY(bad_peers, tc)
+{
+ int s, d, fd;
+
+ ATF_REQUIRE((s = socket(PF_UNIX, SOCK_STREAM, 0)) >= 0);
+ ATF_REQUIRE((d = socket(PF_UNIX, SOCK_DGRAM, 0)) >= 0);
+
+ /* Non-socket descriptor. */
+ ATF_REQUIRE((fd = open(".", O_RDONLY)) >= 0);
+ ATF_REQUIRE_ERRNO(ENOTSOCK, fdconnect(fd, s) == -1);
+ close(fd);
+
+ /* Socket from another domain. */
+ ATF_REQUIRE((fd = socket(PF_INET, SOCK_STREAM, 0)) >= 0);
+ ATF_REQUIRE_ERRNO(EPROTOTYPE, fdconnect(fd, s) == -1);
+ close(fd);
+
+ /* Type mismatch between the two unix sockets. */
+ fd = mklistener("mismatch.sock");
+ ATF_REQUIRE_ERRNO(EPROTOTYPE, fdconnect(fd, d) == -1);
+
+ close(fd);
+
+ /* Stream peer that is not listening: 's' never called listen(2). */
+ ATF_REQUIRE((fd = socket(PF_UNIX, SOCK_STREAM, 0)) >= 0);
+ ATF_REQUIRE_ERRNO(ECONNREFUSED, fdconnect(s, fd) == -1);
+
+ close(fd);
+ close(d);
+ close(s);
+}
+
+/*
+ * A descriptor limited to CAP_CONNECTAT is a pure connect-to-me token:
+ * it can be connected to, but not listened on, accepted from, or read.
+ */
+ATF_TC_WITHOUT_HEAD(cap_connectat);
+ATF_TC_BODY(cap_connectat, tc)
+{
+ cap_rights_t rights;
+ char buf[8];
+ int l, s, token, a;
+
+ l = mklistener("cap.sock");
+ ATF_REQUIRE((token = dup(l)) >= 0);
+ ATF_REQUIRE_EQ(0, cap_rights_limit(token,
+ cap_rights_init(&rights, CAP_CONNECTAT)));
+
+ ATF_REQUIRE_ERRNO(ENOTCAPABLE, listen(token, 1) == -1);
+ ATF_REQUIRE_ERRNO(ENOTCAPABLE, accept(token, NULL, NULL) == -1);
+ ATF_REQUIRE_ERRNO(ENOTCAPABLE, read(token, buf, sizeof(buf)) == -1);
+
+ ATF_REQUIRE((s = socket(PF_UNIX, SOCK_STREAM, 0)) >= 0);
+ ATF_REQUIRE_EQ(0, fdconnect(token, s));
+ ATF_REQUIRE((a = accept(l, NULL, NULL)) >= 0);
+
+ close(a);
+ close(s);
+ close(token);
+ close(l);
+}
+
+/* Without CAP_CONNECTAT, the descriptor cannot be a connect target. */
+ATF_TC_WITHOUT_HEAD(cap_connectat_denied);
+ATF_TC_BODY(cap_connectat_denied, tc)
+{
+ cap_rights_t rights;
+ int l, s, token;
+
+ l = mklistener("capdeny.sock");
+ ATF_REQUIRE((token = dup(l)) >= 0);
+ ATF_REQUIRE_EQ(0, cap_rights_limit(token,
+ cap_rights_init(&rights, CAP_READ, CAP_WRITE)));
+
+ ATF_REQUIRE((s = socket(PF_UNIX, SOCK_STREAM, 0)) >= 0);
+ ATF_REQUIRE_ERRNO(ENOTCAPABLE, fdconnect(token, s) == -1);
+
+ close(s);
+ close(token);
+ close(l);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, stream);
+ ATF_TP_ADD_TC(tp, stream_bound);
+ ATF_TP_ADD_TC(tp, dgram);
+ ATF_TP_ADD_TC(tp, empty_path_vnode);
+ ATF_TP_ADD_TC(tp, path);
+ ATF_TP_ADD_TC(tp, devfd);
+ ATF_TP_ADD_TC(tp, devfd_relative);
+ ATF_TP_ADD_TC(tp, empty_path_devfd);
+ ATF_TP_ADD_TC(tp, devfd_indirect);
+ ATF_TP_ADD_TC(tp, empty_path_at_fdcwd);
+ ATF_TP_ADD_TC(tp, bad_peers);
+ ATF_TP_ADD_TC(tp, cap_connectat);
+ ATF_TP_ADD_TC(tp, cap_connectat_denied);
+
+ return (atf_no_error());
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Aug 1, 4:21 PM (21 h, 45 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
35825615
Default Alt Text
D58405.id182712.diff (16 KB)
Attached To
Mode
D58405: unix: allow `connectat(2)` to name the peer socket by descriptor
Attached
Detach File
Event Timeline
Log In to Comment