Roly's Blog

Whatever will be, will be, the future's not ours to see.

0%

Android NDK 使用 Address Sanitizer

从 API 级别 27 (Android O MR 1) 开始,Android NDK 支持 Address Sanitizer(也称为 ASan)。ASan 是一种基于编译器的快速检测工具,用于检测C/C++代码中的内存错误。ASan 的 CPU 开销约为 2 倍,代码大小开销在 50% 到 2 倍之间,并且内存开销很大。

注意: ASan在Android 平台上不支持检测内存泄漏!!!

ASan 可以检测以下问题:

  • 堆栈和堆缓冲区上溢/下溢
  • 释放之后的堆使用情况
  • 超出范围的堆栈使用情况
  • 重复释放/错误释放

ASan 可在 32 位和 64 位 ARM 以及 x86 和 x86-64 上运行。Android NDK 也支持 HWAddress Sanitizer(也称为 HWASan)。与ASan 相比,HWASan功能类似,开销较小,但是使用麻烦。

  • NDK r21 和 Android 10(API 级别 29)以上
  • HWASan 仅适用于 64 位 Arm 设备
  • HWASan 应用需要将预构建的映像刷写到支持的 Pixel 手机上。

Build

如需使用 Address Sanitizer 构建应用的原生 (JNI) 代码,请执行以下操作:

Quote from: https://developer.android.com/ndk/guides/asan#building

1.在模块的 build.gradle 中:

1
2
3
4
5
6
7
8
9
10
android {
defaultConfig {
externalNativeBuild {
cmake {
# Can also use system or none as ANDROID_STL.
arguments "-DANDROID_ARM_MODE=arm", "-DANDROID_STL=c++_shared"
}
}
}
}

2.对于 CMakeLists.txt 中的每个目标:

1
2
target_compile_options(${TARGET} PUBLIC -fsanitize=address -fno-omit-frame-pointer)
set_target_properties(${TARGET} PROPERTIES LINK_FLAGS -fsanitize=address)

第一步

还可以添加开关,只在debug版本中开启该功能:

在模块的 build.gradle 中:

1
2
3
cmake {
arguments "-DANDROID_STL=c++_shared", "-DSANITIZE=TRUE"
}

在模块的 CMakeLists.txt 中添加:

1
2
3
4
# Enable address sanitizer only for debug builds
if (SANITIZE)
...
endif()

第二步

还可以使用如下配置:

1
2
3
4
SET (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer")
SET (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer")
SET (CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fsanitize=address")
SET (CMAKE_ANDROID_ARM_MODE ARM)

Run

ASan需要在进程启动时启用,要求以全新的进程来运行应用,而不是从 zygote 克隆。从 Android O MR1(API 级别 27)开始,可替换应用进程的封装 Shell 脚本。这样可调试的应用就可对其应用启动过程进行自定义,以便在生产设备上使用 ASan。

Quote from: https://developer.android.com/ndk/guides/asan#running

  1. android:debuggableandroid:extractNativeLibs=true 添加到应用清单。请注意,后者是某些配置的默认设置。如需了解详情,请参阅封装 Shell 脚本

  2. 将 ASan 运行时库添加到应用模块的 jniLibs 中。

  3. 将包含以下内容的 wrap.sh 文件添加到每个相同的目录中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #!/system/bin/sh
    HERE="$(cd "$(dirname "$0")" && pwd)"
    export ASAN_OPTIONS=log_to_syslog=false,allow_user_segv_handler=1
    ASAN_LIB=$(ls $HERE/libclang_rt.asan-*-android.so)
    if [ -f "$HERE/libc++_shared.so" ]; then
    # Workaround for https://github.com/android-ndk/ndk/issues/988.
    export LD_PRELOAD="$ASAN_LIB $HERE/libc++_shared.so"
    else
    export LD_PRELOAD="$ASAN_LIB"
    fi
    "$@"

第一步

目前发现, android:debuggable 默认是 "false",即使不设置为 "true",也不影响结果。

如果没有在 AndroidManifest.xml配置, android:extractNativeLibs默认值是 "true"

注意:如果使用app bundle, Android Gradle 插件从3.6.0 默认会将 extractNativeLibs 设置为 "false"。也就是说,原生库将保持页面对齐状态并以未压缩的形式打包。

第二步

除了手动拷贝,还可以使用下面的两种方法:

(1)在CMakeLists.txt中添加如下配置, 自动拷贝*.asan*${ARCH}*-android.so, 需要设置库的名称LibraryName:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
set(LIBNAME "LibraryName")

# A function to retrieve the architecture from the ABI
# (neither ANDROID_ARCH_NAME nor CMAKE_SYSTEM_PROCESSOR work for this)

function(get_architecture ABI ARCH)
if (ABI STREQUAL armeabi-v7a)
set(${ARCH} arm PARENT_SCOPE)
elseif(ABI STREQUAL arm64-v8a)
set(${ARCH} aarch64 PARENT_SCOPE)
elseif(ABI STREQUAL x86)
set(${ARCH} i686 PARENT_SCOPE)
elseif(ABI STREQUAL x86_64)
set(${ARCH} x86_64 PARENT_SCOPE)
else()
message(FATAL_ERROR "Unsupported ABI")
endif()
endfunction()

# Asan libs are "somewhere" in the toolchain's root, we try to find the
# right one for the current architecture and copy it to the libs output dir
# (so that it will be packed in the apk):

get_architecture(${ANDROID_ABI} ARCH)
file(GLOB_RECURSE ASAN_SRC ${ANDROID_TOOLCHAIN_ROOT}/*.asan*${ARCH}*-android.so)
set(ASAN_DEST ${CMAKE_LIBRARY_OUTPUT_DIRECTORY})

add_custom_command(
TARGET ${LIBNAME} PRE_BUILD
COMMAND ${CMAKE_COMMAND} -E copy ${ASAN_SRC} ${ASAN_DEST}
)

(2)使用asan_device_setup,将*.asan*${ARCH}*-android.so添加到设备,设备需要root。

Mac 系统下,其路径为ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/lib64/clang/9.0.9/bin

1
2
3
4
5
6
7
8
9
❯ ./asan_device_setup --help                                                                                     
usage: ./asan_device_setup [--revert] [--device device-id] [--lib path] [--extra-options options]
--revert: Uninstall ASan from the device.
--lib: Path to ASan runtime library.
--extra-options: Extra ASAN_OPTIONS.
--device: Install to the given device. Use 'adb devices' to find
device-id.
--use-su: Use 'su -c' prefix for every adb command instead of using
'adb root' once.

如果执行成功,那么设备就会自动重启,测试完成最好移除这些库。

第三步

NDK包含一个推荐的针对ASan的wrap.sh文件

Mac 系统下,其路径为ANDROID_NDK_HOME/wrap.sh/asan.sh

asan.sh重命名为wrap.sh,在模块的跟目录下添加 wrap, 目录结构应包含以下内容 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<project root>
└── app
├── src
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ ├── cpp
│ │ └── res
│ ├── androidTest
│ └── test
└── wrap
└── lib
├── arm64-v8a
│ └── wrap.sh
├── armeabi-v7a
│ └── wrap.sh
├── x86
│ └── wrap.sh
└── x86_64
└── wrap.sh

在模块的 build.gradle 中添加:

1
2
3
4
5
6
7
8
9
10
11
12
android {
buildTypes {
debug {
sourceSets.debug.resources.srcDir "wrap"
externalNativeBuild {
cmake {
arguments "-DANDROID_STL=c++_shared", "-DSANITIZE=TRUE"
}
}
}
}
}

Test Case

heap-use-after-free error

1
2
3
4
5
6
7
8
9
IMPL_FUNC(void,doHeapUseAfterFree) {

// https://docs.microsoft.com/en-us/cpp/sanitizers/error-heap-use-after-free?view=msvc-160
// heap-use-after-free error
char * volatile p = new char[10];
delete[] p;

p[5] = 42; // Boom!
}

heap-buffer-overflow error

1
2
3
4
5
6
7
8
9
10
11
12
IMPL_FUNC(void,doHeapBufferOverflow) {

// https://docs.microsoft.com/en-us/cpp/sanitizers/error-heap-buffer-overflow?view=msvc-160
// heap-buffer-overflow error
char *x = (char*)malloc(10 * sizeof(char));
memset(x, 0, 10);

int res = x[10]; // Boom!

LOGE("res: %d",res);
free(x);
}

stack-buffer-overflow error

1
2
3
4
5
6
7
8
9
10
IMPL_FUNC(void,doStackBufferOverflow) {

// https://docs.microsoft.com/en-us/cpp/sanitizers/error-stack-buffer-overflow?view=msvc-160
// stack-buffer-overflow error
char x[10];
memset(x, 0, 10);
int res = x[10]; // Boom! Classic stack buffer overflow

LOGE("res: %d",res);
}

stack-buffer-underflow error

1
2
3
4
5
6
7
8
IMPL_FUNC(void,doStackBufferUnderflow) {

// https://docs.microsoft.com/en-us/cpp/sanitizers/error-stack-buffer-underflow?view=msvc-160
// stack-buffer-underflow error
int subscript = -1;
char buffer[42];
buffer[subscript] = 42; // Boom!
}

stack-use-after-return error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
char* x;
void foo() {
char stack_buffer[42];
x = &stack_buffer[13];
}

IMPL_FUNC(void,doStackUseAfterReturn) {
// https://docs.microsoft.com/en-us/cpp/sanitizers/error-stack-use-after-return?view=msvc-160
// stack-use-after-return error
foo();
*x = 42; // Boom!

// This check requires code generation that's activated by an extra compiler option,
// fsanitize-address-use-after-return, and by setting the environment variable
// ASAN_OPTIONS=detect_stack_use_after_return=1.
LOGE("This should set the environment variable ASAN_OPTIONS=detect_stack_use_after_return=1.");

}

double-free error

1
2
3
4
5
6
7
8
IMPL_FUNC(void,doDoubleFree) {
// https://docs.microsoft.com/en-us/cpp/sanitizers/error-double-free?view=msvc-160
// double-free error
char * volatile p = new char[16];
delete[] p;

delete[] p; // Boom!
}

null-pointer dereference error

1
2
3
4
5
IMPL_FUNC(void,doNullpointerDereference) {
//null-pointer dereference error
char * volatile p = (char *)nullptr;
p[42] = 1; // Boom!
}

memcpy-param-overlap

1
2
3
4
5
6
7
8
9
10
IMPL_FUNC(void,doMemcpyParamOverlap) {
// https://docs.microsoft.com/en-us/cpp/sanitizers/error-memcpy-param-overlap?view=msvc-160
// memcpy-param-overlap
// The function memcpy doesn't support overlapping memory.
// It provides an alternative to memcpy that does support overlapping memory: memmove.

char buffer[] = "hello";

memcpy(buffer, buffer + 1, 5); // BOOM!
}

strncat-param-overlap

1
2
3
4
5
6
7
8
IMPL_FUNC(void,doStrncatParamOverlap) {
// https://docs.microsoft.com/en-us/cpp/sanitizers/error-strncat-param-overlap?view=msvc-160
// strncat-param-overlap
// Code that moves memory in overlapping buffer can cause hard-to-diagnose errors.

char buffer[] = "hello\0XXX";
strncat(buffer, buffer + 1, 3); // BOOM
}

Sample

https://github.com/rolyyu/AndroidASan

Reference

https://developer.android.com/ndk/guides/asan

https://developer.android.com/ndk/guides/wrap-script

https://github.com/google/sanitizers

https://stackoverflow.com/questions/41552966/getting-new-delete-type-mismatch-from-asan

https://docs.microsoft.com/en-us/cpp/sanitizers/asan-error-examples?view=msvc-160

https://docs.microsoft.com/en-us/cpp/sanitizers/error-new-delete-type-mismatch?view=msvc-160