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