SIGBUS

Comic-style illustration. On the left, Jorge in front of a cracked mirror, wearing an orange shirt and blue shorts, with a panicked expression. On the right, his reflection inside the mirror with the colors swapped — blue shirt and orange shorts — also horrified. The mirror has a central impact point with radial cracks and shards of glass falling to the floor. It represents the SIGBUS bug in Swift Testing: when values don't match, the runtime tries to use Mirror to describe the difference and the mirror shatters.

Table of contents


My Problem 🤔

I’m writing tests with Swift Testing, Apple’s native stack. I compare two models with #expect(a == b) and verify errors with #expect(throws: MyError.self) — the API the library ships with out of the box. I run the tests. And my binary dies with this:

*** Signal 10: Backtracing from 0x18a3c4e8c... done ***
Process terminated by signal 10 (SIGBUS)
Stack trace:
  ...
  _swift_buildDemanglingForMetadata
  ...
  swift_testing.Issue.record(...)

SIGBUS. It’s not an assertion failure. It’s the Swift runtime breaking inside the test runner itself. And it only happens when a test fails — if everything passes, I never notice the problem.

The pattern is very specific. The runtime crashes when I use #expect(==) on enums with associated values, or when I use #expect(throws:) on an error type that is an enum with associated values. As soon as the test fails, swift-testing tries to build the error message by asking the runtime to reconstruct the type metadata of the values it compared. And there it blows up: _swift_buildDemanglingForMetadata, signal 10, end of story.

It’s bug swiftlang/swift#76608, open since September 2024 and reproducible on Swift 6.3.1 on macOS 26 — both in debug and release.

The curious thing is that the happy path never touches this bug. If the comparison passes, there’s no error message to format, no Mirror, no SIGBUS. The bug only shows up exactly when you need the test output the most: when something fails. And I couldn’t move a single piece forward until I solved it.


My Solution 🧩

The key clue is where swift-testing crashes: on the failure message path, formatting the values. If I do the comparison myself and only pass swift-testing a Bool, there’s nothing for it to reconstruct. No metadata to reflect on. The mirror doesn’t break because it’s never used.

The trick that avoids the SIGBUS fits in two lines:

let isEqual = actual == expected
#expect(isEqual)

That’s the whole idea. I compute the equality myself, swift-testing only receives an already-resolved Bool. With no values to reflect on, there’s no Mirror, no SIGBUS. The price is losing the values in the failure message — but I get that back by formatting the text by hand before passing it to Issue.record.

From there I built a TestKit package with three helpers that apply the same idea to the three cases where I was tripping over the bug:

  • expectEqual(actual, expected) — comparison of any Equatable: structs, models, enums with associated values. Replaces #expect(a == b) across every test in the monorepo.
  • expectThrows(E.self) { … } — verifies that a closure throws an error of the expected type. It does do/catch by hand and checks with catch is E — only the type, never the value. Covers what #expect(throws:) tried to provide out of the box. Has both sync and async overloads.
  • expectEqualLines(actual, expected) — line-by-line diff for verifying generated SQL and other inline snapshots. The comparison is also reduced to Bool before touching the reporter.

Three helpers, one single idea: compute the result outside the macro, pass only the boolean. When Swift 6.4 eventually closes issue 76608, I just have to swap the body of the helpers for direct #expect calls and the suite won’t even notice.

There’s one extra detail to keep failure messages readable: I conformed the error and schema types to CustomStringConvertible with a static switch over the cases — never interpolating (self), because that would invoke Mirror again and reopen the trap.


My Result 🎯

My test suite runs again without SIGBUS and with reasonably readable failure messages. The rule I now apply across the monorepo:

  • expectEqual(a, b) for any comparison between structs, enums or models
  • expectThrows(E.self) { … } to verify the type of error thrown
  • #expect(…) directly only when the argument is already Bool, nil or contains — there’s no value metadata to reflect on, and the bug is not triggered

The benefits:

  • A suite that doesn’t crash — failures count as failures, not as process aborts
  • Minimal stack — I only depend on Testing and Swift’s own toolchain
  • Custom messages — I control how each side is printed on failure, thanks to CustomStringConvertible
  • Runtime bug isolation — when Swift 6.4 fixes it, I just swap the body of the helpers and the suite won’t even notice

The lesson I’m taking away: when a runtime tool breaks on the error path, the solution isn’t to give up on the language, it’s to keep the tool from going down that path. I do the comparison myself, I pass a boolean to the reporter, and the mirror stays intact.

Keep coding, keep running 🏃‍♂️