Jakub Czapiga has uploaded this change for review. ( https://review.coreboot.org/c/coreboot/+/59497 )
Change subject: libpatload: Implement new CBFS access API ......................................................................
libpatload: Implement new CBFS access API
Change-Id: I00da0658dbac0cddf92ad55611def947932d23c7 Signed-off-by: Jakub Czapiga jacz@semihalf.com --- M payloads/libpayload/Kconfig M payloads/libpayload/include/cbfs.h A payloads/libpayload/include/cbfs/cbfs_err.h A payloads/libpayload/include/cbfs/cbfs_glue.h A payloads/libpayload/include/cbfs/cbfs_mdata.h A payloads/libpayload/include/cbfs/cbfs_private.h M payloads/libpayload/include/cbfs_core.h A payloads/libpayload/libcbfs/Kconfig M payloads/libpayload/libcbfs/cbfs.c M payloads/libpayload/libcbfs/cbfs_core.c A payloads/libpayload/libcbfs/cbfs_mcache.c A payloads/libpayload/tests/include/mocks/cbfs_util.h A payloads/libpayload/tests/libcbfs/Makefile.inc A payloads/libpayload/tests/libcbfs/cbfs-lookup-test.c A payloads/libpayload/tests/mocks/cbfs_file_mock.c A payloads/libpayload/tests/mocks/die.c 16 files changed, 2,077 insertions(+), 56 deletions(-)
git pull ssh://review.coreboot.org:29418/coreboot refs/changes/97/59497/1
diff --git a/payloads/libpayload/Kconfig b/payloads/libpayload/Kconfig index 382f5af..eef72b2 100644 --- a/payloads/libpayload/Kconfig +++ b/payloads/libpayload/Kconfig @@ -214,6 +214,8 @@ help CBFS is the archive format of coreboot
+source "libcbfs/Kconfig" + config LZMA bool "LZMA decoder" default y diff --git a/payloads/libpayload/include/cbfs.h b/payloads/libpayload/include/cbfs.h index ab23b02..2edac16 100644 --- a/payloads/libpayload/include/cbfs.h +++ b/payloads/libpayload/include/cbfs.h @@ -46,8 +46,113 @@ #define _CBFS_H_
#include <cbfs_core.h> +#include <cbfs/cbfs_err.h> +#include <cbfs/cbfs_mdata.h> +#include <stdbool.h>
-/* legacy APIs */ + +/********************************************************************************************** + * CBFS FILE ACCESS APIs * + **********************************************************************************************/ + +/* + * These are the APIs used to access files in the CBFS. In order to keep the calls simple and + * free of clutter in the common cases, but still offer all advanced functionality when needed, + * there are mary different variations that are implemented by wrapping the same underlying API + * with static inlines. All accessors have in common that they look up files by name, and will + * transparently decompress files that are compressed. + * + * There are three main flavors of the CBFS accessors: + * + * size_t cbfs_load(char *name, void *buf, size_t size): Loads the contents of a CBFS file into + * a buffer provided by the caller (by providing pointer and size to it). Will return the + * amount of bytes loaded on success, or 0 on error. + * + * void *cbfs_map(char *name, size_t *size_out): Maps a file into the address space. If the file + * is not compressed and the platform supports direct memory-mapping for the boot medium, + * a pointer to the platform mapping is returned directly. In all other cases, memory will + * be allocated from the heap and file data will be loaded into there. Returns a pointer + * to the mapping on success, or NULL on error. If an optional size_out parameter is passed + * in, it will be filled out with the size of the mapped data. Caller should call + * cbfs_unmap() after it is done using the mapping to free up the memory, if possible. + * + * void cbfs_alloc(char *name, cbfs_allocator_t allocator, void *arg, size_t *size_out): Loads + * file data into memory provided by a custom allocator function that the caller passes in. + * The caller may pass an argument, that is passed through verbatim to the allocator. + * Returns the pointer returned by the allocator (where the file data was loaded to) on + * success, or NULL on error. If an optional size_out parameter is passed in, it will be + * filled out with the suze of the loaded data. + */ + +/* + * An allocator function for passing to cbfs_alloc(). Takes the void-pointer argument to the + * allocator context data, the size of the file to bew loaded, and a pointer to the already + * loaded and verified file metadata (for rare cases where the allocator needs to check custom + * attributes). Must return a pointer to space of the requested size where file data should be + * loaded, or NULL to make operation fail. */ +typedef void *(*cbfs_allocator_t)(void *arg, size_t size, const union cbfs_mdata *mdata); + +static inline size_t cbfs_load(const char *name, void *buf, size_t size); +static inline size_t cbfs_ro_load(const char *name, void *buf, size_t size); + +static inline void *cbfs_map(const char *name, size_t *size_out); +static inline void *cbfs_ro_map(const char *name, size_t *size_out); + +/* Removes a previously allocated CBFS mapping. Should try to unmap mappings in strict LIFO + order where possible, since mapping backends often don't support more complicated cases */ +void cbfs_unmap(void *mapping); + +/********************************************************************************************** + * INTERNAL HELPERS FOR INLINES, DO NOT USE. * + **********************************************************************************************/ + +void *_cbfs_alloc(const char *name, cbfs_allocator_t allocator, void *arg, size_t *size_out, + bool force_ro, enum cbfs_type *type); + +struct _cbfs_default_allocator_arg { + void *buf; + size_t buf_size; +}; +void *_cbfs_default_allocator(void *arg, size_t size, const union cbfs_mdata *unused); + + +/********************************************************************************************** + * INLINE IMPLEMENTATIONS * + **********************************************************************************************/ + +static inline void *cbfs_map(const char *name, size_t *size_out) +{ + return _cbfs_alloc(name, NULL, NULL, size_out, false, NULL); +} + +static inline void *cbfs_ro_map(const char *name, size_t *size_out) +{ + return _cbfs_alloc(name, NULL, NULL, size_out, true, NULL); +} + +static inline size_t _cbfs_load(const char *name, void *buf, size_t size, bool force_ro, + enum cbfs_type *type) +{ + struct _cbfs_default_allocator_arg arg = {.buf = buf, .buf_size = size}; + if (_cbfs_alloc(name, _cbfs_default_allocator, &arg, &size, force_ro, type)) + return size; + else + return false; +} + +static inline size_t cbfs_load(const char *name, void *buf, size_t size) +{ + return _cbfs_load(name, buf, size, false, NULL); +} + +static inline size_t cbfs_ro_load(const char *name, void *buf, size_t size) +{ + return _cbfs_load(name, buf, size, true, NULL); +} + +/********************************************************************************************** + * LEGACY API * + **********************************************************************************************/ const struct cbfs_header *get_cbfs_header(void); struct cbfs_file *cbfs_find(const char *name); void *cbfs_find_file(const char *name, int type); diff --git a/payloads/libpayload/include/cbfs/cbfs_err.h b/payloads/libpayload/include/cbfs/cbfs_err.h new file mode 100644 index 0000000..60521a5 --- /dev/null +++ b/payloads/libpayload/include/cbfs/cbfs_err.h @@ -0,0 +1,21 @@ +/* SPDX-License-Identifier: BSD-3-Clause OR GPL-2.0-or-later */ + +#ifndef _CBFS_CBFS_ERR_H_ +#define _CBFS_CBFS_ERR_H_ + +#include <stdint.h> + +enum cbfs_err { + CBFS_SUCCESS = 0, /**< CBFS function call completed successfully */ + CBFS_ERR = -1, /**< Generic CBFS error */ + CBFS_ERR_ARG = -2, /**< Invalid argument */ + + CBFS_IO = -400, /**< Underlying */ + CBFS_NOT_FOUND = -401, /**< File not found in directory */ + CBFS_HASH_MISPATCH = -402, /**< Hash validation failed */ + CBFS_CACHE_FULL = -403, /**< Metadata cache overflowed */ +}; + +typedef int32_t cbfs_err_t; + +#endif /* _CBFS_CBFS_ERR_H_ */ diff --git a/payloads/libpayload/include/cbfs/cbfs_glue.h b/payloads/libpayload/include/cbfs/cbfs_glue.h new file mode 100644 index 0000000..56f2fa5 --- /dev/null +++ b/payloads/libpayload/include/cbfs/cbfs_glue.h @@ -0,0 +1,41 @@ +#ifndef _CBFS_CBFS_GLUE_H +#define _CBFS_CBFS_GLUE_H + +#include <libpayload-config.h> +#include <boot_device.h> +#include <stdio.h> + + +#define CBFS_ENABLE_HASHING 0 + +#define ERROR(...) printf("CBFS ERROR: " __VA_ARGS__) +#define LOG(...) printf("CBFS: " __VA_ARGS__) +#define DEBUG(...) \ + do { \ + if (CONFIG(LP_DEBUG_CBFS)) \ + printf("CBFS DEBUG: " __VA_ARGS__); \ + } while (0) + +struct cbfs_dev { + size_t offset; + size_t size; +}; + +typedef const struct cbfs_dev *cbfs_dev_t; + +static inline size_t cbfs_dev_offset(cbfs_dev_t dev) +{ + return dev->offset; +} + +static inline ssize_t cbfs_dev_read(cbfs_dev_t dev, void *buffer, size_t offset, size_t size) +{ + return libpayload_boot_device_read(buffer, cbfs_dev_offset(dev) + offset, size); +} + +static inline size_t cbfs_dev_size(cbfs_dev_t dev) +{ + return dev->size; +} + +#endif /* _CBFS_CBFS_GLUE_H */ diff --git a/payloads/libpayload/include/cbfs/cbfs_mdata.h b/payloads/libpayload/include/cbfs/cbfs_mdata.h new file mode 100644 index 0000000..5bfcddd --- /dev/null +++ b/payloads/libpayload/include/cbfs/cbfs_mdata.h @@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: BSD-3-Clause OR GPL-2.0-or-later */ + +#ifndef _CBFS_CBFS_MDATA_H_ +#define _CBFS_CBFS_MDATA_H_ + +#include <cbfs_core.h> +#include <stddef.h> + +/* + * Helper structure to allocate space for a blob of metadata on the stack. All functions using + * a cbfs_mdata should be getting it via cbfs_walk(), and can rely on the fact that cbfs_walk() + * has already fully validated the header (range checks for `len`, `attributes_offset` and + * `offset`, and null-termination for `filename`). + * NOTE: The fields in any union cbfs_mdata or any of its substructures from cbfs_serialized.h + * should always remain in the same byte order as they are stored on flash (= big endian). To + * avoid byte-order confusion, fields should always and only be converted to host byte order at + * exactly the time they are read from one of these structures into their own separate variable. + */ +union cbfs_mdata { + struct cbfs_file h; + uint8_t raw[CBFS_METADATA_MAX_SIZE]; +}; + + +/* + * Finds a CBFS file attribute in a medatada block. Attribute returned as-is (still big-endian) + * If |size_check| is not 0, will check that it matches the length of the attribute (if found) + * else the caller is responsible for checking the |len| field to avoid reading out-of-bounds. + */ +const void *cbfs_find_attr(const union cbfs_mdata *mdata, uint32_t attr_tag, size_t size_check); + +#endif /* _CBFS_CBFS_MDATA_H_ */ diff --git a/payloads/libpayload/include/cbfs/cbfs_private.h b/payloads/libpayload/include/cbfs/cbfs_private.h new file mode 100644 index 0000000..3a682e0 --- /dev/null +++ b/payloads/libpayload/include/cbfs/cbfs_private.h @@ -0,0 +1,134 @@ +/* SPDX-License-Identifier: BSD-3-Clause OR GPL-2.0-or-later */ + +#ifndef _CBFS_CBFS_PRIVATE_H_ +#define _CBFS_CBFS_PRIVATE_H_ + + +#include <cbfs/cbfs_err.h> +#include <cbfs/cbfs_mdata.h> +#include <stdbool.h> +#include <stdint.h> +#include <stddef.h> + +/* + * This header implements low-level CBFS access APIs that can be shared across different + * host applications (e.g. coreboot, libpayload, cbfstool). For verification purposes it + * implements the metadata hashing part but not the file hashing part, so the host application + * will need to verify file hashes itself after loading each file. Host applications that use + * verification should implement wrapper APIs that combine the lookup, loading and hashing steps + * into a single, safe function call and outside of the code implementing those APIs should not + * be accessing the low-level APIs in this file directly (e.g. coreboot SoC/driver code should + * never directly #include this file, and always use the higher level APIs in src/lib/cbfs.c). + * + * <cbfs_glue.h> needs to be provided by the host application using this CBFS library. It must + * define the following type, macros and functions: + * + * cbfs_dev_t An opaque type representing a CBFS storage backend. + * CBFS_ENABLE_HASHING Should be 0 to avoid linking hashing features, 1 otherwise. (Only for + * metadata hashing. Host application needs to check file hashes itself.) + * ERROR(...) printf-style macro to print errors. + * LOG(...) printf-style macro to print normal-operation log messages. + * DEBUG(...) printf-style macro to print detailed debug output. + * + * ssize_t cbfs_dev_read(cbfs_dev_t dev, void *buffer, size_t offset, size_t size); + * Read |size| bytes starting at |offset| from |dev| into |buffer|. + * Returns amount of bytes read on success and < 0 on error. + * This function *MUST* sanity-check offset/size on its own. + * + * size_t cbfs_dev_size(cbfs_dev_t dev); + * Return the total size in bytes of the CBFS storage (actual CBFS area). + */ +#include <cbfs/cbfs_glue.h> + +/* + * Data structure that represents "a" CBFS boot device, with optional metadata cache. + */ +struct cbfs_boot_device { + struct cbfs_dev dev; + void *mcache; + size_t mcache_size; +}; + +/* Helper to fill out |mcache| and |mcache_size| in a cbfs_boot_device. + * Only RO and RW are supported + */ +void cbfs_boot_device_find_mcache(struct cbfs_boot_device *cbd, bool is_ro); + +const struct cbfs_boot_device *cbfs_get_boot_device(bool force_ro); + +/* Flags that modify behavior of cbfs_walk(). */ +enum cbfs_walk_flags { + /* Write the calculated hash back out to |metadata_hash->hash| rather than comparing it. + |metadata_hash->algo| must still have been initialized by the caller. */ + CBFS_WALK_WRITEBACK_HASH = (1 << 0), + /* Call |walker| for empty file entries (i.e. entries with one of the CBFS_TYPE_DELETED + types that mark free space in the CBFS). Otherwise, those entries will be skipped. + Either way, these entries are never included in the metadata_hash calculation. */ + CBFS_WALK_INCLUDE_EMPTY = (1 << 1), +}; + +/* + * Traverse a CBFS and call a |walker| callback function for every file. Can additionally + * calculate a hash over the metadata of all files in the CBFS. If |metadata_hash| is NULL, + * hashing is disabled. If |walker| is NULL, will just traverse and hash the CBFS without + * invoking any callbacks (and always return CB_CBFS_NOT_FOUND unless there was another error). + * + * |arg| and |dev| will be passed through to |walker| unmodified. |offset| is the absolute + * offset in |dev| at which the current file metadata starts. |mdata| is a temporary buffer + * (only valid for the duration of this call to |walker|) containing already read metadata from + * the current file, up to |already_read| bytes. This will always at least contain the header + * fields and filename, but may contain more (i.e. attributes), depending on whether hashing is + * enabled. |walker| should call into cbfs_copy_fill_medadata() to copy the metadata of a file + * to a persistent buffer and automatically load remaining metadata from |dev| as needed based + * on the value of |already_read|. + * + * |walker| should return CB_CBFS_NOT_FOUND if it wants to continue being called for further + * files. Any other return code will be used as the final return code for cbfs_walk(). It will + * return immediately unless it needs to calculate a hash in which case it will still traverse + * the remaining CBFS (but not call |walker| anymore). + * + * Returns, from highest to lowest priority: + * CBFS_IO - There was an IO error with the CBFS device (always considered fatal) + * CBFS_HASH_MISMATCH - |metadata_hash| was provided and did not match the CBFS + * CBFS_SUCCESS/<other> - First non-CBFS_NOT_FOUND code returned by walker() + * CBFS_NOT_FOUND - walker() returned CBFS_NOT_FOUND for every file in the CBFS + */ +cbfs_err_t cbfs_walk(cbfs_dev_t dev, + cbfs_err_t (*walker)(cbfs_dev_t dev, size_t offset, + const union cbfs_mdata *mdata, size_t already_read, + void *arg), + void *arg, void *metadata_hash_unused, enum cbfs_walk_flags); + +/* + * Helper function that can be used by a |walker| callback to cbfs_walk() to copy the metadata + * of a file into a permanent buffer. Will copy the |already_read| metadata from |src| into + * |dst| and load remaining metadata from |dev| as required. + */ +cbfs_err_t cbfs_copy_fill_metadata(union cbfs_mdata *dst, const union cbfs_mdata *src, + size_t already_read, cbfs_dev_t dev, size_t offset); + +/* Find a file named |name| in the CBFS on |dev|. Copy its metadata (including attributes) + * into |mdata_out| and pass out the offset to the file data on the CBFS device. + * Verify the metadata with |metadata_hash| if provided. */ +cbfs_err_t cbfs_lookup(cbfs_dev_t dev, const char *name, union cbfs_mdata *mdata_out, + size_t *data_offset_out, void *metadata_hash_unused); + +/* Both base address and size of CBFS mcaches must be aligned to this value! */ +#define CBFS_MCACHE_ALIGNMENT sizeof(uint32_t) /* Largest data type used in CBFS */ + +/* Build an in-memory CBFS metadata cache out of the CBFS on |dev| into a |mcache_size| bytes + * memory area at |mcache|. Also verify |metadata_hash| unless it is NULL. If this returns + * CB_CBFS_CACHE_FULL, the mcache is still valid and can be used, but lookups may return + * CB_CBFS_CACHE_FULL for files that didn't fit to indicate that the caller needs to fall back + * to cbfs_lookup(). */ +cbfs_err_t cbfs_mcache_build(cbfs_dev_t dev, void *mcache, size_t mcache_size, + void *metadata_hash_unused); + +/* + * Find a file named |name| in a CBFS metadata cache and copy its metadata into |mdata_out|. + * Pass out offset to the file data (on the original CBFS device used for cbfs_mcache_build()). + */ +cbfs_err_t cbfs_mcache_lookup(const void *mcache, size_t mcache_size, const char *name, + union cbfs_mdata *mdata_out, size_t *data_offset_out); + +#endif /* _CBFS_CBFS_PRIVATE_H_ */ diff --git a/payloads/libpayload/include/cbfs_core.h b/payloads/libpayload/include/cbfs_core.h index fc4caa4..5c14737 100644 --- a/payloads/libpayload/include/cbfs_core.h +++ b/payloads/libpayload/include/cbfs_core.h @@ -50,32 +50,51 @@ #include <stdint.h> #include <stdlib.h>
+/* This function needs to be implemented to use _cbfs_alloc() based API */ +size_t cbfs_read(size_t address, size_t size); + /** These are standard values for the known compression alogrithms that coreboot knows about for stages and payloads. Of course, other CBFS users can use whatever values they want, as long as they understand them. */
-#define CBFS_COMPRESS_NONE 0 -#define CBFS_COMPRESS_LZMA 1 -#define CBFS_COMPRESS_LZ4 2 +enum cbfs_compression { + CBFS_COMPRESS_NONE = 0, + CBFS_COMPRESS_LZMA = 1, + CBFS_COMPRESS_LZ4 = 2, +};
/** These are standard component types for well known components (i.e - those that coreboot needs to consume. Users are welcome to use any other value for their components */
-#define CBFS_TYPE_STAGE 0x10 -#define CBFS_TYPE_SELF 0x20 -#define CBFS_TYPE_FIT 0x21 -#define CBFS_TYPE_OPTIONROM 0x30 -#define CBFS_TYPE_BOOTSPLASH 0x40 -#define CBFS_TYPE_RAW 0x50 -#define CBFS_TYPE_VSA 0x51 -#define CBFS_TYPE_MBI 0x52 -#define CBFS_TYPE_MICROCODE 0x53 -#define CBFS_TYPE_STRUCT 0x70 -#define CBFS_COMPONENT_CMOS_DEFAULT 0xaa -#define CBFS_COMPONENT_CMOS_LAYOUT 0x01aa +enum cbfs_type { + CBFS_TYPE_QUERY = 0, + CBFS_TYPE_DELETED = 0x00000000, + CBFS_TYPE_NULL = 0xffffffff, + CBFS_TYPE_BOOTBLOCK = 0x01, + CBFS_TYPE_CBFSHEADER = 0x02, + CBFS_TYPE_LEGACY_STAGE = 0x10, + CBFS_TYPE_STAGE = 0x11, + CBFS_TYPE_SELF = 0x20, + CBFS_TYPE_FIT = 0x21, + CBFS_TYPE_OPTIONROM = 0x30, + CBFS_TYPE_BOOTSPLASH = 0x40, + CBFS_TYPE_RAW = 0x50, + CBFS_TYPE_VSA = 0x51, + CBFS_TYPE_MBI = 0x52, + CBFS_TYPE_MICROCODE = 0x53, + CBFS_TYPE_FSP = 0x60, + CBFS_TYPE_MRC = 0x61, + CBFS_TYPE_MMA = 0x62, + CBFS_TYPE_EFI = 0x63, + CBFA_TYPE_STRUCT = 0x70, + CBFS_TYPE_CMOS_DEFAULT = 0xaa, + CBFS_TYPE_SPD = 0xab, + CBFS_TYPE_MRC_CACHE = 0xac, + CBFS_TYPE_CMOS_LAYOUT = 0x01aa, +};
#define CBFS_HEADER_MAGIC 0x4F524243 #define CBFS_HEADER_VERSION1 0x31313131 @@ -105,10 +124,16 @@ /* "Unknown" refers to CBFS headers version 1, * before the architecture was defined (i.e., x86 only). */ -#define CBFS_ARCHITECTURE_UNKNOWN 0xFFFFFFFF -#define CBFS_ARCHITECTURE_X86 0x00000001 -#define CBFS_ARCHITECTURE_ARM 0x00000010 -#define CBFS_ARCHITECTURE_ARM64 0x00000011 +enum cbfs_architecture { + CBFS_ARCHITECTURE_UNKNOWN = 0xFFFFFFFF, + CBFS_ARCHITECTURE_X86 = 0x00000001, + CBFS_ARCHITECTURE_ARM = 0x00000010, + CBFS_ARCHITECTURE_ARM64 = 0x00000011, + CBFS_ARCHITECTURE_AARCH64 = 0x0000aa64, + CBFS_ARCHITECTURE_MIPS = 0x00000100, + CBFS_ARCHITECTURE_RISCV = 0xc001d0de, + CBFS_ARCHITECTURE_PPC64 = 0x407570ff, +};
/** This is a component header - every entry in the CBFS will have this header. @@ -126,6 +151,7 @@ */
#define CBFS_FILE_MAGIC "LARCHIVE" +#define CBFS_METADATA_MAX_SIZE 256
struct cbfs_file { char magic[8]; @@ -133,16 +159,26 @@ uint32_t type; uint32_t attributes_offset; uint32_t offset; - char filename[]; + char filename[0]; } __packed;
+#if defined __GNUC__ && (__GNUC__ * 100 + __GNUC_MINOR__) >= 406 +_Static_assert(sizeof(struct cbfs_file) == 24, "cbfs_file size mismatch"); +#endif + /* Depending on how the header was initialized, it may be backed with 0x00 or * 0xff. Support both. */ -#define CBFS_FILE_ATTR_TAG_UNUSED 0 -#define CBFS_FILE_ATTR_TAG_UNUSED2 0xffffffff -#define CBFS_FILE_ATTR_TAG_COMPRESSION 0x42435a4c -#define CBFS_FILE_ATTR_TAG_HASH 0x68736148 -#define CBFS_FILE_ATTR_TAG_IBB 0x32494242 /* Initial BootBlock */ +enum cbfs_file_attr_tag { + CBFS_FILE_ATTR_TAG_UNUSED = 0, + CBFS_FILE_ATTR_TAG_UNUSED2 = 0xffffffff, + CBFS_FILE_ATTR_TAG_COMPRESSION = 0x42435a4c, + CBFS_FILE_ATTR_TAG_HASH = 0x68736148, + CBFS_FILE_ATTR_TAG_POSITION = 0x42435350, + CBFS_FILE_ATTR_TAG_ALIGNMENT = 0x42434c41, + CBFS_FILE_ATTR_TAG_IBB = 0x32494242, /* Initial BootBlock */ + CBFS_FILE_ATTR_TAG_PADDING = 0x47444150, + CBFS_FILE_ATTR_TAG_STAGEHEADER = 0x53746748, +};
/* The common fields of extended cbfs file attributes. Attributes are expected to start with tag/len, then append their @@ -154,6 +190,9 @@ uint8_t data[0]; } __packed;
+/* All attribute sizes must be divisible by this value! */ +#define CBFS_ATTRIBUTE_ALIGN 4 + struct cbfs_file_attr_compression { uint32_t tag; uint32_t len; @@ -167,7 +206,27 @@ uint32_t len; uint32_t hash_type; /* hash_data is len - sizeof(struct) bytes */ - uint8_t hash_data[]; + uint8_t hash_data[0]; +} __packed; + +struct cbfs_file_attr_position { + uint32_t tag; + uint32_t len; + uint32_t position; +} __packed; + +struct cbfs_file_attr_align { + uint32_t tag; + uint32_t len; + uint32_t alignment; +} __packed; + +struct cbfs_file_attr_stageheader { + uint32_t tag; + uint32_t len; + uint64_t loadaddr; /* Memory address to load the code to. */ + uint32_t entry_offset; /* Offset of entry point form loadaddr. */ + uint32_t memlen; /* Total length (including BSS) in memory. */ } __packed;
/*** Component sub-headers ***/ @@ -202,11 +261,13 @@ struct cbfs_payload_segment segments; };
-#define PAYLOAD_SEGMENT_CODE 0x45444F43 -#define PAYLOAD_SEGMENT_DATA 0x41544144 -#define PAYLOAD_SEGMENT_BSS 0x20535342 -#define PAYLOAD_SEGMENT_PARAMS 0x41524150 -#define PAYLOAD_SEGMENT_ENTRY 0x52544E45 +enum cbfs_payload_segment_type { + PAYLOAD_SEGMENT_CODE = 0x45444F43, + PAYLOAD_SEGMENT_DATA = 0x41544144, + PAYLOAD_SEGMENT_BSS = 0x20535342, + PAYLOAD_SEGMENT_PARAMS = 0x41524150, + PAYLOAD_SEGMENT_ENTRY = 0x52544E45, +};
struct cbfs_optionrom { uint32_t compression; diff --git a/payloads/libpayload/libcbfs/Kconfig b/payloads/libpayload/libcbfs/Kconfig new file mode 100644 index 0000000..39992a0 --- /dev/null +++ b/payloads/libpayload/libcbfs/Kconfig @@ -0,0 +1,27 @@ +## SPDX-License-Identifier: BSD-3-Clause OR GPL-2.0-or-later + +if CBFS + +config DEBUG_CBFS + bool "Output verbose CBFS debug messages" + default n + help + This option enables additional CBFS related debug messages. + +config NO_CBFS_MCACHE + bool + help + Disables the CBFS metadata cache. This means that your platform does + not need to provide coreboot table CB_TAG_CBMEM_ENTRY entries with + CBMEM_ID_CBFS_RO_MCACHE and CBMEM_ID_CBFS_RW_MCACHE. In that case + every signe CBFS file lookup must re-read the same CBFS directory + entries from flash to find the respective file. + +config ENABLE_CBFS_FALLBACK + bool + default n + help + When this option is enabled, cbfs_boot_locate will look for a CBFS file + in the RO (COREBOOT) if it isn't available in the acrive RW region. + +endif diff --git a/payloads/libpayload/libcbfs/cbfs.c b/payloads/libpayload/libcbfs/cbfs.c index d24b528..f715ea5 100644 --- a/payloads/libpayload/libcbfs/cbfs.c +++ b/payloads/libpayload/libcbfs/cbfs.c @@ -38,37 +38,410 @@ # include <lz4.h> # define CBFS_CORE_WITH_LZ4 # endif -# define CBFS_MINI_BUILD -#elif defined(__SMM__) -# define CBFS_MINI_BUILD -#else -# define CBFS_CORE_WITH_LZMA -# define CBFS_CORE_WITH_LZ4 -# include <lib.h> #endif
+#include <assert.h> #include <cbfs.h> +#include <cbfs/cbfs_mdata.h> +#include <cbfs/cbfs_private.h> +#include <fmap_serialized.h> +#include <libpayload.h> +#include <sysinfo.h> #include <string.h>
-#ifdef LIBPAYLOAD -# include <stdio.h> -# define DEBUG(x...) -# define LOG(x...) -# define ERROR(x...) printf(x) -#else -# include <console/console.h> -# define ERROR(x...) printk(BIOS_ERR, "CBFS: " x) -# define LOG(x...) printk(BIOS_INFO, "CBFS: " x) -# if CONFIG_LP_DEBUG_CBFS -# define DEBUG(x...) printk(BIOS_SPEW, "CBFS: " x) -# else -# define DEBUG(x...) -# endif -#endif - #include "cbfs_core.c"
#ifndef __SMM__ + +void cbfs_boot_device_find_mcache(struct cbfs_boot_device *cbd, bool is_ro) +{ + if (CONFIG(LP_NO_CBFS_MCACHE)) + return; + + if (cbd->mcache_size) + return; + + if (is_ro) { + cbd->mcache = (void *)lib_sysinfo.cbfs_ro_mcache_offset; + cbd->mcache_size = lib_sysinfo.cbfs_ro_mcache_size; + } else { + cbd->mcache = (void *)lib_sysinfo.cbfs_rw_mcache_offset; + cbd->mcache_size = lib_sysinfo.cbfs_rw_mcache_size; + } +} + +const struct cbfs_boot_device *cbfs_get_boot_device(bool force_ro) +{ + static struct cbfs_boot_device ro; + static struct cbfs_boot_device rw; + + /* RO should always be initialized */ + if (!cbfs_dev_size(&ro.dev) && !force_ro) + cbfs_get_boot_device(true); + + if (!force_ro) { + if (!cbfs_dev_size(&rw.dev)) { + if (lib_sysinfo.cbfs_offset != 0 && lib_sysinfo.cbfs_size != 0) { + rw.dev.offset = lib_sysinfo.cbfs_offset; + rw.dev.size = lib_sysinfo.cbfs_size; + } else { + DEBUG("Rw region not present, switching to RO\n"); + return cbfs_get_boot_device(true); + } + } + cbfs_boot_device_find_mcache(&rw, false); + return &rw; + } + + cbfs_boot_device_find_mcache(&ro, false); + + if (cbfs_dev_size(&ro.dev)) + return &ro; + + uint32_t offset = 0; + uint32_t size = 0; + if (!lib_sysinfo.fmap_offset + || fmap_region_by_name(lib_sysinfo.fmap_offset, "COREBOOT", &offset, &size)) + die("Cannot locate primary CBFS"); + + ro.dev.offset = offset; + ro.dev.size = size; + return &ro; +} + +static cbfs_err_t read_next_header(cbfs_dev_t dev, size_t *offset, struct cbfs_file *buffer, + const size_t devsize) +{ + DEBUG("Looking for the next file @%#zx...\n", *offset); + *offset = ALIGN_UP(*offset, CBFS_ALIGNMENT); + while (*offset + sizeof(*buffer) < devsize) { + if (cbfs_dev_read(dev, buffer, *offset, sizeof(*buffer)) != sizeof(*buffer)) + return CBFS_IO; + + if (memcmp(buffer->magic, CBFS_FILE_MAGIC, sizeof(buffer->magic)) == 0) + return CBFS_SUCCESS; + + *offset += CBFS_ALIGNMENT; + } + + DEBUG("End of CBFS reached\n"); + return CBFS_NOT_FOUND; +} + +cbfs_err_t cbfs_walk(cbfs_dev_t dev, + cbfs_err_t (*walker)(cbfs_dev_t dev, size_t offset, + const union cbfs_mdata *mdata, size_t already_read, + void *arg), + void *arg, void *metadata_hash_unused, enum cbfs_walk_flags flags) +{ + const size_t devsize = cbfs_dev_size(dev); + size_t offset = 0; + cbfs_err_t ret_header; + cbfs_err_t ret_walker = CBFS_NOT_FOUND; + union cbfs_mdata mdata; + while ((ret_header = read_next_header(dev, &offset, &mdata.h, devsize)) + == CBFS_SUCCESS) { + const uint32_t attr_offset = be32toh(mdata.h.attributes_offset); + const uint32_t data_offset = be32toh(mdata.h.offset); + const uint32_t data_length = be32toh(mdata.h.len); + const uint32_t type = be32toh(mdata.h.type); + const bool empty = (type == CBFS_TYPE_DELETED || type == CBFS_TYPE_NULL); + + DEBUG("found CBFS header @%#zx (type %d, attr +%#x, data +%#x, length %#x)\n", + offset, type, attr_offset, data_offset, data_length); + if (data_offset > sizeof(mdata) || data_length > devsize + || offset + data_offset + data_length > devsize) { + ERROR("File @%#zx too large\n", offset); + offset += CBFS_ALIGNMENT; + continue; + } + + if (empty && !(flags & CBFS_WALK_INCLUDE_EMPTY)) + goto next_file; + + ssize_t todo; + if (attr_offset == 0) + todo = data_offset - sizeof(mdata.h); + else + todo = attr_offset - sizeof(mdata.h); + if (todo <= 0 || data_offset < attr_offset) { + ERROR("Corrupt file header @%#zx\n", offset); + goto next_file; + } + + /* Read the rest of the metadata (filename, and possibly attributes) */ + assert(todo > 0 && todo <= sizeof(mdata) - sizeof(mdata.h)); + if (cbfs_dev_read(dev, mdata.raw + sizeof(mdata.h), offset + sizeof(mdata.h), + todo) + != todo) + return CBFS_IO; + + /* Force filename null-termination, just in case. */ + mdata.raw[attr_offset ? attr_offset - 1 : data_offset - 1] = '\0'; + DEBUG("File name: '%s'\n", mdata.h.filename); + + if (walker && ret_walker == CBFS_NOT_FOUND) + ret_walker = walker(dev, offset, &mdata, sizeof(mdata.h) + todo, arg); + + /* Return IO errors immediately. For others, finish the hash first if needed. */ + if (ret_walker != CBFS_NOT_FOUND) + return ret_walker; + +next_file: + offset += data_offset + data_length; + } + + if (ret_header != CBFS_NOT_FOUND) + return ret_header; + + return ret_header; +} + +cbfs_err_t cbfs_copy_fill_metadata(union cbfs_mdata *dst, const union cbfs_mdata *src, + size_t already_read, cbfs_dev_t dev, size_t offset) +{ + /* First copy the stuff that cbfs_walk already read for us. */ + memcpy(dst, src, already_read); + + /* then read in whatever metadata may be left (will only happen in non-hashing case). */ + const size_t todo = be32toh(src->h.offset) - already_read; + assert(todo <= sizeof(*dst) - already_read); + if (todo + && libpayload_boot_device_read(dst->raw + already_read, + dev->offset + offset + already_read, todo) + != todo) + return CBFS_IO; + return CBFS_SUCCESS; +} + +struct cbfs_lookup_args { + union cbfs_mdata *mdata_out; + const char *name; + size_t namesize; + size_t *data_offset_out; +}; + +static cbfs_err_t lookup_walker(cbfs_dev_t dev, size_t offset, const union cbfs_mdata *mdata, + size_t already_read, void *arg) +{ + struct cbfs_lookup_args *args = arg; + /* Check if the name we are looking for could fit, then we can safely memcmp() it. */ + if (args->namesize > already_read - offsetof(union cbfs_mdata, h.filename) + || memcmp(args->name, mdata->h.filename, args->namesize) != 0) + return CBFS_NOT_FOUND; + + LOG("Found '%s' @%#zx size %#x\n", args->name, offset, be32toh(mdata->h.len)); + if (cbfs_copy_fill_metadata(args->mdata_out, mdata, already_read, dev, offset)) + return CBFS_IO; + + *args->data_offset_out = offset + be32toh(mdata->h.offset); + return CBFS_SUCCESS; +} + +cbfs_err_t cbfs_lookup(cbfs_dev_t dev, const char *name, union cbfs_mdata *mdata_out, + size_t *data_offset_out, void *metadata_hash_unused) +{ + struct cbfs_lookup_args args = { + .mdata_out = mdata_out, + .name = name, + .namesize = strlen(name) + 1, + .data_offset_out = data_offset_out, + }; + return cbfs_walk(dev, lookup_walker, &args, metadata_hash_unused, 0); +} + +static cbfs_err_t cbfs_boot_lookup(const char *name, bool force_ro, union cbfs_mdata *mdata, + struct cbfs_dev *dev) +{ + const struct cbfs_boot_device *cbd = cbfs_get_boot_device(force_ro); + if (!cbd) + return CBFS_ERR; + + size_t data_offset; + cbfs_err_t err = CBFS_CACHE_FULL; + if (!CONFIG(LP_NO_CBFS_MCACHE) && cbd->mcache_size) + err = cbfs_mcache_lookup(cbd->mcache, cbd->mcache_size, name, mdata, + &data_offset); + + if (err == CBFS_CACHE_FULL) + err = cbfs_lookup(&cbd->dev, name, mdata, &data_offset, NULL); + + /* Fallback to RO if possible. */ + if (CONFIG(LP_VBOOT_ENABLE_CBFS_FALLBACK) && !force_ro && err == CBFS_NOT_FOUND) { + LOG("Fall back to RO region for '%s'\n", name); + return cbfs_boot_lookup(name, true, mdata, dev); + } + + if (err) { + if (err == CBFS_NOT_FOUND) + LOG("'%s' not found.\n", name); + else if (err == CBFS_HASH_MISPATCH) + ERROR("Metadata hash mismatch for '%s'\n", name); + else + ERROR("Error %d when looking up '%s'\n", err, name); + return err; + } + + dev->offset = cbfs_dev_offset(&cbd->dev) + data_offset; + dev->size = be32toh(mdata->h.len); + + return CBFS_SUCCESS; +} + +void cbfs_unmap(void *mapping) +{ + free(mapping); +} + +const void *cbfs_find_attr(const union cbfs_mdata *mdata, uint32_t attr_tag, size_t size_check) +{ + uint32_t offset = be32toh(mdata->h.attributes_offset); + uint32_t end = be32toh(mdata->h.offset); + + if (!offset) + return NULL; + + while (offset + sizeof(struct cbfs_file_attribute) <= end) { + const struct cbfs_file_attribute *attr = (const void *) mdata->raw + offset; + const uint32_t tag = be32toh(attr->tag); + const uint32_t len = be32toh(attr->len); + + if (len < sizeof(struct cbfs_file_attribute) || len > end - offset) { + ERROR("Attribute '%s'[%x] invalid length: %u\n", mdata->h.filename, tag, + len); + return NULL; + } + if (tag == attr_tag) { + if (size_check && len != size_check) { + ERROR("Attribute '%s'[%x] size mispatch %u != %zu\n", + mdata->h.filename, tag, len, size_check); + return NULL; + } + return attr; + } + offset += len; + } + + return NULL; +} + +static size_t cbfs_load_and_decompress(cbfs_dev_t dev, void *buffer, size_t buffer_size, + uint32_t compression, void *file_hash_unused) +{ + size_t in_size = cbfs_dev_size(dev); + size_t out_size = 0; + void *map; + + DEBUG("Decompressing %zu bytes to %p with algo %d\n", in_size, buffer, compression); + + switch (compression) { + case CBFS_COMPRESS_NONE: + if (buffer_size < in_size) + return 0; + if (libpayload_boot_device_read(buffer, cbfs_dev_offset(dev), in_size) + != in_size) + return 0; + return in_size; +#if CONFIG(LP_LZ4) + case CBFS_COMPRESS_LZ4: + map = malloc(in_size); + if (!map) + return 0; + + if (libpayload_boot_device_read(map, cbfs_dev_offset(dev), in_size) + != in_size) { + ERROR("Failed to read contents of file\n"); + free(map); + return 0; + } + + out_size = ulz4fn(map, in_size, buffer, buffer_size); + free(map); + + return out_size; +#endif +#if CONFIG(LP_LZMA) + case CBFS_COMPRESS_LZMA: + map = malloc(in_size); + if (!map) + return 0; + + if (libpayload_boot_device_read(map, cbfs_dev_offset(dev), in_size) + != in_size) { + ERROR("Failed to read contents of file\n"); + free(map); + return 0; + } + + out_size = ulzman(map, in_size, buffer, buffer_size); + free(map); + + return out_size; +#endif + default: + ERROR("Decompression algo %d not supported\n", compression); + return 0; + } +} + +void *_cbfs_alloc(const char *name, cbfs_allocator_t allocator, void *arg, size_t *size_out, + bool force_ro, enum cbfs_type *type) +{ + struct cbfs_dev dev; + union cbfs_mdata mdata; + void *loc; + + DEBUG("%s(name='%s', alloc=%p(%p), force_ro=%s, type=%d)\n", __func__, name, allocator, + arg, force_ro ? "true" : "false", type ? *type : -1); + + if (cbfs_boot_lookup(name, force_ro, &mdata, &dev)) + return NULL; + + if (type) { + const enum cbfs_type real_type = be32toh(mdata.h.type); + if (*type == CBFS_TYPE_QUERY) { + *type = real_type; + } else if (*type != real_type) { + ERROR("'%s' type mispatch (is %u, expected %u)\n", mdata.h.filename, + real_type, *type); + return NULL; + } + } + + size_t size = cbfs_dev_size(&dev); + uint32_t compression = CBFS_COMPRESS_NONE; + const struct cbfs_file_attr_compression *cattr = + cbfs_find_attr(&mdata, CBFS_FILE_ATTR_TAG_COMPRESSION, sizeof(*cattr)); + if (cattr) { + compression = be32toh(cattr->compression); + size = be32toh(cattr->decompressed_size); + } + + if (size_out) + *size_out = size; + + /* allocator == NULL means do a cbfs_map() */ + if (allocator) + loc = allocator(arg, size, &mdata); + else + loc = malloc(size); + + if (!loc) { + ERROR("'%s' allocation failure\n", mdata.h.filename); + return NULL; + } + + size = cbfs_load_and_decompress(&dev, loc, size, compression, NULL); + if (!size) + return NULL; + + return loc; +} + +/********************************************************************************************** + * Legacy API * + **********************************************************************************************/ + static inline int tohex4(unsigned int c) { return (c <= 9) ? (c + '0') : (c - 10 + 'a'); diff --git a/payloads/libpayload/libcbfs/cbfs_core.c b/payloads/libpayload/libcbfs/cbfs_core.c index 82c2846..bc51368 100644 --- a/payloads/libpayload/libcbfs/cbfs_core.c +++ b/payloads/libpayload/libcbfs/cbfs_core.c @@ -242,7 +242,6 @@ cbfs_get_attr(handle, CBFS_FILE_ATTR_TAG_COMPRESSION); if (comp) { algo = ntohl(comp->compression); - DEBUG("File '%s' is compressed (alg=%d)\n", name, algo); *size = ntohl(comp->decompressed_size); /* TODO: Implement partial decompression with |limit| */ } diff --git a/payloads/libpayload/libcbfs/cbfs_mcache.c b/payloads/libpayload/libcbfs/cbfs_mcache.c new file mode 100644 index 0000000..ab98e1f --- /dev/null +++ b/payloads/libpayload/libcbfs/cbfs_mcache.c @@ -0,0 +1,70 @@ +/* SPDX-License-Identifier: BSD-3-Clause OR GPL-2.0-or-later */ + +#include <assert.h> +#include <cbfs/cbfs_err.h> +#include <cbfs/cbfs_private.h> + +/* + * A CBFS metadata cache is an in-memory data structure storing CBFS file headers (== metadata). + * It is defined by its start pointer and size. It contains a sequence of variable-length + * union mcache_entry entries. There is no overall header structure for the cache. + * + * Each mcache_entry is the raw metadata for a CBFS file (including attributes) in the same form + * as stored on tlash (i.e. values are in big-endian), except that the CBFS magic signature in + * the first 8 bytes ('LARCHIVE') is overwritten witch mcache-internal bookkeeping data. + * The first 4 bytes are a magic number (MCACHE_MAGIC_FILE) and the next 4 bytes are + * the absolute offset in bytes on the cbfs_dev_t that this metadata blob was found at. (Note + * that depending on the cbfs_dev_t, this offset may still be relative to the start + * of a subregion of the underlying storage device.) + * + * The length of an mcache_entry (i.e. length of the underlying metadata blob) is encoded in the + * metadata (entry->file.h.offset). The next mcache_entry begins at the next + * CBFS_MCACHE_ALIGNMENT boundary after that. The cache is terminated ba a special 4-byte + * mcache_entry that consists only of a magic number (MCACHE_MAGIC_END or MCACHE_MAGIC_FULL). + */ + +#define MCACHE_MAGIC_FILE 0x454c4946 /* 'FILE' */ +#define MCACHE_MAGIC_FULL 0x4c4c5546 /* 'FULL' */ +#define MCACHE_MAGIC_END 0x444e4524 /* '$END' */ + +union mcache_entry { + union cbfs_mdata file; + struct { /* These fields exactly overlap file.h.magic */ + uint32_t magic; + uint32_t offset; + } __packed; +}; + +cbfs_err_t cbfs_mcache_lookup(const void *mcache, size_t mcache_size, const char *name, + union cbfs_mdata *mdata_out, size_t *data_offset_out) +{ + const size_t namesize = strlen(name) + 1; /* Count trailing \0 so we can memcmp() it */ + const void *end = mcache + mcache_size; + const void *current = mcache; + + while (current + sizeof(uint32_t) <= end) { + const union mcache_entry *entry = current; + + if (entry->magic == MCACHE_MAGIC_END) + return CBFS_NOT_FOUND; + if (entry->magic == MCACHE_MAGIC_FULL) + return CBFS_CACHE_FULL; + + assert(entry->magic == MCACHE_MAGIC_FILE); + const uint32_t data_offset = be32toh(entry->file.h.offset); + const uint32_t data_length = be32toh(entry->file.h.len); + if (namesize <= data_offset - offsetof(union cbfs_mdata, h.filename) + && memcmp(name, entry->file.h.filename, namesize) == 0) { + LOG("Found '%s' @%#x size %#x in mcache @%p\n", name, + entry->offset, data_length, current); + *data_offset_out = entry->offset + data_offset; + memcpy(mdata_out, &entry->file, data_offset); + return CBFS_SUCCESS; + } + + current += ALIGN_UP(data_offset, CBFS_MCACHE_ALIGNMENT); + } + + ERROR("CBFS_mcache_is_not_terminated!\n"); /* Should never happen */ + return CBFS_ERR; +} diff --git a/payloads/libpayload/tests/include/mocks/cbfs_util.h b/payloads/libpayload/tests/include/mocks/cbfs_util.h new file mode 100644 index 0000000..2dd171b --- /dev/null +++ b/payloads/libpayload/tests/include/mocks/cbfs_util.h @@ -0,0 +1,98 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef MOCKS_CBFS_UTIL_H +#define MOCKS_CBFS_UTIL_H + +#include <cbfs.h> +#include <stddef.h> +#include <tests/test.h> + +#define BE32(be32) EMPTY_WRAP(\ + ((be32) >> 24) & 0xff, ((be32) >> 16) & 0xff, \ + ((be32) >> 8) & 0xff, ((be32) >> 0) & 0xff) + +#define BE64(be64) EMPTY_WRAP( \ + BE32(((be64) >> 32) & 0xFFFFFFFF), \ + BE32(((be64) >> 0) & 0xFFFFFFFF)) + +#define LE32(val32) EMPTY_WRAP(\ + ((val32) >> 0) & 0xff, ((val32) >> 8) & 0xff, \ + ((val32) >> 16) & 0xff, ((val32) >> 24) & 0xff) + +#define LE64(val64) EMPTY_WRAP( \ + BE32(((val64) >> 0) & 0xFFFFFFFF), \ + BE32(((val64) >> 32) & 0xFFFFFFFF)) + +#define FILENAME_SIZE 16 + +struct cbfs_test_file { + struct cbfs_file header; + u8 filename[FILENAME_SIZE]; + u8 attrs_and_data[200]; +}; + +#define TEST_MCACHE_SIZE (2 * MiB) + +#define HEADER_INITIALIZER(ftype, attr_len, file_len) { \ + .magic = CBFS_FILE_MAGIC, \ + .len = htobe32(file_len), \ + .type = htobe32(ftype), \ + .attributes_offset = \ + htobe32(attr_len ? sizeof(struct cbfs_file) + FILENAME_SIZE : 0), \ + .offset = htobe32(sizeof(struct cbfs_file) + FILENAME_SIZE + attr_len), \ +} + +#define HASH_ATTR_SIZE (offsetof(struct cbfs_file_attr_hash, hash.raw) + VB2_SHA256_DIGEST_SIZE) + +/* This macro basically does nothing but suppresses linter messages */ +#define EMPTY_WRAP(...) __VA_ARGS__ + +#define TEST_DATA_1_FILENAME "test/data/1" +#define TEST_DATA_1_SIZE sizeof((u8[]){TEST_DATA_1}) +#define TEST_DATA_1 EMPTY_WRAP( \ + '!', '"', '#', '$', '%', '&', ''', '(', ')', '*', '+', ',', '-', '.', '/', \ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', \ + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', \ + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', \ + '[', '\', ']', '^', '_', '`', \ + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', \ + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z') + +#define TEST_DATA_2_FILENAME "test/data/2" +#define TEST_DATA_2_SIZE sizeof((u8[]){TEST_DATA_2}) +#define TEST_DATA_2 EMPTY_WRAP( \ + 0x9d, 0xa9, 0x91, 0xac, 0x5d, 0xb2, 0x70, 0x76, 0x37, 0x94, 0x94, 0xa8, 0x8b, 0x78, \ + 0xb9, 0xaa, 0x1a, 0x8e, 0x9a, 0x16, 0xbe, 0xdc, 0x29, 0x42, 0x46, 0x58, 0xd4, 0x37, \ + 0x94, 0xca, 0x05, 0xdb, 0x54, 0xfa, 0xd8, 0x6e, 0x54, 0xd8, 0x30, 0x46, 0x5d, 0x62, \ + 0xc2, 0xce, 0xd8, 0x74, 0x60, 0xaf, 0x83, 0x8f, 0xfa, 0x97, 0xdd, 0x6e, 0xcb, 0x60, \ + 0xfa, 0xed, 0x8b, 0x55, 0x9e, 0xc1, 0xc2, 0x18, 0x4f, 0xe2, 0x28, 0x7e, 0xd7, 0x2f, \ + 0xa2, 0x86, 0xfb, 0x4d, 0x3e, 0x00, 0x5a, 0xf7, 0xc2, 0xad, 0x0e, 0xa7, 0xa2, 0xf7, \ + 0x38, 0x66, 0xe6, 0x5c, 0x76, 0x98, 0x89, 0x63, 0xeb, 0xc5, 0xf5, 0xb7, 0xa7, 0x58, \ + 0xe0, 0xf0, 0x2e, 0x2f, 0xb0, 0x95, 0xb7, 0x43, 0x28, 0x19, 0x2d, 0xef, 0x1a, 0xb3, \ + 0x42, 0x31, 0x55, 0x0f, 0xbc, 0xcd, 0x01, 0xe5, 0x39, 0x18, 0x88, 0x83, 0xb2, 0xc5, \ + 0x4b, 0x3b, 0x38, 0xe7) + +#define TEST_DATA_INT_1_FILENAME "test-int-1" +#define TEST_DATA_INT_1_SIZE 8 +#define TEST_DATA_INT_1 0xFEDCBA9876543210ULL + +#define TEST_DATA_INT_2_FILENAME "test-int-2" +#define TEST_DATA_INT_2_SIZE 8 +#define TEST_DATA_INT_2 0x10FE32DC54A97698ULL + +#define TEST_DATA_INT_3_FILENAME "test-int-3" +#define TEST_DATA_INT_3_SIZE 8 +#define TEST_DATA_INT_3 0xFA57F003B0036667ULL + +extern const u8 test_data_1[TEST_DATA_1_SIZE]; +extern const u8 test_data_2[TEST_DATA_2_SIZE]; +extern const u8 test_data_int_1[TEST_DATA_INT_1_SIZE]; +extern const u8 test_data_int_2[TEST_DATA_INT_2_SIZE]; +extern const u8 test_data_int_3[TEST_DATA_INT_3_SIZE]; + +extern const struct cbfs_test_file test_file_1; +extern const struct cbfs_test_file test_file_2; +extern const struct cbfs_test_file test_file_int_1; +extern const struct cbfs_test_file test_file_int_2; +extern const struct cbfs_test_file test_file_int_3; + +#endif /* MOCKS_CBFS_UTIL_H */ diff --git a/payloads/libpayload/tests/libcbfs/Makefile.inc b/payloads/libpayload/tests/libcbfs/Makefile.inc new file mode 100644 index 0000000..efdcff4 --- /dev/null +++ b/payloads/libpayload/tests/libcbfs/Makefile.inc @@ -0,0 +1,19 @@ +tests-y += cbfs-lookup-no-mcache-test +# WPI: tests-y += cbfs-lookup-mcache-test + +cbfs-lookup-no-mcache-test-srcs := tests/libcbfs/cbfs-lookup-test.c +cbfs-lookup-no-mcache-test-srcs += tests/mocks/cbfs_file_mock.c +cbfs-lookup-no-mcache-test-srcs += tests/mocks/die.c +cbfs-lookup-no-mcache-test-srcs += libcbfs/cbfs.c +cbfs-lookup-no-mcache-test-config += CONFIG_LP_NO_CBFS_MCACHE=1 +cbfs-lookup-no-mcache-test-config += CONFIG_LP_DEBUG_CBFS=1 +cbfs-lookup-no-mcache-test-config += CONFIG_LP_LZ4=1 +cbfs-lookup-no-mcache-test-config += CONFIG_LP_LZMA=1 +cbfs-lookup-no-mcache-test-mocks += cbfs_get_boot_device cbfs_lookup +cbfs-lookup-no-mcache-test-cflags += -ggdb -Og -fsanitize=address -lasan + +# WIP: +# $(call copy-test, cbfs-lookup-no-mcache-test, cbfs-lookup-mcache-test) +# cbfs-lookup-mcache-test-srcs += libcbfs/cbfs_mcache.c +# cbfs-lookup-mcache-test-config += CONFIG_LP_NO_CBFS_MCACHE=0 +# cbfs-lookup-mcache-test-mocks += cbfs_mcache_lookup diff --git a/payloads/libpayload/tests/libcbfs/cbfs-lookup-test.c b/payloads/libpayload/tests/libcbfs/cbfs-lookup-test.c new file mode 100644 index 0000000..612a801 --- /dev/null +++ b/payloads/libpayload/tests/libcbfs/cbfs-lookup-test.c @@ -0,0 +1,963 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ + +#include <libpayload-config.h> +#include <cbfs.h> +#include <cbfs/cbfs_glue.h> +#include <cbfs/cbfs_private.h> +#include <endian.h> +#include <libpayload.h> +#include <mocks/cbfs_util.h> +#include <mocks/boot_device.h> +#include <stdlib.h> +#include <sysinfo.h> +#include <tests/test.h> + +/* This test focuses on checking and covering the lookup mechanisms of the libCBFS API. + * Specifically the cbfs_map() and cbfs_load() functions. Moreover it checks if the MCache works + * correctly. The RO fallback and verification are not tested here, because it would + * overcomplicate the test. + */ + + +/* Mocks */ + +struct sysinfo_t lib_sysinfo; +static struct cbfs_boot_device cbd; + +size_t ulzman(const void *src, size_t srcn, void *dst, size_t dstn) +{ + check_expected(srcn); + check_expected(dstn); + memcpy(dst, src, dstn); + return dstn; +} + +size_t ulz4fn(const void *src, size_t srcn, void *dst, size_t dstn) +{ + check_expected(srcn); + check_expected(dstn); + memcpy(dst, src, dstn); + return dstn; +} + +int fmap_region_by_name(const uint32_t fmap_offset, const char *const name, + uint32_t *const offset, uint32_t *const size) +{ + *offset = mock_type(uint32_t); + *size = mock_type(uint32_t); + return 0; +} + +void setup_fmap_region_by_name_call(size_t offset, size_t size) +{ + will_return(fmap_region_by_name, offset); + will_return(fmap_region_by_name, size); +} + +ssize_t libpayload_boot_device_read(void *buf, size_t offset, size_t size) +{ + /* Offset should be based on an address from lib_sysinfo.cbfs_offset */ + memcpy(buf, (void *)offset, size); + + return size; +} + +extern cbfs_err_t __real_cbfs_lookup(cbfs_dev_t dev, const char *name, + union cbfs_mdata *mdata_out, size_t *data_offset_out, + void *metadata_hash_unused); + + +cbfs_err_t cbfs_lookup(cbfs_dev_t dev, const char *name, union cbfs_mdata *mdata_out, + size_t *data_offset_out, void *metadata_hash_unused) +{ + const cbfs_err_t err = + __real_cbfs_lookup(dev, name, mdata_out, data_offset_out, metadata_hash_unused); + function_called(); + assert_int_equal(err, mock_type(cbfs_err_t)); + return err; +} + +extern cbfs_err_t __real_cbfs_mcache_lookup(const void *mcache, size_t mcache_size, + const char *name, union cbfs_mdata *mdata_out, + size_t *data_offset_out); + + +cbfs_err_t cbfs_mcache_lookup(const void *mcache, size_t mcache_size, const char *name, + union cbfs_mdata *mdata_out, size_t *data_offset_out) +{ + const cbfs_err_t err = __real_cbfs_mcache_lookup(mcache, mcache_size, name, mdata_out, + data_offset_out); + function_called(); + assert_int_equal(err, mock_type(cbfs_err_t)); + return err; +} + +static void expect_lookup_result(cbfs_err_t res) +{ + if (CONFIG(LP_NO_CBFS_MCACHE)) { + will_return(cbfs_lookup, res); + expect_function_call(cbfs_lookup); + } else { + will_return(cbfs_mcache_lookup, res); + expect_function_call(cbfs_mcache_lookup); + } +} + +const struct cbfs_boot_device *cbfs_get_boot_device(bool force_ro) +{ + return &cbd; +} + +/* Utils */ + +static int create_cbfs(const struct cbfs_test_file *files[], const size_t nfiles, u8 *buffer, + const size_t buffer_size) +{ + u8 *data_ptr = buffer; + + size_t file_size = 0; + memset(buffer, 0, buffer_size); + + for (size_t i = 0; i < nfiles; ++i) { + if (files[i] == NULL) { + file_size = CBFS_ALIGNMENT; + assert_true(&data_ptr[file_size] < &buffer[buffer_size]); + } else { + file_size = be32toh(files[i]->header.len) + + be32toh(files[i]->header.offset); + assert_true(&data_ptr[file_size] < &buffer[buffer_size]); + memcpy(data_ptr, files[i], file_size); + } + + data_ptr = &data_ptr[file_size]; + const uintptr_t offset = (uintptr_t)data_ptr - (uintptr_t)buffer; + data_ptr = &buffer[ALIGN_UP(offset, CBFS_ALIGNMENT)]; + } + + return 0; +} + +size_t calc_cbfs_file_size(const void *file_ptr) +{ + const struct cbfs_file *f = file_ptr; + return be32toh(f->offset) + be32toh(f->len); +} + +/* Setup */ + +static uint8_t aligned_cbfs_buffer[(sizeof(struct cbfs_test_file) + CBFS_ALIGNMENT * 50)] + __aligned(CBFS_ALIGNMENT); +static const size_t aligned_cbfs_buffer_size = sizeof(aligned_cbfs_buffer); + +static uint8_t *unaligned_cbfs_buffer = &aligned_cbfs_buffer[5]; +static const size_t unaligned_cbfs_buffer_size = aligned_cbfs_buffer_size - 5; + +static uint8_t cbfs_mcache[TEST_MCACHE_SIZE] __aligned(CBFS_MCACHE_ALIGNMENT); + +struct cbfs_test_state_ex { + uint32_t file_length; + uint32_t lookup_result; +}; + +struct cbfs_test_state { + uint8_t *cbfs_buf; + size_t cbfs_size; + uint8_t *mcache_ro_buf; + size_t mcache_ro_size; + uint8_t *mcache_rw_buf; + size_t mcache_rw_size; + + /* Optional */ + struct cbfs_test_state_ex ex; +}; + +static void _setup_cbd(struct cbfs_test_state *s) +{ + cbd.dev.offset = (size_t)s->cbfs_buf; + cbd.dev.size = s->cbfs_size; + + if (!CONFIG(LP_NO_CBFS_MCACHE)) { + cbd.mcache = cbfs_mcache; + cbd.mcache_size = sizeof(cbfs_mcache); + } +} + +static int setup_test_cbfs_aligned(void **state) +{ + struct cbfs_test_state *s = calloc(1, sizeof(*s)); + + if (!s) + return 1; + s->cbfs_buf = aligned_cbfs_buffer; + s->cbfs_size = aligned_cbfs_buffer_size; + memset(s->cbfs_buf, 0, s->cbfs_size); + + /* Prestate */ + if (*state != NULL) + s->ex = *((struct cbfs_test_state_ex *)*state); + + _setup_cbd(s); + + *state = s; + return 0; +} + +static int setup_test_cbfs_unaligned(void **state) +{ + struct cbfs_test_state *s = calloc(1, sizeof(*s)); + + if (!s) + return 1; + s->cbfs_buf = unaligned_cbfs_buffer; + s->cbfs_size = unaligned_cbfs_buffer_size; + memset(s->cbfs_buf, 0, s->cbfs_size); + + /* Prestate */ + if (*state != NULL) + s->ex = *((struct cbfs_test_state_ex *)*state); + + _setup_cbd(s); + + *state = s; + return 0; +} + +static int teardown_test_cbfs(void **state) +{ + free(*state); + return 0; +} + +/* Tests */ + +void test_cbfs_map(void **state) +{ + struct cbfs_test_state *s = *state; + void *mapping; + size_t size_out; + + const struct cbfs_test_file *cbfs_files[] = { + &test_file_int_1, &test_file_2, NULL, &test_file_int_2, + &test_file_1, NULL, NULL, &test_file_int_3, + }; + assert_int_equal( + 0, create_cbfs(cbfs_files, ARRAY_SIZE(cbfs_files), s->cbfs_buf, s->cbfs_size)); + + size_out = 0; + expect_lookup_result(CBFS_SUCCESS); + mapping = cbfs_map(TEST_DATA_1_FILENAME, &size_out); + assert_non_null(mapping); + assert_int_equal(TEST_DATA_1_SIZE, size_out); + assert_memory_equal(test_data_1, mapping, TEST_DATA_1_SIZE); + cbfs_unmap(mapping); + + size_out = 0; + expect_value(ulzman, srcn, TEST_DATA_2_SIZE); + expect_value(ulzman, dstn, TEST_DATA_2_SIZE); + expect_lookup_result(CBFS_SUCCESS); + mapping = cbfs_map(TEST_DATA_2_FILENAME, &size_out); + assert_memory_equal(test_data_2, mapping, TEST_DATA_2_SIZE); + cbfs_unmap(mapping); + + size_out = 0; + expect_lookup_result(CBFS_SUCCESS); + mapping = cbfs_map(TEST_DATA_INT_1_FILENAME, &size_out); + assert_non_null(mapping); + assert_int_equal(TEST_DATA_INT_1_SIZE, size_out); + assert_memory_equal(test_data_int_1, mapping, TEST_DATA_INT_1_SIZE); + cbfs_unmap(mapping); + + /* Should work correctly without size pointer. */ + expect_lookup_result(CBFS_SUCCESS); + mapping = cbfs_map(TEST_DATA_INT_2_FILENAME, NULL); + assert_non_null(mapping); + assert_memory_equal(test_data_int_2, mapping, TEST_DATA_INT_2_SIZE); + cbfs_unmap(mapping); + + size_out = 0; + expect_value(ulz4fn, srcn, TEST_DATA_INT_3_SIZE); + expect_value(ulz4fn, dstn, TEST_DATA_INT_3_SIZE); + expect_lookup_result(CBFS_SUCCESS); + mapping = cbfs_map(TEST_DATA_INT_3_FILENAME, &size_out); + assert_non_null(mapping); + assert_int_equal(TEST_DATA_INT_3_SIZE, size_out); + assert_memory_equal(test_data_int_3, mapping, TEST_DATA_INT_3_SIZE); + cbfs_unmap(mapping); + + /* Nonexistent files */ + size_out = 0; + expect_lookup_result(CBFS_NOT_FOUND); + mapping = cbfs_map("unknown file", &size_out); + assert_null(mapping); + + size_out = 0; + expect_lookup_result(CBFS_NOT_FOUND); + mapping = cbfs_map("", &size_out); + assert_null(mapping); +} + +static void test_cbfs_image_not_aligned(void **state) +{ + struct cbfs_test_state *s = *state; + void *mapping; + size_t size_out; + const struct cbfs_test_file *cbfs_files[] = { + &test_file_int_1, &test_file_2, + }; + assert_int_equal(0, create_cbfs(cbfs_files, ARRAY_SIZE(cbfs_files), &s->cbfs_buf[5], + s->cbfs_size - 5)); + + size_out = 0; + expect_lookup_result(CBFS_NOT_FOUND); + mapping = cbfs_map(TEST_DATA_INT_1_FILENAME, &size_out); + assert_null(mapping); + + size_out = 0; + expect_lookup_result(CBFS_NOT_FOUND); + mapping = cbfs_map(TEST_DATA_2_FILENAME, &size_out); + assert_null(mapping); +} + +static void test_cbfs_file_not_aligned(void **state) +{ + struct cbfs_test_state *s = *state; + void *mapping; + size_t size_out; + + memcpy(s->cbfs_buf, &test_file_int_2, calc_cbfs_file_size(&test_file_int_2)); + memcpy(&s->cbfs_buf[ALIGN_UP(calc_cbfs_file_size(&test_file_int_2), CBFS_ALIGNMENT) + + 3], + &test_file_1, calc_cbfs_file_size(&test_file_1)); + + size_out = 0; + expect_lookup_result(CBFS_SUCCESS); + mapping = cbfs_map(TEST_DATA_INT_2_FILENAME, &size_out); + assert_int_equal(TEST_DATA_INT_2_SIZE, size_out); + assert_memory_equal(&s->cbfs_buf[be32toh(test_file_int_2.header.offset)], mapping, + TEST_DATA_INT_2_SIZE); + cbfs_unmap(mapping); + + size_out = 0; + expect_lookup_result(CBFS_NOT_FOUND); + mapping = cbfs_map(TEST_DATA_1_FILENAME, &size_out); + assert_null(mapping); +} + +static void test_cbfs_garbage_data_before_aligned_file(void **state) +{ + struct cbfs_test_state *s = *state; + void *mapping; + size_t size_out; + const char garbage[] = + "NOT so USEFUL DaTa BYTES that should have at least CBFS_ALIGNMENT bytes"; + const size_t garbage_sz = CBFS_ALIGNMENT; + + /* Garbage data size has to be aligned to CBFS_ALIGNMENT */ + memcpy(s->cbfs_buf, garbage, garbage_sz); + memcpy(&s->cbfs_buf[garbage_sz], &test_file_int_2, sizeof(test_file_int_2)); + + size_out = 0; + expect_lookup_result(CBFS_SUCCESS); + mapping = cbfs_map(TEST_DATA_INT_2_FILENAME, &size_out); + assert_int_equal(size_out, be32toh(test_file_int_2.header.len)); + assert_memory_equal(mapping, + &s->cbfs_buf[garbage_sz + be32toh(test_file_int_2.header.offset)], + size_out); + cbfs_unmap(mapping); +} + +static void test_cbfs_garbage_data_before_unaligned_file(void **state) +{ + struct cbfs_test_state *s = *state; + void *mapping; + size_t size_out; + const char garbage[] = + "NOT so USEFUL DaTa BYTES that should have at least CBFS_ALIGNMENT + 3 bytes"; + const size_t garbage_sz = CBFS_ALIGNMENT + 3; + + assert_true(garbage_sz == (CBFS_ALIGNMENT + 3)); + memcpy(s->cbfs_buf, garbage, garbage_sz); + memcpy(&s->cbfs_buf[garbage_sz], &test_file_int_2, sizeof(test_file_int_2)); + + size_out = 0; + expect_lookup_result(CBFS_NOT_FOUND); + mapping = cbfs_map(TEST_DATA_INT_2_FILENAME, &size_out); + assert_null(mapping); +} + +static void test_cbfs_file_bigger_than_cbfs_region(void **state) +{ + struct cbfs_test_state *s = *state; + void *mapping; + size_t size_out; + struct cbfs_test_file *f; + memcpy(s->cbfs_buf, &test_file_1, sizeof(test_file_1)); + f = (struct cbfs_test_file *)s->cbfs_buf; + /* File with length equal to region_device size will go beyond it */ + f->header.len = htobe32(s->cbfs_size); + + size_out = 0; + /* Lookup should not succeed, because data is too long, so reading it later would cause + memory access issues */ + expect_lookup_result(CBFS_NOT_FOUND); + mapping = cbfs_map(TEST_DATA_1_FILENAME, &size_out); + assert_null(mapping); +} + +static void test_cbfs_fail_beyond_device(void **state) +{ + struct cbfs_test_state *s = *state; + void *mapping; + size_t size_out; + size_t second_file_start = ALIGN_UP(sizeof(test_file_1), CBFS_ALIGNMENT); + + memcpy(s->cbfs_buf, &test_file_1, sizeof(test_file_1)); + memcpy(&s->cbfs_buf[second_file_start], &test_file_2, s->ex.file_length); + assert_true((second_file_start + s->ex.file_length) <= cbfs_dev_size(&cbd.dev)); + /* Adjust size of region device to cut everything after selected offset */ + cbd.dev.size = second_file_start + s->ex.file_length; + + size_out = 0; + expect_lookup_result(CBFS_SUCCESS); + mapping = cbfs_map(TEST_DATA_1_FILENAME, &size_out); + assert_int_equal(size_out, TEST_DATA_1_SIZE); + assert_memory_equal(mapping, &s->cbfs_buf[be32toh(test_file_1.header.offset)], + size_out); + cbfs_unmap(mapping); + + size_out = 0; + mapping = NULL; + if (s->ex.lookup_result == CBFS_SUCCESS) { + expect_value(ulzman, srcn, TEST_DATA_2_SIZE); + expect_value(ulzman, dstn, TEST_DATA_2_SIZE); + } + expect_lookup_result(s->ex.lookup_result); + mapping = cbfs_map(TEST_DATA_2_FILENAME, &size_out); + if (s->ex.lookup_result == CBFS_SUCCESS) + cbfs_unmap(mapping); + else + assert_null(mapping); +} + +static void test_cbfs_unaligned_file_in_the_middle(void **state) +{ + struct cbfs_test_state *s = *state; + void *mapping; + size_t size_out; + size_t second_file_start = ALIGN_UP(sizeof(test_file_1), CBFS_ALIGNMENT) + 5; + size_t third_file_start = + ALIGN_UP(sizeof(test_file_int_1) + second_file_start, CBFS_ALIGNMENT); + + memcpy(s->cbfs_buf, &test_file_1, sizeof(test_file_1)); + memcpy(&s->cbfs_buf[second_file_start], &test_file_int_1, sizeof(test_file_int_1)); + memcpy(&s->cbfs_buf[third_file_start], &test_file_int_2, sizeof(test_file_int_2)); + + size_out = 0; + expect_lookup_result(CBFS_SUCCESS); + mapping = cbfs_map(TEST_DATA_1_FILENAME, &size_out); + assert_int_equal(size_out, be32toh(test_file_1.header.len)); + assert_memory_equal(mapping, &s->cbfs_buf[be32toh(test_file_1.header.offset)], + size_out); + cbfs_unmap(mapping); + + size_out = 0; + expect_lookup_result(CBFS_NOT_FOUND); + mapping = cbfs_map(TEST_DATA_INT_1_FILENAME, &size_out); + assert_null(mapping); + + size_out = 0; + expect_lookup_result(CBFS_SUCCESS); + mapping = cbfs_map(TEST_DATA_INT_2_FILENAME, &size_out); + assert_int_equal(size_out, be32toh(test_file_int_2.header.len)); + assert_memory_equal( + mapping, + &s->cbfs_buf[third_file_start + be32toh(test_file_int_2.header.offset)], + size_out); + cbfs_unmap(mapping); +} + +static void test_cbfs_overlapping_files(void **state) +{ + struct cbfs_test_state *s = *state; + void *mapping; + size_t size_out; + size_t second_file_start = ALIGN_UP(sizeof(test_file_1), CBFS_ALIGNMENT); + size_t third_file_start = + ALIGN_UP(sizeof(test_file_int_1) + second_file_start, CBFS_ALIGNMENT); + size_t second_file_size = + third_file_start + sizeof(test_file_int_2) - second_file_start; + struct cbfs_test_file *f; + + /* Third file is inside second file, thus it should not be found */ + memcpy(s->cbfs_buf, &test_file_1, sizeof(test_file_1)); + memcpy(&s->cbfs_buf[second_file_start], &test_file_int_1, sizeof(test_file_int_1)); + memcpy(&s->cbfs_buf[third_file_start], &test_file_int_2, sizeof(test_file_int_2)); + f = (struct cbfs_test_file *)&s->cbfs_buf[second_file_start]; + f->header.len = htobe32(second_file_size); + + size_out = 0; + expect_lookup_result(CBFS_SUCCESS); + mapping = cbfs_map(TEST_DATA_1_FILENAME, &size_out); + assert_int_equal(size_out, be32toh(test_file_1.header.len)); + assert_memory_equal(mapping, &s->cbfs_buf[be32toh(test_file_1.header.offset)], + size_out); + cbfs_unmap(mapping); + + size_out = 0; + expect_lookup_result(CBFS_SUCCESS); + mapping = cbfs_map(TEST_DATA_INT_1_FILENAME, &size_out); + assert_int_equal(size_out, second_file_size); + assert_memory_equal( + mapping, + &s->cbfs_buf[second_file_start + be32toh(test_file_int_1.header.offset)], + size_out); + cbfs_unmap(mapping); + + size_out = 0; + expect_lookup_result(CBFS_NOT_FOUND); + mapping = cbfs_map(TEST_DATA_INT_2_FILENAME, &size_out); + assert_null(mapping); +} + +static void test_cbfs_incorrect_file_in_the_middle(void **state) +{ + struct cbfs_test_state *s = *state; + void *mapping; + size_t size_out; + size_t second_file_start = ALIGN_UP(sizeof(test_file_1), CBFS_ALIGNMENT); + size_t third_file_start = + ALIGN_UP(sizeof(test_file_int_1) + second_file_start, CBFS_ALIGNMENT); + struct cbfs_test_file *f; + + /* Zero offset is illegal. File is not correct */ + memcpy(s->cbfs_buf, &test_file_1, sizeof(test_file_1)); + memcpy(&s->cbfs_buf[second_file_start], &test_file_int_1, sizeof(test_file_int_1)); + memcpy(&s->cbfs_buf[third_file_start], &test_file_int_2, sizeof(test_file_int_2)); + f = (struct cbfs_test_file *)&s->cbfs_buf[second_file_start]; + f->header.offset = htobe32(0); + + size_out = 0; + expect_lookup_result(CBFS_SUCCESS); + mapping = cbfs_map(TEST_DATA_1_FILENAME, &size_out); + assert_int_equal(size_out, be32toh(test_file_1.header.len)); + assert_memory_equal(mapping, &s->cbfs_buf[be32toh(test_file_1.header.offset)], + size_out); + cbfs_unmap(mapping); + + size_out = 0; + expect_lookup_result(CBFS_NOT_FOUND); + mapping = cbfs_map(TEST_DATA_INT_1_FILENAME, &size_out); + assert_null(mapping); + + size_out = 0; + expect_lookup_result(CBFS_SUCCESS); + mapping = cbfs_map(TEST_DATA_INT_2_FILENAME, &size_out); + assert_int_equal(size_out, be32toh(test_file_int_2.header.len)); + assert_memory_equal( + mapping, + &s->cbfs_buf[third_file_start + be32toh(test_file_int_2.header.offset)], + size_out); + cbfs_unmap(mapping); +} + +static void test_cbfs_two_files_with_same_name(void **state) +{ + void *mapping; + size_t size_out; + struct cbfs_test_state *s = *state; + size_t second_file_start = ALIGN_UP(sizeof(test_file_1), CBFS_ALIGNMENT); + size_t third_file_start = + ALIGN_UP(sizeof(test_file_1) + second_file_start, CBFS_ALIGNMENT); + + /* Only first occurrence of file will be found */ + memcpy(s->cbfs_buf, &test_file_1, sizeof(test_file_1)); + memcpy(&s->cbfs_buf[second_file_start], &test_file_1, sizeof(test_file_1)); + memcpy(&s->cbfs_buf[third_file_start], &test_file_int_1, sizeof(test_file_int_1)); + + size_out = 0; + expect_lookup_result(CBFS_SUCCESS); + mapping = cbfs_map(TEST_DATA_1_FILENAME, &size_out); + assert_int_equal(size_out, be32toh(test_file_1.header.len)); + assert_memory_equal(mapping, &s->cbfs_buf[be32toh(test_file_1.header.offset)], + size_out); + cbfs_unmap(mapping); + + size_out = 0; + expect_lookup_result(CBFS_SUCCESS); + mapping = cbfs_map(TEST_DATA_INT_1_FILENAME, &size_out); + assert_int_equal(size_out, be32toh(test_file_int_1.header.len)); + assert_memory_equal( + mapping, + &s->cbfs_buf[third_file_start + be32toh(test_file_int_1.header.offset)], + size_out); + cbfs_unmap(mapping); +} + +static void test_cbfs_filename_not_terminated(void **state) +{ + struct cbfs_test_state *s = *state; + void *mapping; + size_t size_out; + struct cbfs_test_file *f; + const char fname[] = "abcdefghijklmnop"; + + assert_true(sizeof(test_file_1.filename) == strlen(fname)); + memcpy(s->cbfs_buf, &test_file_1, sizeof(test_file_1)); + f = (struct cbfs_test_file *)s->cbfs_buf; + memcpy(f->filename, fname, strlen(fname)); + + size_out = 0; + /* Filename is too long and does not include NULL-terminator. */ + expect_lookup_result(CBFS_NOT_FOUND); + mapping = cbfs_map(fname, &size_out); + assert_null(mapping); +} + +static void test_cbfs_filename_terminated_but_too_long(void **state) +{ + void *mapping; + size_t size_out; + struct cbfs_test_state *s = *state; + struct cbfs_test_file *f; + + /* Filename length in header offset field is too short by one to include + NULL-terminator of filename */ + memcpy(s->cbfs_buf, &test_file_1, sizeof(test_file_1)); + f = (struct cbfs_test_file *)s->cbfs_buf; + f->header.offset = htobe32(offsetof(struct cbfs_test_file, filename) + + strlen(TEST_DATA_1_FILENAME)); + + size_out = 0; + expect_lookup_result(CBFS_NOT_FOUND); + mapping = cbfs_map(TEST_DATA_1_FILENAME, &size_out); + assert_null(mapping); +} + +static void test_cbfs_attributes_offset_larger_than_offset(void **state) +{ + void *mapping; + size_t size_out; + struct cbfs_test_state *s = *state; + struct cbfs_test_file *f; + + /* Require attributes for this test */ + assert_true(be32toh(test_file_2.header.attributes_offset) != 0); + memcpy(s->cbfs_buf, &test_file_2, sizeof(test_file_2)); + f = (struct cbfs_test_file *)s->cbfs_buf; + f->header.attributes_offset = htobe32( + sizeof(struct cbfs_file) + FILENAME_SIZE + + sizeof(struct cbfs_file_attr_compression)); + f->header.offset = htobe32(sizeof(struct cbfs_file) + FILENAME_SIZE); + + size_out = 0; + expect_lookup_result(CBFS_NOT_FOUND); + mapping = cbfs_map(TEST_DATA_2_FILENAME, &size_out); + assert_null(mapping); +} + +static void test_cbfs_attributes_offset_cut_off_at_len(void **state) +{ + void *mapping; + size_t size_out; + struct cbfs_test_state *s = *state; + struct cbfs_test_file *f; + + /* Require attributes for this test */ + assert_true(be32toh(test_file_2.header.attributes_offset) != 0); + memcpy(s->cbfs_buf, &test_file_2, sizeof(test_file_2)); + f = (struct cbfs_test_file *)s->cbfs_buf; + f->header.attributes_offset = + htobe32(offsetof(struct cbfs_test_file, attrs_and_data) + + offsetof(struct cbfs_file_attribute, len)); + + /* No attributes will be found, because attributes_offset value is too big to cover + cbfs_file_attribute tag. Compression attribute of ths file will not be found, and + that is why there is no need to call expect_value(ulzma). + However, file will be found, because the offset is correct. */ + size_out = 0; + expect_lookup_result(CBFS_SUCCESS); + mapping = cbfs_map(TEST_DATA_2_FILENAME, &size_out); + assert_int_equal(size_out, TEST_DATA_2_SIZE); + assert_memory_equal(mapping, &s->cbfs_buf[be32toh(f->header.offset)], size_out); + cbfs_unmap(mapping); +} + +static void test_cbfs_attributes_offset_cut_off_at_data(void **state) +{ + struct cbfs_test_state *s = *state; + void *mapping; + size_t size_out; + struct cbfs_test_file *f; + + /* Require attributes for this test */ + assert_true(be32toh(test_file_2.header.attributes_offset) != 0); + memcpy(s->cbfs_buf, &test_file_2, sizeof(test_file_2)); + f = (struct cbfs_test_file *)s->cbfs_buf; + f->header.attributes_offset = htobe32(sizeof(struct cbfs_file) + FILENAME_SIZE + + offsetof(struct cbfs_file_attribute, data)); + + /* No attributes will be found, because attributes_offset value is too big to cover + cbfs_file_attribute tag and length. Compression attribute of ths file will not be + found, and that is why there is no need to call expect_value(ulzma). + However, file will be found, because the offset is correct. */ + size_out = 0; + expect_lookup_result(CBFS_SUCCESS); + mapping = cbfs_map(TEST_DATA_2_FILENAME, &size_out); + assert_int_equal(size_out, TEST_DATA_2_SIZE); + assert_memory_equal(mapping, &s->cbfs_buf[be32toh(f->header.offset)], size_out); + cbfs_unmap(mapping); +} + +static void test_cbfs_attributes_offset_smaller_than_file_struct(void **state) +{ + struct cbfs_test_state *s = *state; + void *mapping; + size_t size_out; + struct cbfs_test_file *f; + + assert_true(be32toh(test_file_2.header.attributes_offset) != 0); + memcpy(s->cbfs_buf, &test_file_2, sizeof(test_file_2)); + f = (struct cbfs_test_file *)s->cbfs_buf; + f->header.attributes_offset = htobe32(sizeof(struct cbfs_file) / 2); + + size_out = 0; + expect_lookup_result(CBFS_NOT_FOUND); + mapping = cbfs_map(TEST_DATA_2_FILENAME, &size_out); + assert_null(mapping); +} + +static void test_cbfs_offset_smaller_than_header_size(void **state) +{ + struct cbfs_test_state *s = *state; + void *mapping; + size_t size_out; + struct cbfs_test_file *f; + + assert_true(be32toh(test_file_int_1.header.attributes_offset) == 0); + memcpy(s->cbfs_buf, &test_file_int_1, sizeof(test_file_int_1)); + f = (struct cbfs_test_file *)s->cbfs_buf; + f->header.offset = htobe32(sizeof(struct cbfs_file) / 2); + + size_out = 0; + expect_lookup_result(CBFS_NOT_FOUND); + mapping = cbfs_map(TEST_DATA_INT_1_FILENAME, &size_out); + assert_null(mapping); +} + +static void test_cbfs_attributes_offset_is_zero(void **state) +{ + struct cbfs_test_state *s = *state; + void *mapping; + size_t size_out; + + assert_true(be32toh(test_file_int_1.header.attributes_offset) == 0); + memcpy(s->cbfs_buf, &test_file_int_1, sizeof(test_file_int_1)); + + size_out = 0; + expect_lookup_result(CBFS_SUCCESS); + mapping = cbfs_map(TEST_DATA_INT_1_FILENAME, &size_out); + assert_int_equal(TEST_DATA_INT_1_SIZE, size_out); + assert_memory_equal(mapping, &s->cbfs_buf[be32toh(test_file_int_1.header.offset)], + size_out); + cbfs_unmap(mapping); +} + +static void test_cbfs_offset_is_zero(void **state) +{ + struct cbfs_test_state *s = *state; + void *mapping; + size_t size_out; + struct cbfs_test_file *f; + + assert_true(be32toh(test_file_int_1.header.attributes_offset) == 0); + memcpy(s->cbfs_buf, &test_file_int_1, sizeof(test_file_int_1)); + f = (struct cbfs_test_file *)s->cbfs_buf; + f->header.offset = htobe32(0); + + size_out = 0; + expect_lookup_result(CBFS_NOT_FOUND); + mapping = cbfs_map(TEST_DATA_INT_1_FILENAME, &size_out); + assert_null(mapping); +} + +static void test_cbfs_attributes_too_large(void **state) +{ + struct cbfs_test_state *s = *state; + void *mapping; + size_t size_out; + struct cbfs_test_file *f; + + assert_true(be32toh(test_file_2.header.attributes_offset) != 0); + memcpy(s->cbfs_buf, &test_file_2, sizeof(test_file_2)); + f = (struct cbfs_test_file *)s->cbfs_buf; + /* Offset determines size of header and attributes. CBFS module uses cbfs_mdata union to + store it, so offset (thus attributes) bigger than it should cause an error in the + lookup code. */ + f->header.offset = + htobe32(be32toh(f->header.offset) + sizeof(union cbfs_mdata)); + + size_out = 0; + expect_lookup_result(CBFS_NOT_FOUND); + mapping = cbfs_map(TEST_DATA_2_FILENAME, &size_out); + assert_null(mapping); +} + +/* Requires cbfs_test_state.ex.file_length to be set */ +static void test_cbfs_file_length(void **state) +{ + struct cbfs_test_state *s = *state; + void *mapping; + size_t size_out; + struct cbfs_test_file *f; + + assert_true(be32toh(test_file_1.header.attributes_offset) == 0); + memcpy(s->cbfs_buf, &test_file_1, sizeof(test_file_1)); + f = (struct cbfs_test_file *)s->cbfs_buf; + f->header.len = htobe32(s->ex.file_length); + + size_out = 0; + expect_lookup_result(CBFS_NOT_FOUND); + mapping = cbfs_map(TEST_DATA_1_FILENAME, &size_out); + assert_null(mapping); +} + +static void test_cbfs_attributes_offset_uint32_max(void **state) +{ + struct cbfs_test_state *s = *state; + void *mapping; + size_t size_out; + struct cbfs_test_file *f; + + assert_true(be32toh(test_file_1.header.attributes_offset) == 0); + memcpy(s->cbfs_buf, &test_file_1, sizeof(test_file_1)); + f = (struct cbfs_test_file *)s->cbfs_buf; + f->header.attributes_offset = htobe32(UINT32_MAX); + + size_out = 0; + expect_lookup_result(CBFS_NOT_FOUND); + mapping = cbfs_map(TEST_DATA_1_FILENAME, &size_out); + assert_null(mapping); +} + +/* Tests setup macros */ +#define CBFS_LOOKUP_NAME_SETUP_PRESTATE_COMMON_TEST(name, test_fn, setup_fn, prestate) \ + EMPTY_WRAP((struct CMUnitTest){ \ + (name), \ + (test_fn), \ + (setup_fn), \ + teardown_test_cbfs, \ + (prestate), \ + }) + +#define CBFS_LOOKUP_NAME_PRESTATE_TEST(name, test_fn, prestate) \ + EMPTY_WRAP( \ + CBFS_LOOKUP_NAME_SETUP_PRESTATE_COMMON_TEST( \ + (name ", aligned"), (test_fn), setup_test_cbfs_aligned, (prestate)), \ + CBFS_LOOKUP_NAME_SETUP_PRESTATE_COMMON_TEST( \ + (name ", unaligned"), (test_fn), setup_test_cbfs_unaligned, (prestate))) + +#define CBFS_LOOKUP_TEST(test_fn) CBFS_LOOKUP_NAME_PRESTATE_TEST(#test_fn, test_fn, NULL) + +#define CBFS_LOOKUP_TEST_FAIL_BEYOND_DEVICE(name, file_len, lookup_res) \ + CBFS_LOOKUP_NAME_PRESTATE_TEST(name, test_cbfs_fail_beyond_device, \ + (&(struct cbfs_test_state_ex){ \ + .file_length = (file_len), \ + .lookup_result = (lookup_res), \ + })) + +#define CBFS_LOOKUP_TEST_FILE_LENGTH(file_len) \ + CBFS_LOOKUP_NAME_PRESTATE_TEST("test_cbfs_file_length, " #file_len, \ + test_cbfs_file_length, \ + (&(struct cbfs_test_state_ex){ \ + .file_length = (file_len), \ + })) + +int main(void) +{ + const struct CMUnitTest tests[] = { + CBFS_LOOKUP_TEST(test_cbfs_map), + + CBFS_LOOKUP_TEST(test_cbfs_image_not_aligned), + CBFS_LOOKUP_TEST(test_cbfs_file_not_aligned), + + CBFS_LOOKUP_TEST(test_cbfs_garbage_data_before_aligned_file), + CBFS_LOOKUP_TEST(test_cbfs_garbage_data_before_unaligned_file), + + CBFS_LOOKUP_TEST(test_cbfs_file_bigger_than_cbfs_region), + + /* Correct file */ + CBFS_LOOKUP_TEST_FAIL_BEYOND_DEVICE( + "File fitting in device", sizeof(struct cbfs_test_file), CBFS_SUCCESS), + + /* Attributes beyond device */ + CBFS_LOOKUP_TEST_FAIL_BEYOND_DEVICE( + "Attributes and data beyond device", + offsetof(struct cbfs_test_file, attrs_and_data), CBFS_NOT_FOUND), + + /* Attributes except tag beyond rdev */ + CBFS_LOOKUP_TEST_FAIL_BEYOND_DEVICE( + "Attributes except tag beyond rdev", + offsetof(struct cbfs_test_file, attrs_and_data) + - offsetof(struct cbfs_file_attribute, len), + CBFS_NOT_FOUND), + + /* Attributes except tag and len beyond rdev */ + CBFS_LOOKUP_TEST_FAIL_BEYOND_DEVICE( + "Attributes except tag and len beyond rdev", + offsetof(struct cbfs_test_file, attrs_and_data) + - offsetof(struct cbfs_file_attribute, data), + CBFS_NOT_FOUND), + + /* Filename beyond rdev */ + CBFS_LOOKUP_TEST_FAIL_BEYOND_DEVICE("Filename beyond rdev", + offsetof(struct cbfs_test_file, filename), + CBFS_NOT_FOUND), + + /* Part of filename beyond rdev */ + CBFS_LOOKUP_TEST_FAIL_BEYOND_DEVICE("Part of filename beyond rdev", + offsetof(struct cbfs_test_file, filename) + + FILENAME_SIZE / 2, + CBFS_NOT_FOUND), + + /* Part of cbfs_file struct beyond rdev */ + CBFS_LOOKUP_TEST_FAIL_BEYOND_DEVICE( + "Part of cbfs_file struct beyond rdev", + offsetof(struct cbfs_test_file, filename) / 2, CBFS_NOT_FOUND), + + CBFS_LOOKUP_TEST(test_cbfs_unaligned_file_in_the_middle), + CBFS_LOOKUP_TEST(test_cbfs_overlapping_files), + CBFS_LOOKUP_TEST(test_cbfs_incorrect_file_in_the_middle), + + CBFS_LOOKUP_TEST(test_cbfs_two_files_with_same_name), + + CBFS_LOOKUP_TEST(test_cbfs_filename_not_terminated), + CBFS_LOOKUP_TEST(test_cbfs_filename_terminated_but_too_long), + + CBFS_LOOKUP_TEST(test_cbfs_attributes_offset_larger_than_offset), + CBFS_LOOKUP_TEST(test_cbfs_attributes_offset_cut_off_at_len), + CBFS_LOOKUP_TEST(test_cbfs_attributes_offset_cut_off_at_data), + + CBFS_LOOKUP_TEST(test_cbfs_attributes_offset_smaller_than_file_struct), + + CBFS_LOOKUP_TEST(test_cbfs_offset_smaller_than_header_size), + CBFS_LOOKUP_TEST(test_cbfs_attributes_offset_is_zero), + CBFS_LOOKUP_TEST(test_cbfs_offset_is_zero), + CBFS_LOOKUP_TEST(test_cbfs_attributes_too_large), + + CBFS_LOOKUP_TEST_FILE_LENGTH(UINT32_MAX), + CBFS_LOOKUP_TEST_FILE_LENGTH(UINT32_MAX + - offsetof(struct cbfs_test_file, attrs_and_data)), + CBFS_LOOKUP_TEST_FILE_LENGTH( + UINT32_MAX - offsetof(struct cbfs_test_file, attrs_and_data) / 2), + CBFS_LOOKUP_TEST_FILE_LENGTH( + UINT32_MAX - offsetof(struct cbfs_test_file, attrs_and_data) * 2), + CBFS_LOOKUP_TEST_FILE_LENGTH( + UINT32_MAX - offsetof(struct cbfs_test_file, attrs_and_data) - 1), + CBFS_LOOKUP_TEST_FILE_LENGTH( + UINT32_MAX - offsetof(struct cbfs_test_file, attrs_and_data) + 1), + + CBFS_LOOKUP_TEST(test_cbfs_attributes_offset_uint32_max), + }; + + return lp_run_group_tests(tests, NULL, NULL); +} diff --git a/payloads/libpayload/tests/mocks/cbfs_file_mock.c b/payloads/libpayload/tests/mocks/cbfs_file_mock.c new file mode 100644 index 0000000..a24f217 --- /dev/null +++ b/payloads/libpayload/tests/mocks/cbfs_file_mock.c @@ -0,0 +1,60 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ + +#include <mocks/cbfs_util.h> + + +const u8 test_data_1[TEST_DATA_1_SIZE] = { TEST_DATA_1 }; +const u8 test_data_2[TEST_DATA_2_SIZE] = { TEST_DATA_2 }; +const u8 test_data_int_1[TEST_DATA_INT_1_SIZE] = { LE64(TEST_DATA_INT_1) }; +const u8 test_data_int_2[TEST_DATA_INT_2_SIZE] = { LE64(TEST_DATA_INT_2) }; +const u8 test_data_int_3[TEST_DATA_INT_3_SIZE] = { LE64(TEST_DATA_INT_3) }; + +const struct cbfs_test_file test_file_1 = { + .header = HEADER_INITIALIZER(CBFS_TYPE_RAW, 0, TEST_DATA_1_SIZE), + .filename = TEST_DATA_1_FILENAME, + .attrs_and_data = { + TEST_DATA_1, + }, +}; + +const struct cbfs_test_file test_file_2 = { + .header = HEADER_INITIALIZER(CBFS_TYPE_RAW, sizeof(struct cbfs_file_attr_compression), + TEST_DATA_2_SIZE), + .filename = TEST_DATA_2_FILENAME, + .attrs_and_data = { + BE32(CBFS_FILE_ATTR_TAG_COMPRESSION), + BE32(sizeof(struct cbfs_file_attr_compression)), + BE32(CBFS_COMPRESS_LZMA), + BE32(TEST_DATA_2_SIZE), + TEST_DATA_2, + }, +}; + +const struct cbfs_test_file test_file_int_1 = { + .header = HEADER_INITIALIZER(CBFS_TYPE_RAW, 0, TEST_DATA_INT_1_SIZE), + .filename = TEST_DATA_INT_1_FILENAME, + .attrs_and_data = { + LE64(TEST_DATA_INT_1), + }, +}; + +const struct cbfs_test_file test_file_int_2 = { + .header = HEADER_INITIALIZER(CBFS_TYPE_RAW, 0, TEST_DATA_INT_2_SIZE), + .filename = TEST_DATA_INT_2_FILENAME, + .attrs_and_data = { + LE64(TEST_DATA_INT_2), + }, +}; + +const struct cbfs_test_file test_file_int_3 = { + .header = HEADER_INITIALIZER(CBFS_TYPE_RAW, sizeof(struct cbfs_file_attr_compression), + TEST_DATA_INT_3_SIZE), + .filename = TEST_DATA_INT_3_FILENAME, + .attrs_and_data = { + BE32(CBFS_FILE_ATTR_TAG_COMPRESSION), + BE32(sizeof(struct cbfs_file_attr_compression)), + BE32(CBFS_COMPRESS_LZ4), + BE32(TEST_DATA_INT_3_SIZE), + LE64(TEST_DATA_INT_3), + }, +}; diff --git a/payloads/libpayload/tests/mocks/die.c b/payloads/libpayload/tests/mocks/die.c new file mode 100644 index 0000000..a67105a --- /dev/null +++ b/payloads/libpayload/tests/mocks/die.c @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ + +#include <tests/test.h> +#include <stdbool.h> + +void die_work(const char *file, const char *func, int line, const char *fmt, ...) +{ + /* Failing asserts are jumping to the user code (test) if expect_assert_failed() was + previously called. Otherwise it jumps to the cmocka code and fails the test. */ + mock_assert(false, "Mock assetion called", file, line); + + /* Should never be reached */ + print_error("%s() called...\n", __func__); + while (1) + ; +}