swift-collections: OrderedSet.intersection crashes due to out of bounds index

If have two OrderedSets of what are essentially UUIDs (wrapped in Tagged). Finding the intersection from setA works.

setA.intersection(setB)

But taking the two, same, sets and flipping the order causes an out of bounds crash in RandomAccessCollection+Offsets.swift#28.

setB.intersection(setA)

The items in the sets are 1389 vs. 1388 if that could somehow matter.

Here’s a stack trace if it helps:

#0	0x000000018f8d9a3c in _swift_runtime_on_report ()
#1	0x000000018f949530 in _swift_stdlib_reportFatalErrorInFile ()
#2	0x000000018f64b5d8 in closure #1 in closure #1 in closure #1 in _assertionFailure(_:_:file:line:flags:) ()
#3	0x000000018f64b37c in closure #1 in closure #1 in _assertionFailure(_:_:file:line:flags:) ()
#4	0x000000018f64ad2c in _assertionFailure(_:_:file:line:flags:) ()
#5	0x000000018f64aff0 in _fatalErrorMessage(_:_:file:line:flags:) ()
#6	0x000000018f848f8c in UnsafeBufferPointer.subscript.read ()
#7	0x000000018f848dfc in protocol witness for Collection.subscript.read in conformance UnsafeBufferPointer<τ_0_0> ()
#8	0x00000001046bf4c0 in RandomAccessCollection.subscript.getter at swift-collections/Sources/OrderedCollections/Utilities/RandomAccessCollection+Offsets.swift:28
#9	0x000000010467d904 in _HashTable.UnsafeHandle._find<τ_0_0>(_:in:) at swift-collections/Sources/OrderedCollections/HashTable/_HashTable+UnsafeHandle.swift:299
#10	0x00000001046bd5e4 in closure #1 in closure #1 in OrderedSet._find_inlined(_:) at swift-collections/Sources/OrderedCollections/OrderedSet/OrderedSet.swift:396
#11	0x00000001046bd648 in thunk for @callee_guaranteed (@unowned _HashTable.UnsafeHandle) -> (@unowned Int?, @unowned _HashTable.Bucket, @error @owned Error) ()
#12	0x00000001046bf154 in partial apply for thunk for @callee_guaranteed (@unowned _HashTable.UnsafeHandle) -> (@unowned Int?, @unowned _HashTable.Bucket, @error @owned Error) ()
#13	0x00000001046837d8 in closure #1 in _HashTable.read<τ_0_0>(_:) at swift-collections/Sources/OrderedCollections/HashTable/_HashTable.swift:151
#14	0x0000000104683ec0 in partial apply for closure #1 in _HashTable.read<τ_0_0>(_:) ()
#15	0x000000018f730e18 in ManagedBuffer.withUnsafeMutablePointers<τ_0_0>(_:) ()
#16	0x00000001046836fc in _HashTable.read<τ_0_0>(_:) at swift-collections/Sources/OrderedCollections/HashTable/_HashTable.swift:149
#17	0x00000001046bd454 in closure #1 in OrderedSet._find_inlined(_:) at swift-collections/Sources/OrderedCollections/OrderedSet/OrderedSet.swift:395
#18	0x00000001046bd6e0 in thunk for @callee_guaranteed (@unowned UnsafeBufferPointer<τ_0_0>) -> (@unowned Int?, @unowned _HashTable.Bucket, @error @owned Error) ()
#19	0x00000001046bebe8 in partial apply for thunk for @callee_guaranteed (@unowned UnsafeBufferPointer<τ_0_0>) -> (@unowned Int?, @unowned _HashTable.Bucket, @error @owned Error) ()
#20	0x000000018f62ef9c in ContiguousArray.withUnsafeBufferPointer<τ_0_0>(_:) ()
#21	0x00000001046bd180 in OrderedSet._find_inlined(_:) atswift-collections/Sources/OrderedCollections/OrderedSet/OrderedSet.swift:391
#22	0x00000001046ab544 in OrderedSet.contains(_:) at swift-collections/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra+Basics.swift:47
#23	0x00000001046abdb8 in OrderedSet.intersection(_:) at swift-collections/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra+Operations.swift:164

Information

  • Package version: version 0.0.7?
  • Platform version: iOS 15, iPhone 12 Pro simulator.
  • Swift version: I assume Swift 5.5 (Xcode 13 beta 5)

Checklist

  • If possible, I’ve reproduced the issue using the main branch of this package.
  • I’ve searched for existing GitHub issues.

Did not attempt to reproduce on main as swift-collections is a sub-dependency on one of my dependencies. But could absolutely try if requested. Getting late where I am 😅

Steps to Reproduce

I have not been able to reproduce this issue unfortunately. I’m doing this a on a few different pairs of sets, and only one of them with this specific type (Tagged<Product, UUID> — product being a custom struct) causes the issue. The other sets are also Tagged<X, UUID>.

Happy to attempt to provide more attempts to reproduce. The inner workings of all this is a bit over my head though.

Expected behavior

I expect the “call order” to not matter.

Actual behavior

Out of bounds crash.

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 1
  • Comments: 19 (10 by maintainers)

Most upvoted comments

This turned out to be caused by a missing uniqueness check in a couple of OrderedSet mutation operations, leading to a violation of value semantics.

Embarrassingly, this wasn’t caught by the existing tests that check value semantics, because they only checked that iteration over old snapshots still provided consistent results, not that those snapshots were fully self-consistent. D’oh!

The fix is #123; this will ship in a new release very soon.

Big shoutout to @KaiOelfke for providing me with a reproducer!

In a small learning project I can reproduce this or a similar crash in a 100% reliable manner. I cloned the swift-collections repo and added it all into a xcworkspace as recommended by @lorentey.

Calling state.cards._dictionary._checkInvariants() directly after state.cards.insert(card, at: 0) succeeds. But the next SwiftUI view update crashes. Calling po self.cards._dictionary._checkInvariants() immediately before the crash results in a precondition failure.

OrderedCollections/OrderedSet+Invariants.swift:28: Precondition failed: Index 0 not found in hash table (element: FA9A6EA9-7E4C-40B2-A8C0-4D3ACC5660E0)
2021-11-07 16:55:16.885554+0100 SwipePreview[13530:5205563] OrderedCollections/OrderedSet+Invariants.swift:28: Precondition failed: Index 0 not found in hash table (element: FA9A6EA9-7E4C-40B2-A8C0-4D3ACC5660E0)
error: Execution was interrupted, reason: EXC_BREAKPOINT (code=1, subcode=0x18f786a00).

After the insert there’s 30 items. However in the view update the inserted item is not present anymore. There’s only 29 items.

Thanks for taking a look!

  1. Yep, the literal type, as described by the debugger is Tagged<AppModels.Product, Foundation.UUID>. Tagged conforms to Hashable whenever it’s RawValue does, and the implementation is synthesized.
  2. I do not, knowingly, do any async or concurrent work. I tend to do everything on the main queue until I see a need to do anything else. The code is running within a Composable Architecture Effect which is essentially a Combine publisher. I’ve added a debounce on the main queue, crash still happens. I can’t rule out reads though. The struct that holds the set is shared app state, but there are no mutations happening anywhere else.
  3. The set is actually the keys property of an OrderedDictionary. The OrderedDictionary or the type that wraps it is long lived within my code. It sees one big insertion of the 1300 items when they are loaded from the database. Then one single insertion that triggers the crashing code when I diff the new set with a copy of the previous version. The big insertion from the database is manually dispatched to the main queue. There are no removals. The IdentifiedArray is initialized using this initializer which seems to call out to this initializer to create its internal OrderedDictionary.

Ran the app with Adress Sanitizer turned on (and “Detect use of stack after return”). Never done that before, but didn’t see any warnings in the console or in Xcode.

Tried running with the build conditions. Not sure if I did it right though. image image


Sorry if this is not of much help. Let me know if there’s anything else I can do to attempt to pin-point the issue. I understand it’s very likely that the cause is something I’m doing or any of the layers between me and the corrupted set.

Hi folks, I’ve been seeing a similar issue via the IdentifiedArray type that wraps an OrderedDictionary and the crash log looks really similar as well, with 21 of 23 lines here being the same: https://github.com/pointfreeco/swift-identified-collections/issues/26#issuecomment-943193186

Poking around in the stacktrace a bit and it seems like at some point _UnsafeHashTable._find(_:in:) tries to subscript the elements at an _offset of 31, while the ordered dictionary in question has 31 elements. Interestingly in my case, I can only reproduce this crash with this particular set of data. This is for an app where I’m adding a message to a list, and only this one particular list produces this crash; any time I add a message here it crashes, adding a message to any other thread does not. This is with an n of ~5, so presumably there are other crashing values too; my point here is for some reason only particular bits of data cause the crash.

I’m trying to make a small reproducible test case, which is made a bit difficult by the fact that pretty much the same code does not crash in another code path; i’m adding the reply to the list after it’s accepted by the server. When I relaunch the app and attempt to sync again, it sends back an array with the same exact value, and this time the code does not crash, which has me a bit befuddled. Adding a fresh reply after this initial merge however does cause the same crash, and so I’m not really sure what’s happening here.

I realise this probably isn’t very helpful without a reproducible sample but until I can have that, please do let me know if I can help in any other way.

I have been trying to reproduce this in a smaller sample project to no avail. It is still 100% reproducible in my main project, even using version 1.0.1 of Collections. I’d be willing to send you the code for my entire app through some private means and walk you through how to trigger the issue if you think that’d help!

I have not forgotten about this. Just a little tied up at the moment, as I mentioned.

The instance of Collections that I’m using is a dependency, to a dependency, to a dependency at the moment. If I add my own dependency on Collections in my Package.json, will that version be used by my dependencies as well? Not if it’s pinned to another version, right?

(0.0.7 has the same OrderedSet implementation as main – there is no reason to spend effort on reproducing this there.)