From 2b956d5de5c49bcb97d333a3a3ca20c81b6e5bd6 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 20 May 2026 20:31:11 +0000 Subject: [PATCH 01/11] Harden LEB128 decoding against malformed Mach-O input Add an explicit end-of-buffer bound to readUnsignedLeb128/readSignedLeb128 so truncated or crafted dyld info / function-starts streams can no longer read past the mapped file. Thread the bound through every caller, replace release-stripped asserts on segment indices with runtime checks, and bound the bind-opcode symbol-name scan with memchr instead of strlen. Also extend crash-regression with truncated copies of the sample binaries so the LEB128 and trie-walk bounds checks are exercised against real LC_DYLD_INFO / LC_FUNCTION_STARTS payloads, and add the missing noexcept on the non-Apple NodeException::what() override so the parser builds under modern GCC/Clang. https://claude.ai/code/session_013kBiVXftgoEsyGVyrvfGok --- src/libmoex/node/Node.h | 2 +- src/libmoex/node/Util.cpp | 28 ++++++-- src/libmoex/node/Util.h | 7 +- .../node/loadcmd/LoadCommand_DYLD_INFO.cpp | 67 +++++++++++-------- .../loadcmd/LoadCommand_LINKEDIT_DATA.cpp | 6 +- .../views/DynamicLoaderInfoViewNode.cpp | 12 ++-- tests/regression/run_crash_regression.sh | 14 ++++ 7 files changed, 90 insertions(+), 46 deletions(-) diff --git a/src/libmoex/node/Node.h b/src/libmoex/node/Node.h index 6e178e1..60516d7 100644 --- a/src/libmoex/node/Node.h +++ b/src/libmoex/node/Node.h @@ -21,7 +21,7 @@ class NodeException : public std::exception{ #ifdef __APPLE__ const char* what() const _NOEXCEPT override { return error_.c_str();} #else - const char* what() const override { return error_.c_str();} + const char* what() const noexcept override { return error_.c_str();} #endif }; diff --git a/src/libmoex/node/Util.cpp b/src/libmoex/node/Util.cpp index 878951f..356153a 100644 --- a/src/libmoex/node/Util.cpp +++ b/src/libmoex/node/Util.cpp @@ -421,20 +421,27 @@ namespace util { } // return next offset - const char * readUnsignedLeb128(const char *cur_offset,uint64_t & data,uint32_t & occupy_size) { + const char * readUnsignedLeb128(const char *cur_offset,const char *end,uint64_t & data,uint32_t & occupy_size) { const uint8_t* p = (const uint8_t*)cur_offset; + const uint8_t* pend = (const uint8_t*)end; uint64_t result = 0; int bit = 0; do { + if (p >= pend){ + // truncated / out of bounds + data = 0; + occupy_size = 0; + return nullptr; + } uint64_t slice = *p & 0x7f; if (bit >= 64 || slice << bit >> bit != slice){ // error data = 0; occupy_size = 0; - return 0; + return nullptr; } else { result |= (slice << bit); bit += 7; @@ -443,30 +450,37 @@ namespace util { while (*p++ & 0x80); data = result; - occupy_size = p - (const uint8_t*)cur_offset; + occupy_size = (uint32_t)(p - (const uint8_t*)cur_offset); return (const char *)p; } - const char * readSignedLeb128(const char *cur_offset,int64_t & data,uint32_t & occupy_size){ + const char * readSignedLeb128(const char *cur_offset,const char *end,int64_t & data,uint32_t & occupy_size){ const uint8_t* p = (const uint8_t*)cur_offset; + const uint8_t* pend = (const uint8_t*)end; int64_t result = 0; int bit = 0; uint8_t byte=0; do { + if (p >= pend || bit >= 64){ + // truncated / out of bounds + data = 0; + occupy_size = 0; + return nullptr; + } byte = *p++; - result |= ((byte & 0x7f) << bit); + result |= ((int64_t)(byte & 0x7f) << bit); bit += 7; } while (byte & 0x80); // sign extend negative numbers - if ( (byte & 0x40) != 0 ) + if ( (byte & 0x40) != 0 && bit < 64 ) { result |= (-1LL) << bit; } data = result; - occupy_size = p - (const uint8_t*)cur_offset; + occupy_size = (uint32_t)(p - (const uint8_t*)cur_offset); return (const char *)p; } diff --git a/src/libmoex/node/Util.h b/src/libmoex/node/Util.h index 6698c07..02e6241 100644 --- a/src/libmoex/node/Util.h +++ b/src/libmoex/node/Util.h @@ -90,8 +90,11 @@ std::vector> ParseProts(qv_vm_prot_t prot); std::string FormatTimeStamp(uint32_t timestamp); std::string FormatVersion(uint32_t ver); -const char * readUnsignedLeb128(const char *cur_offset,uint64_t & data,uint32_t & occupy_size); -const char * readSignedLeb128(const char *cur_offset,int64_t & data,uint32_t & occupy_size); +// Decode (un)signed LEB128. `end` is one-past-the-last readable byte; if the +// encoding would read at/past it (truncated/malformed input) the functions +// return nullptr with data=0 and occupy_size=0 instead of reading out of bounds. +const char * readUnsignedLeb128(const char *cur_offset,const char *end,uint64_t & data,uint32_t & occupy_size); +const char * readSignedLeb128(const char *cur_offset,const char *end,int64_t & data,uint32_t & occupy_size); std::vector ParseStringLiteral(char * offset,uint32_t size); diff --git a/src/libmoex/node/loadcmd/LoadCommand_DYLD_INFO.cpp b/src/libmoex/node/loadcmd/LoadCommand_DYLD_INFO.cpp index a3d7c8d..39cba24 100644 --- a/src/libmoex/node/loadcmd/LoadCommand_DYLD_INFO.cpp +++ b/src/libmoex/node/loadcmd/LoadCommand_DYLD_INFO.cpp @@ -99,8 +99,9 @@ void LoadCommand_DYLD_INFO::ForEachRebaseOpcode(std::functionheader_start() + cmd()->rebase_off; uint32_t size = cmd()->rebase_size; + char * end = begin + size; char * cur = begin; - while(cur < begin + size && !done){ + while(cur < end && !done){ // read and move next ctx.pbyte = (uint8_t*)cur; cur += sizeof(uint8_t); @@ -132,15 +133,17 @@ void LoadCommand_DYLD_INFO::ForEachRebaseOpcode(std::functionsegment_index = ctx.immediate; code->offset_addr = (uint8_t*)cur; - moex::util::readUnsignedLeb128(cur,code->offset,code->offset_size); + moex::util::readUnsignedLeb128(cur,end,code->offset,code->offset_size); cur+=code->offset_size; if(header()->Is64()){ - assert(code->segment_index < header()->GetSegments64().size()); - ctx.address = header()->GetSegments64().at(code->segment_index)->cmd()->vmaddr; + auto &segs = header()->GetSegments64(); + if(code->segment_index < segs.size()) + ctx.address = segs.at(code->segment_index)->cmd()->vmaddr; }else{ - assert(code->segment_index < header()->GetSegments().size()); - ctx.address = header()->GetSegments().at(code->segment_index)->cmd()->vmaddr + code->offset; + auto &segs = header()->GetSegments(); + if(code->segment_index < segs.size()) + ctx.address = segs.at(code->segment_index)->cmd()->vmaddr + code->offset; } callback(&ctx,code.get()); @@ -150,7 +153,7 @@ void LoadCommand_DYLD_INFO::ForEachRebaseOpcode(std::function(); code->offset_addr = (uint8_t*)cur; - moex::util::readUnsignedLeb128(cur,code->offset,code->offset_size); + moex::util::readUnsignedLeb128(cur,end,code->offset,code->offset_size); cur+=code->offset_size; callback(&ctx,code.get()); @@ -184,7 +187,7 @@ void LoadCommand_DYLD_INFO::ForEachRebaseOpcode(std::functioncount_addr = (uint8_t*)cur; - moex::util::readUnsignedLeb128(cur,code->count,code->count_size); + moex::util::readUnsignedLeb128(cur,end,code->count,code->count_size); cur+=code->count_size; callback(&ctx,code.get()); @@ -203,7 +206,7 @@ void LoadCommand_DYLD_INFO::ForEachRebaseOpcode(std::functionoffset_addr = (uint8_t*)cur; - moex::util::readUnsignedLeb128(cur,code->offset,code->offset_size); + moex::util::readUnsignedLeb128(cur,end,code->offset,code->offset_size); cur+=code->offset_size; callback(&ctx,code.get()); @@ -219,11 +222,11 @@ void LoadCommand_DYLD_INFO::ForEachRebaseOpcode(std::functioncount_addr = (uint8_t*)cur; - moex::util::readUnsignedLeb128(cur,code->count,code->count_size); + moex::util::readUnsignedLeb128(cur,end,code->count,code->count_size); cur+=code->count_size; code->skip_addr = (uint8_t*)cur; - moex::util::readUnsignedLeb128(cur,code->skip,code->skip_size); + moex::util::readUnsignedLeb128(cur,end,code->skip,code->skip_size); cur+=code->skip_size; callback(&ctx,code.get()); @@ -252,8 +255,9 @@ void LoadCommand_DYLD_INFO::ForEachBindingOpcode(BindNodeType node_type,uint32_t bool done = false; char * begin = header()->header_start() + bind_off; uint32_t size = bind_size; + char * end = begin + size; char * cur = begin; - while(cur < begin + size && !done) { + while(cur < end && !done) { // read and move next ctx.pbyte = (uint8_t *) cur; cur += sizeof(uint8_t); @@ -284,7 +288,7 @@ void LoadCommand_DYLD_INFO::ForEachBindingOpcode(BindNodeType node_type,uint32_t case BIND_OPCODE_SET_DYLIB_ORDINAL_ULEB:{ auto code = std::make_shared(); code->lib_oridinal_addr = (uint8_t*)cur; - moex::util::readUnsignedLeb128(cur,code->lib_oridinal,code->lib_oridinal_size); + moex::util::readUnsignedLeb128(cur,end,code->lib_oridinal,code->lib_oridinal_size); cur+=code->lib_oridinal_size; callback(&ctx,code.get()); @@ -309,8 +313,13 @@ void LoadCommand_DYLD_INFO::ForEachBindingOpcode(BindNodeType node_type,uint32_t code->symbol_flags = ctx.immediate; char * name = (char*)cur; - int len = strlen(name); - code->symbol_name = std::string(name); + const char *name_end = (const char*)memchr(name, '\0', (size_t)(end - cur)); + if(name_end == nullptr){ + done = true; + break; + } + int len = (int)(name_end - name); + code->symbol_name = std::string(name, (size_t)len); code->symbol_name_addr = (uint8_t*)name; code->symbol_name_size = len + 1; @@ -332,7 +341,7 @@ void LoadCommand_DYLD_INFO::ForEachBindingOpcode(BindNodeType node_type,uint32_t auto code = std::make_shared(); code->addend_addr = (uint8_t*)cur; - moex::util::readSignedLeb128(cur,code->addend,code->addend_size); + moex::util::readSignedLeb128(cur,end,code->addend,code->addend_size); cur+=code->addend_size; callback(&ctx,code.get()); @@ -344,15 +353,17 @@ void LoadCommand_DYLD_INFO::ForEachBindingOpcode(BindNodeType node_type,uint32_t code->segment_index = ctx.immediate; code->offset_addr = (uint8_t*)cur; - moex::util::readUnsignedLeb128(cur,code->offset,code->offset_size); + moex::util::readUnsignedLeb128(cur,end,code->offset,code->offset_size); cur+=code->offset_size; if(header()->Is64()){ - assert(code->segment_index < header()->GetSegments64().size()); - ctx.address = header()->GetSegments64().at(code->segment_index)->cmd()->vmaddr; + auto &segs = header()->GetSegments64(); + if(code->segment_index < segs.size()) + ctx.address = segs.at(code->segment_index)->cmd()->vmaddr; }else{ - assert(code->segment_index < header()->GetSegments().size()); - ctx.address = header()->GetSegments().at(code->segment_index)->cmd()->vmaddr + code->offset; + auto &segs = header()->GetSegments(); + if(code->segment_index < segs.size()) + ctx.address = segs.at(code->segment_index)->cmd()->vmaddr + code->offset; } callback(&ctx,code.get()); @@ -363,7 +374,7 @@ void LoadCommand_DYLD_INFO::ForEachBindingOpcode(BindNodeType node_type,uint32_t auto code = std::make_shared(); code->offset_addr = (uint8_t*)cur; - moex::util::readUnsignedLeb128(cur,code->offset,code->offset_size); + moex::util::readUnsignedLeb128(cur,end,code->offset,code->offset_size); cur+=code->offset_size; ctx.address += code->offset; @@ -387,7 +398,7 @@ void LoadCommand_DYLD_INFO::ForEachBindingOpcode(BindNodeType node_type,uint32_t uint64_t start_next_rebase = (uint64_t)cur; code->offset_addr = (uint8_t*)cur; - moex::util::readUnsignedLeb128(cur,code->offset,code->offset_size); + moex::util::readUnsignedLeb128(cur,end,code->offset,code->offset_size); cur+=code->offset_size; callback(&ctx,code.get()); @@ -411,11 +422,11 @@ void LoadCommand_DYLD_INFO::ForEachBindingOpcode(BindNodeType node_type,uint32_t uint64_t start_next_rebase = (uint64_t)cur; code->count_addr = (uint8_t*)cur; - moex::util::readUnsignedLeb128(cur,code->count,code->count_size); + moex::util::readUnsignedLeb128(cur,end,code->count,code->count_size); cur+=code->count_size; code->skip_addr = (uint8_t*)cur; - moex::util::readUnsignedLeb128(cur,code->skip,code->skip_size); + moex::util::readUnsignedLeb128(cur,end,code->skip,code->skip_size); cur+=code->skip_size; callback(&ctx,code.get()); @@ -471,12 +482,12 @@ void LoadCommand_DYLD_INFO::ForEachExportItem(std::function 0){ item.flags_addr = (uint8_t*)cur; - const char *next = moex::util::readUnsignedLeb128(cur,item.flags,item.flags_size); + const char *next = moex::util::readUnsignedLeb128(cur,end,item.flags,item.flags_size); if (next == nullptr || next > end) continue; cur+= item.flags_size; item.offset_addr = (uint8_t*)cur; - next = moex::util::readUnsignedLeb128(cur,item.offset,item.offset_size); + next = moex::util::readUnsignedLeb128(cur,end,item.offset,item.offset_size); if (next == nullptr || next > end) continue; cur+= item.offset_size; } @@ -503,7 +514,7 @@ void LoadCommand_DYLD_INFO::ForEachExportItem(std::function end) break; child.skip_addr = (uint8_t*)cur; - const char *next = moex::util::readUnsignedLeb128(cur,child.skip,child.skip_size); + const char *next = moex::util::readUnsignedLeb128(cur,end,child.skip,child.skip_size); if (next == nullptr || next > end) break; cur+= child.skip_size; diff --git a/src/libmoex/node/loadcmd/LoadCommand_LINKEDIT_DATA.cpp b/src/libmoex/node/loadcmd/LoadCommand_LINKEDIT_DATA.cpp index 949d71c..5adb113 100644 --- a/src/libmoex/node/loadcmd/LoadCommand_LINKEDIT_DATA.cpp +++ b/src/libmoex/node/loadcmd/LoadCommand_LINKEDIT_DATA.cpp @@ -46,10 +46,12 @@ std::vector &LoadCommand_LC_FUNCTION_STARTS::GetFunctions(){ const char* start = (char*)this->header_->header_start() + cmd_->dataoff; const char* end = start + cmd_->datasize; const char* cur_offset = start; - while(cur_offset < start + cmd_->datasize){ + while(cur_offset < end){ Uleb128Data data; data.offset = (uint64_t)cur_offset; - cur_offset = util::readUnsignedLeb128(cur_offset,data.data,data.occupy_size); + const char* next = util::readUnsignedLeb128(cur_offset,end,data.data,data.occupy_size); + if(next == nullptr) break; + cur_offset = next; functions_.push_back(data); } diff --git a/src/libmoex/viewnode/views/DynamicLoaderInfoViewNode.cpp b/src/libmoex/viewnode/views/DynamicLoaderInfoViewNode.cpp index 5f85649..fb89870 100644 --- a/src/libmoex/viewnode/views/DynamicLoaderInfoViewNode.cpp +++ b/src/libmoex/viewnode/views/DynamicLoaderInfoViewNode.cpp @@ -675,7 +675,7 @@ void ModernExportsTrieViewNode::InitViewDatas() uint64_t terminal_size = 0; uint32_t ts_size = 0; - const char *p = util::readUnsignedLeb128(cur.node, terminal_size, ts_size); + const char *p = util::readUnsignedLeb128(cur.node, blob_end, terminal_size, ts_size); if (p == nullptr || p > blob_end) continue; t->AddRow({AsAddress(dataoff + node_off), "node", cur.prefix.empty() ? "" : cur.prefix, @@ -685,7 +685,7 @@ void ModernExportsTrieViewNode::InitViewDatas() if (terminal_end <= blob_end && terminal_size > 0) { uint64_t flags = 0; uint32_t flags_size = 0; - const char *q = util::readUnsignedLeb128(p, flags, flags_size); + const char *q = util::readUnsignedLeb128(p, blob_end, flags, flags_size); if (q != nullptr && q <= terminal_end) { std::string flag_kind = "regular"; const uint64_t kind = flags & EXPORT_SYMBOL_FLAGS_KIND_MASK; @@ -699,7 +699,7 @@ void ModernExportsTrieViewNode::InitViewDatas() uint64_t value = 0; uint32_t value_size = 0; - const char *r = util::readUnsignedLeb128(q, value, value_size); + const char *r = util::readUnsignedLeb128(q, blob_end, value, value_size); if (r != nullptr && r <= terminal_end) { t->AddRow({AsAddress(dataoff + static_cast(q - blob_begin)), "terminal.value", @@ -709,7 +709,7 @@ void ModernExportsTrieViewNode::InitViewDatas() if (flags & EXPORT_SYMBOL_FLAGS_REEXPORT) { uint64_t import_name_off = 0; uint32_t import_name_size = 0; - const char *import_name_ptr = util::readUnsignedLeb128(r, import_name_off, import_name_size); + const char *import_name_ptr = util::readUnsignedLeb128(r, blob_end, import_name_off, import_name_size); if (import_name_ptr != nullptr && import_name_ptr <= terminal_end) { const char *name_end = static_cast(memchr(import_name_ptr, '\0', static_cast(terminal_end - import_name_ptr))); std::string import_name = ""; @@ -725,7 +725,7 @@ void ModernExportsTrieViewNode::InitViewDatas() } else if (flags & EXPORT_SYMBOL_FLAGS_STUB_AND_RESOLVER) { uint64_t resolver = 0; uint32_t resolver_size = 0; - const char *resolver_ptr = util::readUnsignedLeb128(r, resolver, resolver_size); + const char *resolver_ptr = util::readUnsignedLeb128(r, blob_end, resolver, resolver_size); if (resolver_ptr != nullptr && resolver_ptr <= terminal_end) { t->AddRow({AsAddress(dataoff + static_cast(resolver_ptr - blob_begin)), "terminal.resolver", @@ -760,7 +760,7 @@ void ModernExportsTrieViewNode::InitViewDatas() uint64_t child_delta = 0; uint32_t child_size = 0; - const char *after = util::readUnsignedLeb128(children_ptr, child_delta, child_size); + const char *after = util::readUnsignedLeb128(children_ptr, blob_end, child_delta, child_size); if (after == nullptr || after > blob_end) break; children_ptr = after; diff --git a/tests/regression/run_crash_regression.sh b/tests/regression/run_crash_regression.sh index d46bf4f..014f28e 100755 --- a/tests/regression/run_crash_regression.sh +++ b/tests/regression/run_crash_regression.sh @@ -63,6 +63,20 @@ printf '\x00\x00\x00\x0c\x00\x00\x00\x00' \ printf '\xcf\xfa\xed\xfe\x07\x00\x00\x01\x03\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ | dd of="${valid_fat64}" bs=1 seek=4096 conv=notrunc 2>/dev/null +# Truncated copies of real sample binaries. These carry LC_DYLD_INFO / +# LC_FUNCTION_STARTS payloads, so cutting them mid-stream exercises the LEB128 +# and trie-walk bounds checks against genuine (not synthetic) data. +for sample in simple complex; do + src_bin="${ROOT_DIR}/sample/${sample}" + [[ -f "${src_bin}" ]] || continue + total_bytes="$(wc -c < "${src_bin}")" + for frac in 60 75 90; do + cut_bytes=$(( total_bytes * frac / 100 )) + [[ "${cut_bytes}" -gt 0 ]] || continue + head -c "${cut_bytes}" "${src_bin}" > "${TMP_DIR}/trunc_${sample}_${frac}.bin" + done +done + FAIL=0 TOTAL=0 REJECTED=0 From d07f74becefa87d99ea04fd6c7ab8b307e5b1176 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 20 May 2026 21:16:02 +0000 Subject: [PATCH 02/11] Reject misaligned/out-of-bounds Mach-O structures Bounds-check the NodeData copy against the mapped file before reading, so a struct that straddles EOF can no longer be memcpy'd out of range. Read the load-command header through an aligned copy and require cmdsize to honor the pointer-size alignment, and reject fat slices whose offset is not 8-byte aligned. Together these stop the parser from dereferencing Mach-O headers and load commands at misaligned addresses on crafted input (undefined behaviour flagged by UBSan). https://claude.ai/code/session_013kBiVXftgoEsyGVyrvfGok --- src/libmoex/node/FatHeader.cpp | 7 +++++++ src/libmoex/node/MachHeader.cpp | 16 ++++++++++++---- src/libmoex/node/Node.h | 13 +++++++++++++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/libmoex/node/FatHeader.cpp b/src/libmoex/node/FatHeader.cpp index 3c21454..b469ae3 100644 --- a/src/libmoex/node/FatHeader.cpp +++ b/src/libmoex/node/FatHeader.cpp @@ -38,6 +38,13 @@ void FatArch::Init(void *offset, NodeContextPtr &ctx) { if (object_size < sizeof(uint32_t)) { throw NodeException("Malformed Fat Mach-O: arch payload too small"); } + // Slices are required to be at least pointer-aligned; real fat binaries + // page-align them. Reject under-aligned offsets so the embedded Mach-O + // header and its load commands are not dereferenced at misaligned + // addresses (undefined behaviour). + if (object_offset % 8 != 0) { + throw NodeException("Malformed Fat Mach-O: arch offset is misaligned"); + } void *mach_offset = reinterpret_cast(ctx_->file_start) + object_offset; mh_ = std::make_shared(); diff --git a/src/libmoex/node/MachHeader.cpp b/src/libmoex/node/MachHeader.cpp index 9baeba2..dea5d8d 100644 --- a/src/libmoex/node/MachHeader.cpp +++ b/src/libmoex/node/MachHeader.cpp @@ -64,6 +64,7 @@ void MachHeader::Parse(void *offset,NodeContextPtr& ctx) { throw NodeException("Malformed Mach-O: load commands exceed file size"); } + const uint32_t cmd_align = is64_ ? 8 : 4; qv_load_command *first_cmd = reinterpret_cast((char*)offset + cur_datasize); qv_load_command *cur_cmd = first_cmd; uint64_t parsed_size = 0; @@ -71,10 +72,17 @@ void MachHeader::Parse(void *offset,NodeContextPtr& ctx) { if (parsed_size + sizeof(qv_load_command) > sizeofcmds) { throw NodeException("Malformed Mach-O: truncated load command"); } - if (cur_cmd->cmdsize < sizeof(qv_load_command)) { + // Load commands may sit at a misaligned address in a crafted file, so + // read the header through an aligned copy instead of dereferencing. + qv_load_command lc_head{}; + memcpy(&lc_head, cur_cmd, sizeof(qv_load_command)); + if (lc_head.cmdsize < sizeof(qv_load_command)) { throw NodeException("Malformed Mach-O: invalid load command size"); } - if (parsed_size + cur_cmd->cmdsize > sizeofcmds) { + if (lc_head.cmdsize % cmd_align != 0) { + throw NodeException("Malformed Mach-O: misaligned load command size"); + } + if (parsed_size + lc_head.cmdsize > sizeofcmds) { throw NodeException("Malformed Mach-O: load command size overflow"); } @@ -83,8 +91,8 @@ void MachHeader::Parse(void *offset,NodeContextPtr& ctx) { loadcmds_.push_back(cmd); // next - parsed_size += cur_cmd->cmdsize; - cur_cmd = reinterpret_cast((char*)cur_cmd + cur_cmd->cmdsize); + parsed_size += lc_head.cmdsize; + cur_cmd = reinterpret_cast((char*)cur_cmd + lc_head.cmdsize); } } diff --git a/src/libmoex/node/Node.h b/src/libmoex/node/Node.h index 60516d7..a5b84a7 100644 --- a/src/libmoex/node/Node.h +++ b/src/libmoex/node/Node.h @@ -34,6 +34,16 @@ struct NodeContext{ }; using NodeContextPtr = std::shared_ptr; +// True when [addr, addr+size) lies entirely within the mapped file. +static inline bool NodeInFile(const NodeContextPtr &ctx, const void *addr, std::size_t size){ + if(!ctx || ctx->file_start == nullptr) return false; + const char *p = static_cast(addr); + const char *start = static_cast(ctx->file_start); + const char *end = start + ctx->file_size; + if(p < start || p > end) return false; + return size <= static_cast(end - p); +} + // Base class for each MachO element class Node{ public: @@ -88,6 +98,9 @@ class NodeData : public NodeOffset{ // Init function which should be called in every child class's Init function void Init(void *offset,NodeContextPtr & ctx){ NodeOffset::Init(offset,ctx); + if(!NodeInFile(ctx, offset, NodeOffset::DATA_SIZE())){ + throw NodeException("Malformed file: struct read out of bounds"); + } memcpy(&data_,offset,NodeOffset::DATA_SIZE()); } }; From 8ed196ebe78d9cb3af93f3699dd428c4b3d03e48 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 20 May 2026 21:25:27 +0000 Subject: [PATCH 03/11] Fix crash when viewing a fat binary's Fat Header FatHeaderViewNode built its table with the no-argument CreateTableView(), leaving TableViewData::GetRAW unset, so the AddRow(field,...) template called an empty std::function and aborted with bad_function_call. This crashed both the Fat Header node in the GUI and any --cli dump of a fat binary. Give the fat header table a real GetRAW so row offsets are correct, and make the AddRow template tolerate an unset callback as a safety net. Extend the CLI smoke test to analyze the fat sample so the regression is covered. https://claude.ai/code/session_013kBiVXftgoEsyGVyrvfGok --- src/libmoex/viewnode/ViewNode.h | 2 +- src/libmoex/viewnode/views/FatHeaderViewNode.cpp | 2 +- tests/regression/run_cli_smoke.sh | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/libmoex/viewnode/ViewNode.h b/src/libmoex/viewnode/ViewNode.h index b8f2c09..a66b462 100644 --- a/src/libmoex/viewnode/ViewNode.h +++ b/src/libmoex/viewnode/ViewNode.h @@ -109,7 +109,7 @@ template void TableViewData::AddRow(T& field,const char *desc,const std::string &val){ AddRow((void*)&(field), (uint64_t)sizeof(field), - GetRAW(&(field)), + GetRAW ? GetRAW(&(field)) : 0, desc, val); } diff --git a/src/libmoex/viewnode/views/FatHeaderViewNode.cpp b/src/libmoex/viewnode/views/FatHeaderViewNode.cpp index 5accb00..16141e0 100644 --- a/src/libmoex/viewnode/views/FatHeaderViewNode.cpp +++ b/src/libmoex/viewnode/views/FatHeaderViewNode.cpp @@ -26,7 +26,7 @@ void FatHeaderViewNode::InitViewDatas(){ // Table { - auto t = CreateTableView(); + auto t = CreateTableView(d_.get()); const qv_fat_header * h = d_->offset(); t->AddRow(h->magic,"Magic Number",d_->GetMagicString()); diff --git a/tests/regression/run_cli_smoke.sh b/tests/regression/run_cli_smoke.sh index b3ecaf3..d28c544 100755 --- a/tests/regression/run_cli_smoke.sh +++ b/tests/regression/run_cli_smoke.sh @@ -7,8 +7,10 @@ APP_BUNDLE_BIN="${BUILD_DIR}/MachOExplorer.app/Contents/MacOS/MachOExplorer" APP_BIN="${APP_BUNDLE_BIN}" APP_BUNDLE_DIR="${BUILD_DIR}/MachOExplorer.app" SAMPLE_FILE="${ROOT_DIR}/sample/simple" +FAT_SAMPLE_FILE="${ROOT_DIR}/sample/complex" OUT_TEXT="/tmp/moex-cli-smoke.txt" OUT_JSON="/tmp/moex-cli-smoke.json" +OUT_FAT="/tmp/moex-cli-smoke-fat.txt" if [[ ! -x "${APP_BIN}" ]]; then echo "cli-smoke: missing app binary: ${APP_BIN}; build first" @@ -90,4 +92,17 @@ if ! ( exit 1 fi +# Fat binaries exercise FatHeaderViewNode, which must produce a table without +# crashing on an unset GetRAW callback. +if [[ -f "${FAT_SAMPLE_FILE}" ]]; then + if ! "${APP_BIN}" --cli "${FAT_SAMPLE_FILE}" >"${OUT_FAT}" 2>/dev/null; then + echo "cli-smoke: fat binary analysis failed" + exit 1 + fi + if [[ ! -s "${OUT_FAT}" ]]; then + echo "cli-smoke: fat binary produced no output" + exit 1 + fi +fi + echo "cli-smoke: passed" From 5e4679d8a5dc64411ab0ac9bde16fabe4087ef83 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 20 May 2026 21:30:47 +0000 Subject: [PATCH 04/11] Parse binaries off the GUI thread Opening a large binary or dyld shared cache previously blocked the UI thread for the whole parse because LayoutController::initModel parsed and built the tree synchronously inside LayoutDockWidget::openFile. Split the controller into a thread-safe parse() (libmoex only, no Qt objects) and a GUI-thread buildModel(), and run parse() through QtConcurrent with a QFutureWatcher. The tree is cleared while parsing and rebuilt on completion; re-entrant opens are ignored until the in-flight parse finishes so the worker never races a deleted controller. https://claude.ai/code/session_013kBiVXftgoEsyGVyrvfGok --- src/CMakeLists.txt | 5 ++- src/src/controller/LayoutController.cpp | 25 ++++++------ src/src/controller/LayoutController.h | 10 ++++- src/src/dock/LayoutDockWidget.cpp | 51 ++++++++++++++++++++++--- src/src/dock/LayoutDockWidget.h | 2 + 5 files changed, 70 insertions(+), 23 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f772098..58ade5d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -16,8 +16,8 @@ if(WIN32) add_compile_definitions(NOMINMAX WIN32_LEAN_AND_MEAN) endif() -find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Gui Widgets Network) -find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Gui Widgets Network) +find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Gui Widgets Network Concurrent) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Gui Widgets Network Concurrent) find_path(CAPSTONE_INCLUDE_DIR capstone/capstone.h) find_library(CAPSTONE_LIBRARY capstone) @@ -57,6 +57,7 @@ target_link_libraries(MachOExplorer PRIVATE Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::Network + Qt${QT_VERSION_MAJOR}::Concurrent ) if(APPLE) diff --git a/src/src/controller/LayoutController.cpp b/src/src/controller/LayoutController.cpp index 80bd7e8..09d71ce 100755 --- a/src/src/controller/LayoutController.cpp +++ b/src/src/controller/LayoutController.cpp @@ -18,15 +18,24 @@ LayoutController::~LayoutController() } } -bool LayoutController::initModel(QString & error) +bool LayoutController::parse(QString & error) { if(filePath_.length() == 0){ error = "File path is empty"; return false; } - WS()->addLog("Start parsing " + filePath_); + std::string filepath = filePath_.toStdString(); + std::string init_error; + if(!vnm_.Init(filepath,init_error)){ + error = QString("Exception : %1").arg(init_error.c_str()); + return false; + } + return true; +} +void LayoutController::buildModel() +{ // Init model if(model_) delete model_; model_ = new QStandardItemModel(); @@ -35,16 +44,6 @@ bool LayoutController::initModel(QString & error) << QStringLiteral("name") ); - // Parse file - std::string filepath = filePath_.toStdString(); - std::string init_error; - if(!vnm_.Init(filepath,init_error)){ - error = QString("Exception : %1").arg(init_error.c_str()); - WS()->addLog(error); - return false; - } - WS()->addLog("Parse succeed"); - // Root item moex::ViewNode *root = vnm_.GetRootNode(); QStandardItem *item = new QStandardItem(QString::fromStdString(root->GetDisplayName())); @@ -53,8 +52,6 @@ bool LayoutController::initModel(QString & error) // Children initChildren(root,item); - - return true; } void LayoutController::initChildren(moex::ViewNode *parentNode,QStandardItem *parentItem){ diff --git a/src/src/controller/LayoutController.h b/src/src/controller/LayoutController.h index 4d533fd..dc35574 100755 --- a/src/src/controller/LayoutController.h +++ b/src/src/controller/LayoutController.h @@ -18,10 +18,17 @@ class LayoutController void setFilePath(const QString & filePath){ filePath_ = filePath;} - bool initModel(QString & error); + // Heavy parsing step. Safe to call off the GUI thread: it only touches + // libmoex data and never creates Qt UI objects. + bool parse(QString & error); + // Builds the tree model from the parsed result. GUI thread only. + void buildModel(); void initChildren(moex::ViewNode *parentNode,QStandardItem *parentItem); int getExpandDepth(); + QString lastError() const { return lastError_; } + void setLastError(const QString & e){ lastError_ = e; } + QStandardItemModel* model(){return model_;} moex::ViewNode *rootNode(){return vnm_.GetRootNode();} @@ -29,6 +36,7 @@ class LayoutController private: QStandardItemModel *model_; QString filePath_; + QString lastError_; moex::ViewNodeManager vnm_; }; diff --git a/src/src/dock/LayoutDockWidget.cpp b/src/src/dock/LayoutDockWidget.cpp index b73a242..207e71b 100755 --- a/src/src/dock/LayoutDockWidget.cpp +++ b/src/src/dock/LayoutDockWidget.cpp @@ -10,6 +10,8 @@ #include #include #include +#include +#include LayoutDockWidget::LayoutDockWidget(QWidget *parent) : QDockWidget(parent) { @@ -38,16 +40,53 @@ LayoutDockWidget::LayoutDockWidget(QWidget *parent) : QDockWidget(parent) void LayoutDockWidget::openFile(const QString &filePath) { + if(parsing_){ + WS()->addLog("Still parsing the previous file; please wait..."); + return; + } + if(controller) delete controller; controller = new LayoutController(); - controller->setFilePath(filePath); - QString error; - if(!controller->initModel(error)){ - util::showError(this,error); - return; - } + // Clear the current tree while the new file parses in the background so the + // UI thread stays responsive on large binaries / dyld shared caches. + treeView->setModel(nullptr); + parsing_ = true; + WS()->addLog("Start parsing " + filePath); + + LayoutController *ctrl = controller; + auto *watcher = new QFutureWatcher(this); + connect(watcher, &QFutureWatcher::finished, this, [this, watcher, ctrl](){ + watcher->deleteLater(); + parsing_ = false; + + // Bail if the controller was replaced while we were parsing. + if(ctrl != controller){ + return; + } + + if(!watcher->result()){ + util::showError(this, controller->lastError()); + WS()->addLog(controller->lastError()); + return; + } + + WS()->addLog("Parse succeed"); + controller->buildModel(); + populateTree(); + }); + + watcher->setFuture(QtConcurrent::run([ctrl]() -> bool { + QString error; + bool ok = ctrl->parse(error); + ctrl->setLastError(error); + return ok; + })); +} + +void LayoutDockWidget::populateTree() +{ treeView->setModel(controller->model()); treeView->setEditTriggers(QAbstractItemView::NoEditTriggers); treeView->setColumnWidth(0,300); diff --git a/src/src/dock/LayoutDockWidget.h b/src/src/dock/LayoutDockWidget.h index 7034b36..ed74d3c 100755 --- a/src/src/dock/LayoutDockWidget.h +++ b/src/src/dock/LayoutDockWidget.h @@ -20,10 +20,12 @@ class LayoutDockWidget : public QDockWidget private: LayoutTreeView *treeView; LayoutController *controller; + bool parsing_ = false; private: void showViewNode(moex::ViewNode * node); void showTreeIndex(const QModelIndex &index); + void populateTree(); public: explicit LayoutDockWidget(QWidget *parent = 0); From 1d5c11656cd0d61e5eb9482d7e264fdef56452b1 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 20 May 2026 22:07:46 +0000 Subject: [PATCH 05/11] Support building and testing on Linux The two dialog sources included their generated uic headers with a lowercased name (ui_aboutdialog.h) that only resolves on a case-insensitive filesystem, so AutoUic failed on Linux. Match the .ui filename case so the project configures and builds out of the box with stock qt6-base-dev. Make the CLI smoke test fall back to the plain build/MachOExplorer binary when there is no macOS .app bundle, and add a build_linux.sh helper plus Linux build notes. The full regression suite now passes on Linux. https://claude.ai/code/session_013kBiVXftgoEsyGVyrvfGok --- DEVELOP.md | 13 ++++++++++++- build_linux.sh | 27 +++++++++++++++++++++++++++ src/src/dialog/AboutDialog.cpp | 2 +- src/src/dialog/CheckUpdateDialog.cpp | 2 +- tests/regression/run_cli_smoke.sh | 22 ++++++++++++---------- 5 files changed, 53 insertions(+), 13 deletions(-) create mode 100755 build_linux.sh diff --git a/DEVELOP.md b/DEVELOP.md index 4cf0db9..46cc0df 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -4,8 +4,9 @@ This document is for contributors and maintainers. ## Prerequisites - CMake >= 3.16 -- Qt 6 (recommended) +- Qt 6 (recommended), components: Core, Gui, Widgets, Network, Concurrent - C++14 compiler +- Optional: Capstone (`libcapstone-dev`) for `__TEXT,__text` disassembly - macOS release tooling: `macdeployqt`, `hdiutil`, `gh` ## Build @@ -17,6 +18,16 @@ cmake --build build -j8 ./build/MachOExplorer ``` +### Linux +```bash +# Debian/Ubuntu deps: sudo apt-get install qt6-base-dev libgl1-mesa-dev +# (optional disassembly: libcapstone-dev) +./build_linux.sh # or: cmake -S src -B build && cmake --build build -j$(nproc) +./build/MachOExplorer +``` +On Linux the binary is `build/MachOExplorer` (no `.app` bundle). The CLI mode +(`--cli`) runs without a display, so it works in headless/CI environments. + ### Windows ```powershell cmake -S src -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH="C:/Qt/6.x.x/msvcXXXX_64" diff --git a/build_linux.sh b/build_linux.sh new file mode 100755 index 0000000..1fcc2d3 --- /dev/null +++ b/build_linux.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Build MachOExplorer on Linux. +# +# Requirements: +# - CMake >= 3.16 +# - Qt 6 base + concurrent (e.g. Debian/Ubuntu: qt6-base-dev) +# - A C++14 compiler (g++ or clang++) +# - Optional: libcapstone-dev for __TEXT,__text disassembly +# +# Usage: +# ./build_linux.sh [build_type] +# where build_type defaults to Release. + +ROOT_DIR="$(cd "$(dirname "$0")" && pwd)" +BUILD_DIR="${ROOT_DIR}/build" +BUILD_TYPE="${1:-Release}" + +cmake -S "${ROOT_DIR}/src" -B "${BUILD_DIR}" -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" +cmake --build "${BUILD_DIR}" -j"$(nproc)" + +echo "---------" +echo "built: ${BUILD_DIR}/MachOExplorer" +echo "run: ${BUILD_DIR}/MachOExplorer" +echo "cli: ${BUILD_DIR}/MachOExplorer --cli " +echo "---------" diff --git a/src/src/dialog/AboutDialog.cpp b/src/src/dialog/AboutDialog.cpp index fa0f3ae..c2c5bdf 100644 --- a/src/src/dialog/AboutDialog.cpp +++ b/src/src/dialog/AboutDialog.cpp @@ -3,7 +3,7 @@ // Copyright © 2017 everettjf. All rights reserved. // #include "AboutDialog.h" -#include "ui_aboutdialog.h" +#include "ui_AboutDialog.h" #include "src/base/AppInfo.h" #include "src/utility/Utility.h" diff --git a/src/src/dialog/CheckUpdateDialog.cpp b/src/src/dialog/CheckUpdateDialog.cpp index 9e96204..dae1a38 100644 --- a/src/src/dialog/CheckUpdateDialog.cpp +++ b/src/src/dialog/CheckUpdateDialog.cpp @@ -3,7 +3,7 @@ // Copyright © 2017 everettjf. All rights reserved. // #include "CheckUpdateDialog.h" -#include "ui_checkupdatedialog.h" +#include "ui_CheckUpdateDialog.h" #include #include "src/base/AppInfo.h" #include "src/utility/Utility.h" diff --git a/tests/regression/run_cli_smoke.sh b/tests/regression/run_cli_smoke.sh index d28c544..b7b80c7 100755 --- a/tests/regression/run_cli_smoke.sh +++ b/tests/regression/run_cli_smoke.sh @@ -4,21 +4,23 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" BUILD_DIR="${ROOT_DIR}/build" APP_BUNDLE_BIN="${BUILD_DIR}/MachOExplorer.app/Contents/MacOS/MachOExplorer" -APP_BIN="${APP_BUNDLE_BIN}" -APP_BUNDLE_DIR="${BUILD_DIR}/MachOExplorer.app" +PLAIN_BIN="${BUILD_DIR}/MachOExplorer" SAMPLE_FILE="${ROOT_DIR}/sample/simple" FAT_SAMPLE_FILE="${ROOT_DIR}/sample/complex" OUT_TEXT="/tmp/moex-cli-smoke.txt" OUT_JSON="/tmp/moex-cli-smoke.json" OUT_FAT="/tmp/moex-cli-smoke-fat.txt" -if [[ ! -x "${APP_BIN}" ]]; then - echo "cli-smoke: missing app binary: ${APP_BIN}; build first" - exit 2 -fi - -if [[ ! -d "${APP_BUNDLE_DIR}" ]]; then - echo "cli-smoke: missing app bundle dir: ${APP_BUNDLE_DIR}; build first" +# Prefer the macOS .app bundle; fall back to the plain binary (Linux/Windows). +# DIR_CASE is a directory used to verify the parser rejects directory paths. +if [[ -x "${APP_BUNDLE_BIN}" ]]; then + APP_BIN="${APP_BUNDLE_BIN}" + DIR_CASE="${BUILD_DIR}/MachOExplorer.app" +elif [[ -x "${PLAIN_BIN}" ]]; then + APP_BIN="${PLAIN_BIN}" + DIR_CASE="${BUILD_DIR}" +else + echo "cli-smoke: missing app binary in ${BUILD_DIR}; build first" exit 2 fi @@ -76,7 +78,7 @@ if ! "${summary_mode_cmd[@]}" "${OUT_JSON}.filtered"; then exit 1 fi -if "${APP_BIN}" --cli "${APP_BUNDLE_DIR}" >/tmp/moex-cli-bundle.out 2>/tmp/moex-cli-bundle.err; then +if "${APP_BIN}" --cli "${DIR_CASE}" >/tmp/moex-cli-bundle.out 2>/tmp/moex-cli-bundle.err; then echo "cli-smoke: app bundle path should fail gracefully" exit 1 fi From 7035ab1d902ca3338854dc68d1941271c31b0bf5 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 20 May 2026 22:10:22 +0000 Subject: [PATCH 06/11] Parse code signature blobs and entitlements The Code Signature node previously showed only the raw __LINKEDIT blob. Parse the embedded-signature SuperBlob (big-endian, byte-wise so unaligned offsets stay safe and every access is bounds-checked) and list each sub-blob with its slot type, magic, offset and length. When an entitlements blob is present, decode and display the plist XML line by line with file offsets, capped so a large blob cannot flood the table. https://claude.ai/code/session_013kBiVXftgoEsyGVyrvfGok --- .../viewnode/views/CodeSignatureViewNode.cpp | 161 +++++++++++++++++- 1 file changed, 156 insertions(+), 5 deletions(-) diff --git a/src/libmoex/viewnode/views/CodeSignatureViewNode.cpp b/src/libmoex/viewnode/views/CodeSignatureViewNode.cpp index 70ec503..205a007 100644 --- a/src/libmoex/viewnode/views/CodeSignatureViewNode.cpp +++ b/src/libmoex/viewnode/views/CodeSignatureViewNode.cpp @@ -6,20 +6,171 @@ MOEX_NAMESPACE_BEGIN +namespace { + +// Code signing blob magics (all big-endian on disk). +constexpr uint32_t kCSMagicEmbeddedSignature = 0xfade0cc0; +constexpr uint32_t kCSMagicEmbeddedSignatureOld = 0xfade0b02; +constexpr uint32_t kCSMagicCodeDirectory = 0xfade0c02; +constexpr uint32_t kCSMagicRequirements = 0xfade0c01; +constexpr uint32_t kCSMagicRequirement = 0xfade0c00; +constexpr uint32_t kCSMagicEmbeddedEntitlements = 0xfade7171; +constexpr uint32_t kCSMagicEmbeddedDerEntitlements = 0xfade7172; +constexpr uint32_t kCSMagicBlobWrapper = 0xfade0b01; + +// Slot index types. +constexpr uint32_t kCSSlotEntitlements = 5; +constexpr uint32_t kCSSlotDerEntitlements = 7; + +uint32_t ReadBE32(const uint8_t *p) { + return (static_cast(p[0]) << 24) | + (static_cast(p[1]) << 16) | + (static_cast(p[2]) << 8) | + static_cast(p[3]); +} + +std::string MagicName(uint32_t magic) { + switch (magic) { + case kCSMagicEmbeddedSignature: return "Embedded Signature"; + case kCSMagicEmbeddedSignatureOld: return "Embedded Signature (old)"; + case kCSMagicCodeDirectory: return "Code Directory"; + case kCSMagicRequirements: return "Requirements"; + case kCSMagicRequirement: return "Requirement"; + case kCSMagicEmbeddedEntitlements: return "Entitlements (XML)"; + case kCSMagicEmbeddedDerEntitlements: return "Entitlements (DER)"; + case kCSMagicBlobWrapper: return "CMS Signature"; + default: return "Unknown"; + } +} + +std::string SlotName(uint32_t type) { + switch (type) { + case 0: return "Code Directory"; + case 1: return "Info.plist"; + case 2: return "Requirements"; + case 3: return "Resource Directory"; + case 4: return "Application"; + case kCSSlotEntitlements: return "Entitlements"; + case 6: return "Repspecific"; + case kCSSlotDerEntitlements: return "DER Entitlements"; + case 0x1000: return "Alternate Code Directory"; + case 0x10000: return "CMS Signature"; + default: return "Slot"; + } +} + +} // namespace void CodeSignatureViewNode::InitViewDatas() { using namespace moex::util; auto t = CreateTableView(); - auto seg=mh_->FindLoadCommand({LC_CODE_SIGNATURE}); - if(!seg) + auto seg = mh_->FindLoadCommand({LC_CODE_SIGNATURE}); + if (!seg) return; + NodeContextPtr ctx = mh_->ctx(); + char *blob = (char *)mh_->header_start() + seg->cmd()->dataoff; + const uint32_t datasize = seg->cmd()->datasize; + auto b = CreateBinaryView(); - b->offset = (char*)mh_->header_start() + seg->cmd()->dataoff; - b->size = seg->cmd()->datasize; - b->start_value = (uint64_t)b->offset - (uint64_t)mh_->ctx()->file_start; + b->offset = blob; + b->size = datasize; + b->start_value = (uint64_t)blob - (uint64_t)ctx->file_start; + + if (!NodeInFile(ctx, blob, datasize) || datasize < 12) { + t->AddRow({AsAddress(b->start_value), "error", "code signature blob out of range"}); + return; + } + + const uint8_t *base = reinterpret_cast(blob); + const uint64_t base_off = b->start_value; + const uint32_t super_magic = ReadBE32(base); + const uint32_t super_len = ReadBE32(base + 4); + const uint32_t super_count = ReadBE32(base + 8); + + t->AddRow({AsAddress(base_off), "Magic", + fmt::format("0x{} ({})", AsShortHexString(super_magic), MagicName(super_magic))}); + t->AddRow({AsAddress(base_off + 4), "Length", AsString(super_len)}); + + if (super_magic != kCSMagicEmbeddedSignature && super_magic != kCSMagicEmbeddedSignatureOld) { + // Not a super blob; nothing more to enumerate. + return; + } + + t->AddRow({AsAddress(base_off + 8), "Blob Count", AsString(super_count)}); + t->AddSeparator(); + + // Cap to a sane number of indices and to what the blob can hold. + const uint64_t max_indices = (static_cast(datasize) - 12) / 8; + uint32_t count = super_count; + if (count > max_indices) count = static_cast(max_indices); + + uint32_t entitlements_off = 0; + bool has_entitlements = false; + + for (uint32_t i = 0; i < count; ++i) { + const uint8_t *idx = base + 12 + static_cast(i) * 8; + const uint32_t slot_type = ReadBE32(idx); + const uint32_t blob_off = ReadBE32(idx + 4); + + std::string magic_desc = "-"; + std::string len_desc = "-"; + if (blob_off + 8 <= datasize) { + const uint32_t bmagic = ReadBE32(base + blob_off); + const uint32_t blen = ReadBE32(base + blob_off + 4); + magic_desc = fmt::format("0x{} ({})", AsShortHexString(bmagic), MagicName(bmagic)); + len_desc = AsString(blen); + if ((slot_type == kCSSlotEntitlements || bmagic == kCSMagicEmbeddedEntitlements) && + !has_entitlements) { + entitlements_off = blob_off; + has_entitlements = true; + } + } + + t->AddRow({AsAddress(base_off + 12 + static_cast(i) * 8), + fmt::format("[{}] {}", i, SlotName(slot_type)), + fmt::format("offset={} magic={} length={}", + blob_off, magic_desc, len_desc)}); + } + + if (!has_entitlements) + return; + + // Decode the entitlements XML blob: magic(4) + length(4) + payload. + if (entitlements_off + 8 > datasize) + return; + const uint32_t ent_magic = ReadBE32(base + entitlements_off); + const uint32_t ent_len = ReadBE32(base + entitlements_off + 4); + if (ent_magic != kCSMagicEmbeddedEntitlements) + return; + if (ent_len < 8 || entitlements_off + ent_len > datasize) + return; + + const char *xml = blob + entitlements_off + 8; + const uint32_t xml_len = ent_len - 8; + + t->AddSeparator(); + t->AddRow({AsAddress(base_off + entitlements_off), "Entitlements", + fmt::format("{} bytes (XML)", xml_len)}); + + // Emit the plist line by line, capped so a huge blob cannot flood the table. + const uint32_t kMaxLines = 1000; + uint32_t line_no = 0; + uint32_t i = 0; + while (i < xml_len && line_no < kMaxLines) { + uint32_t start = i; + while (i < xml_len && xml[i] != '\n') ++i; + std::string line(xml + start, xml + i); + if (!line.empty() && line.back() == '\r') line.pop_back(); + t->AddRow({AsAddress(base_off + entitlements_off + 8 + start), "", line}); + ++line_no; + if (i < xml_len) ++i; // skip '\n' + } + if (i < xml_len) { + t->AddRow({"", "", fmt::format("... ({} more bytes truncated)", xml_len - i)}); + } } From a2bc3ab719515189db2b434fc027dffc74c466f9 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 20 May 2026 22:16:05 +0000 Subject: [PATCH 07/11] Build node view data off the GUI thread Selecting a node with a large table (symbols, dyld cache images, ObjC metadata) ran ViewNode::Init() synchronously on the GUI thread, briefly freezing the UI. Build uninitialized nodes through QtConcurrent and display them when ready; already-parsed nodes still render instantly. Builds are single-flight and the most recent selection always wins, so rapid clicking never races or shows a stale table. https://claude.ai/code/session_013kBiVXftgoEsyGVyrvfGok --- src/libmoex/viewnode/ViewNode.h | 2 ++ src/src/controller/Workspace.cpp | 8 +++++++ src/src/controller/Workspace.h | 2 ++ src/src/dock/LayoutDockWidget.cpp | 40 +++++++++++++++++++++++++++++-- src/src/dock/LayoutDockWidget.h | 3 +++ 5 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/libmoex/viewnode/ViewNode.h b/src/libmoex/viewnode/ViewNode.h index a66b462..cf6c824 100644 --- a/src/libmoex/viewnode/ViewNode.h +++ b/src/libmoex/viewnode/ViewNode.h @@ -150,6 +150,8 @@ class ViewNode { void SetViewData(BinaryViewDataPtr data){binary_ = data;} void SetViewData(TableViewDataPtr data){table_ = data;} + bool inited() const { return inited_; } + void Init(){ if(!inited_){ inited_ = true; diff --git a/src/src/controller/Workspace.cpp b/src/src/controller/Workspace.cpp index 52479f0..2c513ac 100755 --- a/src/src/controller/Workspace.cpp +++ b/src/src/controller/Workspace.cpp @@ -38,6 +38,14 @@ void Workspace::showNode(moex::ViewNode *node) // Lazy init node->Init(); + displayNode(node); +} + +void Workspace::displayNode(moex::ViewNode *node) +{ + if(!node) + return; + // central widget ui_->main->showTableViewData(node->table().get()); diff --git a/src/src/controller/Workspace.h b/src/src/controller/Workspace.h index f3bd21e..627a507 100755 --- a/src/src/controller/Workspace.h +++ b/src/src/controller/Workspace.h @@ -42,6 +42,8 @@ class Workspace QString currentFilePath() const { return currentFilePath_; } void addLog(const QString & log); void showNode(moex::ViewNode *node); + // Display an already-initialized node (GUI thread only; performs no parsing). + void displayNode(moex::ViewNode *node); void selectHexRange(void *data,uint64_t size); void clearHexSelection(); void setInformation(const QString & info); diff --git a/src/src/dock/LayoutDockWidget.cpp b/src/src/dock/LayoutDockWidget.cpp index 207e71b..57205a4 100755 --- a/src/src/dock/LayoutDockWidget.cpp +++ b/src/src/dock/LayoutDockWidget.cpp @@ -108,8 +108,44 @@ void LayoutDockWidget::showViewNode(moex::ViewNode *node) if(!node) return; - qDebug() << QString::fromStdString(node->GetDisplayName()); - WS()->showNode(node); + // Record the latest selection so it always wins over in-flight builds. + pendingNode_ = node; + + // A build is already running; its completion handler will pick up the + // latest pendingNode_. + if(nodeBuilding_) + return; + + // Already parsed: display immediately on the GUI thread. + if(node->inited()){ + WS()->displayNode(node); + return; + } + + buildAndShowNode(node); +} + +void LayoutDockWidget::buildAndShowNode(moex::ViewNode *node) +{ + nodeBuilding_ = true; + auto *watcher = new QFutureWatcher(this); + connect(watcher, &QFutureWatcher::finished, this, [this, watcher](){ + watcher->deleteLater(); + nodeBuilding_ = false; + + moex::ViewNode *latest = pendingNode_; + if(latest == nullptr) + return; + + // If the latest selection still needs parsing, build it; otherwise it + // is ready to display now. + if(!latest->inited()){ + buildAndShowNode(latest); + } else { + WS()->displayNode(latest); + } + }); + watcher->setFuture(QtConcurrent::run([node](){ node->Init(); })); } void LayoutDockWidget::clickedTreeNode(QModelIndex index) diff --git a/src/src/dock/LayoutDockWidget.h b/src/src/dock/LayoutDockWidget.h index ed74d3c..e1d3ae2 100755 --- a/src/src/dock/LayoutDockWidget.h +++ b/src/src/dock/LayoutDockWidget.h @@ -21,11 +21,14 @@ class LayoutDockWidget : public QDockWidget LayoutTreeView *treeView; LayoutController *controller; bool parsing_ = false; + bool nodeBuilding_ = false; + moex::ViewNode *pendingNode_ = nullptr; private: void showViewNode(moex::ViewNode * node); void showTreeIndex(const QModelIndex &index); void populateTree(); + void buildAndShowNode(moex::ViewNode *node); public: explicit LayoutDockWidget(QWidget *parent = 0); From bace6ac5e3ebf5391d35bc4e7ab31c43c85c2e1e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 20 May 2026 22:19:56 +0000 Subject: [PATCH 08/11] Add a search box to filter the layout tree There was no way to search across the whole structure tree; only individual tables could be filtered. Add a search field above the layout tree backed by a recursive QSortFilterProxyModel: typing keeps matching nodes and their ancestors and expands the tree to reveal them, clearing restores the default expansion. Selection handling maps proxy indices back to the source model so node activation keeps working. https://claude.ai/code/session_013kBiVXftgoEsyGVyrvfGok --- src/src/dock/LayoutDockWidget.cpp | 49 ++++++++++++++++++++++++++++--- src/src/dock/LayoutDockWidget.h | 5 ++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/src/dock/LayoutDockWidget.cpp b/src/src/dock/LayoutDockWidget.cpp index 57205a4..27cd369 100755 --- a/src/src/dock/LayoutDockWidget.cpp +++ b/src/src/dock/LayoutDockWidget.cpp @@ -8,10 +8,13 @@ #include "src/controller/LayoutController.h" #include +#include #include #include #include #include +#include +#include LayoutDockWidget::LayoutDockWidget(QWidget *parent) : QDockWidget(parent) { @@ -19,6 +22,15 @@ LayoutDockWidget::LayoutDockWidget(QWidget *parent) : QDockWidget(parent) controller = nullptr; + proxyModel = new QSortFilterProxyModel(this); + proxyModel->setRecursiveFilteringEnabled(true); + proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + proxyModel->setFilterKeyColumn(0); + + searchEdit = new QLineEdit(this); + searchEdit->setPlaceholderText(tr("Search layout...")); + searchEdit->setClearButtonEnabled(true); + treeView = new LayoutTreeView(this); treeView->setMinimumWidth(200); treeView->setSizePolicy(QSizePolicy::Preferred,QSizePolicy::Expanding); @@ -28,8 +40,17 @@ LayoutDockWidget::LayoutDockWidget(QWidget *parent) : QDockWidget(parent) treeView->setSelectionBehavior(QAbstractItemView::SelectRows); treeView->setAllColumnsShowFocus(true); treeView->setUniformRowHeights(true); - setWidget(treeView); + QWidget *container = new QWidget(this); + QVBoxLayout *layout = new QVBoxLayout(container); + layout->setContentsMargins(2, 2, 2, 2); + layout->setSpacing(2); + layout->addWidget(searchEdit); + layout->addWidget(treeView); + setWidget(container); + + connect(searchEdit, &QLineEdit::textChanged, + this, &LayoutDockWidget::onSearchTextChanged); connect(treeView, &QTreeView::clicked, this, &LayoutDockWidget::clickedTreeNode); connect(treeView, &QTreeView::activated, @@ -51,6 +72,7 @@ void LayoutDockWidget::openFile(const QString &filePath) // Clear the current tree while the new file parses in the background so the // UI thread stays responsive on large binaries / dyld shared caches. + searchEdit->clear(); treeView->setModel(nullptr); parsing_ = true; WS()->addLog("Start parsing " + filePath); @@ -87,7 +109,8 @@ void LayoutDockWidget::openFile(const QString &filePath) void LayoutDockWidget::populateTree() { - treeView->setModel(controller->model()); + proxyModel->setSourceModel(controller->model()); + treeView->setModel(proxyModel); treeView->setEditTriggers(QAbstractItemView::NoEditTriggers); treeView->setColumnWidth(0,300); @@ -96,12 +119,29 @@ void LayoutDockWidget::populateTree() treeView->expandToDepth(controller->getExpandDepth()); - QModelIndex rootIndex = controller->model()->index(0, 0); + QModelIndex rootIndex = proxyModel->mapFromSource(controller->model()->index(0, 0)); treeView->setCurrentIndex(rootIndex); treeView->scrollTo(rootIndex, QAbstractItemView::PositionAtTop); treeView->setFocus(Qt::OtherFocusReason); } +void LayoutDockWidget::onSearchTextChanged(const QString &text) +{ + if(!proxyModel) + return; + + proxyModel->setFilterFixedString(text); + + if(!text.isEmpty()){ + // Expand everything so matches deep in the tree become visible. + treeView->expandAll(); + } else { + treeView->collapseAll(); + if(controller) + treeView->expandToDepth(controller->getExpandDepth()); + } +} + void LayoutDockWidget::showViewNode(moex::ViewNode *node) { @@ -158,7 +198,8 @@ void LayoutDockWidget::showTreeIndex(const QModelIndex &index) if(!controller || !index.isValid()) return; - QStandardItem *item = controller->model()->itemFromIndex(index); + const QModelIndex sourceIndex = proxyModel->mapToSource(index); + QStandardItem *item = controller->model()->itemFromIndex(sourceIndex); if(!item) return; diff --git a/src/src/dock/LayoutDockWidget.h b/src/src/dock/LayoutDockWidget.h index e1d3ae2..a1e815b 100755 --- a/src/src/dock/LayoutDockWidget.h +++ b/src/src/dock/LayoutDockWidget.h @@ -12,6 +12,8 @@ #include "../widget/LayoutTreeView.h" class LayoutController; +class QLineEdit; +class QSortFilterProxyModel; class LayoutDockWidget : public QDockWidget @@ -20,6 +22,8 @@ class LayoutDockWidget : public QDockWidget private: LayoutTreeView *treeView; LayoutController *controller; + QLineEdit *searchEdit; + QSortFilterProxyModel *proxyModel; bool parsing_ = false; bool nodeBuilding_ = false; moex::ViewNode *pendingNode_ = nullptr; @@ -39,6 +43,7 @@ class LayoutDockWidget : public QDockWidget public slots: void clickedTreeNode(QModelIndex index); void currentTreeNodeChanged(const QModelIndex ¤t, const QModelIndex &previous); + void onSearchTextChanged(const QString &text); }; #endif // LAYOUTVIEW_H From 51b95dd8d8f13dd7aa3386f2bc85ded99f722ccb Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 20 May 2026 22:30:23 +0000 Subject: [PATCH 09/11] Search opened table contents, not just node names The layout search only matched node display names. Add a custom filter proxy that also searches the cell contents of any node whose view data has already been built, so once a table (symbols, strings, ...) has been opened its rows become searchable from the layout search box. Nodes that have not been parsed are still matched by name, so lazy loading is preserved and no node is forced to parse just to search. https://claude.ai/code/session_013kBiVXftgoEsyGVyrvfGok --- src/src/controller/LayoutFilterProxyModel.h | 83 +++++++++++++++++++++ src/src/dock/LayoutDockWidget.cpp | 11 +-- src/src/dock/LayoutDockWidget.h | 4 +- 3 files changed, 89 insertions(+), 9 deletions(-) create mode 100644 src/src/controller/LayoutFilterProxyModel.h diff --git a/src/src/controller/LayoutFilterProxyModel.h b/src/src/controller/LayoutFilterProxyModel.h new file mode 100644 index 0000000..b9cf784 --- /dev/null +++ b/src/src/controller/LayoutFilterProxyModel.h @@ -0,0 +1,83 @@ +// +// Created by everettjf +// Copyright © 2017 everettjf. All rights reserved. +// +#ifndef LAYOUTFILTERPROXYMODEL_H +#define LAYOUTFILTERPROXYMODEL_H + +#include +#include +#include +#include +#include +#include + +// Filters the layout tree by node display name and, for nodes whose view data +// has already been built, by their table cell contents. Already-parsed nodes +// are searched without forcing every node to be parsed, so lazy loading is +// preserved. Recursive filtering keeps the ancestors of any match. +class LayoutFilterProxyModel : public QSortFilterProxyModel { +public: + explicit LayoutFilterProxyModel(QObject *parent = nullptr) + : QSortFilterProxyModel(parent) { + setRecursiveFilteringEnabled(true); + setFilterCaseSensitivity(Qt::CaseInsensitive); + setFilterKeyColumn(0); + } + + void setPattern(const QString &pattern) { + if (pattern_ == pattern) + return; + pattern_ = pattern; + invalidateFilter(); + } + +protected: + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override { + if (pattern_.isEmpty()) + return true; + + QAbstractItemModel *src = sourceModel(); + if (!src) + return true; + + const QModelIndex idx = src->index(source_row, 0, source_parent); + if (!idx.isValid()) + return false; + + const QString name = src->data(idx, Qt::DisplayRole).toString(); + if (name.contains(pattern_, Qt::CaseInsensitive)) + return true; + + // Content search for nodes that have already been parsed. + const QVariant v = src->data(idx, Qt::UserRole + 1); + auto *node = reinterpret_cast(v.value()); + if (node != nullptr && node->inited() && node->table()) { + const std::string needle = pattern_.toLower().toStdString(); + const moex::TableViewData *table = node->table().get(); + for (const auto &row : table->rows) { + for (const auto &item : row->items) { + if (ContainsLower(item->data, needle)) + return true; + } + } + } + return false; + } + +private: + static bool ContainsLower(const std::string &haystack, const std::string &needle_lower) { + if (needle_lower.empty()) + return true; + std::string lower; + lower.resize(haystack.size()); + for (std::size_t i = 0; i < haystack.size(); ++i) { + lower[i] = static_cast(std::tolower(static_cast(haystack[i]))); + } + return lower.find(needle_lower) != std::string::npos; + } + + QString pattern_; +}; + +#endif // LAYOUTFILTERPROXYMODEL_H diff --git a/src/src/dock/LayoutDockWidget.cpp b/src/src/dock/LayoutDockWidget.cpp index 27cd369..b6e6ada 100755 --- a/src/src/dock/LayoutDockWidget.cpp +++ b/src/src/dock/LayoutDockWidget.cpp @@ -6,6 +6,7 @@ #include "src/utility/Utility.h" #include "src/controller/Workspace.h" #include "src/controller/LayoutController.h" +#include "src/controller/LayoutFilterProxyModel.h" #include #include @@ -14,7 +15,6 @@ #include #include #include -#include LayoutDockWidget::LayoutDockWidget(QWidget *parent) : QDockWidget(parent) { @@ -22,13 +22,10 @@ LayoutDockWidget::LayoutDockWidget(QWidget *parent) : QDockWidget(parent) controller = nullptr; - proxyModel = new QSortFilterProxyModel(this); - proxyModel->setRecursiveFilteringEnabled(true); - proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); - proxyModel->setFilterKeyColumn(0); + proxyModel = new LayoutFilterProxyModel(this); searchEdit = new QLineEdit(this); - searchEdit->setPlaceholderText(tr("Search layout...")); + searchEdit->setPlaceholderText(tr("Search layout and opened tables...")); searchEdit->setClearButtonEnabled(true); treeView = new LayoutTreeView(this); @@ -130,7 +127,7 @@ void LayoutDockWidget::onSearchTextChanged(const QString &text) if(!proxyModel) return; - proxyModel->setFilterFixedString(text); + proxyModel->setPattern(text); if(!text.isEmpty()){ // Expand everything so matches deep in the tree become visible. diff --git a/src/src/dock/LayoutDockWidget.h b/src/src/dock/LayoutDockWidget.h index a1e815b..f6d2b5a 100755 --- a/src/src/dock/LayoutDockWidget.h +++ b/src/src/dock/LayoutDockWidget.h @@ -12,8 +12,8 @@ #include "../widget/LayoutTreeView.h" class LayoutController; +class LayoutFilterProxyModel; class QLineEdit; -class QSortFilterProxyModel; class LayoutDockWidget : public QDockWidget @@ -23,7 +23,7 @@ class LayoutDockWidget : public QDockWidget LayoutTreeView *treeView; LayoutController *controller; QLineEdit *searchEdit; - QSortFilterProxyModel *proxyModel; + LayoutFilterProxyModel *proxyModel; bool parsing_ = false; bool nodeBuilding_ = false; moex::ViewNode *pendingNode_ = nullptr; From f04d2a5ef7b145088ce9bcfdd56285c3e2d29b43 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 07:25:00 +0000 Subject: [PATCH 10/11] Fix Capstone xref code path build The disassembly-based xref scanner did not compile against modern Capstone: cs_regs_access requires uint16_t register arrays (cs_regs is uint16_t[64], not uint8_t), and ReadPointerAtVm takes a MachHeader* but was passed the MachHeaderPtr shared_ptr. Use the correct register array type and pass the raw pointer so a Capstone-enabled build succeeds and the xref report resolves call/jump targets. https://claude.ai/code/session_013kBiVXftgoEsyGVyrvfGok --- src/libmoex/viewnode/views/XrefViewNode.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/libmoex/viewnode/views/XrefViewNode.cpp b/src/libmoex/viewnode/views/XrefViewNode.cpp index e0c85d0..1202e0c 100644 --- a/src/libmoex/viewnode/views/XrefViewNode.cpp +++ b/src/libmoex/viewnode/views/XrefViewNode.cpp @@ -194,8 +194,8 @@ void XrefViewNode::InitViewDatas() } if (arch == CS_ARCH_ARM64) { - uint8_t regs_read[36] = {0}; - uint8_t regs_write[36] = {0}; + uint16_t regs_read[64] = {0}; + uint16_t regs_write[64] = {0}; uint8_t read_count = 0; uint8_t write_count = 0; if (cs_regs_access(handle, &ci, regs_read, &read_count, regs_write, &write_count) == 0) { @@ -272,7 +272,7 @@ void XrefViewNode::InitViewDatas() if (hit != arm64_reg_targets.end()) { const uint64_t mem_vmaddr = static_cast(static_cast(hit->second) + arm.operands[1].mem.disp); uint64_t pointed = 0; - if (ReadPointerAtVm(mh_, mem_vmaddr, true, pointed) && pointed != 0) { + if (ReadPointerAtVm(mh_.get(),mem_vmaddr, true, pointed) && pointed != 0) { arm64_reg_targets[arm.operands[0].reg] = pointed; } } @@ -282,15 +282,15 @@ void XrefViewNode::InitViewDatas() arm.operands[1].type == ARM64_OP_IMM) { const uint64_t mem_vmaddr = static_cast(arm.operands[1].imm); uint64_t pointed = 0; - if (ReadPointerAtVm(mh_, mem_vmaddr, true, pointed) && pointed != 0) { + if (ReadPointerAtVm(mh_.get(),mem_vmaddr, true, pointed) && pointed != 0) { arm64_reg_targets[arm.operands[0].reg] = pointed; } } } if (arch == CS_ARCH_X86) { - uint8_t regs_read[36] = {0}; - uint8_t regs_write[36] = {0}; + uint16_t regs_read[64] = {0}; + uint16_t regs_write[64] = {0}; uint8_t read_count = 0; uint8_t write_count = 0; if (cs_regs_access(handle, &ci, regs_read, &read_count, regs_write, &write_count) == 0) { @@ -328,7 +328,7 @@ void XrefViewNode::InitViewDatas() (x.operands[1].mem.base == X86_REG_RIP || x.operands[1].mem.base == X86_REG_EIP)) { const uint64_t mem_vmaddr = static_cast(static_cast(ci.address) + ci.size + x.operands[1].mem.disp); uint64_t pointed = 0; - if (ReadPointerAtVm(mh_, mem_vmaddr, is64, pointed) && pointed != 0) { + if (ReadPointerAtVm(mh_.get(),mem_vmaddr, is64, pointed) && pointed != 0) { x86_reg_targets[x.operands[0].reg] = pointed; } } @@ -367,7 +367,7 @@ void XrefViewNode::InitViewDatas() const uint64_t mem_vmaddr = static_cast(static_cast(ci.address) + ci.size + op.mem.disp); uint64_t target = 0; const bool is64ptr = (mode == CS_MODE_64); - if (ReadPointerAtVm(mh_, mem_vmaddr, is64ptr, target) && target != 0) { + if (ReadPointerAtVm(mh_.get(),mem_vmaddr, is64ptr, target) && target != 0) { add_ref(target, {"__TEXT/__text", is_call ? "call-ripmem" : "jump-ripmem", sect->GetRAW(sect->GetOffset() + (ci.address - base)), ci.address}); } @@ -378,7 +378,7 @@ void XrefViewNode::InitViewDatas() static_cast(hit->second) + op.mem.disp); uint64_t target = 0; const bool is64ptr = (mode == CS_MODE_64); - if (ReadPointerAtVm(mh_, mem_vmaddr, is64ptr, target) && target != 0) { + if (ReadPointerAtVm(mh_.get(),mem_vmaddr, is64ptr, target) && target != 0) { add_ref(target, {"__TEXT/__text", is_call ? "call-regmem" : "jump-regmem", sect->GetRAW(sect->GetOffset() + (ci.address - base)), ci.address}); } From 2804356b76b5ed76a15a0f182be8cc57beba7e22 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 07:25:33 +0000 Subject: [PATCH 11/11] Add Linux CI workflow Build the project (Qt6 + Capstone) and run the regression suite on Ubuntu for every push to master and every pull request, so the Linux build and the parser hardening / crash regressions stay green. https://claude.ai/code/session_013kBiVXftgoEsyGVyrvfGok --- .github/workflows/linux.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/linux.yml diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml new file mode 100644 index 0000000..7475091 --- /dev/null +++ b/.github/workflows/linux.yml @@ -0,0 +1,33 @@ +name: Linux + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +jobs: + build-and-test: + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + cmake \ + g++ \ + qt6-base-dev \ + libgl1-mesa-dev \ + libcapstone-dev + + - name: Configure + run: cmake -S src -B build -DCMAKE_BUILD_TYPE=Release + + - name: Build + run: cmake --build build -j"$(nproc)" + + - name: Regression suite + run: tests/regression/run_all.sh