Files
swift-mirror/test/IRGen/async/task_dealloc_memory_effects.sil
Anthony Latsis fd1b162534 [IRGen] Include argument memory in swift_task_dealloc's memory effects
```
*** Program crashed: Bad pointer dereference at 0xcdcdcdcdcdcdcdd5 ***
Thread 1 crashed:
 0                  0x00000001049aa564 TaskGroup.next(isolation:) + 40 in libswift_Concurrency.dylib
 1 [async]          0x00000001049a9958 TaskGroup.awaitAllRemainingTasks() in libswift_Concurrency.dylib
 2 [async]          0x00000001049a9bd0 withTaskGroup<A, B>(of:returning:isolation:body:) in libswift_Concurrency.dylib
 3 [async]          withTaskGroup<A, B>(of:returning:isolation:body:) in a.out
Registers:
 x22 0xcdcdcdcdcdcdcdcd   (StackAllocator::magicUninitialized)
 x10 0xdeadbeafdeadbeaf   (StackAllocator::magicEndOfAllocation)
  pc 0x00000001049aa564  c0 06 40 f9  -> ldr x0, [x22, #8]
  lr 0x00000001049aa560  d6 02 40 f9  -> ldr x22, [x22]
```

8b07a6f311 changed swift_task_alloc/swift_task_dealloc from
`memory(argmem: readwrite)` to `memory(inaccessiblemem: readwrite)` so
that they can no longer be reordered with respect to one another. For
the deallocation entry points that is unsound: deallocating invalidates
the storage the argument points to, so it has to be modelled as a write
to argument memory as well -- this is exactly why LLVM gives `free`
`memory(argmem: readwrite, inaccessiblemem: readwrite)`. Advertising
inaccessible memory only lets any pass move accesses to the deallocated
buffer across the call. Concretely, InstCombine's
`isSafeAndProfitableToSinkLoad` treats a call that
`onlyAccessesInaccessibleMemory()` as not blocking a sink, so
`foldPHIArgLoadIntoPHI` sank the reload of `calleeContext->Parent` in
the two-suspend back-deployment thunk of `TaskGroup.next(isolation:)`
below `swift_task_dealloc(calleeContext)`. The resulting funclet reads
the freed async context, which an asserts-build task allocator has
already poisoned with `magicUninitialized`, and the return branches
through 0xcdcdcdcdcdcdcdcd.

swift_task_alloc keeps `memory(inaccessiblemem: readwrite)`: its only
argument is a size, so argument memory is vacuous there, and the storage
it returns cannot alias anything already in flight.

Assisted-by: Claude Code
2026-08-07 12:13:23 +01:00

29 lines
1.0 KiB
Plaintext

// RUN: %target-swift-frontend -primary-file %s -emit-irgen -sil-verify-all | %FileCheck %s
// REQUIRES: concurrency
// Deallocating invalidates the storage the argument points to, so the memory
// effects of the swift_task_dealloc entry points must cover argument memory
// (this is also how LLVM models `free`). Advertising inaccessible memory only
// lets LLVM move accesses to the deallocated async context across the call;
// InstCombine's phi-of-load fold in particular does exactly that.
sil_stage canonical
import Builtin
import Swift
import _Concurrency
sil hidden_external @callee : $@convention(thin) @async () -> ()
sil hidden @caller : $@convention(thin) @async () -> () {
bb0:
%0 = function_ref @callee : $@convention(thin) @async () -> ()
%1 = apply %0() : $@convention(thin) @async () -> ()
%2 = tuple ()
return %2 : $()
}
// CHECK: declare{{( dllimport)?}} swiftcc void @swift_task_dealloc(ptr) #[[DEALLOC_ATTRS:[0-9]+]]
// CHECK: attributes #[[DEALLOC_ATTRS]] = { nounwind memory(argmem: readwrite, inaccessiblemem: readwrite) }