Skip to content

macOS/JVM: SIGSEGV in objc_registerClassPair if IdeaRunnable is already registered #626

Description

@Moriafly

FileKit 0.14.2 can crash the JVM when opening a picker if the process already contains an Objective-C class named IdeaRunnable

I initially suspected a JBR issue because the failure is reported as a native SIGSEGV, but the call stack consistently ends here:

C  [libobjc.A.dylib+0xd348]  objc_registerClassPair+0x54

jdk.proxy1.$Proxy0.objc_registerClassPair(...)
io.github.vinceglb.filekit.dialogs.platform.mac.foundation.Foundation.registerObjcClassPair(...)
io.github.vinceglb.filekit.dialogs.platform.mac.foundation.Foundation.initRunnableSupport(...)
io.github.vinceglb.filekit.dialogs.platform.mac.foundation.Foundation.executeOnMainThread(...)

The relevant code is in Foundation.initRunnableSupport:

https://github.com/vinceglb/FileKit/blob/0.14.2/filekit-dialogs/src/jvmMain/kotlin/io/github/vinceglb/filekit/dialogs/platform/mac/foundation/Foundation.kt#L326-L330

objc_allocateClassPair returns nil when the requested class name is already in use. With the current JNA mapping, that comes back as an ID whose value is zero. It is then passed to objc_registerClassPair without being checked

The crash report confirms the null class argument:

siginfo: si_signo: 11 (SIGSEGV), si_addr: 0x0000000000000000
x0=0x0000000000000000

This is reproducible in an otherwise isolated JVM. The only setup required is registering IdeaRunnable before initializing FileKit:

import com.sun.jna.Library
import com.sun.jna.Native
import com.sun.jna.NativeLong
import com.sun.jna.Pointer
import io.github.vinceglb.filekit.FileKit
import io.github.vinceglb.filekit.dialogs.openDirectoryPicker
import kotlinx.coroutines.runBlocking

private interface ObjCRuntime : Library {
    fun objc_getClass(name: String): Pointer?

    fun objc_allocateClassPair(
        superclass: Pointer?,
        name: String,
        extraBytes: NativeLong
    ): Pointer?

    fun objc_registerClassPair(cls: Pointer?)
}

private val objc = Native.load("Foundation", ObjCRuntime::class.java)

private fun registerIdeaRunnable() {
    val nsObject = requireNotNull(objc.objc_getClass("NSObject"))
    val cls = requireNotNull(
        objc.objc_allocateClassPair(
            nsObject,
            "IdeaRunnable",
            NativeLong(0)
        )
    )
    objc.objc_registerClassPair(cls)
}

fun main() = runBlocking {
    registerIdeaRunnable()
    FileKit.openDirectoryPicker()
}

I ran this reproducer in a separate JBR process and got the same objc_registerClassPair+0x54 crash

The Foundation helper appears to be derived from IntelliJ Platform's implementation. IdeaRunnable was introduced there as an internal adapter class:

JetBrains/intellij-community@37dd1fb

That implementation assumes it owns the process-level class name. The assumption is reasonable inside the IDE, but it does not hold as well for a reusable library, where another dependency may contain the same helper

The straightforward fix would be to give the class a FileKit-specific name and reject a nil allocation before calling objc_registerClassPair. The check should use isNil, rather than only checking Kotlin nullability, because the returned ID may be non-null while holding a zero value

I would avoid reusing a pre-existing class with the same name: its run: implementation and callback state may belong to a different copy of the helper. Replacing the adapter with a GCD dispatch would remove the global class registration entirely, but a namespaced class name plus the defensive check should be sufficient for this failure

Environment:

  • FileKit 0.14.2
  • JNA 5.19.1
  • JBR 25.0.2+10-b315.62
  • Kotlin 2.4.10
  • macOS 26.5.2
  • Apple Silicon

Metadata

Metadata

Assignees

No one assigned

    Labels

    nextScheduled for the next version

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions