修复Trap23:异常释放后复监听隔离并排空RX残渣,上传中止截断消除(RST10/10+FIN5/5通过,正常50/50)

This commit is contained in:
2026-09-23 01:31:07 +08:00
parent 5a41b966fc
commit 7d70445c16
3 changed files with 100 additions and 1 deletions
+80
View File
@@ -1240,6 +1240,46 @@ static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags,
* 函数说明:1. 调用 ch395f_close_socket 关闭 CH395F 硬件 Socket
* 2. 清空整个控制块(memset 0),释放该 Socket 资源
*/
/* Trap 23(2026-09-23 生产路径实测):异常释放(芯片 TIMEOUT,例如客户端中止上传)
* 后的**隔离期**。实测时序:中止时对端仍有在途数据 → 芯片 TIMEOUT(此时 MCU 侧 recv=0)
* → 在途段随后落进芯片 RX 缓冲(约 2KB 残渣)→ 若立即 open+listen,残渣被下一条连接
* 当作自己的数据读走:新上传只收 2KB 就被判定结束、回报成功(静默截断)。
* 处置:异常释放的 socket 先隔离 NET_RELISTEN_QUARANTINE_MS:期间持续排空 RX、主动
* close 并排空中断、等芯片回到 CLOSED/TCP_CLOSED;隔离期满才 open+listen。上限
* NET_RELISTEN_SETTLE_MS 后强制放行,保证监听器不会永久失效。
* 正常 FIN 释放(handle_disconnect_event)不走此路径——既有行为零改动。
* 声明须早于 net_relisten_locked(其余重试状态见其后区块)。 */
#define NET_RELISTEN_QUARANTINE_MS 400U /* 隔离时长(等在途数据落尽 + 排空残渣) */
#define NET_RELISTEN_SETTLE_MS 800U /* 隔离上限(须 < NET_RELISTEN_DEADLINE_MS,
* 否则隔离耗尽重试预算 → 监听被放弃,2026-09-23 实测) */
static uint8_t s_relisten_quarantine[NET_MAX_SOCKETS]; /* 1 = 本次释放为异常,需隔离 */
static uint32_t s_relisten_settle_start[NET_MAX_SOCKETS]; /* 隔离起点(0 = 未开始) */
static uint32_t s_relisten_settle_dead[NET_MAX_SOCKETS]; /* 隔离上限到期时刻 */
/*
* 函数功能:清空芯片该 socket 的 RX 残留(Trap 23)
* 入口参数:sockfd - Socket 描述符 (0~7) int
* 返回值:无
* 限定条件:netTask 上下文;socket 即将 close/open(数据已无意义,丢弃)
* 函数说明:中止/异常断开时芯片 RX 缓冲可能残留上一条连接的数据(实测残留约一个
* 缓冲 = 2048B);残渣带入下一条连接会使其在约 2KB 处被芯片拆链(上传被
* 静默截断)。关闭前主动读空即清除。分块读避免新增静态缓冲。
*/
static void net_drain_chip_rx(int sockfd) {
uint8_t scratch[256];
for (int guard = 0; guard < 16; guard++) { /* 上限 16×256 = 4KB(缓冲仅 2KB),防死循环 */
int n = (int)ch395f_get_recv_len((uint8_t)sockfd);
if (n <= 0) {
break;
}
if (n > (int)sizeof(scratch)) {
n = (int)sizeof(scratch);
}
ch395f_read_recv_buf((uint8_t)sockfd, scratch, (uint16_t)n);
}
}
/*
* 函数功能:单连接模式断连后自动重新进入 LISTEN(不清空控制块,保留 fd)
* 入口参数:p_sock - 已断开(CLOSED)的 TCP 监听型 Socket 控制块 net_sock_t *
@@ -1256,6 +1296,36 @@ static int net_relisten_locked(net_sock_t *p_sock) {
uint8_t status;
int sockfd = (int)(p_sock - s_net_socks);
if (s_relisten_quarantine[sockfd] != 0U) {
uint8_t st[2] = {0, 0};
uint32_t now = HAL_GetTick();
if (s_relisten_settle_start[sockfd] == 0U) {
s_relisten_settle_start[sockfd] = now;
s_relisten_settle_dead[sockfd] = now + NET_RELISTEN_SETTLE_MS;
}
net_drain_chip_rx(sockfd); /* 隔离期内持续排空 RX 残渣 */
ch395f_close_socket((uint8_t)sockfd); /* 主动关:清旧连接收尾状态 */
(void)ch395f_get_sock_int_status((uint8_t)sockfd);
(void)ch395f_get_glob_int_status_all();
ch395f_get_socket_status((uint8_t)sockfd, st);
if (((st[0] != CH395F_SOCKET_CLOSED) || (st[1] != CH395F_TCP_CLOSED) ||
((uint32_t)(now - s_relisten_settle_start[sockfd]) < NET_RELISTEN_QUARANTINE_MS))) {
if ((int32_t)(now - s_relisten_settle_dead[sockfd]) < 0) {
DBG_INFO("sock%d relisten quarantine (sock=0x%02X tcp=0x%02X)",
sockfd, st[0], st[1]);
s_net_errno = NET_ERR_BUSY;
return -1; /* 交重试轮询(30ms 后复检) */
}
DBG_INFO("sock%d relisten quarantine timeout, force open (sock=0x%02X tcp=0x%02X)",
sockfd, st[0], st[1]);
}
s_relisten_quarantine[sockfd] = 0U; /* 隔离完成(或强制放行) */
s_relisten_settle_start[sockfd] = 0U;
s_relisten_settle_dead[sockfd] = 0U;
}
ch395f_set_proto_type((uint8_t)sockfd, CH395F_PROTO_TYPE_TCP);
ch395f_set_sour_port((uint8_t)sockfd, p_sock->local_port);
@@ -1319,6 +1389,8 @@ static void net_relisten_clear(int sockfd) {
s_relisten_wait[sockfd] = 0U;
s_relisten_next[sockfd] = 0U;
s_relisten_dead[sockfd] = 0U;
s_relisten_settle_dead[sockfd] = 0U; /* Trap 23:一并清沉降/隔离状态 */
s_relisten_settle_start[sockfd] = 0U;
}
/*
@@ -2570,6 +2642,14 @@ static void handle_timeout_event(net_sock_t *p_sock) {
p_sock->state = NET_SOCK_STATE_CREATED;
} else if (p_sock->state == NET_SOCK_STATE_ESTABLISHED) {
DBG_ERROR("sock%d TIMEOUT", p_sock->ch395_sock);
/* Trap 23:异常释放(TIMEOUT,例如客户端中止上传)→ 复监听前隔离,
* 等对端在途数据落尽并排空 RX 残渣(详见 net_relisten_locked 区块注释) */
{
int qfd = (int)(p_sock - s_net_socks);
if ((qfd >= 0) && (qfd < NET_MAX_SOCKETS)) {
s_relisten_quarantine[qfd] = 1U;
}
}
p_sock->state = NET_SOCK_STATE_CLOSED;
} else if (p_sock->state == NET_SOCK_STATE_LISTENING) {
/* 监听超时(异常情况) */
+1 -1
View File
@@ -14,7 +14,7 @@
| N-3 | **L5 队列往返基线(1223/988µs)的 L3 后重验**——L5 测 DONTWAIT 队列全程;L3 对该路径逐位不变、pending tick 无 pending 时为 µs 级指针读 → 机制上无影响,但基线数值属分片时代,严格重锁需跑一轮 Phase 13(需 PC peer,窗口与 N-1 定位可并案) | 2026-09-22(L3 收尾留档) | 低风险:10ms/100ms 阈值余量 >8×;下次 Phase 13 窗口顺带重锁即可 | `链路延迟测试说明.md` §6.4 L5 注¹ |
| N-6 | **Trap 22:快速重连致 CONNECT 中断丢失 → 单连接监听静默死亡(P1,修复待批准)**——轻负载 + 重监听历史 + 断连后 <~1s 重连:芯片握手正常但 MCU 无 standalone accept(状态滞留 LISTENING)→ `net_send` 全失败(-1)→ 断开时 DISCONNECT 落 LISTENING 分支仅告警 → 芯片自动关闭 Socket → 端口永久拒连(不自恢复) | 2026-09-22(Trap 21 二次台架 A 组 run2;出现 1/1,未做 10/10 规模复验) | 修复方案:`handle_disconnect_event` LISTENING 分支回读芯片 socket 状态,非可监听即 `net_relisten_try` 重建(D20 的 LISTENING 分支姊妹情形)+ 可选:重监听后清残留中断位;应用侧规避:单连接 socket 断连后重连 ≥1~2s 退避(lftpd 快速 PASV / 客户端快速重试是生产风险场景)。生产影响:数据通道监听静默死亡 → 后续数据连接全拒,严重度等同端口失效 | `CH395F_Trap_Records.md` Trap 22 |
| N-7 | **⛔ 已撤回(2026-09-23):前置机理为台架假象(分类器误判 + 固件内读者吞噬对端数据),"读必读空"未被任何有效台架验证 → 本项暂不实施**(原提案:net 层非阻塞 RX 读强制排空至空;撤回依据与本轮证据见 `CH395F_Trap_Records.md` Trap 21 顶部更正块。原"致命类 = 部分窗口重通告(排空后缓冲残留):大 Δ(1KB 块读,C/C0 配方)10/10 确定性停摆;小 Δ(64B)相位刀口(一次性落点 10/10 过 vs 5/5 停摆仅数 ms 时移之差)且持续重复必停摆(50/50);排空至空(窗口→满)恒安全(40/40,含 10ms 节拍 E0)、Δ=0 命令(状态读/中断读/poll/TX)恒安全 → **无需时间守卫/冷却**(旧"排空后 RX 暂缓 + 120ms 冷却"设计作废:C0 证明部分排空自身即致命,E0 证明排空至空后 10ms 再排空安全) | 2026-09-22(精确定位轮:E/O/B/F1/S1–S4/C/C0/V1F/O1f 共 11 组台架 110+ 运行 + netcmd 命令序列日志) | 实施要点(2026-09-22 复核细化,完整方案见 `问题记录/CH395F_Trap21_机制图解.md` §5):net 层新增 `net_drain_recv()` 助手,替换 5 处 `min(货架现有, 调用方len)` 读路径——`net_recv_locked:1205`、`net_pending_tick:1514`/`:1536`、`net_pending_settle_close:1618`、`net_recvfrom:2072`:先交付中转筐 → 筐空则 `avail = get_recv_len()` 后**按 avail 全量读空**入筐 → 只交 `min(avail, len)` 给调用方,余量留筐、下次不发芯片命令直接交付(上层筐大小与解析逻辑零改动);中转槽设计(2026-09-22 复核:**一个共用槽即可**,8KB 方案作废):数据通道自带筐 1KB→2KB(`lftpd.c:187`,`len=2048 ⇒ min(avail,len)==avail`,天然读必读空、永不入槽、不参与争用),控制行 512B / UI 64B / 时间同步 16B 共用**一个 2048B 槽**;槽被别的 socket 占用时该读暂缓(WOULDBLOCK / pending 继续等),不发命令即无残余 → 合计 **+3KB**(主 SRAM 余量 47.2KB,占 6.3%),槽数 1→2 仅一行改动;争用暂缓记限频日志。**只改 lftpd 数据筐不足以覆盖**:控制行读、UI 64B、时间同步 16B 为同型雷(且单纯加大控制行缓冲会丢流水线命令;零槽方案需 +6.5KB 并重构控制行读取器)。台架回归:C/C0 与 V1 配方由 10/10 停摆 → 10/10 通过,E 系列保持 40/40;Δ=0 命令(状态读/poll/TX)不受限 | `CH395F_Trap_Records.md` Trap 21;`CH395F_Trap21_机制图解.md`;`Drivers/BSP/NET/net_socket.c`(5 处读路径);`Drivers/BSP/NET/lftpd/lftpd.c` receive_file 块循环 |
| N-8 | **Trap 23:上传中止后首笔复连上传被静默截断(≤2048B)并误报成功(P1,已可信复现 15/15,修复待批准)**——生产路径 FTP STOR 中途断开(RST 或 FIN 均可)后,紧跟的第一笔复连上传被截断到 ≤2048B(芯片单 socket RX 缓冲)且回报 226 成功 → 客户端认为上传成功 = 静默数据损坏;第二笔起恢复正常。机制:中止后 net 层立即 relisten(端口复用),旧连接收尾的陈旧 DISCONNECT 落在新连接上 → lftpd 读循环误判 EOF,把已到的 2048B 当整文件。对照:无中止时 177 轮(8KB~1MB)SHA-256 全匹配 | 2026-09-23(可信复现首轮;`MDK-ARM/ftp_abort_probe.py` 自动判定,RST 10/10 + 优雅 FIN 5/5) | 修复方向:① relisten 前确认芯片 socket 真 CLOSED 并清中断(现有单次清理不足)② 新连接 accept 后未读任何数据即收 DISCONNECT → 判陈旧中断并重新 relisten(勿当 EOF 结束上传)③ `receive_file` 区分正常 EOF 与异常结束,异常回 5xx 而非 226 ④ 回归:`ftp_abort_probe.py` followup FAIL→OK(10/10),177 轮对照仍全绿 | `CH395F_Trap_Records.md` Trap 23;`Drivers/BSP/NET/lftpd/lftpd.c` receive_file;`Drivers/BSP/NET/net_socket.c` relisten/事件处理 |
| N-8 | **✅ 已修复(2026-09-23 当日闭环):Trap 23 上传中止后首笔复连上传被静默截断(≤2048B)并误报成功**——修复前可信复现 15/15(RST 10/10 + 优雅 FIN 5/5);修复后 RST **10/10**、FIN **5/5** 全通过,正常路径回归 30/30 + 20/20(SHA-256 全匹配)。**真因**:中止时对端仍有在途数据 → 芯片 TIMEOUT 后才把在途段落进 RX 缓冲(约 2048B 残渣)→ 立即复监听时残渣被下一条连接当作自己的数据读走(新上传只"收"2048B 就判结束并回 226)。**修复**:异常释放(TIMEOUT)的 socket 复监听前隔离(持续排空 RX 残渣 + 主动 close + 等 CLOSED/TCP_CLOSED),400ms 起、800ms 上限强制放行(上限须 < 重试放弃时限 2000ms,否则监听被放弃,已实测修正);`net_drain_chip_rx()` 分块读空、零新增静态缓冲;仅异常路径生效,正常 FIN 路径零改动。详见 Trap 23"修复实施与验证"——生产路径 FTP STOR 中途断开(RST 或 FIN 均可)后,紧跟的第一笔复连上传被截断到 ≤2048B(芯片单 socket RX 缓冲)且回报 226 成功 → 客户端认为上传成功 = 静默数据损坏;第二笔起恢复正常 | 2026-09-23(可信复现 + 当日修复闭环;`MDK-ARM/ftp_abort_probe.py` 自动判定) | 已实施:`net_socket.c` 新增 `s_relisten_quarantine[]`(`handle_timeout_event` 置位)+ `net_drain_chip_rx()` + `net_relisten_locked` 隔离门(`NET_RELISTEN_QUARANTINE_MS`=400 / `NET_RELISTEN_SETTLE_MS`=800 < `NET_RELISTEN_DEADLINE_MS`=2000) | `CH395F_Trap_Records.md` Trap 23;`Drivers/BSP/NET/net_socket.c`(`net_relisten_locked` / `handle_timeout_event` / `net_drain_chip_rx`) |
## 存储 / 日志管线
+19
View File
@@ -673,3 +673,22 @@ CH395F 片内 TCP 状态机的角(芯片固件,MCU 侧无法修补)。2026
### 发现时间
2026-09-23(可信复现首轮:真实生产路径 FTP 中止/复连场景,由 `MDK-ARM/ftp_abort_probe.py` 自动判定)
### 修复实施与验证(2026-09-23 当日闭环)
**真因(设备侧日志实证,含正常对照)**:
1. 客户端中止上传时对端**仍有在途数据**;芯片给出 `sock4 TIMEOUT`(此刻 MCU 侧 `recv=0`),net 层随即释放该 socket 并复监听;
2. 那些**在途段随后才落进芯片 RX 缓冲**(约一个缓冲 = 2048B 残渣),而正常传输时 DISCONNECT 到达刻芯片里也仍有数据(对照实测 `recv=64512/65536`,由应用读空 → 完整);
3. 中止后立即 `open+listen`,残渣被**下一条连接当作自己的数据读走**:新上传只“收到”2048B 就被判定结束并回报 226(首笔复连必中招,第二笔起芯片已干净 → 恢复正常)。
**修复(`Drivers/BSP/NET/net_socket.c`,仅异常释放路径,正常 FIN 路径零改动)**:
1. `handle_timeout_event`(ESTABLISHED 超时释放)置 per-fd **隔离标记** `s_relisten_quarantine[]`;
2. `net_relisten_locked` 对带隔离标记的 socket:**隔离期内持续排空芯片 RX**(新增 `net_drain_chip_rx()`,分块读空、零新增静态缓冲)+ **主动 `ch395f_close_socket` 并排空该 socket 中断** + 等芯片回到 `CLOSED/TCP_CLOSED`,隔离满 `NET_RELISTEN_QUARANTINE_MS`(400ms) 才 `open+listen`;
3. 上限 `NET_RELISTEN_SETTLE_MS`(800ms) 强制放行 —— **必须小于** `NET_RELISTEN_DEADLINE_MS`(2000ms):实测取 2000ms 会把重试预算耗尽 → 重试轮询"give up" → **监听器死亡(:21 拒连)**,已修正并在注释中留痕;
4. 隔离状态在 `net_relisten_clear` 内一并清除(关闭/新建 fd 时不残留)。
**验证(可信判据:FTP 协议结果 + 紧接一笔上传的 SHA-256 往返 + 控制通道存活)**:
- 修复前:RST 中止 **0/10**、优雅 FIN 中止 **0/5**(首笔复连上传全部被截断且回报成功);
- 修复后:RST 中止 **10/10**、优雅 FIN 中止 **5/5** 全部通过;
- 正常路径回归:30×64KB 零间隔 **30/30**、20×64KB **20/20**,SHA-256 全匹配;
- 启动门禁:`App init OK` / `net init: OK`、FORBID 0 行。