/* cbfstool, CLI utility for CBFS file manipulation */ /* SPDX-License-Identifier: GPL-2.0-only */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <strings.h> #include <ctype.h> #include <unistd.h> #include <getopt.h> #include "common.h" #include "cbfs.h" #include "cbfs_image.h" #include "cbfs_sections.h" #include "elfparsing.h" #include "partitioned_file.h" #include "lz4/lib/xxhash.h" #include <commonlib/bsd/cbfs_private.h> #include <commonlib/bsd/compression.h> #include <commonlib/bsd/metadata_hash.h> #include <commonlib/fsp.h> #include <commonlib/endian.h> #include <commonlib/helpers.h> #include <commonlib/region.h> #include <vboot_host.h> struct command { const char *name; const char *optstring; int (*function) (void); // Whether to populate param.image_region before invoking function bool accesses_region; // This set to true means two things: // - in case of a command operating on a region, the region's contents // will be written back to image_file at the end // - write access to the file is required bool modifies_region; }; static struct param { partitioned_file_t *image_file; struct buffer *image_region; const char *name; const char *filename; const char *fmap; const char *region_name; const char *source_region; const char *bootblock; const char *ignore_sections; const char *ucode_region; uint64_t u64val; uint32_t type; uint32_t baseaddress; /* * Input can be negative. It will be transformed to offset from start of region (if * negative) and stored in baseaddress. */ long long int baseaddress_input; uint32_t baseaddress_assigned; uint32_t loadaddress; uint32_t headeroffset; /* * Input can be negative. It will be transformed to offset from start of region (if * negative) and stored in baseaddress. */ long long int headeroffset_input; uint32_t headeroffset_assigned; uint32_t entrypoint; uint32_t size; uint32_t alignment; uint32_t pagesize; uint32_t cbfsoffset; /* * Input can be negative. It will be transformed to corresponding region offset (if * negative) and stored in baseaddress. */ long long int cbfsoffset_input; uint32_t cbfsoffset_assigned; uint32_t arch; uint32_t padding; uint32_t topswap_size; bool u64val_assigned; bool fill_partial_upward; bool fill_partial_downward; bool show_immutable; bool stage_xip; bool force_pow2_pagesize; bool autogen_attr; bool machine_parseable; bool unprocessed; bool ibb; enum cbfs_compression compression; int precompression; enum vb2_hash_algorithm hash; /* For linux payloads */ char *initrd; char *cmdline; int force; /* * Base and size of extended window for decoding SPI flash greater than 16MiB in host * address space on x86 platforms. The assumptions here are: * 1. Top 16MiB is still decoded in the fixed decode window just below 4G boundary. * 2. Rest of the SPI flash below the top 16MiB is mapped at the top of extended * window. Even though the platform might support a larger extended window, the SPI * flash part used by the mainboard might not be large enough to be mapped in the entire * window. In such cases, the mapping is assumed to be in the top part of the extended * window with the bottom part remaining unused. */ uint32_t ext_win_base; uint32_t ext_win_size; } param = { /* All variables not listed are initialized as zero. */ .arch = CBFS_ARCHITECTURE_UNKNOWN, .compression = CBFS_COMPRESS_NONE, .hash = VB2_HASH_INVALID, .headeroffset = HEADER_OFFSET_UNKNOWN, .region_name = SECTION_NAME_PRIMARY_CBFS, .u64val = -1, }; /* * This "metadata_hash cache" caches the value and location of the CBFS metadata * hash embedded in the bootblock when CBFS verification is enabled. The first * call to get_mh_cache() searches for the cache by scanning the whole bootblock * for its 8-byte signature, later calls will just return the previously found * information again. If the cbfs_hash.algo member in the result is * VB2_HASH_INVALID, that means no metadata hash was found and this image does * not use CBFS verification. */ struct mh_cache { const char *region; size_t offset; struct vb2_hash cbfs_hash; platform_fixup_func fixup; bool initialized; }; static struct mh_cache *get_mh_cache(void) { static struct mh_cache mhc; if (mhc.initialized) return &mhc; mhc.initialized = true; const struct fmap *fmap = partitioned_file_get_fmap(param.image_file); if (!fmap) goto no_metadata_hash; /* Find the metadata_hash container. If there is a "BOOTBLOCK" FMAP section, it's there. If not, it's a normal file in the primary CBFS section. */ size_t offset, size; struct buffer buffer; if (fmap_find_area(fmap, SECTION_NAME_BOOTBLOCK)) { if (!partitioned_file_read_region(&buffer, param.image_file, SECTION_NAME_BOOTBLOCK)) goto no_metadata_hash; mhc.region = SECTION_NAME_BOOTBLOCK; offset = 0; size = buffer.size; } else { struct cbfs_image cbfs; struct cbfs_file *mh_container; if (!partitioned_file_read_region(&buffer, param.image_file, SECTION_NAME_PRIMARY_CBFS)) goto no_metadata_hash; mhc.region = SECTION_NAME_PRIMARY_CBFS; if (cbfs_image_from_buffer(&cbfs, &buffer, param.headeroffset)) goto no_metadata_hash; mh_container = cbfs_get_entry(&cbfs, "bootblock"); if (!mh_container || be32toh(mh_container->type) != CBFS_TYPE_BOOTBLOCK) { /* Check for apu/amdfw file */ mh_container = cbfs_get_entry(&cbfs, "apu/amdfw"); if (!mh_container || be32toh(mh_container->type) != CBFS_TYPE_AMDFW) goto no_metadata_hash; } offset = (void *)mh_container + be32toh(mh_container->offset) - buffer_get(&cbfs.buffer); size = be32toh(mh_container->len); } /* Find and validate the metadata hash anchor inside the containing file and record its exact byte offset from the start of the FMAP region. */ struct metadata_hash_anchor *anchor = memmem(buffer_get(&buffer) + offset, size, METADATA_HASH_ANCHOR_MAGIC, sizeof(anchor->magic)); if (anchor) { if (!vb2_digest_size(anchor->cbfs_hash.algo)) { ERROR("Unknown CBFS metadata hash type: %d\n", anchor->cbfs_hash.algo); goto no_metadata_hash; } mhc.cbfs_hash = anchor->cbfs_hash; mhc.offset = (void *)anchor - buffer_get(&buffer); mhc.fixup = platform_fixups_probe(&buffer, mhc.offset, mhc.region); return &mhc; } no_metadata_hash: mhc.cbfs_hash.algo = VB2_HASH_INVALID; return &mhc; } static void update_and_info(const char *name, void *dst, void *src, size_t size) { if (!memcmp(dst, src, size)) return; char *src_str = bintohex(src, size); char *dst_str = bintohex(dst, size); INFO("Updating %s from %s to %s\n", name, dst_str, src_str); memcpy(dst, src, size); free(src_str); free(dst_str); } static int update_anchor(struct mh_cache *mhc, uint8_t *fmap_hash) { struct buffer buffer; if (!partitioned_file_read_region(&buffer, param.image_file, mhc->region)) return -1; struct metadata_hash_anchor *anchor = buffer_get(&buffer) + mhc->offset; /* The metadata hash anchor should always still be where we left it. */ assert(!memcmp(anchor->magic, METADATA_HASH_ANCHOR_MAGIC, sizeof(anchor->magic)) && anchor->cbfs_hash.algo == mhc->cbfs_hash.algo); update_and_info("CBFS metadata hash", anchor->cbfs_hash.raw, mhc->cbfs_hash.raw, vb2_digest_size(anchor->cbfs_hash.algo)); if (fmap_hash) { update_and_info("FMAP hash", metadata_hash_anchor_fmap_hash(anchor), fmap_hash, vb2_digest_size(anchor->cbfs_hash.algo)); } if (mhc->fixup && mhc->fixup(&buffer, mhc->offset) != 0) return -1; if (!partitioned_file_write_region(param.image_file, &buffer)) return -1; return 0; } /* This should be called after every time CBFS metadata might have changed. It will recalculate and update the metadata hash in the bootblock if needed. */ static int maybe_update_metadata_hash(struct cbfs_image *cbfs) { if (strcmp(param.region_name, SECTION_NAME_PRIMARY_CBFS)) return 0; /* Metadata hash only embedded in primary CBFS. */ struct mh_cache *mhc = get_mh_cache(); if (mhc->cbfs_hash.algo == VB2_HASH_INVALID) return 0; enum cb_err err = cbfs_walk(cbfs, NULL, NULL, &mhc->cbfs_hash, CBFS_WALK_WRITEBACK_HASH); if (err != CB_CBFS_NOT_FOUND) { ERROR("Unexpected cbfs_walk() error %d\n", err); return -1; } return update_anchor(mhc, NULL); } /* This should be called after every time the FMAP or the bootblock itself might have changed, and will write the new FMAP hash into the metadata hash anchor in the bootblock if required (usually when the bootblock is first added). */ static int maybe_update_fmap_hash(void) { if (strcmp(param.region_name, SECTION_NAME_BOOTBLOCK) && strcmp(param.region_name, SECTION_NAME_FMAP) && param.type != CBFS_TYPE_BOOTBLOCK && param.type != CBFS_TYPE_AMDFW) return 0; /* FMAP and bootblock didn't change. */ struct mh_cache *mhc = get_mh_cache(); if (mhc->cbfs_hash.algo == VB2_HASH_INVALID) return 0; struct vb2_hash fmap_hash; const struct fmap *fmap = partitioned_file_get_fmap(param.image_file); if (!fmap || vb2_hash_calculate(false, fmap, fmap_size(fmap), mhc->cbfs_hash.algo, &fmap_hash)) return -1; return update_anchor(mhc, fmap_hash.raw); } static bool verification_exclude(enum cbfs_type type) { switch (type) { case CBFS_TYPE_BOOTBLOCK: case CBFS_TYPE_CBFSHEADER: case CBFS_TYPE_INTEL_FIT: case CBFS_TYPE_AMDFW: return true; default: return false; } } static bool region_is_flashmap(const char *region) { return partitioned_file_region_check_magic(param.image_file, region, FMAP_SIGNATURE, strlen(FMAP_SIGNATURE)); } /* @return Same as cbfs_is_valid_cbfs(), but for a named region. */ static bool region_is_modern_cbfs(const char *region) { return partitioned_file_region_check_magic(param.image_file, region, CBFS_FILE_MAGIC, strlen(CBFS_FILE_MAGIC)); } /* This describes a window from the SPI flash address space into the host address space. */ struct mmap_window { struct region flash_space; struct region host_space; }; /* Should be enough for now */ #define MMAP_MAX_WINDOWS 3 /* Table of all the decode windows supported by the platform. */ static int mmap_window_table_size; static struct mmap_window mmap_window_table[MMAP_MAX_WINDOWS]; static void add_mmap_window(size_t flash_offset, size_t host_offset, size_t window_size) { if (mmap_window_table_size >= MMAP_MAX_WINDOWS) { ERROR("Too many memory map windows\n"); return; } mmap_window_table[mmap_window_table_size].flash_space.offset = flash_offset; mmap_window_table[mmap_window_table_size].host_space.offset = host_offset; mmap_window_table[mmap_window_table_size].flash_space.size = window_size; mmap_window_table[mmap_window_table_size].host_space.size = window_size; mmap_window_table_size++; } static int decode_mmap_arg(char *arg) { if (arg == NULL) return 1; union { unsigned long int array[3]; struct { unsigned long int flash_base; unsigned long int mmap_base; unsigned long int mmap_size; }; } mmap_args; char *suffix = NULL; char *substring = strtok(arg, ":"); for (size_t i = 0; i < ARRAY_SIZE(mmap_args.array); i++) { if (!substring) { ERROR("Invalid mmap arguments '%s'.\n", arg); return 1; } mmap_args.array[i] = strtol(substring, &suffix, 0); if (suffix && *suffix) { ERROR("Invalid mmap arguments '%s'.\n", arg); return 1; } substring = strtok(NULL, ":"); } if (substring != NULL) { ERROR("Invalid argument, too many substrings '%s'.\n", arg); return 1; } add_mmap_window(mmap_args.flash_base, mmap_args.mmap_base, mmap_args.mmap_size); return 0; } #define DEFAULT_DECODE_WINDOW_TOP (4ULL * GiB) #define DEFAULT_DECODE_WINDOW_MAX_SIZE (16 * MiB) static bool create_mmap_windows(void) { static bool done; if (done) return done; // No memory map provided, use a default one if (mmap_window_table_size == 0) { const size_t image_size = partitioned_file_total_size(param.image_file); printf("Image SIZE %zu\n", image_size); const size_t std_window_size = MIN(DEFAULT_DECODE_WINDOW_MAX_SIZE, image_size); const size_t std_window_flash_offset = image_size - std_window_size; /* * Default decode window lives just below 4G boundary in host space and maps up to a * maximum of 16MiB. If the window is smaller than 16MiB, the SPI flash window is mapped * at the top of the host window just below 4G. */ add_mmap_window(std_window_flash_offset, DEFAULT_DECODE_WINDOW_TOP - std_window_size, std_window_size); } else { /* * Check provided memory map */ for (int i = 0; i < mmap_window_table_size; i++) { for (int j = i + 1; j < mmap_window_table_size; j++) { if (region_overlap(&mmap_window_table[i].flash_space, &mmap_window_table[j].flash_space)) { ERROR("Flash space windows (base=0x%zx, limit=0x%zx) and (base=0x%zx, limit=0x%zx) overlap!\n", region_offset(&mmap_window_table[i].flash_space), region_end(&mmap_window_table[i].flash_space), region_offset(&mmap_window_table[j].flash_space), region_end(&mmap_window_table[j].flash_space)); return false; } if (region_overlap(&mmap_window_table[i].host_space, &mmap_window_table[j].host_space)) { ERROR("Host space windows (base=0x%zx, limit=0x%zx) and (base=0x%zx, limit=0x%zx) overlap!\n", region_offset(&mmap_window_table[i].flash_space), region_end(&mmap_window_table[i].flash_space), region_offset(&mmap_window_table[j].flash_space), region_end(&mmap_window_table[j].flash_space)); return false; } } } } done = true; return done; } static unsigned int convert_address(const struct region *to, const struct region *from, unsigned int addr) { /* * Calculate the offset in the "from" region and use that offset to calculate * corresponding address in the "to" region. */ size_t offset = addr - region_offset(from); return region_offset(to) + offset; } enum mmap_addr_type { HOST_SPACE_ADDR, FLASH_SPACE_ADDR, }; static int find_mmap_window(enum mmap_addr_type addr_type, unsigned int addr) { size_t i; for (i = 0; i < ARRAY_SIZE(mmap_window_table); i++) { const struct region *reg; if (addr_type == HOST_SPACE_ADDR) reg = &mmap_window_table[i].host_space; else reg = &mmap_window_table[i].flash_space; if (region_offset(reg) <= addr && ((uint64_t)region_offset(reg) + (uint64_t)region_sz(reg) - 1) >= addr) return i; } return -1; } static unsigned int convert_host_to_flash(const struct buffer *region, unsigned int addr) { int idx; const struct region *to, *from; idx = find_mmap_window(HOST_SPACE_ADDR, addr); if (idx == -1) { ERROR("Host address(%x) not in any mmap window!\n", addr); return 0; } to = &mmap_window_table[idx].flash_space; from = &mmap_window_table[idx].host_space; /* region->offset is subtracted because caller expects offset in the given region. */ return convert_address(to, from, addr) - region->offset; } static unsigned int convert_flash_to_host(const struct buffer *region, unsigned int addr) { int idx; const struct region *to, *from; /* * region->offset is added because caller provides offset in the given region. This is * converted to an absolute address in the SPI flash space. This is done before the * conversion as opposed to after in convert_host_to_flash() above because the address * is actually an offset within the region. So, it needs to be converted into an * absolute address in the SPI flash space before converting into an address in host * space. */ addr += region->offset; idx = find_mmap_window(FLASH_SPACE_ADDR, addr); if (idx == -1) { ERROR("SPI flash address(%x) not in any mmap window!\n", addr); return 0; } to = &mmap_window_table[idx].host_space; from = &mmap_window_table[idx].flash_space; return convert_address(to, from, addr); } static unsigned int convert_addr_space(const struct buffer *region, unsigned int addr) { assert(region); assert(create_mmap_windows()); if (IS_HOST_SPACE_ADDRESS(addr)) return convert_host_to_flash(region, addr); else return convert_flash_to_host(region, addr); } /* * This function takes offset value which represents the offset from one end of the region and * converts it to offset from the other end of the region. offset is expected to be positive. */ static int convert_region_offset(unsigned int offset, uint32_t *region_offset) { size_t size; if (param.size) { size = param.size; } else { assert(param.image_region); size = param.image_region->size; } if (size < offset) { ERROR("Cannot convert region offset (size=0x%zx, offset=0x%x)\n", size, offset); return 1; } *region_offset = size - offset; return 0; } static int do_cbfs_locate(uint32_t *cbfs_addr, size_t data_size) { uint32_t metadata_size = 0; if (!param.name) { ERROR("You need to specify -n/--name.\n"); return 1; } struct cbfs_image image; if (cbfs_image_from_buffer(&image, param.image_region, param.headeroffset)) return 1; if (cbfs_get_entry(&image, param.name)) WARN("'%s' already in CBFS.\n", param.name); if (!data_size) { ERROR("File '%s' is empty?\n", param.name); return 1; } /* Compute required page size */ if (param.force_pow2_pagesize) { param.pagesize = 1; while (param.pagesize < data_size) param.pagesize <<= 1; DEBUG("Page size is %d (0x%x)\n", param.pagesize, param.pagesize); } /* Include cbfs_file size along with space for with name. */ metadata_size += cbfs_calculate_file_header_size(param.name); /* Adjust metadata_size if additional attributes were added */ if (param.autogen_attr) { if (param.alignment) metadata_size += sizeof(struct cbfs_file_attr_align); if (param.baseaddress_assigned || param.stage_xip) metadata_size += sizeof(struct cbfs_file_attr_position); } if (param.precompression || param.compression != CBFS_COMPRESS_NONE) metadata_size += sizeof(struct cbfs_file_attr_compression); if (param.type == CBFS_TYPE_STAGE) metadata_size += sizeof(struct cbfs_file_attr_stageheader); /* Take care of the hash attribute if it is used */ if (param.hash != VB2_HASH_INVALID) metadata_size += cbfs_file_attr_hash_size(param.hash); int32_t address = cbfs_locate_entry(&image, data_size, param.pagesize, param.alignment, metadata_size); if (address < 0) { ERROR("'%s'(%u + %zu) can't fit in CBFS for page-size %#x, align %#x.\n", param.name, metadata_size, data_size, param.pagesize, param.alignment); return 1; } *cbfs_addr = address; return 0; } typedef int (*convert_buffer_t)(struct buffer *buffer, uint32_t *offset, struct cbfs_file *header); static int cbfs_add_integer_component(const char *name, uint64_t u64val, uint32_t offset, uint32_t headeroffset) { struct cbfs_image image; struct cbfs_file *header = NULL; struct buffer buffer; int i, ret = 1; if (!name) { ERROR("You need to specify -n/--name.\n"); return 1; } if (buffer_create(&buffer, 8, name) != 0) return 1; for (i = 0; i < 8; i++) buffer.data[i] = (u64val >> i*8) & 0xff; if (cbfs_image_from_buffer(&image, param.image_region, headeroffset)) { ERROR("Selected image region is not a CBFS.\n"); goto done; } if (cbfs_get_entry(&image, name)) { ERROR("'%s' already in ROM image.\n", name); goto done; } header = cbfs_create_file_header(CBFS_TYPE_RAW, buffer.size, name); if (!header) goto done; enum vb2_hash_algorithm algo = get_mh_cache()->cbfs_hash.algo; if (algo != VB2_HASH_INVALID) if (cbfs_add_file_hash(header, &buffer, algo)) { ERROR("couldn't add hash for '%s'\n", name); goto done; } if (cbfs_add_entry(&image, &buffer, offset, header, 0) != 0) { ERROR("Failed to add %llu into ROM image as '%s'.\n", (long long unsigned)u64val, name); goto done; } ret = maybe_update_metadata_hash(&image); done: free(header); buffer_delete(&buffer); return ret; } static int is_valid_topswap(void) { switch (param.topswap_size) { case (64 * KiB): case (128 * KiB): case (256 * KiB): case (512 * KiB): case (1 * MiB): break; default: ERROR("Invalid topswap_size %d, topswap can be 64K|128K|256K|512K|1M\n", param.topswap_size); return 0; } return 1; } static void fill_header_offset(void *location, uint32_t offset) { // TODO: When we have a BE target, we'll need to store this as BE write_le32(location, offset); } static int update_master_header_loc_topswap(struct cbfs_image *image, void *h_loc, uint32_t header_offset) { struct cbfs_file *entry; void *ts_h_loc = h_loc; entry = cbfs_get_entry(image, "bootblock"); if (entry == NULL) { ERROR("Bootblock not in ROM image?!?\n"); return 1; } /* * Check if the existing topswap boundary matches with * the one provided. */ if (param.topswap_size != be32toh(entry->len)/2) { ERROR("Top swap boundary does not match\n"); return 1; } ts_h_loc -= param.topswap_size; fill_header_offset(ts_h_loc, header_offset); return 0; } static int cbfs_add_master_header(void) { const char * const name = "cbfs master header"; struct cbfs_image image; struct cbfs_file *header = NULL; struct buffer buffer; int ret = 1; size_t offset; size_t size; void *h_loc; if (cbfs_image_from_buffer(&image, param.image_region, param.headeroffset)) { ERROR("Selected image region is not a CBFS.\n"); return 1; } if (cbfs_get_entry(&image, name)) { ERROR("'%s' already in ROM image.\n", name); return 1; } if (buffer_create(&buffer, sizeof(struct cbfs_header), name) != 0) return 1; struct cbfs_header *h = (struct cbfs_header *)buffer.data; h->magic = htobe32(CBFS_HEADER_MAGIC); h->version = htobe32(CBFS_HEADER_VERSION); /* The 4 bytes are left out for two reasons: * 1. the cbfs master header pointer resides there * 2. some cbfs implementations assume that an image that resides * below 4GB has a bootblock and get confused when the end of the * image is at 4GB == 0. */ h->bootblocksize = htobe32(4); h->align = htobe32(CBFS_ALIGNMENT); /* The offset and romsize fields within the master header are absolute * values within the boot media. As such, romsize needs to relfect * the end 'offset' for a CBFS. To achieve that the current buffer * representing the CBFS region's size is added to the offset of * the region within a larger image. */ offset = buffer_get(param.image_region) - buffer_get_original_backing(param.image_region); size = buffer_size(param.image_region); h->romsize = htobe32(size + offset); h->offset = htobe32(offset); h->architecture = htobe32(CBFS_ARCHITECTURE_UNKNOWN); /* Never add a hash attribute to the master header. */ header = cbfs_create_file_header(CBFS_TYPE_CBFSHEADER, buffer_size(&buffer), name); if (!header) goto done; if (cbfs_add_entry(&image, &buffer, 0, header, 0) != 0) { ERROR("Failed to add cbfs master header into ROM image.\n"); goto done; } struct cbfs_file *entry; if ((entry = cbfs_get_entry(&image, name)) == NULL) { ERROR("'%s' not in ROM image?!?\n", name); goto done; } uint32_t header_offset = CBFS_SUBHEADER(entry) - buffer_get(&image.buffer); header_offset = -(buffer_size(&image.buffer) - header_offset); h_loc = (void *)(buffer_get(&image.buffer) + buffer_size(&image.buffer) - 4); fill_header_offset(h_loc, header_offset); /* * If top swap present, update the header * location in secondary bootblock */ if (param.topswap_size) { if (update_master_header_loc_topswap(&image, h_loc, header_offset)) goto done; } ret = maybe_update_metadata_hash(&image); done: free(header); buffer_delete(&buffer); return ret; } static int add_topswap_bootblock(struct buffer *buffer, uint32_t *offset) { size_t bb_buf_size = buffer_size(buffer); if (bb_buf_size > param.topswap_size) { ERROR("Bootblock bigger than the topswap boundary\n"); ERROR("size = %zd, ts = %d\n", bb_buf_size, param.topswap_size); return 1; } /* * Allocate topswap_size*2 bytes for bootblock to * accommodate the second bootblock. */ struct buffer new_bootblock, bb1, bb2; if (buffer_create(&new_bootblock, 2 * param.topswap_size, buffer->name)) return 1; buffer_splice(&bb1, &new_bootblock, param.topswap_size - bb_buf_size, bb_buf_size); buffer_splice(&bb2, &new_bootblock, buffer_size(&new_bootblock) - bb_buf_size, bb_buf_size); /* Copy to first bootblock */ memcpy(buffer_get(&bb1), buffer_get(buffer), bb_buf_size); /* Copy to second bootblock */ memcpy(buffer_get(&bb2), buffer_get(buffer), bb_buf_size); buffer_delete(buffer); buffer_clone(buffer, &new_bootblock); /* Update the location (offset) of bootblock in the region */ return convert_region_offset(buffer_size(buffer), offset); } static int cbfs_add_component(const char *filename, const char *name, uint32_t headeroffset, convert_buffer_t convert) { /* * The steps used to determine the final placement offset in CBFS, in order: * * 1. If --base-address was passed, that value is used. If it was passed in the host * address space, convert it to flash address space. (After that, |*offset| is always * in the flash address space.) * * 2. The convert() function may write a location back to |offset|, usually by calling * do_cbfs_locate(). In this case, it needs to ensure that the location found can fit * the CBFS file in its final form (after any compression and conversion). * * 3. If --align was passed and the offset is still undecided at this point, * do_cbfs_locate() is called to find an appropriately aligned location. * * 4. If |offset| is still 0 at the end, cbfs_add_entry() will find the first available * location that fits. */ uint32_t offset = param.baseaddress_assigned ? param.baseaddress : 0; size_t len_align = 0; if (param.alignment && param.baseaddress_assigned) { ERROR("Cannot specify both alignment and base address\n"); return 1; } if (param.stage_xip && param.compression != CBFS_COMPRESS_NONE) { ERROR("Cannot specify compression for XIP.\n"); return 1; } if (!filename) { ERROR("You need to specify -f/--filename.\n"); return 1; } if (!name) { ERROR("You need to specify -n/--name.\n"); return 1; } if (param.type == 0) { ERROR("You need to specify a valid -t/--type.\n"); return 1; } struct cbfs_image image; if (cbfs_image_from_buffer(&image, param.image_region, headeroffset)) return 1; if (cbfs_get_entry(&image, name)) { ERROR("'%s' already in ROM image.\n", name); return 1; } struct buffer buffer; if (buffer_from_file(&buffer, filename) != 0) { ERROR("Could not load file '%s'.\n", filename); return 1; } struct cbfs_file *header = cbfs_create_file_header(param.type, buffer.size, name); if (!header) goto error; /* Bootblock and CBFS header should never have file hashes. When adding the bootblock it is important that we *don't* look up the metadata hash yet (before it is added) or we'll cache an outdated result. */ if (!verification_exclude(param.type)) { enum vb2_hash_algorithm mh_algo = get_mh_cache()->cbfs_hash.algo; if (mh_algo != VB2_HASH_INVALID && param.hash != mh_algo) { if (param.hash == VB2_HASH_INVALID) { param.hash = mh_algo; } else { ERROR("Cannot specify hash %s that's different from metadata hash algorithm %s\n", vb2_get_hash_algorithm_name(param.hash), vb2_get_hash_algorithm_name(mh_algo)); goto error; } } } /* * Check if Intel CPU topswap is specified this will require a * second bootblock to be added. */ if (param.type == CBFS_TYPE_BOOTBLOCK && param.topswap_size) if (add_topswap_bootblock(&buffer, &offset)) goto error; /* With --base-address we allow host space addresses -- if so, convert it here. */ if (IS_HOST_SPACE_ADDRESS(offset)) offset = convert_addr_space(param.image_region, offset); if (convert && convert(&buffer, &offset, header) != 0) { ERROR("Failed to parse file '%s'.\n", filename); goto error; } /* This needs to run after convert() to take compression into account. */ if (!offset && param.alignment) if (do_cbfs_locate(&offset, buffer_size(&buffer))) goto error; /* This needs to run after convert() to hash the actual final file data. */ if (param.hash != VB2_HASH_INVALID && cbfs_add_file_hash(header, &buffer, param.hash) == -1) { ERROR("couldn't add hash for '%s'\n", name); goto error; } if (param.autogen_attr) { /* Add position attribute if assigned */ if (param.baseaddress_assigned || param.stage_xip) { struct cbfs_file_attr_position *attrs = (struct cbfs_file_attr_position *) cbfs_add_file_attr(header, CBFS_FILE_ATTR_TAG_POSITION, sizeof(struct cbfs_file_attr_position)); if (attrs == NULL) goto error; attrs->position = htobe32(offset); } /* Add alignment attribute if used */ if (param.alignment) { struct cbfs_file_attr_align *attrs = (struct cbfs_file_attr_align *) cbfs_add_file_attr(header, CBFS_FILE_ATTR_TAG_ALIGNMENT, sizeof(struct cbfs_file_attr_align)); if (attrs == NULL) goto error; attrs->alignment = htobe32(param.alignment); } } if (param.ibb) { /* Mark as Initial Boot Block */ struct cbfs_file_attribute *attrs = cbfs_add_file_attr(header, CBFS_FILE_ATTR_TAG_IBB, sizeof(struct cbfs_file_attribute)); if (attrs == NULL) goto error; /* For Intel TXT minimum align is 16 */ len_align = 16; } if (param.padding) { const uint32_t hs = sizeof(struct cbfs_file_attribute); uint32_t size = ALIGN_UP(MAX(hs, param.padding), CBFS_ATTRIBUTE_ALIGN); INFO("Padding %d bytes\n", size); struct cbfs_file_attribute *attr = (struct cbfs_file_attribute *)cbfs_add_file_attr( header, CBFS_FILE_ATTR_TAG_PADDING, size); if (attr == NULL) goto error; } if (cbfs_add_entry(&image, &buffer, offset, header, len_align) != 0) { ERROR("Failed to add '%s' into ROM image.\n", filename); goto error; } free(header); buffer_delete(&buffer); return maybe_update_metadata_hash(&image) || maybe_update_fmap_hash(); error: free(header); buffer_delete(&buffer); return 1; } static int cbfstool_convert_raw(struct buffer *buffer, unused uint32_t *offset, struct cbfs_file *header) { char *compressed; int decompressed_size, compressed_size; comp_func_ptr compress; decompressed_size = buffer->size; if (param.precompression) { param.compression = read_le32(buffer->data); decompressed_size = read_le32(buffer->data + sizeof(uint32_t)); compressed_size = buffer->size - 8; compressed = malloc(compressed_size); if (!compressed) return -1; memcpy(compressed, buffer->data + 8, compressed_size); } else { if (param.compression == CBFS_COMPRESS_NONE) goto out; compress = compression_function(param.compression); if (!compress) return -1; compressed = calloc(buffer->size, 1); if (!compressed) return -1; if (compress(buffer->data, buffer->size, compressed, &compressed_size)) { WARN("Compression failed - disabled\n"); free(compressed); goto out; } } struct cbfs_file_attr_compression *attrs = (struct cbfs_file_attr_compression *) cbfs_add_file_attr(header, CBFS_FILE_ATTR_TAG_COMPRESSION, sizeof(struct cbfs_file_attr_compression)); if (attrs == NULL) { free(compressed); return -1; } attrs->compression = htobe32(param.compression); attrs->decompressed_size = htobe32(decompressed_size); free(buffer->data); buffer->data = compressed; buffer->size = compressed_size; out: header->len = htobe32(buffer->size); return 0; } static int cbfstool_convert_fsp(struct buffer *buffer, uint32_t *offset, struct cbfs_file *header) { uint32_t address; struct buffer fsp; /* * There are 4 different cases here: * * 1. --xip and --base-address: we need to place the binary at the given base address * in the CBFS image and relocate it to that address. *offset was already filled in, * but we need to convert it to the host address space for relocation. * * 2. --xip but no --base-address: we implicitly force a 4K minimum alignment so that * relocation can occur. Call do_cbfs_locate() here to find an appropriate *offset. * This also needs to be converted to the host address space for relocation. * * 3. No --xip but a --base-address: special case where --base-address does not have its * normal meaning, instead we use it as the relocation target address. We explicitly * reset *offset to 0 so that the file will be placed wherever it fits in CBFS. * * 4. No --xip and no --base-address: this means that the FSP was pre-linked and should * not be relocated. Just chain directly to convert_raw() for compression. */ if (param.stage_xip) { if (!param.baseaddress_assigned) { param.alignment = 4*1024; if (do_cbfs_locate(offset, buffer_size(buffer))) return -1; } assert(!IS_HOST_SPACE_ADDRESS(*offset)); address = convert_addr_space(param.image_region, *offset); } else { if (param.baseaddress_assigned == 0) { INFO("Honoring pre-linked FSP module, no relocation.\n"); return cbfstool_convert_raw(buffer, offset, header); } else { address = param.baseaddress; *offset = 0; } } /* Create a copy of the buffer to attempt relocation. */ if (buffer_create(&fsp, buffer_size(buffer), "fsp")) return -1; memcpy(buffer_get(&fsp), buffer_get(buffer), buffer_size(buffer)); /* Replace the buffer contents w/ the relocated ones on success. */ if (fsp_component_relocate(address, buffer_get(&fsp), buffer_size(&fsp)) > 0) { buffer_delete(buffer); buffer_clone(buffer, &fsp); } else { buffer_delete(&fsp); WARN("Invalid FSP variant.\n"); } /* Let the raw path handle all the cbfs metadata logic. */ return cbfstool_convert_raw(buffer, offset, header); } static int cbfstool_convert_mkstage(struct buffer *buffer, uint32_t *offset, struct cbfs_file *header) { struct buffer output; size_t data_size; int ret; if (elf_program_file_size(buffer, &data_size) < 0) { ERROR("Could not obtain ELF size\n"); return 1; } /* * We need a final location for XIP parsing, so we need to call do_cbfs_locate() early * here. That is okay because XIP stages may not be compressed, so their size cannot * change anymore at a later point. */ if (param.stage_xip && do_cbfs_locate(offset, data_size)) { ERROR("Could not find location for stage.\n"); return 1; } struct cbfs_file_attr_stageheader *stageheader = (void *) cbfs_add_file_attr(header, CBFS_FILE_ATTR_TAG_STAGEHEADER, sizeof(struct cbfs_file_attr_stageheader)); if (!stageheader) return -1; if (param.stage_xip) { uint32_t host_space_address = convert_addr_space(param.image_region, *offset); assert(IS_HOST_SPACE_ADDRESS(host_space_address)); ret = parse_elf_to_xip_stage(buffer, &output, host_space_address, param.ignore_sections, stageheader); } else { ret = parse_elf_to_stage(buffer, &output, param.ignore_sections, stageheader); } if (ret != 0) return -1; /* Store a hash of original uncompressed stage to compare later. */ size_t decmp_size = buffer_size(&output); uint32_t decmp_hash = XXH32(buffer_get(&output), decmp_size, 0); /* Chain to base conversion routine to handle compression. */ ret = cbfstool_convert_raw(&output, offset, header); if (ret != 0) goto fail; /* Special care must be taken for LZ4-compressed stages that the BSS is large enough to provide scratch space for in-place decompression. */ if (!param.precompression && param.compression == CBFS_COMPRESS_LZ4) { size_t memlen = be32toh(stageheader->memlen); size_t compressed_size = buffer_size(&output); uint8_t *compare_buffer = malloc(memlen); uint8_t *start = compare_buffer + memlen - compressed_size; if (!compare_buffer) { ERROR("Out of memory\n"); goto fail; } memcpy(start, buffer_get(&output), compressed_size); ret = ulz4fn(start, compressed_size, compare_buffer, memlen); if (ret == 0) { ERROR("Not enough scratch space to decompress LZ4 in-place -- increase BSS size or disable compression!\n"); free(compare_buffer); goto fail; } else if (ret != (int)decmp_size || decmp_hash != XXH32(compare_buffer, decmp_size, 0)) { ERROR("LZ4 compression BUG! Report to mailing list.\n"); free(compare_buffer); goto fail; } free(compare_buffer); } buffer_delete(buffer); buffer_clone(buffer, &output); return 0; fail: buffer_delete(&output); return -1; } static int cbfstool_convert_mkpayload(struct buffer *buffer, unused uint32_t *offset, struct cbfs_file *header) { struct buffer output; int ret; /* Per default, try and see if payload is an ELF binary */ ret = parse_elf_to_payload(buffer, &output, param.compression); /* If it's not an ELF, see if it's a FIT */ if (ret != 0) { ret = parse_fit_to_payload(buffer, &output, param.compression); if (ret == 0) header->type = htobe32(CBFS_TYPE_FIT_PAYLOAD); } /* If it's not an FIT, see if it's a UEFI FV */ if (ret != 0) ret = parse_fv_to_payload(buffer, &output, param.compression); /* If it's neither ELF nor UEFI Fv, try bzImage */ if (ret != 0) ret = parse_bzImage_to_payload(buffer, &output, param.initrd, param.cmdline, param.compression); /* Not a supported payload type */ if (ret != 0) { ERROR("Not a supported payload type (ELF / FV).\n"); buffer_delete(buffer); return -1; } buffer_delete(buffer); // Direct assign, no dupe. memcpy(buffer, &output, sizeof(*buffer)); header->len = htobe32(output.size); return 0; } static int cbfstool_convert_mkflatpayload(struct buffer *buffer, unused uint32_t *offset, struct cbfs_file *header) { struct buffer output; if (parse_flat_binary_to_payload(buffer, &output, param.loadaddress, param.entrypoint, param.compression) != 0) { return -1; } buffer_delete(buffer); // Direct assign, no dupe. memcpy(buffer, &output, sizeof(*buffer)); header->len = htobe32(output.size); return 0; } static int cbfs_add(void) { convert_buffer_t convert = cbfstool_convert_raw; if (param.type == CBFS_TYPE_FSP) { convert = cbfstool_convert_fsp; } else if (param.type == CBFS_TYPE_STAGE) { ERROR("stages can only be added with cbfstool add-stage\n"); return 1; } else if (param.stage_xip) { ERROR("cbfstool add supports xip only for FSP component type\n"); return 1; } return cbfs_add_component(param.filename, param.name, param.headeroffset, convert); } static int cbfs_add_stage(void) { if (param.stage_xip && param.baseaddress_assigned) { ERROR("Cannot specify base address for XIP.\n"); return 1; } param.type = CBFS_TYPE_STAGE; return cbfs_add_component(param.filename, param.name, param.headeroffset, cbfstool_convert_mkstage); } static int cbfs_add_payload(void) { param.type = CBFS_TYPE_SELF; return cbfs_add_component(param.filename, param.name, param.headeroffset, cbfstool_convert_mkpayload); } static int cbfs_add_flat_binary(void) { if (param.loadaddress == 0) { ERROR("You need to specify a valid " "-l/--load-address.\n"); return 1; } if (param.entrypoint == 0) { ERROR("You need to specify a valid " "-e/--entry-point.\n"); return 1; } param.type = CBFS_TYPE_SELF; return cbfs_add_component(param.filename, param.name, param.headeroffset, cbfstool_convert_mkflatpayload); } static int cbfs_add_integer(void) { if (!param.u64val_assigned) { ERROR("You need to specify a value to write.\n"); return 1; } return cbfs_add_integer_component(param.name, param.u64val, param.baseaddress, param.headeroffset); } static int cbfs_remove(void) { if (!param.name) { ERROR("You need to specify -n/--name.\n"); return 1; } struct cbfs_image image; if (cbfs_image_from_buffer(&image, param.image_region, param.headeroffset)) return 1; if (cbfs_remove_entry(&image, param.name) != 0) { ERROR("Removing file '%s' failed.\n", param.name); return 1; } return maybe_update_metadata_hash(&image); } static int cbfs_create(void) { struct cbfs_image image; memset(&image, 0, sizeof(image)); buffer_clone(&image.buffer, param.image_region); if (param.fmap) { if (param.arch != CBFS_ARCHITECTURE_UNKNOWN || param.size || param.baseaddress_assigned || param.headeroffset_assigned || param.cbfsoffset_assigned || param.bootblock) { ERROR("Since -M was provided, -m, -s, -b, -o, -H, and -B should be omitted\n"); return 1; } return cbfs_image_create(&image, image.buffer.size); } if (param.arch == CBFS_ARCHITECTURE_UNKNOWN) { ERROR("You need to specify -m/--machine arch.\n"); return 1; } struct buffer bootblock; if (!param.bootblock) { DEBUG("-B not given, creating image without bootblock.\n"); if (buffer_create(&bootblock, 0, "(dummy)") != 0) return 1; } else if (buffer_from_file(&bootblock, param.bootblock)) { return 1; } if (!param.alignment) param.alignment = CBFS_ALIGNMENT; // Set default offsets. x86, as usual, needs to be a special snowflake. if (!param.baseaddress_assigned) { if (param.arch == CBFS_ARCHITECTURE_X86) { // Make sure there's at least enough room for rel_offset param.baseaddress = param.size - MAX(bootblock.size, sizeof(int32_t)); DEBUG("x86 -> bootblock lies at end of ROM (%#x).\n", param.baseaddress); } else { param.baseaddress = 0; DEBUG("bootblock starts at address 0x0.\n"); } } if (!param.headeroffset_assigned) { if (param.arch == CBFS_ARCHITECTURE_X86) { param.headeroffset = param.baseaddress - sizeof(struct cbfs_header); DEBUG("x86 -> CBFS header before bootblock (%#x).\n", param.headeroffset); } else { param.headeroffset = align_up(param.baseaddress + bootblock.size, sizeof(uint32_t)); DEBUG("CBFS header placed behind bootblock (%#x).\n", param.headeroffset); } } if (!param.cbfsoffset_assigned) { if (param.arch == CBFS_ARCHITECTURE_X86) { param.cbfsoffset = 0; DEBUG("x86 -> CBFS entries start at address 0x0.\n"); } else { param.cbfsoffset = align_up(param.headeroffset + sizeof(struct cbfs_header), CBFS_ALIGNMENT); DEBUG("CBFS entries start beind master header (%#x).\n", param.cbfsoffset); } } int ret = cbfs_legacy_image_create(&image, param.arch, CBFS_ALIGNMENT, &bootblock, param.baseaddress, param.headeroffset, param.cbfsoffset); buffer_delete(&bootblock); return ret; } static int cbfs_layout(void) { const struct fmap *fmap = partitioned_file_get_fmap(param.image_file); if (!fmap) { LOG("This is a legacy image composed entirely of a single CBFS.\n"); return 1; } printf("This image contains the following sections that can be %s with this tool:\n", param.show_immutable ? "accessed" : "manipulated"); puts(""); for (unsigned i = 0; i < fmap->nareas; ++i) { const struct fmap_area *current = fmap->areas + i; bool readonly = partitioned_file_fmap_count(param.image_file, partitioned_file_fmap_select_children_of, current) || region_is_flashmap((const char *)current->name); if (!param.show_immutable && readonly) continue; printf("'%s'", current->name); // Detect consecutive sections that describe the same region and // show them as aliases. This cannot find equivalent entries // that aren't adjacent; however, fmaptool doesn't generate // FMAPs with such sections, so this convenience feature works // for all but the strangest manually created FMAP binaries. // TODO: This could be done by parsing the FMAP into some kind // of tree that had duplicate lists in addition to child lists, // which would allow covering that weird, unlikely case as well. unsigned lookahead; for (lookahead = 1; i + lookahead < fmap->nareas; ++lookahead) { const struct fmap_area *consecutive = fmap->areas + i + lookahead; if (consecutive->offset != current->offset || consecutive->size != current->size) break; printf(", '%s'", consecutive->name); } if (lookahead > 1) fputs(" are aliases for the same region", stdout); const char *qualifier = ""; if (readonly) qualifier = "read-only, "; else if (region_is_modern_cbfs((const char *)current->name)) qualifier = "CBFS, "; else if (current->flags & FMAP_AREA_PRESERVE) qualifier = "preserve, "; printf(" (%ssize %u, offset %u)\n", qualifier, current->size, current->offset); i += lookahead - 1; } puts(""); if (param.show_immutable) { puts("It is at least possible to perform the read action on every section listed above."); } else { puts("It is possible to perform either the write action or the CBFS add/remove actions on every section listed above."); puts("To see the image's read-only sections as well, rerun with the -w option."); } return 0; } static enum cb_err verify_walker(__always_unused cbfs_dev_t dev, size_t offset, const union cbfs_mdata *mdata, size_t already_read, void *arg) { uint32_t type = be32toh(mdata->h.type); uint32_t data_offset = be32toh(mdata->h.offset); if (verification_exclude(type)) return CB_CBFS_NOT_FOUND; assert(already_read == data_offset); const struct vb2_hash *hash = cbfs_file_hash(mdata); if (!hash) return CB_ERR; void *file_data = arg + offset + data_offset; if (vb2_hash_verify(false, file_data, be32toh(mdata->h.len), hash) != VB2_SUCCESS) return CB_CBFS_HASH_MISMATCH; return CB_CBFS_NOT_FOUND; } static int cbfs_print(void) { struct cbfs_image image; if (cbfs_image_from_buffer(&image, param.image_region, param.headeroffset)) return 1; if (param.machine_parseable) { if (verbose) printf("[FMAP REGION]\t%s\n", param.region_name); cbfs_print_parseable_directory(&image); } else { printf("FMAP REGION: %s\n", param.region_name); cbfs_print_directory(&image); } if (verbose) { const char *verification_state = "fully valid"; struct mh_cache *mhc = get_mh_cache(); if (mhc->cbfs_hash.algo == VB2_HASH_INVALID) return 0; struct vb2_hash real_hash = { .algo = mhc->cbfs_hash.algo }; enum cb_err err = cbfs_walk(&image, verify_walker, buffer_get(&image.buffer), &real_hash, CBFS_WALK_WRITEBACK_HASH); if (err == CB_CBFS_HASH_MISMATCH) verification_state = "invalid file hashes"; else if (err != CB_CBFS_NOT_FOUND) verification_state = "missing file hashes"; char *hash_str = bintohex(real_hash.raw, vb2_digest_size(real_hash.algo)); printf("[METADATA HASH]\t%s:%s", vb2_get_hash_algorithm_name(real_hash.algo), hash_str); if (!strcmp(param.region_name, SECTION_NAME_PRIMARY_CBFS)) { if (!memcmp(mhc->cbfs_hash.raw, real_hash.raw, vb2_digest_size(real_hash.algo))) { printf(":valid"); } else { printf(":invalid"); verification_state = "invalid metadata hash"; } } printf("\n"); printf("[CBFS VERIFICATION (%s)]\t%s\n", param.region_name, verification_state); free(hash_str); } return 0; } static int cbfs_extract(void) { if (!param.filename) { ERROR("You need to specify -f/--filename.\n"); return 1; } if (!param.name) { ERROR("You need to specify -n/--name.\n"); return 1; } struct cbfs_image image; if (cbfs_image_from_buffer(&image, param.image_region, param.headeroffset)) return 1; return cbfs_export_entry(&image, param.name, param.filename, param.arch, !param.unprocessed); } static int cbfs_write(void) { if (!param.filename) { ERROR("You need to specify a valid input -f/--file.\n"); return 1; } if (!partitioned_file_is_partitioned(param.image_file)) { ERROR("This operation isn't valid on legacy images having CBFS master headers\n"); return 1; } if (!param.force && region_is_modern_cbfs(param.region_name)) { ERROR("Target image region '%s' is a CBFS and must be manipulated using add and remove\n", param.region_name); return 1; } struct buffer new_content; if (buffer_from_file(&new_content, param.filename)) return 1; if (buffer_check_magic(&new_content, FMAP_SIGNATURE, strlen(FMAP_SIGNATURE))) { ERROR("File '%s' appears to be an FMAP and cannot be added to an existing image\n", param.filename); buffer_delete(&new_content); return 1; } if (!param.force && buffer_check_magic(&new_content, CBFS_FILE_MAGIC, strlen(CBFS_FILE_MAGIC))) { ERROR("File '%s' appears to be a CBFS and cannot be inserted into a raw region\n", param.filename); buffer_delete(&new_content); return 1; } unsigned offset = 0; if (param.fill_partial_upward && param.fill_partial_downward) { ERROR("You may only specify one of -u and -d.\n"); buffer_delete(&new_content); return 1; } else if (!param.fill_partial_upward && !param.fill_partial_downward) { if (new_content.size != param.image_region->size) { ERROR("File to add is %zu bytes and would not fill %zu-byte target region (did you mean to pass either -u or -d?)\n", new_content.size, param.image_region->size); buffer_delete(&new_content); return 1; } } else { if (new_content.size > param.image_region->size) { ERROR("File to add is %zu bytes and would overflow %zu-byte target region\n", new_content.size, param.image_region->size); buffer_delete(&new_content); return 1; } if (param.u64val == (uint64_t)-1) { WARN("Written area will abut %s of target region: any unused space will keep its current contents\n", param.fill_partial_upward ? "bottom" : "top"); } else if (param.u64val > 0xff) { ERROR("given fill value (%x) is larger than a byte\n", (unsigned)(param.u64val & 0xff)); buffer_delete(&new_content); return 1; } else { memset(buffer_get(param.image_region), param.u64val & 0xff, buffer_size(param.image_region)); } if (param.fill_partial_downward) offset = param.image_region->size - new_content.size; } memcpy(param.image_region->data + offset, new_content.data, new_content.size); buffer_delete(&new_content); return maybe_update_fmap_hash(); } static int cbfs_read(void) { if (!param.filename) { ERROR("You need to specify a valid output -f/--file.\n"); return 1; } if (!partitioned_file_is_partitioned(param.image_file)) { ERROR("This operation isn't valid on legacy images having CBFS master headers\n"); return 1; } return buffer_write_file(param.image_region, param.filename); } static int cbfs_copy(void) { struct cbfs_image src_image; struct buffer src_buf; if (!param.source_region) { ERROR("You need to specify -R/--source-region.\n"); return 1; } /* Obtain the source region and convert it to a cbfs_image. */ if (!partitioned_file_read_region(&src_buf, param.image_file, param.source_region)) { ERROR("Region not found in image: %s\n", param.source_region); return 1; } if (cbfs_image_from_buffer(&src_image, &src_buf, param.headeroffset)) return 1; return cbfs_copy_instance(&src_image, param.image_region); } static int cbfs_compact(void) { struct cbfs_image image; if (cbfs_image_from_buffer(&image, param.image_region, param.headeroffset)) return 1; WARN("Compacting a CBFS doesn't honor alignment or fixed addresses!\n"); return cbfs_compact_instance(&image); } static int cbfs_expand(void) { struct buffer src_buf; /* Obtain the source region. */ if (!partitioned_file_read_region(&src_buf, param.image_file, param.region_name)) { ERROR("Region not found in image: %s\n", param.source_region); return 1; } return cbfs_expand_to_region(param.image_region); } static int cbfs_truncate(void) { struct buffer src_buf; /* Obtain the source region. */ if (!partitioned_file_read_region(&src_buf, param.image_file, param.region_name)) { ERROR("Region not found in image: %s\n", param.source_region); return 1; } uint32_t size; int result = cbfs_truncate_space(param.image_region, &size); if (!result) printf("0x%x\n", size); return result; } static const struct command commands[] = { {"add", "H:r:f:n:t:c:b:a:p:yvA:j:gh?", cbfs_add, true, true}, {"add-flat-binary", "H:r:f:n:l:e:c:b:p:vA:gh?", cbfs_add_flat_binary, true, true}, {"add-payload", "H:r:f:n:c:b:a:C:I:p:vA:gh?", cbfs_add_payload, true, true}, {"add-stage", "a:H:r:f:n:t:c:b:P:QS:p:yvA:gh?", cbfs_add_stage, true, true}, {"add-int", "H:r:i:n:b:vgh?", cbfs_add_integer, true, true}, {"add-master-header", "H:r:vh?j:", cbfs_add_master_header, true, true}, {"compact", "r:h?", cbfs_compact, true, true}, {"copy", "r:R:h?", cbfs_copy, true, true}, {"create", "M:r:s:B:b:H:o:m:vh?", cbfs_create, true, true}, {"extract", "H:r:m:n:f:Uvh?", cbfs_extract, true, false}, {"layout", "wvh?", cbfs_layout, false, false}, {"print", "H:r:vkh?", cbfs_print, true, false}, {"read", "r:f:vh?", cbfs_read, true, false}, {"remove", "H:r:n:vh?", cbfs_remove, true, true}, {"write", "r:f:i:Fudvh?", cbfs_write, true, true}, {"expand", "r:h?", cbfs_expand, true, true}, {"truncate", "r:h?", cbfs_truncate, true, true}, }; enum { /* begin after ASCII characters */ LONGOPT_START = 256, LONGOPT_IBB = LONGOPT_START, LONGOPT_MMAP, LONGOPT_END, }; static struct option long_options[] = { {"alignment", required_argument, 0, 'a' }, {"base-address", required_argument, 0, 'b' }, {"bootblock", required_argument, 0, 'B' }, {"cmdline", required_argument, 0, 'C' }, {"compression", required_argument, 0, 'c' }, {"topswap-size", required_argument, 0, 'j' }, {"empty-fits", required_argument, 0, 'x' }, {"entry-point", required_argument, 0, 'e' }, {"file", required_argument, 0, 'f' }, {"fill-downward", no_argument, 0, 'd' }, {"fill-upward", no_argument, 0, 'u' }, {"flashmap", required_argument, 0, 'M' }, {"fmap-regions", required_argument, 0, 'r' }, {"force", no_argument, 0, 'F' }, {"source-region", required_argument, 0, 'R' }, {"hash-algorithm",required_argument, 0, 'A' }, {"header-offset", required_argument, 0, 'H' }, {"help", no_argument, 0, 'h' }, {"ignore-sec", required_argument, 0, 'S' }, {"initrd", required_argument, 0, 'I' }, {"int", required_argument, 0, 'i' }, {"load-address", required_argument, 0, 'l' }, {"machine", required_argument, 0, 'm' }, {"name", required_argument, 0, 'n' }, {"offset", required_argument, 0, 'o' }, {"padding", required_argument, 0, 'p' }, {"pow2page", no_argument, 0, 'Q' }, {"ucode-region", required_argument, 0, 'q' }, {"size", required_argument, 0, 's' }, {"type", required_argument, 0, 't' }, {"verbose", no_argument, 0, 'v' }, {"with-readonly", no_argument, 0, 'w' }, {"xip", no_argument, 0, 'y' }, {"gen-attribute", no_argument, 0, 'g' }, {"mach-parseable",no_argument, 0, 'k' }, {"unprocessed", no_argument, 0, 'U' }, {"ibb", no_argument, 0, LONGOPT_IBB }, {"mmap", required_argument, 0, LONGOPT_MMAP }, {NULL, 0, 0, 0 } }; static int get_region_offset(long long int offset, uint32_t *region_offset) { /* If offset is not negative, no transformation required. */ if (offset >= 0) { *region_offset = offset; return 0; } /* Calculate offset from start of region. */ return convert_region_offset(-offset, region_offset); } static int calculate_region_offsets(void) { int ret = 0; if (param.baseaddress_assigned) ret |= get_region_offset(param.baseaddress_input, ¶m.baseaddress); if (param.headeroffset_assigned) ret |= get_region_offset(param.headeroffset_input, ¶m.headeroffset); if (param.cbfsoffset_assigned) ret |= get_region_offset(param.cbfsoffset_input, ¶m.cbfsoffset); return ret; } static int dispatch_command(struct command command) { if (command.accesses_region) { assert(param.image_file); if (partitioned_file_is_partitioned(param.image_file)) { INFO("Performing operation on '%s' region...\n", param.region_name); } if (!partitioned_file_read_region(param.image_region, param.image_file, param.region_name)) { ERROR("The image will be left unmodified.\n"); return 1; } if (command.modifies_region) { // We (intentionally) don't support overwriting the FMAP // section. If you find yourself wanting to do this, // consider creating a new image rather than performing // whatever hacky transformation you were planning. if (region_is_flashmap(param.region_name)) { ERROR("Image region '%s' is read-only because it contains the FMAP.\n", param.region_name); ERROR("The image will be left unmodified.\n"); return 1; } // We don't allow writing raw data to regions that // contain nested regions, since doing so would // overwrite all such subregions. if (partitioned_file_region_contains_nested( param.image_file, param.region_name)) { ERROR("Image region '%s' is read-only because it contains nested regions.\n", param.region_name); ERROR("The image will be left unmodified.\n"); return 1; } } /* * Once image region is read, input offsets can be adjusted accordingly if the * inputs are provided as negative integers i.e. offsets from end of region. */ if (calculate_region_offsets()) return 1; } if (command.function()) { if (partitioned_file_is_partitioned(param.image_file)) { ERROR("Failed while operating on '%s' region!\n", param.region_name); ERROR("The image will be left unmodified.\n"); } return 1; } return 0; } static void usage(char *name) { printf ("cbfstool: Management utility for CBFS formatted ROM images\n\n" "USAGE:\n" " %s [-h]\n" " %s FILE COMMAND [-v] [PARAMETERS]...\n\n" "OPTIONs:\n" " -H header_offset Do not search for header; use this offset*\n" " -T Output top-aligned memory address\n" " -u Accept short data; fill upward/from bottom\n" " -d Accept short data; fill downward/from top\n" " -F Force action\n" " -g Generate position and alignment arguments\n" " -U Unprocessed; don't decompress or make ELF\n" " -v Provide verbose output (-v=INFO -vv=DEBUG output)\n" " -h Display this help message\n\n" " --ext-win-base Base of extended decode window in host address\n" " space(x86 only)\n" " --ext-win-size Size of extended decode window in host address\n" " space(x86 only)\n" "COMMANDs:\n" " add [-r image,regions] -f FILE -n NAME -t TYPE [-A hash] \\\n" " [-c compression] [-b base-address | -a alignment] \\\n" " [-p padding size] [-y|--xip if TYPE is FSP] \\\n" " [-j topswap-size] (Intel CPUs only) [--ibb] \\\n" " [--ext-win-base win-base --ext-win-size win-size] " "Add a component\n" " " " -j valid size: 0x10000 0x20000 0x40000 0x80000 0x100000 \n" " add-payload [-r image,regions] -f FILE -n NAME [-A hash] \\\n" " [-c compression] [-b base-address] \\\n" " (linux specific: [-C cmdline] [-I initrd]) " "Add a payload to the ROM\n" " add-stage [-r image,regions] -f FILE -n NAME [-A hash] \\\n" " [-c compression] [-b base] \\\n" " [-S comma-separated-section(s)-to-ignore] \\\n" " [-a alignment] [-Q|--pow2page] \\\n" " [-y|--xip] [--ibb] \\\n" " [--ext-win-base win-base --ext-win-size win-size] " "Add a stage to the ROM\n" " add-flat-binary [-r image,regions] -f FILE -n NAME \\\n" " [-A hash] -l load-address -e entry-point \\\n" " [-c compression] [-b base] " "Add a 32bit flat mode binary\n" " add-int [-r image,regions] -i INTEGER -n NAME [-b base] " "Add a raw 64-bit integer value\n" " add-master-header [-r image,regions] \\ \n" " [-j topswap-size] (Intel CPUs only) " "Add a legacy CBFS master header\n" " remove [-r image,regions] -n NAME " "Remove a component\n" " compact -r image,regions " "Defragment CBFS image.\n" " copy -r image,regions -R source-region " "Create a copy (duplicate) cbfs instance in fmap\n" " create -m ARCH -s size [-b bootblock offset] \\\n" " [-o CBFS offset] [-H header offset] [-B bootblock] " "Create a legacy ROM file with CBFS master header*\n" " create -M flashmap [-r list,of,regions,containing,cbfses] " "Create a new-style partitioned firmware image\n" " layout [-w] " "List mutable (or, with -w, readable) image regions\n" " print [-r image,regions] [-k] " "Show the contents of the ROM\n" " extract [-r image,regions] [-m ARCH] -n NAME -f FILE [-U] " "Extracts a file from ROM\n" " write [-F] -r image,regions -f file [-u | -d] [-i int] " "Write file into same-size [or larger] raw region\n" " read [-r fmap-region] -f file " "Extract raw region contents into binary file\n" " truncate [-r fmap-region] " "Truncate CBFS and print new size on stdout\n" " expand [-r fmap-region] " "Expand CBFS to span entire region\n" "OFFSETs:\n" " Numbers accompanying -b, -H, and -o switches* may be provided\n" " in two possible formats: if their value is greater than\n" " 0x80000000, they are interpreted as a top-aligned x86 memory\n" " address; otherwise, they are treated as an offset into flash.\n" "ARCHes:\n", name, name ); print_supported_architectures(); printf("TYPEs:\n"); print_supported_filetypes(); printf( "\n* Note that these actions and switches are only valid when\n" " working with legacy images whose structure is described\n" " primarily by a CBFS master header. New-style images, in\n" " contrast, exclusively make use of an FMAP to describe their\n" " layout: this must minimally contain an '%s' section\n" " specifying the location of this FMAP itself and a '%s'\n" " section describing the primary CBFS. It should also be noted\n" " that, when working with such images, the -F and -r switches\n" " default to '%s' for convenience, and the -b switch becomes\n" " relative to the selected CBFS region's lowest address.\n" " The one exception to this rule is the top-aligned address,\n" " which is always relative to the end of the entire image\n" " rather than relative to the local region; this is true for\n" " for both input (sufficiently large) and output (-T) data.\n", SECTION_NAME_FMAP, SECTION_NAME_PRIMARY_CBFS, SECTION_NAME_PRIMARY_CBFS ); } static bool valid_opt(size_t i, int c) { /* Check if it is one of the optstrings supported by the command. */ if (strchr(commands[i].optstring, c)) return true; /* * Check if it is one of the non-ASCII characters. Currently, the * non-ASCII characters are only checked against the valid list * irrespective of the command. */ if (c >= LONGOPT_START && c < LONGOPT_END) return true; return false; } int main(int argc, char **argv) { size_t i; int c; if (argc < 3) { usage(argv[0]); return 1; } char *image_name = argv[1]; char *cmd = argv[2]; optind += 2; for (i = 0; i < ARRAY_SIZE(commands); i++) { if (strcmp(cmd, commands[i].name) != 0) continue; while (1) { char *suffix = NULL; int option_index = 0; c = getopt_long(argc, argv, commands[i].optstring, long_options, &option_index); if (c == -1) { if (optind < argc) { ERROR("%s: excessive argument -- '%s'" "\n", argv[0], argv[optind]); return 1; } break; } /* Filter out illegal long options */ if (!valid_opt(i, c)) { ERROR("%s: invalid option -- '%d'\n", argv[0], c); c = '?'; } switch(c) { case 'n': param.name = optarg; break; case 't': if (intfiletype(optarg) != ((uint64_t) - 1)) param.type = intfiletype(optarg); else param.type = strtoul(optarg, NULL, 0); if (param.type == 0) WARN("Unknown type '%s' ignored\n", optarg); break; case 'c': { if (strcmp(optarg, "precompression") == 0) { param.precompression = 1; break; } int algo = cbfs_parse_comp_algo(optarg); if (algo >= 0) param.compression = algo; else WARN("Unknown compression '%s' ignored.\n", optarg); break; } case 'A': { if (!vb2_lookup_hash_alg(optarg, ¶m.hash)) { ERROR("Unknown hash algorithm '%s'.\n", optarg); return 1; } break; } case 'M': param.fmap = optarg; break; case 'r': param.region_name = optarg; break; case 'R': param.source_region = optarg; break; case 'b': param.baseaddress_input = strtoll(optarg, &suffix, 0); if (!*optarg || (suffix && *suffix)) { ERROR("Invalid base address '%s'.\n", optarg); return 1; } // baseaddress may be zero on non-x86, so we // need an explicit "baseaddress_assigned". param.baseaddress_assigned = 1; break; case 'l': param.loadaddress = strtoul(optarg, &suffix, 0); if (!*optarg || (suffix && *suffix)) { ERROR("Invalid load address '%s'.\n", optarg); return 1; } break; case 'e': param.entrypoint = strtoul(optarg, &suffix, 0); if (!*optarg || (suffix && *suffix)) { ERROR("Invalid entry point '%s'.\n", optarg); return 1; } break; case 's': param.size = strtoul(optarg, &suffix, 0); if (!*optarg) { ERROR("Empty size specified.\n"); return 1; } switch (tolower((int)suffix[0])) { case 'k': param.size *= 1024; break; case 'm': param.size *= 1024 * 1024; break; case '\0': break; default: ERROR("Invalid suffix for size '%s'.\n", optarg); return 1; } break; case 'B': param.bootblock = optarg; break; case 'H': param.headeroffset_input = strtoll(optarg, &suffix, 0); if (!*optarg || (suffix && *suffix)) { ERROR("Invalid header offset '%s'.\n", optarg); return 1; } param.headeroffset_assigned = 1; break; case 'a': param.alignment = strtoul(optarg, &suffix, 0); if (!*optarg || (suffix && *suffix)) { ERROR("Invalid alignment '%s'.\n", optarg); return 1; } break; case 'p': param.padding = strtoul(optarg, &suffix, 0); if (!*optarg || (suffix && *suffix)) { ERROR("Invalid pad size '%s'.\n", optarg); return 1; } break; case 'Q': param.force_pow2_pagesize = 1; break; case 'o': param.cbfsoffset_input = strtoll(optarg, &suffix, 0); if (!*optarg || (suffix && *suffix)) { ERROR("Invalid cbfs offset '%s'.\n", optarg); return 1; } param.cbfsoffset_assigned = 1; break; case 'f': param.filename = optarg; break; case 'F': param.force = 1; break; case 'i': param.u64val = strtoull(optarg, &suffix, 0); param.u64val_assigned = 1; if (!*optarg || (suffix && *suffix)) { ERROR("Invalid int parameter '%s'.\n", optarg); return 1; } break; case 'u': param.fill_partial_upward = true; break; case 'd': param.fill_partial_downward = true; break; case 'w': param.show_immutable = true; break; case 'j': param.topswap_size = strtol(optarg, NULL, 0); if (!is_valid_topswap()) return 1; break; case 'q': param.ucode_region = optarg; break; case 'v': verbose++; break; case 'm': param.arch = string_to_arch(optarg); break; case 'I': param.initrd = optarg; break; case 'C': param.cmdline = optarg; break; case 'S': param.ignore_sections = optarg; break; case 'y': param.stage_xip = true; break; case 'g': param.autogen_attr = true; break; case 'k': param.machine_parseable = true; break; case 'U': param.unprocessed = true; break; case LONGOPT_IBB: param.ibb = true; break; case LONGOPT_MMAP: if (decode_mmap_arg(optarg)) return 1; break; case 'h': case '?': usage(argv[0]); return 1; default: break; } } if (commands[i].function == cbfs_create) { if (param.fmap) { struct buffer flashmap; if (buffer_from_file(&flashmap, param.fmap)) return 1; param.image_file = partitioned_file_create( image_name, &flashmap); buffer_delete(&flashmap); } else if (param.size) { param.image_file = partitioned_file_create_flat( image_name, param.size); } else { ERROR("You need to specify a valid -M/--flashmap or -s/--size.\n"); return 1; } } else { bool write_access = commands[i].modifies_region; param.image_file = partitioned_file_reopen(image_name, write_access); } if (!param.image_file) return 1; unsigned num_regions = 1; for (const char *list = strchr(param.region_name, ','); list; list = strchr(list + 1, ',')) ++num_regions; // If the action needs to read an image region, as indicated by // having accesses_region set in its command struct, that // region's buffer struct will be stored here and the client // will receive a pointer to it via param.image_region. It // need not write the buffer back to the image file itself, // since this behavior can be requested via its modifies_region // field. Additionally, it should never free the region buffer, // as that is performed automatically once it completes. struct buffer image_regions[num_regions]; memset(image_regions, 0, sizeof(image_regions)); bool seen_primary_cbfs = false; char region_name_scratch[strlen(param.region_name) + 1]; strcpy(region_name_scratch, param.region_name); param.region_name = strtok(region_name_scratch, ","); for (unsigned region = 0; region < num_regions; ++region) { if (!param.region_name) { ERROR("Encountered illegal degenerate region name in -r list\n"); ERROR("The image will be left unmodified.\n"); partitioned_file_close(param.image_file); return 1; } if (strcmp(param.region_name, SECTION_NAME_PRIMARY_CBFS) == 0) seen_primary_cbfs = true; param.image_region = image_regions + region; if (dispatch_command(commands[i])) { partitioned_file_close(param.image_file); return 1; } param.region_name = strtok(NULL, ","); } if (commands[i].function == cbfs_create && !seen_primary_cbfs) { ERROR("The creation -r list must include the mandatory '%s' section.\n", SECTION_NAME_PRIMARY_CBFS); ERROR("The image will be left unmodified.\n"); partitioned_file_close(param.image_file); return 1; } if (commands[i].modifies_region) { assert(param.image_file); for (unsigned region = 0; region < num_regions; ++region) { if (!partitioned_file_write_region( param.image_file, image_regions + region)) { partitioned_file_close( param.image_file); return 1; } } } partitioned_file_close(param.image_file); return 0; } ERROR("Unknown command '%s'.\n", cmd); usage(argv[0]); return 1; }