blob: c5c74dcc335e4d391f13d17d49438d3e0bfd27d3 [file] [log] [blame]
// Copyright 2022 The Centipede Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Instrumentation callbacks for SanitizerCoverage (sancov).
// https://clang.llvm.org/docs/SanitizerCoverage.html
#include <pthread.h>
#include <cstddef>
#include <cstdint>
#include "./centipede/feature.h"
#include "./centipede/pc_info.h"
#include "./centipede/reverse_pc_table.h"
#include "./centipede/runner.h"
#include "./centipede/runner_utils.h"
namespace centipede {
void RunnerSancov() {} // to be referenced in runner.cc
} // namespace centipede
using centipede::PCGuard;
using centipede::PCInfo;
using centipede::RunnerCheck;
using centipede::state;
using centipede::tls;
// Tracing data flow.
// The instrumentation is provided by
// https://clang.llvm.org/docs/SanitizerCoverage.html#tracing-data-flow.
// For every load we get the address of the load. We can also get the caller PC.
// If the load address in
// [main_object.start_address, main_object.start_address + main_object.size),
// it is likely a global.
// We form a feature from a pair of {caller_pc, address_of_load}.
// The rationale here is that loading from a global address unique for the
// given PC is an interesting enough behavior that it warrants its own feature.
//
// Downsides:
// * The instrumentation is expensive, it can easily add 2x slowdown.
// * This creates plenty of features, easily 10x compared to control flow,
// and bloats the corpus. But this is also what we want to achieve here.
// NOTE: In addition to `always_inline`, also use `inline`, because some
// compilers require both to actually enforce inlining, e.g. GCC:
// https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html.
#define ENFORCE_INLINE __attribute__((always_inline)) inline
// Use this attribute for functions that must not be instrumented even if
// the runner is built with sanitizers (asan, etc).
#define NO_SANITIZE __attribute__((no_sanitize("all")))
// NOTE: Enforce inlining so that `__builtin_return_address` works.
ENFORCE_INLINE static void TraceLoad(void *addr) {
if (!state.run_time_flags.use_dataflow_features) return;
auto caller_pc = reinterpret_cast<uintptr_t>(__builtin_return_address(0));
auto load_addr = reinterpret_cast<uintptr_t>(addr);
auto pc_offset = caller_pc - state.main_object.start_address;
if (pc_offset >= state.main_object.size) return; // PC outside main obj.
auto addr_offset = load_addr - state.main_object.start_address;
if (addr_offset >= state.main_object.size) return; // Not a global address.
state.data_flow_feature_set.set(centipede::ConvertPcPairToNumber(
pc_offset, addr_offset, state.main_object.size));
}
// NOTE: Enforce inlining so that `__builtin_return_address` works.
ENFORCE_INLINE static void TraceCmp(uint64_t Arg1, uint64_t Arg2) {
if (!state.run_time_flags.use_cmp_features) return;
auto caller_pc = reinterpret_cast<uintptr_t>(__builtin_return_address(0));
auto pc_offset = caller_pc - state.main_object.start_address;
uintptr_t hash =
centipede::Hash64Bits(pc_offset) ^ tls.path_ring_buffer.hash();
if (Arg1 == Arg2) {
state.cmp_eq_set.set(hash);
} else {
hash <<= 6; // ABTo* generate 6-bit numbers.
state.cmp_moddiff_set.set(hash | centipede::ABToCmpModDiff(Arg1, Arg2));
state.cmp_hamming_set.set(hash | centipede::ABToCmpHamming(Arg1, Arg2));
state.cmp_difflog_set.set(hash | centipede::ABToCmpDiffLog(Arg1, Arg2));
}
}
//------------------------------------------------------------------------------
// Implementations of the external sanitizer coverage hooks.
//------------------------------------------------------------------------------
extern "C" {
NO_SANITIZE void __sanitizer_cov_load1(uint8_t *addr) { TraceLoad(addr); }
NO_SANITIZE void __sanitizer_cov_load2(uint16_t *addr) { TraceLoad(addr); }
NO_SANITIZE void __sanitizer_cov_load4(uint32_t *addr) { TraceLoad(addr); }
NO_SANITIZE void __sanitizer_cov_load8(uint64_t *addr) { TraceLoad(addr); }
NO_SANITIZE void __sanitizer_cov_load16(__uint128_t *addr) { TraceLoad(addr); }
NO_SANITIZE
void __sanitizer_cov_trace_const_cmp1(uint8_t Arg1, uint8_t Arg2) {
TraceCmp(Arg1, Arg2);
}
NO_SANITIZE
void __sanitizer_cov_trace_const_cmp2(uint16_t Arg1, uint16_t Arg2) {
TraceCmp(Arg1, Arg2);
if (Arg1 != Arg2 && state.run_time_flags.use_auto_dictionary)
tls.cmp_trace2.Capture(Arg1, Arg2);
}
NO_SANITIZE
void __sanitizer_cov_trace_const_cmp4(uint32_t Arg1, uint32_t Arg2) {
TraceCmp(Arg1, Arg2);
if (Arg1 != Arg2 && state.run_time_flags.use_auto_dictionary)
tls.cmp_trace4.Capture(Arg1, Arg2);
}
NO_SANITIZE
void __sanitizer_cov_trace_const_cmp8(uint64_t Arg1, uint64_t Arg2) {
TraceCmp(Arg1, Arg2);
if (Arg1 != Arg2 && state.run_time_flags.use_auto_dictionary)
tls.cmp_trace8.Capture(Arg1, Arg2);
}
NO_SANITIZE
void __sanitizer_cov_trace_cmp1(uint8_t Arg1, uint8_t Arg2) {
TraceCmp(Arg1, Arg2);
}
NO_SANITIZE
void __sanitizer_cov_trace_cmp2(uint16_t Arg1, uint16_t Arg2) {
TraceCmp(Arg1, Arg2);
if (Arg1 != Arg2 && state.run_time_flags.use_auto_dictionary)
tls.cmp_trace2.Capture(Arg1, Arg2);
}
NO_SANITIZE
void __sanitizer_cov_trace_cmp4(uint32_t Arg1, uint32_t Arg2) {
TraceCmp(Arg1, Arg2);
if (Arg1 != Arg2 && state.run_time_flags.use_auto_dictionary)
tls.cmp_trace4.Capture(Arg1, Arg2);
}
NO_SANITIZE
void __sanitizer_cov_trace_cmp8(uint64_t Arg1, uint64_t Arg2) {
TraceCmp(Arg1, Arg2);
if (Arg1 != Arg2 && state.run_time_flags.use_auto_dictionary)
tls.cmp_trace8.Capture(Arg1, Arg2);
}
// TODO(kcc): [impl] handle switch.
NO_SANITIZE
void __sanitizer_cov_trace_switch(uint64_t Val, uint64_t *Cases) {}
// This function is called at startup when
// -fsanitize-coverage=inline-8bit-counters is used.
// See https://clang.llvm.org/docs/SanitizerCoverage.html#inline-8bit-counters
void __sanitizer_cov_8bit_counters_init(uint8_t *beg, uint8_t *end) {
if (state.inline_8bit_counters_start == nullptr) {
state.inline_8bit_counters_start = beg;
state.inline_8bit_counters_stop = end;
} else {
RunnerCheck(
state.inline_8bit_counters_start == beg &&
state.inline_8bit_counters_stop == end,
"__sanitizer_cov_8bit_counters_init is called with different "
"arguments than previously. This may indicate more than one DSO "
"instrumented with sancov. This is currently not supported by the "
"Centipede runner. Please let the Centipede developers know if this is "
"an important use case.");
}
}
// https://clang.llvm.org/docs/SanitizerCoverage.html#pc-table
// This function is called at the DSO init time, potentially several times.
// When called from the same DSO, the arguments will always be the same.
// If a different DSO calls this function, it will have different arguments.
// We currently do not support more than one sancov-instrumented DSO.
void __sanitizer_cov_pcs_init(const PCInfo *beg, const PCInfo *end) {
size_t guard_size = state.pc_guard_stop - state.pc_guard_start;
size_t counter_size =
state.inline_8bit_counters_stop - state.inline_8bit_counters_start;
RunnerCheck(guard_size != 0 || counter_size != 0,
"__sanitizer_cov_pcs_init is called before either of"
"__sanitizer_cov_trace_pc_guard_init or "
"__sanitizer_cov_8bit_counters_init");
RunnerCheck(guard_size == 0 || counter_size == 0,
"Simulteneously using __sanitizer_cov_trace_pc_guard_init and "
"__sanitizer_cov_8bit_counters_init");
RunnerCheck(std::max(guard_size, counter_size) == end - beg,
"__sanitizer_cov_pcs_init: mismatch between guard/counter size"
" and pc table size");
if (state.pcs_beg == nullptr) {
state.pcs_beg = beg;
state.pcs_end = end;
if (guard_size != 0) {
// Set is_function_entry for all the guards.
for (size_t i = 0, n = end - beg; i < n; ++i) {
state.pc_guard_start[i].is_function_entry =
beg[i].has_flag(PCInfo::kFuncEntry);
}
}
} else {
RunnerCheck(
state.pcs_beg == beg && state.pcs_end == end,
"__sanitizer_cov_pcs_init is called with different "
"arguments than previously. This may indicate more than one DSO "
"instrumented with sancov. This is currently not supported by the "
"Centipede runner. Please let the Centipede developers know if this is "
"an important use case.");
}
}
// https://clang.llvm.org/docs/SanitizerCoverage.html#tracing-control-flow
// This function is called at the DSO init time.
void __sanitizer_cov_cfs_init(const uintptr_t *beg, const uintptr_t *end) {
state.cfs_beg = beg;
state.cfs_end = end;
}
// Updates the state of the paths, `path_level > 0`.
// Marked noinline so that not to create spills/fills on the fast path
// of __sanitizer_cov_trace_pc_guard.
__attribute__((noinline)) static void HandlePath(uintptr_t normalized_pc) {
uintptr_t hash = tls.path_ring_buffer.push(normalized_pc);
state.path_feature_set.set(hash);
}
// Handles one observed PC.
// `normalized_pc` is an integer representation of PC that is stable between
// the executions.
// `is_function_entry` is true if the PC is known to be a function entry.
// With __sanitizer_cov_trace_pc_guard this is an index of PC in the PC table.
// With __sanitizer_cov_trace_pc this is PC itself, normalized by subtracting
// the DSO's dynamic start address.
static inline void HandleOnePc(PCGuard pc_guard) {
if (!state.run_time_flags.use_pc_features) return;
state.pc_counter_set.SaturatedIncrement(pc_guard.pc_index);
if (pc_guard.is_function_entry && state.run_time_flags.callstack_level != 0) {
uintptr_t sp = reinterpret_cast<uintptr_t>(__builtin_frame_address(0));
if (sp < tls.lowest_sp) tls.lowest_sp = sp;
tls.call_stack.OnFunctionEntry(pc_guard.pc_index, sp);
state.callstack_set.set(tls.call_stack.Hash());
}
// path features.
if (state.run_time_flags.path_level != 0) HandlePath(pc_guard.pc_index);
}
// Caller PC is the PC of the call instruction.
// Return address is the PC where the callee will return upon completion.
// On x86_64, CallerPC == ReturnAddress - 5
// On AArch64, CallerPC == ReturnAddress - 4
static uintptr_t ReturnAddressToCallerPc(uintptr_t return_address) {
#ifdef __x86_64__
return return_address - 5;
#elif defined(__aarch64__)
return return_address - 4;
#else
#error "unsupported architecture"
#endif
}
// Lazily initializes actual_pc_counter_set_size_aligned.
static void LazyAllocatePcCounters(size_t size) {
if (state.actual_pc_counter_set_size_aligned) return;
constexpr size_t kAlignment = state.pc_counter_set.kSizeMultiple;
constexpr size_t kMask = kAlignment - 1;
state.actual_pc_counter_set_size_aligned = (size + kMask) & ~kMask;
}
// MainObjectLazyInit() and helpers allow us to initialize state.main_object
// lazily and thread-safely on the first call to __sanitizer_cov_trace_pc().
//
// TODO(kcc): consider removing :dl_path_suffix= since with lazy init
// we can auto-detect the instrumented DSO.
//
// TODO(kcc): this lazy init is brittle.
// It assumes that __sanitizer_cov_trace_pc is the only code that touches
// state.main_object concurrently. I.e. we can not blindly reuse this lazy init
// for other instrumentation callbacks that use state.main_object.
// This code is also considered *temporary* because
// a) __sanitizer_cov_trace_pc is obsolete and we hope to not need it in future.
// b) a better option might be to do a non-lazy init by intercepting dlopen.
//
// We do not call MainObjectLazyInit() in
// __sanitizer_cov_trace_pc_guard() because
// a) there is not use case for that currently and
// b) it will slowdown the hot function.
static pthread_once_t main_object_lazy_init_once = PTHREAD_ONCE_INIT;
static void MainObjectLazyInitOnceCallback() {
state.main_object =
centipede::GetDlInfo(state.GetStringFlag(":dl_path_suffix="));
fprintf(stderr, "MainObjectLazyInitOnceCallback %zx\n",
state.main_object.start_address);
LazyAllocatePcCounters(state.reverse_pc_table.NumPcs());
}
__attribute__((noinline)) static void MainObjectLazyInit() {
pthread_once(&main_object_lazy_init_once, MainObjectLazyInitOnceCallback);
}
// TODO(kcc): [impl] add proper testing for this callback.
// TODO(kcc): make sure the pc_table in the engine understands the raw PCs.
// TODO(kcc): this implementation is temporary. In order for symbolization to
// work we will need to translate the PC into a PCIndex or make pc_table sparse.
// See https://clang.llvm.org/docs/SanitizerCoverage.html#tracing-pcs.
// This instrumentation is redundant if other instrumentation
// (e.g. trace-pc-guard) is available, but GCC as of 2022-04 only supports
// this variant.
void __sanitizer_cov_trace_pc() {
uintptr_t pc = reinterpret_cast<uintptr_t>(__builtin_return_address(0));
if (!state.main_object.start_address ||
!state.actual_pc_counter_set_size_aligned) {
// Don't track coverage at all before the PC table is initialized.
if (state.reverse_pc_table.NumPcs() == 0) return;
MainObjectLazyInit();
}
pc -= state.main_object.start_address;
pc = ReturnAddressToCallerPc(pc);
const auto pc_guard = state.reverse_pc_table.GetPCGuard(pc);
// TODO(kcc): compute is_function_entry for this case.
if (pc_guard.IsValid()) HandleOnePc(pc_guard);
}
// This function is called at the DSO init time.
void __sanitizer_cov_trace_pc_guard_init(PCGuard *start, PCGuard *stop) {
if (state.pc_guard_start == nullptr) {
RunnerCheck(state.pcs_beg == nullptr,
"__sanitizer_cov_pcs_init was called before "
"__sanitizer_cov_trace_pc_guard_init");
RunnerCheck(stop - start <= PCGuard::kMaxNumPCs,
"__sanitizer_cov_trace_pc_guard_init: too many PCs");
state.pc_guard_start = start;
state.pc_guard_stop = stop;
LazyAllocatePcCounters(stop - start);
size_t idx = 0;
for (PCGuard *guard = start; guard != stop; ++guard) {
guard->pc_index = idx;
++idx;
}
} else {
RunnerCheck(
state.pc_guard_start == start && state.pc_guard_stop == stop,
"__sanitizer_cov_trace_pc_guard_init is called with different "
"arguments than previously. This may indicate more than one DSO "
"instrumented with sancov. This is currently not supported by the "
"Centipede runner. Please let the Centipede developers know if this is "
"an important use case.");
}
}
// This function is called on every instrumented edge.
NO_SANITIZE
void __sanitizer_cov_trace_pc_guard(PCGuard *guard) {
// Very early at process startup, the `*guard` may still be not initialized.
// But in this case it's just going to be zero.
// TODO(kcc): the check below seems almost reduntant. See if we can remove it.
if (state.pc_guard_start == nullptr) return;
HandleOnePc(*guard);
}
} // extern "C"