Android native memory debugging

16 minute read

malloc_debug

malloc debug is a method for debugging android native memory issues, it can detects following three types of memory problems:

  • memory corruption
  • memory leak
  • use after free

Please be noted that before Android 4.4 (API level 19), this feature may not fully functional, using it with modern version is recommended, this post is based on Android O (8.1.0).

By using this feature, these memory related calls are wrapped by adding a shim layer:

  • malloc
  • free
  • calloc
  • realloc
  • posix_memalign
  • memalign
  • aligned_alloc
  • malloc_usable_size

On 32 bit systems, these two functions which are deprecated, are also replaced:

  • pvalloc
  • valloc

The detected memory problems will be reported in logcat with tag malloc_debug, check this out with:

logcat -s libc malloc_debug

How to use malloc_debug

There are many options in malloc_debug, you can use one or more of these options by setting property libc.debug.malloc.options:

setprop libc.debug.malloc.options "backtrace guards"

By default, malloc debug will track all the processes which use native code, if you wanna to track only specific program, then libc.debug.malloc.program can be used, for example we only interest in bootanimation:

setprop libc.debug.malloc.program bootanimation

In case you are tracking programs during system startup, then add these two properties in /vendor/build.prop:

libc.debug.malloc.options=backtrace gurads
libc.debug.malloc.program=bootanimation

Enable backtrace tracking for zygote and zygote based processes by adding below to build.prop file:

libc.debug.malloc.options=backtrace
libc.debug.malloc.program=app_process

By using environment variable, any process spawned from this shell will run with malloc debug enabled:

export LIBC_DEBUG_MALLOC_OPTIONS="backtrace leak_track"
memleak

This method works on Android O and later version.

This is how malloc debug was used for memory problem tracking, now let’s explain its options.

malloc debug options

front_guard[=SIZE_BYTES]

When this option is enabled, a small buffer is placed before the allocated data. This buffer is called front guard, and will be filled with 0xaa on allocation, The guard will be checked when the allocation is freed, if any part of the front guard is modified, an error will be reported in logcat indicating what bytes were changed.

For example, the following code will report front guard issue:

#include <stdio.h>
#include <stdlib.h>

int main()
{
	int i;
	char *a = malloc(10);

	a[-1] = 0xfe;
	for (i = 0; i < 10; i++)
		a[i] = i;

	free(a);

	return 0;
}

Save above test code to memleak.c, and create Android.bp for building:

cc_binary {
    name: "memleak",
    srcs: ["memleak.c"],
    shared_libs: ["libcutils"],
    cflags: [
        "-Werror",
        "-O0",
    ],
}

Now set properties and run memleak to see what happens:

setprop libc.debug.malloc.options front_guard
setprop libc.debug.malloc.program memleak

front guard In the above log, the address 0xb3e8d030 indicates the start address of the allocation.

By default, the front guard size is 32 bytes, and can be changed with SIZE_BYTES, the max size is 16384. SIZE_BYTES may change if the size you specified is not aligned with 8 bytes on 32 bit systems or 16 bytes on 64 bit systems.

Keep two things in mind:

  • Make sure to add -O0 to cflags to force gcc not to optimize our code.
  • guard check only happends when the allocation is freed, so if you do not call free, the error will not be reported.

rear_guard[=SIZE_BYTES]

Similar to front guard, rear guard is to place SIZE_BYTES sized buffer after the allocated data, and filled it with 0xbb in this case. When the memory is freed, the guard is checked.

The SIZE_BYTES has the same meaning as front guard.

setprop libc.debug.malloc.options rear_guard
setprop libc.debug.malloc.program memleak

rear guard

guard[=SIZE_BYTES]

This enables both front guard and rear gaurd on all allocations.

setprop libc.debug.malloc.options guard
setprop libc.debug.malloc.program memleak

guard

backtrace[=MAX_FRAMES]

With backtrace enabled, it will capture the backtrace of each allocation site. This option may slow down allocations by order of magnitude, if tracking system wide allocations, decrease the maximum number of frames by specifying MAX_FRAMES.

The default value of MAX_FRAMES is 16, and the maximum value can be set up to 256.

This option is usually used with other options such as leak_track, for test purpose we set backtrace frames to 8:

setprop libc.debug.malloc.options "backtrace=8 leak_track"
setprop libc.debug.malloc.program memleak

backtrace

backtrace_enable_on_signal[=MAX_FRAMES]

fill_on_alloc[=MAX_FILLED_BYTES]

Enabling this option will fill all allocation with value 0xeb, except calloc. When doing realloc to a lager size, the new allocated space will be filled with 0xeb.

The whole allocated memory was filled by default, unless MAX_FILLED_BYTES specified.

fill_on_free[=MAX_FILLED_BYTES]

When an allocation is freed, fill it with 0xef.

Fill Entire allocation unless MAX_FILLED_BYTES specified.

fill[=MAX_FILLED_BYTES]

This enables both fill_on_alloc and fill_on_free.

expand_alloc[=EXPAND_BYTES]

Allocate extra EXPAND_BYTES bytes on allocation, default is 16 bytes, and can be up to 16384 bytes.

free_track[=ALLOCATION_COUNT]

setprop libc.debug.malloc.options leak_track
setprop libc.debug.malloc.program memleak

If free_track is enabled, when calling free, the memory is not freed immediately, instead the pointer was added to a list and the entire allocation was be filled with 0xef, and the backtrace at the time of free is recorded.

The freed allocation list has a default length of 100 records if ALLOCATION_COUNT was not set, the maximum record is 16384.

Here the backtrace recording is completely separate from the backtrace option, and will record 16 frames of backtrace by default, this can be changed with option free_track_backtrace_num_frames which will described in the next section.

Assign value to a freed allocation is detected by malloc_debug: free track

free_track_backtrace_num_frames[=MAX_FRAMES]

Only makes sense when free_track is enabled, if this value is set to zero, then the backtrace will be disabled completely.

The maximum of backtrace frames to be recorded is 256.

leak_track

setprop libc.debug.malloc.options leak_track
setprop libc.debug.malloc.program memleak

This option enables tracking every allocation, when a program terminated, all leaked allocation will be dumped to log: leak track

With backtrace enabled, we can track down the exact line where it leaks with the help of addr2line:

setprop libc.debug.malloc.options "backtrace leak_track"
setprop libc.debug.malloc.program memleak

leak track with backtrace

But there is one point I don’t get it: it reports extra 4 bytes leak (the second one), which is incorrect, since there is only one malloc in the test code.

record_allocs[=TOTAL_ENTRIES]

setprop libc.debug.malloc.options record_allocs
setprop libc.debug.malloc.program memleak

Every allocation/free operation made on every thread will be recorded to file /data/local/tmp/record_allocs.txt when signal 46 (SIGRTMAX - 18) is received, the file location can be changed with option record_allocs_file.

By default the total number of allocation/free records that dumped to file is 8,000,000, and the maximum value is 50,000,000, which can be set by TOTAL_ENTRIES.

All the records will be cleared upon receving the signal, and new record will be dumped to the file. Any allocations/frees occuring while the data is being dumped to the file are ignored.

This option only exists on Android O and later version.

If this option is set, when program started, following message will be shown in the log: record allocs log

Since our test code only include malloc and free operations, so there is only few records in the file, the dumped record will be like this:

record allocs

From the above records, it makes sense leak_track reported 4 bytes of leak. But the question is still there, will find out where the extra two mallocs comes from later.

Here is the full list of different allocation/free operations with log format and example records:

call example log format example
  THREAD_ID: thread_done 0x0 187: thread_done 0x0
pointer = malloc(size) THREAD_ID: malloc pointer size 186: malloc 0xb6038060 20
free(pointer) THREAD_ID: free pointer 186: free 0xb6038060
pointer = calloc(nmemb, size) THREAD_ID: calloc pointer nmemb size 186: calloc 0xb609f080 32 4
new_pointer = realloc(old_pointer, size) THREAD_ID: realloc new_pointer old_pointer size 186: realloc 0xb609f080 0xb603e9a0 12
pointer = memalign(alignment, size) THREAD_ID: memalign pointer alignment size  
posix_memalign(&pointer, alignment, size) THREAD_ID: memalign pointer alignment size 186: memalign 0x85423660 16 104
pointer = valloc(size) THREAD_ID: memalign pointer 4096 size 186: memalign 0x85423660 4096 112
pointer = pvalloc(size) THREAD_ID: memalign pointer 4096 SIZE_ROUNDED_UP_TO_4096 186: memalign 0x85423660 4096 8192

record_allocs_file[=FILE_NAME]

This option only available for Android O and later version, it does not make any sense if record_allocs is disabled.

valgrind

Setup Environment

$ mmm external/valgrind/
$ adb shell mkdir /system/lib/valgrind
$ adb push out/target/product/rpi3/system/lib/valgrind/* system/lib/valgrind/
$ adb push out/target/product/rpi3/system/bin/valgrind /system/bin/

Using valgrind with native app

Here we use the same test app as above with valgrind to detect memory issue, run with:

rpi3:/ # valgrind --leak-check=full memleak

All three errors(invalid access, leak) are detected, showing exact line of code that leads to memory issue: valgrind

Using valgrind with system server

setprop wrap.system_server "logwrapper valgrind"
stop && start

Doing this will dump the log to logcat with tag valgrind.

Using valgrind with apk

setprop wrap.com.example.testapp "logwrapper valgrind"
am start -a android.intent.action.MAIN -n com.example.testapp/.MainActivity

How to attach Valgrind to a running process

The faq in Official website says No.

The environment that Valgrind provides for running programs is significantly different to that for normal programs, e.g. due to different layout of memory. Therefore Valgrind has to have full control from the very start.

It is possible to achieve something like this by running your program without any instrumentation (which involves a slow-down of about 5x, less than that of most tools), and then adding instrumentation once you get to a point of interest. Support for this must be provided by the tool, however, and Callgrind is the only tool that currently has such support. See the instructions on the callgrind_control program for details.

Google has deprecated Valgrind, it is recommended to use ASan instead.

AddressSanitizer

ASan is another great tool for android native memory debugging, it can detects:

  • Use after free (dangling pointer dereference)
  • Heap buffer overflow
  • Stack buffer overflow
  • Global buffer overflow
  • Use after return
  • Use after scope
  • Initialization order bugs
  • Memory leaks (Not available for Android)
  • Double free/wild free

Since ASan and Valgrind are very similar, but there are some differences about what kind of memory problems that they can detect, here are some points they defer:

tools AddressSanitizer Valgrind
technology compile-time instrumentation dynamic binary instrumentation
slow down 2x 20x
Heap buffer overflow yes yes
Stack buffer overflow yes no
Global buffer overflow yes no
Use after free (aka dangling pointer) yes yes
Use after return yes no
Uninitialized memory reads no yes
Leaks yes yes

In addition to the above differences, ASan is much faster and less memory overhead.

Sanitizing native app with ASan

In order to use Asan, a rebuild of the app is required by adding sanitize: { address: true } in Android.bp:

cc_binary {
    name: "memleak",
    srcs: ["memleak.c"],
    shared_libs: ["libcutils"],
    sanitize: {
        address: true,
    },
    cflags: [
        "-Werror",
        "-O0",
    ],
}

or add LOCAL_SANITIZE:=address if using Android.mk.

There is a shared library called libclang_rt.asan-arm-android.so is needed to run the newly built executable, or it will report file not found issue, so don’t forget to push it to target system:

$ adb push out/target/product/rpi3/system/lib/libclang_rt.asan-arm-android.so /system/lib/

Now execute test app, to see issues reported by ASan: heap buffer overflow

ASan dump detailed report to both standard output and logcat, there is no log tag for this, so be sure not to filter with tags.

ASan will abort the program when error detected..

From the above report we know that, invalid memory access happens at offset 0x6cb with the help of arm-linux-androideabi-addr2line we can convert the offset to line number.

If you are using binary with symbols, and with llvm-symbolizer installed to android system, it will show line number instead of offset, which is more convenient for debugging, this is the recommended way to do sanitizing: heap buffer overflow line number

The code of llvm-symbolizer can be found in external/llvm/tools/llvm-symbolizer.

There is another option for translating offset to line numbers: symbolize.py, save the report to file then feed to this python script:

source build/envsetup.sh
lunch lineage_rpi3-userdebug
cat report.txt | external/compiler-rt/lib/asan/scripts/symbolize.py

The output is not that colorful than previous one, but it works though.

Sanitizing shared libraries

Sanitizing shared libraries needs the target executables be also built with ASan support, here we split formerly used example code to a library named libmem_test:

// cat vendor/brcm/libmem_test/libmem_test.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int heap_overflow(void)
{
	char *a = malloc(10);

	a[-1] = 0xfe;
	free(a);

	return 0;
}

int use_after_free(void)
{
	char *a = malloc(10);

	free(a);
	a[1] = 0xfd;

	return 0;
}

int leak_test(void)
{
	char *a = malloc(10);
	a[1] = 0xfd;
	return 0;
}

And the Android.bp to build this library:

// cat vendor/brcm/libmem_test/Android.bp
cc_library_shared {
    name: "libmem_test",
    relative_install_path: "asan",
    srcs: ["libmem_test.c"],
    shared_libs: ["libcutils"],
    sanitize: {
        address: true,
    },
    cflags: [
        "-Werror",
        "-O0",
    ],
}

The library will be installed to out/target/product/rpi3/data/asan/system/lib/asan/.

In case of using Android.mk, the equivalent to do this is:

LOCAL_SANITIZE:=address
LOCAL_MODULE_RELATIVE_PATH := asan

Now, build a executable to test libmem_test, and then push them to target system:

adb shell mkdir -p data/asan/system/lib/asan/
adb push out/target/product/rpi3/symbols/data/asan/system/lib/asan/libmem_test.so data/asan/system/lib/asan/
adb push out/target/product/rpi3/symbols/system/bin/memleak /system/bin/

Since the library we test is not in default /system/lib/, we need to tell where to find the library by specifying LD_LIBRARY_PATH:

LD_LIBRARY_PATH=/data/asan/system/lib/asan memleak

If you are sanitizing native services, LD_LIBRARY_PATH can be set in init.rc with:

setenv LD_LIBRARY_PATH /data/asan/system/lib/asan

If both the library and the executable are with symbols, the report will be similar to below output, pointing out where the issue exists in the library, and where it is called by the executable: heap use after free

Sanitizing entire Android system

From Android 7.0, sanitizing entire Android system is supported, build with:

make -j16
SANITIZE_TARGET=address make -j16

The second make will generate ASan-instrumented executables and libraries, and these libraries were installed to data partition at /data/asan/lib/, so reflash the userdata is required. And all the executables will be replaced with ASan-instrumented ones.

Note, the android build system will remove all the intermediate object directories when variable $SANITIZE_TARGET has changed.

There are three types of targets that cannot be built with ASan:

  • staticlly linked executables
  • LOCAL_CLANG:=false targets
  • LOCAL_SANITIZE:=false

Undefined Behavior Sanitizer

UBSan was used for undefined behavior dectection, can be enabled by assigning one or more supported types to LOCAL_SANITIZE and LOCAL_SANITIZE_DIAG:

LOCAL_SANITIZE := alignment bounds null unreachable integer
LOCAL_SANITIZE_DIAG := alignment bounds null unreachable integer

Only subset of llvm checks are supported by Android system, they are:

  • alignment
  • bool
  • bounds
  • enum
  • float-cast-overflow
  • float-divide-by-zero
  • nonnull-attribute
  • null
  • return
  • returns-nonnull-attribute
  • shift-base
  • shift-exponent
  • integer-divide-by-zero
  • signed-integer-overflow
  • unsigned-integer-overflow
  • unreachable
  • vla-bound

Here are modules in Android which uses UBSan:

system/extras/verity/Android.mk:LOCAL_SANITIZE := integer
system/extras/verity/fec/Android.mk:LOCAL_SANITIZE := integer
system/extras/libfec/test/Android.mk:LOCAL_SANITIZE := integer
system/security/keystore/Android.mk:LOCAL_SANITIZE := integer
system/core/init/Android.mk:LOCAL_SANITIZE := signed-integer-overflow
system/core/fs_mgr/Android.mk:LOCAL_SANITIZE := integer
system/netd/server/Android.mk:LOCAL_SANITIZE := unsigned-integer-overflow

You can use check groups in llvm, Android also support this feature, but has less check types, for example, the above integer enables integer-divide-by-zero, signed-integer-overflow and unsigned-integer-overflow.

Another check group android supported is called default-ub, this group can only be used with SANITIZE_TARGET, which enables:

  • bool
  • integer-divide-by-zero
  • return
  • returns-nonnull-attribute
  • shift-exponent
  • unreachable
  • vla-bound

which is defined in build/core/clang/config.mk, and following can be enabled if compiler performance is not the key point we care about, by uncommenting the below:

# CLANG_DEFAULT_UB_CHECKS += alignment
# CLANG_DEFAULT_UB_CHECKS += bounds
# CLANG_DEFAULT_UB_CHECKS += enum
# CLANG_DEFAULT_UB_CHECKS += float-cast-overflow
# CLANG_DEFAULT_UB_CHECKS += float-divide-by-zero
# CLANG_DEFAULT_UB_CHECKS += nonnull-attribute
# CLANG_DEFAULT_UB_CHECKS += null
# CLANG_DEFAULT_UB_CHECKS += shift-base
# CLANG_DEFAULT_UB_CHECKS += signed-integer-overflow

Bound Sanitizer

BoundSan is used to sanitizing array access, namely out of bound checking.

Enabling Bound Sanitizer

Like UBSan, BoundSan can be enabled for both Android.bp and Android.mk files by adding bounds to misc_undefined and LOCAL_SANITIZE respectively:
For Android.bp

cc_binary {
    name: "bounds",
    srcs: [
        "bounds.c"
    ],
    sanitize: {
        misc_undefined: ["bounds"],
        diag: {
            misc_undefined: ["bounds"],
        },
    },
}

For Android.mk

LOCAL_SANITIZE := bounds
LOCAL_SANITIZE_DIAG := bounds

diag enables diagnostic mode, in this mode BoundSan does not abort program, it is optional and can only be used for debug purpose, as it brings high performance overhead.

Take the follow code for example:

int main()
{
	int a[10];

	a[10] = 0x15;

	return 0;
}

Enabling diagnostics mode will dump a runtime error when an out of bound issue occurs:

vendor/brcm/rpi3/gist/bounds.c:8:5: runtime error: index 10 out of bounds for type 'int [10]'

Otherwise a tombstone is generated:

08-03 17:50:08.617  3060  3060 F libc    : Fatal signal 6 (SIGABRT), code -6 (SI_TKILL) in tid 3060 (bounds), pid 3060 (bounds)
08-03 17:50:08.674  3063  3063 I crash_dump32: obtaining output fd from tombstoned, type: kDebuggerdTombstone
08-03 17:50:08.675   200   200 I /system/bin/tombstoned: received crash request for pid 3060
08-03 17:50:08.676  3063  3063 I crash_dump32: performing dump of process 3060 (target tid = 3060)
08-03 17:50:08.679  3063  3063 F DEBUG   : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
08-03 17:50:08.679  3063  3063 F DEBUG   : LineageOS Version: '16.0-20200803-UNOFFICIAL-rpi3'
08-03 17:50:08.680  3063  3063 F DEBUG   : Build fingerprint[63631.967129] init: Untracked pid 3063 exited with status 0
: 'Raspberry/lineage_rpi3/rpi3:9/PQ3A.190801.002/fdbai08031438:userdebug/test-keys'
08-03 17:50:08.680  3063  3063 F DEBUG   : Revision: '0'
08-03 17:50:08.680  3063  3063 F DEBUG   : ABI: 'arm'
08-03 17:50:08.680  3063  3063 F DEBUG   : pid: 3060, tid: 3060, name: bounds  >>> bounds <<<
08-03 17:50:08.680  3063  3063 F DEBUG   : signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
08-03 17:50:08.680  3063  3063 F DEBUG   :     r0  00000000  r1  00000bf4  r2  00000006  r3  00000008
08-03 17:50:08.680  3063  3063 F DEBUG   :     r4  00000bf4  r5  00000bf4  r6  ffcdc25c  r7  0000010c
08-03 17:50:08.681  3063  3063 F DEBUG   :     r8  00000000  r9  00000000  r10 00000000  r11 00000000
08-03 17:50:08.681  3063  3063 F DEBUG   :     ip  ffcdc2bc  sp  ffcdc248  lr  e82aded9  pc  e82a5d2a
08-03 17:50:08.687  3063  3063 F DEBUG   :
08-03 17:50:08.687  3063  3063 F DEBUG   : backtrace:
08-03 17:50:08.687  3063  3063 F DEBUG   :     #00 pc 0001cd2a  /system/lib/libc.so (abort+58)
08-03 17:50:08.688  3063  3063 F DEBUG   :     #01 pc 00000663  /vendor/bin/bounds (main+2)
08-03 17:50:08.688  3063  3063 F DEBUG   :     #02 pc 00088bc1  /system/lib/libc.so (__libc_init+48)
08-03 17:50:08.688  3063  3063 F DEBUG   :     #03 pc 0000061f  /vendor/bin/bounds (_start_main+38)
08-03 17:50:08.688  3063  3063 F DEBUG   :     #04 pc 00000306  <anonymous:e85b7000>

Please be noted that, compiler also checks the array access by option -Warray-bounds and will generate warning on invalid access:

vendor/brcm/rpi3/gist/bounds.c:8:5: warning: array index 10 is past the end of the array (which contains 10 elements) [-Warray-bounds]
    a[10] = 0x15;
    ^ ~~
vendor/brcm/rpi3/gist/bounds.c:6:5: note: array 'a' declared here
    int a[10];
    ^

It is always a good habit to NOT ignore any of the warnings, they comes with a reason.

Disabling Bound Sanitizer

Functions or source files can be disabled from sanitizing with the help of function attributes or blacklist option, e.g. you can disable BoundSan for a function by adding below before the function:

#if defined(__clang__)
__attribute__((no_sanitize("bounds")))
#endif

or put these two lines to Android.bp and Android.mk respectively:

# For Android.bp
blacklist: "modulename_blacklist.txt",
# For Android.mk
LOCAL_SANITIZE_BLACKLIST := modulename_blacklist.txt

The file name does not matter, you can use other names.

This is a list of examples stolen from Sanitizer special case list in Clang 12 documentation:

# Lines starting with # are ignored.
# Turn off checks for the source file (use absolute path or path relative
# to the current working directory):
src:/path/to/source/file.c
# Turn off checks for a particular functions (use mangled names):
fun:MyFooBar
fun:_Z8MyFooBarv
# Extended regular expressions are supported:
fun:bad_(foo|bar)
src:bad_source[1-9].c
# Shell like usage of * is supported (* is treated as .*):
src:bad/sources/*
fun:*BadFunction*
# Specific sanitizer tools may introduce categories.
src:/special/path/*=special_sources
# Sections can be used to limit blacklist entries to specific sanitizers
[address]
fun:*BadASanFunc*
# Section names are regular expressions
[cfi-vcall|cfi-icall]
fun:*BadCfiCall
# Entries without sections are placed into [*] and apply to all sanitizers

Integer Overflow Sanitizer

Enabling and disabling IntSan are similar with BoundSan. For static binaries and libraries, individual sanitizers must be specified as integer_overflow does not support them:

   sanitize: {
      misc_undefined: ["signed-integer-overflow", "unsigned-integer-overflow"],
      diag: {
          misc_undefined: ["signed-integer-overflow",
                           "unsigned-integer-overflow",],
      },
      blacklist: "modulename_blacklist.txt",
   },

This example shows a signed-integer-overflow undefined behavior, taken from llvm:

$ cat test.c
int main(int argc, char **argv)
{
	int k = 0x7fffffff;
	k += argc;
	return 0;
}

Build above example with this Android.mk:

LOCAL_PATH:= $(call my-dir)

include $(CLEAR_VARS)

LOCAL_SRC_FILES:= ubsan.c
LOCAL_MODULE:= ubsan
LOCAL_SANITIZE := signed-integer-overflow
LOCAL_SANITIZE_DIAG := signed-integer-overflow
LOCAL_CLANG := true

include $(BUILD_EXECUTABLE)

or use equivalent Android.bp

cc_binary {
    cflags: [
        "-std=c11",
        "-Wall",
        "-O0",
    ],
    srcs: ["ubsan.c"],
    name: "ubsan",
    sanitize: {
        misc_undefined: [
            "signed-integer-overflow",
        ],
        diag: {
            undefined : true
        },
    },
}

A runtime library called libclang_rt.ubsan_standalone-arm-android.so is required to run executables when diagnostic mode is enabled with LOCAL_SANITIZE_DIAG in Android.mk or diag in blueprint file:

adb push out/target/product/rpi3/system/lib/libclang_rt.ubsan_standalone-arm-android.so /system/lib/

signed integer overflow

This message is also logged into logcat.

There is a series of post in LLVM PROJECT BLOG talking about undefined behavior:
What Every C Programmer Should Know About Undefined Behavior

References