// SPDX-License-Identifier: GPL-2.0-only /* * Some devices made by H3C use a "VFS" filesystem to store firmware images. * This parses the start of the filesystem to read the length of the first * file (the kernel image). It then searches for the rootfs after the end of * the file data. This driver assumes that the filesystem was generated by * mkh3cvfs, and only works if the filesystem matches the expected layout, * which includes the file name of the kernel image. */ #include #include #include #include #include #include #include #include "mtdsplit.h" #define VFS_ERASEBLOCK_SIZE 0x10000 #define VFS_BLOCK_SIZE 0x400 #define VFS_BLOCKS_PER_ERASEBLOCK (VFS_ERASEBLOCK_SIZE / VFS_BLOCK_SIZE) #define FORMAT_FLAG_OFFSET 0x0 #define FORMAT_FLAG (VFS_ERASEBLOCK_SIZE << 12 | VFS_BLOCK_SIZE) #define FILE_ENTRY_OFFSET 0x800 #define FILE_ENTRY_FLAGS 0x3f #define FILE_ENTRY_PARENT_BLOCK 0 #define FILE_ENTRY_PARENT_INDEX 0 #define FILE_ENTRY_DATA_BLOCK 2 #define FILE_ENTRY_NAME "openwrt-kernel.bin" #define NR_PARTS 2 struct file_entry { uint8_t flags; uint8_t res0[5]; uint16_t year; uint8_t month; uint8_t day; uint8_t hour; uint8_t minute; uint8_t second; uint8_t res1[3]; uint32_t length; uint32_t parent_block; uint16_t parent_index; uint8_t res2[2]; uint32_t data_block; char name[96]; } __attribute__ ((packed)); static inline size_t block_offset(int block) { return VFS_ERASEBLOCK_SIZE * (block / (VFS_BLOCKS_PER_ERASEBLOCK-1)) + VFS_BLOCK_SIZE * (1 + (block % (VFS_BLOCKS_PER_ERASEBLOCK-1))); } static inline int block_count(size_t size) { return (size + VFS_BLOCK_SIZE - 1) / VFS_BLOCK_SIZE; } static int mtdsplit_h3c_vfs_parse(struct mtd_info *mtd, const struct mtd_partition **pparts, struct mtd_part_parser_data *data) { struct mtd_partition *parts; uint32_t format_flag; struct file_entry file_entry; size_t retlen; int err; size_t kernel_size; size_t expected_offset; size_t rootfs_offset; if (mtd->erasesize != VFS_ERASEBLOCK_SIZE) return -EINVAL; /* Check format flag */ err = mtd_read(mtd, FORMAT_FLAG_OFFSET, sizeof(format_flag), &retlen, (void *) &format_flag); if (err) return err; if (retlen != sizeof(format_flag)) return -EIO; if (format_flag != FORMAT_FLAG) return -EINVAL; /* Check file entry */ err = mtd_read(mtd, FILE_ENTRY_OFFSET, sizeof(file_entry), &retlen, (void *) &file_entry); if (err) return err; if (retlen != sizeof(file_entry)) return -EIO; if (file_entry.flags != FILE_ENTRY_FLAGS) return -EINVAL; if (file_entry.parent_block != FILE_ENTRY_PARENT_BLOCK) return -EINVAL; if (file_entry.parent_index != FILE_ENTRY_PARENT_INDEX) return -EINVAL; if (file_entry.data_block != FILE_ENTRY_DATA_BLOCK) return -EINVAL; if (strncmp(file_entry.name, FILE_ENTRY_NAME, sizeof(file_entry.name)) != 0) return -EINVAL; /* Find rootfs offset */ kernel_size = block_offset(file_entry.data_block + block_count(file_entry.length) - 1) + VFS_BLOCK_SIZE; expected_offset = mtd_roundup_to_eb(kernel_size, mtd); err = mtd_find_rootfs_from(mtd, expected_offset, mtd->size, &rootfs_offset, NULL); if (err) return err; parts = kzalloc(NR_PARTS * sizeof(*parts), GFP_KERNEL); if (!parts) return -ENOMEM; parts[0].name = KERNEL_PART_NAME; parts[0].offset = 0; parts[0].size = rootfs_offset; parts[1].name = ROOTFS_PART_NAME; parts[1].offset = rootfs_offset; parts[1].size = mtd->size - rootfs_offset; *pparts = parts; return NR_PARTS; } static const struct of_device_id mtdsplit_h3c_vfs_of_match_table[] = { { .compatible = "h3c,vfs-firmware" }, {}, }; MODULE_DEVICE_TABLE(of, mtdsplit_h3c_vfs_of_match_table); static struct mtd_part_parser mtdsplit_h3c_vfs_parser = { .owner = THIS_MODULE, .name = "h3c-vfs", .of_match_table = mtdsplit_h3c_vfs_of_match_table, .parse_fn = mtdsplit_h3c_vfs_parse, .type = MTD_PARSER_TYPE_FIRMWARE, }; module_mtd_part_parser(mtdsplit_h3c_vfs_parser);