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 root This 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 root This 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 Path The 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