diff --git a/Drivers/BSP/NET/lftpd/lftpd.c b/Drivers/BSP/NET/lftpd/lftpd.c index 1e5e5f60..2b9cee69 100644 --- a/Drivers/BSP/NET/lftpd/lftpd.c +++ b/Drivers/BSP/NET/lftpd/lftpd.c @@ -199,6 +199,9 @@ static int receive_file(int socket, const char *path) lftpd_io_close(fh); if (err < 0) { + /* 异常结束(连接重置/超时/预算耗尽):删除截断文件防残留并返回错误码 + * → STOR 回报 450 而非误报 226(Trap 23 待办 #3) */ + lftpd_io_unlink(path); return err; } if (total == 0) { diff --git a/Drivers/BSP/NET/net_socket.c b/Drivers/BSP/NET/net_socket.c index ab105c4d..f6a82f9c 100644 --- a/Drivers/BSP/NET/net_socket.c +++ b/Drivers/BSP/NET/net_socket.c @@ -789,6 +789,7 @@ int net_socket(int domain, int type) { p_sock->in_use = 1; p_sock->send_ready = 1; p_sock->readable = 0; + p_sock->released_abnormal = 0; /* 新 fd 不得继承上一条连接的释放原因(Trap 23 待办 #3) */ net_relisten_clear(sockfd); /* 新 fd 不得继承上一次的待重试状态 */ net_pending_clear(sockfd); /* 新 fd 不得继承上一次挂起的 recv 请求(L3) */ @@ -1541,6 +1542,7 @@ static int net_relisten_locked(net_sock_t *p_sock) { } p_sock->state = NET_SOCK_STATE_LISTENING; + p_sock->released_abnormal = 0U; /* 新监听不得继承上一条连接的释放原因(Trap 23 待办 #3) */ p_sock->remote_port = 0; p_sock->remote_ip = 0; memset(p_sock->remote_ip_arr, 0, sizeof(p_sock->remote_ip_arr)); @@ -1814,7 +1816,16 @@ static void net_pending_tick(void) { if (p_sock->auto_relisten != 0U) { (void)net_relisten_try(p_sock); /* 非阻塞:失败由重试轮询接管 */ } - net_pending_complete(i, 0); + /* Trap 23 待办 #3:异常释放(TIMEOUT/RST)不是正常 EOF——对端未发 + * 正常 FIN → 以连接重置了结,应用(lftpd STOR)对异常结束回 5xx + * 而非误报 226;正常 FIN(DISCONNECT)仍回 0(FTP STOR 无长度声明, + * 在途 FIN 与正常完成不可区分——协议边界,见 Trap 23 记录) */ + if (p_sock->released_abnormal != 0U) { + s_net_errno = NET_ERR_CONNRESET; + net_pending_complete(i, NET_ERR_CONNRESET); + } else { + net_pending_complete(i, 0); + } } continue; } @@ -2809,6 +2820,7 @@ static void handle_disconnect_event(net_sock_t *p_sock) { s_relisten_quarantine[qfd] = NET_RELISTEN_SETTLE_GRACEFUL; } } + p_sock->released_abnormal = 0U; /* 正常释放(对端 FIN):断连了结回 EOF(0) */ p_sock->state = NET_SOCK_STATE_CLOSED; fire_event(p_sock, NET_EVENT_DISCONNECTED); } else if (p_sock->state != NET_SOCK_STATE_CLOSED && @@ -2878,6 +2890,10 @@ static void handle_timeout_event(net_sock_t *p_sock) { s_relisten_quarantine[qfd] = 1U; } } + /* Trap 23 待办 #3:标记异常释放——L3 挂起 recv 排空断连后以 + * NET_ERR_CONNRESET 了结(而非 EOF),应用(lftpd STOR)回报 5xx + * 而非误报 226(见 net_pending_tick 断连分支) */ + p_sock->released_abnormal = 1U; p_sock->state = NET_SOCK_STATE_CLOSED; } else if (p_sock->state == NET_SOCK_STATE_LISTENING) { /* 监听超时(异常情况) */ diff --git a/Drivers/BSP/NET/net_types.h b/Drivers/BSP/NET/net_types.h index 6e8b4994..c3a99328 100644 --- a/Drivers/BSP/NET/net_types.h +++ b/Drivers/BSP/NET/net_types.h @@ -171,6 +171,14 @@ typedef struct { /* 单连接模式自动重监听:连接断开后由 net 层自动回到 LISTEN(BSD 风格监听常驻) */ uint8_t auto_relisten; + /* 释放来源标记(Trap 23 待办 #3):1 = 异常释放(芯片 TIMEOUT,对端 RST/掉链), + * 0 = 正常释放(芯片 DISCONNECT,对端 FIN)或新连接。L3 挂起 recv 的了结断连 + * 分支据此决定回 EOF(0) 还是 NET_ERR_CONNRESET:异常结束上报应用(lftpd STOR) + * 为错误 → 回 5xx 而非误报 226。由 handle_timeout_event / handle_disconnect_event + * 置位,新连接建立时(net_socket / relisten 成功)清零——新连接不得继承上一条 + * 连接的释放原因。 */ + uint8_t released_abnormal; + /* 统计信息 */ uint32_t recv_bytes; /* 累计接收字节数 */ uint32_t send_bytes; /* 累计发送字节数 */ diff --git a/MDK-ARM/ftp_abort_probe.py b/MDK-ARM/ftp_abort_probe.py index 950bb67e..914b6477 100644 --- a/MDK-ARM/ftp_abort_probe.py +++ b/MDK-ARM/ftp_abort_probe.py @@ -9,9 +9,13 @@ # 3. the device's relisten actually happened (observable via a successful follow-up). # # Usage: ftp_abort_probe.py [--ip ..] [--port 21] [--rounds 10] [--send 8192] [--gap 2] +# [--graceful] [--check-abort] # Output: TS / ABORT sent= ctrl= alive= followup= +# abort5xx= (only with --check-abort: RST expects 4xx/5xx on the +# aborted STOR = no false 226 (Trap 23 hardening); graceful FIN expects 2xx +# = protocol boundary, still reported as success) # SUMMARY rounds= pass= fail= -# Exit 0 all follow-ups pass, 1 otherwise. +# Exit 0 all rounds pass, 1 otherwise. import argparse, hashlib, io, random, re, socket, struct, sys, time from ftplib import FTP, all_errors @@ -67,6 +71,9 @@ def main(): ap.add_argument("--gap", type=float, default=2.0) ap.add_argument("--graceful", action="store_true", help="close the data connection with FIN instead of RST") + ap.add_argument("--check-abort", action="store_true", + help="also assert the aborted STOR's control reply: RST -> 4xx/5xx " + "(no false 226, Trap 23 hardening); graceful FIN -> 2xx (boundary)") a = ap.parse_args() npass = nfail = 0 @@ -108,11 +115,18 @@ def main(): except Exception: alive = "no" fu = followup_ok(a.ip, a.port) - ok = (alive == "yes" and fu) + abort_ok = True + if a.check_abort: + code = ctl_reply[0] if (ctl_reply and ctl_reply[0].isdigit()) else "?" + # RST: device must report 4xx/5xx on the aborted STOR (450); graceful + # FIN still gets 226 (STOR has no length declaration - protocol boundary) + abort_ok = (code == "2") if a.graceful else (code in ("4", "5")) + ok = (alive == "yes" and fu and abort_ok) npass += 1 if ok else 0 nfail += 0 if ok else 1 - log("ABORT %d sent=%d ctrl=%s alive=%s followup=%s" % - (n, a.send, ctl_reply.replace("\r\n", "|")[:40], alive, "OK" if fu else "FAIL")) + log("ABORT %d sent=%d ctrl=%s alive=%s followup=%s abort5xx=%s" % + (n, a.send, ctl_reply.replace("\r\n", "|")[:40], alive, + "OK" if fu else "FAIL", "OK" if abort_ok else "FAIL" if a.check_abort else "-")) if a.gap > 0 and n < a.rounds: time.sleep(a.gap) log("SUMMARY rounds=%d pass=%d fail=%d" % (a.rounds, npass, nfail)) diff --git a/docs/问题记录/CH395F_Trap_Records.md b/docs/问题记录/CH395F_Trap_Records.md index 337c20d7..e5fd9ed8 100644 --- a/docs/问题记录/CH395F_Trap_Records.md +++ b/docs/问题记录/CH395F_Trap_Records.md @@ -637,11 +637,11 @@ NET 层 TCP Client(阶段 7)`net_connect()` 返回 0,但 `net_poll()` 报 & '.venv\Scripts\python.exe' MDK-ARM\ftp_abort_probe.py --rounds 5 --send 8192 --gap 1 --graceful ``` -### 修复方向(待办 N-8) -1. **relisten 前让芯片 socket 真正沉降**:确认 `ch395f_get_socket_status == CLOSED` 且清掉该 socket 中断状态后再 listen(现有 relisten 的单次清理在中止场景下不足); -2. **新连接的陈旧中断防御**:数据 socket 刚 accept、尚未读到任何数据就收到 DISCONNECT 时判为陈旧中断 → 重新 relisten,而不是当作正常 EOF 结束上传; -3. **应用侧兜底**:`receive_file` 区分"正常 EOF"与"连接异常结束",异常时回 5xx(让客户端重试)而非 226,避免误报成功; -4. **回归判据**:`ftp_abort_probe.py` 的 `followup` 由 FAIL → **OK(10/10)**,且 `ftp_freeze_probe.py` 的 177 轮对照仍全匹配。 +### 修复方向(待办 N-8,全部闭环) +1. ✅(2026-09-23 一阶段)**relisten 前让芯片 socket 真正沉降**——隔离期:持续排空 RX + 主动 close + 等 `CLOSED/TCP_CLOSED`,隔离满 400ms 才 `open+listen`; +2. ✅(2026-09-23/24)**新连接的陈旧中断防御**——陈旧 DISCONNECT 以芯片状态回读甄别(D20:芯片 TCP 仍 ESTABLISHED 判陈旧丢弃)+ 释放残留经两级沉降/隔离(100ms/400ms)后不再投递给下一条连接; +3. ✅(2026-09-25)**应用侧兜底**——异常释放(TIMEOUT/RST)的挂起 recv 以 `NET_ERR_CONNRESET` 了结(正常 FIN 仍回 0——协议边界)→ STOR 异常结束回 450 并删除截断文件; +4. ✅ **回归判据**——`ftp_abort_probe.py` followup:RST 10/10 + FIN 5/5(一阶段)、8/8 + 5/5 + 5/5(二阶段);`--check-abort` 断言中止 STOR 应答码(RST→450 / FIN→226 边界);177 轮对照当时全匹配,其后各轮正常路径回归全过。 ### 发现时间 2026-09-23(可信复现首轮:真实生产路径 FTP 中止/复连场景,由 `MDK-ARM/ftp_abort_probe.py` 自动判定) @@ -701,3 +701,37 @@ NET 层 TCP Client(阶段 7)`net_connect()` 返回 0,但 `net_poll()` 报 **结论**:缺陷位于 **fd 复用生命周期**(芯片上一条连接的残留状态投递给下一条连接),不在 FTP 命令逻辑——lftpd 只是把它放大成误报 226。RST/TIMEOUT 半边由一阶段隔离处理,DISCONNECT 半边由本阶段沉降处理,合计覆盖全部"收过数据后关闭"的释放路径。已知残余边界:中止那一笔 STOR 自身仍回报 226(STOR 无长度声明、在途 FIN 与正常完成不可区分)——后续硬化见待办 #3。 **对应文件**:`Drivers/BSP/NET/net_socket.c`(`handle_disconnect_event` 沉降登记 + `net_relisten_locked` 两级沉降);`MDK-ARM/ftp_abort_probe.py` / `MDK-ARM/bench_diag_abort_gap.py`(边界甄别与回归判据)。 + +### 后续硬化(2026-09-25):异常结束回 5xx(待办 #3 闭环) + +**目标**:中止那一笔 STOR 自身不再误报 226——异常结束(连接重置/超时)时回 450 并删除截断文件,把"静默损坏"变成"显式失败"(纵深防御;客户端按协议重试)。 + +**实现(3 个工程文件 + 1 个台架脚本)**: +1. `net_types.h`:`net_sock_t` 增加释放来源标记 `released_abnormal`(RAM +8B:1B×8 socket,BSS,运行堆零影响); +2. `net_socket.c`:`handle_timeout_event`(异常释放/TIMEOUT)置 1;`handle_disconnect_event`(正常释放/DISCONNECT)置 0;`net_socket()`(新 fd)与 `net_relisten_locked` 成功尾(新监听)清零——新连接不得继承上一条连接的释放原因; +3. `net_socket.c` `net_pending_tick` 断连了结(③):排空缓冲后,标记为 1 以 `NET_ERR_CONNRESET` 了结(对端未发正常 FIN,不是正常 EOF),标记为 0 仍 EOF(0); +4. `lftpd.c` `receive_file`:`err < 0` 时 `unlink` 截断文件(原先残留于盘)并返回错误码 → STOR 处理器回 450; +5. `MDK-ARM/ftp_abort_probe.py` 增加 `--check-abort`:RST 中止要求中止 STOR 控制应答 4xx/5xx,优雅 FIN 要求 2xx(协议边界,见下)。 + +**协议边界(不可判定,留痕)**:FTP STOR 无长度声明,"在途 FIN 关闭"与"正常完成"**逐字节不可区分**(芯片均报 DISCONNECT、缓冲均排空)→ 优雅中止仍回 226(中止方客户端通常也不等应答);仅 RST/掉链(芯片 TIMEOUT)可识别 → 450。FIN 边界由客户端长度校验(SIZE/RETR 往返)兜底。 + +**各消费端负值处理复核**(均按"负值=结束"与 0 同路,行为不变): +- lftpd 控制通道 `read_line`:负值 → `line_len != 0` → 会话清理,与 0 同路(控制通道不 close,auto_relisten 恢复); +- time_sync(`:8888`):n<0 → break 弃连接,与 0 路径一致;8B 时间包已收齐时仍写 RTC; +- UI sink(`:7003`):DONTWAIT 非阻塞,不走 L3 pending,零影响; +- 测试套件:产品构建不启用;测试对端干净关闭,无影响。 + +**验证(硬化后,2026-09-25 台架)**: + +| 检查项 | 结果 | +|---|---| +| `ftp_abort_probe --check-abort --rounds 5`(RST) | **5/5**——中止 STOR → **450**(原 226),followup 5/5,控制存活 5/5 | +| 设备侧证据 | `sock4 TIMEOUT` → `[ERR][FTP] UPLOAD '/ab0001.bin' FAILED` → 450;followup `UPLOAD '/ab_ok.bin' 4096 bytes OK` | +| `--check-abort --graceful --rounds 5`(FIN) | **5/5**——中止 STOR → **226**(边界不变),followup 5/5 | +| 正常往返 freeze 4096×4 / 64KB×2 | **4/4 / 2/2**(SHA-256 全 MATCH) | +| Trap 22:`:21` 0ms×50 / 100ms×50,`:7003` 300ms×10,`:8888` 0ms×20 | **50/50 / 50/50 / 10/10 / 20/20** | +| 非 FTP `:7003` sink 复用(RST×4 / FIN×4,400 帧在途) | **4/4 / 4/4**(ack 2000/2000,LCD 3209/3209,CRC 全过) | +| 时间同步 `:8888` 真实收包 | `wall clock updated (epoch=…)` 写 RTC 成功,监听恢复 | +| 构建 / 回读 / 启动门禁 / 控制台 / 堆 | 0 错 0 警、`No difference found`、FORBID 0、`CONSOLE dropped 0`、`heap free=8776 min_ever=4816`(不变) | + +**对应文件**:`Drivers/BSP/NET/net_types.h`(`released_abnormal`)、`Drivers/BSP/NET/net_socket.c`(三处置位/清除 + `net_pending_tick` 了结分支)、`Drivers/BSP/NET/lftpd/lftpd.c`(`receive_file` 错误路径 unlink)、`MDK-ARM/ftp_abort_probe.py`(`--check-abort`)。