Page MenuHomeFreeBSD

D58405.id182712.diff
No OneTemporary

D58405.id182712.diff

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

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)

Event Timeline