W5波+N-19收口:FTP鉴权落地、Trap25三连定案修复(N-30)、N-20/N-23/N-26

N-19 FTP 鉴权:
- lftpd_auth(USER/PASS 状态机 + 连续5次失败锁60s)+ SHA-256
  (user||pwd) 口令哈希;config.ini [ftp] 段 user/pwd_hash/write,
  出厂 admin/admin 在用有启动告警;lftpd_log.c 删除并入 lftpd_auth
- ftp_cfg 配置读写;台架 ftp_auth_bench.py 13/13

N-30 / Trap 25(CH395F_Trap_Records.md 四次定案全记录):
- 修复1 closed-with-data accept:客户端发完正常 FIN 后芯片按
  FUN_PARA(0x09) §5.52② 保留 Socket+RX 缓冲,旧代码把 CLOSED 一律
  relisten 自毁缓冲 → STOR 必挂 425;改为接受"已关但可读"连接排空
- 修复2 丢 CONNECT 中断探针 net_lost_connect_probe + last_peer_port
  幽灵甄别(拆链窗口握手完成但中断丢失,双信源去抖补 accept)
- 修复3 send_list/send_nlst 批量单发:背靠背 ~60B 小段毒化芯片 TX
  (芯片报 SEND_OK 但对端全丢),整份目录一次性写入
- 证伪存档:固定沉降/Socket 轮换冷却/listen 门控(台架 v7 矩阵)
- 台架工具 test/board/ch395f_reuse_bench.{c,h} + host 脚本存档
  (不注册编译;实验分支 n30-chip-bench 保留)

N-20:Error_Handler 死循环降级(记录+复位路径)
N-23:scripts/check_registration.py 双端注册反向校验(pre-commit)
N-26:GD5F NAND 直 DMA 调用方缓冲窗口断言

验证:BUILD_OK 0/0;ftp_regression 连跑 4 轮 11/11(修复前
LIST#4/#7 100% 挂、STOR#2 曾 100% 425);ftp_auth_bench 13/13;
@test 启动标记 PASS;ci_local L0+L0.5 全过;doc-links 通过。
登记簿 N-30 已销项。
This commit is contained in:
2026-10-11 18:50:24 +08:00
parent eddbedeee8
commit d1e6bb4ba7
75 changed files with 3928 additions and 352 deletions
+143 -11
View File
@@ -581,8 +581,18 @@ static void net_reconcile_tick(void) {
bad = 1;
} else if ((p_sock->state == NET_SOCK_STATE_LISTENING) &&
(st[1] == CH395F_TCP_ESTABLISHED)) {
/* ② 本层 LISTENING 而芯片已完成握手(丢 CONNECT 中断)→ 补做 accept */
bad = 2;
/* ② 本层 LISTENING 而芯片已完成握手(丢 CONNECT 中断)→ 补做 accept
* N-30(Trap 25)幽灵态甄别:拆链收尾窗口内芯片假报 ESTABLISHED,
* 回读对端端口 = 上一条连接端口 → 不补 accept(补了会把数据发给
* 已死的旧连接 = 生产 LIST#4 数据被吞的直接成因),等拆净或新 SYN */
uint8_t remote[6] = {0, 0, 0, 0, 0, 0};
ch395f_get_remot_ipp((uint8_t)i, remote);
uint16_t peer = (uint16_t)(remote[4] | (remote[5] << 8));
/* 仅"端口非零且 = 上一条连接端口"判幽灵拦截;peer==0 不拦截
* (对端端口提交滞后时不得挡掉丢中断救援,见 net_lost_connect_probe 注释) */
if (!((peer != 0U) && (peer == p_sock->last_peer_port))) {
bad = 2;
}
} else if ((p_sock->state == NET_SOCK_STATE_LISTENING) &&
((st[0] == CH395F_SOCKET_CLOSED) || (st[1] != CH395F_TCP_LISTEN))) {
/* ③ 本层 LISTENING 而芯片监听已消失 → 重建监听 */
@@ -768,6 +778,9 @@ void net_process_messages(void) {
* close/重监听命令(会摧毁芯片侧仍健康的监听器 → 端口永拒,
* run8 实证 170s 拒绝);新连接的 CONNECT 中断会把两侧重新同步 */
p_sock->state = NET_SOCK_STATE_LISTENING;
/* N-30(Trap 25):死连接对端端口转登幽灵甄别基准 + 去抖清零 */
p_sock->last_peer_port = p_sock->remote_port;
p_sock->lost_conn_seen = 0U;
/* 取消本 fd 在死连接上的挂起 recv(必通知,防调用方挂死) */
net_pending_cancel(msg.sockfd, NET_ERR_CONNRESET);
/* 仅修正 MCU 侧状态,不发芯片命令,直接返回成功 */
@@ -1090,6 +1103,8 @@ int net_listen_locked(int sockfd) {
p_sock->state = NET_SOCK_STATE_LISTENING;
/* 单连接模式:连接断开后由 net 层自动回到 LISTEN(模拟 BSD 监听常驻) */
p_sock->auto_relisten = 1;
/* N-30(Trap 25):新监听会话,丢中断去抖计数清零 */
p_sock->lost_conn_seen = 0U;
/* 单连接:CONNECT 中断直接作用于本 Socket 自身;启动监听前清空各
* Socket 及全局中断状态寄存器(读即清除),避免历史残留/伪中断在
* 监听刚开始时误触发连接或断开事件 */
@@ -1164,6 +1179,84 @@ int net_listen(int sockfd) {
* 限定条件:Socket 处于 LISTENING 状态
* 函数说明:单连接:监听 Socket 自身在 CONNECT 后已转为 ESTABLISHED
*/
/* ===== N-30 accept 路径"丢 CONNECT 中断"补 accept(2026-10-11 裸机芯片台架实证)=====
* 台架结论(剥离 OS/net/FTP 直接对芯片做受控 close→open→listen→connect 矩阵):
* 芯片关闭连接后的拆链收尾窗口**变长且不可见**(状态寄存器显示 CLOSED 也不代表
* 拆完,实测 >2s 仍可能未拆完);该窗口内重新 listen 后,新连接握手照常完成
* (芯片状态寄存器 → ESTABLISHED)但 **CONNECT 中断被丢**——纯事件驱动的 accept
* 永远等不到 → 数据被吞 / 425。固定沉降延时、Socket 轮换、listen 后 close 重试、
* 缓冲重分配均已被实验证伪;唯一有效 = "中断 + 状态轮询"双信源(修复组 15/15
* 全过,机制触发 5 次全部救回)。→ CH395F_Trap_Records.md Trap 25
* 幽灵态甄别:拆链窗口内芯片也会直接假报 ESTABLISHED(旧连接未拆净),此时回读
* 对端端口 = 上一条连接端口;新连接临时端口几乎必然不同 → 端口相同不 accept,
* 等旧连接拆净 / 新 SYN 到达(新连接若也丢中断,后续轮询以新端口命中)。 */
#define NET_LOST_CONN_STABLE_N 2U /* 连续确认轮次(accept 轮询 100ms/轮,≈200ms 去抖) */
/*
* 函数功能:监听等待期的芯片状态轮询探针——芯片已 ESTABLISHED 而 CONNECT 中断
* 未到(丢中断)时,去抖满后补做 accept(复用 CONNECT 事件簿记路径)。
* 入口参数:p_sock - 处于 LISTENING 的监听控制块 net_sock_t *
* 出口参数:无(命中时 p_sock->state 经 handle_connect_event 转 ESTABLISHED)
* 返回值:无
* 限定条件:netTask 上下文(net_accept_locked 内调用,SPI 访问安全)
* 函数说明:1. 每轮 accept 轮询(100ms)读一次芯片状态,兼作状态第二信源
* 2. 幽灵态(对端端口 = last_peer_port)不动手,去抖清零
* 3. 连续 NET_LOST_CONN_STABLE_N 轮确认后才补 accept(防"刚 listen
* 尚未生效"的瞬态误判——2026-09-23 实测教训)
*/
static void net_lost_connect_probe(net_sock_t *p_sock) {
/* 芯片侧状态两字节:[0]=socket 状态,[1]=TCP 状态 */
uint8_t st[2] = {0, 0};
/* 对端 IP+端口回读缓冲(4B IP + 2B 端口小端) */
uint8_t remote[6] = {0, 0, 0, 0, 0, 0};
/* 读芯片状态 */
ch395f_get_socket_status(p_sock->ch395_sock, st);
/* 芯片尚未完成握手:去抖计数清零,本轮无动作 */
if (st[1] != CH395F_TCP_ESTABLISHED) {
p_sock->lost_conn_seen = 0U;
return;
}
/* 已 ESTABLISHED:回读对端端口做幽灵甄别 */
ch395f_get_remot_ipp(p_sock->ch395_sock, remote);
/* 对端端口(小端 2 字节) */
uint16_t peer = (uint16_t)(remote[4] | (remote[5] << 8));
/* 幽灵甄别:仅"端口非零且 = 上一条连接端口"才判幽灵拦截;
* peer==0(芯片对端端口提交滞后,Trap 15 同类)不拦截——宁可补 accept
* 也不退回丢中断死等(2026-10-11 验证轮实测:peer==0 拦截把首连接
* 丢中断的原有对账救援也挡死,LIST#1 卡满 20s,比修复前更糟) */
if ((peer != 0U) && (peer == p_sock->last_peer_port)) {
p_sock->lost_conn_seen = 0U;
return;
}
/* 去抖:连续确认满门槛才补 accept(封顶防溢出) */
if (p_sock->lost_conn_seen < NET_LOST_CONN_STABLE_N) {
p_sock->lost_conn_seen++;
}
if (p_sock->lost_conn_seen < NET_LOST_CONN_STABLE_N) {
return;
}
/* 动手前清去抖计数 */
p_sock->lost_conn_seen = 0U;
/* 留痕:丢中断自愈命中 */
DBG_INFO("sock%d lost-CONNECT self-accept (peer=%u)",
(int)p_sock->ch395_sock, peer);
/* 复用 CONNECT 事件的簿记路径(转 ESTABLISHED + 记对端 + 事件回调) */
handle_connect_event(p_sock);
}
/*
* 函数功能:TCP Server 接受新连接(内部版本,必须在 netTask 上下文调用)
* 入口参数:sockfd - 监听 Socket 描述符 int
* 出口参数:addr - 输出:客户端地址结构体指针 struct net_sockaddr *(可为 NULL)
* addrlen - 输入输出:地址结构体长度 int *(可为 NULL)
* 返回值:新连接的 Socket 描述符,失败返回 -1 int
* 限定条件:Socket 处于 LISTENING 状态
* 函数说明:1. 单连接:监听 Socket 自身在 CONNECT 后已转为 ESTABLISHED
* 2. 等待期叠加芯片状态轮询探针(丢 CONNECT 中断自愈,Trap 25)
* 3. CLOSED + 芯片 RX 缓冲有残留数据:接受"已关但可读"连接排空
* (FUN_PARA §5.52 ② 保留现场契约,Trap 25 三次定案)
*/
static int net_accept_locked(int sockfd, struct net_sockaddr *addr, int *addrlen) {
/* 监听 Socket 控制块 */
net_sock_t *p_listen_sock;
@@ -1178,18 +1271,43 @@ static int net_accept_locked(int sockfd, struct net_sockaddr *addr, int *addrlen
/* 先轮询一次:把状态机刷新到芯片侧最新状态 */
net_poll();
/* N-30(Trap 25):监听等待期兼做芯片状态第二信源——丢 CONNECT 中断时
* 去抖补 accept(裸机台架实证:拆链窗口丢中断是数据被吞/425 的主根因,
* 纯事件等待无自愈可能;详见 net_lost_connect_probe 区块注释) */
if (p_listen_sock->state == NET_SOCK_STATE_LISTENING) {
net_lost_connect_probe(p_listen_sock);
}
/* 单连接:本 Socket 自身在 CONNECT 后处于 ESTABLISHED 才表示有可接受的连接
* 芯片丢中断导致的失同步不在此处自愈:单次状态读会误判"刚 listen 尚未生效"
* (2026-09-23 实测风暴:监听被反复重建直至放弃),统一由 net_reconcile_tick() 周期对账(带去抖)处理 */
* (含上一步探针补 accept 的命中);其余失同步由 net_reconcile_tick()
* 周期对账(带去抖)兜底 */
if (p_listen_sock->state != NET_SOCK_STATE_ESTABLISHED) {
/* 连接已断开(CLOSED)且启用自动重监听:先把 Socket 重新置回 LISTEN,
* 下一次 accept 即可等待新连接(BSD 风格监听常驻) */
/* N-30(2026-10-11 手册 §5.23⑤ + 生产时间线定案):
* "closed-but-readable" 接受——STOR 类传输客户端**发完数据立即关数据连接**
* (正常 FTP 行为,Python ftplib sendall 后即 close),芯片按
* FUN_PARA(SOCKET_CLOSE=0) 规格保留 Socket 与 RX 缓冲供 MCU 排空
* (实测 DISCONNECT 后 st=0x05/0x00、recv_len=1027=完整载荷)。
* 若 accept 轮询晚于对端 FIN,盲目 relisten(close+open 清缓冲)会把
* 已到达的数据销毁 → 425(此前 N-30"425/被吞"的主根因,并非芯片杀连接)。
* 处置:芯片 RX 缓冲有数据 → 接受这条"已关但可读"连接,应用经
* net_recv_locked 快路径(无状态门槛)排空,排空后 EOF 自然触发 relisten。 */
int drain_pending = 0;
if (p_listen_sock->state == NET_SOCK_STATE_CLOSED && p_listen_sock->auto_relisten) {
/* 非阻塞:失败由 D10 重试轮询接管 */
(void)net_relisten_try(p_listen_sock);
if (ch395f_get_recv_len((uint8_t)sockfd) > 0) {
drain_pending = 1;
DBG_INFO("sock%d closed-with-data accept (drain pending)", sockfd);
} else {
/* 连接已断开(CLOSED)且无残留数据:先把 Socket 重新置回 LISTEN,
* 下一次 accept 即可等待新连接(BSD 风格监听常驻) */
/* 非阻塞:失败由 D10 重试轮询接管 */
(void)net_relisten_try(p_listen_sock);
}
}
/* 当前无连接 → WOULDBLOCK */
return NET_ERR_RETURN_1(NET_ERR_WOULDBLOCK);
if (drain_pending == 0) {
/* 当前无连接 → WOULDBLOCK */
return NET_ERR_RETURN_1(NET_ERR_WOULDBLOCK);
}
/* drain_pending:落入下方 accept 返回(对端簿记仍在控制块,排空后 EOF 了结) */
}
/* 填充对端地址(若调用方需要) */
@@ -1895,6 +2013,10 @@ static int net_relisten_locked(net_sock_t *p_sock) {
p_sock->state = NET_SOCK_STATE_LISTENING;
/* 新监听不得继承上一条连接的释放原因(Trap 23 待办 #3) */
p_sock->released_abnormal = 0U;
/* N-30(Trap 25):清零前把对端端口转登为幽灵甄别基准 */
p_sock->last_peer_port = p_sock->remote_port;
/* N-30:新监听会话,丢中断去抖计数清零 */
p_sock->lost_conn_seen = 0U;
/* 清对端端口(新连接待用) */
p_sock->remote_port = 0;
/* 清对端 IP */
@@ -2432,7 +2554,11 @@ static int net_close_locked(net_sock_t *p_sock) {
} while (1);
/* 清零控制块(memset 0 = 释放本 fd 全部状态) */
/* N-30(Trap 25):幽灵甄别基准跨 fd 释放保留(幽灵属芯片 Socket 级状态,
* 与 fd 复用无关——close 后新 listen 落在同一芯片 Socket 上仍可能撞幽灵) */
uint16_t keep_peer = p_sock->remote_port;
memset(p_sock, 0, sizeof(net_sock_t));
p_sock->last_peer_port = keep_peer;
/* 关闭即取消该 fd 的待重试(D10 重试队列) */
net_relisten_clear(sockfd);
/* 关闭即清除该 fd 的挂起状态(L3;幂等) */
@@ -3478,7 +3604,8 @@ static void handle_disconnect_event(net_sock_t *p_sock) {
}
/* 单连接:先不关 CH395F Socket,
* 留给 net_recv_locked 把剩余数据读出来再关,置 CLOSED 等待应用重新监听 */
DBG_INFO("sock%d DISCONNECTED, releasing (chip tcp=0x%02X)", p_sock->ch395_sock, st[1]);
DBG_INFO("sock%d DISCONNECTED, releasing (chip st=0x%02X/0x%02X)",
p_sock->ch395_sock, st[0], st[1]);
/* Trap 23 推广(2026-09-24 台架实证):正常 DISCONNECT 释放留下的芯片收尾状态
* 会在复用同一 fd 的下一条连接上以"伪 DISCONNECT"形式投递(实测 accept 后约 10~20ms)→ 该连接被提前释放,应用把已到的 2048B 当整份文件并回报 226;
* 仅**本连接收过数据**时登记沉降(实测触发源 = 收过数据的数据通道被关闭),纯连接即关/只发不收的连接免沉降,保住 Trap 22 的快速复连时序(:21 @0ms ×50 全通过) */
@@ -3494,6 +3621,9 @@ static void handle_disconnect_event(net_sock_t *p_sock) {
}
/* 正常释放(对端 FIN):断连了结回 EOF(0) */
p_sock->released_abnormal = 0U;
/* N-30(Trap 25):登记本连接对端端口(幽灵态甄别基准,须在状态
* 迁移前记录——remote_port 此刻仍有效,relisten 清零) */
p_sock->last_peer_port = p_sock->remote_port;
/* 状态 → CLOSED */
p_sock->state = NET_SOCK_STATE_CLOSED;
/* 触发断连事件回调 */
@@ -3581,6 +3711,8 @@ static void handle_timeout_event(net_sock_t *p_sock) {
* NET_ERR_CONNRESET 了结(而非 EOF),应用(lftpd STOR)回报 5xx
* 而非误报 226(见 net_pending_tick 断连分支) */
p_sock->released_abnormal = 1U;
/* N-30(Trap 25):异常释放同样登记对端端口(幽灵甄别基准) */
p_sock->last_peer_port = p_sock->remote_port;
/* 状态 → CLOSED */
p_sock->state = NET_SOCK_STATE_CLOSED;
} else if (p_sock->state == NET_SOCK_STATE_LISTENING) {