The redundant load elimination pass may incorrectly remove a load even though a function call with side effects occurs immediately beforehand

Hi folks!

My colleague showed me a runtime crash that, with the right conditions, turned out to be triggered by a use-after-free problem. The minimal reproducible code is below and must be compiled at the -Osize optimization level. Enabling the address sanitizer immediately reveals that the crash results from use-after-free:

let storage = ValueStorage()
storage.append(1)
// Crash at runtime!
storage.append(2)
public class ValueStorage {
    
    private class Data {
    
        var values: [Int] = []
    
    }
    
    private var data = Data()
    
    private func withAutoreleasingUnsafeMutableData<R>(_ body: (_ dataPtr: AutoreleasingUnsafeMutablePointer<Data>) throws -> R) rethrows -> R {
        try withUnsafeMutablePointer(to: &data) { pointer in
            try body(AutoreleasingUnsafeMutablePointer(pointer))
        }
    }
    
    public func append(_ value: Int) {
        withAutoreleasingUnsafeMutableData { dataPtr in
            // Immediately crashed line
            dataPtr.pointee.values.append(value)
        }
    }
    
}

Replacing AutoreleasingUnsafeMutablePointer with UnsafeMutablePointer eliminates the issue.

I investigated this issue for a while and believe that this is caused by a mistake in the "redundant load elimination" pass of the latest SIL optimizer rewritten in Swift. The latest version of this pass employs a "backward" instruction scanning algorithm instead of the "forward" one which employed by the original C++ implementation. It reversely analysis the instructions before the load instruction tried to eliminate. If function applications with side-effects to the address represented by the operand of the load instruction were detected in this process, the load elimination stopped. This is what alias analysis does.

However, the alias analysis can only use the side-effects of a function application when the operand of the load instruction is escaping. In the latest implementation of the Swift SIL optimizer, when the compiler analyzing whether the operand of the load instruction is escaping, it walks up the use-def chain of the operand to check the whether there is a def location in the middle or at the end is escaping.

When the aforementioned code was compiled, the AutoreleasingUnsafeMutablePointer<Pointee>.pointee employs unchecked_ref_cast instruction to cast an Optional <Unmanaged<AnyObject>> type into Pointee type.

This unchecked_ref_cast instruction lies in the middle of the load's operand's use-def chain and involves an implicit optional unwrapping. This breaks the walk-up implementation in ValueUseDefWalker.

I had a fix solution for this issue and posted the details in my blog: https://wezzard.com/post/2025/03/when-the-swift-compiler-deleted-code-in-stdlib-9067

Issue: Potential use-after-free when modifying heap data get from AutoreleasingUnsafeMutablePointer · Issue #79871 · swiftlang/swift · GitHub
Pull request: [SILOptimizer] Fixes the ValueDefUseWalker to handle potential unchecked_ref_cast between optional and non-optional class types. by WeZZard · Pull Request #79872 · swiftlang/swift · GitHub

I’d love to hear from compiler experts here who can take a look at my fix and share any advice or feedback. Thank you so much for your time and help—looking forward to your insights!

Thank you folks.

5 Likes