Skip to content

Avoid strong reference reader count overflow - #131

Open
fallintoplace wants to merge 1 commit into
apple:mainfrom
fallintoplace:fix-strong-reference-reader-overflow
Open

Avoid strong reference reader count overflow#131
fallintoplace wants to merge 1 commit into
apple:mainfrom
fallintoplace:fix-strong-reference-reader-overflow

Conversation

@fallintoplace

Copy link
Copy Markdown

Summary

  • Prevent atomic strong-reference reader counts from wrapping at capacity.
  • Replace masking/assert-only reader count encoding with checked preconditions.
  • Add a checked reader increment path that returns nil when the reader counter is full, causing _startLoading to reload and retry instead of publishing a wrapped count.
  • Add a focused regression test using a tiny injected reader mask through an underscored testing SPI.

Why

The strong-reference storage reserves a small reader-count field inside DoubleWord and previously incremented it with wrapping arithmetic. If enough readers entered concurrently, the count could wrap and make the storage appear to have fewer active readers than it actually did.

Testing

  • swift build passes locally.
  • swift test --filter StrongReferenceRace.testReaderCountDoesNotWrapAtCapacity could not run locally because this Swift toolchain cannot import XCTest.

@lorentey lorentey left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! Interesting. Have you actually hit this overflow issue in practice?

The maximum reader count is 255 on platforms with 32-bit pointers, and 65535 on 64-bit platforms. We'd need hundreds/tens of thousands of distinct threads all trying to concurrently load the same reference to end up overflowing this counter.

I believe that would be difficult to impossible to achieve in practice -- for one thing, platforms typically have a lower system-wide maximum thread count. But, granted, this is an assumption that isn't checked anywhere; and it would make sense to enforce it. I believe the right move is for the code to trap on congestion overflow! We never expect that many readers to hammer the same reference all at once, so a runtime trap would be the right response if it does somehow happen.

Comment on lines +392 to +396
@_spi(Testing)
public static func _testStartLoadingReaderCounts(
for value: Value,
readerMask: UInt
) -> (counts: [Int], overflowed: Bool) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not add test code to the public module. If we follow my recommendation that congestion overflow should result in a trap, then we (currently) have no good way to test that -- so the new test need to be removed, or adjusted to only exercise _startLoading up to, but not exceeding, the maximum reader count. (If you go with adjusting the test, move this code directly into a test method -- there is no need to define a utility function for it. Get rid of the configurable readerMask; use the real _readerMask.)

}

private static func _finishLoading(
fileprivate static func _finishLoading(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fileprivate static func _finishLoading(
package static func _finishLoading(

Comment on lines +126 to +138

@inline(__always)
fileprivate func _incrementingReaders(
readerMask: UInt = Self._readersMask
) -> Self? {
precondition(readerMask <= Self._readersMask)
let readers = UInt(bitPattern: _readers)
guard readers < readerMask else { return nil }
return DoubleWord(
_raw: _raw,
readers: Int(readers + 1),
version: _version)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no real need for this; dw._readers += 1 will suffice.

Suggested change
@inline(__always)
fileprivate func _incrementingReaders(
readerMask: UInt = Self._readersMask
) -> Self? {
precondition(readerMask <= Self._readersMask)
let readers = UInt(bitPattern: _readers)
guard readers < readerMask else { return nil }
return DoubleWord(
_raw: _raw,
readers: Int(readers + 1),
version: _version)
}

let r = UInt(bitPattern: readers) & Self._readersMask
assert(r == readers)
let r = UInt(bitPattern: readers)
precondition(r <= Self._readersMask)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a message to explain what happened:

Suggested change
precondition(r <= Self._readersMask)
precondition(r <= Self._readersMask, "Congestion overflow while loading an atomic reference")

let n = UInt(bitPattern: newValue) & Self._readersMask
assert(n == newValue)
let n = UInt(bitPattern: newValue)
precondition(n <= Self._readersMask)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
precondition(n <= Self._readersMask)
precondition(n <= Self._readersMask, "Congestion overflow while loading an atomic reference")

Comment on lines 178 to 216
@@ -166,17 +179,30 @@ internal struct _AtomicReferenceStorage {
from pointer: UnsafeMutablePointer<Self>,
hint: DoubleWord? = nil
) -> DoubleWord {
var hint = hint
while true {
if let result = _tryStartLoading(from: pointer, hint: hint) {
return result
}
hint = nil
}
}

fileprivate static func _tryStartLoading(
from pointer: UnsafeMutablePointer<Self>,
hint: DoubleWord? = nil,
readerMask: UInt = DoubleWord._readersMask
) -> DoubleWord? {
var old = hint ?? Storage.atomicLoad(at: pointer._extract, ordering: .relaxed)
if old._raw == nil {
atomicMemoryFence(ordering: .acquiring)
return old
}
// Increment reader count
while true {
let new = DoubleWord(
_raw: old._raw,
readers: old._readers &+ 1,
version: old._version)
guard let new = old._incrementingReaders(readerMask: readerMask) else {
return nil
}
var done: Bool
(done, old) = Storage.atomicWeakCompareExchange(
expected: old,
@@ -189,7 +215,7 @@ internal struct _AtomicReferenceStorage {
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
package static func _startLoading(
from pointer: UnsafeMutablePointer<Self>,
hint: DoubleWord? = nil
) -> DoubleWord {
var old = hint ?? Storage.atomicLoad(at: pointer._extract, ordering: .relaxed)
if old._raw == nil {
atomicMemoryFence(ordering: .acquiring)
return old
}
// Increment reader count
while true {
let new = DoubleWord(
_raw: old._raw,
readers: old._readers &+ 1,
version: old._version)
var done: Bool
(done, old) = Storage.atomicWeakCompareExchange(
expected: old,
desired: new,
at: pointer._extract,
successOrdering: .acquiring,
failureOrdering: .acquiring)
if done { return new }
if old._raw == nil { return old }
}
}

@fallintoplace
fallintoplace force-pushed the fix-strong-reference-reader-overflow branch from 4285a49 to ed49515 Compare August 12, 2026 10:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants