Summary
sqlite3_connection_pool currently requires the Dart VM to allocate executable memory whenever a pool is opened, even when native update hooks are disabled. This causes release/AOT Flutter applications to abort on GrapheneOS when the per-app Dynamic code loading from memory restriction is enabled.
This came up while investigating ente/ente#3436.
Root cause
RawSqliteConnectionPool.open unconditionally creates an initializer with:
NativeCallable<Pointer<InitializedPool> Function()>.isolateLocal(...)
Dart implements NativeCallable by allocating an FFI callback trampoline page and changing it from writable to executable (Dart VM source). GrapheneOS denies that mprotect operation when in-memory dynamic code loading is restricted, and the Dart VM terminates the process. The failure is fatal and cannot be caught by the application.
Setting PoolConnections(enableNativeUpdateHooks: false) does not avoid this requirement: It disables the SQLite update hooks, but the initializer NativeCallable is still constructed before pkg_sqlite3_connection_pool_open is called.
Requested behavior
Could pool initialization support an AOT path that does not dynamically allocate executable callback trampolines?
Two possible directions appear to be:
- Use a statically compiled
Pointer.fromFunction initializer backed by carefully scoped isolate-local initialization state.
- Adjust the native API so the initialized connections/options can be passed without a Dart callback.
Ideally, open and openAsync would retain their current multi-isolate, exception propagation and cleanup behavior while working when new anonymous executable mappings are prohibited.
Summary
sqlite3_connection_poolcurrently requires the Dart VM to allocate executable memory whenever a pool is opened, even when native update hooks are disabled. This causes release/AOT Flutter applications to abort on GrapheneOS when the per-app Dynamic code loading from memory restriction is enabled.This came up while investigating ente/ente#3436.
Root cause
RawSqliteConnectionPool.openunconditionally creates an initializer with:Dart implements
NativeCallableby allocating an FFI callback trampoline page and changing it from writable to executable (Dart VM source). GrapheneOS denies thatmprotectoperation when in-memory dynamic code loading is restricted, and the Dart VM terminates the process. The failure is fatal and cannot be caught by the application.Setting
PoolConnections(enableNativeUpdateHooks: false)does not avoid this requirement: It disables the SQLite update hooks, but the initializerNativeCallableis still constructed beforepkg_sqlite3_connection_pool_openis called.Requested behavior
Could pool initialization support an AOT path that does not dynamically allocate executable callback trampolines?
Two possible directions appear to be:
Pointer.fromFunctioninitializer backed by carefully scoped isolate-local initialization state.Ideally,
openandopenAsyncwould retain their current multi-isolate, exception propagation and cleanup behavior while working when new anonymous executable mappings are prohibited.