fix(net): B3 只清本 Socket 中断;B10 对账认领孤儿监听修永久拒连
B3(net_listen_locked / net_relisten_locked 两处):
原先对全部 8 个 Socket 做 GET_INT_STATUS_SN(读即清)并丢弃式读全局中断
状态。依据三条:§5.15 明文全局寄存器整体读即清且无按位清除手段;net_poll
只在全局位置位时才读该 Socket 的 SINT(位图门控);SINT 粘滞到被读
(Trap 14 实证 close/open 都不清)→ 读走即永久销毁其它在用 Socket 的
CONNECT/DISCONNECT/TIM_OUT/SENBUF_FREE。git 考古定性为多连接时代调试遗留
(e862009bc → 319d00e3 → db64e6acf),不属于任何已登记 Trap 的修复;官方
SDK 21 个例程一律"位图门控 + 单 Socket 读 + 立刻转存",open/listen 前后
不清任何中断,无一例全量读 8 个 SINT。现收窄为只清本 Socket 的 SINT,
全局交 net_poll 集中读清,与 AGENTS 模块约定一致。
B10(孤儿监听 → 端口永久拒连):
断开时 handle_disconnect_event 释放分支置 CLOSED 并注释"等待应用重新监
听",而 ui_net_sink 只在 net_recv 返 0(EOF)时才清 g_client,EOF 已被该
事件分支消费 → net 层等应用、应用等 EOF、对账只裁 ESTABLISHED/LISTENING
直接跳过、芯片不会自己回 LISTEN,四方互等。实测孤儿态 state=0 /
chip=0x05/0x00 持续 12s+,:7003 300ms×10 为 0/10 且 +30s 仍拒连。
net_reconcile_tick 新增类型④认领孤儿,复用 net_relisten_try(自带"已在
重试队列不重复登记"保护,与 D10 队列、Trap 23 沉降隔离兼容)。门槛中
auto_relisten 是决定性一条:缺它则出向连接(板级 Phase 7 TC701~703 绑本
地口)会被误判孤儿并被 relisten 成监听器。
工具:ftp_abort_probe.py 仍用 anonymous 登录,N-19 后 530 拒 → Trap 23 两
条腿假 0/5(round1 PASV failed,且半认证会话占死 FTP 唯一会话)。改
admin/admin 后 RST 5/5、FIN 5/5。
回归::7003 300ms×10 由 0/10 永久死 → 10/10(+20s 存活);:21 0ms×50 与
100ms×50 均 50/50;:8888 0ms×20 为 20/20;ftp_regression 11/11×2;Trap 23
RST 5/5、FIN 5/5;@test PASS(600ms、回读无差异);@build 0/0,
Code=197084。
残余:B10 生产者未根治,释放分支仍把状态停在 CLOSED 等应用重建,类型④
是兜底;根治涉及 Trap 25 closed-with-data 契约,单列评估。
This commit is contained in:
@@ -940,13 +940,14 @@ Trap 22 的周期对账(200ms 节拍 + 400ms 去抖 + 1s 冷却)设计目标
|
||||
| A6 | 低 | `ch395f.c:1176` | `write_send_buf` 超限**静默 return**(void、无日志),且上限取 DMA 2048 而非发送缓冲 1024;读路径有 OVERSIZE 金丝雀、写路径没有 | ✅ 已修(补 `[ERR]` 留痕 + >1 KB 告警,行为不变) |
|
||||
| B1 | ~~中~~ **已推翻** | `net_socket.c:3553` | 原判"产品路径从未启用 Socket KeepAlive → 静默断开无人知晓"(§5.56 服务器侧应在 CONNECT 后开,产品三服务全是 Server,60s/5s/3 参数空转)。**2026-10-12 实测推翻前提**:连接静置时禁用 PC 网卡(真·静默死亡),芯片 **+2 s** 即报 `[ERR][NET] sock0 TIMEOUT` → 本层 `relisten quarantine` 释放重开;2 s 恰为 §5.17 重传 8×200 ms=1.6 s 量级,**与 KeepAlive 无关**(IDLE 最短 60 s)。链路层失效本就被快速覆盖。 | ✅ 已按 §5.56 补过使能并实测,**依 Trap 25「不留未证明的复杂度」撤回**,代码内留注释记录;残余场景见下「台架注入陷阱」 |
|
||||
| B2 | 中低 | `net_socket.c:1638` | `ch395f_tcp_connect()` 返回值被丢弃(同文件 open/listen 都查)。§9.2.2 ⑧ 明文:`GET_CMD_STATUS` 返回 SUCCESS 仅表示**命令**执行成功、其他值表示**命令执行失败**,丢弃它 = 命令级被拒要等 30 s recv 超时才暴露 | ✅ 已修(2026-10-12):非 SUCCESS 且**非 BUSY** 才判失败——`poll_cmd_status` 超时返回的就是 `ERR_BUSY`(芯片仍在执行、命令未被拒),把它当失败会误杀慢芯片;明确失败 → `[ERR] rejected by chip: 0xNN` + 按 §9.2.2 ⑧"从 ⑤ 重开"关 Socket 回滚 + 返 `-1`。**Phase 7 实测回归**:TC701 回显 64 B 校验 PASS、TC702 重连 ×3 全 PASS(正常连接无误杀);不可达对端时芯片回的是 SUCCESS、`ret=0` 且不打 `rejected by chip`,失败仍由 `CONNECT TIMEOUT`(~1.6 s) 裁定 → 行为未变。⚠ 残余:`net_reconcile_tick` 仍只裁 ESTABLISHED/LISTENING,`CONNECTING` 丢中断无人收口(产品不走出向连接、无法实测,按"不加未证明复杂度"暂不动);TC703 断言 `rc==0` 把"返回值被丢弃"写进了判据,真发生命令级被拒时它会 FAIL(属正确告警) |
|
||||
| B3 | 低 | `net_socket.c:1120-1123` | `net_listen_locked` 对 8 个 Socket 全量 `GET_INT_STATUS_SN`(读即清)→ 吞掉其它在用 Socket 的挂起事件(RECV 是电平语义可下轮补回,CONNECT/DISCONNECT 边沿语义会丢) | ⏳ 待修(只清本 Socket) |
|
||||
| B3 | 低 | **两处**:`net_socket.c:1120-1125`(`net_listen_locked`)+ `net_socket.c:2060-2065`(`net_relisten_locked`,审计原只登记前者,**后者才是每次断连的热路径**) | 两处对**全部 8 个 Socket** `GET_INT_STATUS_SN`(读即清)+ 无条件读清全局。机制:§5.15 明文「此命令执行完毕后…将全局中断状态清除」= 全局寄存器**整体读即清、无按位清除手段**,而 `net_poll` 只在**全局位置位时**才去读该 Socket 的 SINT(`:469` 门控)→ 丢弃式读全局把别的在用 Socket 的门一并焊死;SINT 又**粘滞到被读**(Trap 14 实证 close/open 都不清 SINT)→ 全量读走即**永久销毁**其 CONNECT/DISCONNECT/TIM_OUT/SENBUF_FREE(唯 RECV 是条件位可下轮补,Trap 08)。git 考古:`e862009bc`(2026-07-19 多连接时代,无注释引入,同提交还删掉了 net_poll 里"勿在此读 per-socket 中断"的警告) → `319d00e3` 原样搬进单连接 → `db64e6acf` 复制进 relisten;**不属于任何已登记 Trap 的修复**(Trap 14/17/23 指定的预清都在别处且是单 Socket)。官方 SDK 21 个例程一律"全局位图门控 + 单 Socket 读 + 立刻转存",open/listen 前后**不清任何中断**,无一例全量读 8 个 SINT | ✅ 已修(2026-10-12):两处收窄为**只清本 Socket 的 SINT** + **删除丢弃式全局读清**(全局交 `net_poll` 集中读清,与 AGENTS 模块约定「所有 CH395F 中断在 net_poll 内集中读取并清除」一致);本 Socket 残留的全局位被 `net_poll` 读到时 SINT 已为 0,无害了结。**两路子 Agent 曾分歧**:一路建议保留全局读清("只搁浅不销毁"),一路警告"保留=修一半"——采后者,芯片无按位清除,保留即热路径继续焊门。**回归**:`@build` 0/0 `Code=197008`(−24 B);`@test` PASS;Trap 25 `ftp_regression` **11/11 ×3**;Trap 23 `ftp_abort_probe --check-abort` RST **5/5**(450) + FIN **5/5**(226);Trap 22 `:21` 0ms×50 **50/50**、100ms×50 **50/50**、`:8888` 0ms×20 **20/20**(均与文档基线一致);`:7003` 腿见 B10(A/B 已证明与本改动无关;B10 修完后该腿回到 **10/10**)。⚠ 残余:`net_connect_locked:1601` / `net_close_locked:2571` / `net_relisten_locked:1986` 三处无条件全局读清仍会**搁浅**(延后、非销毁)其它 Socket 挂起事件——它们是 Trap 14/17/23 的指定两级预清,本次不动,宜与 B4 一并处置 |
|
||||
| B4 | 低 | `net_socket.c:446/469` | 全局中断 bit0-3(`UNREACH`/`IP_CONFLI`/`PHY_CHANGE`/`DHCP`)被读清但从不消费;`ch395f_get_unreach_info()` 全仓零调用 → IP 冲突、ARP 不可达、链路抖动三类事件完全静默 | ⏳ 待修(可观测性缺口) |
|
||||
| B5 | 低 | `net_socket.c:3849-3854` → `lftpd.c:803-809` | `net_getsockname` 的 `s_addr` 组装方向与本层约定(`net_inet_addr`/`net_inet_ntoa`)相反 → FTP 日志把设备自身 IP 打成 `100.1.168.192`。⚠ 同一 `getsockname` 的 PASV 227 消费点(`lftpd.c:497-503`)做了 `net_htonl` 再 MSB 拆,**双重反转恰好正确,勿误判**;风险前瞻:若把该 `s_addr` 喂给 `net_connect`/`net_sendto` 就会连到反序地址 | ⏳ 待修(改一处须同时核对 PASV) |
|
||||
| B5 | 低 | `net_socket.c:3878-3881`(`net_getsockname`,函数 :3851-3887;原登记 :3849-3854 已陈旧)→ `lftpd.c:803-809` | `net_getsockname` 的 `s_addr` 组装方向与本层约定(`net_inet_addr`/`net_inet_ntoa`)相反 → FTP 日志把设备自身 IP 打成 `100.1.168.192`。⚠ 同一 `getsockname` 的 PASV 227 消费点(`lftpd.c:497-503`)做了 `net_htonl` 再 MSB 拆,**双重反转恰好正确,勿误判**;风险前瞻:若把该 `s_addr` 喂给 `net_connect`/`net_sendto` 就会连到反序地址 | ⏳ 待修(改一处须同时核对 PASV) |
|
||||
| B6 | 低 | `net_socket.c:591/1225/3608` | 3 处状态判据只看 `byte1`、未按 §5.22 用 `byte0` 门控;reconcile 的 else-if 链里 `(00H, 陈旧 04H)` 会先命中"补 accept"而非"重建监听" | ⏳ 待修(Trap 07 实测 `byte0=00H` 时 `byte1` 读回 `0x00`,故为低概率) |
|
||||
| B7 | 低 | `ch395f_test_task.c:1379/1504` | Phase 5 单次写 4093 B 超 1 KB 发送缓冲与 2048 DMA 载荷 → 整块被丢;注释仍写"4096 B = 发送缓冲上限"(2026-09-22 布局解耦前的旧事实)。A6 修后会打 `[ERR]` 而非静默 stall | ⏳ 待修(chunk 改 ≤1024 或分块) |
|
||||
| B8 | 低 | `ch395f_test_task.c:1290-1292` | Phase 4 把 sock6 缓冲重排到块 24-27,与 `net_init` 固定布局中 sock4 的 24-27 **交叠**(§5.45 禁止) | ⏳ 待修 |
|
||||
| B9 | 低 | `ch395f_reuse_bench.c:93`、`net_socket.c:39`、`net_socket.c:262` | bench 用 `get_phy_status() != 0` 判链路(01H=断开也成立,`net_task.c:122` 的 `!= CH395F_PHY_DISCONN` 才对);`NET_PHY_CHECK_INTERVAL_MS` 死宏;无芯片版本门控(§5.16 <0x44 不支持 ALL 版、§5.52 bit3/4 需 ≥0x46,实测 0x4A 无影响) | ⏳ 待修(前两项死代码/卫生) |
|
||||
| B10 | **中** | `net_reconcile_tick` 认领范围 + `handle_disconnect_event` 释放分支(`net_socket.c:3663-3686`) | **孤儿监听 → 端口永久拒连**。断开时释放分支把状态置 `CLOSED` 并注释「留给 `net_recv_locked` 把剩余数据读出来再关,**等待应用重新监听**」——即由应用负责重建;但 `ui_net_sink` 只在 `net_recv` 返 0(EOF)时才清 `g_client`,而 EOF 已被这个事件分支消费掉 → **net 层等应用重建 / 应用等 net 层给 EOF / 对账只裁 ESTABLISHED·LISTENING 直接跳过 / 芯片不会自己回 LISTEN**,四方互等。实测(临时探针):`TEMP orphan sock2 state=0 port=7003 chip=0x05/0x00` 每秒一条持续 12 s+,`:7003 300ms×10` **0/10**、+30 s 仍拒连。与 Trap 22 的关系:Trap 22 的①③两类都要求本层处于 ESTABLISHED/LISTENING,**孤儿态恰好落在两类之外**,所以那套对账全程沉默 | ✅ 已修(2026-10-12):`net_reconcile_tick` 新增**类型④**认领孤儿(`in_use` + `state==CLOSED` + `auto_relisten` + `type==STREAM` + `local_port!=0`,芯片侧不在 LISTEN)→ 复用 `net_relisten_try`(自带"已在重试队列则不重复登记"保护,与 D10 重试队列、Trap 23 沉降隔离兼容)。⚠ **`auto_relisten` 是决定性门**:少了它,"本层 CLOSED + 已绑端口"的**出向连接**(板级 Phase 7 TC701~703 用 `net_connect` 绑本地口)会被误判孤儿并被 relisten 成监听器。**回归**:`:7003 300ms×10` **0/10 → 10/10**(+20 s 存活);`:21` 0ms×50 **50/50**、100ms×50 **50/50**;`:8888` 0ms×20 **20/20**;`ftp_regression` **11/11 ×2**;Trap 23 RST **5/5**、FIN **5/5**;`@test` PASS(600 ms、回读无差异);`Code` 197008→197084。⚠ **残余(未根治)**:生产者仍在——释放分支依旧把状态停在 `CLOSED` 等应用重建,④是**兜底**(实测救援后端口 1 s 内恢复,日志 `reconcile: orphan listener … -> relisten` 可见);根治要在释放分支对 `auto_relisten` 直接重监听,但那会破坏「让 `net_recv_locked` 读干剩余数据」的 Trap 25 closed-with-data 契约,须单列评估 |
|
||||
|
||||
### 文档侧待修清单(本轮只改了最要紧的几处,其余登记在此)
|
||||
|
||||
@@ -959,6 +960,11 @@ Trap 22 的周期对账(200ms 节拍 + 400ms 去抖 + 1s 冷却)设计目标
|
||||
- 日期:Trap 01/02/03/04/06/07 的"2025-07"均早于仓库首提交 `b1d05091`(2026-06-05);Trap 03/04/06 的 Phase 编号也错(DHCP=阶段 4、UDP=阶段 3、KeepAlive=阶段 11)。
|
||||
- 证据通则:`*.log` 在 `.gitignore` 内 → 本文所有台架计数(40/40、34/34、15/15、4 轮 11/11、13/13…)**脚本在库、日志未入库 = 不可复核**;Trap 25 引用的实验分支 `n30-chip-bench`(tip `eddbede`)不含 bench 源文件与 uvprojx 注册,裸机芯片台架无法从任何已提交状态重建。
|
||||
|
||||
### 主机侧测试工具过期(2026-10-12 实测)
|
||||
|
||||
- **`MDK-ARM/ftp_abort_probe.py` 已修**:`raw_login` 用 `USER anonymous`、`followup_ok` 用 `login("anonymous","x")`,N-19 后匿名被 530 拒 → Trap 23 两条腿**假 0/5**(round 1 报 `RuntimeError: PASV failed`,且这条半认证会话占死 FTP 唯一会话,导致后续轮全变 `ConnectionRefused`)。改 `admin/admin`(两处)后 RST **5/5**、FIN **5/5**,与 `:779`/`:781` 基线一致。**教训:跑既有回归腿前先确认凭据仍有效,否则会把工具过期读成固件回归。**
|
||||
- **同族未修**:`test/host/ftp_up_dl_test.py`(:123-126)、`ftp_speed_test.py`(:116-121)、`ftp_test.py`(:93-99)仍用匿名登录 → 对当前固件必挂登录步;`docs/测试说明/FTP_Test_Guide.md` 推荐的这几个入口因此已失效(现行可用入口是 `test/host/ftp_regression.py`,默认 `admin/admin`)。
|
||||
|
||||
### 台架注入陷阱(2026-10-12,B1 复核过程中踩到)
|
||||
|
||||
- **Windows 出站防火墙规则不能用来模拟"对端静默死亡"**:WFP 对**已建立**的连接有状态缓存,中途 `New-NetFirewallRule -Direction Outbound -Action Block -RemoteAddress <设备IP>` 不影响既有流——实测静置 + 阻断 250 s 芯片毫无反应,看似"KeepAlive 不生效",实则 ACK 一直在正常往返。
|
||||
@@ -968,3 +974,6 @@ Trap 22 的周期对账(200ms 节拍 + 400ms 去抖 + 1s 冷却)设计目标
|
||||
- **主机侧回显服务器收不到入站连接 → Phase 7 全量 `CONNECT TIMEOUT`(2026-10-12 实测)**:本机对 `C:\programdata\anaconda3\python.exe` 有两条 **Public 入站 Block** 规则(`C:\python314\python.exe` 才是 Allow),而 `.venv\Scripts\python.exe` 的基座解释器正是 Anaconda python(构建报错自曝 `C:\ProgramData\anaconda3\python.exe: can't open file ...`)。Windows 防火墙**块规则优先于放行规则** → 中途加的 `New-NetFirewallRule -Direction Inbound -Action Allow -Protocol TCP -LocalPort 8081 -Profile Any` **完全无效**(实测两轮全灭),只有 `Set-NetFirewallProfile -Name Public -Enabled False` 才通。症状极具误导性:芯片 ~1.6 s 报 `CONNECT TIMEOUT`、`net_connect` 仍返 0、`[CONN] openOK` 照常打印,看着像芯片/驱动故障,实为主机防火墙。固定跑法:`try { 关 Public → 复跑 } finally { 开 Public }`(或改用 `C:\python314\python.exe` 起服务器)。
|
||||
- **Phase 7 的两条既有失败与改动无关,别追**:① TC704 `socket create #5` FAIL = E-23(`NET_MAX_SOCKETS=8` 减 3 个常驻监听 → 并发上限 5,而 `PHASE7_MULTI_COUNT` 仍是 8);② TC703 要求 PC 服务器**离线**,与 701/702/704 环境互斥,同轮连跑必有一侧失真(本轮服务器在线时它反而 PASS,只因芯片先 `CONNECT TIMEOUT`)。
|
||||
- **判读教训(与 Trap 25 同源)**:看到 `TIMEOUT` 先**算时序再归因**——2 s 与 60 s 一眼可分;否则会把"重传超时"误记成"KeepAlive 生效",把相关性当因果。
|
||||
- **A/B 归因前必须每腿硬复位(B10,2026-10-12)**:`:7003` 这类腿一旦把监听打死就**不自恢复**,下一腿是接着死状态跑的 → 对照组失真。本次第一腿(基线 1/10)就是这么废的;正确做法是**每个样本前 `stlink_flash.py reset` + 等 12 s 干净启动**,并取多样本(本轮基线 5/1/1/5 vs 改动 3/7/2/1,均值 3.0 vs 3.25 → 统计不可区分,才敢判"与改动无关")。
|
||||
- **`[PROF]`/`[PROF2]` 字段不是计数器,别拿来判"任务卡死"(B10)**:`netp` = `net_poll` **段最长耗时(µs)**、`netn` = 采样瞬间在段内的时长(`App/util/task_prof.c:172`)→ 数值恒定只说明"没刷新最大值"。真正的停摆信号是 `[ERR][WD] task stall`;且其 `miss=` 打印的是**累计** `s_miss_total`(不是连续缺席数,连续数封顶在容忍值 8),容忍 = 8 轮 ×100 ms = **连续 800 ms 有任务缺席即停喂**、IWDG 2.5 s 溢出(`App/util/task_watchdog.c:466-492`)。缺席位图解码:`req=0x77` 为 DEFAULT/NET/FTP/UI/TIMESYNC/ADC,`mask=0x41` → 缺席 NET+FTP+UI+TIMESYNC 而 DEFAULT/ADC 正常 = "netTask 卡住、所有做 `net_*` 的任务阻塞在 `osMessageQueuePut(osWaitForever)`+`xTaskNotifyWait(portMAX_DELAY)` 后面"的指纹。
|
||||
- **静默黑洞用临时探针抓状态,测完即删(B10 手法)**:B10 的孤儿态在日志里毫无痕迹,靠在对账跳过分支加一条 `TEMP orphan … state=%d port=%u chip=0x%02X/0x%02X` 才直接看到 `state=0 / chip=0x05/0x00`;同一轮还顺手用探针**否掉了自己的第一假设**(怀疑 `net_listen_locked` 的 `osDelay(500)` 冻结 netTask → 探针 0 次命中)。假设写盘前先测,比推理便宜。
|
||||
|
||||
Reference in New Issue
Block a user