AccessEnforcementOpts leaves a merged access scope open on a loop's continue edge: false exclusivity trap at -O · Issue #93411 · swiftlang/swift · GitHub
You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
AccessEnforcementOpts leaves a merged access scope open on a loop's continue edge: false exclusivity trap at -O #93411
With -O, AccessEnforcementOpts merges two dynamic [modify] accesses to the same stored property into one scope that spans a guard ... else { continue }. The continue edge leaves the loop body without an end_access. On the next iteration, the next access to that property traps with a false "Simultaneous accesses" exclusivity violation.
The program has no overlapping accesses. It runs correctly at -Onone, and at -O when the pass is disabled with -Xllvm -sil-disable-pass=access-enforcement-opts.
The optional closure call in Clock.tick() matters: it makes the accesses tracked ([dynamic] without [no_nested_conflict]), so the runtime records the open scope. The x counter matters too: without it, the loop runs once to a break and no trap occurs.
Reproduction
// repro.swift
structClock{varcycles=0varhook:(()->Void)?mutatingfunc tick(){
cycles +=1hook?()}}finalclassMachine{varclock=Clock()vartaken=[false,true,false,true,false,false]varnext=0varok=false@inline(never)func pick(){
clock.tick()
ok = !taken[next % taken.count]
next +=1}func run(){varx=2whiletrue{pick()
clock.tick()guard ok else{continue} // taken: try again
clock.tick()
x -=1if x ==0{break}}}}letm=Machine()
m.run()print("ok", m.clock.cycles)
$ swiftc -Onone repro.swift -o repro && ./repro
ok 8
$ swiftc -O -Xllvm -sil-disable-pass=access-enforcement-opts repro.swift -o repro && ./repro
ok 8
$ swiftc -O repro.swift -o repro && ./repro
Simultaneous accesses to 0x100c7d4a0, but modification requires exclusive access.
Previous access (a modification) started at repro`Machine.run() + 192 (0x100c48be0).
Current access (a modification) started at:
0 libswiftCore.dylib 0x00000001ae8eb69c _swift_reportExclusivityConflict + 332
1 libswiftCore.dylib 0x00000001ae882f34 swift_beginAccess + 192
2 repro 0x0000000100c49058 Machine.pick() + 44
3 repro 0x0000000100c48b20 Machine.run() + 172
4 repro 0x0000000100c489d0 main + 76
5 dyld 0x0000000199f3e460 start + 6688
Fatal access conflict detected.
The optimized SIL of Machine.run() (swiftc -O -emit-sil repro.swift, trimmed) shows the scope %14, which covers both clock.tick() calls. It is ended in bb8, but the guard's failure edge bb4 -> bb11 -> bb1 branches back to the loop header with %14 still open. There, pick() begins another [modify] access to clock:
The program prints ok 8 at -O, as it does at -Onone. A merged access scope must end on every path that leaves it, or the two accesses must not be merged when the second does not post-dominate the end of the first.
Environment
swift-driver version: 1.168.6 Apple Swift version 6.4 (swiftlang-6.4.0.34.1 clang-2100.3.34.1)
Target: arm64-apple-macosx27.0.0
Xcode 27.1 (27A9275), macOS 27.0.1 (26A434)
Additional information
Found in a release build of a larger app. In a loop of the same shape (retry with continue until a random cell is free, then count down), the app trapped in the first call after the continue. Disabling the pass for that module, with -Xllvm -sil-disable-pass=access-enforcement-opts, is the workaround in use; exclusivity checking stays enabled.
Possibly related, but with a different trigger (fixed by #83999): #83924.
Activity