Skip to content

Crash in Swift 6.1 when using async let inside a do {} block (Xcode 16.3+) #81771

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

Open
brightspread opened this issue May 26, 2025 · 7 comments
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. crash Bug: A crash, i.e., an abnormal termination of software triage needed This issue needs more specific labels

Comments

@brightspread
Copy link

Description

In Swift 6.1 (Xcode 16.3 and 16.4), placing async let bindings inside a do block causes a runtime crash—even when those values are properly awaited and the code is enclosed in do-catch. The crash does not occur in Xcode 16.2 or earlier.

This behavior seems to be related to how task deallocation is handled for async let when declared inside lexical scopes.

Reproduction

func crashingAsyncLetUsage() async {
  do {
    async let value1 = fetchValue1()
    async let value2 = fetchValue2()

    let result1 = try await value1()
    let result2 = try await value2()

    let model = buildModel(result1, result2)
    await consume(model)
  } catch {
    print("Caught error: \(error)")
  }
}

func safeAsyncLetUsage() async {
  async let value1 = fetchValue1()
  async let value2 = fetchValue2()

  do {
    let result1 = try await value1()
    let result2 = try await value2()

    let model = buildModel(result1, result2)
    await consume(model)
  } catch {
    print("Caught error: \(error)")
  }
}

Stack dump

Thread 12 Crashed:
0   libsystem_kernel.dylib        	       0x102628874 __pthread_kill + 8
1   libsystem_pthread.dylib       	       0x1015be2ec pthread_kill + 264
2   libsystem_c.dylib             	       0x180170568 __abort + 112
3   libsystem_c.dylib             	       0x1801704f8 abort + 116
4   libswift_Concurrency.dylib    	       0x2493a346c swift::swift_Concurrency_fatalErrorv(unsigned int, char const*, char*) + 28
5   libswift_Concurrency.dylib    	       0x2493a3488 swift::swift_Concurrency_fatalError(unsigned int, char const*, ...) + 28
6   libswift_Concurrency.dylib    	       0x2493a6718 swift_task_dealloc + 124
7   libswift_Concurrency.dylib    	       0x2493a27a4 asyncLet_finish_after_task_completion(swift::AsyncContext*, swift::AsyncLet*, void (swift::AsyncContext* swift_async_context) swiftasynccall*, swift::AsyncContext*, void*) + 72
8   Dev.debug.dylib               	       0x113f12529 implicit closure #6 in implicit closure #5 in implicit closure #4 in FooViewModel.sendAction<A>(_:) + 1
9   Dev.debug.dylib               	       0x113f1cddd partial apply for implicit closure #6 in implicit closure #5 in implicit closure #4 in FooViewModel.sendAction<A>(_:) + 1
10  Dev.debug.dylib               	       0x113ec8d6d thunk for @escaping @callee_guaranteed @async () -> () + 1
11  Dev.debug.dylib               	       0x113f1ccf9 partial apply for thunk for @escaping @callee_guaranteed @async () -> () + 1
12  Dev.debug.dylib               	       0x113f0f959 FooViewModel.sendAction<A>(_:) + 1 (FooViewModel.swift:)
13  Dev.debug.dylib               	       0x113f1b5c1 protocol witness for ActionCaster.sendAction<A>(_:) in conformance FooViewModel + 1
14  Dev.debug.dylib               	       0x113f0b10d closure #1 in FooViewModel.viewWillAppear(_:) + 1 (FooViewModel.swift:)
15  Dev.debug.dylib               	       0x113f0b2c5 partial apply for closure #1 in FooViewController.viewWillAppear(_:) + 1
16  Dev.debug.dylib               	       0x113e74cbd thunk for @escaping @isolated(any) @callee_guaranteed @async () -> (@out A) + 1
17  Dev.debug.dylib               	       0x113e74e29 partial apply for thunk for @escaping @isolated(any) @callee_guaranteed @async () -> (@out A) + 1
18  libswift_Concurrency.dylib    	       0x2493a5829 completeTaskWithClosure(swift::AsyncContext*, swift::SwiftError*) + 1

Expected behavior

async let declared inside a do block should behave the same as when declared outside it, as long as all values are awaited. It should not result in a crash.

Environment

swift-driver version: 1.120.5 Apple Swift version 6.1.2 (swiftlang-6.1.2.1.2 clang-1700.0.13.5)
Target: arm64-apple-macosx15.0

Additional information

This may be a regression introduced by recent changes to task deallocation logic in Swift 6.1. The crash appears related to how async let scopes are finalized when declared in lexical blocks like do {}.

@brightspread brightspread added bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. crash Bug: A crash, i.e., an abnormal termination of software triage needed This issue needs more specific labels labels May 26, 2025
@brightspread
Copy link
Author

I want to report an interesting finding regarding the crash described here.

If I use async let to concurrently fetch two values, then await them, and then use the results to map into an array of model objects (especially when assigning to a property that may be observed/@published), the process often crashes with “freed pointer was not the last allocation”.

However, if I wrap the entire async let/await block inside an inner closure and return a tuple (containing the array and other values), the crash no longer occurs—even though the logic and data are exactly the same.

Here’s a minimal reproduction:

// CRASHES:
async let foo = fetchFoo()
async let bar = fetchBar()
let fooResult = await foo
let barResult = await bar
let array = fooResult.items.map { ... use barResult ... }

// DOES NOT CRASH:
let (array, fooResult) = await {
    async let foo = fetchFoo()
    async let bar = fetchBar()
    let fooResult = await foo
    let barResult = await bar
    return (
        fooResult.items.map { ... use barResult ... },
        fooResult
    )
}()

I suspect this is due to a difference in how memory is managed across the async let stack frame and the closure’s context, but I hope this helps others reproduce the issue and clarify what is happening!

@jamieQ
Copy link
Contributor

jamieQ commented May 26, 2025

i'd be interested to see more info about the example, like the signatures of the involved types & functions. i tried a simplified reduction and could not reproduce the problem locally. additionally i'd be curious to know whether running the offending code with either the address or thread sanitizer enabled yields any additional information as to what could be going wrong.

@mikeash
Copy link
Contributor

mikeash commented May 27, 2025

Are you able to put together a complete example? (Reduced would be nice, but a large project that replicates the issue would be great too, if possible.) I tried adapting your code above but they don't crash when I add some stubs to make them build.

The basic issue here is that the generated code allocates memory on the async task stack, then deallocates that memory out of order. This can be caused by issues in the code such as throwing ObjC exceptions through async Swift code, but in this case it looks likely to be a compiler bug. The changes you mention that fix the issue probably convince the compiler to deallocate the memory in the proper order.

@brightspread
Copy link
Author

I ran both address sanitizer and thread sanitizer, but neither produced any relevant logs.
Also, I tried recreating the issue with a simplified version of the code in a separate sample project, but the crash does not occur there, so unfortunately, I can’t provide a minimal reproducible example at this time.

That said, I do agree that this is likely a compiler bug.
I did discover one pattern difference that seems to be related.
Here’s a simplified version of the problematic pattern:

async let fooResult = try? fooUseCase.fetchFoo()
let result = await fooResult

let list = filteredList.map { item in
    let foo = result?[item.key]
    return FooModel(
        key: item.key,
        value: foo?.value ?? 0
    )
}

await reduce(.setData(list))

This pattern seems to cause a crash when the async let value is awaited, and then its result is used inside a map transformation.
However, I found that refactoring the code like this avoids the crash:

let list = **await** {
    async let fooResult = try? fooUseCase.fetchFoo()
    let result = await fooResult

    return filteredList.map { item in
        let foo = result?[item.key]
        return FooModel(
            key: item.key,
            value: foo?.value ?? 0
        )
    }
}()

By moving the async let inside an await { ... } block, the crash no longer occurs.

This reinforces the idea that the original crash is caused by the compiler generating incorrect deallocation logic for async let when used outside a tightly-scoped await block.

Let me know if there’s anything else I can provide to help investigate further.

@mikeash
Copy link
Contributor

mikeash commented May 29, 2025

Yeah, if it's what I suspect, then the issue is that the compiler-emitted calls should be:

A = alloc()
B = alloc()
dealloc(B)
dealloc(A)

But are actually:

A = alloc()
B = alloc()
dealloc(A)
dealloc(B)

Sanitizers won't help with that, since everything is fine in terms of basic memory access and thread safety.

Are you able to provide the full project? A non-minimal reproducer would be helpful too. Otherwise, I'll keep an eye on this and see if I can figure anything out.

The good news is that this should be consistent in any given binary. If you run down this particular code path and it doesn't encounter this error once, then you know it's doing things in the right order and is solid, so you can have confidence in your workarounds at least.

@brightspread
Copy link
Author

This issue doesn’t always reproduce consistently, even within the same project — it seems to depend on the specific screen or code path.
Unfortunately, I’m unable to share the full project as it contains confidential company information.
Thank you for your insights regarding the workaround.
If I’m able to create a minimal reproducer, I’ll be sure to share it with you right away.
Thanks again!

@mikeash
Copy link
Contributor

mikeash commented May 30, 2025

Understood, often these things can't be shared. Hopefully we can track it down one way or another. I appreciate all the info so far.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. crash Bug: A crash, i.e., an abnormal termination of software triage needed This issue needs more specific labels
Projects
None yet
Development

No branches or pull requests

3 participants