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 dbe88446e858b390f131ebc75d47611f678769cc..8840b6c9bc436dfbff801621046c7c7f5b315066 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 e8e26ac30e733d8b8dd12a16e6d19744c01d0a7a..1872e40b41e7c6e218513ce053c6a614c99ef787 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 2b217edcc8346fcf5994a2729637698a377ec715..33f31193c343d81ebb0793de3bd7553836ff3770 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 6fe160d1c545ef00989a426b0c383e80ad6bb877..b02f64464fdc250e97c1f6ccf3383c7b8fdd95b0 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 06c771bcd3eada114a5fc4aa1a13974a428e0fc6..f26b2dd721d32aac9aaee3bdb26b11c26e1b52d0 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 7a1dfe8e80ded883f46eb4c5ea197c14cef6f0ed..7b0bb9c9490a742f6fb6a8d965f9da8940de9fa1 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