Skip to content

Mach-O: Do not report a relocatable object as missing MH_TWOLEVEL - #845

Closed
zardus wants to merge 1 commit into
masterfrom
fix/macho-object-twolevel
Closed

zardus wants to merge 1 commit into
masterfrom
fix/macho-object-twolevel

Conversation

@zardus

@zardus zardus commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Problem

Loading a Mach-O relocatable object logs an error asking the reader to open an
issue. tests/x86_64/relocatable_object.macho is one of this repository's own
fixtures, loaded by tests/test_macho.py::test_relocatable_object:

ERROR cle.backends.macho.macho:167 Binary is not using MH_TWOLEVEL namespacing.This isn't properly implemented yet and will degrade results in unpredictable ways.Please open an issue if you encounter this with a binary you can share

Nothing is wrong with the object: it loads with its sections, its symbols and a
base address of 0, exactly as test_relocatable_object asserts. Relocatable
objects are the ordinary contents of a static archive or a build tree, so the
line is noise at ERROR on a supported and correct load. test_relocatable_object
loads two of them.

Root cause

MachO.__init__ reads the header flag and nothing else:

if not bool(self.flags & MH_flags.MH_TWOLEVEL):  # ensure MH_TWOLEVEL
    log.error("Binary is not using MH_TWOLEVEL namespacing....")

MH_TWOLEVEL says a linked image binds its undefined symbols against the
specific libraries it lists, rather than looking them up in a flat namespace. A
relocatable object is the static linker's input: it lists no dependent
libraries, carries no dyld information and has no imports, so no toolchain sets
the flag on one. On both fixtures deps is [], imported_libraries is
['Self'], every binding blob is absent and there are no imports, so the path
the message warns about — BIND_SPECIAL_DYLIB_FLAT_LOOKUP in
cle/backends/macho/symbol.py, which really is unhandled — cannot be reached.
The absence of the flag on an MH_OBJECT is not information.

The line came from #290, which replaced raise CLEInvalidBinaryError("Cannot handle non MH_TWOLEVEL binaries") with it because such binaries were "very
rare". #787 then made relocatable objects loadable, and every relocatable object
clears the flag.

Fix

Skip the message when the file type is MH_OBJECT, and leave every other case
alone. An image that lists libraries and still clears the flag keeps reporting:
that is what #290 was written for.

Deliberately not done: the wording and the ERROR level of the remaining message
are unchanged, and no flat-namespace binding is implemented here.

Testing

tests/test_macho.py::test_relocatable_object_is_not_reported_as_flat_namespace
loads both relocatable_object.macho fixtures and asserts that nothing at ERROR
mentions MH_TWOLEVEL, then loads tests/aarch64/langdetect_go.macho and asserts
that it still does. On master it fails on that first MH_TWOLEVEL assertion, at
the aarch64 fixture, with the message above. The before and after are in the
output comment.

Validation: #845 (comment)

session: sharpen

MH_TWOLEVEL says a linked image binds its undefined symbols against
the libraries it lists rather than looking them up in a flat
namespace. A relocatable object is the static linker's input: it
lists no libraries and binds nothing, so no toolchain sets the flag
on one and its absence says nothing about the load.

The check reads the header flag alone, so since MH_OBJECT became a
supported file type every relocatable object has logged an error
asking the reader to open an issue about a degradation that cannot
happen. Both relocatable_object.macho fixtures angr/binaries tracks
trip it, and so does cle's own test_relocatable_object.

Skip the message for MH_OBJECT. An image that lists libraries and
still clears the flag keeps it: that is the case it was written for,
and BIND_SPECIAL_DYLIB_FLAT_LOOKUP is still unhandled in symbol.py.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@zardus

zardus commented Sep 25, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Validation record for head c4df37dbc74383795ef61a736f5f4fb12620d442 against baseline 7c5e1a2c1e25524863ed5e778eef63b4d8bc56a1, which is origin/master. Measured with angr be7190ce9833487330448d6dac01b77645838df3, angr/binaries 213d9d4c310eca1be489aefe42ab91d214cd4c6a, pyvex 688cc9210601a5098d761ccd1fc125a00b5bb8fe, archinfo 3030afa4811f0496500e07c38a6952dca7574040, pypcode 559aacdc9d363fd19477d9daa40721279cd99248 and angr-management 25bc8aa222cfd514c3ccf06f1db7bd5e84da72da, all at their master heads. Python 3.12.13.

  • Regression: pytest --import-mode=append -q tests/test_macho.py — on the head 16 passed. With this branch's tests against the baseline package the new test fails, on assert ['Binary is n...ou can share'] == [], reporting ERROR cle.backends.macho.macho:macho.py:167 from the load of tests/aarch64/relocatable_object.macho, the first fixture its loop reaches.
  • Full cle suite: pytest --import-mode=append -q tests/ — on the head 277 passed, 9 skipped; against the baseline package, with this branch's tests, 1 failed, 276 passed, 9 skipped, the one failure being the new test.
  • Merge-base lint and type check, the comparison angr CI's Lint and Typecheck jobs make: pylint per changed file 10.00 to 10.00 on cle/backends/macho/macho.py and 9.95 to 9.96 on tests/test_macho.py; pyright error counts 22 to 22 and 2 to 2. Neither file gets worse on either measure.
  • Lint hooks: the repository's pre-commit hooks ran on both changed files as the commit was made — every hook passed and no file was modified.
  • Workspace gate: ./feature.sh test fix-c-17, every repository built from the branch's collection, all 14 suites ran. cle 277 passed, 9 skipped; angr 3,410 passed, 47 skipped, 2 xfailed, 452 subtests; pyvex 87 passed; pypcode 46 passed, 187 subtests; archinfo 42 passed, 34 subtests; and the workspace, test-input, test-package, pre-commit, feature-build, pysoot and angr-Rust suites all passed. Two suites failed and neither can be reached from this change; the paragraph below is the measurement, not an assurance.
  • Census over the 1,974 files angr/binaries tracks, reading file starts, fat slices and ar members: 29 files carry a Mach-O header and there are 31 headers in all. Seven clear MH_TWOLEVEL — the two relocatable_object.macho fixtures (MH_OBJECT, flags 0x2000), the two MH_OBJECT members of tests/aarch64/bsd_symdef_archive.a (flags 0x0), and three flat-namespace Go executables (tests/aarch64/langdetect_go.macho, tests/x86_64/terramate.macho, tests/aarch64/go/corpus/age-v1.3.1-darwin-arm64, all MH_EXECUTE, flags 0x200004). All four MH_OBJECT headers clear the flag and none sets it. After this change the two relocatable-object files load silently and all three images still report, file by file in the output comment.

The two suites that failed are mono and angr-management, and both were re-measured afterwards. mono is one of this workspace's own tooling suites: the test that fails compares two of the workspace's own files with each other, reads nothing from cle, and still fails with the same assertion when it is run by itself (Ran 1 test, FAILED (failures=1)). The drift it reports is an uncommitted edit to one of those two workspace files, made more than eight hours before this gate run started. angr-management reported worker 'gw3' crashed on one GUI test, with 725 passing; re-running that whole file under the same offscreen-Qt xdist command gives 22 passed, and the crashed test on its own gives 1 passed, so the crash is nondeterministic. The same angr-management revision passed all 726 of those tests in an earlier run of this gate. Neither suite reads the Mach-O backend and no cle test failed in either run.

tests/x86_64/terramate.macho does not load on either revision — the thread state in its LC_UNIXTHREAD is refused, unrelated to this change — so for that file the record is only that the MH_TWOLEVEL message, which is emitted before the refusal, is unchanged by this branch.

@zardus

zardus commented Sep 25, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

What cle logs while loading Mach-O files from angr/binaries whose own header
clears MH_TWOLEVEL, with tests/x86_64/fauxware.macho as the control that sets
it, before and after this change.

Each file is loaded with cle.Loader(path, auto_load_libs=False), and every ERROR
record mentioning MH_TWOLEVEL is printed with the line that emitted it; each of
these loads emits at most one. tests/x86_64/terramate.macho is refused by the
loader on both revisions for an unrelated reason — the thread state in its
LC_UNIXTHREAD, #727 — and the message is emitted before that, so it is shown
here too.

Before — every file that clears the flag is reported, including the two
relocatable objects, which have no dependent libraries to bind against:

cle master 7c5e1a2
tests/x86_64/relocatable_object.macho
  filetype MH_OBJECT  flags 0x2000  MH_TWOLEVEL clear  dependent libraries []
  ERROR cle.backends.macho.macho:167 Binary is not using MH_TWOLEVEL namespacing.This isn't properly implemented yet and will degrade results in unpredictable ways.Please open an issue if you encounter this with a binary you can share

tests/aarch64/relocatable_object.macho
  filetype MH_OBJECT  flags 0x2000  MH_TWOLEVEL clear  dependent libraries []
  ERROR cle.backends.macho.macho:167 Binary is not using MH_TWOLEVEL namespacing.This isn't properly implemented yet and will degrade results in unpredictable ways.Please open an issue if you encounter this with a binary you can share

tests/aarch64/langdetect_go.macho
  filetype MH_EXECUTE  flags 0x200004  MH_TWOLEVEL clear  dependent libraries ['libSystem.B.dylib', 'libresolv.9.dylib']
  ERROR cle.backends.macho.macho:167 Binary is not using MH_TWOLEVEL namespacing.This isn't properly implemented yet and will degrade results in unpredictable ways.Please open an issue if you encounter this with a binary you can share

tests/x86_64/terramate.macho
  filetype MH_EXECUTE  flags 0x200004  MH_TWOLEVEL clear  dependent libraries load refused (CLECompatibilityError: )
  ERROR cle.backends.macho.macho:167 Binary is not using MH_TWOLEVEL namespacing.This isn't properly implemented yet and will degrade results in unpredictable ways.Please open an issue if you encounter this with a binary you can share

tests/aarch64/go/corpus/age-v1.3.1-darwin-arm64
  filetype MH_EXECUTE  flags 0x200004  MH_TWOLEVEL clear  dependent libraries ['libSystem.B.dylib', 'libresolv.9.dylib', 'CoreFoundation', 'Security']
  ERROR cle.backends.macho.macho:167 Binary is not using MH_TWOLEVEL namespacing.This isn't properly implemented yet and will degrade results in unpredictable ways.Please open an issue if you encounter this with a binary you can share

tests/x86_64/fauxware.macho
  filetype MH_EXECUTE  flags 0x200085  MH_TWOLEVEL set  dependent libraries ['libSystem.B.dylib']
  (nothing logged at ERROR about MH_TWOLEVEL)

After — the relocatable objects are silent; the flat-namespace images, which do
list libraries, are reported exactly as before:

with this change, c4df37d
tests/x86_64/relocatable_object.macho
  filetype MH_OBJECT  flags 0x2000  MH_TWOLEVEL clear  dependent libraries []
  (nothing logged at ERROR about MH_TWOLEVEL)

tests/aarch64/relocatable_object.macho
  filetype MH_OBJECT  flags 0x2000  MH_TWOLEVEL clear  dependent libraries []
  (nothing logged at ERROR about MH_TWOLEVEL)

tests/aarch64/langdetect_go.macho
  filetype MH_EXECUTE  flags 0x200004  MH_TWOLEVEL clear  dependent libraries ['libSystem.B.dylib', 'libresolv.9.dylib']
  ERROR cle.backends.macho.macho:171 Binary is not using MH_TWOLEVEL namespacing.This isn't properly implemented yet and will degrade results in unpredictable ways.Please open an issue if you encounter this with a binary you can share

tests/x86_64/terramate.macho
  filetype MH_EXECUTE  flags 0x200004  MH_TWOLEVEL clear  dependent libraries load refused (CLECompatibilityError: )
  ERROR cle.backends.macho.macho:171 Binary is not using MH_TWOLEVEL namespacing.This isn't properly implemented yet and will degrade results in unpredictable ways.Please open an issue if you encounter this with a binary you can share

tests/aarch64/go/corpus/age-v1.3.1-darwin-arm64
  filetype MH_EXECUTE  flags 0x200004  MH_TWOLEVEL clear  dependent libraries ['libSystem.B.dylib', 'libresolv.9.dylib', 'CoreFoundation', 'Security']
  ERROR cle.backends.macho.macho:171 Binary is not using MH_TWOLEVEL namespacing.This isn't properly implemented yet and will degrade results in unpredictable ways.Please open an issue if you encounter this with a binary you can share

tests/x86_64/fauxware.macho
  filetype MH_EXECUTE  flags 0x200085  MH_TWOLEVEL set  dependent libraries ['libSystem.B.dylib']
  (nothing logged at ERROR about MH_TWOLEVEL)

@angr-bot

Copy link
Copy Markdown
Member

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

@zardus

zardus commented Sep 26, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Closing this pull request: we could not show a measured impact of this change on real binaries from our datasets, so we are withdrawing it.

@zardus zardus closed this Sep 26, 2026
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.

2 participants