Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[sqlcipher_flutter_libs] openCipherOnAndroid not working on Android 6 #55

Closed
ikbendewilliam opened this issue Oct 22, 2021 · 20 comments
Closed

Comments

@ikbendewilliam
Copy link

We received some reports that our app doesn't startup on some Android 6 devices. I've looked into it and found that the applyWorkaroundToOpenSqlCipherOnOldAndroidVersions function threw an error. (Null check operator used on a null value).
So I looked into it deeper and after searching for a while I found that there was an issue with the location of the libsqlcipher.so file on my emulator with Android 6.
If the file can't be found, you check /data/data/$appId/lib/libsqlcipher.so. I found the file however in /data/app/$appId-2/lib/x86_64/libsqlcipher.so. After some testing it was gone and appeared to be in /data/app/$appId-1/lib/x86_64/libsqlcipher.so. So the int seams to be different some times.
So I've put a try catch around applyWorkaroundToOpenSqlCipherOnOldAndroidVersions and reworked the function openCipherOnAndroid. This now works fine on my emulator

DynamicLibrary openCipherOnAndroid() {
  try {
    return DynamicLibrary.open('libsqlcipher.so');
  } catch (_) {
    // On some (especially old) Android devices, we somehow can't dlopen
    // libraries shipped with the apk. We need to find the full path of the
    // library (/data/data/<id>/lib/libsqlcipher.so) and open that one.
    // For details, see https://github.com/simolus3/moor/issues/420
    final appIdAsBytes = File('/proc/self/cmdline').readAsBytesSync();

    // app id ends with the first \0 character in here.
    final endOfAppId = max(appIdAsBytes.indexOf(0), 0);
    final appId = String.fromCharCodes(appIdAsBytes.sublist(0, endOfAppId));

    try {
      return DynamicLibrary.open('/data/data/$appId/lib/libsqlcipher.so');
    } catch (_) {
      return openCipherOnAndroidAt(appId, 0);
    }
  }
}

DynamicLibrary openCipherOnAndroidAt(String appId, int i) {
  if (i > 9) throw ArgumentError('db not found in data/app');
  try {
    return DynamicLibrary.open('/data/app/$appId-$i/lib/x86_64/libsqlcipher.so');
  } catch (_) {
    return openCipherOnAndroidAt(appId, i + 1);
  }
}
@simolus3
Copy link
Owner

What the hell is Android doing here? 😧 But it's interesting to know that the object gets put there...

applyWorkaroundToOpenSqlCipherOnOldAndroidVersions function threw an error. (Null check operator used on a null value).

Are you using the function before runApp by any chance? It sounds like a missed initialization on the Flutter method channel, are you calling FlutterWidgetsBinding.ensureInitialized()? But even then it's weird that this only fails on some devices... Do you have a stack trace by any chance?

@ikbendewilliam
Copy link
Author

I'm not sure... 🙈
Before we do anything, we do WidgetsFlutterBinding.ensureInitialized();, so the function is called after this, but before the runApp() (inside a preResolve).

  @singleton
  @preResolve
  Future<DatabaseConnection> provideDatabaseConnection(...) async {
    ...
    await Isolate.spawn(
      _startBackground, <--- This function calls the applyWorkaroundToOpenSqlCipherOnOldAndroidVersions
     ...
    );

The stacktrace is

E/flutter (10337): [ERROR:flutter/runtime/dart_isolate.cc(1137)] Unhandled exception:
E/flutter (10337): Null check operator used on a null value
E/flutter (10337): #0      MethodChannel.binaryMessenger package:flutter/…/services/platform_channel.dart:142
E/flutter (10337): #1      MethodChannel._invokeMethod package:flutter/…/services/platform_channel.dart:148
E/flutter (10337): #2      MethodChannel.invokeMethod package:flutter/…/services/platform_channel.dart:331
E/flutter (10337): #3      applyWorkaroundToOpenSqlCipherOnOldAndroidVersions package:sqlcipher_flutter_libs/sqlcipher_flutter_libs.dart:34
E/flutter (10337): #4      _startBackground package:our_package/di/injectable.dart:156
E/flutter (10337): #5      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:286:17)
E/flutter (10337): #6      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)

The reason that this only fails on some devices is that the workaround only calls invokeMethod when the DynamicLibrary.open('libsqlcipher.so'); fails. And for most devices, opening the file is not an issue.

Future<void> applyWorkaroundToOpenSqlCipherOnOldAndroidVersions() async {
  if (!Platform.isAndroid) return;
  try {
    DynamicLibrary.open('libsqlcipher.so');
  } on ArgumentError {
    // Ok, the regular approach failed. Try to open sqlite3 in Java, which seems
    // to fix the problem.
    await _platform.invokeMethod('doesnt_matter');

    // Try again. If it still fails we're out of luck.
    DynamicLibrary.open('libsqlcipher.so');
  }
}

I'll try removing the preresolve next week, but this has quite a few implications in the app and will still result in using a workaround, which I don't really like 🤔 (not that my suggestion is fool-proof 😬)

@simolus3
Copy link
Owner

From the stack trace, it looks like you're opening the native library in a new isolate. That's completely fine, but it means that you also need to call FlutterWidgetsBinding.ensureInitialized() in _startBackground. Essentially, the binding (and the messenger) need to be initialized on each isolate separately.

If possible, an alternative would be to always open the library in the main isolate (even if it's not used there). Once opened correctly, the default path attempting to open libsqlcipher.so in a new isolate will work too.

@vanlooverenkoen
Copy link

vanlooverenkoen commented Oct 25, 2021

I think it has something to do with aab files. I built the apk and installed it with adb. And everything works fine. If I download the app from the playstore. -> AAB -> the database doesn't work.

On a side note. We are also using obfuscation in dart & android. But again that works perfectly when building the aab files.

I am using a Nexus 5 (Phone) & Nexus 7 (Tablet). Both have this issue (reproducable when using the playstore)

@vanlooverenkoen
Copy link

vanlooverenkoen commented Oct 25, 2021

@simolus3 We do have this in our build.gradle

release {
...
      ndk {
          abiFilters 'armeabi-v7a','arm64-v8a','x86_64', 'x86'
      }
...
}

Could that result in issues?

@simolus3
Copy link
Owner

The abiFilters should not be a problem (but are you sure that you're actually doing a Flutter release build on 32-bit x86? That would require some hacks).

App bundles changing the location of the actual library could explain this. Anyway, did you try using applyWorkaroundToOpenSqlCipherOnOldAndroidVersions with FlutterWidgetsBinding.ensureInitialized() in the background isolate? AFAIK that should also fix the problem.

@vanlooverenkoen
Copy link

I am building to the playstore internal test because that is the only place where I can reproduce this issue. The FlutterWidgetsBinding.ensureInitialized() should be the first thing we do in the _startBackground right? I did not see this in your isolate/encryption guide.

@simolus3
Copy link
Owner

Yes, calling ensureInitialized before applyWorkaroundToOpenSqlCipherOnOldAndroidVersions is necessary for each isolate. So doing that as a first call in _startBackground can't hurt :D

I think there were some general issues with using platform channels in background isolates, but AFAIK most of them have been resolved.

@vanlooverenkoen
Copy link

That will be WidgetsFlutterBinding.ensureInitialized(); instead of FlutterWidgetsBinding.ensureInitialized() right :D

@vanlooverenkoen
Copy link

WidgetsFlutterBinding.ensureInitialized(); does not work. The app get stuck at startup. (When creating the dependency tree, I will try this in debug to get a stacktrace but in release (playstore I do not get a stacktrace)

@vanlooverenkoen
Copy link

vanlooverenkoen commented Oct 25, 2021

This is the error I got (After deobfuscation)

10-25 17:32:20.629 11211-11300/? E/flutter: [ERROR:flutter/runtime/dart_isolate.cc(1137)] Unhandled exception:
    LateInitializationError: Field '_tbc@15065589' has not been initialized.
    Warning: This VM has been configured to produce stack traces that violate the Dart standard.
    *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
    pid: 11211, tid: 11300, name DartWorker
    build_id: '4ff9bfa0daa533efd010fb425f5e2abb'
    isolate_dso_base: 9f5ae000, vm_dso_base: 9f5ae000
    isolate_instructions: 9f5ba000, vm_instructions: 9f5b0000
    #0      PlatformDispatcher.initialLifecycleState (dart:ui/platform_dispatcher.dart)
    #1      SingletonFlutterWindow.initialLifecycleState (dart:ui/window.dart:399:58)
    #2      ServicesBinding.readInitialLifecycleStateFromNativeWindow (package:flutter/src/services/binding.dart:185:71)
    #3      ServicesBinding.initInstances (package:flutter/src/services/binding.dart:35:5)
    #4      PaintingBinding.initInstances (package:flutter/src/painting/binding.dart:22:11)
    #5      SemanticsBinding.initInstances (package:flutter/src/semantics/binding.dart:22:11)
    #6      RendererBinding.initInstances (package:flutter/src/rendering/binding.dart:28:11)
    #7      WidgetsBinding.initInstances (package:flutter/src/widgets/binding.dart:277:11)
    #8      new BindingBase (package:flutter/src/foundation/binding.dart:56:5)
    #9      new _WidgetsFlutterBinding&BindingBase&GestureBinding (package:flutter/src/widgets/binding.dart)
    #10     new _WidgetsFlutterBinding&BindingBase&GestureBinding&SchedulerBinding (package:flutter/src/widgets/binding.dart)
    #11     new _WidgetsFlutterBinding&BindingBase&GestureBinding&SchedulerBinding&ServicesBinding&PaintingBinding (package:flutter/src/widgets/binding.dart)
    #12     new _WidgetsFlutterBinding&BindingBase&GestureBinding&SchedulerBinding&ServicesBinding&PaintingBinding&SemanticsBinding (package:flutter/src/widgets/binding.dart)
    #13     new Completer (dart:async/future.dart)
    #14     new _WidgetsFlutterBinding&BindingBase&GestureBinding&SchedulerBinding&ServicesBinding&PaintingBinding&SemanticsBinding&RendererBinding&WidgetsBinding (package:flutter/src/widgets/binding.dart:704:48)
    #15     WidgetsFlutterBinding.ensureInitialized (package:flutter/src/widgets/binding.dart)
    #16     _startBackground (package:our_app/di/injectable.dart:153:25)
    #17     _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:286:17)
    #18     _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)

late String _initialLifecycleState; is still null

@vanlooverenkoen
Copy link

It has something to do with the aab files. If I build with --split-apk it works perfectly. If I build an aab and upload that one to the playstore it is broken.

@simolus3
Copy link
Owner

That's unfortunate. Do you start a main UI isolate before starting the background isolate? If you have a chance to await applyWorkaroundToOpenSqlCipherOnOldAndroidVersions on the main isolate before starting the background isolate, it might work because the loaded libraries are shared across threads.

There's a bit more information on this workaround here: simolus3/drift#895 (comment)

@vanlooverenkoen
Copy link

I will try that tomorrow. I have been working on this today for more than 9 hours. I couldn't think straight anymore. 🤞🤞🤞

@vanlooverenkoen
Copy link

The applyWorkaroundToOpenSqlCipherOnOldAndroidVersions should be called in the _startBackground or before?

@simolus3
Copy link
Owner

Try calling it before. So in the main isolate before you do Isolate.spawn(_startBackground).

@vanlooverenkoen
Copy link

vanlooverenkoen commented Oct 26, 2021

If we only call applyWorkaroundToOpenSqlCipherOnOldAndroidVersions before we start the new isolate it doesn't work.

We now call applyWorkaroundToOpenSqlCipherOnOldAndroidVersions before the start of the isolate. And in _startBackground be do this:

Future<void> _startBackground(_IsolateStartRequest request) async {
  if (Platform.isIOS) {
    open.overrideFor(OperatingSystem.iOS, () => DynamicLibrary.process());
  } else if (Platform.isAndroid) {
    await applyWorkaroundToOpenSqlCipherOnOldAndroidVersions();
    open.overrideFor(OperatingSystem.android, openCipherOnAndroid);
  }
  final executor = VmDatabase(File(request.targetPath), setup: (db) => db.execute("PRAGMA key = '${request.password}'"));
  final moorIsolate = MoorIsolate.inCurrent(() => DatabaseConnection.fromExecutor(executor));
  request.sendMoorIsolate.send(moorIsolate);
}

Because calling WidgetsFlutterBinding.ensureInitialized(); crashes as well. We don't do that in _startBackground

@ikbendewilliam
Copy link
Author

That fixed it and a new release is in the make. Thank you for your support!

@vanlooverenkoen
Copy link

Yes indeed! Thanks again for your support. We do think that this step should be documented somewhere.
When to call applyWorkaroundToOpenSqlCipherOnOldAndroidVersions with isolate
And when to call applyWorkaroundToOpenSqlCipherOnOldAndroidVersions without isolate

@simolus3
Copy link
Owner

I have added matching warnings to the readme in f05f5d6.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants