allow for heapprofd's signal to be multiplexed

This patch refactors heapprofd_malloc to make it easier to reuse the
reserved signal for multiple purposes. We define a new generic signal
handler for profilers, which dispatches to more specific logic based on
the signal's payload (si_value).

The profiler signal handler is installed during libc preinit, after
malloc initialization (so races against synchronous heapprofd
initialization need not be considered). In terms of code organization, I
copied the existing approach with a loosely referenced function in
bionic_globals.h. Do tell if you'd rather a different approach here.

The profileability of a process is quite tied to the malloc
files/interfaces in bionic - in particular, it's set through
android_mallopt. I do not change that, but instead introduce a new
android_mallopt option to be able to query profileability of the
process (which is now used by the new profiler signal handler). As part
of that, gZygoteChildProfileable is moved from heapprofd_malloc to
common (alongside gZygoteChild).

I've removed the masking and reraising of the heapprofd signal when
racing against malloc_limit init. We're ok with taking a simpler
approach and dropping the heapprofd signal in such an unlikely race.

Note: this requires a corresponding change in heapprofd to use sigqueue()
instead of kill(), as the latter leaves the si_value uninitialized(?) on
the receiving side.

Bug: 144281346
Change-Id: I93bb2e82cff5870e5ca499cf86439860aca9dfa5
This commit is contained in:
Ryan Savitski 2020-01-02 19:54:57 +00:00
parent a04764bd28
commit 175c8867b0
12 changed files with 188 additions and 110 deletions

View File

@ -1613,6 +1613,7 @@ filegroup {
"bionic/icu.cpp",
"bionic/malloc_common.cpp",
"bionic/malloc_common_dynamic.cpp",
"bionic/android_profiling_dynamic.cpp",
"bionic/malloc_heapprofd.cpp",
"bionic/malloc_limit.cpp",
"bionic/ndk_cruft.cpp",

View File

@ -0,0 +1,79 @@
/*
* Copyright (C) 2020 The Android Open Source Project
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
#if defined(LIBC_STATIC)
#error This file should not be compiled for static targets.
#endif
#include <signal.h>
#include <async_safe/log.h>
#include <platform/bionic/malloc.h>
#include <platform/bionic/reserved_signals.h>
#include "malloc_heapprofd.h"
// This file defines the handler for the reserved signal sent by the Android
// platform's profilers. The accompanying signal value discriminates between
// specific requestors:
// 0: heapprofd heap profiler.
static constexpr int kHeapprofdSignalValue = 0;
static void HandleProfilingSignal(int, siginfo_t*, void*);
// Called during dynamic libc preinit.
__LIBC_HIDDEN__ void __libc_init_profiling_handlers() {
struct sigaction action = {};
action.sa_flags = SA_SIGINFO | SA_RESTART;
action.sa_sigaction = HandleProfilingSignal;
sigaction(BIONIC_SIGNAL_PROFILER, &action, nullptr);
}
static void HandleProfilingSignal(int /*signal_number*/, siginfo_t* info, void* /*ucontext*/) {
if (info->si_code != SI_QUEUE)
return;
int signal_value = info->si_value.sival_int;
async_safe_format_log(ANDROID_LOG_WARN, "libc", "%s: received profiling signal with si_value: %d",
getprogname(), signal_value);
// Proceed only if the process is considered profileable.
bool profileable = false;
android_mallopt(M_GET_PROCESS_PROFILEABLE, &profileable, sizeof(profileable));
if (!profileable) {
async_safe_write_log(ANDROID_LOG_ERROR, "libc", "profiling signal rejected (not profileable)");
return;
}
if (signal_value == kHeapprofdSignalValue) {
HandleHeapprofdSignal();
} else {
async_safe_format_log(ANDROID_LOG_ERROR, "libc", "unrecognized profiling signal si_value: %d",
signal_value);
}
}

View File

@ -99,6 +99,9 @@ static void __libc_preinit_impl() {
// Hooks for various libraries to let them know that we're starting up.
__libc_globals.mutate(__libc_init_malloc);
// Install reserved signal handlers for assisting the platform's profilers.
__libc_init_profiling_handlers();
__libc_init_fork_handler();
#if __has_feature(hwaddress_sanitizer)

View File

@ -74,8 +74,6 @@ __END_DECLS
#endif
extern bool gZygoteChild;
static inline const MallocDispatch* GetDispatchTable() {
return atomic_load_explicit(&__libc_globals->current_dispatch_table, memory_order_acquire);
}

View File

@ -75,9 +75,19 @@
// =============================================================================
pthread_mutex_t gGlobalsMutateLock = PTHREAD_MUTEX_INITIALIZER;
bool gZygoteChild = false;
_Atomic bool gGlobalsMutating = false;
static bool gZygoteChild = false;
// In a Zygote child process, this is set to true if profiling of this process
// is allowed. Note that this is set at a later time than gZygoteChild. The
// latter is set during the fork (while still in zygote's SELinux domain). While
// this bit is set after the child is specialized (and has transferred SELinux
// domains if applicable). These two flags are read by the
// BIONIC_SIGNAL_PROFILER handler, which does nothing if the process is not
// profileable.
static _Atomic bool gZygoteChildProfileable = false;
// =============================================================================
static constexpr MallocDispatch __libc_malloc_default_dispatch
@ -391,13 +401,10 @@ static void MallocInitImpl(libc_globals* globals) {
if (HeapprofdShouldLoad()) {
HeapprofdInstallHooksAtInit(globals);
}
// Install this last to avoid as many race conditions as possible.
HeapprofdInstallSignalHandler();
} else {
// Install a signal handler that prints an error since we don't support
// heapprofd and any other hook to be installed at the same time.
HeapprofdInstallErrorSignalHandler();
// Record the fact that incompatible hooks are active, to skip any later
// heapprofd signal handler invocations.
HeapprofdRememberHookConflict();
}
}
@ -476,6 +483,27 @@ extern "C" bool android_mallopt(int opcode, void* arg, size_t arg_size) {
gZygoteChild = true;
return true;
}
if (opcode == M_INIT_ZYGOTE_CHILD_PROFILING) {
if (arg != nullptr || arg_size != 0) {
errno = EINVAL;
return false;
}
atomic_store_explicit(&gZygoteChildProfileable, true, memory_order_release);
// Also check if heapprofd should start profiling from app startup.
HeapprofdInitZygoteChildProfiling();
return true;
}
if (opcode == M_GET_PROCESS_PROFILEABLE) {
if (arg == nullptr || arg_size != sizeof(bool)) {
errno = EINVAL;
return false;
}
// Native processes are considered profileable. Zygote children are considered
// profileable only when appropriately tagged.
*reinterpret_cast<bool*>(arg) =
!gZygoteChild || atomic_load_explicit(&gZygoteChildProfileable, memory_order_acquire);
return true;
}
if (opcode == M_SET_ALLOCATION_LIMIT_BYTES) {
return LimitEnable(arg, arg_size);
}
@ -503,6 +531,7 @@ extern "C" bool android_mallopt(int opcode, void* arg, size_t arg_size) {
if (opcode == M_SET_HEAP_TAGGING_LEVEL) {
return SetHeapTaggingLevel(arg, arg_size);
}
// Try heapprofd's mallopt, as it handles options not covered here.
return HeapprofdMallopt(opcode, arg, arg_size);
}
// =============================================================================

View File

@ -49,18 +49,18 @@
static constexpr char kHeapprofdSharedLib[] = "heapprofd_client.so";
static constexpr char kHeapprofdPrefix[] = "heapprofd";
static constexpr char kHeapprofdPropertyEnable[] = "heapprofd.enable";
static constexpr int kHeapprofdSignal = __SIGRTMIN + 4;
// The logic for triggering heapprofd (at runtime) is as follows:
// 1. HEAPPROFD_SIGNAL is received by the process, entering the
// MaybeInstallInitHeapprofdHook signal handler.
// 1. A reserved profiling signal is received by the process, its si_value
// discriminating between different handlers. For the case of heapprofd,
// HandleHeapprofdSignal is called.
// 2. If the initialization is not already in flight
// (gHeapprofdInitInProgress is false), the malloc hook is set to
// point at InitHeapprofdHook, and gHeapprofdInitInProgress is set to
// true.
// 3. The next malloc call enters InitHeapprofdHook, which removes the malloc
// hook, and spawns a detached pthread to run the InitHeapprofd task.
// (gHeapprofdInitHook_installed atomic is used to perform this once.)
// (gHeapprofdInitHookInstalled atomic is used to perform this once.)
// 4. InitHeapprofd, on a dedicated pthread, loads the heapprofd client library,
// installs the full set of heapprofd hooks, and invokes the client's
// initializer. The dedicated pthread then terminates.
@ -79,12 +79,9 @@ static _Atomic (void*) gHeapprofdHandle = nullptr;
static _Atomic bool gHeapprofdInitInProgress = false;
static _Atomic bool gHeapprofdInitHookInstalled = false;
// In a Zygote child process, this is set to true if profiling of this process
// is allowed. Note that this is set at a later time than the global
// gZygoteChild. The latter is set during the fork (while still in
// zygote's SELinux domain). While this bit is set after the child is
// specialized (and has transferred SELinux domains if applicable).
static _Atomic bool gZygoteChildProfileable = false;
// Set to true if the process has enabled malloc_debug or malloc_hooks, which
// are incompatible (and take precedence over) heapprofd.
static _Atomic bool gHeapprofdIncompatibleHooks = false;
extern "C" void* MallocInitHeapprofdHook(size_t);
@ -93,7 +90,7 @@ static constexpr MallocDispatch __heapprofd_init_dispatch
Malloc(calloc),
Malloc(free),
Malloc(mallinfo),
MallocInitHeapprofdHook,
MallocInitHeapprofdHook, // malloc replacement
Malloc(malloc_usable_size),
Malloc(memalign),
Malloc(posix_memalign),
@ -112,38 +109,6 @@ static constexpr MallocDispatch __heapprofd_init_dispatch
Malloc(malloc_info),
};
static void MaybeInstallInitHeapprofdHook(int) {
// Zygote child processes must be marked profileable.
if (gZygoteChild &&
!atomic_load_explicit(&gZygoteChildProfileable, memory_order_acquire)) {
error_log("%s: not enabling heapprofd, not marked profileable.", getprogname());
return;
}
// Checking this variable is only necessary when this could conflict with
// the change to enable the allocation limit. All other places will
// not ever have a conflict modifying the globals.
if (!atomic_exchange(&gGlobalsMutating, true)) {
if (!atomic_exchange(&gHeapprofdInitInProgress, true)) {
__libc_globals.mutate([](libc_globals* globals) {
atomic_store(&globals->default_dispatch_table, &__heapprofd_init_dispatch);
auto dispatch_table = GetDispatchTable();
if (dispatch_table == nullptr || dispatch_table == &globals->malloc_dispatch_table) {
atomic_store(&globals->current_dispatch_table, &__heapprofd_init_dispatch);
}
});
}
atomic_store(&gGlobalsMutating, false);
} else {
// The only way you can get to this point is if the signal has been
// blocked by a call to HeapprofdMaskSignal. The raise below will
// do nothing until a call to HeapprofdUnmaskSignal, which will cause
// the signal to be resent. Using this avoids the need for a busy loop
// waiting for gGlobalsMutating to change back to false.
raise(kHeapprofdSignal);
}
}
constexpr char kHeapprofdProgramPropertyPrefix[] = "heapprofd.enable.";
constexpr size_t kHeapprofdProgramPropertyPrefixSize = sizeof(kHeapprofdProgramPropertyPrefix) - 1;
constexpr size_t kMaxCmdlineSize = 512;
@ -203,6 +168,39 @@ static bool GetHeapprofdProgramProperty(char* data, size_t size) {
return true;
}
// Runtime triggering entry-point. Two possible call sites:
// * when receiving a profiling signal with a si_value indicating heapprofd.
// * when a Zygote child is marking itself as profileable, and there's a
// matching profiling request for this process (in which case heapprofd client
// is loaded synchronously).
// In both cases, the caller is responsible for verifying that the process is
// considered profileable.
void HandleHeapprofdSignal() {
if (atomic_load_explicit(&gHeapprofdIncompatibleHooks, memory_order_acquire)) {
error_log("%s: not enabling heapprofd, malloc_debug/malloc_hooks are enabled.", getprogname());
return;
}
// Checking this variable is only necessary when this could conflict with
// the change to enable the allocation limit. All other places will
// not ever have a conflict modifying the globals.
if (!atomic_exchange(&gGlobalsMutating, true)) {
if (!atomic_exchange(&gHeapprofdInitInProgress, true)) {
__libc_globals.mutate([](libc_globals* globals) {
atomic_store(&globals->default_dispatch_table, &__heapprofd_init_dispatch);
auto dispatch_table = GetDispatchTable();
if (dispatch_table == nullptr || dispatch_table == &globals->malloc_dispatch_table) {
atomic_store(&globals->current_dispatch_table, &__heapprofd_init_dispatch);
}
});
}
atomic_store(&gGlobalsMutating, false);
}
// Otherwise, we're racing against malloc_limit's enable logic (at most once
// per process, and a niche feature). This is highly unlikely, so simply give
// up if it does happen.
}
bool HeapprofdShouldLoad() {
// First check for heapprofd.enable. If it is set to "all", enable
// heapprofd for all processes. Otherwise, check heapprofd.enable.${prog},
@ -226,38 +224,8 @@ bool HeapprofdShouldLoad() {
return property_value[0] != '\0';
}
void HeapprofdInstallSignalHandler() {
struct sigaction action = {};
action.sa_handler = MaybeInstallInitHeapprofdHook;
sigaction(kHeapprofdSignal, &action, nullptr);
}
extern "C" int __rt_sigprocmask(int, const sigset64_t*, sigset64_t*, size_t);
void HeapprofdMaskSignal() {
sigset64_t mask_set;
// Need to use this function instead because sigprocmask64 filters
// out this signal.
__rt_sigprocmask(SIG_SETMASK, nullptr, &mask_set, sizeof(mask_set));
sigaddset64(&mask_set, kHeapprofdSignal);
__rt_sigprocmask(SIG_SETMASK, &mask_set, nullptr, sizeof(mask_set));
}
void HeapprofdUnmaskSignal() {
sigset64_t mask_set;
__rt_sigprocmask(SIG_SETMASK, nullptr, &mask_set, sizeof(mask_set));
sigdelset64(&mask_set, kHeapprofdSignal);
__rt_sigprocmask(SIG_SETMASK, &mask_set, nullptr, sizeof(mask_set));
}
static void DisplayError(int) {
error_log("Cannot install heapprofd while malloc debug/malloc hooks are enabled.");
}
void HeapprofdInstallErrorSignalHandler() {
struct sigaction action = {};
action.sa_handler = DisplayError;
sigaction(kHeapprofdSignal, &action, nullptr);
void HeapprofdRememberHookConflict() {
atomic_store_explicit(&gHeapprofdIncompatibleHooks, true, memory_order_release);
}
static void CommonInstallHooks(libc_globals* globals) {
@ -326,15 +294,12 @@ extern "C" void* MallocInitHeapprofdHook(size_t bytes) {
return Malloc(malloc)(bytes);
}
// Marks this process as a profileable zygote child.
static bool HandleInitZygoteChildProfiling() {
atomic_store_explicit(&gZygoteChildProfileable, true, memory_order_release);
bool HeapprofdInitZygoteChildProfiling() {
// Conditionally start "from startup" profiling.
if (HeapprofdShouldLoad()) {
// Directly call the signal handler (will correctly guard against
// concurrent signal delivery).
MaybeInstallInitHeapprofdHook(kHeapprofdSignal);
// Directly call the signal handler codepath (properly protects against
// concurrent invocations).
HandleHeapprofdSignal();
}
return true;
}
@ -358,13 +323,6 @@ static bool DispatchReset() {
}
bool HeapprofdMallopt(int opcode, void* arg, size_t arg_size) {
if (opcode == M_INIT_ZYGOTE_CHILD_PROFILING) {
if (arg != nullptr || arg_size != 0) {
errno = EINVAL;
return false;
}
return HandleInitZygoteChildProfiling();
}
if (opcode == M_RESET_HOOKS) {
if (arg != nullptr || arg_size != 0) {
errno = EINVAL;

View File

@ -36,12 +36,11 @@ bool HeapprofdShouldLoad();
void HeapprofdInstallHooksAtInit(libc_globals* globals);
void HeapprofdInstallSignalHandler();
void HeapprofdRememberHookConflict();
void HeapprofdInstallErrorSignalHandler();
void HandleHeapprofdSignal();
void HeapprofdMaskSignal();
void HeapprofdUnmaskSignal();
bool HeapprofdInitZygoteChildProfiling();
bool HeapprofdMallopt(int optcode, void* arg, size_t arg_size);

View File

@ -264,7 +264,6 @@ static bool EnableLimitDispatchTable() {
}
#else
static bool EnableLimitDispatchTable() {
HeapprofdMaskSignal();
pthread_mutex_lock(&gGlobalsMutateLock);
// All other code that calls mutate will grab the gGlobalsMutateLock.
// However, there is one case where the lock cannot be acquired, in the
@ -272,7 +271,7 @@ static bool EnableLimitDispatchTable() {
// threads calling mutate at the same time, use an atomic variable to
// verify that only this function or the signal handler are calling mutate.
// If this function is called at the same time as the signal handler is
// being called, allow up to five ms for the signal handler to complete
// being called, allow a short period for the signal handler to complete
// before failing.
bool enabled = false;
size_t num_tries = 20;
@ -291,7 +290,6 @@ static bool EnableLimitDispatchTable() {
usleep(1000);
}
pthread_mutex_unlock(&gGlobalsMutateLock);
HeapprofdUnmaskSignal();
if (enabled) {
info_log("malloc_limit: Allocation limit enabled, max size %" PRId64 " bytes\n", gAllocLimit);
} else {

View File

@ -91,6 +91,12 @@ enum {
// arg_size = sizeof(HeapTaggingLevel)
M_SET_HEAP_TAGGING_LEVEL = 8,
#define M_SET_HEAP_TAGGING_LEVEL M_SET_HEAP_TAGGING_LEVEL
// Query whether the current process is considered to be profileable by the
// Android platform. Result is assigned to the arg pointer's destination.
// arg = bool*
// arg_size = sizeof(bool)
M_GET_PROCESS_PROFILEABLE = 9,
#define M_GET_PROCESS_PROFILEABLE M_GET_PROCESS_PROFILEABLE
};
enum HeapTaggingLevel {

View File

@ -39,7 +39,7 @@
// 33 (__SIGRTMIN + 1) libbacktrace
// 34 (__SIGRTMIN + 2) libcore
// 35 (__SIGRTMIN + 3) debuggerd
// 36 (__SIGRTMIN + 4) heapprofd native dumps
// 36 (__SIGRTMIN + 4) platform profiling (heapprofd)
// 37 (__SIGRTMIN + 5) coverage (libprofile-extras)
// 38 (__SIGRTMIN + 6) heapprofd ART managed heap dumps
//
@ -47,6 +47,7 @@
// in <android/legacy_signal_inlines.h> to match.
#define BIONIC_SIGNAL_DEBUGGER __SIGRTMIN + 3
#define BIONIC_SIGNAL_PROFILER __SIGRTMIN + 4
#define __SIGRT_RESERVED 7
static inline __always_inline sigset64_t filter_reserved_signals(sigset64_t sigset, int how) {

View File

@ -99,6 +99,7 @@ struct libc_shared_globals {
__LIBC_HIDDEN__ libc_shared_globals* __libc_shared_globals();
__LIBC_HIDDEN__ void __libc_init_fdsan();
__LIBC_HIDDEN__ void __libc_init_profiling_handlers();
__LIBC_HIDDEN__ void __libc_init_malloc(libc_globals* globals);
__LIBC_HIDDEN__ void __libc_init_setjmp_cookie(libc_globals* globals);

View File

@ -18,14 +18,15 @@
#include <elf.h>
#include <limits.h>
#include <malloc.h>
#include <pthread.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <malloc.h>
#include <unistd.h>
#include <atomic>
@ -962,7 +963,11 @@ static void SetAllocationLimitMultipleThreads() {
// Let them go all at once.
go = true;
ASSERT_EQ(0, kill(getpid(), __SIGRTMIN + 4));
// Send hardcoded signal (BIONIC_SIGNAL_PROFILER with value 0) to trigger
// heapprofd handler.
union sigval signal_value;
signal_value.sival_int = 0;
ASSERT_EQ(0, sigqueue(getpid(), __SIGRTMIN + 4, signal_value));
size_t num_successful = 0;
for (size_t i = 0; i < kNumThreads; i++) {