Attempt database fallback recovery on some schema errors 47/188047/3
authorKonrad Lipinski <k.lipinski2@partner.samsung.com>
Thu, 30 Aug 2018 14:04:38 +0000 (16:04 +0200)
committerKonrad Lipinski <k.lipinski2@partner.samsung.com>
Fri, 31 Aug 2018 11:03:34 +0000 (13:03 +0200)
Done per HQ request for extra robustness in the face of unforeseen
database corruption.

Schema error detection amounts to preparing sqlite query templates. It
takes place at the end of database connection bringup (once the database
is verified to be up to date and passes integrity checks) by means of
calling sqlite3_prepare_v2 for every statement template ever to be used
at runtime. Sqlite statement compilation may fail due to lack of schema
compatibility.  If such a failure occurs, fallback recovery is attempted
unless already tried.

Change-Id: I6ef8a262f8db11552f3e92ed3a601227558c3899

packaging/security-manager.spec
src/common/include/privilege_db.h
src/common/privilege_db.cpp
test/privilege_db_fixture.cpp
test/privilege_db_fixture.h
test/test_privilege_db_migration.cpp

index dbe8844..8840b6c 100644 (file)
@@ -149,6 +149,8 @@ dd bs=1K count=$(($(stat -c%s %{buildroot}/%{db_test_dir}/.security-manager-test
 cp -a %{buildroot}/%{db_test_dir}/.security-manager-test.db-journal %{buildroot}/%{db_test_dir}/.security-manager-test-corrupted.db-journal
 echo -n > %{buildroot}/%{db_test_dir}/.security-manager-test-empty.db
 echo -n > %{buildroot}/%{db_test_dir}/.security-manager-test-empty.db-journal
+sqlite3 %{buildroot}/%{db_test_dir}/.security-manager-test-wrong-schema.db < db/db.sql
+sqlite3 %{buildroot}/%{db_test_dir}/.security-manager-test-wrong-schema.db "drop view client_license_view"
 
 cp -a %{SOURCE1} %{SOURCE3} %{SOURCE4} %{SOURCE5} %{buildroot}%{_datadir}/
 
@@ -244,6 +246,8 @@ chsmack -a System %{db_test_dir}/.security-manager-test-corrupted.db
 chsmack -a System %{db_test_dir}/.security-manager-test-corrupted.db-journal
 chsmack -a System %{db_test_dir}/.security-manager-test-empty.db
 chsmack -a System %{db_test_dir}/.security-manager-test-empty.db-journal
+chsmack -a System %{db_test_dir}/.security-manager-test-wrong-schema.db
+chsmack -a System %{db_test_dir}/.security-manager-test-wrong-schema.db-journal
 
 %files -n security-manager
 %manifest %{_datadir}/security-manager.manifest
@@ -313,6 +317,8 @@ chsmack -a System %{db_test_dir}/.security-manager-test-empty.db-journal
 %attr(0600,root,root) %{db_test_dir}/.security-manager-test-corrupted.db-journal
 %attr(0600,root,root) %{db_test_dir}/.security-manager-test-empty.db
 %attr(0600,root,root) %{db_test_dir}/.security-manager-test-empty.db-journal
+%attr(0600,root,root) %{db_test_dir}/.security-manager-test-wrong-schema.db
+%attr(0600,root,root) %{db_test_dir}/.security-manager-test-wrong-schema.db-journal
 
 %files -n security-license-manager
 %{_libdir}/cynara/plugin/client/liblicense-manager-plugin-client.so
index e8e26ac..1872e40 100644 (file)
@@ -48,7 +48,8 @@
 namespace SecurityManager {
 
 std::string genJournalPath(const std::string &dbPath);
-void initDb();
+enum class InitDbDidFallback : uint8_t { no, yes };
+InitDbDidFallback initDb();
 
 enum class StmtType : uint8_t {
     EAddApplication,
index 2b217ed..33f3119 100644 (file)
@@ -215,7 +215,7 @@ void applyFallbackDb(DB::SqlConnection &conn, const std::string &dbPath, const s
     tryCatchDbInit([&]{ connectMigrateVerify(conn, dbPath); });
 }
 
-void initDb(DB::SqlConnection &conn, const std::string &path, const std::string &roFallbackPath) {
+InitDbDidFallback initDb(DB::SqlConnection &conn, const std::string &path, const std::string &roFallbackPath) {
     removeRecoveryFlagFile(path);
     if (!FS::fileStatus(path)) {
         createRecoveryFlagFile(path);
@@ -223,12 +223,14 @@ void initDb(DB::SqlConnection &conn, const std::string &path, const std::string
         applyFallbackDb(conn, path, roFallbackPath);
     } else try {
         connectMigrateVerify(conn, path);
+        return InitDbDidFallback::no;
     } catch (DB::SqlConnection::Exception::Base &e) {
         createRecoveryFlagFile(path);
         LogError("Database initialization error (" << e.DumpToString() << "), attempting fallback");
         tryCatchDbInit([&]{ conn.Disconnect(); });
         applyFallbackDb(conn, path, roFallbackPath);
     }
+    return InitDbDidFallback::yes;
 }
 } //namespace
 
@@ -236,9 +238,9 @@ std::string genJournalPath(const std::string &dbPath) {
     return dbPath + "-journal";
 }
 
-void initDb() {
+InitDbDidFallback initDb() {
     DB::SqlConnection conn;
-    initDb(conn, Config::privilegeDbPath, Config::privilegeDbFallbackPath);
+    return initDb(conn, Config::privilegeDbPath, Config::privilegeDbFallbackPath);
 }
 
 PrivilegeDb::PrivilegeDb()
@@ -248,14 +250,35 @@ PrivilegeDb::PrivilegeDb()
 
 PrivilegeDb::PrivilegeDb(const std::string &path, const std::string &roFallbackPath)
 {
-    initDb(mSqlConnection, path, roFallbackPath);
-    tryCatchDbInit([&]{ initDataCommands(); });
+    const auto didFallback = initDb(mSqlConnection, path, roFallbackPath);
+    try {
+        tryCatchDbInit([&]{ initDataCommands(); });
+    } catch (...) {
+        createRecoveryFlagFile(path);
+        switch (didFallback) {
+            case InitDbDidFallback::no:
+                LogError("Database initialization error during query preparation - attempting fallback");
+                tryCatchDbInit([&]{ mSqlConnection.Disconnect(); });
+                applyFallbackDb(mSqlConnection, path, roFallbackPath);
+                tryCatchDbInit([&]{ initDataCommands(); });
+                break;
+            case InitDbDidFallback::yes:
+                throwDbInitEx("Database initialization error during query preparation on fallback db - giving up");
+        }
+    }
 }
 
 void PrivilegeDb::initDataCommands()
 {
-    for (size_t i = 0; i < StmtTypeCount; i++)
-        m_commands[i] = prepare(mSqlConnection, g_queries[i]);
+    for (size_t i = 0; i < StmtTypeCount; i++) {
+        try {
+            m_commands[i] = prepare(mSqlConnection, g_queries[i]);
+        } catch (...) {
+            for (size_t j = 0; j < i; j++)
+                m_commands[j].reset();
+            throw;
+        }
+    }
 }
 
 PrivilegeDb::StatementWrapper::StatementWrapper(DB::SqlConnection::DataCommandAutoPtr &ref)
index 6fe160d..b02f644 100644 (file)
@@ -79,11 +79,11 @@ PrivilegeDBFixture::PrivilegeDBFixture(const std::string &src, const std::string
 
 PrivilegeDBFixture::~PrivilegeDBFixture()
 {
+    delete testPrivDb;
     BOOST_REQUIRE_MESSAGE(remove(dbPath.c_str()) == 0, "Could not delete test database file: " << dbPath);
     auto journalPath = genJournalPath(dbPath);
     BOOST_REQUIRE_MESSAGE(remove(journalPath.c_str()) == 0, "Could not delete test database journal file: " << journalPath);
     BOOST_REQUIRE(!FS::fileStatus(Config::dbRecoveryFlagFileName(dbPath)));
-    delete testPrivDb;
 }
 
 PrivilegeDb* PrivilegeDBFixture::getPrivDb() {
index 06c771b..f26b2dd 100644 (file)
@@ -27,6 +27,7 @@
 #define PRIVILEGE_DB_EXAMPLE_V0 DB_TEST_DIR"/.security-manager-test-v0.db"
 #define PRIVILEGE_DB_CORRUPTED DB_TEST_DIR"/.security-manager-test-corrupted.db"
 #define PRIVILEGE_DB_EMPTY DB_TEST_DIR"/.security-manager-test-empty.db"
+#define PRIVILEGE_DB_WRONG_SCHEMA DB_TEST_DIR"/.security-manager-test-wrong-schema.db"
 
 #define TEST_PRIVILEGE_DB_PATH "/tmp/.security-manager-test.db"
 #define TEST_PRIVILEGE_DB_PATH_2 "/tmp/.security-manager-test-2.db"
index 7a1dfe8..7b0bb9c 100644 (file)
@@ -53,6 +53,8 @@ public:
     }
 };
 
+struct Empty {};
+
 struct PrivilegeV0DBFixture : PrivilegeDBFixture {
     PrivilegeV0DBFixture() : PrivilegeDBFixture(PRIVILEGE_DB_EXAMPLE_V0) {}
 };
@@ -68,6 +70,40 @@ struct PrivilegeFallbackV0DBFixture : PrivilegeDBFixture {
 struct PrivilegeFallbackEmptyDBFixture : PrivilegeDBFixture {
     PrivilegeFallbackEmptyDBFixture() : PrivilegeDBFixture(PRIVILEGE_DB_CORRUPTED, PRIVILEGE_DB_EMPTY, HaveBrokenFlagFile::yes) {}
 };
+struct PrivilegeFallbackWrongDBFixture : PrivilegeDBFixture {
+    PrivilegeFallbackWrongDBFixture() : PrivilegeDBFixture(PRIVILEGE_DB_WRONG_SCHEMA, PRIVILEGE_DB_TEMPLATE, HaveBrokenFlagFile::yes) {}
+};
+struct PrivilegeFallbackWrongV0DBFixture : PrivilegeDBFixture {
+    PrivilegeFallbackWrongV0DBFixture() : PrivilegeDBFixture(PRIVILEGE_DB_WRONG_SCHEMA, PRIVILEGE_DB_EXAMPLE_V0, HaveBrokenFlagFile::yes) {}
+};
+struct PrivilegeFallbackWrongEmptyDBFixture : PrivilegeDBFixture {
+    PrivilegeFallbackWrongEmptyDBFixture() : PrivilegeDBFixture(PRIVILEGE_DB_WRONG_SCHEMA, PRIVILEGE_DB_EMPTY, HaveBrokenFlagFile::yes) {}
+};
+
+void testFallback() {
+    requireTestDbContents(PRIVILEGE_DB_TEMPLATE);
+    BOOST_REQUIRE(!FS::fileSize(genJournalPath(TEST_PRIVILEGE_DB_PATH)));
+}
+void testFallbackSchemaApplication() {
+    requireTestDbAndJournalContents(PRIVILEGE_DB_TEMPLATE);
+}
+void testFallbackMigration() {
+    PrivilegeDBFixture v0(PRIVILEGE_DB_EXAMPLE_V0, PRIVILEGE_DB_TEMPLATE,
+            PrivilegeDBFixture::HaveBrokenFlagFile::no, TEST_PRIVILEGE_DB_PATH_2);
+    requireTestDbContents(TEST_PRIVILEGE_DB_PATH_2);
+}
+void testFallbackMissingMigration() {
+    const std::string missingDbPath("/tmp/thisNotExists.db");
+    requireNoDb(missingDbPath);
+    const auto missingDbPathJournal = genJournalPath(missingDbPath);
+    const auto missingDbPathFlag = Config::dbRecoveryFlagFileName(missingDbPath);
+    BOOST_REQUIRE_NO_THROW(PrivilegeDb(missingDbPath, PRIVILEGE_DB_EXAMPLE_V0));
+    requireTestDbContents(missingDbPath);
+    BOOST_REQUIRE(!remove(missingDbPath.c_str()));
+    BOOST_REQUIRE(!remove(missingDbPathJournal.c_str()));
+    BOOST_REQUIRE(!FS::fileSize(missingDbPathFlag));
+    BOOST_REQUIRE(!remove(missingDbPathFlag.c_str()));
+}
 } //namespace
 
 BOOST_GLOBAL_FIXTURE(TestConfig)
@@ -80,40 +116,71 @@ BOOST_AUTO_TEST_SUITE_END()
 
 BOOST_FIXTURE_TEST_SUITE(PRIVILEGE_DB_TEST_FALLBACK, PrivilegeFallbackDBFixture)
 BOOST_AUTO_TEST_CASE(T1510_fallback) {
-    requireTestDbContents(PRIVILEGE_DB_TEMPLATE);
-    BOOST_REQUIRE(!FS::fileSize(genJournalPath(TEST_PRIVILEGE_DB_PATH)));
+    testFallback();
 }
 BOOST_AUTO_TEST_SUITE_END()
 
 BOOST_FIXTURE_TEST_SUITE(PRIVILEGE_DB_TEST_FALLBACK_EMPTY, PrivilegeFallbackEmptyDBFixture)
 BOOST_AUTO_TEST_CASE(T1520_fallback_schema_application) {
-    requireTestDbAndJournalContents(PRIVILEGE_DB_TEMPLATE);
+    testFallbackSchemaApplication();
 }
 BOOST_AUTO_TEST_SUITE_END()
 
 BOOST_FIXTURE_TEST_SUITE(PRIVILEGE_DB_TEST_FALLBACK_V0, PrivilegeFallbackV0DBFixture)
 
 BOOST_AUTO_TEST_CASE(T1530_fallback_migration) {
-    PrivilegeDBFixture v0(PRIVILEGE_DB_EXAMPLE_V0, PRIVILEGE_DB_TEMPLATE,
-            PrivilegeDBFixture::HaveBrokenFlagFile::no, TEST_PRIVILEGE_DB_PATH_2);
-    requireTestDbContents(TEST_PRIVILEGE_DB_PATH_2);
+    testFallbackMigration();
 }
 
 BOOST_AUTO_TEST_CASE(T1540_db_missing_fallback_migration) {
-    const std::string missingDbPath("/tmp/thisNotExists.db");
-    requireNoDb(missingDbPath);
-    const auto missingDbPathJournal = genJournalPath(missingDbPath);
-    const auto missingDbPathFlag = Config::dbRecoveryFlagFileName(missingDbPath);
-    BOOST_REQUIRE_NO_THROW(PrivilegeDb(missingDbPath, PRIVILEGE_DB_EXAMPLE_V0));
-    requireTestDbContents(missingDbPath);
-    BOOST_REQUIRE(!remove(missingDbPath.c_str()));
-    BOOST_REQUIRE(!remove(missingDbPathJournal.c_str()));
-    BOOST_REQUIRE(!FS::fileSize(missingDbPathFlag));
-    BOOST_REQUIRE(!remove(missingDbPathFlag.c_str()));
+    testFallbackMissingMigration();
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_FIXTURE_TEST_SUITE(PRIVILEGE_DB_TEST_FALLBACK_WRONG, PrivilegeFallbackWrongDBFixture)
+BOOST_AUTO_TEST_CASE(T1550_fallback_wrong) {
+    testFallback();
+}
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_FIXTURE_TEST_SUITE(PRIVILEGE_DB_TEST_FALLBACK_WRONG_EMPTY, PrivilegeFallbackWrongEmptyDBFixture)
+BOOST_AUTO_TEST_CASE(T1560_fallback_wrong_schema_application) {
+    testFallbackSchemaApplication();
+}
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_FIXTURE_TEST_SUITE(PRIVILEGE_DB_TEST_FALLBACK_WRONG_V0, PrivilegeFallbackWrongV0DBFixture)
+
+BOOST_AUTO_TEST_CASE(T1570_fallback_wrong_migration) {
+    testFallbackMigration();
+}
+
+BOOST_AUTO_TEST_CASE(T1580_db_missing_fallback_wrong_migration) {
+    testFallbackMissingMigration();
 }
 
 BOOST_AUTO_TEST_SUITE_END()
 
+BOOST_FIXTURE_TEST_SUITE(PRIVILEGE_DB_TEST_FALLBACK_MIGRATION_FAILURE, Empty)
+BOOST_AUTO_TEST_CASE(T1590_fallback_migration_failure) {
+    const auto go = [](const char *db, const char *fallback){
+        std::unique_ptr<PrivilegeDBFixture> fix;
+        BOOST_REQUIRE_THROW(fix.reset(new PrivilegeDBFixture(db, fallback, PrivilegeDBFixture::HaveBrokenFlagFile::yes)), PrivilegeDb::Exception::IOError);
+        fix.reset();
+        BOOST_REQUIRE(!remove(TEST_PRIVILEGE_DB_PATH));
+        auto journal = genJournalPath(TEST_PRIVILEGE_DB_PATH);
+        BOOST_REQUIRE(!remove(journal.c_str()));
+        auto recovered = Config::dbRecoveryFlagFileName(TEST_PRIVILEGE_DB_PATH);
+        BOOST_REQUIRE(!remove(recovered.c_str()));
+    };
+    go(PRIVILEGE_DB_CORRUPTED, PRIVILEGE_DB_CORRUPTED);
+    go(PRIVILEGE_DB_CORRUPTED, PRIVILEGE_DB_WRONG_SCHEMA);
+    go(PRIVILEGE_DB_WRONG_SCHEMA, PRIVILEGE_DB_CORRUPTED);
+    go(PRIVILEGE_DB_WRONG_SCHEMA, PRIVILEGE_DB_WRONG_SCHEMA);
+}
+BOOST_AUTO_TEST_SUITE_END()
+
 //TODO
 // Tests below are arbitrarily copy-pasted from test_privilege_db_transaction.cpp
 // but applied to a migrated V0 database instead. The pragmatic way of doing this