From 382f141fead03714820f274319bc606883a61c48 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 6 Feb 2026 15:04:11 +0100 Subject: [PATCH 1/7] units: symlink well-known Varlink services into /run/varlink/registry/ So far we didn't provide any concept to enumerate local Varlink services. Let's change that. Let's define very light-weight scheme for this: provide a well-known dir /run/varlink/registry/ where services that implement public interfaces can link their sockets into. When enumerating services it's thus sufficient to enumerate inodes in that directory. The usecase for this is twofold: 1. It's simply very useful to be able to see which public services are bound on the local system, for debugging/admin/development purposes. 2. At Amutable we'd like to optionally provide a HTTP-to-Varlink bridge on individual nodes, that allows remote peers (after authentication) to access local Varlink services. For that it's essential we know the list of services and their entrypoints to expose, it would be security-wise highly problematic for clients to provide AF_UNIX entrypoint paths when connecting. hence: let's instead just have a dir with the public stuff, and let's ensure the HTTP-to-Varlink bridge simply exposes that stuff, and nothing else. Non-public interfaces (such as the oomd interfaces between PID 1 and oomd), and interfaces with multiple implementors (such as the resolved hook interface, or the metrics collection stuff) should not be linked in. This is inspired by the Varlink.org "registry" concept, briefly explained here: https://varlink.org/FAQ#how-do-i-find-the-service-which-implements-a-local-interface Note however that the described Varlink interface is not actually implemented here, the directory is introduced however in a fashion that conceptually matches the registry defined there, and would allow us to implement the registry interface on top of it. (One of the reason the registry Varlink API is not implemented right now is that the URI format it relies on is entirely unspecified in the Varlink docs right now. Some research needs to be done to extract what's implemented in the reference implementation and to determine how it maps to the Varlink entrypoint address format systemd's own tooling currently uses) This primarily installs the symlinks via Symlinks= in unit files and via a new tmpfiles.d/ drop-in. But since we touch all .socket units relating to Varlink this also sets the FileDescriptorName= to varlink for each, just to minimize diffrences and make things work more alike (the services in questin don't care about the name, so this doesn't change). In one case we replace a pair of separate sockets for two closely related varlink services by a socket and a symlink, so that we can safely use Symlinks= to also install the registry symlinks. --- tmpfiles.d/20-systemd-varlink.conf | 18 ++++++++++++++++++ tmpfiles.d/meson.build | 3 +++ tmpfiles.d/user/20-systemd-varlink.conf | 7 +++++++ tmpfiles.d/user/meson.build | 11 +++++++++++ units/systemd-ask-password.socket | 1 + units/systemd-bootctl.socket | 1 + units/systemd-creds.socket | 1 + units/systemd-factory-reset.socket | 1 + units/systemd-hostnamed.socket | 1 + units/systemd-importd.socket | 1 + units/systemd-logind-varlink.socket | 1 + units/systemd-machined.socket | 2 +- units/systemd-mountfsd.socket | 2 ++ units/systemd-mute-console.socket | 1 + units/systemd-networkd-varlink.socket | 1 + units/systemd-nsresourced.socket | 3 ++- units/systemd-pcrextend.socket | 1 + units/systemd-pcrlock.socket | 1 + units/systemd-repart.socket | 1 + units/systemd-resolved-monitor.socket | 1 + units/systemd-resolved-varlink.socket | 1 + units/systemd-sysext.socket | 1 + units/systemd-udevd-varlink.socket | 1 + units/systemd-userdbd.socket | 3 ++- units/user/systemd-ask-password.socket | 1 + units/user/systemd-importd.socket | 1 + units/user/systemd-machined.socket | 2 +- 27 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 tmpfiles.d/20-systemd-varlink.conf create mode 100644 tmpfiles.d/user/20-systemd-varlink.conf create mode 100644 tmpfiles.d/user/meson.build diff --git a/tmpfiles.d/20-systemd-varlink.conf b/tmpfiles.d/20-systemd-varlink.conf new file mode 100644 index 0000000000..96905ff4f1 --- /dev/null +++ b/tmpfiles.d/20-systemd-varlink.conf @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# See tmpfiles.d(5) for details. + +# Varlink AF_UNIX entrypoint socket inodes of relevant system services may be +# linked into this directory, to make them "well-known". Typically, tools such +# as the Varlink HTTP bridge expose services linked in here as public +# interfaces. The symlink should be named after the Varlink interface to +# expose. Interfaces that may be implemented by multiple services (such as +# the generic "io.systemd.service") should not be symlinked here. +d /run/varlink/registry/ 0755 root root + +# Socket activated services should use Symlinks= in the .socket unit file to +# create these symlinks. If that's not applicable, consider creating the +# symlinks via a tmpfiles.d/ snippet, like we do here, so that registration can +# be influenced by the administrator. +L /run/varlink/registry/io.systemd.Unit - - - - ../../systemd/io.systemd.Manager +L /run/varlink/registry/io.systemd.Manager - - - - ../../systemd/io.systemd.Manager +L /run/varlink/registry/io.systemd.Journal - - - - ../../systemd/journal/io.systemd.journal diff --git a/tmpfiles.d/meson.build b/tmpfiles.d/meson.build index 7f3fce48c9..c8f9015b2e 100644 --- a/tmpfiles.d/meson.build +++ b/tmpfiles.d/meson.build @@ -13,6 +13,7 @@ files = [['README' ], ['systemd-nspawn.conf', 'ENABLE_MACHINED' ], ['systemd-pstore.conf', 'ENABLE_PSTORE' ], ['systemd-resolve.conf', 'ENABLE_RESOLVE' ], + ['20-systemd-varlink.conf' ], ['systemd-tmp.conf' ], ['tmp.conf' ], ['x11.conf' ], @@ -59,3 +60,5 @@ endforeach if install_sysconfdir install_emptydir(sysconfdir / 'tmpfiles.d') endif + +subdir('user') diff --git a/tmpfiles.d/user/20-systemd-varlink.conf b/tmpfiles.d/user/20-systemd-varlink.conf new file mode 100644 index 0000000000..6df6408f18 --- /dev/null +++ b/tmpfiles.d/user/20-systemd-varlink.conf @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# See tmpfiles.d(5) for details. + +d %t/varlink/registry/ 0755 + +L %t/varlink/registry/io.systemd.Unit - - - - ../../systemd/io.systemd.Manager +L %t/varlink/registry/io.systemd.Manager - - - - ../../systemd/io.systemd.Manager diff --git a/tmpfiles.d/user/meson.build b/tmpfiles.d/user/meson.build new file mode 100644 index 0000000000..551e4fe56b --- /dev/null +++ b/tmpfiles.d/user/meson.build @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +files = [ '20-systemd-varlink.conf' ] + +foreach f : files + install_data(f, install_dir : usertmpfilesdir) +endforeach + +if install_sysconfdir + install_emptydir(sysconfdir / 'user-tmpfiles.d') +endif diff --git a/units/systemd-ask-password.socket b/units/systemd-ask-password.socket index c8d203b077..df5eaffc22 100644 --- a/units/systemd-ask-password.socket +++ b/units/systemd-ask-password.socket @@ -15,6 +15,7 @@ Before=sockets.target [Socket] ListenStream=/run/systemd/io.systemd.AskPassword +Symlinks=/run/varlink/registry/io.systemd.AskPassword FileDescriptorName=varlink SocketMode=0666 Accept=yes diff --git a/units/systemd-bootctl.socket b/units/systemd-bootctl.socket index f9553b840b..e720f24f54 100644 --- a/units/systemd-bootctl.socket +++ b/units/systemd-bootctl.socket @@ -16,6 +16,7 @@ Before=sockets.target [Socket] ListenStream=/run/systemd/io.systemd.BootControl +Symlinks=/run/varlink/registry/io.systemd.BootControl FileDescriptorName=varlink SocketMode=0600 Accept=yes diff --git a/units/systemd-creds.socket b/units/systemd-creds.socket index bf13c11e0f..3ea3ca5b05 100644 --- a/units/systemd-creds.socket +++ b/units/systemd-creds.socket @@ -15,6 +15,7 @@ Before=sockets.target [Socket] ListenStream=/run/systemd/io.systemd.Credentials +Symlinks=/run/varlink/registry/io.systemd.Credentials FileDescriptorName=varlink SocketMode=0666 Accept=yes diff --git a/units/systemd-factory-reset.socket b/units/systemd-factory-reset.socket index d833ffdb99..467517ec24 100644 --- a/units/systemd-factory-reset.socket +++ b/units/systemd-factory-reset.socket @@ -15,6 +15,7 @@ Before=sockets.target [Socket] ListenStream=/run/systemd/io.systemd.FactoryReset +Symlinks=/run/varlink/registry/io.systemd.FactoryReset FileDescriptorName=varlink SocketMode=0666 Accept=yes diff --git a/units/systemd-hostnamed.socket b/units/systemd-hostnamed.socket index 2a2cfce653..288e736b47 100644 --- a/units/systemd-hostnamed.socket +++ b/units/systemd-hostnamed.socket @@ -15,5 +15,6 @@ Documentation=man:machine-info(5) [Socket] ListenStream=/run/systemd/io.systemd.Hostname +Symlinks=/run/varlink/registry/io.systemd.Hostname FileDescriptorName=varlink SocketMode=0666 diff --git a/units/systemd-importd.socket b/units/systemd-importd.socket index d1c9c22a4c..a538ef0d0e 100644 --- a/units/systemd-importd.socket +++ b/units/systemd-importd.socket @@ -20,5 +20,6 @@ Before=shutdown.target [Socket] ListenStream=/run/systemd/io.systemd.Import +Symlinks=/run/varlink/registry/io.systemd.Import FileDescriptorName=varlink SocketMode=0666 diff --git a/units/systemd-logind-varlink.socket b/units/systemd-logind-varlink.socket index 3978e8ab90..1d3652e049 100644 --- a/units/systemd-logind-varlink.socket +++ b/units/systemd-logind-varlink.socket @@ -13,6 +13,7 @@ Documentation=man:systemd-logind.service(8) [Socket] ListenStream=/run/systemd/io.systemd.Login +Symlinks=/run/varlink/registry/io.systemd.Login FileDescriptorName=varlink SocketMode=0666 Service=systemd-logind.service diff --git a/units/systemd-machined.socket b/units/systemd-machined.socket index 3df5c7d8c3..75a91bb0cc 100644 --- a/units/systemd-machined.socket +++ b/units/systemd-machined.socket @@ -13,6 +13,6 @@ Documentation=man:systemd-machined.service(8) [Socket] ListenStream=/run/systemd/machine/io.systemd.Machine -ListenStream=/run/systemd/machine/io.systemd.MachineImage +Symlinks=/run/systemd/machine/io.systemd.MachineImage /run/varlink/registry/io.systemd.Machine /run/varlink/registry/io.systemd.MachineImage FileDescriptorName=varlink SocketMode=0666 diff --git a/units/systemd-mountfsd.socket b/units/systemd-mountfsd.socket index cd88003af9..431369a1a1 100644 --- a/units/systemd-mountfsd.socket +++ b/units/systemd-mountfsd.socket @@ -16,6 +16,8 @@ Before=sockets.target shutdown.target [Socket] ListenStream=/run/systemd/io.systemd.MountFileSystem +Symlinks=/run/varlink/registry/io.systemd.MountFileSystem +FileDescriptorName=varlink SocketMode=0666 [Install] diff --git a/units/systemd-mute-console.socket b/units/systemd-mute-console.socket index 16032e94b4..5eae6d5acd 100644 --- a/units/systemd-mute-console.socket +++ b/units/systemd-mute-console.socket @@ -17,6 +17,7 @@ Before=shutdown.target [Socket] ListenStream=/run/systemd/io.systemd.MuteConsole +Symlinks=/run/varlink/registry/io.systemd.MuteConsole FileDescriptorName=varlink SocketMode=0600 Accept=yes diff --git a/units/systemd-networkd-varlink.socket b/units/systemd-networkd-varlink.socket index 7d194fff34..1f4db858bc 100644 --- a/units/systemd-networkd-varlink.socket +++ b/units/systemd-networkd-varlink.socket @@ -17,6 +17,7 @@ Conflicts=shutdown.target [Socket] ListenStream=/run/systemd/netif/io.systemd.Network +Symlinks=/run/varlink/registry/io.systemd.Network FileDescriptorName=varlink SocketMode=0666 Service=systemd-networkd.service diff --git a/units/systemd-nsresourced.socket b/units/systemd-nsresourced.socket index 2e3c8e9baa..c159a5676a 100644 --- a/units/systemd-nsresourced.socket +++ b/units/systemd-nsresourced.socket @@ -16,7 +16,8 @@ Before=sockets.target shutdown.target [Socket] ListenStream=/run/systemd/io.systemd.NamespaceResource -Symlinks=/run/systemd/userdb/io.systemd.NamespaceResource +Symlinks=/run/systemd/userdb/io.systemd.NamespaceResource /run/varlink/registry/io.systemd.NamespaceResource +FileDescriptorName=varlink SocketMode=0666 [Install] diff --git a/units/systemd-pcrextend.socket b/units/systemd-pcrextend.socket index 4f74748125..d429150eda 100644 --- a/units/systemd-pcrextend.socket +++ b/units/systemd-pcrextend.socket @@ -17,6 +17,7 @@ ConditionSecurity=measured-uki [Socket] ListenStream=/run/systemd/io.systemd.PCRExtend +Symlinks=/run/varlink/registry/io.systemd.PCRExtend FileDescriptorName=varlink SocketMode=0600 Accept=yes diff --git a/units/systemd-pcrlock.socket b/units/systemd-pcrlock.socket index ca1cd53aea..e166caf3d8 100644 --- a/units/systemd-pcrlock.socket +++ b/units/systemd-pcrlock.socket @@ -17,6 +17,7 @@ ConditionSecurity=measured-uki [Socket] ListenStream=/run/systemd/io.systemd.PCRLock +Symlinks=/run/varlink/registry/io.systemd.PCRLock FileDescriptorName=varlink SocketMode=0600 Accept=yes diff --git a/units/systemd-repart.socket b/units/systemd-repart.socket index c9fc308b1b..ecd275414d 100644 --- a/units/systemd-repart.socket +++ b/units/systemd-repart.socket @@ -17,6 +17,7 @@ Before=shutdown.target [Socket] ListenStream=/run/systemd/io.systemd.Repart +Symlinks=/run/varlink/registry/io.systemd.Repart FileDescriptorName=varlink SocketMode=0600 Accept=yes diff --git a/units/systemd-resolved-monitor.socket b/units/systemd-resolved-monitor.socket index a778e60dd2..3674a1f876 100644 --- a/units/systemd-resolved-monitor.socket +++ b/units/systemd-resolved-monitor.socket @@ -17,6 +17,7 @@ Conflicts=shutdown.target [Socket] Service=systemd-resolved.service ListenStream=/run/systemd/resolve/io.systemd.Resolve.Monitor +Symlinks=/run/varlink/registry/io.systemd.Resolve.Monitor FileDescriptorName=varlink-monitor SocketMode=0666 diff --git a/units/systemd-resolved-varlink.socket b/units/systemd-resolved-varlink.socket index 7ac83e5ec9..a570168373 100644 --- a/units/systemd-resolved-varlink.socket +++ b/units/systemd-resolved-varlink.socket @@ -17,6 +17,7 @@ Conflicts=shutdown.target [Socket] Service=systemd-resolved.service ListenStream=/run/systemd/resolve/io.systemd.Resolve +Symlinks=/run/varlink/registry/io.systemd.Resolve FileDescriptorName=varlink SocketMode=0666 diff --git a/units/systemd-sysext.socket b/units/systemd-sysext.socket index 97d1695780..61d7268f37 100644 --- a/units/systemd-sysext.socket +++ b/units/systemd-sysext.socket @@ -17,6 +17,7 @@ ConditionCapability=CAP_SYS_ADMIN [Socket] ListenStream=/run/systemd/io.systemd.sysext +Symlinks=/run/varlink/registry/io.systemd.sysext FileDescriptorName=varlink SocketMode=0666 Accept=yes diff --git a/units/systemd-udevd-varlink.socket b/units/systemd-udevd-varlink.socket index 74353e7f1a..c2b7652e5e 100644 --- a/units/systemd-udevd-varlink.socket +++ b/units/systemd-udevd-varlink.socket @@ -17,6 +17,7 @@ ConditionPathIsReadWrite=/sys [Socket] Service=systemd-udevd.service ListenStream=/run/udev/io.systemd.Udev +Symlinks=/run/varlink/registry/io.systemd.Udev FileDescriptorName=varlink SocketMode=0600 RemoveOnStop=yes diff --git a/units/systemd-userdbd.socket b/units/systemd-userdbd.socket index 6ef1adeca7..6793d1b41d 100644 --- a/units/systemd-userdbd.socket +++ b/units/systemd-userdbd.socket @@ -15,7 +15,8 @@ Before=sockets.target [Socket] ListenStream=/run/systemd/userdb/io.systemd.Multiplexer -Symlinks=/run/systemd/userdb/io.systemd.NameServiceSwitch /run/systemd/userdb/io.systemd.DropIn +Symlinks=/run/systemd/userdb/io.systemd.NameServiceSwitch /run/systemd/userdb/io.systemd.DropIn /run/varlink/registry/io.systemd.UserDatabase +FileDescriptorName=varlink SocketMode=0666 RemoveOnStop=yes diff --git a/units/user/systemd-ask-password.socket b/units/user/systemd-ask-password.socket index ce9ccd9e55..56492fae54 100644 --- a/units/user/systemd-ask-password.socket +++ b/units/user/systemd-ask-password.socket @@ -15,6 +15,7 @@ Before=sockets.target [Socket] ListenStream=%t/systemd/io.systemd.AskPassword +Symlinks=%t/varlink/registry/io.systemd.AskPassword FileDescriptorName=varlink SocketMode=0600 Accept=yes diff --git a/units/user/systemd-importd.socket b/units/user/systemd-importd.socket index d266d927f8..67ea62bdb0 100644 --- a/units/user/systemd-importd.socket +++ b/units/user/systemd-importd.socket @@ -14,5 +14,6 @@ Documentation=man:org.freedesktop.import1(5) [Socket] ListenStream=%t/systemd/io.systemd.Import +Symlinks=%t/varlink/registry/io.systemd.Import FileDescriptorName=varlink SocketMode=0600 diff --git a/units/user/systemd-machined.socket b/units/user/systemd-machined.socket index d368215e98..17e552a7dc 100644 --- a/units/user/systemd-machined.socket +++ b/units/user/systemd-machined.socket @@ -13,6 +13,6 @@ Documentation=man:systemd-machined.service(8) [Socket] ListenStream=%t/systemd/machine/io.systemd.Machine -ListenStream=%t/systemd/machine/io.systemd.MachineImage +Symlinks=%t/systemd/machine/io.systemd.MachineImage %t/varlink/registry/io.systemd.Machine %t/varlink/registry/io.systemd.MachineImage FileDescriptorName=varlink SocketMode=0600 From c0ee33f48f85b5247f03645e39f854747533bd8d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 6 Feb 2026 15:02:45 +0100 Subject: [PATCH 2/7] stat-util: add stat_verify_socket() helper --- src/basic/stat-util.c | 15 +++++++++++++++ src/basic/stat-util.h | 2 ++ 2 files changed, 17 insertions(+) diff --git a/src/basic/stat-util.c b/src/basic/stat-util.c index d5df4acc28..f50b64e484 100644 --- a/src/basic/stat-util.c +++ b/src/basic/stat-util.c @@ -142,6 +142,21 @@ int is_symlink(const char *path) { return verify_stat_at(AT_FDCWD, path, false, stat_verify_symlink, false); } +int stat_verify_socket(const struct stat *st) { + assert(st); + + if (S_ISLNK(st->st_mode)) + return -ELOOP; + + if (S_ISDIR(st->st_mode)) + return -EISDIR; + + if (!S_ISSOCK(st->st_mode)) + return -ENOTSOCK; + + return 0; +} + int stat_verify_linked(const struct stat *st) { assert(st); diff --git a/src/basic/stat-util.h b/src/basic/stat-util.h index 65af98e76a..e4138e960a 100644 --- a/src/basic/stat-util.h +++ b/src/basic/stat-util.h @@ -20,6 +20,8 @@ int stat_verify_symlink(const struct stat *st); int fd_verify_symlink(int fd); int is_symlink(const char *path); +int stat_verify_socket(const struct stat *st); + int stat_verify_linked(const struct stat *st); int fd_verify_linked(int fd); From 3e4fca7489c20e5cd8bc8896c1fd9d7c24727a89 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 6 Feb 2026 15:03:23 +0100 Subject: [PATCH 3/7] chase: add new flag CHASE_MUST_BE_SOCKET Just like CHASE_MUST_BE_DIRECTORY and CHASE_MUST_BE_REGULAR, but test if the inode is a socket. --- src/basic/chase.c | 24 ++++++++++++++++++------ src/basic/chase.h | 1 + 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/basic/chase.c b/src/basic/chase.c index f360cc26a3..abe85a2a08 100644 --- a/src/basic/chase.c +++ b/src/basic/chase.c @@ -26,7 +26,10 @@ CHASE_STEP | \ CHASE_PROHIBIT_SYMLINKS | \ CHASE_PARENT | \ - CHASE_MKDIR_0755) + CHASE_MKDIR_0755 | \ + CHASE_MUST_BE_DIRECTORY | \ + CHASE_MUST_BE_REGULAR | \ + CHASE_MUST_BE_SOCKET) bool unsafe_transition(const struct stat *a, const struct stat *b) { /* Returns true if the transition from a to b is safe, i.e. that we never transition from unprivileged to @@ -133,7 +136,6 @@ int chaseat(int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int int r; assert(!FLAGS_SET(flags, CHASE_PREFIX_ROOT)); - assert(!FLAGS_SET(flags, CHASE_MUST_BE_DIRECTORY|CHASE_MUST_BE_REGULAR)); assert(!FLAGS_SET(flags, CHASE_STEP|CHASE_EXTRACT_FILENAME)); assert(!FLAGS_SET(flags, CHASE_NO_AUTOFS|CHASE_TRIGGER_AUTOFS)); assert(dir_fd >= 0 || IN_SET(dir_fd, AT_FDCWD, XAT_FDROOT)); @@ -328,6 +330,10 @@ int chaseat(int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int if (FLAGS_SET(flags, CHASE_PARENT)) flags |= CHASE_MUST_BE_DIRECTORY; + /* If multiple flags are set now, fail immediately */ + if (FLAGS_SET(flags, CHASE_MUST_BE_DIRECTORY) + FLAGS_SET(flags, CHASE_MUST_BE_REGULAR) + FLAGS_SET(flags, CHASE_MUST_BE_SOCKET) > 1) + return -EBADSLT; + for (todo = buffer;;) { _cleanup_free_ char *first = NULL; _cleanup_close_ int child = -EBADF; @@ -566,6 +572,12 @@ success: if (r < 0) return r; } + + if (FLAGS_SET(flags, CHASE_MUST_BE_SOCKET)) { + r = stat_verify_socket(&st); + if (r < 0) + return r; + } } if (ret_path) { @@ -872,7 +884,7 @@ int chase_and_opendir(const char *path, const char *root, ChaseFlags chase_flags DIR *d; int r; - assert(!(chase_flags & (CHASE_NONEXISTENT|CHASE_STEP|CHASE_MUST_BE_REGULAR))); + assert(!(chase_flags & (CHASE_NONEXISTENT|CHASE_STEP|CHASE_MUST_BE_REGULAR|CHASE_MUST_BE_SOCKET))); assert(ret_dir); if (empty_or_root(root) && !ret_path && (chase_flags & CHASE_NO_SHORTCUT_MASK) == 0) { @@ -970,7 +982,7 @@ int chase_and_fopen_unlocked( int mode_flags, r; assert(path); - assert(!(chase_flags & (CHASE_NONEXISTENT|CHASE_STEP|CHASE_PARENT|CHASE_MUST_BE_DIRECTORY))); + assert(!(chase_flags & (CHASE_NONEXISTENT|CHASE_STEP|CHASE_PARENT|CHASE_MUST_BE_DIRECTORY|CHASE_MUST_BE_SOCKET))); assert(open_flags); assert(ret_file); @@ -1040,7 +1052,7 @@ int chase_and_openat( _cleanup_free_ char *p = NULL, *fname = NULL; int r; - assert(!(chase_flags & (CHASE_NONEXISTENT|CHASE_STEP))); + assert(!(chase_flags & (CHASE_NONEXISTENT|CHASE_STEP|CHASE_MUST_BE_SOCKET))); XOpenFlags xopen_flags = 0; if (FLAGS_SET(chase_flags, CHASE_MUST_BE_DIRECTORY)) @@ -1086,7 +1098,7 @@ int chase_and_opendirat(int dir_fd, const char *path, ChaseFlags chase_flags, ch DIR *d; int r; - assert(!(chase_flags & (CHASE_NONEXISTENT|CHASE_STEP|CHASE_MUST_BE_REGULAR))); + assert(!(chase_flags & (CHASE_NONEXISTENT|CHASE_STEP|CHASE_MUST_BE_REGULAR|CHASE_MUST_BE_SOCKET))); assert(ret_dir); if (dir_fd == AT_FDCWD && !ret_path && (chase_flags & CHASE_NO_SHORTCUT_MASK) == 0) { diff --git a/src/basic/chase.h b/src/basic/chase.h index 948b83aad0..d0674aae73 100644 --- a/src/basic/chase.h +++ b/src/basic/chase.h @@ -29,6 +29,7 @@ typedef enum ChaseFlags { CHASE_EXTRACT_FILENAME = 1 << 13, /* Only return the last component of the resolved path */ CHASE_MUST_BE_DIRECTORY = 1 << 14, /* Fail if returned inode fd is not a dir */ CHASE_MUST_BE_REGULAR = 1 << 15, /* Fail if returned inode fd is not a regular file */ + CHASE_MUST_BE_SOCKET = 1 << 16, /* Fail if returned inode fd is not a socket */ } ChaseFlags; bool unsafe_transition(const struct stat *a, const struct stat *b); From 607152c742ba2e96a1cc0ef1146ddc1f8b77d8af Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 6 Feb 2026 15:04:48 +0100 Subject: [PATCH 4/7] varlinkctl: add 'list-registry' command --- shell-completion/bash/varlinkctl | 2 +- src/varlinkctl/varlinkctl.c | 136 +++++++++++++++++++++++++++++-- 2 files changed, 130 insertions(+), 8 deletions(-) diff --git a/shell-completion/bash/varlinkctl b/shell-completion/bash/varlinkctl index f7d880b464..e6557ffb02 100644 --- a/shell-completion/bash/varlinkctl +++ b/shell-completion/bash/varlinkctl @@ -63,7 +63,7 @@ _varlinkctl() { fi local -A VERBS=( - [STANDALONE]='help' + [STANDALONE]='help list-registry' [CALL]='call' [FILE]='info list-interfaces validate-idl' [ADDRESS_INTERFACES]='list-methods introspect' diff --git a/src/varlinkctl/varlinkctl.c b/src/varlinkctl/varlinkctl.c index 1c28005647..233423935f 100644 --- a/src/varlinkctl/varlinkctl.c +++ b/src/varlinkctl/varlinkctl.c @@ -10,6 +10,7 @@ #include "build.h" #include "bus-util.h" +#include "chase.h" #include "env-util.h" #include "fd-util.h" #include "fileio.h" @@ -21,10 +22,14 @@ #include "pager.h" #include "parse-argument.h" #include "parse-util.h" +#include "path-lookup.h" +#include "path-util.h" #include "pidfd-util.h" #include "polkit-agent.h" #include "pretty-print.h" #include "process-util.h" +#include "recurse-dir.h" +#include "runtime-scope.h" #include "string-util.h" #include "strv.h" #include "terminal-util.h" @@ -49,6 +54,8 @@ static usec_t arg_timeout = 0; static bool arg_exec = false; static PushFds arg_push_fds = {}; static bool arg_ask_password = true; +static bool arg_legend = true; +static RuntimeScope arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; static void push_fds_done(PushFds *p) { assert(p); @@ -85,6 +92,7 @@ static int help(void) { " Invoke method\n" " --exec call ADDRESS METHOD PARAMS -- CMDLINE…\n" " Invoke method and pass response and fds to command\n" + " list-registry Show list of services in the service registry\n" " validate-idl [FILE] Validate interface description\n" " help Show this help\n" "\n%3$sOptions:%4$s\n" @@ -92,6 +100,8 @@ static int help(void) { " --version Show package version\n" " --no-ask-password Do not prompt for password\n" " --no-pager Do not pipe output into a pager\n" + " --system Enumerate system registry\n" + " --user Enumerate user registry\n" " --more Request multiple responses\n" " --collect Collect multiple responses in a JSON array\n" " --oneway Do not request response\n" @@ -131,6 +141,8 @@ static int parse_argv(int argc, char *argv[]) { ARG_EXEC, ARG_PUSH_FD, ARG_NO_ASK_PASSWORD, + ARG_USER, + ARG_SYSTEM, }; static const struct option options[] = { @@ -147,6 +159,8 @@ static int parse_argv(int argc, char *argv[]) { { "exec", no_argument, NULL, ARG_EXEC }, { "push-fd", required_argument, NULL, ARG_PUSH_FD }, { "no-ask-password", no_argument, NULL, ARG_NO_ASK_PASSWORD }, + { "user", no_argument, NULL, ARG_USER }, + { "system", no_argument, NULL, ARG_SYSTEM }, {}, }; @@ -263,6 +277,14 @@ static int parse_argv(int argc, char *argv[]) { arg_ask_password = false; break; + case ARG_USER: + arg_runtime_scope = RUNTIME_SCOPE_USER; + break; + + case ARG_SYSTEM: + arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; + break; + case '?': return -EINVAL; @@ -973,15 +995,115 @@ static int verb_validate_idl(int argc, char *argv[], void *userdata) { return 0; } +static int verb_list_registry(int argc, char *argv[], void *userdata) { + int r; + + assert(argc <= 1); + + _cleanup_free_ char *reg_path = NULL; + r = runtime_directory_generic(arg_runtime_scope, "varlink/registry", ®_path); + if (r < 0) + return log_error_errno(r, "Failed to determine registry path: %m"); + + _cleanup_(table_unrefp) Table *table = table_new("interface", "entrypoint"); + if (!table) + return log_oom(); + + (void) table_set_sort(table, (size_t) 0); + + _cleanup_close_ int regfd = open(reg_path, O_DIRECTORY|O_CLOEXEC); + if (regfd < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to open '%s': %m", reg_path); + } else { + _cleanup_free_ DirectoryEntries *des = NULL; + r = readdir_all(regfd, RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &des); + if (r < 0) + return log_error_errno(r, "Failed to enumerate '%s': %m", reg_path); + + FOREACH_ARRAY(i, des->entries, des->n_entries) { + struct dirent *de = *i; + + if (!varlink_idl_interface_name_is_valid(de->d_name)) { + log_debug("Found file '%s' whose names does not qualify as valid Varlink interface name, skipping.", de->d_name); + continue; + } + + _cleanup_free_ char *j = path_join(reg_path, de->d_name); + if (!j) + return log_oom(); + + switch (de->d_type) { + case DT_LNK: { + _cleanup_free_ char *resolved = NULL; + + r = chase(j, /* root= */ NULL, CHASE_MUST_BE_SOCKET, &resolved, /* ret_fd= */ NULL); + if (r < 0) { + log_warning_errno(r, "Failed to resolve '%s', skipping: %m", j); + continue; + } + + _cleanup_free_ char *address = strjoin("unix:", resolved); + if (!address) + return log_oom(); + + r = table_add_many( + table, + TABLE_STRING, de->d_name, + TABLE_STRING, address); + if (r < 0) + return r; + + break; + } + + case DT_SOCK: { + _cleanup_free_ char *address = strjoin("unix:", j); + if (!address) + return log_oom(); + + r = table_add_many( + table, + TABLE_STRING, de->d_name, + TABLE_STRING, address); + if (r < 0) + return r; + + break; + } + + default: + log_debug("Ignoring inode '%s' of unexpected type: %m", de->d_name); + } + } + } + + if (!table_isempty(table) || sd_json_format_enabled(arg_json_format_flags)) { + r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, /* show_header= */ true); + if (r < 0) + return log_error_errno(r, "Failed to output table: %m"); + } + + if (arg_legend && !sd_json_format_enabled(arg_json_format_flags)) { + if (table_isempty(table)) + printf("No services registered.\n"); + else + printf("\n%zu registered services listed.\n", table_get_rows(table) - 1); + } + + return 0; +} + static int varlinkctl_main(int argc, char *argv[]) { static const Verb verbs[] = { - { "info", 2, 2, 0, verb_info }, - { "list-interfaces", 2, 2, 0, verb_info }, - { "introspect", 2, VERB_ANY, 0, verb_introspect }, - { "list-methods", 2, VERB_ANY, 0, verb_introspect }, - { "call", 3, VERB_ANY, 0, verb_call }, - { "validate-idl", 1, 2, 0, verb_validate_idl }, - { "help", VERB_ANY, VERB_ANY, 0, verb_help }, + { "info", 2, 2, 0, verb_info }, + { "list-interfaces", 2, 2, 0, verb_info }, + { "introspect", 2, VERB_ANY, 0, verb_introspect }, + { "list-methods", 2, VERB_ANY, 0, verb_introspect }, + { "call", 3, VERB_ANY, 0, verb_call }, + { "list-registry", VERB_ANY, 1, 0, verb_list_registry }, + { "validate-idl", 1, 2, 0, verb_validate_idl }, + { "help", VERB_ANY, VERB_ANY, 0, verb_help }, {} }; From e2f23b6deb7a50c4258d94dedf3047bf3992b196 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 6 Feb 2026 17:07:14 +0100 Subject: [PATCH 5/7] man: document new varlinkctl feature --- man/varlinkctl.xml | 47 ++++++++++++++++++++++++++++++++++++++++ tools/command_ignorelist | 1 + 2 files changed, 48 insertions(+) diff --git a/man/varlinkctl.xml b/man/varlinkctl.xml index f8185f71ca..b96e35372f 100644 --- a/man/varlinkctl.xml +++ b/man/varlinkctl.xml @@ -181,6 +181,16 @@ + + list-registry + + Shows a list of Varlink services currently registered in the service registry, plus + their entrypoint sockets. (Currently, this simply enumerates the sockets and symlinked sockets in + /run/varlink/registry/, see below.) + + + + validate-idl [FILE] @@ -354,6 +364,18 @@ + + + + + + Determines whether to query to the per-system or per-user registry when using the + list-registry command. By default, the per-system registry is queried. + + + + + @@ -361,6 +383,31 @@ + + Files & Directories + + + + /run/varlink/registry/ + + + Directory containing AF_UNIX entrypoint socket inodes (or symlinks to + them) of well-known, public Varlink interfaces on the local system. They are named after the + Varlink interface they implement. + + Use varlinkctl list-registry to show the contents of this + directory. + + (Inodes that neither qualify as socket inodes nor as symlinks to them shall be ignored. A + future extension might introduce regular files and directories to enhance the registry + functionality.) + + + + + + + Examples diff --git a/tools/command_ignorelist b/tools/command_ignorelist index 41e8d01a65..5859e5def9 100644 --- a/tools/command_ignorelist +++ b/tools/command_ignorelist @@ -572,3 +572,4 @@ loader.conf.xml ./refsect1[title="Options"]/variablelist/varlistentry[term="rebo loader.conf.xml ./refsect1[title="Options"]/variablelist/varlistentry[term="reboot-on-error"]/listitem/para/variablelist/varlistentry[term="auto"] loader.conf.xml ./refsect1[title="Options"]/variablelist/varlistentry[term="secure-boot-enroll-action"]/listitem/variablelist/varlistentry[term="reboot"] loader.conf.xml ./refsect1[title="Options"]/variablelist/varlistentry[term="secure-boot-enroll-action"]/listitem/variablelist/varlistentry[term="shutdown"] +varlinkctl.xml ./refsect1[title="Options"]/variablelist/varlistentry[term="--system"] From a1d3fd12026356ebf6b219c36a3d20f3822da486 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 6 Feb 2026 17:13:11 +0100 Subject: [PATCH 6/7] test: add superficial test for list-registry --- test/units/TEST-74-AUX-UTILS.varlinkctl.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/units/TEST-74-AUX-UTILS.varlinkctl.sh b/test/units/TEST-74-AUX-UTILS.varlinkctl.sh index 9e0dece270..c22b5e7030 100755 --- a/test/units/TEST-74-AUX-UTILS.varlinkctl.sh +++ b/test/units/TEST-74-AUX-UTILS.varlinkctl.sh @@ -37,6 +37,10 @@ varlinkctl introspect -j /run/systemd/journal/io.systemd.journal | jq --seq . varlinkctl introspect /run/systemd/journal/io.systemd.journal io.systemd.Journal varlinkctl introspect -j /run/systemd/journal/io.systemd.journal io.systemd.Journal | jq . +varlinkctl list-registry +varlinkctl list-registry -j | jq . +varlinkctl list-registry | grep io.systemd.Manager + if command -v userdbctl >/dev/null; then systemctl start systemd-userdbd varlinkctl call /run/systemd/userdb/io.systemd.Multiplexer io.systemd.UserDatabase.GetUserRecord '{ "userName" : "testuser", "service" : "io.systemd.Multiplexer" }' From 71e651ba82ff02512cbaa9a30f18c65c9654c802 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 6 Feb 2026 18:08:07 +0100 Subject: [PATCH 7/7] update TODO --- TODO | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/TODO b/TODO index 0706275624..03112b381e 100644 --- a/TODO +++ b/TODO @@ -129,6 +129,13 @@ Features: - how to plug facts into this? i.e. hostname, ssh keys, and so on - switch to daan's suggested hierarchy? +* implement a varlink registry service, similar to the one of the reference + implementation, backed by /run/varlink/registry/. Then, also implement + connect-via-registry-resolution in sd-varlink and varlinkctl. Care needs to + be taken to do the resolution asynchronousy. Also, note that the Varlink + reference implementation uses a different address syntax, which needs to be + taken into account. + * downgrade the uid/gid disposition enforcement in udev * have a signal that reloads every unit that supports reloading