Swift 5.5 has serious stack corruption bugs!

5 min read Original article ↗

hi all, I’ve discovered several stack corruption bugs related to async/await which can be reproduced in simple test programs compiled with recent nightly toolchains. i have confirmed that two three four of these bugs are present in the 5.5-RELEASE toolchain.


  1. stack corruption when using values returned by async closures

    the return values of async closures are clobbered at a chaotic, but deterministic memory offset.

    present in 5.5-RELEASE? present in debug builds? present in release builds?
    yes no yes

    reproduction:

// async-return-value-corruption.swift

@main 
enum Main 
{
    actor A
    {
        init() 
        {
        }
        
        func a(_ f:@Sendable () async -> (Int, (Int, Int, Int, Int))?) 
            async -> Void
        {
            guard let (head, tail):(Int, (Int, Int, Int, Int)) = await f()
            else 
            {
                return 
            }
            
            print((head, tail))
            return 
        }
    }
    
    static 
    func p() async -> Bool
    {
        true 
    }
    
    static 
    func main() async
    {
        while true 
        {
            let a:A     = .init()
            
            async let task:Void = a.a
            {
                if await Self.p()
                {
                    return (0, (0, 0, 0 ,0)) 
                }
                else 
                {
                    return nil 
                }
            }
            await task
        }
    }
}
$ swiftc --version
Swift version 5.5 (swift-5.5-RELEASE)
Target: x86_64-unknown-linux-gnu

$ swiftc -parse-as-library -O async-return-value-corruption.swift
$ ./async-return-value-corruption 

(139787763716080, (0, 0, 0, 0)) 
(139787629498352, (0, 0, 0, 0)) 
(139787965042672, (0, 0, 0, 0)) 
(139787763716080, (0, 0, 0, 0)) 
(139787629498352, (0, 0, 0, 0))
...

  1. segmentation faults when using async let

    relatively simple usages of async let suffer from segmentation faults, on both debug, and release builds. while i originally believed this was limited to usage of async let in the main function, i have since also observed this issue in a variety of other contexts.

    present in 5.5-RELEASE? present in debug builds? present in release builds?
    yes yes yes

    reproduction:

// async-let-segfault.swift 
@main 
enum Main 
{
    static 
    func foo() async -> [Void]
    {
        try? await Task.sleep(nanoseconds: 1)
        return []
    }
    static 
    func main() async
    {
        async let task:Void = 
        {
            () async -> () in 
            try? await Task.sleep(nanoseconds: 1)
        }()
        while true 
        {
            let _:[Void] = await Self.foo()
        }
    }
}
$ swiftc --version
Swift version 5.5 (swift-5.5-RELEASE)
Target: x86_64-unknown-linux-gnu

$ swiftc -O -parse-as-library async-let-segfault.swift 
async-let-segfault.swift:23:15: warning: will never be executed
        await task
              ^
async-let-segfault.swift:19:15: note: condition always evaluates to true
        while true 
              ^
$ ./async-let-segfault 
Segmentation fault (core dumped)


  1. stack corruption when passing enums to actor-isolated methods

    the enum values received by an actor-isolated method are not the same values that were passed by the caller. i have observed this issue in both debug and release builds, but is vastly more common and reproducible in debug builds. thankfully, it does not affect the 5.5-RELEASE toolchain, but all of the recent nightlies, including DEVELOPMENT-SNAPSHOT-2021-09-23-a, are affected.

    (update) after further testing, i’ve found that the 5.5-RELEASE toolchain is indeed affected by this bug. it occurs in both debug and release builds. see this follow-up for details.

    present in 5.5-RELEASE? present in debug builds? present in release builds?
    no yes yes sometimes yes

    reproduction:

// async-stack-corruption.swift 

struct Users
{
    enum Access 
    {
        case guest
        case admin(Int)
        case developer(Int, Int, Int, Int)
    }
    actor State  
    {
        init()
        {
        }
        func set(permissions:(user:Int, access:Access?)) 
        {
            print(permissions)
        }
    }
    
    let state:State = .init()
    
    func set(permissions:(user:Int, access:Access?)) async 
    {
        await self.state.set(permissions: permissions)
    }
}
@main 
enum Main 
{
    static 
    func main() async
    {
        let users:Users             = .init()
        let stream:AsyncStream<Int> = .init 
        {
            for i in 0 ..< 10
            {
                $0.yield(i) 
            }
            $0.finish()
        }
        for await i:Int in stream 
        {
            await users.set(permissions: (i, .guest))
        }
    }
}
$ swiftc --version
Swift version 5.6-dev (LLVM ae102eaadf2d38c, Swift be2d00b32742678)
Target: x86_64-unknown-linux-gnu
$ swiftc -parse-as-library async-stack-corruption.swift 
$ ./async-stack-corruption 
(user: 0, access: Optional(main.Users.Access.admin(0)))
(user: 1, access: Optional(main.Users.Access.admin(0)))
(user: 2, access: Optional(main.Users.Access.admin(0)))
(user: 3, access: Optional(main.Users.Access.admin(0)))
(user: 4, access: Optional(main.Users.Access.admin(0)))
(user: 5, access: Optional(main.Users.Access.admin(0)))
(user: 6, access: Optional(main.Users.Access.admin(0)))
(user: 7, access: Optional(main.Users.Access.admin(0)))
(user: 8, access: Optional(main.Users.Access.admin(0)))
(user: 9, access: Optional(main.Users.Access.admin(0)))

i have encountered additional memory corruption bugs, including some weirdness with calling instance methods on actor-isolated properties (thread), but these 3 are the issues i had time to isolate and reproduce this week. i have filed them as:

  1. SR-15225
  2. SR-15241
  3. SR-15240

in my view, bug (1) is by far the most dangerous, as it occurs silently, and affects the 5.5 release toolchain. like bug (3), bug (1) also represents a potential security vulnerability, though it is probably not easily exploitable in naturally occurring code.

(update) now that i’ve found that bug (3) does occur in binaries built with the 5.5 release toolchain, i am now even more concerned about bug (3) than i was about bug (1). because both of them turn into segfaults depending on minor code changes, it is possible that bug (2), which i have so far only been able to reproduce as a segfault, could also manifest as a silent vulnerability.

i would advise that people using concurrency features should not ship anything compiled with the 5.5 toolchain, until these issues are fixed and a patch is released.


(update) it looks like there is yet another issue with task groups and SIMD types with wide alignments. see this followup. i have confirmed this issue affects the 5.5-RELEASE toolchain as well. i was able to reproduce it on both debug and release builds.

  1. present in 5.5-RELEASE? present in debug builds? present in release builds?
    yes yes yes

    reproduction:

@main
enum Main
{
    static
    func main() async
    {
        await withTaskGroup(of: SIMD4<Int32>.self) 
        { 
            (group:inout TaskGroup) in
            group.addTask 
            {
                return SIMD4<Int32>.init(repeating: 0)
            }
        }
    }
}
$ swiftc --version
Swift version 5.5 (swift-5.5-RELEASE)
Target: x86_64-unknown-linux-gnu
$ swiftc -O -parse-as-library async-stack-corruption-simd.swift 
$ ./async-stack-corruption-simd 
Segmentation fault (core dumped)