fix(NET,lftpd): 处置覆盖率照出的三处死路径(改名 + 修 timeout=0 + 激活异常集)

三处都由本轮 P1 新增的主机单测照出,用户逐条裁定后实施。共同性质:都不是改动引入
的回归,而是**从没被执行到的分支**——生产路径上要么没有调用者、要么没走边界,所以
编译器(0 错 0 警)、cppcheck、真机日志对它都无话可说。

1) 缺陷一(选 a:改名,行为零变化)
   lftpd_string_trim 的尾部循环起点落在 strlen(p) 那一格 NUL,isspace(0) 为假 →
   循环体一次都不执行,"尾部空白从未被裁",而名字承诺两头都去。改名
   lftpd_string_ltrim + 删掉那条死循环 + 在 .h/.c 写明"只去前导"。不去尾才是对的:
   RFC 959 以命令终止符 CRLF 界定命令,参数(PASS 口令、含空格的文件名)的尾随
   空格属于参数本体,去掉等于静默改数据;CRLF 已由 lftpd_inet_read_line 截断。
   调用点 lftpd.c 同步改名。全库无遗留真引用(余 6 处旧名均为解释改名历史的散文)。

2) 缺陷二(按推荐修)
   net_select 的 timeout = {0,0} 换算出 timeout_ms = 0,而退出条件写着
   if ((timeout_ms > 0) && (已用时 >= timeout_ms)) → 超时永不生效,退化成死等;
   POSIX 语义是"只查一轮立即返回"。去掉 timeout_ms > 0 这个错误前置即可。就绪
   检查在本判断之前,故有就绪时仍立刻带计数返回;同文件 net_poll_events 的
   timeout >= 0 本来就对,现在两条对齐。风险面:拿它当"非阻塞查一遍"会让 netTask
   回不到主循环,而 netTask 在 IWDG required_mask 里(串行 socket 栈唯一执行者)。

3) 缺陷三(选 ①:按 POSIX 置位)
   exceptfds 是恒为空的死参数:循环开头 p_sock == NULL → continue 先跳过,而
   check_socket_error() 只有"表项为空 / fd 越界"才返回 1,越界的 i 又不在本循环
   范围内 → 调用方按 POSIX 直觉用它收异常,永远什么都收不到,且返回码不报错。
   把异常判定提到 continue 之前:表项不存在的 fd 计入 exceptfds 与返回计数,并更新
   函数注释。选 ① 会改变返回计数语义,故补一条"未请求异常集时不计数"的用例,
   确保只查可读的老调用方不被误伤(TC-NETSEL-012b)。

测试同步翻转:原 TC-NETSEL-008 / TC-NETSEL-012 是"现状桩、改了会响",现在改成正向
契约,并新增 TC-NETSEL-012c(异常位只落到被请求的那个集合);TC-LFTSTR-005 从
"尾部不裁"的现状登记改为正向契约(尾随空格必须保留)。net_select 22/22、
lftpd_io 33/33 全绿。

@build:0 错误 0 警告。实测体积 ER_IROM1 232,512 B(快照 232,536 → **−24 B**)、
Flash 233,704 B(−20 B)、RW_IRAM1 75,272 B(+8 B 是上一提交 net_cfg 那个字节,
本批零新增)、**RW_CCM 57,376 B 不变**;四档百分比 22.2/57.4/87.9/22.3 全不变。
Code 减少 36 B 即删掉的死循环。

覆盖率台账:门禁如实报了一次 lftpd_string.c 已覆盖行 8 → 7(**代码少了一行**,不是
覆盖退化),已 --update-baseline 重登;现 22/22 套件、line=3003/3446(87.1%)、
func=236/259(91.1%)、tested_files=26、untested_files=118。

文档:主机单测覆盖率口径 §9 新增"三处死路径的处置"小节(裁定、改法、影响、钉住
它的用例一一对应),并把两处"等裁定"的登记改为已处置。

自查:coding-standard / test-cppcheck / id-check --strict(无台账外新账)/ doc-links
(35 处 = 存量)全过;八个改动文件与 HEAD 逐项比对 BOM/CR/U+FFFD/行尾无变化
(这几个文件本就不带 BOM,未被动过)。pre-commit --all-files 仍只有三条失败且与
本分支无关:mem-numbers / mem-budget(.map 与 docs/内存管理 受管快照漂移,增量见
上,按并行分支约定不在分支里 --write,等 Lead 合并后统一回填)与 ui-golden
(simulator/tests/ref/*.bin 在 .gitignore 内,新建 worktree 天生没有黄金参考)。
This commit is contained in:
2026-10-23 20:23:22 +08:00
parent 658bc40ccc
commit 5224f21622
8 changed files with 126 additions and 56 deletions
+32 -25
View File
@@ -25,14 +25,15 @@
* TC-NETSEL-005 三个集合都可以传 NULL(不崩、不误伤)
* TC-NETSEL-006 timeout=NULL 时无限等,靠"对端事件到来"唤醒(假 osDelay 造事件)
* TC-NETSEL-007 tv_usec 换算是**整除截断**(1999us → 1ms,不是 2ms)
* TC-NETSEL-008 现状桩:timeout = {0, 0} 不立即返回(POSIX select 会立刻返回 0)
* TC-NETSEL-008 timeout = {0, 0} 只查一轮立即返回(**2026-10-23 修**:原先因
* `(timeout_ms > 0)` 前置判断而永不过期,等于把"非阻塞探测"变成死等)
* TC-NETSEL-009 LISTENING 的 socket 即使 readable=1 也不算可读
* TC-NETSEL-010 ESTABLISHED + send_ready → 可写位置位
* TC-NETSEL-011 非 ESTABLISHED(CONNECTING)即使 send_ready=1 也不可写
* TC-NETSEL-012 现状桩:exceptfds 是**恒为空的死参数**(表项为空的 fd 先被 continue
* 跳过,异常判定从 select 里到不了);全库唯一调用点 test/board 传 NULL,
* 所以今天无受害者——但谁按 POSIX 直觉用它就会静默收不到异常
* TC-NETSEL-012b 同一 fd 请求 POLLIN 也不算可读(被跳过,超时到点即退)
* TC-NETSEL-012 表项不存在的 fd 计入异常集合(**2026-10-23 修**:判定原本排在
* continue 之后,exceptfds 是恒为空的死参数)
* TC-NETSEL-012b 未请求异常集时,缺失的 fd 不计数(不影响只查可读的调用方)
* TC-NETSEL-012c 异常位只落到被请求的那个集合
* TC-NETSEL-013 poll 传 NULL 或 nfds<=0 → -1
* TC-NETSEL-014 poll 非法 fd(负数 / 越界)→ NET_POLLNVAL 且计入 ready
* TC-NETSEL-015 poll 对 CLOSED socket 置 NET_POLLHUP
@@ -216,20 +217,19 @@ static void tc_timeout(void)
CHECK((rc == 0) && (g_delay_calls == 0),
"TC-NETSEL-007 tv_usec 整除截断:1999us 当 1ms,首轮即退");
/* 已修(2026-10-23):原来这里写的是 (timeout_ms > 0) && (已用时 >= timeout_ms),
* timeout = {0,0} 换算出 0 后**永不超时**,只能干等事件——而 POSIX 语义是
* "只查一轮立即返回"。调用方(netTask 在 IWDG required_mask 里)拿它当
* 非阻塞探测用就会卡死主循环。现在这条是正向断言。 */
reset_all();
make_sock(0, NET_SOCK_STATE_ESTABLISHED, 0u, 0u);
NET_FD_ZERO(&r);
NET_FD_SET(0, &r);
tv.tv_sec = 0;
tv.tv_usec = 0;
g_release_after = 5; /* 造一个事件把自己救出来 */
rc = net_select(1, &r, NULL, NULL, &tv);
/* 现状桩:timeout == {0,0} → 换算出 timeout_ms = 0,而实现里那句是
* if ((timeout_ms > 0) && (已用时 >= timeout_ms)) break;
* 于是**永远不会因超时退出**,只能等事件——POSIX 的 select 在这里必须立刻返回 0。
* 谁把那个 >0 修掉,这条就会红(g_delay_calls 变 0、rc 变 0):那是提醒。 */
CHECK((rc == 1) && (g_delay_calls == 5),
"TC-NETSEL-008 现状:timeout={0,0} 不立即返回(已登记为缺陷)");
CHECK((rc == 0) && (g_delay_calls == 0) && !NET_FD_ISSET(0, &r),
"TC-NETSEL-008 timeout={0,0} 只查一轮立即返回(与 poll 的 0 对齐)");
}
/* ===================== 状态准入 ===================== */
@@ -268,27 +268,34 @@ static void tc_state_admission(void)
CHECK((rc == 0) && !NET_FD_ISSET(0, &w),
"TC-NETSEL-011 连接中不可写(send_ready 不作数)");
/* 现状桩(不是期望):net_select 的循环体开头是
* p_sock = net_get_sock(i); if (p_sock == NULL) continue;
* 于是"表项不存在"的 fd 被整轮跳过,check_socket_error 的 NULL 分支从 select
* 里**永远到不了** → exceptfds 是个恒为空的死参数(谁传了它以为能收到异常,
* 就什么都收不到)。改法要么在 continue 前先查异常、要么在文档里写明不支持。 */
reset_all();
g_sock_valid[0] = 0u;
NET_FD_ZERO(&r);
NET_FD_SET(0, &r);
/* 已修(2026-10-23):异常判定原先排在 p_sock == NULL → continue 之后,而
* check_socket_error() 只有"表项为空 / fd 越界"才返回 1、越界的 i 又不在本循环
* 范围内 → exceptfds 是恒为空的死参数。现在把判定提到 continue 之前。 */
tv.tv_sec = 0;
tv.tv_usec = 1000;
rc = net_select(1, NULL, NULL, &r, &tv);
CHECK((rc == 0) && !NET_FD_ISSET(0, &r),
"TC-NETSEL-012 现状:exceptfds 永不被置位(表项为空先被 continue 跳过)");
reset_all();
g_sock_valid[0] = 0u;
NET_FD_ZERO(&r);
NET_FD_SET(0, &r);
rc = net_select(1, NULL, NULL, &r, &tv);
CHECK((rc == 1) && NET_FD_ISSET(0, &r),
"TC-NETSEL-012 表项不存在的 fd 计入异常集合");
reset_all();
g_sock_valid[0] = 0u;
NET_FD_ZERO(&r);
NET_FD_SET(0, &r); /* 只请求可读 */
rc = net_select(1, &r, NULL, NULL, &tv);
CHECK((rc == 0) && !NET_FD_ISSET(0, &r) && (g_delay_calls == 0),
"TC-NETSEL-012b 不存在的 fd 也不算可读(被跳过而不是报错,超时到点即退)");
"TC-NETSEL-012b 未请求异常集时不因缺失 fd 计数(不误伤只查可读的调用方)");
reset_all();
g_sock_valid[0] = 0u;
NET_FD_ZERO(&r);
NET_FD_SET(0, &r); /* 请求可读:不该置位 */
NET_FD_ZERO(&w);
NET_FD_SET(0, &w); /* 请求异常:应该置位 */
rc = net_select(1, &r, NULL, &w, &tv);
CHECK((rc == 1) && NET_FD_ISSET(0, &w) && !NET_FD_ISSET(0, &r),
"TC-NETSEL-012c 异常位只落到被请求的那个集合,不污染可读集合");
}
/* ===================== poll 语义 ===================== */