diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml
index 3a37a636b3c..4e43b4ba20d 100644
--- a/man/org.freedesktop.systemd1.xml
+++ b/man/org.freedesktop.systemd1.xml
@@ -4821,6 +4821,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly b PassCredentials = ...;
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+ readonly b PassFileDescriptorsToExec = ...;
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly b PassSecurity = ...;
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly b PassPacketInfo = ...;
@@ -5488,6 +5490,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
+
+
@@ -6100,6 +6104,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
+
+
@@ -12061,8 +12067,9 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \
MemoryZSwapCurrent were added in version 255.
EffectiveMemoryHigh,
EffectiveMemoryMax,
- EffectiveTasksMax, and
- MemoryZSwapWriteback were added in version 256.
+ EffectiveTasksMax,
+ MemoryZSwapWriteback, and
+ PassFileDescriptorsToExec were added in version 256.
Mount Unit Objects
diff --git a/man/systemd.socket.xml b/man/systemd.socket.xml
index c7166e4f643..50871f7a749 100644
--- a/man/systemd.socket.xml
+++ b/man/systemd.socket.xml
@@ -922,6 +922,20 @@
+
+ PassFileDescriptorsToExec=
+
+ Takes a boolean argument. Defaults to off. If enabled, file descriptors created by
+ the socket unit are passed to ExecStartPost=, ExecStopPre=, and
+ ExecStopPost= commands from the socket unit. The passed file descriptors can be
+ accessed with
+ sd_listen_fds3 as
+ if the commands were invoked from the associated service units. Note that
+ ExecStartPre= command cannot access socket file descriptors.
+
+
+
+
diff --git a/src/core/dbus-socket.c b/src/core/dbus-socket.c
index e77e9e5ccd2..03c5b4ad2ae 100644
--- a/src/core/dbus-socket.c
+++ b/src/core/dbus-socket.c
@@ -86,6 +86,7 @@ const sd_bus_vtable bus_socket_vtable[] = {
SD_BUS_PROPERTY("Transparent", "b", bus_property_get_bool, offsetof(Socket, transparent), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("Broadcast", "b", bus_property_get_bool, offsetof(Socket, broadcast), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("PassCredentials", "b", bus_property_get_bool, offsetof(Socket, pass_cred), SD_BUS_VTABLE_PROPERTY_CONST),
+ SD_BUS_PROPERTY("PassFileDescriptorsToExec", "b", bus_property_get_bool, offsetof(Socket, pass_fds_to_exec), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("PassSecurity", "b", bus_property_get_bool, offsetof(Socket, pass_sec), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("PassPacketInfo", "b", bus_property_get_bool, offsetof(Socket, pass_pktinfo), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("Timestamping", "s", property_get_timestamping, offsetof(Socket, timestamping), SD_BUS_VTABLE_PROPERTY_CONST),
@@ -190,6 +191,9 @@ static int bus_socket_set_transient_property(
if (streq(name, "PassCredentials"))
return bus_set_transient_bool(u, name, &s->pass_cred, message, flags, error);
+ if (streq(name, "PassFileDescriptorsToExec"))
+ return bus_set_transient_bool(u, name, &s->pass_fds_to_exec, message, flags, error);
+
if (streq(name, "PassSecurity"))
return bus_set_transient_bool(u, name, &s->pass_sec, message, flags, error);
diff --git a/src/core/load-fragment-gperf.gperf.in b/src/core/load-fragment-gperf.gperf.in
index c5ea99726a4..27aa27b55a9 100644
--- a/src/core/load-fragment-gperf.gperf.in
+++ b/src/core/load-fragment-gperf.gperf.in
@@ -500,6 +500,7 @@ Socket.FreeBind, config_parse_bool,
Socket.Transparent, config_parse_bool, 0, offsetof(Socket, transparent)
Socket.Broadcast, config_parse_bool, 0, offsetof(Socket, broadcast)
Socket.PassCredentials, config_parse_bool, 0, offsetof(Socket, pass_cred)
+Socket.PassFileDescriptorsToExec, config_parse_bool, 0, offsetof(Socket, pass_fds_to_exec)
Socket.PassSecurity, config_parse_bool, 0, offsetof(Socket, pass_sec)
Socket.PassPacketInfo, config_parse_bool, 0, offsetof(Socket, pass_pktinfo)
Socket.Timestamping, config_parse_socket_timestamping, 0, offsetof(Socket, timestamping)
diff --git a/src/core/socket.c b/src/core/socket.c
index 45656cbda77..5dbbd5a1d25 100644
--- a/src/core/socket.c
+++ b/src/core/socket.c
@@ -590,6 +590,7 @@ static void socket_dump(Unit *u, FILE *f, const char *prefix) {
"%sTransparent: %s\n"
"%sBroadcast: %s\n"
"%sPassCredentials: %s\n"
+ "%sPassFileDescriptorsToExec: %s\n"
"%sPassSecurity: %s\n"
"%sPassPacketInfo: %s\n"
"%sTCPCongestion: %s\n"
@@ -610,6 +611,7 @@ static void socket_dump(Unit *u, FILE *f, const char *prefix) {
prefix, yes_no(s->transparent),
prefix, yes_no(s->broadcast),
prefix, yes_no(s->pass_cred),
+ prefix, yes_no(s->pass_fds_to_exec),
prefix, yes_no(s->pass_sec),
prefix, yes_no(s->pass_pktinfo),
prefix, strna(s->tcp_congestion),
@@ -1921,6 +1923,26 @@ static int socket_spawn(Socket *s, ExecCommand *c, PidRef *ret_pid) {
if (r < 0)
return r;
+ /* Note that ExecStartPre= command doesn't inherit any FDs. It runs before we open listen FDs. */
+ if (s->pass_fds_to_exec) {
+ _cleanup_strv_free_ char **fd_names = NULL;
+ _cleanup_free_ int *fds = NULL;
+ int n_fds;
+
+ n_fds = socket_collect_fds(s, &fds);
+ if (n_fds < 0)
+ return n_fds;
+
+ r = strv_extend_n(&fd_names, socket_fdname(s), n_fds);
+ if (r < 0)
+ return r;
+
+ exec_params.flags |= EXEC_PASS_FDS;
+ exec_params.fds = TAKE_PTR(fds);
+ exec_params.fd_names = TAKE_PTR(fd_names);
+ exec_params.n_socket_fds = n_fds;
+ }
+
r = exec_spawn(UNIT(s),
c,
&s->exec_context,
diff --git a/src/core/socket.h b/src/core/socket.h
index 973a697f861..5e3929c5fa7 100644
--- a/src/core/socket.h
+++ b/src/core/socket.h
@@ -129,6 +129,7 @@ struct Socket {
bool transparent;
bool broadcast;
bool pass_cred;
+ bool pass_fds_to_exec;
bool pass_sec;
bool pass_pktinfo;
SocketTimestamping timestamping;
diff --git a/src/shared/bus-unit-util.c b/src/shared/bus-unit-util.c
index 2fcfb1d3b96..19cebb0cfe1 100644
--- a/src/shared/bus-unit-util.c
+++ b/src/shared/bus-unit-util.c
@@ -2450,6 +2450,7 @@ static int bus_append_socket_property(sd_bus_message *m, const char *field, cons
"Transparent",
"Broadcast",
"PassCredentials",
+ "PassFileDescriptorsToExec",
"PassSecurity",
"PassPacketInfo",
"ReusePort",
diff --git a/test/fuzz/fuzz-unit-file/directives-all.service b/test/fuzz/fuzz-unit-file/directives-all.service
index b05b0a49731..670e589babe 100644
--- a/test/fuzz/fuzz-unit-file/directives-all.service
+++ b/test/fuzz/fuzz-unit-file/directives-all.service
@@ -185,6 +185,7 @@ PAMName=
PIDFile=
PartOf=
PassCredentials=
+PassFileDescriptorsToExec=
PassSecurity=
PassPacketInfo=
PathChanged=
diff --git a/test/testsuite-07.units/pass-fds-to-exec-no.socket b/test/testsuite-07.units/pass-fds-to-exec-no.socket
new file mode 100644
index 00000000000..8b7964b6648
--- /dev/null
+++ b/test/testsuite-07.units/pass-fds-to-exec-no.socket
@@ -0,0 +1,35 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+[Unit]
+Description=Test if ExecXYZ= commands don't inherit listen FDs when PassFileDescriptorsToExec= is unset
+
+[Socket]
+# With Accept= set we don't need a corresponding service unit
+Accept=yes
+FileDescriptorName=foo
+ListenStream=127.0.0.1:1234
+ListenStream=[::1]:1234
+PassFileDescriptorsToExec=no
+ExecStartPre=\
+ test ExecStartPre -a \
+ -z ${LISTEN_FDS} -a \
+ -z ${LISTEN_FDNAMES} -a \
+ ! -e /dev/fd/3 -a \
+ ! -e /dev/fd/4
+ExecStartPost=\
+ test ExecStartPost -a \
+ -z ${LISTEN_FDS} -a \
+ -z ${LISTEN_FDNAMES} -a \
+ ! -e /dev/fd/3 -a \
+ ! -e /dev/fd/4
+ExecStopPre=\
+ test ExecStopPre -a \
+ -z ${LISTEN_FDS} -a \
+ -z ${LISTEN_FDNAMES} -a \
+ ! -e /dev/fd/3 -a \
+ ! -e /dev/fd/4
+ExecStopPost=\
+ test ExecStopPost -a \
+ -z ${LISTEN_FDS} -a \
+ -z ${LISTEN_FDNAMES} -a \
+ ! -e /dev/fd/3 -a \
+ ! -e /dev/fd/4
diff --git a/test/testsuite-07.units/pass-fds-to-exec-yes.socket b/test/testsuite-07.units/pass-fds-to-exec-yes.socket
new file mode 100644
index 00000000000..bff192d559d
--- /dev/null
+++ b/test/testsuite-07.units/pass-fds-to-exec-yes.socket
@@ -0,0 +1,36 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+[Unit]
+Description=Test if ExecXYZ= commands inherit listen FDs when PassFileDescriptorsToExec= is set
+
+[Socket]
+# With Accept= set we don't need a corresponding service unit
+Accept=yes
+FileDescriptorName=foo
+ListenStream=127.0.0.1:1234
+ListenStream=[::1]:1234
+PassFileDescriptorsToExec=yes
+# ExecStartPre runs before we create sockets. Nothing to pass.
+ExecStartPre=\
+ test ExecStartPre -a \
+ -z ${LISTEN_FDS} -a \
+ -z ${LISTEN_FDNAMES} -a \
+ ! -e /dev/fd/3 -a \
+ ! -e /dev/fd/4
+ExecStartPost=\
+ test ExecStartPost -a \
+ ${LISTEN_FDS} = 2 -a \
+ ${LISTEN_FDNAMES} = foo:foo -a \
+ -S /dev/fd/3 -a \
+ -S /dev/fd/4
+ExecStopPre=\
+ test "ExecStopPre" -a \
+ ${LISTEN_FDS} = 2 -a \
+ ${LISTEN_FDNAMES} = foo:foo -a \
+ -S /dev/fd/3 -a \
+ -S /dev/fd/4
+ExecStopPost=\
+ test "ExecStopPost" -a \
+ ${LISTEN_FDS} = 2 -a \
+ ${LISTEN_FDNAMES} = foo:foo -a \
+ -S /dev/fd/3 -a \
+ -S /dev/fd/4
diff --git a/test/units/testsuite-07.socket-pass-fds.sh b/test/units/testsuite-07.socket-pass-fds.sh
new file mode 100755
index 00000000000..a61b1c01f14
--- /dev/null
+++ b/test/units/testsuite-07.socket-pass-fds.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+set -eux
+set -o pipefail
+
+# Test PassFileDescriptorsToExec= option in socket units
+
+for u in pass-fds-to-exec-{no,yes}.socket; do
+ systemctl start "$u"
+ systemctl stop "$u"
+done