]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
sd-varlink: mark varlink sockets and entrypoint inodes as varlink via xattrs
authorLennart Poettering <lennart@amutable.com>
Tue, 2 Jun 2026 15:36:52 +0000 (17:36 +0200)
committerLennart Poettering <lennart@amutable.com>
Tue, 23 Jun 2026 20:55:23 +0000 (22:55 +0200)
man/sd-varlink.xml
src/libsystemd/sd-varlink/sd-varlink.c

index 576bdf1b7f8d1cea79deb0c6d487493e8c5af06d..bd2b35565234d7f682945763df1e3643a6e75744 100644 (file)
     makes the functionality implemented by sd-varlink available from the command line.</para>
   </refsect1>
 
+  <refsect1>
+    <title>Recognizing Varlink Socket Inodes</title>
+
+    <para>On kernels that support extended attributes on socket inodes (available since Linux 7.0),
+    sd-varlink automatically tags the <constant>AF_UNIX</constant> socket inodes it creates and manages with
+    the <varname>user.varlink</varname> extended attribute. The attribute records the role the socket plays
+    in the Varlink communication, allowing other processes to discover and classify Varlink sockets, for
+    example via <command>varlinkctl list-sockets</command> (see
+    <citerefentry><refentrytitle>varlinkctl</refentrytitle><manvolnum>1</manvolnum></citerefentry>).</para>
+
+    <para>Four distinct roles are defined:</para>
+
+    <table>
+      <title><varname>user.varlink</varname> extended attribute roles</title>
+
+      <tgroup cols='2'>
+        <colspec colname='value' />
+        <colspec colname='meaning' />
+        <thead>
+          <row>
+            <entry>Attribute value</entry>
+            <entry>Meaning</entry>
+          </row>
+        </thead>
+        <tbody>
+          <row>
+            <entry><literal>client</literal></entry>
+            <entry>Set on the connecting end of a connection, i.e. a socket created via <function>socket()</function> + <function>connect()</function>. Only for inodes on the anonymous socket file system (<literal>sockfs</literal>).</entry>
+          </row>
+          <row>
+            <entry><literal>server</literal></entry>
+            <entry>Set on the serving end of a connection, i.e. a socket obtained via <function>accept()</function> on a listening socket. Only for inodes on the anonymous socket file system (<literal>sockfs</literal>).</entry>
+          </row>
+          <row>
+            <entry><literal>listen</literal></entry>
+            <entry>Set on a listening socket, i.e. a socket created via <function>socket()</function> + <function>listen()</function>. Only for inodes on the anonymous socket file system (<literal>sockfs</literal>).</entry>
+          </row>
+          <row>
+            <entry><literal>entrypoint</literal></entry>
+            <entry>Set on the entrypoint socket inode, i.e. the socket node bound into the regular file system that clients connect to. Unlike the other three, this attribute lives on a real file-system inode (not on <literal>sockfs</literal>) and is hence visible to any process that can resolve the socket path.</entry>
+          </row>
+        </tbody>
+      </tgroup>
+    </table>
+
+    <para>Note that for socket activated Varlink services it is recommended to set these extended attributes
+    via <varname>XAttrEntryPoint=</varname>, <varname>XAttrListen=</varname> and
+    <varname>XAttrAccept=</varname> settings on the socket units, see
+    <citerefentry><refentrytitle>systemd.socket</refentrytitle><manvolnum>5</manvolnum></citerefentry> for
+    details. Specifically, for socket units for which <varname>Accept=no</varname> is set:</para>
+
+    <programlisting>…
+[Socket]
+SocketMode=0644
+XAttrEntryPoint=user.varlink=entrypoint
+XAttrListen=user.varlink=listen
+…</programlisting>
+
+    <para>For socket units which have <varname>Accept=yes</varname>:</para>
+
+    <programlisting>…
+[Socket]
+Accept=yes
+SocketMode=0644
+XAttrEntryPoint=user.varlink=entrypoint
+XAttrListen=user.varlink=listen
+XAttrAccept=user.varlink=server
+…</programlisting>
+
+    <para>Note that for Varlink sockets that shall not be world-accessible it is recommended to unset the
+    relevant "w" bits in the socket access mode, but to keep the relevant "r" bits set. As the access check
+    done by <function>connect()</function> looks for the "w" bits, this does not make the service
+    unnecessarily accessible, however it has the benefit that the extended attributes that identify the
+    socket as Varlink-related may still be read by other users (as that's restricted by the "r" bit). Or in
+    other words, it's recommended to set <literal>SocketMode=0644</literal> for services that shall not be
+    accessible to arbitrary users, rather than <literal>SocketMode=0600</literal>.</para>
+  </refsect1>
+
   <xi:include href="libsystemd-pkgconfig.xml" />
 
   <refsect1>
       <member><citerefentry><refentrytitle>sd-event</refentrytitle><manvolnum>3</manvolnum></citerefentry></member>
       <member><citerefentry><refentrytitle>sd-json</refentrytitle><manvolnum>3</manvolnum></citerefentry></member>
       <member><citerefentry><refentrytitle>varlinkctl</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
+      <member><citerefentry><refentrytitle>systemd.socket</refentrytitle><manvolnum>5</manvolnum></citerefentry></member>
       <member><citerefentry><refentrytitle>sd-bus</refentrytitle><manvolnum>3</manvolnum></citerefentry></member>
       <member><citerefentry project='die-net'><refentrytitle>pkg-config</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
     </simplelist></para>
index f484c9da4498a47121d65cedfb820c1028ae872a..9f06f6926555c4414aa03bdd386c3b85a5a33869 100644 (file)
@@ -39,6 +39,7 @@
 #include "varlink-io.systemd.h"
 #include "varlink-org.varlink.service.h"
 #include "varlink-util.h"
+#include "xattr-util.h"
 
 #define VARLINK_DEFAULT_CONNECTIONS_MAX 4096U
 #define VARLINK_DEFAULT_CONNECTIONS_PER_UID_MAX 128U
@@ -154,6 +155,33 @@ static int varlink_new(sd_varlink **ret) {
         return 0;
 }
 
+static int mark_varlink_socket(int fd, const char *path, const char *role) {
+        int r;
+
+        assert(wildcard_fd_is_valid(fd));
+        assert(role);
+
+        /* We define four roles:
+         *
+         *    1. "client"     → this socket was created via socket()+connect()     [sockfs]
+         *    2. "server"     → this socket was created via accept()               [sockfs]
+         *    3. "listen"     → this socket was created via socket()+listen()      [sockfs]
+         *    4. "entrypoint" → this is the entrypoint socket inode                [not sockfs]
+         */
+
+        r = socket_xattr_supported(); /* Let's check for the feature directly, to make use of the cache */
+        if (r < 0)
+                return r;
+        if (r == 0)
+                return -EOPNOTSUPP;
+
+        r = xsetxattr(fd, path, AT_EMPTY_PATH, "user.varlink", role);
+        if (r < 0)
+                return log_debug_errno(r, "Failed to set 'user.varlink' to '%s': %m", role);
+
+        return 0;
+}
+
 _public_ int sd_varlink_connect_address(sd_varlink **ret, const char *address) {
         _cleanup_(sd_varlink_unrefp) sd_varlink *v = NULL;
         int r;
@@ -169,14 +197,31 @@ _public_ int sd_varlink_connect_address(sd_varlink **ret, const char *address) {
         if (r < 0)
                 return r;
 
+        (void) mark_varlink_socket(v->stream.input_fd, /* path= */ NULL, "client");
+
         varlink_set_state(v, VARLINK_IDLE_CLIENT);
 
         *ret = TAKE_PTR(v);
         return 0;
 }
 
-_public_ int sd_varlink_connect_exec(sd_varlink **ret, const char *_command, char **_argv) {
+static int varlink_socketpair(int *ret_client_fd, int *ret_server_fd) {
+        assert(ret_client_fd);
+        assert(ret_server_fd);
+
         _cleanup_close_pair_ int pair[2] = EBADF_PAIR;
+        if (socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0, pair) < 0)
+                return log_debug_errno(errno, "Failed to allocate AF_UNIX socket pair: %m");
+
+        (void) mark_varlink_socket(pair[0], /* path= */ NULL, "client");
+        (void) mark_varlink_socket(pair[1], /* path= */ NULL, "server");
+
+        *ret_client_fd = TAKE_FD(pair[0]);
+        *ret_server_fd = TAKE_FD(pair[1]);
+        return 0;
+}
+
+_public_ int sd_varlink_connect_exec(sd_varlink **ret, const char *_command, char **_argv) {
         _cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL;
         _cleanup_free_ char *command = NULL;
         _cleanup_strv_free_ char **argv = NULL;
@@ -200,17 +245,19 @@ _public_ int sd_varlink_connect_exec(sd_varlink **ret, const char *_command, cha
 
         log_debug("Forking off Varlink child process '%s'.", command);
 
-        if (socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0, pair) < 0)
-                return log_debug_errno(errno, "Failed to allocate AF_UNIX socket pair: %m");
+        _cleanup_close_ int client_fd = -EBADF, server_fd = -EBADF;
+        r = varlink_socketpair(&client_fd, &server_fd);
+        if (r < 0)
+                return r;
 
-        r = fd_nonblock(pair[1], false);
+        r = fd_nonblock(server_fd, false);
         if (r < 0)
                 return log_debug_errno(r, "Failed to disable O_NONBLOCK for varlink socket: %m");
 
         r = pidref_safe_fork_full(
                         "(sd-vlexec)",
                         /* stdio_fds= */ NULL,
-                        /* except_fds= */ (int[]) { pair[1] },
+                        /* except_fds= */ (int[]) { server_fd },
                         /* n_except_fds= */ 1,
                         FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_PACK_FDS|FORK_CLOEXEC_OFF|FORK_REOPEN_LOG|FORK_DEATHSIG_SIGTERM|FORK_RLIMIT_NOFILE_SAFE,
                         &pidref);
@@ -249,14 +296,14 @@ _public_ int sd_varlink_connect_exec(sd_varlink **ret, const char *_command, cha
                 _exit(EXIT_FAILURE);
         }
 
-        pair[1] = safe_close(pair[1]);
+        server_fd = safe_close(server_fd);
 
         sd_varlink *v;
         r = varlink_new(&v);
         if (r < 0)
                 return log_debug_errno(r, "Failed to create varlink object: %m");
 
-        int conn_fd = TAKE_FD(pair[0]);
+        int conn_fd = TAKE_FD(client_fd);
         r = json_stream_attach_fds(&v->stream, conn_fd, conn_fd);
         if (r < 0)
                 return r;
@@ -280,7 +327,6 @@ static int ssh_path(const char **ret) {
 }
 
 static int varlink_connect_ssh_unix(sd_varlink **ret, const char *where) {
-        _cleanup_close_pair_ int pair[2] = EBADF_PAIR;
         _cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL;
         int r;
 
@@ -316,12 +362,14 @@ static int varlink_connect_ssh_unix(sd_varlink **ret, const char *where) {
 
         log_debug("Forking off SSH child process '%s -W %s %s'.", ssh, p, h);
 
-        if (socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0, pair) < 0)
-                return log_debug_errno(errno, "Failed to allocate AF_UNIX socket pair: %m");
+        _cleanup_close_ int client_fd = -EBADF, server_fd = -EBADF;
+        r = varlink_socketpair(&client_fd, &server_fd);
+        if (r < 0)
+                return r;
 
         r = pidref_safe_fork_full(
                         "(sd-vlssh)",
-                        /* stdio_fds= */ (int[]) { pair[1], pair[1], STDERR_FILENO },
+                        /* stdio_fds= */ (int[]) { server_fd, server_fd, STDERR_FILENO },
                         /* except_fds= */ NULL,
                         /* n_except_fds= */ 0,
                         FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_REOPEN_LOG|FORK_RLIMIT_NOFILE_SAFE|FORK_REARRANGE_STDIO,
@@ -336,14 +384,14 @@ static int varlink_connect_ssh_unix(sd_varlink **ret, const char *where) {
                 _exit(EXIT_FAILURE);
         }
 
-        pair[1] = safe_close(pair[1]);
+        server_fd = safe_close(server_fd);
 
         sd_varlink *v;
         r = varlink_new(&v);
         if (r < 0)
                 return log_debug_errno(r, "Failed to create varlink object: %m");
 
-        int conn_fd = TAKE_FD(pair[0]);
+        int conn_fd = TAKE_FD(client_fd);
         r = json_stream_attach_fds(&v->stream, conn_fd, conn_fd);
         if (r < 0)
                 return r;
@@ -562,6 +610,8 @@ _public_ int sd_varlink_connect_fd_pair(sd_varlink **ret, int input_fd, int outp
         if (r < 0)
                 return r;
 
+        (void) mark_varlink_socket(v->stream.input_fd, /* path= */ NULL, "client");
+
         if (override_ucred)
                 json_stream_set_peer_ucred(&v->stream, override_ucred);
 
@@ -3283,6 +3333,8 @@ _public_ int sd_varlink_server_add_connection_pair(
                 }
         }
 
+        (void) mark_varlink_socket(input_fd, /* path= */ NULL, "server");
+
         if (ret)
                 *ret = v;
 
@@ -3394,6 +3446,8 @@ _public_ int sd_varlink_server_listen_fd(sd_varlink_server *s, int fd) {
         if (r < 0)
                 return r;
 
+        (void) mark_varlink_socket(ss->fd, /* path= */ NULL, "listen");
+
         LIST_PREPEND(sockets, s->sockets, TAKE_PTR(ss));
         return 0;
 }
@@ -3431,6 +3485,8 @@ _public_ int sd_varlink_server_listen_address(sd_varlink_server *s, const char *
 
         fd = fd_move_above_stdio(fd);
 
+        (void) mark_varlink_socket(fd, /* path= */ NULL, "listen");
+
         /* See the comment in sd_varlink_server_listen_fd() */
         if (FLAGS_SET(s->flags, SD_VARLINK_SERVER_FD_PASSING_INPUT_STRICT))
                 (void) setsockopt_int(fd, SOL_SOCKET, SO_PASSRIGHTS, FLAGS_SET(s->flags, SD_VARLINK_SERVER_ALLOW_FD_PASSING_INPUT));
@@ -3442,6 +3498,9 @@ _public_ int sd_varlink_server_listen_address(sd_varlink_server *s, const char *
         if (r < 0)
                 return r;
 
+        if (path_is_absolute(address))
+                (void) mark_varlink_socket(AT_FDCWD, address, "entrypoint");
+
         if (listen(fd, SOMAXCONN_DELUXE) < 0)
                 return -errno;