From 99bd26c2ef1fedf8af5cf90eabef637f3f2937d1 Mon Sep 17 00:00:00 2001 From: Ilya Matveev Date: Mon, 19 Jul 2021 20:14:14 +0700 Subject: [PATCH] [K/N][Runtime] Switch thread states in termination handlers --- .../backend.native/tests/build.gradle | 32 +- .../interop/threadStates/threadStates.cpp | 13 + .../interop/threadStates/threadStates.def | 3 +- .../threadStates/unhandledException.kt | 22 + .../unhandledExceptionInForeignThread.kt | 22 + .../unhandledException/main.cpp | 19 + .../unhandledException/unhandled.kt | 20 + .../tests/runtime/exceptions/custom_hook.kt | 9 +- .../runtime/src/legacymm/cpp/Memory.cpp | 2 +- .../runtime/src/main/cpp/Exceptions.cpp | 28 +- .../runtime/src/main/cpp/ExceptionsTest.cpp | 458 +++++++++++++----- kotlin-native/runtime/src/main/cpp/Memory.h | 5 +- .../runtime/src/main/cpp/MemoryTest.cpp | 24 + kotlin-native/runtime/src/mm/cpp/Memory.cpp | 2 +- .../runtime/src/mm/cpp/ThreadRegistry.cpp | 3 +- 15 files changed, 536 insertions(+), 126 deletions(-) create mode 100644 kotlin-native/backend.native/tests/interop/threadStates/unhandledException.kt create mode 100644 kotlin-native/backend.native/tests/interop/threadStates/unhandledExceptionInForeignThread.kt create mode 100644 kotlin-native/backend.native/tests/produce_dynamic/unhandledException/main.cpp create mode 100644 kotlin-native/backend.native/tests/produce_dynamic/unhandledException/unhandled.kt create mode 100644 kotlin-native/runtime/src/main/cpp/MemoryTest.cpp diff --git a/kotlin-native/backend.native/tests/build.gradle b/kotlin-native/backend.native/tests/build.gradle index c1d08a84e90..129e66fc8f9 100644 --- a/kotlin-native/backend.native/tests/build.gradle +++ b/kotlin-native/backend.native/tests/build.gradle @@ -2669,7 +2669,7 @@ standaloneTest("kt-37572") { standaloneTest("custom_hook") { enabled = (project.testTarget != 'wasm32') // Uses exceptions. outputChecker = { - it.contains("value 42: Error") && it.contains("Uncaught Kotlin exception: kotlin.Error: an error") + it.contains("value 42: Error. Runnable state: true") && it.contains("Uncaught Kotlin exception: kotlin.Error: an error") } expectedExitStatusChecker = { it != 0 } source = "runtime/exceptions/custom_hook.kt" @@ -4315,7 +4315,7 @@ Task interopTestMultifile(String name, Closure configureClosur } standaloneTest("interop_objc_allocException") { - disabled = !isAppleTarget(project) || isExperimentalMM // Experimental MM doesn't support thread state switching for terminate handlers yet. + disabled = !isAppleTarget(project) expectedExitStatus = 0 source = "interop/objc/allocException.kt" UtilsKt.dependsOnPlatformLibs(it) @@ -4523,6 +4523,24 @@ interopTest("interop_threadStates_callbacksWithExceptions") { interop = "threadStates" } +interopTest("interop_threadStates_unhandledException") { + disabled = (project.testTarget == 'wasm32') || // No interop for wasm yet. + !isExperimentalMM // No thread state switching in the legacy MM. + source = "interop/threadStates/unhandledException.kt" + interop = "threadStates" + outputChecker = { it.contains("Error. Runnable state: true") } + expectedExitStatusChecker = { it != 0 } +} + +interopTest("interop_threadStates_unhandledExceptionInForeignThread") { + disabled = (project.testTarget == 'wasm32') || // No interop for wasm yet. + !isExperimentalMM // No thread state switching in the legacy MM. + source = "interop/threadStates/unhandledExceptionInForeignThread.kt" + interop = "threadStates" + outputChecker = { it.contains("Error. Runnable state: true") } + expectedExitStatusChecker = { it != 0 } +} + interopTest("interop_withSpaces") { disabled = (project.testTarget == 'wasm32') // No interop for wasm yet. interop ='withSpaces' @@ -4805,7 +4823,6 @@ if (PlatformInfo.isAppleTarget(project)) { } interopTest("interop_objc_foreignExceptionMode_default") { - enabled = !isExperimentalMM // Experimental MM doesn't support thread state switching for terminate handlers yet. source = "interop/objc/foreignException/objcExceptionMode.kt" interop = 'foreignExceptionMode_default' goldValue = "OK: Ends with uncaught exception handler\n" @@ -4976,6 +4993,15 @@ for (i in 0..2) { } } +dynamicTest("produce_dynamic_unhandledException") { + disabled = (project.testTarget == 'wasm32') || // Uses exceptions + dynamic is unsupported for wasm. + (cacheTesting != null) // Disabled due to KT-47828. + source = "produce_dynamic/unhandledException/unhandled.kt" + cSource = "$projectDir/produce_dynamic/unhandledException/main.cpp" + clangTool = "clang++" + expectedExitStatusChecker = { it != 0 } + outputChecker = { str -> str.startsWith("Kotlin hook: Exception. Runnable state: true") } +} dynamicTest("interop_concurrentRuntime") { disabled = (project.target.name != project.hostName) diff --git a/kotlin-native/backend.native/tests/interop/threadStates/threadStates.cpp b/kotlin-native/backend.native/tests/interop/threadStates/threadStates.cpp index 32a82e7c2b1..28cb0d6b7a1 100644 --- a/kotlin-native/backend.native/tests/interop/threadStates/threadStates.cpp +++ b/kotlin-native/backend.native/tests/interop/threadStates/threadStates.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -17,4 +18,16 @@ extern "C" void runInNewThread(void(*callback)(void)) { callback(); }); t.join(); +} + +extern "C" void runInForeignThread(void(*callback)(void)) { + std::thread t([callback]() { + // This thread is not attached to the Kotlin runtime. + auto future = std::async(std::launch::async, callback); + + // The machinery of the direct interop doesn't filter out a Kotlin exception thrown by the callback. + // The get() call will re-throw this exception. + future.get(); + }); + t.join(); } \ No newline at end of file diff --git a/kotlin-native/backend.native/tests/interop/threadStates/threadStates.def b/kotlin-native/backend.native/tests/interop/threadStates/threadStates.def index 8cb14cdc4bb..b98c6fded2f 100644 --- a/kotlin-native/backend.native/tests/interop/threadStates/threadStates.def +++ b/kotlin-native/backend.native/tests/interop/threadStates/threadStates.def @@ -17,4 +17,5 @@ int32_t answer() { return 42; } -void runInNewThread(void(*callback)(void)); \ No newline at end of file +void runInNewThread(void(*callback)(void)); +void runInForeignThread(void(*callback)(void)); \ No newline at end of file diff --git a/kotlin-native/backend.native/tests/interop/threadStates/unhandledException.kt b/kotlin-native/backend.native/tests/interop/threadStates/unhandledException.kt new file mode 100644 index 00000000000..9b5cba22bd0 --- /dev/null +++ b/kotlin-native/backend.native/tests/interop/threadStates/unhandledException.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +import kotlin.native.concurrent.* +import kotlin.native.internal.Debugging +import kotlinx.cinterop.staticCFunction +import threadStates.* + +fun main() { + val hook = { throwable: Throwable -> + print("${throwable::class.simpleName}. Runnable state: ${Debugging.isThreadStateRunnable}") + } + if (Platform.memoryModel != MemoryModel.EXPERIMENTAL) { + hook.freeze() + } + + setUnhandledExceptionHook(hook) + + runInNewThread(staticCFunction { throw Error("Error") }) +} \ No newline at end of file diff --git a/kotlin-native/backend.native/tests/interop/threadStates/unhandledExceptionInForeignThread.kt b/kotlin-native/backend.native/tests/interop/threadStates/unhandledExceptionInForeignThread.kt new file mode 100644 index 00000000000..ab60975863b --- /dev/null +++ b/kotlin-native/backend.native/tests/interop/threadStates/unhandledExceptionInForeignThread.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +import kotlin.native.concurrent.* +import kotlin.native.internal.Debugging +import kotlinx.cinterop.staticCFunction +import threadStates.* + +fun main() { + val hook = { throwable: Throwable -> + print("${throwable::class.simpleName}. Runnable state: ${Debugging.isThreadStateRunnable}") + } + if (Platform.memoryModel != MemoryModel.EXPERIMENTAL) { + hook.freeze() + } + + setUnhandledExceptionHook(hook) + + runInForeignThread(staticCFunction { throw Error("Error") }) +} \ No newline at end of file diff --git a/kotlin-native/backend.native/tests/produce_dynamic/unhandledException/main.cpp b/kotlin-native/backend.native/tests/produce_dynamic/unhandledException/main.cpp new file mode 100644 index 00000000000..bcae3511522 --- /dev/null +++ b/kotlin-native/backend.native/tests/produce_dynamic/unhandledException/main.cpp @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +#include "testlib_api.h" + +#include +#include +#include + +int main(int argc, char** argv) { + // The reverse interop machinery will catch the exception on the interop border and terminate the program. + try { + testlib_symbols()->kotlin.root.setHookAndThrow(); + } catch (...) { + std::cout << "Should not happen" << std::endl; + } +} \ No newline at end of file diff --git a/kotlin-native/backend.native/tests/produce_dynamic/unhandledException/unhandled.kt b/kotlin-native/backend.native/tests/produce_dynamic/unhandledException/unhandled.kt new file mode 100644 index 00000000000..d4976c9941b --- /dev/null +++ b/kotlin-native/backend.native/tests/produce_dynamic/unhandledException/unhandled.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +import kotlin.native.concurrent.* +import kotlin.native.internal.* + +fun setHookAndThrow() { + val hook = { throwable: Throwable -> + print("Kotlin hook: ${throwable::class.simpleName}. Runnable state: ${Debugging.isThreadStateRunnable}") + } + if (Platform.memoryModel != MemoryModel.EXPERIMENTAL) { + hook.freeze() + } + + setUnhandledExceptionHook(hook) + + throw Exception("Error") +} diff --git a/kotlin-native/backend.native/tests/runtime/exceptions/custom_hook.kt b/kotlin-native/backend.native/tests/runtime/exceptions/custom_hook.kt index 11614b4a93f..7cb9a179687 100644 --- a/kotlin-native/backend.native/tests/runtime/exceptions/custom_hook.kt +++ b/kotlin-native/backend.native/tests/runtime/exceptions/custom_hook.kt @@ -5,6 +5,7 @@ import kotlin.test.* import kotlin.native.concurrent.* +import kotlin.native.internal.* fun mainLegacyMM() { assertFailsWith { @@ -12,8 +13,8 @@ fun mainLegacyMM() { } val x = 42 - val old = setUnhandledExceptionHook({ - throwable: Throwable -> println("value $x: ${throwable::class.simpleName}") + val old = setUnhandledExceptionHook({ throwable: Throwable -> + println("value $x: ${throwable::class.simpleName}. Runnable state: ${Debugging.isThreadStateRunnable}") }.freeze()) assertNull(old) @@ -26,8 +27,8 @@ fun mainExperimentalMM() { assertNull(unset) val x = 42 - val old = setUnhandledExceptionHook { - throwable: Throwable -> println("value $x: ${throwable::class.simpleName}") + val old = setUnhandledExceptionHook { throwable: Throwable -> + println("value $x: ${throwable::class.simpleName}. Runnable state: ${Debugging.isThreadStateRunnable}") } assertNotNull(old) diff --git a/kotlin-native/runtime/src/legacymm/cpp/Memory.cpp b/kotlin-native/runtime/src/legacymm/cpp/Memory.cpp index 4aeafb410ca..52606dd8a11 100644 --- a/kotlin-native/runtime/src/legacymm/cpp/Memory.cpp +++ b/kotlin-native/runtime/src/legacymm/cpp/Memory.cpp @@ -3738,7 +3738,7 @@ ALWAYS_INLINE void kotlin::AssertThreadState(MemoryState* thread, std::initializ // no-op, used by the new MM only. } -MemoryState* kotlin::mm::GetMemoryState() { +MemoryState* kotlin::mm::GetMemoryState() noexcept { return ::memoryState; } diff --git a/kotlin-native/runtime/src/main/cpp/Exceptions.cpp b/kotlin-native/runtime/src/main/cpp/Exceptions.cpp index bc48f4981ba..de3e4f199a4 100644 --- a/kotlin-native/runtime/src/main/cpp/Exceptions.cpp +++ b/kotlin-native/runtime/src/main/cpp/Exceptions.cpp @@ -47,6 +47,18 @@ void ThrowException(KRef exception) { namespace { +// This function accesses a TLS variable under the hood, so it must not be called from a thread destructor (see kotlin::mm::GetMemoryState). +[[nodiscard]] kotlin::ThreadStateGuard setNativeStateForRegisteredThread(bool reentrant = true) { + auto* memoryState = kotlin::mm::GetMemoryState(); + if (memoryState) { + return kotlin::ThreadStateGuard(kotlin::ThreadState::kNative, reentrant); + } else { + // The current thread is not registered in the Kotlin runtime, + // just return an empty guard which doesn't actually switch the state. + return kotlin::ThreadStateGuard(); + } +} + class { /** * Timeout 5 sec for concurrent (second) terminate attempt to give a chance the first one to finish. @@ -61,6 +73,7 @@ class { // block() is supposed to be NORETURN, otherwise go to normal abort() konan::abort(); } else { + auto guard = setNativeStateForRegisteredThread(); sleep(timeoutSec); // We come here when another terminate handler hangs for 5 sec, that looks fatally broken. Go to forced exit now. } @@ -121,18 +134,25 @@ class TerminateHandler : private kotlin::Pinned { try { std::rethrow_exception(currentException); } catch (ExceptionObjHolder& e) { - // Use the reentrant switch because both states are possible here: - // - runnable, if the exception occured in a pure Kotlin thread (except initialization of globals). - // - native, if the throwing code was called from ObjC/Swift or if the exception occured during initialization of globals. - kotlin::ThreadStateGuard guard(kotlin::ThreadState::kRunnable, /* reentrant = */ true); + // Both thread states are allowed here because there is no guarantee that + // C++ runtime will unwind the stack for an unhandled exception. Thus there + // is no guarantee that state switches made on interop borders will be rolled back. + + // Moreover, a native code can catch an exception thrown by a Kotlin callback, + // store it to a global and then re-throw it in another thread which is not attached + // to the Kotlin runtime. To handle this case, use the CalledFromNativeGuard. + // TODO: Forbid throwing Kotlin exceptions through the interop border to get rid of this case. + kotlin::CalledFromNativeGuard guard(/* reentrant = */ true); processUnhandledException(e.GetExceptionObject()); terminateWithUnhandledException(e.GetExceptionObject()); } catch (...) { // Not a Kotlin exception - call default handler + auto guard = setNativeStateForRegisteredThread(); queuedHandler(); } } // Come here in case of direct terminate() call or unknown exception - go to default terminate handler. + auto guard = setNativeStateForRegisteredThread(); queuedHandler(); } diff --git a/kotlin-native/runtime/src/main/cpp/ExceptionsTest.cpp b/kotlin-native/runtime/src/main/cpp/ExceptionsTest.cpp index 6442b32ea25..4845483c588 100644 --- a/kotlin-native/runtime/src/main/cpp/ExceptionsTest.cpp +++ b/kotlin-native/runtime/src/main/cpp/ExceptionsTest.cpp @@ -2,16 +2,23 @@ * Copyright 2010-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license * that can be found in the LICENSE file. */ - #include "Exceptions.h" +#include +#include +#include + #include "gtest/gtest.h" #include "gmock/gmock.h" +#include "Memory.h" #include "ObjectTestSupport.hpp" #include "TestSupportCompilerGenerated.hpp" #include "TestSupport.hpp" +using namespace kotlin; +using namespace testing; + using namespace kotlin; using ::testing::_; @@ -131,125 +138,125 @@ TEST(ExceptionDeathTest, TerminateWithUnhandledException) { TEST(ExceptionDeathTest, TerminateHandler_WithHook) { test_support::TypeInfoHolder typeHolder{test_support::TypeInfoHolder::ObjectBuilder().setSuperType(theThrowableTypeInfo)}; - kotlin::RunInNewThread([&typeHolder]() { - Object exception(typeHolder.typeInfo()); - exception.header()->typeInfoOrMeta_ = setPointerBits(exception.header()->typeInfoOrMeta_, OBJECT_TAG_PERMANENT_CONTAINER); - exception->value = 42; - auto reportUnhandledExceptionMock = ScopedReportUnhandledExceptionMock(); - auto Kotlin_runUnhandledExceptionHookMock = ScopedKotlin_runUnhandledExceptionHookMock(); - ON_CALL(*reportUnhandledExceptionMock, Call(_)).WillByDefault([](KRef exception) { - konan::consoleErrorf("Reporting %d\n", Object::FromObjHeader(exception)->value); - }); - ON_CALL(*Kotlin_runUnhandledExceptionHookMock, Call(_)).WillByDefault([](KRef exception) { - konan::consoleErrorf("Hook %d\n", Object::FromObjHeader(exception)->value); - }); - EXPECT_DEATH( - { - std::set_terminate([]() { - konan::consoleErrorf("Custom terminate\n"); - if (auto exception = std::current_exception()) { - try { - std::rethrow_exception(exception); - } catch (int i) { - konan::consoleErrorf("Exception %d\n", i); - } catch (...) { - konan::consoleErrorf("Unknown Exception\n"); - } - } - }); - SetKonanTerminateHandler(); - try { - ThrowException(exception.header()); - } catch (...) { - std::terminate(); - } - }, - "Hook 42\n"); + Object exception(typeHolder.typeInfo()); + exception.header()->typeInfoOrMeta_ = setPointerBits(exception.header()->typeInfoOrMeta_, OBJECT_TAG_PERMANENT_CONTAINER); + exception->value = 42; + auto reportUnhandledExceptionMock = ScopedReportUnhandledExceptionMock(); + auto Kotlin_runUnhandledExceptionHookMock = ScopedKotlin_runUnhandledExceptionHookMock(); + ON_CALL(*reportUnhandledExceptionMock, Call(_)).WillByDefault([](KRef exception) { + konan::consoleErrorf("Reporting %d\n", Object::FromObjHeader(exception)->value); }); + ON_CALL(*Kotlin_runUnhandledExceptionHookMock, Call(_)).WillByDefault([](KRef exception) { + konan::consoleErrorf("Hook %d\n", Object::FromObjHeader(exception)->value); + }); + EXPECT_DEATH( + { + std::set_terminate([]() { + konan::consoleErrorf("Custom terminate\n"); + if (auto exception = std::current_exception()) { + try { + std::rethrow_exception(exception); + } catch (int i) { + konan::consoleErrorf("Exception %d\n", i); + } catch (...) { + konan::consoleErrorf("Unknown Exception\n"); + } + } + }); + // The termination handler will check the initialization of the whole runtime, so we cannot use RunInNewThread here. + // This call also sets the K/N termination handler. + Kotlin_initRuntimeIfNeeded(); + try { + ThrowException(exception.header()); + } catch (...) { + std::terminate(); + } + }, + "Hook 42\n"); } TEST(ExceptionDeathTest, TerminateHandler_NoHook) { test_support::TypeInfoHolder typeHolder{test_support::TypeInfoHolder::ObjectBuilder().setSuperType(theThrowableTypeInfo)}; - kotlin::RunInNewThread([&typeHolder]() { - Object exception(typeHolder.typeInfo()); - exception.header()->typeInfoOrMeta_ = setPointerBits(exception.header()->typeInfoOrMeta_, OBJECT_TAG_PERMANENT_CONTAINER); - exception->value = 42; - auto reportUnhandledExceptionMock = ScopedReportUnhandledExceptionMock(); - auto Kotlin_runUnhandledExceptionHookMock = ScopedKotlin_runUnhandledExceptionHookMock(); - ON_CALL(*reportUnhandledExceptionMock, Call(_)).WillByDefault([](KRef exception) { - konan::consoleErrorf("Reporting %d\n", Object::FromObjHeader(exception)->value); - }); - ON_CALL(*Kotlin_runUnhandledExceptionHookMock, Call(_)).WillByDefault([](KRef exception) { - konan::consoleErrorf("Hook %d\n", Object::FromObjHeader(exception)->value); - // Kotlin_runUnhandledExceptionHookMock rethrows original exception when hook is unset. - ThrowException(exception); - }); - EXPECT_DEATH( - { - std::set_terminate([]() { - konan::consoleErrorf("Custom terminate\n"); - if (auto exception = std::current_exception()) { - try { - std::rethrow_exception(exception); - } catch (int i) { - konan::consoleErrorf("Exception %d\n", i); - } catch (...) { - konan::consoleErrorf("Unknown Exception\n"); - } - } - }); - SetKonanTerminateHandler(); - try { - ThrowException(exception.header()); - } catch (...) { - std::terminate(); - } - }, - "Hook 42\nReporting 42\n"); + Object exception(typeHolder.typeInfo()); + exception.header()->typeInfoOrMeta_ = setPointerBits(exception.header()->typeInfoOrMeta_, OBJECT_TAG_PERMANENT_CONTAINER); + exception->value = 42; + auto reportUnhandledExceptionMock = ScopedReportUnhandledExceptionMock(); + auto Kotlin_runUnhandledExceptionHookMock = ScopedKotlin_runUnhandledExceptionHookMock(); + ON_CALL(*reportUnhandledExceptionMock, Call(_)).WillByDefault([](KRef exception) { + konan::consoleErrorf("Reporting %d\n", Object::FromObjHeader(exception)->value); }); + ON_CALL(*Kotlin_runUnhandledExceptionHookMock, Call(_)).WillByDefault([](KRef exception) { + konan::consoleErrorf("Hook %d\n", Object::FromObjHeader(exception)->value); + // Kotlin_runUnhandledExceptionHookMock rethrows original exception when hook is unset. + ThrowException(exception); + }); + EXPECT_DEATH( + { + std::set_terminate([]() { + konan::consoleErrorf("Custom terminate\n"); + if (auto exception = std::current_exception()) { + try { + std::rethrow_exception(exception); + } catch (int i) { + konan::consoleErrorf("Exception %d\n", i); + } catch (...) { + konan::consoleErrorf("Unknown Exception\n"); + } + } + }); + // The termination handler will check the initialization of the whole runtime, so we cannot use RunInNewThread here. + // This call also sets the K/N termination handler. + Kotlin_initRuntimeIfNeeded(); + try { + ThrowException(exception.header()); + } catch (...) { + std::terminate(); + } + }, + "Hook 42\nReporting 42\n"); } TEST(ExceptionDeathTest, TerminateHandler_WithFailingHook) { test_support::TypeInfoHolder typeHolder{test_support::TypeInfoHolder::ObjectBuilder().setSuperType(theThrowableTypeInfo)}; - kotlin::RunInNewThread([&typeHolder]() { - Object exception(typeHolder.typeInfo()); - exception.header()->typeInfoOrMeta_ = setPointerBits(exception.header()->typeInfoOrMeta_, OBJECT_TAG_PERMANENT_CONTAINER); - exception->value = 42; - Object hookException(typeHolder.typeInfo()); - hookException.header()->typeInfoOrMeta_ = setPointerBits(hookException.header()->typeInfoOrMeta_, OBJECT_TAG_PERMANENT_CONTAINER); - hookException->value = 13; - auto reportUnhandledExceptionMock = ScopedReportUnhandledExceptionMock(); - auto Kotlin_runUnhandledExceptionHookMock = ScopedKotlin_runUnhandledExceptionHookMock(); - ON_CALL(*reportUnhandledExceptionMock, Call(_)).WillByDefault([](KRef exception) { - konan::consoleErrorf("Reporting %d\n", Object::FromObjHeader(exception)->value); - }); - ON_CALL(*Kotlin_runUnhandledExceptionHookMock, Call(_)).WillByDefault([&hookException](KRef exception) { - konan::consoleErrorf("Hook %d\n", Object::FromObjHeader(exception)->value); - ThrowException(hookException.header()); - }); - EXPECT_DEATH( - { - std::set_terminate([]() { - konan::consoleErrorf("Custom terminate\n"); - if (auto exception = std::current_exception()) { - try { - std::rethrow_exception(exception); - } catch (int i) { - konan::consoleErrorf("Exception %d\n", i); - } catch (...) { - konan::consoleErrorf("Unknown Exception\n"); - } - } - }); - SetKonanTerminateHandler(); - try { - ThrowException(exception.header()); - } catch (...) { - std::terminate(); - } - }, - "Hook 42\nReporting 13\n"); + Object exception(typeHolder.typeInfo()); + exception.header()->typeInfoOrMeta_ = setPointerBits(exception.header()->typeInfoOrMeta_, OBJECT_TAG_PERMANENT_CONTAINER); + exception->value = 42; + Object hookException(typeHolder.typeInfo()); + hookException.header()->typeInfoOrMeta_ = setPointerBits(hookException.header()->typeInfoOrMeta_, OBJECT_TAG_PERMANENT_CONTAINER); + hookException->value = 13; + auto reportUnhandledExceptionMock = ScopedReportUnhandledExceptionMock(); + auto Kotlin_runUnhandledExceptionHookMock = ScopedKotlin_runUnhandledExceptionHookMock(); + ON_CALL(*reportUnhandledExceptionMock, Call(_)).WillByDefault([](KRef exception) { + konan::consoleErrorf("Reporting %d\n", Object::FromObjHeader(exception)->value); }); + ON_CALL(*Kotlin_runUnhandledExceptionHookMock, Call(_)).WillByDefault([&hookException](KRef exception) { + konan::consoleErrorf("Hook %d\n", Object::FromObjHeader(exception)->value); + ThrowException(hookException.header()); + }); + EXPECT_DEATH( + { + std::set_terminate([]() { + konan::consoleErrorf("Custom terminate\n"); + if (auto exception = std::current_exception()) { + try { + std::rethrow_exception(exception); + } catch (int i) { + konan::consoleErrorf("Exception %d\n", i); + } catch (...) { + konan::consoleErrorf("Unknown Exception\n"); + } + } + }); + // The termination handler will check the initialization of the whole runtime, so we cannot use RunInNewThread here. + // This call also sets the K/N termination handler. + Kotlin_initRuntimeIfNeeded(); + try { + ThrowException(exception.header()); + } catch (...) { + std::terminate(); + } + }, + "Hook 42\nReporting 13\n"); } TEST(ExceptionDeathTest, TerminateHandler_IgnoreHooks) { @@ -287,3 +294,236 @@ TEST(ExceptionDeathTest, TerminateHandler_IgnoreHooks) { "Custom terminate\nException 3\n"); }); } + +namespace { + +using NativeHandlerMock = NiceMock>; +using OnUnhandledExceptionMock = NiceMock>; + +KStdUniquePtr gNativeHandlerMock = nullptr; +KStdUniquePtr> gOnUnhandledExceptionMock = nullptr; + +// Google Test's death tests do not fail in case of a failed EXPECT_*/ASSERT_* check in a death statement. +// To workaround it, manually check the conditions to be asserted, log all failed conditions and then +// validate that there were no failure messages. +void loggingAssert(bool condition, const char* message) noexcept { + if (!condition) { + std::cerr << "FAIL: " << message << std::endl; + } +} + +void log(const char* message) noexcept { + std::cerr << message << std::endl; +} + +NativeHandlerMock& setNativeTerminateHandler() noexcept { + gNativeHandlerMock = make_unique(); + std::set_terminate([]() { + gNativeHandlerMock->Call(); + std::abort(); + }); + return *gNativeHandlerMock; +} + +OnUnhandledExceptionMock& setKotlinTerminationHandler() noexcept { + gOnUnhandledExceptionMock = + make_unique>(ScopedKotlin_runUnhandledExceptionHookMock()); + SetKonanTerminateHandler(); + return gOnUnhandledExceptionMock->get(); +} + +void setupMocks(bool expectRegisteredThread = true) noexcept { + auto& nativeHandlerMock = setNativeTerminateHandler(); + ON_CALL(nativeHandlerMock, Call) + .WillByDefault([expectRegisteredThread]() { + if (expectRegisteredThread) { + loggingAssert(mm::GetMemoryState() != nullptr, "Expected registered thread in the native handler"); + loggingAssert(GetThreadState() == ThreadState::kNative, "Expected kNative thread state in the native handler"); + } else { + loggingAssert(mm::GetMemoryState() == nullptr, "Expected unregistered thread in the native handler"); + } + log("Native handler"); + }); + + auto& onUnhandledExceptionMock = setKotlinTerminationHandler(); + ON_CALL(onUnhandledExceptionMock, Call) + .WillByDefault([]() { + loggingAssert(GetThreadState() == ThreadState::kRunnable, "Expected kRunnable state in the Kotlin handler"); + log("Kotlin handler"); + }); +} + +} // namespace + +#define EXPERIMENTAL_MM_ONLY() \ + do { \ + if (CurrentMemoryModel != MemoryModel::kExperimental) { \ + GTEST_SKIP() << "This test requires the Experimental MM"; \ + } \ + } while(false) + +#define ASSERTS_PASSED AllOf(Not(HasSubstr("FAIL")), Not(HasSubstr("runtime assert"))) +#define KOTLIN_HANDLER_RAN HasSubstr("Kotlin handler") +#define NATIVE_HANDLER_RAN HasSubstr("Native handler") + +TEST(TerminationThreadStateDeathTest, TerminationInRunnableState) { + EXPERIMENTAL_MM_ONLY(); + auto testBlock = []() { + setupMocks(); + + ScopedMemoryInit init; + loggingAssert(GetThreadState() == ThreadState::kRunnable, "Expected kRunnable thread state before std::terminate"); + std::terminate(); + }; + + EXPECT_DEATH(testBlock(), AllOf(ASSERTS_PASSED, NATIVE_HANDLER_RAN, Not(KOTLIN_HANDLER_RAN))); +} + +TEST(TerminationThreadStateDeathTest, TerminationInNativeState) { + EXPERIMENTAL_MM_ONLY(); + auto testBlock = []() { + setupMocks(); + + ScopedMemoryInit init; + ThreadStateGuard stateGuard(ThreadState::kNative); + loggingAssert(GetThreadState() == ThreadState::kNative, "Expected native thread state before std::terminate"); + std::terminate(); + }; + + EXPECT_DEATH(testBlock(), + AllOf(ASSERTS_PASSED, NATIVE_HANDLER_RAN, Not(KOTLIN_HANDLER_RAN))); +} + +TEST(TerminationThreadStateDeathTest, TerminationInForeignThread) { + EXPERIMENTAL_MM_ONLY(); + auto testBlock = []() { + setupMocks(/* expectRegisteredThread = */ false); + + loggingAssert(mm::GetMemoryState() == nullptr, "Expected unregistered thread before std::terminate"); + std::terminate(); + }; + + EXPECT_DEATH(testBlock(), AllOf(ASSERTS_PASSED, NATIVE_HANDLER_RAN, Not(KOTLIN_HANDLER_RAN))); +} + +TEST(TerminationThreadStateDeathTest, UnhandledKotlinExceptionInRunnableState) { + EXPERIMENTAL_MM_ONLY(); + auto testBlock = []() { + setupMocks(); + + // Do not use RunInNewThread because the termination handler will check initiliazation + // of the whole runtime while RunInNewThread initializes the memory only. + std::thread thread([]() { + Kotlin_initRuntimeIfNeeded(); + SwitchThreadState(mm::GetMemoryState(), ThreadState::kRunnable); + + loggingAssert(GetThreadState() == ThreadState::kRunnable, "Expected kRunanble thread state before throwing"); + ObjHeader exception{}; + ExceptionObjHolder::Throw(&exception); + }); + thread.join(); + }; + + EXPECT_DEATH(testBlock(), AllOf(ASSERTS_PASSED, KOTLIN_HANDLER_RAN, Not(NATIVE_HANDLER_RAN))); +} + +TEST(TerminationThreadStateDeathTest, UnhandledKotlinExceptionInNativeState) { + EXPERIMENTAL_MM_ONLY(); + auto testBlock = []() { + setupMocks(); + + // This situation is possible if a Kotlin exception thrown by a Kotlin callback is re-thrown in + // another thread which is attached to the Kotlin runtime but has the kNative state. + + // Do not use RunInNewThread because the termination handler will check initiliazation + // of the whole runtime while RunInNewThread initializes the memory only. + std::thread thread([]() { + Kotlin_initRuntimeIfNeeded(); + + loggingAssert(GetThreadState() == ThreadState::kNative, "Expected kNative thread state before throwing"); + ObjHeader exception{}; + ExceptionObjHolder::Throw(&exception); + }); + thread.join(); + }; + + EXPECT_DEATH(testBlock(), AllOf(ASSERTS_PASSED, KOTLIN_HANDLER_RAN, Not(NATIVE_HANDLER_RAN))); +} + +TEST(TerminationThreadStateDeathTest, UnhandledKotlinExceptionInForeignThread) { + EXPERIMENTAL_MM_ONLY(); + auto testBlock = []() { + setupMocks(/* expectRegisteredThread = */ false); + + // It is possible if a Kotlin exception thrown by a Kotlin callback is re-thrown in + // another thread which is not attached to the Kotlin runtime at all. + std::thread foreignThread([]() { + loggingAssert(mm::GetMemoryState() == nullptr, "Expected unregistered thread before throwing"); + + auto future = std::async(std::launch::async, []() { + // Initial Kotlin exception throwing requires the runtime to be initialized. + // Do not use ScopedMemoryInit because it clears the stable ref queue + // of the current thread on deinitialization. After that the ExceptionObjHolder + // will contain a dangling pointer to the stable ref queue entry. + Kotlin_initRuntimeIfNeeded(); + ObjHeader exception{}; + ExceptionObjHolder::Throw(&exception); + }); + // Re-throw the Kotlin exception in a foreign thread. + future.get(); + }); + foreignThread.join(); + }; + + EXPECT_DEATH(testBlock(), AllOf(ASSERTS_PASSED, KOTLIN_HANDLER_RAN, Not(NATIVE_HANDLER_RAN))); +} + +TEST(TerminationThreadStateDeathTest, UnhandledForeignExceptionInNativeState) { + EXPERIMENTAL_MM_ONLY(); + auto testBlock = []() { + setupMocks(); + + RunInNewThread([](MemoryState* thread) { + SwitchThreadState(thread, ThreadState::kNative); + loggingAssert(GetThreadState(thread) == ThreadState::kNative, "Expected kNative thread state before throwing"); + + throw std::runtime_error("Foreign exception"); + }); + }; + + EXPECT_DEATH(testBlock(), AllOf(ASSERTS_PASSED, NATIVE_HANDLER_RAN, Not(KOTLIN_HANDLER_RAN))); +} + +TEST(TerminationThreadStateDeathTest, UnhandledForeignExceptionInForeignThread) { + EXPERIMENTAL_MM_ONLY(); + auto testBlock = []() { + setupMocks(/* expectRegisteredThread = */ false); + + std::thread foreignThread([]() { + loggingAssert(mm::GetMemoryState() == nullptr, "Expected unregistered thread before throwing"); + throw std::runtime_error("Foreign exception"); + }); + foreignThread.join(); + }; + + EXPECT_DEATH(testBlock(), AllOf(ASSERTS_PASSED, NATIVE_HANDLER_RAN, Not(KOTLIN_HANDLER_RAN))); +} + +// Model a filtering exception handler which terminates the program if an interop call throws a foreign exception. +TEST(TerminationThreadStateDeathTest, TerminationInForeignExceptionCatch) { + EXPERIMENTAL_MM_ONLY(); + auto testBlock = []() { + setupMocks(); + + ScopedMemoryInit init; + loggingAssert(GetThreadState(init.memoryState()) == ThreadState::kRunnable, "Expected kRunnable state before catching"); + + try { + throw std::runtime_error("Foreign exception"); + } catch(...) { + std::terminate(); + } + }; + + EXPECT_DEATH(testBlock(), AllOf(ASSERTS_PASSED, NATIVE_HANDLER_RAN, Not(KOTLIN_HANDLER_RAN))); +} \ No newline at end of file diff --git a/kotlin-native/runtime/src/main/cpp/Memory.h b/kotlin-native/runtime/src/main/cpp/Memory.h index f0592878284..86e2408fc0a 100644 --- a/kotlin-native/runtime/src/main/cpp/Memory.h +++ b/kotlin-native/runtime/src/main/cpp/Memory.h @@ -388,10 +388,11 @@ public: namespace kotlin { namespace mm { -// Returns the MemoryState for the current thread. The runtime must be initialized. +// Returns the MemoryState for the current thread. +// If the memory subsystem isn't initialized for the current thread, returns nullptr. // Try not to use it very often, as (1) thread local access can be slow on some platforms, // (2) TLS gets deallocated before our thread destruction hooks run. -MemoryState* GetMemoryState(); +MemoryState* GetMemoryState() noexcept; } // namespace mm diff --git a/kotlin-native/runtime/src/main/cpp/MemoryTest.cpp b/kotlin-native/runtime/src/main/cpp/MemoryTest.cpp new file mode 100644 index 00000000000..6cd5074ef41 --- /dev/null +++ b/kotlin-native/runtime/src/main/cpp/MemoryTest.cpp @@ -0,0 +1,24 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the LICENSE file. + */ + +#include "Memory.h" + +#include + +#include "TestSupport.hpp" + +using namespace kotlin; + +TEST(MemoryStateTest, GetMemoryStateForUnregisteredThread) { + EXPECT_EQ(mm::GetMemoryState(), nullptr); +} + +TEST(MemoryStateTest, GetMemoryStateForRegisteredThread) { + RunInNewThread([](MemoryState* expectedState) { + MemoryState* actualState = mm::GetMemoryState(); + EXPECT_NE(actualState, nullptr); + EXPECT_EQ(actualState, expectedState); + }); +} \ No newline at end of file diff --git a/kotlin-native/runtime/src/mm/cpp/Memory.cpp b/kotlin-native/runtime/src/mm/cpp/Memory.cpp index 3108682f457..cf8d5c0fc1e 100644 --- a/kotlin-native/runtime/src/mm/cpp/Memory.cpp +++ b/kotlin-native/runtime/src/mm/cpp/Memory.cpp @@ -537,7 +537,7 @@ extern "C" ALWAYS_INLINE RUNTIME_NOTHROW void Kotlin_mm_switchThreadStateRunnabl SwitchThreadState(mm::ThreadRegistry::Instance().CurrentThreadData(), ThreadState::kRunnable); } -MemoryState* kotlin::mm::GetMemoryState() { +MemoryState* kotlin::mm::GetMemoryState() noexcept { return ToMemoryState(ThreadRegistry::Instance().CurrentThreadDataNode()); } diff --git a/kotlin-native/runtime/src/mm/cpp/ThreadRegistry.cpp b/kotlin-native/runtime/src/mm/cpp/ThreadRegistry.cpp index 7d7c9f2ce71..3bc8eda90c0 100644 --- a/kotlin-native/runtime/src/mm/cpp/ThreadRegistry.cpp +++ b/kotlin-native/runtime/src/mm/cpp/ThreadRegistry.cpp @@ -41,7 +41,8 @@ std::unique_lock mm::ThreadRegistry::Lock() noexcept } ALWAYS_INLINE mm::ThreadData* mm::ThreadRegistry::CurrentThreadData() const noexcept { - return CurrentThreadDataNode()->Get(); + auto* threadDataNode = CurrentThreadDataNode(); + return threadDataNode ? threadDataNode->Get() : nullptr; } mm::ThreadRegistry::ThreadRegistry() = default;