How best to debug EXC_BAD_ACCESS of struct?

Code that until recently was working just fine has recently started crashing for me, in release builds only. There have been recent changes at the very bottom of the call-stack (the introduction of a Task { ... } and await), but none relating to the instantiation or usage of the type that is suffering the crash.

I've tried to reproduce the issue in a standalone app, but it doesn't suffer the same problem. The affected type is AnyEncodable here:

import Foundation

struct Submittable: Encodable {
  let id: Int
  let user: Int
  let answers: [Answer]
}

struct Answer: Encodable {
  let id: Int
  var value: AnyEncodable?
}

public struct AnyEncodable: Encodable {
  private let encodeTo: (Encoder) throws -> Void

  public init<T: Encodable>(_ value: T) {
    encodeTo = { encoder in
      var container = encoder.singleValueContainer()
      try container.encode(value)  // Crash occurs here in real app
    }
  }

  public func encode(to encoder: Encoder) throws {
    try encodeTo(encoder)
  }
}

class FormSubmitter {
  func submit(_ form: Submittable) throws {
    let encoder = JSONEncoder()

    let json = try encoder.encode(form)

    let jsonString = String(bytes: json, encoding: .utf8) ?? "<Invalid JSON>"

    print(jsonString)
  }
}

Changing struct AnyEncodable to class AnyEncodable seems to fix the problem, but doesn't feel like it should be required. Hopefully it gives the right minds a strong hint at the likely cause.

For replicating the Task addition, I've simply called Task { try? FormSubmitter().submit(<instance of Submittable>) }.

If anybody is able to offer any debugging suggestions, I'd be happy to try them and report back on the outcome.

No idea, but you can use String(decoding:as) to create a String from Data without the optional.

That's harder then.

Can you show us the stack trace and other bits and bobs of the crash?

Stack-trace, from the crashing thread (thread 2), as requested.

 0 CoreFoundation _exceptionPreprocess
 1 libobjc.A.dylib objc_exception_throw
 2 Foundation -[NSPlaceholderString initWithString:]
 3 Foundation _JSONEncoder.encode(_:)
 4 Foundation protocol witness for SingleValueEncodingContainer.encode(_:) in conformance _JSONEncoder
 5 libswiftCore.dylib protocol witness for Encodable.encode(to:) in conformance String
 6 libswiftCore.dylib dispatch thunk of Encodable.encode (to:)
 7 Foundation _JSONEncoder.box_(:)
 8 Foundation _JSONEncoder.encode<A>(_:)
 9 Foundation protocol witness for SingleValueEncodingContainer.encode<A>(_:) in conformance _JSONEncoder
10 libswiftCore.dylib dispatch thunk of SingleValueEncodingContainer.encode<A>(_:)|
11 AcmeAPI closure #1 in AnyEncodable.init<A>(_:)
12 AcmeAPI partial apply for closure #1 in AnyEncodable.init<A>(_:)
13 AcmeAPI protocol witness for Encodable.encode (to:) in conformance AnyEncodable
14 libswiftCore.dylib dispatch thunk of Encodable.encode(to:)
15 Foundation JSONEncoder.box_(_:)
16 Foundation _JSONKeyedEncodingContainer.encode<A>(_:forKey:)
17 Foundation protocol witness for KeyedEncodingContainerProtocol.encode<A>(_:forKey:) in conformance _JSONKeyedEncodingContainer<A>
18 libswiftCore.dylib KeyedEncodingContainerProtocol.encodelfPresent<A>(_:forKey:)
19 Foundation protocol witness for KeyedEncodingContainerProtocol.encodelfPresent<A>(_:forKey:) in conformance _JSONKeyedEncodingContainer<A>
20 libswiftCore.dylib _KeyedEncodingContainerBox.encodelfPresent<A, B>(_:forKey:)
21 libswiftCore.dylib KeyedEncodingContainer.encodelfPresent<A>(_:forKey:)
22 AcmeAPI ConcussionAnswer.encode (to:)
23 AcmeAPI protocol witness for Encodable.encode(to:) in conformance ConcussionAnswer
24 libswiftCore.dylib dispatch thunk of Encodable.encode (to:)
25 Foundation _JSONEncoder.box_(:)
26 Foundation _JSONUnkeyedEncodingContainer.encode<A>(:)
27 Foundation protocol witness for UnkeyedEncodingContainer.encode<A>(:) in conformance _JSONUnkeyedEncoding Container
28 libswiftCore.dylib Array<A>.encode(to:)
29 libswiftCore.dylib protocol witness for Encodable.encode (to:) in conformance <A> [A]
30 libswiftCore.dylib dispatch thunk of Encodable.encode (to:)
31 Foundation _JSONEncoder.box_(:)
32 Foundation _JSONKeyedEncodingContainer.encode<A>(_:forKey:)
33 Foundation protocol witness for KeyedEncodingContainerProtocol.encode<A>(_:forKey:) in conformance _JSONKeyedEncodingContainer<A>
34 libswiftCore.dylib _KeyedEncodingContainerBox.encode<A, B>(_:forKey:)
35 libswiftCore.dylib KeyedEncodingContainer.encode<A>(_:forKey:)|
36 AcmeAPI ConcussionFormSubmit.encode(to:)
37 AcmeAPI protocol witness for Encodable.encode (to:) in conformance ConcussionFormSubmit
38 libswiftCore.dylib dispatch thunk of Encodable.encode (to:)
39 Foundation JSONEncoder.box_(_:)
40 Foundation JSONEncoder.encode<A>(_:)
41 Foundation dispatch thunk of JSONEncoder.encode<A>(:)
42 AcmeAPI specialized static FormsRoutes.submitAnswersLegacy(orgld:body:)
43 AcmeAPI specialized static FormsRoutes.submitAnswers(orgld:body:)
44 AcmeAPI RestFormsService.submitAnswers(body:organisationld:)
45 AcmeAPI protocol witness for FormSubmitting.submitAnswers (body:organisationld:) in conformance RestFormsService
46 AcmeViews closure #1 in FormSubmitter.submitAnswers (body:orgld:attachments:completion:)
47 AcmeViews partial apply for closure #1 in FormSubmitter.submitAnswers (body:orgId:attachments:completion:)
48 AcmeViews specialized thunk for @escaping @callee_guaranteed @Sendable @async () -> (@out A)
49 AcmeViews partial apply for specialized thunk for @escaping @callee_guaranteed @Sendable @async () →> (@out A)
50 libswift_Concurr…. completeTaskithClosure (swift::AsyncContext*, swift::SwiftError*)

Apologies for the wall of red - I'm not sure how to tell the formatter that this isn't Swift code...

Do you see any exception reason in the crash log? e.g. "NSPlaceholderString was called with nil" or something like that?

Try logging what's being encoded:

    print("encoding '\(value)')
    try container.encode(value)

Hopefully that'll show the thing that triggers the crash and hint what to do next.

I've seen two different exceptions. One is NSInvalidArgumentException - *** -[NSPlaceholderString initWithString:]: nil argument

The other (and the print suffers it too) is of the form: Thread 15: EXC_BAD_ACCESS (code=2, address=0x109ad5148)
(sorry, I should have included that in the first report)

I don't know whether it's debugging a release build that's hurting visibility of values in the debugger, or a consequence of whatever's going wrong. Most times I've looked, the best Xcode has been able to tell me about value is that it's 𝜏_0_0. Strangely this time it's telling me that it's a valid empty string (and yet there's an exception).

Many times debugger lied to me, so I'd try print.

This corresponds to the stack trace you sent above.

This is something else.

While I've seen two outcomes, it certainly feels like they have a common cause. Between where the model is constructed and where the crash occurs, there's nothing but direct function calls - no threads, no async. And yet somehow a model comprising only structs and a closure capturing a struct appears to be trying to access memory that has been over-written.

1 Like

Hard to tell without having a reproducible standalone test case, but if it is reproducible in your full app that's great. I'd try all of these:

  • print("encoding '(value)'") as per above suggestion to see what's being encoded, especially the last thing.
  • Xcode diagnostic tools, I'd try various permutation of those
  • Could be a benign stack overflow (that Xcode can't reliably check and tell you about)

For the last one it's worth checking (in the debugger at the point of crash):

p pthread_get_stackaddr_np(pthread_self())
register read sp
p pthread_get_stacksize_np(pthread_self())

The difference of the first two is how much currently on the stack and this value should be lower (with a generous leeway) than the third value. It would be concerning if there's less than, say, 2K left.

Just to sanity check, have you done a clean build since the issue started?

Yes, I've nuked all of DerivedData. It's not just my machine either - the first TestFlight build to exhibit this was built by somebody else using Xcode 14.3. I'm on 15 and seeing the same result.

It's the end of my day here @tera , but I will work through your suggestions on Monday.

As sp is less than the "base" address, I guess the stack extends downwards. The difference is just 0x1640 (5696 decimal), so I think that means we've got plenty of room:

(lldb) p pthread_get_stackaddr_np(pthread_self())
      (UnsafeMutableRawPointer) 0x16bdf7000

(lldb) register read sp
      sp = 0x000000016bdf59c0

(lldb) p pthread_get_stacksize_np(pthread_self())
      (Int) 536576
1 Like

Smells like a threading issue. Have you tried Thread Sanitizer?

I hadn't (everything between creating the struct and encoding the struct happens on the same thread), but I just tried it now. It didn't flag anything.

Indeed. That doesn't completely remove the possibility of stack overflow though (there could be a quick overflow and back). How big in bytes are the structs in question?

On occasions like that sometimes I had to start with the whole app and strip it down to a minimal reproducible case (sometimes to a handful of lines!) removing the unnecessary parts, testing every step along the way ensuring the bug is still there (if the bug disappears I backtrack and remove something else). Once down to one / few / a handful of lines the issue is immediately obvious as there is no more place for it to hide. In case of a compiler bug the resulting few-lines-of-code test case is very helpful for the compiler folks to fix the issue.

2 Likes

Make sure that you are not suffering from a recursion. :slight_smile: