Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

setjmp/longjmp are Undefined Behavior in Swift #1

Closed
calebkleveter opened this issue Jan 13, 2020 · 9 comments
Closed

setjmp/longjmp are Undefined Behavior in Swift #1

calebkleveter opened this issue Jan 13, 2020 · 9 comments

Comments

@calebkleveter
Copy link
Contributor

I was ecstatic when I saw this repo. Unfortunately, I was informed that the basis of the coroutines defined in this project are unsafe:

setjmp and longjmp aren't safe to use in Swift (or ARC ObjC)—retain counts will leak, and weak refs may corrupt memory.
- Joe Groff

Further references:

https://twitter.com/search?q=from%3Ajckarter%20setjmp&src=typed_query

Unless this information is incorrect, there should probably at least be a warning in the README on this.

@belozierov
Copy link
Owner

belozierov commented Jan 13, 2020

Thanks a lot for your comment.

I did some testing, and really in the current 1.0.2 version there is a big problem with leaks caused by setjmp/longjmp which I’ve missed. So thanks for spotting it.

Setjmp/longjmp is actually a tricky API and can cause leak problems if used incautiously, that’s why Apple has disabled it for Swift. But at the same time, it is a convenient OOTB solution for saving and loading CPU registers. However, this API requires very careful usage and following certain rules to avoid the above issues with ARC.

I’ve already found a way to handle setjmp/longjmp leaks. I’ll need to slightly change the existing implementation and in a few days I will release a new update with fixes. So you’ll be able to check it yourself soon.

I really appreciate your feedback.

@belozierov
Copy link
Owner

In version 1.0.4 a problem with setjmp/longjmp memory leaks was fixed. Please find a test project in the attachment. Here you can check the usage with the leak instrument and look over the memory usage indicators. You can also compare the results with version 1.0.2 and see the difference. Please LMK your feedback in case you'll test this.

Thanks again for raising this issue.
SwiftCoroutineMemoryLeakTest.zip

@belozierov
Copy link
Owner

As to undefined behavior.

It needs to be clarified that setjmp/longjmp has undefined behavior when used with ARC and not with Swift in general. This can also be assumed from the statement by Joe Groff. But the same applies to any other api and assembly code that change the normal function execution and return, including adjusting values of the cpu special registers. This happens because ARC can’t track such things and it can lead to leaks and memory corruption.

But on the other hand, it is impossible to create proper coroutines without such modifications. The suspend/resume feature in coroutine itself implies a change of normal function execution that ARC is unable to track.

Setjmp/longjmp was used in the library to minimize assembly code in the project and also because it is high-level, tested, predictable and is included in the standard C library.

Several tricks were used to avoid ARC disruption when this api is applied. It’s guaranteed that when a coroutine is suspended it will always resume in the same place and only once. This ensures safe routines execution and normal ARC functioning. The only exception is the return from coroutine’s stack with longjmp after its completion. In this case, all objects that could have been captured in the stack by ARC, including weak and unowned references, will be leaked. Therefore, the implementation guarantees that no objects will be captured at the moment of returning from the coroutine. Any api that violates these statements is private and inaccessible to a user.

@belozierov
Copy link
Owner

I discussed this issue with Joe Groff and here is his reply:

If you're always resuming coroutines, then that should address the issue of leaks. Last I checked, calling setjmp directly from Swift didn't lower it correctly in LLVM (it needs the special returns_twice LLVM attribute so that it doesn't get miscompiled), but if you're calling it from C that shouldn't be a problem.

One way to look at it is that you aren't really using setjmp/longjmp in Swift, but as an implementation detail in order to expose coroutine primitives as Swift API. As long as the implementation maintains the invariants assumed by the Swift runtime environment, then it shouldn't matter how the implementation achieves those goals.

@calebkleveter
Copy link
Contributor Author

Thanks for looking into this! I've decided to go ahead and add support to NIO's EventLoopFuture type for the async/await syntax, similar to what your CoFuture type has: https://github.com/calebkleveter/NIOAsync

@nonameplum
Copy link

@belozierov @calebkleveter Based on this input is it this framework production ready or it will stay rather as a curiosity?

@belozierov
Copy link
Owner

@nonameplum The version 1.0.7 is stable and has no bugs that I am aware of. I marked it as a pre-release because in the nearest future I plan to make some changes in api.

Currently I’m working on a new version that will include an improved version of CoFuture with much better performance (inspired by EventLoopFuture). Thus, its api will be slightly changed and finalized. This might lead to some migration.

In case of any issues or questions, feel free to contact me.

@belozierov
Copy link
Owner

@nonameplum To sum up on setjmp/longjmp

Setjmp got some bad fame in early versions of swift because of memory leaks, so it was blocked and is now unavailable in swift. Therefore, the coroutine core was written in C where its usage is native and creates no problems. Joe Groff's answer fully confirms this.

@nonameplum
Copy link

@belozierov Thanks a lot for the explanation.

ladeiko added a commit to ladeiko/SwiftCoroutine that referenced this issue Aug 5, 2022
…detect mods to free blocks

SwiftCoroutineDemo(39856,0x11266e600) malloc: nano zone abandoned due to inability to preallocate reserved vm space.
DispatchQueue::scheduleTask() <OS_dispatch_queue_main: com.apple.main-thread>
SharedCoroutineDispatcher::getFreeQueue(SwiftCoroutine.SharedCoroutineDispatcher)
CoroutineContext::init(stackSize=196608, guardPage=true, SwiftCoroutine.CoroutineContext) returnEnv = 0x000060f0000042d0 stack = 0x000000010effb000
SharedCoroutineQueue::start(SwiftCoroutine.SharedCoroutineQueue)
CoroutineContext::start(SwiftCoroutine.CoroutineContext) enter
CoroutineContext::stackTop(SwiftCoroutine.CoroutineContext) 0x000000010f02b000
CoroutineContext::start(Optional(0x00006060000b8400)) block enter
==39856==WARNING: ASan is ignoring requested __asan_handle_no_return: stack type: default top: 0x7ff7b7045000; bottom 0x00010f028000; size: 0x7ff6a801d000 (140697357373440)
False positive error reports may follow
For details see google/sanitizers#189
CoroutineContext::start(SwiftCoroutine.CoroutineContext) leave
DispatchQueue::scheduleTask() <OS_dispatch_queue_main: com.apple.main-thread>
SharedCoroutineDispatcher::getFreeQueue(SwiftCoroutine.SharedCoroutineDispatcher)
SharedCoroutineQueue::start(SwiftCoroutine.SharedCoroutineQueue)
CoroutineContext::stackTop(SwiftCoroutine.CoroutineContext) 0x000000010f02b000
=================================================================
==39856==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x00010f029ec1 at pc 0x00010949d77d bp 0x7ff7b7040e90 sp 0x7ff7b7040650
READ of size 4416 at 0x00010f029ec1 thread T0
    #0 0x10949d77c in wrap_memcpy+0x16c (libclang_rt.asan_iossim_dynamic.dylib:x86_64+0x1977c)
    belozierov#1 0x109ef7f83 in SharedCoroutine.saveStack() SharedCoroutine.swift:79
    belozierov#2 0x109eff514 in SharedCoroutineQueue.start(dispatcher:scheduler:task:) SharedCoroutineQueue.swift:39
    belozierov#3 0x109efc229 in closure belozierov#1 in SharedCoroutineDispatcher.execute(on:task:) SharedCoroutineDispatcher.swift:27
    belozierov#4 0x109ee7ed9 in OS_dispatch_queue.scheduleTask(_:) CoroutineScheduler+DispatchQueue.swift:16
    belozierov#5 0x109ee88ad in protocol witness for CoroutineScheduler.scheduleTask(_:) in conformance OS_dispatch_queue <compiler-generated>
    belozierov#6 0x109efbf48 in SharedCoroutineDispatcher.execute(on:task:) SharedCoroutineDispatcher.swift:26
    belozierov#7 0x109ee3616 in CoroutineScheduler._startCoroutine(_:) CoroutineScheduler.swift:51
    belozierov#8 0x109ee3c78 in CoroutineScheduler.startCoroutine(in:task:) CoroutineScheduler.swift:69
    belozierov#9 0x108ebe2ed in AppDelegate.test() AppDelegate.swift:43
    belozierov#10 0x108ebdd77 in AppDelegate.application(_:didFinishLaunchingWithOptions:) AppDelegate.swift:18
    belozierov#11 0x108ebe4b7 in @objc AppDelegate.application(_:didFinishLaunchingWithOptions:) <compiler-generated>
    belozierov#12 0x7fff2509a592 in -[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:]+0xd5 (UIKitCore:x86_64+0xcd7592)
    belozierov#13 0x7fff2509c283 in -[UIApplication _callInitializationDelegatesWithActions:forCanvas:payload:fromOriginatingProcess:]+0x101f (UIKitCore:x86_64+0xcd9283)
    belozierov#14 0x7fff250a1c23 in -[UIApplication _runWithMainScene:transitionContext:completion:]+0x4a5 (UIKitCore:x86_64+0xcdec23)
    belozierov#15 0x7fff246124a2 in -[_UISceneLifecycleMultiplexer completeApplicationLaunchWithFBSScene:transitionContext:]+0xb2 (UIKitCore:x86_64+0x24f4a2)
    belozierov#16 0x7fff2509e2ac in -[UIApplication _compellApplicationLaunchToCompleteUnconditionally]+0x3a (UIKitCore:x86_64+0xcdb2ac)
    belozierov#17 0x7fff2509e64b in -[UIApplication _run]+0x391 (UIKitCore:x86_64+0xcdb64b)
    belozierov#18 0x7fff250a32b4 in UIApplicationMain+0x64 (UIKitCore:x86_64+0xce02b4)
    belozierov#19 0x7fff59d55cc1 in UIApplicationMain(_:_:_:_:)+0x61 (libswiftUIKit.dylib:x86_64+0x21cc1)
    belozierov#20 0x108ebf357 in static UIApplicationDelegate.main() <compiler-generated>
    belozierov#21 0x108ebf2a0 in static AppDelegate.$main() AppDelegate.swift:11
    belozierov#22 0x108ebfc77 in main <compiler-generated>
    belozierov#23 0x1090f8f20 in start_sim+0x9 (dyld_sim:x86_64+0x1f20)
    belozierov#24 0x1125f352d  (<unknown module>)

0x00010f029ec1 is located 192193 bytes inside of 200704-byte region [0x00010effb000,0x00010f02c000)
allocated by thread T0 here:
    #0 0x1094c7bb3 in wrap_posix_memalign+0xb3 (libclang_rt.asan_iossim_dynamic.dylib:x86_64+0x43bb3)
    belozierov#1 0x7fff30c9e62c in swift_slowAlloc+0x4c (libswiftCore.dylib:x86_64+0x30762c)
    belozierov#2 0x109ede243 in CoroutineContext.init(stackSize:guardPage:) CoroutineContext.swift:32
    belozierov#3 0x109eddc1f in CoroutineContext.__allocating_init(stackSize:guardPage:) CoroutineContext.swift
    belozierov#4 0x109efe985 in SharedCoroutineQueue.init(stackSize:) SharedCoroutineQueue.swift:28
    belozierov#5 0x109efe5c3 in SharedCoroutineQueue.__allocating_init(stackSize:) SharedCoroutineQueue.swift
    belozierov#6 0x109efcb3d in SharedCoroutineDispatcher.getFreeQueue() SharedCoroutineDispatcher.swift:38
    belozierov#7 0x109efc20d in closure belozierov#1 in SharedCoroutineDispatcher.execute(on:task:) SharedCoroutineDispatcher.swift:27
    belozierov#8 0x109ee7ed9 in OS_dispatch_queue.scheduleTask(_:) CoroutineScheduler+DispatchQueue.swift:16
    belozierov#9 0x109ee88ad in protocol witness for CoroutineScheduler.scheduleTask(_:) in conformance OS_dispatch_queue <compiler-generated>
    belozierov#10 0x109efbf48 in SharedCoroutineDispatcher.execute(on:task:) SharedCoroutineDispatcher.swift:26
    belozierov#11 0x109ee3616 in CoroutineScheduler._startCoroutine(_:) CoroutineScheduler.swift:51
    belozierov#12 0x109ee3c78 in CoroutineScheduler.startCoroutine(in:task:) CoroutineScheduler.swift:69
    belozierov#13 0x108ebe2ed in AppDelegate.test() AppDelegate.swift:43
    belozierov#14 0x108ebdd77 in AppDelegate.application(_:didFinishLaunchingWithOptions:) AppDelegate.swift:18
    belozierov#15 0x108ebe4b7 in @objc AppDelegate.application(_:didFinishLaunchingWithOptions:) <compiler-generated>
    belozierov#16 0x7fff2509a592 in -[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:]+0xd5 (UIKitCore:x86_64+0xcd7592)
    belozierov#17 0x7fff2509c283 in -[UIApplication _callInitializationDelegatesWithActions:forCanvas:payload:fromOriginatingProcess:]+0x101f (UIKitCore:x86_64+0xcd9283)
    belozierov#18 0x7fff250a1c23 in -[UIApplication _runWithMainScene:transitionContext:completion:]+0x4a5 (UIKitCore:x86_64+0xcdec23)
    belozierov#19 0x7fff246124a2 in -[_UISceneLifecycleMultiplexer completeApplicationLaunchWithFBSScene:transitionContext:]+0xb2 (UIKitCore:x86_64+0x24f4a2)
    belozierov#20 0x7fff2509e2ac in -[UIApplication _compellApplicationLaunchToCompleteUnconditionally]+0x3a (UIKitCore:x86_64+0xcdb2ac)
    belozierov#21 0x7fff2509e64b in -[UIApplication _run]+0x391 (UIKitCore:x86_64+0xcdb64b)
    belozierov#22 0x7fff250a32b4 in UIApplicationMain+0x64 (UIKitCore:x86_64+0xce02b4)
    belozierov#23 0x7fff59d55cc1 in UIApplicationMain(_:_:_:_:)+0x61 (libswiftUIKit.dylib:x86_64+0x21cc1)
    belozierov#24 0x108ebf357 in static UIApplicationDelegate.main() <compiler-generated>
    belozierov#25 0x108ebf2a0 in static AppDelegate.$main() AppDelegate.swift:11
    belozierov#26 0x108ebfc77 in main <compiler-generated>
    belozierov#27 0x1090f8f20 in start_sim+0x9 (dyld_sim:x86_64+0x1f20)
    belozierov#28 0x1125f352d  (<unknown module>)

SUMMARY: AddressSanitizer: stack-buffer-overflow (libclang_rt.asan_iossim_dynamic.dylib:x86_64+0x1977c) in wrap_memcpy+0x16c
Shadow bytes around the buggy address:
  0x0001344ad380: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0001344ad390: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0001344ad3a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0001344ad3b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0001344ad3c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0001344ad3d0: 00 00 00 00 f1 f1 f1 f1[01]f3 f3 f3 00 00 00 00
  0x0001344ad3e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0001344ad3f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0001344ad400: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0001344ad410: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0001344ad420: 00 00 00 00 00 00 00 00 00 00 00 00 ca ca ca ca
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==39856==ABORTING
AddressSanitizer report breakpoint hit. Use 'thread info -s' to get extended information about the report.
(lldb)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants