-
Notifications
You must be signed in to change notification settings - Fork 55
Loader: extend_text() does binary shifting — must rewrite as slack-space-only before wiring to patch() #9
Description
Summary
extend_text() and find_text_gaps() are implemented on all three platforms (ELF, MachO, PE) but cannot be safely wired into patch() in their current form. All three implementations perform binary shifting (growing .text and pushing subsequent data forward), which corrupts binary metadata.
Current stage: patch() correctly uses add_segment() to create a dedicated .vmpilot / __VMPILOT / .vmpltt section. This is the right approach for v1.
This issue tracks the future work needed to support stealth .text extension (D15§13.2) in v2.
Problem Analysis
All three platforms do binary shifting
| Platform | Mechanism | What breaks |
|---|---|---|
| ELF | text_sec->set_data(bigger_buf) → ELFIO save() auto-relayouts entire file, pushing all subsequent sections forward |
Our manually-set PT_LOAD file_size/memory_size may conflict with ELFIO's relayout; no control over cascading effects |
| MachO | buf_.insert() at __text end + cascading fixup loop |
Misses LC_SYMTAB, LC_DYSYMTAB, LC_FUNCTION_STARTS, LC_DATA_IN_CODE offset updates; section VA (addr field) not updated — dyld will crash |
| PE | sec->set_data(bigger_buf) grows section data |
Does NOT update SizeOfRawData, SizeOfCode, or SizeOfImage in PE Optional Header — Windows loader will reject or AV will flag |
Measured slack space in test binaries
| Binary | Gap after .text | Source |
|---|---|---|
| MachO ARM64 (basic_binary.Darwin.arm64) | 0 bytes — __text ends exactly where __stubs begins |
otool -l measurement |
| PE x86-64 (basic_binary.Windows.x86_64.exe) | 255 bytes — VirtualSize=0xF01, RawSize=0x1000 (FileAlignment=0x200) |
PE header parsing |
| ELF x86-64 | Typically 0–few dozen bytes of page alignment padding | Linker-dependent |
MachO has near-zero slack
Within __TEXT segment, Apple's linker packs sections (__text, __stubs, __stub_helper, __cstring, __unwind_info) back-to-back. The only slack is at the very end of the segment to the page boundary — but that's shared with __unwind_info and touching it breaks compact unwind.
Correct Future Design: Slack Space Utilization (no shifting)
Per previous design discussion, extend_text() should be strictly redefined as "use existing padding without moving anything":
PE
slack = SizeOfRawData - VirtualSize
if (payload_size > slack) return false // fallback to add_segment
// Write into existing padding zone
// Update: VirtualSize += payload_size, SizeOfCode += payload_size
// Do NOT touch SizeOfRawData or any other section
ELF
slack = next_section.sh_offset - (.text.sh_offset + .text.sh_size)
if (payload_size > slack) return false
// Direct byte write into padding zone (NOT set_data with larger buffer)
// Update: .text sh_size, PT_LOAD memsz/filesz
MachO
slack = segment.fileoff + segment.filesize - last_section_file_end
if (payload_size > slack) return false
// Write into segment tail padding
// Update last section size or add zero-overhead sub-section
find_text_gaps() improvements
Current find_text_gaps() scans for filler bytes (0x90/0xCC/0x00) but has false positive risk:
- 0x00 runs may be embedded data (jump tables, literals), not gaps
- No cross-reference check (gap might be a branch/relocation target)
Future improvement: Use SDK's NativeSymbolTable (has address + size + type=FUNC per symbol) to identify inter-function boundaries. Only trust gaps that:
- Fall between
sym[i].address + sym[i].sizeandsym[i+1].address - Are filled with 0xCC or 0x90 (not 0x00)
patch() wire-in strategy (future)
When extend_text is rewritten:
1. Try extend_text(payload, slack_only=true)
2. If fails: try find_text_gaps() for scatter placement
3. If fails: fallback to add_segment() (current behavior, always works)
Current test coverage
| Platform | extend_text tests | find_text_gaps tests | MachO tests |
|---|---|---|---|
| ELF | 6 tests | 3 tests | — |
| PE | 1 test | 2 tests | — |
| MachO | 0 tests | included in E2E | 0 extend_text tests |
Action items
- Rewrite
extend_text()on all 3 platforms as slack-space-only (no binary shifting) - Add boundary checks: if payload > slack → return error (not silently corrupt)
- Fix ELF padding filler from 0x00 to 0xCC (INT3)
- Fix MachO: remove cascading fixup logic entirely
- Fix PE: add
SizeOfCode/SizeOfImageheader updates - Add MachO extend_text tests
- Integrate
NativeSymbolTableintofind_text_gaps()for safe inter-function gap detection - Wire into
patch()as primary strategy withadd_segment()fallback - Consider whether current (broken) extend_text code should be removed or gated behind a flag to prevent accidental use
References
- D15§13.2: "A dedicated section is a neon sign for static analysis tools. Code cave injection: stubs placed in alignment padding, function gaps, and dead code regions within .text"
- D13§D1: Bytecode injection location options
- SDK
NativeSymbolTable:sdk/include/core/NativeSymbolTable.hpp