// SPDX-License-Identifier: GPL-2.0+ /* * Elide and patch handling for 'fsverity setup' * * Copyright (C) 2018 Google LLC * * Written by Eric Biggers. */ #include <errno.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include "fsverity_uapi.h" #include "fsveritysetup.h" /* An elision or a patch */ struct fsverity_elide_patch { u64 offset; /* byte offset within the original data */ u64 length; /* length in bytes */ bool patch; /* false if elision, true if patch */ u8 data[]; /* replacement data (if patch=true) */ }; /* Maximum supported patch size, in bytes */ #define FS_VERITY_MAX_PATCH_SIZE 255 /* Parse an --elide=OFFSET,LENGTH option */ static struct fsverity_elide_patch *parse_elide_option(const char *optarg) { struct fsverity_elide_patch *ext = NULL; char *sep, *end; unsigned long long offset; unsigned long long length; sep = strchr(optarg, ','); if (!sep || sep == optarg) goto invalid; errno = 0; *sep = '\0'; offset = strtoull(optarg, &end, 10); *sep = ','; if (errno || end != sep) goto invalid; length = strtoull(sep + 1, &end, 10); if (errno || *end) goto invalid; if (length <= 0 || length > UINT64_MAX - offset) { error_msg("Invalid length in '--elide=%s'", optarg); return NULL; } ext = xzalloc(sizeof(*ext)); ext->offset = offset; ext->length = length; ext->patch = false; return ext; invalid: error_msg("Invalid --elide option: '%s'. Must be formatted as OFFSET,LENGTH", optarg); return NULL; } /* Parse a --patch=OFFSET,PATCHFILE option */ static struct fsverity_elide_patch *parse_patch_option(const char *optarg) { struct fsverity_elide_patch *ext = NULL; struct filedes patchfile = { .fd = -1 }; char *sep, *end; unsigned long long offset; u64 length; sep = strchr(optarg, ','); if (!sep || sep == optarg) goto invalid; errno = 0; *sep = '\0'; offset = strtoull(optarg, &end, 10); *sep = ','; if (errno || end != sep) goto invalid; if (!open_file(&patchfile, sep + 1, O_RDONLY, 0)) goto out; if (!get_file_size(&patchfile, &length)) goto out; if (length <= 0) { error_msg("patch file '%s' is empty", patchfile.name); goto out; } if (length > FS_VERITY_MAX_PATCH_SIZE) { error_msg("Patch file '%s' is too long. Max patch size is %d bytes.", patchfile.name, FS_VERITY_MAX_PATCH_SIZE); goto out; } ext = xzalloc(sizeof(*ext) + length); ext->offset = offset; ext->length = length; ext->patch = true; if (!full_read(&patchfile, ext->data, length)) { free(ext); ext = NULL; } out: filedes_close(&patchfile); return ext; invalid: error_msg("Invalid --patch option: '%s'. Must be formatted as OFFSET,PATCHFILE", optarg); goto out; } /* Sort by increasing offset */ static int cmp_elide_patch_exts(const void *_p1, const void *_p2) { const struct fsverity_elide_patch *ext1, *ext2; ext1 = *(const struct fsverity_elide_patch **)_p1; ext2 = *(const struct fsverity_elide_patch **)_p2; if (ext1->offset > ext2->offset) return 1; if (ext1->offset < ext2->offset) return -1; return 0; } /* * Given the lists of --elide and --patch options, validate and load the * elisions and patches into @params. */ bool load_elisions_and_patches(const struct string_list *elide_opts, const struct string_list *patch_opts, struct fsveritysetup_params *params) { const size_t num_exts = elide_opts->length + patch_opts->length; struct fsverity_elide_patch **exts; size_t i, j; if (num_exts == 0) /* Normal case: no elisions or patches */ return true; params->num_elisions_and_patches = num_exts; exts = xzalloc(num_exts * sizeof(exts[0])); params->elisions_and_patches = exts; j = 0; /* Parse the --elide options */ for (i = 0; i < elide_opts->length; i++) { exts[j] = parse_elide_option(elide_opts->strings[i]); if (!exts[j++]) return false; } /* Parse the --patch options */ for (i = 0; i < patch_opts->length; i++) { exts[j] = parse_patch_option(patch_opts->strings[i]); if (!exts[j++]) return false; } /* Sort the elisions and patches by increasing offset */ qsort(exts, num_exts, sizeof(exts[0]), cmp_elide_patch_exts); /* Verify that no elisions or patches overlap */ for (j = 1; j < num_exts; j++) { if (exts[j]->offset < exts[j - 1]->offset + exts[j - 1]->length) { error_msg("%s at [%"PRIu64", %"PRIu64") overlaps " "%s at [%"PRIu64", %"PRIu64")", exts[j - 1]->patch ? "Patch" : "Elision", exts[j - 1]->offset, exts[j - 1]->offset + exts[j - 1]->length, exts[j]->patch ? "patch" : "elision", exts[j]->offset, exts[j]->offset + exts[j]->length); return false; } } return true; } void free_elisions_and_patches(struct fsveritysetup_params *params) { size_t i; for (i = 0; i < params->num_elisions_and_patches; i++) free(params->elisions_and_patches[i]); free(params->elisions_and_patches); } /* * Given the original file @in of length @in_length bytes, create a temporary * file @out_ret and write to it the data with the elisions and patches applied, * with the end zero-padded to the next block boundary. Returns in * @out_length_ret the length of the elided/patched file in bytes. */ bool apply_elisions_and_patches(const struct fsveritysetup_params *params, struct filedes *in, u64 in_length, struct filedes *out_ret, u64 *out_length_ret) { struct fsverity_elide_patch **exts = params->elisions_and_patches; struct filedes *out = out_ret; size_t i; for (i = 0; i < params->num_elisions_and_patches; i++) { if (exts[i]->offset + exts[i]->length > in_length) { error_msg("%s at [%"PRIu64", %"PRIu64") extends beyond end of input file", exts[i]->patch ? "Patch" : "Elision", exts[i]->offset, exts[i]->offset + exts[i]->length); return false; } } if (!filedes_seek(in, 0, SEEK_SET)) return false; if (!open_tempfile(out)) return false; for (i = 0; i < params->num_elisions_and_patches; i++) { printf("Applying %s: offset=%"PRIu64", length=%"PRIu64"\n", exts[i]->patch ? "patch" : "elision", exts[i]->offset, exts[i]->length); if (!copy_file_data(in, out, exts[i]->offset - in->pos)) return false; if (exts[i]->patch && !full_write(out, exts[i]->data, exts[i]->length)) return false; if (!filedes_seek(in, exts[i]->length, SEEK_CUR)) return false; } if (!copy_file_data(in, out, in_length - in->pos)) return false; if (!write_zeroes(out, ALIGN(out->pos, params->blocksize) - out->pos)) return false; *out_length_ret = out->pos; return true; } /* Calculate the size the elisions and patches will take up when serialized */ size_t total_elide_patch_ext_length(const struct fsveritysetup_params *params) { size_t total = 0; size_t i; for (i = 0; i < params->num_elisions_and_patches; i++) { const struct fsverity_elide_patch *ext = params->elisions_and_patches[i]; size_t inner_len; if (ext->patch) { inner_len = sizeof(struct fsverity_extension_patch) + ext->length; } else { inner_len = sizeof(struct fsverity_extension_elide); } total += FSVERITY_EXTLEN(inner_len); } return total; } /* * Append the elide and patch extensions (if any) to the given buffer. * The buffer must have enough space; call total_elide_patch_ext_length() first. */ void append_elide_patch_exts(void **buf_p, const struct fsveritysetup_params *params) { void *buf = *buf_p; size_t i; union { struct { struct fsverity_extension_patch hdr; u8 data[FS_VERITY_MAX_PATCH_SIZE]; } patch; struct fsverity_extension_elide elide; } u; for (i = 0; i < params->num_elisions_and_patches; i++) { const struct fsverity_elide_patch *ext = params->elisions_and_patches[i]; int type; size_t extlen; if (ext->patch) { type = FS_VERITY_EXT_PATCH; u.patch.hdr.offset = cpu_to_le64(ext->offset); ASSERT(ext->length <= sizeof(u.patch.data)); memcpy(u.patch.data, ext->data, ext->length); extlen = sizeof(u.patch.hdr) + ext->length; } else { type = FS_VERITY_EXT_ELIDE; u.elide.offset = cpu_to_le64(ext->offset), u.elide.length = cpu_to_le64(ext->length); extlen = sizeof(u.elide); } fsverity_append_extension(&buf, type, &u, extlen); } *buf_p = buf; }