Submission checklist
Description
Rive Flutter requires the Dart VM to allocate executable memory for FFI callback trampolines. On GrapheneOS, an app using Rive aborts when the per-app Dynamic code loading from memory restriction is enabled:
virtual_memory_posix.cc: error: mprotect failed: 13 (Permission denied)
This came up while investigating ente/ente#3436. The equivalent blocker in sqlite3.dart was fixed in simolus3/sqlite3.dart#396.
Rive-specific cause
Normal Rive usage reaches Dart FFI callbacks, not only optional scripting APIs:
FFIRiveFile in rive_native/lib/src/ffi/rive_ffi.dart registers the view-model callbacks with Pointer.fromFunction when a .riv file is decoded.
FFIFlutterFactory in rive_native/lib/src/ffi/flutter_renderer_ffi.dart registers its renderer callbacks with Pointer.fromFunction.
- Other APIs use
NativeCallable, including state-machine, asset-loader, scripting, and Luau callbacks.
Despite accepting only static functions, Pointer.fromFunction uses the same Dart VM FFI callback trampoline mechanism as NativeCallable, as confirmed in sqlite3.dart#396. The VM allocates a trampoline page and changes it to executable (Dart VM source). When that operation is prohibited, the failure is fatal and cannot be caught by the Flutter application.
Reproduction
A Linux machine with systemd can reproduce the restriction without GrapheneOS. In a minimal Flutter app, initialize Rive and decode a bundled .riv file:
await rive.RiveNative.init();
final data = await rootBundle.load('assets/example.riv');
await rive.File.decode(
data.buffer.asUint8List(),
riveFactory: rive.Factory.flutter,
);
Build the Linux application, then run it with executable-memory creation disabled:
flutter build linux --release
systemd-run --user --scope --property=MemoryDenyWriteExecute=yes \
./build/linux/x64/release/bundle/<app-name>
Loading the Rive file triggers the mprotect failure. The same restriction is exposed on GrapheneOS as Dynamic code loading from memory.
Expected behavior
Rive Flutter should provide a path that does not require Dart FFI callback trampolines or newly executable anonymous mappings, so ordinary .riv loading and rendering work under MemoryDenyWriteExecute=yes and the equivalent GrapheneOS restriction.
Replacing NativeCallable with Pointer.fromFunction is not sufficient; the callback boundary needs a statically compiled native bridge or another design that avoids Dart FFI callbacks requiring runtime executable memory.
Environment
rive: 0.14.11
rive_native: 0.1.11
- Flutter:
3.38.10
- Dart:
3.10.9
- Affected: Android with GrapheneOS memory DCL disabled; reproducible on Linux with
MemoryDenyWriteExecute=yes
Submission checklist
rive(0.14.11) andrive_native(0.1.11) packages.Description
Rive Flutter requires the Dart VM to allocate executable memory for FFI callback trampolines. On GrapheneOS, an app using Rive aborts when the per-app Dynamic code loading from memory restriction is enabled:
This came up while investigating ente/ente#3436. The equivalent blocker in
sqlite3.dartwas fixed in simolus3/sqlite3.dart#396.Rive-specific cause
Normal Rive usage reaches Dart FFI callbacks, not only optional scripting APIs:
FFIRiveFileinrive_native/lib/src/ffi/rive_ffi.dartregisters the view-model callbacks withPointer.fromFunctionwhen a.rivfile is decoded.FFIFlutterFactoryinrive_native/lib/src/ffi/flutter_renderer_ffi.dartregisters its renderer callbacks withPointer.fromFunction.NativeCallable, including state-machine, asset-loader, scripting, and Luau callbacks.Despite accepting only static functions,
Pointer.fromFunctionuses the same Dart VM FFI callback trampoline mechanism asNativeCallable, as confirmed in sqlite3.dart#396. The VM allocates a trampoline page and changes it to executable (Dart VM source). When that operation is prohibited, the failure is fatal and cannot be caught by the Flutter application.Reproduction
A Linux machine with systemd can reproduce the restriction without GrapheneOS. In a minimal Flutter app, initialize Rive and decode a bundled
.rivfile:Build the Linux application, then run it with executable-memory creation disabled:
Loading the Rive file triggers the
mprotectfailure. The same restriction is exposed on GrapheneOS as Dynamic code loading from memory.Expected behavior
Rive Flutter should provide a path that does not require Dart FFI callback trampolines or newly executable anonymous mappings, so ordinary
.rivloading and rendering work underMemoryDenyWriteExecute=yesand the equivalent GrapheneOS restriction.Replacing
NativeCallablewithPointer.fromFunctionis not sufficient; the callback boundary needs a statically compiled native bridge or another design that avoids Dart FFI callbacks requiring runtime executable memory.Environment
rive:0.14.11rive_native:0.1.113.38.103.10.9MemoryDenyWriteExecute=yes