diff --git a/man/repart.d.xml b/man/repart.d.xml index fde83b0978..71c520d643 100644 --- a/man/repart.d.xml +++ b/man/repart.d.xml @@ -426,18 +426,40 @@ CopyFiles= - 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 Format= - option. If CopyFiles= is used without Format= specified - explicitly, Format= with a suitable default is implied (currently - vfat for ESP and XBOOTLDR partitions, and - ext4 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. + Takes a colon-separated triplet in the form + source[:target[:options]]. + source is an absolute path which refers to a source file or directory on the host. + target is an absolute path in the file system of the newly created partition and + formatted file system. options is a comma-separated list of options where each + option is in the form key[=value]. + + This setting may be used to copy files or directories from the host into the file system that + is created due to the Format= option. If CopyFiles= is used + without Format= specified explicitly, Format= with a suitable + default is implied (currently vfat for ESP and + XBOOTLDR partitions, and ext4 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 target 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. + + The options may contain the following values: + + + + fsverity= + May be set to the value off (the default if the option is not + present) or copy. If set to off then no files copied into + the filesystem from this source will have fs-verity enabled. If set to copy + then the fs-verity information for each file will be copied from the corresponding source + file. + + + + + 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. diff --git a/src/basic/include/linux/fsverity.h b/src/basic/include/linux/fsverity.h new file mode 100644 index 0000000000..bdc2ca6639 --- /dev/null +++ b/src/basic/include/linux/fsverity.h @@ -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 +#include + +#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 */ diff --git a/src/growfs/makefs.c b/src/growfs/makefs.c index 3378bd3d70..897b9a7600 100644 --- a/src/growfs/makefs.c +++ b/src/growfs/makefs.c @@ -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, diff --git a/src/home/homework-luks.c b/src/home/homework-luks.c index e92e5e2eb9..01ac02b40a 100644 --- a/src/home/homework-luks.c +++ b/src/home/homework-luks.c @@ -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, diff --git a/src/repart/repart.c b/src/repart/repart.c index 89d4297d62..f3cc69ef11 100644 --- a/src/repart/repart.c +++ b/src/repart/repart.c @@ -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, diff --git a/src/shared/copy.c b/src/shared/copy.c index ca2325459d..0c79121b68 100644 --- a/src/shared/copy.c +++ b/src/shared/copy.c @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include #include @@ -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; diff --git a/src/shared/copy.h b/src/shared/copy.h index 3efe877c4b..1d17f9b01f 100644 --- a/src/shared/copy.h +++ b/src/shared/copy.h @@ -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 { diff --git a/src/shared/mkfs-util.c b/src/shared/mkfs-util.c index d0ecb391d6..629d02207c 100644 --- a/src/shared/mkfs-util.c +++ b/src/shared/mkfs-util.c @@ -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); diff --git a/src/shared/mkfs-util.h b/src/shared/mkfs-util.h index e20d9dc63e..2e83370d03 100644 --- a/src/shared/mkfs-util.h +++ b/src/shared/mkfs-util.h @@ -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, diff --git a/src/test/test-copy.c b/src/test/test-copy.c index 7204779041..9da7778655 100644 --- a/src/test/test-copy.c +++ b/src/test/test-copy.c @@ -1,6 +1,8 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include #include +#include #include #include @@ -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); diff --git a/src/test/test-loop-block.c b/src/test/test-loop-block.c index d59b4b27b1..b9a1ffd540 100644 --- a/src/test/test-loop-block.c +++ b/src/test/test-loop-block.c @@ -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);