Open
fix: 隔离双向事务并保留 MCTP 查询资源 #11
fix: 隔离双向事务并保留 MCTP 查询资源 #11
Open
liuxingsenAdd on 14 days ago
Total: 18 files changed+994-70
@@ -1,5 +1,8 @@
1# Changelog1# Changelog
2All notable changes to this project will be documented in this file.2All notable changes to this project will be documented in this file.
3+## [1.130.16] - 2026-10-26
4+隔离双向事务,按来源和已登记响应分配重组资源,清理查询异常/复位残留并修复PCIe补齐越界。
5+ 
3## [1.130.15] - 2026-08-256## [1.130.15] - 2026-08-25
4更新版本号,触发流水线推包7更新版本号,触发流水线推包
5 8 
@@ -0,0 +1,22 @@
1+# 双向事务隔离与共享资源验证
2+ 
3+VDPCI、NC-SI和通用消息仅允许响应方向完成本端查询;VDPCI继续上报主动通知。NC-SI原端点没有主动报文上报接口,本改动也不宣告AEN通知支持;VDPCI的on_receive契约不能套用到NC-SI。通用端点原本没有主动通知契约,TO=1保留过滤并输出DEBUG类型/来源/Tag。PCIe补齐只使用resize,修复尾后越界写入。
4+ 
5+重组键包含介质、双向物理地址、双向EID、Tag和TO。最多32项,每消息4096B,包间100ms、总期限1s;新片到达同样先执行100ms包间检查;250ms空闲巡检只决定空闲回收粒度,不把包间窗口延长到250ms或1s。拒绝新SOM时保留同键合法半包,通过准入后才替换;容量检查先扣除被替换项,满池仍允许合法重启。普通报文16项、同来源4项;另16项仅留给本端已登记且传输键和消息类型匹配的预期响应,TO=0本身不获优先权。全局预期响应最多64项;超时、释放、EID变化和复位回收半包,旧令牌不能清理新登记。
6+ 
7+同路由的所有上层协议共享8个Tag占用,发送前登记预期响应;发送失败、消费回调异常、查询结束和复位释放。NVMe More Processing按现有等待期限续期,不复活过期登记;旧协程不得擦除复位后创建的同键等待项。
8+ 
9+## 验证归属
10+ 
11+- 原基础快照bfb0756曾为131/134通过;两处旧discovery测试漏传ep_name,修正后3c6f740d为134/134。本说明不把历史失败或通过混作当前分支结果。
12+- 共享准入集成候选4620c09完整167/167通过;其中11项新增覆盖可信响应配额、单来源配额、占用续期、跨类型Tag、异常/复位清理。生产核心ASan/UBSan及精确时钟验证19.999/20ms和续期38.999/39ms边界;本拆分分支单独重编译并运行完整回归,结果见PR自验证报告。
13+- 候选4620c09在173.6.0.14专用43000四卡实测:44次Base/时间/错误码应答成功,28条通知(52帧)全部上报,5次无应答查询均约3.01s超时,NC-SI前后各8次成功;BMC应答桩为空。启动初次探测路由未齐全,重试前保留失败;通知轮库存就绪标志仍为false,不宣告该轮库存验收通过。
14+- 候选原始源码、运行库、安装收据、失败及成功日志位于任务交付目录的`evidence/mctpd-admission-20261026/`。测试后43000已停止、PID0,其他31个容器状态未变,控制台18443和正式39443均HTTP200。
15+ 
16+## 尚未覆盖
17+ 
18+本PR不含PLDM编解码/业务(独立PR !13/!12)。统一异步TX完成、完整跨接口/路由代次、Control专用配额、单包限流、真实NVMe More Processing最终应答和阶段2仍待实现/验收。单元测试中的介质字段不等于实际完整SMBus接口身份;现有Lua等待键尚未完成全部关联维度改造。
19+ 
20+按peer失效接口`invalidate_peer(medium_type, remote, eid)`清理匹配重组并终结对应响应预约,保留其他介质、物理地址及EID。旧Tag保持隔离至原查询截止后再加1秒消息寿命,迟到帧丢弃、旧协程release不能提前解除;隔离记录仍计入64项总预算。后续路由管理调用该接口;本PR不声称已完成上层重绑接线,期限外任意迟到和本端复位后的Tag重用仍需产品时限约束。
21+ 
22+`Send`同样遵守传输Tag占用:8个Tag均忙时明确失败并计入not_send_count,不复用在途查询Tag、不静默丢弃;调用者需要处理背压并按业务策略重试,释放后恢复。
@@ -2,7 +2,7 @@
2 "name": "mctpd",2 "name": "mctpd",
3 "type": "application",3 "type": "application",
4 "deployConfig": "framework.service",4 "deployConfig": "framework.service",
5- "version": "1.130.15",5+ "version": "1.130.16",
6 "author": "Huawei",6 "author": "Huawei",
7 "license": "Mulan PSL v2",7 "license": "Mulan PSL v2",
8 "options": {8 "options": {
@@ -386,6 +386,13 @@ static void register_transport_protocols(lua_State *L, luawrap::stack_table &t)
386 cls.def_property("m_wrong_seq_cnt", c_func_wrap(L, mctp_protocol::get_wrong_seq_cnt),386 cls.def_property("m_wrong_seq_cnt", c_func_wrap(L, mctp_protocol::get_wrong_seq_cnt),
387 c_func_wrap(L, mctp_protocol::set_wrong_seq_cnt));387 c_func_wrap(L, mctp_protocol::set_wrong_seq_cnt));
388 cls.set("set_control_rx_handler", c_func_wrap(L, set_control_rx_handler));388 cls.set("set_control_rx_handler", c_func_wrap(L, set_control_rx_handler));
389+ cls.set("reset_reassembly", c_func_wrap(L, mctp_protocol::reset_reassembly));
390+ cls.set("invalidate_peer", c_func_wrap(L, mctp_protocol::invalidate_peer));
391+ cls.set("reserve_response", c_func_wrap(L, mctp_protocol::reserve_response));
392+ cls.set("response_reserved", c_func_wrap(L, mctp_protocol::response_reserved));
393+ cls.set("renew_response", c_func_wrap(L, mctp_protocol::renew_response));
394+ cls.set("release_response", c_func_wrap(L, mctp_protocol::release_response));
395+ cls.set("expire_reassembly", c_func_wrap(L, mctp_protocol::expire_reassembly));
389 t.set("mctp_protocol", cls);396 t.set("mctp_protocol", cls);
390 });397 });
391}398}
@@ -162,8 +162,7 @@ bool pcie_vdm_protocol::push(const protocol_ctx &ctx, const slice_t &data, const
162 162 
163 m_tx_buf.insert(m_tx_buf.end(), data.begin(), data.end());163 m_tx_buf.insert(m_tx_buf.end(), data.begin(), data.end());
164 if (pad_len > 0) {164 if (pad_len > 0) {
165- m_tx_buf.resize(m_tx_buf.size() + pad_len);165+ m_tx_buf.resize(m_tx_buf.size() + pad_len, 0);
166- std::fill_n(m_tx_buf.begin() + m_tx_buf.size(), pad_len, 0);
167 }166 }
168 167 
169 return protocol::push(ctx, slice(m_tx_buf), medium_type);168 return protocol::push(ctx, slice(m_tx_buf), medium_type);
@@ -12,15 +12,25 @@
12 12 
13#include "mctp_protocol.h"13#include "mctp_protocol.h"
14 14 
15+#include "physical/pcie_vdm_protocol.h"
16+#include "physical/smbus_protocol.h"
17+ 
15namespace {18namespace {
16-const int MCTP_HDR_SEQ_MASK = 3;19+const int MCTP_HDR_SEQ_MASK = 3;
17-const int SMBUS_OEM_TYPE = 0x80;20+const int SMBUS_OEM_TYPE = 0x80;
18-const int SMBUS_OVER_SMC_TYPE = 0x81;21+const int SMBUS_OVER_SMC_TYPE = 0x81;
19-const int SMBUS_OVER_I2COVERLOCALBUS_TYPE = 0x82;22+const int SMBUS_OVER_I2COVERLOCALBUS_TYPE = 0x82;
20-const int SMBUS_TYPE = 0x02;23+const int SMBUS_TYPE = 0x02;
21-const int PCIE_TYPE = 0x0F;24+const int PCIE_TYPE = 0x0F;
22-const int MCTP_VERSION = 1;25+const int MCTP_VERSION = 1;
23-const uint32_t MCTP_RESERVED = 16;26+const uint32_t MCTP_RESERVED = 16;
27+constexpr size_t MAX_REASSEMBLY_ENTRIES = 32;
28+constexpr size_t ORDINARY_REASSEMBLY_ENTRIES = 16;
29+constexpr size_t SOURCE_REASSEMBLY_ENTRIES = 4;
30+constexpr size_t MAX_RESPONSE_RESERVATIONS = 64;
31+constexpr size_t MAX_MESSAGE_SIZE = 4096;
32+constexpr auto INTER_PACKET_TIMEOUT = std::chrono::milliseconds(100);
33+constexpr auto MESSAGE_TIMEOUT = std::chrono::milliseconds(1000);
24} // namespace34} // namespace
25 35 
26using namespace Mctp;36using namespace Mctp;
@@ -43,6 +53,9 @@ mctp_eid_t mctp_protocol::get_eid() const
43 53 
44void mctp_protocol::set_eid(mctp_eid_t eid)54void mctp_protocol::set_eid(mctp_eid_t eid)
45{55{
56+ if (m_eid != eid) {
57+ reset_reassembly();
58+ }
46 m_eid = eid;59 m_eid = eid;
47}60}
48 61 
@@ -53,6 +66,9 @@ mctp_eid_t mctp_protocol::get_pcie_eid() const
53 66 
54void mctp_protocol::set_pcie_eid(mctp_eid_t eid)67void mctp_protocol::set_pcie_eid(mctp_eid_t eid)
55{68{
69+ if (m_pcie_eid != eid) {
70+ reset_reassembly();
L
Lliuxingsen12 days ago

[P2] EID 更新只应清理对应介质的在途状态

PCIe 与 SMBus 共用同一个 mctp_protocol。若 SMBus 查询已收到首片,此时 PCIe 重新发现经 set_bmc_eid 更新本端 EID,这里的 reset_reassembly() 会把两种介质的 m_rx_ctx 和 m_responses 一并清空:随后合法的 SMBus 尾片查不到半包,NVMe More Processing 的预约续期也返回 false,造成无关 SMBus 查询失败;set_smbus_eid 中的同类调用对 PCIe 同样存在影响。本轮用当前 47083fdb 的原始 C++ 做了 ASan/UBSan 对照:SMBus 首片→PCIe EID 10→11→SMBus 尾片,当前提交交付数为 0、预约/续期均为 false;共同基线 0fe122aa 同样操作交付数为 1,当前提交不改 PCIe EID 也交付 1。请按被修改的介质筛选清理重组与预约,将全表清空留给整引擎复位,并补充双向跨介质回归。

likedislike
71+ }
56 m_pcie_eid = eid;72 m_pcie_eid = eid;
57}73}
58 74 
@@ -63,6 +79,9 @@ mctp_eid_t mctp_protocol::get_smbus_eid() const
63 79 
64void mctp_protocol::set_smbus_eid(mctp_eid_t eid)80void mctp_protocol::set_smbus_eid(mctp_eid_t eid)
65{81{
82+ if (m_smbus_eid != eid) {
83+ reset_reassembly();
84+ }
66 m_smbus_eid = eid;85 m_smbus_eid = eid;
67}86}
68 87 
@@ -110,6 +129,134 @@ void mctp_protocol::set_control_rx_handler(std::optional<control_rx_handler_t> h
110{129{
111 m_control_rx = std::move(h);130 m_control_rx = std::move(h);
112}131}
132+ 
133+void mctp_protocol::reset_reassembly()
134+{
135+ m_rx_ctx.clear();
136+ m_responses.clear();
137+}
138+ 
139+void mctp_protocol::invalidate_peer(uint32_t medium_type, uint16_t remote, uint8_t eid)
140+{
141+ expire_reassembly();
142+ const auto matches = [=](const message_key &key) {
143+ return std::get<0>(key) == medium_type && std::get<1>(key) == remote && std::get<3>(key) == eid;
144+ };
145+ for (auto it = m_rx_ctx.begin(); it != m_rx_ctx.end();) {
146+ it = matches(it->first) ? m_rx_ctx.erase(it) : std::next(it);
147+ }
148+ for (auto &entry : m_responses) {
149+ if (matches(entry.first) && !entry.second.retired) {
150+ entry.second.retired = true;
151+ entry.second.deadline += MESSAGE_TIMEOUT;
152+ }
153+ }
154+}
155+ 
156+void mctp_protocol::expire_reassembly()
157+{
158+ const auto now = std::chrono::steady_clock::now();
159+ for (auto it = m_responses.begin(); it != m_responses.end();) {
160+ it = now >= it->second.deadline ? m_responses.erase(it) : std::next(it);
161+ }
162+ for (auto it = m_rx_ctx.begin(); it != m_rx_ctx.end();) {
163+ auto expected = m_responses.find(it->first);
164+ bool cancelled = it->second.reservation &&
165+ (expected == m_responses.end() || expected->second.token != it->second.reservation);
166+ if (cancelled || now - it->second.last_packet >= INTER_PACKET_TIMEOUT ||
O
Oopenubmc-ci-bot13 days ago

💡 建议:250ms 闲置巡检与 100ms 包间阈值不匹配,可能在窗口内误杀在途消息

expire_reassembly 以 now - last_packet >= INTER_PACKET_TIMEOUT(100ms) 判定包间超时并回收半包,但 mctp_engine.lua 的闲置巡检定时器周期是 250ms。于是分片间隔在 100ms~250ms 之间(仍在 1s 总期限与协议合法窗口内)的在途消息,会在下一次 250ms tick 被巡检直接回收;而对端此时只要再发一帧就能收完。

影响:忙碌控制总线或慢速对端,其分片间隔略大于 100ms 时,即使总时长远小于 1s,也会被巡检误杀,表现为"报文莫名收不齐"。

修复建议:将闲置巡检周期与 INTER_PACKET_TIMEOUT 对齐(例如 ≤100ms,或每 tick 精确检查),或在文档中明确"在途重组对分片的实际容忍上限约为 100ms"这一事实,避免运维按 1s 总期限误判。

likedislike
liuxingsen
13 days ago Comments:
167+ now - it->second.started >= MESSAGE_TIMEOUT) {
168+ it = m_rx_ctx.erase(it);
169+ } else {
170+ ++it;
171+ }
172+ }
173+}
174+ 
175+message_key mctp_protocol::response_key(const protocol_ctx &ctx, uint32_t medium_type) const
176+{
177+ const auto &out = ctx.cast<mctp_ctx>();
178+ mctp_hdr hdr{};
179+ hdr.u.src = out.dest;
180+ hdr.u.dest = out.src;
181+ hdr.u.msg_tag = out.tag;
182+ return make_msg_id(ctx, hdr, medium_type);
183+}
184+ 
185+bool mctp_protocol::response_reserved(const protocol_ctx &ctx, uint32_t medium_type)
186+{
187+ expire_reassembly();
188+ return m_responses.count(response_key(ctx, medium_type)) != 0;
189+}
190+ 
191+uint64_t mctp_protocol::reserve_response(const protocol_ctx &ctx, uint32_t medium_type, uint8_t message_type,
192+ uint32_t timeout_ms)
193+{
194+ expire_reassembly();
195+ const auto &out = ctx.cast<mctp_ctx>();
196+ const auto key = response_key(ctx, medium_type);
197+ if (!out.tag_owner || out.tag > 7 || message_type > 0x7f || !timeout_ms ||
198+ m_responses.size() >= MAX_RESPONSE_RESERVATIONS || m_responses.count(key)) {
199+ return 0;
200+ }
201+ if (++m_next_reservation == 0) {
202+ ++m_next_reservation;
203+ }
204+ m_responses.emplace(key,
205+ response_reservation{m_next_reservation, message_type,
206+ std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms)});
207+ return m_next_reservation;
208+}
209+ 
210+void mctp_protocol::release_response(uint64_t token)
211+{
212+ for (auto it = m_responses.begin(); it != m_responses.end(); ++it) {
213+ if (it->second.token == token) {
214+ if (it->second.retired) {
215+ return; // 旧协程清理不能提前解除Tag隔离;仍计入64项总预算。
216+ }
217+ auto rx = m_rx_ctx.find(it->first);
218+ if (rx != m_rx_ctx.end() && rx->second.reservation == token) {
219+ m_rx_ctx.erase(rx);
220+ }
221+ m_responses.erase(it);
222+ return;
223+ }
224+ }
225+}
226+ 
227+bool mctp_protocol::renew_response(uint64_t token, uint32_t timeout_ms)
228+{
229+ expire_reassembly();
230+ if (!timeout_ms) {
231+ return false;
232+ }
233+ for (auto &entry : m_responses) {
234+ if (entry.second.token == token && !entry.second.retired) {
235+ entry.second.deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms);
236+ return true;
237+ }
238+ }
239+ return false;
240+}
241+ 
242+message_key mctp_protocol::make_msg_id(const protocol_ctx &ctx, const mctp_hdr &hdr, uint32_t medium_type) const
243+{
244+ uint16_t remote = 0;
245+ uint16_t local = 0;
246+ for (const protocol_ctx *p = &ctx; p != nullptr; p = p->get_prev()) {
247+ if (const auto *pcie = dynamic_cast<const pcie_vdm_ctx *>(p)) {
248+ remote = pcie->remote_phy_addr;
249+ local = pcie->bmc_phy_addr;
250+ break;
251+ }
252+ if (const auto *smbus = dynamic_cast<const sm_bus_ctx *>(p)) {
253+ remote = smbus->remote_phy_addr;
254+ local = smbus->bmc_phy_addr;
255+ break;
256+ }
257+ }
258+ return {medium_type, remote, local, hdr.u.src, hdr.u.dest, hdr.u.msg_tag, hdr.u.to};
259+}
113bool is_smbus_medium_type(const uint32_t medium_type)260bool is_smbus_medium_type(const uint32_t medium_type)
114{261{
115 return (medium_type == SMBUS_TYPE || medium_type == SMBUS_OEM_TYPE || medium_type == SMBUS_OVER_SMC_TYPE ||262 return (medium_type == SMBUS_TYPE || medium_type == SMBUS_OEM_TYPE || medium_type == SMBUS_OVER_SMC_TYPE ||
@@ -117,6 +264,7 @@ bool is_smbus_medium_type(const uint32_t medium_type)
117}264}
118bool mctp_protocol::pop(const protocol_ctx &ctx, const slice_t &data, const uint32_t medium_type)265bool mctp_protocol::pop(const protocol_ctx &ctx, const slice_t &data, const uint32_t medium_type)
119{266{
267+ expire_reassembly();
120 auto mctp_hdr_len = sizeof(mctp_hdr);268 auto mctp_hdr_len = sizeof(mctp_hdr);
121#ifdef CONAN_DEFS_CHIPV2_ENABLE269#ifdef CONAN_DEFS_CHIPV2_ENABLE
122 if (is_smbus_medium_type(medium_type)) {270 if (is_smbus_medium_type(medium_type)) {
@@ -124,7 +272,8 @@ bool mctp_protocol::pop(const protocol_ctx &ctx, const slice_t &data, const uint
124 }272 }
125#endif273#endif
126 274 
127- if (data.size() < mctp_hdr_len) {275+ if (data.size() <= mctp_hdr_len) {
OO
Oopenubmc-ci-bot14 days ago

💡 建议:header-only MCTP 报文被静默丢弃,建议补充日志便于诊断

本次将 pop() 的长度判断由 data.size() < mctp_hdr_len 收紧为 data.size() <= mctp_hdr_len,即负载为空的报文(仅 MCTP 基头、无消息类型字节)现在会在进入 process 之前被无条件 return false 丢弃,且未输出任何日志。

影响:上层 general/PLDM/NC-SI 都要求负载首字节为消息类型,空负载报文本身无法解析,因此该收紧在多数场景下是合理加固;但如果某些对端(如特定控制报文或异常固件)会发出空负载报头,这种丢弃将是静默的,排查时缺乏线索。

修复建议:在 return false 前补一条 debug_log(DLOG_DEBUG/ERROR, ...)(附带 src/dest/tag/medium),既能保留本次的防御性收紧,又避免以后线上出现"报文莫名消失"时无从追踪。

likedislike
Oopenubmc-ci-bot14 days ago

⚠️ 警告:header-only 报文被静默丢弃,此前标记"已修复"的日志实际并未在头版本出现

此前的 openubmc-ci-bot 检视曾标记该项为已修复并声明"已在 mctp_protocol::pop 的 return false 前补 debug_log",但当前头版本 af4c72b 本行(data.size() <= mctp_hdr_len)仅 return false,grep 显示该分支下并无任何 debug_log。即 header-only(无消息类型字节)报文仍被无痕丢弃。

影响:异常固件或特定控制报文发出空负载 MCTP 包时,排查"报文莫名不到达"仍缺线索。

修复建议:在此 return false 前补一条 debug_log(DLOG_DEBUG/ERROR, ...)(含 medium/src/dest/tag),与 process_eom/process_other 超限分支的日志风格保持一致。

likedislike
liuxingsen
14 days ago Comments:
276+ debug_log(DLOG_DEBUG, "mctp_protocol: short packet medium=%u bytes=%zu", medium_type, data.size());
128 return false;277 return false;
129 }278 }
130 279 
@@ -142,10 +291,20 @@ bool mctp_protocol::pop(const protocol_ctx &ctx, const slice_t &data, const uint
142 return false;291 return false;
143 }292 }
144 293 
145- bool som = hdr.u.som;294+ bool som = hdr.u.som;
146- bool eom = hdr.u.eom;295+ bool eom = hdr.u.eom;
296+ const auto expected = m_responses.find(make_msg_id(ctx, hdr, medium_type));
297+ if (expected != m_responses.end() && expected->second.retired) {
298+ return false;
299+ }
147 if (som && eom) {300 if (som && eom) {
148- return process(ctx, hdr, slice(data, mctp_hdr_len), medium_type);301+ m_rx_ctx.erase(make_msg_id(ctx, hdr, medium_type));
302+ if (data.size() - mctp_hdr_len > MAX_MESSAGE_SIZE) {
303+ debug_log(DLOG_DEBUG, "mctp_protocol: oversized message, medium=%u, src=%u, dest=%u, tag=%u, size=%zu",
304+ medium_type, hdr.u.src, hdr.u.dest, hdr.u.msg_tag, data.size() - mctp_hdr_len);
305+ return false;
306+ }
307+ return process(ctx, hdr, data.substr(mctp_hdr_len), medium_type);
149 } else if (som) {308 } else if (som) {
150 return process_som(ctx, hdr, data, medium_type, mctp_hdr_len);309 return process_som(ctx, hdr, data, medium_type, mctp_hdr_len);
151 } else if (eom) {310 } else if (eom) {
@@ -155,15 +314,49 @@ bool mctp_protocol::pop(const protocol_ctx &ctx, const slice_t &data, const uint
155 }314 }
156}315}
157 316 
158-bool mctp_protocol::process_som(const protocol_ctx &, const mctp_hdr &hdr, const slice_t &data,317+bool mctp_protocol::process_som(const protocol_ctx &ctx, const mctp_hdr &hdr, const slice_t &data,
159 const uint32_t medium_type, const uint32_t mctp_hdr_len)318 const uint32_t medium_type, const uint32_t mctp_hdr_len)
160{319{
161- uint32_t id = make_msg_id(hdr.u.src, hdr.u.dest, hdr.u.msg_tag);320+ const auto id = make_msg_id(ctx, hdr, medium_type);
162- auto it = m_rx_ctx.try_emplace(id);321+ const auto retained = m_rx_ctx.size() - m_rx_ctx.count(id);
163- auto &pkg = it.first->second;322+ if (data.size() - mctp_hdr_len > MAX_MESSAGE_SIZE || retained >= MAX_REASSEMBLY_ENTRIES) {
164- if (!it.second) {323+ debug_log(DLOG_DEBUG,
165- pkg.m_buf.clear();324+ "mctp_protocol: reassembly limit, medium=%u, src=%u, dest=%u, tag=%u, size=%zu, entries=%zu",
325+ medium_type, hdr.u.src, hdr.u.dest, hdr.u.msg_tag, data.size() - mctp_hdr_len, m_rx_ctx.size());
326+ return false;
166 }327 }
328+ // 仅本端登记的完整预期传输键与消息类型可使用响应保留区;TO=0 本身不授予优先级。
329+ const auto expected = m_responses.find(id);
330+ const uint64_t reservation =
331+ expected != m_responses.end() && expected->second.message_type == (data[mctp_hdr_len] & 0x7f)
332+ ? expected->second.token
333+ : 0;
334+ size_t ordinary = 0;
335+ size_t source = 0;
336+ for (const auto &entry : m_rx_ctx) {
337+ if (entry.first == id) {
338+ continue;
339+ }
340+ if (!entry.second.reservation) {
341+ ++ordinary;
342+ const auto &key = entry.first;
343+ if (std::get<0>(key) == std::get<0>(id) && std::get<1>(key) == std::get<1>(id) &&
344+ std::get<2>(key) == std::get<2>(id) && std::get<3>(key) == std::get<3>(id)) {
345+ ++source;
346+ }
347+ }
348+ }
349+ if (reservation ? retained - ordinary >= MAX_REASSEMBLY_ENTRIES - ORDINARY_REASSEMBLY_ENTRIES
350+ : ordinary >= ORDINARY_REASSEMBLY_ENTRIES || source >= SOURCE_REASSEMBLY_ENTRIES) {
351+ debug_log(DLOG_DEBUG, "mctp_protocol: reassembly admission quota, medium=%u, src=%u, tag=%u", medium_type,
352+ hdr.u.src, hdr.u.msg_tag);
353+ return false;
354+ }
355+ m_rx_ctx.erase(id); // 只有通过准入的新SOM才替换同键半包。
356+ auto &pkg = m_rx_ctx.try_emplace(id).first->second;
357+ pkg.reservation = reservation;
358+ pkg.started = pkg.last_packet = std::chrono::steady_clock::now();
359+ pkg.m_buf.reserve(MAX_MESSAGE_SIZE);
167 360 
168 // 保存包大小,用于校验中间的包大小一致361 // 保存包大小,用于校验中间的包大小一致
169 pkg.fragment_size = data.size();362 pkg.fragment_size = data.size();
@@ -175,8 +368,8 @@ bool mctp_protocol::process_som(const protocol_ctx &, const mctp_hdr &hdr, const
175bool mctp_protocol::process_eom(const protocol_ctx &ctx, const mctp_hdr &hdr, const slice_t &data,368bool mctp_protocol::process_eom(const protocol_ctx &ctx, const mctp_hdr &hdr, const slice_t &data,
176 const uint32_t medium_type, const uint32_t mctp_hdr_len)369 const uint32_t medium_type, const uint32_t mctp_hdr_len)
177{370{
178- uint32_t id = make_msg_id(hdr.u.src, hdr.u.dest, hdr.u.msg_tag);371+ const auto id = make_msg_id(ctx, hdr, medium_type);
179- auto it = m_rx_ctx.find(id);372+ auto it = m_rx_ctx.find(id);
180 if (it == m_rx_ctx.end()) {373 if (it == m_rx_ctx.end()) {
181 return false;374 return false;
182 }375 }
@@ -196,8 +389,14 @@ bool mctp_protocol::process_eom(const protocol_ctx &ctx, const mctp_hdr &hdr, co
196 }389 }
197 390 
198 if (data.size() > pkg.fragment_size) {391 if (data.size() > pkg.fragment_size) {
199- debug_log(DLOG_ERROR, "mctp_protocol: unexpected fragment size, expected lt %u, received %u", pkg.fragment_size,392+ debug_log(DLOG_ERROR, "mctp_protocol: unexpected fragment size, expected <= %zu, received %zu",
200- data.size());393+ pkg.fragment_size, data.size());
394+ m_rx_ctx.erase(it);
395+ return false;
396+ }
397+ if (data.size() - mctp_hdr_len > MAX_MESSAGE_SIZE - pkg.m_buf.size()) {
398+ debug_log(DLOG_ERROR, "mctp_protocol: message exceeds %zu bytes", MAX_MESSAGE_SIZE);
399+ m_rx_ctx.erase(it);
201 return false;400 return false;
202 }401 }
203 402 
@@ -208,11 +407,11 @@ bool mctp_protocol::process_eom(const protocol_ctx &ctx, const mctp_hdr &hdr, co
208 return process(ctx, hdr, slice(buf), medium_type);407 return process(ctx, hdr, slice(buf), medium_type);
209}408}
210 409 
211-bool mctp_protocol::process_other(const protocol_ctx &, const mctp_hdr &hdr, const slice_t &data,410+bool mctp_protocol::process_other(const protocol_ctx &ctx, const mctp_hdr &hdr, const slice_t &data,
212 const uint32_t medium_type, const uint32_t mctp_hdr_len)411 const uint32_t medium_type, const uint32_t mctp_hdr_len)
213{412{
214- uint32_t id = make_msg_id(hdr.u.src, hdr.u.dest, hdr.u.msg_tag);413+ const auto id = make_msg_id(ctx, hdr, medium_type);
215- auto it = m_rx_ctx.find(id);414+ auto it = m_rx_ctx.find(id);
216 if (it == m_rx_ctx.end()) {415 if (it == m_rx_ctx.end()) {
217 return false;416 return false;
218 }417 }
@@ -232,12 +431,19 @@ bool mctp_protocol::process_other(const protocol_ctx &, const mctp_hdr &hdr, con
232 }431 }
233 432 
234 if (data.size() != local_ctx.fragment_size) {433 if (data.size() != local_ctx.fragment_size) {
235- debug_log(DLOG_ERROR, "mctp_protocol: unexpected fragment size, expected eq %u, received %u",434+ debug_log(DLOG_ERROR, "mctp_protocol: unexpected fragment size, expected eq %zu, received %zu",
236 local_ctx.fragment_size, data.size());435 local_ctx.fragment_size, data.size());
436+ m_rx_ctx.erase(it);
437+ return false;
438+ }
439+ if (data.size() - mctp_hdr_len > MAX_MESSAGE_SIZE - local_ctx.m_buf.size()) {
440+ debug_log(DLOG_ERROR, "mctp_protocol: message exceeds %zu bytes", MAX_MESSAGE_SIZE);
441+ m_rx_ctx.erase(it);
237 return false;442 return false;
238 }443 }
239 444 
240- local_ctx.last_seq = static_cast<uint8_t>(exp_seq);445+ local_ctx.last_seq = static_cast<uint8_t>(exp_seq);
446+ local_ctx.last_packet = std::chrono::steady_clock::now();
241 local_ctx.m_buf.insert(local_ctx.m_buf.end(), data.begin() + mctp_hdr_len, data.end());447 local_ctx.m_buf.insert(local_ctx.m_buf.end(), data.begin() + mctp_hdr_len, data.end());
242 return true;448 return true;
243}449}
@@ -11,7 +11,9 @@
11 */11 */
12#ifndef MCTP_PROTOCOL_H12#ifndef MCTP_PROTOCOL_H
13#define MCTP_PROTOCOL_H13#define MCTP_PROTOCOL_H
14+#include <chrono>
14#include <map>15#include <map>
16+#include <tuple>
15 17 
16#include "mctp_cmds.h"18#include "mctp_cmds.h"
17#include "protocol.h"19#include "protocol.h"
@@ -76,9 +78,21 @@ enum class mctp_route_policy {
76};78};
77 79 
78struct package_ctx {80struct package_ctx {
79- uint8_t last_seq;81+ uint64_t reservation = 0;
80- size_t fragment_size;82+ uint8_t last_seq;
81- buffer_t m_buf;83+ size_t fragment_size;
84+ buffer_t m_buf;
85+ std::chrono::steady_clock::time_point started;
86+ std::chrono::steady_clock::time_point last_packet;
87+};
88+ 
89+using message_key = std::tuple<uint32_t, uint16_t, uint16_t, uint8_t, uint8_t, uint8_t, bool>;
90+ 
91+struct response_reservation {
92+ uint64_t token;
93+ uint8_t message_type;
94+ std::chrono::steady_clock::time_point deadline;
95+ bool retired = false;
82};96};
83 97 
84using control_rx_handler_t = std::function<bool(const protocol_ctx &, const mctp_ctrl_msg &, size_t, const int &)>;98using control_rx_handler_t = std::function<bool(const protocol_ctx &, const mctp_ctrl_msg &, size_t, const int &)>;
@@ -103,7 +117,14 @@ public:
103 uint32_t get_wrong_seq_cnt() const;117 uint32_t get_wrong_seq_cnt() const;
104 void set_wrong_seq_cnt(uint32_t wrong_seq_cnt);118 void set_wrong_seq_cnt(uint32_t wrong_seq_cnt);
105 119 
106- void set_control_rx_handler(std::optional<control_rx_handler_t> h);120+ void set_control_rx_handler(std::optional<control_rx_handler_t> h);
121+ void reset_reassembly();
122+ void invalidate_peer(uint32_t medium_type, uint16_t remote, uint8_t eid);
123+ void expire_reassembly();
124+ uint64_t reserve_response(const protocol_ctx &ctx, uint32_t medium_type, uint8_t message_type, uint32_t timeout_ms);
125+ bool response_reserved(const protocol_ctx &ctx, uint32_t medium_type);
126+ bool renew_response(uint64_t token, uint32_t timeout_ms);
127+ void release_response(uint64_t token);
107 128 
108 mctp_route_policy get_route_policy() const;129 mctp_route_policy get_route_policy() const;
109 void set_route_policy(mctp_route_policy rp);130 void set_route_policy(mctp_route_policy rp);
@@ -123,14 +144,8 @@ protected:
123 bool process_ctrl_msg(const protocol_ctx &ctx, const slice_t &data, const uint32_t medium_type);144 bool process_ctrl_msg(const protocol_ctx &ctx, const slice_t &data, const uint32_t medium_type);
124 bool is_ctrl_message(const slice_t &data);145 bool is_ctrl_message(const slice_t &data);
125 146 
126- inline uint32_t make_msg_id(uint8_t src, uint8_t dest, uint8_t tag)147+ message_key make_msg_id(const protocol_ctx &ctx, const mctp_hdr &hdr, uint32_t medium_type) const;
127- {148+ message_key response_key(const protocol_ctx &ctx, uint32_t medium_type) const;
128- mctp_hdr h = {.v = 0};
129- h.u.src = src;
130- h.u.dest = dest;
131- h.u.msg_tag = tag;
132- return h.v;
133- }
134 149 
135private:150private:
136 uint8_t m_version;151 uint8_t m_version;
@@ -141,8 +156,10 @@ private:
141 uint32_t m_wrong_seq_cnt;156 uint32_t m_wrong_seq_cnt;
142 mctp_route_policy m_route_policy;157 mctp_route_policy m_route_policy;
143 158 
144- buffer_t m_tx_buf;159+ buffer_t m_tx_buf;
145- std::map<uint32_t, package_ctx> m_rx_ctx;160+ std::map<message_key, package_ctx> m_rx_ctx;
161+ std::map<message_key, response_reservation> m_responses;
162+ uint64_t m_next_reservation = 0;
146 163 
147 std::optional<control_rx_handler_t> m_control_rx;164 std::optional<control_rx_handler_t> m_control_rx;
148};165};
@@ -9,6 +9,7 @@
9-- See the Mulan PSL v2 for more details.9-- See the Mulan PSL v2 for more details.
10 10 
11local class = require('mc.class')11local class = require('mc.class')
12+local log = require('mc.logging')
12local mctp = require('mctp')13local mctp = require('mctp')
13local c_ep_mctp = require('mctp_endpoints.ep_mctp')14local c_ep_mctp = require('mctp_endpoints.ep_mctp')
14 15 
@@ -62,7 +63,17 @@ function c_general_endpoint:send(_, data, req_ctx)
62end63end
63 64 
64function c_general_endpoint:process(_, data)65function c_general_endpoint:process(_, data)
65- self.mctp_engine:wakeup_request(self:make_rsp_id(nil), data)66+ if not self.mctp_ctx.tag_owner then
O
Oopenubmc-ci-bot14 days ago

💡 建议:通用报文中未匹配的 Tag 拥有方报文被静默丢弃,与 VDPCI 的通知契约不一致

process 在 tag_owner 为真(即对端发送的主动/Datagram 方向报文)时会跳过 wakeup_request,但之后没有像 ep_vdpci_endpoint 那样把未匹配报文交给 on_receive 通知,而是直接 return true 丢弃。

影响:当对端向通用端点发送一条本端未发起查询的主动报文时,该报文既不会完成 requester,也不会作为通知被上层感知,属于无痕丢失。VDPCI(ep_vdpci_endpoint.lua)对此保留了 on_receive:emit(data) 的既有通知契约,而 general 端点没有同等回退,行为不一致,线上排查"报文莫名消失"缺少该路径。

修复建议(任选其一):

  • 与 VDPCI 对齐,在 return true 前按既有通知机制透传未匹配报文;
  • 或在注释/文档中明确 general 端点不保留主动报文的通知契约,并在丢弃前补一条 DEBUG 日志便于诊断。

属可选优化,不阻塞合入。

likedislike
67+ self.mctp_engine:wakeup_request(self:make_rsp_id(nil), data)
68+ else
69+ -- 通用端点没有 VDPCI 的主动通知契约,仅消费查询响应。
70+ log:debug(
71+ 'general endpoint: discard unsolicited request, type=%d src=%d tag=%d',
72+ self:get_msg_type(),
73+ self.mctp_ctx.src,
74+ self.mctp_ctx.tag
75+ )
76+ end
66 return true77 return true
67end78end
68 79 
@@ -91,10 +91,23 @@ end
91 91 
92function c_ep_mctp:request(id, cb, timeout_ms, more_process_cb)92function c_ep_mctp:request(id, cb, timeout_ms, more_process_cb)
93 self.req_total_count = self.req_total_count + 193 self.req_total_count = self.req_total_count + 1
94+ -- Tag检查到C++预约之间不yield;64项池满时拒绝发送,换Tag不能增加全局容量。
94 local key = self:prepare_mctp_request(id)95 local key = self:prepare_mctp_request(id)
96+ timeout_ms = timeout_ms and timeout_ms > 0 and math.max(1, math.floor(timeout_ms)) or 15000
97+ local token =
98+ self.mctp_engine.mctp_proto:reserve_response(self.mctp_ctx, self.medium_type, self:get_msg_type(), timeout_ms)
O
Oopenubmc-ci-bot14 days ago

⚠️ 警告:Tag 选择与预期响应预约之间存在竞惹窗口(TOCTOU),且未核查 64 项预约池上限

prepare_mctp_request (本行 159)中只验证了 not response_reserved(...)(单键是否已预约),并未验证 MAX_RESPONSE_RESERVATIONS=64 的全局容量。下一步在 request 中直接调用 reserve_response(第 96<行)时,若本次选择的 tag 在颓发多请求并行场景下被其他协程抢先占据,或预约池已满(64 项),reserve_response 会返回 0,由此直接 error('response reservation unavailable')而不重试其他 tag。

影响:较高并发请求下(如多模块同时发起查询)可能出现偶然的“预约不可用”失败,且不会自动重试,对上层呈现为可勿的发送失败。

修复建议:将“tag 选择 + 预约”合并为原子操作(例如在 C++ 个提供一个 “选择剩余 tag 并占用预约”的组合 API),或在 reserve_response 返回 0 时切换到下一个 tag 重试,而不是直接报错。

likedislike
liuxingsen
14 days ago Comments:
99+ if token == 0 then
100+ self.not_send_count = self.not_send_count + 1
101+ error('request failed: response reservation unavailable')
102+ end
95 -- 暂存本次请求的rsp key,smbus发送时随请求带给service,失败回传后用于完整匹配唤醒103 -- 暂存本次请求的rsp key,smbus发送时随请求带给service,失败回传后用于完整匹配唤醒
96 self.cur_rsp_key = key104 self.cur_rsp_key = key
97- local ret, rsp = self.mctp_engine:request(key, cb, timeout_ms, more_process_cb)105+ local ok, ret, rsp = pcall(self.mctp_engine.request, self.mctp_engine, key, cb, timeout_ms, more_process_cb, token)
106+ self.mctp_engine.mctp_proto:release_response(token)
107+ if not ok then
108+ self.req_error_count = self.req_error_count + 1
109+ error(ret)
110+ end
98 if ret == E_TIMEOUT then111 if ret == E_TIMEOUT then
99 self.req_timeout_count = self.req_timeout_count + 1112 self.req_timeout_count = self.req_timeout_count + 1
100 elseif ret == E_ERROR then113 elseif ret == E_ERROR then
@@ -115,21 +128,18 @@ function c_ep_mctp:send(cb)
115 self.req_total_count = self.req_total_count + 1128 self.req_total_count = self.req_total_count + 1
116 -- 通知类发送不等待响应,清空rsp key,避免发送时携带上一个请求的旧key129 -- 通知类发送不等待响应,清空rsp key,避免发送时携带上一个请求的旧key
117 self.cur_rsp_key = nil130 self.cur_rsp_key = nil
118- self:prepare_mctp_request()131+ local ok, err = pcall(self.prepare_mctp_request, self)
132+ if not ok then
133+ self.not_send_count = self.not_send_count + 1
134+ error(err) -- 明确背压;不复用查询占用的Tag,也不把丢弃当成功。
135+ end
119 cb()136 cb()
120 self.req_success_count = self.req_success_count + 1137 self.req_success_count = self.req_success_count + 1
121end138end
122 139 
123function c_ep_mctp:prepare_mctp_request(id)140function c_ep_mctp:prepare_mctp_request(id)
124 local mctp_engine = self.mctp_engine141 local mctp_engine = self.mctp_engine
125- local cnt = 0
126 local mctp_ctx = self.mctp_ctx142 local mctp_ctx = self.mctp_ctx
127- local tag, key
128- repeat
129- tag = self:get_msg_tag()
130- key = make_key(self.eid, self.phy_addr, tag, id)
131- cnt = cnt + 1
132- until not mctp_engine.rsp_wait_list[key] or cnt > GET_TAG_MAX_RETRY_TIMES
133 143 
134 if mctp_def.s_match(mctp_def.medium_name[self.medium_type], 'SMBus') then144 if mctp_def.s_match(mctp_def.medium_name[self.medium_type], 'SMBus') then
135 mctp_engine:smbus_binding_params_set()145 mctp_engine:smbus_binding_params_set()
@@ -140,13 +150,23 @@ function c_ep_mctp:prepare_mctp_request(id)
140 end150 end
141 mctp_ctx.dest = self.eid151 mctp_ctx.dest = self.eid
142 mctp_ctx.tag_owner = true152 mctp_ctx.tag_owner = true
143- mctp_ctx.tag = tag
144 if mctp_def.s_match(mctp_def.medium_name[self.medium_type], 'SMBus') then153 if mctp_def.s_match(mctp_def.medium_name[self.medium_type], 'SMBus') then
145 self:add_sm_bus_ctx(self.sm_bus_ctx, mctp_engine)154 self:add_sm_bus_ctx(self.sm_bus_ctx, mctp_engine)
146 else155 else
147 self:add_pcie_vdm_ctx(self.pcie_vdm_ctx, mctp_engine)156 self:add_pcie_vdm_ctx(self.pcie_vdm_ctx, mctp_engine)
148 end157 end
149- return key158+ for _ = 1, GET_TAG_MAX_RETRY_TIMES do
159+ local tag = self:get_msg_tag()
160+ mctp_ctx.tag = tag
161+ local key = make_key(self.eid, self.phy_addr, tag, id)
162+ if
163+ not mctp_engine.rsp_wait_list[key]
164+ and not mctp_engine.mctp_proto:response_reserved(mctp_ctx, self.medium_type)
165+ then
166+ return key
167+ end
168+ end
169+ error('request failed: all message tags are occupied')
O
Oopenubmc-ci-bot13 days ago

⚠️ 警告:send() 通知路径也会进入 Tag 耗尽分支并抛异常,此前仅查询路径有该限制

prepare_mctp_request(本方法)新增的 Tag 挑选循环在 8 个 Tag 全被占用时执行 error('request failed: all message tags are occupied')。该分支对查询请求(request)是合理的有界拒绝(PR 描述已声明)。但 c_ep_mctp:send(本文件 :127,通知/主动发送路径)会直接调用本方法且不包 pcall,NC-SI/NVMe/general 的 send 也都经由 super.send 走到这里。

当同一路由上并发挂起多个查询(持有其预约/等待项)导致 8 个 Tag 暂时占满时,一次普通的主动/通知发送会抛出未捕获的异常,而旧实现中该路径从不因 Tag 被占用而失败(只会静默取一个 Tag)。

修复建议:为通知发送路径与查询请求区分 Tag 占用检查的严格性,例如让 send 不因 Tag 耗尽而 throw,而是复用当前 get_msg_tag() 的宽松选择(或失败时降级为 DEBUG 日志丢弃),仅让需要响应预约的 request 路径保留有界拒绝的 error。

属并发场景下的可观测故障风险,建议评估,不阻塞合入。

likedislike
liuxingsen
13 days ago Comments:
150end170end
151 171 
152-- 物理介质是pcie, 增加协议头172-- 物理介质是pcie, 增加协议头
@@ -118,6 +118,9 @@ function c_ncsi_endpoint:get_iid()
118end118end
119 119 
120function c_ncsi_endpoint:process(_, data)120function c_ncsi_endpoint:process(_, data)
121+ if self.mctp_ctx.tag_owner then
O
Oopenubmc-ci-bot14 days ago

💡 建议:NC-SI 对 tag_owner 报文的丢弃缺少诊断日志,与本次 general 端点新增的日志不一致

本 PR 在 ep_general.lua:process 中对同类“对端主动/通知方向”报文增加了一条 DEBUG 日志(含 type/src/tag),但 ep_ncsi.lua:process 这里对 tag_owner 为真的主动报文只是 return true 静默丢弃,没有任何日志,也与 ep_vdpci_endpoint 保留的 on_receive 通知契约不对齐。

影响:当对端发出未匹配本端查询的主动 NC-SI 报文时,该报文被无痕丢弃,线上排查“报文莫名不到达”缺少同 general 端点一致的可观测线索。

修复建议:与本次 ep_general.lua 的做法对齐,在 return true 前补一条 log:debug(...)(含 iid/type/src/tag),或在文档中明确 NC-SI 不保留主动报文通知契约。属可观测性优化,不阻塞合入。

likedislike
liuxingsen
13 days ago Comments:
122+ return true
123+ end
121 local ncsi_ctx = self.ncsi_ctx124 local ncsi_ctx = self.ncsi_ctx
122 local iid = ncsi_ctx.iid125 local iid = ncsi_ctx.iid
123 self.mctp_engine:wakeup_request(self:make_rsp_id(iid), data)126 self.mctp_engine:wakeup_request(self:make_rsp_id(iid), data)
@@ -51,8 +51,9 @@ function c_vdpci_endpoint:send(_, data, req_ctx)
51end51end
52 52 
53function c_vdpci_endpoint:process(_, data)53function c_vdpci_endpoint:process(_, data)
54- local ok = self.mctp_engine:wakeup_request(self:make_rsp_id(nil), data)54+ -- 对端拥有 Tag 的通知不能完成本端 requester,即使双方恰好使用相同 Tag。
55- -- 返回false代表没有对应的请求,通过信号的方式将接收的数据发送出去55+ local ok = not self.mctp_ctx.tag_owner and self.mctp_engine:wakeup_request(self:make_rsp_id(nil), data)
56+ -- 保留既有未匹配报文的通知契约;此处不把通知转换为需要回复的请求。
atomgit-bot
atomgit-botatomgit-bot14 days ago

🔵 Low Priority

本行把“只有 TO=0(对端不是 Tag 拥有者,即响应方向)才允许 wakeup_request”确立为 endpoint 处理接收报文的约定,ep_pldm.lua:120 同步采用;但同一约定下的兄弟实现仍是旧形式:ep_ncsi.lua:120-124 wakeup_request(self:make_rsp_id(iid), data)、ep_general.lua:64-67 wakeup_request(self:make_rsp_id(nil), data),都只按 Tag/IID 匹配等待项,没有任何方向判断。

失败模式:以本 PR 修复的同一机制,对端 TO=1 的主动报文只要恰好使用与本端待办查询相同的 Tag(NC-SI 按 IID、general 按 Tag),就会把该等待项唤醒并塞入通知数据,造成查询拿到错误响应、通知丢失——正是 issue #11 记录的“28 条通知只上报 26 条”同类缺陷,只是换到了 NC-SI / 通用消息类型上。

这是可选的后续一致性改进(本 PR 已在描述中限定范围),不是本次必须修复项;若要闭环该约定,建议把 not ctx.tag_owner(依据 rx 时 copy_ctxs 复制后的 mctp_ctx)抽成一个共用判断,供 ncsi/general 的 process 复用。

建议:在 c_ep_mctp 中提供一个共享辅助方法(如 is_peer_initiated(),读取 rx 时复制进 self.mctp_ctx 的 tag_owner),ep_pldm/ep_vdpci/ep_ncsi/ep_general 的 process 统一用它做方向过滤,避免同一契约在不同 endpoint 上出现两种实现。

likedislike
Incorrect?
liuxingsen
14 days ago Comments:
56 if not ok then57 if not ok then
57 self.on_receive:emit(data)58 self.on_receive:emit(data)
58 end59 end
@@ -235,10 +235,37 @@ function c_mctp_engine:start()
235 self.hw_pcie_proto:vdm_engine_reinit()235 self.hw_pcie_proto:vdm_engine_reinit()
236 self:register_control()236 self:register_control()
237 self:start_worker(fd)237 self:start_worker(fd)
238+ self:start_reassembly_timer()
238 log:notice('[System%s]mctp_engine: open mctp device success', self.system_id)239 log:notice('[System%s]mctp_engine: open mctp device success', self.system_id)
239end240end
240 241 
242+function c_mctp_engine:start_reassembly_timer()
243+ if self.reassembly_timer then
244+ stop_timer(self.reassembly_timer)
245+ end
246+ local generation = {}
247+ self.reassembly_generation = generation
248+ local function expire()
249+ if self.reassembly_generation ~= generation then
250+ return
251+ end
252+ self.mctp_proto:expire_reassembly()
O
Oopenubmc-ci-bot14 days ago

💡 建议:重组过期由空闲定时器与每次 pop 双重触发,语义可进一步收敛

expire_reassembly() 被两处触发:250ms 空闲定时器(expire_reassembly callback)和每次 pop() 开头(mctp_protocol.cpp:172)。后者保证"包间 100ms"在持续收包时被精确执行,前者只在无收包时兜底。

观察:当流量完全停止时,一条未完成分片在 250ms 定时器触发前最多可留存约 250ms(超过其本应 100ms 的包间时限,但仍在 1s 总期限内,且 expire_reassembly 的双条件 \|\| 会在 1s 时兜底清除)。这属于设计内的容忍,不构成缺陷。

说明/建议:可考虑让定时器周期(250ms)与 INTER_PACKET_TIMEOUT(100ms)保持一致的粒度,或将定时器回调拆成"空闲回收"与"收包驱动"两条清晰路径并各自注释,避免后续读者误解 100ms/250ms 的实际语义差异。属可选优化项。

likedislike
253+ self.reassembly_timer = c_timer.new(250, expire)
254+ end
255+ self.reassembly_timer = c_timer.new(250, expire)
256+end
257+ 
258+function c_mctp_engine:stop_reassembly_timer()
259+ self.reassembly_generation = nil
260+ if self.reassembly_timer then
261+ stop_timer(self.reassembly_timer)
262+ self.reassembly_timer = nil
263+ end
264+ self.mctp_proto:reset_reassembly()
265+end
266+ 
241function c_mctp_engine:stop()267function c_mctp_engine:stop()
268+ self:stop_reassembly_timer()
242 self.pcie_read_work:stop()269 self.pcie_read_work:stop()
243 self.pcie_read_work:join()270 self.pcie_read_work:join()
244 self.pcie_read_work = nil271 self.pcie_read_work = nil
@@ -261,6 +288,7 @@ end
261 288 
262function c_mctp_engine:reinit()289function c_mctp_engine:reinit()
263 log:notice('[System%s]mctp_engine: vdm engine reinit', self.system_id)290 log:notice('[System%s]mctp_engine: vdm engine reinit', self.system_id)
291+ self.mctp_proto:reset_reassembly()
264 self.hw_pcie_proto:vdm_engine_reinit()292 self.hw_pcie_proto:vdm_engine_reinit()
265end293end
266 294 
@@ -270,6 +298,9 @@ function c_mctp_engine:reset_all_request()
270 298 
271 local err = errors.mctp_endpoint_reset()299 local err = errors.mctp_endpoint_reset()
272 for _, wait_info in pairs(wait_list) do300 for _, wait_info in pairs(wait_list) do
301+ if wait_info.reservation then
302+ self.mctp_proto:release_response(wait_info.reservation)
303+ end
273 wait_info.err = err304 wait_info.err = err
274 skynet.wakeup(wait_info.co)305 skynet.wakeup(wait_info.co)
275 end306 end
@@ -369,6 +400,9 @@ function c_mctp_engine:transport_register(module_name, pid, msg_src_eid, phy_add
369end400end
370 401 
371function c_mctp_engine:new_timer(wait_info, key)402function c_mctp_engine:new_timer(wait_info, key)
403+ if wait_info.reservation and not self.mctp_proto:renew_response(wait_info.reservation, wait_info.timeout_ms) then
404+ error('request failed: response reservation expired')
405+ end
372 return c_new_timer(wait_info.timeout_ms, function()406 return c_new_timer(wait_info.timeout_ms, function()
373 if self.rsp_wait_list[key] ~= wait_info then407 if self.rsp_wait_list[key] ~= wait_info then
374 return408 return
@@ -416,18 +450,20 @@ function c_mctp_engine:log_mctp_time_out()
416 self.repeat_log_times = (self.repeat_log_times + 1) % 60000450 self.repeat_log_times = (self.repeat_log_times + 1) % 60000
417end451end
418 452 
419-function c_mctp_engine:request(key, cb, timeout_ms, more_process_cb)453+function c_mctp_engine:request(key, cb, timeout_ms, more_process_cb, reservation)
420 local wait_info = self.rsp_wait_list[key]454 local wait_info = self.rsp_wait_list[key]
421 if wait_info then455 if wait_info then
422 local err = string.format('[System%s]mctp_engine: request failed, prev request not completed', self.system_id)456 local err = string.format('[System%s]mctp_engine: request failed, prev request not completed', self.system_id)
423 return E_SEND_FAILED, err457 return E_SEND_FAILED, err
424 end458 end
425 459 
426- wait_info = { co = coroutine.running() }460+ wait_info = { co = coroutine.running(), reservation = reservation }
427 self.rsp_wait_list[key] = wait_info461 self.rsp_wait_list[key] = wait_info
428 local ok, err = pcall(cb, key)462 local ok, err = pcall(cb, key)
429 if not ok then463 if not ok then
430- self.rsp_wait_list[key] = nil464+ if self.rsp_wait_list[key] == wait_info then
465+ self.rsp_wait_list[key] = nil
466+ end
431 log:error('[System%s]mctp_engine: request cb failed, %s', self.system_id, err)467 log:error('[System%s]mctp_engine: request cb failed, %s', self.system_id, err)
432 return E_SEND_FAILED, err468 return E_SEND_FAILED, err
433 end469 end
@@ -441,15 +477,23 @@ function c_mctp_engine:request(key, cb, timeout_ms, more_process_cb)
441 end477 end
442 478 
443 wait_info.timeout_ms = timeout_ms479 wait_info.timeout_ms = timeout_ms
444- wait_info.timer = self:new_timer(wait_info, key)480+ ok, err = pcall(function()
445- 481+ wait_info.timer = self:new_timer(wait_info, key)
446- skynet.wait()482+ skynet.wait()
447- if more_process_cb and type(more_process_cb) == 'function' then483+ if more_process_cb and type(more_process_cb) == 'function' then
448- more_process_cb(wait_info, key)484+ more_process_cb(wait_info, key)
485+ end
486+ end)
487+ -- 复位后同键可能已属于新查询,旧协程只能清理自己的等待项。
488+ if self.rsp_wait_list[key] == wait_info then
489+ self.rsp_wait_list[key] = nil
490+ end
491+ if wait_info.timer then
492+ stop_timer(wait_info.timer)
493+ end
494+ if not ok then
495+ return E_ERROR, err
449 end496 end
450- 
451- self.rsp_wait_list[key] = nil
452- stop_timer(wait_info.timer)
453 497 
454 if wait_info.is_timeout then498 if wait_info.is_timeout then
455 self:log_mctp_time_out()499 self:log_mctp_time_out()
@@ -114,6 +114,7 @@ function TestMctpDiscovery:test_mctp_discovery_success()
114 end)114 end)
115 115 
116 self.mctp_discovery:smbus_set_eid_to_endpoint(116 self.mctp_discovery:smbus_set_eid_to_endpoint(
117+ 'discovery-success',
117 target_eid,118 target_eid,
118 ref_chip,119 ref_chip,
119 medium_type,120 medium_type,
@@ -158,6 +159,7 @@ function TestMctpDiscovery:test_mctp_discovery_failed()
158 local ref_chip = { path = path }159 local ref_chip = { path = path }
159 160 
160 self.mctp_discovery:smbus_set_eid_to_endpoint(161 self.mctp_discovery:smbus_set_eid_to_endpoint(
162+ 'discovery-failed',
161 target_eid,163 target_eid,
162 ref_chip,164 ref_chip,
163 medium_type,165 medium_type,
@@ -14,12 +14,15 @@ local mctp_def = require('mctp_def')
14-- local utils = require('mc.utils')14-- local utils = require('mc.utils')
15local c_mctp_engine = require('mctp_engine')15local c_mctp_engine = require('mctp_engine')
16local c_general_endpoint = require('mctp_endpoints.ep_general')16local c_general_endpoint = require('mctp_endpoints.ep_general')
17+local c_ncsi_endpoint = require('mctp_endpoints.ep_ncsi')
18+local mctp = require('mctp')
17 19 
18TestEndpointGeneral = {}20TestEndpointGeneral = {}
19 21 
20function TestEndpointGeneral:setupClass()22function TestEndpointGeneral:setupClass()
21 self.general_endpoint =23 self.general_endpoint =
22 c_general_endpoint.new(c_mctp_engine.new(), 0, 0, mctp_def.PHY_MEDIUM_PCI, mctp_def.ENDPOINT_IS_DISCOVERY)24 c_general_endpoint.new(c_mctp_engine.new(), 0, 0, mctp_def.PHY_MEDIUM_PCI, mctp_def.ENDPOINT_IS_DISCOVERY)
25+ self.general_endpoint:set_msg_type(0x0f)
23end26end
24 27 
25function TestEndpointGeneral:teardownClass() end28function TestEndpointGeneral:teardownClass() end
@@ -43,3 +46,184 @@ function TestEndpointGeneral:test_request()
43 ret, _ = pcall(self.send, self, req_bin)46 ret, _ = pcall(self.send, self, req_bin)
44 lu.assertEquals(ret, true)47 lu.assertEquals(ret, true)
45end48end
49+ 
50+function TestEndpointGeneral:test_only_response_direction_completes_general_and_ncsi()
51+ for _, case in ipairs({
52+ { endpoint = c_general_endpoint, context = mctp.general_ctx },
53+ { endpoint = c_ncsi_endpoint, context = mctp.ncsi_ctx, iid = 1 },
54+ }) do
55+ local engine = c_mctp_engine.new()
56+ local ep = case.endpoint.new(engine, 0x2000, 9, mctp_def.PHY_MEDIUM_PCI, mctp_def.ENDPOINT_IS_DISCOVERY)
57+ local key = ep:prepare_mctp_request(case.iid)
58+ local pending = { co = coroutine.running(), rsp = 'waiting' }
59+ engine.rsp_wait_list[key] = pending
60+ local transport = mctp.mctp_ctx.new()
61+ transport:set_prev(ep.pcie_vdm_ctx)
62+ transport.src, transport.dest, transport.tag, transport.tag_owner = 9, 11, 0, true
63+ local ctx = case.context.new()
64+ ctx:set_prev(transport)
65+ if case.iid then
66+ ctx.iid = case.iid
67+ end
68+ ep:rx(ctx, 'notification')
69+ lu.assertEquals(pending.rsp, 'waiting')
70+ lu.assertIsNil(pending.err)
71+ lu.assertEquals(engine.rsp_wait_list[key], pending)
72+ transport.tag_owner = false
73+ ep:rx(ctx, 'response')
74+ lu.assertEquals(pending.rsp, 'response')
75+ end
76+end
77+ 
78+TestRequesterReservations = {}
79+local skynet = require('skynet')
80+local c_ep_mctp = require('mctp_endpoints.ep_mctp')
81+ 
82+function TestRequesterReservations:setup()
83+ self.engine = c_mctp_engine.new(nil, 1)
84+ self.engine.bmc_eid, self.engine.bmc_phy_addr = 10, 0x1000
85+ self.threads, self.tags = {}, {}
86+ self.old_wait = skynet.wait
87+ skynet.wait = coroutine.yield
88+ self.engine.general_proto:set_capture_handler(function(is_push, ctx)
89+ if is_push then
90+ self.tags[#self.tags + 1] = ctx:get('tag', 'mctp_ctx') or -1
91+ end
92+ end)
93+end
94+ 
95+function TestRequesterReservations:teardown()
96+ skynet.wait = self.old_wait
97+ for _, co in ipairs(self.threads) do
98+ if coroutine.status(co) == 'suspended' then
99+ coroutine.resume(co)
100+ end
101+ end
102+end
103+ 
104+function TestRequesterReservations:peer(msg_type)
105+ local ep = c_general_endpoint.new(self.engine, 0x20, 9, 15, mctp_def.ENDPOINT_IS_DISCOVERY)
106+ ep:set_msg_type(msg_type)
107+ return ep
108+end
109+ 
110+function TestRequesterReservations:begin(ep)
111+ local id = #self.threads + 1
112+ local co = coroutine.create(function()
113+ return c_ep_mctp.request(ep, id, function()
114+ ep.general_ctx.msg_type = ep:get_msg_type()
115+ self.engine.general_proto:push('query', ep.general_ctx, ep:make_msg_key(ep:get_msg_type()))
116+ end, 1000)
117+ end)
118+ self.threads[#self.threads + 1] = co
119+ local ok, err = coroutine.resume(co)
120+ lu.assertTrue(ok, tostring(err))
121+ lu.assertEquals(coroutine.status(co), 'suspended')
122+ return co
123+end
124+ 
125+function TestRequesterReservations:test_tags_are_shared_across_message_types_and_released()
126+ local first, second = self:peer(15), self:peer(16)
127+ local one = self:begin(first)
128+ self:begin(second)
129+ lu.assertEquals(self.tags, { 0, 1 })
130+ for _ = 1, 6 do
131+ self:begin(first)
132+ end
133+ lu.assertEquals(self.tags, { 0, 1, 2, 3, 4, 5, 6, 7 })
134+ local ok = pcall(c_ep_mctp.request, second, nil, function()
135+ error('callback must not run when all tags are occupied')
136+ end, 1000)
137+ lu.assertFalse(ok)
138+ lu.assertEquals(#self.tags, 8)
139+ lu.assertTrue(coroutine.resume(one))
140+ self:begin(second)
141+ lu.assertEquals(self.tags[9], 0)
142+end
143+ 
144+function TestRequesterReservations:test_send_failure_releases_reservation()
145+ local ep = self:peer(15)
146+ lu.assertFalse(pcall(c_ep_mctp.request, ep, nil, function()
147+ error('send failed')
148+ end, 1000))
149+ lu.assertFalse(self.engine.mctp_proto:response_reserved(ep.mctp_ctx, 15))
150+ ep.msg_tag = 0
151+ self:begin(ep)
152+ lu.assertEquals(self.tags, { 0 })
153+end
154+ 
155+function TestRequesterReservations:test_reset_releases_before_waiting_coroutine_resumes()
156+ local ep = self:peer(15)
157+ self:begin(ep)
158+ lu.assertTrue(self.engine.mctp_proto:response_reserved(ep.mctp_ctx, 15))
159+ self.engine:reset_all_request()
160+ lu.assertFalse(self.engine.mctp_proto:response_reserved(ep.mctp_ctx, 15))
161+end
162+ 
163+function TestRequesterReservations:test_post_response_callback_failure_releases_resources()
164+ local ep = self:peer(15)
165+ skynet.wait = self.old_wait
166+ lu.assertFalse(pcall(c_ep_mctp.request, ep, nil, function() end, 1000, function()
167+ error('consumer failed')
168+ end))
169+ lu.assertEquals(self.engine.rsp_wait_list, {})
170+ lu.assertFalse(self.engine.mctp_proto:response_reserved(ep.mctp_ctx, 15))
171+end
172+ 
173+function TestRequesterReservations:test_reset_during_send_failure_keeps_replacement_waiter()
174+ local replacement = { co = coroutine.running() }
175+ local result = self.engine:request('reused', function()
176+ self.engine:reset_all_request()
177+ self.engine.rsp_wait_list.reused = replacement
178+ error('old send failed after reset')
179+ end, 1000)
180+ lu.assertEquals(result, mctp_def.E_SEND_FAILED)
181+ lu.assertIs(self.engine.rsp_wait_list.reused, replacement)
182+end
183+ 
184+function TestRequesterReservations:test_global_capacity_rejects_before_send_then_recovers()
185+ local tokens, proto = {}, self.engine.mctp_proto
186+ local ep = self:peer(15)
187+ for source = 20, 27 do
188+ ep.eid = source
189+ for _ = 1, 8 do
190+ ep:prepare_mctp_request()
191+ local token = proto:reserve_response(ep.mctp_ctx, 15, 15, 1000)
192+ lu.assertTrue(token > 0)
193+ tokens[#tokens + 1] = token
194+ end
195+ end
196+ ep.eid = 9
197+ local sent = false
198+ lu.assertFalse(pcall(c_ep_mctp.request, ep, nil, function()
199+ sent = true
200+ end, 1000))
201+ lu.assertFalse(sent)
202+ lu.assertEquals(self.engine.rsp_wait_list, {})
203+ lu.assertEquals(ep.not_send_count, 1)
204+ proto:release_response(tokens[1])
205+ self:begin(ep)
206+ lu.assertEquals(#self.tags, 1)
207+ for _, token in ipairs(tokens) do
208+ proto:release_response(token)
209+ end
210+end
211+ 
212+function TestRequesterReservations:test_send_reports_backpressure_without_reusing_active_tags()
213+ local ep, sent = self:peer(15), 0
214+ for _ = 1, 8 do
215+ self:begin(ep)
216+ end
217+ local ok, err = pcall(c_ep_mctp.send, ep, function()
218+ sent = sent + 1
219+ end)
220+ lu.assertFalse(ok)
221+ lu.assertStrContains(tostring(err), 'all message tags are occupied')
222+ lu.assertEquals(sent, 0)
223+ lu.assertEquals(ep.not_send_count, 1)
224+ lu.assertTrue(coroutine.resume(self.threads[1]))
225+ c_ep_mctp.send(ep, function()
226+ sent = sent + 1
227+ end)
228+ lu.assertEquals(sent, 1)
229+end
@@ -9,7 +9,7 @@
9-- See the Mulan PSL v2 for more details.9-- See the Mulan PSL v2 for more details.
10 10 
11local lu = require('luaunit')11local lu = require('luaunit')
12--- local mctp = require('mctp.core')12+local mctp = require('mctp.core')
13local mctp_def = require('mctp_def')13local mctp_def = require('mctp_def')
14-- local utils = require('mc.utils')14-- local utils = require('mc.utils')
15local c_mctp_engine = require('mctp_engine')15local c_mctp_engine = require('mctp_engine')
@@ -43,3 +43,60 @@ function TestEndpointVdPCI:test_request()
43 ret, _ = pcall(self.send, self, req_bin)43 ret, _ = pcall(self.send, self, req_bin)
44 lu.assertEquals(ret, true)44 lu.assertEquals(ret, true)
45end45end
46+ 
47+local function pending_endpoint()
48+ local engine = c_mctp_engine.new(nil, 1)
49+ engine.bmc_eid = 11
50+ local ep = c_vd_pci_endpoint.new(engine, 0x2000, 9, mctp_def.PHY_MEDIUM_PCI, mctp_def.ENDPOINT_IS_DISCOVERY)
51+ local key = ep:prepare_mctp_request()
52+ local pending = { co = coroutine.running(), rsp = 'still waiting' }
53+ engine.rsp_wait_list[key] = pending
54+ local received = {}
55+ ep.on_receive:on(function(data)
56+ received[#received + 1] = data
57+ end)
58+ local ctx = mctp.mctp_ctx.new()
59+ ctx:set_prev(ep.pcie_vdm_ctx)
60+ ctx.src, ctx.dest, ctx.tag = 9, 11, 0
61+ return ep, pending, received, ctx, key
62+end
63+ 
64+function TestEndpointVdPCI:test_same_tag_notification_does_not_complete_request()
65+ local ep, pending, received, ctx, key = pending_endpoint()
66+ ctx.tag_owner = true
67+ for tag = 0, 7 do
68+ ctx.tag = tag
69+ ep:rx(ctx, 'notification' .. tag)
70+ end
71+ lu.assertEquals(pending.rsp, 'still waiting')
72+ lu.assertIsNil(pending.err)
73+ lu.assertEquals(ep.mctp_engine.rsp_wait_list[key], pending)
74+ lu.assertEquals(received, {
75+ 'notification0',
76+ 'notification1',
77+ 'notification2',
78+ 'notification3',
79+ 'notification4',
80+ 'notification5',
81+ 'notification6',
82+ 'notification7',
83+ })
84+end
85+ 
86+function TestEndpointVdPCI:test_matching_response_completes_request()
87+ local ep, pending, received, ctx = pending_endpoint()
88+ ctx.tag_owner = false
89+ ep:rx(ctx, 'response')
90+ lu.assertEquals(pending.rsp, 'response')
91+ lu.assertEquals(received, {})
92+end
93+ 
94+function TestEndpointVdPCI:test_unmatched_response_keeps_legacy_notification()
95+ local ep, pending, received, ctx, key = pending_endpoint()
96+ ctx.tag_owner, ctx.tag = false, 1
97+ ep:rx(ctx, 'legacy notification')
98+ lu.assertEquals(pending.rsp, 'still waiting')
99+ lu.assertIsNil(pending.err)
100+ lu.assertEquals(ep.mctp_engine.rsp_wait_list[key], pending)
101+ lu.assertEquals(received, { 'legacy notification' })
102+end
@@ -24,6 +24,64 @@ local getpid = utils_core.getpid
24local mctp_utils = require('mctp_utils')24local mctp_utils = require('mctp_utils')
25TestMctpEngine = {}25TestMctpEngine = {}
26 26 
27+TestMctpReassemblyTimer = {}
28+ 
29+function TestMctpReassemblyTimer:setup()
30+ self.original_timer = skynet.timer.new
31+ self.timers, self.expired, self.reset = {}, 0, 0
32+ skynet.timer.new = function(delay, callback)
33+ local timer = { delay = delay, callback = callback }
34+ self.timers[#self.timers + 1] = timer
35+ return timer
36+ end
37+ self.engine = {
38+ mctp_proto = {
39+ expire_reassembly = function()
40+ self.expired = self.expired + 1
41+ end,
42+ reset_reassembly = function()
43+ self.reset = self.reset + 1
44+ end,
45+ },
46+ }
47+end
48+ 
49+function TestMctpReassemblyTimer:teardown()
50+ skynet.timer.new = self.original_timer
51+end
52+ 
53+function TestMctpReassemblyTimer:test_idle_tick_expires_without_receive()
54+ c_mctp_engine.start_reassembly_timer(self.engine)
55+ lu.assertEquals(self.timers[1].delay, 250)
56+ self.timers[1].callback()
57+ lu.assertEquals(self.expired, 1)
58+ lu.assertEquals(#self.timers, 2)
59+ lu.assertEquals(self.timers[2].delay, 250)
60+ c_mctp_engine.stop_reassembly_timer(self.engine)
61+ lu.assertEquals(self.reset, 1)
62+ self.timers[2].callback()
63+ lu.assertEquals(self.expired, 1)
64+ lu.assertEquals(#self.timers, 2)
65+end
66+ 
67+function TestMctpReassemblyTimer:test_restart_invalidates_old_callbacks()
68+ c_mctp_engine.start_reassembly_timer(self.engine)
69+ c_mctp_engine.start_reassembly_timer(self.engine)
70+ self.timers[1].callback()
71+ lu.assertEquals(self.expired, 0)
72+ lu.assertEquals(#self.timers, 2)
73+ self.timers[2].callback()
74+ lu.assertEquals(self.expired, 1)
75+ c_mctp_engine.stop_reassembly_timer(self.engine)
76+ c_mctp_engine.start_reassembly_timer(self.engine)
77+ self.timers[3].callback()
78+ lu.assertEquals(self.expired, 1)
79+ lu.assertEquals(#self.timers, 4)
80+ self.timers[4].callback()
81+ lu.assertEquals(self.expired, 2)
82+ c_mctp_engine.stop_reassembly_timer(self.engine)
83+end
84+ 
27function TestMctpEngine:setupClass()85function TestMctpEngine:setupClass()
28 -- discovery的init会启动set eid轮询任务,测试环境loop同步死循环,86 -- discovery的init会启动set eid轮询任务,测试环境loop同步死循环,
29 -- 需在new discovery之前先hook c_tasks使其不自动执行87 -- 需在new discovery之前先hook c_tasks使其不自动执行
@@ -23,6 +23,277 @@ end
23 23 
24local MSG_BYTE = '\x0F' -- general protocol24local MSG_BYTE = '\x0F' -- general protocol
25 25 
26+TestMctpReassemblyIsolation = {}
27+ 
28+local function setup_reassembly(self)
29+ self.proto = mctp.mctp_protocol.new()
30+ self.proto.pcie_eid, self.proto.smbus_eid = 10, 10
31+ self.general = mctp.general_protocol.new()
32+ self.proto:add_children(self.general)
33+ self.received = {}
34+ self.general:set_rx_handler(function(ctx, data, medium)
35+ self.received[#self.received + 1] = { data, ctx:get('tag_owner'), medium, ctx:get('remote_phy_addr') }
36+ return true
37+ end)
38+end
39+ 
40+TestMctpReassemblyIsolation.setup = setup_reassembly
41+ 
42+local function fill_ordinary(self)
43+ local ctx = mctp.pcie_vdm_ctx.new()
44+ for src = 20, 35 do
45+ lu.assertEquals(self.proto:pop(mctp_hdr(10, src, 1, 0, 0, 1, 0) .. MSG_BYTE .. 'old', ctx, 15), E_OK)
46+ end
47+ return ctx
48+end
49+ 
50+local function reassembly_pop(self, ctx, medium, src, owner, som, eom, seq, payload)
51+ return self.proto:pop(mctp_hdr(10, src, som, eom, seq, owner, 0) .. payload, ctx, medium)
52+end
53+ 
54+function TestMctpReassemblyIsolation:test_same_tag_opposite_directions()
55+ local ctx = mctp.pcie_vdm_ctx.new()
56+ ctx.remote_phy_addr = 0x2000
57+ reassembly_pop(self, ctx, 15, 9, 1, 1, 0, 0, MSG_BYTE .. 'request')
58+ reassembly_pop(self, ctx, 15, 9, 0, 1, 0, 0, MSG_BYTE .. 'response')
59+ reassembly_pop(self, ctx, 15, 9, 1, 0, 1, 1, 'A')
60+ reassembly_pop(self, ctx, 15, 9, 0, 0, 1, 1, 'B')
61+ lu.assertEquals(self.received, { { 'requestA', true, 15, 0x2000 }, { 'responseB', false, 15, 0x2000 } })
62+end
63+ 
64+function TestMctpReassemblyIsolation:test_same_tag_different_physical_sources()
65+ local first, second = mctp.pcie_vdm_ctx.new(), mctp.pcie_vdm_ctx.new()
66+ first.remote_phy_addr, second.remote_phy_addr = 0x2000, 0x2100
67+ reassembly_pop(self, first, 15, 9, 1, 1, 0, 0, MSG_BYTE .. 'first')
68+ reassembly_pop(self, second, 15, 9, 1, 1, 0, 0, MSG_BYTE .. 'second')
69+ reassembly_pop(self, first, 15, 9, 1, 0, 1, 1, 'A')
70+ reassembly_pop(self, second, 15, 9, 1, 0, 1, 1, 'B')
71+ lu.assertEquals(self.received, { { 'firstA', true, 15, 0x2000 }, { 'secondB', true, 15, 0x2100 } })
72+end
73+ 
74+function TestMctpReassemblyIsolation:test_same_tag_different_media()
75+ local pci, smbus = mctp.pcie_vdm_ctx.new(), mctp.sm_bus_ctx.new()
76+ pci.remote_phy_addr, smbus.remote_phy_addr = 32, 32
77+ reassembly_pop(self, pci, 15, 9, 1, 1, 0, 0, MSG_BYTE .. 'pcie')
78+ reassembly_pop(self, smbus, 2, 9, 1, 1, 0, 0, MSG_BYTE .. 'smbus')
79+ reassembly_pop(self, pci, 15, 9, 1, 0, 1, 1, 'A')
80+ reassembly_pop(self, smbus, 2, 9, 1, 0, 1, 1, 'B')
81+ lu.assertEquals(self.received, { { 'pcieA', true, 15, 32 }, { 'smbusB', true, 2, 32 } })
82+end
83+ 
84+function TestMctpReassemblyIsolation:test_single_packet_cancels_incomplete_message()
85+ local ctx = mctp.pcie_vdm_ctx.new()
86+ reassembly_pop(self, ctx, 15, 9, 1, 1, 0, 0, MSG_BYTE .. 'old')
87+ reassembly_pop(self, ctx, 15, 9, 1, 1, 1, 0, MSG_BYTE .. 'new')
88+ lu.assertEquals(reassembly_pop(self, ctx, 15, 9, 1, 0, 1, 1, 'X'), E_FAIL)
89+ lu.assertEquals(self.received, { { 'new', true, 15, 0 } })
90+end
91+ 
92+function TestMctpReassemblyIsolation:test_invalid_fragment_size_cancels_message()
93+ for _, eom in ipairs({ 0, 1 }) do
94+ local ctx = mctp.pcie_vdm_ctx.new()
95+ reassembly_pop(self, ctx, 15, 9, 1, 1, 0, 0, MSG_BYTE .. 'old')
96+ lu.assertEquals(reassembly_pop(self, ctx, 15, 9, 1, 0, eom, 1, 'oversize'), E_FAIL)
97+ lu.assertEquals(reassembly_pop(self, ctx, 15, 9, 1, 0, 1, 1, 'X'), E_FAIL)
98+ end
99+ lu.assertEquals(self.received, {})
100+end
101+ 
102+function TestMctpReassemblyIsolation:test_address_change_cancels_reassembly()
103+ local ctx = mctp.pcie_vdm_ctx.new()
104+ reassembly_pop(self, ctx, 15, 9, 1, 1, 0, 0, MSG_BYTE .. 'old')
105+ self.proto.pcie_eid = 11
106+ self.proto.pcie_eid = 10
107+ lu.assertEquals(reassembly_pop(self, ctx, 15, 9, 1, 0, 1, 1, 'X'), E_FAIL)
108+ lu.assertEquals(self.received, {})
109+end
110+ 
111+function TestMctpReassemblyIsolation:test_message_size_is_bounded()
112+ local ctx = mctp.pcie_vdm_ctx.new()
113+ local first = MSG_BYTE .. string.rep('A', 63)
114+ lu.assertEquals(reassembly_pop(self, ctx, 15, 9, 1, 1, 0, 0, first), E_OK)
115+ for i = 1, 63 do
116+ lu.assertEquals(reassembly_pop(self, ctx, 15, 9, 1, 0, 0, i % 4, string.rep('B', 64)), E_OK)
117+ end
118+ lu.assertEquals(reassembly_pop(self, ctx, 15, 9, 1, 0, 1, 0, 'X'), E_FAIL)
119+ lu.assertEquals(self.received, {})
120+end
121+ 
122+function TestMctpReassemblyIsolation:test_pool_is_bounded_and_existing_messages_survive()
123+ local ctx = fill_ordinary(self)
124+ lu.assertEquals(reassembly_pop(self, ctx, 15, 36, 1, 1, 0, 0, MSG_BYTE .. 'extra'), E_FAIL)
125+ lu.assertEquals(reassembly_pop(self, ctx, 15, 20, 1, 1, 0, 0, MSG_BYTE .. string.rep('X', 4096)), E_FAIL)
126+ lu.assertEquals(reassembly_pop(self, ctx, 15, 20, 1, 0, 1, 1, 'X'), E_OK)
127+ lu.assertEquals(self.received, { { 'oldX', true, 15, 0 } })
128+end
129+ 
130+function TestMctpReassemblyIsolation:test_exact_message_limit_is_delivered()
131+ local ctx = mctp.pcie_vdm_ctx.new()
132+ reassembly_pop(self, ctx, 15, 9, 1, 1, 0, 0, MSG_BYTE .. string.rep('A', 63))
133+ for i = 1, 62 do
134+ lu.assertEquals(reassembly_pop(self, ctx, 15, 9, 1, 0, 0, i % 4, string.rep('B', 64)), E_OK)
135+ end
136+ lu.assertEquals(reassembly_pop(self, ctx, 15, 9, 1, 0, 1, 3, string.rep('C', 64)), E_OK)
137+ lu.assertEquals(
138+ self.received,
139+ { { string.rep('A', 63) .. string.rep('B', 3968) .. string.rep('C', 64), true, 15, 0 } }
140+ )
141+end
142+ 
143+function TestMctpReassemblyIsolation:test_idle_fragments_expire_and_pool_recovers()
144+ local ctx = fill_ordinary(self)
145+ require('mc.utils').msleep(120)
146+ self.proto:expire_reassembly()
147+ lu.assertEquals(reassembly_pop(self, ctx, 15, 20, 1, 0, 1, 1, 'X'), E_FAIL)
148+ lu.assertEquals(reassembly_pop(self, ctx, 15, 52, 1, 1, 0, 0, MSG_BYTE .. 'new'), E_OK)
149+ lu.assertEquals(reassembly_pop(self, ctx, 15, 52, 1, 0, 1, 1, 'Y'), E_OK)
150+ lu.assertEquals(self.received, { { 'newY', true, 15, 0 } })
151+end
152+ 
153+function TestMctpReassemblyIsolation:test_reset_discards_partial_messages()
154+ local ctx = mctp.pcie_vdm_ctx.new()
155+ reassembly_pop(self, ctx, 15, 9, 1, 1, 0, 0, MSG_BYTE .. 'old')
156+ self.proto:reset_reassembly()
157+ lu.assertEquals(reassembly_pop(self, ctx, 15, 9, 1, 0, 1, 1, 'X'), E_FAIL)
158+ lu.assertEquals(self.received, {})
159+end
160+ 
161+-- 接收保留名额必须由本端已发起的查询授权,不能只相信对端自报的 TO。
162+TestMctpAdmission = {}
163+TestMctpAdmission.setup = setup_reassembly
164+ 
165+local function expected_response(self, src, tag, timeout)
166+ local physical = mctp.pcie_vdm_ctx.new()
167+ physical.remote_phy_addr = 0x2000
168+ local ctx = mctp.mctp_ctx.new()
169+ ctx:set_prev(physical)
170+ ctx.src, ctx.dest, ctx.tag, ctx.tag_owner = 10, src, tag or 0, true
171+ local token = self.proto:reserve_response(ctx, 15, 0x0f, timeout or 1000)
172+ return token, physical, ctx
173+end
174+ 
175+function TestMctpAdmission:test_peer_invalidation_quarantines_only_matching_reservation()
176+ local token, physical, ctx = expected_response(self, 9)
177+ local other = expected_response(self, 11)
178+ reassembly_pop(self, physical, 15, 9, 0, 1, 0, 0, MSG_BYTE .. 'old')
179+ reassembly_pop(self, physical, 15, 11, 0, 1, 0, 0, MSG_BYTE .. 'keep')
180+ self.proto:invalidate_peer(15, physical.remote_phy_addr, 9)
181+ lu.assertFalse(self.proto:renew_response(token, 1000))
182+ lu.assertTrue(self.proto:renew_response(other, 1000))
183+ lu.assertEquals(reassembly_pop(self, physical, 15, 9, 0, 0, 1, 1, 'X'), E_FAIL)
184+ lu.assertEquals(reassembly_pop(self, physical, 15, 11, 0, 0, 1, 1, 'Y'), E_OK)
185+ self.proto:release_response(token) -- old coroutine cleanup cannot reopen a retired tag
186+ lu.assertTrue(self.proto:response_reserved(ctx, 15))
187+ lu.assertEquals(self.proto:reserve_response(ctx, 15, 0x0f, 1000), 0)
188+ lu.assertEquals(reassembly_pop(self, physical, 15, 9, 0, 1, 1, 0, MSG_BYTE .. 'late'), E_FAIL)
189+ lu.assertEquals(self.received, { { 'keepY', false, 15, 0x2000 } })
190+end
191+ 
192+function TestMctpAdmission:test_peer_invalidation_preserves_other_address_and_medium()
193+ local first, other, smbus = mctp.pcie_vdm_ctx.new(), mctp.pcie_vdm_ctx.new(), mctp.sm_bus_ctx.new()
194+ first.remote_phy_addr, other.remote_phy_addr, smbus.remote_phy_addr = 32, 33, 32
195+ for _, pair in ipairs({ { first, 15 }, { other, 15 }, { smbus, 2 } }) do
196+ reassembly_pop(self, pair[1], pair[2], 9, 1, 1, 0, 0, MSG_BYTE .. 'keep')
197+ end
198+ self.proto:invalidate_peer(15, 32, 9)
199+ self.proto:invalidate_peer(15, 32, 9) -- repeated invalidation is harmless
200+ lu.assertEquals(reassembly_pop(self, first, 15, 9, 1, 0, 1, 1, 'X'), E_FAIL)
201+ lu.assertEquals(reassembly_pop(self, other, 15, 9, 1, 0, 1, 1, 'Y'), E_OK)
202+ lu.assertEquals(reassembly_pop(self, smbus, 2, 9, 1, 0, 1, 1, 'Z'), E_OK)
203+ lu.assertEquals(self.received, { { 'keepY', true, 15, 33 }, { 'keepZ', true, 2, 32 } })
204+end
205+ 
206+function TestMctpAdmission:test_retired_tag_expires_and_can_be_reused()
207+ local token, physical, ctx = expected_response(self, 9, 0, 20)
208+ self.proto:invalidate_peer(15, physical.remote_phy_addr, 9)
209+ self.proto:release_response(token)
210+ lu.assertTrue(self.proto:response_reserved(ctx, 15))
211+ require('utils.core').usleep(1100000)
212+ lu.assertFalse(self.proto:response_reserved(ctx, 15))
213+ lu.assertTrue(self.proto:reserve_response(ctx, 15, 0x0f, 1000) > 0)
214+end
215+ 
216+function TestMctpAdmission:test_ordinary_pool_cannot_consume_response_reserve()
217+ local ctx = fill_ordinary(self)
218+ lu.assertEquals(reassembly_pop(self, ctx, 15, 36, 1, 1, 0, 0, MSG_BYTE .. 'extra'), E_FAIL)
219+ local token, physical = expected_response(self, 9)
220+ lu.assertNotEquals(token, 0)
221+ lu.assertEquals(reassembly_pop(self, physical, 15, 9, 0, 1, 0, 0, MSG_BYTE .. 'answer'), E_OK)
222+ lu.assertEquals(reassembly_pop(self, physical, 15, 9, 0, 1, 0, 0, '\x01fake'), E_FAIL)
223+ lu.assertEquals(reassembly_pop(self, physical, 15, 9, 0, 0, 1, 1, 'X'), E_OK)
224+ lu.assertEquals(self.received, { { 'answerX', false, 15, 0x2000 } })
225+end
226+ 
227+function TestMctpAdmission:test_source_quota_does_not_evict_existing_messages()
228+ local ctx = mctp.pcie_vdm_ctx.new()
229+ for tag = 0, 3 do
230+ lu.assertEquals(self.proto:pop(mctp_hdr(10, 9, 1, 0, 0, 1, tag) .. MSG_BYTE .. 'old', ctx, 15), E_OK)
231+ end
232+ lu.assertEquals(self.proto:pop(mctp_hdr(10, 9, 1, 0, 0, 0, 4) .. MSG_BYTE .. 'extra', ctx, 15), E_FAIL)
233+ lu.assertEquals(reassembly_pop(self, ctx, 15, 10, 1, 1, 0, 0, MSG_BYTE .. 'other'), E_OK)
234+ lu.assertEquals(reassembly_pop(self, ctx, 15, 9, 1, 0, 1, 1, 'X'), E_OK)
235+ lu.assertEquals(self.received, { { 'oldX', true, 15, 0 } })
236+end
237+ 
238+function TestMctpAdmission:test_direction_physical_address_type_and_peer_must_match_pending()
239+ local other = fill_ordinary(self)
240+ local token, physical = expected_response(self, 9)
241+ lu.assertNotEquals(token, 0)
242+ lu.assertEquals(reassembly_pop(self, physical, 15, 9, 1, 1, 0, 0, MSG_BYTE .. 'fake'), E_FAIL)
243+ lu.assertEquals(reassembly_pop(self, other, 15, 9, 0, 1, 0, 0, MSG_BYTE .. 'fake'), E_FAIL)
244+ lu.assertEquals(reassembly_pop(self, physical, 15, 8, 0, 1, 0, 0, MSG_BYTE .. 'fake'), E_FAIL)
245+ lu.assertEquals(reassembly_pop(self, physical, 15, 9, 0, 1, 0, 0, '\x01fake'), E_FAIL)
246+ lu.assertEquals(reassembly_pop(self, physical, 15, 9, 0, 1, 0, 0, MSG_BYTE .. 'answer'), E_OK)
247+end
248+ 
249+function TestMctpAdmission:test_release_invalidates_incomplete_response_and_preserves_other_token()
250+ local token, physical, ctx = expected_response(self, 9)
251+ lu.assertNotEquals(token, 0)
252+ lu.assertTrue(self.proto:response_reserved(ctx, 15))
253+ lu.assertEquals(self.proto:reserve_response(ctx, 15, 0x0f, 1000), 0)
254+ reassembly_pop(self, physical, 15, 9, 0, 1, 0, 0, MSG_BYTE .. 'old')
255+ self.proto:release_response(token)
256+ lu.assertFalse(self.proto:response_reserved(ctx, 15))
257+ lu.assertEquals(reassembly_pop(self, physical, 15, 9, 0, 0, 1, 1, 'X'), E_FAIL)
258+ local replacement = self.proto:reserve_response(ctx, 15, 0x0f, 1000)
259+ lu.assertNotEquals(replacement, token)
260+ self.proto:release_response(token)
261+ lu.assertTrue(self.proto:response_reserved(ctx, 15))
262+ self.proto:reset_reassembly()
263+ lu.assertFalse(self.proto:response_reserved(ctx, 15))
264+end
265+ 
266+function TestMctpAdmission:test_extend_wait_keeps_only_the_current_reservation()
267+ local token, _, ctx = expected_response(self, 9)
268+ lu.assertTrue(self.proto:renew_response(token, 2000))
269+ lu.assertFalse(self.proto:renew_response(token, 0))
270+ self.proto:release_response(token)
271+ local replacement = self.proto:reserve_response(ctx, 15, 0x0f, 1000)
272+ lu.assertFalse(self.proto:renew_response(token, 2000))
273+ lu.assertTrue(self.proto:renew_response(replacement, 2000))
274+end
275+ 
276+function TestMctpAdmission:test_reservations_and_response_pool_are_bounded()
277+ local physical
278+ for src = 20, 83 do
279+ local token, p = expected_response(self, src)
280+ lu.assertNotEquals(token, 0)
281+ physical = p
282+ end
283+ local token = expected_response(self, 84)
284+ lu.assertEquals(token, 0)
285+ for src = 20, 35 do
286+ lu.assertEquals(reassembly_pop(self, physical, 15, src, 0, 1, 0, 0, MSG_BYTE .. 'response'), E_OK)
287+ end
288+ lu.assertEquals(reassembly_pop(self, physical, 15, 36, 0, 1, 0, 0, MSG_BYTE .. 'response'), E_FAIL)
289+ local ordinary = fill_ordinary(self)
290+ lu.assertEquals(reassembly_pop(self, ordinary, 15, 20, 1, 1, 0, 0, MSG_BYTE .. 'new'), E_OK)
291+ lu.assertEquals(reassembly_pop(self, physical, 15, 20, 0, 1, 0, 0, MSG_BYTE .. 'fresh'), E_OK)
292+ lu.assertEquals(reassembly_pop(self, ordinary, 15, 20, 1, 0, 1, 1, 'X'), E_OK)
293+ lu.assertEquals(reassembly_pop(self, physical, 15, 20, 0, 0, 1, 1, 'Y'), E_OK)
294+ lu.assertEquals(self.received, { { 'newX', true, 15, 0 }, { 'freshY', false, 15, 0x2000 } })
295+end
296+ 
26--- Build a complete MCTP fragment (header + payload)297--- Build a complete MCTP fragment (header + payload)
27local function fragment(dest, src, som, eom, seq, to, tag, payload)298local function fragment(dest, src, som, eom, seq, to, tag, payload)
28 return mctp_hdr(dest, src, som, eom, seq, to, tag) .. payload299 return mctp_hdr(dest, src, som, eom, seq, to, tag) .. payload
@@ -14,6 +14,25 @@ local mctp_def = require('mctp_def')
14 14 
15TestMctpPcieVdm = {}15TestMctpPcieVdm = {}
16 16 
17+function TestMctpPcieVdm:test_tx_padding_at_capacity_boundary()
18+ local ctx = mctp.pcie_vdm_ctx.new()
19+ ctx.routing, ctx.bmc_phy_addr, ctx.remote_phy_addr = 2, 3, 32
20+ for _, length in ipairs({ 115, 114, 113, 116, 67, 66, 65, 68 }) do
21+ local proto = mctp.pcie_vdm_protocol.new()
22+ local sent
23+ proto:set_tx_handler(function(_, data)
24+ sent = data
25+ return true
26+ end)
27+ local payload = string.rep('\xa5', length)
28+ proto:push(payload, ctx, mctp_def.PHY_MEDIUM_PCI)
29+ local padding = (4 - length % 4) % 4
30+ lu.assertNotIsNil(sent)
31+ lu.assertEquals(#sent, 12 + length + padding)
32+ lu.assertEquals(sent:sub(13), payload .. string.rep('\0', padding))
33+ end
34+end
35+ 
17local PCIE_ROUTE_TO_RC <const> = 036local PCIE_ROUTE_TO_RC <const> = 0
18local PCIE_ROUTE_BY_ID <const> = 237local PCIE_ROUTE_BY_ID <const> = 2
19local PCIE_BROADCAST_FROM_RC <const> = 338local PCIE_BROADCAST_FROM_RC <const> = 3