Add MediaTek md1img and GFH firmware image parsers (#5974)

- Introduced md1img.h and md1img.c for parsing MediaTek md1img container format.
- Implemented mtk.h and mtk.c for parsing MediaTek GFH firmware images (md1rom).
- Added plugin support for md1img and mtk formats in bin_md1img.c and bin_mtk.c.
- Updated meson.build to include new source files and plugins.
- Enhanced RzBuffer utility with LZMA alone decompression support.

---------

Co-authored-by: Giovanni <561184+wargio@users.noreply.github.com>
This commit is contained in:
Dmitry Opokin 2026-06-23 03:25:48 +07:00 committed by GitHub
parent bb3b7cc7b1
commit 9d37b7cdf2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1828 additions and 1 deletions

View file

@ -0,0 +1,665 @@
// SPDX-FileCopyrightText: 2025 godcodehunter
// SPDX-License-Identifier: LGPL-3.0-only
/**
* \file Parser for MediaTek md1img container format.
*
* The md1img format is a simple section-based container used by MediaTek
* to package modem firmware components: md1rom (GFH firmware image),
* debug info (CATI format), DSP images, certificates, etc.
*
* Each section has a header with magic 0x58881688, a name, data size,
* memory address, and other metadata, followed by the section data
* and alignment padding.
*
* The md1rom section is parsed internally using the mtk GFH parser to
* provide proper base address, entry points, and code/header sections.
* Debug symbols are extracted from the CATI-format dbginfo section.
*
* References:
* - https://github.com/nccgroup/mtk_bp/blob/main/mtk_structs/mtk_img.ksy (md1img)
* - https://github.com/nccgroup/mtk_bp/blob/main/mtk_structs/mtk_dbg_info.ksy (CATI)
*/
#include "md1img.h"
// --- CATI debug info parsing ---
static void mtk_dbg_symbol_free(MtkDbgSymbol *sym) {
if (sym) {
free(sym->name);
free(sym);
}
}
/**
* \brief Parse symbols from a single CATI debug entry.
*
* The debug entry format (after "CATI" magic + type):
* unk3(u32), unk_str1(strz), unk_str2(strz), unk_str3(strz), date_str(strz),
* symbols_start(u32), files_start(u32),
* symbol_entries: [strz name, u32 addr1, u32 addr2] ... until empty name
*/
static bool md1img_parse_cati_debug(RzBuffer *b, RzPVector /*<MtkDbgSymbol *>*/ *symbols) {
// Skip unk3
rz_buf_seek(b, 4, RZ_BUF_CUR);
// Skip 4 strz fields (unk_str1, unk_str2, unk_str3, date_str)
for (int i = 0; i < 4; i++) {
ut64 len = rz_buf_read_string(b, NULL);
if (len == 0) {
return false;
}
}
// Skip symbols_start and files_start
rz_buf_seek(b, 8, RZ_BUF_CUR);
// Parse symbol entries (limit guards against corrupted/missing terminator)
int count = 0;
while (count < 500000) {
char *name = NULL;
ut64 len = rz_buf_read_string(b, &name);
if (len == 0 || !name) {
free(name);
break;
}
// Empty string = terminator
if (name[0] == '\0') {
free(name);
break;
}
ut32 addr1 = 0, addr2 = 0;
if (!rz_buf_read_le32(b, &addr1) || !rz_buf_read_le32(b, &addr2)) {
free(name);
break;
}
MtkDbgSymbol *sym = RZ_NEW0(MtkDbgSymbol);
if (!sym) {
free(name);
break;
}
sym->name = name;
sym->addr = addr1;
// For corrupted entries
sym->size = (addr2 > addr1) ? (addr2 - addr1) : 0;
rz_pvector_push(symbols, sym);
count++;
}
return count > 0;
}
/**
* \brief Parse a CATI container wrapping one or more debug entries.
*/
static bool md1img_parse_cati_container(RzBuffer *b, RzPVector /*<MtkDbgSymbol *>*/ *symbols) {
// unk1(u32), size(u32)
ut32 unk1 = 0, container_size = 0;
if (!rz_buf_read_le32(b, &unk1) || !rz_buf_read_le32(b, &container_size)) {
return false;
}
// 0x10 = CATI header size (magic[4] + type[4] + unk1[4] + size[4])
if (container_size < 0x10) {
return false;
}
// The nested entries are within (container_size - 0x10) bytes
ut64 entries_end = rz_buf_tell(b) + (container_size - 0x10);
ut64 buf_size = rz_buf_size(b);
// Clamp to actual buffer size in case container_size is corrupted
if (entries_end > buf_size) {
entries_end = buf_size;
}
// Parse nested CATI headers
while (rz_buf_tell(b) + 8 <= entries_end) {
ut8 magic[MTK_CATI_MAGIC_SIZE];
if (rz_buf_read(b, magic, MTK_CATI_MAGIC_SIZE) != MTK_CATI_MAGIC_SIZE ||
memcmp(magic, MTK_CATI_MAGIC, MTK_CATI_MAGIC_SIZE) != 0) {
break;
}
ut32 cati_type = 0;
if (!rz_buf_read_le32(b, &cati_type)) {
break;
}
if (cati_type != MTK_CATI_TYPE_DEBUG && cati_type != MTK_CATI_TYPE_DEBUG_DSP) {
break;
}
md1img_parse_cati_debug(b, symbols);
}
return rz_pvector_len(symbols) > 0;
}
/**
* \brief Parse a CATI debug info buffer and extract symbols.
*/
static RzPVector /*<MtkDbgSymbol *>*/ *md1img_parse_dbginfo(RzBuffer *b) {
rz_return_val_if_fail(b, NULL);
ut8 magic[MTK_CATI_MAGIC_SIZE];
if (rz_buf_read_at(b, 0, magic, MTK_CATI_MAGIC_SIZE) != MTK_CATI_MAGIC_SIZE) {
return NULL;
}
if (memcmp(magic, MTK_CATI_MAGIC, MTK_CATI_MAGIC_SIZE) != 0) {
return NULL;
}
rz_buf_seek(b, 0 + MTK_CATI_MAGIC_SIZE, RZ_BUF_SET);
ut32 cati_type = 0;
if (!rz_buf_read_le32(b, &cati_type)) {
return NULL;
}
RzPVector *symbols = rz_pvector_new((RzPVectorFree)mtk_dbg_symbol_free);
if (!symbols) {
return NULL;
}
bool ok = false;
if (cati_type == MTK_CATI_TYPE_CONTAINER) {
ok = md1img_parse_cati_container(b, symbols);
} else if (cati_type == MTK_CATI_TYPE_DEBUG || cati_type == MTK_CATI_TYPE_DEBUG_DSP) {
ok = md1img_parse_cati_debug(b, symbols);
}
if (!ok || rz_pvector_len(symbols) == 0) {
rz_pvector_free(symbols);
return NULL;
}
return symbols;
}
// --- md1img container parsing ---
static bool md1img_read_section_hdr(RzBuffer *b, ut64 section_start, Md1imgSection *sec) {
ut64 offset = section_start;
// Check magic
ut8 magic[MD1IMG_MAGIC_SIZE];
if (!rz_buf_read_offset(b, &offset, magic, MD1IMG_MAGIC_SIZE)) {
return false;
}
if (memcmp(magic, MD1IMG_MAGIC, MD1IMG_MAGIC_SIZE) != 0) {
return false;
}
// Read dsize
if (!rz_buf_read_le32_offset(b, &offset, &sec->dsize)) {
return false;
}
// Read name (32 bytes, null-terminated)
if (!rz_buf_read_offset(b, &offset, (ut8 *)sec->name, MD1IMG_NAME_SIZE)) {
return false;
}
sec->name[MD1IMG_NAME_SIZE - 1] = '\0';
// Read maddr and mode
if (!rz_buf_read_le32_offset(b, &offset, &sec->maddr) ||
!rz_buf_read_le32_offset(b, &offset, &sec->mode)) {
return false;
}
// Verify ext_magic
ut8 ext_magic[MD1IMG_EXT_MAGIC_SIZE];
if (!rz_buf_read_offset(b, &offset, ext_magic, MD1IMG_EXT_MAGIC_SIZE)) {
return false;
}
if (memcmp(ext_magic, MD1IMG_EXT_MAGIC, MD1IMG_EXT_MAGIC_SIZE) != 0) {
return false;
}
// Read remaining header fields
if (!rz_buf_read_le32_offset(b, &offset, &sec->hdr_size) ||
!rz_buf_read_le32_offset(b, &offset, &sec->hdr_version) ||
!rz_buf_read_le32_offset(b, &offset, &sec->img_type) ||
!rz_buf_read_le32_offset(b, &offset, &sec->img_list_end) ||
!rz_buf_read_le32_offset(b, &offset, &sec->align_size) ||
!rz_buf_read_le32_offset(b, &offset, &sec->dsize_extend) ||
!rz_buf_read_le32_offset(b, &offset, &sec->maddr_extend)) {
return false;
}
if (sec->hdr_size < MD1IMG_MIN_HDR_SIZE) {
return false;
}
sec->data_offset = section_start + sec->hdr_size;
return true;
}
RZ_IPI bool md1img_check_buffer(RZ_BORROW RZ_NONNULL RzBuffer *b) {
rz_return_val_if_fail(b, false);
if (rz_buf_size(b) < MD1IMG_MIN_HDR_SIZE) {
return false;
}
ut8 magic[MD1IMG_MAGIC_SIZE];
if (rz_buf_read_at(b, 0, magic, MD1IMG_MAGIC_SIZE) != MD1IMG_MAGIC_SIZE) {
return false;
}
return memcmp(magic, MD1IMG_MAGIC, MD1IMG_MAGIC_SIZE) == 0;
}
/**
* \brief Register the section's virtual file and run per-section parsers
* (GFH for md1rom, CATI for dbginfo). Takes ownership of \p vbuf.
*/
static void md1img_load_section(Md1imgObj *md1, int sec_idx, RZ_BORROW RZ_NONNULL const Md1imgSection *sec, RZ_OWN RZ_NONNULL RzBuffer *vbuf) {
RzBinVirtualFile *vfile = RZ_NEW0(RzBinVirtualFile);
if (!vfile) {
rz_buf_free(vbuf);
return;
}
vfile->buf = vbuf;
vfile->buf_owned = true;
vfile->name = rz_str_dup(sec->name);
rz_pvector_push(md1->vfiles, vfile);
// Parse md1rom section with mtk GFH parser
if (rz_str_casestr(sec->name, "md1rom") && md1->md1rom_idx < 0) {
md1->md1rom_idx = sec_idx;
// Search for GFH magic within md1rom data (may not be at offset 0)
ut64 gfh_off = 0;
ut8 magic_buf[4];
while (gfh_off + MTK_GFH_MIN_FILE_SIZE <= sec->dsize) {
if (rz_buf_read_at(vbuf, gfh_off, magic_buf, 4) != 4) {
break;
}
ut32 magic_val = rz_read_le32(magic_buf);
if ((magic_val & MTK_GFH_MAGIC_MASK) == MTK_GFH_MAGIC) {
break;
}
gfh_off += 4;
}
if (gfh_off + MTK_GFH_MIN_FILE_SIZE <= sec->dsize) {
RzBuffer *gfh_buf = rz_buf_new_slice(vbuf, gfh_off, sec->dsize - gfh_off);
if (gfh_buf) {
md1->mtk = mtk_obj_new(gfh_buf);
rz_buf_free(gfh_buf);
}
if (md1->mtk) {
md1->gfh_offset = gfh_off;
RZ_LOG_INFO("md1img: parsed GFH from '%s' at offset 0x%" PFMT64x " (load_addr=0x%x, entry=0x%x)\n",
sec->name, gfh_off, md1->mtk->file_info.load_addr, md1->mtk->entry_vaddr);
}
}
}
// Parse CATI debug info from dbginfo sections
if (rz_str_casestr(sec->name, "dbginfo") && sec->dsize > MTK_CATI_MAGIC_SIZE) {
RzPVector *syms = md1img_parse_dbginfo(vbuf);
// Try LZMA alone decompression if raw CATI parsing failed (dbginfo may be LZMA-compressed)
if (!syms) {
RzBuffer *decompressed = rz_buf_new_empty(0);
if (decompressed && rz_lzma_alone_dec_buf(vbuf, decompressed, 4096)) {
syms = md1img_parse_dbginfo(decompressed);
}
rz_buf_free(decompressed);
}
if (syms) {
if (!md1->dbg_symbols) {
md1->dbg_symbols = syms;
} else {
// Merge symbols from additional dbginfo sections
void **it;
rz_pvector_foreach (syms, it) {
rz_pvector_push(md1->dbg_symbols, *it);
}
// Disown elements before freeing (ownership transferred)
syms->v.free = NULL;
rz_pvector_free(syms);
}
RZ_LOG_INFO("md1img: loaded debug symbols from '%s'\n", sec->name);
}
}
}
RZ_IPI bool md1img_load_buffer(RZ_BORROW RZ_NONNULL RzBinFile *bf, RZ_BORROW RZ_NONNULL RzBinObject *obj, RZ_BORROW RZ_NONNULL RzBuffer *b, RZ_BORROW RZ_NULLABLE Sdb *sdb) {
rz_return_val_if_fail(bf && obj && b, false);
Md1imgObj *md1 = RZ_NEW0(Md1imgObj);
if (!md1) {
return false;
}
md1->sections = rz_vector_new(sizeof(Md1imgSection), NULL, NULL);
md1->vfiles = rz_pvector_new((RzPVectorFree)rz_bin_virtual_file_free);
md1->md1rom_idx = -1;
if (!md1->sections || !md1->vfiles) {
rz_vector_free(md1->sections);
rz_pvector_free(md1->vfiles);
free(md1);
return false;
}
ut64 buf_size = rz_buf_size(b);
ut64 offset = 0;
int count = 0;
// Limit iteration count as a safeguard against corrupted headers
while (offset + MD1IMG_MIN_HDR_SIZE <= buf_size && count < 256) {
Md1imgSection sec = { 0 };
if (!md1img_read_section_hdr(b, offset, &sec)) {
break;
}
// Validate that data fits in the buffer
if (sec.data_offset + sec.dsize > buf_size) {
RZ_LOG_WARN("md1img: section '%s' data exceeds file size, truncating\n", sec.name);
sec.dsize = buf_size - sec.data_offset;
}
int sec_idx = rz_vector_len(md1->sections);
rz_vector_push(md1->sections, &sec);
// Create a virtual file for this section's data and parse it
RzBuffer *vbuf = rz_buf_new_slice(b, sec.data_offset, sec.dsize);
if (vbuf) {
md1img_load_section(md1, sec_idx, &sec, vbuf);
}
// Advance to next section: data_offset + dsize + alignment padding
ut64 data_end = sec.data_offset + sec.dsize;
ut64 remainder = sec.align_size > 0 ? sec.dsize % sec.align_size : 0;
if (remainder > 0) {
offset = data_end + (sec.align_size - remainder);
} else {
offset = data_end;
}
count++;
}
if (offset < buf_size) {
RZ_LOG_WARN("md1img: %" PFMT64u " trailing bytes after last section header\n", buf_size - offset);
}
if (rz_vector_len(md1->sections) == 0) {
rz_vector_free(md1->sections);
rz_pvector_free(md1->vfiles);
free(md1);
return false;
}
obj->bin_obj = md1;
return true;
}
RZ_IPI void md1img_destroy(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
if (!bf || !bf->o || !bf->o->bin_obj) {
return;
}
Md1imgObj *md1 = bf->o->bin_obj;
rz_vector_free(md1->sections);
rz_pvector_free(md1->vfiles);
rz_pvector_free(md1->dbg_symbols);
mtk_obj_free(md1->mtk);
free(md1);
}
RZ_IPI RZ_OWN RzBinInfo *md1img_info(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
RzBinInfo *info = RZ_NEW0(RzBinInfo);
if (!info) {
return NULL;
}
info->file = bf->file ? rz_str_dup(bf->file) : NULL;
info->type = rz_str_dup("MediaTek md1img container");
info->machine = rz_str_dup("MediaTek Modem");
info->arch = rz_str_dup("mips");
info->cpu = rz_str_dup("nanomips");
info->rclass = rz_str_dup("firmware");
info->subsystem = rz_str_dup("modem");
info->has_va = true;
info->bits = 32;
info->big_endian = false;
return info;
}
RZ_IPI ut64 md1img_baddr(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
return MTK_MODEM_BADDR;
}
RZ_IPI RZ_OWN RzPVector /*<RzBinAddr *>*/ *md1img_entries(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
rz_return_val_if_fail(bf && bf->o && bf->o->bin_obj, NULL);
Md1imgObj *md1 = bf->o->bin_obj;
RzPVector *ret = rz_pvector_new(free);
if (!ret) {
return NULL;
}
if (md1->mtk) {
RzBinAddr *entry = RZ_NEW0(RzBinAddr);
if (entry) {
entry->vaddr = md1img_baddr(bf) + (md1->mtk->file_info.jump_offset - md1->mtk->code_offset);
entry->paddr = md1->gfh_offset + md1->mtk->file_info.jump_offset;
rz_pvector_push(ret, entry);
}
}
return ret;
}
RZ_IPI RZ_OWN RzPVector /*<RzBinVirtualFile *>*/ *md1img_virtual_files(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
rz_return_val_if_fail(bf && bf->o && bf->o->bin_obj, NULL);
Md1imgObj *md1 = bf->o->bin_obj;
RzPVector *ret = rz_pvector_new((RzPVectorFree)rz_bin_virtual_file_free);
if (!ret) {
return NULL;
}
void **it;
rz_pvector_foreach (md1->vfiles, it) {
RzBinVirtualFile *vf = *it;
RzBinVirtualFile *clone = rz_bin_virtual_file_clone(vf);
if (clone) {
rz_pvector_push(ret, clone);
}
}
return ret;
}
RZ_IPI RZ_OWN RzPVector /*<RzBinMap *>*/ *md1img_maps(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
rz_return_val_if_fail(bf && bf->o && bf->o->bin_obj, NULL);
Md1imgObj *md1 = bf->o->bin_obj;
RzPVector *ret = rz_pvector_new((RzPVectorFree)rz_bin_map_free);
if (!ret) {
return NULL;
}
// Only map the md1rom section into the virtual address space.
// Other sections (md1dsp, md1drdi, certs, etc.) belong to different
// processors or address spaces and would conflict with md1rom if mapped.
// They remain accessible as virtual files for raw data viewing.
if (md1->md1rom_idx >= 0) {
Md1imgSection *sec = rz_vector_index_ptr(md1->sections, md1->md1rom_idx);
if (md1->mtk) {
ut64 paddr = md1->gfh_offset + md1->mtk->code_offset;
mtk_append_maps(md1->mtk, paddr, sec->name, ret);
} else {
RzBinMap *map = RZ_NEW0(RzBinMap);
if (map) {
map->name = rz_str_dup(sec->name);
map->vfile_name = rz_str_dup(sec->name);
map->paddr = 0;
map->psize = sec->dsize;
map->vaddr = sec->maddr;
map->vsize = sec->dsize;
map->perm = RZ_PERM_RX;
rz_pvector_push(ret, map);
}
}
}
return ret;
}
RZ_IPI RZ_OWN RzPVector /*<RzBinSection *>*/ *md1img_sections(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
rz_return_val_if_fail(bf && bf->o && bf->o->bin_obj, NULL);
Md1imgObj *md1 = bf->o->bin_obj;
RzPVector *ret = rz_pvector_new((RzPVectorFree)rz_bin_section_free);
if (!ret) {
return NULL;
}
if (md1->mtk && md1->md1rom_idx >= 0 && md1->mtk->code_size > 0) {
Md1imgSection *sec = rz_vector_index_ptr(md1->sections, md1->md1rom_idx);
RzBinSection *code = RZ_NEW0(RzBinSection);
if (code) {
code->name = rz_str_newf("%s.code", sec->name);
code->paddr = md1->gfh_offset + md1->mtk->code_offset;
code->size = md1->mtk->code_size;
code->vsize = md1->mtk->code_size;
code->vaddr = md1img_baddr(bf);
code->perm = RZ_PERM_RX;
rz_pvector_push(ret, code);
}
}
return ret;
}
RZ_IPI RZ_OWN RzPVector /*<RzBinSymbol *>*/ *md1img_symbols(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
rz_return_val_if_fail(bf && bf->o && bf->o->bin_obj, NULL);
Md1imgObj *md1 = bf->o->bin_obj;
RzPVector *ret = rz_pvector_new((RzPVectorFree)rz_bin_symbol_free);
if (!ret) {
return NULL;
}
if (!md1->dbg_symbols) {
return ret;
}
void **it;
rz_pvector_foreach (md1->dbg_symbols, it) {
MtkDbgSymbol *dbg = *it;
RzBinSymbol *sym = RZ_NEW0(RzBinSymbol);
if (!sym) {
continue;
}
sym->name = rz_str_dup(dbg->name);
sym->vaddr = dbg->addr;
sym->size = dbg->size;
sym->type = RZ_BIN_TYPE_FUNC_STR;
// Compute paddr for symbols within mapped regions (kseg0 or kuseg).
if (md1->mtk) {
ut64 code_paddr = md1->gfh_offset + md1->mtk->code_offset;
ut64 kseg0_end = md1img_baddr(bf) + md1->mtk->code_size;
ut64 kuseg_base = md1->mtk->file_info.load_addr + md1->mtk->code_offset;
ut64 kuseg_end = kuseg_base + md1->mtk->code_size;
if (dbg->addr >= md1img_baddr(bf) && dbg->addr < kseg0_end) {
sym->paddr = code_paddr + (dbg->addr - md1img_baddr(bf));
} else if (dbg->addr >= kuseg_base && dbg->addr < kuseg_end) {
sym->paddr = code_paddr + (dbg->addr - kuseg_base);
}
}
rz_pvector_push(ret, sym);
}
return ret;
}
RZ_IPI RZ_OWN RzPVector /*<RzBinString *>*/ *md1img_strings(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
// Container format: suppress string scanning on the raw container buffer.
// Strings from the firmware payload are accessible via the mapped vfiles.
return rz_pvector_new(NULL);
}
RZ_IPI RZ_OWN RzStructuredData *md1img_structure(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
rz_return_val_if_fail(bf && bf->o && bf->o->bin_obj, NULL);
Md1imgObj *md1 = bf->o->bin_obj;
RzStructuredData *root = rz_structured_data_new_map();
if (!root) {
return NULL;
}
rz_structured_data_map_add_unsigned(root, "num_sections", rz_vector_len(md1->sections), false);
RzStructuredData *secs = rz_structured_data_map_add_array(root, "sections");
if (secs) {
Md1imgSection *sec;
rz_vector_foreach (md1->sections, sec) {
RzStructuredData *s = rz_structured_data_array_add_map(secs);
if (!s) {
continue;
}
rz_structured_data_map_add_string(s, "name", sec->name);
rz_structured_data_map_add_unsigned(s, "data_size", sec->dsize, true);
rz_structured_data_map_add_unsigned(s, "data_offset", sec->data_offset, true);
rz_structured_data_map_add_unsigned(s, "maddr", sec->maddr, true);
rz_structured_data_map_add_unsigned(s, "mode", sec->mode, true);
rz_structured_data_map_add_unsigned(s, "hdr_size", sec->hdr_size, true);
rz_structured_data_map_add_unsigned(s, "hdr_version", sec->hdr_version, true);
rz_structured_data_map_add_unsigned(s, "img_type", sec->img_type, true);
rz_structured_data_map_add_unsigned(s, "img_list_end", sec->img_list_end, true);
rz_structured_data_map_add_unsigned(s, "align_size", sec->align_size, true);
}
}
// GFH structure from md1rom section
if (md1->mtk) {
MtkObj *mtk = md1->mtk;
RzStructuredData *gfh = rz_structured_data_map_add_map(root, "gfh_file_info");
if (gfh) {
rz_structured_data_map_add_unsigned(gfh, "magic_version", mtk->first_common.magic_version, true);
rz_structured_data_map_add_unsigned(gfh, "header_block_size", mtk->first_common.size, false);
rz_structured_data_map_add_string(gfh, "header_type", mtk_gfh_type_str(mtk->first_common.type));
rz_structured_data_map_add_string(gfh, "name", mtk->file_info.name);
rz_structured_data_map_add_unsigned(gfh, "file_type", mtk->file_info.file_type, true);
rz_structured_data_map_add_unsigned(gfh, "flash_type", mtk->file_info.flash_type, true);
rz_structured_data_map_add_unsigned(gfh, "sig_type", mtk->file_info.sig_type, true);
rz_structured_data_map_add_unsigned(gfh, "load_addr", mtk->file_info.load_addr, true);
rz_structured_data_map_add_unsigned(gfh, "total_size", mtk->file_info.total_size, true);
rz_structured_data_map_add_unsigned(gfh, "max_size", mtk->file_info.max_size, true);
rz_structured_data_map_add_unsigned(gfh, "hdr_size", mtk->file_info.hdr_size, true);
rz_structured_data_map_add_unsigned(gfh, "sig_size", mtk->file_info.sig_size, true);
rz_structured_data_map_add_unsigned(gfh, "jump_offset", mtk->file_info.jump_offset, true);
rz_structured_data_map_add_unsigned(gfh, "entry_point", mtk->entry_vaddr, true);
}
if (rz_vector_len(mtk->extra_headers) > 0) {
RzStructuredData *hdrs = rz_structured_data_map_add_array(root, "gfh_headers");
if (hdrs) {
MtkGfhHeader *extra;
rz_vector_foreach (mtk->extra_headers, extra) {
RzStructuredData *h = rz_structured_data_array_add_map(hdrs);
if (!h) {
continue;
}
rz_structured_data_map_add_unsigned(h, "file_offset", extra->file_offset, true);
rz_structured_data_map_add_string(h, "type", mtk_gfh_type_str(extra->common.type));
rz_structured_data_map_add_unsigned(h, "type_id", extra->common.type, true);
rz_structured_data_map_add_unsigned(h, "size", extra->common.size, false);
}
}
}
}
if (md1->dbg_symbols) {
rz_structured_data_map_add_unsigned(root, "debug_symbols_count",
rz_pvector_len(md1->dbg_symbols), false);
}
return root;
}

View file

@ -0,0 +1,97 @@
// SPDX-FileCopyrightText: 2025 godcodehunter
// SPDX-License-Identifier: LGPL-3.0-only
/**
* \file Header for MediaTek md1img container format parser.
*
* The md1img format packages modem firmware components: md1rom (GFH image),
* debug info (CATI format), DSP images, certificates, etc.
*
* References:
* - https://github.com/nccgroup/mtk_bp/blob/main/mtk_structs/mtk_img.ksy (md1img)
* - https://github.com/nccgroup/mtk_bp/blob/main/mtk_structs/mtk_dbg_info.ksy (CATI)
*/
#ifndef MD1IMG_H
#define MD1IMG_H
#include <rz_types.h>
#include <rz_util.h>
#include <rz_lib.h>
#include <rz_bin.h>
#include "mtk.h"
#define MD1IMG_MAGIC "\x88\x16\x88\x58"
#define MD1IMG_MAGIC_SIZE 4
#define MD1IMG_EXT_MAGIC "\x89\x16\x89\x58"
#define MD1IMG_EXT_MAGIC_SIZE 4
#define MD1IMG_NAME_SIZE 32
/* Minimum section header size (fields before reserved) */
#define MD1IMG_MIN_HDR_SIZE 0x50
#define MTK_CATI_MAGIC "CATI"
#define MTK_CATI_MAGIC_SIZE 4
/* "CTNR" as LE u32 */
#define MTK_CATI_TYPE_CONTAINER 0x524E5443
#define MTK_CATI_TYPE_DEBUG 1
#define MTK_CATI_TYPE_DEBUG_DSP 2
/**
* \brief A debug symbol parsed from a CATI debug info section.
*
* Reference: https://github.com/nccgroup/mtk_bp/blob/main/mtk_structs/mtk_dbg_info.ksy
*/
typedef struct mtk_dbg_symbol {
char *name; ///< Symbol name
ut32 addr; ///< Symbol address
ut32 size; ///< Symbol size (addr2 - addr1, or 0 if addr2 <= addr1)
} MtkDbgSymbol;
/**
* \brief Parsed section from an md1img container.
*
* Each section has a header (magic + metadata) followed by data and
* alignment padding.
*/
typedef struct md1img_section {
char name[MD1IMG_NAME_SIZE]; ///< Section name (null-terminated)
ut32 dsize; ///< Data size
ut32 maddr; ///< Memory address
ut32 mode; ///< Mode flags
ut32 hdr_size; ///< Header size (total, from start of section to data)
ut32 hdr_version; ///< Header version
ut32 img_type; ///< Image type
ut32 img_list_end; ///< Image list end marker
ut32 align_size; ///< Alignment size for padding after data
ut32 dsize_extend; ///< Extended data size (high bits)
ut32 maddr_extend; ///< Extended memory address (high bits)
ut64 data_offset; ///< File offset where section data starts
} Md1imgSection;
/**
* \brief Top-level parsed object for md1img container.
*/
typedef struct md1img_obj {
RzVector /*<Md1imgSection>*/ *sections; ///< All parsed container sections
RzPVector /*<RzBinVirtualFile *>*/ *vfiles; ///< Virtual file per section
RzPVector /*<MtkDbgSymbol *>*/ *dbg_symbols; ///< Debug symbols from CATI section
MtkObj *mtk; ///< Parsed GFH from md1rom section (or NULL)
ut64 gfh_offset; ///< Offset of GFH within md1rom vfile
int md1rom_idx; ///< Index of md1rom section, or -1
} Md1imgObj;
RZ_IPI bool md1img_check_buffer(RZ_BORROW RZ_NONNULL RzBuffer *b);
RZ_IPI bool md1img_load_buffer(RZ_BORROW RZ_NONNULL RzBinFile *bf, RZ_BORROW RZ_NONNULL RzBinObject *obj, RZ_BORROW RZ_NONNULL RzBuffer *b, RZ_BORROW RZ_NULLABLE Sdb *sdb);
RZ_IPI void md1img_destroy(RZ_BORROW RZ_NONNULL RzBinFile *bf);
RZ_IPI RZ_OWN RzBinInfo *md1img_info(RZ_BORROW RZ_NONNULL RzBinFile *bf);
RZ_IPI ut64 md1img_baddr(RZ_BORROW RZ_NONNULL RzBinFile *bf);
RZ_IPI RZ_OWN RzPVector /*<RzBinAddr *>*/ *md1img_entries(RZ_BORROW RZ_NONNULL RzBinFile *bf);
RZ_IPI RZ_OWN RzPVector /*<RzBinVirtualFile *>*/ *md1img_virtual_files(RZ_BORROW RZ_NONNULL RzBinFile *bf);
RZ_IPI RZ_OWN RzPVector /*<RzBinMap *>*/ *md1img_maps(RZ_BORROW RZ_NONNULL RzBinFile *bf);
RZ_IPI RZ_OWN RzPVector /*<RzBinSection *>*/ *md1img_sections(RZ_BORROW RZ_NONNULL RzBinFile *bf);
RZ_IPI RZ_OWN RzPVector /*<RzBinSymbol *>*/ *md1img_symbols(RZ_BORROW RZ_NONNULL RzBinFile *bf);
RZ_IPI RZ_OWN RzPVector /*<RzBinString *>*/ *md1img_strings(RZ_BORROW RZ_NONNULL RzBinFile *bf);
RZ_IPI RZ_OWN RzStructuredData *md1img_structure(RZ_BORROW RZ_NONNULL RzBinFile *bf);
#endif

416
librz/bin/format/mtk/mtk.c Normal file
View file

@ -0,0 +1,416 @@
// SPDX-FileCopyrightText: 2025 godcodehunter
// SPDX-License-Identifier: LGPL-3.0-only
/**
* \file Implementation of MediaTek GFH firmware image parser (md1rom).
*
* The GFH (Generic File Header) format is used by MediaTek for bootloader
* and modem firmware images. The file starts with a chain of GFH headers
* (the first must be FILE_INFO), followed by the code/data payload.
*
* References:
* - https://github.com/nccgroup/mtk_bp/blob/main/mtk_structs/mtk_img.ksy
* - https://github.com/u-boot/u-boot/blob/master/tools/mtk_image.h
*/
#include "mtk.h"
#include "md1img.h"
static inline bool mtk_is_gfh_magic(ut32 magic_version) {
return (magic_version & MTK_GFH_MAGIC_MASK) == MTK_GFH_MAGIC;
}
/**
* \brief Scan for GFH FILE_INFO header within the first MTK_GFH_SCAN_LIMIT bytes.
* \return offset of the GFH header, or UT64_MAX if not found.
*/
static ut64 mtk_find_gfh(RzBuffer *b) {
ut64 buf_size = rz_buf_size(b);
ut64 limit = RZ_MIN(buf_size, MTK_GFH_SCAN_LIMIT);
// GFH headers are typically aligned to 4 bytes
for (ut64 off = 0; off + MTK_GFH_MIN_FILE_SIZE <= limit; off += 4) {
ut64 cursor = off;
MtkGfhCommonHdr hdr = { 0 };
if (!rz_buf_read_le32_offset(b, &cursor, &hdr.magic_version) ||
!rz_buf_read_le16_offset(b, &cursor, &hdr.size) ||
!rz_buf_read_le16_offset(b, &cursor, (ut16 *)&hdr.type)) {
continue;
}
if (mtk_is_gfh_magic(hdr.magic_version) &&
hdr.type == MTK_GFH_TYPE_FILE_INFO &&
hdr.size >= MTK_GFH_MIN_FILE_SIZE) {
return off;
}
}
return UT64_MAX;
}
static bool mtk_read_common_hdr(RzBuffer *b, ut64 *offset, MtkGfhCommonHdr *hdr) {
return rz_buf_read_le32_offset(b, offset, &hdr->magic_version) &&
rz_buf_read_le16_offset(b, offset, &hdr->size) &&
rz_buf_read_le16_offset(b, offset, (ut16 *)&hdr->type);
}
static bool mtk_read_file_info(RzBuffer *b, ut64 *offset, MtkGfhFileInfo *fi) {
if (!rz_buf_read_offset(b, offset, (ut8 *)fi->name, MTK_GFH_FILE_INFO_NAME_SIZE)) {
return false;
}
fi->name[MTK_GFH_FILE_INFO_NAME_SIZE - 1] = '\0';
return rz_buf_read_le32_offset(b, offset, &fi->unused) &&
rz_buf_read_le16_offset(b, offset, &fi->file_type) &&
rz_buf_read8_offset(b, offset, &fi->flash_type) &&
rz_buf_read8_offset(b, offset, &fi->sig_type) &&
rz_buf_read_le32_offset(b, offset, &fi->load_addr) &&
rz_buf_read_le32_offset(b, offset, &fi->total_size) &&
rz_buf_read_le32_offset(b, offset, &fi->max_size) &&
rz_buf_read_le32_offset(b, offset, &fi->hdr_size) &&
rz_buf_read_le32_offset(b, offset, &fi->sig_size) &&
rz_buf_read_le32_offset(b, offset, &fi->jump_offset) &&
rz_buf_read_le32_offset(b, offset, &fi->processed);
}
RZ_IPI RZ_BORROW const char *mtk_gfh_type_str(MtkGfhType type) {
switch (type) {
case MTK_GFH_TYPE_FILE_INFO:
return "file_info";
case MTK_GFH_TYPE_BL_INFO:
return "bl_info";
case MTK_GFH_TYPE_ANTI_CLONE:
return "anti_clone";
case MTK_GFH_TYPE_BL_SEC_KEY:
return "bl_sec_key";
case MTK_GFH_TYPE_BROM_CFG:
return "brom_cfg";
case MTK_GFH_TYPE_BROM_SEC_CFG:
return "brom_sec_cfg";
case MTK_GFH_TYPE_0x200:
return "type_0x200";
case MTK_GFH_TYPE_RSA_MAYBE:
return "rsa_maybe";
default:
return "unknown";
}
}
// --- GFH format parsing ---
RZ_IPI bool mtk_check_buffer(RZ_BORROW RZ_NONNULL RzBuffer *b) {
rz_return_val_if_fail(b, false);
ut64 buf_size = rz_buf_size(b);
if (buf_size < MTK_GFH_MIN_FILE_SIZE) {
return false;
}
// Reject md1img containers — they have their own plugin
ut8 first4[MD1IMG_MAGIC_SIZE];
if (rz_buf_read_at(b, 0, first4, MD1IMG_MAGIC_SIZE) == MD1IMG_MAGIC_SIZE &&
memcmp(first4, MD1IMG_MAGIC, MD1IMG_MAGIC_SIZE) == 0) {
return false;
}
ut64 gfh_off = mtk_find_gfh(b);
if (gfh_off == UT64_MAX) {
return false;
}
ut64 offset = gfh_off + MTK_GFH_COMMON_HDR_SIZE;
MtkGfhFileInfo fi;
if (!mtk_read_file_info(b, &offset, &fi)) {
return false;
}
if (fi.hdr_size < MTK_GFH_MIN_FILE_SIZE || gfh_off + fi.hdr_size > buf_size) {
return false;
}
if (fi.load_addr == 0) {
return false;
}
return true;
}
RZ_IPI RZ_OWN MtkObj *mtk_obj_new(RZ_BORROW RZ_NONNULL RzBuffer *b) {
rz_return_val_if_fail(b, NULL);
MtkObj *mtk = RZ_NEW0(MtkObj);
if (!mtk) {
return NULL;
}
mtk->extra_headers = rz_vector_new(sizeof(MtkGfhHeader), NULL, NULL);
if (!mtk->extra_headers) {
free(mtk);
return NULL;
}
ut64 gfh_off = mtk_find_gfh(b);
if (gfh_off == UT64_MAX) {
goto fail;
}
mtk->gfh_offset = gfh_off;
ut64 offset = gfh_off;
if (!mtk_read_common_hdr(b, &offset, &mtk->first_common)) {
goto fail;
}
if (!mtk_read_file_info(b, &offset, &mtk->file_info)) {
goto fail;
}
ut64 buf_size = rz_buf_size(b);
if (gfh_off + mtk->file_info.hdr_size > buf_size) {
RZ_LOG_ERROR("MTK: header size (0x%x) exceeds file size\n", mtk->file_info.hdr_size);
goto fail;
}
mtk->code_offset = gfh_off + mtk->file_info.hdr_size;
if (buf_size > mtk->code_offset) {
mtk->code_size = buf_size - mtk->code_offset;
}
// jump_offset is relative to the GFH start
ut32 code_offset_relative = mtk->file_info.hdr_size;
mtk->entry_vaddr = MTK_MODEM_BADDR + (mtk->file_info.jump_offset - code_offset_relative);
// Parse additional GFH headers between the first header and the code area
offset = gfh_off + mtk->first_common.size;
int count = 0;
while (offset + MTK_GFH_COMMON_HDR_SIZE <= mtk->code_offset && count < 100) {
ut64 hdr_start = offset;
MtkGfhCommonHdr extra_common = { 0 };
if (!mtk_read_common_hdr(b, &offset, &extra_common)) {
break;
}
if (!mtk_is_gfh_magic(extra_common.magic_version)) {
break;
}
if (extra_common.size < MTK_GFH_COMMON_HDR_SIZE) {
break;
}
MtkGfhHeader entry = {
.common = extra_common,
.file_offset = hdr_start,
};
rz_vector_push(mtk->extra_headers, &entry);
offset = hdr_start + extra_common.size;
count++;
}
return mtk;
fail:
rz_vector_free(mtk->extra_headers);
free(mtk);
return NULL;
}
RZ_IPI void mtk_obj_free(RZ_OWN RZ_NULLABLE MtkObj *mtk) {
if (!mtk) {
return;
}
rz_vector_free(mtk->extra_headers);
free(mtk);
}
RZ_IPI bool mtk_load_buffer(RZ_BORROW RZ_NONNULL RzBinFile *bf, RZ_BORROW RZ_NONNULL RzBinObject *obj, RZ_BORROW RZ_NONNULL RzBuffer *b, RZ_BORROW RZ_NULLABLE Sdb *sdb) {
rz_return_val_if_fail(bf && obj && b, false);
MtkObj *mtk = mtk_obj_new(b);
if (!mtk) {
return false;
}
obj->bin_obj = mtk;
return true;
}
RZ_IPI void mtk_destroy(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
if (!bf || !bf->o || !bf->o->bin_obj) {
return;
}
mtk_obj_free(bf->o->bin_obj);
}
RZ_IPI ut64 mtk_baddr(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
return MTK_MODEM_BADDR;
}
RZ_IPI RZ_OWN RzPVector /*<RzBinAddr *>*/ *mtk_entries(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
rz_return_val_if_fail(bf && bf->o && bf->o->bin_obj, NULL);
MtkObj *mtk = bf->o->bin_obj;
RzPVector *ret = rz_pvector_new(free);
if (!ret) {
return NULL;
}
RzBinAddr *entry = RZ_NEW0(RzBinAddr);
if (entry) {
entry->paddr = mtk->gfh_offset + mtk->file_info.jump_offset;
entry->vaddr = mtk->entry_vaddr;
rz_pvector_push(ret, entry);
}
return ret;
}
RZ_IPI bool mtk_append_maps(RZ_BORROW RZ_NONNULL MtkObj *mtk, ut64 paddr, RZ_BORROW RZ_NULLABLE const char *name, RZ_BORROW RZ_NONNULL RzPVector /*<RzBinMap *>*/ *ret) {
rz_return_val_if_fail(mtk && ret, false);
if (mtk->code_size == 0) {
return true;
}
// kseg0 map: main code at runtime virtual address
RzBinMap *kseg0 = RZ_NEW0(RzBinMap);
if (!kseg0) {
return false;
}
kseg0->name = name ? rz_str_dup(name) : rz_str_dup("code");
kseg0->vfile_name = name ? rz_str_dup(name) : NULL;
kseg0->paddr = paddr;
kseg0->psize = mtk->code_size;
kseg0->vaddr = MTK_MODEM_BADDR;
kseg0->vsize = mtk->code_size;
kseg0->perm = RZ_PERM_RX;
rz_pvector_push(ret, kseg0);
// kuseg map: same code at physical addresses (ERL=1 identity mapping).
// The boot code uses kuseg addresses before clearing ERL in Status.
RzBinMap *kuseg = RZ_NEW0(RzBinMap);
if (!kuseg) {
return false;
}
kuseg->name = name ? rz_str_newf("%s.kuseg", name) : rz_str_dup("code.kuseg");
kuseg->vfile_name = name ? rz_str_dup(name) : NULL;
kuseg->paddr = paddr;
kuseg->psize = mtk->code_size;
kuseg->vaddr = (ut64)mtk->file_info.load_addr + mtk->file_info.hdr_size;
kuseg->vsize = mtk->code_size;
kuseg->perm = RZ_PERM_RX;
rz_pvector_push(ret, kuseg);
return true;
}
RZ_IPI RZ_OWN RzPVector /*<RzBinMap *>*/ *mtk_maps(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
rz_return_val_if_fail(bf && bf->o && bf->o->bin_obj, NULL);
MtkObj *mtk = bf->o->bin_obj;
RzPVector *ret = rz_pvector_new((RzPVectorFree)rz_bin_map_free);
if (!ret) {
return NULL;
}
mtk_append_maps(mtk, mtk->code_offset, NULL, ret);
return ret;
}
RZ_IPI RZ_OWN RzPVector /*<RzBinSection *>*/ *mtk_sections(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
rz_return_val_if_fail(bf && bf->o && bf->o->bin_obj, NULL);
MtkObj *mtk = bf->o->bin_obj;
RzPVector *ret = rz_pvector_new((RzPVectorFree)rz_bin_section_free);
if (!ret) {
return NULL;
}
// Header section
RzBinSection *hdr_section = RZ_NEW0(RzBinSection);
if (hdr_section) {
hdr_section->name = rz_str_dup("header");
hdr_section->paddr = 0;
hdr_section->size = mtk->code_offset;
hdr_section->vsize = mtk->code_offset;
hdr_section->vaddr = 0;
hdr_section->perm = RZ_PERM_R;
rz_pvector_push(ret, hdr_section);
}
// Code section
if (mtk->code_size > 0) {
RzBinSection *code_section = RZ_NEW0(RzBinSection);
if (code_section) {
code_section->name = rz_str_dup("code");
code_section->paddr = mtk->code_offset;
code_section->size = mtk->code_size;
code_section->vsize = mtk->code_size;
code_section->vaddr = md1img_baddr(bf);
code_section->perm = RZ_PERM_RX;
rz_pvector_push(ret, code_section);
}
}
return ret;
}
RZ_IPI RZ_OWN RzBinInfo *mtk_info(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
RzBinInfo *info = RZ_NEW0(RzBinInfo);
if (!info) {
return NULL;
}
info->file = bf->file ? rz_str_dup(bf->file) : NULL;
info->type = rz_str_dup("MediaTek GFH");
info->machine = rz_str_dup("MediaTek Modem");
info->arch = rz_str_dup("mips");
info->cpu = rz_str_dup("nanomips");
info->rclass = rz_str_dup("firmware");
info->subsystem = rz_str_dup("modem");
info->has_va = true;
info->bits = 32;
info->big_endian = false;
return info;
}
RZ_IPI RZ_OWN RzStructuredData *mtk_structure(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
rz_return_val_if_fail(bf && bf->o && bf->o->bin_obj, NULL);
MtkObj *mtk = bf->o->bin_obj;
RzStructuredData *root = rz_structured_data_new_map();
if (!root) {
return NULL;
}
// File info header
RzStructuredData *fi = rz_structured_data_map_add_map(root, "gfh_file_info");
if (fi) {
rz_structured_data_map_add_unsigned(fi, "magic_version", mtk->first_common.magic_version, true);
rz_structured_data_map_add_unsigned(fi, "header_block_size", mtk->first_common.size, false);
rz_structured_data_map_add_string(fi, "header_type", mtk_gfh_type_str(mtk->first_common.type));
rz_structured_data_map_add_string(fi, "name", mtk->file_info.name);
rz_structured_data_map_add_unsigned(fi, "file_type", mtk->file_info.file_type, true);
rz_structured_data_map_add_unsigned(fi, "flash_type", mtk->file_info.flash_type, true);
rz_structured_data_map_add_unsigned(fi, "sig_type", mtk->file_info.sig_type, true);
rz_structured_data_map_add_unsigned(fi, "load_addr", mtk->file_info.load_addr, true);
rz_structured_data_map_add_unsigned(fi, "total_size", mtk->file_info.total_size, true);
rz_structured_data_map_add_unsigned(fi, "max_size", mtk->file_info.max_size, true);
rz_structured_data_map_add_unsigned(fi, "hdr_size", mtk->file_info.hdr_size, true);
rz_structured_data_map_add_unsigned(fi, "sig_size", mtk->file_info.sig_size, true);
rz_structured_data_map_add_unsigned(fi, "jump_offset", mtk->file_info.jump_offset, true);
rz_structured_data_map_add_unsigned(fi, "entry_point", mtk->entry_vaddr, true);
}
// Extra GFH headers
if (rz_vector_len(mtk->extra_headers) > 0) {
RzStructuredData *hdrs = rz_structured_data_map_add_array(root, "gfh_headers");
if (hdrs) {
MtkGfhHeader *extra;
rz_vector_foreach (mtk->extra_headers, extra) {
RzStructuredData *h = rz_structured_data_array_add_map(hdrs);
if (!h) {
continue;
}
rz_structured_data_map_add_unsigned(h, "file_offset", extra->file_offset, true);
rz_structured_data_map_add_string(h, "type", mtk_gfh_type_str(extra->common.type));
rz_structured_data_map_add_unsigned(h, "type_id", extra->common.type, true);
rz_structured_data_map_add_unsigned(h, "size", extra->common.size, false);
}
}
}
// Derived values
rz_structured_data_map_add_unsigned(root, "code_offset", mtk->code_offset, true);
rz_structured_data_map_add_unsigned(root, "code_size", mtk->code_size, true);
return root;
}

110
librz/bin/format/mtk/mtk.h Normal file
View file

@ -0,0 +1,110 @@
// SPDX-FileCopyrightText: 2025 mrsmith
// SPDX-License-Identifier: LGPL-3.0-only
#ifndef MTK_H
#define MTK_H
#include <rz_types.h>
#include <rz_util.h>
#include <rz_lib.h>
#include <rz_bin.h>
#define MTK_GFH_MAGIC_MASK 0x00FFFFFF
/* "MMM" in lower 3 bytes */
#define MTK_GFH_MAGIC 0x004D4D4D
#define MTK_GFH_COMMON_HDR_SIZE 8
#define MTK_GFH_FILE_INFO_BODY_SIZE 48
#define MTK_GFH_MIN_FILE_SIZE (MTK_GFH_COMMON_HDR_SIZE + MTK_GFH_FILE_INFO_BODY_SIZE)
#define MTK_GFH_FILE_INFO_NAME_SIZE 12
/* Runtime virtual base address for modem code */
#define MTK_MODEM_BADDR 0x90000000ULL
typedef enum {
MTK_GFH_TYPE_FILE_INFO = 0x0000,
MTK_GFH_TYPE_BL_INFO = 0x0001,
MTK_GFH_TYPE_ANTI_CLONE = 0x0002,
MTK_GFH_TYPE_BL_SEC_KEY = 0x0003,
MTK_GFH_TYPE_BROM_CFG = 0x0007,
MTK_GFH_TYPE_BROM_SEC_CFG = 0x0008,
MTK_GFH_TYPE_0x200 = 0x0200,
MTK_GFH_TYPE_RSA_MAYBE = 0x0202,
} MtkGfhType;
/**
* \brief GFH Common Header (8 bytes, present in every GFH block).
*
* MediaTek Generic File Header format. The magic_version field encodes
* "MMM" (0x4D4D4D) in the lower 3 bytes and a version number in the
* upper byte.
*
* Reference: https://github.com/u-boot/u-boot/blob/master/tools/mtk_image.h
*/
typedef struct mtk_gfh_common_hdr {
ut32 magic_version; ///< Lower 3 bytes = "MMM" (0x4D4D4D), upper byte = version
ut16 size; ///< Total size of this header block (common + body)
MtkGfhType type; ///< Header type; on-disk wire size is 2 bytes
} MtkGfhCommonHdr;
/**
* \brief GFH File Info body (48 bytes, follows common header when type=FILE_INFO).
*
* Contains the primary metadata for a MediaTek firmware image: load address,
* entry point offset, header area size, and signature information.
*
* Reference: https://github.com/nccgroup/mtk_bp/blob/main/mtk_structs/mtk_img.ksy
*/
typedef struct mtk_gfh_file_info {
char name[MTK_GFH_FILE_INFO_NAME_SIZE]; ///< Null-terminated identifier
ut32 unused;
ut16 file_type;
ut8 flash_type;
ut8 sig_type;
ut32 load_addr; ///< Base load address in memory
ut32 total_size; ///< Total image size
ut32 max_size;
ut32 hdr_size; ///< Total header area size; code starts at this file offset
ut32 sig_size; ///< Signature size in bytes
ut32 jump_offset; ///< File offset of entry point (from start of image)
ut32 processed;
} MtkGfhFileInfo;
/**
* \brief A parsed additional GFH header (stored generically).
*/
typedef struct mtk_gfh_header {
MtkGfhCommonHdr common; ///< Common header fields
ut64 file_offset; ///< File offset where this header starts
} MtkGfhHeader;
/* Maximum offset to scan for GFH magic when it's not at file start */
#define MTK_GFH_SCAN_LIMIT 0x1000
/**
* \brief Top-level parsed object stored as bin_obj.
*/
typedef struct mtk_obj {
MtkGfhCommonHdr first_common; ///< Common header of the first (file_info) block
MtkGfhFileInfo file_info; ///< Parsed file_info body
RzVector /*<MtkGfhHeader>*/ *extra_headers; ///< Additional GFH headers after file_info
ut64 gfh_offset; ///< File offset where GFH starts (0 if at beginning)
ut32 code_offset; ///< File offset where code starts (= gfh_offset + file_info.hdr_size)
ut32 code_size; ///< Size of code section
ut32 entry_vaddr; ///< = MTK_MODEM_BADDR + (jump_offset - code_offset_relative)
} MtkObj;
RZ_IPI RZ_OWN MtkObj *mtk_obj_new(RZ_BORROW RZ_NONNULL RzBuffer *b);
RZ_IPI void mtk_obj_free(RZ_OWN RZ_NULLABLE MtkObj *mtk);
RZ_IPI RZ_BORROW const char *mtk_gfh_type_str(MtkGfhType type);
RZ_IPI bool mtk_check_buffer(RZ_BORROW RZ_NONNULL RzBuffer *b);
RZ_IPI bool mtk_load_buffer(RZ_BORROW RZ_NONNULL RzBinFile *bf, RZ_BORROW RZ_NONNULL RzBinObject *obj, RZ_BORROW RZ_NONNULL RzBuffer *b, RZ_BORROW RZ_NULLABLE Sdb *sdb);
RZ_IPI void mtk_destroy(RZ_BORROW RZ_NONNULL RzBinFile *bf);
RZ_IPI RZ_OWN RzBinInfo *mtk_info(RZ_BORROW RZ_NONNULL RzBinFile *bf);
RZ_IPI ut64 mtk_baddr(RZ_BORROW RZ_NONNULL RzBinFile *bf);
RZ_IPI RZ_OWN RzPVector /*<RzBinAddr *>*/ *mtk_entries(RZ_BORROW RZ_NONNULL RzBinFile *bf);
RZ_IPI RZ_OWN RzPVector /*<RzBinSection *>*/ *mtk_sections(RZ_BORROW RZ_NONNULL RzBinFile *bf);
RZ_IPI bool mtk_append_maps(RZ_BORROW RZ_NONNULL MtkObj *mtk, ut64 paddr, RZ_BORROW RZ_NULLABLE const char *name, RZ_BORROW RZ_NONNULL RzPVector /*<RzBinMap *>*/ *ret);
RZ_IPI RZ_OWN RzPVector /*<RzBinMap *>*/ *mtk_maps(RZ_BORROW RZ_NONNULL RzBinFile *bf);
RZ_IPI RZ_OWN RzStructuredData *mtk_structure(RZ_BORROW RZ_NONNULL RzBinFile *bf);
#endif

View file

@ -24,8 +24,10 @@ bin_plugins_list = [
'mach0',
'mach064',
'mbn',
'md1img',
'mdmp',
'mdt',
'mtk',
'menuet',
'mz',
'ne',
@ -130,7 +132,9 @@ rz_bin_sources = [
'p/bin_mach064.c',
'p/bin_mbn.c',
'p/bin_mdmp.c',
'p/bin_md1img.c',
'p/bin_mdt.c',
'p/bin_mtk.c',
'p/bin_menuet.c',
'p/bin_mz.c',
'p/bin_ne.c',
@ -242,6 +246,8 @@ rz_bin_sources = [
'format/mdmp/mdmp_pe.c',
'format/mdmp/mdmp_pe64.c',
'format/mdt/mdt.c',
'format/mtk/md1img.c',
'format/mtk/mtk.c',
'format/le/le.c',
'format/luac/luac_common.c',
'format/luac/luac_bin.c',

31
librz/bin/p/bin_md1img.c Normal file
View file

@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2025 godcodehunter
// SPDX-License-Identifier: LGPL-3.0-only
#include "../format/mtk/md1img.h"
RzBinPlugin rz_bin_plugin_md1img = {
.name = "md1img",
.desc = "MediaTek md1img firmware container",
.license = "LGPL3",
.author = "godcodehunter",
.check_buffer = &md1img_check_buffer,
.load_buffer = &md1img_load_buffer,
.destroy = &md1img_destroy,
.baddr = &md1img_baddr,
.entries = &md1img_entries,
.virtual_files = &md1img_virtual_files,
.maps = &md1img_maps,
.sections = &md1img_sections,
.symbols = &md1img_symbols,
.strings = &md1img_strings,
.info = &md1img_info,
.bin_structure = &md1img_structure,
};
#ifndef RZ_PLUGIN_INCORE
RZ_API RzLibStruct rizin_plugin = {
.type = RZ_LIB_TYPE_BIN,
.data = &rz_bin_plugin_md1img,
.version = RZ_VERSION
};
#endif

28
librz/bin/p/bin_mtk.c Normal file
View file

@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2025 godcodehunter
// SPDX-License-Identifier: LGPL-3.0-only
#include "../format/mtk/mtk.h"
RzBinPlugin rz_bin_plugin_mtk = {
.name = "mtk",
.desc = "MediaTek GFH firmware image (md1rom)",
.license = "LGPL3",
.author = "godcodehunter",
.check_buffer = &mtk_check_buffer,
.load_buffer = &mtk_load_buffer,
.destroy = &mtk_destroy,
.baddr = &mtk_baddr,
.entries = &mtk_entries,
.sections = &mtk_sections,
.maps = &mtk_maps,
.info = &mtk_info,
.bin_structure = &mtk_structure,
};
#ifndef RZ_PLUGIN_INCORE
RZ_API RzLibStruct rizin_plugin = {
.type = RZ_LIB_TYPE_BIN,
.data = &rz_bin_plugin_mtk,
.version = RZ_VERSION
};
#endif

View file

@ -402,6 +402,7 @@ RZ_API bool rz_inflatew_buf(RZ_NONNULL RzBuffer *src, RZ_NONNULL RzBuffer *dst,
RZ_API bool rz_inflate_buf(RZ_NONNULL RzBuffer *src, RZ_NONNULL RzBuffer *dst, ut64 block_size, ut8 *src_consumed);
RZ_API bool rz_lzma_dec_buf(RZ_NONNULL RzBuffer *src, RZ_NONNULL RzBuffer *dst, ut64 block_size, ut8 *src_consumed);
RZ_API bool rz_lzma_enc_buf(RZ_NONNULL RzBuffer *src, RZ_NONNULL RzBuffer *dst, ut64 block_size, ut8 *src_consumed);
RZ_API bool rz_lzma_alone_dec_buf(RZ_NONNULL RzBuffer *src, RZ_NONNULL RzBuffer *dst, ut64 block_size);
#ifdef __cplusplus
}

View file

@ -480,3 +480,91 @@ RZ_API bool rz_lzma_dec_buf(RZ_NONNULL RzBuffer *src, RZ_NONNULL RzBuffer *dst,
RZ_API bool rz_lzma_enc_buf(RZ_NONNULL RzBuffer *src, RZ_NONNULL RzBuffer *dst, ut64 block_size, ut8 *src_consumed) {
return lzma_action_buf(src, dst, block_size, src_consumed, true);
}
#if HAVE_LZMA
static bool lzma_alone_action_buf(RZ_NONNULL RzBuffer *src, RZ_NONNULL RzBuffer *dst, ut64 block_size) {
bool res = true;
lzma_stream strm = LZMA_STREAM_INIT;
const ut64 memusage_limit = 0x8000000; // 128 MB
lzma_ret ret = lzma_alone_decoder(&strm, memusage_limit);
if (ret != LZMA_OK) {
return false;
}
lzma_action action = LZMA_RUN;
ut8 *inbuf = RZ_NEWS(ut8, block_size);
ut8 *outbuf = RZ_NEWS(ut8, block_size);
if (!inbuf || !outbuf) {
free(inbuf);
free(outbuf);
lzma_end(&strm);
return false;
}
ut64 src_cursor = 0;
strm.next_in = NULL;
strm.avail_in = 0;
strm.next_out = outbuf;
strm.avail_out = block_size;
while (true) {
if (strm.avail_in == 0) {
strm.next_in = inbuf;
st64 src_readlen = rz_buf_read_at(src, src_cursor, inbuf, block_size);
if (src_readlen < 0) {
res = false;
goto exit;
}
if (src_readlen == 0) {
action = LZMA_FINISH;
}
strm.avail_in = src_readlen;
src_cursor += src_readlen;
}
ret = lzma_code(&strm, action);
if (strm.avail_out == 0 || ret == LZMA_STREAM_END) {
size_t write_size = block_size - strm.avail_out;
if (rz_buf_write(dst, outbuf, write_size) != write_size) {
res = false;
goto exit;
}
strm.next_out = outbuf;
strm.avail_out = block_size;
}
if (ret == LZMA_STREAM_END) {
break;
}
if (ret != LZMA_OK) {
res = false;
goto exit;
}
}
exit:
free(inbuf);
free(outbuf);
lzma_end(&strm);
return res;
}
#else
static bool lzma_alone_action_buf(RZ_NONNULL RzBuffer *src, RZ_NONNULL RzBuffer *dst, ut64 block_size) {
return false;
}
#endif
/**
* \brief Decompress the \p src buffer with LZMA alone (raw LZMA1) algorithm and put the decompressed data in \p dst
*
* Unlike rz_lzma_dec_buf() which handles .xz streams, this function handles
* the legacy LZMA alone format (also known as LZMA1 or .lzma), identifiable
* by the 0x5d first byte followed by a 13-byte header.
*
* \param src Where to read the compressed data from
* \param dst Where to write the decompressed data to
* \param block_size Decompression block size for I/O operations
* \return true if decompression was successful, false otherwise
*/
RZ_API bool rz_lzma_alone_dec_buf(RZ_NONNULL RzBuffer *src, RZ_NONNULL RzBuffer *dst, ut64 block_size) {
return lzma_alone_action_buf(src, dst, block_size);
}

File diff suppressed because one or more lines are too long

227
test/db/formats/mtk Normal file
View file

@ -0,0 +1,227 @@
NAME=md1rom synthetic GFH firmware image
FILE=bins/mtk/md1rom
CMDS=<<EOF
iI
echo ---
iH
echo ---
iS
echo ---
ie
echo ---
s 0x90000000
pi 4
EOF
EXPECT=<<EOF
arch mips
cpu nanomips
features N/A
baddr 0x90000000
binsz 0x00000160
bintype firmware
bits 32
class N/A
compiler N/A
dbg_file N/A
endian LE
hdr.csum N/A
guid N/A
intrp N/A
laddr 0x00000000
lang N/A
machine MediaTek Modem
maxopsz 6
minopsz 2
os N/A
cc N/A
pcalign 2
rpath N/A
subsys modem
stripped false
havecode true
va true
static true
linenum false
lsyms false
canary false
pie false
relrocs false
nx false
---
gfh_file_info:
magic_version: 0x14d4d4d
header_block_size: 56
header_type: "file_info"
name: "MD1_ROM"
file_type: 0x0
flash_type: 0x0
sig_type: 0x0
load_addr: 0x90000000
total_size: 0x160
max_size: 0x160
hdr_size: 0x60
sig_size: 0x0
jump_offset: 0x60
entry_point: 0x90000000
gfh_headers:
- file_offset: 0x38
type: "bl_info"
type_id: 0x1
size: 20
- file_offset: 0x4c
type: "brom_cfg"
type_id: 0x7
size: 16
code_offset: 0x60
code_size: 0x100
---
paddr size vaddr vsize align perm name type flags
---------------------------------------------------------------
0x00000000 0x60 0x00000000 0x60 0x0 -r-- header
0x00000060 0x100 0x90000000 0x100 0x0 -r-x code
---
vaddr paddr hvaddr haddr type
----------------------------------------------------
0x90000000 0x00000060 ---------- ---------- program
---
nop
nop
nop
nop
EOF
RUN
NAME=md1img synthetic container with md1rom section
FILE=bins/mtk/md1img
CMDS=<<EOF
iI
echo ---
iH
echo ---
iS
echo ---
ie
echo ---
s 0x90000000
pi 4
EOF
EXPECT=<<EOF
arch mips
cpu nanomips
features N/A
baddr 0x90000000
binsz 0x00000360
bintype firmware
bits 32
class N/A
compiler N/A
dbg_file N/A
endian LE
hdr.csum N/A
guid N/A
intrp N/A
laddr 0x00000000
lang N/A
machine MediaTek Modem
maxopsz 6
minopsz 2
os N/A
cc N/A
pcalign 2
rpath N/A
subsys modem
stripped false
havecode true
va true
static true
linenum false
lsyms false
canary false
pie false
relrocs false
nx false
---
num_sections: 1
sections:
- name: "md1rom"
data_size: 0x160
data_offset: 0x200
maddr: 0x0
mode: 0x0
hdr_size: 0x200
hdr_version: 0x0
img_type: 0x0
img_list_end: 0x0
align_size: 0x0
gfh_file_info:
magic_version: 0x14d4d4d
header_block_size: 56
header_type: "file_info"
name: "MD1_ROM"
file_type: 0x0
flash_type: 0x0
sig_type: 0x0
load_addr: 0x90000000
total_size: 0x160
max_size: 0x160
hdr_size: 0x60
sig_size: 0x0
jump_offset: 0x60
entry_point: 0x90000000
gfh_headers:
- file_offset: 0x38
type: "bl_info"
type_id: 0x1
size: 20
- file_offset: 0x4c
type: "brom_cfg"
type_id: 0x7
size: 16
---
paddr size vaddr vsize align perm name type flags
--------------------------------------------------------------------
0x00000060 0x100 0x90000000 0x100 0x0 -r-x md1rom.code
---
vaddr paddr hvaddr haddr type
----------------------------------------------------
0x90000000 0x00000060 ---------- ---------- program
---
nop
nop
nop
nop
EOF
RUN
NAME=md1img sections (JSON)
FILE=bins/mtk/md1img
CMDS=<<EOF
iSj
EOF
EXPECT=<<EOF
[{"name":"md1rom.code","size":256,"vsize":256,"perm":"-r-x","paddr":96,"vaddr":2415919104}]
EOF
RUN
NAME=md1img empty exports do not crash
FILE=bins/mtk/md1img
CMDS=<<EOF
iE
EOF
EXPECT=<<EOF
nth paddr vaddr bind type size lib name
----------------------------------------
EOF
RUN
NAME=md1img exposes md1rom as a virtual file
FILE=bins/mtk/md1img
CMDS=<<EOF
ol~vfile
EOF
EXPECT=<<EOF
4 - r-x 0x00000160 vfile://0/md1rom
EOF
RUN

View file

@ -56,9 +56,161 @@ bool test_rz_lzma_enc(void) {
mu_end;
}
// LZMA-alone (legacy LZMA1, .lzma) fixtures generated with: xz --format=lzma -c
// They share inflated payloads with the .xz cases above so the difference is
// purely the container header (0x5d... instead of the .xz magic).
struct {
const char *inflated;
const unsigned char deflated[160];
size_t deflated_length;
} test_cases_alone[] = {
{ "1234567890abcdefghijklmnopqrstuvwxyz\n",
{ 0x5d, 0x00, 0x00, 0x80, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0x00, 0x18, 0x8c, 0x82, 0xb6, 0xc4, 0x11, 0x34, 0x5c, 0x4e, 0xe1,
0xd6, 0x5e, 0xd1, 0x46, 0xf2, 0x6e, 0xf8, 0x5b, 0xdf, 0x60, 0xc9, 0x34,
0x08, 0x05, 0x5f, 0xa3, 0xc3, 0x5d, 0x4c, 0xe6, 0xcd, 0x2d, 0x81, 0x37,
0xfe, 0x2c, 0x76, 0xd3, 0x09, 0xe7, 0xff, 0xff, 0xc9, 0x9d, 0x00, 0x00 },
60 },
{ "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n",
{ 0x5d, 0x00, 0x00, 0x80, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0x00, 0x26, 0x1b, 0xca, 0x46, 0x67, 0x5a, 0xf2, 0x77, 0xb8, 0x7d,
0x86, 0xd8, 0x41, 0xdb, 0x05, 0x35, 0xcd, 0x83, 0xa5, 0x7c, 0x12, 0xa5,
0x05, 0xdb, 0x90, 0xbd, 0x2f, 0x14, 0xd3, 0x71, 0x72, 0x96, 0xa8, 0x8a,
0x7d, 0x84, 0x56, 0x71, 0x8d, 0x6a, 0x22, 0x98, 0xab, 0x9e, 0x3d, 0xc3,
0x55, 0xef, 0xcc, 0xa5, 0xc3, 0xdd, 0x5b, 0x8e, 0xbf, 0x03, 0x81, 0x21,
0x40, 0xd6, 0x26, 0x91, 0x02, 0x45, 0x4f, 0x92, 0xa1, 0x78, 0xbb, 0x8a,
0x00, 0xaf, 0x90, 0x2a, 0x26, 0x92, 0x02, 0x23, 0xe5, 0x5c, 0xb3, 0x2d,
0xe3, 0xe8, 0x5c, 0x2c, 0xfb, 0x32, 0x21, 0xc6, 0x6f, 0x6a, 0x37, 0xb1,
0x66, 0x20, 0xcd, 0xb7, 0x52, 0x7d, 0x66, 0xa4, 0x21, 0x08, 0xd1, 0x44,
0x0f, 0x7f, 0xad, 0x0d, 0x64, 0xff, 0xed, 0x34, 0x38, 0x00 },
130 },
};
bool test_rz_lzma_alone_dec(void) {
// Scenario 1: happy path. Two pre-compressed fixtures must round-trip
// byte-for-byte against their original inflated payloads. Uses a default
// block size large enough to consume the whole stream in one inner pass.
for (size_t i = 0; i < RZ_ARRAY_SIZE(test_cases_alone); i++) {
size_t inflated_len = strlen(test_cases_alone[i].inflated);
RzBuffer *src = rz_buf_new_with_bytes(test_cases_alone[i].deflated, test_cases_alone[i].deflated_length);
RzBuffer *dst = rz_buf_new_empty(inflated_len);
mu_assert_true(rz_lzma_alone_dec_buf(src, dst, 1 << 13), "rz_lzma_alone_dec_buf failed on valid fixture");
mu_assert_eq(rz_buf_size(dst), inflated_len, "decompressed size mismatch");
char *out = calloc(inflated_len + 1, 1);
rz_buf_read_at(dst, 0, (ut8 *)out, inflated_len);
mu_assert_streq(out, test_cases_alone[i].inflated, "decompressed payload mismatch");
free(out);
rz_buf_free(src);
rz_buf_free(dst);
}
// Scenario 2: tiny block_size forces the inner while-loop to iterate many
// times (re-fill input, re-flush output). Exercises the multi-pass code
// path that the large block_size in scenario 1 hides.
{
size_t inflated_len = strlen(test_cases_alone[1].inflated);
RzBuffer *src = rz_buf_new_with_bytes(test_cases_alone[1].deflated, test_cases_alone[1].deflated_length);
RzBuffer *dst = rz_buf_new_empty(inflated_len);
mu_assert_true(rz_lzma_alone_dec_buf(src, dst, 16), "rz_lzma_alone_dec_buf failed with small block_size");
char *out = calloc(inflated_len + 1, 1);
rz_buf_read_at(dst, 0, (ut8 *)out, inflated_len);
mu_assert_streq(out, test_cases_alone[1].inflated, "small-block decompression does not match original");
free(out);
rz_buf_free(src);
rz_buf_free(dst);
}
// Scenario 3: random garbage bytes are not a valid LZMA1 stream. The
// decoder must fail cleanly (return false) rather than crash or produce
// uninitialized output.
{
const ut8 garbage[] = {
0xDE, 0xAD, 0xBE, 0xEF, 0x42, 0x42, 0x42, 0x42,
0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF
};
RzBuffer *src = rz_buf_new_with_bytes(garbage, sizeof(garbage));
RzBuffer *dst = rz_buf_new_empty(0);
mu_assert_false(rz_lzma_alone_dec_buf(src, dst, 1 << 13), "garbage input must be rejected");
rz_buf_free(src);
rz_buf_free(dst);
}
// Scenario 4: empty input buffer triggers the "src_readlen == 0" branch
// immediately, which switches the action to LZMA_FINISH on an unprimed
// stream. Decoder must fail without UB or crash.
{
RzBuffer *src = rz_buf_new_empty(0);
RzBuffer *dst = rz_buf_new_empty(0);
mu_assert_false(rz_lzma_alone_dec_buf(src, dst, 1 << 13), "empty input must be rejected");
rz_buf_free(src);
rz_buf_free(dst);
}
mu_end;
}
// --- Fault injection for lzma_alone_action_buf error-handling branches ---
// A buffer back-end whose every operation reports failure. Used as the
// source to trigger the "src_readlen < 0" branch on the first read attempt.
static st64 failing_read(RZ_BORROW RzBuffer *b, RZ_OUT ut8 *buf, ut64 len) {
(void)b;
(void)buf;
(void)len;
return -1;
}
static const RzBufferMethods failing_src_methods = {
.read = failing_read,
};
// A buffer back-end that accepts no writes. Used as the destination to
// trigger the "rz_buf_write(dst, ...) != write_size" branch after the
// decoder produces its first decompressed chunk.
static st64 failing_write(RzBuffer *b, const ut8 *buf, ut64 len) {
(void)b;
(void)buf;
(void)len;
return -1;
}
static const RzBufferMethods failing_dst_methods = {
.write = failing_write,
};
bool test_rz_lzma_alone_dec_error_paths(void) {
// Branch: `if (src_readlen < 0)` — the first rz_buf_read_at on the
// source returns -1, so the decoder bails out before processing anything.
{
RzBuffer *src = rz_buf_new_with_methods(&failing_src_methods, NULL, RZ_BUFFER_CUSTOM);
RzBuffer *dst = rz_buf_new_empty(0);
mu_assert_notnull(src, "custom failing-src buffer creation failed");
mu_assert_false(rz_lzma_alone_dec_buf(src, dst, 1 << 13),
"src read returning -1 must abort decompression");
rz_buf_free(src);
rz_buf_free(dst);
}
// Branch: `if (rz_buf_write(dst, outbuf, write_size) != write_size)` —
// a valid LZMA-alone stream feeds the decoder, the decoder emits
// decompressed bytes, but the destination buffer's write method always
// returns -1, so the loop must bail out.
{
RzBuffer *src = rz_buf_new_with_bytes(test_cases_alone[0].deflated, test_cases_alone[0].deflated_length);
RzBuffer *dst = rz_buf_new_with_methods(&failing_dst_methods, NULL, RZ_BUFFER_CUSTOM);
mu_assert_notnull(dst, "custom failing-dst buffer creation failed");
mu_assert_false(rz_lzma_alone_dec_buf(src, dst, 1 << 13),
"dst write failure must abort decompression");
rz_buf_free(src);
rz_buf_free(dst);
}
mu_end;
}
int all_tests() {
mu_run_test(test_rz_lzma_dec);
mu_run_test(test_rz_lzma_enc);
mu_run_test(test_rz_lzma_alone_dec);
mu_run_test(test_rz_lzma_alone_dec_error_paths);
return tests_passed != tests_run;
}