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.