Trivial Swift binary segfaults on Linux when instantiating generic class with protocol constraint

Hi folks! I've encountered a very strange segfault when instantiating a generic class with a protocol type constraint that only reproduces on Linux.

I first ran into this issue when trying to update Bazel's rules_swift to a newer grpc-swift version that depended on swift-atomics.

I created a demo project by forking swift-atomics, making a tiny binary that reproduced the issue, and then stripping out code until it had only the minimum necessary to reproduce the issue:

It's actually trivial to reproduce. You just need two targets, a swift library and a swift binary. The swift library contains three files:

AtomicValue.swift

public protocol AtomicValue {
}

AtomicBool.swift

extension Bool: AtomicValue {
}

ManagedAtomic.swift

public class ManagedAtomic<Value: AtomicValue> {
  internal var _storage: Value

  public init(_ value: Value) {
    _storage = value
  }
}

And the swift binary has just one file:

SwiftAtomicsTest.swift

import Atomics

@main
struct SwiftAtomicsTestMain {
  static func main() throws {
    print("before atomic")

    let managedAtomic = ManagedAtomic(false)

    print("after atomic")
  }
}

The issue manifests on linux when the binary logs "before atomic" and then segfaults on the managedAtomic line:

ubuntu@focal:~/swift_atomics_test$ bazelisk run -s --compilation_mode=dbg --sandbox_debug //Sources/SwiftAtomicsTest
INFO: Analyzed target //Sources/SwiftAtomicsTest:SwiftAtomicsTest (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //Sources/SwiftAtomicsTest:SwiftAtomicsTest up-to-date:
  bazel-bin/Sources/SwiftAtomicsTest/SwiftAtomicsTest
INFO: Elapsed time: 0.113s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/Sources/SwiftAtomicsTest/SwiftAtomicsTest
before atomic
ubuntu@focal:~/swift_atomics_test$ 

But you can see here that the same code builds and runs fine with swift build:

ubuntu@focal:~/swift_atomics_test$ swift build && ./.build/debug/SwiftAtomicsTest
Building for debugging...
Build complete! (0.04s)
before atomic
after atomic
ubuntu@focal:~/swift_atomics_test$

The code also works fine on macos with both build systems.
Also I found that merging the files into the binary (and not having a static library) or merging the AtomicBool and AtomicValue files resolved the issue.

Additionally, to remove any variables introduced by Bazel itself, I created a shell script that just invokes swiftc / clang with the same params and it can reproduce the issue as well:

Do you know what might be causing this issue or how I might be able to work around it? Thanks!

1 Like

Following up here, we figured out a fix, though we're not entirely sure why this was necessary. If we set the bazel flag alwayslink to True on the target, it behaved as expected. I believe this flag corresponds to linker behavior that removes object files that are not referenced explicitly. I suspect that nothing was explicitly referencing the object containing the protocol conformance so this might have been stripped out.