diff --git a/NEWS b/NEWS
index 77b38f5d6c..de3d28d668 100644
--- a/NEWS
+++ b/NEWS
@@ -7,6 +7,13 @@ CHANGES WITH 236 in spe:
numdummies=0, preventing the kernel from automatically creating
dummy0. All dummy interfaces must now be explicitly created.
+ * Unknown specifiers are now rejected. This applies to units and
+ tmpfiles.d configuration. Any percent characters that are followed by
+ a letter or digit that are not supposed to be interpreted as the
+ beginning of a specifier should be escaped by doubling ("%%").
+ (So "size=5%" is still accepted, as well as "size=5%,foo=bar", but
+ not "LABEL=x%y%z" since %y and %z are not valid specifiers today.)
+
* systemd-resolved now maintains a new dynamic
/run/systemd/resolve/stub-resolv.conf compatibility file. It is
recommended to make /etc/resolv.conf a symlink to it. This file
diff --git a/man/systemd-tmpfiles.xml b/man/systemd-tmpfiles.xml
index 4dd5b3da6c..24a08b0354 100644
--- a/man/systemd-tmpfiles.xml
+++ b/man/systemd-tmpfiles.xml
@@ -62,10 +62,16 @@
CONFIGFILE
- systemd-tmpfiles-setup.service
- systemd-tmpfiles-setup-dev.service
- systemd-tmpfiles-clean.service
- systemd-tmpfiles-clean.timer
+ System units:
+systemd-tmpfiles-setup.service
+systemd-tmpfiles-setup-dev.service
+systemd-tmpfiles-clean.service
+systemd-tmpfiles-clean.timer
+
+ User units:
+systemd-tmpfiles-setup.service
+systemd-tmpfiles-clean.service
+systemd-tmpfiles-clean.timer
@@ -115,7 +121,7 @@
T,
a, and
A have their ownership, access mode and
- security labels set.
+ security labels set.
@@ -133,11 +139,19 @@
marked with r or R are
removed.
+
+
+
+ Execute "user" configuration, i.e. tmpfiles.d
+ files in user configuration directories.
+
+
Also execute lines with an exclamation mark.
+
Only apply rules with paths that start with
@@ -191,8 +205,12 @@
Exit status
- On success, 0 is returned, a non-zero failure code
- otherwise.
+ On success, 0 is returned. If the configuration was invalid (invalid syntax, missing
+ arguments, …), so some lines had to be ignored, but no other errors occurred,
+ 65 is returned (EX_DATAERR from
+ /usr/include/sysexits.h). Otherwise, 1 is returned
+ (EXIT_FAILURE from /usr/include/stdlib.h).
+
diff --git a/man/systemd.unit.xml b/man/systemd.unit.xml
index 9c40562e08..159f629498 100644
--- a/man/systemd.unit.xml
+++ b/man/systemd.unit.xml
@@ -1297,7 +1297,8 @@
Many settings resolve specifiers which may be used to write
generic unit files referring to runtime or unit parameters that
- are replaced when the unit files are loaded. The following
+ are replaced when the unit files are loaded. Specifiers must be known
+ and resolvable for the setting to be valid. The following
specifiers are understood:
@@ -1356,18 +1357,18 @@
%S
- State directory root
+ State directory rootThis is either /var/lib (for the system manager) or the path $XDG_CONFIG_HOME resolves to (for user managers).%C
- Cache directory root
+ Cache directory rootThis is either /var/cache (for the system manager) or the path $XDG_CACHE_HOME resolves to (for user managers).%L
- Logs 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).
+ 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).%u
diff --git a/man/tmpfiles.d.xml b/man/tmpfiles.d.xml
index 793c124007..f5d97aa38f 100644
--- a/man/tmpfiles.d.xml
+++ b/man/tmpfiles.d.xml
@@ -48,9 +48,17 @@
- /etc/tmpfiles.d/*.conf
- /run/tmpfiles.d/*.conf
- /usr/lib/tmpfiles.d/*.conf
+ /etc/tmpfiles.d/*.conf
+/run/tmpfiles.d/*.conf
+/usr/lib/tmpfiles.d/*.conf
+
+
+ ~/.config/user-tmpfiles.d/*.conf
+$XDG_RUNTIME_DIR/user-tmpfiles.d/*.conf
+~/.local/share/user-tmpfiles.d/*.conf
+…
+/usr/share/user-tmpfiles.d/*.conf
+
@@ -482,51 +490,8 @@ r! /tmp/.X[0-9]*-lock
PathThe file system path specification supports simple
- specifier expansion. The following expansions are
- understood:
-
-
- Specifiers available
-
-
-
-
-
-
- Specifier
- Meaning
- Details
-
-
-
-
- %m
- Machine ID
- The machine ID of the running system, formatted as string. See machine-id5 for more information.
-
-
- %b
- Boot ID
- The boot ID of the running system, formatted as string. See random4 for more information.
-
-
- %H
- Host name
- The hostname of the running system.
-
-
- %v
- Kernel release
- Identical to uname -r output.
-
-
- %%
- Escaped %
- Single percent sign.
-
-
-
-
+ specifier expansion, see below. The path (after expansion) must be
+ absolute.
@@ -628,8 +593,94 @@ r! /tmp/.X[0-9]*-lock
attributes to be set. For h and
H, determines the file attributes to
set. Ignored for all other lines.
-
+ This field can contain specifiers, see below.
+
+
+
+
+ Specifiers
+
+ Specifiers can be used in the "path" and "argument" fields.
+ An unknown or unresolvable specifier is treated as invalid configuration.
+ The following expansions are understood:
+
+ Specifiers available
+
+
+
+
+
+
+ Specifier
+ Meaning
+ Details
+
+
+
+
+ %m
+ Machine ID
+ The machine ID of the running system, formatted as string. See machine-id5 for more information.
+
+
+ %b
+ Boot ID
+ The boot ID of the running system, formatted as string. See random4 for more information.
+
+
+ %H
+ Host name
+ The hostname of the running system.
+
+
+ %v
+ Kernel release
+ Identical to uname -r output.
+
+
+ %U
+ User UID
+ This is the numeric UID of the user running the service manager instance. In case of the system manager this resolves to 0.
+
+
+ %u
+ User name
+ This is the name of the user running the service manager instance. In case of the system manager this resolves to root.
+
+
+ %h
+ User home directory
+ This is the home directory of the user running the service manager instance. In case of the system manager this resolves to /root.
+
+
+ %t
+ System or user runtime directory
+ In --user mode, this is the same $XDG_RUNTIME_DIR, and /run otherwise.
+
+
+ %S
+ System or user state directory
+ In mode, this is the same as $XDG_CONFIG_HOME, and /var/lib otherwise.
+
+
+ %C
+ System or user cache directory
+ In mode, this is the same as $XDG_CACHE_HOME, and /var/cache otherwise.
+
+
+ %L
+ System or user log directory
+ In mode, this is the same as $XDG_CONFIG_HOME with /log appended, and /var/log otherwise.
+
+
+ %%
+ Escaped %
+ Single percent sign.
+
+
+
+
diff --git a/meson.build b/meson.build
index eda74c4765..3bdd87fc6b 100644
--- a/meson.build
+++ b/meson.build
@@ -2209,6 +2209,11 @@ if conf.get('ENABLE_TMPFILES') == 1
install : true,
install_dir : rootbindir)
public_programs += [exe]
+
+ test('test-systemd-tmpfiles',
+ test_systemd_tmpfiles_py,
+ args : exe.full_path())
+ # https://github.com/mesonbuild/meson/issues/2681
endif
if conf.get('ENABLE_HWDB') == 1
@@ -2441,6 +2446,7 @@ subdir('units')
subdir('sysctl.d')
subdir('sysusers.d')
subdir('tmpfiles.d')
+subdir('presets')
subdir('hwdb')
subdir('network')
subdir('man')
@@ -2457,8 +2463,6 @@ install_subdir('factory/etc',
install_data('xorg/50-systemd-user.sh',
install_dir : xinitrcdir)
-install_data('system-preset/90-systemd.preset',
- install_dir : systempresetdir)
install_data('modprobe.d/systemd.conf',
install_dir : modprobedir)
install_data('README',
diff --git a/system-preset/90-systemd.preset b/presets/90-systemd.preset
similarity index 100%
rename from system-preset/90-systemd.preset
rename to presets/90-systemd.preset
diff --git a/presets/meson.build b/presets/meson.build
new file mode 100644
index 0000000000..48aa8c9796
--- /dev/null
+++ b/presets/meson.build
@@ -0,0 +1,22 @@
+# SPDX-License-Identifier: LGPL-2.1+
+#
+# Copyright 2017 Zbigniew Jędrzejewski-Szmek
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# systemd is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with systemd; If not, see .
+
+install_data('90-systemd.preset',
+ install_dir : systempresetdir)
+
+install_data('user/90-systemd.preset',
+ install_dir : userpresetdir)
diff --git a/presets/user/90-systemd.preset b/presets/user/90-systemd.preset
new file mode 100644
index 0000000000..22fe41fc33
--- /dev/null
+++ b/presets/user/90-systemd.preset
@@ -0,0 +1,14 @@
+# SPDX-License-Identifier: LGPL-2.1+
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+# These ones should be enabled by default, even if distributions
+# generally follow a default-off policy.
+
+enable systemd-tmpfiles-setup.service
+enable systemd-tmpfiles-clean.timer
diff --git a/src/basic/path-util.c b/src/basic/path-util.c
index 056fa7d633..fe20799ba2 100644
--- a/src/basic/path-util.c
+++ b/src/basic/path-util.c
@@ -223,8 +223,8 @@ int path_strv_make_absolute_cwd(char **l) {
if (r < 0)
return r;
- free(*s);
- *s = t;
+ path_kill_slashes(t);
+ free_and_replace(*s, t);
}
return 0;
diff --git a/src/shared/path-lookup.c b/src/shared/path-lookup.c
index 57e0757529..d57c78a8b1 100644
--- a/src/shared/path-lookup.c
+++ b/src/shared/path-lookup.c
@@ -39,7 +39,7 @@
#include "user-util.h"
#include "util.h"
-static int user_runtime_dir(char **ret, const char *suffix) {
+int xdg_user_runtime_dir(char **ret, const char *suffix) {
const char *e;
char *j;
@@ -58,7 +58,7 @@ static int user_runtime_dir(char **ret, const char *suffix) {
return 0;
}
-static int user_config_dir(char **ret, const char *suffix) {
+int xdg_user_config_dir(char **ret, const char *suffix) {
const char *e;
char *j;
int r;
@@ -85,7 +85,7 @@ static int user_config_dir(char **ret, const char *suffix) {
return 0;
}
-static int user_data_dir(char **ret, const char *suffix) {
+int xdg_user_data_dir(char **ret, const char *suffix) {
const char *e;
char *j;
int r;
@@ -131,6 +131,41 @@ static const char* const user_config_unit_paths[] = {
NULL
};
+int xdg_user_dirs(char ***ret_config_dirs, char ***ret_data_dirs) {
+ /* Implement the mechanisms defined in
+ *
+ * http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html
+ *
+ * We look in both the config and the data dirs because we
+ * want to encourage that distributors ship their unit files
+ * as data, and allow overriding as configuration.
+ */
+ const char *e;
+ _cleanup_strv_free_ char **config_dirs = NULL, **data_dirs = NULL;
+
+ e = getenv("XDG_CONFIG_DIRS");
+ if (e) {
+ config_dirs = strv_split(e, ":");
+ if (!config_dirs)
+ return -ENOMEM;
+ }
+
+ e = getenv("XDG_DATA_DIRS");
+ if (e)
+ data_dirs = strv_split(e, ":");
+ else
+ data_dirs = strv_new("/usr/local/share",
+ "/usr/share",
+ NULL);
+ if (!data_dirs)
+ return -ENOMEM;
+
+ *ret_config_dirs = config_dirs;
+ *ret_data_dirs = data_dirs;
+ config_dirs = data_dirs = NULL;
+ return 0;
+}
+
static char** user_dirs(
const char *persistent_config,
const char *runtime_config,
@@ -144,38 +179,15 @@ static char** user_dirs(
_cleanup_strv_free_ char **config_dirs = NULL, **data_dirs = NULL;
_cleanup_free_ char *data_home = NULL;
_cleanup_strv_free_ char **res = NULL;
- const char *e;
char **tmp;
int r;
- /* Implement the mechanisms defined in
- *
- * http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html
- *
- * We look in both the config and the data dirs because we
- * want to encourage that distributors ship their unit files
- * as data, and allow overriding as configuration.
- */
-
- e = getenv("XDG_CONFIG_DIRS");
- if (e) {
- config_dirs = strv_split(e, ":");
- if (!config_dirs)
- return NULL;
- }
-
- r = user_data_dir(&data_home, "/systemd/user");
- if (r < 0 && r != -ENXIO)
+ r = xdg_user_dirs(&config_dirs, &data_dirs);
+ if (r < 0)
return NULL;
- e = getenv("XDG_DATA_DIRS");
- if (e)
- data_dirs = strv_split(e, ":");
- else
- data_dirs = strv_new("/usr/local/share",
- "/usr/share",
- NULL);
- if (!data_dirs)
+ r = xdg_user_data_dir(&data_home, "/systemd/user");
+ if (r < 0 && r != -ENXIO)
return NULL;
/* Now merge everything we found. */
@@ -311,7 +323,7 @@ static int acquire_transient_dir(
else if (scope == UNIT_FILE_SYSTEM)
transient = strdup("/run/systemd/transient");
else
- return user_runtime_dir(ret, "/systemd/transient");
+ return xdg_user_runtime_dir(ret, "/systemd/transient");
if (!transient)
return -ENOMEM;
@@ -339,11 +351,11 @@ static int acquire_config_dirs(UnitFileScope scope, char **persistent, char **ru
break;
case UNIT_FILE_USER:
- r = user_config_dir(&a, "/systemd/user");
+ r = xdg_user_config_dir(&a, "/systemd/user");
if (r < 0 && r != -ENXIO)
return r;
- r = user_runtime_dir(runtime, "/systemd/user");
+ r = xdg_user_runtime_dir(runtime, "/systemd/user");
if (r < 0) {
if (r != -ENXIO)
return r;
@@ -399,11 +411,11 @@ static int acquire_control_dirs(UnitFileScope scope, char **persistent, char **r
}
case UNIT_FILE_USER:
- r = user_config_dir(&a, "/systemd/system.control");
+ r = xdg_user_config_dir(&a, "/systemd/system.control");
if (r < 0 && r != -ENXIO)
return r;
- r = user_runtime_dir(runtime, "/systemd/system.control");
+ r = xdg_user_runtime_dir(runtime, "/systemd/system.control");
if (r < 0) {
if (r != -ENXIO)
return r;
diff --git a/src/shared/path-lookup.h b/src/shared/path-lookup.h
index bcf9ca4de6..42a870aa3e 100644
--- a/src/shared/path-lookup.h
+++ b/src/shared/path-lookup.h
@@ -68,6 +68,10 @@ struct LookupPaths {
};
int lookup_paths_init(LookupPaths *p, UnitFileScope scope, LookupPathsFlags flags, const char *root_dir);
+int xdg_user_dirs(char ***ret_config_dirs, char ***ret_data_dirs);
+int xdg_user_runtime_dir(char **ret, const char *suffix);
+int xdg_user_config_dir(char **ret, const char *suffix);
+int xdg_user_data_dir(char **ret, const char *suffix);
bool path_is_user_data_dir(const char *path);
bool path_is_user_config_dir(const char *path);
diff --git a/src/shared/specifier.c b/src/shared/specifier.c
index 9bef890346..6839d1892d 100644
--- a/src/shared/specifier.c
+++ b/src/shared/specifier.c
@@ -41,6 +41,10 @@
*
*/
+/* Any ASCII character or digit: our pool of potential specifiers,
+ * and "%" used for escaping. */
+#define POSSIBLE_SPECIFIERS ALPHANUMERICAL "%"
+
int specifier_printf(const char *text, const Specifier table[], void *userdata, char **_ret) {
char *ret, *t;
const char *f;
@@ -97,7 +101,10 @@ int specifier_printf(const char *text, const Specifier table[], void *userdata,
ret = n;
t = n + j + k;
- } else {
+ } else if (strchr(POSSIBLE_SPECIFIERS, *f))
+ /* Oops, an unknown specifier. */
+ return -EBADSLT;
+ else {
*(t++) = '%';
*(t++) = *f;
}
@@ -200,7 +207,7 @@ int specifier_user_name(char specifier, void *data, void *userdata, char **ret)
/* If we are UID 0 (root), this will not result in NSS, otherwise it might. This is good, as we want to be able
* to run this in PID 1, where our user ID is 0, but where NSS lookups are not allowed.
- * We don't user getusername_malloc() here, because we don't want to look at $USER, to remain consistent with
+ * We don't use getusername_malloc() here, because we don't want to look at $USER, to remain consistent with
* specifer_user_id() below.
*/
diff --git a/src/test/meson.build b/src/test/meson.build
index 327c2859e8..6bb5bd629e 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -54,6 +54,10 @@ test_dlopen_c = files('test-dlopen.c')
############################################################
+test_systemd_tmpfiles_py = find_program('test-systemd-tmpfiles.py')
+
+############################################################
+
tests += [
[['src/test/test-device-nodes.c'],
[],
diff --git a/src/test/test-systemd-tmpfiles.py b/src/test/test-systemd-tmpfiles.py
new file mode 100755
index 0000000000..309cee23e6
--- /dev/null
+++ b/src/test/test-systemd-tmpfiles.py
@@ -0,0 +1,140 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1+
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+import os
+import sys
+import socket
+import subprocess
+import tempfile
+import pwd
+
+try:
+ from systemd import id128
+except ImportError:
+ id128 = None
+
+EX_DATAERR = 65 # from sysexits.h
+EXIT_TEST_SKIP = 77
+
+try:
+ subprocess.run
+except AttributeError:
+ sys.exit(EXIT_TEST_SKIP)
+
+exe = sys.argv[1]
+
+def test_line(line, *, user, returncode=EX_DATAERR, extra={}):
+ args = ['--user'] if user else []
+ print('Running {} {} on {!r}'.format(exe, ' '.join(args), line))
+ c = subprocess.run([exe, '--create', '-'] + args,
+ input=line, stdout=subprocess.PIPE, universal_newlines=True,
+ **extra)
+ assert c.returncode == returncode, c
+
+def test_invalids(*, user):
+ test_line('asdfa', user=user)
+ test_line('f "open quote', user=user)
+ test_line('f closed quote""', user=user)
+ test_line('Y /unknown/letter', user=user)
+ test_line('w non/absolute/path', user=user)
+ test_line('s', user=user) # s is for short
+ test_line('f!! /too/many/bangs', user=user)
+ test_line('f++ /too/many/plusses', user=user)
+ test_line('f+!+ /too/many/plusses', user=user)
+ test_line('f!+! /too/many/bangs', user=user)
+ test_line('w /unresolved/argument - - - - "%Y"', user=user)
+ test_line('w /unresolved/argument/sandwich - - - - "%v%Y%v"', user=user)
+ test_line('w /unresolved/filename/%Y - - - - "whatever"', user=user)
+ test_line('w /unresolved/filename/sandwich/%v%Y%v - - - - "whatever"', user=user)
+ test_line('w - - - - - "no file specfied"', user=user)
+ test_line('C - - - - - "no file specfied"', user=user)
+ test_line('C non/absolute/path - - - - -', user=user)
+ test_line('b - - - - - -', user=user)
+ test_line('b 1234 - - - - -', user=user)
+ test_line('c - - - - - -', user=user)
+ test_line('c 1234 - - - - -', user=user)
+ test_line('t - - -', user=user)
+ test_line('T - - -', user=user)
+ test_line('a - - -', user=user)
+ test_line('A - - -', user=user)
+ test_line('h - - -', user=user)
+ test_line('H - - -', user=user)
+
+def test_unitialized_t():
+ if os.getuid() == 0:
+ return
+
+ test_line('w /foo - - - - "specifier for --user %t"',
+ user=True, returncode=0, extra={'env':{}})
+
+def test_content(line, expected, *, user, extra={}):
+ d = tempfile.TemporaryDirectory(prefix='test-systemd-tmpfiles.')
+ arg = d.name + '/arg'
+ spec = line.format(arg)
+ test_line(spec, user=user, returncode=0, extra=extra)
+ content = open(arg).read()
+ print('expect: {!r}\nactual: {!r}'.format(expected, content))
+ assert content == expected
+
+def test_valid_specifiers(*, user):
+ test_content('f {} - - - - two words', 'two words', user=user)
+ if id128:
+ try:
+ test_content('f {} - - - - %m', '{}'.format(id128.get_machine().hex), user=user)
+ except AssertionError as e:
+ print(e)
+ print('/etc/machine-id: {!r}'.format(open('/etc/machine-id').read()))
+ print('/proc/cmdline: {!r}'.format(open('/proc/cmdline').read()))
+ print('skipping')
+ test_content('f {} - - - - %b', '{}'.format(id128.get_boot().hex), user=user)
+ test_content('f {} - - - - %H', '{}'.format(socket.gethostname()), user=user)
+ test_content('f {} - - - - %v', '{}'.format(os.uname().release), user=user)
+ test_content('f {} - - - - %U', '{}'.format(os.getuid()), user=user)
+
+ user = pwd.getpwuid(os.getuid())
+ test_content('f {} - - - - %u', '{}'.format(user.pw_name), user=user)
+
+ # Note that %h is the only specifier in which we look the environment,
+ # because we check $HOME. Should we even be doing that?
+ home = os.path.expanduser("~")
+ test_content('f {} - - - - %h', '{}'.format(home), user=user)
+
+ xdg_runtime_dir = os.getenv('XDG_RUNTIME_DIR')
+ if xdg_runtime_dir is not None or not user:
+ test_content('f {} - - - - %t',
+ xdg_runtime_dir if user else '/run',
+ user=user)
+
+ xdg_config_home = os.getenv('XDG_CONFIG_HOME')
+ if xdg_config_home is not None or not user:
+ test_content('f {} - - - - %S',
+ xdg_config_home if user else '/var/lib',
+ user=user)
+
+ xdg_cache_home = os.getenv('XDG_CACHE_HOME')
+ if xdg_cache_home is not None or not user:
+ test_content('f {} - - - - %C',
+ xdg_cache_home if user else '/var/cache',
+ user=user)
+
+ if xdg_config_home is not None or not user:
+ test_content('f {} - - - - %L',
+ xdg_config_home + '/log' if user else '/var/log',
+ user=user)
+
+ test_content('f {} - - - - %%', '%', user=user)
+
+if __name__ == '__main__':
+ test_invalids(user=False)
+ test_invalids(user=True)
+ test_unitialized_t()
+
+ test_valid_specifiers(user=False)
+ test_valid_specifiers(user=True)
diff --git a/src/tmpfiles/tmpfiles.c b/src/tmpfiles/tmpfiles.c
index c29087b9d0..2344189426 100644
--- a/src/tmpfiles/tmpfiles.c
+++ b/src/tmpfiles/tmpfiles.c
@@ -33,9 +33,12 @@
#include
#include
#include
+#include
#include
#include
+#include "sd-path.h"
+
#include "acl-util.h"
#include "alloc-util.h"
#include "btrfs-util.h"
@@ -59,6 +62,7 @@
#include "mkdir.h"
#include "mount-util.h"
#include "parse-util.h"
+#include "path-lookup.h"
#include "path-util.h"
#include "rm-rf.h"
#include "selinux-util.h"
@@ -150,6 +154,15 @@ typedef struct ItemArray {
size_t size;
} ItemArray;
+typedef enum DirectoryType {
+ DIRECTORY_RUNTIME = 0,
+ DIRECTORY_STATE,
+ DIRECTORY_CACHE,
+ DIRECTORY_LOGS,
+ _DIRECTORY_TYPE_MAX,
+} DirectoryType;
+
+static bool arg_user = false;
static bool arg_create = false;
static bool arg_clean = false;
static bool arg_remove = false;
@@ -159,20 +172,27 @@ static char **arg_include_prefixes = NULL;
static char **arg_exclude_prefixes = NULL;
static char *arg_root = NULL;
-static const char conf_file_dirs[] = CONF_PATHS_NULSTR("tmpfiles.d");
-
#define MAX_DEPTH 256
static OrderedHashmap *items = NULL, *globs = NULL;
static Set *unix_sockets = NULL;
static int specifier_machine_id_safe(char specifier, void *data, void *userdata, char **ret);
+static int specifier_directory(char specifier, void *data, void *userdata, char **ret);
static const Specifier specifier_table[] = {
{ 'm', specifier_machine_id_safe, NULL },
- { 'b', specifier_boot_id, NULL },
- { 'H', specifier_host_name, NULL },
- { 'v', specifier_kernel_release, NULL },
+ { 'b', specifier_boot_id, NULL },
+ { 'H', specifier_host_name, NULL },
+ { 'v', specifier_kernel_release, NULL },
+
+ { 'U', specifier_user_id, NULL },
+ { 'u', specifier_user_name, NULL },
+ { 'h', specifier_user_home, NULL },
+ { 't', specifier_directory, UINT_TO_PTR(DIRECTORY_RUNTIME) },
+ { 'S', specifier_directory, UINT_TO_PTR(DIRECTORY_STATE) },
+ { 'C', specifier_directory, UINT_TO_PTR(DIRECTORY_CACHE) },
+ { 'L', specifier_directory, UINT_TO_PTR(DIRECTORY_LOGS) },
{}
};
@@ -185,22 +205,56 @@ static int specifier_machine_id_safe(char specifier, void *data, void *userdata,
r = specifier_machine_id(specifier, data, userdata, ret);
if (r == -ENOENT)
- return -ENOKEY;
+ return -ENXIO;
return r;
}
+static int specifier_directory(char specifier, void *data, void *userdata, char **ret) {
+ struct table_entry {
+ uint64_t type;
+ const char *suffix;
+ };
+
+ static const struct table_entry paths_system[] = {
+ [DIRECTORY_RUNTIME] = { SD_PATH_SYSTEM_RUNTIME },
+ [DIRECTORY_STATE] = { SD_PATH_SYSTEM_STATE_PRIVATE },
+ [DIRECTORY_CACHE] = { SD_PATH_SYSTEM_STATE_CACHE },
+ [DIRECTORY_LOGS] = { SD_PATH_SYSTEM_STATE_LOGS },
+ };
+
+ static const struct table_entry paths_user[] = {
+ [DIRECTORY_RUNTIME] = { SD_PATH_USER_RUNTIME },
+ [DIRECTORY_STATE] = { SD_PATH_USER_CONFIGURATION },
+ [DIRECTORY_CACHE] = { SD_PATH_USER_STATE_CACHE },
+ [DIRECTORY_LOGS] = { SD_PATH_USER_CONFIGURATION, "log" },
+ };
+
+ unsigned i;
+ const struct table_entry *paths;
+
+ assert_cc(ELEMENTSOF(paths_system) == ELEMENTSOF(paths_user));
+ paths = arg_user ? paths_user : paths_system;
+
+ i = PTR_TO_UINT(data);
+ assert(i < ELEMENTSOF(paths_system));
+
+ return sd_path_home(paths[i].type, paths[i].suffix, ret);
+}
+
static int log_unresolvable_specifier(const char *filename, unsigned line) {
static bool notified = false;
- /* This is called when /etc is not fully initialized (e.g. in a chroot
- * environment) where some specifiers are unresolvable. These cases are
- * not considered as an error so log at LOG_NOTICE only for the first
- * time and then downgrade this to LOG_DEBUG for the rest. */
+ /* In system mode, this is called when /etc is not fully initialized (e.g.
+ * in a chroot environment) where some specifiers are unresolvable. In user
+ * mode, this is called when some variables are not defined. These cases are
+ * not considered as an error so log at LOG_NOTICE only for the first time
+ * and then downgrade this to LOG_DEBUG for the rest. */
log_full(notified ? LOG_DEBUG : LOG_NOTICE,
- "[%s:%u] Failed to resolve specifier: uninitialized /etc detected, skipping",
- filename, line);
+ "[%s:%u] Failed to resolve specifier: %s, skipping",
+ filename, line,
+ arg_user ? "Required $XDG_... variable not defined" : "uninitialized /etc detected");
if (!notified)
log_notice("All rules containing unresolvable specifiers will be skipped.");
@@ -209,6 +263,57 @@ static int log_unresolvable_specifier(const char *filename, unsigned line) {
return 0;
}
+static int user_config_paths(char*** ret) {
+ _cleanup_strv_free_ char **config_dirs = NULL, **data_dirs = NULL;
+ _cleanup_free_ char *persistent_config = NULL, *runtime_config = NULL, *data_home = NULL;
+ _cleanup_strv_free_ char **res = NULL;
+ int r;
+
+ r = xdg_user_dirs(&config_dirs, &data_dirs);
+ if (r < 0)
+ return r;
+
+ r = xdg_user_config_dir(&persistent_config, "/user-tmpfiles.d");
+ if (r < 0 && r != -ENXIO)
+ return r;
+
+ r = xdg_user_runtime_dir(&runtime_config, "/user-tmpfiles.d");
+ if (r < 0 && r != -ENXIO)
+ return r;
+
+ r = xdg_user_data_dir(&data_home, "/user-tmpfiles.d");
+ if (r < 0 && r != -ENXIO)
+ return r;
+
+ r = strv_extend_strv_concat(&res, config_dirs, "/user-tmpfiles.d");
+ if (r < 0)
+ return r;
+
+ r = strv_extend(&res, persistent_config);
+ if (r < 0)
+ return r;
+
+ r = strv_extend(&res, runtime_config);
+ if (r < 0)
+ return r;
+
+ r = strv_extend(&res, data_home);
+ if (r < 0)
+ return r;
+
+ r = strv_extend_strv_concat(&res, data_dirs, "/user-tmpfiles.d");
+ if (r < 0)
+ return r;
+
+ r = path_strv_make_absolute_cwd(res);
+ if (r < 0)
+ return r;
+
+ *ret = res;
+ res = NULL;
+ return 0;
+}
+
static bool needs_glob(ItemType t) {
return IN_SET(t,
WRITE_FILE,
@@ -670,7 +775,7 @@ static int path_set_perms(Item *i, const char *path) {
return log_error_errno(errno, "Failed to fstat() file %s: %m", path);
if (S_ISLNK(st.st_mode))
- log_debug("Skipping mode an owner fix for symlink %s.", path);
+ log_debug("Skipping mode and owner fix for symlink %s.", path);
else {
char fn[strlen("/proc/self/fd/") + DECIMAL_STR_MAX(int)];
xsprintf(fn, "/proc/self/fd/%i", fd);
@@ -1631,12 +1736,12 @@ static int clean_item(Item *i) {
case CREATE_SUBVOLUME:
case CREATE_SUBVOLUME_INHERIT_QUOTA:
case CREATE_SUBVOLUME_NEW_QUOTA:
- case EMPTY_DIRECTORY:
case TRUNCATE_DIRECTORY:
case IGNORE_PATH:
case COPY_FILES:
clean_item_instance(i, i->path);
return 0;
+ case EMPTY_DIRECTORY:
case IGNORE_DIRECTORY_PATH:
return glob_item(i, clean_item_instance, false);
default:
@@ -1841,7 +1946,7 @@ static int specifier_expansion_from_arg(Item *i) {
return 0;
}
-static int parse_line(const char *fname, unsigned line, const char *buffer) {
+static int parse_line(const char *fname, unsigned line, const char *buffer, bool *invalid_config) {
_cleanup_free_ char *action = NULL, *mode = NULL, *user = NULL, *group = NULL, *age = NULL, *path = NULL;
_cleanup_(item_free_contents) Item i = {};
@@ -1865,9 +1970,15 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) {
&group,
&age,
NULL);
- if (r < 0)
+ if (r < 0) {
+ if (IN_SET(r, -EINVAL, -EBADSLT))
+ /* invalid quoting and such or an unknown specifier */
+ *invalid_config = true;
return log_error_errno(r, "[%s:%u] Failed to parse line: %m", fname, line);
+ }
+
else if (r < 2) {
+ *invalid_config = true;
log_error("[%s:%u] Syntax error.", fname, line);
return -EIO;
}
@@ -1879,6 +1990,7 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) {
}
if (isempty(action)) {
+ *invalid_config = true;
log_error("[%s:%u] Command too short '%s'.", fname, line, action);
return -EINVAL;
}
@@ -1889,6 +2001,7 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) {
else if (action[pos] == '+' && !force)
force = true;
else {
+ *invalid_config = true;
log_error("[%s:%u] Unknown modifiers in command '%s'",
fname, line, action);
return -EINVAL;
@@ -1905,10 +2018,13 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) {
i.force = force;
r = specifier_printf(path, specifier_table, NULL, &i.path);
- if (r == -ENOKEY)
+ if (r == -ENXIO)
return log_unresolvable_specifier(fname, line);
- if (r < 0)
+ if (r < 0) {
+ if (IN_SET(r, -EINVAL, -EBADSLT))
+ *invalid_config = true;
return log_error_errno(r, "[%s:%u] Failed to replace specifiers: %s", fname, line, path);
+ }
switch (i.type) {
@@ -1945,6 +2061,7 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) {
case WRITE_FILE:
if (!i.argument) {
+ *invalid_config = true;
log_error("[%s:%u] Write file requires argument.", fname, line);
return -EBADMSG;
}
@@ -1956,6 +2073,7 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) {
if (!i.argument)
return log_oom();
} else if (!path_is_absolute(i.argument)) {
+ *invalid_config = true;
log_error("[%s:%u] Source path is not absolute.", fname, line);
return -EBADMSG;
}
@@ -1968,11 +2086,13 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) {
unsigned major, minor;
if (!i.argument) {
+ *invalid_config = true;
log_error("[%s:%u] Device file requires argument.", fname, line);
return -EBADMSG;
}
if (sscanf(i.argument, "%u:%u", &major, &minor) != 2) {
+ *invalid_config = true;
log_error("[%s:%u] Can't parse device file major/minor '%s'.", fname, line, i.argument);
return -EBADMSG;
}
@@ -1984,6 +2104,7 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) {
case SET_XATTR:
case RECURSIVE_SET_XATTR:
if (!i.argument) {
+ *invalid_config = true;
log_error("[%s:%u] Set extended attribute requires argument.", fname, line);
return -EBADMSG;
}
@@ -1995,6 +2116,7 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) {
case SET_ACL:
case RECURSIVE_SET_ACL:
if (!i.argument) {
+ *invalid_config = true;
log_error("[%s:%u] Set ACLs requires argument.", fname, line);
return -EBADMSG;
}
@@ -2006,21 +2128,26 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) {
case SET_ATTRIBUTE:
case RECURSIVE_SET_ATTRIBUTE:
if (!i.argument) {
+ *invalid_config = true;
log_error("[%s:%u] Set file attribute requires argument.", fname, line);
return -EBADMSG;
}
r = parse_attribute_from_arg(&i);
+ if (IN_SET(r, -EINVAL, -EBADSLT))
+ *invalid_config = true;
if (r < 0)
return r;
break;
default:
log_error("[%s:%u] Unknown command type '%c'.", fname, line, (char) i.type);
+ *invalid_config = true;
return -EBADMSG;
}
if (!path_is_absolute(i.path)) {
log_error("[%s:%u] Path '%s' not absolute.", fname, line, i.path);
+ *invalid_config = true;
return -EBADMSG;
}
@@ -2030,11 +2157,14 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) {
return 0;
r = specifier_expansion_from_arg(&i);
- if (r == -ENOKEY)
+ if (r == -ENXIO)
return log_unresolvable_specifier(fname, line);
- if (r < 0)
+ if (r < 0) {
+ if (IN_SET(r, -EINVAL, -EBADSLT))
+ *invalid_config = true;
return log_error_errno(r, "[%s:%u] Failed to substitute specifiers in argument: %m",
fname, line);
+ }
if (arg_root) {
char *p;
@@ -2052,8 +2182,8 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) {
r = get_user_creds(&u, &i.uid, NULL, NULL, NULL);
if (r < 0) {
- log_error("[%s:%u] Unknown user '%s'.", fname, line, user);
- return r;
+ *invalid_config = true;
+ return log_error_errno(r, "[%s:%u] Unknown user '%s'.", fname, line, user);
}
i.uid_set = true;
@@ -2064,6 +2194,7 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) {
r = get_group_creds(&g, &i.gid);
if (r < 0) {
+ *invalid_config = true;
log_error("[%s:%u] Unknown group '%s'.", fname, line, group);
return r;
}
@@ -2081,6 +2212,7 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) {
}
if (parse_mode(mm, &m) < 0) {
+ *invalid_config = true;
log_error("[%s:%u] Invalid mode '%s'.", fname, line, mode);
return -EBADMSG;
}
@@ -2099,6 +2231,7 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) {
}
if (parse_sec(a, &i.age) < 0) {
+ *invalid_config = true;
log_error("[%s:%u] Invalid age '%s'.", fname, line, age);
return -EBADMSG;
}
@@ -2114,8 +2247,8 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) {
for (n = 0; n < existing->count; n++) {
if (!item_compatible(existing->items + n, &i)) {
- log_warning("[%s:%u] Duplicate line for path \"%s\", ignoring.",
- fname, line, i.path);
+ log_notice("[%s:%u] Duplicate line for path \"%s\", ignoring.",
+ fname, line, i.path);
return 0;
}
}
@@ -2142,6 +2275,7 @@ static void help(void) {
printf("%s [OPTIONS...] [CONFIGURATION FILE...]\n\n"
"Creates, deletes and cleans up volatile and temporary files and directories.\n\n"
" -h --help Show this help\n"
+ " --user Execute user configuration\n"
" --version Show package version\n"
" --create Create marked files/directories\n"
" --clean Clean up marked directories\n"
@@ -2149,14 +2283,15 @@ static void help(void) {
" --boot Execute actions only safe at boot\n"
" --prefix=PATH Only apply rules with the specified prefix\n"
" --exclude-prefix=PATH Ignore rules with the specified prefix\n"
- " --root=PATH Operate on an alternate filesystem root\n",
- program_invocation_short_name);
+ " --root=PATH Operate on an alternate filesystem root\n"
+ , program_invocation_short_name);
}
static int parse_argv(int argc, char *argv[]) {
enum {
ARG_VERSION = 0x100,
+ ARG_USER,
ARG_CREATE,
ARG_CLEAN,
ARG_REMOVE,
@@ -2168,6 +2303,7 @@ static int parse_argv(int argc, char *argv[]) {
static const struct option options[] = {
{ "help", no_argument, NULL, 'h' },
+ { "user", no_argument, NULL, ARG_USER },
{ "version", no_argument, NULL, ARG_VERSION },
{ "create", no_argument, NULL, ARG_CREATE },
{ "clean", no_argument, NULL, ARG_CLEAN },
@@ -2195,6 +2331,10 @@ static int parse_argv(int argc, char *argv[]) {
case ARG_VERSION:
return version();
+ case ARG_USER:
+ arg_user = true;
+ break;
+
case ARG_CREATE:
arg_create = true;
break;
@@ -2242,7 +2382,7 @@ static int parse_argv(int argc, char *argv[]) {
return 1;
}
-static int read_config_file(const char *fn, bool ignore_enoent) {
+static int read_config_file(const char **config_dirs, const char *fn, bool ignore_enoent, bool *invalid_config) {
_cleanup_fclose_ FILE *_f = NULL;
FILE *f;
char line[LINE_MAX];
@@ -2258,7 +2398,7 @@ static int read_config_file(const char *fn, bool ignore_enoent) {
fn = "";
f = stdin;
} else {
- r = search_and_fopen_nulstr(fn, "re", arg_root, conf_file_dirs, &_f);
+ r = search_and_fopen(fn, "re", arg_root, config_dirs, &_f);
if (r < 0) {
if (ignore_enoent && r == -ENOENT) {
log_debug_errno(r, "Failed to open \"%s\", ignoring: %m", fn);
@@ -2274,6 +2414,7 @@ static int read_config_file(const char *fn, bool ignore_enoent) {
FOREACH_LINE(line, f, break) {
char *l;
int k;
+ bool invalid_line = false;
v++;
@@ -2281,9 +2422,15 @@ static int read_config_file(const char *fn, bool ignore_enoent) {
if (IN_SET(*l, 0, '#'))
continue;
- k = parse_line(fn, v, l);
- if (k < 0 && r == 0)
- r = k;
+ k = parse_line(fn, v, l, &invalid_line);
+ if (k < 0) {
+ if (invalid_line)
+ /* Allow reporting with a special code if the caller requested this */
+ *invalid_config = true;
+ else if (r == 0)
+ /* The first error becomes our return value */
+ r = k;
+ }
}
/* we have to determine age parameter for each entry of type X */
@@ -2327,6 +2474,9 @@ int main(int argc, char *argv[]) {
int r, k;
ItemArray *a;
Iterator iterator;
+ _cleanup_strv_free_ char **config_dirs = NULL;
+ bool invalid_config = false;
+ char **f;
r = parse_argv(argc, argv);
if (r <= 0)
@@ -2350,27 +2500,48 @@ int main(int argc, char *argv[]) {
r = 0;
+ if (arg_user) {
+ r = user_config_paths(&config_dirs);
+ if (r < 0) {
+ log_error_errno(r, "Failed to initialize configuration directory list: %m");
+ goto finish;
+ }
+ } else {
+ config_dirs = strv_split_nulstr(CONF_PATHS_NULSTR("tmpfiles.d"));
+ if (!config_dirs) {
+ r = log_oom();
+ goto finish;
+ }
+ }
+
+ {
+ _cleanup_free_ char *t = NULL;
+
+ t = strv_join(config_dirs, "\n\t");
+ if (t)
+ log_debug("Looking for configuration files in (higher priority first:\n\t%s", t);
+ }
+
if (optind < argc) {
int j;
for (j = optind; j < argc; j++) {
- k = read_config_file(argv[j], false);
+ k = read_config_file((const char**) config_dirs, argv[j], false, &invalid_config);
if (k < 0 && r == 0)
r = k;
}
} else {
_cleanup_strv_free_ char **files = NULL;
- char **f;
- r = conf_files_list_nulstr(&files, ".conf", arg_root, 0, conf_file_dirs);
+ r = conf_files_list_strv(&files, ".conf", arg_root, 0, (const char* const*) config_dirs);
if (r < 0) {
log_error_errno(r, "Failed to enumerate tmpfiles.d files: %m");
goto finish;
}
STRV_FOREACH(f, files) {
- k = read_config_file(*f, true);
+ k = read_config_file((const char**) config_dirs, *f, true, &invalid_config);
if (k < 0 && r == 0)
r = k;
}
@@ -2404,5 +2575,10 @@ finish:
mac_selinux_finish();
- return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
+ if (r < 0)
+ return EXIT_FAILURE;
+ else if (invalid_config)
+ return EX_DATAERR;
+ else
+ return EXIT_SUCCESS;
}
diff --git a/units/systemd-tmpfiles-clean.service.in b/units/systemd-tmpfiles-clean.service.in
index 5963cf27fb..d1025afadb 100644
--- a/units/systemd-tmpfiles-clean.service.in
+++ b/units/systemd-tmpfiles-clean.service.in
@@ -18,4 +18,5 @@ Before=shutdown.target
[Service]
Type=oneshot
ExecStart=@rootbindir@/systemd-tmpfiles --clean
+SuccessExitStatus=65
IOSchedulingClass=idle
diff --git a/units/systemd-tmpfiles-setup-dev.service.in b/units/systemd-tmpfiles-setup-dev.service.in
index 29b4f0bec0..6a6ebed955 100644
--- a/units/systemd-tmpfiles-setup-dev.service.in
+++ b/units/systemd-tmpfiles-setup-dev.service.in
@@ -20,3 +20,4 @@ ConditionCapability=CAP_SYS_MODULE
Type=oneshot
RemainAfterExit=yes
ExecStart=@rootbindir@/systemd-tmpfiles --prefix=/dev --create --boot
+SuccessExitStatus=65
diff --git a/units/systemd-tmpfiles-setup.service.in b/units/systemd-tmpfiles-setup.service.in
index ba24b8d823..0410d0bfd8 100644
--- a/units/systemd-tmpfiles-setup.service.in
+++ b/units/systemd-tmpfiles-setup.service.in
@@ -20,3 +20,4 @@ RefuseManualStop=yes
Type=oneshot
RemainAfterExit=yes
ExecStart=@rootbindir@/systemd-tmpfiles --create --remove --boot --exclude-prefix=/dev
+SuccessExitStatus=65
diff --git a/units/user/meson.build b/units/user/meson.build
index fbe6e3fb13..e463ae226c 100644
--- a/units/user/meson.build
+++ b/units/user/meson.build
@@ -29,6 +29,7 @@ units = [
'sockets.target',
'sound.target',
'timers.target',
+ 'systemd-tmpfiles-clean.timer',
]
foreach file : units
@@ -38,6 +39,8 @@ endforeach
in_units = [
'systemd-exit.service',
+ 'systemd-tmpfiles-clean.service',
+ 'systemd-tmpfiles-setup.service',
]
foreach file : in_units
diff --git a/units/user/systemd-tmpfiles-clean.service.in b/units/user/systemd-tmpfiles-clean.service.in
new file mode 100644
index 0000000000..9cd19720d3
--- /dev/null
+++ b/units/user/systemd-tmpfiles-clean.service.in
@@ -0,0 +1,21 @@
+# SPDX-License-Identifier: LGPL-2.1+
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=Cleanup of User's Temporary Files and Directories
+Documentation=man:tmpfiles.d(5) man:systemd-tmpfiles(8)
+DefaultDependencies=no
+Conflicts=shutdown.target
+Before=basic.target shutdown.target
+
+[Service]
+Type=oneshot
+ExecStart=@rootbindir@/systemd-tmpfiles --user --clean
+SuccessExitStatus=65
+IOSchedulingClass=idle
diff --git a/units/user/systemd-tmpfiles-clean.timer b/units/user/systemd-tmpfiles-clean.timer
new file mode 100644
index 0000000000..d1dbad98de
--- /dev/null
+++ b/units/user/systemd-tmpfiles-clean.timer
@@ -0,0 +1,19 @@
+# SPDX-License-Identifier: LGPL-2.1+
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=Daily Cleanup of User's Temporary Directories
+Documentation=man:tmpfiles.d(5) man:systemd-tmpfiles(8)
+
+[Timer]
+OnStartupSec=5min
+OnUnitActiveSec=1d
+
+[Install]
+WantedBy=timers.target
diff --git a/units/user/systemd-tmpfiles-setup.service.in b/units/user/systemd-tmpfiles-setup.service.in
new file mode 100644
index 0000000000..6467dab896
--- /dev/null
+++ b/units/user/systemd-tmpfiles-setup.service.in
@@ -0,0 +1,25 @@
+# SPDX-License-Identifier: LGPL-2.1+
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=Create User's Volatile Files and Directories
+Documentation=man:tmpfiles.d(5) man:systemd-tmpfiles(8)
+DefaultDependencies=no
+Conflicts=shutdown.target
+Before=basic.target shutdown.target
+RefuseManualStop=yes
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+ExecStart=@rootbindir@/systemd-tmpfiles --user --create --remove --boot
+SuccessExitStatus=65
+
+[Install]
+WantedBy=basic.target