| // 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; |
| } |