-
Notifications
You must be signed in to change notification settings - Fork 564
[runtime] Fix possible race in terminate handler #4194
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
#include <thread> | ||
#include <future> | ||
#include <chrono> | ||
#include <vector> | ||
#include <csignal> // signal.h | ||
|
||
#include "async.h" | ||
|
||
int test_ConcurrentTerminate() { | ||
signal(SIGABRT, *[](int){ exit(99); }); // Windows does not have sigaction | ||
|
||
std::vector<std::future<void>> futures; | ||
#ifdef __linux__ | ||
// TODO: invalid terminate handler called from bridge on non-main thread on Linux X64 | ||
throw std::runtime_error("Reporting error!"); | ||
#endif | ||
|
||
for (size_t i = 0; i < 100; ++i) { | ||
futures.emplace_back(std::async(std::launch::async, | ||
[](size_t param) { | ||
std::this_thread::sleep_for(std::chrono::milliseconds(param)); | ||
throw std::runtime_error("Reporting error!"); | ||
}, | ||
200 - i)); | ||
} | ||
|
||
for (auto &future : futures) future.get(); | ||
return 0; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
#ifdef __cplusplus | ||
extern "C" { | ||
#endif | ||
|
||
int test_ConcurrentTerminate(); | ||
|
||
#ifdef __cplusplus | ||
} | ||
#endif |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,5 @@ | ||||||
package async | ||||||
|
||||||
--- | ||||||
|
||||||
int test_ConcurrentTerminate(); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
(probably in |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
#include "testlib_api.h" | ||
|
||
#include <iostream> | ||
#include <thread> | ||
#include <future> | ||
#include <chrono> | ||
#include <vector> | ||
#include <csignal> // signal.h | ||
|
||
using namespace std; | ||
|
||
static | ||
int runConcurrent() { | ||
|
||
std::vector<std::future<void>> futures; | ||
|
||
for (size_t i = 0; i < 100; ++i) { | ||
futures.emplace_back(std::async(std::launch::async, | ||
[](auto delay) { | ||
std::this_thread::sleep_for(std::chrono::milliseconds(delay)); | ||
testlib_symbols()->kotlin.root.testTerminate(); | ||
}, | ||
100)); | ||
} | ||
|
||
for (auto &future : futures) future.get(); | ||
return 0; | ||
} | ||
|
||
int main() { | ||
signal(SIGABRT, *[](int){ exit(99); }); // Windows does not have sigaction | ||
|
||
set_terminate([](){ | ||
cout << "This is the original terminate handler\n" << flush; | ||
std::abort(); | ||
}); | ||
|
||
try { | ||
runConcurrent(); | ||
} catch(...) { | ||
std::cerr << "Unknown exception caught\n" << std::flush; | ||
} | ||
return 0; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import async.* | ||
import kotlinx.cinterop.* | ||
|
||
fun main() { | ||
test_ConcurrentTerminate() | ||
println("This is not expected.") | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import kotlin.system.exitProcess | ||
|
||
fun testTerminate() { | ||
throw RuntimeException("Example") | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,7 @@ | |
#include <stdint.h> | ||
|
||
#include <exception> | ||
#include <unistd.h> | ||
|
||
#if KONAN_NO_EXCEPTIONS | ||
#define OMIT_BACKTRACE 1 | ||
|
@@ -113,8 +114,6 @@ SourceInfo getSourceInfo(KConstRef stackTrace, int index) { | |
|
||
} // namespace | ||
|
||
extern "C" { | ||
|
||
// TODO: this implementation is just a hack, e.g. the result is inexact; | ||
// however it is better to have an inexact stacktrace than not to have any. | ||
NO_INLINE OBJ_GETTER0(Kotlin_getCurrentStackTrace) { | ||
|
@@ -137,7 +136,7 @@ NO_INLINE OBJ_GETTER0(Kotlin_getCurrentStackTrace) { | |
|
||
int size = backtrace(buffer, maxSize); | ||
if (size < kSkipFrames) | ||
return AllocArrayInstance(theNativePtrArrayTypeInfo, 0, OBJ_RESULT); | ||
return AllocArrayInstance(theNativePtrArrayTypeInfo, 0, OBJ_RESULT); | ||
|
||
ObjHolder resultHolder; | ||
ObjHeader* result = AllocArrayInstance(theNativePtrArrayTypeInfo, size - kSkipFrames, resultHolder.slot()); | ||
|
@@ -235,71 +234,104 @@ void OnUnhandledException(KRef throwable) { | |
} | ||
} | ||
|
||
#if KONAN_REPORT_BACKTRACE_TO_IOS_CRASH_LOG | ||
static bool terminating = false; | ||
static SimpleMutex terminatingMutex; | ||
#endif | ||
namespace { | ||
|
||
RUNTIME_NORETURN void TerminateWithUnhandledException(KRef throwable) { | ||
OnUnhandledException(throwable); | ||
class { | ||
/** | ||
* Timeout 5 sec for concurrent (second) terminate attempt to give a chance the first one to finish. | ||
* If the terminate handler hangs for 5 sec it is probably fatally broken, so let's do abnormal _Exit in that case. | ||
*/ | ||
unsigned int timeoutSec = 5; | ||
knebekaizer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
int terminatingFlag = 0; | ||
public: | ||
template <class Fun> RUNTIME_NORETURN void operator()(Fun block) { | ||
knebekaizer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (compareAndSet(&terminatingFlag, 0, 1)) { | ||
block(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does it invoke the block if |
||
// block() is supposed to be NORETURN, otherwise go to normal abort() | ||
konan::abort(); | ||
} else { | ||
sleep(timeoutSec); | ||
// We come here when another terminate handler hangs for 5 sec, that looks fatally broken. Go to forced exit now. | ||
} | ||
_Exit(EXIT_FAILURE); // force exit | ||
knebekaizer marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm afraid one of my questions got lost. What about logging to stderr facts (1) that there's a concurrent termination attempt and (2) that one of them got tired of waiting and is force quitting? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This may be dangerous, as output itself is a sort of conspicuous regarding races, etc. I consider hanging termination (which already happens here) as extremely emergence case, something is awfully wrong - it may be heap corruption or whatever that affects any printing. So I'd prefer to minimize any actions. |
||
} | ||
} concurrentTerminateWrapper; | ||
|
||
//! Process exception hook (if any) or just printStackTrace + write crash log | ||
void processUnhandledKotlinException(KRef throwable) { | ||
OnUnhandledException(throwable); | ||
#if KONAN_REPORT_BACKTRACE_TO_IOS_CRASH_LOG | ||
{ | ||
LockGuard<SimpleMutex> lock(terminatingMutex); | ||
if (!terminating) { | ||
ReportBacktraceToIosCrashLog(throwable); | ||
} | ||
} | ||
ReportBacktraceToIosCrashLog(throwable); | ||
#endif | ||
} | ||
|
||
} // namespace | ||
|
||
konan::abort(); | ||
RUNTIME_NORETURN void TerminateWithUnhandledException(KRef throwable) { | ||
concurrentTerminateWrapper([=]() { | ||
processUnhandledKotlinException(throwable); | ||
konan::abort(); | ||
}); | ||
} | ||
|
||
// Some libstdc++-based targets has limited support for std::current_exception and other C++11 functions. | ||
// This restriction can be lifted later when toolchains will be updated. | ||
#if KONAN_HAS_CXX11_EXCEPTION_FUNCTIONS | ||
|
||
static void (*oldTerminateHandler)() = nullptr; | ||
|
||
static void callOldTerminateHandler() { | ||
#if KONAN_REPORT_BACKTRACE_TO_IOS_CRASH_LOG | ||
{ | ||
LockGuard<SimpleMutex> lock(terminatingMutex); | ||
terminating = true; | ||
namespace { | ||
class TerminateHandler { | ||
|
||
// In fact, it's safe to call my_handler directly from outside: it will do the job and then invoke original handler, | ||
// even if it has not been initialized yet. So one may want to make it public and/or not the class member | ||
RUNTIME_NORETURN static void kotlinHandler() { | ||
concurrentTerminateWrapper([]() { | ||
if (auto currentException = std::current_exception()) { | ||
try { | ||
std::rethrow_exception(currentException); | ||
} catch (ExceptionObjHolder& e) { | ||
processUnhandledKotlinException(e.obj()); | ||
konan::abort(); | ||
} catch (...) { | ||
// Not a Kotlin exception - call default handler | ||
instance().queuedHandler_(); | ||
} | ||
} | ||
// Come here in case of direct terminate() call or unknown exception - go to default terminate handler. | ||
instance().queuedHandler_(); | ||
}); | ||
} | ||
#endif | ||
|
||
RuntimeCheck(oldTerminateHandler != nullptr, "Underlying exception handler is not set."); | ||
oldTerminateHandler(); | ||
} | ||
using QH = __attribute__((noreturn)) void(*)(); | ||
QH queuedHandler_; | ||
knebekaizer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
static void KonanTerminateHandler() { | ||
auto currentException = std::current_exception(); | ||
if (!currentException) { | ||
// No current exception. | ||
callOldTerminateHandler(); | ||
} else { | ||
try { | ||
std::rethrow_exception(currentException); | ||
} catch (ExceptionObjHolder& e) { | ||
TerminateWithUnhandledException(e.obj()); | ||
} catch (...) { | ||
// Not a Kotlin exception. | ||
callOldTerminateHandler(); | ||
} | ||
/// Use machinery like Meyers singleton to provide thread safety | ||
TerminateHandler() | ||
: queuedHandler_((QH)std::set_terminate(kotlinHandler)) {} | ||
|
||
static TerminateHandler& instance() { | ||
static TerminateHandler singleton [[clang::no_destroy]]; | ||
return singleton; | ||
} | ||
} | ||
|
||
static SimpleMutex konanTerminateHandlerInitializationMutex; | ||
// Copy, move and assign would be safe, but not much useful, so let's delete all (rule of 5) | ||
TerminateHandler(const TerminateHandler&) = delete; | ||
TerminateHandler(TerminateHandler&&) = delete; | ||
TerminateHandler& operator=(const TerminateHandler&) = delete; | ||
TerminateHandler& operator=(TerminateHandler&&) = delete; | ||
// Dtor might be in use to restore original handler. However, consequent install | ||
// will not reconstruct handler anyway, so let's keep dtor deleted to avoid confusion. | ||
~TerminateHandler() = delete; | ||
public: | ||
/// First call will do the job, all consequent will do nothing. | ||
static void install() { | ||
instance(); // Use side effect of warming up | ||
} | ||
}; | ||
} // anon namespace | ||
|
||
// Use one public function to limit access to the class declaration | ||
void SetKonanTerminateHandler() { | ||
if (oldTerminateHandler != nullptr) return; // Already initialized. | ||
|
||
LockGuard<SimpleMutex> lockGuard(konanTerminateHandlerInitializationMutex); | ||
|
||
if (oldTerminateHandler != nullptr) return; // Already initialized. | ||
|
||
oldTerminateHandler = std::set_terminate(&KonanTerminateHandler); | ||
TerminateHandler::install(); | ||
} | ||
|
||
#else // KONAN_OBJC_INTEROP | ||
|
@@ -310,8 +342,6 @@ void SetKonanTerminateHandler() { | |
|
||
#endif // KONAN_OBJC_INTEROP | ||
|
||
} // extern "C" | ||
|
||
void DisallowSourceInfo() { | ||
disallowSourceInfo = true; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just for the record (i.e. not doubting your approach), I used the following pattern to induce races:
On my machine this pattern turned out to be quite reliable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Indeed
future
seems to be a bit "indirect" way, in comparison withstartAtThread
.My snippet is derived from more complicate test involving exception propagation with
promise
andset_exception
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think
std::async
with futures would work just fine either way. My point was in using spinlocks for synchronisation as opposed to sleeping.