sd-varlink: mark varlink sockets and entrypoint inodes as varlink via xattrs

This commit is contained in:
Lennart Poettering
2026-06-23 22:55:23 +02:00
parent f304250315
commit 96a37e3493
2 changed files with 151 additions and 13 deletions
+79
View File
@@ -55,6 +55,84 @@
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>
@@ -87,6 +165,7 @@
<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>
+72 -13
View 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;