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 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/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/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 6e178e1..a5b84a7 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 }; @@ -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()); } }; 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/ViewNode.h b/src/libmoex/viewnode/ViewNode.h index b8f2c09..cf6c824 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); } @@ -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/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)}); + } } 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/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/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}); } 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/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/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/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/src/src/dock/LayoutDockWidget.cpp b/src/src/dock/LayoutDockWidget.cpp index b73a242..b6e6ada 100755 --- a/src/src/dock/LayoutDockWidget.cpp +++ b/src/src/dock/LayoutDockWidget.cpp @@ -6,10 +6,15 @@ #include "src/utility/Utility.h" #include "src/controller/Workspace.h" #include "src/controller/LayoutController.h" +#include "src/controller/LayoutFilterProxyModel.h" #include +#include #include #include +#include +#include +#include LayoutDockWidget::LayoutDockWidget(QWidget *parent) : QDockWidget(parent) { @@ -17,6 +22,12 @@ LayoutDockWidget::LayoutDockWidget(QWidget *parent) : QDockWidget(parent) controller = nullptr; + proxyModel = new LayoutFilterProxyModel(this); + + searchEdit = new QLineEdit(this); + searchEdit->setPlaceholderText(tr("Search layout and opened tables...")); + searchEdit->setClearButtonEnabled(true); + treeView = new LayoutTreeView(this); treeView->setMinimumWidth(200); treeView->setSizePolicy(QSizePolicy::Preferred,QSizePolicy::Expanding); @@ -26,8 +37,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, @@ -38,17 +58,56 @@ 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; - } - treeView->setModel(controller->model()); + // 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); + + 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() +{ + proxyModel->setSourceModel(controller->model()); + treeView->setModel(proxyModel); treeView->setEditTriggers(QAbstractItemView::NoEditTriggers); treeView->setColumnWidth(0,300); @@ -57,20 +116,73 @@ void LayoutDockWidget::openFile(const QString &filePath) 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->setPattern(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) { 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) @@ -83,7 +195,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 7034b36..f6d2b5a 100755 --- a/src/src/dock/LayoutDockWidget.h +++ b/src/src/dock/LayoutDockWidget.h @@ -12,6 +12,8 @@ #include "../widget/LayoutTreeView.h" class LayoutController; +class LayoutFilterProxyModel; +class QLineEdit; class LayoutDockWidget : public QDockWidget @@ -20,10 +22,17 @@ class LayoutDockWidget : public QDockWidget private: LayoutTreeView *treeView; LayoutController *controller; + QLineEdit *searchEdit; + LayoutFilterProxyModel *proxyModel; + 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); @@ -34,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 diff --git a/tests/regression/run_cli_smoke.sh b/tests/regression/run_cli_smoke.sh index b3ecaf3..b7b80c7 100755 --- a/tests/regression/run_cli_smoke.sh +++ b/tests/regression/run_cli_smoke.sh @@ -4,19 +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 @@ -74,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 @@ -90,4 +94,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" 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