Files
systemd/src/sysupdate/sysupdate-transfer.c
Yu Watanabe 4279336618 chase: trigger automount only when explicitly requested
Since c5de7b14ae
file searching implies a new mount api syscall by default,
to trigger automounts.

But, this is not necessary in most cases, e.g. when chasing
syspath in sd-device (actually this causes regression in umockdev,
see https://github.com/martinpitt/umockdev/issues/271).
Another example is reading unit files, especially .network files,
as automount may trigger mounting network filesystems...

Also, when this is used in NSS plugins, programs that load the
plugins may fail because of spuriously configured seccomp. See #38565.

Let's not trigger automount by default, and do only when explicitly
requested.

This introduces CHASE_TRIGGER_AUTOFS, and use it in
- service manager,
- bootctl and finding ESP/xbootldr,
- sysupdate,
- mountfsd,
- systemd-mount.

There may be several more places we should trigger automount, but let's
do that later.

Follow-up for c5de7b14ae.
Fixes #38565.
Replaces #38569.

Co-authored-by: Luca Boccassi <luca.boccassi@gmail.com>
2025-08-20 02:32:02 +09:00

1531 lines
61 KiB
C

/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
#include "sd-id128.h"
#include "alloc-util.h"
#include "build-path.h"
#include "chase.h"
#include "conf-parser.h"
#include "dirent-util.h"
#include "errno-util.h"
#include "event-util.h"
#include "extract-word.h"
#include "fd-util.h"
#include "fs-util.h"
#include "glyph-util.h"
#include "gpt.h"
#include "hashmap.h"
#include "hexdecoct.h"
#include "install-file.h"
#include "mkdir.h"
#include "notify-recv.h"
#include "parse-helpers.h"
#include "parse-util.h"
#include "percent-util.h"
#include "pidref.h"
#include "process-util.h"
#include "rm-rf.h"
#include "signal-util.h"
#include "specifier.h"
#include "stdio-util.h"
#include "strv.h"
#include "sync-util.h"
#include "sysupdate.h"
#include "sysupdate-feature.h"
#include "sysupdate-instance.h"
#include "sysupdate-pattern.h"
#include "sysupdate-resource.h"
#include "sysupdate-transfer.h"
#include "time-util.h"
#include "tmpfile-util.h"
#include "web-util.h"
/* Default value for InstancesMax= for fs object targets */
#define DEFAULT_FILE_INSTANCES_MAX 3
Transfer* transfer_free(Transfer *t) {
if (!t)
return NULL;
t->temporary_path = rm_rf_subvolume_and_free(t->temporary_path);
free(t->id);
free(t->min_version);
strv_free(t->protected_versions);
free(t->current_symlink);
free(t->final_path);
strv_free(t->features);
strv_free(t->requisite_features);
strv_free(t->changelog);
strv_free(t->appstream);
partition_info_destroy(&t->partition_info);
resource_destroy(&t->source);
resource_destroy(&t->target);
return mfree(t);
}
Transfer* transfer_new(Context *ctx) {
Transfer *t;
t = new(Transfer, 1);
if (!t)
return NULL;
*t = (Transfer) {
.source.type = _RESOURCE_TYPE_INVALID,
.target.type = _RESOURCE_TYPE_INVALID,
.remove_temporary = true,
.mode = MODE_INVALID,
.tries_left = UINT64_MAX,
.tries_done = UINT64_MAX,
.verify = true,
/* the three flags, as configured by the user */
.no_auto = -1,
.read_only = -1,
.growfs = -1,
/* the read only flag, as ultimately determined */
.install_read_only = -1,
.partition_info = PARTITION_INFO_NULL,
.context = ctx,
};
return t;
}
static int config_parse_protect_version(
const char *unit,
const char *filename,
unsigned line,
const char *section,
unsigned section_line,
const char *lvalue,
int ltype,
const char *rvalue,
void *data,
void *userdata) {
_cleanup_free_ char *resolved = NULL;
char ***protected_versions = ASSERT_PTR(data);
int r;
assert(rvalue);
r = specifier_printf(rvalue, NAME_MAX, specifier_table, arg_root, NULL, &resolved);
if (r < 0) {
log_syntax(unit, LOG_WARNING, filename, line, r,
"Failed to expand specifiers in ProtectVersion=, ignoring: %s", rvalue);
return 0;
}
if (!version_is_valid(resolved)) {
log_syntax(unit, LOG_WARNING, filename, line, 0,
"ProtectVersion= string is not valid, ignoring: %s", resolved);
return 0;
}
r = strv_extend(protected_versions, resolved);
if (r < 0)
return log_oom();
return 0;
}
static int config_parse_min_version(
const char *unit,
const char *filename,
unsigned line,
const char *section,
unsigned section_line,
const char *lvalue,
int ltype,
const char *rvalue,
void *data,
void *userdata) {
_cleanup_free_ char *resolved = NULL;
char **version = ASSERT_PTR(data);
int r;
assert(rvalue);
r = specifier_printf(rvalue, NAME_MAX, specifier_table, arg_root, NULL, &resolved);
if (r < 0) {
log_syntax(unit, LOG_WARNING, filename, line, r,
"Failed to expand specifiers in MinVersion=, ignoring: %s", rvalue);
return 0;
}
if (!version_is_valid(rvalue)) {
log_syntax(unit, LOG_WARNING, filename, line, 0,
"MinVersion= string is not valid, ignoring: %s", resolved);
return 0;
}
return free_and_replace(*version, resolved);
}
static int config_parse_url_specifiers(
const char *unit,
const char *filename,
unsigned line,
const char *section,
unsigned section_line,
const char *lvalue,
int ltype,
const char *rvalue,
void *data,
void *userdata) {
char ***s = ASSERT_PTR(data);
_cleanup_free_ char *resolved = NULL;
int r;
assert(rvalue);
if (isempty(rvalue)) {
*s = strv_free(*s);
return 0;
}
r = specifier_printf(rvalue, NAME_MAX, specifier_table, arg_root, NULL, &resolved);
if (r < 0) {
log_syntax(unit, LOG_WARNING, filename, line, r,
"Failed to expand specifiers in %s=, ignoring: %s", lvalue, rvalue);
return 0;
}
if (!http_url_is_valid(resolved)) {
log_syntax(unit, LOG_WARNING, filename, line, 0,
"%s= URL is not valid, ignoring: %s", lvalue, rvalue);
return 0;
}
r = strv_push(s, TAKE_PTR(resolved));
if (r < 0)
return log_oom();
return 0;
}
static int config_parse_current_symlink(
const char *unit,
const char *filename,
unsigned line,
const char *section,
unsigned section_line,
const char *lvalue,
int ltype,
const char *rvalue,
void *data,
void *userdata) {
_cleanup_free_ char *resolved = NULL;
char **current_symlink = ASSERT_PTR(data);
int r;
assert(rvalue);
r = specifier_printf(rvalue, NAME_MAX, specifier_table, arg_root, NULL, &resolved);
if (r < 0) {
log_syntax(unit, LOG_WARNING, filename, line, r,
"Failed to expand specifiers in CurrentSymlink=, ignoring: %s", rvalue);
return 0;
}
r = path_simplify_and_warn(resolved, 0, unit, filename, line, lvalue);
if (r < 0)
return 0;
return free_and_replace(*current_symlink, resolved);
}
static int config_parse_instances_max(
const char *unit,
const char *filename,
unsigned line,
const char *section,
unsigned section_line,
const char *lvalue,
int ltype,
const char *rvalue,
void *data,
void *userdata) {
uint64_t *instances_max = data, i;
int r;
assert(rvalue);
assert(data);
if (isempty(rvalue)) {
*instances_max = 0; /* Revert to default logic, see transfer_read_definition() */
return 0;
}
r = safe_atou64(rvalue, &i);
if (r < 0) {
log_syntax(unit, LOG_WARNING, filename, line, r,
"Failed to parse InstancesMax= value, ignoring: %s", rvalue);
return 0;
}
if (i < 2) {
log_syntax(unit, LOG_WARNING, filename, line, 0,
"InstancesMax= value must be at least 2, bumping: %s", rvalue);
*instances_max = 2;
} else
*instances_max = i;
return 0;
}
static int config_parse_resource_pattern(
const char *unit,
const char *filename,
unsigned line,
const char *section,
unsigned section_line,
const char *lvalue,
int ltype,
const char *rvalue,
void *data,
void *userdata) {
char ***patterns = ASSERT_PTR(data);
int r;
assert(rvalue);
if (isempty(rvalue)) {
*patterns = strv_free(*patterns);
return 0;
}
for (;;) {
_cleanup_free_ char *word = NULL, *resolved = NULL;
r = extract_first_word(&rvalue, &word, NULL, EXTRACT_CUNESCAPE|EXTRACT_UNESCAPE_RELAX);
if (r < 0) {
log_syntax(unit, LOG_WARNING, filename, line, r,
"Failed to extract first pattern from MatchPattern=, ignoring: %s", rvalue);
return 0;
}
if (r == 0)
break;
r = specifier_printf(word, NAME_MAX, specifier_table, arg_root, NULL, &resolved);
if (r < 0) {
log_syntax(unit, LOG_WARNING, filename, line, r,
"Failed to expand specifiers in MatchPattern=, ignoring: %s", rvalue);
return 0;
}
if (!pattern_valid(resolved))
return log_syntax(unit, LOG_ERR, filename, line, SYNTHETIC_ERRNO(EINVAL),
"MatchPattern= string is not valid, refusing: %s", resolved);
r = strv_consume(patterns, TAKE_PTR(resolved));
if (r < 0)
return log_oom();
}
strv_uniq(*patterns);
return 0;
}
static int config_parse_resource_path(
const char *unit,
const char *filename,
unsigned line,
const char *section,
unsigned section_line,
const char *lvalue,
int ltype,
const char *rvalue,
void *data,
void *userdata) {
_cleanup_free_ char *resolved = NULL;
Resource *rr = ASSERT_PTR(data);
int r;
assert(rvalue);
if (streq(rvalue, "auto")) {
rr->path_auto = true;
rr->path = mfree(rr->path);
return 0;
}
r = specifier_printf(rvalue, PATH_MAX-1, specifier_table, arg_root, NULL, &resolved);
if (r < 0) {
log_syntax(unit, LOG_WARNING, filename, line, r,
"Failed to expand specifiers in Path=, ignoring: %s", rvalue);
return 0;
}
/* Note that we don't validate the path as being absolute or normalized. We'll do that in
* transfer_read_definition() as we might not know yet whether Path refers to a URL or a file system
* path. */
rr->path_auto = false;
return free_and_replace(rr->path, resolved);
}
static DEFINE_CONFIG_PARSE_ENUM(config_parse_resource_type, resource_type, ResourceType);
static DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT(config_parse_resource_path_relto, path_relative_to, PathRelativeTo,
PATH_RELATIVE_TO_ROOT);
static int config_parse_resource_ptype(
const char *unit,
const char *filename,
unsigned line,
const char *section,
unsigned section_line,
const char *lvalue,
int ltype,
const char *rvalue,
void *data,
void *userdata) {
Resource *rr = ASSERT_PTR(data);
int r;
assert(rvalue);
r = gpt_partition_type_from_string(rvalue, &rr->partition_type);
if (r < 0) {
log_syntax(unit, LOG_WARNING, filename, line, r,
"Failed to parse partition type, ignoring: %s", rvalue);
return 0;
}
rr->partition_type_set = true;
return 0;
}
static int config_parse_partition_uuid(
const char *unit,
const char *filename,
unsigned line,
const char *section,
unsigned section_line,
const char *lvalue,
int ltype,
const char *rvalue,
void *data,
void *userdata) {
Transfer *t = ASSERT_PTR(data);
int r;
assert(rvalue);
r = sd_id128_from_string(rvalue, &t->partition_uuid);
if (r < 0) {
log_syntax(unit, LOG_WARNING, filename, line, r,
"Failed to parse partition UUID, ignoring: %s", rvalue);
return 0;
}
t->partition_uuid_set = true;
return 0;
}
static int config_parse_partition_flags(
const char *unit,
const char *filename,
unsigned line,
const char *section,
unsigned section_line,
const char *lvalue,
int ltype,
const char *rvalue,
void *data,
void *userdata) {
Transfer *t = ASSERT_PTR(data);
int r;
assert(rvalue);
r = safe_atou64(rvalue, &t->partition_flags);
if (r < 0) {
log_syntax(unit, LOG_WARNING, filename, line, r,
"Failed to parse partition flags, ignoring: %s", rvalue);
return 0;
}
t->partition_flags_set = true;
return 0;
}
static bool transfer_decide_if_enabled(Transfer *t, Hashmap *known_features) {
assert(t);
/* Requisite feature disabled -> transfer disabled */
STRV_FOREACH(id, t->requisite_features) {
Feature *f = hashmap_get(known_features, *id);
if (!f || !f->enabled) /* missing features are implicitly disabled */
return false;
}
/* No features defined -> transfer implicitly enabled */
if (strv_isempty(t->features))
return true;
/* At least one feature enabled -> transfer enabled */
STRV_FOREACH(id, t->features) {
Feature *f = hashmap_get(known_features, *id);
if (f && f->enabled)
return true;
}
/* All listed features disabled -> transfer disabled */
return false;
}
int transfer_read_definition(Transfer *t, const char *path, const char **dirs, Hashmap *known_features) {
assert(t);
ConfigTableItem table[] = {
{ "Transfer", "MinVersion", config_parse_min_version, 0, &t->min_version },
{ "Transfer", "ProtectVersion", config_parse_protect_version, 0, &t->protected_versions },
{ "Transfer", "Verify", config_parse_bool, 0, &t->verify },
{ "Transfer", "ChangeLog", config_parse_url_specifiers, 0, &t->changelog },
{ "Transfer", "AppStream", config_parse_url_specifiers, 0, &t->appstream },
{ "Transfer", "Features", config_parse_strv, 0, &t->features },
{ "Transfer", "RequisiteFeatures", config_parse_strv, 0, &t->requisite_features },
{ "Source", "Type", config_parse_resource_type, 0, &t->source.type },
{ "Source", "Path", config_parse_resource_path, 0, &t->source },
{ "Source", "PathRelativeTo", config_parse_resource_path_relto, 0, &t->source.path_relative_to },
{ "Source", "MatchPattern", config_parse_resource_pattern, 0, &t->source.patterns },
{ "Target", "Type", config_parse_resource_type, 0, &t->target.type },
{ "Target", "Path", config_parse_resource_path, 0, &t->target },
{ "Target", "PathRelativeTo", config_parse_resource_path_relto, 0, &t->target.path_relative_to },
{ "Target", "MatchPattern", config_parse_resource_pattern, 0, &t->target.patterns },
{ "Target", "MatchPartitionType", config_parse_resource_ptype, 0, &t->target },
{ "Target", "PartitionUUID", config_parse_partition_uuid, 0, t },
{ "Target", "PartitionFlags", config_parse_partition_flags, 0, t },
{ "Target", "PartitionNoAuto", config_parse_tristate, 0, &t->no_auto },
{ "Target", "PartitionGrowFileSystem", config_parse_tristate, 0, &t->growfs },
{ "Target", "ReadOnly", config_parse_tristate, 0, &t->read_only },
{ "Target", "Mode", config_parse_mode, 0, &t->mode },
{ "Target", "TriesLeft", config_parse_uint64, 0, &t->tries_left },
{ "Target", "TriesDone", config_parse_uint64, 0, &t->tries_done },
{ "Target", "InstancesMax", config_parse_instances_max, 0, &t->instances_max },
{ "Target", "RemoveTemporary", config_parse_bool, 0, &t->remove_temporary },
{ "Target", "CurrentSymlink", config_parse_current_symlink, 0, &t->current_symlink },
{}
};
_cleanup_free_ char *filename = NULL;
char *e;
int r;
assert(path);
assert(dirs);
r = path_extract_filename(path, &filename);
if (r < 0)
return log_error_errno(r, "Failed to extract filename from path '%s': %m", path);
r = config_parse_many(
STRV_MAKE_CONST(path),
dirs,
strjoina(filename, ".d"),
arg_root,
"Transfer\0"
"Source\0"
"Target\0",
config_item_table_lookup, table,
CONFIG_PARSE_WARN,
/* userdata= */ NULL,
/* stats_by_path= */ NULL,
/* drop_in_files= */ NULL);
if (r < 0)
return r;
e = ASSERT_PTR(endswith(filename, ".transfer") ?: endswith(filename, ".conf"));
*e = 0; /* Remove the file extension */
t->id = TAKE_PTR(filename);
t->enabled = transfer_decide_if_enabled(t, known_features);
if (!RESOURCE_IS_SOURCE(t->source.type))
return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
"Source Type= must be one of url-file, url-tar, tar, regular-file, directory, subvolume.");
if (t->target.type < 0) {
switch (t->source.type) {
case RESOURCE_URL_FILE:
case RESOURCE_REGULAR_FILE:
t->target.type =
t->target.path && path_startswith(t->target.path, "/dev/") ?
RESOURCE_PARTITION : RESOURCE_REGULAR_FILE;
break;
case RESOURCE_URL_TAR:
case RESOURCE_TAR:
case RESOURCE_DIRECTORY:
t->target.type = RESOURCE_DIRECTORY;
break;
case RESOURCE_SUBVOLUME:
t->target.type = RESOURCE_SUBVOLUME;
break;
default:
assert_not_reached();
}
}
if (!RESOURCE_IS_TARGET(t->target.type))
return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
"Target Type= must be one of partition, regular-file, directory, subvolume.");
if ((IN_SET(t->source.type, RESOURCE_URL_FILE, RESOURCE_PARTITION, RESOURCE_REGULAR_FILE) &&
!IN_SET(t->target.type, RESOURCE_PARTITION, RESOURCE_REGULAR_FILE)) ||
(IN_SET(t->source.type, RESOURCE_URL_TAR, RESOURCE_TAR, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME) &&
!IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME)))
return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
"Target type '%s' is incompatible with source type '%s', refusing.",
resource_type_to_string(t->target.type), resource_type_to_string(t->source.type));
if (!t->source.path && !t->source.path_auto)
return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
"Source specification lacks Path=.");
if (t->source.path_relative_to == PATH_RELATIVE_TO_EXPLICIT && !arg_transfer_source)
return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
"PathRelativeTo=explicit requires --transfer-source= to be specified.");
if (t->target.path_relative_to == PATH_RELATIVE_TO_EXPLICIT)
return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
"PathRelativeTo=explicit can only be used in source specifications.");
if (t->source.path) {
if (RESOURCE_IS_FILESYSTEM(t->source.type) || t->source.type == RESOURCE_PARTITION)
if (!path_is_absolute(t->source.path) || !path_is_normalized(t->source.path))
return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
"Source path is not a normalized, absolute path: %s", t->source.path);
/* We unofficially support file:// in addition to http:// and https:// for url
* sources. That's mostly for testing, since it relieves us from having to set up a HTTP
* server, and CURL abstracts this away from us thankfully. */
if (RESOURCE_IS_URL(t->source.type))
if (!http_url_is_valid(t->source.path) && !file_url_is_valid(t->source.path))
return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
"Source path is not a valid HTTP or HTTPS URL: %s", t->source.path);
}
if (strv_isempty(t->source.patterns))
return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
"Source specification lacks MatchPattern=.");
if (!t->target.path && !t->target.path_auto)
return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
"Target specification lacks Path= field.");
if (t->target.path &&
(!path_is_absolute(t->target.path) || !path_is_normalized(t->target.path)))
return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
"Target path is not a normalized, absolute path: %s", t->target.path);
if (strv_isempty(t->target.patterns)) {
log_syntax(NULL, LOG_INFO, path, 1, 0, "Target specification lacks MatchPattern= expression. Assuming same value as in source specification.");
strv_free(t->target.patterns);
t->target.patterns = strv_copy(t->source.patterns);
if (!t->target.patterns)
return log_oom();
}
if (t->current_symlink && !RESOURCE_IS_FILESYSTEM(t->target.type) && !path_is_absolute(t->current_symlink))
return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
"Current symlink must be absolute path if target is partition: %s", t->current_symlink);
/* When no instance limit is set, use all available partition slots in case of partitions, or 3 in case of fs objects */
if (t->instances_max == 0)
t->instances_max = t->target.type == RESOURCE_PARTITION ? UINT64_MAX : DEFAULT_FILE_INSTANCES_MAX;
return 0;
}
int transfer_resolve_paths(
Transfer *t,
const char *root,
const char *node) {
int r;
/* If Path=auto is used in [Source] or [Target] sections, let's automatically detect the path of the
* block device to use. Moreover, if this path points to a directory but we need a block device,
* automatically determine the backing block device, so that users can reference block devices by
* mount point. */
assert(t);
r = resource_resolve_path(&t->source, root, arg_transfer_source, node);
if (r < 0)
return r;
r = resource_resolve_path(&t->target, root, /*relative_to_directory=*/ NULL, node);
if (r < 0)
return r;
return 0;
}
static void transfer_remove_temporary(Transfer *t) {
_cleanup_closedir_ DIR *d = NULL;
int r;
assert(t);
if (!t->remove_temporary)
return;
if (!IN_SET(t->target.type, RESOURCE_REGULAR_FILE, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME))
return;
/* Removes all temporary files/dirs from previous runs in the target directory, i.e. all those starting with '.#' */
d = opendir(t->target.path);
if (!d) {
if (errno == ENOENT)
return;
log_debug_errno(errno, "Failed to open target directory '%s', ignoring: %m", t->target.path);
return;
}
for (;;) {
struct dirent *de;
errno = 0;
de = readdir_no_dot(d);
if (!de) {
if (errno != 0)
log_debug_errno(errno, "Failed to read target directory '%s', ignoring: %m", t->target.path);
break;
}
if (!startswith(de->d_name, ".#"))
continue;
r = rm_rf_child(dirfd(d), de->d_name, REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_CHMOD);
if (r == -ENOENT)
continue;
if (r < 0) {
log_warning_errno(r, "Failed to remove temporary resource instance '%s/%s', ignoring: %m", t->target.path, de->d_name);
continue;
}
log_debug("Removed temporary resource instance '%s/%s'.", t->target.path, de->d_name);
}
}
int transfer_vacuum(
Transfer *t,
uint64_t space,
const char *extra_protected_version) {
uint64_t instances_max, limit;
int r, count = 0;
assert(t);
transfer_remove_temporary(t);
/* First, calculate how many instances to keep, based on the instance limit — but keep at least one */
instances_max = arg_instances_max != UINT64_MAX ? arg_instances_max : t->instances_max;
assert(instances_max >= 1);
if (instances_max == UINT64_MAX) /* Keep infinite instances? */
limit = UINT64_MAX;
else if (space == UINT64_MAX) /* forcibly delete all instances? */
limit = 0;
else if (space > instances_max)
return log_error_errno(SYNTHETIC_ERRNO(ENOSPC),
"Asked to delete more instances than total maximum allowed number of instances, refusing.");
else if (space == instances_max)
return log_error_errno(SYNTHETIC_ERRNO(ENOSPC),
"Asked to delete all possible instances, can't allow that. One instance must always remain.");
else
limit = instances_max - space;
if (t->target.type == RESOURCE_PARTITION && space != UINT64_MAX) {
uint64_t rm, remain;
/* If we are looking at a partition table, we also have to take into account how many
* partition slots of the right type are available */
if (t->target.n_empty + t->target.n_instances < 2)
return log_error_errno(SYNTHETIC_ERRNO(ENOSPC),
"Partition table has less than two partition slots of the right type " SD_ID128_UUID_FORMAT_STR " (%s), refusing.",
SD_ID128_FORMAT_VAL(t->target.partition_type.uuid),
gpt_partition_type_uuid_to_string(t->target.partition_type.uuid));
if (space > t->target.n_empty + t->target.n_instances)
return log_error_errno(SYNTHETIC_ERRNO(ENOSPC),
"Partition table does not have enough partition slots of right type " SD_ID128_UUID_FORMAT_STR " (%s) for operation.",
SD_ID128_FORMAT_VAL(t->target.partition_type.uuid),
gpt_partition_type_uuid_to_string(t->target.partition_type.uuid));
if (space == t->target.n_empty + t->target.n_instances)
return log_error_errno(SYNTHETIC_ERRNO(ENOSPC),
"Asked to empty all partition table slots of the right type " SD_ID128_UUID_FORMAT_STR " (%s), can't allow that. One instance must always remain.",
SD_ID128_FORMAT_VAL(t->target.partition_type.uuid),
gpt_partition_type_uuid_to_string(t->target.partition_type.uuid));
rm = LESS_BY(space, t->target.n_empty);
remain = LESS_BY(t->target.n_instances, rm);
limit = MIN(limit, remain);
}
while (t->target.n_instances > limit) {
Instance *oldest;
size_t p = t->target.n_instances - 1;
for (;;) {
oldest = t->target.instances[p];
assert(oldest);
/* If this is listed among the protected versions, then let's not remove it */
if (!strv_contains(t->protected_versions, oldest->metadata.version) &&
(!extra_protected_version || !streq(extra_protected_version, oldest->metadata.version)))
break;
log_debug("Version '%s' is protected, not removing.", oldest->metadata.version);
if (p == 0) {
oldest = NULL;
break;
}
p--;
}
if (!oldest) /* Nothing more to remove */
break;
assert(oldest->resource);
log_info("%s Removing %s '%s' (%s).",
glyph(GLYPH_RECYCLING),
space == UINT64_MAX ? "disabled" : "old",
oldest->path,
resource_type_to_string(oldest->resource->type));
switch (t->target.type) {
case RESOURCE_REGULAR_FILE:
case RESOURCE_DIRECTORY:
case RESOURCE_SUBVOLUME:
r = rm_rf(oldest->path, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_MISSING_OK|REMOVE_CHMOD);
if (r < 0 && r != -ENOENT)
return log_error_errno(r, "Failed to make room, deleting '%s' failed: %m", oldest->path);
(void) rmdir_parents(oldest->path, t->target.path);
break;
case RESOURCE_PARTITION: {
PartitionInfo pinfo = oldest->partition_info;
/* label "_empty" means "no contents" for our purposes */
pinfo.label = (char*) "_empty";
r = patch_partition(t->target.path, &pinfo, PARTITION_LABEL);
if (r < 0)
return r;
t->target.n_empty++;
break;
}
default:
assert_not_reached();
}
instance_free(oldest);
memmove(t->target.instances + p, t->target.instances + p + 1, (t->target.n_instances - p - 1) * sizeof(Instance*));
t->target.n_instances--;
count++;
}
return count;
}
static void compile_pattern_fields(
const Transfer *t,
const Instance *i,
InstanceMetadata *ret) {
assert(t);
assert(i);
assert(ret);
*ret = (InstanceMetadata) {
.version = i->metadata.version,
/* We generally prefer explicitly configured values for the transfer over those automatically
* derived from the source instance. Also, if the source is a tar archive, then let's not
* patch mtime/mode and use the one embedded in the tar file */
.partition_uuid = t->partition_uuid_set ? t->partition_uuid : i->metadata.partition_uuid,
.partition_uuid_set = t->partition_uuid_set || i->metadata.partition_uuid_set,
.partition_flags = t->partition_flags_set ? t->partition_flags : i->metadata.partition_flags,
.partition_flags_set = t->partition_flags_set || i->metadata.partition_flags_set,
.mtime = RESOURCE_IS_TAR(i->resource->type) ? USEC_INFINITY : i->metadata.mtime,
.mode = t->mode != MODE_INVALID ? t->mode : (RESOURCE_IS_TAR(i->resource->type) ? MODE_INVALID : i->metadata.mode),
.size = i->metadata.size,
.tries_done = t->tries_done != UINT64_MAX ? t->tries_done :
i->metadata.tries_done != UINT64_MAX ? i->metadata.tries_done : 0,
.tries_left = t->tries_left != UINT64_MAX ? t->tries_left :
i->metadata.tries_left != UINT64_MAX ? i->metadata.tries_left : 3,
.no_auto = t->no_auto >= 0 ? t->no_auto : i->metadata.no_auto,
.read_only = t->read_only >= 0 ? t->read_only : i->metadata.read_only,
.growfs = t->growfs >= 0 ? t->growfs : i->metadata.growfs,
.sha256sum_set = i->metadata.sha256sum_set,
};
memcpy(ret->sha256sum, i->metadata.sha256sum, sizeof(ret->sha256sum));
}
typedef struct CalloutContext {
const Transfer *transfer;
const Instance *instance;
TransferProgress callback;
PidRef pid;
const char *name;
int helper_errno;
void* userdata;
} CalloutContext;
static CalloutContext *callout_context_free(CalloutContext *ctx) {
if (!ctx)
return NULL;
/* We don't own any data but need to clean up the job pid */
pidref_done(&ctx->pid);
return mfree(ctx);
}
DEFINE_TRIVIAL_CLEANUP_FUNC(CalloutContext*, callout_context_free);
static int callout_context_new(const Transfer *t, const Instance *i, TransferProgress cb,
const char *name, void* userdata, CalloutContext **ret) {
_cleanup_(callout_context_freep) CalloutContext *ctx = NULL;
assert(t);
assert(i);
assert(cb);
ctx = new(CalloutContext, 1);
if (!ctx)
return -ENOMEM;
*ctx = (CalloutContext) {
.transfer = t,
.instance = i,
.callback = cb,
.pid = PIDREF_NULL,
.name = name,
.userdata = userdata,
};
*ret = TAKE_PTR(ctx);
return 0;
}
static int helper_on_exit(sd_event_source *s, const siginfo_t *si, void *userdata) {
CalloutContext *ctx = ASSERT_PTR(userdata);
int r;
assert(s);
assert(si);
assert(ctx);
if (si->si_code == CLD_EXITED) {
if (si->si_status == EXIT_SUCCESS) {
r = 0;
log_debug("%s succeeded.", ctx->name);
} else if (ctx->helper_errno != 0) {
r = -ctx->helper_errno;
log_error_errno(r, "%s failed with exit status %i: %m", ctx->name, si->si_status);
} else {
r = -EPROTO;
log_error("%s failed with exit status %i.", ctx->name, si->si_status);
}
} else {
r = -EPROTO;
if (IN_SET(si->si_code, CLD_KILLED, CLD_DUMPED))
log_error("%s terminated by signal %s.", ctx->name, signal_to_string(si->si_status));
else
log_error("%s failed due to unknown reason.", ctx->name);
}
return sd_event_exit(sd_event_source_get_event(s), r);
}
static int helper_on_notify(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
CalloutContext *ctx = ASSERT_PTR(userdata);
int r;
assert(fd >= 0);
_cleanup_free_ char *buf = NULL;
_cleanup_(pidref_done) PidRef sender_pid = PIDREF_NULL;
r = notify_recv(fd, &buf, /* ret_ucred= */ NULL, &sender_pid);
if (r == -EAGAIN)
return 0;
if (r < 0)
return r;
if (!pidref_equal(&ctx->pid, &sender_pid)) {
log_warning("Got notification datagram from unexpected peer, ignoring.");
return 0;
}
char *errno_str = find_line_startswith(buf, "ERRNO=");
if (errno_str) {
truncate_nl(errno_str);
r = parse_errno(errno_str);
if (r < 0)
log_warning_errno(r, "Got invalid errno value '%s', ignoring: %m", errno_str);
else {
ctx->helper_errno = r;
log_debug_errno(r, "Got errno from callout: %i (%m)", r);
}
}
char *progress_str = find_line_startswith(buf, "X_IMPORT_PROGRESS=");
if (progress_str) {
truncate_nl(progress_str);
int progress = parse_percent(progress_str);
if (progress < 0)
log_warning("Got invalid percent value '%s', ignoring.", progress_str);
else {
r = ctx->callback(ctx->transfer, ctx->instance, progress);
if (r < 0)
return r;
}
}
return 0;
}
static int run_callout(
const char *name,
char *cmdline[],
const Transfer *transfer,
const Instance *instance,
TransferProgress callback,
void *userdata) {
int r;
assert(name);
assert(cmdline);
assert(cmdline[0]);
_cleanup_(callout_context_freep) CalloutContext *ctx = NULL;
r = callout_context_new(transfer, instance, callback, name, userdata, &ctx);
if (r < 0)
return log_oom();
_cleanup_(sd_event_unrefp) sd_event *event = NULL;
r = sd_event_new(&event);
if (r < 0)
return log_error_errno(r, "Failed to create event: %m");
/* Kill the helper & return an error if we get interrupted by a signal */
r = sd_event_add_signal(event, NULL, SIGINT | SD_EVENT_SIGNAL_PROCMASK, NULL, INT_TO_PTR(-ECANCELED));
if (r < 0)
return log_error_errno(r, "Failed to register signal to event: %m");
r = sd_event_add_signal(event, NULL, SIGTERM | SD_EVENT_SIGNAL_PROCMASK, NULL, INT_TO_PTR(-ECANCELED));
if (r < 0)
return log_error_errno(r, "Failed to register signal to event: %m");
_cleanup_free_ char *bind_name = NULL;
r = notify_socket_prepare(
event,
SD_EVENT_PRIORITY_NORMAL - 5,
helper_on_notify,
ctx,
&bind_name);
if (r < 0)
return log_error_errno(r, "Failed to prepare notify socket: %m");
r = pidref_safe_fork(ctx->name, FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_LOG, &ctx->pid);
if (r < 0)
return log_error_errno(r, "Failed to fork process %s: %m", ctx->name);
if (r == 0) {
/* Child */
if (setenv("NOTIFY_SOCKET", bind_name, 1) < 0) {
log_error_errno(errno, "setenv() failed: %m");
_exit(EXIT_FAILURE);
}
r = invoke_callout_binary(cmdline[0], (char *const*) cmdline);
log_error_errno(r, "Failed to execute %s tool: %m", cmdline[0]);
_exit(EXIT_FAILURE);
}
/* Quit the loop w/ when child process exits */
_cleanup_(sd_event_source_unrefp) sd_event_source *exit_source = NULL;
r = event_add_child_pidref(event, &exit_source, &ctx->pid, WEXITED, helper_on_exit, ctx);
if (r < 0)
return log_error_errno(r, "Failed to add child process to event loop: %m");
r = sd_event_source_set_child_process_own(exit_source, true);
if (r < 0)
return log_error_errno(r, "Failed to take ownership of child process: %m");
/* Process events until the helper quits */
return sd_event_loop(event);
}
int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, void *userdata) {
_cleanup_free_ char *formatted_pattern = NULL, *digest = NULL;
char offset[DECIMAL_STR_MAX(uint64_t)+1], max_size[DECIMAL_STR_MAX(uint64_t)+1];
const char *where = NULL;
InstanceMetadata f;
Instance *existing;
int r;
assert(t);
assert(i);
assert(i->resource == &t->source);
assert(cb);
/* Does this instance already exist in the target? Then we don't need to acquire anything */
existing = resource_find_instance(&t->target, i->metadata.version);
if (existing) {
log_info("No need to acquire '%s', already installed.", i->path);
return 0;
}
assert(!t->final_path);
assert(!t->temporary_path);
assert(!strv_isempty(t->target.patterns));
/* Format the target name using the first pattern specified */
compile_pattern_fields(t, i, &f);
r = pattern_format(t->target.patterns[0], &f, &formatted_pattern);
if (r < 0)
return log_error_errno(r, "Failed to format target pattern: %m");
if (RESOURCE_IS_FILESYSTEM(t->target.type)) {
if (!path_is_safe(formatted_pattern))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as file name, refusing: %s", formatted_pattern);
t->final_path = path_join(t->target.path, formatted_pattern);
if (!t->final_path)
return log_oom();
r = mkdir_parents(t->final_path, 0755);
if (r < 0)
return log_error_errno(r, "Cannot create target directory: %m");
r = tempfn_random(t->final_path, "sysupdate", &t->temporary_path);
if (r < 0)
return log_error_errno(r, "Failed to generate temporary target path: %m");
where = t->final_path;
}
if (t->target.type == RESOURCE_PARTITION) {
r = gpt_partition_label_valid(formatted_pattern);
if (r < 0)
return log_error_errno(r, "Failed to determine if formatted pattern is suitable as GPT partition label: %s", formatted_pattern);
if (!r)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as GPT partition label, refusing: %s", formatted_pattern);
r = find_suitable_partition(
t->target.path,
i->metadata.size,
t->target.partition_type_set ? &t->target.partition_type.uuid : NULL,
&t->partition_info);
if (r < 0)
return r;
xsprintf(offset, "%" PRIu64, t->partition_info.start);
xsprintf(max_size, "%" PRIu64, t->partition_info.size);
where = t->partition_info.device;
}
assert(where);
log_info("%s Acquiring %s %s %s...", glyph(GLYPH_DOWNLOAD), i->path, glyph(GLYPH_ARROW_RIGHT), where);
if (RESOURCE_IS_URL(i->resource->type)) {
/* For URL sources we require the SHA256 sum to be known so that we can validate the
* download. */
if (!i->metadata.sha256sum_set)
return log_error_errno(r, "SHA256 checksum not known for download '%s', refusing.", i->path);
digest = hexmem(i->metadata.sha256sum, sizeof(i->metadata.sha256sum));
if (!digest)
return log_oom();
}
switch (i->resource->type) { /* Source */
case RESOURCE_REGULAR_FILE:
switch (t->target.type) { /* Target */
case RESOURCE_REGULAR_FILE:
/* regular file → regular file (why fork off systemd-import for such a simple file
* copy case? implicit decompression mostly, and thus also sandboxing. Also, the
* importer has some tricks up its sleeve, such as sparse file generation, which we
* want to take benefit of, too.) */
r = run_callout("(sd-import-raw)",
STRV_MAKE(
SYSTEMD_IMPORT_PATH,
"raw",
"--direct", /* just copy/unpack the specified file, don't do anything else */
arg_sync ? "--sync=yes" : "--sync=no",
i->path,
t->temporary_path),
t, i, cb, userdata);
break;
case RESOURCE_PARTITION:
/* regular file → partition */
r = run_callout("(sd-import-raw)",
STRV_MAKE(
SYSTEMD_IMPORT_PATH,
"raw",
"--direct", /* just copy/unpack the specified file, don't do anything else */
"--offset", offset,
"--size-max", max_size,
arg_sync ? "--sync=yes" : "--sync=no",
i->path,
t->target.path),
t, i, cb, userdata);
break;
default:
assert_not_reached();
}
break;
case RESOURCE_DIRECTORY:
case RESOURCE_SUBVOLUME:
assert(IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME));
/* directory/subvolume → directory/subvolume */
r = run_callout("(sd-import-fs)",
STRV_MAKE(
SYSTEMD_IMPORT_FS_PATH,
"run",
"--direct", /* just untar the specified file, don't do anything else */
arg_sync ? "--sync=yes" : "--sync=no",
t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no",
i->path,
t->temporary_path),
t, i, cb, userdata);
break;
case RESOURCE_TAR:
assert(IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME));
/* tar → directory/subvolume */
r = run_callout("(sd-import-tar)",
STRV_MAKE(
SYSTEMD_IMPORT_PATH,
"tar",
"--direct", /* just untar the specified file, don't do anything else */
arg_sync ? "--sync=yes" : "--sync=no",
t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no",
i->path,
t->temporary_path),
t, i, cb, userdata);
break;
case RESOURCE_URL_FILE:
switch (t->target.type) {
case RESOURCE_REGULAR_FILE:
/* url file → regular file */
r = run_callout("(sd-pull-raw)",
STRV_MAKE(
SYSTEMD_PULL_PATH,
"raw",
"--direct", /* just download the specified URL, don't download anything else */
"--verify", digest, /* validate by explicit SHA256 sum */
arg_sync ? "--sync=yes" : "--sync=no",
i->path,
t->temporary_path),
t, i, cb, userdata);
break;
case RESOURCE_PARTITION:
/* url file → partition */
r = run_callout("(sd-pull-raw)",
STRV_MAKE(
SYSTEMD_PULL_PATH,
"raw",
"--direct", /* just download the specified URL, don't download anything else */
"--verify", digest, /* validate by explicit SHA256 sum */
"--offset", offset,
"--size-max", max_size,
arg_sync ? "--sync=yes" : "--sync=no",
i->path,
t->target.path),
t, i, cb, userdata);
break;
default:
assert_not_reached();
}
break;
case RESOURCE_URL_TAR:
assert(IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME));
r = run_callout("(sd-pull-tar)",
STRV_MAKE(
SYSTEMD_PULL_PATH,
"tar",
"--direct", /* just download the specified URL, don't download anything else */
"--verify", digest, /* validate by explicit SHA256 sum */
t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no",
arg_sync ? "--sync=yes" : "--sync=no",
i->path,
t->temporary_path),
t, i, cb, userdata);
break;
default:
assert_not_reached();
}
if (r < 0)
return r;
if (RESOURCE_IS_FILESYSTEM(t->target.type)) {
bool need_sync = false;
assert(t->temporary_path);
/* Apply file attributes if set */
if (f.mtime != USEC_INFINITY) {
struct timespec ts;
timespec_store(&ts, f.mtime);
if (utimensat(AT_FDCWD, t->temporary_path, (struct timespec[2]) { ts, ts }, AT_SYMLINK_NOFOLLOW) < 0)
return log_error_errno(errno, "Failed to adjust mtime of '%s': %m", t->temporary_path);
need_sync = true;
}
if (f.mode != MODE_INVALID) {
/* Try with AT_SYMLINK_NOFOLLOW first, because it's the safe thing to do. Older
* kernels don't support that however, in that case we fall back to chmod(). Not as
* safe, but shouldn't be a problem, given that we don't create symlinks here. */
if (fchmodat(AT_FDCWD, t->temporary_path, f.mode, AT_SYMLINK_NOFOLLOW) < 0 &&
(!ERRNO_IS_NOT_SUPPORTED(errno) || chmod(t->temporary_path, f.mode) < 0))
return log_error_errno(errno, "Failed to adjust mode of '%s': %m", t->temporary_path);
need_sync = true;
}
/* Synchronize */
if (arg_sync && need_sync) {
if (t->target.type == RESOURCE_REGULAR_FILE)
r = fsync_path_and_parent_at(AT_FDCWD, t->temporary_path);
else {
assert(IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME));
r = syncfs_path(AT_FDCWD, t->temporary_path);
}
if (r < 0)
return log_error_errno(r, "Failed to synchronize file system backing '%s': %m", t->temporary_path);
}
t->install_read_only = f.read_only;
}
if (t->target.type == RESOURCE_PARTITION) {
free_and_replace(t->partition_info.label, formatted_pattern);
t->partition_change = PARTITION_LABEL;
if (f.partition_uuid_set) {
t->partition_info.uuid = f.partition_uuid;
t->partition_change |= PARTITION_UUID;
}
if (f.partition_flags_set) {
t->partition_info.flags = f.partition_flags;
t->partition_change |= PARTITION_FLAGS;
}
if (f.no_auto >= 0) {
t->partition_info.no_auto = f.no_auto;
t->partition_change |= PARTITION_NO_AUTO;
}
if (f.read_only >= 0) {
t->partition_info.read_only = f.read_only;
t->partition_change |= PARTITION_READ_ONLY;
}
if (f.growfs >= 0) {
t->partition_info.growfs = f.growfs;
t->partition_change |= PARTITION_GROWFS;
}
}
/* For regular file cases the only step left is to install the file in place, which install_file()
* will do via rename(). For partition cases the only step left is to update the partition table,
* which is done at the same place. */
log_info("Successfully acquired '%s'.", i->path);
return 0;
}
int transfer_install_instance(
Transfer *t,
Instance *i,
const char *root) {
int r;
assert(t);
assert(i);
assert(i->resource);
assert(t == container_of(i->resource, Transfer, source));
if (t->temporary_path) {
assert(RESOURCE_IS_FILESYSTEM(t->target.type));
assert(t->final_path);
r = install_file(AT_FDCWD, t->temporary_path,
AT_FDCWD, t->final_path,
INSTALL_REPLACE|
(t->install_read_only > 0 ? INSTALL_READ_ONLY : 0)|
(t->target.type == RESOURCE_REGULAR_FILE ? INSTALL_FSYNC_FULL : INSTALL_SYNCFS));
if (r < 0)
return log_error_errno(r, "Failed to move '%s' into place: %m", t->final_path);
log_info("Successfully installed '%s' (%s) as '%s' (%s).",
i->path,
resource_type_to_string(i->resource->type),
t->final_path,
resource_type_to_string(t->target.type));
t->temporary_path = mfree(t->temporary_path);
}
if (t->partition_change != 0) {
assert(t->target.type == RESOURCE_PARTITION);
r = patch_partition(
t->target.path,
&t->partition_info,
t->partition_change);
if (r < 0)
return r;
log_info("Successfully installed '%s' (%s) as '%s' (%s).",
i->path,
resource_type_to_string(i->resource->type),
t->partition_info.device,
resource_type_to_string(t->target.type));
}
if (t->current_symlink) {
_cleanup_free_ char *buf = NULL, *parent = NULL, *relative = NULL, *resolved = NULL;
const char *link_path, *link_target;
bool resolve_link_path = false;
if (RESOURCE_IS_FILESYSTEM(t->target.type)) {
assert(t->target.path);
if (path_is_absolute(t->current_symlink)) {
link_path = t->current_symlink;
resolve_link_path = true;
} else {
buf = path_make_absolute(t->current_symlink, t->target.path);
if (!buf)
return log_oom();
link_path = buf;
}
link_target = t->final_path;
} else if (t->target.type == RESOURCE_PARTITION) {
assert(path_is_absolute(t->current_symlink));
link_path = t->current_symlink;
link_target = t->partition_info.device;
resolve_link_path = true;
} else
assert_not_reached();
if (resolve_link_path && root) {
r = chase(link_path, root, CHASE_PREFIX_ROOT|CHASE_NONEXISTENT|CHASE_TRIGGER_AUTOFS, &resolved, NULL);
if (r < 0)
return log_error_errno(r, "Failed to resolve current symlink path '%s': %m", link_path);
link_path = resolved;
}
if (link_target) {
r = path_extract_directory(link_path, &parent);
if (r < 0)
return log_error_errno(r, "Failed to extract directory of target path '%s': %m", link_path);
r = path_make_relative(parent, link_target, &relative);
if (r < 0)
return log_error_errno(r, "Failed to make symlink path '%s' relative to '%s': %m", link_target, parent);
r = symlink_atomic(relative, link_path);
if (r < 0)
return log_error_errno(r, "Failed to update current symlink '%s' %s '%s': %m",
link_path,
glyph(GLYPH_ARROW_RIGHT),
relative);
log_info("Updated symlink '%s' %s '%s'.",
link_path, glyph(GLYPH_ARROW_RIGHT), relative);
}
}
return 0;
}