Skip to content

Commit f6027ea

Browse files
authored
Merge pull request #29 from everettjf/claude/review-and-plan-g4p9o
2 parents d678289 + 2804356 commit f6027ea

27 files changed

Lines changed: 643 additions & 108 deletions

.github/workflows/linux.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Linux
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
workflow_dispatch:
8+
9+
jobs:
10+
build-and-test:
11+
runs-on: ubuntu-24.04
12+
steps:
13+
- name: Checkout
14+
uses: actions/checkout@v4
15+
16+
- name: Install dependencies
17+
run: |
18+
sudo apt-get update
19+
sudo apt-get install -y \
20+
cmake \
21+
g++ \
22+
qt6-base-dev \
23+
libgl1-mesa-dev \
24+
libcapstone-dev
25+
26+
- name: Configure
27+
run: cmake -S src -B build -DCMAKE_BUILD_TYPE=Release
28+
29+
- name: Build
30+
run: cmake --build build -j"$(nproc)"
31+
32+
- name: Regression suite
33+
run: tests/regression/run_all.sh

DEVELOP.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ This document is for contributors and maintainers.
44

55
## Prerequisites
66
- CMake >= 3.16
7-
- Qt 6 (recommended)
7+
- Qt 6 (recommended), components: Core, Gui, Widgets, Network, Concurrent
88
- C++14 compiler
9+
- Optional: Capstone (`libcapstone-dev`) for `__TEXT,__text` disassembly
910
- macOS release tooling: `macdeployqt`, `hdiutil`, `gh`
1011

1112
## Build
@@ -17,6 +18,16 @@ cmake --build build -j8
1718
./build/MachOExplorer
1819
```
1920

21+
### Linux
22+
```bash
23+
# Debian/Ubuntu deps: sudo apt-get install qt6-base-dev libgl1-mesa-dev
24+
# (optional disassembly: libcapstone-dev)
25+
./build_linux.sh # or: cmake -S src -B build && cmake --build build -j$(nproc)
26+
./build/MachOExplorer
27+
```
28+
On Linux the binary is `build/MachOExplorer` (no `.app` bundle). The CLI mode
29+
(`--cli`) runs without a display, so it works in headless/CI environments.
30+
2031
### Windows
2132
```powershell
2233
cmake -S src -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH="C:/Qt/6.x.x/msvcXXXX_64"

build_linux.sh

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Build MachOExplorer on Linux.
5+
#
6+
# Requirements:
7+
# - CMake >= 3.16
8+
# - Qt 6 base + concurrent (e.g. Debian/Ubuntu: qt6-base-dev)
9+
# - A C++14 compiler (g++ or clang++)
10+
# - Optional: libcapstone-dev for __TEXT,__text disassembly
11+
#
12+
# Usage:
13+
# ./build_linux.sh [build_type]
14+
# where build_type defaults to Release.
15+
16+
ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
17+
BUILD_DIR="${ROOT_DIR}/build"
18+
BUILD_TYPE="${1:-Release}"
19+
20+
cmake -S "${ROOT_DIR}/src" -B "${BUILD_DIR}" -DCMAKE_BUILD_TYPE="${BUILD_TYPE}"
21+
cmake --build "${BUILD_DIR}" -j"$(nproc)"
22+
23+
echo "---------"
24+
echo "built: ${BUILD_DIR}/MachOExplorer"
25+
echo "run: ${BUILD_DIR}/MachOExplorer"
26+
echo "cli: ${BUILD_DIR}/MachOExplorer --cli <file>"
27+
echo "---------"

src/CMakeLists.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ if(WIN32)
1616
add_compile_definitions(NOMINMAX WIN32_LEAN_AND_MEAN)
1717
endif()
1818

19-
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Gui Widgets Network)
20-
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Gui Widgets Network)
19+
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Gui Widgets Network Concurrent)
20+
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Gui Widgets Network Concurrent)
2121

2222
find_path(CAPSTONE_INCLUDE_DIR capstone/capstone.h)
2323
find_library(CAPSTONE_LIBRARY capstone)
@@ -57,6 +57,7 @@ target_link_libraries(MachOExplorer PRIVATE
5757
Qt${QT_VERSION_MAJOR}::Widgets
5858
Qt${QT_VERSION_MAJOR}::Gui
5959
Qt${QT_VERSION_MAJOR}::Network
60+
Qt${QT_VERSION_MAJOR}::Concurrent
6061
)
6162

6263
if(APPLE)

src/libmoex/node/FatHeader.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ void FatArch::Init(void *offset, NodeContextPtr &ctx) {
3838
if (object_size < sizeof(uint32_t)) {
3939
throw NodeException("Malformed Fat Mach-O: arch payload too small");
4040
}
41+
// Slices are required to be at least pointer-aligned; real fat binaries
42+
// page-align them. Reject under-aligned offsets so the embedded Mach-O
43+
// header and its load commands are not dereferenced at misaligned
44+
// addresses (undefined behaviour).
45+
if (object_offset % 8 != 0) {
46+
throw NodeException("Malformed Fat Mach-O: arch offset is misaligned");
47+
}
4148

4249
void *mach_offset = reinterpret_cast<char *>(ctx_->file_start) + object_offset;
4350
mh_ = std::make_shared<MachHeader>();

src/libmoex/node/MachHeader.cpp

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,25 @@ void MachHeader::Parse(void *offset,NodeContextPtr& ctx) {
6464
throw NodeException("Malformed Mach-O: load commands exceed file size");
6565
}
6666

67+
const uint32_t cmd_align = is64_ ? 8 : 4;
6768
qv_load_command *first_cmd = reinterpret_cast<qv_load_command*>((char*)offset + cur_datasize);
6869
qv_load_command *cur_cmd = first_cmd;
6970
uint64_t parsed_size = 0;
7071
for(uint32_t index = 0; index < cmd_count; ++index){
7172
if (parsed_size + sizeof(qv_load_command) > sizeofcmds) {
7273
throw NodeException("Malformed Mach-O: truncated load command");
7374
}
74-
if (cur_cmd->cmdsize < sizeof(qv_load_command)) {
75+
// Load commands may sit at a misaligned address in a crafted file, so
76+
// read the header through an aligned copy instead of dereferencing.
77+
qv_load_command lc_head{};
78+
memcpy(&lc_head, cur_cmd, sizeof(qv_load_command));
79+
if (lc_head.cmdsize < sizeof(qv_load_command)) {
7580
throw NodeException("Malformed Mach-O: invalid load command size");
7681
}
77-
if (parsed_size + cur_cmd->cmdsize > sizeofcmds) {
82+
if (lc_head.cmdsize % cmd_align != 0) {
83+
throw NodeException("Malformed Mach-O: misaligned load command size");
84+
}
85+
if (parsed_size + lc_head.cmdsize > sizeofcmds) {
7886
throw NodeException("Malformed Mach-O: load command size overflow");
7987
}
8088

@@ -83,8 +91,8 @@ void MachHeader::Parse(void *offset,NodeContextPtr& ctx) {
8391
loadcmds_.push_back(cmd);
8492

8593
// next
86-
parsed_size += cur_cmd->cmdsize;
87-
cur_cmd = reinterpret_cast<qv_load_command*>((char*)cur_cmd + cur_cmd->cmdsize);
94+
parsed_size += lc_head.cmdsize;
95+
cur_cmd = reinterpret_cast<qv_load_command*>((char*)cur_cmd + lc_head.cmdsize);
8896
}
8997
}
9098

src/libmoex/node/Node.h

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class NodeException : public std::exception{
2121
#ifdef __APPLE__
2222
const char* what() const _NOEXCEPT override { return error_.c_str();}
2323
#else
24-
const char* what() const override { return error_.c_str();}
24+
const char* what() const noexcept override { return error_.c_str();}
2525
#endif
2626
};
2727

@@ -34,6 +34,16 @@ struct NodeContext{
3434
};
3535
using NodeContextPtr = std::shared_ptr<NodeContext>;
3636

37+
// True when [addr, addr+size) lies entirely within the mapped file.
38+
static inline bool NodeInFile(const NodeContextPtr &ctx, const void *addr, std::size_t size){
39+
if(!ctx || ctx->file_start == nullptr) return false;
40+
const char *p = static_cast<const char*>(addr);
41+
const char *start = static_cast<const char*>(ctx->file_start);
42+
const char *end = start + ctx->file_size;
43+
if(p < start || p > end) return false;
44+
return size <= static_cast<std::size_t>(end - p);
45+
}
46+
3747
// Base class for each MachO element
3848
class Node{
3949
public:
@@ -88,6 +98,9 @@ class NodeData : public NodeOffset<T>{
8898
// Init function which should be called in every child class's Init function
8999
void Init(void *offset,NodeContextPtr & ctx){
90100
NodeOffset<T>::Init(offset,ctx);
101+
if(!NodeInFile(ctx, offset, NodeOffset<T>::DATA_SIZE())){
102+
throw NodeException("Malformed file: struct read out of bounds");
103+
}
91104
memcpy(&data_,offset,NodeOffset<T>::DATA_SIZE());
92105
}
93106
};

src/libmoex/node/Util.cpp

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -421,20 +421,27 @@ namespace util {
421421
}
422422

423423
// return next offset
424-
const char * readUnsignedLeb128(const char *cur_offset,uint64_t & data,uint32_t & occupy_size) {
424+
const char * readUnsignedLeb128(const char *cur_offset,const char *end,uint64_t & data,uint32_t & occupy_size) {
425425
const uint8_t* p = (const uint8_t*)cur_offset;
426+
const uint8_t* pend = (const uint8_t*)end;
426427

427428
uint64_t result = 0;
428429
int bit = 0;
429430

430431
do {
432+
if (p >= pend){
433+
// truncated / out of bounds
434+
data = 0;
435+
occupy_size = 0;
436+
return nullptr;
437+
}
431438
uint64_t slice = *p & 0x7f;
432439

433440
if (bit >= 64 || slice << bit >> bit != slice){
434441
// error
435442
data = 0;
436443
occupy_size = 0;
437-
return 0;
444+
return nullptr;
438445
} else {
439446
result |= (slice << bit);
440447
bit += 7;
@@ -443,30 +450,37 @@ namespace util {
443450
while (*p++ & 0x80);
444451

445452
data = result;
446-
occupy_size = p - (const uint8_t*)cur_offset;
453+
occupy_size = (uint32_t)(p - (const uint8_t*)cur_offset);
447454
return (const char *)p;
448455
}
449-
const char * readSignedLeb128(const char *cur_offset,int64_t & data,uint32_t & occupy_size){
456+
const char * readSignedLeb128(const char *cur_offset,const char *end,int64_t & data,uint32_t & occupy_size){
450457
const uint8_t* p = (const uint8_t*)cur_offset;
458+
const uint8_t* pend = (const uint8_t*)end;
451459

452460
int64_t result = 0;
453461
int bit = 0;
454462
uint8_t byte=0;
455463

456464
do {
465+
if (p >= pend || bit >= 64){
466+
// truncated / out of bounds
467+
data = 0;
468+
occupy_size = 0;
469+
return nullptr;
470+
}
457471
byte = *p++;
458-
result |= ((byte & 0x7f) << bit);
472+
result |= ((int64_t)(byte & 0x7f) << bit);
459473
bit += 7;
460474
} while (byte & 0x80);
461475

462476
// sign extend negative numbers
463-
if ( (byte & 0x40) != 0 )
477+
if ( (byte & 0x40) != 0 && bit < 64 )
464478
{
465479
result |= (-1LL) << bit;
466480
}
467481

468482
data = result;
469-
occupy_size = p - (const uint8_t*)cur_offset;
483+
occupy_size = (uint32_t)(p - (const uint8_t*)cur_offset);
470484
return (const char *)p;
471485
}
472486

src/libmoex/node/Util.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,11 @@ std::vector<std::tuple<qv_vm_prot_t,std::string>> ParseProts(qv_vm_prot_t prot);
9090
std::string FormatTimeStamp(uint32_t timestamp);
9191
std::string FormatVersion(uint32_t ver);
9292

93-
const char * readUnsignedLeb128(const char *cur_offset,uint64_t & data,uint32_t & occupy_size);
94-
const char * readSignedLeb128(const char *cur_offset,int64_t & data,uint32_t & occupy_size);
93+
// Decode (un)signed LEB128. `end` is one-past-the-last readable byte; if the
94+
// encoding would read at/past it (truncated/malformed input) the functions
95+
// return nullptr with data=0 and occupy_size=0 instead of reading out of bounds.
96+
const char * readUnsignedLeb128(const char *cur_offset,const char *end,uint64_t & data,uint32_t & occupy_size);
97+
const char * readSignedLeb128(const char *cur_offset,const char *end,int64_t & data,uint32_t & occupy_size);
9598

9699
std::vector<char*> ParseStringLiteral(char * offset,uint32_t size);
97100

0 commit comments

Comments
 (0)