Skip to content

[stdlib] Make sure _SwiftNewtypeWrapper hashes the same way as its RawValue #15990

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 17, 2018

Conversation

lorentey
Copy link
Member

_SwiftNewtypeWrapper forwarded hashValue to its rawValue, but it failed to do the same for _hash(into:), which resulted in the wrapper struct having subtly different hashing than the raw value.

This violated an implicit invariant when dictionaries/sets of such types were bridged to Objective-C through AnyHashable, leading to nondeterministic but frequent crashes.

rdar://problem/39398060

…wValue

_SwiftNewtypeWrapper forwarded hashValue to its rawValue, but it failed to do the same for _hash(into:), which resulted in the wrapper struct having subtly different hashing than the raw value.

This violated an implicit invariant when dictionaries/sets of such types were bridged to Objective-C through AnyHashable, leading to nondeterministic but frequent crashes.

rdar://problem/39398060
@lorentey
Copy link
Member Author

@swift-ci please smoke test and merge

@lorentey
Copy link
Member Author

(Tests that will cover this scenario are being developed in #15939.)


@inlinable // FIXME(sil-serialize-all)
public func _hash(into hasher: inout _Hasher) {
hasher.append(rawValue)
Copy link
Member

Choose a reason for hiding this comment

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

Nice catch. Is this something that users are likely to run into, or is it because of the special relationship between newtype'd imports and bridging?

Copy link
Member Author

Choose a reason for hiding this comment

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

The crash was caused by a particularly weird interplay between _hash(into:), newtype, AnyHashable and bridging.

Luckily, I don't see how it could apply to user code. For example, struct Foo below produces the same hashValue as its label, but it doesn't hash the same way through _hash(into:). This isn't an issue, though, because the only way to put them in the same hash table is through AnyHashable, and since they don't have the same AnyHashable representation, they wouldn't compare equal even if they had matching hashes:

struct Foo: Hashable {
  var label: String
  var hashValue: Int { return label.hashValue }
  static func ==(left: Foo, right: Foo) -> Bool { return left.label == right.label }
}

Foo(label: "foo").hashValue == "foo".hashValue // ==> true
(Foo(label: "foo") as AnyHashable) == ("foo" as AnyHashable) // ==> false
let s: Set<AnyHashable> = [Foo(label: "foo"), "foo"]
s.count // ==> 2

The only visible effect seems to be that Dictionary/Set ordering is different for Foo and String, but hash table ordering is explicitly unstable, so code that depends on that is already broken.

Copy link
Member

Choose a reason for hiding this comment

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

Okay. Imported newtypes are specifically weird because we need hashing consistency for the newtype, the type it wraps, and potentially the bridged type (SomeNewType/String/NSString), so yeah--unlikely to affect anyone else.

@swift-ci swift-ci merged commit 076ef91 into swiftlang:master Apr 17, 2018
@lorentey lorentey deleted the newtype-vs-anyhashable branch April 17, 2018 23:45
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.

3 participants