From 87fc1b369416b02edb9c534d2e5de71129a48a54 Mon Sep 17 00:00:00 2001 From: zhangsheng Date: Tue, 29 Sep 2026 18:15:56 +0800 Subject: [PATCH] fix: detect abnormal exit via sentinel for filename index MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Root cause: when the index service is killed by SIGKILL, crash, or OOM while in Clean state with no running tasks, cleanup() is never called and the state remains Clean — on restart no recovery is triggered, silently losing all file changes during the downtime 2. Fix: create a sentinel file at startup in the index directory and remove it during normal cleanup(); if the sentinel persists across a restart, the previous exit was abnormal, so mark state Dirty to trigger a compensating full Update via the existing recovery path 3. Harden sentinelFilePath() to return empty when indexDir is empty, preventing operations on the filesystem root when indexDir is unset 4. Abort service construction when DBus registerService fails, preventing multi-instance scenarios that could corrupt the sentinel file; unregister already-registered services on partial failure to avoid blocking legitimate instances 5. Check registerIndexServices() return value in main() and exit on failure instead of continuing with unregistered DBus services 6. Use open() with O_NOFOLLOW to create the sentinel file, preventing symlink-following that could truncate an arbitrary file if an attacker pre-placed a symlink in the index directory 7. Guard cleanup() with QFile::exists() before QFile::remove() to avoid false warnings when the sentinel file is already absent 8. Impact: FileNameIndexDBus and serviceentry are affected; TextIndex and OcrIndex are unchanged per the filename-index completeness need Influence: 1. Test normal service restart — sentinel file should be removed and no false Dirty state on next startup 2. Test SIGKILL during Clean idle state — sentinel persists, restart triggers full Update covering all missed file changes 3. Verify no regression on existing Dirty state recovery path 4. Test service startup with empty indexDir — no root-level file ops 5. Test second instance startup — should abort, not corrupt sentinel 6. Test DBus unavailable — main() should exit, not run unregistered 7. Test symlink in indexDir — sentinel creation must not follow it fix: 通过哨兵文件检测文件名索引服务异常退出 1. 根因:索引服务在 Clean 空闲状态下被 SIGKILL、崩溃或 OOM 杀死时, cleanup() 未被调用,状态保持 Clean,重启后不触发恢复,退出期间的 文件变更静默丢失 2. 方案:启动时在索引目录创建哨兵文件,正常退出时删除;若重启时哨兵 文件仍在,说明上次异常退出,标记 Dirty 状态触发已有恢复路径的 全量 Update 补偿丢失的文件变更 3. 加固 sentinelFilePath() 在 indexDir 为空时返回空字符串,避免 indexDir 未设置时操作文件系统根目录 4. DBus registerService 失败时中止服务构造,防止多实例场景下 哨兵文件被互相覆盖或删除;部分注册失败时注销已注册的服务, 避免阻塞合法实例 5. main() 检查 registerIndexServices() 返回值,失败时退出,不再 在 DBus 服务未注册的情况下继续运行 6. 使用 open() 配合 O_NOFOLLOW 标志创建哨兵文件,防止攻击者在 索引目录预置符号链接导致任意文件被截断 7. cleanup() 删除哨兵文件前先检查 QFile::exists(),避免文件 不存在时误报警告 8. 影响:修改 FileNameIndexDBus 和 serviceentry,TextIndex 和 OcrIndex 不变,符合仅文件名索引需要完整性保证的需求 Influence: 1. 测试正常重启场景——哨兵文件应被删除,下次启动不误触发 Dirty 2. 测试 Clean 空闲状态下 SIGKILL——哨兵文件残留,重启触发全量 Update 3. 验证已有 Dirty 状态恢复路径无回归 4. 测试 indexDir 为空时启动——不应操作根目录文件 5. 测试第二个实例启动——应中止退出,不破坏哨兵文件 6. 测试 DBus 不可用时启动——main() 应退出,不继续运行 7. 测试索引目录存在符号链接——哨兵文件创建不应跟随符号链接 --- autotests/index/test_plugin.cpp | 13 ++++--- src/index/dbus/filenameindexdbus.cpp | 52 ++++++++++++++++++++++++++++ src/index/main.cpp | 5 ++- src/index/serviceentry.cpp | 21 ++++++----- src/index/serviceentry.h | 2 +- 5 files changed, 75 insertions(+), 18 deletions(-) diff --git a/autotests/index/test_plugin.cpp b/autotests/index/test_plugin.cpp index b0e9fa8..8d705e2 100644 --- a/autotests/index/test_plugin.cpp +++ b/autotests/index/test_plugin.cpp @@ -83,15 +83,14 @@ class PluginTest : public testing::Test stub_ext::StubExt stub; }; -TEST_F(PluginTest, DSMRegister_ReturnsZero) +TEST_F(PluginTest, DSMRegister_ReturnsValidResult) { - // DSMRegister creates DBus objects; should return 0 - // DBus registration may fail in sandbox but the function - // should still return 0 + // Returns 0 on success, -1 if DBus registration fails (e.g. in sandbox). + // Either way the function must not crash. int result = anything_index::registerIndexServices(); - EXPECT_EQ(result, 0); + EXPECT_TRUE(result == 0 || result == -1); - // Clean up via DSMUnRegister + // Clean up via DSMUnRegister (safe to call even if register failed) anything_index::unregisterIndexServices(); } @@ -125,6 +124,6 @@ TEST_F(PluginTest, DSMUnRegister_CalledTwice) TEST_F(PluginTest, DSMRegister_WithNullName) { int result = anything_index::registerIndexServices(); - EXPECT_EQ(result, 0); + EXPECT_TRUE(result == 0 || result == -1); anything_index::unregisterIndexServices(); } diff --git a/src/index/dbus/filenameindexdbus.cpp b/src/index/dbus/filenameindexdbus.cpp index 4a32566..dbc4960 100644 --- a/src/index/dbus/filenameindexdbus.cpp +++ b/src/index/dbus/filenameindexdbus.cpp @@ -11,11 +11,29 @@ #include #include #include +#include + +#include +#include ANYTHING_INDEX_USE_NAMESPACE namespace { +// Sentinel file used to detect abnormal service exit (SIGKILL, crash, OOM). +// Created at startup, removed during normal cleanup(). If it still exists +// when the service starts, the previous run was killed without cleanup. +inline constexpr char kSentinelFileName[] = ".filename_index_running"; + +QString sentinelFilePath(const QString &indexDir) +{ + if (indexDir.isEmpty()) { + return QString(); + } + + return indexDir + QLatin1Char('/') + QLatin1String(kSentinelFileName); +} + QStringList defaultPathsToProcess() { const auto &configuredDirs = DFMSEARCH::Global::defaultIndexedDirectory(); @@ -43,6 +61,32 @@ void FileNameIndexDBusPrivate::initialize() // time because the service process is always started fresh by the daemon. runtime->fsEventController()->setSilentlyRefreshStarted(true); + // Sentinel file: detect abnormal exit from the previous run. + // On normal shutdown cleanup() removes the sentinel. If it still exists + // at startup, the previous process was killed without a chance to clean + // up (SIGKILL, segfault, OOM) — mark state Dirty so handleSilentStart() + // triggers a compensating full Update that covers all missed file changes. + const QString indexDir = runtime->profile().indexDirectory(); + const QString sentinelPath = sentinelFilePath(indexDir); + if (!sentinelPath.isEmpty() && QFile::exists(sentinelPath)) { + qWarning() << "FileNameIndexDBus: Sentinel file found, previous exit was abnormal, marking state as dirty"; + runtime->stateStore().setIndexState(IndexUtility::IndexState::Dirty); + } + + // Create (or recreate) the sentinel file for this run. + // Use O_NOFOLLOW to avoid following symlinks that could truncate an + // arbitrary file if an attacker pre-placed one in the index directory. + if (!indexDir.isEmpty()) { + QDir().mkpath(indexDir); + const QByteArray pathBytes = QFile::encodeName(sentinelPath); + int fd = ::open(pathBytes.constData(), O_CREAT | O_WRONLY | O_NOFOLLOW | O_TRUNC, 0644); + if (fd >= 0) { + ::close(fd); + } else { + qWarning() << "FileNameIndexDBus: Failed to create sentinel file:" << sentinelPath; + } + } + // Check for dirty state at startup and set recovery pending flag // This must be done before any incremental task can complete and clear the Dirty state const IndexUtility::IndexState state = runtime->stateStore().getIndexState(); @@ -209,6 +253,14 @@ void FileNameIndexDBus::cleanup() } StopCurrentTask(); + + // Remove sentinel file on normal exit so the next startup knows + // the previous shutdown was clean. + const QString indexDir = d->runtime->profile().indexDirectory(); + const QString sentinelPath = sentinelFilePath(indexDir); + if (!sentinelPath.isEmpty() && QFile::exists(sentinelPath) && !QFile::remove(sentinelPath)) { + qWarning() << "FileNameIndexDBus: Failed to remove sentinel file:" << sentinelPath; + } } bool FileNameIndexDBus::IsEnabled() diff --git a/src/index/main.cpp b/src/index/main.cpp index bc8476f..75a6f93 100644 --- a/src/index/main.cpp +++ b/src/index/main.cpp @@ -39,7 +39,10 @@ int main(int argc, char *argv[]) QGuiApplication::setApplicationName("deepin-anything-index"); QGuiApplication::setApplicationVersion("1.0.0"); - anything_index::registerIndexServices(); + if (anything_index::registerIndexServices() != 0) { + qWarning() << "deepin-anything-index: failed to register DBus services, exiting"; + return 1; + } // Turn SIGTERM/SIGINT into a graceful shutdown (stop monitoring, stop the // running task, mark unfinished indexes dirty, unregister bus names). diff --git a/src/index/serviceentry.cpp b/src/index/serviceentry.cpp index 80ff284..36b33c4 100644 --- a/src/index/serviceentry.cpp +++ b/src/index/serviceentry.cpp @@ -28,19 +28,22 @@ int registerIndexServices() qDBusRegisterMetaType>(); QDBusConnection bus = QDBusConnection::sessionBus(); - if (!bus.registerService(Defines::kTextIndexDBusService) - && bus.lastError().type() != QDBusError::NoError) { - qWarning() << "deepin-anything-index: failed to register text index DBus service:" << bus.lastError().message(); + if (!bus.registerService(Defines::kTextIndexDBusService)) { + qWarning() << "deepin-anything-index: failed to register text index DBus service, another instance may be running:" << bus.lastError().message(); + return -1; } - if (!bus.registerService(Defines::kOcrIndexDBusService) - && bus.lastError().type() != QDBusError::NoError) { - qWarning() << "deepin-anything-index: failed to register OCR index DBus service:" << bus.lastError().message(); + if (!bus.registerService(Defines::kOcrIndexDBusService)) { + qWarning() << "deepin-anything-index: failed to register OCR index DBus service, another instance may be running:" << bus.lastError().message(); + bus.unregisterService(Defines::kTextIndexDBusService); + return -1; } - if (!bus.registerService(Defines::kFileNameIndexDBusService) - && bus.lastError().type() != QDBusError::NoError) { - qWarning() << "deepin-anything-index: failed to register filename index DBus service:" << bus.lastError().message(); + if (!bus.registerService(Defines::kFileNameIndexDBusService)) { + qWarning() << "deepin-anything-index: failed to register filename index DBus service, another instance may be running:" << bus.lastError().message(); + bus.unregisterService(Defines::kOcrIndexDBusService); + bus.unregisterService(Defines::kTextIndexDBusService); + return -1; } textIndexDBus = new TextIndexDBus(); diff --git a/src/index/serviceentry.h b/src/index/serviceentry.h index 138a215..9bd7e10 100644 --- a/src/index/serviceentry.h +++ b/src/index/serviceentry.h @@ -9,7 +9,7 @@ namespace anything_index { // Registers the three D-Bus service names (org.deepin.Filemanager.TextIndex / // OcrIndex / FileNameIndex), creates the three D-Bus objects and lowers the -// process priority. Returns 0 on success, matching the old DSMRegister contract. +// process priority. Returns 0 on success, -1 if any DBus registration fails. int registerIndexServices(); // Stops monitoring and running tasks, marks unfinished indexes dirty and