Repository navigation
Description
Summary
On macOS, flutter_tools stamps a native asset's framework with the install name @rpath/<name>.framework/<name>, but writes <name>.framework/<name> — without the @rpath/ prefix — into NativeAssetsManifest.json. That is the string the Dart VM passes to dlopen.
The two strings never match, so dyld cannot recognise the library it already has by name and falls back to identifying the file by (device, inode). That identity holds until the file is replaced — which is exactly what rebuilding the app does while a debug instance is running. The next @Native binding to resolve then loads a second copy of the library into the process.
Two copies of a C library do not share its global state. A handle produced by the first copy, passed into the second, dereferences an uninitialised pointer. For package:sqlite3 that is a hard SIGSEGV on the very first write after a rebuild — and it is not a development-only annoyance, because the operation that crashes is silently lost.
Reported downstream as simolus3/sqlite3.dart#381, where it looked like a SQLite binding problem. It is not: the same mechanism will hit any native asset whose bindings resolve lazily.
The mismatch, in the tool's own source
// packages/flutter_tools/lib/src/isolated/native_assets/macos/native_assets.dart:176
final newInstallName = '@rpath/$dylibFileName.framework/$dylibFileName';// packages/flutter_tools/lib/src/isolated/native_assets/macos/native_assets_host.dart:287
return Uri(path: '$fileName.framework/$fileName');The second is what ends up in the bundle:
{"native-assets":{"macos_arm64":{
"package:sqlite3/src/ffi/libsqlite3.g.dart":["absolute","sqlite3.framework/sqlite3"]}}}Note also that the path kind is recorded as absolute while the path is relative. That is a separate inconsistency and not the cause.
The sibling branch of the same file gets this right, and does not exhibit the bug:
// packages/flutter_tools/lib/src/isolated/native_assets/macos/native_assets.dart:237
final String newInstallName = dylibFile.path; // install name == the path usedThe standalone Dart VM writes its JIT asset mapping the same way — install name equal to the absolute path it dlopens — which is why dart run never reproduces this and only Flutter macOS builds do.
Steps to reproduce
Any Flutter macOS app with a native asset whose bindings are not all used at startup. package:sqlite3 is a convenient one, because a typical app only reads until the user acts, so the write-path bindings resolve late.
flutter run -d macosand let the app open a database (reads only).- Edit any source file the app's build depends on.
- Rebuild the same bundle while that instance keeps running (
flutter drive, a secondflutter run, or an IDE build). - In the running window, perform the first operation that binds a text parameter — i.e. the first write.
The process dies with:
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Codes: KERN_INVALID_ADDRESS at 0x0000000000000000
Thread 0 Crashed: com.apple.main-thread
0 ??? 0x0 ???
1 sqlite3 ... vdbeUnbind + 64
2 sqlite3 ... bindText + 52
and its usedImages list contains the asset twice — same UUID, same size, two base addresses, while every other image appears once:
0F642EA9-9A09-3DD0-BE88-C97BF2540F67 0x12f2ac000 .../tm_console.app/Contents/Frameworks/sqlite3.framework/Versions/A/sqlite3
0F642EA9-9A09-3DD0-BE88-C97BF2540F67 0x1353ac000 .../tm_console.app/Contents/Frameworks/sqlite3.framework/Versions/A/sqlite3
Under lldb with a breakpoint on dlopen, the last call before the crash is the second load of the asset:
(char *) $1 = "sqlite3.framework/sqlite3"
* stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
frame #1: sqlite3`vdbeUnbind + 64
frame #2: sqlite3`bindText + 52
Minimal reproduction of the dyld behaviour alone
No Flutter, no native assets, ~40 lines: two dylibs identical in every byte except LC_ID_DYLIB, each opened, replaced on disk (same bytes, new inode) and opened again.
── install name == the path it is opened by
first open 1 mapping(s)
after replacing the file and opening again: 1 mapping(s)
── install name is @rpath/…, as a framework in an app bundle has
first open 1 mapping(s)
after replacing the file and opening again: 2 mapping(s) ← mapped TWICE
A control that opens the same path twice without replacing the file returns one mapping, so the replacement is the variable and not the counting.
Expected vs actual
Expected: rebuilding an app while a debug instance runs does not change what that instance has loaded.
Actual: the next lazily-resolved @Native binding maps a second copy of the asset, and any handle created by the first copy becomes unusable in the second.
Why it surfaces so late and looks so random
@Native bindings resolve one function at a time, on first use, and each resolution asks the embedder to load the asset again (runtime/lib/ffi_dynamic_library.cc, FfiResolveAsset → LoadAssetLibrary; no handle is cached). While the file is untouched, dlopen returns the image already in the process and nothing is wrong. So:
- a window that only reads survives for as long as nobody rebuilds;
- the crash lands on the first write, minutes after launch, which looks like "the Accept button is broken";
- restarting the app fixes it, because a fresh process maps the current file once;
- it never reproduces on a test bench, because nothing rebuilds mid-run;
- it only bites developers, which is why it reads as flakiness.
Proposed fix
Make the manifest carry the same string the install name uses:
// packages/flutter_tools/lib/src/isolated/native_assets/macos/native_assets_host.dart:287
- return Uri(path: '$fileName.framework/$fileName');
+ return Uri(path: '@rpath/$fileName.framework/$fileName');@rpath/ resolves fine through dlopen from inside the bundle — verified, see below.
packages/flutter_tools/lib/src/isolated/native_assets/ios/native_assets.dart calls the same helper and looks like it has the same mismatch; not verified here, as this was measured on macOS only.
Verification
Both directions were applied to an already-built bundle and driven through the reproduction above. The app was launched with flutter run --use-application-binary=` so the rebuild in step 3 could not undo the patch.
| bundle under test | window | asset images | operation | new crash report |
|---|---|---|---|---|
| unmodified | crashed 3/3 | 2 | lost | yes ×3 |
install name → sqlite3.framework/sqlite3 |
survived 2/2 | 1 | committed | none |
manifest → @rpath/sqlite3.framework/sqlite3 |
survived 2/2 | 1 | committed | none |
What is not the fix
- The package. Resolving all 92 of
package:sqlite3's@Nativebindings up front withNative.addressOfwas tried and does not prevent the crash, although it does run. Taking a symbol's address appears not to bind the JIT call site, so the late resolution still happens. Only actually executing the call site before the rebuild avoids it, which a library cannot arrange for arbitrary functions. - Worth knowing if anyone attempts that workaround: resolving every binding in bulk throws on symbols that a given SQLite build omits — e.g.
sqlite3_column_table_namewithoutSQLITE_ENABLE_COLUMN_METADATA. Lazy resolution hides their absence, because nothing ever looks them up.
Also worth considering (separate from this issue)
FfiResolveAsset calls into the embedder's loader on every symbol resolution and keeps no handle. That is safe only while dlopen is idempotent, which is an assumption about the filesystem rather than about the code. Caching the handle per asset in the isolate group would make this class of failure impossible regardless of install names or platform. Filed here for visibility; not required to fix the bug above.
Environment
Flutter 3.44.6 • channel stable
Framework • revision ee80f08bbf • 2026-07-08
Engine • hash d3a3293399556a85388faf8c6f0723a7a5597aa8
Tools • Dart 3.12.2
macOS 26.5.2 (25F84), arm64
package:sqlite3 3.5.1 (precompiled binary from the package's build hook)
Activity
suryaKommana2662 commentedon Aug 10, 2026
@pro100andrey Thank you for the detailed report. I noticed that the issue was tested with Flutter 3.44.6. Could you please verify if the issue is still reproducible on the latest stable Flutter release? Also, could you provide a minimal reproducible
main.dartfile that demonstrates the issue? This would help us reproduce and investigate the issue locally and route it to the appropriate team.pro100andrey commentedon Aug 10, 2026
@suryaKommana2662 good day!
flutter-190799-repro.zip
Both, below. Short answer: yes — unchanged on the current stable (3.44.9), and the two lines are still identical on
master.1. Still reproducible on the latest stable
Reproduced from a fresh
flutter create --platforms=macos, one dependency, nothing else.The mismatch itself needs no app at all — two commands on any built bundle:
The two lines that produce those strings are unchanged on both channels as of today:
6b182d2c75native_assets.dart#L173native_assets_host.dart#L2841cd7c6b603native_assets.dart#L176native_assets_host.dart#L2872. Minimal reproduction
pubspec.yaml— the generated one plus a single line:lib/main.dart— three buttons and a mapping counter. The counter asks dyld directly, so the double mapping is observed rather than inferred from the crash:Steps
flutter run -d macosinstance was launched from:
$ touch lib/main.dart && flutter build macos --debug(
295524898→295525216in my run; same size, and the two mappings in thecrash report below carry the same
LC_UUID, so the bytes really are thesame). Nothing about the asset changes — the file being replaced is the
whole variable.
now says 2
SIGSEGVWhat it printed
(I drove
read()/probeUnusedBinding()/write()over the VM service instead ofclicking, so the run could be scripted. Those are exactly the functions the three
buttons call.)
3. The one-line fix, re-verified on 3.44.9
Patching only the manifest string in an already-built bundle, then launching it with
--use-application-binary=…so that step 3 could not undo the patch:4. One detail worth adding
It is not specific to the write path. In my first pass the probe button called
sqlite3_last_insert_rowid— a read-only call that touches nothing but the handle —and that crashed, identically, because it happened to be the first binding resolved
after the rebuild:
Frame 0 is address
0x0in both reports, which is consistent with the second copynever having run
sqlite3_initialize, leaving the mutex vtable it calls throughnull. So any binding that dereferences library state will do; the write path is
merely what a real app reaches first, and
sqlite3_libversionsurvives only becauseit returns a compile-time constant — which is what makes it usable as the probe
above.
Thanks!
suryaKommana2662 commentedon Aug 10, 2026
@pro100andrey Thanks for providing the complete reproduction app and detailed analysis on latest stable. Routing it to
team-macosfor further investigation and review of the linked PR (#190819).cbracken commentedon Aug 13, 2026
/cc @dcharkes are you the person most familiar with this?
(macOS triage meeting)
dcharkes commentedon Aug 13, 2026
Yep, reviewing the PR. Thanks for the ping! 🙏
[native_assets] Record bundled code assets under their install name
[native_assets] Record bundled code assets under their install name (f…
github-actions commentedon Sep 1, 2026
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.