Address Sanitizer(Asan) & addr2line
I can use Asan to detected:
Stack and heap buffer overflow/underflow
Heap use after free
Stack use outside scope
Double free/wild free
But detecting memory leak is still not support on Android.
Config Address Sanitizer
First add android:debuggable
to the application manifest.
Note: This should not enable in release build.
- local.properties
sdk.dir=/opt/Android/sdk
# ndk.dir=/opt/Android/sdk/ndk
Path to the NDK. This property has been deprecated.
- nativeDebug/asan.gradle
// use ```apply from: rootProject.file('nativeDebug/asan.gradle')``` in your root build gradle.
project.ext {
useASAN = true
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
sdkDir = properties.getProperty('sdk.dir') // for Android Studio 3.5
// ndkDir = properties.getProperty('ndk.dir')
}
project.ext {
useASAN = true
ndkDir = properties.getProperty('ndk.dir')
}
Above version will get below error.
No signature of method: java.util.HashMap.getProperty() is applicable for argument types: (String) values: [ndk.dir]
Possible solutions: hasProperty(java.lang.String), getProperties()
How do I read properties defined in local.properties in build.gradle @StackOverflow
- build.gradle
apply from: rootProject.file('nativeDebug/asan.gradle')
buildscript {}
allprojects {}
- nativeDebug/asan_module.gradle
// use ```apply from: rootProject.file('nativeDebug/asan_module.gradle')``` in your module build gradle.
def abiFiltersForWrapScript = []
def SUPPORTED_ABI = ['arm64-v8a', 'armeabi-v7a', 'x86_64', 'x86']
def ASAN_LIB_LOCATION = 'asan/libs'
def WRAP_SCRIPT_LOCATION = 'asan/res'
def WRAP_SCRIPT_FILE_NAME = 'wrap.sh'
android {
defaultConfig {
externalNativeBuild {
cmake {
arguments "-DANDROID_ARM_MODE=arm", "-DANDROID_STL=c++_shared"
abiFilters.addAll(SUPPORTED_ABI)
}
}
}
buildTypes {
debug {
packagingOptions {
doNotStrip "**.so"
// do not put unused library to APK.
if (rootProject.ext.useASAN && abiFiltersForWrapScript) {
def excludeAbi = ["armeabi", "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"]
.findAll { !(it in abiFiltersForWrapScript) }
.collect { "**/" + it + "/${WRAP_SCRIPT_FILE_NAME}" }
println "exclude ${excludeAbi}"
excludes += excludeAbi
}
}
if (rootProject.ext.useASAN) {
sourceSets {
main {
jniLibs { srcDir { ASAN_LIB_LOCATION } }
resources { srcDir { WRAP_SCRIPT_LOCATION } }
}
}
}
}
}
}
static def getAlias(abi) {
if (abi == 'armeabi-v7a' || abi == 'armeabi') {
return "arm"
} else if (abi == "arm64-v8a") {
return "aarch64"
} else if (abi == "x86_64") {
return "x86_64"
} else if (abi == "x86") {
return "i686"
}
return 'unknown'
}
task copyAsanLibs() {
def sdkDir = rootProject.ext.sdkDir
println "copyAsanLibs(): sdk.dir is " + sdkDir
// def libDir = file("$rootProject.ext.ndkDir").absolutePath + "/toolchains/llvm/prebuilt/"
for (String abi : SUPPORTED_ABI) {
def dir = new File("app/${ASAN_LIB_LOCATION}/" + abi)
dir.mkdirs()
// FileTree tree = fileTree(dir: libDir).include("**/*asan*${getAlias(abi)}*.so")
FileTree tree = fileTree(dir: sdkDir).include("**/*asan*${getAlias(abi)}*.so") // for Studio 3.5
tree.each { File file ->
println "${abi}: ${file.absolutePath} to ${dir.absolutePath}"
copy {
from file
into dir.absolutePath
}
}
}
}
static def generateHelpUtil(file) {
// The NDK contains a recommended wrap.sh file for ASan
// https://android.googlesource.com/platform/ndk/+/refs/heads/master/wrap.sh/asan.sh
file.withWriter { writer ->
writer.write('#!/system/bin/sh\n')
writer.write('HERE="$(cd "$(dirname "$0")" && pwd)"\n')
writer.write('export ASAN_OPTIONS=log_to_syslog=false,allow_user_segv_handler=1\n')
writer.write('ASAN_LIB=$(ls $HERE/libclang_rt.asan-*-android.so)\n')
writer.write('if [ -f "$HERE/libc++_shared.so" ]; then\n')
writer.write(' # Workaround for https://github.com/android-ndk/ndk/issues/988.\n')
writer.write(' export LD_PRELOAD="$ASAN_LIB $HERE/libc++_shared.so"\n')
writer.write('else\n')
writer.write(' export LD_PRELOAD="$ASAN_LIB"\n')
writer.write('fi\n')
writer.write('"$@"\n')
}
}
task createAsanHelpUtilScript(dependsOn: copyAsanLibs) {
println "\ngenerate ${WRAP_SCRIPT_FILE_NAME}"
for (String abi : SUPPORTED_ABI) {
def dir = new File("app/${WRAP_SCRIPT_LOCATION}/lib/" + abi)
dir.mkdirs()
def helpFile = new File(dir, WRAP_SCRIPT_FILE_NAME)
generateHelpUtil(helpFile)
println "${abi}: ${helpFile.path}"
}
}
clean {
delete new File("app/asan/").absolutePath
}
- app/build.gradle
apply from: rootProject.file('nativeDebug/asan_module.gradle')
android {}
dependencies {}
When I build the project, copyAsanLibs
task will auto copy Asan
to the specific folder and generate the sell file. The Asan
library are located at:
$ pwd
/opt/Android/sdk/ndk-bundle/toolchains
$ find . -name "*asan*.so"
./llvm/prebuilt/darwin-x86_64/lib64/clang/8.0.7/lib/linux/libclang_rt.asan-aarch64-android.so
./llvm/prebuilt/darwin-x86_64/lib64/clang/8.0.7/lib/linux/libclang_rt.asan-arm-android.so
./llvm/prebuilt/darwin-x86_64/lib64/clang/8.0.7/lib/linux/libclang_rt.asan-i686-android.so
./llvm/prebuilt/darwin-x86_64/lib64/clang/8.0.7/lib/linux/libclang_rt.asan-x86_64-android.so
./llvm/prebuilt/darwin-x86_64/lib64/clang/8.0.7/lib/linux/libclang_rt.hwasan-aarch64-android.so
./llvm/prebuilt/darwin-x86_64/lib64/clang/8.0.7/lib/linux/libclang_rt.hwasan-x86_64-android.so
- CMakeLists.txt
cmake_minimum_required(VERSION 3.10.2)
add_library(native-call-java-lib SHARED native-call-java-lib.cpp)
find_library(log-lib log)
target_link_libraries(native-call-java-lib ${log-lib})
if (USEASAN)
target_compile_options(${TARGET} PUBLIC -fsanitize=address -fno-omit-frame-pointer)
set_target_properties(${TARGET} PROPERTIES LINK_FLAGS -fsanitize=address)
endif (USEASAN)
Auto copy Asan library and create wrap.sh
Just sync or rebuild project, then it done.
$ tree --version
tree v1.7.0 (c) 1996 - 2014 by Steve Baker, Thomas Moore, Francesc Rocher, Florian Sesser, Kyosuke Tokoro
$ pwd
/Users/HSY/Desktop/NativeDemo
$ tree -C -P '*asan*.so|wrap.sh'
.
├── app
│ ├── asan
│ │ ├── libs
│ │ │ ├── arm64-v8a
│ │ │ │ ├── libclang_rt.asan-aarch64-android.so
│ │ │ │ └── libclang_rt.hwasan-aarch64-android.so
│ │ │ ├── armeabi-v7a
│ │ │ │ └── libclang_rt.asan-arm-android.so
│ │ │ ├── x86
│ │ │ │ └── libclang_rt.asan-i686-android.so
│ │ │ └── x86_64
│ │ │ ├── libclang_rt.asan-x86_64-android.so
│ │ │ └── libclang_rt.hwasan-x86_64-android.so
│ │ └── res
│ │ └── lib
│ │ ├── arm64-v8a
│ │ │ └── wrap.sh
│ │ ├── armeabi-v7a
│ │ │ └── wrap.sh
│ │ ├── x86
│ │ │ └── wrap.sh
│ │ └── x86_64
│ │ └── wrap.sh
Debug error
- native-call-java-lib.cpp
#include <jni.h>
#include <cstdlib>
...
extern "C" JNIEXPORT void JNICALL
Java_com_practice_NativeCallJavaTest_test(JNIEnv *env, jobject) {
int *ptr = (int *) malloc(sizeof(int) * 3);
ptr[4] = 6; // here is line 16
}
Run the app, then below log show the error is heap-buffer-overflow
and the ABI is x86
.
- LogCat
A/DEBUG: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
A/DEBUG: Build fingerprint: 'Android/vbox86p/vbox86p:8.0.0/OPR6.170623.017/233:userdebug/test-keys'
A/DEBUG: Revision: '0'
A/DEBUG: ABI: 'x86'
A/DEBUG: pid: 2087, tid: 2087, name: com.practice >>> com.practice <<<
A/DEBUG: signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
A/DEBUG: Abort message: '=================================================================
==2087==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xcdb1a5e0 at pc 0xbda43a8d bp 0xff8357f8 sp 0xff8357f0
WRITE of size 4 at 0xcdb1a5e0 thread T0 (com.practice)
#0 0xbda43a8c (/data/app/com.practice-S7HmECG-7HxbYvgR8lFVqQ==/lib/x86/libnative-call-java-lib.so+0xa8c)
#1 0xbe26f288 (/data/app/com.practice-S7HmECG-7HxbYvgR8lFVqQ==/oat/x86/base.odex+0xf288)
0xcdb1a5e0 is located 4 bytes to the ri
A/DEBUG: eax 00000000 ebx 00000827 ecx 00000827 edx 00000006
A/DEBUG: esi 00000827 edi ff834d08
A/DEBUG: xcs 00000023 xds 0000002b xes 0000002b xfs 0000006b xss 0000002b
A/DEBUG: eip f40a9ba9 ebp ff834d28 esp ff834cbc flags 00000296
A/DEBUG: backtrace:
A/DEBUG: #00 pc 00000ba9 [vdso:f40a9000] (__kernel_vsyscall+9)
A/DEBUG: #01 pc 00075dcc /system/lib/libc.so (tgkill+28)
A/DEBUG: #02 pc 0001f05e /system/lib/libc.so (abort+110)
A/DEBUG: #03 pc 0005d4c1 /data/app/com.practice-S7HmECG-7HxbYvgR8lFVqQ==/lib/x86/libclang_rt.asan-i686-android.so (offset 0x52000) (_ZN11__sanitizer5AbortEv+81)
A/DEBUG: #04 pc 0005c0bd /data/app/com.practice-S7HmECG-7HxbYvgR8lFVqQ==/lib/x86/libclang_rt.asan-i686-android.so (offset 0x52000) (_ZN11__sanitizer3DieEv+189)
A/DEBUG: #05 pc 000d79fb /data/app/com.practice-S7HmECG-7HxbYvgR8lFVqQ==/lib/x86/libclang_rt.asan-i686-android.so (offset 0x52000) (_ZN6__asan19ScopedInErrorReportD2Ev+491)
A/DEBUG: #06 pc 000d980a /data/app/com.practice-S7HmECG-7HxbYvgR8lFVqQ==/lib/x86/libclang_rt.asan-i686-android.so (offset 0x52000) (_ZN6__asan18ReportGenericErrorEmmmmbmjb+1210)
A/DEBUG: #07 pc 000da4a8 /data/app/com.practice-S7HmECG-7HxbYvgR8lFVqQ==/lib/x86/libclang_rt.asan-i686-android.so (offset 0x52000) (__asan_report_store4+40)
A/DEBUG: #08 pc 00000a8c /data/app/com.practice-S7HmECG-7HxbYvgR8lFVqQ==/lib/x86/libnative-call-java-lib.so (Java_com_practice_NativeCallJavaTest_test+188)
A/DEBUG: #09 pc 0000f288 /data/app/com.practice-S7HmECG-7HxbYvgR8lFVqQ==/oat/x86/base.odex (offset 0xf000)
A/DEBUG: #10 pc 0000307f <anonymous:ce100000>
A/DEBUG: #11 pc 0044db0f /dev/ashmem/dalvik-main space (region space) (deleted)
I found the suspect is libnative-call-java-lib.so
in backtrace part. And the crime scene is 00000a8c
. Now I can ask addr2line
to explain the case.
- find tool in terminal
$ cd /opt/Android/sdk
$ pwd
/opt/Android/sdk
$ find . -name "*addr2line"
./ndk-bundle/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line
./ndk-bundle/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-addr2line
./ndk-bundle/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line
./ndk-bundle/toolchains/llvm/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-addr2line
./ndk-bundle/toolchains/llvm/prebuilt/darwin-x86_64/bin/i686-linux-android-addr2line
./ndk-bundle/toolchains/llvm/prebuilt/darwin-x86_64/bin/x86_64-linux-android-addr2line
./ndk-bundle/toolchains/x86-4.9/prebuilt/darwin-x86_64/bin/i686-linux-android-addr2line
./ndk-bundle/toolchains/x86_64-4.9/prebuilt/darwin-x86_64/bin/x86_64-linux-android-addr2line*
- find
libnative-call-java-lib.so
$ pwd
/Users/yen/Desktop/Native
$ find . -name "libnative-call-java-lib.so"
./app/build/intermediates/cmake/debug/obj/arm64-v8a/libnative-call-java-lib.so
./app/build/intermediates/cmake/debug/obj/armeabi-v7a/libnative-call-java-lib.so
./app/build/intermediates/cmake/debug/obj/x86/libnative-call-java-lib.so
./app/build/intermediates/cmake/debug/obj/x86_64/libnative-call-java-lib.so
./app/build/intermediates/transforms/mergeJniLibs/debug/0/lib/arm64-v8a/libnative-call-java-lib.so
./app/build/intermediates/transforms/mergeJniLibs/debug/0/lib/armeabi-v7a/libnative-call-java-lib.so
./app/build/intermediates/transforms/mergeJniLibs/debug/0/lib/x86/libnative-call-java-lib.so
./app/build/intermediates/transforms/mergeJniLibs/debug/0/lib/x86_64/libnative-call-java-lib.so
./app/build/intermediates/transforms/stripDebugSymbol/debug/0/lib/arm64-v8a/libnative-call-java-lib.so
./app/build/intermediates/transforms/stripDebugSymbol/debug/0/lib/armeabi-v7a/libnative-call-java-lib.so
./app/build/intermediates/transforms/stripDebugSymbol/debug/0/lib/x86/libnative-call-java-lib.so
./app/build/intermediates/transforms/stripDebugSymbol/debug/0/lib/x86_64/libnative-call-java-lib.so
- Use
x86
tool to checkx86 .so
:Native yen$ /opt/Android/sdk/ndk-bundle/toolchains/llvm/prebuilt/darwin-x86_64/bin/i686-linux-android-addr2line -C -i -f -e ./app/build/intermediates/transforms/mergeJniLibs/debug/0/lib/x86/libnative-call-java-lib.so 00000a8c
Java_com_practice_NativeCallJavaTest_test
/Users/yen/Desktop/Native/app/src/main/cpp/native-call-java-lib.cc:16