Files
systemd/src/tmpfiles/tmpfiles.c
T
Zbigniew Jędrzejewski-Szmek c7f0d9e5ac tree-wide: make FOREACH_DIRENT_ALL define the iterator variable
The variable is not useful outside of the loop (it'll always be null
after the loop is finished), so we can declare it inline in the loop.
This saves one variable declaration and reduces the chances that somebody
tries to use the variable outside of the loop.

For consistency, 'de' is used everywhere for the var name.
2021-12-15 16:19:13 +01:00

3861 lines
139 KiB
C

/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <errno.h>
#include <fcntl.h>
#include <fnmatch.h>
#include <getopt.h>
#include <limits.h>
#include <linux/fs.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdlib.h>
#include <sys/file.h>
#include <sys/xattr.h>
#include <sysexits.h>
#include <time.h>
#include <unistd.h>
#include "sd-path.h"
#include "acl-util.h"
#include "alloc-util.h"
#include "btrfs-util.h"
#include "capability-util.h"
#include "chase-symlinks.h"
#include "chattr-util.h"
#include "conf-files.h"
#include "copy.h"
#include "def.h"
#include "dirent-util.h"
#include "dissect-image.h"
#include "env-util.h"
#include "escape.h"
#include "fd-util.h"
#include "fileio.h"
#include "format-util.h"
#include "fs-util.h"
#include "glob-util.h"
#include "io-util.h"
#include "label.h"
#include "log.h"
#include "macro.h"
#include "main-func.h"
#include "missing_stat.h"
#include "missing_syscall.h"
#include "mkdir-label.h"
#include "mount-util.h"
#include "mountpoint-util.h"
#include "offline-passwd.h"
#include "pager.h"
#include "parse-argument.h"
#include "parse-util.h"
#include "path-lookup.h"
#include "path-util.h"
#include "pretty-print.h"
#include "rlimit-util.h"
#include "rm-rf.h"
#include "selinux-util.h"
#include "set.h"
#include "sort-util.h"
#include "specifier.h"
#include "stat-util.h"
#include "stdio-util.h"
#include "string-table.h"
#include "string-util.h"
#include "strv.h"
#include "terminal-util.h"
#include "umask-util.h"
#include "user-util.h"
/* This reads all files listed in /etc/tmpfiles.d/?*.conf and creates
* them in the file system. This is intended to be used to create
* properly owned directories beneath /tmp, /var/tmp, /run, which are
* volatile and hence need to be recreated on bootup. */
typedef enum OperationMask {
OPERATION_CREATE = 1 << 0,
OPERATION_REMOVE = 1 << 1,
OPERATION_CLEAN = 1 << 2,
} OperationMask;
typedef enum ItemType {
/* These ones take file names */
CREATE_FILE = 'f',
TRUNCATE_FILE = 'F', /* deprecated: use f+ */
CREATE_DIRECTORY = 'd',
TRUNCATE_DIRECTORY = 'D',
CREATE_SUBVOLUME = 'v',
CREATE_SUBVOLUME_INHERIT_QUOTA = 'q',
CREATE_SUBVOLUME_NEW_QUOTA = 'Q',
CREATE_FIFO = 'p',
CREATE_SYMLINK = 'L',
CREATE_CHAR_DEVICE = 'c',
CREATE_BLOCK_DEVICE = 'b',
COPY_FILES = 'C',
/* These ones take globs */
WRITE_FILE = 'w',
EMPTY_DIRECTORY = 'e',
SET_XATTR = 't',
RECURSIVE_SET_XATTR = 'T',
SET_ACL = 'a',
RECURSIVE_SET_ACL = 'A',
SET_ATTRIBUTE = 'h',
RECURSIVE_SET_ATTRIBUTE = 'H',
IGNORE_PATH = 'x',
IGNORE_DIRECTORY_PATH = 'X',
REMOVE_PATH = 'r',
RECURSIVE_REMOVE_PATH = 'R',
RELABEL_PATH = 'z',
RECURSIVE_RELABEL_PATH = 'Z',
ADJUST_MODE = 'm', /* legacy, 'z' is identical to this */
} ItemType;
typedef enum AgeBy {
AGE_BY_ATIME = 1 << 0,
AGE_BY_BTIME = 1 << 1,
AGE_BY_CTIME = 1 << 2,
AGE_BY_MTIME = 1 << 3,
/* All file timestamp types are checked by default. */
AGE_BY_DEFAULT_FILE = AGE_BY_ATIME | AGE_BY_BTIME | AGE_BY_CTIME | AGE_BY_MTIME,
AGE_BY_DEFAULT_DIR = AGE_BY_ATIME | AGE_BY_BTIME | AGE_BY_MTIME
} AgeBy;
typedef struct Item {
ItemType type;
char *path;
char *argument;
char **xattrs;
#if HAVE_ACL
acl_t acl_access;
acl_t acl_default;
#endif
uid_t uid;
gid_t gid;
mode_t mode;
usec_t age;
AgeBy age_by_file, age_by_dir;
dev_t major_minor;
unsigned attribute_value;
unsigned attribute_mask;
bool uid_set:1;
bool gid_set:1;
bool mode_set:1;
bool age_set:1;
bool mask_perms:1;
bool attribute_set:1;
bool keep_first_level:1;
bool append_or_force:1;
bool allow_failure:1;
bool try_replace:1;
OperationMask done;
} Item;
typedef struct ItemArray {
Item *items;
size_t n_items;
struct ItemArray *parent;
Set *children;
} ItemArray;
typedef enum DirectoryType {
DIRECTORY_RUNTIME,
DIRECTORY_STATE,
DIRECTORY_CACHE,
DIRECTORY_LOGS,
_DIRECTORY_TYPE_MAX,
} DirectoryType;
static bool arg_cat_config = false;
static bool arg_user = false;
static OperationMask arg_operation = 0;
static bool arg_boot = false;
static PagerFlags arg_pager_flags = 0;
static char **arg_include_prefixes = NULL;
static char **arg_exclude_prefixes = NULL;
static char *arg_root = NULL;
static char *arg_image = NULL;
static char *arg_replace = NULL;
#define MAX_DEPTH 256
static OrderedHashmap *items = NULL, *globs = NULL;
static Set *unix_sockets = NULL;
STATIC_DESTRUCTOR_REGISTER(items, ordered_hashmap_freep);
STATIC_DESTRUCTOR_REGISTER(globs, ordered_hashmap_freep);
STATIC_DESTRUCTOR_REGISTER(unix_sockets, set_freep);
STATIC_DESTRUCTOR_REGISTER(arg_include_prefixes, freep);
STATIC_DESTRUCTOR_REGISTER(arg_exclude_prefixes, freep);
STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
STATIC_DESTRUCTOR_REGISTER(arg_image, freep);
static int specifier_machine_id_safe(char specifier, const void *data, const char *root, const void *userdata, char **ret);
static int specifier_directory(char specifier, const void *data, const char *root, const void *userdata, char **ret);
static const Specifier specifier_table[] = {
{ 'a', specifier_architecture, NULL },
{ 'b', specifier_boot_id, NULL },
{ 'B', specifier_os_build_id, NULL },
{ 'H', specifier_host_name, NULL },
{ 'l', specifier_short_host_name, NULL },
{ 'm', specifier_machine_id_safe, NULL },
{ 'o', specifier_os_id, NULL },
{ 'v', specifier_kernel_release, NULL },
{ 'w', specifier_os_version_id, NULL },
{ 'W', specifier_os_variant_id, NULL },
{ 'h', specifier_user_home, NULL },
{ 'C', specifier_directory, UINT_TO_PTR(DIRECTORY_CACHE) },
{ 'L', specifier_directory, UINT_TO_PTR(DIRECTORY_LOGS) },
{ 'S', specifier_directory, UINT_TO_PTR(DIRECTORY_STATE) },
{ 't', specifier_directory, UINT_TO_PTR(DIRECTORY_RUNTIME) },
COMMON_CREDS_SPECIFIERS,
COMMON_TMP_SPECIFIERS,
{}
};
static int specifier_machine_id_safe(char specifier, const void *data, const char *root, const void *userdata, char **ret) {
int r;
/* If /etc/machine_id is missing or empty (e.g. in a chroot environment) return a recognizable error
* so that the caller can skip the rule gracefully. */
r = specifier_machine_id(specifier, data, root, userdata, ret);
if (IN_SET(r, -ENOENT, -ENOMEDIUM))
return -ENXIO;
return r;
}
static int specifier_directory(char specifier, const void *data, const char *root, const 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" },
};
const struct table_entry *paths;
_cleanup_free_ char *p = NULL;
unsigned i;
int r;
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));
r = sd_path_lookup(paths[i].type, paths[i].suffix, &p);
if (r < 0)
return r;
if (arg_root) {
_cleanup_free_ char *j = NULL;
j = path_join(arg_root, p);
if (!j)
return -ENOMEM;
*ret = TAKE_PTR(j);
} else
*ret = TAKE_PTR(p);
return 0;
}
static int log_unresolvable_specifier(const char *filename, unsigned line) {
static bool notified = false;
/* 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_syntax(NULL,
notified ? LOG_DEBUG : LOG_NOTICE,
filename, line, 0,
"Failed to resolve specifier: %s, skipping",
arg_user ? "Required $XDG_... variable not defined" : "uninitialized /etc detected");
if (!notified)
log_notice("All rules containing unresolvable specifiers will be skipped.");
notified = true;
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 = TAKE_PTR(res);
return 0;
}
static bool needs_glob(ItemType t) {
return IN_SET(t,
WRITE_FILE,
IGNORE_PATH,
IGNORE_DIRECTORY_PATH,
REMOVE_PATH,
RECURSIVE_REMOVE_PATH,
EMPTY_DIRECTORY,
ADJUST_MODE,
RELABEL_PATH,
RECURSIVE_RELABEL_PATH,
SET_XATTR,
RECURSIVE_SET_XATTR,
SET_ACL,
RECURSIVE_SET_ACL,
SET_ATTRIBUTE,
RECURSIVE_SET_ATTRIBUTE);
}
static bool takes_ownership(ItemType t) {
return IN_SET(t,
CREATE_FILE,
TRUNCATE_FILE,
CREATE_DIRECTORY,
EMPTY_DIRECTORY,
TRUNCATE_DIRECTORY,
CREATE_SUBVOLUME,
CREATE_SUBVOLUME_INHERIT_QUOTA,
CREATE_SUBVOLUME_NEW_QUOTA,
CREATE_FIFO,
CREATE_SYMLINK,
CREATE_CHAR_DEVICE,
CREATE_BLOCK_DEVICE,
COPY_FILES,
WRITE_FILE,
IGNORE_PATH,
IGNORE_DIRECTORY_PATH,
REMOVE_PATH,
RECURSIVE_REMOVE_PATH);
}
static struct Item* find_glob(OrderedHashmap *h, const char *match) {
ItemArray *j;
ORDERED_HASHMAP_FOREACH(j, h) {
size_t n;
for (n = 0; n < j->n_items; n++) {
Item *item = j->items + n;
if (fnmatch(item->path, match, FNM_PATHNAME|FNM_PERIOD) == 0)
return item;
}
}
return NULL;
}
static int load_unix_sockets(void) {
_cleanup_set_free_ Set *sockets = NULL;
_cleanup_fclose_ FILE *f = NULL;
int r;
if (unix_sockets)
return 0;
/* We maintain a cache of the sockets we found in /proc/net/unix to speed things up a little. */
f = fopen("/proc/net/unix", "re");
if (!f)
return log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_WARNING, errno,
"Failed to open /proc/net/unix, ignoring: %m");
/* Skip header */
r = read_line(f, LONG_LINE_MAX, NULL);
if (r < 0)
return log_warning_errno(r, "Failed to skip /proc/net/unix header line: %m");
if (r == 0)
return log_warning_errno(SYNTHETIC_ERRNO(EIO), "Premature end of file reading /proc/net/unix.");
for (;;) {
_cleanup_free_ char *line = NULL;
char *p;
r = read_line(f, LONG_LINE_MAX, &line);
if (r < 0)
return log_warning_errno(r, "Failed to read /proc/net/unix line, ignoring: %m");
if (r == 0) /* EOF */
break;
p = strchr(line, ':');
if (!p)
continue;
if (strlen(p) < 37)
continue;
p += 37;
p += strspn(p, WHITESPACE);
p += strcspn(p, WHITESPACE); /* skip one more word */
p += strspn(p, WHITESPACE);
if (!path_is_absolute(p))
continue;
r = set_put_strdup_full(&sockets, &path_hash_ops_free, p);
if (r < 0)
return log_warning_errno(r, "Failed to add AF_UNIX socket to set, ignoring: %m");
}
unix_sockets = TAKE_PTR(sockets);
return 1;
}
static bool unix_socket_alive(const char *fn) {
assert(fn);
if (load_unix_sockets() < 0)
return true; /* We don't know, so assume yes */
return set_contains(unix_sockets, fn);
}
static DIR* xopendirat_nomod(int dirfd, const char *path) {
DIR *dir;
dir = xopendirat(dirfd, path, O_NOFOLLOW|O_NOATIME);
if (dir)
return dir;
log_debug_errno(errno, "Cannot open %sdirectory \"%s\": %m", dirfd == AT_FDCWD ? "" : "sub", path);
if (errno != EPERM)
return NULL;
dir = xopendirat(dirfd, path, O_NOFOLLOW);
if (!dir)
log_debug_errno(errno, "Cannot open %sdirectory \"%s\": %m", dirfd == AT_FDCWD ? "" : "sub", path);
return dir;
}
static DIR* opendir_nomod(const char *path) {
return xopendirat_nomod(AT_FDCWD, path);
}
static inline nsec_t load_statx_timestamp_nsec(const struct statx_timestamp *ts) {
assert(ts);
if (ts->tv_sec < 0)
return NSEC_INFINITY;
if ((nsec_t) ts->tv_sec >= (UINT64_MAX - ts->tv_nsec) / NSEC_PER_SEC)
return NSEC_INFINITY;
return ts->tv_sec * NSEC_PER_SEC + ts->tv_nsec;
}
static bool needs_cleanup(
nsec_t atime,
nsec_t btime,
nsec_t ctime,
nsec_t mtime,
nsec_t cutoff,
const char *sub_path,
AgeBy age_by,
bool is_dir) {
if (FLAGS_SET(age_by, AGE_BY_MTIME) && mtime != NSEC_INFINITY && mtime >= cutoff) {
/* Follows spelling in stat(1). */
log_debug("%s \"%s\": modify time %s is too new.",
is_dir ? "Directory" : "File",
sub_path,
FORMAT_TIMESTAMP_STYLE(mtime / NSEC_PER_USEC, TIMESTAMP_US));
return false;
}
if (FLAGS_SET(age_by, AGE_BY_ATIME) && atime != NSEC_INFINITY && atime >= cutoff) {
log_debug("%s \"%s\": access time %s is too new.",
is_dir ? "Directory" : "File",
sub_path,
FORMAT_TIMESTAMP_STYLE(atime / NSEC_PER_USEC, TIMESTAMP_US));
return false;
}
/*
* Note: Unless explicitly specified by the user, "ctime" is ignored
* by default for directories, because we change it when deleting.
*/
if (FLAGS_SET(age_by, AGE_BY_CTIME) && ctime != NSEC_INFINITY && ctime >= cutoff) {
log_debug("%s \"%s\": change time %s is too new.",
is_dir ? "Directory" : "File",
sub_path,
FORMAT_TIMESTAMP_STYLE(ctime / NSEC_PER_USEC, TIMESTAMP_US));
return false;
}
if (FLAGS_SET(age_by, AGE_BY_BTIME) && btime != NSEC_INFINITY && btime >= cutoff) {
log_debug("%s \"%s\": birth time %s is too new.",
is_dir ? "Directory" : "File",
sub_path,
FORMAT_TIMESTAMP_STYLE(btime / NSEC_PER_USEC, TIMESTAMP_US));
return false;
}
return true;
}
static int dir_cleanup(
Item *i,
const char *p,
DIR *d,
nsec_t self_atime_nsec,
nsec_t self_mtime_nsec,
nsec_t cutoff_nsec,
dev_t rootdev_major,
dev_t rootdev_minor,
bool mountpoint,
int maxdepth,
bool keep_this_level,
AgeBy age_by_file,
AgeBy age_by_dir) {
bool deleted = false;
int r = 0;
FOREACH_DIRENT_ALL(de, d, break) {
_cleanup_free_ char *sub_path = NULL;
nsec_t atime_nsec, mtime_nsec, ctime_nsec, btime_nsec;
if (dot_or_dot_dot(de->d_name))
continue;
/* If statx() is supported, use it. It's preferable over fstatat() since it tells us
* explicitly where we are looking at a mount point, for free as side information. Determining
* the same information without statx() is hard, see the complexity of path_is_mount_point(),
* and also much slower as it requires a number of syscalls instead of just one. Hence, when
* we have modern statx() we use it instead of fstat() and do proper mount point checks,
* while on older kernels's well do traditional st_dev based detection of mount points.
*
* Using statx() for detecting mount points also has the benfit that we handle weird file
* systems such as overlayfs better where each file is originating from a different
* st_dev. */
STRUCT_STATX_DEFINE(sx);
r = statx_fallback(
dirfd(d), de->d_name,
AT_SYMLINK_NOFOLLOW|AT_NO_AUTOMOUNT,
STATX_TYPE|STATX_MODE|STATX_UID|STATX_ATIME|STATX_MTIME|STATX_CTIME|STATX_BTIME,
&sx);
if (r == -ENOENT)
continue;
if (r < 0) {
/* FUSE, NFS mounts, SELinux might return EACCES */
r = log_full_errno(errno == EACCES ? LOG_DEBUG : LOG_ERR, errno,
"statx(%s/%s) failed: %m", p, de->d_name);
continue;
}
if (FLAGS_SET(sx.stx_attributes_mask, STATX_ATTR_MOUNT_ROOT)) {
/* Yay, we have the mount point API, use it */
if (FLAGS_SET(sx.stx_attributes, STATX_ATTR_MOUNT_ROOT)) {
log_debug("Ignoring \"%s/%s\": different mount points.", p, de->d_name);
continue;
}
} else {
/* So we might have statx() but the STATX_ATTR_MOUNT_ROOT flag is not supported, fall
* back to traditional stx_dev checking. */
if (sx.stx_dev_major != rootdev_major ||
sx.stx_dev_minor != rootdev_minor) {
log_debug("Ignoring \"%s/%s\": different filesystem.", p, de->d_name);
continue;
}
/* Try to detect bind mounts of the same filesystem instance; they do not differ in device
* major/minors. This type of query is not supported on all kernels or filesystem types
* though. */
if (S_ISDIR(sx.stx_mode)) {
int q;
q = fd_is_mount_point(dirfd(d), de->d_name, 0);
if (q < 0)
log_debug_errno(q, "Failed to determine whether \"%s/%s\" is a mount point, ignoring: %m", p, de->d_name);
else if (q > 0) {
log_debug("Ignoring \"%s/%s\": different mount of the same filesystem.", p, de->d_name);
continue;
}
}
}
atime_nsec = FLAGS_SET(sx.stx_mask, STATX_ATIME) ? load_statx_timestamp_nsec(&sx.stx_atime) : 0;
mtime_nsec = FLAGS_SET(sx.stx_mask, STATX_MTIME) ? load_statx_timestamp_nsec(&sx.stx_mtime) : 0;
ctime_nsec = FLAGS_SET(sx.stx_mask, STATX_CTIME) ? load_statx_timestamp_nsec(&sx.stx_ctime) : 0;
btime_nsec = FLAGS_SET(sx.stx_mask, STATX_BTIME) ? load_statx_timestamp_nsec(&sx.stx_btime) : 0;
sub_path = path_join(p, de->d_name);
if (!sub_path) {
r = log_oom();
goto finish;
}
/* Is there an item configured for this path? */
if (ordered_hashmap_get(items, sub_path)) {
log_debug("Ignoring \"%s\": a separate entry exists.", sub_path);
continue;
}
if (find_glob(globs, sub_path)) {
log_debug("Ignoring \"%s\": a separate glob exists.", sub_path);
continue;
}
if (S_ISDIR(sx.stx_mode)) {
_cleanup_closedir_ DIR *sub_dir = NULL;
if (mountpoint &&
streq(de->d_name, "lost+found") &&
sx.stx_uid == 0) {
log_debug("Ignoring directory \"%s\".", sub_path);
continue;
}
if (maxdepth <= 0)
log_warning("Reached max depth on \"%s\".", sub_path);
else {
int q;
sub_dir = xopendirat_nomod(dirfd(d), de->d_name);
if (!sub_dir) {
if (errno != ENOENT)
r = log_warning_errno(errno, "Opening directory \"%s\" failed, ignoring: %m", sub_path);
continue;
}
if (flock(dirfd(sub_dir), LOCK_EX|LOCK_NB) < 0) {
log_debug_errno(errno, "Couldn't acquire shared BSD lock on directory \"%s\", skipping: %m", p);
continue;
}
q = dir_cleanup(i,
sub_path, sub_dir,
atime_nsec, mtime_nsec, cutoff_nsec,
rootdev_major, rootdev_minor,
false, maxdepth-1, false,
age_by_file, age_by_dir);
if (q < 0)
r = q;
}
/* Note: if you are wondering why we don't support the sticky bit for excluding
* directories from cleaning like we do it for other file system objects: well, the
* sticky bit already has a meaning for directories, so we don't want to overload
* that. */
if (keep_this_level) {
log_debug("Keeping directory \"%s\".", sub_path);
continue;
}
/*
* Check the file timestamps of an entry against the
* given cutoff time; delete if it is older.
*/
if (!needs_cleanup(atime_nsec, btime_nsec, ctime_nsec, mtime_nsec,
cutoff_nsec, sub_path, age_by_dir, true))
continue;
log_debug("Removing directory \"%s\".", sub_path);
if (unlinkat(dirfd(d), de->d_name, AT_REMOVEDIR) < 0)
if (!IN_SET(errno, ENOENT, ENOTEMPTY))
r = log_warning_errno(errno, "Failed to remove directory \"%s\", ignoring: %m", sub_path);
} else {
/* Skip files for which the sticky bit is set. These are semantics we define, and are
* unknown elsewhere. See XDG_RUNTIME_DIR specification for details. */
if (sx.stx_mode & S_ISVTX) {
log_debug("Skipping \"%s\": sticky bit set.", sub_path);
continue;
}
if (mountpoint &&
S_ISREG(sx.stx_mode) &&
sx.stx_uid == 0 &&
STR_IN_SET(de->d_name,
".journal",
"aquota.user",
"aquota.group")) {
log_debug("Skipping \"%s\".", sub_path);
continue;
}
/* Ignore sockets that are listed in /proc/net/unix */
if (S_ISSOCK(sx.stx_mode) && unix_socket_alive(sub_path)) {
log_debug("Skipping \"%s\": live socket.", sub_path);
continue;
}
/* Ignore device nodes */
if (S_ISCHR(sx.stx_mode) || S_ISBLK(sx.stx_mode)) {
log_debug("Skipping \"%s\": a device.", sub_path);
continue;
}
/* Keep files on this level around if this is requested */
if (keep_this_level) {
log_debug("Keeping \"%s\".", sub_path);
continue;
}
if (!needs_cleanup(atime_nsec, btime_nsec, ctime_nsec, mtime_nsec,
cutoff_nsec, sub_path, age_by_file, false))
continue;
log_debug("Removing \"%s\".", sub_path);
if (unlinkat(dirfd(d), de->d_name, 0) < 0)
if (errno != ENOENT)
r = log_warning_errno(errno, "Failed to remove \"%s\", ignoring: %m", sub_path);
deleted = true;
}
}
finish:
if (deleted) {
struct timespec ts[2];
log_debug("Restoring access and modification time on \"%s\": %s, %s",
p,
FORMAT_TIMESTAMP_STYLE(self_atime_nsec / NSEC_PER_USEC, TIMESTAMP_US),
FORMAT_TIMESTAMP_STYLE(self_mtime_nsec / NSEC_PER_USEC, TIMESTAMP_US));
timespec_store_nsec(ts + 0, self_atime_nsec);
timespec_store_nsec(ts + 1, self_mtime_nsec);
/* Restore original directory timestamps */
if (futimens(dirfd(d), ts) < 0)
log_warning_errno(errno, "Failed to revert timestamps of '%s', ignoring: %m", p);
}
return r;
}
static bool dangerous_hardlinks(void) {
_cleanup_free_ char *value = NULL;
static int cached = -1;
int r;
/* Check whether the fs.protected_hardlinks sysctl is on. If we can't determine it we assume its off, as that's
* what the upstream default is. */
if (cached >= 0)
return cached;
r = read_one_line_file("/proc/sys/fs/protected_hardlinks", &value);
if (r < 0) {
log_debug_errno(r, "Failed to read fs.protected_hardlinks sysctl: %m");
return true;
}
r = parse_boolean(value);
if (r < 0) {
log_debug_errno(r, "Failed to parse fs.protected_hardlinks sysctl: %m");
return true;
}
cached = r == 0;
return cached;
}
static bool hardlink_vulnerable(const struct stat *st) {
assert(st);
return !S_ISDIR(st->st_mode) && st->st_nlink > 1 && dangerous_hardlinks();
}
static mode_t process_mask_perms(mode_t mode, mode_t current) {
if ((current & 0111) == 0)
mode &= ~0111;
if ((current & 0222) == 0)
mode &= ~0222;
if ((current & 0444) == 0)
mode &= ~0444;
if (!S_ISDIR(current))
mode &= ~07000; /* remove sticky/sgid/suid bit, unless directory */
return mode;
}
static int fd_set_perms(Item *i, int fd, const char *path, const struct stat *st) {
struct stat stbuf;
mode_t new_mode;
bool do_chown;
int r;
assert(i);
assert(fd >= 0);
assert(path);
if (!i->mode_set && !i->uid_set && !i->gid_set)
goto shortcut;
if (!st) {
if (fstat(fd, &stbuf) < 0)
return log_error_errno(errno, "fstat(%s) failed: %m", path);
st = &stbuf;
}
if (hardlink_vulnerable(st))
return log_error_errno(SYNTHETIC_ERRNO(EPERM),
"Refusing to set permissions on hardlinked file %s while the fs.protected_hardlinks sysctl is turned off.",
path);
/* Do we need a chown()? */
do_chown =
(i->uid_set && i->uid != st->st_uid) ||
(i->gid_set && i->gid != st->st_gid);
/* Calculate the mode to apply */
new_mode = i->mode_set ? (i->mask_perms ?
process_mask_perms(i->mode, st->st_mode) :
i->mode) :
(st->st_mode & 07777);
if (i->mode_set && do_chown) {
/* Before we issue the chmod() let's reduce the access mode to the common bits of the old and
* the new mode. That way there's no time window where the file exists under the old owner
* with more than the old access modes — and not under the new owner with more than the new
* access modes either. */
if (S_ISLNK(st->st_mode))
log_debug("Skipping temporary mode fix for symlink %s.", path);
else {
mode_t m = new_mode & st->st_mode; /* Mask new mode by old mode */
if (((m ^ st->st_mode) & 07777) == 0)
log_debug("\"%s\" matches temporary mode %o already.", path, m);
else {
log_debug("Temporarily changing \"%s\" to mode %o.", path, m);
r = fchmod_opath(fd, m);
if (r < 0)
return log_error_errno(r, "fchmod() of %s failed: %m", path);
}
}
}
if (do_chown) {
log_debug("Changing \"%s\" to owner "UID_FMT":"GID_FMT,
path,
i->uid_set ? i->uid : UID_INVALID,
i->gid_set ? i->gid : GID_INVALID);
if (fchownat(fd,
"",
i->uid_set ? i->uid : UID_INVALID,
i->gid_set ? i->gid : GID_INVALID,
AT_EMPTY_PATH) < 0)
return log_error_errno(errno, "fchownat() of %s failed: %m", path);
}
/* Now, apply the final mode. We do this in two cases: when the user set a mode explicitly, or after a
* chown(), since chown()'s mangle the access mode in regards to sgid/suid in some conditions. */
if (i->mode_set || do_chown) {
if (S_ISLNK(st->st_mode))
log_debug("Skipping mode fix for symlink %s.", path);
else {
/* Check if the chmod() is unnecessary. Note that if we did a chown() before we always
* chmod() here again, since it might have mangled the bits. */
if (!do_chown && ((new_mode ^ st->st_mode) & 07777) == 0)
log_debug("\"%s\" matches mode %o already.", path, new_mode);
else {
log_debug("Changing \"%s\" to mode %o.", path, new_mode);
r = fchmod_opath(fd, new_mode);
if (r < 0)
return log_error_errno(r, "fchmod() of %s failed: %m", path);
}
}
}
shortcut:
return label_fix(path, 0);
}
static int path_open_parent_safe(const char *path) {
_cleanup_free_ char *dn = NULL;
int r, fd;
if (path_equal(path, "/") || !path_is_normalized(path))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Failed to open parent of '%s': invalid path.",
path);
dn = dirname_malloc(path);
if (!dn)
return log_oom();
r = chase_symlinks(dn, arg_root, CHASE_SAFE|CHASE_WARN, NULL, &fd);
if (r < 0 && r != -ENOLINK)
return log_error_errno(r, "Failed to validate path %s: %m", path);
return r < 0 ? r : fd;
}
static int path_open_safe(const char *path) {
int r, fd;
/* path_open_safe() returns a file descriptor opened with O_PATH after
* verifying that the path doesn't contain unsafe transitions, except
* for its final component as the function does not follow symlink. */
assert(path);
if (!path_is_normalized(path))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Failed to open invalid path '%s'.",
path);
r = chase_symlinks(path, arg_root, CHASE_SAFE|CHASE_WARN|CHASE_NOFOLLOW, NULL, &fd);
if (r < 0 && r != -ENOLINK)
return log_error_errno(r, "Failed to validate path %s: %m", path);
return r < 0 ? r : fd;
}
static int path_set_perms(Item *i, const char *path) {
_cleanup_close_ int fd = -1;
assert(i);
assert(path);
fd = path_open_safe(path);
if (fd < 0)
return fd;
return fd_set_perms(i, fd, path, NULL);
}
static int parse_xattrs_from_arg(Item *i) {
const char *p;
int r;
assert(i);
assert(i->argument);
p = i->argument;
for (;;) {
_cleanup_free_ char *name = NULL, *value = NULL, *xattr = NULL;
r = extract_first_word(&p, &xattr, NULL, EXTRACT_UNQUOTE|EXTRACT_CUNESCAPE);
if (r < 0)
log_warning_errno(r, "Failed to parse extended attribute '%s', ignoring: %m", p);
if (r <= 0)
break;
r = split_pair(xattr, "=", &name, &value);
if (r < 0) {
log_warning_errno(r, "Failed to parse extended attribute, ignoring: %s", xattr);
continue;
}
if (isempty(name) || isempty(value)) {
log_warning("Malformed extended attribute found, ignoring: %s", xattr);
continue;
}
if (strv_push_pair(&i->xattrs, name, value) < 0)
return log_oom();
name = value = NULL;
}
return 0;
}
static int fd_set_xattrs(Item *i, int fd, const char *path, const struct stat *st) {
char **name, **value;
assert(i);
assert(fd >= 0);
assert(path);
STRV_FOREACH_PAIR(name, value, i->xattrs) {
log_debug("Setting extended attribute '%s=%s' on %s.", *name, *value, path);
if (setxattr(FORMAT_PROC_FD_PATH(fd), *name, *value, strlen(*value), 0) < 0)
return log_error_errno(errno, "Setting extended attribute %s=%s on %s failed: %m",
*name, *value, path);
}
return 0;
}
static int path_set_xattrs(Item *i, const char *path) {
_cleanup_close_ int fd = -1;
assert(i);
assert(path);
fd = path_open_safe(path);
if (fd < 0)
return fd;
return fd_set_xattrs(i, fd, path, NULL);
}
static int parse_acls_from_arg(Item *item) {
#if HAVE_ACL
int r;
assert(item);
/* If append_or_force (= modify) is set, we will not modify the acl
* afterwards, so the mask can be added now if necessary. */
r = parse_acl(item->argument, &item->acl_access, &item->acl_default, !item->append_or_force);
if (r < 0)
log_warning_errno(r, "Failed to parse ACL \"%s\": %m. Ignoring", item->argument);
#else
log_warning("ACLs are not supported. Ignoring.");
#endif
return 0;
}
#if HAVE_ACL
static int path_set_acl(const char *path, const char *pretty, acl_type_t type, acl_t acl, bool modify) {
_cleanup_(acl_free_charpp) char *t = NULL;
_cleanup_(acl_freep) acl_t dup = NULL;
int r;
/* Returns 0 for success, positive error if already warned,
* negative error otherwise. */
if (modify) {
r = acls_for_file(path, type, acl, &dup);
if (r < 0)
return r;
r = calc_acl_mask_if_needed(&dup);
if (r < 0)
return r;
} else {
dup = acl_dup(acl);
if (!dup)
return -errno;
/* the mask was already added earlier if needed */
}
r = add_base_acls_if_needed(&dup, path);
if (r < 0)
return r;
t = acl_to_any_text(dup, NULL, ',', TEXT_ABBREVIATE);
log_debug("Setting %s ACL %s on %s.",
type == ACL_TYPE_ACCESS ? "access" : "default",
strna(t), pretty);
r = acl_set_file(path, type, dup);
if (r < 0) {
if (ERRNO_IS_NOT_SUPPORTED(errno))
/* No error if filesystem doesn't support ACLs. Return negative. */
return -errno;
else
/* Return positive to indicate we already warned */
return -log_error_errno(errno,
"Setting %s ACL \"%s\" on %s failed: %m",
type == ACL_TYPE_ACCESS ? "access" : "default",
strna(t), pretty);
}
return 0;
}
#endif
static int fd_set_acls(Item *item, int fd, const char *path, const struct stat *st) {
int r = 0;
#if HAVE_ACL
struct stat stbuf;
assert(item);
assert(fd >= 0);
assert(path);
if (!st) {
if (fstat(fd, &stbuf) < 0)
return log_error_errno(errno, "fstat(%s) failed: %m", path);
st = &stbuf;
}
if (hardlink_vulnerable(st))
return log_error_errno(SYNTHETIC_ERRNO(EPERM),
"Refusing to set ACLs on hardlinked file %s while the fs.protected_hardlinks sysctl is turned off.",
path);
if (S_ISLNK(st->st_mode)) {
log_debug("Skipping ACL fix for symlink %s.", path);
return 0;
}
if (item->acl_access)
r = path_set_acl(FORMAT_PROC_FD_PATH(fd), path, ACL_TYPE_ACCESS, item->acl_access, item->append_or_force);
/* set only default acls to folders */
if (r == 0 && item->acl_default && S_ISDIR(st->st_mode))
r = path_set_acl(FORMAT_PROC_FD_PATH(fd), path, ACL_TYPE_DEFAULT, item->acl_default, item->append_or_force);
if (ERRNO_IS_NOT_SUPPORTED(r)) {
log_debug_errno(r, "ACLs not supported by file system at %s", path);
return 0;
}
if (r > 0)
return -r; /* already warned */
/* The above procfs paths don't work if /proc is not mounted. */
if (r == -ENOENT && proc_mounted() == 0)
r = -ENOSYS;
if (r < 0)
return log_error_errno(r, "ACL operation on \"%s\" failed: %m", path);
#endif
return r;
}
static int path_set_acls(Item *item, const char *path) {
int r = 0;
#if HAVE_ACL
_cleanup_close_ int fd = -1;
assert(item);
assert(path);
fd = path_open_safe(path);
if (fd < 0)
return fd;
r = fd_set_acls(item, fd, path, NULL);
#endif
return r;
}
static int parse_attribute_from_arg(Item *item) {
static const struct {
char character;
unsigned value;
} attributes[] = {
{ 'A', FS_NOATIME_FL }, /* do not update atime */
{ 'S', FS_SYNC_FL }, /* Synchronous updates */
{ 'D', FS_DIRSYNC_FL }, /* dirsync behaviour (directories only) */
{ 'a', FS_APPEND_FL }, /* writes to file may only append */
{ 'c', FS_COMPR_FL }, /* Compress file */
{ 'd', FS_NODUMP_FL }, /* do not dump file */
{ 'e', FS_EXTENT_FL }, /* Extents */
{ 'i', FS_IMMUTABLE_FL }, /* Immutable file */
{ 'j', FS_JOURNAL_DATA_FL }, /* Reserved for ext3 */
{ 's', FS_SECRM_FL }, /* Secure deletion */
{ 'u', FS_UNRM_FL }, /* Undelete */
{ 't', FS_NOTAIL_FL }, /* file tail should not be merged */
{ 'T', FS_TOPDIR_FL }, /* Top of directory hierarchies */
{ 'C', FS_NOCOW_FL }, /* Do not cow file */
{ 'P', FS_PROJINHERIT_FL }, /* Inherit the quota project ID */
};
enum {
MODE_ADD,
MODE_DEL,
MODE_SET
} mode = MODE_ADD;
unsigned value = 0, mask = 0;
const char *p;
assert(item);
p = item->argument;
if (p) {
if (*p == '+') {
mode = MODE_ADD;
p++;
} else if (*p == '-') {
mode = MODE_DEL;
p++;
} else if (*p == '=') {
mode = MODE_SET;
p++;
}
}
if (isempty(p) && mode != MODE_SET)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Setting file attribute on '%s' needs an attribute specification.",
item->path);
for (; p && *p ; p++) {
unsigned i, v;
for (i = 0; i < ELEMENTSOF(attributes); i++)
if (*p == attributes[i].character)
break;
if (i >= ELEMENTSOF(attributes))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Unknown file attribute '%c' on '%s'.",
*p, item->path);
v = attributes[i].value;
SET_FLAG(value, v, IN_SET(mode, MODE_ADD, MODE_SET));
mask |= v;
}
if (mode == MODE_SET)
mask |= CHATTR_ALL_FL;
assert(mask != 0);
item->attribute_mask = mask;
item->attribute_value = value;
item->attribute_set = true;
return 0;
}
static int fd_set_attribute(Item *item, int fd, const char *path, const struct stat *st) {
_cleanup_close_ int procfs_fd = -1;
struct stat stbuf;
unsigned f;
int r;
assert(item);
assert(fd >= 0);
assert(path);
if (!item->attribute_set || item->attribute_mask == 0)
return 0;
if (!st) {
if (fstat(fd, &stbuf) < 0)
return log_error_errno(errno, "fstat(%s) failed: %m", path);
st = &stbuf;
}
/* Issuing the file attribute ioctls on device nodes is not
* safe, as that will be delivered to the drivers, not the
* file system containing the device node. */
if (!S_ISREG(st->st_mode) && !S_ISDIR(st->st_mode))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Setting file flags is only supported on regular files and directories, cannot set on '%s'.",
path);
f = item->attribute_value & item->attribute_mask;
/* Mask away directory-specific flags */
if (!S_ISDIR(st->st_mode))
f &= ~FS_DIRSYNC_FL;
procfs_fd = fd_reopen(fd, O_RDONLY|O_CLOEXEC|O_NOATIME);
if (procfs_fd < 0)
return log_error_errno(procfs_fd, "Failed to re-open '%s': %m", path);
unsigned previous, current;
r = chattr_full(NULL, procfs_fd, f, item->attribute_mask, &previous, &current, CHATTR_FALLBACK_BITWISE);
if (r == -ENOANO)
log_warning("Cannot set file attributes for '%s', maybe due to incompatibility in specified attributes, "
"previous=0x%08x, current=0x%08x, expected=0x%08x, ignoring.",
path, previous, current, (previous & ~item->attribute_mask) | (f & item->attribute_mask));
else if (r < 0)
log_full_errno(ERRNO_IS_NOT_SUPPORTED(r) ? LOG_DEBUG : LOG_WARNING, r,
"Cannot set file attributes for '%s', value=0x%08x, mask=0x%08x, ignoring: %m",
path, item->attribute_value, item->attribute_mask);
return 0;
}
static int path_set_attribute(Item *item, const char *path) {
_cleanup_close_ int fd = -1;
if (!item->attribute_set || item->attribute_mask == 0)
return 0;
fd = path_open_safe(path);
if (fd < 0)
return fd;
return fd_set_attribute(item, fd, path, NULL);
}
static int write_one_file(Item *i, const char *path) {
_cleanup_close_ int fd = -1, dir_fd = -1;
char *bn;
int r;
assert(i);
assert(path);
assert(i->argument);
assert(i->type == WRITE_FILE);
/* Validate the path and keep the fd on the directory for opening the
* file so we're sure that it can't be changed behind our back. */
dir_fd = path_open_parent_safe(path);
if (dir_fd < 0)
return dir_fd;
bn = basename(path);
/* Follows symlinks */
fd = openat(dir_fd, bn,
O_NONBLOCK|O_CLOEXEC|O_WRONLY|O_NOCTTY|(i->append_or_force ? O_APPEND : 0),
i->mode);
if (fd < 0) {
if (errno == ENOENT) {
log_debug_errno(errno, "Not writing missing file \"%s\": %m", path);
return 0;
}
if (i->allow_failure)
return log_debug_errno(errno, "Failed to open file \"%s\", ignoring: %m", path);
return log_error_errno(errno, "Failed to open file \"%s\": %m", path);
}
/* 'w' is allowed to write into any kind of files. */
log_debug("Writing to \"%s\".", path);
r = loop_write(fd, i->argument, strlen(i->argument), false);
if (r < 0)
return log_error_errno(r, "Failed to write file \"%s\": %m", path);
return fd_set_perms(i, fd, path, NULL);
}
static int create_file(Item *i, const char *path) {
_cleanup_close_ int fd = -1, dir_fd = -1;
struct stat stbuf, *st = NULL;
int r = 0;
char *bn;
assert(i);
assert(path);
assert(i->type == CREATE_FILE);
/* 'f' operates on regular files exclusively. */
/* Validate the path and keep the fd on the directory for opening the
* file so we're sure that it can't be changed behind our back. */
dir_fd = path_open_parent_safe(path);
if (dir_fd < 0)
return dir_fd;
bn = basename(path);
RUN_WITH_UMASK(0000) {
mac_selinux_create_file_prepare(path, S_IFREG);
fd = openat(dir_fd, bn, O_CREAT|O_EXCL|O_NOFOLLOW|O_NONBLOCK|O_CLOEXEC|O_WRONLY|O_NOCTTY, i->mode);
mac_selinux_create_file_clear();
}
if (fd < 0) {
/* Even on a read-only filesystem, open(2) returns EEXIST if the
* file already exists. It returns EROFS only if it needs to
* create the file. */
if (errno != EEXIST)
return log_error_errno(errno, "Failed to create file %s: %m", path);
/* Re-open the file. At that point it must exist since open(2)
* failed with EEXIST. We still need to check if the perms/mode
* need to be changed. For read-only filesystems, we let
* fd_set_perms() report the error if the perms need to be
* modified. */
fd = openat(dir_fd, bn, O_NOFOLLOW|O_CLOEXEC|O_PATH, i->mode);
if (fd < 0)
return log_error_errno(errno, "Failed to re-open file %s: %m", path);
if (fstat(fd, &stbuf) < 0)
return log_error_errno(errno, "stat(%s) failed: %m", path);
if (!S_ISREG(stbuf.st_mode))
return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
"%s exists and is not a regular file.",
path);
st = &stbuf;
} else {
log_debug("\"%s\" has been created.", path);
if (i->argument) {
log_debug("Writing to \"%s\".", path);
r = loop_write(fd, i->argument, strlen(i->argument), false);
if (r < 0)
return log_error_errno(r, "Failed to write file \"%s\": %m", path);
}
}
return fd_set_perms(i, fd, path, st);
}
static int truncate_file(Item *i, const char *path) {
_cleanup_close_ int fd = -1, dir_fd = -1;
struct stat stbuf, *st = NULL;
bool erofs = false;
int r = 0;
char *bn;
assert(i);
assert(path);
assert(i->type == TRUNCATE_FILE || (i->type == CREATE_FILE && i->append_or_force));
/* We want to operate on regular file exclusively especially since
* O_TRUNC is unspecified if the file is neither a regular file nor a
* fifo nor a terminal device. Therefore we first open the file and make
* sure it's a regular one before truncating it. */
/* Validate the path and keep the fd on the directory for opening the
* file so we're sure that it can't be changed behind our back. */
dir_fd = path_open_parent_safe(path);
if (dir_fd < 0)
return dir_fd;
bn = basename(path);
RUN_WITH_UMASK(0000) {
mac_selinux_create_file_prepare(path, S_IFREG);
fd = openat(dir_fd, bn, O_CREAT|O_NOFOLLOW|O_NONBLOCK|O_CLOEXEC|O_WRONLY|O_NOCTTY, i->mode);
mac_selinux_create_file_clear();
}
if (fd < 0) {
if (errno != EROFS)
return log_error_errno(errno, "Failed to open/create file %s: %m", path);
/* On a read-only filesystem, we don't want to fail if the
* target is already empty and the perms are set. So we still
* proceed with the sanity checks and let the remaining
* operations fail with EROFS if they try to modify the target
* file. */
fd = openat(dir_fd, bn, O_NOFOLLOW|O_CLOEXEC|O_PATH, i->mode);
if (fd < 0) {
if (errno == ENOENT)
return log_error_errno(SYNTHETIC_ERRNO(EROFS),
"Cannot create file %s on a read-only file system.",
path);
return log_error_errno(errno, "Failed to re-open file %s: %m", path);
}
erofs = true;
}
if (fstat(fd, &stbuf) < 0)
return log_error_errno(errno, "stat(%s) failed: %m", path);
if (!S_ISREG(stbuf.st_mode))
return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
"%s exists and is not a regular file.",
path);
if (stbuf.st_size > 0) {
if (ftruncate(fd, 0) < 0) {
r = erofs ? -EROFS : -errno;
return log_error_errno(r, "Failed to truncate file %s: %m", path);
}
} else
st = &stbuf;
log_debug("\"%s\" has been created.", path);
if (i->argument) {
log_debug("Writing to \"%s\".", path);
r = loop_write(fd, i->argument, strlen(i->argument), false);
if (r < 0) {
r = erofs ? -EROFS : r;
return log_error_errno(r, "Failed to write file %s: %m", path);
}
}
return fd_set_perms(i, fd, path, st);
}
static int copy_files(Item *i) {
_cleanup_close_ int dfd = -1, fd = -1;
char *bn;
int r;
log_debug("Copying tree \"%s\" to \"%s\".", i->argument, i->path);
bn = basename(i->path);
/* Validate the path and use the returned directory fd for copying the
* target so we're sure that the path can't be changed behind our
* back. */
dfd = path_open_parent_safe(i->path);
if (dfd < 0)
return dfd;
r = copy_tree_at(AT_FDCWD, i->argument,
dfd, bn,
i->uid_set ? i->uid : UID_INVALID,
i->gid_set ? i->gid : GID_INVALID,
COPY_REFLINK | COPY_MERGE_EMPTY | COPY_MAC_CREATE | COPY_HARDLINKS);
if (r < 0) {
struct stat a, b;
/* If the target already exists on read-only filesystems, trying
* to create the target will not fail with EEXIST but with
* EROFS. */
if (r == -EROFS && faccessat(dfd, bn, F_OK, AT_SYMLINK_NOFOLLOW) == 0)
r = -EEXIST;
if (r != -EEXIST)
return log_error_errno(r, "Failed to copy files to %s: %m", i->path);
if (stat(i->argument, &a) < 0)
return log_error_errno(errno, "stat(%s) failed: %m", i->argument);
if (fstatat(dfd, bn, &b, AT_SYMLINK_NOFOLLOW) < 0)
return log_error_errno(errno, "stat(%s) failed: %m", i->path);
if ((a.st_mode ^ b.st_mode) & S_IFMT) {
log_debug("Can't copy to %s, file exists already and is of different type", i->path);
return 0;
}
}
fd = openat(dfd, bn, O_NOFOLLOW|O_CLOEXEC|O_PATH);
if (fd < 0)
return log_error_errno(errno, "Failed to openat(%s): %m", i->path);
return fd_set_perms(i, fd, i->path, NULL);
}
typedef enum {
CREATION_NORMAL,
CREATION_EXISTING,
CREATION_FORCE,
_CREATION_MODE_MAX,
_CREATION_MODE_INVALID = -EINVAL,
} CreationMode;
static const char *const creation_mode_verb_table[_CREATION_MODE_MAX] = {
[CREATION_NORMAL] = "Created",
[CREATION_EXISTING] = "Found existing",
[CREATION_FORCE] = "Created replacement",
};
DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(creation_mode_verb, CreationMode);
static int create_directory_or_subvolume(const char *path, mode_t mode, bool subvol, CreationMode *creation) {
_cleanup_close_ int pfd = -1;
CreationMode c;
int r;
assert(path);
if (!creation)
creation = &c;
pfd = path_open_parent_safe(path);
if (pfd < 0)
return pfd;
if (subvol) {
r = getenv_bool("SYSTEMD_TMPFILES_FORCE_SUBVOL");
if (r < 0) {
if (r != -ENXIO) /* env var is unset */
log_warning_errno(r, "Cannot parse value of $SYSTEMD_TMPFILES_FORCE_SUBVOL, ignoring.");
r = btrfs_is_subvol(empty_to_root(arg_root)) > 0;
}
if (!r)
/* Don't create a subvolume unless the root directory is
* one, too. We do this under the assumption that if the
* root directory is just a plain directory (i.e. very
* light-weight), we shouldn't try to split it up into
* subvolumes (i.e. more heavy-weight). Thus, chroot()
* environments and suchlike will get a full brtfs
* subvolume set up below their tree only if they
* specifically set up a btrfs subvolume for the root
* dir too. */
subvol = false;
else {
RUN_WITH_UMASK((~mode) & 0777)
r = btrfs_subvol_make_fd(pfd, basename(path));
}
} else
r = 0;
if (!subvol || r == -ENOTTY)
RUN_WITH_UMASK(0000)
r = mkdirat_label(pfd, basename(path), mode);
if (r < 0) {
int k;
if (!IN_SET(r, -EEXIST, -EROFS))
return log_error_errno(r, "Failed to create directory or subvolume \"%s\": %m", path);
k = is_dir_fd(pfd);
if (k == -ENOENT && r == -EROFS)
return log_error_errno(r, "%s does not exist and cannot be created as the file system is read-only.", path);
if (k < 0)
return log_error_errno(k, "Failed to check if %s exists: %m", path);
if (!k)
return log_warning_errno(SYNTHETIC_ERRNO(EEXIST),
"\"%s\" already exists and is not a directory.", path);
*creation = CREATION_EXISTING;
} else
*creation = CREATION_NORMAL;
log_debug("%s directory \"%s\".", creation_mode_verb_to_string(*creation), path);
r = openat(pfd, basename(path), O_NOCTTY|O_CLOEXEC|O_DIRECTORY);
if (r < 0)
return log_error_errno(errno, "Failed to open directory '%s': %m", basename(path));
return r;
}
static int create_directory(Item *i, const char *path) {
_cleanup_close_ int fd = -1;
assert(i);
assert(IN_SET(i->type, CREATE_DIRECTORY, TRUNCATE_DIRECTORY));
fd = create_directory_or_subvolume(path, i->mode, false, NULL);
if (fd == -EEXIST)
return 0;
if (fd < 0)
return fd;
return fd_set_perms(i, fd, path, NULL);
}
static int create_subvolume(Item *i, const char *path) {
_cleanup_close_ int fd = -1;
CreationMode creation;
int r, q = 0;
assert(i);
assert(IN_SET(i->type, CREATE_SUBVOLUME, CREATE_SUBVOLUME_NEW_QUOTA, CREATE_SUBVOLUME_INHERIT_QUOTA));
fd = create_directory_or_subvolume(path, i->mode, true, &creation);
if (fd == -EEXIST)
return 0;
if (fd < 0)
return fd;
if (creation == CREATION_NORMAL &&
IN_SET(i->type, CREATE_SUBVOLUME_NEW_QUOTA, CREATE_SUBVOLUME_INHERIT_QUOTA)) {
r = btrfs_subvol_auto_qgroup_fd(fd, 0, i->type == CREATE_SUBVOLUME_NEW_QUOTA);
if (r == -ENOTTY)
log_debug_errno(r, "Couldn't adjust quota for subvolume \"%s\" (unsupported fs or dir not a subvolume): %m", i->path);
else if (r == -EROFS)
log_debug_errno(r, "Couldn't adjust quota for subvolume \"%s\" (fs is read-only).", i->path);
else if (r == -ENOTCONN)
log_debug_errno(r, "Couldn't adjust quota for subvolume \"%s\" (quota support is disabled).", i->path);
else if (r < 0)
q = log_error_errno(r, "Failed to adjust quota for subvolume \"%s\": %m", i->path);
else if (r > 0)
log_debug("Adjusted quota for subvolume \"%s\".", i->path);
else if (r == 0)
log_debug("Quota for subvolume \"%s\" already in place, no change made.", i->path);
}
r = fd_set_perms(i, fd, path, NULL);
if (q < 0) /* prefer the quota change error from above */
return q;
return r;
}
static int empty_directory(Item *i, const char *path) {
int r;
assert(i);
assert(i->type == EMPTY_DIRECTORY);
r = is_dir(path, false);
if (r == -ENOENT) {
/* Option "e" operates only on existing objects. Do not
* print errors about non-existent files or directories */
log_debug("Skipping missing directory: %s", path);
return 0;
}
if (r < 0)
return log_error_errno(r, "is_dir() failed on path %s: %m", path);
if (r == 0) {
log_warning("\"%s\" already exists and is not a directory.", path);
return 0;
}
return path_set_perms(i, path);
}
static int create_device(Item *i, mode_t file_type) {
_cleanup_close_ int dfd = -1, fd = -1;
CreationMode creation;
char *bn;
int r;
assert(i);
assert(IN_SET(file_type, S_IFBLK, S_IFCHR));
bn = basename(i->path);
/* Validate the path and use the returned directory fd for copying the
* target so we're sure that the path can't be changed behind our
* back. */
dfd = path_open_parent_safe(i->path);
if (dfd < 0)
return dfd;
RUN_WITH_UMASK(0000) {
mac_selinux_create_file_prepare(i->path, file_type);
r = mknodat(dfd, bn, i->mode | file_type, i->major_minor);
mac_selinux_create_file_clear();
}
if (r < 0) {
struct stat st;
if (errno == EPERM) {
log_debug("We lack permissions, possibly because of cgroup configuration; "
"skipping creation of device node %s.", i->path);
return 0;
}
if (errno != EEXIST)
return log_error_errno(errno, "Failed to create device node %s: %m", i->path);
if (fstatat(dfd, bn, &st, 0) < 0)
return log_error_errno(errno, "stat(%s) failed: %m", i->path);
if ((st.st_mode & S_IFMT) != file_type) {
if (i->append_or_force) {
RUN_WITH_UMASK(0000) {
mac_selinux_create_file_prepare(i->path, file_type);
/* FIXME: need to introduce mknodat_atomic() */
r = mknod_atomic(i->path, i->mode | file_type, i->major_minor);
mac_selinux_create_file_clear();
}
if (r < 0)
return log_error_errno(r, "Failed to create device node \"%s\": %m", i->path);
creation = CREATION_FORCE;
} else {
log_warning("\"%s\" already exists is not a device node.", i->path);
return 0;
}
} else
creation = CREATION_EXISTING;
} else
creation = CREATION_NORMAL;
log_debug("%s %s device node \"%s\" %u:%u.",
creation_mode_verb_to_string(creation),
i->type == CREATE_BLOCK_DEVICE ? "block" : "char",
i->path, major(i->mode), minor(i->mode));
fd = openat(dfd, bn, O_NOFOLLOW|O_CLOEXEC|O_PATH);
if (fd < 0)
return log_error_errno(errno, "Failed to openat(%s): %m", i->path);
return fd_set_perms(i, fd, i->path, NULL);
}
static int create_fifo(Item *i, const char *path) {
_cleanup_close_ int pfd = -1, fd = -1;
CreationMode creation;
struct stat st;
char *bn;
int r;
pfd = path_open_parent_safe(path);
if (pfd < 0)
return pfd;
bn = basename(path);
RUN_WITH_UMASK(0000) {
mac_selinux_create_file_prepare(path, S_IFIFO);
r = mkfifoat(pfd, bn, i->mode);
mac_selinux_create_file_clear();
}
if (r < 0) {
if (errno != EEXIST)
return log_error_errno(errno, "Failed to create fifo %s: %m", path);
if (fstatat(pfd, bn, &st, AT_SYMLINK_NOFOLLOW) < 0)
return log_error_errno(errno, "stat(%s) failed: %m", path);
if (!S_ISFIFO(st.st_mode)) {
if (i->append_or_force) {
RUN_WITH_UMASK(0000) {
mac_selinux_create_file_prepare(path, S_IFIFO);
r = mkfifoat_atomic(pfd, bn, i->mode);
mac_selinux_create_file_clear();
}
if (r < 0)
return log_error_errno(r, "Failed to create fifo %s: %m", path);
creation = CREATION_FORCE;
} else {
log_warning("\"%s\" already exists and is not a fifo.", path);
return 0;
}
} else
creation = CREATION_EXISTING;
} else
creation = CREATION_NORMAL;
log_debug("%s fifo \"%s\".", creation_mode_verb_to_string(creation), path);
fd = openat(pfd, bn, O_NOFOLLOW|O_CLOEXEC|O_PATH);
if (fd < 0)
return log_error_errno(errno, "Failed to openat(%s): %m", path);
return fd_set_perms(i, fd, i->path, NULL);
}
typedef int (*action_t)(Item *i, const char *path);
typedef int (*fdaction_t)(Item *i, int fd, const char *path, const struct stat *st);
static int item_do(Item *i, int fd, const char *path, fdaction_t action) {
struct stat st;
int r = 0, q;
assert(i);
assert(path);
assert(fd >= 0);
if (fstat(fd, &st) < 0) {
r = log_error_errno(errno, "fstat() on file failed: %m");
goto finish;
}
/* This returns the first error we run into, but nevertheless
* tries to go on */
r = action(i, fd, path, &st);
if (S_ISDIR(st.st_mode)) {
_cleanup_closedir_ DIR *d = NULL;
/* The passed 'fd' was opened with O_PATH. We need to convert it into a 'regular' fd before
* reading the directory content. */
d = opendir(FORMAT_PROC_FD_PATH(fd));
if (!d) {
log_error_errno(errno, "Failed to opendir() '%s': %m", FORMAT_PROC_FD_PATH(fd));
if (r == 0)
r = -errno;
goto finish;
}
FOREACH_DIRENT_ALL(de, d, q = -errno; goto finish) {
int de_fd;
if (dot_or_dot_dot(de->d_name))
continue;
de_fd = openat(fd, de->d_name, O_NOFOLLOW|O_CLOEXEC|O_PATH);
if (de_fd < 0)
q = log_error_errno(errno, "Failed to open() file '%s': %m", de->d_name);
else {
_cleanup_free_ char *de_path = NULL;
de_path = path_join(path, de->d_name);
if (!de_path)
q = log_oom();
else
/* Pass ownership of dirent fd over */
q = item_do(i, de_fd, de_path, action);
}
if (q < 0 && r == 0)
r = q;
}
}
finish:
safe_close(fd);
return r;
}
static int glob_item(Item *i, action_t action) {
_cleanup_globfree_ glob_t g = {
.gl_opendir = (void *(*)(const char *)) opendir_nomod,
};
int r = 0, k;
char **fn;
k = safe_glob(i->path, GLOB_NOSORT|GLOB_BRACE, &g);
if (k < 0 && k != -ENOENT)
return log_error_errno(k, "glob(%s) failed: %m", i->path);
STRV_FOREACH(fn, g.gl_pathv) {
k = action(i, *fn);
if (k < 0 && r == 0)
r = k;
}
return r;
}
static int glob_item_recursively(Item *i, fdaction_t action) {
_cleanup_globfree_ glob_t g = {
.gl_opendir = (void *(*)(const char *)) opendir_nomod,
};
int r = 0, k;
char **fn;
k = safe_glob(i->path, GLOB_NOSORT|GLOB_BRACE, &g);
if (k < 0 && k != -ENOENT)
return log_error_errno(k, "glob(%s) failed: %m", i->path);
STRV_FOREACH(fn, g.gl_pathv) {
_cleanup_close_ int fd = -1;
/* Make sure we won't trigger/follow file object (such as
* device nodes, automounts, ...) pointed out by 'fn' with
* O_PATH. Note, when O_PATH is used, flags other than
* O_CLOEXEC, O_DIRECTORY, and O_NOFOLLOW are ignored. */
fd = open(*fn, O_CLOEXEC|O_NOFOLLOW|O_PATH);
if (fd < 0) {
log_error_errno(errno, "Opening '%s' failed: %m", *fn);
if (r == 0)
r = -errno;
continue;
}
k = item_do(i, fd, *fn, action);
if (k < 0 && r == 0)
r = k;
/* we passed fd ownership to the previous call */
fd = -1;
}
return r;
}
static int rm_if_wrong_type_safe(
mode_t mode,
int parent_fd,
const struct stat *parent_st, /* Only used if follow_links below is true. */
const char *name,
int flags) {
_cleanup_free_ char *parent_name = NULL;
bool follow_links = !FLAGS_SET(flags, AT_SYMLINK_NOFOLLOW);
struct stat st;
int r;
assert(name);
assert((mode & ~S_IFMT) == 0);
assert(!follow_links || parent_st);
assert((flags & ~AT_SYMLINK_NOFOLLOW) == 0);
if (!filename_is_valid(name))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "\"%s\" is not a valid filename.", name);
r = fstatat_harder(parent_fd, name, &st, flags, REMOVE_CHMOD | REMOVE_CHMOD_RESTORE);
if (r < 0) {
(void) fd_get_path(parent_fd, &parent_name);
return log_full_errno(r == -ENOENT? LOG_DEBUG : LOG_ERR, r,
"Failed to stat \"%s\" at \"%s\": %m", name, strna(parent_name));
}
/* Fail before removing anything if this is an unsafe transition. */
if (follow_links && unsafe_transition(parent_st, &st)) {
(void) fd_get_path(parent_fd, &parent_name);
return log_error_errno(SYNTHETIC_ERRNO(ENOLINK),
"Unsafe transition from \"%s\" to \"%s\".", parent_name, name);
}
if ((st.st_mode & S_IFMT) == mode)
return 0;
(void) fd_get_path(parent_fd, &parent_name);
log_notice("Wrong file type 0x%x; rm -rf \"%s/%s\"", st.st_mode & S_IFMT, strna(parent_name), name);
/* If the target of the symlink was the wrong type, the link needs to be removed instead of the
* target, so make sure it is identified as a link and not a directory. */
if (follow_links) {
r = fstatat_harder(parent_fd, name, &st, AT_SYMLINK_NOFOLLOW, REMOVE_CHMOD | REMOVE_CHMOD_RESTORE);
if (r < 0)
return log_error_errno(r, "Failed to stat \"%s\" at \"%s\": %m", name, strna(parent_name));
}
/* Do not remove mount points. */
r = fd_is_mount_point(parent_fd, name, follow_links ? AT_SYMLINK_FOLLOW : 0);
if (r < 0)
(void) log_warning_errno(r, "Failed to check if \"%s/%s\" is a mount point: %m; Continuing",
strna(parent_name), name);
else if (r > 0)
return log_error_errno(SYNTHETIC_ERRNO(EBUSY),
"Not removing \"%s/%s\" because it is a mount point.", strna(parent_name), name);
if ((st.st_mode & S_IFMT) == S_IFDIR) {
_cleanup_close_ int child_fd = -1;
child_fd = openat(parent_fd, name, O_NOCTTY | O_CLOEXEC | O_DIRECTORY);
if (child_fd < 0)
return log_error_errno(errno, "Failed to open \"%s\" at \"%s\": %m", name, strna(parent_name));
r = rm_rf_children(TAKE_FD(child_fd), REMOVE_ROOT|REMOVE_SUBVOLUME|REMOVE_PHYSICAL, &st);
if (r < 0)
return log_error_errno(r, "Failed to remove contents of \"%s\" at \"%s\": %m", name, strna(parent_name));
r = unlinkat_harder(parent_fd, name, AT_REMOVEDIR, REMOVE_CHMOD | REMOVE_CHMOD_RESTORE);
} else
r = unlinkat_harder(parent_fd, name, 0, REMOVE_CHMOD | REMOVE_CHMOD_RESTORE);
if (r < 0)
return log_error_errno(r, "Failed to remove \"%s\" at \"%s\": %m", name, strna(parent_name));
/* This is covered by the log_notice "Wrong file type..." It is logged earlier because it gives
* context to other error messages that might follow. */
return -ENOENT;
}
/* If child_mode is non-zero, rm_if_wrong_type_safe will be executed for the last path component. */
static int mkdir_parents_rm_if_wrong_type(mode_t child_mode, const char *path) {
_cleanup_close_ int parent_fd = -1;
struct stat parent_st;
size_t path_len;
int r;
assert(path);
assert((child_mode & ~S_IFMT) == 0);
path_len = strlen(path);
if (!is_path(path))
/* rm_if_wrong_type_safe already logs errors. */
return child_mode != 0 ? rm_if_wrong_type_safe(child_mode, AT_FDCWD, NULL, path, AT_SYMLINK_NOFOLLOW) : 0;
if (child_mode != 0 && endswith(path, "/"))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Trailing path separators are only allowed if child_mode is not set; got \"%s\"", path);
/* Get the parent_fd and stat. */
parent_fd = openat(AT_FDCWD, path_is_absolute(path) ? "/" : ".", O_NOCTTY | O_CLOEXEC | O_DIRECTORY);
if (parent_fd < 0)
return log_error_errno(errno, "Failed to open root: %m");
if (fstat(parent_fd, &parent_st) < 0)
return log_error_errno(errno, "Failed to stat root: %m");
/* Check every parent directory in the path, except the last component */
for (const char *e = path;;) {
_cleanup_close_ int next_fd = -1;
char t[path_len + 1];
const char *s;
/* Find the start of the next path component. */
s = e + strspn(e, "/");
/* Find the end of the next path component. */
e = s + strcspn(s, "/");
/* Copy the path component to t so it can be a null terminated string. */
*((char*) mempcpy(t, s, e - s)) = 0;
/* Is this the last component? If so, then check the type */
if (*e == 0)
return child_mode != 0 ? rm_if_wrong_type_safe(child_mode, parent_fd, &parent_st, t, AT_SYMLINK_NOFOLLOW) : 0;
r = rm_if_wrong_type_safe(S_IFDIR, parent_fd, &parent_st, t, 0);
/* Remove dangling symlinks. */
if (r == -ENOENT)
r = rm_if_wrong_type_safe(S_IFDIR, parent_fd, &parent_st, t, AT_SYMLINK_NOFOLLOW);
if (r == -ENOENT) {
RUN_WITH_UMASK(0000)
r = mkdirat_label(parent_fd, t, 0755);
if (r < 0) {
_cleanup_free_ char *parent_name = NULL;
(void) fd_get_path(parent_fd, &parent_name);
return log_error_errno(r, "Failed to mkdir \"%s\" at \"%s\": %m", t, strnull(parent_name));
}
} else if (r < 0)
/* rm_if_wrong_type_safe already logs errors. */
return r;
next_fd = RET_NERRNO(openat(parent_fd, t, O_NOCTTY | O_CLOEXEC | O_DIRECTORY));
if (next_fd < 0) {
_cleanup_free_ char *parent_name = NULL;
(void) fd_get_path(parent_fd, &parent_name);
return log_error_errno(next_fd, "Failed to open \"%s\" at \"%s\": %m", t, strnull(parent_name));
}
r = RET_NERRNO(fstat(next_fd, &parent_st));
if (r < 0) {
_cleanup_free_ char *parent_name = NULL;
(void) fd_get_path(parent_fd, &parent_name);
return log_error_errno(r, "Failed to stat \"%s\" at \"%s\": %m", t, strnull(parent_name));
}
CLOSE_AND_REPLACE(parent_fd, next_fd);
}
}
static int mkdir_parents_item(Item *i, mode_t child_mode) {
int r;
if (i->try_replace) {
r = mkdir_parents_rm_if_wrong_type(child_mode, i->path);
if (r < 0 && r != -ENOENT)
return r;
} else
RUN_WITH_UMASK(0000)
(void) mkdir_parents_label(i->path, 0755);
return 0;
}
static int create_item(Item *i) {
CreationMode creation;
int r = 0;
assert(i);
log_debug("Running create action for entry %c %s", (char) i->type, i->path);
switch (i->type) {
case IGNORE_PATH:
case IGNORE_DIRECTORY_PATH:
case REMOVE_PATH:
case RECURSIVE_REMOVE_PATH:
return 0;
case TRUNCATE_FILE:
case CREATE_FILE:
r = mkdir_parents_item(i, S_IFREG);
if (r < 0)
return r;
if ((i->type == CREATE_FILE && i->append_or_force) || i->type == TRUNCATE_FILE)
r = truncate_file(i, i->path);
else
r = create_file(i, i->path);
if (r < 0)
return r;
break;
case COPY_FILES:
r = mkdir_parents_item(i, 0);
if (r < 0)
return r;
r = copy_files(i);
if (r < 0)
return r;
break;
case WRITE_FILE:
r = glob_item(i, write_one_file);
if (r < 0)
return r;
break;
case CREATE_DIRECTORY:
case TRUNCATE_DIRECTORY:
r = mkdir_parents_item(i, S_IFDIR);
if (r < 0)
return r;
r = create_directory(i, i->path);
if (r < 0)
return r;
break;
case CREATE_SUBVOLUME:
case CREATE_SUBVOLUME_INHERIT_QUOTA:
case CREATE_SUBVOLUME_NEW_QUOTA:
r = mkdir_parents_item(i, S_IFDIR);
if (r < 0)
return r;
r = create_subvolume(i, i->path);
if (r < 0)
return r;
break;
case EMPTY_DIRECTORY:
r = glob_item(i, empty_directory);
if (r < 0)
return r;
break;
case CREATE_FIFO:
r = mkdir_parents_item(i, S_IFIFO);
if (r < 0)
return r;
r = create_fifo(i, i->path);
if (r < 0)
return r;
break;
case CREATE_SYMLINK: {
r = mkdir_parents_item(i, S_IFLNK);
if (r < 0)
return r;
mac_selinux_create_file_prepare(i->path, S_IFLNK);
r = symlink(i->argument, i->path);
mac_selinux_create_file_clear();
if (r < 0) {
_cleanup_free_ char *x = NULL;
if (errno != EEXIST)
return log_error_errno(errno, "symlink(%s, %s) failed: %m", i->argument, i->path);
r = readlink_malloc(i->path, &x);
if (r < 0 || !streq(i->argument, x)) {
if (i->append_or_force) {
mac_selinux_create_file_prepare(i->path, S_IFLNK);
r = symlink_atomic(i->argument, i->path);
mac_selinux_create_file_clear();
if (IN_SET(r, -EISDIR, -EEXIST, -ENOTEMPTY)) {
r = rm_rf(i->path, REMOVE_ROOT|REMOVE_PHYSICAL);
if (r < 0)
return log_error_errno(r, "rm -fr %s failed: %m", i->path);
mac_selinux_create_file_prepare(i->path, S_IFLNK);
r = RET_NERRNO(symlink(i->argument, i->path));
mac_selinux_create_file_clear();
}
if (r < 0)
return log_error_errno(r, "symlink(%s, %s) failed: %m", i->argument, i->path);
creation = CREATION_FORCE;
} else {
log_debug("\"%s\" is not a symlink or does not point to the correct path.", i->path);
return 0;
}
} else
creation = CREATION_EXISTING;
} else
creation = CREATION_NORMAL;
log_debug("%s symlink \"%s\".", creation_mode_verb_to_string(creation), i->path);
break;
}
case CREATE_BLOCK_DEVICE:
case CREATE_CHAR_DEVICE:
if (have_effective_cap(CAP_MKNOD) == 0) {
/* In a container we lack CAP_MKNOD. We shouldn't attempt to create the device node in that
* case to avoid noise, and we don't support virtualized devices in containers anyway. */
log_debug("We lack CAP_MKNOD, skipping creation of device node %s.", i->path);
return 0;
}
r = mkdir_parents_item(i, i->type == CREATE_BLOCK_DEVICE ? S_IFBLK : S_IFCHR);
if (r < 0)
return r;
r = create_device(i, i->type == CREATE_BLOCK_DEVICE ? S_IFBLK : S_IFCHR);
if (r < 0)
return r;
break;
case ADJUST_MODE:
case RELABEL_PATH:
r = glob_item(i, path_set_perms);
if (r < 0)
return r;
break;
case RECURSIVE_RELABEL_PATH:
r = glob_item_recursively(i, fd_set_perms);
if (r < 0)
return r;
break;
case SET_XATTR:
r = glob_item(i, path_set_xattrs);
if (r < 0)
return r;
break;
case RECURSIVE_SET_XATTR:
r = glob_item_recursively(i, fd_set_xattrs);
if (r < 0)
return r;
break;
case SET_ACL:
r = glob_item(i, path_set_acls);
if (r < 0)
return r;
break;
case RECURSIVE_SET_ACL:
r = glob_item_recursively(i, fd_set_acls);
if (r < 0)
return r;
break;
case SET_ATTRIBUTE:
r = glob_item(i, path_set_attribute);
if (r < 0)
return r;
break;
case RECURSIVE_SET_ATTRIBUTE:
r = glob_item_recursively(i, fd_set_attribute);
if (r < 0)
return r;
break;
}
return 0;
}
static int remove_item_instance(Item *i, const char *instance) {
int r;
assert(i);
switch (i->type) {
case REMOVE_PATH:
if (remove(instance) < 0 && errno != ENOENT)
return log_error_errno(errno, "rm(%s): %m", instance);
break;
case RECURSIVE_REMOVE_PATH:
/* FIXME: we probably should use dir_cleanup() here instead of rm_rf() so that 'x' is honoured. */
log_debug("rm -rf \"%s\"", instance);
r = rm_rf(instance, REMOVE_ROOT|REMOVE_SUBVOLUME|REMOVE_PHYSICAL);
if (r < 0 && r != -ENOENT)
return log_error_errno(r, "rm_rf(%s): %m", instance);
break;
default:
assert_not_reached();
}
return 0;
}
static int remove_item(Item *i) {
int r;
assert(i);
log_debug("Running remove action for entry %c %s", (char) i->type, i->path);
switch (i->type) {
case TRUNCATE_DIRECTORY:
/* FIXME: we probably should use dir_cleanup() here instead of rm_rf() so that 'x' is honoured. */
log_debug("rm -rf \"%s\"", i->path);
r = rm_rf(i->path, REMOVE_PHYSICAL);
if (r < 0 && r != -ENOENT)
return log_error_errno(r, "rm_rf(%s): %m", i->path);
return 0;
case REMOVE_PATH:
case RECURSIVE_REMOVE_PATH:
return glob_item(i, remove_item_instance);
default:
return 0;
}
}
static char *age_by_to_string(AgeBy ab, bool is_dir) {
static const char ab_map[] = { 'a', 'b', 'c', 'm' };
size_t j = 0;
char *ret;
ret = new(char, ELEMENTSOF(ab_map) + 1);
if (!ret)
return NULL;
for (size_t i = 0; i < ELEMENTSOF(ab_map); i++)
if (FLAGS_SET(ab, 1U << i))
ret[j++] = is_dir ? ascii_toupper(ab_map[i]) : ab_map[i];
ret[j] = 0;
return ret;
}
static int clean_item_instance(Item *i, const char* instance) {
_cleanup_closedir_ DIR *d = NULL;
STRUCT_STATX_DEFINE(sx);
int mountpoint, r;
usec_t cutoff, n;
assert(i);
if (!i->age_set)
return 0;
n = now(CLOCK_REALTIME);
if (n < i->age)
return 0;
cutoff = n - i->age;
d = opendir_nomod(instance);
if (!d) {
if (IN_SET(errno, ENOENT, ENOTDIR)) {
log_debug_errno(errno, "Directory \"%s\": %m", instance);
return 0;
}
return log_error_errno(errno, "Failed to open directory %s: %m", instance);
}
r = statx_fallback(dirfd(d), "", AT_EMPTY_PATH, STATX_MODE|STATX_INO|STATX_ATIME|STATX_MTIME, &sx);
if (r < 0)
return log_error_errno(r, "statx(%s) failed: %m", instance);
if (FLAGS_SET(sx.stx_attributes_mask, STATX_ATTR_MOUNT_ROOT))
mountpoint = FLAGS_SET(sx.stx_attributes, STATX_ATTR_MOUNT_ROOT);
else {
struct stat ps;
if (fstatat(dirfd(d), "..", &ps, AT_SYMLINK_NOFOLLOW) != 0)
return log_error_errno(errno, "stat(%s/..) failed: %m", i->path);
mountpoint =
sx.stx_dev_major != major(ps.st_dev) ||
sx.stx_dev_minor != minor(ps.st_dev) ||
sx.stx_ino != ps.st_ino;
}
if (DEBUG_LOGGING) {
_cleanup_free_ char *ab_f = NULL, *ab_d = NULL;
ab_f = age_by_to_string(i->age_by_file, false);
if (!ab_f)
return log_oom();
ab_d = age_by_to_string(i->age_by_dir, true);
if (!ab_d)
return log_oom();
log_debug("Cleanup threshold for %s \"%s\" is %s; age-by: %s%s",
mountpoint ? "mount point" : "directory",
instance,
FORMAT_TIMESTAMP_STYLE(cutoff, TIMESTAMP_US),
ab_f, ab_d);
}
return dir_cleanup(i, instance, d,
load_statx_timestamp_nsec(&sx.stx_atime),
load_statx_timestamp_nsec(&sx.stx_mtime),
cutoff * NSEC_PER_USEC,
sx.stx_dev_major, sx.stx_dev_minor, mountpoint,
MAX_DEPTH, i->keep_first_level,
i->age_by_file, i->age_by_dir);
}
static int clean_item(Item *i) {
assert(i);
log_debug("Running clean action for entry %c %s", (char) i->type, i->path);
switch (i->type) {
case CREATE_DIRECTORY:
case CREATE_SUBVOLUME:
case CREATE_SUBVOLUME_INHERIT_QUOTA:
case CREATE_SUBVOLUME_NEW_QUOTA:
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);
default:
return 0;
}
}
static int process_item(Item *i, OperationMask operation) {
OperationMask todo;
_cleanup_free_ char *_path = NULL;
const char *path;
int r, q, p;
assert(i);
todo = operation & ~i->done;
if (todo == 0) /* Everything already done? */
return 0;
i->done |= operation;
path = i->path;
if (string_is_glob(path)) {
/* We can't easily check whether a glob matches any autofs path, so let's do the check only
* for the non-glob part. */
r = glob_non_glob_prefix(path, &_path);
if (r < 0 && r != -ENOENT)
return log_debug_errno(r, "Failed to deglob path: %m");
if (r >= 0)
path = _path;
}
r = chase_symlinks(path, arg_root, CHASE_NO_AUTOFS|CHASE_NONEXISTENT|CHASE_WARN, NULL, NULL);
if (r == -EREMOTE) {
log_notice_errno(r, "Skipping %s", i->path); /* We log the configured path, to not confuse the user. */
return 0;
}
if (r < 0)
log_debug_errno(r, "Failed to determine whether '%s' is below autofs, ignoring: %m", i->path);
r = FLAGS_SET(operation, OPERATION_CREATE) ? create_item(i) : 0;
/* Failure can only be tolerated for create */
if (i->allow_failure)
r = 0;
q = FLAGS_SET(operation, OPERATION_REMOVE) ? remove_item(i) : 0;
p = FLAGS_SET(operation, OPERATION_CLEAN) ? clean_item(i) : 0;
return r < 0 ? r :
q < 0 ? q :
p;
}
static int process_item_array(ItemArray *array, OperationMask operation) {
int r = 0;
size_t n;
assert(array);
/* Create any parent first. */
if (FLAGS_SET(operation, OPERATION_CREATE) && array->parent)
r = process_item_array(array->parent, operation & OPERATION_CREATE);
/* Clean up all children first */
if ((operation & (OPERATION_REMOVE|OPERATION_CLEAN)) && !set_isempty(array->children)) {
ItemArray *c;
SET_FOREACH(c, array->children) {
int k;
k = process_item_array(c, operation & (OPERATION_REMOVE|OPERATION_CLEAN));
if (k < 0 && r == 0)
r = k;
}
}
for (n = 0; n < array->n_items; n++) {
int k;
k = process_item(array->items + n, operation);
if (k < 0 && r == 0)
r = k;
}
return r;
}
static void item_free_contents(Item *i) {
assert(i);
free(i->path);
free(i->argument);
strv_free(i->xattrs);
#if HAVE_ACL
acl_free(i->acl_access);
acl_free(i->acl_default);
#endif
}
static ItemArray* item_array_free(ItemArray *a) {
size_t n;
if (!a)
return NULL;
for (n = 0; n < a->n_items; n++)
item_free_contents(a->items + n);
set_free(a->children);
free(a->items);
return mfree(a);
}
static int item_compare(const Item *a, const Item *b) {
/* Make sure that the ownership taking item is put first, so
* that we first create the node, and then can adjust it */
if (takes_ownership(a->type) && !takes_ownership(b->type))
return -1;
if (!takes_ownership(a->type) && takes_ownership(b->type))
return 1;
return CMP(a->type, b->type);
}
static bool item_compatible(Item *a, Item *b) {
assert(a);
assert(b);
assert(streq(a->path, b->path));
if (takes_ownership(a->type) && takes_ownership(b->type))
/* check if the items are the same */
return streq_ptr(a->argument, b->argument) &&
a->uid_set == b->uid_set &&
a->uid == b->uid &&
a->gid_set == b->gid_set &&
a->gid == b->gid &&
a->mode_set == b->mode_set &&
a->mode == b->mode &&
a->age_set == b->age_set &&
a->age == b->age &&
a->age_by_file == b->age_by_file &&
a->age_by_dir == b->age_by_dir &&
a->mask_perms == b->mask_perms &&
a->keep_first_level == b->keep_first_level &&
a->major_minor == b->major_minor;
return true;
}
static bool should_include_path(const char *path) {
char **prefix;
STRV_FOREACH(prefix, arg_exclude_prefixes)
if (path_startswith(path, *prefix)) {
log_debug("Entry \"%s\" matches exclude prefix \"%s\", skipping.",
path, *prefix);
return false;
}
STRV_FOREACH(prefix, arg_include_prefixes)
if (path_startswith(path, *prefix)) {
log_debug("Entry \"%s\" matches include prefix \"%s\".", path, *prefix);
return true;
}
/* no matches, so we should include this path only if we have no allow list at all */
if (strv_isempty(arg_include_prefixes))
return true;
log_debug("Entry \"%s\" does not match any include prefix, skipping.", path);
return false;
}
static int specifier_expansion_from_arg(Item *i) {
int r;
assert(i);
if (!i->argument)
return 0;
switch (i->type) {
case COPY_FILES:
case CREATE_SYMLINK:
case CREATE_FILE:
case TRUNCATE_FILE:
case WRITE_FILE: {
_cleanup_free_ char *unescaped = NULL, *resolved = NULL;
ssize_t l;
l = cunescape(i->argument, 0, &unescaped);
if (l < 0)
return log_error_errno(l, "Failed to unescape parameter to write: %s", i->argument);
r = specifier_printf(unescaped, PATH_MAX-1, specifier_table, arg_root, NULL, &resolved);
if (r < 0)
return r;
return free_and_replace(i->argument, resolved);
}
case SET_XATTR:
case RECURSIVE_SET_XATTR: {
char **xattr;
STRV_FOREACH(xattr, i->xattrs) {
_cleanup_free_ char *resolved = NULL;
r = specifier_printf(*xattr, SIZE_MAX, specifier_table, arg_root, NULL, &resolved);
if (r < 0)
return r;
free_and_replace(*xattr, resolved);
}
return 0;
}
default:
return 0;
}
}
static int patch_var_run(const char *fname, unsigned line, char **path) {
const char *k;
char *n;
assert(path);
assert(*path);
/* Optionally rewrites lines referencing /var/run/, to use /run/ instead. Why bother? tmpfiles merges lines in
* some cases and detects conflicts in others. If files/directories are specified through two equivalent lines
* this is problematic as neither case will be detected. Ideally we'd detect these cases by resolving symlinks
* early, but that's precisely not what we can do here as this code very likely is running very early on, at a
* time where the paths in question are not available yet, or even more importantly, our own tmpfiles rules
* might create the paths that are intermediary to the listed paths. We can't really cover the generic case,
* but the least we can do is cover the specific case of /var/run vs. /run, as /var/run is a legacy name for
* /run only, and we explicitly document that and require that on systemd systems the former is a symlink to
* the latter. Moreover files below this path are by far the primary usecase for tmpfiles.d/. */
k = path_startswith(*path, "/var/run/");
if (isempty(k)) /* Don't complain about other paths than /var/run, and not about /var/run itself either. */
return 0;
n = path_join("/run", k);
if (!n)
return log_oom();
/* Also log about this briefly. We do so at LOG_NOTICE level, as we fixed up the situation automatically, hence
* there's no immediate need for action by the user. However, in the interest of making things less confusing
* to the user, let's still inform the user that these snippets should really be updated. */
log_syntax(NULL, LOG_NOTICE, fname, line, 0,
"Line references path below legacy directory /var/run/, updating %s → %s; please update the tmpfiles.d/ drop-in file accordingly.",
*path, n);
free_and_replace(*path, n);
return 0;
}
static int find_uid(const char *user, uid_t *ret_uid, Hashmap **cache) {
int r;
assert(user);
assert(ret_uid);
/* First: parse as numeric UID string */
r = parse_uid(user, ret_uid);
if (r >= 0)
return r;
/* Second: pass to NSS if we are running "online" */
if (!arg_root)
return get_user_creds(&user, ret_uid, NULL, NULL, NULL, 0);
/* Third, synthesize "root" unconditionally */
if (streq(user, "root")) {
*ret_uid = 0;
return 0;
}
/* Fourth: use fgetpwent() to read /etc/passwd directly, if we are "offline" */
return name_to_uid_offline(arg_root, user, ret_uid, cache);
}
static int find_gid(const char *group, gid_t *ret_gid, Hashmap **cache) {
int r;
assert(group);
assert(ret_gid);
/* First: parse as numeric GID string */
r = parse_gid(group, ret_gid);
if (r >= 0)
return r;
/* Second: pass to NSS if we are running "online" */
if (!arg_root)
return get_group_creds(&group, ret_gid, 0);
/* Third, synthesize "root" unconditionally */
if (streq(group, "root")) {
*ret_gid = 0;
return 0;
}
/* Fourth: use fgetgrent() to read /etc/group directly, if we are "offline" */
return name_to_gid_offline(arg_root, group, ret_gid, cache);
}
static int parse_age_by_from_arg(const char *age_by_str, Item *item) {
AgeBy ab_f = 0, ab_d = 0;
static const struct {
char age_by_chr;
AgeBy age_by_flag;
} age_by_types[] = {
{ 'a', AGE_BY_ATIME },
{ 'b', AGE_BY_BTIME },
{ 'c', AGE_BY_CTIME },
{ 'm', AGE_BY_MTIME },
};
assert(age_by_str);
assert(item);
if (isempty(age_by_str))
return -EINVAL;
for (const char *s = age_by_str; *s != 0; s++) {
size_t i;
/* Ignore whitespace. */
if (strchr(WHITESPACE, *s))
continue;
for (i = 0; i < ELEMENTSOF(age_by_types); i++) {
/* Check lower-case for files, upper-case for directories. */
if (*s == age_by_types[i].age_by_chr) {
ab_f |= age_by_types[i].age_by_flag;
break;
} else if (*s == ascii_toupper(age_by_types[i].age_by_chr)) {
ab_d |= age_by_types[i].age_by_flag;
break;
}
}
/* Invalid character. */
if (i >= ELEMENTSOF(age_by_types))
return -EINVAL;
}
/* No match. */
if (ab_f == 0 && ab_d == 0)
return -EINVAL;
item->age_by_file = ab_f > 0 ? ab_f : AGE_BY_DEFAULT_FILE;
item->age_by_dir = ab_d > 0 ? ab_d : AGE_BY_DEFAULT_DIR;
return 0;
}
static int parse_line(
const char *fname,
unsigned line,
const char *buffer,
bool *invalid_config,
Hashmap **uid_cache,
Hashmap **gid_cache) {
_cleanup_free_ char *action = NULL, *mode = NULL, *user = NULL, *group = NULL, *age = NULL, *path = NULL;
_cleanup_(item_free_contents) Item i = {
/* The "age-by" argument considers all file timestamp types by default. */
.age_by_file = AGE_BY_DEFAULT_FILE,
.age_by_dir = AGE_BY_DEFAULT_DIR,
};
ItemArray *existing;
OrderedHashmap *h;
int r, pos;
bool append_or_force = false, boot = false, allow_failure = false, try_replace = false;
assert(fname);
assert(line >= 1);
assert(buffer);
r = extract_many_words(
&buffer,
NULL,
EXTRACT_UNQUOTE,
&action,
&path,
&mode,
&user,
&group,
&age,
NULL);
if (r < 0) {
if (IN_SET(r, -EINVAL, -EBADSLT))
/* invalid quoting and such or an unknown specifier */
*invalid_config = true;
return log_syntax(NULL, LOG_ERR, fname, line, r, "Failed to parse line: %m");
} else if (r < 2) {
*invalid_config = true;
return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG), "Syntax error.");
}
if (!empty_or_dash(buffer)) {
i.argument = strdup(buffer);
if (!i.argument)
return log_oom();
}
if (isempty(action)) {
*invalid_config = true;
return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG), "Command too short '%s'.", action);
}
for (pos = 1; action[pos]; pos++) {
if (action[pos] == '!' && !boot)
boot = true;
else if (action[pos] == '+' && !append_or_force)
append_or_force = true;
else if (action[pos] == '-' && !allow_failure)
allow_failure = true;
else if (action[pos] == '=' && !try_replace)
try_replace = true;
else {
*invalid_config = true;
return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG), "Unknown modifiers in command '%s'", action);
}
}
if (boot && !arg_boot) {
log_syntax(NULL, LOG_DEBUG, fname, line, 0, "Ignoring entry %s \"%s\" because --boot is not specified.", action, path);
return 0;
}
i.type = action[0];
i.append_or_force = append_or_force;
i.allow_failure = allow_failure;
i.try_replace = try_replace;
r = specifier_printf(path, PATH_MAX-1, specifier_table, arg_root, NULL, &i.path);
if (r == -ENXIO)
return log_unresolvable_specifier(fname, line);
if (r < 0) {
if (IN_SET(r, -EINVAL, -EBADSLT))
*invalid_config = true;
return log_syntax(NULL, LOG_ERR, fname, line, r, "Failed to replace specifiers in '%s': %m", path);
}
r = patch_var_run(fname, line, &i.path);
if (r < 0)
return r;
switch (i.type) {
case CREATE_DIRECTORY:
case CREATE_SUBVOLUME:
case CREATE_SUBVOLUME_INHERIT_QUOTA:
case CREATE_SUBVOLUME_NEW_QUOTA:
case EMPTY_DIRECTORY:
case TRUNCATE_DIRECTORY:
case CREATE_FIFO:
case IGNORE_PATH:
case IGNORE_DIRECTORY_PATH:
case REMOVE_PATH:
case RECURSIVE_REMOVE_PATH:
case ADJUST_MODE:
case RELABEL_PATH:
case RECURSIVE_RELABEL_PATH:
if (i.argument)
log_syntax(NULL, LOG_WARNING, fname, line, 0, "%c lines don't take argument fields, ignoring.", i.type);
break;
case CREATE_FILE:
case TRUNCATE_FILE:
break;
case CREATE_SYMLINK:
if (!i.argument) {
i.argument = path_join("/usr/share/factory", i.path);
if (!i.argument)
return log_oom();
}
break;
case WRITE_FILE:
if (!i.argument) {
*invalid_config = true;
return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG), "Write file requires argument.");
}
break;
case COPY_FILES:
if (!i.argument) {
i.argument = path_join("/usr/share/factory", i.path);
if (!i.argument)
return log_oom();
} else if (!path_is_absolute(i.argument)) {
*invalid_config = true;
return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG), "Source path '%s' is not absolute.", i.argument);
}
if (!empty_or_root(arg_root)) {
char *p;
p = path_join(arg_root, i.argument);
if (!p)
return log_oom();
free_and_replace(i.argument, p);
}
path_simplify(i.argument);
break;
case CREATE_CHAR_DEVICE:
case CREATE_BLOCK_DEVICE:
if (!i.argument) {
*invalid_config = true;
return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG), "Device file requires argument.");
}
r = parse_dev(i.argument, &i.major_minor);
if (r < 0) {
*invalid_config = true;
return log_syntax(NULL, LOG_ERR, fname, line, r, "Can't parse device file major/minor '%s'.", i.argument);
}
break;
case SET_XATTR:
case RECURSIVE_SET_XATTR:
if (!i.argument) {
*invalid_config = true;
return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG),
"Set extended attribute requires argument.");
}
r = parse_xattrs_from_arg(&i);
if (r < 0)
return r;
break;
case SET_ACL:
case RECURSIVE_SET_ACL:
if (!i.argument) {
*invalid_config = true;
return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG),
"Set ACLs requires argument.");
}
r = parse_acls_from_arg(&i);
if (r < 0)
return r;
break;
case SET_ATTRIBUTE:
case RECURSIVE_SET_ATTRIBUTE:
if (!i.argument) {
*invalid_config = true;
return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG),
"Set file attribute requires argument.");
}
r = parse_attribute_from_arg(&i);
if (IN_SET(r, -EINVAL, -EBADSLT))
*invalid_config = true;
if (r < 0)
return r;
break;
default:
*invalid_config = true;
return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG),
"Unknown command type '%c'.", (char) i.type);
}
if (!path_is_absolute(i.path)) {
*invalid_config = true;
return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG),
"Path '%s' not absolute.", i.path);
}
path_simplify(i.path);
if (!should_include_path(i.path))
return 0;
r = specifier_expansion_from_arg(&i);
if (r == -ENXIO)
return log_unresolvable_specifier(fname, line);
if (r < 0) {
if (IN_SET(r, -EINVAL, -EBADSLT))
*invalid_config = true;
return log_syntax(NULL, LOG_ERR, fname, line, r, "Failed to substitute specifiers in argument: %m");
}
if (!empty_or_root(arg_root)) {
char *p;
p = path_join(arg_root, i.path);
if (!p)
return log_oom();
free_and_replace(i.path, p);
}
if (!empty_or_dash(user)) {
r = find_uid(user, &i.uid, uid_cache);
if (r < 0) {
*invalid_config = true;
return log_syntax(NULL, LOG_ERR, fname, line, r, "Failed to resolve user '%s': %m", user);
}
i.uid_set = true;
}
if (!empty_or_dash(group)) {
r = find_gid(group, &i.gid, gid_cache);
if (r < 0) {
*invalid_config = true;
return log_syntax(NULL, LOG_ERR, fname, line, r, "Failed to resolve group '%s'.", group);
}
i.gid_set = true;
}
if (!empty_or_dash(mode)) {
const char *mm = mode;
unsigned m;
if (*mm == '~') {
i.mask_perms = true;
mm++;
}
r = parse_mode(mm, &m);
if (r < 0) {
*invalid_config = true;
return log_syntax(NULL, LOG_ERR, fname, line, r, "Invalid mode '%s'.", mode);
}
i.mode = m;
i.mode_set = true;
} else
i.mode = IN_SET(i.type, CREATE_DIRECTORY, TRUNCATE_DIRECTORY, CREATE_SUBVOLUME, CREATE_SUBVOLUME_INHERIT_QUOTA, CREATE_SUBVOLUME_NEW_QUOTA) ? 0755 : 0644;
if (!empty_or_dash(age)) {
const char *a = age;
_cleanup_free_ char *seconds = NULL, *age_by = NULL;
if (*a == '~') {
i.keep_first_level = true;
a++;
}
/* Format: "age-by:age"; where age-by is "[abcmABCM]+". */
r = split_pair(a, ":", &age_by, &seconds);
if (r == -ENOMEM)
return log_oom();
if (r < 0 && r != -EINVAL)
return log_error_errno(r, "Failed to parse age-by for '%s': %m", age);
if (r >= 0) {
/* We found a ":", parse the "age-by" part. */
r = parse_age_by_from_arg(age_by, &i);
if (r == -ENOMEM)
return log_oom();
if (r < 0) {
*invalid_config = true;
return log_syntax(NULL, LOG_ERR, fname, line, r, "Invalid age-by '%s'.", age_by);
}
/* For parsing the "age" part, after the ":". */
a = seconds;
}
r = parse_sec(a, &i.age);
if (r < 0) {
*invalid_config = true;
return log_syntax(NULL, LOG_ERR, fname, line, r, "Invalid age '%s'.", a);
}
i.age_set = true;
}
h = needs_glob(i.type) ? globs : items;
existing = ordered_hashmap_get(h, i.path);
if (existing) {
size_t n;
for (n = 0; n < existing->n_items; n++) {
if (!item_compatible(existing->items + n, &i) && !i.append_or_force) {
log_syntax(NULL, LOG_NOTICE, fname, line, 0, "Duplicate line for path \"%s\", ignoring.", i.path);
return 0;
}
}
} else {
existing = new0(ItemArray, 1);
if (!existing)
return log_oom();
r = ordered_hashmap_put(h, i.path, existing);
if (r < 0) {
free(existing);
return log_oom();
}
}
if (!GREEDY_REALLOC(existing->items, existing->n_items + 1))
return log_oom();
existing->items[existing->n_items++] = i;
i = (struct Item) {};
/* Sort item array, to enforce stable ordering of application */
typesafe_qsort(existing->items, existing->n_items, item_compare);
return 0;
}
static int cat_config(char **config_dirs, char **args) {
_cleanup_strv_free_ char **files = NULL;
int r;
r = conf_files_list_with_replacement(arg_root, config_dirs, arg_replace, &files, NULL);
if (r < 0)
return r;
return cat_files(NULL, files, 0);
}
static int exclude_default_prefixes(void) {
int r;
/* Provide an easy way to exclude virtual/memory file systems from what we do here. Useful in
* combination with --root= where we probably don't want to apply stuff to these dirs as they are
* likely over-mounted if the root directory is actually used, and it wouldbe less than ideal to have
* all kinds of files created/adjusted underneath these mount points. */
r = strv_extend_strv(
&arg_exclude_prefixes,
STRV_MAKE("/dev",
"/proc",
"/run",
"/sys"),
true);
if (r < 0)
return log_oom();
return 0;
}
static int help(void) {
_cleanup_free_ char *link = NULL;
int r;
r = terminal_urlify_man("systemd-tmpfiles", "8", &link);
if (r < 0)
return log_oom();
printf("%s [OPTIONS...] [CONFIGURATION FILE...]\n"
"\n%sCreates, deletes and cleans up volatile and temporary files and directories.%s\n\n"
" -h --help Show this help\n"
" --user Execute user configuration\n"
" --version Show package version\n"
" --cat-config Show configuration files\n"
" --create Create marked files/directories\n"
" --clean Clean up marked directories\n"
" --remove Remove marked files/directories\n"
" --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"
" -E Ignore rules prefixed with /dev, /proc, /run, /sys\n"
" --root=PATH Operate on an alternate filesystem root\n"
" --image=PATH Operate on disk image as filesystem root\n"
" --replace=PATH Treat arguments as replacement for PATH\n"
" --no-pager Do not pipe output into a pager\n"
"\nSee the %s for details.\n",
program_invocation_short_name,
ansi_highlight(),
ansi_normal(),
link);
return 0;
}
static int parse_argv(int argc, char *argv[]) {
enum {
ARG_VERSION = 0x100,
ARG_CAT_CONFIG,
ARG_USER,
ARG_CREATE,
ARG_CLEAN,
ARG_REMOVE,
ARG_BOOT,
ARG_PREFIX,
ARG_EXCLUDE_PREFIX,
ARG_ROOT,
ARG_IMAGE,
ARG_REPLACE,
ARG_NO_PAGER,
};
static const struct option options[] = {
{ "help", no_argument, NULL, 'h' },
{ "user", no_argument, NULL, ARG_USER },
{ "version", no_argument, NULL, ARG_VERSION },
{ "cat-config", no_argument, NULL, ARG_CAT_CONFIG },
{ "create", no_argument, NULL, ARG_CREATE },
{ "clean", no_argument, NULL, ARG_CLEAN },
{ "remove", no_argument, NULL, ARG_REMOVE },
{ "boot", no_argument, NULL, ARG_BOOT },
{ "prefix", required_argument, NULL, ARG_PREFIX },
{ "exclude-prefix", required_argument, NULL, ARG_EXCLUDE_PREFIX },
{ "root", required_argument, NULL, ARG_ROOT },
{ "image", required_argument, NULL, ARG_IMAGE },
{ "replace", required_argument, NULL, ARG_REPLACE },
{ "no-pager", no_argument, NULL, ARG_NO_PAGER },
{}
};
int c, r;
assert(argc >= 0);
assert(argv);
while ((c = getopt_long(argc, argv, "hE", options, NULL)) >= 0)
switch (c) {
case 'h':
return help();
case ARG_VERSION:
return version();
case ARG_CAT_CONFIG:
arg_cat_config = true;
break;
case ARG_USER:
arg_user = true;
break;
case ARG_CREATE:
arg_operation |= OPERATION_CREATE;
break;
case ARG_CLEAN:
arg_operation |= OPERATION_CLEAN;
break;
case ARG_REMOVE:
arg_operation |= OPERATION_REMOVE;
break;
case ARG_BOOT:
arg_boot = true;
break;
case ARG_PREFIX:
if (strv_push(&arg_include_prefixes, optarg) < 0)
return log_oom();
break;
case ARG_EXCLUDE_PREFIX:
if (strv_push(&arg_exclude_prefixes, optarg) < 0)
return log_oom();
break;
case ARG_ROOT:
r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_root);
if (r < 0)
return r;
break;
case ARG_IMAGE:
#ifdef STANDALONE
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
"This systemd-tmpfiles version is compiled without support for --image=.");
#else
r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_image);
if (r < 0)
return r;
#endif
/* Imply -E here since it makes little sense to create files persistently in the /run mountpoint of a disk image */
_fallthrough_;
case 'E':
r = exclude_default_prefixes();
if (r < 0)
return r;
break;
case ARG_REPLACE:
if (!path_is_absolute(optarg) ||
!endswith(optarg, ".conf"))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"The argument to --replace= must an absolute path to a config file");
arg_replace = optarg;
break;
case ARG_NO_PAGER:
arg_pager_flags |= PAGER_DISABLE;
break;
case '?':
return -EINVAL;
default:
assert_not_reached();
}
if (arg_operation == 0 && !arg_cat_config)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"You need to specify at least one of --clean, --create or --remove.");
if (arg_replace && arg_cat_config)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Option --replace= is not supported with --cat-config");
if (arg_replace && optind >= argc)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"When --replace= is given, some configuration items must be specified");
if (arg_root && arg_user)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Combination of --user and --root= is not supported.");
if (arg_image && arg_root)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Please specify either --root= or --image=, the combination of both is not supported.");
return 1;
}
static int read_config_file(char **config_dirs, const char *fn, bool ignore_enoent, bool *invalid_config) {
_cleanup_(hashmap_freep) Hashmap *uid_cache = NULL, *gid_cache = NULL;
_cleanup_fclose_ FILE *_f = NULL;
_cleanup_free_ char *pp = NULL;
unsigned v = 0;
FILE *f;
ItemArray *ia;
int r = 0;
assert(fn);
if (streq(fn, "-")) {
log_debug("Reading config from stdin…");
fn = "<stdin>";
f = stdin;
} else {
r = search_and_fopen(fn, "re", arg_root, (const char**) config_dirs, &_f, &pp);
if (r < 0) {
if (ignore_enoent && r == -ENOENT) {
log_debug_errno(r, "Failed to open \"%s\", ignoring: %m", fn);
return 0;
}
return log_error_errno(r, "Failed to open '%s': %m", fn);
}
log_debug("Reading config file \"%s\"…", pp);
fn = pp;
f = _f;
}
for (;;) {
_cleanup_free_ char *line = NULL;
bool invalid_line = false;
char *l;
int k;
k = read_line(f, LONG_LINE_MAX, &line);
if (k < 0)
return log_error_errno(k, "Failed to read '%s': %m", fn);
if (k == 0)
break;
v++;
l = strstrip(line);
if (IN_SET(*l, 0, '#'))
continue;
k = parse_line(fn, v, l, &invalid_line, &uid_cache, &gid_cache);
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 */
ORDERED_HASHMAP_FOREACH(ia, globs)
for (size_t ni = 0; ni < ia->n_items; ni++) {
ItemArray *ja;
Item *i = ia->items + ni, *candidate_item = NULL;
if (i->type != IGNORE_DIRECTORY_PATH)
continue;
ORDERED_HASHMAP_FOREACH(ja, items)
for (size_t nj = 0; nj < ja->n_items; nj++) {
Item *j = ja->items + nj;
if (!IN_SET(j->type, CREATE_DIRECTORY,
TRUNCATE_DIRECTORY,
CREATE_SUBVOLUME,
CREATE_SUBVOLUME_INHERIT_QUOTA,
CREATE_SUBVOLUME_NEW_QUOTA))
continue;
if (path_equal(j->path, i->path)) {
candidate_item = j;
break;
}
if (candidate_item
? (path_startswith(j->path, candidate_item->path) && fnmatch(i->path, j->path, FNM_PATHNAME | FNM_PERIOD) == 0)
: path_startswith(i->path, j->path) != NULL)
candidate_item = j;
}
if (candidate_item && candidate_item->age_set) {
i->age = candidate_item->age;
i->age_set = true;
}
}
if (ferror(f)) {
log_error_errno(errno, "Failed to read from file %s: %m", fn);
if (r == 0)
r = -EIO;
}
return r;
}
static int parse_arguments(char **config_dirs, char **args, bool *invalid_config) {
char **arg;
int r;
STRV_FOREACH(arg, args) {
r = read_config_file(config_dirs, *arg, false, invalid_config);
if (r < 0)
return r;
}
return 0;
}
static int read_config_files(char **config_dirs, char **args, bool *invalid_config) {
_cleanup_strv_free_ char **files = NULL;
_cleanup_free_ char *p = NULL;
char **f;
int r;
r = conf_files_list_with_replacement(arg_root, config_dirs, arg_replace, &files, &p);
if (r < 0)
return r;
STRV_FOREACH(f, files)
if (p && path_equal(*f, p)) {
log_debug("Parsing arguments at position \"%s\"…", *f);
r = parse_arguments(config_dirs, args, invalid_config);
if (r < 0)
return r;
} else
/* Just warn, ignore result otherwise.
* read_config_file() has some debug output, so no need to print anything. */
(void) read_config_file(config_dirs, *f, true, invalid_config);
return 0;
}
static int link_parent(ItemArray *a) {
const char *path;
char *prefix;
int r;
assert(a);
/* Finds the closest "parent" item array for the specified item array. Then registers the specified item array
* as child of it, and fills the parent in, linking them both ways. This allows us to later create parents
* before their children, and clean up/remove children before their parents. */
if (a->n_items <= 0)
return 0;
path = a->items[0].path;
prefix = newa(char, strlen(path) + 1);
PATH_FOREACH_PREFIX(prefix, path) {
ItemArray *j;
j = ordered_hashmap_get(items, prefix);
if (!j)
j = ordered_hashmap_get(globs, prefix);
if (j) {
r = set_ensure_put(&j->children, NULL, a);
if (r < 0)
return log_oom();
a->parent = j;
return 1;
}
}
return 0;
}
DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(item_array_hash_ops, char, string_hash_func, string_compare_func,
ItemArray, item_array_free);
static int run(int argc, char *argv[]) {
#ifndef STANDALONE
_cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
_cleanup_(decrypted_image_unrefp) DecryptedImage *decrypted_image = NULL;
_cleanup_(umount_and_rmdir_and_freep) char *unlink_dir = NULL;
#endif
_cleanup_strv_free_ char **config_dirs = NULL;
bool invalid_config = false;
ItemArray *a;
enum {
PHASE_REMOVE_AND_CLEAN,
PHASE_CREATE,
_PHASE_MAX
} phase;
int r, k;
r = parse_argv(argc, argv);
if (r <= 0)
return r;
log_setup();
/* We require /proc/ for a lot of our operations, i.e. for adjusting access modes, for anything
* SELinux related, for recursive operation, for xattr, acl and chattr handling, for btrfs stuff and
* a lot more. It's probably the majority of invocations where /proc/ is required. Since people
* apparently invoke it without anyway and are surprised about the failures, let's catch this early
* and output a nice and friendly warning. */
if (proc_mounted() == 0)
return log_error_errno(SYNTHETIC_ERRNO(ENOSYS),
"/proc/ is not mounted, but required for successful operation of systemd-tmpfiles. "
"Please mount /proc/. Alternatively, consider using the --root= or --image= switches.");
/* Descending down file system trees might take a lot of fds */
(void) rlimit_nofile_bump(HIGH_RLIMIT_NOFILE);
if (arg_user) {
r = user_config_paths(&config_dirs);
if (r < 0)
return log_error_errno(r, "Failed to initialize configuration directory list: %m");
} else {
config_dirs = strv_split_nulstr(CONF_PATHS_NULSTR("tmpfiles.d"));
if (!config_dirs)
return log_oom();
}
if (DEBUG_LOGGING) {
_cleanup_free_ char *t = NULL;
char **i;
STRV_FOREACH(i, config_dirs) {
_cleanup_free_ char *j = NULL;
j = path_join(arg_root, *i);
if (!j)
return log_oom();
if (!strextend(&t, "\n\t", j))
return log_oom();
}
log_debug("Looking for configuration files in (higher priority first):%s", t);
}
if (arg_cat_config) {
pager_open(arg_pager_flags);
return cat_config(config_dirs, argv + optind);
}
umask(0022);
r = mac_selinux_init();
if (r < 0)
return r;
#ifndef STANDALONE
if (arg_image) {
assert(!arg_root);
r = mount_image_privately_interactively(
arg_image,
DISSECT_IMAGE_GENERIC_ROOT |
DISSECT_IMAGE_REQUIRE_ROOT |
DISSECT_IMAGE_VALIDATE_OS |
DISSECT_IMAGE_RELAX_VAR_CHECK |
DISSECT_IMAGE_FSCK |
DISSECT_IMAGE_GROWFS,
&unlink_dir,
&loop_device,
&decrypted_image);
if (r < 0)
return r;
arg_root = strdup(unlink_dir);
if (!arg_root)
return log_oom();
}
#else
assert(!arg_image);
#endif
items = ordered_hashmap_new(&item_array_hash_ops);
globs = ordered_hashmap_new(&item_array_hash_ops);
if (!items || !globs)
return log_oom();
/* If command line arguments are specified along with --replace, read all
* configuration files and insert the positional arguments at the specified
* place. Otherwise, if command line arguments are specified, execute just
* them, and finally, without --replace= or any positional arguments, just
* read configuration and execute it.
*/
if (arg_replace || optind >= argc)
r = read_config_files(config_dirs, argv + optind, &invalid_config);
else
r = parse_arguments(config_dirs, argv + optind, &invalid_config);
if (r < 0)
return r;
/* Let's now link up all child/parent relationships */
ORDERED_HASHMAP_FOREACH(a, items) {
r = link_parent(a);
if (r < 0)
return r;
}
ORDERED_HASHMAP_FOREACH(a, globs) {
r = link_parent(a);
if (r < 0)
return r;
}
/* If multiple operations are requested, let's first run the remove/clean operations, and only then the create
* operations. i.e. that we first clean out the platform we then build on. */
for (phase = 0; phase < _PHASE_MAX; phase++) {
OperationMask op;
if (phase == PHASE_REMOVE_AND_CLEAN)
op = arg_operation & (OPERATION_REMOVE|OPERATION_CLEAN);
else if (phase == PHASE_CREATE)
op = arg_operation & OPERATION_CREATE;
else
assert_not_reached();
if (op == 0) /* Nothing requested in this phase */
continue;
/* The non-globbing ones usually create things, hence we apply them first */
ORDERED_HASHMAP_FOREACH(a, items) {
k = process_item_array(a, op);
if (k < 0 && r >= 0)
r = k;
}
/* The globbing ones usually alter things, hence we apply them second. */
ORDERED_HASHMAP_FOREACH(a, globs) {
k = process_item_array(a, op);
if (k < 0 && r >= 0)
r = k;
}
}
if (ERRNO_IS_RESOURCE(r))
return r;
if (invalid_config)
return EX_DATAERR;
if (r < 0)
return EX_CANTCREAT;
return 0;
}
DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run);