Skip to content

Defining user symbols from more than one thread at the same time, with no undo transaction open, crashes the core #8531

Description

@WeiN76LQh

The following issue was identified, triaged and written by Claude Fable 5.1, I have read through it to make sure the information is coherent and useful.


Version and Platform (required):

  • Binary Ninja Version: 6.1.10594-dev
  • Edition: Ultimate
  • OS: macOS
  • OS Version: 26.5.1
  • CPU Architecture: Apple Silicon (arm64)

Bug Description:
Defining user symbols from more than one thread at the same time, with no undo transaction open, crashes the core. With four Python threads each redefining the user symbol at every function start of /bin/ls, the headless process dies on most runs, either with a segmentation fault or with libmalloc aborting on a bad free: ___BUG_IN_CLIENT_OF_LIBMALLOC_POINTER_BEING_FREED_WAS_NOT_ALLOCATED, thirteen frames below BNDefineUserSymbol.

The same loop on one thread never crashes. The same loop with define_auto_symbol in place of define_user_symbol never crashes with eight threads. And the same loop headless with the whole run inside one begin_undo_actions() / commit_undo_actions() pair never crashes either, while the GUI Python console, which runs each command inside a transaction, survived 17 runs. So the fault looks to be in the undo entry that each user-symbol call opens and commits for itself when no transaction is open. The threads do not need to touch the same addresses: giving each thread its own disjoint set of function starts still crashes 2 of 5 runs.

Steps To Reproduce:

  1. Save the script below as concurrent_user_symbols.py.
  2. Run it headless with any Mach-O: python3 concurrent_user_symbols.py /bin/ls
  3. It dies within a second or two. If a run happens to survive, run it again; here it crashed 5 of 5 runs with four threads.
"""Redefining user symbols from several threads at once crashes the core.

Run with any Mach-O, e.g.:  python3 concurrent_user_symbols.py /bin/ls

Each thread walks every function start, undefines whatever symbol is there and defines a user
symbol with a fresh name, four times over. Four threads crash the process on most runs; the same
loop on one thread never does, and neither does the same loop with define_auto_symbol in place of
define_user_symbol.
"""
import sys
import threading

import binaryninja as bn

bv = bn.load(sys.argv[1])
bv.update_analysis_and_wait()
starts = [f.start for f in bv.functions]


def redefine(tag):
    for round in range(4):
        for start in starts:
            existing = bv.get_symbol_at(start)
            if existing is not None:
                if existing.auto:
                    bv.undefine_auto_symbol(existing)
                else:
                    bv.undefine_user_symbol(existing)
            bv.define_user_symbol(bn.Symbol(bn.SymbolType.FunctionSymbol, start, f"{tag}_{round}_{start:x}"))


threads = [threading.Thread(target=redefine, args=(f"t{i}",)) for i in range(4)]
for t in threads:
    t.start()
for t in threads:
    t.join()
print("no crash this run; run it again")

Crash frequency over five runs per configuration, headless, all with BN_DISABLE_USER_PLUGINS=1:

Threads Operation per address Undo transaction Crashes
4 undefine existing, then define_user_symbol none 5 / 5
4 define_user_symbol only none 2 / 5
4 undefine, then define_user_symbol, each thread on disjoint addresses none 2 / 5
4 undefine, then define_user_symbol one, opened before the threads start and committed after they join 0 / 5
4 undefine, then define_user_symbol one per thread 2 / 5
2 undefine, then define_user_symbol none 0 / 5
1, with update_analysis() running alongside undefine, then define_user_symbol none 0 / 5
8 define_auto_symbol only none 0 / 5

Expected Behavior:
Either the calls are safe to make from several threads, as the rest of the BinaryView API is, or the documentation says user-symbol definition must be serialised by the caller, or made inside an undo transaction. Silent memory corruption is neither.

Screenshots/Video Recording:
Not applicable; the crashing thread from the macOS crash report of one run (SIGABRT variant):

exception: EXC_CRASH SIGABRT
  0 libsystem_kernel.dylib             __pthread_kill + 8
  1 libsystem_pthread.dylib            pthread_kill + 296
  2 libsystem_c.dylib                  abort + 148
  3 libsystem_malloc.dylib             malloc_vreport + 892
  4 libsystem_malloc.dylib             malloc_report + 64
  5 libsystem_malloc.dylib             ___BUG_IN_CLIENT_OF_LIBMALLOC_POINTER_BEING_FREED_WAS_NOT_ALLOCATED + 76
  6 libbinaryninjacore.1.dylib         +0xe15010
  7 libbinaryninjacore.1.dylib         +0xe12810
  8 libbinaryninjacore.1.dylib         +0xe123a4
  9 libbinaryninjacore.1.dylib         +0x5a2c08
 10 libbinaryninjacore.1.dylib         +0x95b7a4
 11 libbinaryninjacore.1.dylib         +0x58ef30
 12 libbinaryninjacore.1.dylib         +0x28d45c
 13 libbinaryninjacore.1.dylib         BNDefineUserSymbol + 60
 14 libffi.dylib                       ffi_call_SYSV + 80
 15 libffi.dylib                       ffi_call_int + 1220
 16 _ctypes.cpython-313-darwin.so      _ctypes_callproc + 732
 17 _ctypes.cpython-313-darwin.so      PyCFuncPtr_call + 264
 18 Python                             _PyObject_MakeTpCall + 120
 19 Python                             _PyEval_EvalFrameDefault + 19532
 20 Python                             method_vectorcall + 152
 21 Python                             thread_run + 180

Other runs die with EXC_BAD_ACCESS / SIGSEGV in the same region of the core (frames +0xe15010 and +0xe123a4 recur). The full .ips files can be attached on request.

Binary:
Not binary-specific: /bin/ls from macOS 26.5.1 was used, and any Mach-O with a few dozen functions reproduces it.

Additional Information:
The GUI Python console does not reproduce this: the same loop pasted there survived 17 runs (16 with four threads, one with eight). Headless, it also stops reproducing when the whole run is wrapped in a single bv.begin_undo_actions() / bv.commit_undo_actions() pair (0 of 5), while one transaction per thread still crashes (2 of 5). So the fault appears to be in the undo entry each user-symbol call creates and commits for itself when no transaction is open, which is the situation in every headless script and never in the console. Python 3.13 headless with the API module from the application bundle and no user plugins loaded. The undefine-then-define pair is the shape that crashes most reliably, but define_user_symbol alone from four threads is enough.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions