diff --git a/compiler-rt/lib/asan/asan_fuchsia.cpp b/compiler-rt/lib/asan/asan_fuchsia.cpp --- a/compiler-rt/lib/asan/asan_fuchsia.cpp +++ b/compiler-rt/lib/asan/asan_fuchsia.cpp @@ -62,6 +62,11 @@ UNIMPLEMENTED(); } +void UnpoisonAllKnownStacks() { + UnpoisonStack(GetCurrentStackBoundsFromCacheOrOs(), "default"); + UnpoisonCurrentFakeStack(); +} + // We can use a plain thread_local variable for TSD. static thread_local void *per_thread; diff --git a/compiler-rt/lib/asan/asan_internal.h b/compiler-rt/lib/asan/asan_internal.h --- a/compiler-rt/lib/asan/asan_internal.h +++ b/compiler-rt/lib/asan/asan_internal.h @@ -83,6 +83,27 @@ void AsanCheckDynamicRTPrereqs(); void AsanCheckIncompatibleRT(); +// asan_posix.cpp / asan_rtems.cpp / asan_fuchsia.cpp / asan_win.cpp +// Unpoisons all known stacks on the current platform: default, fake, sigalt +void UnpoisonAllKnownStacks(); + +struct StackBounds { + uptr bottom; + uptr top; + bool have_bounds; +}; +// asan_rtl.cpp +// Attempts to get the stack bounds of the current thread first from cache, then +// from OS. +StackBounds GetCurrentStackBoundsFromCacheOrOs(); +// Unpoisons the stack defined by bounds. +// - If bounds.have_bounds is false, does nothing. +// - If the boundaries don't look correct for a stack, then prints a warning +// containing stack_type and does not unpoison. +void UnpoisonStack(StackBounds bounds, char const *stack_type); +// Unpoisons the fake stack if the current stack is known and has a fake stack. +void UnpoisonCurrentFakeStack(); + // asan_thread.cpp AsanThread *CreateMainThread(); diff --git a/compiler-rt/lib/asan/asan_posix.cpp b/compiler-rt/lib/asan/asan_posix.cpp --- a/compiler-rt/lib/asan/asan_posix.cpp +++ b/compiler-rt/lib/asan/asan_posix.cpp @@ -24,6 +24,7 @@ #include "sanitizer_common/sanitizer_procmaps.h" #include +#include #include #include #include @@ -37,6 +38,42 @@ ReportDeadlySignal(sig); } +void UnpoisonAllKnownStacks() { + bool on_sig_alt_stack; + { + StackBounds alt_bounds; + stack_t signal_stack; + CHECK(0 == sigaltstack(nullptr, &signal_stack)); + alt_bounds.bottom = (uptr)signal_stack.ss_sp; + alt_bounds.top = (uptr)((char *)signal_stack.ss_sp + signal_stack.ss_size); + // If we're executing on the signal alternate stack AND the Linux flag + // SS_AUTODISARM was used, then we cannot get the signal alternate stack + // bounds from sigaltstack -- sigaltstack's output looks just as if no + // alternate stack has ever been set up. + alt_bounds.have_bounds = (signal_stack.ss_flags != SS_DISABLE); + on_sig_alt_stack = (signal_stack.ss_flags == SS_ONSTACK); + if (alt_bounds.have_bounds) + UnpoisonStack(alt_bounds, "sigalt"); + } + + if (on_sig_alt_stack) { + // Since we're on the signal altnerate stack, we cannot find the DEFAULT + // stack bottom using a local variable. + StackBounds default_bounds; + uptr tls_addr, tls_size, stack_size; + GetThreadStackAndTls(/*main=*/false, &default_bounds.bottom, &stack_size, + &tls_addr, &tls_size); + default_bounds.top = default_bounds.bottom + stack_size; + default_bounds.have_bounds = true; + + UnpoisonStack(default_bounds, "default"); + } else { + UnpoisonStack(GetCurrentStackBoundsFromCacheOrOs(), "default"); + } + + UnpoisonCurrentFakeStack(); +} + // ---------------------- TSD ---------------- {{{1 #if SANITIZER_NETBSD && !ASAN_DYNAMIC diff --git a/compiler-rt/lib/asan/asan_rtems.cpp b/compiler-rt/lib/asan/asan_rtems.cpp --- a/compiler-rt/lib/asan/asan_rtems.cpp +++ b/compiler-rt/lib/asan/asan_rtems.cpp @@ -64,6 +64,11 @@ UNIMPLEMENTED(); } +void UnpoisonAllKnownStacks { + UnpoisonStack(GetCurrentStackBoundsFromCacheOrOs(), "default"); + UnpoisonCurrentFakeStack(); +} + void EarlyInit() { // Provide early initialization of shadow memory so that // instrumented code running before full initialzation will not diff --git a/compiler-rt/lib/asan/asan_rtl.cpp b/compiler-rt/lib/asan/asan_rtl.cpp --- a/compiler-rt/lib/asan/asan_rtl.cpp +++ b/compiler-rt/lib/asan/asan_rtl.cpp @@ -551,52 +551,69 @@ static AsanInitializer asan_initializer; #endif // ASAN_DYNAMIC -} // namespace __asan - -// ---------------------- Interface ---------------- {{{1 -using namespace __asan; - -void NOINLINE __asan_handle_no_return() { - if (asan_init_is_running) - return; - - int local_stack; - AsanThread *curr_thread = GetCurrentThread(); - uptr PageSize = GetPageSizeCached(); - uptr top, bottom; - if (curr_thread) { - top = curr_thread->stack_top(); - bottom = ((uptr)&local_stack - PageSize) & ~(PageSize - 1); +StackBounds GetCurrentStackBoundsFromCacheOrOs() { + StackBounds bounds; + if (AsanThread *curr_thread = GetCurrentThread()) { + int local_stack; + const uptr page_size = GetPageSizeCached(); + bounds.top = curr_thread->stack_top(); + bounds.bottom = ((uptr)&local_stack - page_size) & ~(page_size - 1); + bounds.have_bounds = true; } else if (SANITIZER_RTEMS) { // Give up On RTEMS. - return; + internal_memset(&bounds, 0, sizeof(bounds)); } else { CHECK(!SANITIZER_FUCHSIA); // If we haven't seen this thread, try asking the OS for stack bounds. - uptr tls_addr, tls_size, stack_size; - GetThreadStackAndTls(/*main=*/false, &bottom, &stack_size, &tls_addr, - &tls_size); - top = bottom + stack_size; + uptr TlsAddr, TlsSize, StackSize; + GetThreadStackAndTls(/*main=*/false, &bounds.bottom, &StackSize, &TlsAddr, + &TlsSize); + bounds.top = bounds.bottom + StackSize; + bounds.have_bounds = true; } + return bounds; +} + +void UnpoisonStack(StackBounds bounds, const char *stack_type) { + if (!bounds.have_bounds) + return; + static const uptr kMaxExpectedCleanupSize = 64 << 20; // 64M - if (top - bottom > kMaxExpectedCleanupSize) { + if (bounds.top - bounds.bottom > kMaxExpectedCleanupSize) { static bool reported_warning = false; if (reported_warning) return; reported_warning = true; Report("WARNING: ASan is ignoring requested __asan_handle_no_return: " - "stack top: %p; bottom %p; size: %p (%zd)\n" + "stack type: %s; stack top: %p; bottom %p; size: %p (%zd)\n" "False positive error reports may follow\n" "For details see " "https://github.com/google/sanitizers/issues/189\n", - top, bottom, top - bottom, top - bottom); + stack_type, bounds.top, bounds.bottom, bounds.top - bounds.bottom, + bounds.top - bounds.bottom); return; } - PoisonShadow(bottom, top - bottom, 0); + PoisonShadow(bounds.bottom, bounds.top - bounds.bottom, 0); +} + +void UnpoisonCurrentFakeStack() { + AsanThread *curr_thread = GetCurrentThread(); if (curr_thread && curr_thread->has_fake_stack()) curr_thread->fake_stack()->HandleNoReturn(); } +} // namespace __asan + +// ---------------------- Interface ---------------- {{{1 +using namespace __asan; + +void NOINLINE __asan_handle_no_return() { + if (asan_init_is_running) + return; + + UnpoisonAllKnownStacks(); +} + extern "C" void *__asan_extra_spill_area() { AsanThread *t = GetCurrentThread(); CHECK(t); diff --git a/compiler-rt/lib/asan/asan_win.cpp b/compiler-rt/lib/asan/asan_win.cpp --- a/compiler-rt/lib/asan/asan_win.cpp +++ b/compiler-rt/lib/asan/asan_win.cpp @@ -268,6 +268,11 @@ void AsanOnDeadlySignal(int, void *siginfo, void *context) { UNIMPLEMENTED(); } +void UnpoisonAllKnownStacks() { + UnpoisonStack(GetCurrentStackBoundsFromCacheOrOs(), "default"); + UnpoisonCurrentFakeStack(); +} + #if SANITIZER_WINDOWS64 // Exception handler for dealing with shadow memory. static LONG CALLBACK diff --git a/compiler-rt/test/asan/TestCases/unpoison-alternate-stack.cpp b/compiler-rt/test/asan/TestCases/unpoison-alternate-stack.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/test/asan/TestCases/unpoison-alternate-stack.cpp @@ -0,0 +1,203 @@ +// Tests that __asan_handle_no_return properly unpoisons the signal alternate +// stack. + +// Don't optimize, otherwise the variables which create redzones might be +// dropped. +// RUN: %clangxx_asan -std=c++20 -fexceptions -O0 %s -o %t -pthread +// RUN: %run %t + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#define HANDLE_ERROR_FROM_RETVAL(EXPRESSION) \ + handleErrorFromRetval((EXPRESSION), #EXPRESSION, __FUNCTION__, __FILE__, \ + __LINE__) + +// If EXPRESSION == 0, no error happened. +#define HANDLE_ERROR_FROM_ERRNO(EXPRESSION) \ + handleErrorFromErrno((EXPRESSION) != 0, #EXPRESSION, __FUNCTION__, __FILE__, \ + __LINE__) + +#define ERROR(STR, ERRNO) \ + error(ERRNO, STR, __FUNCTION__, __FILE__, __LINE__) + +namespace { + +void error(int Err, char const *Expression, + char const *Function, char const *File, int Line) { + std::fprintf(stderr, + "Error %d: %s\n" + "In expression %s\n" + "In function %s\n" + "In file %s:%d\n" + "Aborting.\n\n", + Err, std::strerror(Err), Expression, Function, File, Line); + std::abort(); +} + +void handleErrorFromRetval(int Err, char const *Expression, + char const *Function, char const *File, int Line) { + if (Err) + error(Err, Expression, Function, File, Line); +} + +void handleErrorFromErrno(bool Failed, char const *Expression, + char const *Function, char const *File, int Line) { + if (Failed) + error(errno, Expression, Function, File, Line); +} + +struct TestContext { + char *LeftRedzone; + char *RightRedzone; + std::jmp_buf JmpBuf; +}; + +TestContext defaultStack; +TestContext signalStack; + +// Create a new stack frame to ensure that logically, the stack frame should be +// unpoisoned when the function exits. Exit is performed via jump, not return, +// such that we trigger __asan_handle_no_return and not ordinary unpoisoning. +template +void __attribute__((noinline)) poisonStackAndJump(TestContext &c, Jump jump) { + char Blob[100]; // This variable must not be optimized out, because we use it + // to create redzones. + + c.LeftRedzone = Blob - 1; + c.RightRedzone = Blob + sizeof(Blob); + + assert(__asan_address_is_poisoned(c.LeftRedzone)); + assert(__asan_address_is_poisoned(c.RightRedzone)); + + // Jump to avoid normal cleanup of redzone markers. Instead, + // __asan_handle_no_return is called which unpoisons the stacks. + jump(); +} + +void testOnCurrentStack() { + TestContext c; + + if (0 == setjmp(c.JmpBuf)) + poisonStackAndJump(c, [&] { longjmp(c.JmpBuf, 1); }); + + assert(0 == __asan_region_is_poisoned(c.LeftRedzone, + c.RightRedzone - c.LeftRedzone)); +} + +void signalHandler(int, siginfo_t *, void *) { + { + stack_t Stack; + HANDLE_ERROR_FROM_ERRNO(sigaltstack(nullptr, &Stack)); + assert(Stack.ss_flags == SS_ONSTACK); + } + + // test on signal alternate stack + testOnCurrentStack(); + + // test unpoisoning when jumping between stacks + poisonStackAndJump(signalStack, []{ longjmp(defaultStack.JmpBuf, 1); }); +} + +void setSignalAlternateStack(void *AltStack) { + HANDLE_ERROR_FROM_ERRNO(sigaltstack((stack_t const *)AltStack, nullptr)); + + struct sigaction Action = { + .sa_sigaction = signalHandler, + .sa_flags = SA_SIGINFO | SA_NODEFER | SA_ONSTACK, + }; + HANDLE_ERROR_FROM_ERRNO(sigemptyset(&Action.sa_mask)); + + HANDLE_ERROR_FROM_ERRNO(sigaction(SIGUSR1, &Action, nullptr)); +} + +// Main test function. +// Must be run on another thread to be able to control memory placement between +// default stack and alternate signal stack. +// If the alternate signal stack is placed in close proximity before the +// default stack, __asan_handle_no_return might unpoison both, even without +// being aware of the signal alternate stack. +// We want to test reliably that __asan_handle_no_return can properly unpoison +// the signal alternate stack. +void *threadFun(void *AltStack) { + // first test on default stack (sanity check), no signal alternate stack set + testOnCurrentStack(); + + setSignalAlternateStack(AltStack); + + // test on default stack again, but now the signal alternate stack is set + testOnCurrentStack(); + + // set up jump to test unpoisoning when jumping between stacks + if (0 == setjmp(defaultStack.JmpBuf)) + // Test on signal alternate stack, via signalHandler + poisonStackAndJump(defaultStack, [] { raise(SIGUSR1); }); + + assert(0 == __asan_region_is_poisoned( + defaultStack.LeftRedzone, + defaultStack.RightRedzone - defaultStack.LeftRedzone)); + + assert(0 == __asan_region_is_poisoned( + signalStack.LeftRedzone, + signalStack.RightRedzone - signalStack.LeftRedzone)); + + return nullptr; +} + +} // namespace + +// Check that __asan_handle_no_return properly unpoisons a signal alternate +// stack. +// __asan_handle_no_return tries to determine the stack boundaries and +// unpoisons all memory inside those. If this is not done properly, redzones for +// variables on can remain in shadow memory which might lead to false positive +// reports when the stack is reused. +int main() { + size_t const PageSize = sysconf(_SC_PAGESIZE); + // To align the alternate stack, we round this up to page_size. + size_t const DefaultStackSize = + (PTHREAD_STACK_MIN - 1 + PageSize) & ~(PageSize - 1); + // The alternate stack needs a certain size, or the signal handler segfaults. + size_t const AltStackSize = 10 * PageSize; + size_t const MappingSize = DefaultStackSize + AltStackSize; + // Using mmap guarantees proper alignment. + void *const Mapping = mmap(nullptr, MappingSize, + PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, + -1, 0); + if ((void *)-1 == Mapping) + ERROR("mmap", errno); + + stack_t const AltStack = { + .ss_sp = (char *)Mapping + DefaultStackSize, + .ss_flags = 0, + .ss_size = AltStackSize, + }; + + pthread_t Thread; + pthread_attr_t ThreadAttr; + HANDLE_ERROR_FROM_RETVAL(pthread_attr_init(&ThreadAttr)); + HANDLE_ERROR_FROM_RETVAL(pthread_attr_setstack(&ThreadAttr, Mapping, + DefaultStackSize)); + HANDLE_ERROR_FROM_RETVAL(pthread_create(&Thread, &ThreadAttr, &threadFun, + (void *)&AltStack)); + + if (int const Err = pthread_join(Thread, nullptr)) + if (EINVAL != Err) // EINVAL means the other thread exited first + ERROR("pthread_join", Err); + + HANDLE_ERROR_FROM_RETVAL(munmap(Mapping, MappingSize)); +}