Skip to content

macOS: SIGSEGV (branch to 0x0) in sqlite3_update_hook during Database.close() — native-asset framework mapped twice in one process #381

Description

@djbuys

Environment

  • sqlite3: 3.1.7 (transitive via drift 2.32), loaded through native assets with the hook override:
    hooks:
      user_defines:
        sqlite3:
          source: sqlite3mc
  • Flutter 3.44.0 stable / Dart 3.12.0
  • macOS 26.4.1 (25E253), arm64 (MacBook Pro, Mac17,9)
  • App: Flutter desktop POS, several sqlite3mc-encrypted databases (drift NativeDatabase + occasional raw sqlite3.open health probes)

Symptom

Intermittent hard crash — 3 occurrences in one day of testing, same signature every time:

  • EXC_BAD_ACCESS (SIGSEGV), KERN_INVALID_ADDRESS at 0x0, pc = 0x0 (Instruction Abort — a branch to NULL)
  • lr = sqlite3mc sqlite3_update_hook + 48
  • Main thread, inside the Dart microtask queue, during package:sqlite3 Database.close() of a short-lived probe database (sqlite3.openPRAGMA schema_versionclose()), while the app holds other sqlite3mc databases open. close() detaches hooks via sqlite3_update_hook(db, NULL, NULL), which is the crashing frame.
Thread 0 Crashed::  Dispatch queue: com.apple.main-thread
0   ???                  0x0 ???
1   sqlite3mc            0x102d5f130 sqlite3_update_hook + 48
2   App                  0x117e45590 kDartVmSnapshotInstructions + 21904
...
21  FlutterMacOS         0x103c84a58 tonic::DartMicrotaskQueue::RunMicrotasks() + 176

The smoking gun: the native-asset framework is mapped TWICE in one process

Every crash report's Binary Images section lists the sqlite3mc native asset twice — same UUID, same on-disk path, two different load addresses — and the crashing address (0x102d5f130) lies inside the second mapping:

0x1225a0000 - 0x12274ffff io.flutter.flutter.native-assets.sqlite3mc (1.0) <dc12900b-c778-31a7-9fba-d6bd308fe478>
    .../Contents/Frameworks/sqlite3mc.framework/Versions/A/sqlite3mc
0x102d40000 - 0x102eeffff io.flutter.flutter.native-assets.sqlite3mc (1.0) <dc12900b-c778-31a7-9fba-d6bd308fe478>
    .../Contents/Frameworks/sqlite3mc.framework/Versions/A/sqlite3mc

With two copies of the library's static state in the process, a sqlite3* handle created through one copy can be passed to a function resolved against the other. If that second copy never ran sqlite3_initialize(), sqlite3GlobalConfig.mutex.xMutexEnter is NULL and sqlite3_update_hook's mutex-enter is a branch to 0 — which matches the register state (pc=0, lr=sqlite3_update_hook+48) exactly.

Two likely-related packaging artifacts from the same hook build:

  1. Every flutter build macos emits a warning along the lines of "different framework names for different architectures … ignoring sqlite3mc1.framework" — suggesting the hook produces two asset registrations for one library.
  2. The app bundle also ships CSQLite.framework alongside sqlite3mc.framework (a third sqlite copy), even though the user-define switches the source to sqlite3mc.

These persist through flutter clean and reproduce on every build with 3.1.7 (and per a quick changelog read, nothing addressing this landed through 3.3.x — happy to be corrected).

Reproduction

Not deterministic — it needs the right interleaving of open/close against multiple encrypted DBs, but it fired three times in one ~11-hour test day (09:30, 10:57, 20:07), always within ~100ms of a burst of sqlite3.open/close() probe activity. I have all three full .ips crash reports and can attach them.

Workaround

We reduced raw open/close frequency (probing each DB file at most once per process launch), which shrinks the window but obviously doesn't fix the dual mapping.

Happy to test patches, dump dyld info from a live process, or provide the full crash reports / a sample app skeleton.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions