Page MenuHomePhabricator

[native] refactor `clearSensitiveData()` method to static member of `SQLiteQueryExecutor`
ClosedPublic

Authored by kamil on Dec 22 2022, 2:32 AM.
Tags
None
Referenced Files
Unknown Object (File)
Sun, Apr 21, 10:09 PM
Unknown Object (File)
Sun, Apr 21, 10:09 PM
Unknown Object (File)
Sun, Apr 21, 10:09 PM
Unknown Object (File)
Sun, Apr 21, 10:07 PM
Unknown Object (File)
Thu, Apr 4, 11:43 AM
Unknown Object (File)
Wed, Apr 3, 2:28 AM
Unknown Object (File)
Wed, Apr 3, 2:28 AM
Unknown Object (File)
Wed, Apr 3, 2:28 AM
Subscribers

Details

Summary

Problem with previous approach:
Let's assume that some error occurs in db and migrate() function fails throwing an error. To fix that we want to delete the database file, but calling clearSensitiveData requires SQLiteQueryExecutor instance, which will call constructor, constructor will call migrate(), migrate() will fail, and we're in a dead loop where only uinstalling app by user will help.
To fix this we need to make this method work separate from SQLiteQueryExecutor instance.

I would take this even further and consider making this method part of DatabaseManager. It is not connected to the query executor instance, which by definition should operate on db schema, but performs action on db file. But we need to rethink how to share things like db path between these two classes so it'll require some more changes, I believe.

Test Plan

Build the app and logout to execute clearSensitiveData, this should behave the same way as previously

Diff Detail

Repository
rCOMM Comm
Lint
Lint Not Applicable
Unit
Tests Not Applicable

Event Timeline

kamil held this revision as a draft.
kamil published this revision for review.Dec 22 2022, 3:30 AM
marcin requested changes to this revision.Dec 23 2022, 2:39 AM

Two things:

  • "Let's assume that some error occurs in db and migrate() function fails throwing an error. To fix that we want to delete the database file, but calling clearSensitiveData requires SQLiteQueryExecutor instance, which will call constructor, constructor will call migrate(), migrate() will fail, and we're in a dead loop where only uinstalling app by user will help. To fix this we need to make this method work separate from SQLiteQueryExecutor instance."

    I am confused about this reasoning. migrate() is called when SQLiteQueryExecutor constructor is called, which is called exactly once for a thread (thread_local). Therefore calling clearSensitiveData via DatabaseManager::getQueryEXecutor() will not call SQLiteQueryExecutor constructor. Additionally making clearSensitiveData static does not solve the problem since it still calls migrate() at the end of its execution.
  • (Refer to line 1836 in` CommCoreModule`). As far as I am familiar with our codebase this shouldn't happen. One of the reasons we use DatabaseManager::getQueryExecutor() instead of directly calling ~SQLiteQueryExecutor` instance (kept as singleton) is to make CommCoreModule agnostic of the type of the database. Mixing agnostic and direct approaches to database query execution reduces code quality. Unfortunately C++ does not offer possibility to implement static virtual methods. If we actually need to be able to call SQLiteQueryExecutor constructor (if my reasoning from previous section is wrong) but do not want to call migrate, then we can overload it to take some boolean flag and check it as to whether migrate() should be called. Nevertheless if my reasoning is correct changes from this differential are not necessary.
This revision now requires changes to proceed.Dec 23 2022, 2:39 AM
kamil requested review of this revision.Dec 29 2022, 6:50 AM

I am confused about this reasoning. migrate() is called when SQLiteQueryExecutor constructor is called, which is called exactly once for a thread (thread_local). Therefore calling clearSensitiveData via DatabaseManager::getQueryEXecutor() will not call SQLiteQueryExecutor constructor.

That's not true.
According to the docs of thread_local: If the initialization throws an exception, the variable is not considered to be initialized, and initialization will be attempted again.
(sorry, I didn't linked this in summary but probably I should have done it). This means if the first call will throw an exception (see D5994) and as a result we will try do call clearSensitiveData which will call SQLiteQueryExecutor constructor, and the app will be stuck in a dead loop. Additionally I tested this and I can provide steps if you want to check it empirically.

Additionally making clearSensitiveData static does not solve the problem since it still calls migrate() at the end of
its execution.

I think it does since migrate() is now static (see D5988)

(Refer to line 1836 in` CommCoreModule`).

Did you mean line 1036?

As far as I am familiar with our codebase this shouldn't happen. One of the reasons we use DatabaseManager::getQueryExecutor() instead of directly calling ~SQLiteQueryExecutor` instance (kept as singleton) is to make CommCoreModule agnostic of the type of the database. Mixing agnostic and direct approaches to database query execution reduces code quality. Unfortunately C++ does not offer possibility to implement static virtual methods. If we actually need to be able to call SQLiteQueryExecutor constructor (if my reasoning from previous section is wrong) but do not want to call migrate, then we can overload it to take some boolean flag and check it as to whether migrate() should be called. Nevertheless if my reasoning is correct changes from this differential are not necessary.

I don't feel like calling SQLiteQueryExecutor constructor without migrate to avoid errors will be a good solution here, if we want to make it agnostic as you said we can move this method to DatabaseManager as I said in summary but it will require bigger refactor.

marcin added a reviewer: tomek.

That's not true.
According to the docs of thread_local: If the initialization throws an exception, the variable is not considered to be initialized, and initialization will be attempted again.
(sorry, I didn't linked this in summary but probably I should have done it). This means if the first call will throw an exception (see D5994) and as a result we will try do call clearSensitiveData which will call SQLiteQueryExecutor constructor, and the app will be stuck in a dead loop. Additionally I tested this and I can provide steps if you want to check it empirically.

This is convincing.

Additionally making clearSensitiveData static does not solve the problem since it still calls migrate() at the end of
its execution.

I think it does since migrate() is now static (see D5988)

What I was trying to say here is that I had doubts about reacting with clearSensitiveData to resolve problems that occurred during migrate since it calls migrate at the end. Not sure, why static migrate solves the problem here, but after thinking for a while I realised that that migrate called after clearSensitiveData is called on brand new database. With the fact that claerSensitiveData is static and migrate is not called before is (constructor is not called) it solves the problem.

I don't feel like calling SQLiteQueryExecutor constructor without migrate to avoid errors will be a good solution here, if we want to make it agnostic as you said we can move this method to DatabaseManager as I said in summary but it will require bigger refactor.

I think this refactor is really worth doing. Calling raw SQLiteQueryExecutor in the code where everything else happens via DatabaseManager is a kind of outlier. Doing this looks like we could actually remove this abstraction layer and refactor to use SQLiteQueryExecutor, SQLiteManager etc. directly. Nevertheless, I do not feel confident enough as a reviewer yet to request such change. So I am accepting this review so that @tomek can decide whether this refactor should be obligatory or left up to you.

I don't think that adding a flag to a constructor is a good idea. Calling migrate from constructor gives us a really strong guarantee that if an instance is created it will have all the migrations applied. By introducing a flag we would allow having partially initialized object - now all the instance methods would have to check how far the initialization went and if the can safely execute.

A lot of issues here come from the fact that this design uses objects but isn't really object oriented. For example, why do we have an instance of SQLiteQueryExecutor when all of its properties are static? A better approach would be to make them bound to instances - that will allow connecting to multiple databases, which should be possible with any database connection implementation. Regardless, this refactoring isn't required here and we can wait with it until we understand our system better.

Calling SQLiteQueryExecutor directly from CommCoreModule is indeed an issue, but we can easily work around it by exposing a static method on database manager which will call it instead. With almost all things being static and only a couple things interacting the perfect design isn't mandatory, as it is currently really easy to reason about the interactions, but still it makes sense to have a good plan. In this case we could have 3 layers: executor, manager and provider. An executor would be responsible for what it already does - executing the queries. A manager would provide an executor and will manage the db, e.g. delete / create a file where the db is stored. A provider would return a concrete instance of abstract manager. In the current implementation we hardcode executors concrete class, because we haven't got a good idea how to handle multiple implementation and haven't any need for that. Because of that, I don't think that introducing this third layer (provider) makes sense now, but we can assume that it will be introduced later.

So in summary, for me the only thing we have to change here is to introduce a static method on database manager that calls a static clear sensitive data on executor. Other architectural changes might be beneficial, but aren't really worth it now.

This revision is now accepted and ready to land.Jan 2 2023, 3:46 AM