From d3eee4d3b120f2fe30932b2f29d935cc9ac9ddb3 Mon Sep 17 00:00:00 2001 From: Daniel Golle Date: Sat, 16 May 2026 00:37:43 +0100 Subject: image: fit: add dm-verity property name constants Add FIT_VERITY_NODENAME and the complete set of FIT_VERITY_*_PROP constants for the dm-verity child node of filesystem-type images, plus the five optional boolean error-handling property names aligned with the flat-image-tree specification. Signed-off-by: Daniel Golle Reviewed-by: Simon Glass Reviewed-by: Tom Rini --- include/image.h | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'include/image.h') diff --git a/include/image.h b/include/image.h index 34efac6056d..482446a8115 100644 --- a/include/image.h +++ b/include/image.h @@ -1079,6 +1079,23 @@ int booti_setup(ulong image, ulong *relocated_addr, ulong *size, #define FIT_CIPHER_NODENAME "cipher" #define FIT_ALGO_PROP "algo" +/* dm-verity node */ +#define FIT_VERITY_NODENAME "dm-verity" +#define FIT_VERITY_ALGO_PROP "algo" +#define FIT_VERITY_DBS_PROP "data-block-size" +#define FIT_VERITY_HBS_PROP "hash-block-size" +#define FIT_VERITY_NBLK_PROP "num-data-blocks" +#define FIT_VERITY_HBLK_PROP "hash-start-block" +#define FIT_VERITY_DIGEST_PROP "digest" +#define FIT_VERITY_SALT_PROP "salt" + +/* dm-verity error-handling modes (optional boolean property names) */ +#define FIT_VERITY_OPT_RESTART "restart-on-corruption" +#define FIT_VERITY_OPT_PANIC "panic-on-corruption" +#define FIT_VERITY_OPT_RERR "restart-on-error" +#define FIT_VERITY_OPT_PERR "panic-on-error" +#define FIT_VERITY_OPT_ONCE "check-at-most-once" + /* image node */ #define FIT_DATA_PROP "data" #define FIT_DATA_POSITION_PROP "data-position" -- cgit v1.3.1 From cafe3d6e90e661bd9d42b19f1e2d891da48f3fce Mon Sep 17 00:00:00 2001 From: Daniel Golle Date: Sat, 16 May 2026 00:37:52 +0100 Subject: boot: fit: support generating DM verity cmdline parameters Add fit_verity_build_cmdline(): when a FILESYSTEM loadable carries a dm-verity subnode, construct the dm-mod.create= kernel cmdline parameter from the verity metadata (block-size, data-blocks, algo, root-hash, salt) and append it to bootargs. Also add dm-mod.waitfor=/dev/fit0[,/dev/fitN] for each dm-verity device so the kernel waits for the underlying FIT block device to appear before setting up device-mapper targets. This is needed when the block driver probes late, e.g. because it depends on NVMEM calibration data. The dm-verity target references /dev/fitN where N is the loadable's index in the configuration -- matching the order Linux's FIT block driver assigns block devices. hash-start-block is read directly from the FIT dm-verity node; mkimage ensures its value equals num-data-blocks by invoking veritysetup with --no-superblock. Signed-off-by: Daniel Golle Reviewed-by: Simon Glass --- boot/Kconfig | 20 ++++ boot/bootm.c | 13 +++ boot/image-board.c | 5 + boot/image-fit.c | 337 +++++++++++++++++++++++++++++++++++++++++++++++++++++ include/image.h | 80 ++++++++++++- 5 files changed, 454 insertions(+), 1 deletion(-) (limited to 'include/image.h') diff --git a/boot/Kconfig b/boot/Kconfig index ae6f09a6ede..e1114aea843 100644 --- a/boot/Kconfig +++ b/boot/Kconfig @@ -142,6 +142,26 @@ config FIT_CIPHER Enable the feature of data ciphering/unciphering in the tool mkimage and in the u-boot support of the FIT image. +config FIT_VERITY + bool "dm-verity boot parameter generation from FIT metadata" + depends on FIT && OF_LIBFDT + help + When a FIT configuration contains loadable sub-images of type + IH_TYPE_FILESYSTEM with a dm-verity subnode, this option enables + building the dm-mod.create= and dm-mod.waitfor= kernel + command-line parameters from the verity metadata + (data-block-size, hash-block-size, num-data-blocks, + hash-start-block, algorithm, digest, salt) stored in the FIT. + + The generated parameters reference /dev/fitN block devices that + Linux's uImage.FIT block driver assigns to loadable sub-images. + + During FIT parsing (BOOTM_STATE_FINDOTHER), verity cmdline + fragments are stored in struct bootm_headers and automatically + appended to the bootargs environment variable during + BOOTM_STATE_OS_PREP. This works from both the bootm command + and BOOTSTD bootmeths. + config FIT_VERBOSE bool "Show verbose messages when FIT images fail" help diff --git a/boot/bootm.c b/boot/bootm.c index 4836d6b2d41..ec74873b503 100644 --- a/boot/bootm.c +++ b/boot/bootm.c @@ -243,6 +243,13 @@ static int boot_get_kernel(const char *addr_fit, struct bootm_headers *images, static int bootm_start(void) { + /* + * Free dm-verity allocations from a prior boot attempt before + * zeroing the structure. The pointers are guaranteed to be valid + * or NULL: .bss is zero-initialised, and memset() below zeroes + * them again after every boot. + */ + fit_verity_free(&images); memset((void *)&images, 0, sizeof(images)); images.verify = env_get_yesno("verify"); @@ -1071,6 +1078,12 @@ int bootm_run_states(struct bootm_info *bmi, int states) /* For Linux OS do all substitutions at console processing */ if (images->os.os == IH_OS_LINUX) flags = BOOTM_CL_ALL; + ret = fit_verity_apply_bootargs(images); + if (ret) { + printf("dm-verity bootargs failed (err=%d)\n", ret); + ret = CMD_RET_FAILURE; + goto err; + } ret = bootm_process_cmdline_env(flags); if (ret) { printf("Cmdline setup failed (err=%d)\n", ret); diff --git a/boot/image-board.c b/boot/image-board.c index 005d60caf5c..265f29d44ff 100644 --- a/boot/image-board.c +++ b/boot/image-board.c @@ -810,6 +810,11 @@ int boot_get_loadable(struct bootm_headers *images) fit_loadable_process(img_type, img_data, img_len); } + + fit_img_result = fit_verity_build_cmdline(buf, conf_noffset, + images); + if (fit_img_result < 0) + return fit_img_result; break; default: printf("The given image format is not supported (corrupt?)\n"); diff --git a/boot/image-fit.c b/boot/image-fit.c index b0fcaf6e17f..d3d68579ea1 100644 --- a/boot/image-fit.c +++ b/boot/image-fit.c @@ -21,8 +21,11 @@ extern void *aligned_alloc(size_t alignment, size_t size); #else #include +#include #include +#include #include +#include #include #include #include @@ -243,6 +246,39 @@ static void fit_image_print_data(const void *fit, int noffset, const char *p, } } +static __maybe_unused void fit_image_print_dm_verity(const void *fit, + int noffset, + const char *p) +{ +#if defined(USE_HOSTCC) || CONFIG_IS_ENABLED(FIT_VERITY) + const char *algo; + const uint8_t *bin; + int len, i; + + algo = fdt_getprop(fit, noffset, FIT_VERITY_ALGO_PROP, NULL); + if (algo) + printf("%s Verity algo: %s\n", p, algo); + + bin = fdt_getprop(fit, noffset, FIT_VERITY_DIGEST_PROP, + &len); + if (bin && len > 0) { + printf("%s Verity hash: ", p); + for (i = 0; i < len; i++) + printf("%02x", bin[i]); + printf("\n"); + } + + bin = fdt_getprop(fit, noffset, FIT_VERITY_SALT_PROP, + &len); + if (bin && len > 0) { + printf("%s Verity salt: ", p); + for (i = 0; i < len; i++) + printf("%02x", bin[i]); + printf("\n"); + } +#endif +} + /** * fit_image_print_verification_data() - prints out the hash/signature details * @fit: pointer to the FIT format image header @@ -271,6 +307,11 @@ static void fit_image_print_verification_data(const void *fit, int noffset, strlen(FIT_SIG_NODENAME))) { fit_image_print_data(fit, noffset, p, "Sign"); } +#if defined(USE_HOSTCC) || CONFIG_IS_ENABLED(FIT_VERITY) + else if (!strcmp(name, FIT_VERITY_NODENAME)) { + fit_image_print_dm_verity(fit, noffset, p); + } +#endif } /** @@ -2642,3 +2683,299 @@ out: return fdt_noffset; } #endif + +#if !defined(USE_HOSTCC) && CONFIG_IS_ENABLED(FIT_VERITY) + +static const char *const verity_opt_props[] = { + FIT_VERITY_OPT_RESTART, + FIT_VERITY_OPT_PANIC, + FIT_VERITY_OPT_RERR, + FIT_VERITY_OPT_PERR, + FIT_VERITY_OPT_ONCE, +}; + +/** + * fit_verity_build_target() - build one dm-verity target specification + * @fit: pointer to the FIT blob + * @img_noffset: image node offset containing the dm-verity subnode + * @loadable_idx: index of this loadable (for /dev/fitN) + * @uname: unit name of the image + * @separator: true if a ";" prefix is needed (not the first target) + * @buf: output buffer, or NULL to measure only + * @bufsize: size of @buf (ignored when @buf is NULL) + * + * Parses all dm-verity properties from the image's ``dm-verity`` child + * node and writes (or measures) a dm target specification string of the + * form used by the ``dm-mod.create`` kernel parameter. + * + * Return: number of characters that would be written (excluding '\0'), + * or -ve errno on error (e.g. missing mandatory property) + */ +static int fit_verity_build_target(const void *fit, int img_noffset, + int loadable_idx, const char *uname, + bool separator, char *buf, int bufsize) +{ + const char *algorithm; + const u8 *digest_raw, *salt_raw; + const fdt32_t *val; + char *digest_hex = NULL, *salt_hex = NULL, *opt_buf = NULL; + int verity_node; + unsigned int data_block_size, hash_block_size; + int num_data_blocks, hash_start_block; + u64 data_sectors; + int digest_len, salt_len; + int opt_count, opt_off, opt_buf_size; + int len; + int i; + + verity_node = fdt_subnode_offset(fit, img_noffset, FIT_VERITY_NODENAME); + if (verity_node < 0) + return -ENOENT; + + /* Mandatory u32 properties */ + val = fdt_getprop(fit, verity_node, FIT_VERITY_DBS_PROP, NULL); + if (!val) + return -EINVAL; + data_block_size = fdt32_to_cpu(*val); + + val = fdt_getprop(fit, verity_node, FIT_VERITY_HBS_PROP, NULL); + if (!val) + return -EINVAL; + hash_block_size = fdt32_to_cpu(*val); + + val = fdt_getprop(fit, verity_node, FIT_VERITY_NBLK_PROP, NULL); + if (!val) + return -EINVAL; + num_data_blocks = fdt32_to_cpu(*val); + + val = fdt_getprop(fit, verity_node, FIT_VERITY_HBLK_PROP, NULL); + if (!val) + return -EINVAL; + hash_start_block = fdt32_to_cpu(*val); + + if (data_block_size < 512U || !is_power_of_2(data_block_size) || + hash_block_size < 512U || !is_power_of_2(hash_block_size) || + !num_data_blocks) + return -EINVAL; + + /* Mandatory string */ + algorithm = fdt_getprop(fit, verity_node, FIT_VERITY_ALGO_PROP, NULL); + if (!algorithm) + return -EINVAL; + + /* Mandatory byte arrays */ + digest_raw = fdt_getprop(fit, verity_node, FIT_VERITY_DIGEST_PROP, + &digest_len); + if (!digest_raw || digest_len <= 0) + return -EINVAL; + + salt_raw = fdt_getprop(fit, verity_node, FIT_VERITY_SALT_PROP, + &salt_len); + if (!salt_raw || salt_len <= 0) + return -EINVAL; + + /* Hex-encode digest and salt into dynamically sized buffers */ + digest_hex = malloc(digest_len * 2 + 1); + salt_hex = malloc(salt_len * 2 + 1); + if (!digest_hex || !salt_hex) { + len = -ENOMEM; + goto out; + } + *bin2hex(digest_hex, digest_raw, digest_len) = '\0'; + *bin2hex(salt_hex, salt_raw, salt_len) = '\0'; + + data_sectors = (u64)num_data_blocks * ((u64)data_block_size / 512); + + /* Compute space needed for optional boolean properties */ + opt_buf_size = 1; /* NUL terminator */ + for (i = 0; i < ARRAY_SIZE(verity_opt_props); i++) + opt_buf_size += strlen(verity_opt_props[i]) + 1; + opt_buf = malloc(opt_buf_size); + if (!opt_buf) { + len = -ENOMEM; + goto out; + } + + /* Collect optional boolean properties */ + opt_count = 0; + opt_off = 0; + opt_buf[0] = '\0'; + for (i = 0; i < ARRAY_SIZE(verity_opt_props); i++) { + if (fdt_getprop(fit, verity_node, + verity_opt_props[i], NULL)) { + const char *s = verity_opt_props[i]; + int slen = strlen(s); + + if (opt_off) + opt_buf[opt_off++] = ' '; + /* Copy with hyphen-to-underscore conversion */ + while (slen-- > 0) { + opt_buf[opt_off++] = + (*s == '-') ? '_' : *s; + s++; + } + opt_buf[opt_off] = '\0'; + opt_count++; + } + } + + /* Emit (or measure) the target spec */ + len = snprintf(buf, buf ? bufsize : 0, + "%s%s,,, ro,0 %llu verity 1 /dev/fit%d /dev/fit%d %u %u %d %d %s %s %s", + separator ? ";" : "", uname, + (unsigned long long)data_sectors, loadable_idx, loadable_idx, + data_block_size, hash_block_size, + num_data_blocks, hash_start_block, + algorithm, digest_hex, salt_hex); + if (opt_count) { + int extra = snprintf(buf ? buf + len : NULL, + buf ? bufsize - len : 0, + " %d %s", opt_count, opt_buf); + len += extra; + } + +out: + free(digest_hex); + free(salt_hex); + free(opt_buf); + return len; +} + +int fit_verity_build_cmdline(const void *fit, int conf_noffset, + struct bootm_headers *images) +{ + int images_noffset; + int dm_create_len = 0, dm_waitfor_len = 0; + char *dm_create = NULL, *dm_waitfor = NULL; + const char *uname; + int loadable_idx; + int found = 0; + int ret = 0; + + images_noffset = fdt_path_offset(fit, FIT_IMAGES_PATH); + if (images_noffset < 0) + return 0; + + for (loadable_idx = 0; + (uname = fdt_stringlist_get(fit, conf_noffset, + FIT_LOADABLE_PROP, + loadable_idx, NULL)); + loadable_idx++) { + int img_noffset, need; + u8 img_type; + char *tmp; + + img_noffset = fdt_subnode_offset(fit, images_noffset, uname); + if (img_noffset < 0) + continue; + + if (fit_image_get_type(fit, img_noffset, &img_type) || + img_type != IH_TYPE_FILESYSTEM) + continue; + + /* Measure first, then allocate and write */ + need = fit_verity_build_target(fit, img_noffset, + loadable_idx, uname, + found > 0, NULL, 0); + if (need == -ENOENT) + continue; /* no dm-verity subnode -- fine */ + if (need < 0) { + log_err("FIT: broken dm-verity metadata in '%s'\n", + uname); + ret = need; + goto err; + } + + tmp = realloc(dm_create, dm_create_len + need + 1); + if (!tmp) { + ret = -ENOMEM; + goto err; + } + dm_create = tmp; + fit_verity_build_target(fit, img_noffset, loadable_idx, + uname, found > 0, + dm_create + dm_create_len, + need + 1); + dm_create_len += need; + + /* Grow dm_waitfor buffer */ + need = snprintf(NULL, 0, "%s/dev/fit%d", + dm_waitfor_len ? "," : "", + loadable_idx); + tmp = realloc(dm_waitfor, dm_waitfor_len + need + 1); + if (!tmp) { + ret = -ENOMEM; + goto err; + } + dm_waitfor = tmp; + sprintf(dm_waitfor + dm_waitfor_len, "%s/dev/fit%d", + dm_waitfor_len ? "," : "", + loadable_idx); + dm_waitfor_len += need; + + found++; + } + + if (found) { + /* Transfer ownership to the bootm_headers */ + images->dm_mod_create = dm_create; + images->dm_mod_waitfor = dm_waitfor; + } else { + free(dm_create); + free(dm_waitfor); + } + + return 0; + +err: + free(dm_create); + free(dm_waitfor); + return ret; +} + +/** + * fmt used by both the measurement and the actual write of bootargs. + * Shared to guarantee they stay in sync. + */ +#define VERITY_BOOTARGS_FMT "%s%sdm-mod.create=\"%s\" dm-mod.waitfor=\"%s\"" + +int fit_verity_apply_bootargs(const struct bootm_headers *images) +{ + const char *existing; + char *newargs; + int len; + + if (!images->dm_mod_create) + return 0; + + existing = env_get("bootargs"); + if (!existing) + existing = ""; + + /* Measure */ + len = snprintf(NULL, 0, VERITY_BOOTARGS_FMT, + existing, existing[0] ? " " : "", + images->dm_mod_create, images->dm_mod_waitfor); + + newargs = malloc(len + 1); + if (!newargs) + return -ENOMEM; + + snprintf(newargs, len + 1, VERITY_BOOTARGS_FMT, + existing, existing[0] ? " " : "", + images->dm_mod_create, images->dm_mod_waitfor); + + env_set("bootargs", newargs); + free(newargs); + + return 0; +} + +void fit_verity_free(struct bootm_headers *images) +{ + free(images->dm_mod_create); + free(images->dm_mod_waitfor); + images->dm_mod_create = NULL; + images->dm_mod_waitfor = NULL; +} +#endif /* FIT_VERITY */ diff --git a/include/image.h b/include/image.h index 482446a8115..fe2361a667e 100644 --- a/include/image.h +++ b/include/image.h @@ -396,7 +396,19 @@ struct bootm_headers { ulong cmdline_start; ulong cmdline_end; struct bd_info *kbd; -#endif + +#if CONFIG_IS_ENABLED(FIT_VERITY) + /* + * dm-verity kernel command-line fragments, populated during FIT + * parsing by fit_verity_build_cmdline(). Bootmeths can check + * fit_verity_active() between bootm states, and + * fit_verity_apply_bootargs() appends these to the "bootargs" + * env var during BOOTM_STATE_OS_PREP. + */ + char *dm_mod_create; + char *dm_mod_waitfor; +#endif /* FIT_VERITY */ +#endif /* !USE_HOSTCC */ int verify; /* env_get("verify")[0] != 'n' */ @@ -756,6 +768,72 @@ int fit_image_load(struct bootm_headers *images, ulong addr, int arch, int image_ph_type, int bootstage_id, enum fit_load_op load_op, ulong *datap, ulong *lenp); +#if !defined(USE_HOSTCC) && CONFIG_IS_ENABLED(FIT_VERITY) +/** + * fit_verity_build_cmdline() - build dm-verity cmdline from FIT metadata + * @fit: pointer to the FIT blob + * @conf_noffset: configuration node offset in @fit + * @images: bootm headers; dm_mod_create / dm_mod_waitfor are + * populated on success + * + * Called automatically from boot_get_loadable() during FIT parsing. + * For each IH_TYPE_FILESYSTEM loadable with a dm-verity subnode, + * builds the corresponding dm target specification. + * + * Return: 0 on success, -ve errno on error + */ +int fit_verity_build_cmdline(const void *fit, int conf_noffset, + struct bootm_headers *images); + +/** + * fit_verity_apply_bootargs() - append dm-verity params to bootargs env + * @images: bootm headers with dm-verity cmdline fragments + * + * Called from BOOTM_STATE_OS_PREP before bootm_process_cmdline_env(). + * + * Return: 0 on success, -ve errno on error + */ +int fit_verity_apply_bootargs(const struct bootm_headers *images); + +/** + * fit_verity_active() - check whether dm-verity targets were found + * @images: bootm headers + * + * Return: true if at least one dm-verity target was built + */ +static inline bool fit_verity_active(const struct bootm_headers *images) +{ + return !!images->dm_mod_create; +} + +/** + * fit_verity_free() - free dm-verity cmdline allocations + * @images: bootm headers + */ +void fit_verity_free(struct bootm_headers *images); + +#else /* !FIT_VERITY */ + +static inline int fit_verity_build_cmdline(const void *fit, int conf_noffset, + struct bootm_headers *images) +{ + return 0; +} + +static inline int fit_verity_apply_bootargs(const struct bootm_headers *images) +{ + return 0; +} + +static inline bool fit_verity_active(const struct bootm_headers *images) +{ + return false; +} + +static inline void fit_verity_free(struct bootm_headers *images) {} + +#endif /* FIT_VERITY */ + /** * image_locate_script() - Locate the raw script in an image * -- cgit v1.3.1 From 1e4829855234d8dc91c2840a79e9a64a2e8bf3a6 Mon Sep 17 00:00:00 2001 From: Daniel Golle Date: Sat, 16 May 2026 00:38:07 +0100 Subject: tools: mkimage: add dm-verity Merkle-tree generation When mkimage encounters a dm-verity subnode inside a component image node it now automatically invokes veritysetup(8) with --no-superblock to generate the Merkle hash tree, screen-scrapes the Root hash and Salt from the tool output, and writes the computed properties back into the FIT blob. The user only needs to specify algorithm, data-block-size, and hash-block-size in the ITS; mkimage fills in digest, salt, num-data-blocks, and hash-start-block. Because --no-superblock is used, hash-start-block equals num-data-blocks with no off-by-one. The image data property is replaced with the expanded content (original data followed directly by the hash tree) so that subsequent hash and signature subnodes operate on the complete image. fit_image_add_verification_data() is restructured into two passes: dm-verity first (may grow data), then hashes and signatures. Signed-off-by: Daniel Golle Reviewed-by: Simon Glass --- include/image.h | 18 +++ tools/fit_image.c | 91 +++++++++++- tools/image-host.c | 414 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 514 insertions(+), 9 deletions(-) (limited to 'include/image.h') diff --git a/include/image.h b/include/image.h index fe2361a667e..7b16284257a 100644 --- a/include/image.h +++ b/include/image.h @@ -1427,6 +1427,24 @@ int fit_add_verification_data(const char *keydir, const char *keyfile, const char *cmdname, const char *algo_name, struct image_summary *summary); +#ifdef USE_HOSTCC +/** + * fit_verity_get_expanded() - look up the cached dm-verity expanded buffer + * + * After mkimage has run veritysetup on a FILESYSTEM image, the original + * data concatenated with the Merkle hash tree is cached in memory keyed + * by image name. fit_extract_data() retrieves it to write the external + * data section without having to re-read a temporary file from disk. + * + * @name: image unit name (FDT node name under /images) + * @data: output -- pointer to cached buffer (do NOT free; lifetime + * ends when mkimage exits) + * @size: output -- size of @data in bytes + * Return: 0 if a cache entry exists for @name, -ENOENT otherwise + */ +int fit_verity_get_expanded(const char *name, const void **data, size_t *size); +#endif /* USE_HOSTCC */ + /** * fit_image_verify_with_data() - Verify an image with given data * diff --git a/tools/fit_image.c b/tools/fit_image.c index 1dbc14c63e4..b53088bf783 100644 --- a/tools/fit_image.c +++ b/tools/fit_image.c @@ -40,10 +40,10 @@ static int fit_estimate_hash_sig_size(struct image_tool_params *params, const ch return -EIO; /* - * Walk the FIT image, looking for nodes named hash* and - * signature*. Since the interesting nodes are subnodes of an - * image or configuration node, we are only interested in - * those at depth exactly 3. + * Walk the FIT image, looking for nodes named hash*, + * signature*, and dm-verity. Since the interesting nodes are + * subnodes of an image or configuration node, we are only + * interested in those at depth exactly 3. * * The estimate for a hash node is based on a sha512 digest * being 64 bytes, with another 64 bytes added to account for @@ -55,6 +55,10 @@ static int fit_estimate_hash_sig_size(struct image_tool_params *params, const ch * account for fdt overhead and the various other properties * (hashed-nodes etc.) that will also be filled in. * + * For a dm-verity node the small metadata properties (digest, + * salt, two u32s and a temp-file path) are written into the + * FDT by fit_image_process_verity(). + * * One could try to be more precise in the estimates by * looking at the "algo" property and, in the case of * configuration signatures, the sign-images property. Also, @@ -76,6 +80,18 @@ static int fit_estimate_hash_sig_size(struct image_tool_params *params, const ch if (signing && !strncmp(name, FIT_SIG_NODENAME, strlen(FIT_SIG_NODENAME))) estimate += 1024; + + if (!strcmp(name, FIT_VERITY_NODENAME)) { + if (!params->external_data) { + fprintf(stderr, + "%s: dm-verity requires external data (-E)\n", + params->cmdname); + munmap(fdt, sbuf.st_size); + close(fd); + return -EINVAL; + } + estimate += 256; + } } munmap(fdt, sbuf.st_size); @@ -470,6 +486,41 @@ static int fit_write_images(struct image_tool_params *params, char *fdt) return 0; } +/** + * fit_copy_image_data() - copy image data, using cached verity expansion + * @fdt: FIT blob + * @node: image node offset + * @buf: destination buffer + * @buf_ptr: write offset within @buf + * @data: embedded image data (used when no dm-verity expansion exists) + * @lenp: in/out: on entry, length of @data; on exit, bytes written + * + * When fit_image_process_verity() has run, the expanded image data + * (original + hash tree) is cached in memory. Look it up by image name + * and copy from the cached buffer rather than the embedded ``data`` + * property; fall back to @data otherwise. + * + * Return: 0 on success + */ +static int fit_copy_image_data(void *fdt, int node, void *buf, + int buf_ptr, const void *data, int *lenp) +{ + const char *image_name = fdt_get_name(fdt, node, NULL); + const void *vdata; + size_t vsize; + + if (image_name && + !fit_verity_get_expanded(image_name, &vdata, &vsize)) { + memcpy(buf + buf_ptr, vdata, vsize); + *lenp = vsize; + return 0; + } + + memcpy(buf + buf_ptr, data, *lenp); + + return 0; +} + /** * fit_write_configs() - Write out a list of configurations to the FIT * @@ -653,6 +704,8 @@ static int fit_extract_data(struct image_tool_params *params, const char *fname) int node; int align_size = 0; int len = 0; + int verity_extra = 0; + int orig_len; fd = mmap_fdt(params->cmdname, fname, 0, &fdt, &sbuf, false, false); if (fd < 0) @@ -686,11 +739,34 @@ static int fit_extract_data(struct image_tool_params *params, const char *fname) align_size += 4; } + /* + * When dm-verity is active the external data for an image is + * larger than the embedded data property (original + hash tree). + * Walk images once more and consult the in-memory cache for the + * actual expanded size. + */ + fdt_for_each_subnode(node, fdt, images) { + const char *image_name; + const void *vdata; + size_t vsize; + + orig_len = 0; + if (fdt_subnode_offset(fdt, node, FIT_VERITY_NODENAME) < 0) + continue; + image_name = fdt_get_name(fdt, node, NULL); + if (!image_name || + fit_verity_get_expanded(image_name, &vdata, &vsize)) + continue; + fdt_getprop(fdt, node, FIT_DATA_PROP, &orig_len); + if ((int)vsize > orig_len) + verity_extra += (int)vsize - orig_len; + } + /* * Allocate space to hold the image data we will extract, * extral space allocate for image alignment to prevent overflow. */ - buf = calloc(1, fit_size + align_size); + buf = calloc(1, fit_size + align_size + verity_extra); if (!buf) { ret = -ENOMEM; goto err_munmap; @@ -721,7 +797,10 @@ static int fit_extract_data(struct image_tool_params *params, const char *fname) data = fdt_getprop(fdt, node, FIT_DATA_PROP, &len); if (!data) continue; - memcpy(buf + buf_ptr, data, len); + + ret = fit_copy_image_data(fdt, node, buf, buf_ptr, data, &len); + if (ret) + goto err_munmap; debug("Extracting data size %x\n", len); ret = fdt_delprop(fdt, node, FIT_DATA_PROP); diff --git a/tools/image-host.c b/tools/image-host.c index 8b550af0dc1..8f1e7be4066 100644 --- a/tools/image-host.c +++ b/tools/image-host.c @@ -12,8 +12,12 @@ #include #include #include +#include #include +#include +#include + #if CONFIG_IS_ENABLED(FIT_SIGNATURE) #include #include @@ -626,6 +630,376 @@ int fit_image_cipher_data(const char *keydir, void *keydest, image_noffset, cipher_node_offset, data, size, cmdname); } +/* + * In-memory cache of dm-verity expanded buffers (original data followed + * by the Merkle hash tree), keyed by image unit name. Populated by + * fit_image_process_verity() and consumed by fit_extract_data() / + * fit_copy_image_data() so that the expanded content never leaves the + * mkimage process address space. + */ +struct fit_verity_blob { + char *name; + void *data; + size_t size; + struct fit_verity_blob *next; +}; + +static struct fit_verity_blob *fit_verity_blobs; + +/* Stash a malloc'd expanded buffer; takes ownership of @data on success. */ +static int fit_verity_stash(const char *name, void *data, size_t size) +{ + struct fit_verity_blob *b; + + b = calloc(1, sizeof(*b)); + if (!b) + return -ENOMEM; + b->name = strdup(name); + if (!b->name) { + free(b); + return -ENOMEM; + } + b->data = data; + b->size = size; + b->next = fit_verity_blobs; + fit_verity_blobs = b; + + return 0; +} + +int fit_verity_get_expanded(const char *name, const void **data, size_t *size) +{ + struct fit_verity_blob *b; + + for (b = fit_verity_blobs; b; b = b->next) { + if (!strcmp(b->name, name)) { + *data = b->data; + *size = b->size; + return 0; + } + } + + return -ENOENT; +} + +/** + * fit_image_process_verity() - Run veritysetup and fill dm-verity properties + * + * Extracts the embedded image data to a temporary file, runs + * ``veritysetup format`` to generate the Merkle hash tree (appended to the + * same file), parses Root hash / Salt from its stdout, and writes the + * computed properties (digest, salt, num-data-blocks, hash-start-block) + * back into the FIT dm-verity subnode. + * + * The expanded data (original data + hash tree) is read back into a + * malloc'd buffer and stashed in an in-memory cache keyed by @image_name + * via fit_verity_stash(). The same buffer is returned through + * @expanded_data / @expanded_size so that hash and signature subnodes + * can be computed over the complete image; the returned pointer is a + * *view* of the cached buffer and must not be freed by the caller. + * fit_extract_data() later retrieves the same buffer via + * fit_verity_get_expanded() to write the external data section. + * + * @fit: FIT blob (read-write) + * @image_name: image unit name (for diagnostics) + * @verity_noffset: dm-verity subnode offset + * @data: embedded image data + * @data_size: size of @data in bytes + * @expanded_data: output -- malloc'd buffer with expanded content + * @expanded_size: output -- size of @expanded_data + * Return: 0 on success, -ve on error (-ENOSPC when the FIT blob is full) + */ +static int fit_image_process_verity(void *fit, const char *image_name, + int verity_noffset, + const void *data, size_t data_size, + void **expanded_data, size_t *expanded_size) +{ + const char *algo_prop; + char algo[64]; + const fdt32_t *val; + unsigned int data_block_size, hash_block_size; + uint32_t num_data_blocks; + size_t hash_offset; + uint32_t hash_start_block; + char tmpfile[] = "/tmp/mkimage-verity-XXXXXX"; + char dbs_arg[32], hbs_arg[32], algo_arg[80], hoff_arg[40]; + int pipefd[2]; + pid_t pid; + FILE *fp; + char line[256]; + char *colon, *value, *end; + char root_hash_hex[256] = {0}; + char salt_hex[256] = {0}; + uint8_t digest_bin[FIT_MAX_HASH_LEN]; + uint8_t salt_bin[FIT_MAX_HASH_LEN]; + int digest_len = 0, salt_len = 0; + void *expanded = NULL; + struct stat st; + int fd, ret; + + *expanded_data = NULL; + *expanded_size = 0; + + algo_prop = fdt_getprop(fit, verity_noffset, FIT_VERITY_ALGO_PROP, + NULL); + if (!algo_prop) { + fprintf(stderr, + "Missing '%s' in dm-verity node of '%s'\n", + FIT_VERITY_ALGO_PROP, image_name); + return -EINVAL; + } + /* Local copy -- the FDT pointer goes stale after fdt_setprop(). */ + snprintf(algo, sizeof(algo), "%s", algo_prop); + + val = fdt_getprop(fit, verity_noffset, FIT_VERITY_DBS_PROP, NULL); + if (!val) { + fprintf(stderr, + "Missing '%s' in dm-verity node of '%s'\n", + FIT_VERITY_DBS_PROP, image_name); + return -EINVAL; + } + data_block_size = fdt32_to_cpu(*val); + + val = fdt_getprop(fit, verity_noffset, FIT_VERITY_HBS_PROP, NULL); + if (!val) { + fprintf(stderr, + "Missing '%s' in dm-verity node of '%s'\n", + FIT_VERITY_HBS_PROP, image_name); + return -EINVAL; + } + hash_block_size = fdt32_to_cpu(*val); + + if (data_block_size < 512 || (data_block_size & (data_block_size - 1)) || + hash_block_size < 512 || (hash_block_size & (hash_block_size - 1))) { + fprintf(stderr, + "Block sizes must be >= 512 and a power of two in dm-verity node of '%s'\n", + image_name); + return -EINVAL; + } + + if (data_size % data_block_size) { + fprintf(stderr, + "Image '%s' size %zu not a multiple of data-block-size %d\n", + image_name, data_size, data_block_size); + return -EINVAL; + } + + if (data_size / data_block_size > UINT32_MAX || + data_size / hash_block_size > UINT32_MAX) { + fprintf(stderr, + "Image '%s' too large for dm-verity (> 2^32 blocks)\n", + image_name); + return -EINVAL; + } + num_data_blocks = data_size / data_block_size; + hash_offset = data_size; + + fd = mkstemp(tmpfile); + if (fd < 0) { + fprintf(stderr, "Can't create temp file: %s\n", + strerror(errno)); + return -EIO; + } + + if (write(fd, data, data_size) != (ssize_t)data_size) { + fprintf(stderr, "Can't write temp file: %s\n", + strerror(errno)); + ret = -EIO; + goto err_unlink; + } + close(fd); + fd = -1; + + /* + * Invoke veritysetup via fork/execvp -- no shell, so each argument + * goes verbatim to the binary and the algo string cannot inject + * additional commands no matter how crafted the .its is. + */ + snprintf(algo_arg, sizeof(algo_arg), "--hash=%s", algo); + snprintf(dbs_arg, sizeof(dbs_arg), "--data-block-size=%u", + data_block_size); + snprintf(hbs_arg, sizeof(hbs_arg), "--hash-block-size=%u", + hash_block_size); + snprintf(hoff_arg, sizeof(hoff_arg), "--hash-offset=%zu", hash_offset); + + if (pipe(pipefd) < 0) { + fprintf(stderr, "Can't create pipe: %s\n", strerror(errno)); + ret = -EIO; + goto err_unlink; + } + + pid = fork(); + if (pid < 0) { + fprintf(stderr, "Can't fork: %s\n", strerror(errno)); + close(pipefd[0]); + close(pipefd[1]); + ret = -EIO; + goto err_unlink; + } + + if (pid == 0) { + /* child: redirect stdout+stderr to pipe write-end, then exec */ + char *argv[] = { + "veritysetup", "format", tmpfile, tmpfile, + "--no-superblock", algo_arg, dbs_arg, hbs_arg, + hoff_arg, NULL, + }; + + close(pipefd[0]); + if (dup2(pipefd[1], STDOUT_FILENO) < 0 || + dup2(pipefd[1], STDERR_FILENO) < 0) + _exit(127); + close(pipefd[1]); + execvp(argv[0], argv); + fprintf(stderr, "Can't exec veritysetup: %s\n", + strerror(errno)); + _exit(127); + } + + /* parent: parse key: value lines from veritysetup stdout */ + close(pipefd[1]); + fp = fdopen(pipefd[0], "r"); + if (!fp) { + fprintf(stderr, "Can't fdopen veritysetup pipe: %s\n", + strerror(errno)); + close(pipefd[0]); + waitpid(pid, NULL, 0); + ret = -EIO; + goto err_unlink; + } + + while (fgets(line, sizeof(line), fp)) { + colon = strchr(line, ':'); + if (!colon) + continue; + value = colon + 1; + while (*value == ' ' || *value == '\t') + value++; + end = value + strlen(value) - 1; + while (end > value && (*end == '\n' || *end == '\r' || + *end == ' ')) + *end-- = '\0'; + + if (!strncmp(line, "Root hash:", 10)) + snprintf(root_hash_hex, sizeof(root_hash_hex), + "%s", value); + else if (!strncmp(line, "Salt:", 5)) + snprintf(salt_hex, sizeof(salt_hex), "%s", value); + } + fclose(fp); + + if (waitpid(pid, &ret, 0) < 0 || !WIFEXITED(ret) || + WEXITSTATUS(ret) != 0) { + fprintf(stderr, "veritysetup failed for '%s'\n", image_name); + ret = -EIO; + goto err_unlink; + } + + if (!root_hash_hex[0] || !salt_hex[0]) { + fprintf(stderr, "Failed to parse veritysetup output for '%s'\n", + image_name); + ret = -EIO; + goto err_unlink; + } + + digest_len = strlen(root_hash_hex) / 2; + salt_len = strlen(salt_hex) / 2; + + if (digest_len > (int)sizeof(digest_bin) || + salt_len > (int)sizeof(salt_bin)) { + fprintf(stderr, "Hash/salt too long for '%s'\n", image_name); + ret = -EINVAL; + goto err_unlink; + } + + if (hex2bin(digest_bin, root_hash_hex, digest_len) || + hex2bin(salt_bin, salt_hex, salt_len)) { + fprintf(stderr, "Invalid hex in veritysetup output for '%s'\n", + image_name); + ret = -EINVAL; + goto err_unlink; + } + + if (stat(tmpfile, &st)) { + fprintf(stderr, "Can't stat temp file: %s\n", + strerror(errno)); + ret = -EIO; + goto err_unlink; + } + + expanded = malloc(st.st_size); + if (!expanded) { + ret = -ENOMEM; + goto err_unlink; + } + + fd = open(tmpfile, O_RDONLY); + if (fd < 0 || read(fd, expanded, st.st_size) != st.st_size) { + fprintf(stderr, "Can't read back temp file: %s\n", + strerror(errno)); + ret = -EIO; + goto err_free; + } + close(fd); + fd = -1; + + /* Temp file is no longer needed -- expanded buffer lives in memory. */ + unlink(tmpfile); + + /* hash tree starts immediately after data (no superblock) */ + hash_start_block = hash_offset / hash_block_size; + + ret = fdt_setprop(fit, verity_noffset, FIT_VERITY_DIGEST_PROP, + digest_bin, digest_len); + if (ret) { + ret = (ret == -FDT_ERR_NOSPACE) ? -ENOSPC : -EIO; + goto err_free; + } + + ret = fdt_setprop(fit, verity_noffset, FIT_VERITY_SALT_PROP, + salt_bin, salt_len); + if (ret) { + ret = (ret == -FDT_ERR_NOSPACE) ? -ENOSPC : -EIO; + goto err_free; + } + + ret = fdt_setprop_u32(fit, verity_noffset, FIT_VERITY_NBLK_PROP, + num_data_blocks); + if (ret) { + ret = (ret == -FDT_ERR_NOSPACE) ? -ENOSPC : -EIO; + goto err_free; + } + + ret = fdt_setprop_u32(fit, verity_noffset, FIT_VERITY_HBLK_PROP, + hash_start_block); + if (ret) { + ret = (ret == -FDT_ERR_NOSPACE) ? -ENOSPC : -EIO; + goto err_free; + } + + /* + * Stash the expanded buffer in the in-process cache; fit_extract_data() + * looks it up via fit_verity_get_expanded() to populate the external + * data section. On success the cache takes ownership of @expanded. + */ + ret = fit_verity_stash(image_name, expanded, st.st_size); + if (ret) + goto err_free; + + *expanded_data = expanded; + *expanded_size = st.st_size; + + return 0; + +err_free: + free(expanded); +err_unlink: + if (fd >= 0) + close(fd); + unlink(tmpfile); + return ret; +} + /** * fit_image_add_verification_data() - calculate/set verig. data for image node * @@ -652,6 +1026,8 @@ int fit_image_cipher_data(const char *keydir, void *keydest, * * For signature details, please see doc/usage/fit/signature.rst * + * For dm-verity details, please see doc/usage/fit/dm-verity.rst + * * @keydir Directory containing *.key and *.crt files (or NULL) * @keydest FDT Blob to write public keys into (NULL if none) * @fit: Pointer to the FIT format image header @@ -667,9 +1043,16 @@ int fit_image_add_verification_data(const char *keydir, const char *keyfile, const char *cmdname, const char* algo_name) { const char *image_name; + const char *node_name; const void *data; size_t size; + /* + * View pointer into the dm-verity cache (owned by image-host.c). + * Do not free; the cache lives until mkimage exits. + */ + void *verity_data = NULL; int noffset; + int ret; /* Get image data and data length */ if (fit_image_get_emb_data(fit, image_noffset, &data, &size)) { @@ -679,13 +1062,38 @@ int fit_image_add_verification_data(const char *keydir, const char *keyfile, image_name = fit_get_name(fit, image_noffset, NULL); - /* Process all hash subnodes of the component image node */ + /* + * Pass 1 -- dm-verity: run veritysetup to produce the Merkle + * hash tree and fill in computed metadata. The expanded + * content (original data + hash tree) is returned in + * verity_data so that pass 2 hashes the complete image. + */ for (noffset = fdt_first_subnode(fit, image_noffset); noffset >= 0; noffset = fdt_next_subnode(fit, noffset)) { - const char *node_name; - int ret = 0; + if (!strcmp(fit_get_name(fit, noffset, NULL), + FIT_VERITY_NODENAME)) { + ret = fit_image_process_verity(fit, image_name, + noffset, + data, size, + &verity_data, + &size); + if (ret) + return ret; + if (verity_data) + data = verity_data; + break; + } + } + /* + * Pass 2 -- hashes and signatures: compute over the (possibly + * expanded) image data. + */ + for (noffset = fdt_first_subnode(fit, image_noffset); + noffset >= 0; + noffset = fdt_next_subnode(fit, noffset)) { + ret = 0; /* * Check subnode name, must be equal to "hash" or "signature". * Multiple hash nodes require unique unit node -- cgit v1.3.1 From 623f6c5b6ab7fa270a9e36db0c6136c5983a45a0 Mon Sep 17 00:00:00 2001 From: Randolph Sapp Date: Thu, 4 Jun 2026 10:50:35 -0500 Subject: boot: image-fdt: free old dtb reservations Add a free flag and an initial call to free allocations covered by the global FDT. This assumes that all calls to boot_fdt_add_mem_rsv_regions occur before the transition to the new device tree, thus we can access the currently active device tree through the global data pointer. This allows us to clearly indicate to the user when a device tree reservation fails. How we handle this can still use some improvement. Right now we'll keep the default behavior and try to boot anyway. Fixes: 5a6aa7d5913 ("boot: fdt: Handle already reserved memory in boot_fdt_reserve_region()") Signed-off-by: Randolph Sapp Acked-by: Ilias Apalodimas Reviewed-by: Simon Glass Fixes: tag with a 12-char hash: Fixes: 5a6aa7d59133 ("boot: fdt: Handle already reserved memory in --- boot/image-fdt.c | 77 +++++++++++++++++++++++++++++++++++++++++--------------- include/image.h | 2 +- 2 files changed, 58 insertions(+), 21 deletions(-) (limited to 'include/image.h') diff --git a/boot/image-fdt.c b/boot/image-fdt.c index a3a4fb8b558..1150131a11e 100644 --- a/boot/image-fdt.c +++ b/boot/image-fdt.c @@ -69,35 +69,50 @@ static const struct legacy_img_hdr *image_get_fdt(ulong fdt_addr) } #endif -static void boot_fdt_reserve_region(u64 addr, u64 size, u32 flags) +/** + * boot_fdt_handle_region - Reserve or free a given FDT region in LMB + * @addr: Reservation base address + * @size: Reservation size + * @flags: Reservation flags + * @free: Indicate if region is being freed or allocated + * + * Add or free a given reservation from LMB. This reports to the user if any + * errors occurred during either operation. + */ +static void boot_fdt_handle_region(u64 addr, u64 size, u32 flags, bool free) { long ret; phys_addr_t rsv_addr; rsv_addr = (phys_addr_t)addr; - ret = lmb_alloc_mem(LMB_MEM_ALLOC_ADDR, 0, &rsv_addr, size, flags); + if (free) + ret = lmb_free(rsv_addr, size, flags); + else + ret = lmb_alloc_mem(LMB_MEM_ALLOC_ADDR, 0, &rsv_addr, size, + flags); + if (!ret) { - debug(" reserving fdt memory region: addr=%llx size=%llx flags=%x\n", - (unsigned long long)addr, + debug(" %s fdt memory region: addr=%llx size=%llx flags=%x\n", + free ? "freed" : "reserved", (unsigned long long)addr, (unsigned long long)size, flags); - } else if (ret != -EEXIST && ret != -EINVAL) { - puts("ERROR: reserving fdt memory region failed "); - printf("(addr=%llx size=%llx flags=%x)\n", - (unsigned long long)addr, - (unsigned long long)size, flags); + } else { + printf("ERROR: %s fdt memory region failed (addr=%llx size=%llx flags=%x): %ld\n", + free ? "freeing" : "reserving", (unsigned long long)addr, + (unsigned long long)size, flags, ret); } } /** - * boot_fdt_add_mem_rsv_regions - Mark the memreserve and reserved-memory - * sections as unusable + * boot_fdt_handle_mem_rsv_regions - Handle FDT memreserve and reserved-memory + * sections * @fdt_blob: pointer to fdt blob base address + * @free: indicate if regions are being freed * - * Adds the and reserved-memorymemreserve regions in the dtb to the lmb block. - * Adding the memreserve regions prevents u-boot from using them to store the - * initrd or the fdt blob. + * Adds or removes reserved-memory and memreserve regions in the dtb to the lmb + * block. Adding the memreserve regions prevents u-boot from using them to store + * the initrd or the fdt blob. */ -void boot_fdt_add_mem_rsv_regions(void *fdt_blob) +static void boot_fdt_handle_mem_rsv_regions(const void *fdt_blob, bool free) { uint64_t addr, size; int i, total, ret; @@ -105,15 +120,12 @@ void boot_fdt_add_mem_rsv_regions(void *fdt_blob) struct fdt_resource res; u32 flags; - if (fdt_check_header(fdt_blob) != 0) - return; - /* process memreserve sections */ total = fdt_num_mem_rsv(fdt_blob); for (i = 0; i < total; i++) { if (fdt_get_mem_rsv(fdt_blob, i, &addr, &size) != 0) continue; - boot_fdt_reserve_region(addr, size, LMB_NOOVERWRITE); + boot_fdt_handle_region(addr, size, LMB_NOOVERWRITE, free); } /* process reserved-memory */ @@ -131,7 +143,7 @@ void boot_fdt_add_mem_rsv_regions(void *fdt_blob) flags = LMB_NOMAP; addr = res.start; size = res.end - res.start + 1; - boot_fdt_reserve_region(addr, size, flags); + boot_fdt_handle_region(addr, size, flags, free); } subnode = fdt_next_subnode(fdt_blob, subnode); @@ -139,6 +151,31 @@ void boot_fdt_add_mem_rsv_regions(void *fdt_blob) } } +/** + * boot_fdt_add_mem_rsv_regions - Add FDT memreserve and reserved-memory + * sections + * @fdt_blob: pointer to fdt blob base address + * + * Adds reserved-memory and memreserve regions in the dtb to the lmb block. + * Adding the memreserve regions prevents u-boot from using them to store the + * initrd or the fdt blob. + * + * This function will attempt to clean the currently active reservations if a + * new device tree blob is given. This must be called before gd->fdt_blob is + * switched. + */ +void boot_fdt_add_mem_rsv_regions(const void *fdt_blob) +{ + if (fdt_check_header(fdt_blob) != 0) + return; + + /* Remove old regions */ + if (gd->fdt_blob != fdt_blob) + boot_fdt_handle_mem_rsv_regions(gd->fdt_blob, true); + + boot_fdt_handle_mem_rsv_regions(fdt_blob, false); +} + /** * boot_relocate_fdt - relocate flat device tree * @of_flat_tree: pointer to a char* variable, will hold fdt start address diff --git a/include/image.h b/include/image.h index 34efac6056d..151619f42bf 100644 --- a/include/image.h +++ b/include/image.h @@ -827,7 +827,7 @@ int boot_get_fdt(void *buf, const char *select, uint arch, struct bootm_headers *images, char **of_flat_tree, ulong *of_size); -void boot_fdt_add_mem_rsv_regions(void *fdt_blob); +void boot_fdt_add_mem_rsv_regions(const void *fdt_blob); int boot_relocate_fdt(char **of_flat_tree, ulong *of_size); int boot_ramdisk_high(ulong rd_data, ulong rd_len, ulong *initrd_start, -- cgit v1.3.1