Skip to content

Mach-O: Answer the section predicates from the section - #762

Merged
ltfish merged 2 commits into
masterfrom
feature/macho-instr-sections
Aug 18, 2026
Merged

Mach-O: Answer the section predicates from the section#762
ltfish merged 2 commits into
masterfrom
feature/macho-instr-sections

Conversation

@zardus

@zardus zardus commented Aug 17, 2026

Copy link
Copy Markdown
Member

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

MachOSection answers two questions about a section with something that is not
the section: is_executable forwards to the parent segment, and
only_contains_uninitialized_data returns a hardcoded False. ld64 marks the
whole of __TEXT r-x, so the first reports every constant pool, string
literal and unwind table as executable and CFGFast decodes them as code.

Both now come from the section's own flags, as ELFSection and PESection
already do: the S_ATTR_*INSTRUCTIONS attributes for the first, the zero-fill
section types for the second. Nothing reads page protection from a Section
memory setup uses segments.

Covered by tests over tests/x86_64/fauxware.macho, whose __TEXT holds
__cstring and __unwind_info beside its code, and
tests/aarch64/dyld_ios15.macho.

Reported as angr/angr#6860 and angr/angr#6856.
Validation: #762 (comment)

zardus added 2 commits August 17, 2026 05:11
MachOSection.only_contains_uninitialized_data returned a hardcoded False, so a
consumer asking whether a section holds nothing but zeroes was told no for
__bss, __common, __thread_bss and __noptrbss alike, and read the zeroes CLE pads
the segment with as if the program had put them there.

The format states it. The low byte of a section's flags is its type, which
MachOSection.type already exposes, and S_ZEROFILL, S_GB_ZEROFILL and
S_THREAD_LOCAL_ZEROFILL have no bytes in the file at all. Name the section types
in macho_enums and answer the property from them.
MachOSection.is_executable forwarded to its parent segment. ld64 marks the whole
of __TEXT r-x, so every constant pool, string literal, unwind table and Swift or
Objective-C metadata section in it reported as executable, and CFGFast scanned
them: on one x86_64 dylib 413 of 35,985 blocks began inside __const, __cstring
and __gcc_except_tab, one of them on the bytes of the string "/string".

The section states it. S_ATTR_PURE_INSTRUCTIONS and S_ATTR_SOME_INSTRUCTIONS
occupy the high bytes of a section's flags, which MachOSection.attributes already
exposes, and they separate code from data inside an r-x segment exactly as
SHF_EXECINSTR does for an ELF whose .rodata shares a load segment with .text.
ELFSection and PESection both answer this property from their own section flags;
Mach-O was the only backend that did not.

Measured across 29,184 thin Mach-O images: 33,241 of 100,001 sections in an
executable segment carry an instruction attribute, and the section names that do
are __text, __stubs, __stub_helper, __objc_stubs, __StaticInit, __symbol_stub1,
__symbolstub1, __text_startup, text_env, and __TEXT_EXEC's __text and
__auth_stubs, with nothing else. No section carries the attribute outside an
executable segment or on a zero-fill type, so the new answer is a subset of the
old one. Against the addresses the files themselves name as function entries,
all 438,738 LC_FUNCTION_STARTS entries of 704 images fall inside a section that
states instructions, and none outside.

307 of the 29,184 state no instructions anywhere: 304 relocatable objects whose
only content is __LLVM,__bitcode, __DWARF or __DATA,__data, and 3 stub dylibs.
Those files hold no instructions, so reporting none is the right answer.

This lands together with the preceding commit, which repairs
only_contains_uninitialized_data in the same class for the same reason: both
predicates answered a question about a section with something other than the
section.
@zardus

zardus commented Aug 17, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Validation record for head 410d41ca80ca67df5ea9c682a794c77f9b92e5cd against
baseline 45c6509c753d07f740099035cd41f7f473dc6f31.

  • Regression: pytest tests/test_macho.py::test_instruction_sections — fails on baseline (every __TEXT and __DATA section of fauxware.macho reports executable), passes on head
  • Regression: pytest tests/test_macho.py::test_zerofill_sections — fails on baseline (__DATA,__bss of dyld_ios15.macho reports initialized), passes on head
  • Focused: pytest tests/test_macho.py — 12 passed
  • Full suite: cle 227 passed, 9 skipped
  • Complete workspace gate, cle and angr both adopted so neither was skipped: cle 227 passed / 9 skipped; angr Python 2481 passed, 46 skipped, 2 xfailed, 260 subtests passed; angr Rust 35 passed, 0 failed; environment, workspace checks, test-inputs, all pre-commit hooks and feature-instances passed
  • Lint/type: run-ci-diff-checks.py --repository cle — pylint 10.00→10.00, 10.00→10.00, 9.93→9.94; pyright badness 0.0→0.0, 0.189→0.085, 0.055→0.049; no regressions
  • Gate skipped archinfo, pypcode, pyvex, claripy and angr-management, none of which this change touches; angr-management has two section.is_executable consumers but no Mach-O fixture, so its suite cannot observe the change

The gate's own worktree check reports a change to the shared angr/binaries
checkout, and it is not from this branch. binaries is one clone reached through
a symlink by every concurrent development instance on this host, and angr writes
an LMDB store beside any fixture it analyses, so those stores appear and
disappear under a running gate for reasons unrelated to the change under test.

In this run the difference is three such stores being removed between the
before and after snapshots — tests/mips64/libc.so.6_angr_rtdb/ and
tests/x86_64/windows/1309c8993adeb587e629615eb6838a280f0a1faa6ac74fdb11b80d5bddc1c94f_angr_rtdb/
among them. They were deleted by workspace housekeeping partway through, while
this run was between its cle and angr suites. Nothing in the branch creates or
deletes them.

Every suite passed on the exact publication state: cle 227 passed and 9 skipped;
angr Python 2481 passed, 46 skipped, 2 xfailed and 260 subtests passed in 48
minutes; angr Rust 35 passed and 0 failed; plus the environment checks, the
workspace checks, the test-input check, all configured pre-commit hooks and all
per-feature instance scenarios. The log contains no FAILED and no ERROR
line. The only non-zero result is the worktree comparison, which is measuring
the shared environment rather than this change.

An earlier attempt failed the same check in the other direction, on a store a
sibling instance created mid-run, and its log is retained beside this one. The
workspace has since been changed to default RTDB_BASE to a gitignored path per
shell, which stops this at the source for shells entered after it.

Worst single object found, x86_64 Mach-O e7c69086ad518bfd, 6,799,520 bytes:
17,047 of 46,604 blocks begin in a section stating no instructions — __const
16,911, __cstring 117, __unwind_info 18, __init_offsets 1. Its
__TEXT,__const is 2,922,769 bytes against 441,945 of __text, so 87.0% of the
scan region is stated non-instruction data, and that data is a multi-megabyte
table of ascending 16-bit words. All 1,563 of its LC_FUNCTION_STARTS entries
are inside __text/__stubs, and no flagged block is reached from a block
inside them. Detail in angr/angr#6860.

Evidence for is_executable, read from file headers with angr not loaded:

  • 29,184 thin Mach-O images: 33,241 of 100,001 sections in an executable segment carry an instruction attribute
  • section names that carry one: __text, __stubs, __stub_helper, __objc_stubs, __StaticInit, __symbol_stub1, __symbolstub1, __text_startup, text_env, __TEXT_EXEC,__text, __TEXT_EXEC,__auth_stubs, and nothing else
  • 0 sections carry one outside an executable segment, and 0 on a zero-fill type, so the new answer is a strict subset of the old
  • 704 images: all 438,738 LC_FUNCTION_STARTS entries fall inside a section stating instructions, 0 outside
  • 704 images: 157,745 of 598,974 N_SECT symbols fall outside, every one in __gcc_except_tab, __const, __cstring or Swift/Objective-C metadata
  • 307 of the 29,184 state no instructions anywhere: 304 relocatable objects holding only __LLVM,__bitcode, __DWARF or __DATA,__data, and 3 stub dylibs. angr already warns and analyses nothing when its regions cover no bytes (CFG: Warn when the regions to analyze cover no bytes angr#6825), and force_segment=True remains available
  • the census counted only sections whose segment's initprot has execute, so it understates the effect: MachOSegment.is_executable is initprot | maxprot, and on fauxware.macho that makes __DATA executable too, so __got, __data, __la_symbol_ptr and __nl_symbol_ptr reported executable before this change and do not after

A/B over every Mach-O fixture in binaries, each object in its own process, both
predicates reverted in-process for the baseline side:

  • 21 fixtures load on both sides; aarch64/IPwnKit.macho.kext loads on neither (MH_KEXT_BUNDLE unsupported)
  • 11 fixtures change: 33 blocks removed, 8 added
  • removed: __unwind_info 7, __objc_methname 16, __cstring 6, __objc_classname 1, plus one on armhf/FileProtection-05.armv7.macho that begins in the last word of __stub_helper and runs 194 bytes into __objc_methname, starting the 198-byte chain that walks the whole ObjC name table
  • added: 8 identical, a 4-byte brk #1 at the last word of __objc_stubs on eight aarch64 fixtures, inside a section stating instructions
  • 0 blocks beginning inside a section stating instructions are lost on any fixture
  • the zero-fill predicate changes no fixture's CFG on its own; its effect reaches CFG recovery through the angr-side change of JumpTableResolver never installs BSSHook on a Mach-O object, so a table base read from __bss resolves to 0 angr#6856, which is not in this pull request

Evidence for only_contains_uninitialized_data is separate and belongs to the
agent that wrote that commit: angr/angr#6856 reproduces on an x86_64 dylib where
JumpTableResolver finds no BSS region, three jump tables resolve against base 0
and one function absorbs 2,648 blocks; section rules were compared over 963
loadable objects of a 1,590-object stratified sample. That agent's cross-object
A/B of its angr-side change was still running at hand-over.

Corpus objects are identified by digest only; they come from a private dataset.
Reproduce with any clang-built macOS image whose __TEXT carries __cstring.

@angr-bot

Copy link
Copy Markdown
Member

Corpus decompilation diffs can be found at angr/dec-snapshots@master...angr/cle_762

@ltfish
ltfish merged commit f101f3c into master Aug 18, 2026
18 checks passed
@ltfish
ltfish deleted the feature/macho-instr-sections branch August 18, 2026 09:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants