Skip to content

rive_native: support environments that prohibit runtime executable memory for FFI callbacks #656

Description

@laurens-pilot

Submission checklist

  • This is present in the latest rive (0.14.11) and rive_native (0.1.11) packages.
  • I searched the documentation, forums, and existing issues.
  • I found no existing issue covering this.

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions