JetBrains / kotlin-native

Kotlin/Native infrastructure
Apache License 2.0
7.02k stars 566 forks source link

NSException.raise ignores ObjC try-catch and crashes program #3553

Closed vganin closed 3 years ago

vganin commented 4 years ago

Turns out that it is impossible to catch NSExceptions in any code (neither Kotlin nor ObjC) with Kotlin 1.3.50 if it has Kotlin stack frame on the unwinding path.

It is impossible to catch it from Kotlin because NSException is not a Throwable.

It is impossible to catch it with regular ObjC @try { ... } @catch { ... } blocks but I don't know why really. It seems that it should just work unless Kotlin frame breaks stack unwinding for ObjC somehow.

Simple example proves it:

// KotlinNativeFramework.kt
class KotlinNativeFramework {
    fun helloFromKotlin() {
        NSException.raise("test", "test")
    }
}
// AppDelegate.m
@try {
    [[[KNFKotlinNativeFramework alloc] init] helloFromKotlin];
}
@catch (NSException *exception) {
    NSLog(@"Caught");
}

It crashes with

2019-11-05 22:55:54.572795+0300 NSExceptionRaise[22151:509901] *** Terminating app due to uncaught exception 'test', reason: 'test'
*** First throw call stack:
(
    0   CoreFoundation                      0x00007fff23b98bde __exceptionPreprocess + 350
    1   libobjc.A.dylib                     0x00007fff503b5b20 objc_exception_throw + 48
    2   CoreFoundation                      0x00007fff23b98a1c +[NSException raise:format:] + 188
    3   KotlinNativeFramework               0x0000000102543dde kfun:KotlinNativeFramework.helloFromKotlin() + 270
    4   KotlinNativeFramework               0x000000010254677d blockCopyHelper + 573
    5   UIKitCore                           0x00007fff4715994f -[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:] + 232
    6   UIKitCore                           0x00007fff4715b2e7 -[UIApplication _callInitializationDelegatesWithActions:forCanvas:payload:fromOriginatingProcess:] + 3980
    7   UIKitCore                           0x00007fff47160c05 -[UIApplication _runWithMainScene:transitionContext:completion:] + 1281
    8   UIKitCore                           0x00007fff468b58ea -[_UISceneLifecycleMultiplexer completeApplicationLaunchWithFBSScene:transitionContext:] + 179
    9   UIKitCore                           0x00007fff4715d1a5 -[UIApplication _compellApplicationLaunchToCompleteUnconditionally] + 59
    10  UIKitCore                           0x00007fff4715d4a4 -[UIApplication _run] + 754
    11  UIKitCore                           0x00007fff47162a67 UIApplicationMain + 1621
    12  NSExceptionRaise                    0x0000000102443e94 main + 116
    13  libdyld.dylib                       0x00007fff5123bcf5 start + 1
    14  ???                                 0x0000000000000001 0x0 + 1
SvyatoslavScherbina commented 4 years ago

NSException isn't forwarded through Kotlin frames as intended.

vganin commented 4 years ago

But is there any workaround for now? My case is to wrap foreign library API which throws (which is intended by library designers). I can wrap it completely in ObjC to catch all exceptions but is it the only way now?

SvyatoslavScherbina commented 4 years ago

Which languages do you expect these wrappers to be called from? Swift, Objective-C, Kotlin?

vganin commented 4 years ago

I'm calling it from Kotlin Common actually in MPP setting.

vganin commented 4 years ago

Since NSException isn't a Throwable, I'm trying to write function something like native_objc_try_catch which will allow to catch NSExceptions in Kotlin. And I stumbled upon this problem that NSException cannot be catched through Kotlin frame even in Objective-C.

SvyatoslavScherbina commented 4 years ago

There is no workaround available currently. Btw, is this library usable from Swift?

vganin commented 4 years ago

Yes it is.

SvyatoslavScherbina commented 4 years ago

How do you catch an Objective-C exception when calling this library from Swift?

vganin commented 4 years ago

I have the following helper in objective-c:

// NSException+YXTry.h

#import <Foundation/Foundation.h>

typedef void (^YXTryBlock)(void);
typedef void (^YXCatchBlock)(NSException *exception);

@interface NSException (YXTry)

+ (void)yx_try:(YXTryBlock)tryBlock catch:(YXCatchBlock)catchBlock;

@end

// NSException+YXTry.m

@implementation NSException (YXTry)

+ (void)yx_try:(YXTryBlock)tryBlock catch:(YXCatchBlock)catchBlock {
    @try {
        if (tryBlock) {
            tryBlock();
        }
    }
    @catch (NSException *exception) {
        if (catchBlock) {
            catchBlock(exception);
        }
    }
}

@end

And then I use it in Swift like this

NSException.yx_try(
    {
        // Something
    },
    catch: {
        // Something
    }
)
SvyatoslavScherbina commented 4 years ago

Doesn't this cause memory leaks? Both Objective-C and Swift may leak if an exception is thrown.

vganin commented 4 years ago

I think it does not cause memory leaks if you compile with -fobjc-arc-exceptions but I may be wrong.

SvyatoslavScherbina commented 4 years ago

AFAIK, it doesn't affect Swift.

SvyatoslavScherbina commented 4 years ago

Btw, which library do you use?

vganin commented 4 years ago

Yandex MapKit - C++ library with platform bindings. It can throw unexpectedly so we are trying to wrap it to report all exceptions to some crash-reporting service safely.

SvyatoslavScherbina commented 4 years ago

It can throw unexpectedly so we are trying to wrap it to report all exceptions to some crash-reporting service safely.

Should the application get terminated after reporting such an exception?

vganin commented 4 years ago

I think it shouldn't.

yackermann commented 4 years ago

Any updates on this issue? I am having same problem.

The code:

private fun canEvaluatePolicyWrapper(policy: LAPolicy): Boolean {
        val authContext = LAContext()
            try {
                return authContext.canEvaluatePolicy(policy, null)
            } catch(e: Exception) {
                print("Error while evaluating policy. The message is: ${e.message}")
                return false
            }
        }
    }
yackermann commented 4 years ago

Error

2020-08-27 23:56:12.105991+0300 iosSDKDemo[5310:76767] [LAErrorHelper] Error Domain=com.apple.LocalAuthentication Code=-1001 "Unknown policy: '0'" UserInfo={NSLocalizedDescription=Unknown policy: '0'}
2020-08-27 23:56:12.122039+0300 iosSDKDemo[5310:76767] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Error Domain=com.apple.LocalAuthentication Code=-1001 "Unknown policy: '0'" UserInfo={NSLocalizedDescription=Unknown policy: '0'}'
*** First throw call stack:
(
    0   CoreFoundation                      0x00007fff23e3de6e __exceptionPreprocess + 350
    1   libobjc.A.dylib                     0x00007fff512539b2 objc_exception_throw + 48
    2   CoreFoundation                      0x00007fff23e3dcac +[NSException raise:format:] + 188
    3   SharedUtils                         0x00007fff2775d797 +[LAErrorHelper raiseExceptionOnError:] + 165
    4   LocalAuthentication                 0x00007fff277327ad -[LAClient evaluatePolicy:options:uiDelegate:reply:] + 240
    5   LocalAuthentication                 0x00007fff27732e26 -[LAClient evaluatePolicy:options:reply:] + 91
    6   LocalAuthentication                 0x00007fff27736686 __51-[LAContext _evaluatePolicy:options:log:cid:reply:]_block_invoke + 168
    7   libdispatch.dylib                   0x000000010acb0e8e _dispatch_client_callout + 8
    8   libdispatch.dylib                   0x000000010acb3da2 _dispatch_block_invoke_direct + 300
    9   libdispatch.dylib                   0x000000010acb3c6b dispatch_block_perform + 124
    10  LocalAuthentication                 0x00007fff2773653a -[LAContext _evaluatePolicy:options:log:cid:reply:] + 470
    11  LocalAuthentication                 0x00007fff27736cec -[LAContext _evaluatePolicy:options:log:cid:error:] + 262
    12  LocalAuthentication                 0x00007fff27737b19 -[LAContext canEvaluatePolicy:error:] + 398
    13  iosSDKDemo                          0x000000010a29645f _416e64726f6964554146313153646b3a536861726564436f6465_knbridge13 + 31
    14  iosSDKDemo                          0x000000010a23f4cb kfun:uaf.asms.primary.interfaces.Crypto.canEvaluatePolicyWrapper#internal + 955
    15  iosSDKDemo                          0x000000010a23ddfa kfun:uaf.asms.primary.interfaces.Crypto#getAvailableBiometrics(){}uaf.asms.primary.interfaces.ASMBiometricsInfo + 154
    16  iosSDKDemo                          0x000000010a1d6973 kfun:uaf.asms.primary.BaseASM.$registerCOROUTINE$4#invokeSuspend(kotlin.Result<kotlin.Any?>){}kotlin.Any? + 5091
    17  iosSDKDemo                          0x000000010a1d8eab kfun:uaf.asms.primary.BaseASM#register(kotlin.String;kotlin.String;kotlin.ByteArray){}kotlin.Pair<uaf.interfaces.registry.ASMErrorCodes,uaf.interfaces.uglybugfix.UAFResponseAssertion?> + 475
    18  iosSDKDemo                          0x000000010a1d0a58 kfun:uaf.Client.$processUAFRequestCOROUTINE$3#invokeSuspend(kotlin.Result<kotlin.Any?>){}kotlin.Any? + 11256
    19  iosSDKDemo                          0x000000010a1d3670 kfun:uaf.Client#processUAFRequest(kotlin.String){}kotlin.Pair<kotlin.Int,kotlin.String?> + 320
    20  iosSDKDemo                          0x000000010a24a2f1 objc2kotlin.34 + 369
    21  SwiftUI                             0x00007fff2c3f7dc0 $s7SwiftUI33PrimitiveButtonStyleConfigurationV7triggeryyF + 16
    22  SwiftUI                             0x00007fff2c4f2ca0 $s7SwiftUI33PrimitiveButtonStyleConfigurationV7triggeryyFTA + 16
    23  SwiftUI                             0x00007fff2c722aa4 $s7SwiftUI25PressableGestureCallbacksV8dispatch5phase5stateyycSgAA0D5PhaseOyxG_SbztFyycfU_TA + 36
    24  SwiftUI                             0x00007fff2c574acc $sIeg_ytIegr_TR + 12
    25  SwiftUI                             0x00007fff2c6e4fd1 $sIeg_ytIegr_TRTA + 17
    26  SwiftUI                             0x00007fff2c6e5e89 $sIeg_ytIegr_TRTA.46 + 9
    27  SwiftUI                             0x00007fff2c574aec $sytIegr_Ieg_TR + 12
    28  SwiftUI                             0x00007fff2c574acc $sIeg_ytIegr_TR + 12
    29  SwiftUI                             0x00007fff2c6e4fd1 $sIeg_ytIegr_TRTA + 17
    30  SwiftUI                             0x00007fff2c6e5e99 $sIeg_ytIegr_TRTA.54 + 9
    31  SwiftUI                             0x00007fff2c567a5e $s7SwiftUI6UpdateO15dispatchActionsyyFZ + 414
    32  SwiftUI                             0x00007fff2c56745a $s7SwiftUI6UpdateO3endyyFZ + 106
    33  SwiftUI                             0x00007fff2c5a6f5c $s7SwiftUI19EventBindingManagerC4sendyySDyAA0C2IDVAA0C4Type_pGF + 284
    34  SwiftUI                             0x00007fff2c8bf993 $s7SwiftUI22UIKitGestureRecognizerC4send025_062C14327F4C9197D92807A7H6DF7F3BLL7touches5event5phaseSayAA7EventIDVGShySo7UITouchCG_So7UIEventCAA0Q5PhaseOtF + 67
    35  SwiftUI                             0x00007fff2c8bfc2c $s7SwiftUI22UIKitGestureRecognizerC12touchesEnded_4withyShySo7UITouchCG_So7UIEventCtF + 12
    36  SwiftUI                             0x00007fff2c8c0343 $s7SwiftUI22UIKitGestureRecognizerC12touchesBegan_4withyShySo7UITouchCG_So7UIEventCtFToTm + 131
    37  SwiftUI                             0x00007fff2c8bfc68 $s7SwiftUI22UIKitGestureRecognizerC12touchesEnded_4withyShySo7UITouchCG_So7UIEventCtFTo + 40
    38  UIKitCore                           0x00007fff48eb8f88 -[UIGestureRecognizer _touchesEnded:withEvent:] + 141
    39  UIKitCore                           0x00007fff493aea38 -[UITouchesEvent _sendEventToGestureRecognizer:] + 673
    40  UIKitCore                           0x00007fff48eaccd2 __47-[UIGestureEnvironment _updateForEvent:window:]_block_invoke + 70
    41  UIKitCore                           0x00007fff48eace04 -[UIGestureEnvironment _deliverEvent:toGestureRecognizers:usingBlock:] + 259
    42  UIKitCore                           0x00007fff48eacc5c -[UIGestureEnvironment _updateForEvent:window:] + 225
    43  UIKitCore                           0x00007fff49364277 -[UIWindow sendEvent:] + 4479
    44  UIKitCore                           0x00007fff4933e6d1 -[UIApplication sendEvent:] + 356
    45  UIKit                               0x000000010ede60b8 -[UIApplicationAccessibility sendEvent:] + 85
    46  UIKitCore                           0x00007fff493c94ce __dispatchPreprocessedEventFromEventQueue + 7628
    47  UIKitCore                           0x00007fff493cc692 __handleEventQueueInternal + 6584
    48  UIKitCore                           0x00007fff493c2f35 __handleHIDEventFetcherDrain + 88
    49  CoreFoundation                      0x00007fff23da1c91 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    50  CoreFoundation                      0x00007fff23da1bbc __CFRunLoopDoSource0 + 76
    51  CoreFoundation                      0x00007fff23da1394 __CFRunLoopDoSources0 + 180
    52  CoreFoundation                      0x00007fff23d9bf8e __CFRunLoopRun + 974
    53  CoreFoundation                      0x00007fff23d9b8a4 CFRunLoopRunSpecific + 404
    54  GraphicsServices                    0x00007fff38c39bbe GSEventRunModal + 139
    55  UIKitCore                           0x00007fff49325968 UIApplicationMain + 1605
    56  iosSDKDemo                          0x000000010a1c279b main + 75
    57  libdyld.dylib                       0x00007fff520ce1fd start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
(lldb) 
SvyatoslavScherbina commented 4 years ago

@herrjemand

Any updates on this issue? I am having same problem.

Yes. Work in progress: https://github.com/JetBrains/kotlin-native/pull/4307

yackermann commented 4 years ago

@SvyatoslavScherbina any way I can temporary work around?

SvyatoslavScherbina commented 4 years ago

@herrjemand

any way I can temporary work around?

Wrapping Objective-C methods that throw exceptions to Objective-C methods or top-level functions with @try-@catch should help.

knebekaizer commented 3 years ago

Please see #4307 The commit provides opt-in mode for wrapping unhandled NSException in Kotlin exception object.

HowTo: Use command line cinterop -Xforeign-exception-mode objc-wrap or add foreignExceptionMode = objc-wrap property to .def file. The option affects kotlin-native binding to all functions and methods exported from this native module. Kotlin runtime environment will handle and wrap any NSException propagating from this native library, i.e. NSException that raised and not caught in native. The wrapper type is ForeignException, as any other Kotlin exception it will not propagate back to native and must be handled in Kotlin code. The original NSException object is stored as ForeignException.nativeException property, so it may be accessed inside of ForeignException catch block for advanced use.