repart: Copy fs-verity status for CopyFiles= (#35401)

When populating a filesytem with CopyFiles=, we first copy the files to
a temporary directory. Make sure we use the (new) COPY_FS_VERITY flag
when doing that copy so that the `mkfs` that we invoke can see the files
with fs-verity enabled.

Closes #35352
This commit is contained in:
Zbigniew Jędrzejewski-Szmek
2025-05-27 14:01:05 +02:00
committed by GitHub
11 changed files with 478 additions and 90 deletions

View File

@@ -426,18 +426,40 @@
<varlistentry>
<term><varname>CopyFiles=</varname></term>
<listitem><para>Takes a pair of colon separated absolute file system paths. The first path refers to
a source file or directory on the host, the second path refers to a target in the file system of the
newly created partition and formatted file system. This setting may be used to copy files or
directories from the host into the file system that is created due to the <varname>Format=</varname>
option. If <varname>CopyFiles=</varname> is used without <varname>Format=</varname> specified
explicitly, <literal>Format=</literal> with a suitable default is implied (currently
<literal>vfat</literal> for <literal>ESP</literal> and <literal>XBOOTLDR</literal> partitions, and
<literal>ext4</literal> otherwise, but this may change in the future). This option may be used
multiple times to copy multiple files or directories from host into the newly formatted file system.
The colon and second path may be omitted in which case the source path is also used as the target
path (relative to the root of the newly created file system). If the source path refers to a
directory it is copied recursively.</para>
<listitem><para>Takes a colon-separated triplet in the form
<literal><varname>source</varname>[:<varname>target</varname>[:<varname>options</varname>]]</literal>.
<varname>source</varname> is an absolute path which refers to a source file or directory on the host.
<varname>target</varname> is an absolute path in the file system of the newly created partition and
formatted file system. <varname>options</varname> is a comma-separated list of options where each
option is in the form <literal><varname>key</varname>[=<varname>value</varname>]</literal>.</para>
<para>This setting may be used to copy files or directories from the host into the file system that
is created due to the <varname>Format=</varname> option. If <varname>CopyFiles=</varname> is used
without <varname>Format=</varname> specified explicitly, <literal>Format=</literal> with a suitable
default is implied (currently <literal>vfat</literal> for <literal>ESP</literal> and
<literal>XBOOTLDR</literal> partitions, and <literal>ext4</literal> otherwise, but this may change in
the future). This option may be used multiple times to copy multiple files or directories from host
into the newly formatted file system.</para>
<para>The <varname>target</varname> path may be omitted in which case the <varname>source</varname>
path is also used as the target path (relative to the root of the newly created file system). If
the source path refers to a directory it is copied recursively.</para>
<para>The <varname>options</varname> may contain the following values:</para>
<variablelist>
<varlistentry>
<term><varname>fsverity=</varname></term>
<listitem><para>May be set to the value <literal>off</literal> (the default if the option is not
present) or <literal>copy</literal>. If set to <literal>off</literal> then no files copied into
the filesystem from this source will have fs-verity enabled. If set to <literal>copy</literal>
then the fs-verity information for each file will be copied from the corresponding source
file.</para>
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
</varlistentry>
</variablelist>
<para>This option has no effect if the partition already exists: it cannot be used to copy additional
files into an existing partition, it may only be used to populate a file system created anew.</para>

View File

@@ -0,0 +1,96 @@
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
/*
* fs-verity user API
*
* These ioctls can be used on filesystems that support fs-verity. See the
* "User API" section of Documentation/filesystems/fsverity.rst.
*
* Copyright 2019 Google LLC
*/
#ifndef _LINUX_FSVERITY_H
#define _LINUX_FSVERITY_H
#include <linux/ioctl.h>
#include <linux/types.h>
#define FS_VERITY_HASH_ALG_SHA256 1
#define FS_VERITY_HASH_ALG_SHA512 2
struct fsverity_enable_arg {
__u32 version;
__u32 hash_algorithm;
__u32 block_size;
__u32 salt_size;
__u64 salt_ptr;
__u32 sig_size;
__u32 __reserved1;
__u64 sig_ptr;
__u64 __reserved2[11];
};
struct fsverity_digest {
__u16 digest_algorithm;
__u16 digest_size; /* input/output */
__u8 digest[];
};
/*
* Struct containing a file's Merkle tree properties. The fs-verity file digest
* is the hash of this struct. A userspace program needs this struct only if it
* needs to compute fs-verity file digests itself, e.g. in order to sign files.
* It isn't needed just to enable fs-verity on a file.
*
* Note: when computing the file digest, 'sig_size' and 'signature' must be left
* zero and empty, respectively. These fields are present only because some
* filesystems reuse this struct as part of their on-disk format.
*/
struct fsverity_descriptor {
__u8 version; /* must be 1 */
__u8 hash_algorithm; /* Merkle tree hash algorithm */
__u8 log_blocksize; /* log2 of size of data and tree blocks */
__u8 salt_size; /* size of salt in bytes; 0 if none */
__le32 __reserved_0x04; /* must be 0 */
__le64 data_size; /* size of file the Merkle tree is built over */
__u8 root_hash[64]; /* Merkle tree root hash */
__u8 salt[32]; /* salt prepended to each hashed block */
__u8 __reserved[144]; /* must be 0's */
};
/*
* Format in which fs-verity file digests are signed in built-in signatures.
* This is the same as 'struct fsverity_digest', except here some magic bytes
* are prepended to provide some context about what is being signed in case the
* same key is used for non-fsverity purposes, and here the fields have fixed
* endianness.
*
* This struct is specific to the built-in signature verification support, which
* is optional. fs-verity users may also verify signatures in userspace, in
* which case userspace is responsible for deciding on what bytes are signed.
* This struct may still be used, but it doesn't have to be. For example,
* userspace could instead use a string like "sha256:$digest_as_hex_string".
*/
struct fsverity_formatted_digest {
char magic[8]; /* must be "FSVerity" */
__le16 digest_algorithm;
__le16 digest_size;
__u8 digest[];
};
#define FS_VERITY_METADATA_TYPE_MERKLE_TREE 1
#define FS_VERITY_METADATA_TYPE_DESCRIPTOR 2
#define FS_VERITY_METADATA_TYPE_SIGNATURE 3
struct fsverity_read_metadata_arg {
__u64 metadata_type;
__u64 offset;
__u64 length;
__u64 buf_ptr;
__u64 __reserved;
};
#define FS_IOC_ENABLE_VERITY _IOW('f', 133, struct fsverity_enable_arg)
#define FS_IOC_MEASURE_VERITY _IOWR('f', 134, struct fsverity_digest)
#define FS_IOC_READ_VERITY_METADATA \
_IOWR('f', 135, struct fsverity_read_metadata_arg)
#endif /* _LINUX_FSVERITY_H */

View File

@@ -70,8 +70,7 @@ static int run(int argc, char *argv[]) {
label,
/* root = */ NULL,
uuid,
/* discard = */ true,
/* quiet = */ true,
MKFS_DISCARD | MKFS_QUIET,
/* sector_size = */ 0,
/* compression = */ NULL,
/* compression_level = */ NULL,

View File

@@ -2382,8 +2382,7 @@ int home_create_luks(
user_record_user_name_and_realm(h),
/* root = */ NULL,
fs_uuid,
user_record_luks_discard(h),
/* quiet = */ true,
(user_record_luks_discard(h) ? MKFS_DISCARD : 0) | MKFS_QUIET,
/* sector_size = */ 0,
/* compression = */ NULL,
/* compression_level= */ NULL,

View File

@@ -277,6 +277,23 @@ static PartitionEncryptedVolume* partition_encrypted_volume_free(PartitionEncryp
return mfree(c);
}
typedef struct CopyFiles {
char *source;
char *target;
CopyFlags flags;
} CopyFiles;
static void copy_files_free_many(CopyFiles *f, size_t n) {
assert(f || n == 0);
FOREACH_ARRAY(i, f, n) {
free(i->source);
free(i->target);
}
free(f);
}
typedef enum SubvolumeFlags {
SUBVOLUME_RO = 1 << 0,
_SUBVOLUME_FLAGS_MASK = SUBVOLUME_RO,
@@ -377,7 +394,6 @@ typedef struct Partition {
uint64_t copy_blocks_done;
char *format;
char **copy_files;
char **exclude_files_source;
char **exclude_files_target;
char **make_directories;
@@ -394,6 +410,8 @@ typedef struct Partition {
char *compression_level;
int add_validatefs;
CopyFiles *copy_files;
size_t n_copy_files;
uint64_t gpt_flags;
int no_auto;
@@ -633,7 +651,6 @@ static Partition* partition_free(Partition *p) {
safe_close(p->copy_blocks_fd);
free(p->format);
strv_free(p->copy_files);
strv_free(p->exclude_files_source);
strv_free(p->exclude_files_target);
strv_free(p->make_directories);
@@ -644,11 +661,12 @@ static Partition* partition_free(Partition *p) {
free(p->compression);
free(p->compression_level);
copy_files_free_many(p->copy_files, p->n_copy_files);
iovec_done(&p->roothash);
free(p->split_name_format);
unlink_and_free(p->split_path);
partition_mountpoint_free_many(p->mountpoints, p->n_mountpoints);
p->mountpoints = NULL;
p->n_mountpoints = 0;
@@ -674,7 +692,6 @@ static void partition_foreignize(Partition *p) {
p->copy_blocks_root = NULL;
p->format = mfree(p->format);
p->copy_files = strv_free(p->copy_files);
p->exclude_files_source = strv_free(p->exclude_files_source);
p->exclude_files_target = strv_free(p->exclude_files_target);
p->make_directories = strv_free(p->make_directories);
@@ -685,6 +702,10 @@ static void partition_foreignize(Partition *p) {
p->compression = mfree(p->compression);
p->compression_level = mfree(p->compression_level);
copy_files_free_many(p->copy_files, p->n_copy_files);
p->copy_files = NULL;
p->n_copy_files = 0;
p->priority = 0;
p->weight = 1000;
p->padding_weight = 0;
@@ -1749,9 +1770,9 @@ static int config_parse_copy_files(
void *data,
void *userdata) {
_cleanup_free_ char *source = NULL, *buffer = NULL, *resolved_source = NULL, *resolved_target = NULL;
_cleanup_free_ char *source = NULL, *buffer = NULL, *resolved_source = NULL, *resolved_target = NULL, *options = NULL;
Partition *partition = ASSERT_PTR(data);
const char *p = rvalue, *target;
char ***copy_files = ASSERT_PTR(data);
int r;
assert(rvalue);
@@ -1772,9 +1793,38 @@ static int config_parse_copy_files(
else
target = buffer;
r = extract_first_word(&p, &options, ":", EXTRACT_CUNESCAPE|EXTRACT_DONT_COALESCE_SEPARATORS);
if (r < 0)
return log_syntax(unit, LOG_ERR, filename, line, r, "Failed to extract options: %s", rvalue);
if (!isempty(p))
return log_syntax(unit, LOG_ERR, filename, line, SYNTHETIC_ERRNO(EINVAL), "Too many arguments: %s", rvalue);
CopyFlags flags = COPY_REFLINK|COPY_HOLES|COPY_MERGE|COPY_REPLACE|COPY_SIGINT|COPY_HARDLINKS|COPY_ALL_XATTRS|COPY_GRACEFUL_WARN|COPY_TRUNCATE|COPY_RESTORE_DIRECTORY_TIMESTAMPS;
for (const char *opts = options;;) {
_cleanup_free_ char *word = NULL;
const char *val;
r = extract_first_word(&opts, &word, ",", EXTRACT_DONT_COALESCE_SEPARATORS | EXTRACT_UNESCAPE_SEPARATORS);
if (r < 0)
return log_syntax(unit, LOG_ERR, filename, line, r, "Failed to parse CopyFile options: %s", options);
if (r == 0)
break;
if (isempty(word))
continue;
if ((val = startswith(word, "fsverity="))) {
if (streq(val, "copy"))
flags |= COPY_PRESERVE_FS_VERITY;
else if (streq(val, "off"))
flags &= ~COPY_PRESERVE_FS_VERITY;
else
log_syntax(unit, LOG_WARNING, filename, line, 0, "fsverity= expects either 'off' or 'copy'.");
} else
log_syntax(unit, LOG_WARNING, filename, line, 0, "Encountered unknown option '%s', ignoring.", word);
}
r = specifier_printf(source, PATH_MAX-1, system_and_tmp_specifier_table, arg_root, NULL, &resolved_source);
if (r < 0) {
log_syntax(unit, LOG_WARNING, filename, line, r,
@@ -1797,10 +1847,15 @@ static int config_parse_copy_files(
if (r < 0)
return 0;
r = strv_consume_pair(copy_files, TAKE_PTR(resolved_source), TAKE_PTR(resolved_target));
if (r < 0)
if (!GREEDY_REALLOC(partition->copy_files, partition->n_copy_files + 1))
return log_oom();
partition->copy_files[partition->n_copy_files++] = (CopyFiles) {
.source = TAKE_PTR(resolved_source),
.target = TAKE_PTR(resolved_target),
.flags = flags,
};
return 0;
}
@@ -2358,13 +2413,31 @@ static bool partition_needs_populate(const Partition *p) {
assert(p);
assert(!p->supplement_for || !p->suppressing); /* Avoid infinite recursion */
return !strv_isempty(p->copy_files) ||
return p->n_copy_files > 0 ||
!strv_isempty(p->make_directories) ||
!strv_isempty(p->make_symlinks) ||
partition_add_validatefs(p) ||
(p->suppressing && partition_needs_populate(p->suppressing));
}
static MakeFileSystemFlags partition_mkfs_flags(const Partition *p) {
MakeFileSystemFlags flags = 0;
if (arg_discard)
flags |= MKFS_DISCARD;
if (streq(p->format, "erofs") && !DEBUG_LOGGING)
flags |= MKFS_QUIET;
FOREACH_ARRAY(cf, p->copy_files, p->n_copy_files)
if (cf->flags & COPY_PRESERVE_FS_VERITY) {
flags |= MKFS_FS_VERITY;
break;
}
return flags;
}
static int partition_read_definition(Partition *p, const char *path, const char *const *conf_file_dirs) {
ConfigTableItem table[] = {
@@ -2381,7 +2454,7 @@ static int partition_read_definition(Partition *p, const char *path, const char
{ "Partition", "FactoryReset", config_parse_bool, 0, &p->factory_reset },
{ "Partition", "CopyBlocks", config_parse_copy_blocks, 0, p },
{ "Partition", "Format", config_parse_fstype, 0, &p->format },
{ "Partition", "CopyFiles", config_parse_copy_files, 0, &p->copy_files },
{ "Partition", "CopyFiles", config_parse_copy_files, 0, p },
{ "Partition", "ExcludeFiles", config_parse_exclude_files, 0, &p->exclude_files_source },
{ "Partition", "ExcludeFilesTarget", config_parse_exclude_files, 0, &p->exclude_files_target },
{ "Partition", "MakeDirectories", config_parse_make_dirs, 0, &p->make_directories },
@@ -5643,7 +5716,6 @@ static int file_is_denylisted(const char *source, Hashmap *denylist) {
static int do_copy_files(Context *context, Partition *p, const char *root) {
_cleanup_strv_free_ char **subvolumes = NULL;
_cleanup_free_ char **override_copy_files = NULL;
int r;
assert(p);
@@ -5653,31 +5725,36 @@ static int do_copy_files(Context *context, Partition *p, const char *root) {
if (r < 0)
return r;
_cleanup_free_ CopyFiles *copy_files = newdup(CopyFiles, p->copy_files, p->n_copy_files);
if (!copy_files)
return log_oom();
size_t n_copy_files = p->n_copy_files;
if (p->suppressing) {
r = shallow_join_strv(&override_copy_files, p->copy_files, p->suppressing->copy_files);
if (r < 0)
return r;
if (!GREEDY_REALLOC_APPEND(copy_files, n_copy_files,
p->suppressing->copy_files, p->suppressing->n_copy_files))
return log_oom();
}
/* copy_tree_at() automatically copies the permissions of source directories to target directories if
* it created them. However, the root directory is created by us, so we have to manually take care
* that it is initialized. We use the first source directory targeting "/" as the metadata source for
* the root directory. */
STRV_FOREACH_PAIR(source, target, override_copy_files ?: p->copy_files) {
FOREACH_ARRAY(line, copy_files, n_copy_files) {
_cleanup_close_ int rfd = -EBADF, sfd = -EBADF;
if (!path_equal(*target, "/"))
if (!path_equal(line->target, "/"))
continue;
rfd = open(root, O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW);
if (rfd < 0)
return -errno;
sfd = chase_and_open(*source, arg_copy_source, CHASE_PREFIX_ROOT, O_PATH|O_DIRECTORY|O_CLOEXEC|O_NOCTTY, NULL);
sfd = chase_and_open(line->source, arg_copy_source, CHASE_PREFIX_ROOT, O_PATH|O_DIRECTORY|O_CLOEXEC|O_NOCTTY, NULL);
if (sfd == -ENOTDIR)
continue;
if (sfd < 0)
return log_error_errno(sfd, "Failed to open source file '%s%s': %m", strempty(arg_copy_source), *source);
return log_error_errno(sfd, "Failed to open source file '%s%s': %m", strempty(arg_copy_source), line->source);
(void) copy_xattr(sfd, NULL, rfd, NULL, COPY_ALL_XATTRS);
(void) copy_access(sfd, rfd);
@@ -5686,50 +5763,50 @@ static int do_copy_files(Context *context, Partition *p, const char *root) {
break;
}
STRV_FOREACH_PAIR(source, target, override_copy_files ?: p->copy_files) {
FOREACH_ARRAY(line, copy_files, n_copy_files) {
_cleanup_hashmap_free_ Hashmap *denylist = NULL;
_cleanup_set_free_ Set *subvolumes_by_source_inode = NULL;
_cleanup_close_ int sfd = -EBADF, pfd = -EBADF, tfd = -EBADF;
usec_t ts = epoch_or_infinity();
r = make_copy_files_denylist(context, p, *source, *target, &denylist);
r = make_copy_files_denylist(context, p, line->source, line->target, &denylist);
if (r < 0)
return r;
if (r > 0)
continue;
r = make_subvolumes_set(p, *source, *target, &subvolumes_by_source_inode);
r = make_subvolumes_set(p, line->source, line->target, &subvolumes_by_source_inode);
if (r < 0)
return r;
sfd = chase_and_open(*source, arg_copy_source, CHASE_PREFIX_ROOT, O_CLOEXEC|O_NOCTTY, NULL);
sfd = chase_and_open(line->source, arg_copy_source, CHASE_PREFIX_ROOT, O_CLOEXEC|O_NOCTTY, NULL);
if (sfd == -ENOENT) {
log_notice_errno(sfd, "Failed to open source file '%s%s', skipping: %m", strempty(arg_copy_source), *source);
log_notice_errno(sfd, "Failed to open source file '%s%s', skipping: %m", strempty(arg_copy_source), line->source);
continue;
}
if (sfd < 0)
return log_error_errno(sfd, "Failed to open source file '%s%s': %m", strempty(arg_copy_source), *source);
return log_error_errno(sfd, "Failed to open source file '%s%s': %m", strempty(arg_copy_source), line->source);
r = fd_verify_regular(sfd);
if (r < 0) {
if (r != -EISDIR)
return log_error_errno(r, "Failed to check type of source file '%s': %m", *source);
return log_error_errno(r, "Failed to check type of source file '%s': %m", line->source);
/* We are looking at a directory */
tfd = chase_and_open(*target, root, CHASE_PREFIX_ROOT, O_RDONLY|O_DIRECTORY|O_CLOEXEC, NULL);
tfd = chase_and_open(line->target, root, CHASE_PREFIX_ROOT, O_RDONLY|O_DIRECTORY|O_CLOEXEC, NULL);
if (tfd < 0) {
_cleanup_free_ char *dn = NULL, *fn = NULL;
if (tfd != -ENOENT)
return log_error_errno(tfd, "Failed to open target directory '%s': %m", *target);
return log_error_errno(tfd, "Failed to open target directory '%s': %m", line->target);
r = path_extract_filename(*target, &fn);
r = path_extract_filename(line->target, &fn);
if (r < 0)
return log_error_errno(r, "Failed to extract filename from '%s': %m", *target);
return log_error_errno(r, "Failed to extract filename from '%s': %m", line->target);
r = path_extract_directory(*target, &dn);
r = path_extract_directory(line->target, &dn);
if (r < 0)
return log_error_errno(r, "Failed to extract directory from '%s': %m", *target);
return log_error_errno(r, "Failed to extract directory from '%s': %m", line->target);
r = mkdir_p_root_full(root, dn, UID_INVALID, GID_INVALID, 0755, ts, subvolumes);
if (r < 0)
@@ -5743,41 +5820,41 @@ static int do_copy_files(Context *context, Partition *p, const char *root) {
sfd, ".",
pfd, fn,
UID_INVALID, GID_INVALID,
COPY_REFLINK|COPY_HOLES|COPY_MERGE|COPY_REPLACE|COPY_SIGINT|COPY_HARDLINKS|COPY_ALL_XATTRS|COPY_GRACEFUL_WARN|COPY_TRUNCATE|COPY_RESTORE_DIRECTORY_TIMESTAMPS,
line->flags,
denylist, subvolumes_by_source_inode);
} else
r = copy_tree_at(
sfd, ".",
tfd, ".",
UID_INVALID, GID_INVALID,
COPY_REFLINK|COPY_HOLES|COPY_MERGE|COPY_REPLACE|COPY_SIGINT|COPY_HARDLINKS|COPY_ALL_XATTRS|COPY_GRACEFUL_WARN|COPY_TRUNCATE|COPY_RESTORE_DIRECTORY_TIMESTAMPS,
line->flags,
denylist, subvolumes_by_source_inode);
if (r < 0)
return log_error_errno(r, "Failed to copy '%s%s' to '%s%s': %m",
strempty(arg_copy_source), *source, strempty(root), *target);
strempty(arg_copy_source), line->source, strempty(root), line->target);
} else {
_cleanup_free_ char *dn = NULL, *fn = NULL;
/* We are looking at a regular file */
r = file_is_denylisted(*source, denylist);
r = file_is_denylisted(line->source, denylist);
if (r < 0)
return r;
if (r > 0) {
log_debug("%s is in the denylist, ignoring", *source);
log_debug("%s is in the denylist, ignoring", line->source);
continue;
}
r = path_extract_filename(*target, &fn);
r = path_extract_filename(line->target, &fn);
if (r == -EADDRNOTAVAIL || r == O_DIRECTORY)
return log_error_errno(SYNTHETIC_ERRNO(EISDIR),
"Target path '%s' refers to a directory, but source path '%s' refers to regular file, can't copy.", *target, *source);
"Target path '%s' refers to a directory, but source path '%s' refers to regular file, can't copy.", line->target, line->source);
if (r < 0)
return log_error_errno(r, "Failed to extract filename from '%s': %m", *target);
return log_error_errno(r, "Failed to extract filename from '%s': %m", line->target);
r = path_extract_directory(*target, &dn);
r = path_extract_directory(line->target, &dn);
if (r < 0)
return log_error_errno(r, "Failed to extract directory from '%s': %m", *target);
return log_error_errno(r, "Failed to extract directory from '%s': %m", line->target);
r = mkdir_p_root_full(root, dn, UID_INVALID, GID_INVALID, 0755, ts, subvolumes);
if (r < 0)
@@ -5789,11 +5866,11 @@ static int do_copy_files(Context *context, Partition *p, const char *root) {
tfd = openat(pfd, fn, O_CREAT|O_EXCL|O_WRONLY|O_CLOEXEC, 0700);
if (tfd < 0)
return log_error_errno(errno, "Failed to create target file '%s': %m", *target);
return log_error_errno(errno, "Failed to create target file '%s': %m", line->target);
r = copy_bytes(sfd, tfd, UINT64_MAX, COPY_REFLINK|COPY_HOLES|COPY_SIGINT|COPY_TRUNCATE);
if (r < 0)
return log_error_errno(r, "Failed to copy '%s' to '%s%s': %m", *source, strempty(arg_copy_source), *target);
return log_error_errno(r, "Failed to copy '%s' to '%s%s': %m", line->source, strempty(arg_copy_source), line->target);
(void) copy_xattr(sfd, NULL, tfd, NULL, COPY_ALL_XATTRS);
(void) copy_access(sfd, tfd);
@@ -6254,8 +6331,7 @@ static int context_mkfs(Context *context) {
return r;
r = make_filesystem(partition_target_path(t), p->format, strempty(p->new_label), root,
p->fs_uuid, arg_discard,
/* quiet = */ streq(p->format, "erofs") && !DEBUG_LOGGING,
p->fs_uuid, partition_mkfs_flags(p),
context->fs_sector_size, p->compression, p->compression_level,
extra_mkfs_options);
if (r < 0)
@@ -7849,8 +7925,7 @@ static int context_minimize(Context *context) {
strempty(p->new_label),
root,
fs_uuid,
arg_discard,
/* quiet = */ streq(p->format, "erofs") && !DEBUG_LOGGING,
partition_mkfs_flags(p),
context->fs_sector_size,
p->compression,
p->compression_level,
@@ -7941,8 +8016,7 @@ static int context_minimize(Context *context) {
strempty(p->new_label),
root,
p->fs_uuid,
arg_discard,
/* quiet = */ streq(p->format, "erofs") && !DEBUG_LOGGING,
partition_mkfs_flags(p),
context->fs_sector_size,
p->compression,
p->compression_level,

View File

@@ -2,6 +2,8 @@
#include <fcntl.h>
#include <linux/btrfs.h>
#include <linux/fsverity.h>
#include <stddef.h>
#include <stdio.h>
#include <sys/file.h>
#include <sys/ioctl.h>
@@ -790,6 +792,64 @@ static int prepare_nocow(int fdf, const char *from, int fdt, unsigned *chattr_ma
return 0;
}
/* Copies fs-verity status. May re-open fdt to do its job. */
static int copy_fs_verity(int fdf, int *fdt) {
int r;
assert(fdf >= 0);
assert(fdt);
assert(*fdt >= 0);
r = fd_verify_regular(fdf);
if (r < 0)
return r;
struct fsverity_descriptor desc = {};
struct fsverity_read_metadata_arg read_arg = {
.metadata_type = FS_VERITY_METADATA_TYPE_DESCRIPTOR,
.buf_ptr = (uintptr_t) &desc,
.length = sizeof(desc),
};
r = ioctl(fdf, FS_IOC_READ_VERITY_METADATA, &read_arg);
if (r < 0) {
/* ENODATA means that the file doesn't have fs-verity,
* so the correct thing to do is to do nothing at all. */
if (errno == ENODATA)
return 0;
return log_error_errno(errno, "Failed to read fs-verity metadata from source file: %m");
}
/* Make sure that the descriptor is completely initialized */
assert(r == (int) sizeof desc);
r = fd_verify_regular(*fdt);
if (r < 0)
return r;
/* Okay. We're doing this now. We need to re-open fdt as read-only because
* we can't enable fs-verity while writable file descriptors are outstanding. */
_cleanup_close_ int reopened_fd = -EBADF;
r = fd_reopen_condition(*fdt, O_RDONLY|O_CLOEXEC|O_NOCTTY, O_ACCMODE_STRICT|O_PATH, &reopened_fd);
if (r < 0)
return r;
if (reopened_fd >= 0)
close_and_replace(*fdt, reopened_fd);
struct fsverity_enable_arg enable_arg = {
.version = desc.version,
.hash_algorithm = desc.hash_algorithm,
.block_size = UINT32_C(1) << desc.log_blocksize,
.salt_size = desc.salt_size,
.salt_ptr = (uintptr_t) &desc.salt,
};
if (ioctl(*fdt, FS_IOC_ENABLE_VERITY, &enable_arg) < 0)
return log_error_errno(errno, "Failed to set fs-verity metadata: %m");
return 0;
}
static int fd_copy_tree_generic(
int df,
const char *from,
@@ -874,6 +934,15 @@ static int fd_copy_regular(
return r;
}
/* NB: fs-verity cannot be enabled when a writable file descriptor is outstanding.
* copy_fs_verity() may well re-open 'fdt' as O_RDONLY. All code below this point
* needs to be able to work with a read-only file descriptor. */
if (FLAGS_SET(copy_flags, COPY_PRESERVE_FS_VERITY)) {
r = copy_fs_verity(fdf, &fdt);
if (r < 0)
goto fail;
}
if (copy_flags & COPY_FSYNC) {
if (fsync(fdt) < 0) {
r = -errno;

View File

@@ -32,6 +32,7 @@ typedef enum CopyFlags {
* copy because reflinking from COW to NOCOW files is not supported.
*/
COPY_NOCOW_AFTER = 1 << 20,
COPY_PRESERVE_FS_VERITY = 1 << 21, /* Preserve fs-verity when copying. */
} CopyFlags;
typedef enum DenyType {

View File

@@ -322,8 +322,7 @@ int make_filesystem(
const char *label,
const char *root,
sd_id128_t uuid,
bool discard,
bool quiet,
MakeFileSystemFlags flags,
uint64_t sector_size,
char *compression,
char *compression_level,
@@ -335,7 +334,7 @@ int make_filesystem(
_cleanup_(unlink_and_freep) char *protofile = NULL;
char vol_id[CONST_MAX(SD_ID128_UUID_STRING_MAX, 8U + 1U)] = {};
int stdio_fds[3] = { -EBADF, STDERR_FILENO, STDERR_FILENO};
ForkFlags flags = FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT|
ForkFlags fork_flags = FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT|
FORK_CLOSE_ALL_FDS|FORK_REARRANGE_STDIO|FORK_REOPEN_LOG;
int r;
@@ -424,7 +423,7 @@ int make_filesystem(
"-U", vol_id,
"-I", "256",
"-m", "0",
"-E", discard ? "discard,lazy_itable_init=1" : "nodiscard,lazy_itable_init=1",
"-E", FLAGS_SET(flags, MKFS_DISCARD) ? "discard,lazy_itable_init=1" : "nodiscard,lazy_itable_init=1",
"-b", "4096",
"-T", "default");
if (!argv)
@@ -433,7 +432,10 @@ int make_filesystem(
if (root && strv_extend_many(&argv, "-d", root) < 0)
return log_oom();
if (quiet && strv_extend(&argv, "-q") < 0)
if (FLAGS_SET(flags, MKFS_QUIET) && strv_extend(&argv, "-q") < 0)
return log_oom();
if (FLAGS_SET(flags, MKFS_FS_VERITY) && strv_extend_many(&argv, "-O", "verity") < 0)
return log_oom();
if (strv_extend(&argv, node) < 0)
@@ -454,13 +456,13 @@ int make_filesystem(
if (!argv)
return log_oom();
if (!discard && strv_extend(&argv, "--nodiscard") < 0)
if (!FLAGS_SET(flags, MKFS_DISCARD) && strv_extend(&argv, "--nodiscard") < 0)
return log_oom();
if (root && strv_extend_many(&argv, "-r", root) < 0)
return log_oom();
if (quiet && strv_extend(&argv, "-q") < 0)
if (FLAGS_SET(flags, MKFS_QUIET) && strv_extend(&argv, "-q") < 0)
return log_oom();
if (compression) {
@@ -479,7 +481,7 @@ int make_filesystem(
/* mkfs.btrfs unconditionally warns about several settings changing from v5.15 onwards which
* isn't silenced by "-q", so let's redirect stdout to /dev/null as well. */
if (quiet)
if (FLAGS_SET(flags, MKFS_QUIET))
stdio_fds[1] = -EBADF;
/* mkfs.btrfs expects a sector size of at least 4k bytes. */
@@ -495,11 +497,14 @@ int make_filesystem(
"-f", /* force override, without this it doesn't seem to want to write to an empty partition */
"-l", label,
"-U", vol_id,
"-t", one_zero(discard));
"-t", one_zero(FLAGS_SET(flags, MKFS_DISCARD)));
if (!argv)
return log_oom();
if (quiet && strv_extend(&argv, "-q") < 0)
if (FLAGS_SET(flags, MKFS_QUIET) && strv_extend(&argv, "-q") < 0)
return log_oom();
if (FLAGS_SET(flags, MKFS_FS_VERITY) && strv_extend_many(&argv, "-O", "verity") < 0)
return log_oom();
if (sector_size > 0) {
@@ -525,7 +530,7 @@ int make_filesystem(
if (!argv)
return log_oom();
if (!discard && strv_extend(&argv, "-K") < 0)
if (!FLAGS_SET(flags, MKFS_DISCARD) && strv_extend(&argv, "-K") < 0)
return log_oom();
if (root) {
@@ -557,7 +562,7 @@ int make_filesystem(
return log_oom();
}
if (quiet && strv_extend(&argv, "-q") < 0)
if (FLAGS_SET(flags, MKFS_QUIET) && strv_extend(&argv, "-q") < 0)
return log_oom();
if (strv_extend(&argv, node) < 0)
@@ -584,7 +589,7 @@ int make_filesystem(
return log_oom();
/* mkfs.vfat does not have a --quiet option so let's redirect stdout to /dev/null instead. */
if (quiet)
if (FLAGS_SET(flags, MKFS_QUIET))
stdio_fds[1] = -EBADF;
} else if (streq(fstype, "swap")) {
@@ -597,7 +602,7 @@ int make_filesystem(
if (!argv)
return log_oom();
if (quiet)
if (FLAGS_SET(flags, MKFS_QUIET))
stdio_fds[1] = -EBADF;
} else if (streq(fstype, "squashfs")) {
@@ -617,7 +622,7 @@ int make_filesystem(
}
/* mksquashfs -quiet option is pretty new so let's redirect stdout to /dev/null instead. */
if (quiet)
if (FLAGS_SET(flags, MKFS_QUIET))
stdio_fds[1] = -EBADF;
} else if (streq(fstype, "erofs")) {
@@ -626,7 +631,7 @@ int make_filesystem(
if (!argv)
return log_oom();
if (quiet && strv_extend(&argv, "--quiet") < 0)
if (FLAGS_SET(flags, MKFS_QUIET) && strv_extend(&argv, "--quiet") < 0)
return log_oom();
if (compression) {
@@ -663,7 +668,7 @@ int make_filesystem(
return log_error_errno(r, "Failed to stat '%s': %m", node);
if (S_ISBLK(st.st_mode))
flags |= FORK_NEW_MOUNTNS;
fork_flags |= FORK_NEW_MOUNTNS;
}
if (DEBUG_LOGGING) {
@@ -678,7 +683,7 @@ int make_filesystem(
stdio_fds,
/*except_fds=*/ NULL,
/*n_except_fds=*/ 0,
flags,
fork_flags,
/*ret_pid=*/ NULL);
if (r < 0)
return r;
@@ -695,7 +700,7 @@ int make_filesystem(
* on unformatted free space, so let's trick it and other mkfs tools into thinking no
* partitions are mounted. See https://github.com/kdave/btrfs-progs/issues/640 for more
° information. */
if (flags & FORK_NEW_MOUNTNS)
if (fork_flags & FORK_NEW_MOUNTNS)
(void) mount_nofollow_verbose(LOG_DEBUG, "/dev/null", "/proc/self/mounts", NULL, MS_BIND, NULL);
execvp(mkfs, argv);

View File

@@ -5,6 +5,12 @@
#include "forward.h"
typedef enum MakeFilesystemFlags {
MKFS_QUIET = 1 << 0, /* Suppress mkfs command output */
MKFS_DISCARD = 1 << 1, /* Enable 'discard' mode on the filesystem */
MKFS_FS_VERITY = 1 << 2, /* Enable fs-verity support on the filesystem */
} MakeFileSystemFlags;
int mkfs_exists(const char *fstype);
int mkfs_supports_root_option(const char *fstype);
@@ -15,8 +21,7 @@ int make_filesystem(
const char *label,
const char *root,
sd_id128_t uuid,
bool discard,
bool quiet,
MakeFileSystemFlags flags,
uint64_t sector_size,
char *compression,
char *compression_level,

View File

@@ -1,6 +1,8 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <linux/fsverity.h>
#include <sys/file.h>
#include <sys/ioctl.h>
#include <sys/xattr.h>
#include <unistd.h>
@@ -591,4 +593,120 @@ TEST(copy_verify_linked) {
assert_se(faccessat(tfd, "to_2", F_OK, AT_SYMLINK_NOFOLLOW) < 0 && errno == ENOENT);
}
static bool enable_fsverity(int dirfd, const char *name, int algo, const char *salt, size_t salt_size) {
_cleanup_close_ int fd = -EBADF;
ASSERT_OK_ERRNO(fd = openat(dirfd, name, O_RDONLY | O_CLOEXEC | O_NOCTTY));
struct fsverity_enable_arg enable_arg = {
.version = 1,
.hash_algorithm = algo,
.block_size = 4096,
.salt_size = salt_size,
.salt_ptr = (uintptr_t) salt
};
if (ioctl(fd, FS_IOC_ENABLE_VERITY, &enable_arg) == 0)
return true;
/* We might get this if we're on a filesystem without support. */
ASSERT_TRUE(IN_SET(errno, EOPNOTSUPP, ENOTTY));
return false;
}
static struct fsverity_digest* alloc_fsverity_digest(size_t digest_size) {
struct fsverity_digest *digest = malloc(sizeof (struct fsverity_digest) + digest_size);
ASSERT_NOT_NULL(digest);
digest->digest_size = digest_size;
return digest;
}
static bool measure_fsverity(int dirfd, const char *name, struct fsverity_digest *digest) {
_cleanup_close_ int fd = -EBADF;
ASSERT_OK_ERRNO(fd = openat(dirfd, name, O_RDONLY | O_CLOEXEC | O_NOCTTY));
if (ioctl(fd, FS_IOC_MEASURE_VERITY, digest) == 0)
return true;
/* Make sure that the error is caused by the file simply not having fs-verity. */
ASSERT_TRUE(errno == ENODATA);
return false;
}
static void assert_no_fsverity(int dirfd, const char *name) {
_cleanup_free_ struct fsverity_digest *digest = alloc_fsverity_digest(0);
ASSERT_FALSE(measure_fsverity(dirfd, name, digest));
}
static void assert_fsverity_eq(int dirfd_a, int dirfd_b, const char *name) {
_cleanup_free_ struct fsverity_digest *digest_a = alloc_fsverity_digest(64);
_cleanup_free_ struct fsverity_digest *digest_b = alloc_fsverity_digest(64);
bool enabled_a = measure_fsverity(dirfd_a, name, digest_a);
bool enabled_b = measure_fsverity(dirfd_b, name, digest_b);
ASSERT_EQ(enabled_a, enabled_b);
if (enabled_a) {
ASSERT_EQ(digest_a->digest_algorithm, digest_b->digest_algorithm);
ASSERT_EQ(digest_a->digest_size, digest_b->digest_size);
ASSERT_EQ(memcmp(digest_a->digest, digest_b->digest, digest_a->digest_size), 0);
}
}
TEST_RET(copy_with_verity) {
_cleanup_(rm_rf_physical_and_freep) char *srcp = NULL, *dstp = NULL, *badsrcp = NULL, *baddstp = NULL;
const char *files[] = { "disabled", "simple", "bigsha", "salty" };
_cleanup_close_ int src = -EBADF, dst = -EBADF, badsrc = -EBADF, baddst = -EBADF;
/* We're more likely to hit a filesystem with fs-verity enabled on /var/tmp than on /tmp (tmpfs) */
ASSERT_OK(src = mkdtemp_open("/var/tmp/test-copy_file-src.XXXXXX", 0, &srcp));
ASSERT_OK(dst = mkdtemp_open("/var/tmp/test-copy_file-dst.XXXXXX", 0, &dstp));
/* Populate some data to differentiate the files. */
FOREACH_ELEMENT(file, files)
ASSERT_OK(write_string_file_at(src, *file, "src file", WRITE_STRING_FILE_CREATE));
/* Enable on some file using a range of options */
if (!enable_fsverity(src, "simple", FS_VERITY_HASH_ALG_SHA256, NULL, 0))
return log_tests_skipped_errno(errno, "/var/tmp: fs-verity is not supported here");
ASSERT_TRUE(enable_fsverity(src, "bigsha", FS_VERITY_HASH_ALG_SHA512, NULL, 0));
ASSERT_TRUE(enable_fsverity(src, "salty", FS_VERITY_HASH_ALG_SHA512, "edamame", 8));
/* Copy without fs-verity enabled and make sure nothing is set on the destination */
ASSERT_OK(copy_tree_at(src, ".", dst, ".", UID_INVALID, GID_INVALID, COPY_REPLACE|COPY_MERGE, NULL, NULL));
FOREACH_ELEMENT(file, files)
assert_no_fsverity(dst, *file);
/* Copy *with* fs-verity enabled and make sure it works properly */
int r = copy_tree_at(src, ".", dst, ".", UID_INVALID, GID_INVALID, COPY_REFLINK|COPY_REPLACE|COPY_MERGE|COPY_PRESERVE_FS_VERITY, NULL, NULL);
if (r == -ENOTTY)
/* This can happen on some versions of btrfs, for example */
return log_tests_skipped_errno(errno, "/var/tmp: fs-verity supported, but not reading metadata");
ASSERT_OK(r);
FOREACH_ELEMENT(file, files)
assert_fsverity_eq(src, dst, *file);
/* Now try to create files where we know fs-verity doesn't work: tmpfs */
ASSERT_OK(badsrc = mkdtemp_open("/tmp/test-copy_file-src.XXXXXX", 0, &badsrcp));
ASSERT_OK(baddst = mkdtemp_open("/tmp/test-copy_file-src.XXXXXX", 0, &baddstp));
/* Populate the source, same as before */
FOREACH_ELEMENT(file, files)
ASSERT_OK(write_string_file_at(badsrc, *file, "src file", WRITE_STRING_FILE_CREATE));
/* Ensure the attempting to enable fs-verity here will fail */
if (enable_fsverity(badsrc, "simple", FS_VERITY_HASH_ALG_SHA256, NULL, 0))
return log_tests_skipped_errno(errno, "/tmp: fs-verity *is* unexpectedly supported here");
/* Copy from our non-verity filesystem into dst, requesting verity and making sure we notice that
* we failed to read verity from the source. */
ASSERT_ERROR(copy_tree_at(badsrc, ".", dst, ".", UID_INVALID, GID_INVALID, COPY_REFLINK|COPY_REPLACE|COPY_MERGE|COPY_PRESERVE_FS_VERITY, NULL, NULL), ENOTTY);
/* Copy from our verity filesystem into our baddst, requesting verity and making sure we notice that
* we failed to set verity on the destination. */
ASSERT_ERROR(copy_tree_at(src, ".", baddst, ".", UID_INVALID, GID_INVALID, COPY_REFLINK|COPY_REPLACE|COPY_MERGE|COPY_PRESERVE_FS_VERITY, NULL, NULL), ENOTTY);
/* Of course this should fail too... */
ASSERT_ERROR(copy_tree_at(badsrc, ".", baddst, ".", UID_INVALID, GID_INVALID, COPY_REFLINK|COPY_REPLACE|COPY_MERGE|COPY_PRESERVE_FS_VERITY, NULL, NULL), ENOTTY);
return 0;
}
DEFINE_TEST_MAIN(LOG_DEBUG);

View File

@@ -266,16 +266,16 @@ static int run(int argc, char *argv[]) {
assert_se(r >= 0);
assert_se(sd_id128_randomize(&id) >= 0);
assert_se(make_filesystem(dissected->partitions[PARTITION_ESP].node, "vfat", "EFI", NULL, id, true, false, 0, NULL, NULL, NULL) >= 0);
assert_se(make_filesystem(dissected->partitions[PARTITION_ESP].node, "vfat", "EFI", NULL, id, MKFS_DISCARD, 0, NULL, NULL, NULL) >= 0);
assert_se(sd_id128_randomize(&id) >= 0);
assert_se(make_filesystem(dissected->partitions[PARTITION_XBOOTLDR].node, "vfat", "xbootldr", NULL, id, true, false, 0, NULL, NULL, NULL) >= 0);
assert_se(make_filesystem(dissected->partitions[PARTITION_XBOOTLDR].node, "vfat", "xbootldr", NULL, id, MKFS_DISCARD, 0, NULL, NULL, NULL) >= 0);
assert_se(sd_id128_randomize(&id) >= 0);
assert_se(make_filesystem(dissected->partitions[PARTITION_ROOT].node, "ext4", "root", NULL, id, true, false, 0, NULL, NULL, NULL) >= 0);
assert_se(make_filesystem(dissected->partitions[PARTITION_ROOT].node, "ext4", "root", NULL, id, MKFS_DISCARD, 0, NULL, NULL, NULL) >= 0);
assert_se(sd_id128_randomize(&id) >= 0);
assert_se(make_filesystem(dissected->partitions[PARTITION_HOME].node, "ext4", "home", NULL, id, true, false, 0, NULL, NULL, NULL) >= 0);
assert_se(make_filesystem(dissected->partitions[PARTITION_HOME].node, "ext4", "home", NULL, id, MKFS_DISCARD, 0, NULL, NULL, NULL) >= 0);
dissected = dissected_image_unref(dissected);