// SPDX-License-Identifier: GPL-2.0 /* * Copyright 2018 Google LLC */ #include #include #include #include #include #include #include #include #include #include "format.h" #include "data_mgmt.h" struct backing_file_context *incfs_alloc_bfc(struct mount_info *mi, struct file *backing_file) { struct backing_file_context *result = NULL; result = kzalloc(sizeof(*result), GFP_NOFS); if (!result) return ERR_PTR(-ENOMEM); result->bc_file = get_file(backing_file); result->bc_cred = mi->mi_owner; mutex_init(&result->bc_mutex); return result; } void incfs_free_bfc(struct backing_file_context *bfc) { if (!bfc) return; if (bfc->bc_file) fput(bfc->bc_file); mutex_destroy(&bfc->bc_mutex); kfree(bfc); } static loff_t incfs_get_end_offset(struct file *f) { /* * This function assumes that file size and the end-offset * are the same. This is not always true. */ return i_size_read(file_inode(f)); } /* * Truncate the tail of the file to the given length. * Used to rollback partially successful multistep writes. */ static int truncate_backing_file(struct backing_file_context *bfc, loff_t new_end) { struct inode *inode = NULL; struct dentry *dentry = NULL; loff_t old_end = 0; struct iattr attr; int result = 0; if (!bfc) return -EFAULT; LOCK_REQUIRED(bfc->bc_mutex); if (!bfc->bc_file) return -EFAULT; old_end = incfs_get_end_offset(bfc->bc_file); if (old_end == new_end) return 0; if (old_end < new_end) return -EINVAL; inode = bfc->bc_file->f_inode; dentry = bfc->bc_file->f_path.dentry; attr.ia_size = new_end; attr.ia_valid = ATTR_SIZE; inode_lock(inode); result = notify_change(dentry, &attr, NULL); inode_unlock(inode); return result; } static int write_to_bf(struct backing_file_context *bfc, const void *buf, size_t count, loff_t pos) { ssize_t res = incfs_kwrite(bfc, buf, count, pos); if (res < 0) return res; if (res != count) return -EIO; return 0; } static int append_zeros_no_fallocate(struct backing_file_context *bfc, size_t file_size, size_t len) { u8 buffer[256] = {}; size_t i; for (i = 0; i < len; i += sizeof(buffer)) { int to_write = len - i > sizeof(buffer) ? sizeof(buffer) : len - i; int err = write_to_bf(bfc, buffer, to_write, file_size + i); if (err) return err; } return 0; } /* Append a given number of zero bytes to the end of the backing file. */ static int append_zeros(struct backing_file_context *bfc, size_t len) { loff_t file_size = 0; loff_t new_last_byte_offset = 0; int result; if (!bfc) return -EFAULT; if (len == 0) return 0; LOCK_REQUIRED(bfc->bc_mutex); /* * Allocate only one byte at the new desired end of the file. * It will increase file size and create a zeroed area of * a given size. */ file_size = incfs_get_end_offset(bfc->bc_file); new_last_byte_offset = file_size + len - 1; result = vfs_fallocate(bfc->bc_file, 0, new_last_byte_offset, 1); if (result != -EOPNOTSUPP) return result; return append_zeros_no_fallocate(bfc, file_size, len); } /* * Append a given metadata record to the backing file and update a previous * record to add the new record the the metadata list. */ static int append_md_to_backing_file(struct backing_file_context *bfc, struct incfs_md_header *record) { int result = 0; loff_t record_offset; loff_t file_pos; __le64 new_md_offset; size_t record_size; if (!bfc || !record) return -EFAULT; if (bfc->bc_last_md_record_offset < 0) return -EINVAL; LOCK_REQUIRED(bfc->bc_mutex); record_size = le16_to_cpu(record->h_record_size); file_pos = incfs_get_end_offset(bfc->bc_file); record->h_next_md_offset = 0; /* Write the metadata record to the end of the backing file */ record_offset = file_pos; new_md_offset = cpu_to_le64(record_offset); result = write_to_bf(bfc, record, record_size, file_pos); if (result) return result; /* Update next metadata offset in a previous record or a superblock. */ if (bfc->bc_last_md_record_offset) { /* * Find a place in the previous md record where new record's * offset needs to be saved. */ file_pos = bfc->bc_last_md_record_offset + offsetof(struct incfs_md_header, h_next_md_offset); } else { /* * No metadata yet, file a place to update in the * file_header. */ file_pos = offsetof(struct incfs_file_header, fh_first_md_offset); } result = write_to_bf(bfc, &new_md_offset, sizeof(new_md_offset), file_pos); if (result) return result; bfc->bc_last_md_record_offset = record_offset; return result; } /* * Reserve 0-filled space for the blockmap body, and append * incfs_blockmap metadata record pointing to it. */ int incfs_write_blockmap_to_backing_file(struct backing_file_context *bfc, u32 block_count) { struct incfs_blockmap blockmap = {}; int result = 0; loff_t file_end = 0; size_t map_size = block_count * sizeof(struct incfs_blockmap_entry); if (!bfc) return -EFAULT; blockmap.m_header.h_md_entry_type = INCFS_MD_BLOCK_MAP; blockmap.m_header.h_record_size = cpu_to_le16(sizeof(blockmap)); blockmap.m_header.h_next_md_offset = cpu_to_le64(0); blockmap.m_block_count = cpu_to_le32(block_count); LOCK_REQUIRED(bfc->bc_mutex); /* Reserve 0-filled space for the blockmap body in the backing file. */ file_end = incfs_get_end_offset(bfc->bc_file); result = append_zeros(bfc, map_size); if (result) return result; /* Write blockmap metadata record pointing to the body written above. */ blockmap.m_base_offset = cpu_to_le64(file_end); result = append_md_to_backing_file(bfc, &blockmap.m_header); if (result) /* Error, rollback file changes */ truncate_backing_file(bfc, file_end); return result; } int incfs_write_signature_to_backing_file(struct backing_file_context *bfc, struct mem_range sig, u32 tree_size, loff_t *tree_offset, loff_t *sig_offset) { struct incfs_file_signature sg = {}; int result = 0; loff_t rollback_pos = 0; loff_t tree_area_pos = 0; size_t alignment = 0; if (!bfc) return -EFAULT; LOCK_REQUIRED(bfc->bc_mutex); rollback_pos = incfs_get_end_offset(bfc->bc_file); sg.sg_header.h_md_entry_type = INCFS_MD_SIGNATURE; sg.sg_header.h_record_size = cpu_to_le16(sizeof(sg)); sg.sg_header.h_next_md_offset = cpu_to_le64(0); if (sig.data != NULL && sig.len > 0) { sg.sg_sig_size = cpu_to_le32(sig.len); sg.sg_sig_offset = cpu_to_le64(rollback_pos); result = write_to_bf(bfc, sig.data, sig.len, rollback_pos); if (result) goto err; } tree_area_pos = incfs_get_end_offset(bfc->bc_file); if (tree_size > 0) { if (tree_size > 5 * INCFS_DATA_FILE_BLOCK_SIZE) { /* * If hash tree is big enough, it makes sense to * align in the backing file for faster access. */ loff_t offset = round_up(tree_area_pos, PAGE_SIZE); alignment = offset - tree_area_pos; tree_area_pos = offset; } /* * If root hash is not the only hash in the tree. * reserve 0-filled space for the tree. */ result = append_zeros(bfc, tree_size + alignment); if (result) goto err; sg.sg_hash_tree_size = cpu_to_le32(tree_size); sg.sg_hash_tree_offset = cpu_to_le64(tree_area_pos); } /* Write a hash tree metadata record pointing to the hash tree above. */ result = append_md_to_backing_file(bfc, &sg.sg_header); err: if (result) /* Error, rollback file changes */ truncate_backing_file(bfc, rollback_pos); else { if (tree_offset) *tree_offset = tree_area_pos; if (sig_offset) *sig_offset = rollback_pos; } return result; } static int write_new_status_to_backing_file(struct backing_file_context *bfc, u32 data_blocks_written, u32 hash_blocks_written) { int result; loff_t rollback_pos; struct incfs_status is = { .is_header = { .h_md_entry_type = INCFS_MD_STATUS, .h_record_size = cpu_to_le16(sizeof(is)), }, .is_data_blocks_written = cpu_to_le32(data_blocks_written), .is_hash_blocks_written = cpu_to_le32(hash_blocks_written), }; LOCK_REQUIRED(bfc->bc_mutex); rollback_pos = incfs_get_end_offset(bfc->bc_file); result = append_md_to_backing_file(bfc, &is.is_header); if (result) truncate_backing_file(bfc, rollback_pos); return result; } int incfs_write_status_to_backing_file(struct backing_file_context *bfc, loff_t status_offset, u32 data_blocks_written, u32 hash_blocks_written) { struct incfs_status is; int result; if (!bfc) return -EFAULT; if (status_offset == 0) return write_new_status_to_backing_file(bfc, data_blocks_written, hash_blocks_written); result = incfs_kread(bfc, &is, sizeof(is), status_offset); if (result != sizeof(is)) return -EIO; is.is_data_blocks_written = cpu_to_le32(data_blocks_written); is.is_hash_blocks_written = cpu_to_le32(hash_blocks_written); result = incfs_kwrite(bfc, &is, sizeof(is), status_offset); if (result != sizeof(is)) return -EIO; return 0; } int incfs_write_verity_signature_to_backing_file( struct backing_file_context *bfc, struct mem_range signature, loff_t *offset) { struct incfs_file_verity_signature vs = {}; int result; loff_t pos; /* No verity signature section is equivalent to an empty section */ if (signature.data == NULL || signature.len == 0) return 0; pos = incfs_get_end_offset(bfc->bc_file); vs = (struct incfs_file_verity_signature) { .vs_header = (struct incfs_md_header) { .h_md_entry_type = INCFS_MD_VERITY_SIGNATURE, .h_record_size = cpu_to_le16(sizeof(vs)), .h_next_md_offset = cpu_to_le64(0), }, .vs_size = cpu_to_le32(signature.len), .vs_offset = cpu_to_le64(pos), }; result = write_to_bf(bfc, signature.data, signature.len, pos); if (result) goto err; result = append_md_to_backing_file(bfc, &vs.vs_header); if (result) goto err; *offset = pos; err: if (result) /* Error, rollback file changes */ truncate_backing_file(bfc, pos); return result; } /* * Write a backing file header * It should always be called only on empty file. * fh.fh_first_md_offset is 0 for now, but will be updated * once first metadata record is added. */ int incfs_write_fh_to_backing_file(struct backing_file_context *bfc, incfs_uuid_t *uuid, u64 file_size) { struct incfs_file_header fh = {}; loff_t file_pos = 0; if (!bfc) return -EFAULT; fh.fh_magic = cpu_to_le64(INCFS_MAGIC_NUMBER); fh.fh_version = cpu_to_le64(INCFS_FORMAT_CURRENT_VER); fh.fh_header_size = cpu_to_le16(sizeof(fh)); fh.fh_first_md_offset = cpu_to_le64(0); fh.fh_data_block_size = cpu_to_le16(INCFS_DATA_FILE_BLOCK_SIZE); fh.fh_file_size = cpu_to_le64(file_size); fh.fh_uuid = *uuid; LOCK_REQUIRED(bfc->bc_mutex); file_pos = incfs_get_end_offset(bfc->bc_file); if (file_pos != 0) return -EEXIST; return write_to_bf(bfc, &fh, sizeof(fh), file_pos); } /* * Write a backing file header for a mapping file * It should always be called only on empty file. */ int incfs_write_mapping_fh_to_backing_file(struct backing_file_context *bfc, incfs_uuid_t *uuid, u64 file_size, u64 offset) { struct incfs_file_header fh = {}; loff_t file_pos = 0; if (!bfc) return -EFAULT; fh.fh_magic = cpu_to_le64(INCFS_MAGIC_NUMBER); fh.fh_version = cpu_to_le64(INCFS_FORMAT_CURRENT_VER); fh.fh_header_size = cpu_to_le16(sizeof(fh)); fh.fh_original_offset = cpu_to_le64(offset); fh.fh_data_block_size = cpu_to_le16(INCFS_DATA_FILE_BLOCK_SIZE); fh.fh_mapped_file_size = cpu_to_le64(file_size); fh.fh_original_uuid = *uuid; fh.fh_flags = cpu_to_le32(INCFS_FILE_MAPPED); LOCK_REQUIRED(bfc->bc_mutex); file_pos = incfs_get_end_offset(bfc->bc_file); if (file_pos != 0) return -EEXIST; return write_to_bf(bfc, &fh, sizeof(fh), file_pos); } /* Write a given data block and update file's blockmap to point it. */ int incfs_write_data_block_to_backing_file(struct backing_file_context *bfc, struct mem_range block, int block_index, loff_t bm_base_off, u16 flags) { struct incfs_blockmap_entry bm_entry = {}; int result = 0; loff_t data_offset = 0; loff_t bm_entry_off = bm_base_off + sizeof(struct incfs_blockmap_entry) * block_index; if (!bfc) return -EFAULT; if (block.len >= (1 << 16) || block_index < 0) return -EINVAL; LOCK_REQUIRED(bfc->bc_mutex); data_offset = incfs_get_end_offset(bfc->bc_file); if (data_offset <= bm_entry_off) { /* Blockmap entry is beyond the file's end. It is not normal. */ return -EINVAL; } /* Write the block data at the end of the backing file. */ result = write_to_bf(bfc, block.data, block.len, data_offset); if (result) return result; /* Update the blockmap to point to the newly written data. */ bm_entry.me_data_offset_lo = cpu_to_le32((u32)data_offset); bm_entry.me_data_offset_hi = cpu_to_le16((u16)(data_offset >> 32)); bm_entry.me_data_size = cpu_to_le16((u16)block.len); bm_entry.me_flags = cpu_to_le16(flags); return write_to_bf(bfc, &bm_entry, sizeof(bm_entry), bm_entry_off); } int incfs_write_hash_block_to_backing_file(struct backing_file_context *bfc, struct mem_range block, int block_index, loff_t hash_area_off, loff_t bm_base_off, loff_t file_size) { struct incfs_blockmap_entry bm_entry = {}; int result; loff_t data_offset = 0; loff_t file_end = 0; loff_t bm_entry_off = bm_base_off + sizeof(struct incfs_blockmap_entry) * (block_index + get_blocks_count_for_size(file_size)); if (!bfc) return -EFAULT; LOCK_REQUIRED(bfc->bc_mutex); data_offset = hash_area_off + block_index * INCFS_DATA_FILE_BLOCK_SIZE; file_end = incfs_get_end_offset(bfc->bc_file); if (data_offset + block.len > file_end) { /* Block is located beyond the file's end. It is not normal. */ return -EINVAL; } result = write_to_bf(bfc, block.data, block.len, data_offset); if (result) return result; bm_entry.me_data_offset_lo = cpu_to_le32((u32)data_offset); bm_entry.me_data_offset_hi = cpu_to_le16((u16)(data_offset >> 32)); bm_entry.me_data_size = cpu_to_le16(INCFS_DATA_FILE_BLOCK_SIZE); return write_to_bf(bfc, &bm_entry, sizeof(bm_entry), bm_entry_off); } int incfs_read_blockmap_entry(struct backing_file_context *bfc, int block_index, loff_t bm_base_off, struct incfs_blockmap_entry *bm_entry) { int error = incfs_read_blockmap_entries(bfc, bm_entry, block_index, 1, bm_base_off); if (error < 0) return error; if (error == 0) return -EIO; if (error != 1) return -EFAULT; return 0; } int incfs_read_blockmap_entries(struct backing_file_context *bfc, struct incfs_blockmap_entry *entries, int start_index, int blocks_number, loff_t bm_base_off) { loff_t bm_entry_off = bm_base_off + sizeof(struct incfs_blockmap_entry) * start_index; const size_t bytes_to_read = sizeof(struct incfs_blockmap_entry) * blocks_number; int result = 0; if (!bfc || !entries) return -EFAULT; if (start_index < 0 || bm_base_off <= 0) return -ENODATA; result = incfs_kread(bfc, entries, bytes_to_read, bm_entry_off); if (result < 0) return result; return result / sizeof(*entries); } int incfs_read_file_header(struct backing_file_context *bfc, loff_t *first_md_off, incfs_uuid_t *uuid, u64 *file_size, u32 *flags) { ssize_t bytes_read = 0; struct incfs_file_header fh = {}; if (!bfc || !first_md_off) return -EFAULT; bytes_read = incfs_kread(bfc, &fh, sizeof(fh), 0); if (bytes_read < 0) return bytes_read; if (bytes_read < sizeof(fh)) return -EBADMSG; if (le64_to_cpu(fh.fh_magic) != INCFS_MAGIC_NUMBER) return -EILSEQ; if (le64_to_cpu(fh.fh_version) > INCFS_FORMAT_CURRENT_VER) return -EILSEQ; if (le16_to_cpu(fh.fh_data_block_size) != INCFS_DATA_FILE_BLOCK_SIZE) return -EILSEQ; if (le16_to_cpu(fh.fh_header_size) != sizeof(fh)) return -EILSEQ; if (first_md_off) *first_md_off = le64_to_cpu(fh.fh_first_md_offset); if (uuid) *uuid = fh.fh_uuid; if (file_size) *file_size = le64_to_cpu(fh.fh_file_size); if (flags) *flags = le32_to_cpu(fh.fh_flags); return 0; } /* * Read through metadata records from the backing file one by one * and call provided metadata handlers. */ int incfs_read_next_metadata_record(struct backing_file_context *bfc, struct metadata_handler *handler) { const ssize_t max_md_size = INCFS_MAX_METADATA_RECORD_SIZE; ssize_t bytes_read = 0; size_t md_record_size = 0; loff_t next_record = 0; int res = 0; struct incfs_md_header *md_hdr = NULL; if (!bfc || !handler) return -EFAULT; if (handler->md_record_offset == 0) return -EPERM; memset(&handler->md_buffer, 0, max_md_size); bytes_read = incfs_kread(bfc, &handler->md_buffer, max_md_size, handler->md_record_offset); if (bytes_read < 0) return bytes_read; if (bytes_read < sizeof(*md_hdr)) return -EBADMSG; md_hdr = &handler->md_buffer.md_header; next_record = le64_to_cpu(md_hdr->h_next_md_offset); md_record_size = le16_to_cpu(md_hdr->h_record_size); if (md_record_size > max_md_size) { pr_warn("incfs: The record is too large. Size: %zu", md_record_size); return -EBADMSG; } if (bytes_read < md_record_size) { pr_warn("incfs: The record hasn't been fully read."); return -EBADMSG; } if (next_record <= handler->md_record_offset && next_record != 0) { pr_warn("incfs: Next record (%lld) points back in file.", next_record); return -EBADMSG; } switch (md_hdr->h_md_entry_type) { case INCFS_MD_NONE: break; case INCFS_MD_BLOCK_MAP: if (handler->handle_blockmap) res = handler->handle_blockmap( &handler->md_buffer.blockmap, handler); break; case INCFS_MD_FILE_ATTR: /* * File attrs no longer supported, ignore section for * compatibility */ break; case INCFS_MD_SIGNATURE: if (handler->handle_signature) res = handler->handle_signature( &handler->md_buffer.signature, handler); break; case INCFS_MD_STATUS: if (handler->handle_status) res = handler->handle_status( &handler->md_buffer.status, handler); break; case INCFS_MD_VERITY_SIGNATURE: if (handler->handle_verity_signature) res = handler->handle_verity_signature( &handler->md_buffer.verity_signature, handler); break; default: res = -ENOTSUPP; break; } if (!res) { if (next_record == 0) { /* * Zero offset for the next record means that the last * metadata record has just been processed. */ bfc->bc_last_md_record_offset = handler->md_record_offset; } handler->md_prev_record_offset = handler->md_record_offset; handler->md_record_offset = next_record; } return res; } ssize_t incfs_kread(struct backing_file_context *bfc, void *buf, size_t size, loff_t pos) { const struct cred *old_cred = override_creds(bfc->bc_cred); int ret = kernel_read(bfc->bc_file, buf, size, &pos); revert_creds(old_cred); return ret; } ssize_t incfs_kwrite(struct backing_file_context *bfc, const void *buf, size_t size, loff_t pos) { const struct cred *old_cred = override_creds(bfc->bc_cred); int ret = kernel_write(bfc->bc_file, buf, size, &pos); revert_creds(old_cred); return ret; }