Hey,
I think I have identified a use-after-free bug in libshiboken that’s bundled with IDA and is highly timing dependent.
Ignored it until now because I thought it was my own plugin causing this, but when I started looking into this, it became clear this isn’t inside my plugins (or any plugin for that matter, since this also happened with all plugins disabled).
IDA / OS / hardware context:
- IDA Pro 9.3.251224
- Apple M4 Max / 128GB memory / macOS Tahoe
- Memory pressure was around 85GB - 90GB at time of testing
- CPU pressure around 10% - 15%
IDA’s error handler consistently caught the error, but I figured running IDA in autonomous mode skips the error handler registration. Running under lldb didn’t yield any crash after 20 iterations, indicating a race condition.
After a bit of fiddling I figured I’ll run MallocGuardEdges=1 /Applications/IDA Professional 9.3.app/Contents/MacOS/ida to check if there’s some kind of buffer overflow, which yielded zero crashes. This reinforced that it was likely due to timing (since MallocGuardEdges has a non-trivial impact on timing).
I realised if the error was a segfault and it’s timing related, there must be a ref to uninitialized memory somewhere.
To make sure this wasn’t an initialization issue, and given the fact that the crash happens around 35% of the time, I suspected this crash is due to a use after free in memory that happens to be in a contested memory region. Lo and behold, MallocScribble=1 /Applications/IDA Professional 9.3.app/Contents/MacOS/ida made IDA crash 100% of the time (MallocScribble makes malloc’s free overwrite all freed memory with 0x55).
At this point my observations looked like this:
| Environment | Crash Rate |
|---|---|
| Normal | ~35% |
| MallocScribble=1 | 100% (5/5) |
| MallocGuardEdges=1 | 0% (0/5) |
This was pretty annoying to debug since I couldn’t debug it, so I tried dtrace tracing it at 5 kHz and found the last good stack to be ida -> libida.dylib -> idapython3.dylib -> Python -> PyImport_ImportModuleLevelObject -> dlopen.
Out of sheer bruteforce debugging at this point I ran MallocScribble=1 MallocStackLogging=1 MallocStackLoggingNoCompact=1 /Applications/IDA Professional 9.3.app/Contents/MacOS/ida.
This yielded:
qt.qpa.fonts: Populating font family aliases took 1043 ms. Replace uses of missing font family "Courier" with one that exists to avoid this cost.
Assertion failed: (previous_refcount >= refs), function uniquing_table_node_release_internal, file uniquing_table_mutator.c, line 1288.
Which is interesting because now it looked like a clear use-after-free or double-free bug.
I kept brute forcing and figured since lldb disabled ASLR and MallocScribble made it crash 100% of the time, I’ll try that:
for i in $(seq 1 5); do
lldb -b \
-o "settings set target.disable-aslr false" \
-o "env MallocScribble=1" \
-o "process launch -- -B" \
-o "process handle SIGSEGV -n true -p true -s true" \
-o "continue" \
-o "bt all" \
"/Applications/IDA Professional 9.3.app/Contents/MacOS/ida" 2>&1 | tail -80
echo "=== Run $i done ==="
done
That gave me:
Trying lldb with proper settings...
(lldb) target create "/Applications/IDA Professional 9.3.app/Contents/MacOS/ida"
Current executable set to '/Applications/IDA Professional 9.3.app/Contents/MacOS/ida' (arm64).
(lldb) settings set target.disable-aslr false
(lldb) env MallocScribble=1
(lldb) process launch -- -B
ida(90897,0x1ee2bf000) malloc: enabling scribbling to detect mods to free blocks
2026-01-13 16:13:17.041001+0100 ida[90897:35867907] [qt.qpa.fonts] Populating font family aliases took 620 ms. Replace uses of missing font family "Courier" with one that exists to avoid this cost.
Process 90897 launched: '/Applications/IDA Professional 9.3.app/Contents/MacOS/ida' (arm64)
Process 90897 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x5f5f796f72740060)
frame #0: 0x000000018151daa4 libsystem_platform.dylib`_platform_strlen + 4
libsystem_platform.dylib`_platform_strlen:
-> 0x18151daa4 <+4>: ldr q0, [x1]
0x18151daa8 <+8>: adr x3, 0x18151d9e0 ; ___lldb_unnamed_symbol329
0x18151daac <+12>: ldr q2, [x3], #0x10
0x18151dab0 <+16>: and x2, x0, #0xf
Target 0: (ida) stopped.
... etc. ..
Lets try again for the stack traces:
cat > /tmp/lldb_crash.lldb << 'EOF'
settings set target.disable-aslr false
env MallocScribble=1
breakpoint set -n main
run -B
breakpoint delete 1
continue
EOF
# Run with the script and capture all output
for i in $(seq 1 10); do
echo "=== Attempt $i ==="
lldb --source /tmp/lldb_crash.lldb \
"/Applications/IDA Professional 9.3.app/Contents/MacOS/ida" << 'CMDS' 2>&1 | grep -A100 "EXC_BAD_ACCESS\|stop reason" | head -60
thread backtrace
frame info
register read x0 x1 x19 x20 pc lr
quit
CMDS
if [ ${PIPESTATUS[0]} -eq 0 ]; then
echo "Exited normally"
fi
done
That gave me:
=== Attempt 2 ===
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0xffffff006e6f6970)
frame #0: 0x000000018151daa4 libsystem_platform.dylib`_platform_strlen + 4
...
(lldb) thread backtrace
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0xffffff006e6f6970)
* frame #0: 0x000000018151daa4 libsystem_platform.dylib`_platform_strlen + 4
frame #1: 0x000000012b393e84 libshiboken6.abi3.6.8.dylib`___lldb_unnamed_symbol876 + 52
frame #2: 0x000000012b395c0c libshiboken6.abi3.6.8.dylib`___lldb_unnamed_symbol883 + 128
frame #3: 0x0000000124fe1df4 Python`builtin_getattr + 136
...
=== Attempt 3 ===
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x1)
frame #0: 0x00000001292d391c libshiboken6.abi3.6.8.dylib`___lldb_unnamed_symbol945 + 112
...
(lldb) thread backtrace
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x1)
* frame #0: 0x00000001292d391c libshiboken6.abi3.6.8.dylib`___lldb_unnamed_symbol945 + 112
frame #1: 0x0000000122e5a758 Python`cfunction_call + 72
...
frame #26: 0x0000000122df37a4 Python`PyObject_CallFunctionObjArgs + 56
frame #27: 0x00000001292b7aac libshiboken6.abi3.6.8.dylib`___lldb_unnamed_symbol772 + 1544
frame #28: 0x00000001292bbe68 libshiboken6.abi3.6.8.dylib`Shiboken::initShibokenSupport(_object*) + 112
frame #29: 0x0000000129121460 Shiboken.abi3.so`PyInit_Shiboken + 304
frame #30: 0x0000000122f7858c Python`_PyImport_RunModInitFunc + 88
...
=== Attempt 10 ===
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x5f657361635f656b)
frame #0: 0x000000012b02391c libshiboken6.abi3.6.8.dylib`___lldb_unnamed_symbol945 + 112
...
I can see two different crash sites here:
- Crash in
_platform_strlen(Attempt 2): Shiboken callsstrlenon a freed string pointer - Crash in
___lldb_unnamed_symbol945(Attempts 3, 7, 10): Shiboken’s custom import hook dereferences freed memory directly
Findings from the invalid memory addresses:
0xffffff006e6f6970- scribbled memory containing “pion”0x1- near-NULL pointer0x5f657361635f656b- scribbled memory containing “ke_case_” (part ofmake_snake_case_nameinside libshiboken6.abi3.6.8.dylib)
Edit: I really wanted to find out what “pion” / “noip” was, but only found references in ./libclang.dylib, i.e. BLRNoIP, GPR64common_and_GPR64noip, etc. Looks like the pointer was originally initialized to -1 (i.e. 0xFF…). Maybe Qt’s QVariant /QString’s tagged representations (i.e. struct QString { Data *d; ... }, if LSB set, maybe small string inlining? idk but neither LE / BE variants of this string appear in any binary)?
Reading the backtrace again (attempt 3):
frame #30: Python`_PyImport_RunModInitFunc <- Python importing a C extension
frame #29: Shiboken.abi3.so`PyInit_Shiboken <- Shiboken module init entry point
frame #28: Shiboken::initShibokenSupport() <- Setting up Shiboken infrastructure
frame #27: ___lldb_unnamed_symbol772 <- Internal Shiboken function
frame #26: PyObject_CallFunctionObjArgs <- Calls back into Python
... (recursive Python import machinery via Shiboken's custom __import__ hook) ...
frame #19: ___lldb_unnamed_symbol945 + 28 <- Shiboken's import hook (earlier call)
... (more nested imports) ...
frame #0: ___lldb_unnamed_symbol945 + 112 <- CRASH in same hook (later call)
The crash occurs in ___lldb_unnamed_symbol945, which appears to be Shiboken’s custom import hook. It’s called recursively during nested module imports, and at some point it dereferences a string pointer that has already been freed.
tl;dr
The use-after-free occurs during Shiboken::initShibokenSupport() when Python is initializing PySide6. It’s a race condition in the library’s startup code where a string gets freed while Shiboken’s import hook still holds a reference to it.
But why stop here?
I wanted to find out if it’s related to threading:
CRASHES=0
for i in $(seq 1 10); do
rm -f /Users/int/dev/isa-harvester/armgen/programs/template_madness.i64 2>/dev/null
PYTHONDONTWRITEBYTECODE=1 PYTHONNOUSERSITE=1 timeout 30 /Applications/IDA\ Professional\ 9.3.app/Contents/MacOS/ida -A /Users/int/dev/isa-harvester/armgen/programs/template_madness >/dev/null 2>&1
[ $? -eq 139 ] && ((CRASHES++))
done
echo "Crashes with PYTHONDONTWRITEBYTECODE: $CRASHES/10"
Output:
Testing with PYTHONDONTWRITEBYTECODE...
Crashes with PYTHONDONTWRITEBYTECODE: 10/10
These env vars mainly impact timing, making the crash more likely.
Final Summary
What I found:
- Bug Type: Use-after-free race condition
- Location:
libshiboken6.abi3.6.8.dylib(PySide6 6.8.0) duringShiboken::initShibokenSupport() - Trigger: Python module import initialization at IDA startup
- Observation: Happens even with all plugins disabled and no input file
Reliable Reproduction:
MallocScribble=1 /Applications/IDA\ Professional\ 9.3.app/Contents/MacOS/ida -B
Backtrace (bottom-up):
Python`_PyImport_RunModInitFunc + 88
-> Shiboken.abi3.so`PyInit_Shiboken + 304
-> libshiboken6`Shiboken::initShibokenSupport(_object*) + 112
-> libshiboken6`___lldb_unnamed_symbol772 + 1544
-> Python`PyObject_CallFunctionObjArgs + 56
-> ... (recursive imports via Shiboken's __import__ hook) ...
-> libshiboken6`___lldb_unnamed_symbol945 + 112 <- CRASH
(or _platform_strlen when the hook passes a freed string)
![]()
![]()
![]()