ASan (and it's younger brother, HWASan) are incredible tools for debugging memory integrity bugs in native applications. This guide explains how to set up HWASan for Geode mods on Android and how to analyze crashes.
Requirements:
- Device running Android 14+, lower is possible but only with ASan, which has a slightly different way of setting up. See here
- Android Studio or any other way of building the launcher
Build the mod #
Simply pass -DANDROID_SANITIZE=hwaddress as a cmake argument:
geode build -p android64 --config Debug -- -DANDROID_SANITIZE=hwaddress
Build launcher #
Clone the launcher, and add a file app/src/main/resources/lib/arm64-v8a/wrap.sh with the following contents:
#!/system/bin/sh
LD_HWASAN=1 exec "$@"
Build a debug version and deploy it to your device.
Test #
To see if it's working, let's add some example UB code to our mod
auto test = new int;
delete test;
*(volatile int*)test = 1;
Before testing, make sure to disable the crash handler! In the launcher settings, open developer settings and enter --geode:disable-crash-handler in Launch arguments.
If this code does not immediately crash, ASan is likely not properly working. You can add exit as the first line (after the shebang) to your wrap.sh script and rebuild the launcher to see if the script is ever getting ran.
If you did everything right, the game should've crashed. Tombstones are available in /data/tombstones. Make sure you pull the most recent one, because this directory includes crash dumps of all apps on your system.
$ adb shell
$ cd /data/tombstones
$ ls -lt # puts most recent at top
$ exit
$ adb pull /data/tombstones/tombstone_xx ./
If the tombstone says something about seccomp disallowing syscalls, you likely pulled the wrong crashlog or did not disable the crash handler.
Analyzing #
Here's an example of a tombstone:
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
crDroid Version: '12.1'
Build fingerprint: 'google/tegu/tegu:16/BP2A.250805.005/13691446:user/release-keys'
Revision: 'MP1.0'
ABI: 'arm64'
Timestamp: 2026-01-12 14:05:22.951988409+0100
Process uptime: 203s
Cmdline: com.geode.launcher
pid: 31028, tid: 31963, name: Main >>> com.geode.launcher <<<
uid: 10637
tagged_addr_ctrl: 0000000000000001 (PR_TAGGED_ADDR_ENABLE)
pac_enabled_keys: 000000000000000f (PR_PAC_APIAKEY, PR_PAC_APIBKEY, PR_PAC_APDAKEY, PR_PAC_APDBKEY)
signal 6 (SIGABRT), code -1 (SI_QUEUE), fault addr --------
Abort message: '==31028==ERROR: HWAddressSanitizer: tag-mismatch on address 0x004649442de0 at pc 0x006d75f0f724
WRITE of size 4 at 0x004649442de0 tags: c4/aa (ptr/mem) in thread T48
#0 0x006d75f0f724 (/data/data/com.geode.launcher/files/geode/unzipped/dankmeme.globed2/dankmeme.globed2.android64.so+0x495724) (BuildId: c5eada8116c0f6e8eb8a6a995810433c3dcdb594)
#1 0x007819bc4b9c (/data/app/~~dZyWKnk3vxy_emc7LuY4yA==/com.robtopx.geometryjump-6y1HhI10jOa9Pdg7lr_TzQ==/split_config.arm64_v8a.apk!/lib/arm64-v8a/libcocos2dcpp.so+0xac9b9c) (BuildId: 42f8945feca43dcb3a375f99b8b68cf32456dda8)
[0x004649442de0,0x004649442e00) is a small unallocated heap chunk; size: 32 offset: 0
Cause: use-after-free
0x004649442de0 is located 0 bytes inside a 4-byte region [0x004649442de0,0x004649442de4)
freed by thread T48 here:
#0 0x00783615e57c (/apex/com.android.runtime/lib64/bionic/libclang_rt.hwasan-aarch64-android.so+0x2957c) (BuildId: 742636119ffb24484fe45c5d21e284c968700fe3)
#1 0x006d75f0f710 (/data/data/com.geode.launcher/files/geode/unzipped/dankmeme.globed2/dankmeme.globed2.android64.so+0x495710) (BuildId: c5eada8116c0f6e8eb8a6a995810433c3dcdb594)
#2 0x007819bc4b9c (/data/app/~~dZyWKnk3vxy_emc7LuY4yA==/com.robtopx.geometryjump-6y1HhI10jOa9Pdg7lr_TzQ==/split_config.arm64_v8a.apk!/lib/arm64-v8a/libcocos2dcpp.so+0xac9b9c) (BuildId: 42f8945feca43dcb3a375f99b8b68cf32456dda8)
#3 0x00785d3f0374 (<unknown module>)
#4 0x0078178d9870 (/data/data/com.geode.launcher/files/copied/Geode.so+0x441870) (BuildId: d3561961f18e8a154709164e8596abd2b31049ff)
#5 0x00785d3f0374 (<unknown module>)
#6 0x007819667c14 (/data/app/~~dZyWKnk3vxy_emc7LuY4yA==/com.robtopx.geometryjump-6y1HhI10jOa9Pdg7lr_TzQ==/split_config.arm64_v8a.apk!/lib/arm64-v8a/libcocos2dcpp.so+0x56cc14) (BuildId: 42f8945feca43dcb3a375f99b8b68cf32456dda8)
#7 0x007819bc21fc (/data/app/~~dZyWKnk3vxy_emc7LuY4yA==/com.robtopx.geometryjump-6y1HhI10jOa9Pdg7lr_TzQ==/split_config.arm64_v8a.apk!/lib/arm64-v8a/libcocos2dcpp.so+0xac71fc) (BuildId: 42f8945feca43dcb3a375f99b8b68cf32456dda8)
#8 0x00785d3f0438 (<unknown module>)
#9 0x0078178d9ef0 (/data/data/com.geode.launcher/files/copied/Geode.so+0x441ef0) (BuildId: d3561961f18e8a154709164e8596abd2b31049ff)
#10 0x00785d3f0438 (<unknown module>)
#11 0x007819c0ea7c (/data/app/~~dZyWKnk3vxy_emc7LuY4yA==/com.robtopx.geometryjump-6y1HhI10jOa9Pdg7lr_TzQ==/split_config.arm64_v8a.apk!/lib/arm64-v8a/libcocos2dcpp.so+0xb13a7c) (BuildId: 42f8945feca43dcb3a375f99b8b68cf32456dda8)
#12 0x00785d3f55e4 (<unknown module>)
#13 0x00785d3f55e4 (<unknown module>)
#14 0x007819bda0d8 (/data/app/~~dZyWKnk3vxy_emc7LuY4yA==/com.robtopx.geometryjump-6y1HhI10jOa9Pdg7lr_TzQ==/split_config.arm64_v8a.apk!/lib/arm64-v8a/libcocos2dcpp.so+0xadf0d8) (BuildId: 42f8945feca43dcb3a375f99b8b68cf32456dda8)
#15 0x007819bdf7dc (/data/app/~~dZyWKnk3vxy_emc7LuY4yA==/com.robtopx.geometryjump-6y1HhI10jOa9Pdg7lr_TzQ==/split_config.arm64_v8a.apk!/lib/arm64-v8a/libcocos2dcpp.so+0xae47dc) (BuildId: 42f8945feca43dcb3a375f99b8b68cf32456dda8)
#16 0x00782714eb00 (/apex/com.android.art/lib64/libart.so+0x2f4b00) (BuildId: 46ae5976d47c8c3676437662907d49b7)
#17 0x007860334c61 (<unknown module>)
#18 0x007820c3739c ([anon:stack_and_tls:31963]+0x10139c)
previously allocated by thread T48 here:
#0 0x00783615ebd8 (/apex/com.android.runtime/lib64/bionic/libclang_rt.hwasan-aarch64-android.so+0x29bd8) (BuildId: 742636119ffb24484fe45c5d21e284c968700fe3)
#1 0x007867474434 (/apex/com.android.runtime/lib64/bionic/hwasan/libc.so+0x66434) (BuildId: d7cecead4fb896c9aff20bdb7ae10f0d)
#2 0x006d75f0f708 (/data/data/com.geode.launcher/files/geode/unzipped/dankmeme.globed2/dankmeme.globed2.android64.so+0x495708) (BuildId: c5eada8116c0f6e8eb8a6a995810433c3dcdb594)
#3 0x007819bc4b9c (/data/app/~~dZyWKnk3vxy_emc7LuY4yA==/com.robtopx.geometryjump-6y1HhI10jOa9Pdg7lr_TzQ==/split_config.arm64_v8a.apk!/lib/arm64-v8a/libcocos2dcpp.so+0xac9b9c) (BuildId: 42f8945feca43dcb3a375f99b8b68cf32456dda8)
#4 0x00785d3f0374 (<unknown module>)
#5 0x0078178d9870 (/data/data/com.geode.launcher/files/copied/Geode.so+0x441870) (BuildId: d3561961f18e8a154709164e8596abd2b31049ff)
#6 0x00785d3f0374 (<unknown module>)
#7 0x007819667c14 (/data/app/~~dZyWKnk3vxy_emc7LuY4yA==/com.robtopx.geometryjump-6y1HhI10jOa9Pdg7lr_TzQ==/split_config.arm64_v8a.apk!/lib/arm64-v8a/libcocos2dcpp.so+0x56cc14) (BuildId: 42f8945feca43dcb3a375f99b8b68cf32456dda8)
#8 0x007819bc21fc (/data/app/~~dZyWKnk3vxy_emc7LuY4yA==/com.robtopx.geometryjump-6y1HhI10jOa9Pdg7lr_TzQ==/split_config.arm64_v8a.apk!/lib/arm64-v8a/libcocos2dcpp.so+0xac71fc) (BuildId: 42f8945feca43dcb3a375f99b8b68cf32456dda8)
#9 0x00785d3f0438 (<unknown module>)
#10 0x0078178d9ef0 (/data/data/com.geode.launcher/files/copied/Geode.so+0x441ef0) (BuildId: d3561961f18e8a154709164e8596abd2b31049ff)
#11 0x00785d3f0438 (<unknown module>)
#12 0x007819c0ea7c (/data/app/~~dZyWKnk3vxy_emc7LuY4yA==/com.robtopx.geometryjump-6y1HhI10jOa9Pdg7lr_TzQ==/split_config.arm64_v8a.apk!/lib/arm64-v8a/libcocos2dcpp.so+0xb13a7c) (BuildId: 42f8945feca43dcb3a375f99b8b68cf32456dda8)
#13 0x00785d3f55e4 (<unknown module>)
#14 0x00785d3f55e4 (<unknown module>)
#15 0x007819bda0d8 (/data/app/~~dZyWKnk3vxy_emc7LuY4yA==/com.robtopx.geometryjump-6y1HhI10jOa9Pdg7lr_TzQ==/split_config.arm64_v8a.apk!/lib/arm64-v8a/libcocos2dcpp.so+0xadf0d8) (BuildId: 42f8945feca43dcb3a375f99b8b68cf32456dda8)
#16 0x007819bdf7dc (/data/app/~~dZyWKnk3vxy_emc7LuY4yA==/com.robtopx.geometryjump-6y1HhI10jOa9Pdg7lr_TzQ==/split_config.arm64_v8a.apk!/lib/arm64-v8a/libcocos2dcpp.so+0xae47dc) (BuildId: 42f8945feca43dcb3a375f99b8b68cf32456dda8)
#17 0x00782714eb00 (/apex/com.android.art/lib64/libart.so+0x2f4b00) (BuildId: 46ae5976d47c8c3676437662907d49b7)
#18 0x007860334c61 (<unknown module>)
#19 0x007820c3739c ([anon:stack_and_tls:31963]+0x10139c)
(less useful parts cut away)
This reveals a lot of useful information, like where exactly memory was allocated, freed, and then accessed afterwards. You'll notice it is missing symbols and other debug information, and that can be fixed by using hwasan_symbolize. Run the following command, replacing /opt/android-ndk with the NDK path, and 19 with whatever clang version is used in your NDK
/opt/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/19/bin/hwasan_symbolize \
--symbols ./build-android64 \
< ~/tombstone.txt \
> ~/tombstone-sym.txt
Now all the addresses in your mod should be replaced by useful code paths:
WRITE of size 4 at 0x004036ae0180 tags: bd/9e (ptr/mem) in thread T39
#0 globed::HookedMenuLayer::onGlobedButton(cocos2d::CCObject*) in src/core/hooks/MenuLayer.cpp:111:26
#1 0x007c52cf3b9c (/data/app/~~dZyWKnk3vxy_emc7LuY4yA==/com.robtopx.geometryjump-6y1HhI10jOa9Pdg7lr_TzQ==/split_config.arm64_v8a.apk!/lib/arm64-v8a/libcocos2dcpp.so+0xac9b9c) (BuildId: 42f8945feca43dcb3a375f99b8b68cf32456dda8)
For completeness sake, here is the script I use (called symstone.sh) that pulls the most recent tombstone, symbolizes and opens it in vscode:
#!/bin/bash
adb pull /data/tombstones/$(adb shell "ls -t /data/tombstones | head -n 1") /tmp/tombstone.txt
/opt/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/19/bin/hwasan_symbolize \
--symbols ./build-android64 < /tmp/tombstone.txt > /tmp/tombstone-sym.txt
code /tmp/tombstone-sym.txt
Bonus: debugging Geode #
You must use shared libc++ instead of static when building Geode:
geode build -p android64 --config Debug -- -G Ninja \
-DANDROID_SANITIZE=hwaddress -DANDROID_STL=c++_shared
adb push ./bin/nightly/Geode.android64.so /sdcard/Android/media/com.geode.launcher/
adb push \
/opt/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so \
/sdcard/Android/media/com.geode.launcher/