How to track down a crash that shows up in Array._getCount

I'm looking at an EXC_BAD_ACCESS crash in

|#0|0x000000018f6c0538 in Array._getCount() ()|
|#1|0x000000018f6c3764 in protocol witness for Collection.endIndex.getter in conformance [τ_0_0] ()|
|#2|0x000000018f6b7e8c in Collection.isEmpty.getter ()|

which occurs at a check of body.isEmpty.

In the debugger, I see

(lldb) p body
([graphQL.GQL]) $R2 = 0 values {}
(lldb) p body._buffer
(_ArrayBuffer<graphQL.GQL>) $R1 = {
  _storage = (rawValue = 0x0000000000000000)
}
(lldb) p body.isEmpty
error: Execution was interrupted, reason: EXC_BAD_ACCESS (code=1, address=0x10).

body is the payload of an indirect enum case with associated values. The crash occurs a couple levels into the recursion on the enum contents. I'm new to this codebase, it will take a fair bit of time and effort to trim it down to a small shareable test case.

What tools would you recommend to track down what appears to be some sort of corruption in the payload of an indirect enum with associated values? The memory checks and sanitizers show no indication of trouble. (Are there any known compiler bugs having to do with indirect enums?)

I found it is typically the quickest way to find the issue, binary dissection to get to the distilled version that still crashes then it is obvious what's going on. Might take some time though.

The simplest thing to check if indirect placed on enum itself vs it being placed on the individual case(s) change anything irt crashing.

1 Like

On further investigation, looks like it has nothing to do with indirect enum, and is an issue with order of initialization of globals. The following snippet seems to illustrate it or something very similar.

enum Foo {
    case x([Foo])
}

let gFoo1 = Foo.x([gFoo2])
let gFoo2 = Foo.x([])

print(gFoo2) // prints x([])
print(gFoo1) // crashes in Array._getCount()

Would you classify that as programmer error or compiler bug?

3 Likes

It’s basically a language design bug. The top-level declarations in main.swift are globals, but instead of being lazily initialized like globals in other source files, they are initialized when the appropriate line of code executes. That means they are visible before they are actually initialized—both in code above the declaration and in other files where it may be difficult to tell whether the variable will be initialized yet.

We’ve discussed fixing this by changing the language—probably by turning top-level main.swift variables into locals—but I don’t think anything’s come of that yet.

8 Likes

On more further inspection, it took me a while to realize my crash was due to a circular reference.

enum E {
    case x([E])
}

struct S {
    static var list: [E] = [.x(S.list)] // compiles without error
}

print(S.list) // EXC_BAD_ACCESS

Should the compiler have caught that?

For reference

let gFoo = E.x(gFoo) // compile error: circular reference
3 Likes

Nice one. I wonder if that's possible to catch this at compile time in general case, example:

var someGlobalVariable: Bool = false

struct S {
    static func foo() -> [E] {
        someGlobalVariable ? [] : list
    }
    static var list: [E] = [.x(foo())]
}

print(S.list) // EXC_BAD_ACCESS

Edit: it looks reasonably easy to detect such cases during compilation and reject the code assuming the pessimistic scenario:

struct S {
    static func foo() -> [E] {
        // list is obviously won't be used here, but
        // still consider that to be a "usage"
        true ? [] : list
    }
    static var list: [E] = [.x(foo())] // Error: uninitialised "list" is used in this expression
}

Here the information about "list is being initialized" could be somehow passed down the call chain in the parser, and whenever parser sees a usage of a thing that's on the current "initialisation list" it should reject back with an error.

1 Like

The general case would probably require solving the halting problem, but it'd be nice to catch it in more cases even if catching it in all cases is impossible.

I added my crashes in a comment to Recursive static property on struct creates an empty struct · Issue #60094 · apple/swift · GitHub which looks like the same or a closely related issue.

If we are ready to raise the bar and reject some previously valid code (by assuming the worst possible case) it seems possible to catch all cases. The code like this would be rejected:

struct Foo {
	static func foo(useY: Bool) -> Int {
		useY ? y : 0
	}
	
	static func bar(useX: Bool) -> Int {
		useX ? x : 0
	}
	
	static let x = foo(useY: true) // Error
	static let y = bar(useX: false) // Error
}

but IMHO such a code deserves to be rejected.

The Swift file containing these static vars is programmatically generated from json from a databse and is huge. The generated file compiles without error, but generates fields that crash an isEmpty check.

Is there a way to detect this corruption at runtime without crashing?

Might a future compiler turn this into a compile-time error?