Possible Bug in Swift 5.6 stdlib: Custom Hashable implementation in protocol extension causes infinite loop when used by RawRepresentable types

Steps to reproduce:

  • Create a new interface builder based iOS app
  • Navigate to ViewController.swift
  • Paste the following code
import UIKit

protocol P: Hashable {
    var name: String { get }
}
extension P {
    func hash(into hasher: inout Hasher) {
        hasher.combine(name)
    }
}

enum E: String, P {
    case a
    var name: String {
        rawValue
    }
}

struct S: RawRepresentable, P {
    let rawValue: String
    init?(rawValue: String) {
        self.rawValue = rawValue
    }
    var name: String {
        rawValue
    }
}

struct SNonRaw: P {
    let rawValue: String
    init?(rawValue: String) {
        self.rawValue = rawValue
    }
    var name: String {
        rawValue
    }
}

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        print("BEFORE NON RAW STRUCT")
        
        let _ = Set<SNonRaw>([SNonRaw(rawValue: "a")!, SNonRaw(rawValue: "a")!, SNonRaw(rawValue: "a")!, SNonRaw(rawValue: "a")!, SNonRaw(rawValue: "a")!])
        
        print("AFTER NON RAW STRUCT")
        
        print("BEFORE STRUCT")
        
        let _ = Set<S>([S(rawValue: "a")!, S(rawValue: "a")!, S(rawValue: "a")!, S(rawValue: "a")!, S(rawValue: "a")!])
        
        print("AFTER STRUCT")
        
        print("BEFORE ENUM")

        let _ = Set<E>([.a, .a, .a, .a, .a, .a, .a, .a, .a, .a, .a])

        print("AFTER ENUM")
    }
}

If you run this on iOS 15.5 simulator you'll get the following console log:

BEFORE NON RAW STRUCT
AFTER NON RAW STRUCT
BEFORE STRUCT

Also, the app hangs. If you stop the app you will get a Problem Report window that gives you the following stacktrace:

Thread 0 Crashed::  Dispatch queue: com.apple.main-thread
0   libswiftCore.dylib            	    0x7fff309e094a RawRepresentable<>._rawHashValue(seed:) + 74
1   HashableRawRepresentable      	       0x109753bd4 protocol witness for Hashable._rawHashValue(seed:) in conformance S + 52
2   HashableRawRepresentable      	       0x1097539fd S.hash(into:) + 109
3   HashableRawRepresentable      	       0x109753b91 protocol witness for Hashable.hash(into:) in conformance S + 17
4   libswiftCore.dylib            	    0x7fff309e0974 RawRepresentable<>._rawHashValue(seed:) + 116
5   HashableRawRepresentable      	       0x109753bd4 protocol witness for Hashable._rawHashValue(seed:) in conformance S + 52
6   HashableRawRepresentable      	       0x1097539fd S.hash(into:) + 109
7   HashableRawRepresentable      	       0x109753b91 protocol witness for Hashable.hash(into:) in conformance S + 17
8   libswiftCore.dylib            	    0x7fff309e0974 RawRepresentable<>._rawHashValue(seed:) + 116
9   HashableRawRepresentable      	       0x109753bd4 protocol witness for Hashable._rawHashValue(seed:) in conformance S + 52
10  HashableRawRepresentable      	       0x1097539fd S.hash(into:) + 109
11  HashableRawRepresentable      	       0x109753b91 protocol witness for Hashable.hash(into:) in conformance S + 17
12  libswiftCore.dylib            	    0x7fff309e0974 RawRepresentable<>._rawHashValue(seed:) + 116
13  HashableRawRepresentable      	       0x109753bd4 protocol witness for Hashable._rawHashValue(seed:) in conformance S + 52
14  HashableRawRepresentable      	       0x1097539fd S.hash(into:) + 109
15  HashableRawRepresentable      	       0x109753b91 protocol witness for Hashable.hash(into:) in conformance S + 17
16  libswiftCore.dylib            	    0x7fff309e0974 RawRepresentable<>._rawHashValue(seed:) + 116
17  HashableRawRepresentable      	       0x109753bd4 protocol witness for Hashable._rawHashValue(seed:) in conformance S + 52
... (continues forever...)

If you run this on iOS 15.2 simulator everything works and you'll get the following console log:

BEFORE NON RAW STRUCT
AFTER NON RAW STRUCT
BEFORE STRUCT
AFTER STRUCT
BEFORE ENUM
AFTER ENUM

I suspect that this change is responsible for the issue:

https://github.com/apple/swift/pull/39155/commits/f8d72f819ad4f871ce4c72c7ebedb7f479ac92bb

If I basically "revert" this change by adding the following code, the code will run on iOS 15.5 as well:

extension RawRepresentable where RawValue: Hashable, Self: Hashable {
    var hashValue: Int {
        return rawValue.hashValue
    }
}

First post here, so hopefully this has enough information to work with.

If you need any further details, feel free to comment.

Cheers

Don't know the answer, but the protocol itself:

protocol P: Hashable {
    var name: String { get }
}
extension P {
    func hash(into hasher: inout Hasher) {
        hasher.combine(name)
    }
}

is quite concerning. Makes it very easy to shoot yourself in the foot.

struct S: P {
    let name: String
    let surname: String
}

let a = S(name: "Alice", surname: "Cooper")
let hash = a.hashValue

for _ in 0 ..< 100000 {
    let b = S(name: "Alice", surname: UUID().uuidString)
    precondition(b.hashValue == hash)
}
print("this test 'passes'")

True, but it still was a deliberate decision to avoid a lot of code duplication.

The naming of the hashed property is more along the lines of "identifier" rather than "name" and we mostly use this for RawRepresentable enums, which mitigates the "shoot yourself in the foot" part a bit.

Nevertheless, I think that the deadlock is not intended here.

Please file a bug over on GitHub :)
cc @lorentey

2 Likes

Thanks very much for writing this up -- what an unusual bug! I posted a potential workaround on the GitHub issue; let's continue the discussion there.

1 Like