Closed
fix(drivers): 内存边界与异步生命周期修复及压力测试 #1307
fix(drivers): 内存边界与异步生命周期修复及压力测试 #1307
Closed
liuxingsenAdd on Sep 27Closed at 25 days ago
Total: 119 files changed+4577-797
@@ -0,0 +1,53 @@
1+# component_drivers 历史 gtest 50 轮记录
2+ 
3+本报告保留 2026-09-27 旧构建的 50 轮全量串行压力测试记录。报告提交 `8bdd569f` 之后分支仍有修复,现又整合最新主线;以下历史数字不代表当前提交通过验收。当前提交的实际验证以 PR 自验证报告为准。
4+ 
5+## 验收结果
6+ 
7+- 轮次:50/50 通过,失败轮次 0。
8+- 每轮 Meson 测试可执行文件:207/207 通过。
9+- 每轮 gtest 用例:11,566/11,566 通过,失败 0,跳过 0,不完整 0。
O

💡 建议:验收文档声称“跳过 0”,与 PR 描述声明的 4 个既有 ScannerTest 跳过用例不一致

docs/gtest-stress-50-20260927.md(第 9、35 行)写明最终 50 轮“跳过 0,不完整 0”,并要求 suites_without_cases 为空。但 PR 描述反复声明“每轮全量测试包含 4 个既有 ScannerTest 跳过用例,报告单独计数”;且按当前 scripts/run_memory_audit_stress.py 的 no_cases 判定,这类 suite 是 OK 但 0 个 RUN 用例,会落入 no_cases 使 passed=False——这正是此前检视指出的“无法作为通过/失败门禁”问题。

修复建议:两份文档需互相佐证——若最终构建已消除这 4 个跳过用例(跳过 0 成立),请同步更新 PR 描述的“4 个既有 ScannerTest 跳过用例”表述;若 4 个用例仍跳过,则本验收文档的“跳过 0 / suites_without_cases 为空”与脚本统计逻辑矛盾,需说明这些 suite 如何在 no_cases 判定下被“报告单独计数”而不计为错误。

likedislike
liuxingsen
25 days ago Comments:
10+- 累计 Meson 可执行文件运行:10,350 次。
11+- 累计 gtest 用例运行:578,300 次。
12+- 历史全量构建通过。该构建未启用 sanitizer 插桩;仅设置 ASan/MSan/UBSan 环境变量不能证明内存检查通过。
13+ 
14+最终证据:
15+ 
16+- 摘要:`test_failure_logs/gtest-50-20260927/final-50-rerun-2/run-20260927T142812Z-5/summary.json`
17+- 摘要 SHA-256:`5f8c48d87bd9141bdb797daaeaa60d113f300f2e9c3fbf4bcf5b820d608b02b2`
18+- 摘要中 50 轮均为 `passed=true`,且 `suite_errors`、`suites_without_cases`、`suites_with_incomplete_cases`、`missing_suites`、`unexpected_suites` 和 `errors` 均为空。
19+ 
20+## 执行方式
21+ 
22+当时复用与旧源码绑定的 `builddir_memory_audit`,先完成全量构建及 1 轮全量预检,再从第 1 轮开始正式计数。测试在独立 mount、IPC、network 和 PID 命名空间内串行执行,避免测试 D-Bus 及其他任务互相干扰。
23+ 
24+```bash
25+ninja -C builddir_memory_audit -j8
26+ 
27+unshare --mount --ipc --net --pid --fork --mount-proc \
28+ python3 test_failure_logs/gtest-20-20260927/isolated_run.py \
29+ python3 scripts/run_memory_audit_stress.py \
30+ --build-dir builddir_memory_audit \
31+ --rounds 50 \
32+ --log-root test_failure_logs/gtest-50-20260927/final-50-rerun-2
33+```
34+ 
35+正式运行前的全量预检同样通过:207/207 个可执行文件、11,566/11,566 个用例,失败/跳过/不完整均为 0。
36+ 
37+## 压测发现及闭环
38+ 
39+| 问题 | 修复与回归覆盖 |
40+| --- | --- |
41+| CSR1 aggregate 最终强引用释放时的宿主竞争 | 访问宿主时提升 weak owner;新增阻塞析构确定性用例。 |
42+| Scanner/CSR1 嵌入接口 timer 在宿主析构后被释放 | timer 脱离嵌入对象父子树,显式绑定宿主 executor;覆盖替换、停止及跨生命周期释放。 |
43+| Broadcom 光模块/网络端口 timer 重启后旧回调仍在运行 | 启动新 timer 前停止并等待旧 timer;异步用例使用条件变量等待真实事件。 |
44+| Huawei 升级工作线程与设备析构顺序 | 析构前等待升级完成;结果用例 join 后再读取,并覆盖重试、并发拒绝和成员生命期。 |
45+| Huawei VPD/MCTP/日志定时器在嵌入接口析构后回调,正式旧批次第 32 轮触发 SIGSEGV | 六类 timer 脱离嵌入对象树并绑定宿主 executor,重启前停止旧实例,设备析构入口停止全部 VPD/MCTP 任务;新增 3 个定时器回归用例,完整 `test_hw_nvme_abi` 100/100 次通过。最终 50 轮的第 32 轮也通过。 |
46+| Samsung/Huawei/Dera SMART 将 128 位 `PowerOnHours` 的高半部在 `uint64_t` 上左移 64 位以上 | 高低 64 位分别解析,按 `double` 属性契约组合;覆盖高位、低位和组合极值。 |
47+| gtest 日志行与后台调试日志交错,导致通过用例被统计为 incomplete | 统计器按待完成用例名前缀识别交错行;新增交错日志成功/失败/跳过解析回归用例。 |
48+ 
49+其他已闭环内容包括 Scanner 确定性用例、SMBus 帧式传输覆盖、Hisi/Hi1822/Emulex 生命周期用例恢复、Samsung 遥测定时器及 OEM 析构顺序,以及 Huawei 升级工作线程回收。
50+ 
51+## 范围边界
52+ 
53+该历史记录仅对应当时的主机构建、配置及注册的 207 个测试程序。上面的隔离辅助脚本和日志路径是原环境记录,仓库本身不包含这些文件;新环境不能直接复制命令重跑。它不代替 BMC/Zephyr 真机负载、未启用构建配置、其他架构或预编译外部依赖内部的动态检测。
@@ -24,6 +24,11 @@ Interface_Bus::Interface_Bus()
24 Timeout = static_cast<uint16_t>(0);24 Timeout = static_cast<uint16_t>(0);
25}25}
26 26 
27+Interface_Bus::~Interface_Bus()
28+{
29+ stop_access_timer();
30+}
31+ 
27void Interface_Bus::stop_access_timer()32void Interface_Bus::stop_access_timer()
28{33{
29 if (m_access_timer != nullptr) {34 if (m_access_timer != nullptr) {
@@ -94,12 +99,17 @@ void Interface_Bus::SetAccessibility(bool status, uint16_t disable_duration)
94 ilog("SetAccessibility: disabling access for bus ${name}, duration=${dur}s",99 ilog("SetAccessibility: disabling access for bus ${name}, duration=${dur}s",
95 ("name", get_owner() != nullptr ? get_owner()->get_object_name() : "unknown")("dur", disable_duration));100 ("name", get_owner() != nullptr ? get_owner()->get_object_name() : "unknown")("dur", disable_duration));
96 internal_bus->set_access_enabled(false);101 internal_bus->set_access_enabled(false);
97- m_access_timer = mc::make_shared<mc::timer>(this);102+ // The member owns the timer. A queued handler can retain it beyond this
103+ // embedded interface's lifetime, so it must not retain an interface parent.
104+ m_access_timer = mc::make_shared<mc::timer>();
105+ auto* owner = get_owner();
106+ m_access_timer->bind_executor(owner != nullptr ? owner->executor() : executor());
98 m_access_timer->set_single_shot(true);107 m_access_timer->set_single_shot(true);
99 m_access_timer->timeout.connect([this]() {108 m_access_timer->timeout.connect([this]() {
100 AccessEnabled = true;109 AccessEnabled = true;
101 Timeout = 0;110 Timeout = 0;
102- m_access_timer.reset();111+ // Keep the timer until stop_access_timer(): destruction must still be
112+ // able to wait for this callback before destroying the interface.
103 if (auto* bus = resolve_internal_bus(); bus != nullptr) {113 if (auto* bus = resolve_internal_bus(); bus != nullptr) {
104 bus->set_access_enabled(true);114 bus->set_access_enabled(true);
105 } else {115 } else {
@@ -29,7 +29,7 @@ namespace dev::csr1 {
29class Interface_Bus : public mc::engine::interface<Interface_Bus, dev::resource_gen::Bus> {29class Interface_Bus : public mc::engine::interface<Interface_Bus, dev::resource_gen::Bus> {
30public:30public:
31 Interface_Bus();31 Interface_Bus();
32- ~Interface_Bus() = default;32+ ~Interface_Bus();
33 33 
34 void SetAccessibility(bool status, uint16_t disable_duration) override;34 void SetAccessibility(bool status, uint16_t disable_duration) override;
35 bool isAccessAllowed();35 bool isAccessAllowed();
@@ -427,6 +427,10 @@ Interface_Aggregate::~Interface_Aggregate()
427void Interface_Aggregate::set_owner_object(mc::object_base* owner)427void Interface_Aggregate::set_owner_object(mc::object_base* owner)
428{428{
429 m_owner_object = owner;429 m_owner_object = owner;
430+ m_owner_weak.reset();
431+ if (owner != nullptr) {
432+ m_owner_weak = owner->weak_from_this();
O

💡 建议:新的 weak_from_this() 宿主保活丢失了原实现 is_managed() / try-catch 的防护

set_owner_object() 现在对任意非空 owner 无条件执行 owner->weak_from_this()(i_aggregate.cpp:432),host_guard() 改为 m_owner_weak.lock()。原 host_guard() 会先判 !m_owner_object->is_managed() 直接返回空,再用 try/catch 包裹 shared_from_this()。而 chip_base 继承的是 std::enable_shared_from_this(chip.h:100),其 weak_from_this() 在对象从未被任何 shared_ptr 持有(唯一/栈上的测试夹具对象,smc_dfx_info 注释也明确存在这类“未托管对象”)时会抛 std::bad_weak_ptr,不是返回空 guard。

修复建议:在 set_owner_object() 中保留 owner->is_managed() 防护(仅对托管对象建立弱引用),或在注释中显式约定 owner 恒为托管对象,避免把原“非托管返回空保活”的语义退化为“抛异常”。

likedislike
liuxingsen
25 days ago Comments:
433+ }
430 if (m_dispatcher) {434 if (m_dispatcher) {
431 m_dispatcher->set_owner_object(owner);435 m_dispatcher->set_owner_object(owner);
432 }436 }
@@ -486,14 +490,7 @@ void Interface_Aggregate::init(uint32_t base_offset, uint32_t length, uint32_t p
486// 宿主对象强引用保活:跨对象使用 master 指针期间借住宿主,防止宿主并发析构导致 UAF490// 宿主对象强引用保活:跨对象使用 master 指针期间借住宿主,防止宿主并发析构导致 UAF
487mc::shared_ptr<mc::object_base> Interface_Aggregate::host_guard() const491mc::shared_ptr<mc::object_base> Interface_Aggregate::host_guard() const
488{492{
489- if (m_owner_object == nullptr || !m_owner_object->is_managed()) {493+ return m_owner_weak.lock();
490- return {};
491- }
492- try {
493- return m_owner_object->shared_from_this();
494- } catch (const std::exception&) {
495- return {};
496- }
497}494}
498 495 
499// 存活的共享 master 引用:注册表核对 + 持注册表锁取得宿主保活 guard(防悬挂转发)496// 存活的共享 master 引用:注册表核对 + 持注册表锁取得宿主保活 guard(防悬挂转发)
@@ -511,7 +508,7 @@ Interface_Aggregate::MasterRef Interface_Aggregate::active_master_ref() const
511 return ref; // 已注销/被替换508 return ref; // 已注销/被替换
512 }509 }
513 // 持注册表锁取得 guard:master 析构的 erase 需同一把锁,核对与取 guard 之间510 // 持注册表锁取得 guard:master 析构的 erase 需同一把锁,核对与取 guard 之间
514- // 不可能插入注销;宿主析构中 shared_from_this 抛出 → guard 为空 → 视同无 master511+ // 不可能插入注销;宿主进入 final release 后弱引用升级失败 → guard 为空 → 视同无 master
515 ref.host = m_master_aggregate->host_guard();512 ref.host = m_master_aggregate->host_guard();
516 if (ref.host != nullptr) {513 if (ref.host != nullptr) {
517 ref.master = m_master_aggregate;514 ref.master = m_master_aggregate;
@@ -14,6 +14,7 @@
14#define CSR1_INTERFACE_AGGREGATE_H14#define CSR1_INTERFACE_AGGREGATE_H
15 15 
16#include <mc/future.h>16#include <mc/future.h>
17+#include <mc/memory/weak_ptr.h>
17#include <resource_tree/interface/Chip/Aggregate.h>18#include <resource_tree/interface/Chip/Aggregate.h>
18 19 
19#include "i_scheduler.h"20#include "i_scheduler.h"
@@ -242,6 +243,9 @@ private:
242 243 
243 // 宿主芯片对象,传递给 dispatcher 的 owner_guard244 // 宿主芯片对象,传递给 dispatcher 的 owner_guard
244 mc::object_base* m_owner_object = nullptr;245 mc::object_base* m_owner_object = nullptr;
246+ // 在宿主仍存活时建立弱引用。host_guard() 必须通过非抛异常的 lock()
247+ // 升级,不能在 final-release 窗口对裸指针调用 noexcept shared_ptr 构造。
248+ mc::weak_ptr<mc::object_base> m_owner_weak;
245 249 
246 // 芯片去重:避免同一芯片创建多个 Aggregate。250 // 芯片去重:避免同一芯片创建多个 Aggregate。
247 // 声明 key = 物理芯片身份 + 声明窗口(BaseOffset+Length):窗口不同不共享251 // 声明 key = 物理芯片身份 + 声明窗口(BaseOffset+Length):窗口不同不共享
@@ -41,6 +41,9 @@ std::vector<uint8_t> Interface_BitIO::Read(uint32_t offset, uint8_t length, uint
41void Interface_BitIO::Write(uint32_t offset, uint8_t length, uint32_t mask, const std::vector<uint8_t>& indata)41void Interface_BitIO::Write(uint32_t offset, uint8_t length, uint32_t mask, const std::vector<uint8_t>& indata)
42{42{
43 ensure_chip_accessible(m_chip, m_access_state, m_interface_chip);43 ensure_chip_accessible(m_chip, m_access_state, m_interface_chip);
44+ if (length > indata.size()) {
45+ MC_THROW(mc::invalid_arg_exception, "BitIO Write length exceeds input buffer size");
46+ }
44 std::vector<uint8_t> out_buffer;47 std::vector<uint8_t> out_buffer;
45 BusLock lock(*m_chip->get_host_bus());48 BusLock lock(*m_chip->get_host_bus());
46 std::string error;49 std::string error;
@@ -243,6 +243,12 @@ Interface_Chip::Interface_Chip()
243 243 
244Interface_Chip::~Interface_Chip()244Interface_Chip::~Interface_Chip()
245{245{
246+ // The periodic callback also captures this; cancel and join it before
247+ // tearing down any state it can access.
248+ if (m_access_watchdog != nullptr) {
249+ m_access_watchdog->stop();
250+ m_access_watchdog.reset();
251+ }
246 // 停掉在途的自动恢复计时器:stop(true) 会等待正在执行的回调结束——接口在禁用252 // 停掉在途的自动恢复计时器:stop(true) 会等待正在执行的回调结束——接口在禁用
247 // 窗口内被析构(SHM 重建/热插拔)时,若计时器继续到点触发,回调里的253 // 窗口内被析构(SHM 重建/热插拔)时,若计时器继续到点触发,回调里的
248 // is_host_bus_accessible() / m_on_accessibility_changed 会解引用已析构的本接口;254 // is_host_bus_accessible() / m_on_accessibility_changed 会解引用已析构的本接口;
@@ -523,7 +529,10 @@ void Interface_Chip::apply_access_command_ms(bool status, int64_t disable_durati
523 }529 }
524 access_state->access_enabled = false;530 access_state->access_enabled = false;
525 531 
526- access_state->access_timer = mc::make_shared<mc::timer>(this);532+ // State owns the timer; queued handlers must not retain a parent
533+ // pointer to this embedded interface after cancellation.
534+ access_state->access_timer = mc::make_shared<mc::timer>();
535+ access_state->access_timer->bind_executor(owner != nullptr ? owner->executor() : executor());
527 access_state->access_timer->set_single_shot(true);536 access_state->access_timer->set_single_shot(true);
528 access_state->access_timer->timeout.connect([this, owner_weak, access_state, name]() {537 access_state->access_timer->timeout.connect([this, owner_weak, access_state, name]() {
529 // 在途回调可能在接口析构后触发(禁用窗口内 SHM 重建/热插拔):先经538 // 在途回调可能在接口析构后触发(禁用窗口内 SHM 重建/热插拔):先经
@@ -655,7 +664,9 @@ int32_t Interface_Chip::SetLockStatus(const mc::engine::context& context, uint8_
655 // 显式拷贝 shared_ptr 到本地变量,确保 lambda 捕获时能正确保活664 // 显式拷贝 shared_ptr 到本地变量,确保 lambda 捕获时能正确保活
656 auto timer_bus = physical_bus;665 auto timer_bus = physical_bus;
657 auto lock_id = new_lock->lock_id;666 auto lock_id = new_lock->lock_id;
658- new_lock->lock_timer = mc::make_shared<mc::timer>(this);667+ new_lock->lock_timer = mc::make_shared<mc::timer>();
668+ auto* owner = get_owner();
669+ new_lock->lock_timer->bind_executor(owner != nullptr ? owner->executor() : executor());
659 new_lock->lock_timer->set_single_shot(true);670 new_lock->lock_timer->set_single_shot(true);
660 new_lock->lock_timer->timeout.connect(671 new_lock->lock_timer->timeout.connect(
661 [this, timer_bus, bus_name, channel_chain, current_chip_name, lock_id]() {672 [this, timer_bus, bus_name, channel_chain, current_chip_name, lock_id]() {
@@ -968,7 +979,9 @@ void Interface_Chip::start_access_watchdog()
968 if (m_access_watchdog != nullptr) {979 if (m_access_watchdog != nullptr) {
969 return; // 已启动(幂等)980 return; // 已启动(幂等)
970 }981 }
971- m_access_watchdog = mc::make_shared<mc::timer>(this);982+ m_access_watchdog = mc::make_shared<mc::timer>();
983+ auto* owner = get_owner();
984+ m_access_watchdog->bind_executor(owner != nullptr ? owner->executor() : executor());
972 m_access_watchdog->timeout.connect([this]() {985 m_access_watchdog->timeout.connect([this]() {
973 on_access_watchdog_tick();986 on_access_watchdog_tick();
974 });987 });
@@ -59,9 +59,10 @@ void Scheduler::register_task(ITask* task)
59 // Reuse the state when a task is registered more than once. This keeps59 // Reuse the state when a task is registered more than once. This keeps
60 // the single-flight guarantee attached to the task rather than to an60 // the single-flight guarantee attached to the task rather than to an
61 // accidental duplicate Entry.61 // accidental duplicate Entry.
62- for (const auto& existing : m_entries) {62+ for (const auto& state : m_retired_states) {
63- if (existing.task == task && existing.run_state) {63+ if (state->task == task) {
64- entry.run_state = existing.run_state;64+ entry.run_state = state;
65+ entry.next_run_ms = UINT64_MAX;
65 break;66 break;
66 }67 }
67 }68 }
@@ -85,10 +86,17 @@ void Scheduler::unregister_task(ITask* task)
85 {86 {
86 std::lock_guard<std::recursive_mutex> lock(m_mutex);87 std::lock_guard<std::recursive_mutex> lock(m_mutex);
87 88 
88- auto it = std::remove_if(m_entries.begin(), m_entries.end(), [&states, task](const Entry& e) {89+ auto it = std::remove_if(m_entries.begin(), m_entries.end(), [this, &states, task](const Entry& e) {
89 if (e.task == task) {90 if (e.task == task) {
90 if (e.run_state) {91 if (e.run_state) {
91- states.push_back(e.run_state);92+ if (e.run_state->can_retire && e.run_state->running.load(std::memory_order_acquire)) {
93+ if (std::find(m_retired_states.begin(), m_retired_states.end(), e.run_state) ==
94+ m_retired_states.end()) {
95+ m_retired_states.push_back(e.run_state);
96+ }
97+ } else {
98+ states.push_back(e.run_state);
99+ }
92 }100 }
93 return true;101 return true;
94 }102 }
@@ -101,8 +109,9 @@ void Scheduler::unregister_task(ITask* task)
101 arm_next();109 arm_next();
102 }110 }
103 111 
104- // 生命周期保证:等待被注销 Entry 的 run() 结束,避免 owner 在异步112+ // Tasks bound to their retained host can unregister inside an asynchronous
105- // callback 仍使用 task 时销毁 task。等待放在锁外,避免 run() 反向访问 Scheduler。113+ // callback without waiting on themselves. Unmanaged and replaceable child
114+ // tasks must finish before their caller can destroy or replace them.
106 wait_for_tasks(states);115 wait_for_tasks(states);
107}116}
108 117 
@@ -172,6 +181,8 @@ void Scheduler::clear()
172 }181 }
173 }182 }
174 m_entries.clear();183 m_entries.clear();
184+ states.insert(states.end(), m_retired_states.begin(), m_retired_states.end());
185+ m_retired_states.clear();
175 }186 }
176 187 
177 // 与 unregister_task 相同,只等待 clear() 移除的任务。188 // 与 unregister_task 相同,只等待 clear() 移除的任务。
@@ -211,13 +222,18 @@ void Scheduler::on_timer(uint64_t expected_generation)
211 continue;222 continue;
212 }223 }
213 224 
214- if (entry.task != nullptr) {
215- entry.period_ms = std::max(entry.task->period_ms(), MIN_TICK_MS);
216- }
217- 
218 if (!entry.run_state) {225 if (!entry.run_state) {
219 entry.run_state = std::make_shared<RunState>(entry.task);226 entry.run_state = std::make_shared<RunState>(entry.task);
220 }227 }
228+ auto owner = entry.run_state->owner.lock();
229+ if (entry.run_state->has_owner && !owner) {
230+ // The host is being destroyed and will unregister its task.
231+ // Do not dereference the task or dispatch another read.
232+ continue;
233+ }
234+ if (entry.task != nullptr) {
235+ entry.period_ms = std::max(entry.task->period_ms(), MIN_TICK_MS);
236+ }
221 // 任务仍在执行时丢弃本轮,不把慢 I/O 变成排队堆积。237 // 任务仍在执行时丢弃本轮,不把慢 I/O 变成排队堆积。
222 bool claimed = false;238 bool claimed = false;
223 {239 {
@@ -227,6 +243,7 @@ void Scheduler::on_timer(uint64_t expected_generation)
227 std::memory_order_acquire);243 std::memory_order_acquire);
228 }244 }
229 if (claimed) {245 if (claimed) {
246+ entry.run_state->owner_guard = std::move(owner);
230 // 下一次触发严格从本轮完成后重新计算;慢任务期间不保留247 // 下一次触发严格从本轮完成后重新计算;慢任务期间不保留
231 // 周期 deadline,也不做 missed-period 补偿。248 // 周期 deadline,也不做 missed-period 补偿。
232 entry.next_run_ms = UINT64_MAX;249 entry.next_run_ms = UINT64_MAX;
@@ -283,6 +300,7 @@ void Scheduler::execute_task(const std::shared_ptr<RunState>& state)
283 300 
284void Scheduler::complete_task(const std::shared_ptr<RunState>& state)301void Scheduler::complete_task(const std::shared_ptr<RunState>& state)
285{302{
303+ mc::shared_ptr<mc::object_base> completed_owner;
286 {304 {
287 std::lock_guard<std::recursive_mutex> scheduler_lock(m_mutex);305 std::lock_guard<std::recursive_mutex> scheduler_lock(m_mutex);
288 for (auto& entry : m_entries) {306 for (auto& entry : m_entries) {
@@ -296,8 +314,11 @@ void Scheduler::complete_task(const std::shared_ptr<RunState>& state)
296 entry.next_run_ms = now_ms + entry.period_ms;314 entry.next_run_ms = now_ms + entry.period_ms;
297 break;315 break;
298 }316 }
317+ m_retired_states.erase(std::remove(m_retired_states.begin(), m_retired_states.end(), state),
318+ m_retired_states.end());
299 {319 {
300 std::lock_guard<std::mutex> state_lock(state->mutex);320 std::lock_guard<std::mutex> state_lock(state->mutex);
321+ completed_owner = std::move(state->owner_guard);
301 state->running.store(false, std::memory_order_release);322 state->running.store(false, std::memory_order_release);
302 }323 }
303 arm_next();324 arm_next();
@@ -52,6 +52,13 @@ public:
52 {52 {
53 return {};53 return {};
54 }54 }
55+ // Opt in only when retaining the owner also guarantees that this task
56+ // cannot be replaced/destroyed. Replaceable child dispatchers must wait
57+ // for their in-flight run before unregister_task returns.
58+ virtual bool lifetime_bound_to_owner() const
59+ {
60+ return false;
61+ }
55};62};
56 63 
57class SchedulerTestHelper; // 测试辅助类,friend 访问私有成员64class SchedulerTestHelper; // 测试辅助类,friend 访问私有成员
@@ -78,10 +85,16 @@ private:
78 85 
79 struct RunState {86 struct RunState {
80 explicit RunState(ITask* task, mc::shared_ptr<mc::object_base> owner_guard = {})87 explicit RunState(ITask* task, mc::shared_ptr<mc::object_base> owner_guard = {})
81- : task(task), owner_guard(std::move(owner_guard))88+ : task(task), owner(owner_guard), has_owner(owner_guard != nullptr),
89+ can_retire(has_owner && task->lifetime_bound_to_owner())
82 {}90 {}
83 91 
84- ITask* task;92+ ITask* task;
93+ mc::weak_ptr<mc::object_base> owner;
94+ bool has_owner;
95+ bool can_retire;
96+ // Only an in-flight task owns its host. Idle registration must not
97+ // prevent the host destructor from unregistering the task.
85 mc::shared_ptr<mc::object_base> owner_guard;98 mc::shared_ptr<mc::object_base> owner_guard;
86 std::atomic<bool> running{false};99 std::atomic<bool> running{false};
87 std::mutex mutex;100 std::mutex mutex;
@@ -101,8 +114,11 @@ private:
101 void complete_task(const std::shared_ptr<RunState>& state);114 void complete_task(const std::shared_ptr<RunState>& state);
102 static void wait_for_tasks(const std::vector<std::shared_ptr<RunState>>& states);115 static void wait_for_tasks(const std::vector<std::shared_ptr<RunState>>& states);
103 116 
104- std::vector<Entry> m_entries;117+ std::vector<Entry> m_entries;
105- mc::shared_ptr<mc::timer> m_timer;118+ // Managed tasks may unregister from their own asynchronous completion.
119+ // Keep their state until completion for shutdown and re-registration.
120+ std::vector<std::shared_ptr<RunState>> m_retired_states;
121+ mc::shared_ptr<mc::timer> m_timer;
106 122 
107 // 时间零点(steady)123 // 时间零点(steady)
108 std::chrono::steady_clock::time_point m_epoch = std::chrono::steady_clock::now();124 std::chrono::steady_clock::time_point m_epoch = std::chrono::steady_clock::now();
@@ -151,7 +151,9 @@ void IScanner::init_startup_delay_timer()
151 m_startup_delay_timer.reset();151 m_startup_delay_timer.reset();
152 }152 }
153 153 
154- m_startup_delay_timer = mc::make_shared<mc::timer>(this);154+ // The member owns this timer; an embedded interface is not a safe weak parent
155+ // when a cancellation completion releases the timer after Scanner destruction.
156+ m_startup_delay_timer = mc::make_shared<mc::timer>();
155 m_startup_delay_timer->bind_executor(executor());157 m_startup_delay_timer->bind_executor(executor());
156 m_startup_delay_timer->timeout.connect([this]() {158 m_startup_delay_timer->timeout.connect([this]() {
157 m_at_start_up = false;159 m_at_start_up = false;
@@ -239,7 +241,8 @@ void IScanner::start_scanner_task()
239 // 创建定时器并启动241 // 创建定时器并启动
240 m_scan_active = true;242 m_scan_active = true;
241 m_adjust_period = period_ms;243 m_adjust_period = period_ms;
242- m_scanner_timer = mc::make_shared<mc::timer>(this);244+ // Keep ownership in the member, without a parent link to this embedded interface.
245+ m_scanner_timer = mc::make_shared<mc::timer>();
243 m_scanner_timer->bind_executor(executor());246 m_scanner_timer->bind_executor(executor());
244 m_scanner_timer->timeout.connect([this]() {247 m_scanner_timer->timeout.connect([this]() {
245 handle_scanner_timeout();248 handle_scanner_timeout();
@@ -120,6 +120,11 @@ public:
120 // poll runtime 时卸载可能重入并释放 owner,guard 使 owner 在整个执行批次120 // poll runtime 时卸载可能重入并释放 owner,guard 使 owner 在整个执行批次
121 // 内存活。owner 非托管(测试夹具)或正在析构时返回空。121 // 内存活。owner 非托管(测试夹具)或正在析构时返回空。
122 mc::shared_ptr<mc::object_base> owner_guard() override;122 mc::shared_ptr<mc::object_base> owner_guard() override;
123+ bool lifetime_bound_to_owner() const override
124+ {
125+ // SmcDfxInfo_object owns this dispatcher as a non-replaceable member.
126+ return true;
127+ }
123 128 
124 // 设置 owner(SmcDfxInfo_object 在 init 早期调用;follower 亦需设置——129 // 设置 owner(SmcDfxInfo_object 在 init 早期调用;follower 亦需设置——
125 // promotion 后其任务注册时 guard 必须可用)130 // promotion 后其任务注册时 guard 必须可用)
@@ -123,6 +123,10 @@ uint32_t bus_can::write(ACCESS_DATA_S& input)
123{123{
124 ensure_driver_ready(m_can_drv, "write", m_object_name, m_id);124 ensure_driver_ready(m_can_drv, "write", m_object_name, m_id);
125 125 
126+ if (input.length > input.in_buffer.size()) {
127+ MC_THROW(mc::invalid_op_exception, "CAN write length exceeds input buffer size");
128+ }
129+ 
126 try {130 try {
127 m_can_drv->write(input.in_buffer.data(), input.length, input.write_time_out);131 m_can_drv->write(input.in_buffer.data(), input.length, input.write_time_out);
128 return 0;132 return 0;
@@ -110,6 +110,15 @@ void bus_i2c::bus_unlock()
110 110 
111std::string_view bus_i2c::format_tx_buf(const ACCESS_DATA_S& data)111std::string_view bus_i2c::format_tx_buf(const ACCESS_DATA_S& data)
112{112{
113+ // On reads, data.length describes the reply, not the transmit buffer.
114+ return format_tx_buf(data, data.in_buffer.size());
115+}
116+ 
117+std::string_view bus_i2c::format_tx_buf(const ACCESS_DATA_S& data, size_t write_length)
118+{
119+ if (write_length > data.in_buffer.size()) {
120+ MC_THROW(mc::invalid_arg_exception, "write length exceeds input buffer size");
121+ }
113 // 验证偏移量宽度122 // 验证偏移量宽度
114 if (data.offset_width > MAX_OFFSET_WIDTH) {123 if (data.offset_width > MAX_OFFSET_WIDTH) {
115 MC_THROW(mc::invalid_arg_exception, "offsetWidth must be 0~4");124 MC_THROW(mc::invalid_arg_exception, "offsetWidth must be 0~4");
@@ -119,7 +128,7 @@ std::string_view bus_i2c::format_tx_buf(const ACCESS_DATA_S& data)
119 m_tx_buffer.clear();128 m_tx_buffer.clear();
120 129 
121 // 计算总长度:地址(1字节) + 偏移量(offset_width字节) + 数据长度130 // 计算总长度:地址(1字节) + 偏移量(offset_width字节) + 数据长度
122- size_t total_length = 1 + data.offset_width + data.length;131+ size_t total_length = 1 + data.offset_width + write_length;
123 m_tx_buffer.reserve(total_length);132 m_tx_buffer.reserve(total_length);
124 133 
125 // 添加地址字节134 // 添加地址字节
@@ -131,8 +140,8 @@ std::string_view bus_i2c::format_tx_buf(const ACCESS_DATA_S& data)
131 }140 }
132 141 
133 // 添加数据缓冲区内容142 // 添加数据缓冲区内容
134- if (!data.in_buffer.empty() && data.length > 0) {143+ if (write_length > 0) {
135- m_tx_buffer.append(reinterpret_cast<const char*>(data.in_buffer.data()), data.length);144+ m_tx_buffer.append(reinterpret_cast<const char*>(data.in_buffer.data()), write_length);
136 }145 }
137 146 
138 return std::string_view(m_tx_buffer);147 return std::string_view(m_tx_buffer);
@@ -260,7 +269,7 @@ uint32_t bus_i2c::write(ACCESS_DATA_S& data)
260 }269 }
261 270 
262 // 准备写入数据271 // 准备写入数据
263- const std::string_view tx_buf = format_tx_buf(data);272+ const std::string_view tx_buf = format_tx_buf(data, data.length);
264 273 
265 int32_t ret = 0;274 int32_t ret = 0;
266 try {275 try {
@@ -81,6 +81,8 @@ public:
81#endif81#endif
82 82 
83private:83private:
84+ std::string_view format_tx_buf(const ACCESS_DATA_S& data, size_t write_length);
85+ 
84 uint8_t m_id = 0;86 uint8_t m_id = 0;
85 uint32_t m_speed = DEFAULT_SPEED;87 uint32_t m_speed = DEFAULT_SPEED;
86 uint8_t m_mode = DEFAULT_MODE;88 uint8_t m_mode = DEFAULT_MODE;
@@ -504,6 +504,9 @@ int32_t chip_base::data_access(uint32_t mask, uint32_t offset, uint32_t length,
504 ACCESS_DATA_S data = {};504 ACCESS_DATA_S data = {};
505 m_last_access_error.clear();505 m_last_access_error.clear();
506 try {506 try {
507+ if (optype == CHIP_OP_TYPE_WRITE && length > in_buffer.size()) {
508+ MC_THROW(mc::invalid_arg_exception, "write length exceeds input buffer size");
509+ }
507 data = pack_access_data(mask, offset, length, in_buffer, out_buffer, optype, protocol_flag);510 data = pack_access_data(mask, offset, length, in_buffer, out_buffer, optype, protocol_flag);
508 result = input(data);511 result = input(data);
509 512 
@@ -519,7 +522,7 @@ int32_t chip_base::data_access(uint32_t mask, uint32_t offset, uint32_t length,
519 }522 }
520 523 
521 // 对于读取操作,将读取的数据移动回原始的 out_buffer524 // 对于读取操作,将读取的数据移动回原始的 out_buffer
522- if (optype == CHIP_OP_TYPE_READ && !data.out_buffer.empty()) {525+ if (optype == CHIP_OP_TYPE_READ) {
523 out_buffer = std::move(data.out_buffer);526 out_buffer = std::move(data.out_buffer);
524 }527 }
525 } catch (const std::exception& e) {528 } catch (const std::exception& e) {
@@ -627,6 +630,15 @@ int32_t chip_base::bit_read(uint32_t offset, uint8_t length, uint32_t mask, std:
627 return ret;630 return ret;
628 }631 }
629 632 
633+ // An empty reply must not be replaced by the caller's preallocated buffer.
634+ const size_t required_length = (length == 0 && mask != 0) ? 1 : length;
635+ if (out_buffer.size() < required_length) {
636+ error = "bit_read response too short";
637+ record_error_history(error);
638+ out_buffer.clear();
639+ return HAS_ERR;
640+ }
641+ 
630 // 对读取的数据进行掩码处理。642 // 对读取的数据进行掩码处理。
631 // 注意:has_shifted_bit_read=true 的总线(如 LocalBus)已在 HAL 层完成643 // 注意:has_shifted_bit_read=true 的总线(如 LocalBus)已在 HAL 层完成
632 // (value & mask) >> ctz(mask) 的位域提取,返回的是移位后的字段值,此处无需重复处理;644 // (value & mask) >> ctz(mask) 的位域提取,返回的是移位后的字段值,此处无需重复处理;
@@ -641,13 +653,16 @@ int32_t chip_base::bit_read(uint32_t offset, uint8_t length, uint32_t mask, std:
641 result_buffer.push_back(chip_base_get_value_with_mask(mask, data_buf[0]));653 result_buffer.push_back(chip_base_get_value_with_mask(mask, data_buf[0]));
642 break;654 break;
643 case sizeof(uint16_t): {655 case sizeof(uint16_t): {
644- uint16_t value = chip_base_get_value_with_mask(mask, *((uint16_t*)data_buf));656+ uint16_t raw = static_cast<uint16_t>(data_buf[0]) | (static_cast<uint16_t>(data_buf[1]) << 8);
657+ uint16_t value = chip_base_get_value_with_mask(mask, raw);
645 result_buffer.push_back(value & 0xFF);658 result_buffer.push_back(value & 0xFF);
646 result_buffer.push_back(value >> 8);659 result_buffer.push_back(value >> 8);
647 break;660 break;
648 }661 }
649 case sizeof(uint32_t): {662 case sizeof(uint32_t): {
650- uint32_t value = chip_base_get_value_with_mask(mask, *((uint32_t*)data_buf));663+ uint32_t raw = static_cast<uint32_t>(data_buf[0]) | (static_cast<uint32_t>(data_buf[1]) << 8) |
664+ (static_cast<uint32_t>(data_buf[2]) << 16) | (static_cast<uint32_t>(data_buf[3]) << 24);
665+ uint32_t value = chip_base_get_value_with_mask(mask, raw);
651 result_buffer.push_back(value & 0xFF);666 result_buffer.push_back(value & 0xFF);
652 result_buffer.push_back((value >> 8) & 0xFF);667 result_buffer.push_back((value >> 8) & 0xFF);
653 result_buffer.push_back((value >> 16) & 0xFF);668 result_buffer.push_back((value >> 16) & 0xFF);
@@ -497,7 +497,10 @@ int32_t chip_canbus_chip::bit_write(uint32_t offset, uint8_t length, uint32_t ma
497 uint8_t cmd = static_cast<uint8_t>((offset >> 16) & 0xFF);497 uint8_t cmd = static_cast<uint8_t>((offset >> 16) & 0xFF);
498 uint16_t sig_id = static_cast<uint16_t>(offset & 0xFFFF);498 uint16_t sig_id = static_cast<uint16_t>(offset & 0xFFFF);
499 uint8_t pos = find_data_pos(length);499 uint8_t pos = find_data_pos(length);
500- uint8_t pad = pos - kEngcanSigIdLen;500+ if (in_buffer.size() < length) {
501+ MC_THROW(mc::invalid_arg_exception, "CAN bit_write input buffer too short");
502+ }
503+ uint8_t pad = pos - kEngcanSigIdLen;
501 504 
502 // Build CAN ID505 // Build CAN ID
503 uint32_t can_id = pack_can_id(0, 0x3F, 1, kEngcanCmdBase + cmd, m_address, m_protocol, kExtendedFrameType);506 uint32_t can_id = pack_can_id(0, 0x3F, 1, kEngcanCmdBase + cmd, m_address, m_protocol, kExtendedFrameType);
@@ -167,7 +167,12 @@ int32_t chip_smc::bit_read(uint32_t offset, uint8_t size, uint32_t mask, std::ve
167 para--;167 para--;
168 }168 }
169 169 
170- uint32_t data_length = read_buffer[SMC_READ_LEN_OFFSET] - SMC_EXTRA_LEN;170+ const uint32_t response_length = read_buffer[SMC_READ_LEN_OFFSET];
171+ if (response_length < SMC_EXTRA_LEN) {
172+ error = "response error, invalid smc response length";
173+ return HAS_ERR;
174+ }
175+ const uint32_t data_length = response_length - SMC_EXTRA_LEN;
171 if (para * size + size > data_length) {176 if (para * size + size > data_length) {
172 error = "request error, error para, para: " + std::to_string(para) + ", size: " + std::to_string(size) +177 error = "request error, error para, para: " + std::to_string(para) + ", size: " + std::to_string(size) +
173 ", read_len: " + std::to_string(data_length);178 ", read_len: " + std::to_string(data_length);
@@ -176,27 +181,14 @@ int32_t chip_smc::bit_read(uint32_t offset, uint8_t size, uint32_t mask, std::ve
176 181 
177 uint8_t* dataBuf = read_buffer + SMC_READ_DATA_OFFSET + para * size;182 uint8_t* dataBuf = read_buffer + SMC_READ_DATA_OFFSET + para * size;
178 183 
179- switch (size) {184+ // SMC register payloads are little-endian byte sequences.
180- case sizeof(uint8_t):185+ uint32_t raw_value = 0;
181- out_buffer.push_back(chip_smc_get_value_with_mask(mask, dataBuf[0]));186+ for (uint8_t i = 0; i < size; ++i) {
182- break;187+ raw_value |= static_cast<uint32_t>(dataBuf[i]) << (BITS_PER_BYTE * i);
183- case sizeof(uint16_t): {188+ }
184- uint16_t value = chip_smc_get_value_with_mask(mask, *((uint16_t*)dataBuf));189+ const uint32_t value = chip_smc_get_value_with_mask(mask, raw_value);
185- out_buffer.push_back(value & 0xFF);190+ for (uint8_t i = 0; i < size; ++i) {
186- out_buffer.push_back(value >> 8);191+ out_buffer.push_back(static_cast<uint8_t>(value >> (BITS_PER_BYTE * i)));
187- break;
188- }
189- case sizeof(uint32_t): {
190- uint32_t value = chip_smc_get_value_with_mask(mask, *((uint32_t*)dataBuf));
191- out_buffer.push_back(value & 0xFF);
192- out_buffer.push_back((value >> 8) & 0xFF);
193- out_buffer.push_back((value >> 16) & 0xFF);
194- out_buffer.push_back(value >> 24);
195- break;
196- }
197- default:
198- // 早期校验已拒绝 size=0/3/>4,此处不可达
199- __builtin_unreachable();
200 }192 }
201 193 
202 return HAS_OK;194 return HAS_OK;
@@ -214,8 +206,14 @@ int32_t chip_smc::bit_write(uint32_t offset, uint8_t size, uint32_t mask, const
214 return HAS_OK;206 return HAS_OK;
215 }207 }
216 208 
217- uint32_t para = offset & SMC_PARA_MASK;209+ if (in_buffer.size() < size) {
218- if (para == 0 && ((1U << (BITS_PER_BYTE * size)) - 1) == mask) {210+ error = "request error, bit_write input length is shorter than size";
211+ return HAS_ERR;
212+ }
213+ 
214+ uint32_t para = offset & SMC_PARA_MASK;
215+ const uint32_t full_mask = size == sizeof(uint32_t) ? UINT32_MAX : ((1U << (BITS_PER_BYTE * size)) - 1U);
216+ if (para == 0 && full_mask == mask) {
219 // mask 全为1时不需要读取硬件值, 直接写入硬件217 // mask 全为1时不需要读取硬件值, 直接写入硬件
220 return write(offset, size, in_buffer.data(), error);218 return write(offset, size, in_buffer.data(), error);
221 }219 }
@@ -230,28 +228,27 @@ int32_t chip_smc::bit_write(uint32_t offset, uint8_t size, uint32_t mask, const
230 return ret;228 return ret;
231 }229 }
232 230 
233- uint32_t data_length = read_buffer[SMC_READ_LEN_OFFSET] - 1;231+ const uint32_t response_length = read_buffer[SMC_READ_LEN_OFFSET];
232+ if (response_length < SMC_EXTRA_LEN) {
233+ error = "response error, invalid smc response length";
234+ return HAS_ERR;
235+ }
236+ const uint32_t data_length = response_length - SMC_EXTRA_LEN;
234 if (para * size + size > data_length) {237 if (para * size + size > data_length) {
235 error = "bit_write para size overflow";238 error = "bit_write para size overflow";
236 return HAS_ERR;239 return HAS_ERR;
237 }240 }
238 241 
239- uint8_t* dataBuf = read_buffer + SMC_READ_DATA_OFFSET + para * size;242+ uint8_t* dataBuf = read_buffer + SMC_READ_DATA_OFFSET + para * size;
240- switch (size) {243+ uint32_t original = 0;
241- case sizeof(uint8_t):244+ uint32_t input = 0;
242- *((uint8_t*)dataBuf) = chip_smc_set_value_with_mask(mask, *((uint8_t*)dataBuf), in_buffer[0]);245+ for (uint8_t i = 0; i < size; ++i) {
243- break;246+ original |= static_cast<uint32_t>(dataBuf[i]) << (BITS_PER_BYTE * i);
244- case sizeof(uint16_t):247+ input |= static_cast<uint32_t>(in_buffer[i]) << (BITS_PER_BYTE * i);
245- *((uint16_t*)dataBuf) =248+ }
246- chip_smc_set_value_with_mask(mask, *((uint16_t*)dataBuf), *((uint16_t*)in_buffer.data()));249+ const uint32_t value = chip_smc_set_value_with_mask(mask, original, input);
247- break;250+ for (uint8_t i = 0; i < size; ++i) {
248- case sizeof(uint32_t):251+ dataBuf[i] = static_cast<uint8_t>(value >> (BITS_PER_BYTE * i));
249- *((uint32_t*)dataBuf) =
250- chip_smc_set_value_with_mask(mask, *((uint32_t*)dataBuf), *((uint32_t*)in_buffer.data()));
251- break;
252- default:
253- // 早期校验已拒绝 size=0/3/>4,此处不可达
254- __builtin_unreachable();
255 }252 }
256 253 
257 return write(offset, size, dataBuf, error);254 return write(offset, size, dataBuf, error);
@@ -446,11 +443,16 @@ uint32_t chip_smc::read_data(uint32_t read_length, uint8_t* out_buffer, uint8_t
446 return HAS_ERR;443 return HAS_ERR;
447 }444 }
448 445 
449- if (out_buffer_vec.at(SMC_RESP_RAW_CC_OFFSET) != SMC_CC_SUCCESS) {446+ if (out_buffer_vec.size() <= SMC_RESP_RAW_CC_OFFSET) {
450- if (out_buffer_vec.at(SMC_RESP_RAW_CC_OFFSET) == SMC_CC_NOT_READY) {447+ error = "response error, smc response length is too short";
448+ return HAS_ERR;
449+ }
450+ 
451+ if (out_buffer_vec[SMC_RESP_RAW_CC_OFFSET] != SMC_CC_SUCCESS) {
452+ if (out_buffer_vec[SMC_RESP_RAW_CC_OFFSET] == SMC_CC_NOT_READY) {
451 continue;453 continue;
452 } else {454 } else {
453- m_last_smc_cc = out_buffer_vec.at(SMC_RESP_RAW_CC_OFFSET);455+ m_last_smc_cc = out_buffer_vec[SMC_RESP_RAW_CC_OFFSET];
454 error = get_object_name() + " Smc read_data : request error, " +456 error = get_object_name() + " Smc read_data : request error, " +
455 std::string(smc_cc_to_string(m_last_smc_cc)) +457 std::string(smc_cc_to_string(m_last_smc_cc)) +
456 " , SMC completion code:" + std::to_string(m_last_smc_cc);458 " , SMC completion code:" + std::to_string(m_last_smc_cc);
@@ -459,12 +461,23 @@ uint32_t chip_smc::read_data(uint32_t read_length, uint8_t* out_buffer, uint8_t
459 }461 }
460 }462 }
461 463 
462- if (static_cast<uint32_t>(out_buffer_vec.at(SMC_RESP_RAW_READ_LEN_OFFSET) + SMC_MIN_READ_LEN) > read_length) {464+ const uint32_t response_length = out_buffer_vec[SMC_RESP_RAW_READ_LEN_OFFSET];
463- read_length = out_buffer_vec.at(SMC_RESP_RAW_READ_LEN_OFFSET) + SMC_MIN_READ_LEN;465+ if (response_length < SMC_EXTRA_LEN) {
466+ error = "response error, invalid smc response length";
467+ return HAS_ERR;
468+ }
469+ const uint32_t frame_length = response_length + SMC_MIN_READ_LEN;
470+ if (frame_length > read_length) {
471+ read_length = frame_length;
464 continue;472 continue;
465 }473 }
466 474 
467- out_buffer_vec.resize(out_buffer_vec.at(SMC_RESP_RAW_READ_LEN_OFFSET) + SMC_MIN_READ_LEN);475+ if (out_buffer_vec.size() < frame_length) {
476+ error = "response error, smc response length exceeds received bytes";
477+ return HAS_ERR;
478+ }
479+ 
480+ out_buffer_vec.resize(frame_length);
468 481 
469 // 倒序插入, 拼凑原始响应数据计算响应crc482 // 倒序插入, 拼凑原始响应数据计算响应crc
470 out_buffer_vec.insert(out_buffer_vec.begin(), m_address | 1);483 out_buffer_vec.insert(out_buffer_vec.begin(), m_address | 1);
@@ -473,8 +486,7 @@ uint32_t chip_smc::read_data(uint32_t read_length, uint8_t* out_buffer, uint8_t
473 486 
474 // 构建crc_input时去掉最后一个字节的响应crc487 // 构建crc_input时去掉最后一个字节的响应crc
475 std::string crc_input(out_buffer_vec.begin(), out_buffer_vec.end() - 1);488 std::string crc_input(out_buffer_vec.begin(), out_buffer_vec.end() - 1);
476- uint8_t real_length = out_buffer_vec.at(SMC_READ_LEN_OFFSET);489+ uint8_t crc8 = mc::crypto::CRC8().calculate(crc_input);
477- uint8_t crc8 = mc::crypto::CRC8().calculate(crc_input);
478 if (crc8 != out_buffer_vec.back()) {490 if (crc8 != out_buffer_vec.back()) {
479 error =491 error =
480 "crc mismatch, crc8: " + std::to_string(crc8) + ", expected: " + std::to_string(out_buffer_vec.back());492 "crc mismatch, crc8: " + std::to_string(crc8) + ", expected: " + std::to_string(out_buffer_vec.back());
@@ -623,7 +635,13 @@ uint32_t chip_smc::forward_read(uint32_t length, std::vector<uint8_t>& data_out)
623 return ret;635 return ret;
624 }636 }
625 637 
626- uint32_t actual_length = std::min(length, static_cast<uint32_t>(out_buffer[SMC_READ_LEN_OFFSET] - 1U));638+ const uint32_t response_length = out_buffer[SMC_READ_LEN_OFFSET];
639+ if (response_length < SMC_EXTRA_LEN ||
640+ response_length - SMC_EXTRA_LEN > SMC_MAX_BUFFER_LEN - SMC_READ_DATA_OFFSET) {
641+ elog("forward_read failed: invalid smc response length");
642+ return HAS_ERR;
643+ }
644+ const uint32_t actual_length = std::min(length, response_length - SMC_EXTRA_LEN);
627 data_out.resize(actual_length);645 data_out.resize(actual_length);
628 for (uint32_t i = 0; i < actual_length; i++) {646 for (uint32_t i = 0; i < actual_length; i++) {
629 data_out[i] = out_buffer[i + SMC_READ_DATA_OFFSET];647 data_out[i] = out_buffer[i + SMC_READ_DATA_OFFSET];
@@ -685,10 +703,20 @@ int32_t chip_smc::forward_by_front_chip(ACCESS_DATA_S& data, const std::string&
685 return forward_read_mctp(data.length, data.out_buffer, error);703 return forward_read_mctp(data.length, data.out_buffer, error);
686 }704 }
687 705 
706+ if (data.offset_width > sizeof(data.offset)) {
707+ elog("forward request has invalid offset width: ${width}", ("width", data.offset_width));
708+ return HAS_ERR;
709+ }
710+ 
688 if (data.optype == CHIP_OP_TYPE_WRITE) {711 if (data.optype == CHIP_OP_TYPE_WRITE) {
689 uint8_t buffer[BUFFER_LEN] = {0};712 uint8_t buffer[BUFFER_LEN] = {0};
690 uint8_t chip_type = is_smc ? FORWARD_TYPE_SMC : 0x00;713 uint8_t chip_type = is_smc ? FORWARD_TYPE_SMC : 0x00;
691 714 
715+ if (data.length > data.in_buffer.size() || data.length > BUFFER_LEN - data.offset_width) {
716+ elog("forward write length exceeds input or buffer capacity");
717+ return HAS_ERR;
718+ }
719+ 
692 uint32_t offset = calc_forward_opcode(data.addr, data.length, chip_type, bus_id);720 uint32_t offset = calc_forward_opcode(data.addr, data.length, chip_type, bus_id);
693 uint32_t buffer_offset = 0;721 uint32_t buffer_offset = 0;
694 for (uint32_t i = data.offset_width; i > 0; i--) {722 for (uint32_t i = data.offset_width; i > 0; i--) {
@@ -118,9 +118,18 @@ bool NetworkPort::update_partition_info_by_ncsi()
118void NetworkPort::start_ncsi_update_task_broadcom(ncsi_over_mctp_broadcom_ptr ncsi_over_mctp, mc::milliseconds interval,118void NetworkPort::start_ncsi_update_task_broadcom(ncsi_over_mctp_broadcom_ptr ncsi_over_mctp, mc::milliseconds interval,
119 uint8_t package_id)119 uint8_t package_id)
120{120{
121+ // A queued callback can retain the old timer beyond member replacement.
122+ // Stop and join it before changing the state that its callback accesses.
123+ if (m_ncsi_timer) {
124+ m_ncsi_timer->stop();
125+ }
121 m_ncsi_over_mctp_broadcom = ncsi_over_mctp;126 m_ncsi_over_mctp_broadcom = ncsi_over_mctp;
122- m_ncsi_timer = mc::make_shared<mc::timer>(this);127+ // The interface is a value member; queued timers must not retain its
123- m_package_id = package_id;128+ // parent link, and must run on the owning device's executor.
129+ m_ncsi_timer = mc::make_shared<mc::timer>();
130+ auto* owner = get_owner();
131+ m_ncsi_timer->bind_executor(owner != nullptr ? owner->executor() : executor());
132+ m_package_id = package_id;
124 133 
125 m_ncsi_timer->timeout.connect([this]() {134 m_ncsi_timer->timeout.connect([this]() {
126 update_partition_info_by_ncsi();135 update_partition_info_by_ncsi();
@@ -56,6 +56,7 @@ public:
56 bool SetLLDPTxEnable(bool enable) override;56 bool SetLLDPTxEnable(bool enable) override;
57 57 
58private:58private:
59+ friend struct BoardcomNcsiTimerTestAccess;
59 ncsi_over_mctp_broadcom_ptr m_ncsi_over_mctp_broadcom;60 ncsi_over_mctp_broadcom_ptr m_ncsi_over_mctp_broadcom;
60 mc::shared_ptr<mc::timer> m_ncsi_timer;61 mc::shared_ptr<mc::timer> m_ncsi_timer;
61 uint8_t m_package_id = 0;62 uint8_t m_package_id = 0;
@@ -44,9 +44,18 @@ bool OpticalModule::get_presence_info_by_ncsi()
44void OpticalModule::start_ncsi_update_task_broadcom(ncsi_over_mctp_broadcom_ptr ncsi_over_mctp,44void OpticalModule::start_ncsi_update_task_broadcom(ncsi_over_mctp_broadcom_ptr ncsi_over_mctp,
45 mc::milliseconds interval, uint8_t package_id)45 mc::milliseconds interval, uint8_t package_id)
46{46{
47+ // A queued callback can retain the old timer beyond member replacement.
48+ // Stop and join it before changing the state that its callback accesses.
49+ if (m_ncsi_timer) {
50+ m_ncsi_timer->stop();
51+ }
47 m_ncsi_over_mctp_broadcom = ncsi_over_mctp;52 m_ncsi_over_mctp_broadcom = ncsi_over_mctp;
48- m_ncsi_timer = mc::make_shared<mc::timer>(this);53+ // The interface is a value member; queued timers must not retain its
49- m_package_id = package_id;54+ // parent link, and must run on the owning device's executor.
55+ m_ncsi_timer = mc::make_shared<mc::timer>();
56+ auto* owner = get_owner();
57+ m_ncsi_timer->bind_executor(owner != nullptr ? owner->executor() : executor());
58+ m_package_id = package_id;
50 m_ncsi_timer->timeout.connect([this]() {59 m_ncsi_timer->timeout.connect([this]() {
51 get_presence_info_by_ncsi();60 get_presence_info_by_ncsi();
52 });61 });
@@ -62,6 +62,7 @@ public:
62 }62 }
63 63 
64private:64private:
65+ friend struct BoardcomNcsiTimerTestAccess;
65 ncsi_over_mctp_broadcom_ptr m_ncsi_over_mctp_broadcom;66 ncsi_over_mctp_broadcom_ptr m_ncsi_over_mctp_broadcom;
66 mc::shared_ptr<mc::timer> m_ncsi_timer;67 mc::shared_ptr<mc::timer> m_ncsi_timer;
67 uint8_t m_package_id = 0;68 uint8_t m_package_id = 0;
@@ -18,6 +18,7 @@
18#include <mc/runtime.h>18#include <mc/runtime.h>
19#include <string>19#include <string>
20 20 
21+#include "../../owned_timer.h"
21#include "hi1825_board_card.h"22#include "hi1825_board_card.h"
22 23 
23namespace dev {24namespace dev {
@@ -29,7 +30,8 @@ hi1825_board_card::hi1825_board_card()
29 30 
30hi1825_board_card::~hi1825_board_card()31hi1825_board_card::~hi1825_board_card()
31{32{
32- // 清理资源33+ stop_owned_timer(m_smbus_start_delay_timer);
34+ stop_owned_timer(m_smbus_update_timer);
33}35}
34 36 
35void hi1825_board_card::init_network_ports()37void hi1825_board_card::init_network_ports()
@@ -260,6 +262,8 @@ void hi1825_board_card::attach_smbus_obj(smbus_obj_ptr smbus_obj)
260 262 
261bool hi1825_board_card::start_smbus_protocol()263bool hi1825_board_card::start_smbus_protocol()
262{264{
265+ stop_owned_timer(m_smbus_start_delay_timer);
266+ stop_owned_timer(m_smbus_update_timer);
263 register_RefChip();267 register_RefChip();
264 if (!init_smbus_protocol()) {268 if (!init_smbus_protocol()) {
265 return false;269 return false;
@@ -277,16 +281,16 @@ bool hi1825_board_card::start_smbus_protocol()
277 281 
278 const uint8_t slot = m_pcie_device.Slot.get_value().as<uint8_t>();282 const uint8_t slot = m_pcie_device.Slot.get_value().as<uint8_t>();
279 283 
280- m_smbus_update_timer = mc::make_shared<mc::timer>(this);284+ m_smbus_update_timer = mc::make_shared<mc::timer>();
285+ m_smbus_update_timer->bind_executor(executor());
281 m_smbus_update_timer->timeout.connect([this]() {286 m_smbus_update_timer->timeout.connect([this]() {
282 update_info_by_smbus();287 update_info_by_smbus();
283 });288 });
284 m_smbus_update_timer->set_single_shot(false);289 m_smbus_update_timer->set_single_shot(false);
285 290 
286- m_smbus_start_delay_timer = mc::make_shared<mc::timer>(this);291+ m_smbus_start_delay_timer = mc::make_shared<mc::timer>();
292+ m_smbus_start_delay_timer->bind_executor(executor());
287 m_smbus_start_delay_timer->timeout.connect([this]() {293 m_smbus_start_delay_timer->timeout.connect([this]() {
288- m_smbus_start_delay_timer->stop();
289- m_smbus_start_delay_timer.reset();
290 m_smbus_update_timer->start(m_smbus_interval);294 m_smbus_update_timer->start(m_smbus_interval);
291 });295 });
292 m_smbus_start_delay_timer->set_single_shot(true);296 m_smbus_start_delay_timer->set_single_shot(true);
@@ -296,14 +300,8 @@ bool hi1825_board_card::start_smbus_protocol()
296 300 
297void hi1825_board_card::stop_smbus_protocol()301void hi1825_board_card::stop_smbus_protocol()
298{302{
299- if (m_smbus_start_delay_timer) {303+ stop_owned_timer(m_smbus_start_delay_timer);
300- m_smbus_start_delay_timer->stop();304+ stop_owned_timer(m_smbus_update_timer);
301- m_smbus_start_delay_timer.reset();
302- }
303- if (m_smbus_update_timer) {
304- m_smbus_update_timer->stop();
305- m_smbus_update_timer.reset();
306- }
307 m_network_adapter_log_collection.clean_smbus_obj();305 m_network_adapter_log_collection.clean_smbus_obj();
308 m_network_adapter_oem.clean_smbus_obj();306 m_network_adapter_oem.clean_smbus_obj();
309 m_pcie_card.clean_smbus_obj();307 m_pcie_card.clean_smbus_obj();
@@ -12,6 +12,9 @@
12 12 
13#include <endian.h>13#include <endian.h>
14 14 
15+#include <cstddef>
16+#include <cstring>
17+ 
15#include "interface/network_adapter/prbs_test.h"18#include "interface/network_adapter/prbs_test.h"
16 19 
17#include <securec.h>20#include <securec.h>
@@ -126,6 +129,15 @@ struct prbs_get_result_port_rsp_t {
126};129};
127#pragma pack()130#pragma pack()
128 131 
132+template <typename Response>
133+static uint32_t read_bit_error_rate(const std::string& rsp, size_t index)
134+{
135+ uint32_t value = 0;
136+ const auto offset = offsetof(Response, bit_error_rate) + index * sizeof(value);
137+ std::memcpy(&value, rsp.data() + offset, sizeof(value));
138+ return le32toh(value);
139+}
140+ 
129void NetworkAdapter_PRBSTest::set_smbus_obj(mc::shared_ptr<smbus> smbus_obj)141void NetworkAdapter_PRBSTest::set_smbus_obj(mc::shared_ptr<smbus> smbus_obj)
130{142{
131 m_smbus_obj = smbus_obj;143 m_smbus_obj = smbus_obj;
@@ -193,7 +205,7 @@ static std::tuple<uint8_t, std::vector<uint32_t>, uint32_t, uint32_t> parse_prbs
193 std::vector<uint32_t> bit_error_rates;205 std::vector<uint32_t> bit_error_rates;
194 bit_error_rates.reserve(PRBS_CHANNEL_NUM);206 bit_error_rates.reserve(PRBS_CHANNEL_NUM);
195 for (size_t i = 0; i < PRBS_CHANNEL_NUM; i++) {207 for (size_t i = 0; i < PRBS_CHANNEL_NUM; i++) {
196- bit_error_rates.push_back(i < lane_num ? rsp_struct->bit_error_rate[i] : 0);208+ bit_error_rates.push_back(i < lane_num ? read_bit_error_rate<prbs_get_result_om_rsp_t>(rsp, i) : 0);
197 }209 }
198 uint32_t rx_lost_of_lock = rsp_struct->rx_loss_of_lock;210 uint32_t rx_lost_of_lock = rsp_struct->rx_loss_of_lock;
199 uint32_t tx_lost_of_lock = rsp_struct->tx_loss_of_lock;211 uint32_t tx_lost_of_lock = rsp_struct->tx_loss_of_lock;
@@ -215,7 +227,7 @@ static std::tuple<uint8_t, std::vector<uint32_t>> parse_prbs_get_result_port(con
215 std::vector<uint32_t> bit_error_rates;227 std::vector<uint32_t> bit_error_rates;
216 bit_error_rates.reserve(PRBS_CHANNEL_NUM);228 bit_error_rates.reserve(PRBS_CHANNEL_NUM);
217 for (size_t i = 0; i < PRBS_CHANNEL_NUM; i++) {229 for (size_t i = 0; i < PRBS_CHANNEL_NUM; i++) {
218- bit_error_rates.push_back(rsp_struct->bit_error_rate[i]);230+ bit_error_rates.push_back(read_bit_error_rate<prbs_get_result_port_rsp_t>(rsp, i));
219 }231 }
220 return {rsp_struct->exe_result, bit_error_rates};232 return {rsp_struct->exe_result, bit_error_rates};
221}233}
@@ -12,6 +12,7 @@
12 12 
13#include <mc/log.h>13#include <mc/log.h>
14 14 
15+#include "../../../owned_timer.h"
15#include "interface/network_port/fibrechannel.h"16#include "interface/network_port/fibrechannel.h"
16 17 
17namespace dev {18namespace dev {
@@ -65,7 +66,8 @@ void NetworkPort_FibreChannel::start_pldm_update_task_huawei(pldm_over_mctp_hw_p
65 stop_pldm_update_task();66 stop_pldm_update_task();
66 67 
67 m_pldm_over_mctp_hw = pldm_over_mctp;68 m_pldm_over_mctp_hw = pldm_over_mctp;
68- m_pldm_timer = mc::make_shared<mc::timer>(this);69+ m_pldm_timer = mc::make_shared<mc::timer>();
70+ m_pldm_timer->bind_executor(executor());
69 m_pldm_timer->timeout.connect([this, port_id]() {71 m_pldm_timer->timeout.connect([this, port_id]() {
70 update_fcinfo_by_pldm(port_id);72 update_fcinfo_by_pldm(port_id);
71 });73 });
@@ -75,10 +77,7 @@ void NetworkPort_FibreChannel::start_pldm_update_task_huawei(pldm_over_mctp_hw_p
75 77 
76void NetworkPort_FibreChannel::stop_pldm_update_task()78void NetworkPort_FibreChannel::stop_pldm_update_task()
77{79{
78- if (m_pldm_timer) {80+ stop_owned_timer(m_pldm_timer);
79- m_pldm_timer->stop();
80- m_pldm_timer.reset();
81- }
82 if (m_pldm_over_mctp_hw) {81 if (m_pldm_over_mctp_hw) {
83 m_pldm_over_mctp_hw = nullptr;82 m_pldm_over_mctp_hw = nullptr;
84 }83 }
@@ -15,6 +15,7 @@
15#include <cmath>15#include <cmath>
16#include <vector>16#include <vector>
17 17 
18+#include "../../../owned_timer.h"
18#include "interface/network_port/serdes.h"19#include "interface/network_port/serdes.h"
19#include "securec.h"20#include "securec.h"
20 21 
@@ -117,7 +118,8 @@ void NetworkPort_Serdes::start_smbus_update_task(smbus_obj_ptr smbus_obj, mc::mi
117 stop_smbus_update_task();118 stop_smbus_update_task();
118 119 
119 m_smbus_obj = smbus_obj;120 m_smbus_obj = smbus_obj;
120- m_smbus_timer = mc::make_shared<mc::timer>(this);121+ m_smbus_timer = mc::make_shared<mc::timer>();
122+ m_smbus_timer->bind_executor(executor());
121 m_smbus_timer->timeout.connect([this, port_id]() {123 m_smbus_timer->timeout.connect([this, port_id]() {
122 update_port_serdes_info_by_smbus(port_id);124 update_port_serdes_info_by_smbus(port_id);
123 });125 });
@@ -127,10 +129,7 @@ void NetworkPort_Serdes::start_smbus_update_task(smbus_obj_ptr smbus_obj, mc::mi
127 129 
128void NetworkPort_Serdes::stop_smbus_update_task()130void NetworkPort_Serdes::stop_smbus_update_task()
129{131{
130- if (m_smbus_timer) {132+ stop_owned_timer(m_smbus_timer);
131- m_smbus_timer->stop();
132- m_smbus_timer.reset();
133- }
134 m_smbus_obj = nullptr;133 m_smbus_obj = nullptr;
135 restore_property_defaults();134 restore_property_defaults();
136}135}
@@ -12,6 +12,7 @@
12 12 
13#include "interface/nvme/smart.h"13#include "interface/nvme/smart.h"
14#include <algorithm>14#include <algorithm>
15+#include <cmath>
15#include <cstddef>16#include <cstddef>
16#include <cstring>17#include <cstring>
17#include <mc/log.h>18#include <mc/log.h>
@@ -73,19 +74,20 @@ bool NVMe_Smart::update_smart_by_mctp()
73 size_t copy_len = std::min(smart_data.size() - header_size, sizeof(nvme_smart_log_response_t));74 size_t copy_len = std::min(smart_data.size() - header_size, sizeof(nvme_smart_log_response_t));
74 (void)memcpy_s(&smart_log, sizeof(nvme_smart_log_response_t), smart_data.data() + header_size, copy_len);75 (void)memcpy_s(&smart_log, sizeof(nvme_smart_log_response_t), smart_data.data() + header_size, copy_len);
75 76 
76- uint64_t power_on_hours = 0;77+ uint64_t power_on_hours_low = 0;
78+ uint64_t power_on_hours_high = 0;
77 for (int i = 0; i < 8; ++i) {79 for (int i = 0; i < 8; ++i) {
78- power_on_hours |= (static_cast<uint64_t>(smart_log.power_on_hours_l[i]) << (i * 8));80+ power_on_hours_low |= static_cast<uint64_t>(smart_log.power_on_hours_l[i]) << (i * 8);
79- }81+ power_on_hours_high |= static_cast<uint64_t>(smart_log.power_on_hours_h[i]) << (i * 8);
80- for (int i = 0; i < 8; ++i) {
81- power_on_hours |= (static_cast<uint64_t>(smart_log.power_on_hours_h[i]) << ((i + 8) * 8));
82 }82 }
83+ const double power_on_hours =
84+ std::ldexp(static_cast<double>(power_on_hours_high), 64) + static_cast<double>(power_on_hours_low);
83 85 
84 CriticalWarning.set_value(smart_log.critical_warning);86 CriticalWarning.set_value(smart_log.critical_warning);
85 AvailableSpare.set_value(smart_log.avail_spare);87 AvailableSpare.set_value(smart_log.avail_spare);
86 UsedPercentage.set_value(smart_log.percent_used);88 UsedPercentage.set_value(smart_log.percent_used);
87 TLCSpareBlockPercentage.set_value(smart_log.avail_spare);89 TLCSpareBlockPercentage.set_value(smart_log.avail_spare);
88- PowerOnHours.set_value(static_cast<double>(power_on_hours));90+ PowerOnHours.set_value(power_on_hours);
89 91 
90 return true;92 return true;
91}93}
@@ -14,6 +14,7 @@
14#include <device_tree/interface/PCIeDevice.h>14#include <device_tree/interface/PCIeDevice.h>
15#include <mc/log.h>15#include <mc/log.h>
16#include <nvme/nvme_mi_over_mctp_huawei.h>16#include <nvme/nvme_mi_over_mctp_huawei.h>
17+#include "interface/timer_lifetime.h"
17 18 
18namespace dev {19namespace dev {
19 20 
@@ -25,12 +26,9 @@ hw_nvme_pcie4::hw_nvme_pcie4() : m_mctp_object(nullptr), m_ctrl_id(0)
25 26 
26hw_nvme_pcie4::~hw_nvme_pcie4()27hw_nvme_pcie4::~hw_nvme_pcie4()
27{28{
28- if (m_nvme_mi_over_mctp_obj) {29+ m_upgrade.wait_for_completion();
29- m_nvme_mi_over_mctp_obj.reset();30+ stop_vpd_update_tasks();
30- }31+ stop_mctp_update_tasks();
31- if (m_vpd_obj) {
32- m_vpd_obj.reset();
33- }
34}32}
35 33 
36bool hw_nvme_pcie4::init(mc::mutable_dict& csr_object, const mc::dict& connector)34bool hw_nvme_pcie4::init(mc::mutable_dict& csr_object, const mc::dict& connector)
@@ -359,7 +357,7 @@ void hw_nvme_pcie4::start_vpd_update_tasks()
359 m_nvme_product_info.update_product_info_by_vpd();357 m_nvme_product_info.update_product_info_by_vpd();
360 m_nvme_multi_record.update_multi_record_by_vpd();358 m_nvme_multi_record.update_multi_record_by_vpd();
361 };359 };
362- m_static_update_timer = mc::make_shared<mc::timer>(this);360+ huawei::replace_timer(m_static_update_timer);
363 m_static_update_timer->timeout.connect(refresh_static_vpd);361 m_static_update_timer->timeout.connect(refresh_static_vpd);
364 m_static_update_timer->set_single_shot(false);362 m_static_update_timer->set_single_shot(false);
365 m_static_update_timer->start(m_vpd_static_interval);363 m_static_update_timer->start(m_vpd_static_interval);
@@ -405,10 +403,7 @@ void hw_nvme_pcie4::stop_vpd_update_tasks()
405 m_cooling.stop_vpd_update_task();403 m_cooling.stop_vpd_update_task();
406 m_nvme_status.stop_vpd_update_task();404 m_nvme_status.stop_vpd_update_task();
407 m_nvme_basic_management.stop_vpd_update_task();405 m_nvme_basic_management.stop_vpd_update_task();
408- if (m_static_update_timer) {406+ huawei::stop_and_release_timer(m_static_update_timer);
409- m_static_update_timer->stop();
410- m_static_update_timer.reset();
411- }
412 if (m_vpd_obj) {407 if (m_vpd_obj) {
413 m_vpd_obj.reset();408 m_vpd_obj.reset();
414 }409 }
@@ -88,6 +88,7 @@ public:
88 NVMe_Oem m_nvme_oem;88 NVMe_Oem m_nvme_oem;
89 89 
90private:90private:
91+ friend struct HuaweiTimerTestAccess;
91 // VPD 协议类型(华为只支持 NVMe-MI 和 SFF)92 // VPD 协议类型(华为只支持 NVMe-MI 和 SFF)
92 // NVME_MI = 0, SFF = 193 // NVME_MI = 0, SFF = 1
93 enum class VpdProtocolType {94 enum class VpdProtocolType {
@@ -12,6 +12,7 @@
12 12 
13#include "interface/cooling.h"13#include "interface/cooling.h"
14#include <mc/log.h>14#include <mc/log.h>
15+#include "interface/timer_lifetime.h"
15 16 
16namespace {17namespace {
17 18 
@@ -41,13 +42,12 @@ Cooling::Cooling(mc::milliseconds no_data_debounce) : m_temp_no_data_debounce(no
41 42 
42void Cooling::start_vpd_update_task(vpd_protocol_ptr vpd, mc::milliseconds interval)43void Cooling::start_vpd_update_task(vpd_protocol_ptr vpd, mc::milliseconds interval)
43{44{
44- m_vpd_obj = vpd;45+ huawei::replace_timer(m_vpd_timer);
45- m_vpd_timer = mc::make_shared<mc::timer>(this);46+ m_vpd_obj = vpd;
46 m_vpd_timer->timeout.connect([this]() {47 m_vpd_timer->timeout.connect([this]() {
47 update_cooling_by_vpd();48 update_cooling_by_vpd();
48 });49 });
49 m_vpd_timer->set_single_shot(false);50 m_vpd_timer->set_single_shot(false);
50- m_vpd_timer->start(interval);
51 51 
52 // 设备重新插入后再次 start,重置温度防抖状态使新会话从干净状态开始52 // 设备重新插入后再次 start,重置温度防抖状态使新会话从干净状态开始
53 // (0x80 的 5 分钟计时不跨会话累计)53 // (0x80 的 5 分钟计时不跨会话累计)
@@ -58,14 +58,12 @@ void Cooling::start_vpd_update_task(vpd_protocol_ptr vpd, mc::milliseconds inter
58 m_temp_avg_debounce.clear_debounce_val();58 m_temp_avg_debounce.clear_debounce_val();
59 m_temp_fail_debounce = mc::debounce::BinaryContinue(3, 1);59 m_temp_fail_debounce = mc::debounce::BinaryContinue(3, 1);
60 m_has_valid_temp = false;60 m_has_valid_temp = false;
61+ m_vpd_timer->start(interval);
61}62}
62 63 
63void Cooling::stop_vpd_update_task()64void Cooling::stop_vpd_update_task()
64{65{
65- if (m_vpd_timer) {66+ huawei::stop_and_release_timer(m_vpd_timer);
66- m_vpd_timer->stop();
67- m_vpd_timer.reset();
68- }
69}67}
70 68 
71bool Cooling::update_cooling_by_vpd()69bool Cooling::update_cooling_by_vpd()
@@ -38,6 +38,7 @@ public:
38 bool update_cooling_by_vpd();38 bool update_cooling_by_vpd();
39 39 
40private:40private:
41+ friend struct HuaweiTimerTestAccess;
41 vpd_protocol_ptr m_vpd_obj;42 vpd_protocol_ptr m_vpd_obj;
42 mc::shared_ptr<mc::timer> m_vpd_timer;43 mc::shared_ptr<mc::timer> m_vpd_timer;
43 44 
@@ -13,6 +13,7 @@
13#include "interface/nvme/basic_management.h"13#include "interface/nvme/basic_management.h"
14#include <chrono>14#include <chrono>
15#include <mc/log.h>15#include <mc/log.h>
16+#include "interface/timer_lifetime.h"
16#include "mc/debounce/binary_continue.h"17#include "mc/debounce/binary_continue.h"
17 18 
18namespace dev {19namespace dev {
@@ -20,17 +21,16 @@ namespace dev {
20void NVMe_BasicManagement::start_vpd_update_task(vpd_protocol_ptr vpd, mc::milliseconds interval,21void NVMe_BasicManagement::start_vpd_update_task(vpd_protocol_ptr vpd, mc::milliseconds interval,
21 std::function<void()> on_update_callback)22 std::function<void()> on_update_callback)
22{23{
24+ huawei::replace_timer(m_vpd_timer);
23 m_vpd_obj = vpd;25 m_vpd_obj = vpd;
24 m_on_update_callback = on_update_callback;26 m_on_update_callback = on_update_callback;
25 m_start_time = std::chrono::steady_clock::now();27 m_start_time = std::chrono::steady_clock::now();
26- m_vpd_timer = mc::make_shared<mc::timer>(this);
27 m_vpd_timer->timeout.connect([this]() {28 m_vpd_timer->timeout.connect([this]() {
28 if (update_basic_management_by_vpd() && m_on_update_callback) {29 if (update_basic_management_by_vpd() && m_on_update_callback) {
29 m_on_update_callback();30 m_on_update_callback();
30 }31 }
31 });32 });
32 m_vpd_timer->set_single_shot(false);33 m_vpd_timer->set_single_shot(false);
33- m_vpd_timer->start(interval);
34 debounce_port0_link_active =34 debounce_port0_link_active =
35 std::make_unique<mc::debounce::BinaryContinue>(NVME_PL_DEBOUNCE_HIGH, NVME_PL_DEBOUNCE_LOW);35 std::make_unique<mc::debounce::BinaryContinue>(NVME_PL_DEBOUNCE_HIGH, NVME_PL_DEBOUNCE_LOW);
36 debounce_drive_functional =36 debounce_drive_functional =
@@ -39,14 +39,12 @@ void NVMe_BasicManagement::start_vpd_update_task(vpd_protocol_ptr vpd, mc::milli
39 std::make_unique<mc::debounce::BinaryContinue>(NVME_DN_DEBOUNCE_HIGH, NVME_DN_DEBOUNCE_LOW);39 std::make_unique<mc::debounce::BinaryContinue>(NVME_DN_DEBOUNCE_HIGH, NVME_DN_DEBOUNCE_LOW);
40 debounce_link_fault =40 debounce_link_fault =
41 std::make_unique<mc::debounce::BinaryContinue>(NVME_LINK_FAULT_DEBOUNCE_HIGH, NVME_LINK_FAULT_DEBOUNCE_LOW);41 std::make_unique<mc::debounce::BinaryContinue>(NVME_LINK_FAULT_DEBOUNCE_HIGH, NVME_LINK_FAULT_DEBOUNCE_LOW);
42+ m_vpd_timer->start(interval);
42}43}
43 44 
44void NVMe_BasicManagement::stop_vpd_update_task()45void NVMe_BasicManagement::stop_vpd_update_task()
45{46{
46- if (m_vpd_timer) {47+ huawei::stop_and_release_timer(m_vpd_timer);
47- m_vpd_timer->stop();
48- m_vpd_timer.reset();
49- }
50}48}
51 49 
52bool NVMe_BasicManagement::update_basic_management_by_vpd()50bool NVMe_BasicManagement::update_basic_management_by_vpd()
@@ -47,6 +47,7 @@ public:
47 bool update_basic_management_by_vpd();47 bool update_basic_management_by_vpd();
48 48 
49private:49private:
50+ friend struct HuaweiTimerTestAccess;
50 vpd_protocol_ptr m_vpd_obj;51 vpd_protocol_ptr m_vpd_obj;
51 mc::shared_ptr<mc::timer> m_vpd_timer;52 mc::shared_ptr<mc::timer> m_vpd_timer;
52 std::function<void()> m_on_update_callback;53 std::function<void()> m_on_update_callback;
@@ -20,6 +20,7 @@
20#include <mc/log.h>20#include <mc/log.h>
21#include <mutex>21#include <mutex>
22#include <sstream>22#include <sstream>
23+#include "interface/timer_lifetime.h"
23#include "nvme/nvme_mi_over_mctp.h"24#include "nvme/nvme_mi_over_mctp.h"
24 25 
25namespace dev {26namespace dev {
@@ -770,7 +771,7 @@ void NVMe_LogCollection::start_log_collection_task()
770 return;771 return;
771 }772 }
772 773 
773- m_log_timer = mc::make_shared<mc::timer>(this);774+ huawei::replace_timer(m_log_timer);
774 m_log_timer->timeout.connect([this]() {775 m_log_timer->timeout.connect([this]() {
775 collect_all_logs();776 collect_all_logs();
776 });777 });
@@ -785,10 +786,7 @@ void NVMe_LogCollection::start_log_collection_task()
785 786 
786void NVMe_LogCollection::stop_log_collection_task()787void NVMe_LogCollection::stop_log_collection_task()
787{788{
788- if (m_log_timer) {789+ huawei::stop_and_release_timer(m_log_timer);
789- m_log_timer->stop();
790- m_log_timer.reset();
791- }
792}790}
793 791 
794} // namespace dev792} // namespace dev
@@ -29,8 +29,11 @@ constexpr auto LOG_COLLECTION_INTERVAL = std::chrono::hours(24);
29 29 
30class MC_API NVMe_LogCollection : public mc::engine::interface<NVMe_LogCollection, gen::NVMe_LogCollection> {30class MC_API NVMe_LogCollection : public mc::engine::interface<NVMe_LogCollection, gen::NVMe_LogCollection> {
31public:31public:
32- NVMe_LogCollection() = default;32+ NVMe_LogCollection() = default;
33- ~NVMe_LogCollection() = default;33+ ~NVMe_LogCollection()
34+ {
35+ stop_log_collection_task();
36+ }
34 37 
35 bool DumpErrorInfo(const std::string& path) override;38 bool DumpErrorInfo(const std::string& path) override;
36 bool DumpSmartInfo(const std::string& path) override;39 bool DumpSmartInfo(const std::string& path) override;
@@ -53,6 +56,7 @@ public:
53 void collect_all_logs();56 void collect_all_logs();
54 57 
55private:58private:
59+ friend struct HuaweiTimerTestAccess;
56 nvme_mi_over_mctp_ptr m_nvme_mi_obj;60 nvme_mi_over_mctp_ptr m_nvme_mi_obj;
57 uint32_t m_ctrl_id = 0;61 uint32_t m_ctrl_id = 0;
58 mc::shared_ptr<mc::timer> m_log_timer;62 mc::shared_ptr<mc::timer> m_log_timer;
@@ -12,11 +12,13 @@
12 12 
13#include "interface/nvme/smart.h"13#include "interface/nvme/smart.h"
14#include <algorithm>14#include <algorithm>
15+#include <cmath>
15#include <cstddef>16#include <cstddef>
16#include <cstring>17#include <cstring>
17#include <mc/log.h>18#include <mc/log.h>
18#include <nvme/nvme_mi_over_mctp.h>19#include <nvme/nvme_mi_over_mctp.h>
19#include "interface/nvme/oem.h"20#include "interface/nvme/oem.h"
21+#include "interface/timer_lifetime.h"
20#include "securec.h"22#include "securec.h"
21 23 
22namespace dev {24namespace dev {
@@ -24,9 +26,9 @@ namespace dev {
24void NVMe_Smart::start_mctp_update_task(nvme_mi_over_mctp_ptr nvme_mi_obj, mc::milliseconds interval,26void NVMe_Smart::start_mctp_update_task(nvme_mi_over_mctp_ptr nvme_mi_obj, mc::milliseconds interval,
25 std::function<void()> on_update_callback)27 std::function<void()> on_update_callback)
26{28{
29+ huawei::replace_timer(m_mctp_timer);
27 m_nvme_mi_obj = nvme_mi_obj;30 m_nvme_mi_obj = nvme_mi_obj;
28 m_on_update_callback = on_update_callback;31 m_on_update_callback = on_update_callback;
29- m_mctp_timer = mc::make_shared<mc::timer>(this);
30 m_mctp_timer->timeout.connect([this]() {32 m_mctp_timer->timeout.connect([this]() {
31 if (update_smart_by_mctp() && m_on_update_callback) {33 if (update_smart_by_mctp() && m_on_update_callback) {
32 m_on_update_callback();34 m_on_update_callback();
@@ -38,10 +40,7 @@ void NVMe_Smart::start_mctp_update_task(nvme_mi_over_mctp_ptr nvme_mi_obj, mc::m
38 40 
39void NVMe_Smart::stop_mctp_update_task()41void NVMe_Smart::stop_mctp_update_task()
40{42{
41- if (m_mctp_timer) {43+ huawei::stop_and_release_timer(m_mctp_timer);
42- m_mctp_timer->stop();
43- m_mctp_timer.reset();
44- }
45}44}
46 45 
47bool NVMe_Smart::update_smart_by_mctp()46bool NVMe_Smart::update_smart_by_mctp()
@@ -83,23 +82,24 @@ bool NVMe_Smart::update_smart_by_mctp()
83 size_t copy_len = std::min(smart_data.size() - header_size, sizeof(nvme_smart_log_response_t));82 size_t copy_len = std::min(smart_data.size() - header_size, sizeof(nvme_smart_log_response_t));
84 (void)memcpy_s(&smart_log, sizeof(nvme_smart_log_response_t), smart_data.data() + header_size, copy_len);83 (void)memcpy_s(&smart_log, sizeof(nvme_smart_log_response_t), smart_data.data() + header_size, copy_len);
85 84 
86- uint64_t power_on_hours = 0;85+ uint64_t power_on_hours_low = 0;
86+ uint64_t power_on_hours_high = 0;
87 for (int i = 0; i < 8; ++i) {87 for (int i = 0; i < 8; ++i) {
88- power_on_hours |= (static_cast<uint64_t>(smart_log.power_on_hours_l[i]) << (i * 8));88+ power_on_hours_low |= static_cast<uint64_t>(smart_log.power_on_hours_l[i]) << (i * 8);
89- }89+ power_on_hours_high |= static_cast<uint64_t>(smart_log.power_on_hours_h[i]) << (i * 8);
90- for (int i = 0; i < 8; ++i) {
91- power_on_hours |= (static_cast<uint64_t>(smart_log.power_on_hours_h[i]) << ((i + 8) * 8));
92 }90 }
91+ const double power_on_hours =
92+ std::ldexp(static_cast<double>(power_on_hours_high), 64) + static_cast<double>(power_on_hours_low);
93 93 
94 CriticalWarning.set_value(smart_log.critical_warning);94 CriticalWarning.set_value(smart_log.critical_warning);
95 AvailableSpare.set_value(smart_log.avail_spare);95 AvailableSpare.set_value(smart_log.avail_spare);
96 UsedPercentage.set_value(smart_log.percent_used);96 UsedPercentage.set_value(smart_log.percent_used);
97 TLCSpareBlockPercentage.set_value(smart_log.avail_spare);97 TLCSpareBlockPercentage.set_value(smart_log.avail_spare);
98- PowerOnHours.set_value(static_cast<double>(power_on_hours));98+ PowerOnHours.set_value(power_on_hours);
99 // 记录带外获取到的值到快照,用于 BMA 断开后恢复99 // 记录带外获取到的值到快照,用于 BMA 断开后恢复
100 if (m_nvme_oem) {100 if (m_nvme_oem) {
101 m_nvme_oem->update_prop_without_bma("TLCSpareBlockPercentage", mc::variant(smart_log.avail_spare));101 m_nvme_oem->update_prop_without_bma("TLCSpareBlockPercentage", mc::variant(smart_log.avail_spare));
102- m_nvme_oem->update_prop_without_bma("PowerOnHours", mc::variant(static_cast<double>(power_on_hours)));102+ m_nvme_oem->update_prop_without_bma("PowerOnHours", mc::variant(power_on_hours));
103 }103 }
104 return true;104 return true;
105}105}
@@ -48,6 +48,7 @@ public:
48 }48 }
49 49 
50private:50private:
51+ friend struct HuaweiTimerTestAccess;
51 nvme_mi_over_mctp_ptr m_nvme_mi_obj;52 nvme_mi_over_mctp_ptr m_nvme_mi_obj;
52 mc::shared_ptr<mc::timer> m_mctp_timer;53 mc::shared_ptr<mc::timer> m_mctp_timer;
53 std::function<void()> m_on_update_callback;54 std::function<void()> m_on_update_callback;
@@ -12,30 +12,28 @@
12 12 
13#include "interface/nvme/status.h"13#include "interface/nvme/status.h"
14#include <mc/log.h>14#include <mc/log.h>
15+#include "interface/timer_lifetime.h"
15#include "mc/debounce/continue.h"16#include "mc/debounce/continue.h"
16 17 
17namespace dev {18namespace dev {
18 19 
19void NVMe_Status::start_vpd_update_task(vpd_protocol_ptr vpd, mc::milliseconds interval)20void NVMe_Status::start_vpd_update_task(vpd_protocol_ptr vpd, mc::milliseconds interval)
20{21{
22+ huawei::replace_timer(m_vpd_timer);
21 m_vpd_obj = vpd;23 m_vpd_obj = vpd;
22 m_start_time = std::chrono::steady_clock::now();24 m_start_time = std::chrono::steady_clock::now();
23- m_vpd_timer = mc::make_shared<mc::timer>(this);
24 m_vpd_timer->timeout.connect([this]() {25 m_vpd_timer->timeout.connect([this]() {
25 update_status_by_vpd();26 update_status_by_vpd();
26 });27 });
27 m_vpd_timer->set_single_shot(false);28 m_vpd_timer->set_single_shot(false);
28- m_vpd_timer->start(interval);
29 debounce_failure = std::make_unique<mc::debounce::Continue>(NVME_FAILURE_DEBOUNCE_COUNT);29 debounce_failure = std::make_unique<mc::debounce::Continue>(NVME_FAILURE_DEBOUNCE_COUNT);
30 debounce_pre_failure = std::make_unique<mc::debounce::Continue>(NVME_PRE_FAILURE_DEBOUNCE_COUNT);30 debounce_pre_failure = std::make_unique<mc::debounce::Continue>(NVME_PRE_FAILURE_DEBOUNCE_COUNT);
31+ m_vpd_timer->start(interval);
31}32}
32 33 
33void NVMe_Status::stop_vpd_update_task()34void NVMe_Status::stop_vpd_update_task()
34{35{
35- if (m_vpd_timer) {36+ huawei::stop_and_release_timer(m_vpd_timer);
36- m_vpd_timer->stop();
37- m_vpd_timer.reset();
38- }
39}37}
40 38 
41bool NVMe_Status::update_status_by_vpd()39bool NVMe_Status::update_status_by_vpd()
@@ -40,6 +40,7 @@ public:
40 bool update_status_by_vpd();40 bool update_status_by_vpd();
41 41 
42private:42private:
43+ friend struct HuaweiTimerTestAccess;
43 vpd_protocol_ptr m_vpd_obj;44 vpd_protocol_ptr m_vpd_obj;
44 mc::shared_ptr<mc::timer> m_vpd_timer;45 mc::shared_ptr<mc::timer> m_vpd_timer;
45 std::chrono::steady_clock::time_point m_start_time;46 std::chrono::steady_clock::time_point m_start_time;
@@ -12,14 +12,15 @@
12 12 
13#include "interface/nvme/vendor_specific_smart.h"13#include "interface/nvme/vendor_specific_smart.h"
14#include <mc/log.h>14#include <mc/log.h>
15+#include "interface/timer_lifetime.h"
15 16 
16namespace dev {17namespace dev {
17 18 
18void NVMe_VendorSpecificSmart::start_mctp_update_task(nvme_mi_over_mctp_huawei_ptr nvme_mi_obj,19void NVMe_VendorSpecificSmart::start_mctp_update_task(nvme_mi_over_mctp_huawei_ptr nvme_mi_obj,
19 mc::milliseconds interval)20 mc::milliseconds interval)
20{21{
22+ huawei::replace_timer(m_mctp_timer);
21 m_nvme_mi_obj = nvme_mi_obj;23 m_nvme_mi_obj = nvme_mi_obj;
22- m_mctp_timer = mc::make_shared<mc::timer>(this);
23 m_mctp_timer->timeout.connect([this]() {24 m_mctp_timer->timeout.connect([this]() {
24 update_vendor_specific_smart_by_mctp();25 update_vendor_specific_smart_by_mctp();
25 });26 });
@@ -29,10 +30,7 @@ void NVMe_VendorSpecificSmart::start_mctp_update_task(nvme_mi_over_mctp_huawei_p
29 30 
30void NVMe_VendorSpecificSmart::stop_mctp_update_task()31void NVMe_VendorSpecificSmart::stop_mctp_update_task()
31{32{
32- if (m_mctp_timer) {33+ huawei::stop_and_release_timer(m_mctp_timer);
33- m_mctp_timer->stop();
34- m_mctp_timer.reset();
35- }
36}34}
37 35 
38std::vector<uint8_t> NVMe_VendorSpecificSmart::GetLogPage(uint8_t vendor_specific_lid)36std::vector<uint8_t> NVMe_VendorSpecificSmart::GetLogPage(uint8_t vendor_specific_lid)
@@ -37,6 +37,7 @@ public:
37 std::vector<uint8_t> GetLogPage(uint8_t vendor_specific_lid) override;37 std::vector<uint8_t> GetLogPage(uint8_t vendor_specific_lid) override;
38 38 
39private:39private:
40+ friend struct HuaweiTimerTestAccess;
40 nvme_mi_over_mctp_huawei_ptr m_nvme_mi_obj;41 nvme_mi_over_mctp_huawei_ptr m_nvme_mi_obj;
41 mc::shared_ptr<mc::timer> m_mctp_timer;42 mc::shared_ptr<mc::timer> m_mctp_timer;
42};43};
@@ -0,0 +1,39 @@
1+/*
2+ * Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+ * openUBMC is licensed under Mulan PSL v2.
4+ */
5+ 
6+#ifndef DEV_HUAWEI_NVME_TIMER_LIFETIME_H
7+#define DEV_HUAWEI_NVME_TIMER_LIFETIME_H
8+ 
9+#include <mc/memory.h>
10+#include <mc/runtime/runtime_strand.h>
11+#include <mc/timer.h>
12+ 
13+namespace dev::huawei {
14+ 
15+inline void stop_and_release_timer(mc::shared_ptr<mc::timer>& timer)
16+{
17+ if (!timer) {
18+ return;
19+ }
20+ 
21+ // stop() drains an active callback. The pending cancellation completion
22+ // only holds a weak reference, so the owner can release the timer now.
23+ timer->stop();
24+ timer.reset();
25+}
26+ 
27+inline void replace_timer(mc::shared_ptr<mc::timer>& timer)
28+{
29+ stop_and_release_timer(timer);
30+ 
31+ // The owner already controls the timer's lifetime. A parentless timer
32+ // avoids a late cancellation completion following a stale object parent.
33+ timer = mc::make_shared<mc::timer>();
34+ timer->bind_executor(mc::runtime::runtime_strand{});
O
Oopenubmc-ci-bot26 days ago

⚠️ 警告:huawei replace_timer 绑定脱离的 runtime_strand{},与同批 NVMe/NIC 侧统一绑定 owner->executor() 不一致,无父 timer 回调与设备生命周期不在同一串行边界

samsung/timer_lifetime.h/samsung_nvme_pcie4.cpp、Broadcom network_port/optical_module、Hisi serdes/fibrechannel/hi1825_board 等无父 timer 均统一绑定到 owner->executor()(宿主设备串行 executor)。而本处 replace_timer(华为 NVMe 侧 VPD/MCTP/静态定时器共用)却绑定默认构造的 mc::runtime::runtime_strand{}:

  • 回调捕获嵌入接口 this 并访问 m_nvme_oem、m_nvme_mi_obj 等兄弟/宿主成员;运行在脱离宿主串行器的独立 strand 上,与设备生命周期操作(stop_vpd_update_tasks() / 析构中的同步 stop())不共享同一执行边界。
  • runtime_strand{} 每次 replace_timer 都新建一个独立 strand,定时器重启后回调落到新的执行上下文,与 Samsung 侧恒定 owner->executor() 的行为不一致。

修复建议:与 Samsung 保持一致改为绑定宿主执行器:

timer->bind_executor(owner != nullptr ? owner->executor() : mc::runtime::runtime_strand{});

若 runtime_strand{} 是有意选择(无父 timer 避免取消完成跟随旧对象父树),请在函数注释与调用侧显式说明其不与宿主 executor 共享串行边界,并确认 stop()/析构排空仍与设备生命周期正确串行化。

likedislike
liuxingsen
25 days ago Comments:
35+}
36+ 
37+} // namespace dev::huawei
38+ 
39+#endif // DEV_HUAWEI_NVME_TIMER_LIFETIME_H
@@ -41,6 +41,11 @@ constexpr uint8_t RETRY_TIMES_DEFAULT = 3;
41} // namespace41} // namespace
42 42 
43NVMe_Upgrade::~NVMe_Upgrade()43NVMe_Upgrade::~NVMe_Upgrade()
44+{
45+ wait_for_completion();
46+}
47+ 
48+void NVMe_Upgrade::wait_for_completion()
44{49{
45 if (m_upgrade_thread && m_upgrade_thread->joinable()) {50 if (m_upgrade_thread && m_upgrade_thread->joinable()) {
46 m_upgrade_thread->join();51 m_upgrade_thread->join();
@@ -33,6 +33,9 @@ public:
33 NVMe_Upgrade() = default;33 NVMe_Upgrade() = default;
34 ~NVMe_Upgrade();34 ~NVMe_Upgrade();
35 35 
36+ // Join before the owning device tears down resources used by the worker.
37+ void wait_for_completion();
38+ 
36 bool Upgrade(const std::string& firmware_type, const std::string& protocol, const std::string& file_path) override;39 bool Upgrade(const std::string& firmware_type, const std::string& protocol, const std::string& file_path) override;
37 bool Active(const std::string& firmware_type) override;40 bool Active(const std::string& firmware_type) override;
38 41 
@@ -42,6 +45,7 @@ public:
42 }45 }
43 46 
44private:47private:
48+ friend struct NVMeUpgradeTestAccess;
45 void update_progress(uint8_t percent);49 void update_progress(uint8_t percent);
46 void update_result(bool success, uint32_t code, const std::string& detail);50 void update_result(bool success, uint32_t code, const std::string& detail);
47 void do_download_async(const std::string& file_path);51 void do_download_async(const std::string& file_path);
@@ -12,6 +12,7 @@
12 12 
13#include "interface/cooling.h"13#include "interface/cooling.h"
14#include <mc/log.h>14#include <mc/log.h>
15+#include "interface/timer_lifetime.h"
15 16 
16namespace {17namespace {
17 18 
@@ -41,13 +42,16 @@ Cooling::Cooling(mc::milliseconds no_data_debounce) : m_temp_no_data_debounce(no
41 42 
42void Cooling::start_vpd_update_task(vpd_protocol_ptr vpd, mc::milliseconds interval)43void Cooling::start_vpd_update_task(vpd_protocol_ptr vpd, mc::milliseconds interval)
43{44{
44- m_vpd_obj = vpd;45+ stop_vpd_update_task();
45- m_vpd_timer = mc::make_shared<mc::timer>(this);46+ m_vpd_obj = vpd;
47+ // Queued handlers may retain the timer beyond this embedded interface.
48+ m_vpd_timer = mc::make_shared<mc::timer>();
49+ auto* owner = get_owner();
50+ m_vpd_timer->bind_executor(owner != nullptr ? owner->executor() : executor());
46 m_vpd_timer->timeout.connect([this]() {51 m_vpd_timer->timeout.connect([this]() {
47 update_cooling_by_vpd();52 update_cooling_by_vpd();
48 });53 });
49 m_vpd_timer->set_single_shot(false);54 m_vpd_timer->set_single_shot(false);
50- m_vpd_timer->start(interval);
51 55 
52 // 设备重新插入后再次 start,重置温度防抖状态使新会话从干净状态开始56 // 设备重新插入后再次 start,重置温度防抖状态使新会话从干净状态开始
53 // (0x80 的 5 分钟计时不跨会话累计)57 // (0x80 的 5 分钟计时不跨会话累计)
@@ -58,14 +62,12 @@ void Cooling::start_vpd_update_task(vpd_protocol_ptr vpd, mc::milliseconds inter
58 m_temp_avg_debounce.clear_debounce_val();62 m_temp_avg_debounce.clear_debounce_val();
59 m_temp_fail_debounce = mc::debounce::BinaryContinue(3, 1);63 m_temp_fail_debounce = mc::debounce::BinaryContinue(3, 1);
60 m_has_valid_temp = false;64 m_has_valid_temp = false;
65+ m_vpd_timer->start(interval);
61}66}
62 67 
63void Cooling::stop_vpd_update_task()68void Cooling::stop_vpd_update_task()
64{69{
65- if (m_vpd_timer) {70+ samsung::stop_and_release_timer(m_vpd_timer);
66- m_vpd_timer->stop();
67- m_vpd_timer.reset();
68- }
69}71}
70 72 
71bool Cooling::update_cooling_by_vpd()73bool Cooling::update_cooling_by_vpd()
@@ -38,6 +38,7 @@ public:
38 bool update_cooling_by_vpd();38 bool update_cooling_by_vpd();
39 39 
40private:40private:
41+ friend struct SamsungVpdTimerTestAccess;
41 vpd_protocol_ptr m_vpd_obj;42 vpd_protocol_ptr m_vpd_obj;
42 mc::shared_ptr<mc::timer> m_vpd_timer;43 mc::shared_ptr<mc::timer> m_vpd_timer;
43 44 
@@ -13,6 +13,7 @@
13#include "interface/nvme/basic_management.h"13#include "interface/nvme/basic_management.h"
14#include <chrono>14#include <chrono>
15#include <mc/log.h>15#include <mc/log.h>
16+#include "interface/timer_lifetime.h"
16#include "mc/debounce/binary_continue.h"17#include "mc/debounce/binary_continue.h"
17 18 
18namespace dev {19namespace dev {
@@ -20,17 +21,19 @@ namespace dev {
20void NVMe_BasicManagement::start_vpd_update_task(vpd_protocol_ptr vpd, mc::milliseconds interval,21void NVMe_BasicManagement::start_vpd_update_task(vpd_protocol_ptr vpd, mc::milliseconds interval,
21 std::function<void()> on_update_callback)22 std::function<void()> on_update_callback)
22{23{
24+ stop_vpd_update_task();
23 m_vpd_obj = vpd;25 m_vpd_obj = vpd;
24 m_on_update_callback = on_update_callback;26 m_on_update_callback = on_update_callback;
25 m_start_time = std::chrono::steady_clock::now();27 m_start_time = std::chrono::steady_clock::now();
26- m_vpd_timer = mc::make_shared<mc::timer>(this);28+ m_vpd_timer = mc::make_shared<mc::timer>();
29+ auto* owner = get_owner();
30+ m_vpd_timer->bind_executor(owner != nullptr ? owner->executor() : executor());
27 m_vpd_timer->timeout.connect([this]() {31 m_vpd_timer->timeout.connect([this]() {
28 if (update_basic_management_by_vpd() && m_on_update_callback) {32 if (update_basic_management_by_vpd() && m_on_update_callback) {
29 m_on_update_callback();33 m_on_update_callback();
30 }34 }
31 });35 });
32 m_vpd_timer->set_single_shot(false);36 m_vpd_timer->set_single_shot(false);
33- m_vpd_timer->start(interval);
34 debounce_port0_link_active =37 debounce_port0_link_active =
35 std::make_unique<mc::debounce::BinaryContinue>(NVME_PL_DEBOUNCE_HIGH, NVME_PL_DEBOUNCE_LOW);38 std::make_unique<mc::debounce::BinaryContinue>(NVME_PL_DEBOUNCE_HIGH, NVME_PL_DEBOUNCE_LOW);
36 debounce_drive_functional =39 debounce_drive_functional =
@@ -39,14 +42,12 @@ void NVMe_BasicManagement::start_vpd_update_task(vpd_protocol_ptr vpd, mc::milli
39 std::make_unique<mc::debounce::BinaryContinue>(NVME_DN_DEBOUNCE_HIGH, NVME_DN_DEBOUNCE_LOW);42 std::make_unique<mc::debounce::BinaryContinue>(NVME_DN_DEBOUNCE_HIGH, NVME_DN_DEBOUNCE_LOW);
40 debounce_link_fault =43 debounce_link_fault =
41 std::make_unique<mc::debounce::BinaryContinue>(NVME_LINK_FAULT_DEBOUNCE_HIGH, NVME_LINK_FAULT_DEBOUNCE_LOW);44 std::make_unique<mc::debounce::BinaryContinue>(NVME_LINK_FAULT_DEBOUNCE_HIGH, NVME_LINK_FAULT_DEBOUNCE_LOW);
45+ m_vpd_timer->start(interval);
42}46}
43 47 
44void NVMe_BasicManagement::stop_vpd_update_task()48void NVMe_BasicManagement::stop_vpd_update_task()
45{49{
46- if (m_vpd_timer) {50+ samsung::stop_and_release_timer(m_vpd_timer);
47- m_vpd_timer->stop();
48- m_vpd_timer.reset();
49- }
50}51}
51 52 
52bool NVMe_BasicManagement::update_basic_management_by_vpd()53bool NVMe_BasicManagement::update_basic_management_by_vpd()
@@ -48,6 +48,7 @@ public:
48 bool update_basic_management_by_vpd();48 bool update_basic_management_by_vpd();
49 49 
50private:50private:
51+ friend struct SamsungVpdTimerTestAccess;
51 vpd_protocol_ptr m_vpd_obj;52 vpd_protocol_ptr m_vpd_obj;
52 mc::shared_ptr<mc::timer> m_vpd_timer;53 mc::shared_ptr<mc::timer> m_vpd_timer;
53 std::function<void()> m_on_update_callback;54 std::function<void()> m_on_update_callback;
@@ -20,6 +20,7 @@
20#include <mc/log.h>20#include <mc/log.h>
21#include <mutex>21#include <mutex>
22#include <sstream>22#include <sstream>
23+#include "interface/timer_lifetime.h"
23#include "nvme/nvme_mi_over_mctp.h"24#include "nvme/nvme_mi_over_mctp.h"
24 25 
25namespace dev {26namespace dev {
@@ -770,7 +771,11 @@ void NVMe_LogCollection::start_log_collection_task()
770 return;771 return;
771 }772 }
772 773 
773- m_log_timer = mc::make_shared<mc::timer>(this);774+ // The interface is embedded in the device. A delayed cancellation must
775+ // not retain a raw parent pointer after the device has been destroyed.
776+ m_log_timer = mc::make_shared<mc::timer>();
777+ auto* owner = get_owner();
778+ m_log_timer->bind_executor(owner != nullptr ? owner->executor() : executor());
774 m_log_timer->timeout.connect([this]() {779 m_log_timer->timeout.connect([this]() {
775 collect_all_logs();780 collect_all_logs();
776 });781 });
@@ -785,10 +790,7 @@ void NVMe_LogCollection::start_log_collection_task()
785 790 
786void NVMe_LogCollection::stop_log_collection_task()791void NVMe_LogCollection::stop_log_collection_task()
787{792{
788- if (m_log_timer) {793+ samsung::stop_and_release_timer(m_log_timer);
789- m_log_timer->stop();
790- m_log_timer.reset();
791- }
792}794}
793 795 
794} // namespace dev796} // namespace dev
@@ -28,8 +28,11 @@ constexpr auto LOG_COLLECTION_INTERVAL = std::chrono::hours(24);
28 28 
29class MC_API NVMe_LogCollection : public mc::engine::interface<NVMe_LogCollection, gen::NVMe_LogCollection> {29class MC_API NVMe_LogCollection : public mc::engine::interface<NVMe_LogCollection, gen::NVMe_LogCollection> {
30public:30public:
31- NVMe_LogCollection() = default;31+ NVMe_LogCollection() = default;
32- ~NVMe_LogCollection() = default;32+ ~NVMe_LogCollection()
33+ {
34+ stop_log_collection_task();
35+ }
33 36 
34 bool DumpErrorInfo(const std::string& path) override;37 bool DumpErrorInfo(const std::string& path) override;
35 bool DumpSmartInfo(const std::string& path) override;38 bool DumpSmartInfo(const std::string& path) override;
@@ -52,6 +55,7 @@ public:
52 void collect_all_logs();55 void collect_all_logs();
53 56 
54private:57private:
58+ friend struct SamsungLogTimerTestAccess;
55 nvme_mi_over_mctp_ptr m_nvme_mi_obj;59 nvme_mi_over_mctp_ptr m_nvme_mi_obj;
56 uint32_t m_ctrl_id = 0;60 uint32_t m_ctrl_id = 0;
57 mc::shared_ptr<mc::timer> m_log_timer;61 mc::shared_ptr<mc::timer> m_log_timer;
@@ -12,11 +12,13 @@
12 12 
13#include "interface/nvme/smart.h"13#include "interface/nvme/smart.h"
14#include <algorithm>14#include <algorithm>
15+#include <cmath>
15#include <cstddef>16#include <cstddef>
16#include <cstring>17#include <cstring>
17#include <mc/log.h>18#include <mc/log.h>
18#include <nvme/nvme_mi_over_mctp.h>19#include <nvme/nvme_mi_over_mctp.h>
19#include "interface/nvme/oem.h"20#include "interface/nvme/oem.h"
21+#include "interface/timer_lifetime.h"
20#include "securec.h"22#include "securec.h"
21 23 
22namespace dev {24namespace dev {
@@ -24,9 +26,14 @@ namespace dev {
24void NVMe_Smart::start_mctp_update_task(nvme_mi_over_mctp_ptr nvme_mi_obj, mc::milliseconds interval,26void NVMe_Smart::start_mctp_update_task(nvme_mi_over_mctp_ptr nvme_mi_obj, mc::milliseconds interval,
25 std::function<void()> on_update_callback)27 std::function<void()> on_update_callback)
26{28{
29+ stop_mctp_update_task();
27 m_nvme_mi_obj = nvme_mi_obj;30 m_nvme_mi_obj = nvme_mi_obj;
28 m_on_update_callback = on_update_callback;31 m_on_update_callback = on_update_callback;
29- m_mctp_timer = mc::make_shared<mc::timer>(this);32+ // The interface is embedded in the device. A queued timer handler can
33+ // outlive it, so only the member owns the timer; there is no parent link.
34+ m_mctp_timer = mc::make_shared<mc::timer>();
35+ auto* owner = get_owner();
36+ m_mctp_timer->bind_executor(owner != nullptr ? owner->executor() : executor());
30 m_mctp_timer->timeout.connect([this]() {37 m_mctp_timer->timeout.connect([this]() {
31 if (update_smart_by_mctp() && m_on_update_callback) {38 if (update_smart_by_mctp() && m_on_update_callback) {
32 m_on_update_callback();39 m_on_update_callback();
@@ -38,10 +45,7 @@ void NVMe_Smart::start_mctp_update_task(nvme_mi_over_mctp_ptr nvme_mi_obj, mc::m
38 45 
39void NVMe_Smart::stop_mctp_update_task()46void NVMe_Smart::stop_mctp_update_task()
40{47{
41- if (m_mctp_timer) {48+ samsung::stop_and_release_timer(m_mctp_timer);
42- m_mctp_timer->stop();
43- m_mctp_timer.reset();
44- }
45}49}
46 50 
47bool NVMe_Smart::update_smart_by_mctp()51bool NVMe_Smart::update_smart_by_mctp()
@@ -73,23 +77,26 @@ bool NVMe_Smart::update_smart_by_mctp()
73 size_t copy_len = std::min(smart_data.size() - header_size, sizeof(nvme_smart_log_response_t));77 size_t copy_len = std::min(smart_data.size() - header_size, sizeof(nvme_smart_log_response_t));
74 (void)memcpy_s(&smart_log, sizeof(nvme_smart_log_response_t), smart_data.data() + header_size, copy_len);78 (void)memcpy_s(&smart_log, sizeof(nvme_smart_log_response_t), smart_data.data() + header_size, copy_len);
75 79 
76- uint64_t power_on_hours = 0;80+ uint64_t power_on_hours_low = 0;
81+ uint64_t power_on_hours_high = 0;
77 for (int i = 0; i < 8; ++i) {82 for (int i = 0; i < 8; ++i) {
78- power_on_hours |= (static_cast<uint64_t>(smart_log.power_on_hours_l[i]) << (i * 8));83+ power_on_hours_low |= static_cast<uint64_t>(smart_log.power_on_hours_l[i]) << (i * 8);
79- }84+ power_on_hours_high |= static_cast<uint64_t>(smart_log.power_on_hours_h[i]) << (i * 8);
80- for (int i = 0; i < 8; ++i) {
81- power_on_hours |= (static_cast<uint64_t>(smart_log.power_on_hours_h[i]) << ((i + 8) * 8));
82 }85 }
86+ // The NVMe counter is 128 bits; the exposed property is a double.
87+ // Scale the upper half in floating point rather than shifting a uint64_t by 64 or more.
88+ const double power_on_hours =
89+ std::ldexp(static_cast<double>(power_on_hours_high), 64) + static_cast<double>(power_on_hours_low);
83 90 
84 CriticalWarning.set_value(smart_log.critical_warning);91 CriticalWarning.set_value(smart_log.critical_warning);
85 AvailableSpare.set_value(smart_log.avail_spare);92 AvailableSpare.set_value(smart_log.avail_spare);
86 UsedPercentage.set_value(smart_log.percent_used);93 UsedPercentage.set_value(smart_log.percent_used);
87 TLCSpareBlockPercentage.set_value(smart_log.avail_spare);94 TLCSpareBlockPercentage.set_value(smart_log.avail_spare);
88- PowerOnHours.set_value(static_cast<double>(power_on_hours));95+ PowerOnHours.set_value(power_on_hours);
89 // 记录带外获取到的值到快照,用于 BMA 断开后恢复96 // 记录带外获取到的值到快照,用于 BMA 断开后恢复
90 if (m_nvme_oem) {97 if (m_nvme_oem) {
91 m_nvme_oem->update_prop_without_bma("TLCSpareBlockPercentage", mc::variant(smart_log.avail_spare));98 m_nvme_oem->update_prop_without_bma("TLCSpareBlockPercentage", mc::variant(smart_log.avail_spare));
92- m_nvme_oem->update_prop_without_bma("PowerOnHours", mc::variant(static_cast<double>(power_on_hours)));99+ m_nvme_oem->update_prop_without_bma("PowerOnHours", mc::variant(power_on_hours));
93 }100 }
94 101 
95 return true;102 return true;
@@ -12,30 +12,31 @@
12 12 
13#include "interface/nvme/status.h"13#include "interface/nvme/status.h"
14#include <mc/log.h>14#include <mc/log.h>
15+#include "interface/timer_lifetime.h"
15#include "mc/debounce/continue.h"16#include "mc/debounce/continue.h"
16 17 
17namespace dev {18namespace dev {
18 19 
19void NVMe_Status::start_vpd_update_task(vpd_protocol_ptr vpd, mc::milliseconds interval)20void NVMe_Status::start_vpd_update_task(vpd_protocol_ptr vpd, mc::milliseconds interval)
20{21{
22+ stop_vpd_update_task();
21 m_vpd_obj = vpd;23 m_vpd_obj = vpd;
22 m_start_time = std::chrono::steady_clock::now();24 m_start_time = std::chrono::steady_clock::now();
23- m_vpd_timer = mc::make_shared<mc::timer>(this);25+ m_vpd_timer = mc::make_shared<mc::timer>();
26+ auto* owner = get_owner();
27+ m_vpd_timer->bind_executor(owner != nullptr ? owner->executor() : executor());
24 m_vpd_timer->timeout.connect([this]() {28 m_vpd_timer->timeout.connect([this]() {
25 update_status_by_vpd();29 update_status_by_vpd();
26 });30 });
27 m_vpd_timer->set_single_shot(false);31 m_vpd_timer->set_single_shot(false);
28- m_vpd_timer->start(interval);
29 debounce_failure = std::make_unique<mc::debounce::Continue>(NVME_FAILURE_DEBOUNCE_COUNT);32 debounce_failure = std::make_unique<mc::debounce::Continue>(NVME_FAILURE_DEBOUNCE_COUNT);
30 debounce_pre_failure = std::make_unique<mc::debounce::Continue>(NVME_PRE_FAILURE_DEBOUNCE_COUNT);33 debounce_pre_failure = std::make_unique<mc::debounce::Continue>(NVME_PRE_FAILURE_DEBOUNCE_COUNT);
34+ m_vpd_timer->start(interval);
31}35}
32 36 
33void NVMe_Status::stop_vpd_update_task()37void NVMe_Status::stop_vpd_update_task()
34{38{
35- if (m_vpd_timer) {39+ samsung::stop_and_release_timer(m_vpd_timer);
36- m_vpd_timer->stop();
37- m_vpd_timer.reset();
38- }
39}40}
40 41 
41bool NVMe_Status::update_status_by_vpd()42bool NVMe_Status::update_status_by_vpd()
@@ -40,6 +40,7 @@ public:
40 bool update_status_by_vpd();40 bool update_status_by_vpd();
41 41 
42private:42private:
43+ friend struct SamsungVpdTimerTestAccess;
43 vpd_protocol_ptr m_vpd_obj;44 vpd_protocol_ptr m_vpd_obj;
44 mc::shared_ptr<mc::timer> m_vpd_timer;45 mc::shared_ptr<mc::timer> m_vpd_timer;
45 std::chrono::steady_clock::time_point m_start_time;46 std::chrono::steady_clock::time_point m_start_time;
@@ -0,0 +1,27 @@
1+/*
2+ * Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+ * openUBMC is licensed under Mulan PSL v2.
4+ */
5+ 
6+#ifndef DEV_SAMSUNG_NVME_TIMER_LIFETIME_H
7+#define DEV_SAMSUNG_NVME_TIMER_LIFETIME_H
8+ 
9+#include <mc/timer.h>
10+ 
11+namespace dev::samsung {
12+ 
13+inline void stop_and_release_timer(mc::shared_ptr<mc::timer>& timer)
14+{
15+ if (!timer) {
16+ return;
17+ }
18+ 
19+ // stop() drains an active callback. The pending cancellation completion
20+ // only holds a weak reference, so the owner can release the timer now.
21+ timer->stop();
O

⚠️ 警告:同步释放 timer 依赖一个未在代码内证实的 mc::timer::stop() 契约

本提交把 stop_and_release_timer 从「异步 executor.post 延迟释放」(前一提交 e54828bd 的 Samsung 版,注释明确写着『Keep the timer alive until its serial executor processes the cancellation boundary so a delayed completion cannot destroy it』)改为同步 timer->stop(); timer.reset();,依据是注释里的断言:stop() drains an active callback 且延迟取消回调 only holds a weak reference。

这两处注释对同一 mc::timer::stop() 语义给出了相反的结论。若当前断言不成立——即 stop() 不能同步排空在途回调,或取消完成持有强引用——那么对无父对象、绑定到独立/owner->executor() 的 timer 立即 reset(),会让仍在执行的、捕获了裸 this 并访问成员(如 hi182x_high_optical_module::m_optical_module*、NVMe_LogCollection::collect_all_logs())的回调在宿主对象析构后继续跑,构成释放后访问(UAF),与本 PR 的边界/生命周期修复目标背道而驰。

新增的 stopped_parentless_timer_is_released_synchronously 测试通过阻塞 executor 后同步期望 weak_timer.expired() 成立来验证该契约——这只有在 stop() 不依赖该 executor 时才能通过,恰恰说明整个方案压在该断言上。

修复建议:在注释/文档中给出 mc::timer::stop() 取消完成引用强度的明确依据(引用 libmcpp 实现位置或接口规格);并复核所有将回调绑定到 owner->executor() 的调用点(尤其 NVMe_LogCollection 与 hisi 光模块),确保宿主析构路径在 stop() 返回后没有任何引用 this 的回调仍会触发。若无法保证同步排空,应保留异步延迟释放或先停止所有任务再析构。

likedislike
liuxingsen
25 days ago Comments:
22+ timer.reset();
23+}
24+ 
25+} // namespace dev::samsung
26+ 
27+#endif // DEV_SAMSUNG_NVME_TIMER_LIFETIME_H
@@ -12,6 +12,7 @@
12#include "samsung_nvme_pcie4.h"12#include "samsung_nvme_pcie4.h"
13#include <device_tree/interface/PCIeDevice.h>13#include <device_tree/interface/PCIeDevice.h>
14#include <mc/log.h>14#include <mc/log.h>
15+#include "interface/timer_lifetime.h"
15 16 
16namespace dev {17namespace dev {
17 18 
@@ -32,6 +33,13 @@ samsung_nvme_pcie4::samsung_nvme_pcie4() : m_mctp_object(nullptr), m_ctrl_id(0)
32 33 
33samsung_nvme_pcie4::~samsung_nvme_pcie4()34samsung_nvme_pcie4::~samsung_nvme_pcie4()
34{35{
36+ // VPD callbacks also access sibling interfaces; join them before any
37+ // members (including the cached protocol and static timer) are destroyed.
38+ stop_vpd_update_tasks();
39+ // MCTP callbacks access sibling interfaces, especially m_nvme_oem, which
40+ // is destroyed before the telemetry interfaces by reverse member order.
41+ m_nvme_smart.stop_mctp_update_task();
42+ m_nvme_log_collection.stop_log_collection_task();
35 if (m_nvme_mi_over_mctp_obj) {43 if (m_nvme_mi_over_mctp_obj) {
36 m_nvme_mi_over_mctp_obj.reset();44 m_nvme_mi_over_mctp_obj.reset();
37 }45 }
@@ -390,7 +398,9 @@ void samsung_nvme_pcie4::start_vpd_update_tasks()
390 m_nvme_product_info.update_product_info_by_vpd();398 m_nvme_product_info.update_product_info_by_vpd();
391 m_nvme_multi_record.update_multi_record_by_vpd();399 m_nvme_multi_record.update_multi_record_by_vpd();
392 };400 };
393- m_static_update_timer = mc::make_shared<mc::timer>(this);401+ samsung::stop_and_release_timer(m_static_update_timer);
402+ m_static_update_timer = mc::make_shared<mc::timer>();
403+ m_static_update_timer->bind_executor(executor());
394 m_static_update_timer->timeout.connect(refresh_static_vpd);404 m_static_update_timer->timeout.connect(refresh_static_vpd);
395 m_static_update_timer->set_single_shot(false);405 m_static_update_timer->set_single_shot(false);
396 m_static_update_timer->start(m_vpd_static_interval);406 m_static_update_timer->start(m_vpd_static_interval);
@@ -436,10 +446,7 @@ void samsung_nvme_pcie4::stop_vpd_update_tasks()
436 m_cooling.stop_vpd_update_task();446 m_cooling.stop_vpd_update_task();
437 m_nvme_status.stop_vpd_update_task();447 m_nvme_status.stop_vpd_update_task();
438 m_nvme_basic_management.stop_vpd_update_task();448 m_nvme_basic_management.stop_vpd_update_task();
439- if (m_static_update_timer) {449+ samsung::stop_and_release_timer(m_static_update_timer);
440- m_static_update_timer->stop();
441- m_static_update_timer.reset();
442- }
443 if (m_vpd_obj) {450 if (m_vpd_obj) {
444 m_vpd_obj.reset();451 m_vpd_obj.reset();
445 }452 }
@@ -86,6 +86,7 @@ public:
86 NVMe_Oem m_nvme_oem;86 NVMe_Oem m_nvme_oem;
87 87 
88private:88private:
89+ friend struct SamsungVpdTimerTestAccess;
89 // VPD 协议类型(与1.0保持一致)90 // VPD 协议类型(与1.0保持一致)
90 // NVME_MI = 0, SFF = 1, SAMSUNG_SFF = 291 // NVME_MI = 0, SFF = 1, SAMSUNG_SFF = 2
91 enum class VpdProtocolType {92 enum class VpdProtocolType {
@@ -253,6 +253,10 @@ bool ncsi_over_mctp_grt::get_mac_addr(uint8_t package_id, uint8_t channel_id, ui
253 return false;253 return false;
254 }254 }
255 255 
256+ if (len > sizeof(rsp->mac_addr)) {
257+ ilog("requested MAC length exceeds response field size");
258+ return false;
259+ }
256 (void)memcpy_s(mac_addr, len, rsp->mac_addr, len);260 (void)memcpy_s(mac_addr, len, rsp->mac_addr, len);
257 return true;261 return true;
258}262}
@@ -279,6 +283,10 @@ bool ncsi_over_mctp_grt::get_default_mac_addr(uint8_t package_id, uint8_t channe
279 return false;283 return false;
280 }284 }
281 285 
286+ if (len > sizeof(rsp->mac_addr)) {
287+ ilog("requested MAC length exceeds response field size");
288+ return false;
289+ }
282 (void)memcpy_s(mac_addr, len, rsp->mac_addr, len);290 (void)memcpy_s(mac_addr, len, rsp->mac_addr, len);
283 return true;291 return true;
284}292}
@@ -358,6 +358,10 @@ bool ncsi_over_mctp_huawei::get_mac_addr(uint8_t package_id, uint8_t channel_id,
358 dlog("get mac addr count is 0");358 dlog("get mac addr count is 0");
359 }359 }
360 // 存在多个mac地址,取第一个360 // 存在多个mac地址,取第一个
361+ if (len > sizeof(rsp->mac_addr[0])) {
362+ ilog("requested MAC length exceeds response field size");
363+ return false;
364+ }
361 (void)memcpy_s(mac_addr, len, rsp->mac_addr[0], len);365 (void)memcpy_s(mac_addr, len, rsp->mac_addr[0], len);
362 return true;366 return true;
363}367}
@@ -387,6 +391,10 @@ bool ncsi_over_mctp_huawei::get_default_mac_addr(uint8_t package_id, uint8_t cha
387 dlog("get default mac addr count is 0");391 dlog("get default mac addr count is 0");
388 }392 }
389 // 存在多个mac地址,取第一个393 // 存在多个mac地址,取第一个
394+ if (len > sizeof(rsp->mac_addr[0])) {
395+ ilog("requested MAC length exceeds response field size");
396+ return false;
397+ }
390 (void)memcpy_s(mac_addr, len, rsp->mac_addr[0], len);398 (void)memcpy_s(mac_addr, len, rsp->mac_addr[0], len);
391 return true;399 return true;
392}400}
@@ -679,6 +687,11 @@ bool ncsi_over_mctp_huawei::get_optical_module_info(uint8_t package_id, uint8_t
679 if (!ret) {687 if (!ret) {
680 return false;688 return false;
681 }689 }
690+ if (rsp_str.size() < sizeof(oem_huawei_public_rsp_t)) {
691+ ilog("get optical module info rsp size ${rsp_size} is shorter than the public header",
692+ ("rsp_size", rsp_str.size()));
693+ return false;
694+ }
682 // 如果reason_code为0x8008,则表示光模块不存在,直接返回false695 // 如果reason_code为0x8008,则表示光模块不存在,直接返回false
683 rsp = reinterpret_cast<oem_huawei_optical_module_info_rsp_t*>(rsp_str.data());696 rsp = reinterpret_cast<oem_huawei_optical_module_info_rsp_t*>(rsp_str.data());
684 const uint16_t rsp_reason_code = SHORT_BY_BIG_ENDIAN(rsp->public_rsp_header.reason_code);697 const uint16_t rsp_reason_code = SHORT_BY_BIG_ENDIAN(rsp->public_rsp_header.reason_code);
@@ -976,8 +989,15 @@ bool ncsi_over_mctp_huawei::get_log_by_ncsi_internal(uint8_t package_id, uint32_
976 std::string response;989 std::string response;
977 bool ret = send_get_log_command(package_id, sub_cmd, log_type, log_request, response);990 bool ret = send_get_log_command(package_id, sub_cmd, log_type, log_request, response);
978 991 
979- // 检查发送结果和响应数据992+ constexpr size_t kDataOffset = sizeof(oem_huawei_public_rsp_t) + 7;
980- if (!ret || !check_respense_data(reinterpret_cast<oem_huawei_public_rsp_t*>(response.data()), sub_cmd)) {993+ const bool valid_size =
994+ response.size() >= kDataOffset && response.size() <= kDataOffset + OEM_HUAWEI_LOG_FRAME_SIZE;
995+ if (ret && !valid_size) {
996+ ilog("get log by ncsi failed, response size ${size} is invalid", ("size", response.size()));
997+ }
998+ // 检查发送结果和响应数据;新命令失败时保留旧命令回退行为
999+ if (!ret || !valid_size ||
1000+ !check_respense_data(reinterpret_cast<oem_huawei_public_rsp_t*>(response.data()), sub_cmd)) {
O

⚠️ 警告:新增的 valid_size 校验未能阻止对过短响应解析头部

当 response.size() < kDataOffset 时 valid_size 为 false,会进入旧命令回退分支,但代码并未因 !valid_size 而跳过 check_respense_data:它仍把 response.data() 按 oem_huawei_public_rsp_t 转换并读取 response_code/reason_code/manufacturer_id/sub_command_id(读取约前 4 字节)。当响应真正很短时,这会越过其逻辑长度读取内存(SSO 内或堆上越界),并依据垃圾头部字节判断请求成败——与本 PR「读取前校验响应长度」的边界修复目标直接矛盾。

修复建议:先以 !ret || !valid_size 短路进入回退,仅在长度校验通过后再调用 check_respense_data:

if (!ret || !valid_size) { 回退/continue; }
if (!check_respense_data(...)) { 回退/continue; }

并将长度下限收紧到 response.size() >= sizeof(oem_huawei_public_rsp_t) 而非直接用 kDataOffset。

likedislike
981 if (sub_cmd == OEM_HUAWEI_GET_LOG_OLD) {1001 if (sub_cmd == OEM_HUAWEI_GET_LOG_OLD) {
982 // 旧命令也失败,返回错误1002 // 旧命令也失败,返回错误
983 ilog("get log by ncsi failed");1003 ilog("get log by ncsi failed");
@@ -991,18 +1011,13 @@ bool ncsi_over_mctp_huawei::get_log_by_ncsi_internal(uint8_t package_id, uint32_
991 continue;1011 continue;
992 }1012 }
993 1013 
994- // 日志返回的数据大小不一致,只需要包含响应头即可1014+ const size_t actual_data_size = response.size() - kDataOffset;
995- if (response.size() >= sizeof(oem_huawei_public_rsp_t)) {1015+ const size_t max_log_size = static_cast<size_t>(total_frame) * OEM_HUAWEI_LOG_FRAME_SIZE;
996- oem_huawei_get_log_rsp_t* log_rsp = nullptr;1016+ if (log_data.size() > max_log_size || actual_data_size > max_log_size - log_data.size()) {
997- log_rsp = reinterpret_cast<oem_huawei_get_log_rsp_t*>(response.data());1017+ ilog("get log by ncsi failed, log data exceeds the requested frame count");
998- size_t actual_data_size = response.size() - sizeof(oem_huawei_public_rsp_t) - 7; // 减去7字节保留字段
999- if (actual_data_size > 0) {
1000- log_data.append(reinterpret_cast<const char*>(log_rsp->data), actual_data_size);
1001- }
1002- } else {
1003- ilog("get log by ncsi failed, response size ${size} is invalid", ("size", response.size()));
1004 return false;1018 return false;
1005 }1019 }
1020+ log_data.append(response.data() + kDataOffset, actual_data_size);
1006 1021 
1007 frame_index++;1022 frame_index++;
1008 }1023 }
@@ -123,6 +123,10 @@ bool ncsi_over_mctp_mellanox::get_mac_addr(uint8_t package_id, uint8_t channel_i
123 return false;123 return false;
124 }124 }
125 125 
126+ if (len > sizeof(rsp->mac_addr)) {
127+ ilog("requested MAC length exceeds response field size");
128+ return false;
129+ }
126 (void)memcpy_s(mac_addr, len, rsp->mac_addr, len);130 (void)memcpy_s(mac_addr, len, rsp->mac_addr, len);
127 return true;131 return true;
128}132}
@@ -156,6 +160,10 @@ bool ncsi_over_mctp_mellanox::get_default_mac_addr(uint8_t package_id, uint8_t c
156 return false;160 return false;
157 }161 }
158 162 
163+ if (len > sizeof(rsp->mac_addr)) {
164+ ilog("requested MAC length exceeds response field size");
165+ return false;
166+ }
159 (void)memcpy_s(mac_addr, len, rsp->mac_addr, len);167 (void)memcpy_s(mac_addr, len, rsp->mac_addr, len);
160 return true;168 return true;
161}169}
@@ -80,6 +80,10 @@ bool ncsi_over_mctp_mucse::get_mac_addr(uint8_t package_id, uint8_t channel_id,
80 if (!check_response_data(reinterpret_cast<oem_mucse_public_rsp_t*>(rsp), OEM_MUCSE_GET_MAC_ADDR)) {80 if (!check_response_data(reinterpret_cast<oem_mucse_public_rsp_t*>(rsp), OEM_MUCSE_GET_MAC_ADDR)) {
81 return false;81 return false;
82 }82 }
83+ if (len > sizeof(rsp->mac_addr)) {
84+ ilog("requested MAC length exceeds response field size");
85+ return false;
86+ }
83 errno_t err = memcpy_s(mac_addr, len, rsp->mac_addr, len);87 errno_t err = memcpy_s(mac_addr, len, rsp->mac_addr, len);
84 if (err != EOK) {88 if (err != EOK) {
85 ilog("memcpy_s failed, err: ${err}", ("err", err));89 ilog("memcpy_s failed, err: ${err}", ("err", err));
@@ -226,6 +226,10 @@ bool ncsi_over_mctp_standard::disable_hardware_arbittration(uint8_t package_id,
226 ilog("unable to select package id, package_id: ${package_id}", ("package_id", package_id));226 ilog("unable to select package id, package_id: ${package_id}", ("package_id", package_id));
227 return false;227 return false;
228 }228 }
229+ if (rsp_str.size() < SELECT_PACKAGE_RSP_LEN) {
230+ ilog("select package response size ${size} is invalid", ("size", rsp_str.size()));
231+ return false;
232+ }
229 233 
230 rsp = reinterpret_cast<select_package_rsp*>(rsp_str.data());234 rsp = reinterpret_cast<select_package_rsp*>(rsp_str.data());
231 uint16_t response_code = SHORT_BY_BIG_ENDIAN(rsp->rsp_code);235 uint16_t response_code = SHORT_BY_BIG_ENDIAN(rsp->rsp_code);
@@ -11,6 +11,7 @@
11 */11 */
12 12 
13#include "ncsi_over_mctp_wangxun.h"13#include "ncsi_over_mctp_wangxun.h"
14+#include <cstddef>
14#include <iomanip>15#include <iomanip>
15#include <ncsi/ncsi_protocol.h>16#include <ncsi/ncsi_protocol.h>
16#include <ncsi/ncsi_wangxun.h>17#include <ncsi/ncsi_wangxun.h>
@@ -116,6 +117,10 @@ bool ncsi_over_mctp_wangxun::get_mac_address(uint8_t package_id, uint8_t channel
116 if (!check_response_data(ctx, reinterpret_cast<oem_wx_common_rsp_t*>(rsp), WX_CMD_GET_MAC_ADDRESS)) {117 if (!check_response_data(ctx, reinterpret_cast<oem_wx_common_rsp_t*>(rsp), WX_CMD_GET_MAC_ADDRESS)) {
117 return false;118 return false;
118 }119 }
120+ if (len > sizeof(rsp->mac_addr)) {
121+ ilog("Error: requested MAC length exceeds response field size");
122+ return false;
123+ }
119 (void)memcpy_s(mac_addr, len, rsp->mac_addr, len);124 (void)memcpy_s(mac_addr, len, rsp->mac_addr, len);
120 125 
121 return true;126 return true;
@@ -289,8 +294,8 @@ bool ncsi_over_mctp_wangxun::get_sff_info(uint8_t package_id, uint8_t channel_id
289 ilog("Error: failed to send NCSI get_sff_info command");294 ilog("Error: failed to send NCSI get_sff_info command");
290 return false;295 return false;
291 }296 }
292- if (rsp_str.empty()) {297+ if (rsp_str.size() < sizeof(oem_wx_get_sff_info_rsp_t)) {
293- ilog("Error: cannot get sff info");298+ ilog("Error: invalid sff info response size ${size}", ("size", rsp_str.size()));
294 return false;299 return false;
295 }300 }
296 301 
@@ -315,8 +320,9 @@ bool ncsi_over_mctp_wangxun::get_vpd_sn(uint8_t package_id, uint8_t channel_id,
315 ilog("Error: failed to send NCSI get_vpd_sn command");320 ilog("Error: failed to send NCSI get_vpd_sn command");
316 return false;321 return false;
317 }322 }
318- if (rsp_str.empty()) {323+ constexpr size_t data_offset = offsetof(oem_wx_get_vpd_sn_rsp_t, sn);
319- ilog("Error: cannot get vpd sn");324+ if (rsp_str.size() < data_offset) {
325+ ilog("Error: invalid vpd sn response size ${size}", ("size", rsp_str.size()));
320 return false;326 return false;
321 }327 }
322 328 
@@ -324,6 +330,10 @@ bool ncsi_over_mctp_wangxun::get_vpd_sn(uint8_t package_id, uint8_t channel_id,
324 if (!check_response_data(ctx, reinterpret_cast<oem_wx_common_rsp_t*>(rsp), WX_CMD_GET_VPD_SN)) {330 if (!check_response_data(ctx, reinterpret_cast<oem_wx_common_rsp_t*>(rsp), WX_CMD_GET_VPD_SN)) {
325 return false;331 return false;
326 }332 }
333+ if (rsp->sn_len > sizeof(rsp->sn) || rsp->sn_len > rsp_str.size() - data_offset) {
334+ ilog("Error: invalid vpd sn field length ${size}", ("size", rsp->sn_len));
335+ return false;
336+ }
327 337 
328 std::stringstream ss;338 std::stringstream ss;
329 for (size_t i = 0; i < rsp->sn_len; i++) {339 for (size_t i = 0; i < rsp->sn_len; i++) {
@@ -347,8 +357,9 @@ bool ncsi_over_mctp_wangxun::get_vpd_pn(uint8_t package_id, uint8_t channel_id,
347 ilog("Error: failed to send NCSI get_vpd_pn command");357 ilog("Error: failed to send NCSI get_vpd_pn command");
348 return false;358 return false;
349 }359 }
350- if (rsp_str.empty()) {360+ constexpr size_t data_offset = offsetof(oem_wx_get_vpd_pn_rsp_t, pn);
351- ilog("Error: cannot get vpd pn");361+ if (rsp_str.size() < data_offset) {
362+ ilog("Error: invalid vpd pn response size ${size}", ("size", rsp_str.size()));
352 return false;363 return false;
353 }364 }
354 365 
@@ -356,6 +367,10 @@ bool ncsi_over_mctp_wangxun::get_vpd_pn(uint8_t package_id, uint8_t channel_id,
356 if (!check_response_data(ctx, reinterpret_cast<oem_wx_common_rsp_t*>(rsp), WX_CMD_GET_VPD_PN)) {367 if (!check_response_data(ctx, reinterpret_cast<oem_wx_common_rsp_t*>(rsp), WX_CMD_GET_VPD_PN)) {
357 return false;368 return false;
358 }369 }
370+ if (rsp->pn_len > sizeof(rsp->pn) || rsp->pn_len > rsp_str.size() - data_offset) {
371+ ilog("Error: invalid vpd pn field length ${size}", ("size", rsp->pn_len));
372+ return false;
373+ }
359 374 
360 std::stringstream ss;375 std::stringstream ss;
361 for (size_t i = 0; i < rsp->pn_len; i++) {376 for (size_t i = 0; i < rsp->pn_len; i++) {
@@ -0,0 +1,294 @@
1+#!/usr/bin/env python3
2+"""Run every configured Meson/GoogleTest executable serially for many rounds.
3+ 
4+The build is intentionally external: build first, then run this script against
5+that exact build directory. Each invocation creates a new results directory.
6+"""
7+ 
8+import argparse
9+from collections import Counter
10+from datetime import datetime, timezone
11+import json
12+import os
13+from pathlib import Path
14+import re
15+import shlex
16+import signal
17+import subprocess
18+import sys
19+ 
20+ 
21+ROOT = Path(__file__).resolve().parents[1]
22+ANSI = re.compile(r"\x1b\[[0-9;]*m")
23+GTEST_CASE = re.compile(r"\[\s*(RUN|OK|FAILED|SKIPPED)\s*\]\s+([^\s\[]+\.[^\s\[]+)")
24+ 
25+ 
26+def wrapper_exec(command):
27+ """Put the matching libmcpp runtime ahead of Meson's per-test LD path."""
28+ if not command:
29+ raise ValueError("Meson wrapper received no test command")
30+ prebuilt = [
31+ ROOT / ".libmcpp_prebuilt" / relative
32+ for relative in (
33+ "src", "libraries/mcbase/src", "libraries/mcengine/src",
34+ "libraries/mcexpr/src", "libraries/mcdbus/src", "libraries/mcapp/src",
35+ )
36+ ]
37+ lua = ROOT / "temp/usr/lib64"
38+ lua_support = ROOT / ".lua_runtime_libs"
39+ test_name = Path(command[0]).name
40+ lua_test = test_name.startswith(("test_lua_plugin", "test_lua_seri", "test_chip_base"))
41+ front = ([lua] if lua_test else []) + prebuilt + [lua_support]
42+ back = ([] if lua_test else [lua]) + [Path(p) for p in os.environ.get("LD_LIBRARY_PATH", "").split(":") if p]
43+ paths = []
44+ for path in front + back:
45+ value = str(path)
46+ if path.is_dir() and value not in paths:
47+ paths.append(value)
48+ os.environ["LD_LIBRARY_PATH"] = ":".join(paths)
49+ os.execvpe(command[0], command, os.environ)
50+ 
51+ 
52+def pretty_name(test):
53+ suites = test.get("suite", [])
54+ project = suites[0].split(":", 1)[0] if suites else "component_drivers"
55+ labels = [s.split(":", 1)[1] for s in suites if ":" in s and s.split(":", 1)[1]]
56+ name = f"{project}:{test['name']}"
57+ return f"{'+'.join(labels)} - {name}" if labels else name
58+ 
59+ 
60+def parse_cases(output):
61+ started = Counter()
62+ ended = Counter()
63+ counts = Counter()
64+ for raw in output.splitlines():
65+ # Runtime workers and GoogleTest share the captured output stream. A
66+ # worker log may be concatenated before a marker without a newline, or
67+ # multiple markers may land on the same physical line.
68+ for match in GTEST_CASE.finditer(ANSI.sub("", raw)):
69+ outcome, parsed_name = match.groups()
70+ # Google's final failure list repeats case names without '(N ms)'.
71+ # Count only results for cases that have a corresponding RUN line.
72+ if outcome == "RUN":
73+ started[parsed_name] += 1
74+ continue
75+ 
76+ name = parsed_name
77+ if started[name] <= ended[name]:
78+ # Runtime log messages can race with GoogleTest's stdout and be
79+ # appended directly after the case name. Recover the outstanding
80+ # case by prefix instead of treating a successful case as missing.
81+ pending = [
82+ candidate
83+ for candidate in started
84+ if started[candidate] > ended[candidate] and parsed_name.startswith(candidate)
85+ ]
86+ if not pending:
87+ continue
88+ name = max(pending, key=len)
89+ ended[name] += 1
90+ counts[outcome.lower()] += 1
91+ counts["run"] = sum(started.values())
92+ counts["incomplete"] = sum((started - ended).values())
93+ return dict(counts)
94+ 
95+ 
96+def read_meson_log(path):
97+ entries = []
98+ with path.open(encoding="utf-8", errors="replace") as stream:
99+ for line_number, line in enumerate(stream, 1):
100+ if line.strip():
101+ try:
102+ entries.append(json.loads(line))
103+ except json.JSONDecodeError as error:
104+ raise ValueError(f"{path}:{line_number}: {error}") from error
105+ return entries
106+ 
107+ 
108+def gtest_count_error(actual, expected):
109+ if expected is None or actual == expected:
110+ return None
111+ return f"GoogleTest case count changed from {expected} to {actual}"
112+ 
113+ 
114+def run_round(build_dir, result_dir, index, expected, timeout_seconds):
115+ base = f"memory_audit_{result_dir.name}_round_{index:03d}"
116+ console = result_dir / f"round_{index:03d}.console.log"
117+ wrapper = " ".join(shlex.quote(part) for part in (sys.executable, str(Path(__file__).resolve()), "--wrapper-exec"))
118+ command = [
119+ "meson", "test", "-C", str(build_dir), "--no-rebuild",
120+ "--num-processes", "1", "--logbase", base, "--wrapper", wrapper,
121+ ]
122+ timed_out = False
123+ interrupted = False
124+ with console.open("wb") as stream:
125+ process = subprocess.Popen(command, stdout=stream, stderr=subprocess.STDOUT, start_new_session=True)
126+ try:
127+ returncode = process.wait(timeout=timeout_seconds)
128+ except (subprocess.TimeoutExpired, KeyboardInterrupt) as error:
129+ timed_out = isinstance(error, subprocess.TimeoutExpired)
130+ interrupted = isinstance(error, KeyboardInterrupt)
131+ try:
132+ os.killpg(process.pid, signal.SIGTERM)
133+ except ProcessLookupError:
134+ pass
135+ try:
136+ process.wait(timeout=5)
137+ except subprocess.TimeoutExpired:
138+ try:
139+ os.killpg(process.pid, signal.SIGKILL)
140+ except ProcessLookupError:
141+ pass
142+ process.wait()
143+ returncode = 124
144+ 
145+ generated = {}
146+ for extension in ("json", "txt", "xml"):
147+ matches = list((build_dir / "meson-logs").glob(f"{base}*.{extension}"))
148+ if len(matches) == 1:
149+ destination = result_dir / f"round_{index:03d}.meson.{extension}"
150+ destination.write_bytes(matches[0].read_bytes())
151+ generated[extension] = destination
152+ 
153+ errors = []
154+ if "json" not in generated:
155+ errors.append("Meson JSON test log missing or ambiguous")
156+ entries = []
157+ else:
158+ try:
159+ entries = read_meson_log(generated["json"])
160+ except ValueError as error:
161+ errors.append(str(error))
162+ entries = []
163+ 
164+ actual = Counter(entry.get("name", "") for entry in entries)
165+ missing = expected - actual
166+ unexpected = actual - expected
167+ if missing:
168+ errors.append(f"{sum(missing.values())} configured test executables did not report a result")
169+ if unexpected:
170+ errors.append(f"{sum(unexpected.values())} unexpected or duplicate test results")
171+ if timed_out:
172+ errors.append(f"whole round exceeded {timeout_seconds} seconds")
173+ if interrupted:
174+ errors.append("run interrupted")
175+ if returncode:
176+ errors.append(f"meson test exited with {returncode}")
177+ 
178+ meson_results = Counter(entry.get("result", "MISSING") for entry in entries)
179+ gtest = Counter()
180+ suite_errors = []
181+ no_cases = []
182+ incomplete = []
183+ for entry in entries:
184+ name = entry.get("name", "<unnamed>")
185+ result = entry.get("result", "MISSING")
186+ cases = parse_cases(entry.get("stdout", "") + "\n" + entry.get("stderr", ""))
187+ gtest.update(cases)
188+ if result != "OK":
189+ suite_errors.append({"name": name, "result": result, "returncode": entry.get("returncode")})
190+ if result == "OK" and cases.get("run", 0) == 0:
191+ no_cases.append(name)
O

💡 建议:已知全跳过用例会让每轮固定判 FAIL,削弱压测的「通过/失败」门禁

PR 描述里已声明「每轮全量测试包含 4 个既有 ScannerTest 跳过用例,报告单独计数」,但脚本并没有把这些用例单独计数:parse_cases 不统计 gtest 的 SKIPPED 用例([ SKIPPED ] 无 [ RUN ] 对应),因此这类 OK 但 0 个 RUN 用例的 suite(第 169 行的判定)会进入 no_cases,进而使 errors 非空、passed=False。

结果是:即便压测 100 轮全绿,只要这 4 个已知跳过用例存在,failures == 0 就永远无法成立,工具无法作为「通过/失败」门禁。

修复建议:把已知「永远跳过」的 suite(如 ScannerTest 的 4 个用例)白名单化,或在 no_cases/missing 判定时对它们单独计数(如 PR 描述所承诺的「报告单独计数」),不写入 errors。

likedislike
liuxingsen
25 days ago Comments:
192+ if cases.get("incomplete", 0):
193+ incomplete.append(name)
194+ if suite_errors:
195+ errors.append(f"{len(suite_errors)} Meson suites failed, skipped, or timed out")
196+ if no_cases:
197+ errors.append(f"{len(no_cases)} successful Meson suites ran no GoogleTest cases")
198+ if incomplete:
199+ errors.append(f"{len(incomplete)} suites left GoogleTest cases without a result")
200+ if gtest["failed"]:
201+ errors.append(f"{gtest['failed']} GoogleTest cases failed")
202+ if gtest["skipped"]:
203+ errors.append(f"{gtest['skipped']} GoogleTest cases skipped")
204+ 
205+ return {
206+ "round": index, "passed": not errors, "interrupted": interrupted,
207+ "meson_exit_code": returncode,
208+ "executables_expected": sum(expected.values()),
209+ "executables_reported": len(entries),
210+ "meson_results": dict(meson_results),
211+ "gtest_cases": {key: gtest[key] for key in ("run", "ok", "failed", "skipped", "incomplete")},
212+ "suite_errors": suite_errors, "suites_without_cases": no_cases,
213+ "suites_with_incomplete_cases": incomplete,
214+ "missing_suites": list(missing.elements()),
215+ "unexpected_suites": list(unexpected.elements()),
216+ "errors": errors,
217+ "logs": {"console": str(console), **{key: str(path) for key, path in generated.items()}},
218+ }
219+ 
220+ 
221+def write_summary(path, summary):
222+ path.write_text(json.dumps(summary, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
223+ 
224+ 
225+def interrupt_on_term(_signum, _frame):
226+ raise KeyboardInterrupt
227+ 
228+ 
229+def main():
230+ parser = argparse.ArgumentParser(description=__doc__)
231+ parser.add_argument("--build-dir", type=Path, default=ROOT / "builddir_memory_audit")
232+ parser.add_argument("--rounds", type=int, default=20)
233+ parser.add_argument("--round-timeout", type=int, default=7200, metavar="SECONDS")
234+ parser.add_argument("--log-root", type=Path, default=ROOT / "test_failure_logs/memory-audit-20260927")
235+ args = parser.parse_args()
236+ signal.signal(signal.SIGTERM, interrupt_on_term)
237+ if args.rounds < 1 or args.round_timeout < 1:
238+ parser.error("--rounds and --round-timeout must be positive")
239+ build_dir = args.build_dir.resolve()
240+ intro = build_dir / "meson-info/intro-tests.json"
241+ if not intro.is_file():
242+ parser.error(f"configured Meson tests not found: {intro}")
243+ tests = json.loads(intro.read_text(encoding="utf-8"))
244+ if not tests:
245+ parser.error(f"no tests registered in {build_dir}")
246+ missing_executables = [str(command) for test in tests for command in test["cmd"][:1]
247+ if Path(command).is_absolute() and not Path(command).is_file()]
248+ if missing_executables:
249+ parser.error(f"build is incomplete: {len(missing_executables)} test executables missing; first: {missing_executables[0]}")
250+ 
251+ result_dir = args.log_root.resolve() / datetime.now(timezone.utc).strftime("run-%Y%m%dT%H%M%SZ")
252+ result_dir = result_dir.with_name(f"{result_dir.name}-{os.getpid()}")
253+ result_dir.mkdir(parents=True, exist_ok=False)
254+ expected = Counter(pretty_name(test) for test in tests)
O

💡 建议:expected 把全部已配置 suite 都计入,Meson 层禁用/skip 的 suite 会被误报为缺失

expected = Counter(pretty_name(test) for test in tests) 包含 intro-tests.json 里所有已配置测试。若某个 suite 在 Meson 层被禁用/跳过(meson test 以 SKIP 记录,可能不生成 JSON 结果条目),它会持续落入第 144 行的 missing = expected - actual,每轮都报「configured test executables did not report a result」并判 FAIL。

修复建议:expected 应只统计「本应运行」的 suite——根据 intro-tests.json 中的 skip/disabled 元数据过滤,而非简单取全部;并把被过滤的 suite 记入单独的 skipped_suites 字段(与 PR 描述「报告单独计数」一致的思路),而不是计入 missing/errors。

likedislike
liuxingsen
25 days ago Comments:
255+ summary = {
256+ "build_dir": str(build_dir), "result_dir": str(result_dir),
257+ "requested_rounds": args.rounds, "configured_executables": len(tests),
258+ "command": ["meson", "test", "-C", str(build_dir), "--no-rebuild", "--num-processes", "1"],
259+ "rounds": [],
260+ }
261+ print(f"Running {len(tests)} Meson test executables serially for {args.rounds} rounds; logs: {result_dir}", flush=True)
262+ expected_gtest_cases = None
263+ for index in range(1, args.rounds + 1):
264+ outcome = run_round(build_dir, result_dir, index, expected, args.round_timeout)
265+ cases = outcome["gtest_cases"]
266+ count_error = gtest_count_error(cases["run"], expected_gtest_cases)
O

⚠️ 警告:以首轮 run 计数作为硬基线 + 首败即断,会把解析抖动放大为整场压测门禁失败

新增的 gtest_count_error(本行)把第 1 个 passing 轮的 gtest_cases.run 次数固化为基线,后续每轮的 run 必须逐次相等,否则判 FAIL;而 main 尾部的 if not outcome["passed"]: break 会遇首个 FAIL 立即终止全部后续轮次。

问题在于:parse_cases 的 run 计数来自对交错日志里 [ RUN ] 标记的解析,而这正是本提交引入 finditer 想容忍的“worker 日志与 gtest stdout 交错”带来的固有抖动——某轮少解析到一个 [ RUN ](marker 被拦腰截断或与日志粘连成一字排开)都会让该轮的 run 计数比基线少一,于是该轮及之后所有轮全被判为“case count changed”、直接 break。这与 PR 描述里“将失败轮次或单次重跑当作闭环结果”“需 50 轮完整证据定位 SIGSEGV”的诉求相矛盾:一个解析抖动就提前掐断全量证据收集。

修复建议:基线不应取第一轮单点值。可改为:取前 N 轮 run 计数的中位数/众数作为期望,且只在明显偏离(如差 > 若干)时才报错;或将 run 计数差异降级为可观测指标(记录在 summary,不判 FAIL 不断流);同时在 break 前保留已跑轮次证据,避免单一抖动截断后续证据。

likedislike
liuxingsen
25 days ago Comments:
267+ if count_error:
268+ outcome["errors"].append(count_error)
269+ outcome["passed"] = False
270+ elif outcome["passed"] and expected_gtest_cases is None:
271+ expected_gtest_cases = cases["run"]
272+ summary["gtest_cases_per_round"] = expected_gtest_cases
273+ summary["rounds"].append(outcome)
274+ write_summary(result_dir / "summary.json", summary)
275+ print(f"Round {index}/{args.rounds}: {'PASS' if outcome['passed'] else 'FAIL'}; "
276+ f"suites {outcome['executables_reported']}/{len(tests)}, "
277+ f"gtest passed={cases['ok']} failed={cases['failed']} skipped={cases['skipped']} "
278+ f"incomplete={cases['incomplete']}; "
279+ f"Meson results={outcome['meson_results']}", flush=True)
280+ if not outcome["passed"]:
281+ break
282+ failures = sum(not item["passed"] for item in summary["rounds"])
283+ summary["passed"] = failures == 0 and len(summary["rounds"]) == args.rounds
284+ summary["failed_rounds"] = failures
285+ write_summary(result_dir / "summary.json", summary)
286+ print(f"Completed {len(summary['rounds'])}/{args.rounds} rounds; failures={failures}; {result_dir / 'summary.json'}", flush=True)
287+ return 0 if summary["passed"] else 1
288+ 
289+ 
290+if __name__ == "__main__":
291+ if len(sys.argv) > 1 and sys.argv[1] == "--wrapper-exec":
292+ wrapper_exec(sys.argv[2:])
293+ else:
294+ sys.exit(main())
@@ -0,0 +1,5 @@
1+# The bundled libmcpp deallocation callback downcasts shared_base after
2+# the derived destructor has run (shared_ptr.h:40). This external dependency
3+# report is recorded, not counted as fixed by component_drivers. Keep all
4+# other UBSan checks and ASan/LeakSanitizer enabled.
O

💡 建议:libmcpp-vptr.supp 抑制范围过宽

vptr:*/libmcpp/libraries/mcbase/include/mc/memory/shared_ptr.h 覆盖了该头文件中发生的任何来源 vptr UBSan 报告,而不论调用栈来源和类。如果本项目自己的代码在某个 shared_ptr 释放框架中触发了真正的 invalid-vptr(如对已运行析构的对象调用虚函数),它将被安静抑制,使压测「绿色」结果产生误导。

修复建议:将抑制收紧到特定外部符号/函数(即注释中指出的 libmcpp deallocation 回调 shared_ptr.h:40),而非针对整个 shared_ptr.h 做类型级抑制;同时在说明文档中记录预期报告数量与内容,以便将来有变化时能被发现。

likedislike
liuxingsen
25 days ago Comments:
5+vptr:*/libmcpp/libraries/mcbase/include/mc/memory/shared_ptr.h
@@ -257,10 +257,18 @@ LIBMCPP_SRCDIR="$PROJECT_DIR/subprojects/libmcpp"
257PCDIR="$PROJECT_DIR/.pkg-config"257PCDIR="$PROJECT_DIR/.pkg-config"
258BUILD_DIR="$PROJECT_DIR/builddir"258BUILD_DIR="$PROJECT_DIR/builddir"
259 259 
260-# bingo/conan 把 Lua 放进 temp/ 时不必再强制 apt 装 liblua5.4-dev。260+lua_headers_complete() {
261+ local header
262+ for header in lua.h lauxlib.h lualib.h luaconf.h; do
263+ [ -f "$1/$header" ] || return 1
264+ done
265+}
266+ 
267+# bingo/conan 把完整的 Lua 放进 temp/ 时不必再强制 apt 装 liblua5.4-dev。
261conan_temp_lua_ready() {268conan_temp_lua_ready() {
262 local t="${PROJECT_DIR}/temp"269 local t="${PROJECT_DIR}/temp"
263- { [ -f "$t/include/luajit/lua.h" ] || [ -f "$t/include/lua.h" ]; } && [ -e "$t/usr/lib64/liblua.so" ]270+ { lua_headers_complete "$t/include/luajit" || lua_headers_complete "$t/include"; } &&
271+ [ -e "$t/usr/lib64/liblua.so" ]
264}272}
265CACHE_LAYOUT_VERSION="5"273CACHE_LAYOUT_VERSION="5"
266 274 
@@ -1011,18 +1019,24 @@ ensure_host_packages() {
1011}1019}
1012 1020 
1013# 把一份 Lua 头/库接到 libmcpp 要求的 lua_root(必须在源码树外),并写 native-file。1021# 把一份 Lua 头/库接到 libmcpp 要求的 lua_root(必须在源码树外),并写 native-file。
1014-# $1 头文件目录(内有 lua.h) $2 库目录 $3 链接名(lua / lua5.4) $4 日志说明1022+# $1 完整头文件目录 $2 库目录 $3 链接名(lua / lua5.4) $4 日志说明
1015activate_lua_toolchain() {1023activate_lua_toolchain() {
1016 local hdr_dir="$1"1024 local hdr_dir="$1"
1017 local lib_dir="$2"1025 local lib_dir="$2"
1018 local link_name="$3"1026 local link_name="$3"
1019 local label="$4"1027 local label="$4"
1020 local _h _so1028 local _h _so
1029+ local _lua_headers=(lua.h lauxlib.h lualib.h luaconf.h lua.hpp luajit.h compat-5.3.h lprefix_53.h lbuffer.h)
1030+ 
1031+ if ! lua_headers_complete "$hdr_dir" || [ ! -e "$lib_dir/lib${link_name}.so" ]; then
1032+ print_error "Lua 工具链不完整: hdr=${hdr_dir} lib=${lib_dir}/lib${link_name}.so"
1033+ return 1
1034+ fi
1021 1035 
1022 local _lua_inc_dir="$PROJECT_DIR/.lua54_inc"1036 local _lua_inc_dir="$PROJECT_DIR/.lua54_inc"
1023 mkdir -p "$_lua_inc_dir"1037 mkdir -p "$_lua_inc_dir"
1024 # 新来源没有的头要删掉旧软链,避免可选的 lua.hpp 等仍指到上一份 Lua。1038 # 新来源没有的头要删掉旧软链,避免可选的 lua.hpp 等仍指到上一份 Lua。
1025- for _h in lua.h lauxlib.h lualib.h luaconf.h lua.hpp; do1039+ for _h in "${_lua_headers[@]}"; do
1026 if [ -f "$hdr_dir/$_h" ]; then1040 if [ -f "$hdr_dir/$_h" ]; then
1027 ln -sfn "$hdr_dir/$_h" "$_lua_inc_dir/$_h"1041 ln -sfn "$hdr_dir/$_h" "$_lua_inc_dir/$_h"
1028 elif [ -L "$_lua_inc_dir/$_h" ]; then1042 elif [ -L "$_lua_inc_dir/$_h" ]; then
@@ -1035,7 +1049,7 @@ activate_lua_toolchain() {
1035 _workspace_key="$(printf '%s\n' "$PROJECT_DIR" | sha256sum | cut -d' ' -f1)"1049 _workspace_key="$(printf '%s\n' "$PROJECT_DIR" | sha256sum | cut -d' ' -f1)"
1036 _lua_root="${XDG_CACHE_HOME:-${HOME:-/tmp}/.cache}/openubmc/component_drivers_lua_root/${_workspace_key}"1050 _lua_root="${XDG_CACHE_HOME:-${HOME:-/tmp}/.cache}/openubmc/component_drivers_lua_root/${_workspace_key}"
1037 mkdir -p "$_lua_root/include" "$_lua_root/usr/lib64"1051 mkdir -p "$_lua_root/include" "$_lua_root/usr/lib64"
1038- for _h in lua.h lauxlib.h lualib.h luaconf.h lua.hpp; do1052+ for _h in "${_lua_headers[@]}"; do
1039 if [ -f "$hdr_dir/$_h" ]; then1053 if [ -f "$hdr_dir/$_h" ]; then
1040 ln -sfn "$hdr_dir/$_h" "$_lua_root/include/$_h"1054 ln -sfn "$hdr_dir/$_h" "$_lua_root/include/$_h"
1041 elif [ -L "$_lua_root/include/$_h" ]; then1055 elif [ -L "$_lua_root/include/$_h" ]; then
@@ -1052,7 +1066,7 @@ activate_lua_toolchain() {
1052 done1066 done
1053 shopt -u nullglob1067 shopt -u nullglob
1054 1068 
1055- if [ ! -f "$_lua_root/include/lua.h" ] || [ ! -e "$_lua_root/usr/lib64/lib${link_name}.so" ]; then1069+ if ! lua_headers_complete "$_lua_root/include" || [ ! -e "$_lua_root/usr/lib64/lib${link_name}.so" ]; then
1056 print_error "Lua 工具链不完整: hdr=${hdr_dir} lib=${lib_dir}/lib${link_name}.so"1070 print_error "Lua 工具链不完整: hdr=${hdr_dir} lib=${lib_dir}/lib${link_name}.so"
1057 exit 11071 exit 1
1058 fi1072 fi
@@ -1071,7 +1085,7 @@ c_link_args = ['-L${lib_dir}', '-l${link_name}']
1071NEOF1085NEOF
1072 # 路径和内容都属于工具链:同一目录升级 Lua 也须重建预编译库与主构建配置。1086 # 路径和内容都属于工具链:同一目录升级 Lua 也须重建预编译库与主构建配置。
1073 local _lua_inputs=("$_luajit_native_ini" "$lib_dir/lib${link_name}.so")1087 local _lua_inputs=("$_luajit_native_ini" "$lib_dir/lib${link_name}.so")
1074- for _h in lua.h lauxlib.h lualib.h luaconf.h lua.hpp; do1088+ for _h in "${_lua_headers[@]}"; do
1075 if [ -f "$hdr_dir/$_h" ]; then1089 if [ -f "$hdr_dir/$_h" ]; then
1076 _lua_inputs+=("$hdr_dir/$_h")1090 _lua_inputs+=("$hdr_dir/$_h")
1077 fi1091 fi
@@ -1107,7 +1121,7 @@ _lua_header_try_dir() {
1107 *) return 1 ;;1121 *) return 1 ;;
1108 esac1122 esac
1109 for sub in "/$version_dir" ""; do1123 for sub in "/$version_dir" ""; do
1110- if [ -f "${base}${sub}/lua.h" ]; then1124+ if lua_headers_complete "${base}${sub}"; then
1111 # 通用 include 下可能还留有 Lua 5.1;必须与选中的 .pc/库版本一致。1125 # 通用 include 下可能还留有 Lua 5.1;必须与选中的 .pc/库版本一致。
1112 version=$(awk '1126 version=$(awk '
1113 /^[[:space:]]*#[[:space:]]*define[[:space:]]+LUA_VERSION_NUM[[:space:]]+/ {1127 /^[[:space:]]*#[[:space:]]*define[[:space:]]+LUA_VERSION_NUM[[:space:]]+/ {
@@ -1206,7 +1220,7 @@ setup_system_lua() {
1206 libdir="$(_lua_lib_dir_from_pc lua5.3 lua5.3 || true)"1220 libdir="$(_lua_lib_dir_from_pc lua5.3 lua5.3 || true)"
1207 link_name="lua5.3"1221 link_name="lua5.3"
1208 fi1222 fi
1209- if [ -z "$inc" ] || [ ! -f "$inc/lua.h" ] || [ -z "$libdir" ] || [ ! -e "$libdir/lib${link_name}.so" ]; then1223+ if [ -z "$inc" ] || ! lua_headers_complete "$inc" || [ -z "$libdir" ] || [ ! -e "$libdir/lib${link_name}.so" ]; then
1210 print_error "未找到系统 Lua 5.4/5.3(需要 lua.h 与 liblua5.4.so)"1224 print_error "未找到系统 Lua 5.4/5.3(需要 lua.h 与 liblua5.4.so)"
1211 print_error "请安装 liblua5.4-dev,或提供 temp/include/lua.h 与 temp/usr/lib64/liblua.so"1225 print_error "请安装 liblua5.4-dev,或提供 temp/include/lua.h 与 temp/usr/lib64/liblua.so"
1212 exit 11226 exit 1
@@ -1415,9 +1429,9 @@ main() {
1415 # bingo/conan temp 布局:旧包把头放在 include/luajit/,skynet 的 Lua 5.4 则直接1429 # bingo/conan temp 布局:旧包把头放在 include/luajit/,skynet 的 Lua 5.4 则直接
1416 # 放在 include/(与 gtest/bus 等混放)。两种都接受。1430 # 放在 include/(与 gtest/bus 等混放)。两种都接受。
1417 local _lua_hdr=""1431 local _lua_hdr=""
1418- if [ -f "$_conan_temp/include/luajit/lua.h" ]; then1432+ if lua_headers_complete "$_conan_temp/include/luajit"; then
1419 _lua_hdr="$_conan_temp/include/luajit/lua.h"1433 _lua_hdr="$_conan_temp/include/luajit/lua.h"
1420- elif [ -f "$_conan_temp/include/lua.h" ]; then1434+ elif lua_headers_complete "$_conan_temp/include"; then
1421 _lua_hdr="$_conan_temp/include/lua.h"1435 _lua_hdr="$_conan_temp/include/lua.h"
1422 fi1436 fi
1423 if [ -n "$_lua_hdr" ] && [ -e "$_conan_temp/usr/lib64/liblua.so" ]; then1437 if [ -n "$_lua_hdr" ] && [ -e "$_conan_temp/usr/lib64/liblua.so" ]; then
@@ -0,0 +1,106 @@
1+#!/usr/bin/env python3
2+ 
3+from collections import Counter
4+from pathlib import Path
5+import shutil
6+import subprocess
7+import tempfile
8+import unittest
9+ 
10+import run_memory_audit_stress as stress
11+ 
12+ 
13+class ParseCasesTest(unittest.TestCase):
14+ def test_accepts_run_marker_interleaved_after_runtime_log(self):
15+ output = """\
16+DEBUG thread_pool.cpp:494 operator() | io thread 1 started[ RUN ] MucseInterfaceTest.CoolingUpdatesSignedTemperatureBySmbus
17+[ OK ] MucseInterfaceTest.CoolingUpdatesSignedTemperatureBySmbus (0 ms)
18+"""
19+ 
20+ self.assertEqual(
21+ stress.parse_cases(output),
22+ {"run": 1, "ok": 1, "incomplete": 0},
23+ )
24+ 
25+ def test_accepts_log_text_interleaved_after_result_name(self):
26+ output = """\
27+[ RUN ] MucseInterfaceTest.CoolingUpdatesTemperatureByNcsi
28+[ OK ] MucseInterfaceTest.CoolingUpdatesTemperatureByNcsiDEBUG runtime started (1 ms)
29+"""
30+ 
31+ self.assertEqual(
32+ stress.parse_cases(output),
33+ {"run": 1, "ok": 1, "incomplete": 0},
34+ )
35+ 
36+ def test_ignores_repeated_failure_summary_without_run(self):
37+ output = """\
38+[ FAILED ] Suite.FailedCase
39+"""
40+ 
41+ self.assertEqual(stress.parse_cases(output), {"run": 0, "incomplete": 0})
42+ 
43+ def test_accepts_multiple_markers_on_one_line(self):
44+ output = """\
45+[ RUN ] Suite.First[ OK ] Suite.First (0 ms)[ RUN ] Suite.Second
46+[ OK ] Suite.Second (0 ms)
47+"""
48+ 
49+ self.assertEqual(
50+ stress.parse_cases(output),
51+ {"run": 2, "ok": 2, "incomplete": 0},
52+ )
53+ 
54+ 
55+class GtestCountErrorTest(unittest.TestCase):
56+ def test_accepts_first_and_matching_round_counts(self):
57+ self.assertIsNone(stress.gtest_count_error(11574, None))
58+ self.assertIsNone(stress.gtest_count_error(11574, 11574))
59+ 
60+ def test_rejects_changed_round_count(self):
61+ self.assertEqual(
62+ stress.gtest_count_error(11573, 11574),
63+ "GoogleTest case count changed from 11574 to 11573",
64+ )
65+ 
66+ 
67+@unittest.skipUnless(shutil.which("meson"), "requires Meson")
68+class RoundResultTest(unittest.TestCase):
69+ def run_fixture(self, skipped):
70+ with tempfile.TemporaryDirectory(prefix="stress-round-result-") as tmp:
71+ root = Path(tmp)
72+ (root / "meson.build").write_text(
73+ "project('stress-fixture')\n"
74+ "python = import('python').find_installation('python3')\n"
75+ "test('fixture', python, args: files('fixture.py'))\n"
76+ )
77+ output = "[ RUN ] Suite.First\n[ OK ] Suite.First (0 ms)\n"
78+ if skipped:
79+ output += "[ RUN ] Suite.Second\n[ SKIPPED ] Suite.Second (0 ms)\n"
80+ (root / "fixture.py").write_text(f"print({output!r})\n")
81+ build = root / "build"
82+ setup = subprocess.run(
83+ ["meson", "setup", str(build), str(root)],
84+ text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
85+ )
86+ self.assertEqual(setup.returncode, 0, setup.stdout)
87+ results = root / "results"
88+ results.mkdir()
89+ return stress.run_round(
90+ build, results, 1, Counter({"stress-fixture:fixture": 1}), 30,
91+ )
92+ 
93+ def test_accepts_complete_successful_round(self):
94+ outcome = self.run_fixture(skipped=False)
95+ self.assertTrue(outcome["passed"], outcome)
96+ self.assertEqual(outcome["gtest_cases"]["ok"], 1)
97+ 
98+ def test_rejects_skipped_cases_even_when_meson_reports_success(self):
99+ outcome = self.run_fixture(skipped=True)
100+ self.assertEqual(outcome["meson_exit_code"], 0, outcome)
101+ self.assertEqual(outcome["gtest_cases"]["skipped"], 1, outcome)
102+ self.assertFalse(outcome["passed"], outcome)
103+ 
104+ 
105+if __name__ == "__main__":
106+ unittest.main()
@@ -0,0 +1,17 @@
1+#!/usr/bin/env python3
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+"""Compatibility entry point for the unified Lua build-script tests."""
11+from pathlib import Path
12+import runpy
13+ 
14+ 
15+if __name__ == "__main__":
16+ runpy.run_path(str(Path(__file__).with_name("tests") / "test_smart_build_lua.py"),
17+ run_name="__main__")
@@ -8,10 +8,12 @@
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 FIT 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-"""只运行 smart_build 的 Lua 辅助函数,不执行主构建或包安装。"""11+"""验证 Lua 选择、缓存和小型编译探针,不执行驱动主构建或包安装。"""
12 12 
13import os13import os
14from pathlib import Path14from pathlib import Path
15+import shlex
16+import shutil
15import subprocess17import subprocess
16import tempfile18import tempfile
17import unittest19import unittest
@@ -51,6 +53,8 @@ class LuaToolchainTests(unittest.TestCase):
51 def header(self, directory, version):53 def header(self, directory, version):
52 directory.mkdir(parents=True, exist_ok=True)54 directory.mkdir(parents=True, exist_ok=True)
53 (directory / "lua.h").write_text(f"#define LUA_VERSION_NUM {version}\n")55 (directory / "lua.h").write_text(f"#define LUA_VERSION_NUM {version}\n")
56+ for name in ("lauxlib.h", "lualib.h", "luaconf.h"):
57+ (directory / name).touch()
54 return directory58 return directory
55 59 
56 def pc(self, name, cflags=""):60 def pc(self, name, cflags=""):
@@ -119,6 +123,22 @@ meson_extra_args=()
119 self.assertNotEqual(result.returncode, 0)123 self.assertNotEqual(result.returncode, 0)
120 self.assertEqual(result.stdout, "")124 self.assertEqual(result.stdout, "")
121 125 
126+ def test_rejects_matching_version_with_missing_core_header(self):
127+ self.header(self.include, 504)
128+ (self.include / "lauxlib.h").unlink()
129+ result = self.shell('_lua_header_try_dir "$TEST_ROOT/include" 504')
130+ self.assertNotEqual(result.returncode, 0)
131+ self.assertEqual(result.stdout, "")
132+ 
133+ def test_conan_temp_readiness_requires_complete_headers(self):
134+ inc = self.header(self.root / "project/temp/include", 504)
135+ lib = self.root / "project/temp/usr/lib64"
136+ lib.mkdir(parents=True)
137+ (lib / "liblua.so").touch()
138+ self.assertEqual(self.shell("conan_temp_lua_ready").returncode, 0)
139+ (inc / "luaconf.h").unlink()
140+ self.assertNotEqual(self.shell("conan_temp_lua_ready").returncode, 0)
141+ 
122 def test_accepts_matching_header_directly_in_include(self):142 def test_accepts_matching_header_directly_in_include(self):
123 self.header(self.include, 504)143 self.header(self.include, 504)
124 self.assert_directory(144 self.assert_directory(
@@ -198,6 +218,13 @@ setup_libmcpp_cache
198 'printf "\\n/* updated header */\\n" >> "$TEST_ROOT/include/lua.h"'218 'printf "\\n/* updated header */\\n" >> "$TEST_ROOT/include/lua.h"'
199 )219 )
200 220 
221+ def test_prebuilt_cache_rebuilds_when_compatibility_header_changes(self):
222+ self.include.mkdir()
223+ (self.include / "compat-5.3.h").write_text("/* initial compatibility header */\n")
224+ self.assert_cache_rebuilt(
225+ 'printf "updated compatibility header\\n" >> "$TEST_ROOT/include/compat-5.3.h"'
226+ )
227+ 
201 def test_prebuilt_cache_rebuilds_when_library_changes(self):228 def test_prebuilt_cache_rebuilds_when_library_changes(self):
202 self.assert_cache_rebuilt(229 self.assert_cache_rebuilt(
203 'printf "new library bytes\\n" > "$TEST_ROOT/lib/liblua5.4.so"'230 'printf "new library bytes\\n" > "$TEST_ROOT/lib/liblua5.4.so"'
@@ -206,7 +233,7 @@ setup_libmcpp_cache
206 def test_prebuilt_cache_rebuilds_when_lua_source_moves(self):233 def test_prebuilt_cache_rebuilds_when_lua_source_moves(self):
207 self.assert_cache_rebuilt(r"""234 self.assert_cache_rebuilt(r"""
208mkdir "$TEST_ROOT/moved"235mkdir "$TEST_ROOT/moved"
209-cp "$TEST_ROOT/include/lua.h" "$TEST_ROOT/moved/lua.h"236+cp "$TEST_ROOT/include/"*.h "$TEST_ROOT/moved/"
210hdr="$TEST_ROOT/moved"237hdr="$TEST_ROOT/moved"
211""")238""")
212 239 
@@ -246,5 +273,161 @@ lua_dependency_matches "$BUILD_DIR"
246 self.assertEqual((self.root / "setup-calls").read_text().splitlines(), ["setup", "setup"])273 self.assertEqual((self.root / "setup-calls").read_text().splitlines(), ["setup", "setup"])
247 274 
248 275 
276+HARNESS = r'''
277+source "$SMART_BUILD_SCRIPT"
278+PROJECT_DIR="$LUA_TEST_PROJECT"
279+BUILD_DIR="$PROJECT_DIR/builddir"
280+CACHE_DIR="$PROJECT_DIR/.libmcpp_prebuilt"
281+LIBMCPP_SRCDIR="$PROJECT_DIR/subprojects/libmcpp"
282+PCDIR="$PROJECT_DIR/.pkg-config"
283+# Keep dependency selection real; suppress unrelated package/cache/build side effects.
284+ensure_host_packages() { :; }
285+setup_relative_timestamp() { :; }
286+setup_ccache() { :; }
287+get_system_info() { echo '4 8 8 0'; }
288+resolve_libmcpp_revision() { :; }
289+setup_libmcpp_cache() {
290+ printf '%s\n' "${meson_extra_args[@]}" > "$PROJECT_DIR/selected-args.txt"
291+ touch "$PROJECT_DIR/reached-build"
292+ exit 0
293+}
294+main --setup-only
295+'''
296+ 
297+ 
298+class LuaSelectionIntegrationTests(unittest.TestCase):
299+ @classmethod
300+ def setUpClass(cls):
301+ if not shutil.which("meson") or subprocess.run(
302+ ["pkg-config", "--exists", "lua5.4"], check=False
303+ ).returncode:
304+ raise unittest.SkipTest("requires Meson and the Lua 5.4 development package")
305+ cls.cflags = shlex.split(subprocess.check_output(
306+ ["pkg-config", "--cflags-only-I", "lua5.4"], text=True))
307+ cls.headers = next(Path(f[2:]) for f in cls.cflags if (Path(f[2:]) / "lua.h").is_file())
308+ cls.libdir = Path(subprocess.check_output(
309+ ["pkg-config", "--variable=libdir", "lua5.4"], text=True).strip())
310+ 
311+ def setUp(self):
312+ self.tmp = tempfile.TemporaryDirectory(prefix="smart-build-lua-")
313+ self.addCleanup(self.tmp.cleanup)
314+ self.project = Path(self.tmp.name) / "project"
315+ self.project.mkdir()
316+ (self.project / "meson.build").touch()
317+ self.env = {**os.environ, "SMART_BUILD_SCRIPT": str(SCRIPT),
318+ "_SMART_BUILD_ORIG_SCRIPT": str(SCRIPT),
319+ "LUA_TEST_PROJECT": str(self.project),
320+ "XDG_CACHE_HOME": str(Path(self.tmp.name) / "cache"),
321+ "PKG_CONFIG_PATH": "", "PKG_CONFIG_SYSROOT_DIR": "",
322+ "LD_LIBRARY_PATH": "", "SMART_BUILD_NO_TIMESTAMP": "1"}
323+ 
324+ def select(self):
325+ return subprocess.run(["bash", "-c", HARNESS], env=self.env,
326+ text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=30)
327+ 
328+ def compile_and_run(self, require_temp=False):
329+ native = self.project / "builddir_luajit_native.ini"
330+ self.assertTrue(native.is_file(), "Lua native file was not generated")
331+ args = (self.project / "selected-args.txt").read_text().splitlines()
332+ roots = [arg.split("=", 1)[1] for arg in args if arg.startswith("-Dlibmcpp:lua_root=")]
333+ self.assertEqual(len(roots), 1, args)
334+ self.assertFalse(Path(roots[0]).is_relative_to(self.project))
335+ # Consume the emitted native file and lua_root with the real compiler/linker.
336+ (self.project / "meson_options.txt").write_text("option('lua_root', type: 'string')\n")
337+ (self.project / "meson.build").write_text("""project('lua-probe', 'cpp')
338+cc = meson.get_compiler('cpp')
339+root = get_option('lua_root')
340+lua = cc.find_library('lua', dirs: root / 'usr/lib64')
341+executable('probe', 'probe.cpp', dependencies: lua,
342+ include_directories: include_directories(root / 'include'))
343+""")
344+ (self.project / "probe.cpp").write_text('''extern "C" {
345+#include <lua.h>
346+#include <lauxlib.h>
347+#include <lualib.h>
348+}
349+static_assert(LUA_VERSION_NUM == 504, "Lua 5.4 is required");
350+''' + ('#ifndef SMART_BUILD_TEST_TEMP\n#error Conan headers lost priority\n#endif\n' if require_temp else '') + '''
351+int main() {
352+ lua_State *state = luaL_newstate();
353+ if (!state) return 1;
354+ luaL_openlibs(state);
355+ int status = luaL_dostring(state, "assert(_VERSION == 'Lua 5.4')");
356+ lua_close(state);
357+ return status;
358+}
359+''')
360+ build = self.project / "probe-build"
361+ commands = [["meson", "setup", str(build), str(self.project),
362+ "--native-file=" + str(native), "-Dlua_root=" + roots[0]],
363+ ["meson", "compile", "-C", str(build)], [str(build / "probe")]]
364+ for command in commands:
365+ result = subprocess.run(command, env=self.env, text=True,
366+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
367+ self.assertEqual(result.returncode, 0, result.stdout)
368+ 
369+ def test_missing_temp_uses_system_lua_and_regenerates_native_paths(self):
370+ (self.project / "builddir_luajit_native.ini").write_text("stale paths from another checkout\n")
371+ result = self.select()
372+ self.assertEqual(result.returncode, 0, result.stdout)
373+ self.compile_and_run()
374+ 
375+ def test_complete_temp_keeps_priority_over_system_lua(self):
376+ inc = self.project / "temp/include"
377+ lib = self.project / "temp/usr/lib64"
378+ inc.mkdir(parents=True)
379+ lib.mkdir(parents=True)
380+ for header in self.headers.glob("*.h*"):
381+ shutil.copyfile(header, inc / header.name)
382+ with (inc / "lua.h").open("a") as output:
383+ output.write("\n#define SMART_BUILD_TEST_TEMP 1\n")
384+ (lib / "liblua.so").symlink_to(self.libdir / "liblua5.4.so")
385+ result = self.select()
386+ self.assertEqual(result.returncode, 0, result.stdout)
387+ self.compile_and_run(require_temp=True)
388+ 
389+ def test_incomplete_temp_does_not_mix_its_headers_with_system_library(self):
390+ inc = self.project / "temp/include"
391+ inc.mkdir(parents=True)
392+ (inc / "lua.h").write_text("#error incomplete temp headers must not be selected\n")
393+ result = self.select()
394+ self.assertEqual(result.returncode, 0, result.stdout)
395+ self.compile_and_run()
396+ 
397+ def test_temp_with_library_but_missing_core_headers_falls_back_as_a_pair(self):
398+ self.assertEqual(self.select().returncode, 0)
399+ inc = self.project / "temp/include"
400+ lib = self.project / "temp/usr/lib64"
401+ inc.mkdir(parents=True)
402+ lib.mkdir(parents=True)
403+ (inc / "lua.h").write_text("#error incomplete temp must not replace system headers\n")
404+ (lib / "liblua.so").symlink_to(self.libdir / "liblua5.4.so")
405+ result = self.select()
406+ self.assertEqual(result.returncode, 0, result.stdout)
407+ self.compile_and_run()
408+ 
409+ def test_selection_removes_stale_optional_headers_and_library_aliases(self):
410+ self.assertEqual(self.select().returncode, 0)
411+ args = (self.project / "selected-args.txt").read_text().splitlines()
412+ root = Path(next(arg.split("=", 1)[1] for arg in args
413+ if arg.startswith("-Dlibmcpp:lua_root=")))
414+ stale = [self.project / ".lua54_inc/luajit.h", root / "include/luajit.h",
415+ root / "usr/lib64/liblua.so.99"]
416+ for path in stale:
417+ path.symlink_to(self.project / "removed-old-dependency")
418+ result = self.select()
419+ self.assertEqual(result.returncode, 0, result.stdout)
420+ for path in stale:
421+ self.assertFalse(path.is_symlink(), f"stale dependency remains: {path}")
422+ self.compile_and_run()
423+ 
424+ def test_missing_both_sources_stops_before_build_with_actionable_error(self):
425+ self.env["PKG_CONFIG_LIBDIR"] = str(Path(self.tmp.name) / "empty-pkgconfig")
426+ result = self.select()
427+ self.assertNotEqual(result.returncode, 0, result.stdout)
428+ self.assertIn("liblua5.4-dev", result.stdout)
429+ self.assertFalse((self.project / "reached-build").exists())
430+ 
431+ 
249if __name__ == "__main__":432if __name__ == "__main__":
250 unittest.main(verbosity=2)433 unittest.main(verbosity=2)
@@ -15,11 +15,17 @@
15#include <gtest/gtest.h>15#include <gtest/gtest.h>
16 16 
17#include <chrono>17#include <chrono>
18+#include <future>
19+#include <mc/timer.h>
20+#include <resource_tree/interface/Bus.h>
18#include <string>21#include <string>
19#include <thread>22#include <thread>
20 23 
21-#include "../../../../../drivers/csr1/bus/common/bus_object_base.h"24+#define private public
22#include "../../../../../drivers/csr1/bus/common/interface/i_bus.h"25#include "../../../../../drivers/csr1/bus/common/interface/i_bus.h"
26+#undef private
27+ 
28+#include "../../../../../drivers/csr1/bus/common/bus_object_base.h"
23#include "../../../../../drivers/internal/manager.h"29#include "../../../../../drivers/internal/manager.h"
24 30 
25class TestBusObject : public mc::engine::object<TestBusObject> {31class TestBusObject : public mc::engine::object<TestBusObject> {
@@ -340,4 +346,71 @@ TEST_F(InterfaceBusNoInternalTest, enable_without_internal_bus_is_noop_keeping_s
340 EXPECT_TRUE(m_object->m_interface_bus.AccessEnabled.get_value().as<bool>());346 EXPECT_TRUE(m_object->m_interface_bus.AccessEnabled.get_value().as<bool>());
341}347}
342 348 
349+TEST_F(InterfaceBusTest, cancelled_access_timer_does_not_retain_interface_parent)
350+{
351+ m_object->m_interface_bus.SetAccessibility(false, 1800);
352+ auto timer = m_object->m_interface_bus.m_access_timer;
353+ ASSERT_NE(timer, nullptr);
354+ m_object->m_interface_bus.SetAccessibility(true, 0);
355+ EXPECT_FALSE(timer->is_active());
356+ EXPECT_EQ(timer->get_parent(), nullptr);
357+ m_object.reset();
358+}
359+ 
360+TEST_F(InterfaceBusTest, replaced_access_timer_does_not_retain_interface_parent)
361+{
362+ m_object->m_interface_bus.SetAccessibility(false, 1800);
363+ auto timer = m_object->m_interface_bus.m_access_timer;
364+ ASSERT_NE(timer, nullptr);
365+ m_object->m_interface_bus.SetAccessibility(false, 1800);
366+ EXPECT_NE(timer, m_object->m_interface_bus.m_access_timer);
367+ EXPECT_FALSE(timer->is_active());
368+ EXPECT_EQ(timer->get_parent(), nullptr);
369+ m_object.reset();
370+}
371+ 
372+TEST_F(InterfaceBusTest, completed_access_timer_does_not_retain_interface_parent)
373+{
374+ m_object->m_interface_bus.SetAccessibility(false, 1800);
375+ auto timer = m_object->m_interface_bus.m_access_timer;
376+ ASSERT_NE(timer, nullptr);
377+ // Run the installed completion callback deterministically; the existing
378+ // timer_callback_re_enables_access test covers actual expiry dispatch.
379+ timer->stop();
380+ timer->timeout();
381+ EXPECT_TRUE(m_object->m_interface_bus.isAccessAllowed());
382+ EXPECT_EQ(m_object->m_interface_bus.m_access_timer, timer);
383+ EXPECT_EQ(timer->get_parent(), nullptr);
384+ m_object.reset();
385+}
386+ 
387+TEST_F(InterfaceBusTest, destruction_stops_access_timer_retained_by_pending_work)
388+{
389+ m_object->m_interface_bus.SetAccessibility(false, 1800);
390+ auto timer = m_object->m_interface_bus.m_access_timer;
391+ ASSERT_NE(timer, nullptr);
392+ m_object.reset();
393+ EXPECT_FALSE(timer->is_active());
394+ EXPECT_EQ(timer->get_parent(), nullptr);
395+ timer->stop();
396+}
397+ 
398+TEST_F(InterfaceBusTest, access_timer_callback_uses_owner_executor)
399+{
400+ mc::any_executor owner_executor{mc::runtime::runtime_strand{}};
401+ m_object->bind_executor(owner_executor);
402+ m_object->m_interface_bus.SetAccessibility(false, 1800);
403+ auto timer = m_object->m_interface_bus.m_access_timer;
404+ ASSERT_NE(timer, nullptr);
405+ auto result = std::make_shared<std::promise<bool>>();
406+ auto ready = result->get_future();
407+ timer->timeout.connect([owner_executor, result]() {
408+ result->set_value(owner_executor.running_in_this_thread());
409+ });
410+ timer->start(mc::milliseconds(1));
411+ ASSERT_EQ(ready.wait_for(std::chrono::seconds(2)), std::future_status::ready);
412+ EXPECT_TRUE(ready.get());
413+ timer->stop();
414+}
415+ 
343} // namespace416} // namespace
@@ -81,6 +81,18 @@ public:
81 return 0;81 return 0;
82 }82 }
83 83 
84+ static bool task_is_idle(ITask* task)
85+ {
86+ auto& sched = Scheduler::get_instance();
87+ std::lock_guard<std::recursive_mutex> lock(sched.m_mutex);
88+ for (const auto& entry : sched.m_entries) {
89+ if (entry.task == task) {
90+ return entry.run_state && !entry.run_state->running.load(std::memory_order_acquire);
91+ }
92+ }
93+ return false;
94+ }
95+ 
84 // 将某 task 的 entry 重新置为到期(next_run_ms=0),驱动下一轮 on_timer 执行96 // 将某 task 的 entry 重新置为到期(next_run_ms=0),驱动下一轮 on_timer 执行
85 static void make_task_due(ITask* task)97 static void make_task_due(ITask* task)
86 {98 {
@@ -165,9 +177,9 @@ public:
165 }177 }
166 178 
167private:179private:
168- uint64_t m_period;180+ uint64_t m_period;
169- uint32_t m_run_count;181+ std::atomic<uint32_t> m_run_count;
170- bool m_throw_std;182+ bool m_throw_std;
171};183};
172 184 
173// ===== Task that registers another task during run: 验证锁外执行 =====185// ===== Task that registers another task during run: 验证锁外执行 =====
@@ -349,6 +361,104 @@ private:
349 std::atomic<uint32_t> m_run_count{0};361 std::atomic<uint32_t> m_run_count{0};
350};362};
351 363 
364+// The completion promise lives outside the task so a pending hardware result
365+// does not itself keep the host alive and hide Scheduler ownership mistakes.
366+struct owned_task_signals {
367+ mc::promise<void> completion{mc::runtime::immediate_executor{}};
368+ std::atomic<bool> entered{false};
369+ std::atomic<uint32_t> run_count{0};
370+ std::atomic<bool> destroyed{false};
371+};
372+ 
373+class owned_async_task : public mc::object_base, public dev::csr1::ITask {
374+public:
375+ explicit owned_async_task(std::shared_ptr<owned_task_signals> signals) : m_signals(std::move(signals))
376+ {}
377+ 
378+ ~owned_async_task() override
379+ {
380+ dev::csr1::Scheduler::get_instance().unregister_task(this);
381+ m_signals->destroyed.store(true, std::memory_order_release);
382+ }
383+ 
384+ void run() override
385+ {
386+ ADD_FAILURE() << "Scheduler must use run_async";
387+ }
388+ 
389+ mc::future<void> run_async() override
390+ {
391+ if (m_signals->run_count.fetch_add(1, std::memory_order_acq_rel) != 0) {
392+ return mc::resolve();
393+ }
394+ auto completion = m_signals->completion.get_future();
395+ m_signals->entered.store(true, std::memory_order_release);
396+ return completion;
397+ }
398+ 
399+ uint64_t period_ms() const override
400+ {
401+ return 60000;
402+ }
403+ 
404+ mc::shared_ptr<mc::object_base> owner_guard() override
405+ {
406+ return shared_from_this();
407+ }
408+ bool lifetime_bound_to_owner() const override
409+ {
410+ return true;
411+ }
412+ 
413+private:
414+ std::shared_ptr<owned_task_signals> m_signals;
415+};
416+ 
417+// Unlike owned_async_task, keeping this task's host alive does not prevent
418+// reinitialization from replacing its unique_ptr child.
419+class replaceable_async_task : public dev::csr1::ITask {
420+public:
421+ replaceable_async_task(mc::object_base* owner, std::shared_ptr<owned_task_signals> signals)
422+ : m_owner(owner), m_signals(std::move(signals))
423+ {}
424+ 
425+ ~replaceable_async_task() override
426+ {
427+ m_signals->destroyed.store(true, std::memory_order_release);
428+ }
429+ 
430+ void run() override
431+ {
432+ ADD_FAILURE() << "Scheduler must use run_async";
433+ }
434+ 
435+ mc::future<void> run_async() override
436+ {
437+ auto completion = m_signals->completion.get_future();
438+ m_signals->entered.store(true, std::memory_order_release);
439+ return completion;
440+ }
441+ 
442+ uint64_t period_ms() const override
443+ {
444+ return 60000;
445+ }
446+ 
447+ mc::shared_ptr<mc::object_base> owner_guard() override
448+ {
449+ return m_owner->shared_from_this();
450+ }
451+ 
452+private:
453+ mc::object_base* m_owner;
454+ std::shared_ptr<owned_task_signals> m_signals;
455+};
456+ 
457+class replaceable_task_host : public mc::object_base {
458+public:
459+ std::unique_ptr<replaceable_async_task> task;
460+};
461+ 
352template <typename Predicate>462template <typename Predicate>
353bool wait_until(Predicate&& predicate, std::chrono::milliseconds timeout)463bool wait_until(Predicate&& predicate, std::chrono::milliseconds timeout)
354{464{
@@ -381,6 +491,220 @@ protected:
381 }491 }
382};492};
383 493 
494+TEST_F(SchedulerTest, idle_registration_does_not_retain_owner)
495+{
496+ auto& sched = dev::csr1::Scheduler::get_instance();
497+ auto signals = std::make_shared<owned_task_signals>();
498+ auto owner = mc::make_shared<owned_async_task>(signals);
499+ mc::weak_ptr<owned_async_task> tracked = owner;
500+ auto* task = owner.get();
501+ sched.register_task(task);
502+ 
503+ owner.reset();
504+ 
505+ EXPECT_TRUE(tracked.expired());
506+ EXPECT_TRUE(signals->destroyed.load(std::memory_order_acquire));
507+ EXPECT_FALSE(sched.has_task(task));
508+ EXPECT_FALSE(signals->entered.load(std::memory_order_acquire));
509+}
510+ 
511+TEST_F(SchedulerTest, async_owner_is_retained_until_future_completion_then_released)
512+{
513+ auto& sched = dev::csr1::Scheduler::get_instance();
514+ auto signals = std::make_shared<owned_task_signals>();
515+ auto owner = mc::make_shared<owned_async_task>(signals);
516+ mc::weak_ptr<owned_async_task> tracked = owner;
517+ auto* task = owner.get();
518+ sched.register_task(task);
519+ dev::csr1::SchedulerTestHelper::make_task_due(task);
520+ dev::csr1::SchedulerTestHelper::invoke_on_timer();
521+ const bool entered = wait_until([&] {
522+ return signals->entered.load(std::memory_order_acquire);
523+ }, std::chrono::seconds(2));
524+ EXPECT_TRUE(entered);
525+ 
526+ // Release from another thread so a broken implementation that enters the
527+ // destructor early can still be unblocked by settling the future below.
528+ auto released = std::async(std::launch::async, [owner = std::move(owner)]() mutable {
529+ owner.reset();
530+ });
531+ EXPECT_EQ(released.wait_for(std::chrono::seconds(2)), std::future_status::ready);
532+ EXPECT_FALSE(tracked.expired());
533+ EXPECT_FALSE(signals->destroyed.load(std::memory_order_acquire));
534+ 
535+ signals->completion.set_value();
536+ released.wait();
537+ EXPECT_TRUE(wait_until([&] {
538+ return signals->destroyed.load(std::memory_order_acquire);
539+ }, std::chrono::seconds(2)));
540+ EXPECT_TRUE(tracked.expired());
541+ EXPECT_FALSE(sched.has_task(task));
542+}
543+ 
544+TEST_F(SchedulerTest, guarded_async_callback_can_unregister_before_completing_its_future)
545+{
546+ auto& sched = dev::csr1::Scheduler::get_instance();
547+ auto signals = std::make_shared<owned_task_signals>();
548+ auto owner = mc::make_shared<owned_async_task>(signals);
549+ mc::weak_ptr<owned_async_task> tracked = owner;
550+ auto* task = owner.get();
551+ sched.register_task(task);
552+ dev::csr1::SchedulerTestHelper::make_task_due(task);
553+ dev::csr1::SchedulerTestHelper::invoke_on_timer();
554+ EXPECT_TRUE(wait_until([&] {
555+ return signals->entered.load(std::memory_order_acquire);
556+ }, std::chrono::seconds(2)));
557+ 
558+ // Model a hardware completion on a different worker than run_async().
559+ // Unregistration happens inside that callback, before its future settles.
560+ // No external owner reference remains; the scheduler guard must protect it.
561+ owner.reset();
562+ std::atomic<bool> settled{false};
563+ auto callback = std::async(std::launch::async, [&] {
564+ sched.unregister_task(task);
565+ if (!settled.exchange(true)) {
566+ signals->completion.set_value();
567+ }
568+ });
569+ const auto status = callback.wait_for(std::chrono::seconds(2));
570+ // Break the deadlock on a failing implementation so the regression has a
571+ // bounded failure and can still clean up its worker and runtime safely.
572+ if (!settled.exchange(true)) {
573+ signals->completion.set_value();
574+ }
575+ callback.wait();
576+ EXPECT_EQ(status, std::future_status::ready);
577+ EXPECT_TRUE(wait_until([&] {
578+ return signals->destroyed.load(std::memory_order_acquire);
579+ }, std::chrono::seconds(2)));
580+ EXPECT_TRUE(tracked.expired());
581+ EXPECT_FALSE(sched.has_task(task));
582+}
583+ 
584+TEST_F(SchedulerTest, managed_unregister_reregister_preserves_pending_single_flight)
585+{
586+ auto& sched = dev::csr1::Scheduler::get_instance();
587+ auto signals = std::make_shared<owned_task_signals>();
588+ auto owner = mc::make_shared<owned_async_task>(signals);
589+ auto* task = owner.get();
590+ sched.register_task(task);
591+ dev::csr1::SchedulerTestHelper::make_task_due(task);
592+ dev::csr1::SchedulerTestHelper::invoke_on_timer();
593+ EXPECT_TRUE(wait_until([&] {
594+ return signals->entered.load(std::memory_order_acquire);
595+ }, std::chrono::seconds(2)));
596+ 
597+ auto removed = std::async(std::launch::async, [&] {
598+ sched.unregister_task(task);
599+ });
600+ const auto status = removed.wait_for(std::chrono::seconds(2));
601+ if (status != std::future_status::ready) {
602+ signals->completion.set_value();
603+ }
604+ removed.wait();
605+ ASSERT_EQ(status, std::future_status::ready);
606+ EXPECT_FALSE(sched.has_task(task));
607+ 
608+ sched.register_task(task);
609+ EXPECT_TRUE(sched.has_task(task));
610+ dev::csr1::SchedulerTestHelper::make_task_due(task);
611+ dev::csr1::SchedulerTestHelper::invoke_on_timer();
612+ EXPECT_FALSE(wait_until([&] {
613+ return signals->run_count.load(std::memory_order_acquire) > 1;
614+ }, std::chrono::milliseconds(100)));
615+ EXPECT_EQ(signals->run_count.load(std::memory_order_acquire), 1u);
616+ 
617+ signals->completion.set_value();
618+ EXPECT_TRUE(wait_until([&] {
619+ dev::csr1::SchedulerTestHelper::make_task_due(task);
620+ dev::csr1::SchedulerTestHelper::invoke_on_timer();
621+ return signals->run_count.load(std::memory_order_acquire) >= 2;
622+ }, std::chrono::seconds(2)));
623+ sched.clear();
624+}
625+ 
626+TEST_F(SchedulerTest, clear_waits_for_unregistered_managed_pending_future)
627+{
628+ auto& sched = dev::csr1::Scheduler::get_instance();
629+ auto signals = std::make_shared<owned_task_signals>();
630+ auto owner = mc::make_shared<owned_async_task>(signals);
631+ auto* task = owner.get();
632+ sched.register_task(task);
633+ dev::csr1::SchedulerTestHelper::make_task_due(task);
634+ dev::csr1::SchedulerTestHelper::invoke_on_timer();
635+ EXPECT_TRUE(wait_until([&] {
636+ return signals->entered.load(std::memory_order_acquire);
637+ }, std::chrono::seconds(2)));
638+ 
639+ auto removed = std::async(std::launch::async, [&] {
640+ sched.unregister_task(task);
641+ });
642+ const auto status = removed.wait_for(std::chrono::seconds(2));
643+ if (status != std::future_status::ready) {
644+ signals->completion.set_value();
645+ }
646+ removed.wait();
647+ ASSERT_EQ(status, std::future_status::ready);
648+ EXPECT_FALSE(sched.has_task(task));
649+ 
650+ std::promise<void> clear_started;
651+ auto started = clear_started.get_future();
652+ auto cleared = std::async(std::launch::async, [&] {
653+ clear_started.set_value();
654+ sched.clear();
655+ });
656+ EXPECT_EQ(started.wait_for(std::chrono::seconds(2)), std::future_status::ready);
657+ EXPECT_EQ(cleared.wait_for(std::chrono::milliseconds(100)), std::future_status::timeout);
658+ signals->completion.set_value();
659+ EXPECT_EQ(cleared.wait_for(std::chrono::seconds(2)), std::future_status::ready);
660+ cleared.wait();
661+ owner.reset();
662+ EXPECT_TRUE(wait_until([&] {
663+ return signals->destroyed.load(std::memory_order_acquire);
664+ }, std::chrono::seconds(2)));
665+}
666+ 
667+TEST_F(SchedulerTest, managed_host_unregister_waits_before_replacing_child_task)
668+{
669+ auto& sched = dev::csr1::Scheduler::get_instance();
670+ auto signals = std::make_shared<owned_task_signals>();
671+ auto owner = mc::make_shared<replaceable_task_host>();
672+ mc::weak_ptr<replaceable_task_host> tracked = owner;
673+ owner->task = std::make_unique<replaceable_async_task>(owner.get(), signals);
674+ auto* task = owner->task.get();
675+ sched.register_task(task);
676+ dev::csr1::SchedulerTestHelper::make_task_due(task);
677+ dev::csr1::SchedulerTestHelper::invoke_on_timer();
678+ EXPECT_TRUE(wait_until([&] {
679+ return signals->entered.load(std::memory_order_acquire);
680+ }, std::chrono::seconds(2)));
681+ 
682+ std::promise<void> unregister_started;
683+ auto started = unregister_started.get_future();
684+ auto replaced = std::async(std::launch::async, [&] {
685+ unregister_started.set_value();
686+ sched.unregister_task(task);
687+ // Model Interface_Aggregate::init replacing its dispatcher after stop.
688+ owner->task.reset();
689+ });
690+ EXPECT_EQ(started.wait_for(std::chrono::seconds(2)), std::future_status::ready);
691+ EXPECT_TRUE(wait_until([&] {
692+ return !sched.has_task(task);
693+ }, std::chrono::seconds(2)));
694+ EXPECT_EQ(replaced.wait_for(std::chrono::milliseconds(100)), std::future_status::timeout);
695+ EXPECT_FALSE(signals->destroyed.load(std::memory_order_acquire));
696+ EXPECT_FALSE(tracked.expired());
697+ 
698+ // Always settle the operation, including on the broken implementation,
699+ // so the failure does not leave a blocked worker or outstanding future.
700+ signals->completion.set_value();
701+ EXPECT_EQ(replaced.wait_for(std::chrono::seconds(2)), std::future_status::ready);
702+ replaced.wait();
703+ EXPECT_TRUE(signals->destroyed.load(std::memory_order_acquire));
704+ EXPECT_FALSE(tracked.expired());
705+ EXPECT_EQ(owner->task, nullptr);
706+}
707+ 
384// ===== 场景五(P0):task 异常逃逸不应阻止后续 task 与重新 arm =====708// ===== 场景五(P0):task 异常逃逸不应阻止后续 task 与重新 arm =====
385 709 
386TEST_F(SchedulerTest, throwing_task_run_throws_expected_exception)710TEST_F(SchedulerTest, throwing_task_run_throws_expected_exception)
@@ -866,8 +1190,8 @@ public:
866 }1190 }
867 1191 
868private:1192private:
869- uint64_t m_period;1193+ std::atomic<uint64_t> m_period;
870- uint32_t m_run_count;1194+ std::atomic<uint32_t> m_run_count;
871};1195};
872 1196 
873} // namespace1197} // namespace
@@ -883,9 +1207,16 @@ TEST_F(SchedulerTest, period_change_takes_effect_on_next_round)
883 1207 
884 // 第一轮:按注册周期执行1208 // 第一轮:按注册周期执行
885 dev::csr1::SchedulerTestHelper::invoke_on_timer();1209 dev::csr1::SchedulerTestHelper::invoke_on_timer();
886- ASSERT_TRUE(wait_until([&task] {1210+ // run_count changes before complete_task() publishes the next deadline.
887- return task.run_count() == 1u;1211+ // Wait for that completion under the scheduler lock before forcing due.
888- }, std::chrono::milliseconds(300)));1212+ const bool completed = wait_until([&task] {
1213+ return task.run_count() == 1u && dev::csr1::SchedulerTestHelper::task_is_idle(&task);
1214+ }, std::chrono::milliseconds(300));
1215+ EXPECT_TRUE(completed);
1216+ if (!completed) {
1217+ sched.unregister_task(&task);
1218+ return;
1219+ }
889 EXPECT_EQ(task.run_count(), 1u);1220 EXPECT_EQ(task.run_count(), 1u);
890 EXPECT_EQ(dev::csr1::SchedulerTestHelper::get_entry_period(&task), 2000u);1221 EXPECT_EQ(dev::csr1::SchedulerTestHelper::get_entry_period(&task), 2000u);
891 1222 
@@ -895,8 +1226,8 @@ TEST_F(SchedulerTest, period_change_takes_effect_on_next_round)
895 1226 
896 // 第二轮:entry.period_ms 刷新为新周期1227 // 第二轮:entry.period_ms 刷新为新周期
897 dev::csr1::SchedulerTestHelper::invoke_on_timer();1228 dev::csr1::SchedulerTestHelper::invoke_on_timer();
898- ASSERT_TRUE(wait_until([&task] {1229+ EXPECT_TRUE(wait_until([&task] {
899- return task.run_count() == 2u;1230+ return task.run_count() == 2u && dev::csr1::SchedulerTestHelper::task_is_idle(&task);
900 }, std::chrono::milliseconds(300)));1231 }, std::chrono::milliseconds(300)));
901 EXPECT_EQ(task.run_count(), 2u);1232 EXPECT_EQ(task.run_count(), 2u);
902 EXPECT_EQ(dev::csr1::SchedulerTestHelper::get_entry_period(&task), 400u);1233 EXPECT_EQ(dev::csr1::SchedulerTestHelper::get_entry_period(&task), 400u);
@@ -2499,6 +2499,26 @@ TEST_F(InterfaceBitIOTest, Read_success_without_interface_chip_no_callback)
2499 2499 
2500// ===== Write: bit_write 成功路径 =====2500// ===== Write: bit_write 成功路径 =====
2501 2501 
2502+TEST_F(InterfaceBitIOTest, memory_safety_write_rejects_short_input)
2503+{
2504+ auto chip = std::make_shared<mock_chip_for_bitio>();
2505+ m_bitio->m_chip = chip.get();
2506+ for (uint8_t length : {2, 4, 255}) {
2507+ SCOPED_TRACE(+length);
2508+ EXPECT_THROW(m_bitio->Write(0x20, length, 0xffffffffU, {0x5a}), mc::invalid_arg_exception);
2509+ EXPECT_EQ(chip->get_last_write_offset(), 0U);
2510+ }
2511+}
2512+ 
2513+TEST_F(InterfaceBitIOTest, memory_safety_write_preserves_prefix_length)
2514+{
2515+ auto chip = std::make_shared<mock_chip_for_bitio>();
2516+ m_bitio->m_chip = chip.get();
2517+ EXPECT_NO_THROW(m_bitio->Write(0x20, 1, 0xff, {0x34, 0x56}));
2518+ EXPECT_EQ(chip->get_last_write_offset(), 0x20U);
2519+ EXPECT_EQ(chip->get_last_write_length(), 1U);
2520+}
2521+ 
2502TEST_F(InterfaceBitIOTest, Write_success)2522TEST_F(InterfaceBitIOTest, Write_success)
2503{2523{
2504 auto mock_chip = std::make_shared<mock_chip_for_bitio>();2524 auto mock_chip = std::make_shared<mock_chip_for_bitio>();
@@ -31,6 +31,7 @@
31#include "../../../../../../drivers/internal/hasdef.h"31#include "../../../../../../drivers/internal/hasdef.h"
32 32 
33#include <atomic>33#include <atomic>
34+#include <condition_variable>
34#include <deque>35#include <deque>
35#include <thread>36#include <thread>
36 37 
@@ -97,6 +98,35 @@ public:
97 dev::csr1::Interface_Aggregate aggregate;98 dev::csr1::Interface_Aggregate aggregate;
98};99};
99 100 
101+struct AggregateHostDestructionGate {
102+ std::mutex mutex;
103+ std::condition_variable cv;
104+ bool entered = false;
105+ bool release = false;
106+};
107+ 
108+// Keep the Aggregate member alive after the host's final strong reference has
109+// reached zero. This makes the otherwise narrow final-release/destructor
110+// window deterministic for host_guard() regression coverage.
111+class BlockingAggregateHostObject : public mc::engine::object<BlockingAggregateHostObject> {
112+public:
113+ MC_OBJECT(BlockingAggregateHostObject, "BlockingAggregateHost", "/bmc/test/BlockingAggregateHost/${object_name}",
114+ (dev::csr1::Interface_Aggregate))
115+ 
116+ ~BlockingAggregateHostObject()
117+ {
118+ std::unique_lock<std::mutex> lock(gate->mutex);
119+ gate->entered = true;
120+ gate->cv.notify_all();
121+ gate->cv.wait(lock, [this] {
122+ return gate->release;
123+ });
124+ }
125+ 
126+ std::shared_ptr<AggregateHostDestructionGate> gate;
127+ dev::csr1::Interface_Aggregate aggregate;
128+};
129+ 
100// 真实互斥的总线 mock:bus_lock/bus_unlock 背后是真 mutex(与生产总线一致),130// 真实互斥的总线 mock:bus_lock/bus_unlock 背后是真 mutex(与生产总线一致),
101// 同时暴露锁深度计数,供用例断言"读期间持锁、读后释放"131// 同时暴露锁深度计数,供用例断言"读期间持锁、读后释放"
102class counted_locking_bus : public dev::bus_base {132class counted_locking_bus : public dev::bus_base {
@@ -284,6 +314,7 @@ MC_REFLECT(dev::csr1::test::WindowScannerInterface,
284 (ReplyFromDfx)(ReplyFromDfxFailure)(FallbackToIndependent)(ReplyFromDfxSlow))314 (ReplyFromDfx)(ReplyFromDfxFailure)(FallbackToIndependent)(ReplyFromDfxSlow))
285MC_REFLECT(dev::csr1::test::WindowScannerObject, ((scanner, "bmc.kepler.WindowScanner")))315MC_REFLECT(dev::csr1::test::WindowScannerObject, ((scanner, "bmc.kepler.WindowScanner")))
286MC_REFLECT(dev::csr1::test::AggregateHostObject, ((aggregate, "bmc.kepler.Chip.Aggregate")))316MC_REFLECT(dev::csr1::test::AggregateHostObject, ((aggregate, "bmc.kepler.Chip.Aggregate")))
317+MC_REFLECT(dev::csr1::test::BlockingAggregateHostObject, ((aggregate, "bmc.kepler.Chip.Aggregate")))
287 318 
288namespace {319namespace {
289 320 
@@ -297,6 +328,18 @@ std::vector<uint8_t> pattern_bytes(uint32_t size)
297 return data;328 return data;
298}329}
299 330 
331+bool wait_for_host_release(const mc::weak_ptr<dev::csr1::test::AggregateHostObject>& host)
332+{
333+ const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
334+ while (!host.expired()) {
335+ if (std::chrono::steady_clock::now() >= deadline) {
336+ return false;
337+ }
338+ std::this_thread::sleep_for(std::chrono::milliseconds(1));
339+ }
340+ return true;
341+}
342+ 
300class window_regression_service : public mc::engine::service {343class window_regression_service : public mc::engine::service {
301public:344public:
302 explicit window_regression_service(const std::string& name) : mc::engine::service(name)345 explicit window_regression_service(const std::string& name) : mc::engine::service(name)
@@ -826,12 +869,15 @@ TEST_F(AggregateWindowRegressionTest, follower_degrades_after_master_destroyed)
826 m_agg_b->start();869 m_agg_b->start();
827 EXPECT_EQ(m_agg_b->m_master_aggregate, agg_a);870 EXPECT_EQ(m_agg_b->m_master_aggregate, agg_a);
828 871 
872+ mc::weak_ptr<dev::csr1::test::AggregateHostObject> master_host = m_hosts.front();
829 {873 {
830 // 宿主连同聚合一起析构并注销(模拟芯片卸载):本用例的首个宿主即 master 宿主874 // 宿主连同聚合一起析构并注销(模拟芯片卸载):本用例的首个宿主即 master 宿主
831 auto host_a = m_hosts.front();875 auto host_a = m_hosts.front();
832 m_service->unregister_object(std::string(host_a->get_object_path()));876 m_service->unregister_object(std::string(host_a->get_object_path()));
833 m_hosts.erase(m_hosts.begin());877 m_hosts.erase(m_hosts.begin());
834 } // master 析构并注销878 } // master 析构并注销
879+ // Engine unregister callbacks can briefly retain the host asynchronously.
880+ ASSERT_TRUE(wait_for_host_release(master_host));
835 881 
836 // follower 不再向已析构 master 转发:回放迁移旧 entry + 新注册落自身 dispatcher882 // follower 不再向已析构 master 转发:回放迁移旧 entry + 新注册落自身 dispatcher
837 m_agg_b->AddScanner("AfterDeathScanner", 0, 1, 0, 0xFF, 0);883 m_agg_b->AddScanner("AfterDeathScanner", 0, 1, 0, 0xFF, 0);
@@ -843,6 +889,50 @@ TEST_F(AggregateWindowRegressionTest, follower_degrades_after_master_destroyed)
843 EXPECT_EQ(m_chip_b->read_records().size(), 1u);889 EXPECT_EQ(m_chip_b->read_records().size(), 1u);
844}890}
845 891 
892+TEST_F(AggregateWindowRegressionTest, host_guard_refuses_owner_during_final_release)
893+{
894+ auto gate = std::make_shared<dev::csr1::test::AggregateHostDestructionGate>();
895+ auto host = mc::make_shared<dev::csr1::test::BlockingAggregateHostObject>();
896+ host->gate = gate;
897+ host->set_object_name("FinalReleaseHost");
898+ 
899+ m_chip_a = std::make_shared<dev::csr1::test::windowed_mock_chip>("Chip_FPGA_1_01010315", 0x70);
900+ auto* aggregate = &host->aggregate;
901+ aggregate->m_chip = m_chip_a.get();
902+ aggregate->set_owner_object(host.get());
903+ m_service->register_object(host);
904+ aggregate->init(336, 16, 60000);
905+ aggregate->AddScanner("FinalReleaseScanner", 4, 2, 0, 0xFFFF, 0);
906+ aggregate->start();
907+ 
908+ m_service->unregister_object(std::string(host->get_object_path()));
909+ std::thread destroyer([host = std::move(host)]() mutable {
910+ host.reset();
911+ });
912+ 
913+ bool destructor_entered = false;
914+ {
915+ std::unique_lock<std::mutex> lock(gate->mutex);
916+ destructor_entered = gate->cv.wait_for(lock, std::chrono::seconds(5), [&gate] {
917+ return gate->entered;
918+ });
919+ }
920+ 
921+ if (destructor_entered) {
922+ // The raw owner still points at live storage, but its strong count is
923+ // already zero. Retention must use a non-throwing weak upgrade.
924+ EXPECT_EQ(aggregate->host_guard(), nullptr);
925+ }
926+ 
927+ {
928+ std::lock_guard<std::mutex> lock(gate->mutex);
929+ gate->release = true;
930+ }
931+ gate->cv.notify_all();
932+ destroyer.join();
933+ ASSERT_TRUE(destructor_entered);
934+}
935+ 
846// ===== 8.6 读通道:Aggregate 共享读与 Scanner 独立读共用总线互斥锁 =====936// ===== 8.6 读通道:Aggregate 共享读与 Scanner 独立读共用总线互斥锁 =====
847 937 
848TEST_F(AggregateWindowRegressionTest, aggregate_tick_holds_bus_lock_during_read_and_releases)938TEST_F(AggregateWindowRegressionTest, aggregate_tick_holds_bus_lock_during_read_and_releases)
@@ -936,11 +1026,13 @@ TEST_F(AggregateWindowRegressionTest, master_destroyed_existing_scanners_migrate
936 EXPECT_EQ(m_agg_b->dispatcher()->entry_count(), 0u);1026 EXPECT_EQ(m_agg_b->dispatcher()->entry_count(), 0u);
937 1027 
938 // master 宿主析构(模拟芯片卸载)1028 // master 宿主析构(模拟芯片卸载)
1029+ mc::weak_ptr<dev::csr1::test::AggregateHostObject> master_host = m_hosts.front();
939 {1030 {
940 auto host_a = m_hosts.front();1031 auto host_a = m_hosts.front();
941 m_service->unregister_object(std::string(host_a->get_object_path()));1032 m_service->unregister_object(std::string(host_a->get_object_path()));
942 m_hosts.erase(m_hosts.begin());1033 m_hosts.erase(m_hosts.begin());
943 }1034 }
1035+ ASSERT_TRUE(wait_for_host_release(master_host));
944 1036 
945 // follower 继续 tick:已转发的 entry 迁移回自身 dispatcher,原 Scanner 仍被供数1037 // follower 继续 tick:已转发的 entry 迁移回自身 dispatcher,原 Scanner 仍被供数
946 m_agg_b->start();1038 m_agg_b->start();
@@ -28,7 +28,22 @@
28#include <string>28#include <string>
29#include <vector>29#include <vector>
30 30 
31-using dev::test::Csr1ChipObjectTestBase;31+// Only the complex chip owns aggregate scheduler tasks. Keep this cleanup in
32+// its fixture so other chip tests need not link the complex scheduler library.
33+class Csr1ChipObjectTestBase : public dev::test::Csr1ChipObjectTestBase {
34+protected:
35+ static void TearDownTestSuite()
36+ {
37+ dev::csr1::Scheduler::get_instance().clear();
38+ dev::test::Csr1ChipObjectTestBase::TearDownTestSuite();
39+ }
40+ 
41+ void TearDown() override
42+ {
43+ dev::csr1::Scheduler::get_instance().clear();
44+ dev::test::Csr1ChipObjectTestBase::TearDown();
45+ }
46+};
32 47 
33class mock_chip_bus : public dev::bus_base {48class mock_chip_bus : public dev::bus_base {
34public:49public:
@@ -33,6 +33,7 @@
33#include <gtest/gtest.h>33#include <gtest/gtest.h>
34 34 
35#include <chrono>35#include <chrono>
36+#include <future>
36#include <memory>37#include <memory>
37#include <string>38#include <string>
38#include <thread>39#include <thread>
@@ -255,4 +256,129 @@ TEST_F(ChipWatchdogTest, real_watchdog_timer_pauses_and_resumes_within_poll_wind
255 EXPECT_TRUE(events->at(1));256 EXPECT_TRUE(events->at(1));
256}257}
257 258 
259+TEST_F(ChipWatchdogTest, cancelled_and_replaced_access_timers_do_not_retain_interface_parent)
260+{
261+ auto chip = make_chip("timer_access_cancel");
262+ auto& iface = chip->m_interface_chip;
263+ iface.SetAccessibility(false, 1800);
264+ auto first = iface.m_access_state->access_timer;
265+ ASSERT_NE(first, nullptr);
266+ iface.SetAccessibility(false, 1800);
267+ auto second = iface.m_access_state->access_timer;
268+ EXPECT_NE(first, second);
269+ EXPECT_FALSE(first->is_active());
270+ EXPECT_EQ(first->get_parent(), nullptr);
271+ iface.SetAccessibility(true, 0);
272+ EXPECT_FALSE(second->is_active());
273+ EXPECT_EQ(second->get_parent(), nullptr);
274+ chip.reset();
275+}
276+ 
277+TEST_F(ChipWatchdogTest, completed_access_timer_has_no_interface_parent)
278+{
279+ auto chip = make_chip("timer_access_complete");
280+ auto& iface = chip->m_interface_chip;
281+ iface.SetAccessibility(false, 1800);
282+ auto timer = iface.m_access_state->access_timer;
283+ ASSERT_NE(timer, nullptr);
284+ timer->stop();
285+ timer->timeout();
286+ EXPECT_TRUE(iface.m_access_state->access_enabled.load());
287+ EXPECT_EQ(timer->get_parent(), nullptr);
288+ chip.reset();
289+}
290+ 
291+TEST_F(ChipWatchdogTest, destroyed_interface_stops_retained_watchdog)
292+{
293+ auto chip = make_chip("timer_watchdog_destroy");
294+ auto timer = chip->m_interface_chip.m_access_watchdog;
295+ ASSERT_NE(timer, nullptr);
296+ chip.reset();
297+ EXPECT_FALSE(timer->is_active());
298+ EXPECT_EQ(timer->get_parent(), nullptr);
299+ timer->stop();
300+}
301+ 
302+TEST_F(ChipWatchdogTest, access_and_lock_timers_are_safe_to_release_after_interface_destruction)
303+{
304+ auto chip = make_chip("timer_access_lock_destroy");
305+ auto bus = std::make_shared<test_watchdog_bus>();
306+ attach_bus(*chip, bus);
307+ auto& iface = chip->m_interface_chip;
308+ iface.SetAccessibility(false, 1800);
309+ auto access_timer = iface.m_access_state->access_timer;
310+ mc::engine::context context;
311+ context.set_arg("Requestor", std::string("timer_test"));
312+ ASSERT_EQ(iface.SetLockStatus(context, 1, 1800), 0);
313+ auto lock_timer = bus->get_bus_lock_info()->lock_timer;
314+ // Retain timers past interface destruction, as queued work can do.
315+ EXPECT_EQ(access_timer->get_parent(), nullptr);
316+ EXPECT_EQ(lock_timer->get_parent(), nullptr);
317+ chip.reset();
318+ EXPECT_FALSE(access_timer->is_active());
319+ EXPECT_FALSE(lock_timer->is_active());
320+ EXPECT_EQ(bus->get_bus_lock_info(), nullptr);
321+}
322+ 
323+TEST_F(ChipWatchdogTest, renewed_unlocked_and_completed_lock_timers_have_no_interface_parent)
324+{
325+ auto chip = make_chip("timer_lock_replace");
326+ auto bus = std::make_shared<test_watchdog_bus>();
327+ attach_bus(*chip, bus);
328+ auto& iface = chip->m_interface_chip;
329+ mc::engine::context context;
330+ context.set_arg("Requestor", std::string("timer_test"));
331+ ASSERT_EQ(iface.SetLockStatus(context, 1, 1800), 0);
332+ auto first = bus->get_bus_lock_info()->lock_timer;
333+ ASSERT_EQ(iface.SetLockStatus(context, 1, 1800), 0);
334+ auto second = bus->get_bus_lock_info()->lock_timer;
335+ EXPECT_NE(first, second);
336+ EXPECT_FALSE(first->is_active());
337+ EXPECT_EQ(first->get_parent(), nullptr);
338+ ASSERT_EQ(iface.SetLockStatus(context, 0, 0), 0);
339+ EXPECT_FALSE(second->is_active());
340+ EXPECT_EQ(second->get_parent(), nullptr);
341+ 
342+ ASSERT_EQ(iface.SetLockStatus(context, 1, 1800), 0);
343+ auto completed = bus->get_bus_lock_info()->lock_timer;
344+ completed->stop();
345+ completed->timeout();
346+ EXPECT_EQ(bus->get_bus_lock_info(), nullptr);
347+ EXPECT_EQ(iface.LockStatus.get_value().as<uint8_t>(), 0);
348+ EXPECT_EQ(completed->get_parent(), nullptr);
349+ chip.reset();
350+}
351+ 
352+TEST_F(ChipWatchdogTest, interface_timers_use_owner_executor)
353+{
354+ auto chip = make_chip("timer_owner_executor");
355+ auto bus = std::make_shared<test_watchdog_bus>();
356+ attach_bus(*chip, bus);
357+ auto& iface = chip->m_interface_chip;
358+ // Recreate the watchdog after assigning the executor, matching production
359+ // initialization where the owner is attached before its interface starts.
360+ iface.m_access_watchdog->stop();
361+ iface.m_access_watchdog.reset();
362+ mc::any_executor owner_executor{mc::runtime::runtime_strand{}};
363+ chip->bind_executor(owner_executor);
364+ iface.start_access_watchdog();
365+ iface.SetAccessibility(false, 1800);
366+ mc::engine::context context;
367+ context.set_arg("Requestor", std::string("timer_test"));
368+ ASSERT_EQ(iface.SetLockStatus(context, 1, 1800), 0);
369+ auto lock_timer = bus->get_bus_lock_info()->lock_timer;
370+ for (auto timer : {iface.m_access_watchdog, iface.m_access_state->access_timer, lock_timer}) {
371+ auto result = std::make_shared<std::promise<bool>>();
372+ auto ready = result->get_future();
373+ timer->timeout.connect([owner_executor, result]() {
374+ result->set_value(owner_executor.running_in_this_thread());
375+ });
376+ timer->set_single_shot(true);
377+ timer->start(mc::milliseconds(1));
378+ ASSERT_EQ(ready.wait_for(std::chrono::seconds(2)), std::future_status::ready);
379+ EXPECT_TRUE(ready.get());
380+ timer->stop();
381+ }
382+}
383+ 
258} // namespace384} // namespace
@@ -1583,6 +1583,45 @@ TEST_F(Csr1ScannerTest, dfx_value_is_not_fanned_out_to_merge_members)
1583 drain_io_queue();1583 drain_io_queue();
1584}1584}
1585 1585 
1586+TEST_F(Csr1ScannerTest, stopped_timers_do_not_retain_embedded_interface_parent)
1587+{
1588+ auto csr = create_valid_scanner_csr();
1589+ ASSERT_TRUE(m_scanner->init(csr, mc::dict{}));
1590+ ASSERT_TRUE(m_scanner->start());
1591+ 
1592+ // A queued cancellation completion can retain either timer beyond stop().
1593+ auto startup_timer = m_scanner->i_scanner.m_startup_delay_timer;
1594+ auto scan_timer = m_scanner->i_scanner.m_scanner_timer;
1595+ ASSERT_NE(startup_timer, nullptr);
1596+ ASSERT_NE(scan_timer, nullptr);
1597+ ASSERT_TRUE(m_scanner->stop());
1598+ 
1599+ EXPECT_EQ(startup_timer->get_parent(), nullptr);
1600+ EXPECT_EQ(scan_timer->get_parent(), nullptr);
1601+ m_scanner.reset();
1602+ startup_timer.reset();
1603+ scan_timer.reset();
1604+}
1605+ 
1606+TEST_F(Csr1ScannerTest, replaced_timers_do_not_retain_embedded_interface_parent)
1607+{
1608+ auto csr = create_valid_scanner_csr();
1609+ ASSERT_TRUE(m_scanner->init(csr, mc::dict{}));
1610+ ASSERT_TRUE(m_scanner->start());
1611+ auto startup_timer = m_scanner->i_scanner.m_startup_delay_timer;
1612+ auto scan_timer = m_scanner->i_scanner.m_scanner_timer;
1613+ ASSERT_NE(startup_timer, nullptr);
1614+ ASSERT_NE(scan_timer, nullptr);
1615+ 
1616+ m_scanner->i_scanner.init_startup_delay_timer();
1617+ ASSERT_TRUE(m_scanner->start());
1618+ 
1619+ EXPECT_EQ(startup_timer->get_parent(), nullptr);
1620+ EXPECT_EQ(scan_timer->get_parent(), nullptr);
1621+ EXPECT_NE(m_scanner->i_scanner.m_startup_delay_timer, startup_timer);
1622+ EXPECT_NE(m_scanner->i_scanner.m_scanner_timer, scan_timer);
1623+}
1624+ 
1586TEST_F(ScannerMergeRegistryTest, unregister_master_with_no_followers_erases_group)1625TEST_F(ScannerMergeRegistryTest, unregister_master_with_no_followers_erases_group)
1587{1626{
1588 auto& reg = dev::csr1::ScannerMergeRegistry::instance();1627 auto& reg = dev::csr1::ScannerMergeRegistry::instance();
@@ -1732,7 +1732,7 @@ TEST_F(Csr1SmcDfxInfoTest, owner_guard_keeps_owner_alive_across_concurrent_relea
1732// 回归(真实 due 快照路径):经 Scheduler::on_timer 的完整 Phase1/Phase2 执行,1732// 回归(真实 due 快照路径):经 Scheduler::on_timer 的完整 Phase1/Phase2 执行,
1733// tick 内(mock BlockIO.Read 注入点,等价于同步调用递归 poll runtime 的重入窗口)1733// tick 内(mock BlockIO.Read 注入点,等价于同步调用递归 poll runtime 的重入窗口)
1734// 对象被注销且本地强引用释放,due 快照的 owner_guard 保活整个批次;1734// 对象被注销且本地强引用释放,due 快照的 owner_guard 保活整个批次;
1735-// on_timer 返回(快照销毁)后对象才析构。1735+// on_timer 只投递任务;异步硬件访问完成后才允许对象析构。
1736TEST_F(Csr1SmcDfxInfoTest, scheduler_due_snapshot_keeps_owner_alive_during_tick)1736TEST_F(Csr1SmcDfxInfoTest, scheduler_due_snapshot_keeps_owner_alive_during_tick)
1737{1737{
1738 auto obj = mc::make_shared<dev::csr1::SmcDfxInfo_object>();1738 auto obj = mc::make_shared<dev::csr1::SmcDfxInfo_object>();
@@ -1746,8 +1746,9 @@ TEST_F(Csr1SmcDfxInfoTest, scheduler_due_snapshot_keeps_owner_alive_during_tick)
1746 1746 
1747 mc::weak_ptr<dev::csr1::SmcDfxInfo_object> tracked = obj;1747 mc::weak_ptr<dev::csr1::SmcDfxInfo_object> tracked = obj;
1748 1748 
1749- auto& sched = dev::csr1::Scheduler::get_instance();1749+ auto& sched = dev::csr1::Scheduler::get_instance();
1750- auto* disp = &obj->get_dispatcher();1750+ auto* disp = &obj->get_dispatcher();
1751+ std::shared_ptr<dev::csr1::Scheduler::RunState> run_state;
1751 1752 
1752 // tick 内注入"卸载重入":注销(teardown 清理)+ 释放本地强引用,1753 // tick 内注入"卸载重入":注销(teardown 清理)+ 释放本地强引用,
1753 // 模拟递归 poll runtime 中卸载流程释放对象的全部非快照引用。1754 // 模拟递归 poll runtime 中卸载流程释放对象的全部非快照引用。
@@ -1767,12 +1768,24 @@ TEST_F(Csr1SmcDfxInfoTest, scheduler_due_snapshot_keeps_owner_alive_during_tick)
1767 for (auto& e : sched.m_entries) {1768 for (auto& e : sched.m_entries) {
1768 if (e.task == disp) {1769 if (e.task == disp) {
1769 e.next_run_ms = 0;1770 e.next_run_ms = 0;
1771+ run_state = e.run_state;
1770 }1772 }
1771 }1773 }
1772 }1774 }
1775+ ASSERT_NE(run_state, nullptr);
1773 EXPECT_NO_THROW(sched.on_timer());1776 EXPECT_NO_THROW(sched.on_timer());
1774 1777 
1775- // 批次结束(快照销毁)后对象析构1778+ // 等待实际任务完成,不能把 on_timer 返回当作硬件访问已经结束。
1779+ {
1780+ std::unique_lock<std::mutex> lock(run_state->mutex);
1781+ ASSERT_TRUE(run_state->cv.wait_for(lock, std::chrono::seconds(5), [&] {
1782+ return !run_state->running.load();
1783+ }));
1784+ }
1785+ const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
1786+ while (!tracked.expired() && std::chrono::steady_clock::now() < deadline) {
1787+ std::this_thread::yield();
1788+ }
1776 EXPECT_TRUE(tracked.expired());1789 EXPECT_TRUE(tracked.expired());
1777 1790 
1778 m_chip->blockio.on_read = nullptr;1791 m_chip->blockio.on_read = nullptr;
@@ -386,4 +386,30 @@ TEST_F(BusCanTest, extend_read_invalid_read_length_throws)
386 }386 }
387}387}
388 388 
389+TEST_F(BusCanTest, memory_safety_write_rejects_oversized_source_length)
390+{
391+ mc::mutable_dict csr;
392+ ASSERT_TRUE(m_bus.init(csr));
393+ for (uint32_t length : {2U, 4U, 255U}) {
394+ SCOPED_TRACE(length);
395+ ACCESS_DATA_S data{};
396+ data.in_buffer = {0x5a};
397+ data.length = length;
398+ EXPECT_THROW(m_bus.write(data), mc::invalid_op_exception);
399+ EXPECT_EQ(DRIVER_CANBUS::test_state().last_write_len, 0U);
400+ DRIVER_CANBUS::reset_test_state();
401+ }
402+}
403+ 
404+TEST_F(BusCanTest, memory_safety_write_preserves_explicit_prefix_length)
405+{
406+ mc::mutable_dict csr;
407+ ASSERT_TRUE(m_bus.init(csr));
408+ ACCESS_DATA_S data{};
409+ data.in_buffer = {0x34, 0x56};
410+ data.length = 1;
411+ EXPECT_EQ(m_bus.write(data), HAS_OK);
412+ EXPECT_EQ(DRIVER_CANBUS::test_state().last_write_len, 1U);
413+}
414+ 
389} // namespace415} // namespace
@@ -619,4 +619,49 @@ TEST_F(bus_i2c_test, init_skips_iocfg_when_multiio_missing)
619 EXPECT_EQ(DRIVER_COMM::comm_test_state().iocfg_config_count, 0);619 EXPECT_EQ(DRIVER_COMM::comm_test_state().iocfg_config_count, 0);
620}620}
621 621 
622+// The transmit source length is independent of the requested reply length.
623+TEST_F(bus_i2c_test, memory_safety_read_uses_actual_input_size)
624+{
625+ ACCESS_DATA_S data{};
626+ data.addr = 0x70;
627+ data.offset_width = 1;
628+ data.offset = 0x12;
629+ data.in_buffer = {0x34, 0x56};
630+ for (uint32_t read_length : {0U, 1U, 2U, 4U, 255U}) {
631+ SCOPED_TRACE(read_length);
632+ data.length = read_length;
633+ EXPECT_EQ(std::string(m_bus.format_tx_buf(data)), std::string("\x70\x12\x34\x56", 4));
634+ }
635+ data.in_buffer.clear();
636+ EXPECT_EQ(std::string(m_bus.format_tx_buf(data)), std::string("\x70\x12", 2));
637+}
638+ 
639+TEST_F(bus_i2c_test, memory_safety_write_rejects_oversized_source_length)
640+{
641+ mc::mutable_dict csr;
642+ ASSERT_TRUE(m_bus.init(csr));
643+ for (uint32_t length : {2U, 4U, 255U}) {
644+ SCOPED_TRACE(length);
645+ ACCESS_DATA_S data{};
646+ data.addr = 0x70;
647+ data.length = length;
648+ data.in_buffer = {0x5a};
649+ EXPECT_THROW(m_bus.write(data), mc::invalid_arg_exception);
650+ }
651+}
652+ 
653+TEST_F(bus_i2c_test, memory_safety_write_preserves_explicit_prefix_length)
654+{
655+ mc::mutable_dict csr;
656+ ASSERT_TRUE(m_bus.init(csr));
657+ ACCESS_DATA_S data{};
658+ data.addr = 0x70;
659+ data.offset_width = 1;
660+ data.offset = 0x12;
661+ data.length = 1;
662+ data.in_buffer = {0x34, 0x56};
663+ ASSERT_EQ(m_bus.write(data), HAS_OK);
664+ EXPECT_EQ(m_bus.m_tx_buffer, std::string("\x70\x12\x34", 3));
665+}
666+ 
622} // namespace667} // namespace
@@ -895,6 +895,20 @@ TEST_F(chip_canbus_chip_can_bus_test, bit_read_wrong_bus_type_returns_err)
895// bit_write (offset: bits 31-16 = cmd, bits 15-0 = sig_id)895// bit_write (offset: bits 31-16 = cmd, bits 15-0 = sig_id)
896// ===========================================================================896// ===========================================================================
897 897 
898+TEST_F(chip_canbus_chip_can_bus_test, memory_safety_bit_write_rejects_short_input)
899+{
900+ auto bus = attach_can_bus();
901+ for (uint8_t length : {1, 2, 4, 6}) {
902+ for (size_t size = 0; size < length; ++size) {
903+ SCOPED_TRACE(::testing::Message() << +length << "/" << size);
904+ std::vector<uint8_t> input(size, 0x5a), output;
905+ std::string error;
906+ EXPECT_EQ(m_chip->bit_write(0x00011234, length, 0xff, input, output, error), HAS_ERR);
907+ EXPECT_FALSE(error.empty());
908+ }
909+ }
910+}
911+ 
898TEST_F(chip_canbus_chip_can_bus_test, bit_write_length2_success)912TEST_F(chip_canbus_chip_can_bus_test, bit_write_length2_success)
899{913{
900 auto bus = attach_can_bus();914 auto bus = attach_can_bus();
@@ -1067,4 +1067,60 @@ TEST_F(chip_topo_path_test, topo_path_different_addr_are_different)
1067 EXPECT_NE(m_chip1->get_topo_path(), m_chip3->get_topo_path());1067 EXPECT_NE(m_chip1->get_topo_path(), m_chip3->get_topo_path());
1068}1068}
1069 1069 
1070+TEST_F(chip_base_test, memory_safety_bit_read_rejects_every_short_reply)
1071+{
1072+ m_chip->set_left_bus(m_bus);
1073+ m_chip->set_host_bus(m_bus);
1074+ for (uint8_t width : {1, 2, 4}) {
1075+ for (size_t received = 0; received < width; ++received) {
1076+ for (uint32_t mask : {0U, 0xffffffffU}) {
1077+ SCOPED_TRACE(::testing::Message() << +width << "/" << received << "/" << mask);
1078+ m_bus->add_read_payload(std::vector<uint8_t>(received, 0x5a));
1079+ std::vector<uint8_t> input;
1080+ // A preallocated caller buffer must not disguise an empty reply.
1081+ std::vector<uint8_t> output(width, 0xff);
1082+ std::string error;
1083+ EXPECT_EQ(m_chip->bit_read(0x10, width, mask, input, output, error), HAS_ERR);
1084+ EXPECT_FALSE(error.empty());
1085+ }
1086+ }
1087+ }
1088+}
1089+ 
1090+TEST_F(chip_base_test, memory_safety_shifted_bus_preserves_value_and_rejects_short_reply)
1091+{
1092+ m_chip->set_left_bus(m_bus);
1093+ m_chip->set_host_bus(m_bus);
1094+ m_bus->has_shifted_bit_read = true;
1095+ std::vector<uint8_t> input, output;
1096+ std::string error;
1097+ 
1098+ // LocalBus has already extracted (register & 0xf0) >> 4.
1099+ m_bus->add_read_payload({3, 0, 0, 0});
1100+ ASSERT_EQ(m_chip->bit_read(0x10, 4, 0xf0, input, output, error), HAS_OK);
1101+ EXPECT_EQ(output, (std::vector<uint8_t>{3, 0, 0, 0}));
1102+ 
1103+ // A shifted response must still contain the requested number of bytes.
1104+ m_bus->add_read_payload({3});
1105+ EXPECT_EQ(m_chip->bit_read(0x10, 4, 0xf0, input, output, error), HAS_ERR);
1106+ EXPECT_TRUE(output.empty());
1107+ EXPECT_FALSE(error.empty());
1108+}
1109+ 
1110+TEST_F(chip_base_test, memory_safety_bit_write_rejects_short_input_before_bus)
1111+{
1112+ m_chip->set_left_bus(m_bus);
1113+ m_chip->set_host_bus(m_bus);
1114+ for (uint8_t width : {1, 2, 4}) {
1115+ for (size_t provided = 0; provided < width; ++provided) {
1116+ SCOPED_TRACE(::testing::Message() << +width << "/" << provided);
1117+ std::vector<uint8_t> input(provided, 0x5a), output;
1118+ std::string error;
1119+ EXPECT_EQ(m_chip->bit_write(0x10, width, 0xffffffffU, input, output, error), HAS_ERR);
1120+ EXPECT_FALSE(error.empty());
1121+ EXPECT_EQ(m_bus->m_write_call_count, 0);
1122+ }
1123+ }
1124+}
1125+ 
1070} // namespace1126} // namespace
@@ -320,12 +320,13 @@ static std::vector<uint8_t> smc_read_frame(uint8_t addr, uint8_t cmd, const std:
320class chip_smc_exposed : public dev::chip_smc {320class chip_smc_exposed : public dev::chip_smc {
321public:321public:
322 using dev::chip_smc::forward_by_front_chip;322 using dev::chip_smc::forward_by_front_chip;
323+ using dev::chip_smc::forward_read;
323};324};
324 325 
325-static std::shared_ptr<smc_test_bus> init_exposed_chip(chip_smc_exposed& chip)326+static std::shared_ptr<smc_test_bus> init_exposed_chip(chip_smc_exposed& chip, uint8_t address = 0x10)
326{327{
327 mc::mutable_dict csr;328 mc::mutable_dict csr;
328- csr["Address"] = static_cast<uint32_t>(0x10);329+ csr["Address"] = static_cast<uint32_t>(address);
329 csr["OffsetWidth"] = static_cast<uint8_t>(1);330 csr["OffsetWidth"] = static_cast<uint8_t>(1);
330 csr["AddrWidth"] = static_cast<uint8_t>(1);331 csr["AddrWidth"] = static_cast<uint8_t>(1);
331 csr["object_name"] = std::string("smc_test");332 csr["object_name"] = std::string("smc_test");
@@ -408,6 +409,32 @@ TEST_F(chip_smc_test, bit_read_para_overflow_error)
408 EXPECT_NE(error.find("para"), std::string::npos);409 EXPECT_NE(error.find("para"), std::string::npos);
409}410}
410 411 
412+TEST_F(chip_smc_test, bit_read_rejects_zero_response_length_before_offset)
413+{
414+ auto bus = bind_test_bus();
415+ bus->enqueue_read_payload({0x00, 0x00});
416+ 
417+ std::vector<uint8_t> in, out;
418+ std::string error;
419+ EXPECT_EQ(m_chip.bit_read(0x21FF, 1, 0xFF, in, out, error), HAS_ERR);
420+ EXPECT_NE(error.find("length"), std::string::npos);
421+ EXPECT_TRUE(out.empty());
422+}
423+ 
424+TEST_F(chip_smc_test, bit_read_rejects_truncated_response)
425+{
426+ auto bus = bind_test_bus();
427+ // The old parser padded the missing data/CRC with zero; that padded
428+ // frame has CRC8 == 0 and was accepted despite missing received bytes.
429+ bus->enqueue_read_payload({0x03, 0x00, 0x1C});
430+ 
431+ std::vector<uint8_t> in, out;
432+ std::string error;
433+ EXPECT_EQ(m_chip.bit_read(0x2100, 1, 0xFF, in, out, error), HAS_ERR);
434+ EXPECT_NE(error.find("length"), std::string::npos);
435+ EXPECT_TRUE(out.empty());
436+}
437+ 
411TEST_F(chip_smc_test, bit_read_uint16)438TEST_F(chip_smc_test, bit_read_uint16)
412{439{
413 auto bus = bind_test_bus();440 auto bus = bind_test_bus();
@@ -430,6 +457,21 @@ TEST_F(chip_smc_test, bit_read_uint32)
430 EXPECT_EQ(out, (std::vector<uint8_t>{0x78, 0x56, 0x34, 0x12}));457 EXPECT_EQ(out, (std::vector<uint8_t>{0x78, 0x56, 0x34, 0x12}));
431}458}
432 459 
460+TEST_F(chip_smc_test, bit_read_accepts_last_byte_of_maximum_payload)
461+{
462+ auto bus = bind_test_bus();
463+ std::vector<uint8_t> payload(254, 0x00);
464+ payload.back() = 0xA5;
465+ auto frame = smc_read_frame(0x10, 0x21, payload);
466+ bus->enqueue_read_payload(frame);
467+ bus->enqueue_read_payload(std::move(frame));
468+ 
469+ std::vector<uint8_t> in, out;
470+ std::string error;
471+ EXPECT_EQ(m_chip.bit_read(0x21FE, 1, 0xFF, in, out, error), HAS_OK);
472+ EXPECT_EQ(out, (std::vector<uint8_t>{0xA5}));
473+}
474+ 
433// ---- bit_write:读-改-写分支 ----475// ---- bit_write:读-改-写分支 ----
434 476 
435TEST_F(chip_smc_test, bit_write_rejects_invalid_lengths)477TEST_F(chip_smc_test, bit_write_rejects_invalid_lengths)
@@ -442,6 +484,60 @@ TEST_F(chip_smc_test, bit_write_rejects_invalid_lengths)
442 }484 }
443}485}
444 486 
487+TEST_F(chip_smc_test, bit_write_rejects_short_input_before_bus_access)
488+{
489+ auto bus = bind_test_bus();
490+ for (uint8_t width : {1, 2, 4}) {
491+ for (size_t supplied = 0; supplied < width; ++supplied) {
492+ for (uint32_t mask : {0x0FU, 0xFFFFFFFFU}) {
493+ SCOPED_TRACE(::testing::Message() << +width << "/" << supplied << "/" << mask);
494+ std::vector<uint8_t> in(supplied, 0x12), out;
495+ std::string error;
496+ EXPECT_EQ(m_chip.bit_write(0x2100, width, mask, in, out, error), HAS_ERR);
497+ EXPECT_NE(error.find("input"), std::string::npos);
498+ }
499+ }
500+ }
501+ EXPECT_EQ(bus->write_call_count(), 0);
502+ EXPECT_EQ(bus->read_call_count(), 0);
503+}
504+ 
505+TEST_F(chip_smc_test, bit_write_full_uint32_mask_writes_without_read)
506+{
507+ auto bus = bind_test_bus();
508+ std::vector<uint8_t> in = {0x78, 0x56, 0x34, 0x12};
509+ std::vector<uint8_t> out;
510+ std::string error;
511+ EXPECT_EQ(m_chip.bit_write(0x2100, 4, 0xFFFFFFFF, in, out, error), HAS_OK);
512+ EXPECT_EQ(bus->read_call_count(), 0);
513+ ASSERT_GE(bus->last_write_buffer().size(), 9U);
514+ const auto& frame = bus->last_write_buffer();
515+ EXPECT_EQ((std::vector<uint8_t>(frame.end() - 5, frame.end() - 1)), in);
516+}
517+ 
518+TEST_F(chip_smc_test, bit_write_rejects_zero_response_length_before_offset)
519+{
520+ auto bus = bind_test_bus();
521+ bus->enqueue_read_payload({0x00, 0x00});
522+ 
523+ std::vector<uint8_t> in = {0x12};
524+ std::vector<uint8_t> out;
525+ std::string error;
526+ EXPECT_EQ(m_chip.bit_write(0x21FF, 1, 0x0F, in, out, error), HAS_ERR);
527+ EXPECT_NE(error.find("length"), std::string::npos);
528+}
529+ 
530+TEST_F(chip_smc_test, bit_write_rejects_truncated_response_with_valid_padded_crc)
531+{
532+ auto bus = bind_test_bus();
533+ bus->enqueue_read_payload({0x03, 0x00, 0x1C});
534+ std::vector<uint8_t> input = {0x12}, output;
535+ std::string error;
536+ EXPECT_EQ(m_chip.bit_write(0x2100, 1, 0x0F, input, output, error), HAS_ERR);
537+ // One command starts the read; no register write may follow the short reply.
538+ EXPECT_EQ(bus->write_call_count(), 1);
539+}
540+ 
445TEST_F(chip_smc_test, bit_write_read_modify_write_byte)541TEST_F(chip_smc_test, bit_write_read_modify_write_byte)
446{542{
447 auto bus = bind_test_bus();543 auto bus = bind_test_bus();
@@ -816,6 +912,106 @@ TEST(chip_smc_forward, forward_by_front_chip_smc_write)
816 EXPECT_EQ(chip.forward_by_front_chip(d, "smc", 1), HAS_OK);912 EXPECT_EQ(chip.forward_by_front_chip(d, "smc", 1), HAS_OK);
817}913}
818 914 
915+TEST(chip_smc_forward, forward_by_front_chip_rejects_length_larger_than_input)
916+{
917+ chip_smc_exposed chip;
918+ auto bus = init_exposed_chip(chip);
919+ 
920+ ACCESS_DATA_S d{};
921+ d.optype = CHIP_OP_TYPE_WRITE;
922+ d.addr = 0x60;
923+ d.offset = 0x20;
924+ d.length = 4;
925+ d.offset_width = 1;
926+ d.in_buffer = {0x11};
927+ 
928+ EXPECT_EQ(chip.forward_by_front_chip(d, "smc", 1), HAS_ERR);
929+ EXPECT_EQ(bus->write_call_count(), 0);
930+}
931+ 
932+TEST(chip_smc_forward, forward_by_front_chip_rejects_oversized_offset_width)
933+{
934+ chip_smc_exposed chip;
935+ auto bus = init_exposed_chip(chip);
936+ 
937+ ACCESS_DATA_S d{};
938+ d.optype = CHIP_OP_TYPE_WRITE;
939+ d.addr = 0x60;
940+ d.offset = 0x20;
941+ d.length = 1;
942+ d.in_buffer = {0x11};
943+ for (uint8_t width : {5, 251, 255}) {
944+ for (auto operation : {CHIP_OP_TYPE_READ, CHIP_OP_TYPE_WRITE}) {
945+ SCOPED_TRACE(::testing::Message() << +width << "/" << operation);
946+ d.offset_width = width;
947+ d.optype = operation;
948+ EXPECT_EQ(chip.forward_by_front_chip(d, "eeprom", 1), HAS_ERR);
949+ }
950+ }
951+ EXPECT_EQ(bus->write_call_count(), 0);
952+ EXPECT_EQ(bus->read_call_count(), 0);
953+}
954+ 
955+TEST(chip_smc_forward, forward_write_capacity_boundary_with_offset)
956+{
957+ for (uint8_t width : {0, 1, 4}) {
958+ for (uint32_t extra : {0U, 1U}) {
959+ SCOPED_TRACE(::testing::Message() << +width << "/" << extra);
960+ chip_smc_exposed chip;
961+ auto bus = init_exposed_chip(chip);
962+ ACCESS_DATA_S d{};
963+ d.optype = CHIP_OP_TYPE_WRITE;
964+ d.addr = 0x60;
965+ d.offset = 0x12345678;
966+ d.offset_width = width;
967+ d.length = 250 - width + extra;
968+ d.in_buffer.assign(d.length, 0xAB);
969+ EXPECT_EQ(chip.forward_by_front_chip(d, "smc", 1), extra ? HAS_ERR : HAS_OK);
970+ EXPECT_EQ(bus->write_call_count(), extra ? 0 : 1);
971+ }
972+ }
973+}
974+ 
975+TEST(chip_smc_forward, forward_read_rejects_zero_response_length)
976+{
977+ chip_smc_exposed chip;
978+ auto bus = init_exposed_chip(chip, 0x88);
979+ // CRC8([0x88, 0x31, 0x89, 0x00]) is zero, so the legacy parser accepts
980+ // this malformed frame and exposes the length subtraction bug.
981+ bus->enqueue_read_payload({0x00, 0x00});
982+ 
983+ std::vector<uint8_t> out;
984+ EXPECT_EQ(chip.forward_read(1, out), HAS_ERR);
985+ EXPECT_TRUE(out.empty());
986+}
987+ 
988+TEST(chip_smc_forward, forward_read_rejects_truncated_response)
989+{
990+ chip_smc_exposed chip;
991+ auto bus = init_exposed_chip(chip);
992+ bus->enqueue_read_payload({0x03, 0x00, 0xAB});
993+ 
994+ std::vector<uint8_t> out;
995+ EXPECT_EQ(chip.forward_read(1, out), HAS_ERR);
996+ EXPECT_TRUE(out.empty());
997+}
998+ 
999+TEST(chip_smc_forward, forward_read_accepts_maximum_payload)
1000+{
1001+ chip_smc_exposed chip;
1002+ auto bus = init_exposed_chip(chip);
1003+ std::vector<uint8_t> payload(254, 0x00);
1004+ payload.front() = 0x11;
1005+ payload.back() = 0xEE;
1006+ auto frame = smc_read_frame(0x10, 0x31, payload);
1007+ bus->enqueue_read_payload(frame);
1008+ bus->enqueue_read_payload(std::move(frame));
1009+ 
1010+ std::vector<uint8_t> out;
1011+ EXPECT_EQ(chip.forward_read(255, out), HAS_OK);
1012+ EXPECT_EQ(out, payload);
1013+}
1014+ 
819TEST(chip_smc_forward, forward_by_front_chip_smc_write_memcpy_fail)1015TEST(chip_smc_forward, forward_by_front_chip_smc_write_memcpy_fail)
820{1016{
821 chip_smc_exposed chip;1017 chip_smc_exposed chip;
@@ -21,6 +21,7 @@
21#include <thread>21#include <thread>
22#include <vector>22#include <vector>
23#include <gtest/gtest.h>23#include <gtest/gtest.h>
24+#include "../interface/test_async_wait.h"
24#include "emulex_card.h"25#include "emulex_card.h"
25#include "emulex_om.h"26#include "emulex_om.h"
26#include "emulex_port.h"27#include "emulex_port.h"
@@ -136,6 +137,46 @@ std::unique_ptr<emulex_test_service> EmulexOpticalModuleTest::m_test_service;
136 137 
137// ========================= 基本功能测试 =========================138// ========================= 基本功能测试 =========================
138 139 
140+TEST_F(EmulexOpticalModuleTest, EmbeddedNcsiTimerCanOutliveDevice)
141+{
142+ auto device = mc::make_shared<dev::emulex_optical_module>();
143+ auto& iface = device->m_optical_module;
144+ iface.start_ncsi_update_task_broadcom(nullptr, mc::milliseconds(60000), 0);
145+ auto retained = dev::BoardcomNcsiTimerTestAccess::timer(iface);
146+ ASSERT_NE(retained, nullptr);
147+ // A value-member interface must never own the queued timer as a child:
148+ // get_parent() alone may hide a dangling link when weak promotion fails.
149+ EXPECT_TRUE(iface.get_children().empty());
150+ iface.stop_ncsi_update_task();
151+ EXPECT_FALSE(retained->is_active());
152+ device.reset();
153+ retained.reset();
154+}
155+ 
156+TEST_F(EmulexOpticalModuleTest, EmbeddedNcsiTimerUsesOwnerExecutor)
157+{
158+ auto device = mc::make_shared<dev::emulex_optical_module>();
159+ mc::any_executor expected{mc::runtime::runtime_strand{}};
160+ device->bind_executor(expected);
161+ auto& iface = device->m_optical_module;
162+ iface.start_ncsi_update_task_broadcom(nullptr, mc::milliseconds(60000), 0);
163+ auto timer = dev::BoardcomNcsiTimerTestAccess::timer(iface);
164+ ASSERT_NE(timer, nullptr);
165+ auto observed = std::make_shared<std::promise<bool>>();
166+ auto result = observed->get_future();
167+ timer->timeout.connect([observed, expected] {
168+ observed->set_value(expected.running_in_this_thread());
169+ });
170+ timer->set_single_shot(true);
171+ timer->start(mc::milliseconds(1));
172+ const auto ready = result.wait_for(std::chrono::seconds(2));
173+ iface.stop_ncsi_update_task();
174+ EXPECT_EQ(ready, std::future_status::ready);
175+ if (ready == std::future_status::ready) {
176+ EXPECT_TRUE(result.get());
177+ }
178+}
179+ 
139TEST_F(EmulexOpticalModuleTest, Constructor)180TEST_F(EmulexOpticalModuleTest, Constructor)
140{181{
141 // Constructor 测试只验证对象能否正确创建182 // Constructor 测试只验证对象能否正确创建
@@ -25,6 +25,7 @@
25#include <thread>25#include <thread>
26#include <vector>26#include <vector>
27#include <gtest/gtest.h>27#include <gtest/gtest.h>
28+#include "../interface/test_async_wait.h"
28#include "emulex_card.h"29#include "emulex_card.h"
29#include "emulex_port.h"30#include "emulex_port.h"
30 31 
@@ -218,6 +219,46 @@ mc::shared_ptr<test_imu_ipmb_object> EmulexPortTest::m_ipmb_obj;
218 219 
219// ========================= 基本功能测试 =========================220// ========================= 基本功能测试 =========================
220 221 
222+TEST_F(EmulexPortTest, EmbeddedNcsiTimerCanOutliveDevice)
223+{
224+ auto device = mc::make_shared<dev::emulex_port>();
225+ auto& iface = device->m_network_port;
226+ iface.start_ncsi_update_task_broadcom(nullptr, mc::milliseconds(60000), 0);
227+ auto retained = dev::BoardcomNcsiTimerTestAccess::timer(iface);
228+ ASSERT_NE(retained, nullptr);
229+ // A value-member interface must never own the queued timer as a child:
230+ // get_parent() alone may hide a dangling link when weak promotion fails.
231+ EXPECT_TRUE(iface.get_children().empty());
232+ iface.stop_ncsi_update_task();
233+ EXPECT_FALSE(retained->is_active());
234+ device.reset();
235+ retained.reset();
236+}
237+ 
238+TEST_F(EmulexPortTest, EmbeddedNcsiTimerUsesOwnerExecutor)
239+{
240+ auto device = mc::make_shared<dev::emulex_port>();
241+ mc::any_executor expected{mc::runtime::runtime_strand{}};
242+ device->bind_executor(expected);
243+ auto& iface = device->m_network_port;
244+ iface.start_ncsi_update_task_broadcom(nullptr, mc::milliseconds(60000), 0);
245+ auto timer = dev::BoardcomNcsiTimerTestAccess::timer(iface);
246+ ASSERT_NE(timer, nullptr);
247+ auto observed = std::make_shared<std::promise<bool>>();
248+ auto result = observed->get_future();
249+ timer->timeout.connect([observed, expected] {
250+ observed->set_value(expected.running_in_this_thread());
251+ });
252+ timer->set_single_shot(true);
253+ timer->start(mc::milliseconds(1));
254+ const auto ready = result.wait_for(std::chrono::seconds(2));
255+ iface.stop_ncsi_update_task();
256+ EXPECT_EQ(ready, std::future_status::ready);
257+ if (ready == std::future_status::ready) {
258+ EXPECT_TRUE(result.get());
259+ }
260+}
261+ 
221TEST_F(EmulexPortTest, Constructor)262TEST_F(EmulexPortTest, Constructor)
222{263{
223 dev::emulex_port port;264 dev::emulex_port port;
@@ -580,11 +621,7 @@ TEST_F(EmulexPortTest, InitStartStopPldmProtocol)
580 621 
581// ========================= start_pldm_protocol 边界测试 =========================622// ========================= start_pldm_protocol 边界测试 =========================
582 623 
583-// 临时禁用:start_pldm_protocol 内部对入参指针无 null check,传入 nullptr 会段错误,624+TEST_F(EmulexPortTest, StartPldmProtocol_WithNullPldmPtr)
584-// EXPECT_NO_THROW 无法捕获信号;保留测试体作为后续修代码后的回归用例。
585-// 覆盖影响:init() 路径已被其他测试覆盖,start_pldm_protocol() 路径已被
586-// StartPldmProtocol_StartsProtocol/InitStartStopPldmProtocol 等覆盖,禁用不影响覆盖率。
587-TEST_F(EmulexPortTest, DISABLED_StartPldmProtocol_WithNullPldmPtr)
588{625{
589 auto csr = create_test_csr();626 auto csr = create_test_csr();
590 csr["bmc.dev.NetworkPort"] = mc::dict{{"PortId", 0}};627 csr["bmc.dev.NetworkPort"] = mc::dict{{"PortId", 0}};
@@ -595,8 +632,11 @@ TEST_F(EmulexPortTest, DISABLED_StartPldmProtocol_WithNullPldmPtr)
595 mc::milliseconds interval(100);632 mc::milliseconds interval(100);
596 EXPECT_NO_THROW({633 EXPECT_NO_THROW({
597 bool result = m_port->start_pldm_protocol(nullptr, interval);634 bool result = m_port->start_pldm_protocol(nullptr, interval);
598- EXPECT_TRUE(result == true || result == false);635+ EXPECT_TRUE(result);
599 });636 });
637+ EXPECT_FALSE(m_port->m_network_port_link_info.update_link_status_by_pldm(0));
638+ EXPECT_FALSE(m_port->m_network_port_fibrechannel.update_fcinfo_by_pldm(0));
639+ m_port->stop_pldm_protocol();
600}640}
601 641 
602// ========================= update_port_bdf 边界测试 =========================642// ========================= update_port_bdf 边界测试 =========================
@@ -0,0 +1,90 @@
1+/*
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.
11+ */
12+ 
13+#pragma once
14+ 
15+#include <chrono>
16+#include <condition_variable>
17+#include <future>
18+#include <mc/object.h>
19+#include <mc/timer.h>
20+#include <mutex>
21+#include "interface/network_port.h"
22+#include "interface/optical_module.h"
23+ 
24+namespace dev {
25+struct BoardcomNcsiTimerTestAccess {
26+ static auto timer(const NetworkPort& port)
27+ {
28+ return port.m_ncsi_timer;
29+ }
30+ static auto timer(const OpticalModule& module)
31+ {
32+ return module.m_ncsi_timer;
33+ }
34+};
35+} // namespace dev
36+ 
37+namespace drivers::pcie_nic_card::boardcom::interface::test {
38+// Wait for real timer requests, with synchronized state and a bounded deadline.
39+class RequestCounter {
40+public:
41+ void record()
42+ {
43+ std::lock_guard<std::mutex> lock(m_mutex);
44+ ++m_count;
45+ m_changed.notify_all();
46+ }
47+ int count()
48+ {
49+ std::lock_guard<std::mutex> lock(m_mutex);
50+ return m_count;
51+ }
52+ bool wait_for(int target, std::chrono::milliseconds timeout = std::chrono::seconds(2))
53+ {
54+ std::unique_lock<std::mutex> lock(m_mutex);
55+ return m_changed.wait_for(lock, timeout, [&] {
56+ return m_count >= target;
57+ });
58+ }
59+ 
60+private:
61+ std::mutex m_mutex;
62+ std::condition_variable m_changed;
63+ int m_count = 0;
64+};
65+ 
66+// Join the update timer without invoking an interface stop that resets the
67+// observed properties. Access the owned timer even when it has no parent.
68+inline auto find_timer(const dev::NetworkPort& object)
69+{
70+ return dev::BoardcomNcsiTimerTestAccess::timer(object);
71+}
72+inline auto find_timer(const dev::OpticalModule& object)
73+{
74+ return dev::BoardcomNcsiTimerTestAccess::timer(object);
75+}
76+inline void stop_child_timers(const dev::NetworkPort& object)
77+{
78+ if (auto timer = find_timer(object)) {
79+ timer->stop();
80+ }
81+}
82+inline void stop_child_timers(const mc::object& object)
83+{
84+ for (const auto& child : object.get_children()) {
85+ if (auto timer = mc::dynamic_pointer_cast<mc::timer>(child)) {
86+ timer->stop();
87+ }
88+ }
89+}
90+} // namespace drivers::pcie_nic_card::boardcom::interface::test
@@ -29,6 +29,7 @@
29#include <gtest/gtest.h>29#include <gtest/gtest.h>
30#include "interface/cooling.h"30#include "interface/cooling.h"
31#include "interface/network_adapter/oem.h"31#include "interface/network_adapter/oem.h"
32+#include "test_async_wait.h"
32 33 
33using namespace dev;34using namespace dev;
34 35 
@@ -551,8 +552,10 @@ TEST_F(BoardcomNetworkAdapterCoolingTest, StopPldmUpdateTask_MultipleStop)
551 552 
552TEST_F(BoardcomNetworkAdapterCoolingTest, TimerCallback_UpdatesTemperature)553TEST_F(BoardcomNetworkAdapterCoolingTest, TimerCallback_UpdatesTemperature)
553{554{
555+ RequestCounter calls;
554 // 设置 MCTP 请求处理器556 // 设置 MCTP 请求处理器
555- dev::set_mctp_request_handler([](const mc::dict& ctx, const std::vector<uint8_t>& data, std::string& rsp) {557+ dev::set_mctp_request_handler([&calls](const mc::dict& ctx, const std::vector<uint8_t>& data, std::string& rsp) {
558+ calls.record();
556 if (!ctx.contains("PldmType") || !ctx.contains("CommandCode")) {559 if (!ctx.contains("PldmType") || !ctx.contains("CommandCode")) {
557 return false;560 return false;
558 }561 }
@@ -586,12 +589,11 @@ TEST_F(BoardcomNetworkAdapterCoolingTest, TimerCallback_UpdatesTemperature)
586 while (std::chrono::steady_clock::now() < deadline && m_cooling->TemperatureCelsius.get_value() != 60) {589 while (std::chrono::steady_clock::now() < deadline && m_cooling->TemperatureCelsius.get_value() != 60) {
587 std::this_thread::sleep_for(std::chrono::milliseconds(20));590 std::this_thread::sleep_for(std::chrono::milliseconds(20));
588 }591 }
592+ m_cooling->stop_pldm_update_task();
589 593 
590 // 验证温度已被更新594 // 验证温度已被更新
591 EXPECT_EQ(m_cooling->TemperatureCelsius.get_value(), 60);595 EXPECT_EQ(m_cooling->TemperatureCelsius.get_value(), 60);
592 EXPECT_EQ(m_cooling->TemperatureStatus.get_value(), 0);596 EXPECT_EQ(m_cooling->TemperatureStatus.get_value(), 0);
593- 
594- m_cooling->stop_pldm_update_task();
595}597}
596 598 
597TEST_F(BoardcomNetworkAdapterCoolingTest, TimerCallback_HandlesFailure)599TEST_F(BoardcomNetworkAdapterCoolingTest, TimerCallback_HandlesFailure)
@@ -1162,12 +1164,15 @@ TEST_F(BoardcomNetworkAdapterCoolingTest, RestoreNcsiPropertyDefaults_DirectCall
1162 1164 
1163TEST_F(BoardcomNetworkAdapterCoolingTest, NcsiTimerCallback_UpdatesTemperature)1165TEST_F(BoardcomNetworkAdapterCoolingTest, NcsiTimerCallback_UpdatesTemperature)
1164{1166{
1165- dev::set_mctp_request_handler([](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {1167+ RequestCounter calls;
1168+ dev::set_mctp_request_handler([&calls](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {
1169+ calls.record();
1166 rsp = make_ncsi_temp_rsp_bytes(80, 45);1170 rsp = make_ncsi_temp_rsp_bytes(80, 45);
1167 return true;1171 return true;
1168 });1172 });
1169 m_cooling->start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(50), 0);1173 m_cooling->start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(50), 0);
1170- std::this_thread::sleep_for(std::chrono::milliseconds(120));1174+ EXPECT_TRUE(calls.wait_for(1));
1175+ stop_child_timers(*m_cooling);
1171 EXPECT_EQ(m_cooling->TemperatureCelsius.get_value(), 45);1176 EXPECT_EQ(m_cooling->TemperatureCelsius.get_value(), 45);
1172 m_cooling->stop_ncsi_update_task();1177 m_cooling->stop_ncsi_update_task();
1173}1178}
@@ -28,6 +28,7 @@
28#include "interface/network_port.h"28#include "interface/network_port.h"
29#include "interface/network_port/link_info.h"29#include "interface/network_port/link_info.h"
30#include "interface/network_port/oem.h"30#include "interface/network_port/oem.h"
31+#include "test_async_wait.h"
31 32 
32using namespace dev;33using namespace dev;
33 34 
@@ -754,22 +755,36 @@ TEST_F(BoardcomNetworkPortNcsiTest, UpdatePartitionInfoByNcsi_OneMac_OnlyMacAddr
754 755 
755// ----- start_ncsi_update_task_broadcom / stop_ncsi_update_task -----756// ----- start_ncsi_update_task_broadcom / stop_ncsi_update_task -----
756 757 
758+TEST_F(BoardcomNetworkPortNcsiTest, ReplacingRetainedTimerStopsPreviousTimer)
759+{
760+ m_network_port->start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(60000), 0);
761+ auto previous = find_timer(*m_network_port);
762+ EXPECT_NE(previous, nullptr);
763+ m_network_port->start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(60000), 1);
764+ if (previous) {
765+ EXPECT_FALSE(previous->is_active());
766+ // Keep failure paths safe on the original implementation too.
767+ previous->stop();
768+ }
769+ m_network_port->stop_ncsi_update_task();
770+}
771+ 
757TEST_F(BoardcomNetworkPortNcsiTest, StartNcsiUpdateTask_TimerFiresInvokesUpdatePartitionInfo)772TEST_F(BoardcomNetworkPortNcsiTest, StartNcsiUpdateTask_TimerFiresInvokesUpdatePartitionInfo)
758{773{
759- int call_count = 0;774+ RequestCounter calls;
760- dev::set_mctp_request_handler([&call_count](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {775+ dev::set_mctp_request_handler([&calls](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {
761- ++call_count;776+ calls.record();
762 rsp = make_partition_config_rsp_bytes(0x18, 0x02, 0x0, {{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}});777 rsp = make_partition_config_rsp_bytes(0x18, 0x02, 0x0, {{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}});
763 return true;778 return true;
764 });779 });
765 m_network_port->start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(10), 0);780 m_network_port->start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(10), 0);
766- std::this_thread::sleep_for(std::chrono::milliseconds(80));781+ // Join the callback without resetting the BDF observed below.
767- EXPECT_GT(call_count, 0);782+ EXPECT_TRUE(calls.wait_for(1));
783+ stop_child_timers(*m_network_port);
768 EXPECT_EQ(m_network_port->BDF.get_value().as<std::string>(), "0000:18:02.0");784 EXPECT_EQ(m_network_port->BDF.get_value().as<std::string>(), "0000:18:02.0");
769 m_network_port->stop_ncsi_update_task();785 m_network_port->stop_ncsi_update_task();
770- int prev = call_count;786+ const int prev = calls.count();
771- std::this_thread::sleep_for(std::chrono::milliseconds(50));787+ EXPECT_FALSE(calls.wait_for(prev + 1, std::chrono::milliseconds(50)));
772- EXPECT_EQ(call_count, prev);
773}788}
774 789 
775TEST_F(BoardcomNetworkPortNcsiTest, StopNcsiUpdateTask_StopsAndResetsTimer)790TEST_F(BoardcomNetworkPortNcsiTest, StopNcsiUpdateTask_StopsAndResetsTimer)
@@ -918,39 +933,36 @@ TEST_F(BoardcomNetworkPortNcsiTest, UpdatePartitionInfoByNcsi_TLVOffsetExceedsBu
918 933 
919TEST_F(BoardcomNetworkPortNcsiTest, StartNcsiUpdateTask_ConsecutiveStarts_ReplacesTimer)934TEST_F(BoardcomNetworkPortNcsiTest, StartNcsiUpdateTask_ConsecutiveStarts_ReplacesTimer)
920{935{
921- int call_count = 0;936+ RequestCounter calls;
922- dev::set_mctp_request_handler([&call_count](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {937+ dev::set_mctp_request_handler([&calls](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {
923- ++call_count;938+ calls.record();
924 rsp = make_partition_config_rsp_bytes(0x18, 0x02, 0x0, {{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}});939 rsp = make_partition_config_rsp_bytes(0x18, 0x02, 0x0, {{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}});
925 return true;940 return true;
926 });941 });
927 m_network_port->start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(20), 0);942 m_network_port->start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(20), 0);
928- std::this_thread::sleep_for(std::chrono::milliseconds(30));943+ EXPECT_TRUE(calls.wait_for(1));
929- int prev = call_count;
930- EXPECT_GT(prev, 0);
931 // 第二次 start:覆盖 m_ncsi_timer 和 m_package_id944 // 第二次 start:覆盖 m_ncsi_timer 和 m_package_id
932 m_network_port->start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(20), 1);945 m_network_port->start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(20), 1);
933- std::this_thread::sleep_for(std::chrono::milliseconds(60));946+ const int prev = calls.count();
934- EXPECT_GT(call_count, prev);947+ EXPECT_TRUE(calls.wait_for(prev + 1));
935 m_network_port->stop_ncsi_update_task();948 m_network_port->stop_ncsi_update_task();
936}949}
937 950 
938TEST_F(BoardcomNetworkPortNcsiTest, StartNcsiUpdateTask_StartStopStartStop_StopsBetweenCycles)951TEST_F(BoardcomNetworkPortNcsiTest, StartNcsiUpdateTask_StartStopStartStop_StopsBetweenCycles)
939{952{
940- int call_count = 0;953+ RequestCounter calls;
941- dev::set_mctp_request_handler([&call_count](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {954+ dev::set_mctp_request_handler([&calls](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {
942- ++call_count;955+ calls.record();
943 rsp = make_partition_config_rsp_bytes(0x18, 0x02, 0x0, {{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}});956 rsp = make_partition_config_rsp_bytes(0x18, 0x02, 0x0, {{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}});
944 return true;957 return true;
945 });958 });
946 m_network_port->start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(10), 0);959 m_network_port->start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(10), 0);
947- std::this_thread::sleep_for(std::chrono::milliseconds(30));960+ EXPECT_TRUE(calls.wait_for(1));
948 m_network_port->stop_ncsi_update_task();961 m_network_port->stop_ncsi_update_task();
949- int after_first = call_count;962+ const int after_first = calls.count();
950 m_network_port->start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(10), 0);963 m_network_port->start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(10), 0);
951- std::this_thread::sleep_for(std::chrono::milliseconds(30));964+ EXPECT_TRUE(calls.wait_for(after_first + 1));
952 m_network_port->stop_ncsi_update_task();965 m_network_port->stop_ncsi_update_task();
953- EXPECT_GT(call_count, after_first);
954}966}
955 967 
956// ----- SendNcsiTestPacket 异常路径:覆盖 try/catch 中的 elog 分支 -----968// ----- SendNcsiTestPacket 异常路径:覆盖 try/catch 中的 elog 分支 -----
@@ -28,6 +28,7 @@
28#include "interface/optical_module/power.h"28#include "interface/optical_module/power.h"
29#include "interface/optical_module/temperature_celsius.h"29#include "interface/optical_module/temperature_celsius.h"
30#include "interface/optical_module/voltage.h"30#include "interface/optical_module/voltage.h"
31+#include "test_async_wait.h"
31 32 
32using namespace dev;33using namespace dev;
33 34 
@@ -888,6 +889,11 @@ public:
888 889 
889class BoardcomTemperatureCelsiusTest : public mc::test::TestWithEngine {890class BoardcomTemperatureCelsiusTest : public mc::test::TestWithEngine {
890protected:891protected:
892+ // Tests that call get_om_temp_by_ncsi() directly only need start() to
893+ // inject the NCSI transport. Keep the periodic callback outside the test
894+ // window so it cannot race the direct call and overwrite its assertion.
895+ static constexpr mc::milliseconds MANUAL_POLL_INTERVAL{60000};
896+ 
891 void SetUp() override897 void SetUp() override
892 {898 {
893 m_owner = mc::make_shared<TemperatureCelsiusTestObject>();899 m_owner = mc::make_shared<TemperatureCelsiusTestObject>();
@@ -999,7 +1005,7 @@ TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_NullNcsi_ReturnsFalse)
999 1005 
1000TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_RequestHandlerReturnsFalse_ReturnsFalse)1006TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_RequestHandlerReturnsFalse_ReturnsFalse)
1001{1007{
1002- m_owner->m_tc.start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(10), 0, 0);1008+ m_owner->m_tc.start_ncsi_update_task_broadcom(m_ncsi, MANUAL_POLL_INTERVAL, 0, 0);
1003 dev::set_mctp_request_handler([](const mc::dict&, const std::vector<uint8_t>&, std::string&) {1009 dev::set_mctp_request_handler([](const mc::dict&, const std::vector<uint8_t>&, std::string&) {
1004 return false;1010 return false;
1005 });1011 });
@@ -1009,7 +1015,7 @@ TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_RequestHandlerReturnsFals
1009 1015 
1010TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_EmptyRsp_ReturnsFalse)1016TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_EmptyRsp_ReturnsFalse)
1011{1017{
1012- m_owner->m_tc.start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(10), 0, 0);1018+ m_owner->m_tc.start_ncsi_update_task_broadcom(m_ncsi, MANUAL_POLL_INTERVAL, 0, 0);
1013 dev::set_mctp_request_handler([](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {1019 dev::set_mctp_request_handler([](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {
1014 rsp.clear();1020 rsp.clear();
1015 return true;1021 return true;
@@ -1020,7 +1026,7 @@ TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_EmptyRsp_ReturnsFalse)
1020 1026 
1021TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_ShortRsp_ReturnsFalse)1027TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_ShortRsp_ReturnsFalse)
1022{1028{
1023- m_owner->m_tc.start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(10), 0, 0);1029+ m_owner->m_tc.start_ncsi_update_task_broadcom(m_ncsi, MANUAL_POLL_INTERVAL, 0, 0);
1024 dev::set_mctp_request_handler([](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {1030 dev::set_mctp_request_handler([](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {
1025 rsp = std::string(OEM_BROADCOM_TEMP_RSP_LEN - 1, '\0');1031 rsp = std::string(OEM_BROADCOM_TEMP_RSP_LEN - 1, '\0');
1026 return true;1032 return true;
@@ -1031,7 +1037,7 @@ TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_ShortRsp_ReturnsFalse)
1031 1037 
1032TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_InvalidRspCode_ReturnsFalse)1038TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_InvalidRspCode_ReturnsFalse)
1033{1039{
1034- m_owner->m_tc.start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(10), 0, 0);1040+ m_owner->m_tc.start_ncsi_update_task_broadcom(m_ncsi, MANUAL_POLL_INTERVAL, 0, 0);
1035 dev::set_mctp_request_handler([](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {1041 dev::set_mctp_request_handler([](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {
1036 rsp = make_temp_rsp_bytes(45, 70, BROADCOM_MANUFACTURE_ID_REP, /*response_code=*/0x0001);1042 rsp = make_temp_rsp_bytes(45, 70, BROADCOM_MANUFACTURE_ID_REP, /*response_code=*/0x0001);
1037 return true;1043 return true;
@@ -1042,7 +1048,7 @@ TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_InvalidRspCode_ReturnsFal
1042 1048 
1043TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_BadManufacturerId_ReturnsFalse)1049TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_BadManufacturerId_ReturnsFalse)
1044{1050{
1045- m_owner->m_tc.start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(10), 0, 0);1051+ m_owner->m_tc.start_ncsi_update_task_broadcom(m_ncsi, MANUAL_POLL_INTERVAL, 0, 0);
1046 dev::set_mctp_request_handler([](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {1052 dev::set_mctp_request_handler([](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {
1047 rsp = make_temp_rsp_bytes(45, 70, /*manufacturer_id=*/0xDEADBEEF);1053 rsp = make_temp_rsp_bytes(45, 70, /*manufacturer_id=*/0xDEADBEEF);
1048 return true;1054 return true;
@@ -1054,7 +1060,7 @@ TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_BadManufacturerId_Returns
1054TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_FailSmbiosNotReady_Sets0xFFFF)1060TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_FailSmbiosNotReady_Sets0xFFFF)
1055{1061{
1056 // temp_info 失败 + SmBiosStatus != 3 → 设为 0xFFFF 并 snapshot1062 // temp_info 失败 + SmBiosStatus != 3 → 设为 0xFFFF 并 snapshot
1057- m_owner->m_tc.start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(10), 0, 0);1063+ m_owner->m_tc.start_ncsi_update_task_broadcom(m_ncsi, MANUAL_POLL_INTERVAL, 0, 0);
1058 m_owner->m_aoem.SmBiosStatus.set_value(static_cast<uint8_t>(0));1064 m_owner->m_aoem.SmBiosStatus.set_value(static_cast<uint8_t>(0));
1059 dev::set_mctp_request_handler([](const mc::dict&, const std::vector<uint8_t>&, std::string&) {1065 dev::set_mctp_request_handler([](const mc::dict&, const std::vector<uint8_t>&, std::string&) {
1060 return false;1066 return false;
@@ -1067,7 +1073,7 @@ TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_FailSmbiosNotReady_Sets0x
1067TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_FailSmbiosReady_NoChange)1073TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_FailSmbiosReady_NoChange)
1068{1074{
1069 // temp_info 失败 + SmBiosStatus == 3 → 不写值,直接 return1075 // temp_info 失败 + SmBiosStatus == 3 → 不写值,直接 return
1070- m_owner->m_tc.start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(10), 0, 0);1076+ m_owner->m_tc.start_ncsi_update_task_broadcom(m_ncsi, MANUAL_POLL_INTERVAL, 0, 0);
1071 m_owner->m_tc.TemperatureCelsius.set_value(42.0);1077 m_owner->m_tc.TemperatureCelsius.set_value(42.0);
1072 m_owner->m_aoem.SmBiosStatus.set_value(static_cast<uint8_t>(3));1078 m_owner->m_aoem.SmBiosStatus.set_value(static_cast<uint8_t>(3));
1073 dev::set_mctp_request_handler([](const mc::dict&, const std::vector<uint8_t>&, std::string&) {1079 dev::set_mctp_request_handler([](const mc::dict&, const std::vector<uint8_t>&, std::string&) {
@@ -1086,7 +1092,7 @@ TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_FailNoNetworkAdapterOem_N
1086 tc2->set_optical_module_oem(&m_owner->m_oem);1092 tc2->set_optical_module_oem(&m_owner->m_oem);
1087 // set_network_adapter_oem 故意不调用1093 // set_network_adapter_oem 故意不调用
1088 tc2->set_owner(m_owner.get());1094 tc2->set_owner(m_owner.get());
1089- m_owner->m_tc.start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(10), 0, 0);1095+ m_owner->m_tc.start_ncsi_update_task_broadcom(m_ncsi, MANUAL_POLL_INTERVAL, 0, 0);
1090 // tc2 没 start_ncsi_update_task,ncsi 为 null → 走最早 return false1096 // tc2 没 start_ncsi_update_task,ncsi 为 null → 走最早 return false
1091 EXPECT_FALSE(tc2->get_om_temp_by_ncsi(0));1097 EXPECT_FALSE(tc2->get_om_temp_by_ncsi(0));
1092 m_owner->m_tc.stop_ncsi_update_task();1098 m_owner->m_tc.stop_ncsi_update_task();
@@ -1094,7 +1100,7 @@ TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_FailNoNetworkAdapterOem_N
1094 1100 
1095TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_NormalTemp_SetsValue)1101TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_NormalTemp_SetsValue)
1096{1102{
1097- m_owner->m_tc.start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(10), 0, 0);1103+ m_owner->m_tc.start_ncsi_update_task_broadcom(m_ncsi, MANUAL_POLL_INTERVAL, 0, 0);
1098 m_owner->m_om.Presence.set_value(static_cast<uint8_t>(1));1104 m_owner->m_om.Presence.set_value(static_cast<uint8_t>(1));
1099 dev::set_mctp_request_handler([](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {1105 dev::set_mctp_request_handler([](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {
1100 rsp = make_temp_rsp_bytes(static_cast<int8_t>(50));1106 rsp = make_temp_rsp_bytes(static_cast<int8_t>(50));
@@ -1108,7 +1114,7 @@ TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_NormalTemp_SetsValue)
1108TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_InvalidTempAndPresent_Sets0x8000)1114TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_InvalidTempAndPresent_Sets0x8000)
1109{1115{
1110 // CHIP_TEMP_INVALID (0xFF) + Presence != 0 + m_optical_module != nullptr → 设为 0x80001116 // CHIP_TEMP_INVALID (0xFF) + Presence != 0 + m_optical_module != nullptr → 设为 0x8000
1111- m_owner->m_tc.start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(10), 0, 0);1117+ m_owner->m_tc.start_ncsi_update_task_broadcom(m_ncsi, MANUAL_POLL_INTERVAL, 0, 0);
1112 m_owner->m_om.Presence.set_value(static_cast<uint8_t>(1));1118 m_owner->m_om.Presence.set_value(static_cast<uint8_t>(1));
1113 dev::set_mctp_request_handler([](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {1119 dev::set_mctp_request_handler([](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {
1114 rsp = make_temp_rsp_bytes(static_cast<int8_t>(CHIP_TEMP_INVALID));1120 rsp = make_temp_rsp_bytes(static_cast<int8_t>(CHIP_TEMP_INVALID));
@@ -1122,7 +1128,7 @@ TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_InvalidTempAndPresent_Set
1122TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_InvalidTempNotPresent_NoAlarm)1128TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_InvalidTempNotPresent_NoAlarm)
1123{1129{
1124 // CHIP_TEMP_INVALID + Presence == 0 → 走正常路径,保留 cur_temp 值1130 // CHIP_TEMP_INVALID + Presence == 0 → 走正常路径,保留 cur_temp 值
1125- m_owner->m_tc.start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(10), 0, 0);1131+ m_owner->m_tc.start_ncsi_update_task_broadcom(m_ncsi, MANUAL_POLL_INTERVAL, 0, 0);
1126 m_owner->m_om.Presence.set_value(static_cast<uint8_t>(0));1132 m_owner->m_om.Presence.set_value(static_cast<uint8_t>(0));
1127 dev::set_mctp_request_handler([](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {1133 dev::set_mctp_request_handler([](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {
1128 rsp = make_temp_rsp_bytes(static_cast<int8_t>(CHIP_TEMP_INVALID));1134 rsp = make_temp_rsp_bytes(static_cast<int8_t>(CHIP_TEMP_INVALID));
@@ -1140,7 +1146,7 @@ TEST_F(BoardcomTemperatureCelsiusTest, GetOmTempByNcsi_InvalidTempNoOpticalModul
1140 tc2->set_optical_module_oem(&m_owner->m_oem);1146 tc2->set_optical_module_oem(&m_owner->m_oem);
1141 tc2->set_network_adapter_oem(&m_owner->m_aoem);1147 tc2->set_network_adapter_oem(&m_owner->m_aoem);
1142 tc2->set_owner(m_owner.get());1148 tc2->set_owner(m_owner.get());
1143- tc2->start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(10), 0, 0);1149+ tc2->start_ncsi_update_task_broadcom(m_ncsi, MANUAL_POLL_INTERVAL, 0, 0);
1144 dev::set_mctp_request_handler([](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {1150 dev::set_mctp_request_handler([](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {
1145 rsp = make_temp_rsp_bytes(static_cast<int8_t>(CHIP_TEMP_INVALID));1151 rsp = make_temp_rsp_bytes(static_cast<int8_t>(CHIP_TEMP_INVALID));
1146 return true;1152 return true;
@@ -1311,11 +1317,27 @@ TEST_F(BoardcomOpticalModuleTest, GetPresenceInfoByNcsi_NotPresent_SetsPresenceT
1311 m_optical_module->stop_ncsi_update_task();1317 m_optical_module->stop_ncsi_update_task();
1312}1318}
1313 1319 
1320+TEST_F(BoardcomOpticalModuleTest, ReplacingRetainedTimerStopsPreviousTimer)
1321+{
1322+ m_mctp = mc::make_shared<dev::mctp>(nullptr, 0, dev::MCTP_MESSAGE_TYPE_NCSI, "");
1323+ m_ncsi = std::make_shared<dev::ncsi_over_mctp_broadcom>(m_mctp);
1324+ m_optical_module->start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(60000), 0);
1325+ auto previous = find_timer(*m_optical_module);
1326+ EXPECT_NE(previous, nullptr);
1327+ m_optical_module->start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(60000), 1);
1328+ if (previous) {
1329+ EXPECT_FALSE(previous->is_active());
1330+ // Keep failure paths safe on the original implementation too.
1331+ previous->stop();
1332+ }
1333+ m_optical_module->stop_ncsi_update_task();
1334+}
1335+ 
1314TEST_F(BoardcomOpticalModuleTest, StartNcsiUpdateTask_TimerFiresInvokesGetPresenceInfo)1336TEST_F(BoardcomOpticalModuleTest, StartNcsiUpdateTask_TimerFiresInvokesGetPresenceInfo)
1315{1337{
1316- int call_count = 0;1338+ RequestCounter calls;
1317- dev::set_mctp_request_handler([&call_count](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {1339+ dev::set_mctp_request_handler([&calls](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {
1318- ++call_count;1340+ calls.record();
1319 rsp = make_media_info_rsp_bytes(MEDIA_BIT_FIBER);1341 rsp = make_media_info_rsp_bytes(MEDIA_BIT_FIBER);
1320 return true;1342 return true;
1321 });1343 });
@@ -1325,15 +1347,13 @@ TEST_F(BoardcomOpticalModuleTest, StartNcsiUpdateTask_TimerFiresInvokesGetPresen
1325 m_optical_module->set_port_id(1);1347 m_optical_module->set_port_id(1);
1326 1348 
1327 m_optical_module->start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(20), 0);1349 m_optical_module->start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(20), 0);
1328- std::this_thread::sleep_for(std::chrono::milliseconds(100));1350+ EXPECT_TRUE(calls.wait_for(1));
1329- EXPECT_GT(call_count, 0);1351+ m_optical_module->stop_ncsi_update_task();
1330 EXPECT_EQ(m_optical_module->Presence.get_value(), 1);1352 EXPECT_EQ(m_optical_module->Presence.get_value(), 1);
1331 1353 
1332- m_optical_module->stop_ncsi_update_task();1354+ // No further request may arrive after stop has joined the callback.
1333- // 停止后 call_count 不再增加1355+ const int prev_count = calls.count();
1334- int prev_count = call_count;1356+ EXPECT_FALSE(calls.wait_for(prev_count + 1, std::chrono::milliseconds(80)));
1335- std::this_thread::sleep_for(std::chrono::milliseconds(80));
1336- EXPECT_EQ(call_count, prev_count);
1337}1357}
1338 1358 
1339TEST_F(BoardcomOpticalModuleTest, StopNcsiUpdateTask_WithoutStart_NoCrash)1359TEST_F(BoardcomOpticalModuleTest, StopNcsiUpdateTask_WithoutStart_NoCrash)
@@ -1393,9 +1413,9 @@ TEST_F(BoardcomOpticalModuleTest, SnapshotPropWithoutBma_OemSet_UpdatePropInvoke
1393// 多次启动定时器(覆盖 m_ncsi_timer 重新赋值、m_package_id 更新)1413// 多次启动定时器(覆盖 m_ncsi_timer 重新赋值、m_package_id 更新)
1394TEST_F(BoardcomOpticalModuleTest, StartNcsiUpdateTask_CalledTwice_ReplacesTimer)1414TEST_F(BoardcomOpticalModuleTest, StartNcsiUpdateTask_CalledTwice_ReplacesTimer)
1395{1415{
1396- int call_count = 0;1416+ RequestCounter calls;
1397- dev::set_mctp_request_handler([&call_count](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {1417+ dev::set_mctp_request_handler([&calls](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {
1398- ++call_count;1418+ calls.record();
1399 rsp = make_media_info_rsp_bytes(MEDIA_BIT_FIBER);1419 rsp = make_media_info_rsp_bytes(MEDIA_BIT_FIBER);
1400 return true;1420 return true;
1401 });1421 });
@@ -1405,22 +1425,20 @@ TEST_F(BoardcomOpticalModuleTest, StartNcsiUpdateTask_CalledTwice_ReplacesTimer)
1405 m_optical_module->set_port_id(1);1425 m_optical_module->set_port_id(1);
1406 1426 
1407 m_optical_module->start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(20), 0);1427 m_optical_module->start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(20), 0);
1408- std::this_thread::sleep_for(std::chrono::milliseconds(60));1428+ EXPECT_TRUE(calls.wait_for(1));
1409- int prev = call_count;
1410- EXPECT_GT(prev, 0);
1411 // 第二次 start:覆盖 m_ncsi_timer 和 m_package_id1429 // 第二次 start:覆盖 m_ncsi_timer 和 m_package_id
1412 m_optical_module->start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(20), 1);1430 m_optical_module->start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(20), 1);
1413- std::this_thread::sleep_for(std::chrono::milliseconds(60));1431+ const int prev = calls.count();
1414- EXPECT_GT(call_count, prev);1432+ EXPECT_TRUE(calls.wait_for(prev + 1));
1415 m_optical_module->stop_ncsi_update_task();1433 m_optical_module->stop_ncsi_update_task();
1416}1434}
1417 1435 
1418// start + 触发 timer + stop 后立即重启 + stop1436// start + 触发 timer + stop 后立即重启 + stop
1419TEST_F(BoardcomOpticalModuleTest, StartStopStartStop_StopsTimerBetweenCycles)1437TEST_F(BoardcomOpticalModuleTest, StartStopStartStop_StopsTimerBetweenCycles)
1420{1438{
1421- int call_count = 0;1439+ RequestCounter calls;
1422- dev::set_mctp_request_handler([&call_count](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {1440+ dev::set_mctp_request_handler([&calls](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {
1423- ++call_count;1441+ calls.record();
1424 rsp = make_media_info_rsp_bytes(MEDIA_BIT_FIBER);1442 rsp = make_media_info_rsp_bytes(MEDIA_BIT_FIBER);
1425 return true;1443 return true;
1426 });1444 });
@@ -1429,17 +1447,16 @@ TEST_F(BoardcomOpticalModuleTest, StartStopStartStop_StopsTimerBetweenCycles)
1429 m_ncsi = std::make_shared<dev::ncsi_over_mctp_broadcom>(m_mctp);1447 m_ncsi = std::make_shared<dev::ncsi_over_mctp_broadcom>(m_mctp);
1430 m_optical_module->set_port_id(1);1448 m_optical_module->set_port_id(1);
1431 1449 
1432- // 第一次:start → 跑 30ms → stop1450+ // First cycle must actually issue a request before stopping.
1433 m_optical_module->start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(10), 0);1451 m_optical_module->start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(10), 0);
1434- std::this_thread::sleep_for(std::chrono::milliseconds(30));1452+ EXPECT_TRUE(calls.wait_for(1));
1435 m_optical_module->stop_ncsi_update_task();1453 m_optical_module->stop_ncsi_update_task();
1436- int after_first_stop = call_count;1454+ const int after_first_stop = calls.count();
1437 1455 
1438- // 第二次:start → 跑 30ms → stop1456+ // Restart must issue a new request after the first stop.
1439 m_optical_module->start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(10), 0);1457 m_optical_module->start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(10), 0);
1440- std::this_thread::sleep_for(std::chrono::milliseconds(30));1458+ EXPECT_TRUE(calls.wait_for(after_first_stop + 1));
1441 m_optical_module->stop_ncsi_update_task();1459 m_optical_module->stop_ncsi_update_task();
1442- EXPECT_GT(call_count, after_first_stop);
1443}1460}
1444 1461 
1445// 连续 stop 不崩溃(覆盖 stop 内部 m_ncsi_timer 已被 reset 的分支)1462// 连续 stop 不崩溃(覆盖 stop 内部 m_ncsi_timer 已被 reset 的分支)
@@ -1537,19 +1554,18 @@ TEST_F(BoardcomOpticalModuleTest, GetPresenceInfoByNcsi_MediaInfoRspCodeFail)
1537// 覆盖 set_single_shot(false) 路径1554// 覆盖 set_single_shot(false) 路径
1538TEST_F(BoardcomOpticalModuleTest, StartNcsiUpdateTask_StartsNonSingleShotTimer)1555TEST_F(BoardcomOpticalModuleTest, StartNcsiUpdateTask_StartsNonSingleShotTimer)
1539{1556{
1540- m_mctp = mc::make_shared<dev::mctp>(nullptr, 0, dev::MCTP_MESSAGE_TYPE_NCSI, "");1557+ m_mctp = mc::make_shared<dev::mctp>(nullptr, 0, dev::MCTP_MESSAGE_TYPE_NCSI, "");
1541- m_ncsi = std::make_shared<dev::ncsi_over_mctp_broadcom>(m_mctp);1558+ m_ncsi = std::make_shared<dev::ncsi_over_mctp_broadcom>(m_mctp);
1542- int call_count = 0;1559+ RequestCounter calls;
1543- dev::set_mctp_request_handler([&call_count](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {1560+ dev::set_mctp_request_handler([&calls](const mc::dict&, const std::vector<uint8_t>&, std::string& rsp) {
1544- ++call_count;1561+ calls.record();
1545 rsp = make_media_info_rsp_bytes(MEDIA_BIT_FIBER);1562 rsp = make_media_info_rsp_bytes(MEDIA_BIT_FIBER);
1546 return true;1563 return true;
1547 });1564 });
1548- m_optical_module->start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(20), 0);
1549 m_optical_module->set_port_id(1);1565 m_optical_module->set_port_id(1);
1550- std::this_thread::sleep_for(std::chrono::milliseconds(100));1566+ m_optical_module->start_ncsi_update_task_broadcom(m_ncsi, mc::milliseconds(20), 0);
1551- // 非单次触发器,应该多次调用1567+ // Two real requests distinguish a repeating timer from a single shot.
1552- EXPECT_GT(call_count, 1);1568+ EXPECT_TRUE(calls.wait_for(2));
1553 m_optical_module->stop_ncsi_update_task();1569 m_optical_module->stop_ncsi_update_task();
1554}1570}
1555 1571