diff --git a/CMakeLists.txt b/CMakeLists.txt
index e6592bccb157..9b70226b8dc1 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -65,6 +65,8 @@ FW_UTIL(mkdniimg "" "" "")
 FW_UTIL(mkedimaximg "" "" "")
 FW_UTIL(mkfwimage "" "-Wextra -D_FILE_OFFSET_BITS=64" "${ZLIB_LIBRARIES}")
 FW_UTIL(mkfwimage2 "" "" "${ZLIB_LIBRARIES}")
+FW_UTIL(mkh3cimg "" "" "")
+FW_UTIL(mkh3cvfs "" "" "")
 FW_UTIL(mkheader_gemtek "" "" "${ZLIB_LIBRARIES}")
 FW_UTIL(mkhilinkfw "" "" "${OPENSSL_CRYPTO_LIBRARIES}")
 FW_UTIL(mkmerakifw src/sha1.c "" "")
diff --git a/src/mkh3cimg.c b/src/mkh3cimg.c
new file mode 100644
index 000000000000..d5660537f196
--- /dev/null
+++ b/src/mkh3cimg.c
@@ -0,0 +1,366 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <byteswap.h>
+#include <endian.h>
+#include <getopt.h>
+
+
+#if !defined(__BYTE_ORDER)
+#error "Unknown byte order"
+#endif
+
+#if __BYTE_ORDER == __BIG_ENDIAN
+#define cpu_to_be16(x)  (x)
+#define cpu_to_be32(x)  (x)
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+#define cpu_to_be16(x)  bswap_16(x)
+#define cpu_to_be32(x)  bswap_32(x)
+#else
+#error "Unsupported endianness"
+#endif
+
+
+#define IMAGE_VERSION            1
+#define FILE_VERSION             1
+#define FILE_DESCRIPTION         "OpenWrt"
+#define FILE_TYPE_MASK           0x1
+
+
+#define PACKAGE_FLAG             2
+
+#define FILE_TYPE_APPLICATION    0x04000000
+
+#define VERSION_OFFSET_INVALID   0xffffffff
+
+#define COMPRESSION_TYPE_NONE    0xffffffff
+#define COMPRESSION_TYPE_7Z      0x00000002
+
+
+struct file_header {
+	uint8_t res1[4];
+
+	uint32_t header_crc;
+
+	uint32_t file_type;
+	uint32_t version;
+	uint32_t product_id;
+	uint32_t device_id;
+
+	uint32_t length_unpadded;
+
+	uint32_t version_offset;
+
+	uint16_t year;
+	uint8_t month;
+	uint8_t day;
+	uint8_t res2[1];
+	uint8_t hour;
+	uint8_t minute;
+	uint8_t second;
+
+	uint8_t res3[64];
+
+	char description[224];
+
+	uint32_t length;
+
+	uint32_t file_crc;
+
+	uint32_t compression_type;
+} __attribute__ ((packed));
+
+struct file_desc {
+	uint32_t file_type;
+	uint32_t offset;
+	uint32_t length;
+	uint32_t file_crc;
+	uint32_t version;
+	uint32_t type_mask;
+} __attribute__ ((packed));
+
+struct image_header {
+	uint32_t version;
+
+	uint32_t file_count;
+
+	uint32_t product_id;
+	uint32_t device_id;
+
+	uint16_t year;
+	uint8_t month;
+	uint8_t day;
+	uint8_t res1[1];
+	uint8_t hour;
+	uint8_t minute;
+	uint8_t second;
+
+	uint16_t package_crc;
+	uint16_t package_flag;
+
+	uint32_t length;
+
+	struct file_desc files[128];
+
+	/* RSA signature, not required */
+	uint8_t res2[3072];
+
+	uint32_t header_crc;
+} __attribute__ ((packed));
+
+
+static void *buf;
+static size_t buflen;
+
+static size_t length_unpadded;
+static size_t length;
+
+
+static uint32_t crc16_xmodem(char *buf, size_t len) {
+	uint32_t poly = 0x1021;
+	uint32_t crc = 0;
+	char b;
+	int i, j;
+
+	for (i = 0; i < len; i++) {
+		b = buf[i];
+		crc = crc ^ (b << 8);
+
+		for (j = 0; j < 8; j++) {
+			crc = crc << 1;
+			if (crc & 0x10000) {
+				crc = (crc ^ poly) & 0xffff;
+			}
+		}
+	}
+
+	return crc;
+}
+
+static int create_buffer_and_read_file(char *filename) {
+	FILE *f;
+
+	f = fopen(filename, "r");
+	if (f == NULL) {
+		fprintf(stderr, "failed to open input file\n");
+		goto err;
+	}
+
+	fseek(f, 0L, SEEK_END);
+	length_unpadded = ftell(f);
+	rewind(f);
+
+	length = length_unpadded;
+	if (length_unpadded % 8 != 0) {
+		length += 8 - length_unpadded % 8;
+	}
+
+	buflen = sizeof(struct file_header) + sizeof(struct image_header) + length;
+	buf = malloc(buflen);
+	if (!buf) {
+		fprintf(stderr, "failed to allocate buffer\n");
+		goto err_close;
+	}
+
+	memset(buf, 0, buflen);
+
+	if (fread(buf + sizeof(struct file_header) + sizeof(struct image_header), length_unpadded, 1, f) != 1) {
+		fprintf(stderr, "failed to read input file\n");
+		goto err_free;
+	}
+
+	fclose(f);
+	return 0;
+
+err_free:
+	free(buf);
+err_close:
+	fclose(f);
+err:
+	return -1;
+}
+
+static void build_file_header(uint32_t product_id, uint32_t device_id, uint32_t compression_type) {
+	struct file_header *header = buf + sizeof(struct image_header);
+	uint32_t crc;
+
+	header->file_type = cpu_to_be32(FILE_TYPE_APPLICATION);
+
+	header->version = cpu_to_be32(FILE_VERSION);
+
+	header->product_id = cpu_to_be32(product_id);
+	header->device_id = cpu_to_be32(device_id);
+
+	header->length_unpadded = cpu_to_be32(length_unpadded);
+
+	header->version_offset = cpu_to_be32(VERSION_OFFSET_INVALID);
+
+	header->year = cpu_to_be16(1970);
+	header->month = 1;
+	header->day = 1;
+	header->hour = 0;
+	header->minute = 0;
+	header->second = 0;
+
+	snprintf(header->description, sizeof(header->description), "%s", FILE_DESCRIPTION);
+
+	header->length = cpu_to_be32(length);
+
+	crc = crc16_xmodem(buf + sizeof(struct image_header) + sizeof(struct file_header), length);
+	header->file_crc = cpu_to_be32(crc);
+
+	header->compression_type = cpu_to_be32(compression_type);
+
+	crc = crc16_xmodem((char *)header + sizeof(header->res1) + sizeof(header->header_crc),
+		sizeof(struct file_header) - sizeof(header->res1) - sizeof(header->header_crc));
+	header->header_crc = cpu_to_be32(crc);
+}
+
+static void build_image_header(uint32_t product_id, uint32_t device_id) {
+	struct image_header *header = buf;
+	struct file_header *file_header = buf + sizeof(struct image_header);
+	uint32_t crc;
+
+	header->version = cpu_to_be32(IMAGE_VERSION);
+
+	header->file_count = cpu_to_be32(1);
+
+	header->product_id = cpu_to_be32(product_id);
+	header->device_id = cpu_to_be32(device_id);
+
+	header->year = cpu_to_be16(1970);
+	header->month = 1;
+	header->day = 1;
+	header->hour = 0;
+	header->minute = 0;
+	header->second = 0;
+
+	crc = crc16_xmodem(buf + sizeof(struct file_header), buflen - sizeof(struct file_header));
+	header->package_crc = cpu_to_be16(crc);
+	header->package_flag = cpu_to_be16(PACKAGE_FLAG);
+
+	header->length = cpu_to_be32(buflen - sizeof(struct image_header));
+
+	header->files[0].file_type = file_header->file_type;
+	header->files[0].offset = cpu_to_be32(sizeof(struct image_header));
+	header->files[0].length = cpu_to_be32(sizeof(struct file_header) + length);
+	header->files[0].file_crc = file_header->file_crc;
+	header->files[0].version = file_header->version;
+	header->files[0].type_mask = cpu_to_be32(FILE_TYPE_MASK);
+
+	crc = crc16_xmodem((char *)header, sizeof(struct image_header) - sizeof(header->header_crc));
+	header->header_crc = cpu_to_be32(crc);
+}
+
+static int write_output_file(char *filename) {
+	int ret = 0;
+	FILE *f;
+
+	f = fopen(filename, "w");
+	if (f == NULL) {
+		fprintf(stderr, "failed to open output file\n");
+		ret = -1;
+		goto err;
+	}
+
+	if (fwrite(buf, buflen, 1, f) != 1) {
+		fprintf(stderr, "failed to write output file\n");
+		ret = -1;
+	}
+
+	fclose(f);
+
+err:
+	return ret;
+}
+
+static void usage(char* argv[]) {
+	printf("Usage: %s [OPTIONS...]\n"
+	       "\n"
+	       "Options:\n"
+	       "  -p <product id>   product id (32-bit unsigned integer)\n"
+	       "  -d <device id>    device id (32-bit unsigned integer)\n"
+	       "  -c <compression>  compression type of the input file (7z or none)\n"
+	       "                    (in case of 7z only LZMA compression is allowed)\n"
+	       "  -i <file>         input filename\n"
+	       "  -o <file>         output filename\n"
+	       , argv[0]);
+}
+
+int main(int argc, char* argv[]) {
+	int ret = EXIT_FAILURE;
+
+	static uint32_t product_id = 0;
+	static uint32_t device_id = 0;
+	static uint32_t compression_type = COMPRESSION_TYPE_NONE;
+	static char *input_filename = NULL;
+	static char *output_filename = NULL;
+
+	while ( 1 ) {
+		int c;
+
+		c = getopt(argc, argv, "p:d:c:i:o:");
+		if (c == -1)
+			break;
+
+		switch (c) {
+		case 'p':
+			product_id = strtoul(optarg, NULL, 0);
+			break;
+		case 'd':
+			device_id = strtoul(optarg, NULL, 0);
+			break;
+		case 'c':
+			if (strcmp(optarg, "none") == 0) {
+				compression_type = COMPRESSION_TYPE_NONE;
+			} else if (strcmp(optarg, "7z") == 0) {
+				compression_type = COMPRESSION_TYPE_7Z;
+			} else {
+				usage(argv);
+				return EXIT_FAILURE;
+			}
+			break;
+		case 'i':
+			input_filename = optarg;
+			break;
+		case 'o':
+			output_filename = optarg;
+			break;
+		default:
+			usage(argv);
+			goto err;
+		}
+	}
+
+	if (!product_id || !device_id ||
+		!input_filename || strlen(input_filename) == 0 ||
+		!output_filename || strlen(output_filename) == 0) {
+
+		usage(argv);
+		goto err;
+	}
+
+	if (create_buffer_and_read_file(input_filename)) {
+		goto err;
+	}
+
+	build_file_header(product_id, device_id, compression_type);
+
+	build_image_header(product_id, device_id);
+
+	if (write_output_file(output_filename)) {
+		goto err_free;
+	}
+
+	ret = EXIT_SUCCESS;
+
+err_free:
+	free(buf);
+err:
+	return ret;
+}
diff --git a/src/mkh3cvfs.c b/src/mkh3cvfs.c
new file mode 100644
index 000000000000..486a1b88a0eb
--- /dev/null
+++ b/src/mkh3cvfs.c
@@ -0,0 +1,426 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <byteswap.h>
+#include <endian.h>
+#include <getopt.h>
+
+
+#if !defined(__BYTE_ORDER)
+#error "Unknown byte order"
+#endif
+
+#if __BYTE_ORDER == __BIG_ENDIAN
+#define cpu_to_be16(x)  (x)
+#define cpu_to_be32(x)  (x)
+#define be16_to_cpu(x)  (x)
+#define be32_to_cpu(x)  (x)
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+#define cpu_to_be16(x)  bswap_16(x)
+#define cpu_to_be32(x)  bswap_32(x)
+#define be16_to_cpu(x)  bswap_16(x)
+#define be32_to_cpu(x)  bswap_32(x)
+#else
+#error "Unsupported endianness"
+#endif
+
+
+#define FAT_PTR_FLAGS_GET(x)     ((be32_to_cpu(x) & 0xff000000) >> 24)
+#define FAT_PTR_FLAGS_SET(x, y)  (x = cpu_to_be32((be32_to_cpu(x) & 0x00ffffff) | ((y & 0x000000ff) << 24)))
+
+#define FAT_PTR_VAL_GET(x)     (be32_to_cpu(x) & 0x00ffffff)
+#define FAT_PTR_VAL_SET(x, y)  (x = cpu_to_be32((be32_to_cpu(x) & 0xff000000) | (y & 0x00ffffff)))
+
+
+struct fat_entry {
+	/* first byte contains flags */
+	uint32_t previous;
+
+	/* first byte is reserved */
+	uint32_t next;
+} __attribute__ ((packed));
+
+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 res3[3];
+
+	uint32_t length;
+
+	uint32_t parent_block;
+	uint16_t parent_index;
+
+	uint8_t res4[2];
+
+	uint32_t data_block;
+
+	char name[96];
+} __attribute__ ((packed));
+
+
+#define ERASEBLOCK_SIZE   0x10000
+#define BLOCK_SIZE        0x400
+
+#define BLOCKS_PER_ERASEBLOCK   (ERASEBLOCK_SIZE / BLOCK_SIZE)
+#define FILE_ENTRIES_PER_BLOCK  (BLOCK_SIZE / sizeof(struct file_entry))
+
+
+#define FLAG_FREE       0x80
+#define FLAG_VALID      0x40
+#define FLAG_INVALID    0x20
+#define FLAG_HIDE       0x10
+#define FLAG_DIRECTORY  0x02
+#define FLAG_READONLY   0x01
+
+
+static FILE *f;
+static size_t file_size = 0;
+
+static int dir_block = 1;
+static int dir_count = 0;
+
+static int next_data_block = 2;
+
+
+static inline size_t fat_entry_offset(int block) {
+	return ERASEBLOCK_SIZE * (block / (BLOCKS_PER_ERASEBLOCK-1))
+		+ sizeof(struct fat_entry) * (block % (BLOCKS_PER_ERASEBLOCK-1));
+}
+
+static inline size_t block_offset(int block) {
+	return ERASEBLOCK_SIZE * (block / (BLOCKS_PER_ERASEBLOCK-1))
+		+ BLOCK_SIZE * (1 + (block % (BLOCKS_PER_ERASEBLOCK-1)));
+}
+
+static int init_eraseblock(size_t offset) {
+	size_t end = offset - (offset % ERASEBLOCK_SIZE) + ERASEBLOCK_SIZE;
+	char *fill = "\xff";
+	int i;
+
+	while (file_size < end) {
+		if (fseek(f, file_size, SEEK_SET)) {
+			fprintf(stderr, "failed to seek to end\n");
+			return -1;
+		}
+
+		for (i = 0; i < ERASEBLOCK_SIZE; i++) {
+			if (fwrite(fill, 1, 1, f) != 1) {
+				fprintf(stderr, "failed to write eraseblock\n");
+				return -1;
+			}
+		}
+
+		file_size += ERASEBLOCK_SIZE;
+	}
+
+	return 0;
+}
+
+static inline void init_fat_entry(struct fat_entry *out) {
+	memset(out, '\xff', sizeof(struct fat_entry));
+}
+
+static int read_fat_entry(struct fat_entry *out, int block) {
+	size_t offset = fat_entry_offset(block);
+
+	if (init_eraseblock(offset)) {
+		return -1;
+	}
+
+	if (fseek(f, offset, SEEK_SET)) {
+		fprintf(stderr, "failed to seek to fat entry\n");
+		return -1;
+	}
+
+	if (fread(out, sizeof(struct fat_entry), 1, f) != 1) {
+		fprintf(stderr, "failed to read fat entry\n");
+		return -1;
+	}
+
+	return 0;
+}
+
+static int write_fat_entry(struct fat_entry *in, int block) {
+	size_t offset = fat_entry_offset(block);
+
+	if (init_eraseblock(offset)) {
+		return -1;
+	}
+
+	if (fseek(f, offset, SEEK_SET)) {
+		fprintf(stderr, "failed to seek to fat entry\n");
+		return -1;
+	}
+
+	if (fwrite(in, sizeof(struct fat_entry), 1, f) != 1) {
+		fprintf(stderr, "failed to write fat entry\n");
+		return -1;
+	}
+
+	return 0;
+}
+
+static inline void init_file_entry(struct file_entry *out) {
+	memset(out, '\xff', sizeof(struct file_entry));
+}
+
+static int write_file_entry(struct file_entry *in, int block, int index) {
+	size_t offset = block_offset(block) + sizeof(struct file_entry) * index;
+
+	if (init_eraseblock(offset)) {
+		return -1;
+	}
+
+	if (fseek(f, offset, SEEK_SET)) {
+		fprintf(stderr, "failed to seek to file entry\n");
+		return -1;
+	}
+
+	if (fwrite(in, sizeof(struct file_entry), 1, f) != 1) {
+		fprintf(stderr, "failed to write file entry\n");
+		return -1;
+	}
+
+	return 0;
+}
+
+static int write_block(void *in, size_t len, int block) {
+	size_t offset = block_offset(block);
+
+	if (init_eraseblock(offset)) {
+		return -1;
+	}
+
+	if (fseek(f, offset, SEEK_SET)) {
+		fprintf(stderr, "failed to seek to block\n");
+		return -1;
+	}
+
+	if (fwrite(in, len, 1, f) != 1) {
+		fprintf(stderr, "failed to write block\n");
+		return -1;
+	}
+
+	return 0;
+}
+
+static int create_root_directory() {
+	struct fat_entry fat;
+	struct file_entry file;
+
+	/* write format flag / FAT entry for block 0 (contains root file entry) */
+	init_fat_entry(&fat);
+	fat.previous = cpu_to_be32((ERASEBLOCK_SIZE << 12) | BLOCK_SIZE);
+	if (write_fat_entry(&fat, 0)) {
+		return -1;
+	}
+
+	/* write root file entry */
+	init_file_entry(&file);
+	file.flags = ~(FLAG_FREE | FLAG_VALID) & 0xff;
+	file.parent_block = cpu_to_be32(0);
+	file.data_block = cpu_to_be32(1);
+	if (write_file_entry(&file, 0, 0)) {
+		return -1;
+	}
+
+	/* write FAT entry for block 1 (contains first file entries of root directory) */
+	init_fat_entry(&fat);
+	FAT_PTR_FLAGS_SET(fat.previous, ~(FLAG_FREE | FLAG_VALID));
+	if (write_fat_entry(&fat, 1)) {
+		return -1;
+	}
+
+	return 0;
+}
+
+static int write_file(char *name, char *path) {
+	int ret = -1;
+	struct fat_entry fat;
+	struct file_entry file;
+	FILE *fin;
+	char buf[BLOCK_SIZE];
+	size_t len;
+	size_t total = 0;
+	int first_data_block = next_data_block;
+	int data_block = 0;
+
+	fin = fopen(path, "r");
+	if (fin == NULL) {
+		fprintf(stderr, "failed to open input file\n");
+		return ret;
+	}
+
+	while ((len = fread(buf, 1, BLOCK_SIZE, fin)) != 0 || !data_block) {
+		total += len;
+
+		/* update next pointer of previous FAT entry */
+		if (data_block) {
+			if (read_fat_entry(&fat, data_block)) {
+				goto err;
+			}
+			FAT_PTR_VAL_SET(fat.next, next_data_block);
+			if (write_fat_entry(&fat, data_block)) {
+				goto err;
+			}
+		}
+
+		/* write FAT entry for new block */
+		init_fat_entry(&fat);
+		FAT_PTR_FLAGS_SET(fat.previous, ~(FLAG_FREE | FLAG_VALID));
+		if (data_block) {
+			FAT_PTR_VAL_SET(fat.previous, data_block);
+		}
+		if (write_fat_entry(&fat, next_data_block)) {
+			goto err;
+		}
+
+		/* write data block */
+		if (write_block(buf, len, next_data_block)) {
+			goto err;
+		}
+
+		data_block = next_data_block;
+		next_data_block++;
+	}
+
+	/* create new file entries block if necessary */
+	if (dir_count == FILE_ENTRIES_PER_BLOCK) {
+		/* update next pointer of previous FAT entry */
+		if (read_fat_entry(&fat, dir_block)) {
+			goto err;
+		}
+		FAT_PTR_VAL_SET(fat.next, next_data_block);
+		if (write_fat_entry(&fat, dir_block)) {
+			goto err;
+		}
+
+		/* write FAT entry for new block */
+		init_fat_entry(&fat);
+		FAT_PTR_FLAGS_SET(fat.previous, ~(FLAG_FREE | FLAG_VALID));
+		FAT_PTR_VAL_SET(fat.previous, dir_block);
+		if (write_fat_entry(&fat, next_data_block)) {
+			goto err;
+		}
+
+		dir_block = next_data_block;
+		dir_count = 0;
+		next_data_block++;
+	}
+
+	/* write file entry */
+	init_file_entry(&file);
+
+	file.flags = ~(FLAG_FREE | FLAG_VALID) & 0xff;
+
+	file.year = cpu_to_be16(1970);
+	file.month = 1;
+	file.day = 1;
+	file.hour = 0;
+	file.minute = 0;
+	file.second = 0;
+
+	file.length = cpu_to_be32(total);
+
+	file.parent_block = cpu_to_be32(0);
+	file.parent_index = cpu_to_be16(0);
+
+	file.data_block = cpu_to_be32(first_data_block);
+
+	snprintf(file.name, sizeof(file.name), "%s", name);
+
+	if (write_file_entry(&file, dir_block, dir_count)) {
+		goto err;
+	}
+
+	dir_count++;
+
+	ret = 0;
+err:
+	fclose(fin);
+	return ret;
+}
+
+static void usage(char* argv[]) {
+	printf("Usage: %s [OPTIONS...]\n"
+	       "\n"
+	       "Options:\n"
+	       "  -f <filename>  filename in image\n"
+	       "  -i <file>      input filename\n"
+	       "  -o <file>      output filename\n"
+	       , argv[0]);
+}
+
+int main(int argc, char* argv[]) {
+	int ret = EXIT_FAILURE;
+
+	static char *filename = NULL;
+	static char *input_filename = NULL;
+	static char *output_filename = NULL;
+
+	while ( 1 ) {
+		int c;
+
+		c = getopt(argc, argv, "f:i:o:");
+		if (c == -1)
+			break;
+
+		switch (c) {
+		case 'f':
+			filename = optarg;
+			break;
+		case 'i':
+			input_filename = optarg;
+			break;
+		case 'o':
+			output_filename = optarg;
+			break;
+		default:
+			usage(argv);
+			goto err;
+		}
+	}
+
+	if (!filename || strlen(filename) == 0 ||
+		!input_filename || strlen(input_filename) == 0 ||
+		!output_filename || strlen(output_filename) == 0) {
+
+		usage(argv);
+		goto err;
+	}
+
+	f = fopen(output_filename, "w+");
+	if (f == NULL) {
+		fprintf(stderr, "failed to open output file\n");
+		goto err;
+	}
+
+	if (create_root_directory()) {
+		goto err_close;
+	}
+
+	if (write_file(filename, input_filename)) {
+		goto err_close;
+	}
+
+	ret = EXIT_SUCCESS;
+
+err_close:
+	fclose(f);
+err:
+	return ret;
+}
