[GWP-ASan] Provide runtime configuration through an env var + sysprop.
This patch introduces GWP-ASan system properties and environment
variables to control the internal sampling rates of GWP-ASan. This can
be used for:
1. "Torture testing" the system, i.e. running it under an extremely
high sampling rate under GWP-ASan.
2. Increasing sampling remotely to allow further crash report
collection of rare issues.
There are three sets of system properites:
1. libc.debug.gwp_asan.*.system_default: Default values for native
executables and system apps.
2. libc.debug.gwp_asan.*.app_default: Default values for non-system
apps, and
3. libc.debug.gwp_asan.*.<basename/app_name>: Default values for an
individual app or native process.
There are three variables that can be changed:
1. The allocation sampling rate (default: 2500) - using the environment
variable GWP_ASAN_SAMPLE_RATE or the libc.debug.gwp_asan.sample_rate.*
system property.
2. The process sampling rate (default: 128 for system apps/processes, 1
for opted-in apps) - using the environment variable
GWP_ASAN_PROCESS_SAMPLING or the libc.debug.gwp_asan.process_sampling.*
system property,
3. The number of slots available (default: 32) - using the environment
variable GWP_ASAN_MAX_ALLOCS or the libc.debug.gwp_asan.max_allocs.*
system property.
If not specified, #3 will be calculated as a ratio of the default
|2500 SampleRate : 32 slots|. So, a sample rate of "1250" (i.e. twice as
frequent sampling) will result in a doubling of the max_allocs to "64".
Bug: 219651032
Test: atest bionic-unit-tests
Change-Id: Idb40a2a4d074e01ce3c4e635ad639a91a32d570f
diff --git a/libc/bionic/gwp_asan_wrappers.cpp b/libc/bionic/gwp_asan_wrappers.cpp
index 8c51347..2f513e5 100644
--- a/libc/bionic/gwp_asan_wrappers.cpp
+++ b/libc/bionic/gwp_asan_wrappers.cpp
@@ -26,21 +26,27 @@
* SUCH DAMAGE.
*/
-#include <platform/bionic/android_unsafe_frame_pointer_chase.h>
-#include <platform/bionic/malloc.h>
-#include <private/bionic_arc4random.h>
-#include <private/bionic_globals.h>
-#include <private/bionic_malloc_dispatch.h>
+#include <alloca.h>
+#include <assert.h>
+#include <ctype.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
+#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
-#include "bionic/gwp_asan_wrappers.h"
#include "gwp_asan/guarded_pool_allocator.h"
#include "gwp_asan/options.h"
+#include "gwp_asan_wrappers.h"
#include "malloc_common.h"
+#include "platform/bionic/android_unsafe_frame_pointer_chase.h"
+#include "platform/bionic/malloc.h"
+#include "private/bionic_arc4random.h"
+#include "private/bionic_globals.h"
+#include "private/bionic_malloc_dispatch.h"
+#include "sys/system_properties.h"
+#include "sysprop_helpers.h"
#ifndef LIBC_STATIC
#include "bionic/malloc_common_dynamic.h"
@@ -49,59 +55,14 @@
static gwp_asan::GuardedPoolAllocator GuardedAlloc;
static const MallocDispatch* prev_dispatch;
+using Action = android_mallopt_gwp_asan_options_t::Action;
using Options = gwp_asan::options::Options;
-// ============================================================================
-// Implementation of gFunctions.
-// ============================================================================
+// basename() is a mess, see the manpage. Let's be explicit what handling we
+// want (don't touch my string!).
+extern "C" const char* __gnu_basename(const char* path);
-// This function handles initialisation as asked for by MallocInitImpl. This
-// should always be called in a single-threaded context.
-bool gwp_asan_initialize(const MallocDispatch* dispatch, bool*, const char*) {
- prev_dispatch = dispatch;
-
- Options Opts;
- Opts.Enabled = true;
- Opts.MaxSimultaneousAllocations = 32;
- Opts.SampleRate = 2500;
- Opts.InstallSignalHandlers = false;
- Opts.InstallForkHandlers = true;
- Opts.Backtrace = android_unsafe_frame_pointer_chase;
-
- GuardedAlloc.init(Opts);
- // TODO(b/149790891): The log line below causes ART tests to fail as they're
- // not expecting any output. Disable the output for now.
- // info_log("GWP-ASan has been enabled.");
-
- __libc_shared_globals()->gwp_asan_state = GuardedAlloc.getAllocatorState();
- __libc_shared_globals()->gwp_asan_metadata = GuardedAlloc.getMetadataRegion();
- return true;
-}
-
-void gwp_asan_finalize() {
-}
-
-void gwp_asan_get_malloc_leak_info(uint8_t**, size_t*, size_t*, size_t*, size_t*) {
-}
-
-void gwp_asan_free_malloc_leak_info(uint8_t*) {
-}
-
-ssize_t gwp_asan_malloc_backtrace(void*, uintptr_t*, size_t) {
- // TODO(mitchp): GWP-ASan might be able to return the backtrace for the
- // provided address.
- return -1;
-}
-
-bool gwp_asan_write_malloc_leak_info(FILE*) {
- return false;
-}
-
-void* gwp_asan_gfunctions[] = {
- (void*)gwp_asan_initialize, (void*)gwp_asan_finalize,
- (void*)gwp_asan_get_malloc_leak_info, (void*)gwp_asan_free_malloc_leak_info,
- (void*)gwp_asan_malloc_backtrace, (void*)gwp_asan_write_malloc_leak_info,
-};
+namespace {
// ============================================================================
// Implementation of GWP-ASan malloc wrappers.
@@ -175,66 +136,202 @@
prev_dispatch->malloc_enable();
}
-static const MallocDispatch gwp_asan_dispatch __attribute__((unused)) = {
- gwp_asan_calloc,
- gwp_asan_free,
- Malloc(mallinfo),
- gwp_asan_malloc,
- gwp_asan_malloc_usable_size,
- Malloc(memalign),
- Malloc(posix_memalign),
+const MallocDispatch gwp_asan_dispatch __attribute__((unused)) = {
+ gwp_asan_calloc,
+ gwp_asan_free,
+ Malloc(mallinfo),
+ gwp_asan_malloc,
+ gwp_asan_malloc_usable_size,
+ Malloc(memalign),
+ Malloc(posix_memalign),
#if defined(HAVE_DEPRECATED_MALLOC_FUNCS)
- Malloc(pvalloc),
+ Malloc(pvalloc),
#endif
- gwp_asan_realloc,
+ gwp_asan_realloc,
#if defined(HAVE_DEPRECATED_MALLOC_FUNCS)
- Malloc(valloc),
+ Malloc(valloc),
#endif
- gwp_asan_malloc_iterate,
- gwp_asan_malloc_disable,
- gwp_asan_malloc_enable,
- Malloc(mallopt),
- Malloc(aligned_alloc),
- Malloc(malloc_info),
+ gwp_asan_malloc_iterate,
+ gwp_asan_malloc_disable,
+ gwp_asan_malloc_enable,
+ Malloc(mallopt),
+ Malloc(aligned_alloc),
+ Malloc(malloc_info),
};
-// The probability (1 / kProcessSampleRate) that a process will be ranodmly
-// selected for sampling. kProcessSampleRate should always be a power of two to
-// avoid modulo bias.
-static constexpr uint8_t kProcessSampleRate = 128;
+bool isPowerOfTwo(uint64_t x) {
+ assert(x != 0);
+ return (x & (x - 1)) == 0;
+}
-bool ShouldGwpAsanSampleProcess() {
+bool ShouldGwpAsanSampleProcess(unsigned sample_rate) {
+ if (!isPowerOfTwo(sample_rate)) {
+ warning_log(
+ "GWP-ASan process sampling rate of %u is not a power-of-two, and so modulo bias occurs.",
+ sample_rate);
+ }
+
uint8_t random_number;
__libc_safe_arc4random_buf(&random_number, sizeof(random_number));
- return random_number % kProcessSampleRate == 0;
+ return random_number % sample_rate == 0;
}
-bool MaybeInitGwpAsanFromLibc(libc_globals* globals) {
- // Never initialize the Zygote here. A Zygote chosen for sampling would also
- // have all of its children sampled. Instead, the Zygote child will choose
- // whether it samples or not just after the Zygote forks. For
- // libc_scudo-preloaded executables (like mediaswcodec), the program name
- // might not be available yet. The zygote never uses dynamic libc_scudo.
- const char* progname = getprogname();
- if (progname && strncmp(progname, "app_process", 11) == 0) {
+bool GwpAsanInitialized = false;
+
+// The probability (1 / SampleRate) that an allocation gets chosen to be put
+// into the special GWP-ASan pool.
+using SampleRate_t = typeof(gwp_asan::options::Options::SampleRate);
+constexpr SampleRate_t kDefaultSampleRate = 2500;
+static const char* kSampleRateSystemSysprop = "libc.debug.gwp_asan.sample_rate.system_default";
+static const char* kSampleRateAppSysprop = "libc.debug.gwp_asan.sample_rate.app_default";
+static const char* kSampleRateTargetedSyspropPrefix = "libc.debug.gwp_asan.sample_rate.";
+static const char* kSampleRateEnvVar = "GWP_ASAN_SAMPLE_RATE";
+
+// The probability (1 / ProcessSampling) that a process will be randomly
+// selected for sampling, for system apps and system processes. The process
+// sampling rate should always be a power of two to avoid modulo bias.
+constexpr unsigned kDefaultProcessSampling = 128;
+static const char* kProcessSamplingSystemSysprop =
+ "libc.debug.gwp_asan.process_sampling.system_default";
+static const char* kProcessSamplingAppSysprop = "libc.debug.gwp_asan.process_sampling.app_default";
+static const char* kProcessSamplingTargetedSyspropPrefix = "libc.debug.gwp_asan.process_sampling.";
+static const char* kProcessSamplingEnvVar = "GWP_ASAN_PROCESS_SAMPLING";
+
+// The upper limit of simultaneous allocations supported by GWP-ASan. Any
+// allocations in excess of this limit will be passed to the backing allocator
+// and can't be sampled. This value, if unspecified, will be automatically
+// calculated to keep the same ratio as the default (2500 sampling : 32 allocs).
+// So, if you specify GWP_ASAN_SAMPLE_RATE=1250 (i.e. twice as frequent), we'll
+// automatically calculate that we need double the slots (64).
+using SimultaneousAllocations_t = typeof(gwp_asan::options::Options::MaxSimultaneousAllocations);
+constexpr SimultaneousAllocations_t kDefaultMaxAllocs = 32;
+static const char* kMaxAllocsSystemSysprop = "libc.debug.gwp_asan.max_allocs.system_default";
+static const char* kMaxAllocsAppSysprop = "libc.debug.gwp_asan.max_allocs.app_default";
+static const char* kMaxAllocsTargetedSyspropPrefix = "libc.debug.gwp_asan.max_allocs.";
+static const char* kMaxAllocsEnvVar = "GWP_ASAN_MAX_ALLOCS";
+
+void SetDefaultGwpAsanOptions(Options* options, unsigned* process_sample_rate,
+ const android_mallopt_gwp_asan_options_t& mallopt_options) {
+ options->Enabled = true;
+ options->InstallSignalHandlers = false;
+ options->InstallForkHandlers = true;
+ options->Backtrace = android_unsafe_frame_pointer_chase;
+ options->SampleRate = kDefaultSampleRate;
+ options->MaxSimultaneousAllocations = kDefaultMaxAllocs;
+
+ *process_sample_rate = 1;
+ if (mallopt_options.desire == Action::TURN_ON_WITH_SAMPLING) {
+ *process_sample_rate = kDefaultProcessSampling;
+ }
+}
+
+bool GetGwpAsanOption(unsigned long long* result,
+ const android_mallopt_gwp_asan_options_t& mallopt_options,
+ const char* system_sysprop, const char* app_sysprop,
+ const char* targeted_sysprop_prefix, const char* env_var,
+ const char* descriptive_name) {
+ const char* basename = "";
+ if (mallopt_options.program_name) basename = __gnu_basename(mallopt_options.program_name);
+
+ size_t program_specific_sysprop_size = strlen(targeted_sysprop_prefix) + strlen(basename) + 1;
+ char* program_specific_sysprop_name = static_cast<char*>(alloca(program_specific_sysprop_size));
+ async_safe_format_buffer(program_specific_sysprop_name, program_specific_sysprop_size, "%s%s",
+ targeted_sysprop_prefix, basename);
+
+ const char* sysprop_names[2] = {nullptr, nullptr};
+ // Tests use a blank program name to specify that system properties should not
+ // be used. Tests still continue to use the environment variable though.
+ if (*basename != '\0') {
+ sysprop_names[0] = program_specific_sysprop_name;
+ if (mallopt_options.desire == Action::TURN_ON_FOR_APP) {
+ sysprop_names[1] = app_sysprop;
+ } else {
+ sysprop_names[1] = system_sysprop;
+ }
+ }
+
+ char settings_buf[PROP_VALUE_MAX];
+ if (!get_config_from_env_or_sysprops(env_var, sysprop_names,
+ /* sys_prop_names_size */ 2, settings_buf, PROP_VALUE_MAX)) {
return false;
}
- return MaybeInitGwpAsan(globals);
+
+ char* end;
+ unsigned long long value = strtoull(settings_buf, &end, 10);
+ if (value == ULLONG_MAX || *end != '\0') {
+ warning_log("Invalid GWP-ASan %s: \"%s\". Using default value instead.", descriptive_name,
+ settings_buf);
+ return false;
+ }
+
+ *result = value;
+ return true;
}
-static bool GwpAsanInitialized = false;
+// Initialize the GWP-ASan options structure in *options, taking into account whether someone has
+// asked for specific GWP-ASan settings. The order of priority is:
+// 1. Environment variables.
+// 2. Process-specific system properties.
+// 3. Global system properties.
+// If any of these overrides are found, we return true. Otherwise, use the default values, and
+// return false.
+bool GetGwpAsanOptions(Options* options, unsigned* process_sample_rate,
+ const android_mallopt_gwp_asan_options_t& mallopt_options) {
+ SetDefaultGwpAsanOptions(options, process_sample_rate, mallopt_options);
-// Maybe initializes GWP-ASan. Called by android_mallopt() and libc's
-// initialisation. This should always be called in a single-threaded context.
-bool MaybeInitGwpAsan(libc_globals* globals, bool force_init) {
+ bool had_overrides = false;
+
+ unsigned long long buf;
+ if (GetGwpAsanOption(&buf, mallopt_options, kSampleRateSystemSysprop, kSampleRateAppSysprop,
+ kSampleRateTargetedSyspropPrefix, kSampleRateEnvVar, "sample rate")) {
+ options->SampleRate = buf;
+ had_overrides = true;
+ }
+
+ if (GetGwpAsanOption(&buf, mallopt_options, kProcessSamplingSystemSysprop,
+ kProcessSamplingAppSysprop, kProcessSamplingTargetedSyspropPrefix,
+ kProcessSamplingEnvVar, "process sampling rate")) {
+ *process_sample_rate = buf;
+ had_overrides = true;
+ }
+
+ if (GetGwpAsanOption(&buf, mallopt_options, kMaxAllocsSystemSysprop, kMaxAllocsAppSysprop,
+ kMaxAllocsTargetedSyspropPrefix, kMaxAllocsEnvVar,
+ "maximum simultaneous allocations")) {
+ options->MaxSimultaneousAllocations = buf;
+ had_overrides = true;
+ } else if (had_overrides) {
+ // Multiply the number of slots available, such that the ratio between
+ // sampling rate and slots is kept the same as the default. For example, a
+ // sampling rate of 1000 is 2.5x more frequent than default, and so
+ // requires 80 slots (32 * 2.5).
+ float frequency_multiplier = static_cast<float>(options->SampleRate) / kDefaultSampleRate;
+ options->MaxSimultaneousAllocations =
+ /* default */ kDefaultMaxAllocs / frequency_multiplier;
+ }
+ return had_overrides;
+}
+
+bool MaybeInitGwpAsan(libc_globals* globals,
+ const android_mallopt_gwp_asan_options_t& mallopt_options) {
if (GwpAsanInitialized) {
error_log("GWP-ASan was already initialized for this process.");
return false;
}
- // If the caller hasn't forced GWP-ASan on, check whether we should sample
- // this process.
- if (!force_init && !ShouldGwpAsanSampleProcess()) {
+ Options options;
+ unsigned process_sample_rate = kDefaultProcessSampling;
+ if (!GetGwpAsanOptions(&options, &process_sample_rate, mallopt_options) &&
+ mallopt_options.desire == Action::DONT_TURN_ON_UNLESS_OVERRIDDEN) {
+ return false;
+ }
+
+ if (options.SampleRate == 0 || process_sample_rate == 0 ||
+ options.MaxSimultaneousAllocations == 0) {
+ return false;
+ }
+
+ if (!ShouldGwpAsanSampleProcess(process_sample_rate)) {
return false;
}
@@ -263,28 +360,48 @@
atomic_store(&globals->current_dispatch_table, &gwp_asan_dispatch);
}
-#ifndef LIBC_STATIC
- SetGlobalFunctions(gwp_asan_gfunctions);
-#endif // LIBC_STATIC
-
GwpAsanInitialized = true;
- gwp_asan_initialize(NativeAllocatorDispatch(), nullptr, nullptr);
+ prev_dispatch = NativeAllocatorDispatch();
+
+ GuardedAlloc.init(options);
+
+ __libc_shared_globals()->gwp_asan_state = GuardedAlloc.getAllocatorState();
+ __libc_shared_globals()->gwp_asan_metadata = GuardedAlloc.getMetadataRegion();
return true;
}
+}; // anonymous namespace
+
+bool MaybeInitGwpAsanFromLibc(libc_globals* globals) {
+ // Never initialize the Zygote here. A Zygote chosen for sampling would also
+ // have all of its children sampled. Instead, the Zygote child will choose
+ // whether it samples or not just after the Zygote forks. Note that the Zygote
+ // changes its name after it's started, at this point it's still called
+ // "app_process" or "app_process64".
+ static const char kAppProcessNamePrefix[] = "app_process";
+ const char* progname = getprogname();
+ if (strncmp(progname, kAppProcessNamePrefix, sizeof(kAppProcessNamePrefix) - 1) == 0)
+ return false;
+
+ android_mallopt_gwp_asan_options_t mallopt_options;
+ mallopt_options.program_name = progname;
+ mallopt_options.desire = Action::TURN_ON_WITH_SAMPLING;
+
+ return MaybeInitGwpAsan(globals, mallopt_options);
+}
bool DispatchIsGwpAsan(const MallocDispatch* dispatch) {
return dispatch == &gwp_asan_dispatch;
}
-bool EnableGwpAsan(bool force_init) {
+bool EnableGwpAsan(const android_mallopt_gwp_asan_options_t& options) {
if (GwpAsanInitialized) {
return true;
}
bool ret_value;
__libc_globals.mutate(
- [&](libc_globals* globals) { ret_value = MaybeInitGwpAsan(globals, force_init); });
+ [&](libc_globals* globals) { ret_value = MaybeInitGwpAsan(globals, options); });
return ret_value;
}