diff --git a/man/systemd.exec.xml b/man/systemd.exec.xml
index 4752e0e0f3..32128d4fab 100644
--- a/man/systemd.exec.xml
+++ b/man/systemd.exec.xml
@@ -1378,7 +1378,7 @@ CapabilityBoundingSet=~CAP_B CAP_C
StateDirectory=
/var/lib/
- $XDG_CONFIG_HOME
+ $XDG_STATE_HOME
$STATE_DIRECTORY
@@ -1390,7 +1390,7 @@ CapabilityBoundingSet=~CAP_B CAP_C
LogsDirectory=
/var/log/
- $XDG_CONFIG_HOME/log/
+ $XDG_STATE_HOME/log/
$LOGS_DIRECTORY
diff --git a/man/systemd.unit.xml b/man/systemd.unit.xml
index e9c7cb238c..8c3329995d 100644
--- a/man/systemd.unit.xml
+++ b/man/systemd.unit.xml
@@ -2131,7 +2131,7 @@ Note that this setting is not influenced by the Us
%L
Log directory root
- This is either /var/log (for the system manager) or the path $XDG_CONFIG_HOME resolves to with /log appended (for user managers).
+ This is either /var/log (for the system manager) or the path $XDG_STATE_HOME resolves to with /log appended (for user managers).
@@ -2171,7 +2171,7 @@ Note that this setting is not influenced by the Us
%S
State directory root
- This is either /var/lib (for the system manager) or the path $XDG_CONFIG_HOME resolves to (for user managers).
+ This is either /var/lib (for the system manager) or the path $XDG_STATE_HOME resolves to (for user managers).
%t
diff --git a/src/core/execute.c b/src/core/execute.c
index 11d707b59c..3e065b2ca8 100644
--- a/src/core/execute.c
+++ b/src/core/execute.c
@@ -2511,6 +2511,61 @@ static int setup_exec_directory(
if (r < 0)
goto fail;
+ if (IN_SET(type, EXEC_DIRECTORY_STATE, EXEC_DIRECTORY_LOGS) && params->runtime_scope == RUNTIME_SCOPE_USER) {
+
+ /* If we are in user mode, and a configuration directory exists but a state directory
+ * doesn't exist, then we likely are upgrading from an older systemd version that
+ * didn't know the more recent addition to the xdg-basedir spec: the $XDG_STATE_HOME
+ * directory. In older systemd versions EXEC_DIRECTORY_STATE was aliased to
+ * EXEC_DIRECTORY_CONFIGURATION, with the advent of $XDG_STATE_HOME is is now
+ * seperated. If a service has both dirs configured but only the configuration dir
+ * exists and the state dir does not, we assume we are looking at an update
+ * situation. Hence, create a compatibility symlink, so that all expectations are
+ * met.
+ *
+ * (We also do something similar with the log directory, which still doesn't exist in
+ * the xdg basedir spec. We'll make it a subdir of the state dir.) */
+
+ /* this assumes the state dir is always created before the configuration dir */
+ assert_cc(EXEC_DIRECTORY_STATE < EXEC_DIRECTORY_LOGS);
+ assert_cc(EXEC_DIRECTORY_LOGS < EXEC_DIRECTORY_CONFIGURATION);
+
+ r = laccess(p, F_OK);
+ if (r == -ENOENT) {
+ _cleanup_free_ char *q = NULL;
+
+ /* OK, we know that the state dir does not exist. Let's see if the dir exists
+ * under the configuration hierarchy. */
+
+ if (type == EXEC_DIRECTORY_STATE)
+ q = path_join(params->prefix[EXEC_DIRECTORY_CONFIGURATION], context->directories[type].items[i].path);
+ else if (type == EXEC_DIRECTORY_LOGS)
+ q = path_join(params->prefix[EXEC_DIRECTORY_CONFIGURATION], "log", context->directories[type].items[i].path);
+ else
+ assert_not_reached();
+ if (!q) {
+ r = -ENOMEM;
+ goto fail;
+ }
+
+ r = laccess(q, F_OK);
+ if (r >= 0) {
+ /* It does exist! This hence looks like an update. Symlink the
+ * configuration directory into the state directory. */
+
+ r = symlink_idempotent(q, p, /* make_relative= */ true);
+ if (r < 0)
+ goto fail;
+
+ log_notice("Unit state directory %s missing but matching configuration directory %s exists, assuming update from systemd 253 or older, creating compatibility symlink.", p, q);
+ continue;
+ } else if (r != -ENOENT)
+ log_warning_errno(r, "Unable to detect whether unit configuration directory '%s' exists, assuming not: %m", q);
+
+ } else if (r < 0)
+ log_warning_errno(r, "Unable to detect whether unit state directory '%s' is missing, assuming it is: %m", p);
+ }
+
if (exec_directory_is_private(context, type)) {
/* So, here's one extra complication when dealing with DynamicUser=1 units. In that
* case we want to avoid leaving a directory around fully accessible that is owned by
diff --git a/src/core/manager.c b/src/core/manager.c
index 23df5ce191..8a081d0056 100644
--- a/src/core/manager.c
+++ b/src/core/manager.c
@@ -720,9 +720,9 @@ static int manager_setup_prefix(Manager *m) {
static const struct table_entry paths_user[_EXEC_DIRECTORY_TYPE_MAX] = {
[EXEC_DIRECTORY_RUNTIME] = { SD_PATH_USER_RUNTIME, NULL },
- [EXEC_DIRECTORY_STATE] = { SD_PATH_USER_CONFIGURATION, NULL },
+ [EXEC_DIRECTORY_STATE] = { SD_PATH_USER_STATE_PRIVATE, NULL },
[EXEC_DIRECTORY_CACHE] = { SD_PATH_USER_STATE_CACHE, NULL },
- [EXEC_DIRECTORY_LOGS] = { SD_PATH_USER_CONFIGURATION, "log" },
+ [EXEC_DIRECTORY_LOGS] = { SD_PATH_USER_STATE_PRIVATE, "log" },
[EXEC_DIRECTORY_CONFIGURATION] = { SD_PATH_USER_CONFIGURATION, NULL },
};
diff --git a/src/core/unit-printf.c b/src/core/unit-printf.c
index 3977082cc1..9f95984eb6 100644
--- a/src/core/unit-printf.c
+++ b/src/core/unit-printf.c
@@ -209,8 +209,8 @@ int unit_full_printf_full(const Unit *u, const char *format, size_t max_length,
* %C: the cache directory root (e.g. /var/cache or $XDG_CACHE_HOME)
* %d: the credentials directory ($CREDENTIALS_DIRECTORY)
* %E: the configuration directory root (e.g. /etc or $XDG_CONFIG_HOME)
- * %L: the log directory root (e.g. /var/log or $XDG_CONFIG_HOME/log)
- * %S: the state directory root (e.g. /var/lib or $XDG_CONFIG_HOME)
+ * %L: the log directory root (e.g. /var/log or $XDG_STATE_HOME/log)
+ * %S: the state directory root (e.g. /var/lib or $XDG_STATE_HOME)
* %t: the runtime directory root (e.g. /run or $XDG_RUNTIME_DIR)
*
* %h: the homedir of the running user
diff --git a/test/test-execute/exec-specifier-user.service b/test/test-execute/exec-specifier-user.service
index ee0301a426..ab565fb4fb 100644
--- a/test/test-execute/exec-specifier-user.service
+++ b/test/test-execute/exec-specifier-user.service
@@ -5,7 +5,7 @@ Description=Test for specifiers
[Service]
Type=oneshot
ExecStart=sh -c 'test %t = $$XDG_RUNTIME_DIR'
-ExecStart=sh -c 'test %S = %h/.config'
+ExecStart=sh -c 'test %S = %h/.local/state'
ExecStart=sh -c 'test %C = %h/.cache'
-ExecStart=sh -c 'test %L = %h/.config/log'
+ExecStart=sh -c 'test %L = %h/.local/state/log'
ExecStart=sh -c 'test %E = %h/.config'