已合并
fix(driver): 修复注册失败清理并补齐插件测试部署 #416
liuxingsen创建于 9月24日
fix(driver): 修复注册失败清理并补齐插件测试部署 #416
已合并
liuxingsen创建于 9月24日
共 23 个文件变更+443-18
@@ -1,3 +1,13 @@
1+# Copyright (c) 2025 Huawei Technologies Co., Ltd.
2+# openUBMC is licensed under Mulan PSL v2.
3+# You can use this software according to the terms and conditions of the Mulan PSL v2.
4+# You may obtain a copy of Mulan PSL v2 at:
5+# http://license.coscl.org.cn/MulanPSL2
6+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
7+# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
8+# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
9+# See the Mulan PSL v2 for more details.
10+ 
1import os11import os
2from conanbase import ConanBase12from conanbase import ConanBase
3from conan.tools.meson import Meson, MesonToolchain13from conan.tools.meson import Meson, MesonToolchain
@@ -53,6 +53,9 @@ using device_drivers = std::vector<std::shared_ptr<device_driver_t>>;
53 53 
54mc::string dump_device_driver(const device_driver_t& driver, driver_handle_t handle);54mc::string dump_device_driver(const device_driver_t& driver, driver_handle_t handle);
55 55 
56+// 同一实例的加载、卸载和设备访问由调用者串行化;本类不提供实例级并发保护。
57+// 工厂加载候选库时使用独立实例,加载成功后才在缓存锁内发布。
58+// get_device/get_devices 返回的引用及设备中的插件指针不得跨越 unload 的生命周期。
56class device_driver {59class device_driver {
57public:60public:
58 device_driver(const std::string& driver_name);61 device_driver(const std::string& driver_name);
@@ -1,6 +1,13 @@
1/*1/*
2 * Copyright (c) 2026 Huawei Technologies Co., Ltd.2 * Copyright (c) 2026 Huawei Technologies Co., Ltd.
3 * openUBMC is licensed under Mulan PSL v2.3 * openUBMC is licensed under Mulan PSL v2.
4+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
5+ * You may obtain a copy of Mulan PSL v2 at:
6+ * http://license.coscl.org.cn/MulanPSL2
7+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
8+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
9+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
10+ * See the Mulan PSL v2 for more details.
4 */11 */
5 12 
6#ifndef DEVMON_COMMON_DRIVER_SOURCE_H13#ifndef DEVMON_COMMON_DRIVER_SOURCE_H
@@ -1,3 +1,15 @@
1+/*
2+ * Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+ * openUBMC is licensed under Mulan PSL v2.
4+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
5+ * You may obtain a copy of Mulan PSL v2 at:
6+ * http://license.coscl.org.cn/MulanPSL2
7+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
8+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
9+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
10+ * See the Mulan PSL v2 for more details.
11+ */
12+ 
1#include <devmon-common/csr_types.h>13#include <devmon-common/csr_types.h>
2#include <mc/reflect.h>14#include <mc/reflect.h>
3 15 
@@ -66,6 +66,9 @@ void device_driver::load_file(const std::string& library_file, const driver_load
66 MC_THROW(mc::system_exception, "Dynamic driver loading is not supported on Zephyr for driver: ${name}",66 MC_THROW(mc::system_exception, "Dynamic driver loading is not supported on Zephyr for driver: ${name}",
67 ("name", m_driver_name));67 ("name", m_driver_name));
68#else68#else
69+ MC_ASSERT_THROW(m_driver_handle == nullptr, mc::invalid_arg_exception, "Driver already loaded: ${name}",
O

⚠️ 警告:重复加载守卫存在 TOCTOU 竞态

新增的重复加载守卫 MC_ASSERT_THROW(m_driver_handle == nullptr, ...) 是一个先检查后执行的序列(check-then-act),且没有加锁。若同一 device_driver 实例被两个线程并发调用 load_file(),两个线程都可能先读到 nullptr,随后各自 dlopen 出不同句柄并先后写入 m_driver_handle,后写入者会覆盖先前句柄,丢失释放入口——正是本 PR 想消除的句柄覆盖/泄漏问题。

本文件 unload() 里已用 "exchange-then-close" 模式显式规避多线程 dlclose 竞态,说明该代码库关注并发访问。

修复建议:用与 unload() 相同的互斥量将「检查 + dlopen + 赋值」串行化;或在文档与头文件注释中明确:同一实例的并发 load_file 不受支持(当工厂路径总是使用全新实例并通过缓存去重时,这主要是防御性约束)。

likedislike
70+ ("name", m_driver_name));
71+ 
69 auto start = std::chrono::steady_clock::now();72 auto start = std::chrono::steady_clock::now();
70 73 
71 driver_load_info info;74 driver_load_info info;
@@ -100,17 +103,30 @@ void device_driver::load_file(const std::string& library_file, const driver_load
100 103 
101 m_driver_handle = driver_handle;104 m_driver_handle = driver_handle;
102 105 
106+ bool registration_failed = false;
107+ std::string registration_error;
103 try {108 try {
104 register_driver();109 register_driver();
105 info.success = true;110 info.success = true;
106 info.device_count = m_devices.size();111 info.device_count = m_devices.size();
107 } catch (const std::exception& e) {112 } catch (const std::exception& e) {
108- info.error_msg = e.what();113+ registration_error = e.what();
114+ registration_failed = true;
115+ } catch (...) {
116+ registration_error = "unknown driver registration error";
117+ registration_failed = true;
109 }118 }
110 119 
111 info.load_time_ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();120 info.load_time_ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
112 121 
122+ if (registration_failed) {
123+ info.error_msg = registration_error;
124+ unload();
125+ }
113 record_load_info(std::move(info));126 record_load_info(std::move(info));
127+ if (registration_failed) {
128+ MC_THROW(mc::bad_function_call_exception, "Failed to register driver: ${error}", ("error", registration_error));
129+ }
114 130 
115 dlog("Driver ${name} loaded successfully from ${file}", ("name", m_driver_name)("file", library_file));131 dlog("Driver ${name} loaded successfully from ${file}", ("name", m_driver_name)("file", library_file));
116#endif132#endif
@@ -119,8 +135,8 @@ void device_driver::load_file(const std::string& library_file, const driver_load
119void device_driver::unload()135void device_driver::unload()
120{136{
121#ifndef __ZEPHYR__137#ifndef __ZEPHYR__
122- // 先将 handle 保存到局部变量并清空成员,形成 "exchange-then-close" 模式,138+ // 调用者保证同一实例串行访问。先清空成员,使后续串行 unload 不会再次关闭句柄;
123- // 防止多线程场景下 TOCTOU 竞态导致同一 handle 被 dlclose 两次。139+ // 普通指针的读取和赋值并非原子交换,不能据此保证并发 load/unload 安全。
124 void* handle = m_driver_handle;140 void* handle = m_driver_handle;
125 m_driver_handle = nullptr;141 m_driver_handle = nullptr;
126 142 
@@ -1,6 +1,13 @@
1/*1/*
2 * Copyright (c) 2026 Huawei Technologies Co., Ltd.2 * Copyright (c) 2026 Huawei Technologies Co., Ltd.
3 * openUBMC is licensed under Mulan PSL v2.3 * openUBMC is licensed under Mulan PSL v2.
4+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
5+ * You may obtain a copy of Mulan PSL v2 at:
6+ * http://license.coscl.org.cn/MulanPSL2
7+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
8+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
9+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
10+ * See the Mulan PSL v2 for more details.
4 */11 */
5 12 
6#include <devmon-common/driver_source.h>13#include <devmon-common/driver_source.h>
@@ -1,3 +1,15 @@
1+/*
2+ * Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+ * openUBMC is licensed under Mulan PSL v2.
4+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
5+ * You may obtain a copy of Mulan PSL v2 at:
6+ * http://license.coscl.org.cn/MulanPSL2
7+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
8+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
9+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
10+ * See the Mulan PSL v2 for more details.
11+ */
12+ 
1/*13/*
2 * Mock implementation of dlsym and dlclose for testing14 * Mock implementation of dlsym and dlclose for testing
3 */15 */
@@ -0,0 +1,18 @@
1+/*
2+ * Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+ * openUBMC is licensed under Mulan PSL v2.
4+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
5+ * You may obtain a copy of Mulan PSL v2 at:
6+ * http://license.coscl.org.cn/MulanPSL2
7+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
8+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
9+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
10+ * See the Mulan PSL v2 for more details.
11+ */
12+ 
13+#include <devmon-common/driver_abi.h>
14+ 
15+extern "C" __attribute__((visibility("default"))) status_t register_device_driver(device_driver_t**, uint8_t*)
16+{
17+ throw 42;
18+}
@@ -0,0 +1,30 @@
1+/*
2+ * Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+ * openUBMC is licensed under Mulan PSL v2.
4+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
5+ * You may obtain a copy of Mulan PSL v2 at:
6+ * http://license.coscl.org.cn/MulanPSL2
7+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
8+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
9+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
10+ * See the Mulan PSL v2 for more details.
11+ */
12+ 
13+#include <devmon-common/driver_abi.h>
14+ 
15+#include <exception>
16+ 
17+namespace {
18+class plugin_exception : public std::exception {
19+public:
20+ const char* what() const noexcept override
21+ {
22+ return "plugin registration failure";
23+ }
24+};
25+} // namespace
26+ 
27+extern "C" __attribute__((visibility("default"))) status_t register_device_driver(device_driver_t**, uint8_t*)
28+{
29+ throw plugin_exception{};
30+}
@@ -48,7 +48,27 @@ common_test_exe = executable(
48 install_dir: get_option('bindir'),48 install_dir: get_option('bindir'),
49)49)
50 50 
51+memory_throwing_driver = shared_library(
52+ 'memory_throwing_driver',
53+ 'memory_throwing_driver.cpp',
54+ include_directories: [common_inc],
55+ install: true,
56+ install_dir: get_option('bindir'),
57+)
58+ 
59+memory_nonstandard_throwing_driver = shared_library(
60+ 'memory_nonstandard_throwing_driver',
61+ 'memory_nonstandard_throwing_driver.cpp',
62+ include_directories: [common_inc],
63+ install: true,
64+ install_dir: get_option('bindir'),
65+)
66+ 
67+memory_test_drivers = [memory_throwing_driver, memory_nonstandard_throwing_driver]
68+common_test_driver_dir = meson.current_build_dir()
69+ 
51# Register test70# Register test
52common_test_env = environment()71common_test_env = environment()
53common_test_env.set('MC_SHM_REGION', 'devmon.test.common')72common_test_env.set('MC_SHM_REGION', 'devmon.test.common')
54-test('common_test', common_test_exe, env: common_test_env, depends: test_data_deps)73+common_test_env.set('DEVMON_TEST_DRIVER_DIR', common_test_driver_dir)
74+test('common_test', common_test_exe, env: common_test_env, depends: test_data_deps + memory_test_drivers)
@@ -30,8 +30,10 @@
30#include <atomic>30#include <atomic>
31#include <chrono>31#include <chrono>
32#include <condition_variable>32#include <condition_variable>
33+#include <cstdlib>
33#include <cstring>34#include <cstring>
34#include <dlfcn.h>35#include <dlfcn.h>
36+#include <filesystem>
35#include <memory>37#include <memory>
36#include <mutex>38#include <mutex>
37#include <thread>39#include <thread>
@@ -105,6 +107,17 @@ void* open_libdl_handle()
105 return handle;107 return handle;
106}108}
107 109 
110+#ifndef __APPLE__
111+std::filesystem::path throwing_driver_path(const char* library_name)
112+{
113+ const char* driver_dir = std::getenv("DEVMON_TEST_DRIVER_DIR");
114+ if (driver_dir != nullptr && driver_dir[0] != '\0') {
115+ return std::filesystem::path(driver_dir) / library_name;
116+ }
117+ return std::filesystem::read_symlink("/proc/self/exe").parent_path() / library_name;
118+}
119+#endif
120+ 
108bool g_register_driver_force_error = true;121bool g_register_driver_force_error = true;
109bool g_register_driver_return_empty = false;122bool g_register_driver_return_empty = false;
110device_driver_t g_stub_device_entry = {"StubDevice", stub_ctor, stub_init, stub_start, stub_stop};123device_driver_t g_stub_device_entry = {"StubDevice", stub_ctor, stub_init, stub_start, stub_stop};
@@ -394,6 +407,106 @@ TEST(DeviceDriverTest, LoadMissingLibraryThrowsSystemException)
394 EXPECT_THROW(driver.load("/tmp/devmon_missing_path"), mc::system_exception);407 EXPECT_THROW(driver.load("/tmp/devmon_missing_path"), mc::system_exception);
395}408}
396 409 
410+#ifndef __APPLE__
411+TEST(DeviceDriverTest, LoadLibraryWithoutRegistrationRejectsAndClosesHandle)
412+{
413+ devmon::device_driver driver("missing_registration");
414+ devmon::device_driver::remove_load_info("libdl.so.2");
415+ 
416+ EXPECT_THROW(driver.load_file("libdl.so.2"), mc::bad_function_call_exception);
417+ EXPECT_EQ(driver.m_driver_handle, nullptr);
418+ EXPECT_TRUE(driver.get_devices().empty());
419+ 
420+ const auto load_info = devmon::device_driver::get_load_info_snapshot();
421+ const auto info = std::find_if(load_info.begin(), load_info.end(), [](const auto& entry) {
422+ return entry.driver_name == "libdl.so.2";
423+ });
424+ ASSERT_NE(info, load_info.end());
425+ EXPECT_FALSE(info->success);
426+ EXPECT_FALSE(info->error_msg.empty());
427+ devmon::device_driver::remove_load_info("libdl.so.2");
428+}
429+ 
430+TEST(DeviceDriverTest, RepeatedLoadKeepsOriginalHandle)
431+{
432+ devmon::device_driver driver("already_loaded");
433+ void* original_handle = dlopen(nullptr, RTLD_LAZY);
434+ ASSERT_NE(original_handle, nullptr);
435+ driver.m_driver_handle = original_handle;
436+ 
437+ EXPECT_THROW(driver.load_file("libdl.so.2"), mc::invalid_arg_exception);
438+ EXPECT_EQ(driver.m_driver_handle, original_handle);
439+ 
440+ if (driver.m_driver_handle != original_handle) {
441+ dlclose(original_handle);
442+ }
443+}
444+ 
445+TEST(DeviceDriverTest, NonstandardRegistrationExceptionClosesHandle)
446+{
447+ devmon::device_driver driver("throwing_registration");
448+ const auto library_path = throwing_driver_path("libmemory_nonstandard_throwing_driver.so");
449+ ASSERT_TRUE(std::filesystem::is_regular_file(library_path)) << "Missing test driver: " << library_path;
450+ devmon::device_driver::remove_load_info(library_path.filename().string());
451+ 
452+ try {
453+ driver.load_file(library_path.string());
454+ FAIL() << "Expected a driver registration exception";
455+ } catch (const mc::bad_function_call_exception& error) {
456+ EXPECT_STREQ(error.what(), "Failed to register driver: unknown driver registration error");
457+ }
458+ 
459+ EXPECT_EQ(driver.m_driver_handle, nullptr);
460+ EXPECT_TRUE(driver.get_devices().empty());
461+ void* loaded_handle = dlopen(library_path.c_str(), RTLD_NOLOAD | RTLD_LAZY);
462+ EXPECT_EQ(loaded_handle, nullptr);
463+ if (loaded_handle != nullptr) {
464+ dlclose(loaded_handle);
465+ }
466+ 
467+ const auto load_info = devmon::device_driver::get_load_info_snapshot();
468+ const auto info = std::find_if(load_info.begin(), load_info.end(), [&library_path](const auto& entry) {
469+ return entry.driver_name == library_path.filename().string();
470+ });
471+ ASSERT_NE(info, load_info.end());
472+ EXPECT_FALSE(info->success);
473+ EXPECT_EQ(info->error_msg, "unknown driver registration error");
474+ devmon::device_driver::remove_load_info(library_path.filename().string());
475+}
476+ 
477+TEST(DeviceDriverTest, PluginDefinedRegistrationExceptionClosesHandle)
478+{
479+ devmon::device_driver driver("throwing_registration");
480+ const auto library_path = throwing_driver_path("libmemory_throwing_driver.so");
481+ ASSERT_TRUE(std::filesystem::is_regular_file(library_path)) << "Missing test driver: " << library_path;
482+ devmon::device_driver::remove_load_info(library_path.filename().string());
483+ 
484+ try {
485+ driver.load_file(library_path.string());
486+ FAIL() << "Expected a driver registration exception";
487+ } catch (const mc::bad_function_call_exception& error) {
488+ EXPECT_STREQ(error.what(), "Failed to register driver: plugin registration failure");
489+ }
490+ 
491+ EXPECT_EQ(driver.m_driver_handle, nullptr);
492+ EXPECT_TRUE(driver.get_devices().empty());
493+ void* loaded_handle = dlopen(library_path.c_str(), RTLD_NOLOAD | RTLD_LAZY);
494+ EXPECT_EQ(loaded_handle, nullptr);
495+ if (loaded_handle != nullptr) {
496+ dlclose(loaded_handle);
497+ }
498+ 
499+ const auto load_info = devmon::device_driver::get_load_info_snapshot();
500+ const auto info = std::find_if(load_info.begin(), load_info.end(), [&library_path](const auto& entry) {
501+ return entry.driver_name == library_path.filename().string();
502+ });
503+ ASSERT_NE(info, load_info.end());
504+ EXPECT_FALSE(info->success);
505+ EXPECT_EQ(info->error_msg, "plugin registration failure");
506+ devmon::device_driver::remove_load_info(library_path.filename().string());
507+}
508+#endif
509+ 
397TEST(DeviceDriverTest, RegisterDriverThrowsWhenSymbolMissing)510TEST(DeviceDriverTest, RegisterDriverThrowsWhenSymbolMissing)
398{511{
399 devmon::device_driver driver("missing_symbol_driver");512 devmon::device_driver driver("missing_symbol_driver");
@@ -1,6 +1,13 @@
1/*1/*
2 * Copyright (c) 2026 Huawei Technologies Co., Ltd.2 * Copyright (c) 2026 Huawei Technologies Co., Ltd.
3 * openUBMC is licensed under Mulan PSL v2.3 * openUBMC is licensed under Mulan PSL v2.
4+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
5+ * You may obtain a copy of Mulan PSL v2 at:
6+ * http://license.coscl.org.cn/MulanPSL2
7+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
8+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
9+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
10+ * See the Mulan PSL v2 for more details.
4 */11 */
5 12 
6#include <devmon-common/driver_source.h>13#include <devmon-common/driver_source.h>
@@ -3,10 +3,10 @@
3 * openUBMC is licensed under Mulan PSL v2.3 * openUBMC is licensed under Mulan PSL v2.
4 * You can use this software according to the terms and conditions of the Mulan PSL v2.4 * You can use this software according to the terms and conditions of the Mulan PSL v2.
5 * You may obtain a copy of Mulan PSL v2 at:5 * You may obtain a copy of Mulan PSL v2 at:
6- * http://license.coscl.org.cn/Mulan PSL v26+ * http://license.coscl.org.cn/MulanPSL2
7 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,7 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
8 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,8 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
9- * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.9+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
10 * See the Mulan PSL v2 for more details.10 * See the Mulan PSL v2 for more details.
11 */11 */
12 12 
@@ -2,7 +2,7 @@
2 * Copyright (c) 2025 Huawei Technologies Co., Ltd.2 * Copyright (c) 2025 Huawei Technologies Co., Ltd.
3 * openUBMC is licensed under Mulan PSL v2.3 * openUBMC is licensed under Mulan PSL v2.
4 * You can use this software according to the terms and conditions of the Mulan PSL v2.4 * You can use this software according to the terms and conditions of the Mulan PSL v2.
5- * You may obtain a copy of the Mulan PSL v2 at:5+ * You may obtain a copy of Mulan PSL v2 at:
6 * http://license.coscl.org.cn/MulanPSL26 * http://license.coscl.org.cn/MulanPSL2
7 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,7 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
8 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,8 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
@@ -1,3 +1,15 @@
1+/*
2+ * Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+ * openUBMC is licensed under Mulan PSL v2.
4+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
5+ * You may obtain a copy of Mulan PSL v2 at:
6+ * http://license.coscl.org.cn/MulanPSL2
7+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
8+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
9+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
10+ * See the Mulan PSL v2 for more details.
11+ */
12+ 
1#include <devmon/devmon_root_object.h>13#include <devmon/devmon_root_object.h>
2#include <mc/log.h>14#include <mc/log.h>
3 15 
@@ -66,11 +66,12 @@ if devmon_test_sources.length() > 0
66 # Register test66 # Register test
67 devmon_test_env = environment()67 devmon_test_env = environment()
68 devmon_test_env.set('MC_SHM_REGION', 'devmon.test.service')68 devmon_test_env.set('MC_SHM_REGION', 'devmon.test.service')
69+ devmon_test_env.set('DEVMON_TEST_DRIVER_DIR', common_test_driver_dir)
69 test(70 test(
70 'devmon_test',71 'devmon_test',
71 devmon_test_exe,72 devmon_test_exe,
72 env: devmon_test_env,73 env: devmon_test_env,
73- depends: test_data_deps,74+ depends: test_data_deps + memory_test_drivers,
74 timeout: 120,75 timeout: 120,
75 )76 )
76endif77endif
@@ -1,3 +1,15 @@
1+/*
2+ * Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+ * openUBMC is licensed under Mulan PSL v2.
4+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
5+ * You may obtain a copy of Mulan PSL v2 at:
6+ * http://license.coscl.org.cn/MulanPSL2
7+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
8+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
9+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
10+ * See the Mulan PSL v2 for more details.
11+ */
12+ 
1#include <hwdiscovery/discovery/hwdiscovery_root_object.h>13#include <hwdiscovery/discovery/hwdiscovery_root_object.h>
2 14 
3MC_REFLECT(devmon::hwdiscovery_root_object, ((m_root_interface, "root")))15MC_REFLECT(devmon::hwdiscovery_root_object, ((m_root_interface, "root")))
@@ -75,4 +75,5 @@ hwdiscovery_test_exe = executable(
75 75 
76hwdiscovery_test_env = environment()76hwdiscovery_test_env = environment()
77hwdiscovery_test_env.set('MC_SHM_REGION', 'devmon.test.hwdiscovery')77hwdiscovery_test_env.set('MC_SHM_REGION', 'devmon.test.hwdiscovery')
78-test('hwdiscovery_test', hwdiscovery_test_exe, env: hwdiscovery_test_env, depends: test_data_deps)78+hwdiscovery_test_env.set('DEVMON_TEST_DRIVER_DIR', common_test_driver_dir)
79+test('hwdiscovery_test', hwdiscovery_test_exe, env: hwdiscovery_test_env, depends: test_data_deps + memory_test_drivers)
@@ -1,7 +1,7 @@
1-- Copyright (c) 2026 Huawei Technologies Co., Ltd.1-- Copyright (c) 2026 Huawei Technologies Co., Ltd.
2-- openUBMC is licensed under Mulan PSL v2.2-- openUBMC is licensed under Mulan PSL v2.
3--- You can use this software according to the terms and conditions of Mulan PSL v2.3+-- You can use this software according to the terms and conditions of the Mulan PSL v2.
4--- You may obtain a copy of the License at:4+-- You may obtain a copy of Mulan PSL v2 at:
5-- http://license.coscl.org.cn/MulanPSL25-- http://license.coscl.org.cn/MulanPSL2
6-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,6-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
7-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,7-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
@@ -230,25 +230,30 @@ restore_loop_ld_library_path() {
230 fi230 fi
231}231}
232 232 
233-# 从 meson 测试定义解析 devmon_test 路径与运行时库路径233+# 从 meson 测试定义解析 devmon_test 路径、运行时库路径与插件夹具目录
234resolve_devmon_test_runtime() {234resolve_devmon_test_runtime() {
235 TEST_EXE=""235 TEST_EXE=""
236 TEST_LD_PATH=""236 TEST_LD_PATH=""
237+ local test_driver_dir="${BUILD_DIR}/libs/common/tests"
237 238 
238 if command -v meson >/dev/null && [ -f "${BUILD_DIR}/build.ninja" ]; then239 if command -v meson >/dev/null && [ -f "${BUILD_DIR}/build.ninja" ]; then
239- read -r TEST_EXE TEST_LD_PATH < <(240+ local -a test_runtime=()
241+ mapfile -t test_runtime < <(
240 meson introspect "${BUILD_DIR}" --tests 2>/dev/null | python3 -c "242 meson introspect "${BUILD_DIR}" --tests 2>/dev/null | python3 -c "
241import json, sys243import json, sys
242for test in json.load(sys.stdin):244for test in json.load(sys.stdin):
243 if test.get('name') == 'devmon_test':245 if test.get('name') == 'devmon_test':
244 cmd = test.get('cmd') or ['']246 cmd = test.get('cmd') or ['']
245 env = test.get('env') or {}247 env = test.get('env') or {}
246- print(cmd[0], env.get('LD_LIBRARY_PATH', ''))248+ print(cmd[0])
249+ print(env.get('LD_LIBRARY_PATH', ''))
250+ print(env.get('DEVMON_TEST_DRIVER_DIR', ''))
247 break251 break
248-else:252+" 2>/dev/null || true
249- print('', '')
250-" 2>/dev/null || echo " "
251 )253 )
254+ TEST_EXE="${test_runtime[0]:-}"
255+ TEST_LD_PATH="${test_runtime[1]:-}"
256+ test_driver_dir="${test_runtime[2]:-${test_driver_dir}}"
252 fi257 fi
253 258 
254 if [ -z "${TEST_EXE}" ] || [ ! -f "${TEST_EXE}" ]; then259 if [ -z "${TEST_EXE}" ] || [ ! -f "${TEST_EXE}" ]; then
@@ -263,6 +268,7 @@ else:
263 done268 done
264 fi269 fi
265 270 
271+ export DEVMON_TEST_DRIVER_DIR="${test_driver_dir}"
266 restore_loop_ld_library_path272 restore_loop_ld_library_path
267 if [ -n "${TEST_LD_PATH}" ]; then273 if [ -n "${TEST_LD_PATH}" ]; then
268 if [ -n "${LD_LIBRARY_PATH:-}" ]; then274 if [ -n "${LD_LIBRARY_PATH:-}" ]; then
@@ -1,4 +1,13 @@
1#!/bin/bash1#!/bin/bash
2+# Copyright (c) 2025 Huawei Technologies Co., Ltd.
3+# openUBMC is licensed under Mulan PSL v2.
4+# You can use this software according to the terms and conditions of the Mulan PSL v2.
5+# You may obtain a copy of Mulan PSL v2 at:
6+# http://license.coscl.org.cn/MulanPSL2
7+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
8+# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
9+# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
10+# See the Mulan PSL v2 for more details.
2 11 
3# 智能编译脚本 - 根据系统资源动态调整编译并发数12# 智能编译脚本 - 根据系统资源动态调整编译并发数
4# 版本:2.213# 版本:2.2
@@ -1,3 +1,13 @@
1+-- Copyright (c) 2025 Huawei Technologies Co., Ltd.
2+-- openUBMC is licensed under Mulan PSL v2.
3+-- You can use this software according to the terms and conditions of the Mulan PSL v2.
4+-- You may obtain a copy of Mulan PSL v2 at:
5+-- http://license.coscl.org.cn/MulanPSL2
6+-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
7+-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
8+-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
9+-- See the Mulan PSL v2 for more details.
10+ 
1package.path = './temp/opt/bmc/lualib/?.lua;' .. package.path11package.path = './temp/opt/bmc/lualib/?.lua;' .. package.path
2 12 
3-- 动态查找测试可执行文件13-- 动态查找测试可执行文件
@@ -130,8 +130,127 @@ C++ 宿主加载生产 `hwproxy.plugins.*` 快照(power_mgmt / general_hardwar
130- 连接器位置解析(多种整数类型、字符串、边界情况、类型容错)130- 连接器位置解析(多种整数类型、字符串、边界情况、类型容错)
131- Unit 信息提取(反射解析、类型验证、异常处理、字段容错)131- Unit 信息提取(反射解析、类型验证、异常处理、字段容错)
132 132 
133+## 动态驱动加载资源回归
134+ 
135+本节跟踪 [PR !416](https://gitcode.com/openUBMC/devmon/pull/416) / [任务 #144](https://gitcode.com/openUBMC/devmon/issues/144) 的动态加载问题。生产代码位于 [device_driver.cpp](../libs/common/src/device_driver.cpp),回归用例位于 [test_driver.cpp](../libs/common/tests/test_driver.cpp),真实插件夹具为 [memory_throwing_driver.cpp](../libs/common/tests/memory_throwing_driver.cpp) 和 [memory_nonstandard_throwing_driver.cpp](../libs/common/tests/memory_nonstandard_throwing_driver.cpp)。范围限于注册失败清理和重复加载保护,不代表全库内存安全验收。
136+ 
137+### 问题与清理顺序
138+ 
139+原 `device_driver::load_file()` 在 `dlopen` 成功、注册失败后仍正常返回,工厂随后可能缓存失败驱动;再次加载还可能覆盖旧句柄,丢失对应的 `dlclose` 入口。当前实现先拒绝已有句柄的重复加载,并在注册失败时清理后向调用者报告失败。
140+ 
141+以下是同一函数的关键片段;省略日志和耗时统计,不是完整函数。
142+ 
143+```cpp
144+// 修复前:捕获注册异常后继续正常返回。
145+try {
146+ register_driver();
147+ info.success = true;
148+ info.device_count = m_devices.size();
149+} catch (const std::exception& e) {
150+ info.error_msg = e.what();
151+}
152+```
153+ 
154+```cpp
155+// 当前:先复制插件错误,离开 catch 后再卸载动态库。
156+bool registration_failed = false;
157+std::string registration_error;
158+try {
159+ register_driver();
160+ info.success = true;
161+ info.device_count = m_devices.size();
162+} catch (const std::exception& e) {
163+ registration_error = e.what();
164+ registration_failed = true;
165+} catch (...) {
166+ registration_error = "unknown driver registration error";
167+ registration_failed = true;
168+}
169+```
170+ 
171+`try` 内的函数抛出异常后,控制流进入匹配的 `catch`,不会继续执行成功赋值。`const std::exception& e` 是对异常对象的只读引用,可类比 C 中借用对象的指针;它不会复制异常对象。`e.what()` 返回的错误文本也不应跨越异常对象的寿命使用,赋给 `std::string` 才得到独立拥有的文本副本,类似 C 中另行分配并复制错误字符串。`catch (...)` 还接住 `throw 42` 这类不继承 `std::exception` 的异常。
172+ 
173+当前失败路径的执行顺序为:复制错误文本 → 离开 `catch` 并销毁本路径的插件异常对象 → `unload()` 清空设备项并 `dlclose` → 记录失败信息 → 抛出 `mc::bad_function_call_exception`。C++ 异常对象销毁时会执行其析构函数;若异常类型由插件定义,这段代码可能位于 `.so`,所以必须先结束异常处理再卸载库。复制后的错误文本仍保留到日志记录和框架异常构造完成。`mc::bad_function_call_exception` 是项目的错误类型,并非 C++ 标准库提供的资源清理机制。
174+ 
175+[device_driver.h](../libs/common/include/devmon-common/device_driver.h) 明确约定:同一实例的加载、卸载和设备访问由调用者串行化。工厂为每次候选加载创建独立实例,在缓存锁内发布成功结果;竞争失败的独立实例会卸载自身句柄。`unload()` 的普通指针赋值与 C 的 `void *handle = obj->handle; obj->handle = NULL;` 一样,并非原子交换,也没有互斥作用。`get_device()` / `get_devices()` 返回借用引用,设备中的函数指针仍指向插件;调用方必须在卸载前结束使用,不能把 `std::shared_ptr` 持有的设备结构体误认为动态库也被自动保活。
176+ 
177+### 回归用例与运行入口
178+ 
179+以下用例均属于 `DeviceDriverTest`:
180+ 
181+| 用例 | 场景及关键断言 |
182+| --- | --- |
183+| `LoadLibraryWithoutRegistrationRejectsAndClosesHandle` | 库缺注册入口:抛出错误、句柄置空、设备集合为空,并记录失败 |
184+| `RepeatedLoadKeepsOriginalHandle` | 已有句柄时再次加载:拒绝操作,保留原句柄 |
185+| `NonstandardRegistrationExceptionClosesHandle` | 真实插件抛出非标准异常:清理后报错,`RTLD_NOLOAD` 检查插件不再驻留 |
186+| `PluginDefinedRegistrationExceptionClosesHandle` | 真实插件抛出自定义 `std::exception`:异常对象先于库卸载销毁,检查句柄及驻留状态 |
187+ 
188+从仓库根目录使用正式测试入口:
189+ 
190+```bash
191+bingo test -ut -jit
192+bingo test -ut -jit -as
193+```
194+ 
195+这是 CI 环境的完整复现入口。本轮本机 Bingo 缺少 `test` 子命令,因此本地使用复用依赖的原生 Meson 构建,不能将其表述为 Bingo 全流程通过。当前 Lua 包装入口不会将 Bingo 的 `-f` 参数转交给 GTest,不能用 `-f 'DeviceDriverTest.*'` 证明这些 C++ 用例已运行;应检查日志中的实际 `RUN` / `OK` / `FAILED` 记录。执行前核对实际构建源码、测试程序和依赖版本,同时保留实际可执行文件路径。新增用例依赖 `libdl.so.2`、`/proc/self/exe` 和 `RTLD_NOLOAD`,目前验证范围为 Linux/glibc。
196+ 
197+### 插件构建、安装与用例隔离
198+ 
199+[common 测试构建配置](../libs/common/tests/meson.build) 将 `test_driver.cpp` 同时编入 `common_test` 可执行文件及 `common_test_lib` 静态库;[devmon 测试构建配置](../libs/devmon/tests/meson.build) 链接该静态库。`test_csr.cpp` 引用同一对象文件中的 `test_driver::create()`,因此插件回归用例也会进入 `devmon_test`。这与 C 链接静态库时为满足一个函数引用而取出整个 `.o` 的情形相同。
200+ 
201+原配置仅构建插件,未安装;测试又只在自身可执行文件目录找库,导致 CI 的 `temp/bin/devmon_test` 缺少夹具。现将两个插件安装到与测试程序相同的 `bindir`,并在所有引用这些用例的 Meson 测试入口声明插件构建依赖:
202+ 
203+| 入口 | 插件定位方式 |
204+| --- | --- |
205+| Meson `common_test` / `devmon_test` / unidev 的 `hwdiscovery_test` | Meson 设置 `DEVMON_TEST_DRIVER_DIR`,指向构建目录的 common 测试夹具目录 |
206+| 安装或移动后的测试程序(包括 CI `temp/bin/devmon_test`) | 未设置该变量时,在 `/proc/self/exe` 所在目录找插件 |
207+ 
208+`scripts/run_tests_loop.sh` 从 Meson 测试定义读取并传递插件目录;缺少定义时使用构建目录的 `libs/common/tests`。直接运行构建目录内的 `devmon_test` 时也应设置 `DEVMON_TEST_DRIVER_DIR`,或使用 Meson 测试入口。部署验收必须取消该变量,防止意外从构建树借到插件而掩盖漏打包。测试先检查夹具存在,再断言注册失败和插件卸载;不能把缺库导致的 `mc::system_exception` 当作注册失败的 `mc::bad_function_call_exception`。
209+ 
210+两个插件分别固定抛出整数异常和插件定义的 `std::exception`,不再通过 `DEVMON_TEST_STD_EXCEPTION` 切换行为。这里抛整数用于覆盖 `catch (...)`;另一个插件的异常对象包含定义在 `.so` 中的虚函数实现,测试确保捕获结束后才卸载库。路径变量只配置测试资产所在目录,不改变插件行为。夹具只在启用测试的构建中安装,不进入关闭测试的产品构建。
211+ 
212+### 路径修复与 C++ 写法
213+ 
214+原测试直接使用可执行文件目录,无法覆盖插件位于 common 目录、程序位于 devmon 目录的构建布局:
215+ 
216+```cpp
217+// 修改前,来自原测试。
218+auto library_path = std::filesystem::read_symlink("/proc/self/exe").parent_path() / "libmemory_throwing_driver.so";
219+```
220+ 
221+现在两个用例各自选固定插件,并提前诊断缺失文件。以下摘自整数异常用例:
222+ 
223+```cpp
224+const auto library_path = throwing_driver_path("libmemory_nonstandard_throwing_driver.so");
225+ASSERT_TRUE(std::filesystem::is_regular_file(library_path)) << "Missing test driver: " << library_path;
226+```
227+ 
228+`auto` 让编译器从右侧推导类型,此处得到 `std::filesystem::path`;`const` 表示这个局部路径不再修改。`path` 自己保存路径内容,可类比一个管理字符串的 C 结构体;路径之间的 `/` 是 C++ 重载的拼接操作,不是整数除法。`throwing_driver_path()` 先通过与 C 相同的 `getenv()` 借用目录字符串,再构造自己的路径对象;未设置目录时回到可执行文件目录。`ASSERT_TRUE` 是 GTest 的断言宏,失败即结束当前用例;后面的 `<<` 为诊断输出追加实际路径。它先排除“测试资产缺失”,后面的异常类型、错误文本、加载记录和 `RTLD_NOLOAD` 断言才检验“插件已执行但注册失败并卸载”。
229+ 
230+```mermaid
231+flowchart LR
232+ A[原安装配置:只安装测试程序] --> B[temp/bin 缺少插件]
233+ B --> C[dlopen 失败,未执行注册函数]
234+ D[修复:两个固定插件随测试程序安装] --> E[同目录找到插件]
235+ E --> F[调用注册函数并捕获异常]
236+ F --> G[复制错误文本,异常对象销毁]
237+ G --> H[清空设备并 dlclose]
238+ H --> I[断言错误文本及插件不再驻留]
239+```
240+ 
241+### 验证记录与限制
242+ 
243+- 本轮合入主线 `9eea32c` 后,以 GCC 9、原生 Meson、本机既有依赖完成构建;三个已配置测试入口全部通过:调度测试 59 项、`common_test` 415 项、`devmon_test` 1024 项通过,后者另有 14 项 CSR 引用对象用例按现有条件跳过。不是完整 Bingo/产品依赖构建结果。
244+- 构建目录中两个主要入口的 `DeviceDriverTest.*` 各 17 项通过。执行实际 `meson install` 后,取消插件目录变量,以安装目录中的库运行两个测试程序,各 17 项随机顺序重复 10 轮通过;临时移走两个已安装夹具时,目标两用例明确因缺文件失败,恢复夹具后通过。
245+- `unidev=true` 单独完成 Meson 配置,核对 `common_test`、`devmon_test`、`hwdiscovery_test` 均具有插件目录和两个插件依赖;本轮没有编译或运行完整 unidev 套件。循环脚本的实际 Meson 参数解析、空库路径字段及解析失败回退已验证。
246+- 历史本地 `57e786db` 的 ASAN 驱动测试 17/17、`common_test` 408/408 属于旧提交与旧构建,不能计入本轮验证。本轮未重新执行 ASAN 或目标板测试。
247+- 用户提供的 2026-10-13 CI 日志对应源提交 `5aa8a6a` 的合成提交 `42a06e874afcb094d0e5cb2dfce139ffa0b7d6b7`:编译了插件,包内 `.so` 列表却不含插件,实际运行 `temp/bin/devmon_test`,最终列出两条插件异常用例失败。附件仅包含失败摘要,没有原始断言全文。
248+- [tests/main.cpp](main.cpp) 使用 `std::quick_exit`;仅设置 `detect_leaks=1` 不能证明 LSAN 正常退出扫描已完成。远端 CI 必须按推送后的实际提交单独核对,不能复用上述历史失败或本地结果替代。
249+ 
133## 测试统计250## 测试统计
134 251 
252+以下总数、覆盖率和连续运行统计为既有文档记录,未在本轮重新测量;不能替代上节按提交和入口区分的验证结果。
253+ 
135### 总体统计254### 总体统计
136 255 
137该测试套件共包含 **646 个测试用例**:256该测试套件共包含 **646 个测试用例**:
@@ -242,7 +361,7 @@ C++ 宿主加载生产 `hwproxy.plugins.*` 快照(power_mgmt / general_hardwar
242- ✅ 代码行覆盖率良好(93.1%)361- ✅ 代码行覆盖率良好(93.1%)
243- ✅ 函数覆盖率良好(72.0%)362- ✅ 函数覆盖率良好(72.0%)
244- ⚠️ 分支覆盖率较低(47.6%,仍需改进)363- ⚠️ 分支覆盖率较低(47.6%,仍需改进)
245-- ✅ 测试稳定性良好(100 次连续运行全部通过)364+- 历史稳定性记录为 100 次连续运行通过;动态加载回归仍有上文列出的 CI 失败,不能据此判断当前完整测试通过。
246 365 
247### 改进方向366### 改进方向
2481. **提高分支覆盖率**:增加条件分支和边界情况的测试3671. **提高分支覆盖率**:增加条件分支和边界情况的测试