From 9608b7a8ea8faa587e1d7b4aa671ce49bb94a0f2 Mon Sep 17 00:00:00 2001 From: super_agent_bot Date: Wed, 4 Nov 2026 00:46:08 +0800 Subject: [PATCH] =?UTF-8?q?test(net/udp):=20=E5=8A=A0=20240=20=E8=BD=AE?= =?UTF-8?q?=E5=AE=9A=E7=A7=8D=E5=AD=90=E9=9A=8F=E6=9C=BA=E9=95=BF=E8=B7=91?= =?UTF-8?q?=20UDP-24=EF=BC=8C=E9=85=8D=E5=A5=97=E5=81=87=E4=BB=B6=E8=A1=A5?= =?UTF-8?q?=E5=8F=AF=E9=87=8D=E6=8E=92=E6=94=B6=E5=8C=85=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=EF=BC=88#73=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 真机台架被断电和断线卡住,这一段只推进纯软件侧:把 UDP 数据面的随机序列 扫起来,并修掉随机长跑逼出的两条"测法自误"。 - ns_s7_udp.c 新增 UDP-24:固定种子 20261104 的 240 轮随机交互,每轮随机 载荷长度 1~112(112 = 假件收包流 128 B 上限减 8 字节信息头)、源端口、 源 IP、SERVER/CLIENT 形态、显式对端 sendto 与按记账对端 sendto,每 17 轮 close + 重 bind 一次。断言收在六族不变量上(长度等于芯片宣布值、既不吞 信息头也不丢真字节、源地址按形态分别取自表头/对端记账、udp_mode 与命令 一致、close/重 bind 后出参槽与挂起槽不残留、每次 sendto 交给芯片恰好 len 字节),并要求 ≥90% 轮次端到端干净(实测 240/240)。 - 长跑逼出的两处都是测法问题,不是被测码 bug,但只有长跑才逼得出: 1) 连续 sendto 不注入 SENBUF_FREE 时,第二次发送会合法转挂起,而本套的 挂起 send 替身从不完成,于是 FATAL 掐死整套件(连汇总行都没有)。改法 照 440i/440j:set_sint_sticky(SENBUF_FREE) + push_glob + g_ns_int_pin_set=0, 注释点名 TRAP-NET-04,背靠背发送的真机面留给 TC-NET-1609。 2) 假件 ch395f_get_recv_len 是"按调用序号取、耗尽后重复最后一个"的脚本且 容量有限,240 轮只追加不清空,序号撞满容量后每轮读到同一个陈旧长度 (症状:len 在随机而 n 恒为 28,前 24 轮全绿,最容易误判成产品 bug)。 补加法助手 ch395f_fake_reset_recv_len(sock),只归零该 sock 的脚本长度与 序号,既有单轮用例不调用它、行为一字未变。 - 覆盖率口径第 4 轮同步:用例面记到 24 组、全套 654/654(改造前 470), 新增一条"随机长跑逼出两条测法自误"的教训(长跑对假件的第一要求是 状态可重排,不是能追加)。 验证:net_socket 套件 654/654 绿;全量覆盖率 47/47 退出码 0,net_udp.c 237/239 行 99.2%、14/14 函数,net_socket.c 966/969 行 99.7%、59/59 函数, 总行 8372/9290(90.1%),无 REGRESS;cppcheck(--hook-stage manual)退出码 0; check_doc_links --all 通过(213 个文档,无法判定 0);pre-commit 对本批文件 全绿。基线 JSON 无变化(本轮只加测试,未动生产码),故不随本提交改动。 --- docs/测试说明/单元测试/主机单测覆盖率口径.md | 3 +- test/host/common/ch395f_fake.c | 13 ++ test/host/common/ch395f_fake.h | 3 + test/host/net_socket/ns_s7_udp.c | 203 +++++++++++++++++++ 4 files changed, 221 insertions(+), 1 deletion(-) diff --git a/docs/测试说明/单元测试/主机单测覆盖率口径.md b/docs/测试说明/单元测试/主机单测覆盖率口径.md index 150e3e41..0be47d23 100644 --- a/docs/测试说明/单元测试/主机单测覆盖率口径.md +++ b/docs/测试说明/单元测试/主机单测覆盖率口径.md @@ -719,10 +719,11 @@ uint16_t ch395f_link_change_count(void); - **不写第二份分块逻辑**:UDP 发送复用 `net_send_locked`(1KB 分块 + 挂起 send 都在里面)。为此把"会话是否已死"收成一条判据 `net_send_state_down()`(`net_pending.c`):TCP 看 `ESTABLISHED`、UDP 看 `UDP_OPEN`;原先 `net_send_locked` 两处 + `net_send_pending_tick` + `net_pending_tick` 各写一遍,共 4 处。 - **顺带修掉的形态缺陷**(真隐患):`net_sendto` 为发一个包把 DesIP 从 `0xFFFFFFFF` 写成具体 IP,芯片静默切到 UDP Client 形态(收包**不带** 8 字节信息头),而旧 `recvfrom` 无条件剥 8 字节 → 请求-响应式协议(SNTP 48 B 应答)会丢载荷前 8 字节并交出垃圾源地址。**修法**:`net_sock_t` 加 `udp_mode`(塞进原有 2 字节 padding,`sizeof(net_sock_t)` 不变)、`bind` 恒 SERVER、显式对端 `sendto` 切 CLIENT、`net_udp_set_peer(fd, NULL)` 轻量回 SERVER(不重开 Socket 因此不清芯片缓冲)、只在 SERVER 且 `avail >= 8` 时剥头(残包不猜头,TC-NETSOCK-440o 契约不变)。 - **截断语义定案**:一次调用交付一个数据报,`buf` 装不下的尾巴**必须排空**——`ch395f_read_recv_buf` 是按长度推进读指针的消费式读,不排空则下一次 `recvfrom` 交出上一个包的残料。排空用 64 B 栈上 scratch 循环读,**不用** `ch395f_clear_recv_buf`:它清整个接收缓冲,会把后面已经到达的包一起丢。 -- **用例面**:新分片 `test/host/net_socket/ns_s7_udp.c` 共 23 组 UDP-1~23(bind 的 5 条芯片命令**顺序**、未 bind 一律 -5、服务器形态剥头逐字节、客户端形态不剥头、`sendto` 切形态、挂起收包在后续 poll 轮到达、预算超时按调用方 deadline 计、close 与重建结算不许静默丢通知、出参槽互斥与泄漏、截断排空、netTask 自等守卫、`net_init` 重建取消、流式 API 拒 `DGRAM`、非 netTask 调用零芯片事务、执行体与公开入口的参数守卫(越界 fd / `STREAM` / 未 bind / 零长 / 出参容量不足——拒时必须零芯片事务)、"队列未绑定"与"队列满"两条失败出口不许漏槽、`net_udp_set_peer` 轻/重两种回服务器模式(重开丢缓冲 vs 不重开)、挂起 send 由 `net_send_pending` 结算且登记时不许抢发第二份回执、`net_udp_tick` 的会话作废与 stale-readable 兜底、重 bind 必须把还在等包的调用方按 -8 结算、`bind` 遇满队列必须 BUSY 且不给 fd 投毒、**芯片 `open` 回 `ERR_BUSY` 时精确码必须沿通知值回到等待方**(真芯片会这么拒)、**队列里"别人的失败"排在我这条之前时不许污染我的回执**(同 545 在 TCP 侧的钉子,UDP bind 侧补一条)、`net_recvfrom_to` 的"真失败"与"超时"两条出口分开、**netTask 直调路径(板级用例走的那条)的 `set_peer` / `recvfrom` 正例与三种拒绝**;改写 TC-NETSOCK-440(补 bind 前置 + 新增 440p~440u)、555(表头逐字节按形态分路)、560 组("拒在 netTask 外"→"经队列后非 netTask 仍零 SPI")、500h-i / 506 / 545。全套 **647/647 绿**(改造前 470)。 +- **用例面**:新分片 `test/host/net_socket/ns_s7_udp.c` 共 24 组 UDP-1~24(bind 的 5 条芯片命令**顺序**、未 bind 一律 -5、服务器形态剥头逐字节、客户端形态不剥头、`sendto` 切形态、挂起收包在后续 poll 轮到达、预算超时按调用方 deadline 计、close 与重建结算不许静默丢通知、出参槽互斥与泄漏、截断排空、netTask 自等守卫、`net_init` 重建取消、流式 API 拒 `DGRAM`、非 netTask 调用零芯片事务、执行体与公开入口的参数守卫(越界 fd / `STREAM` / 未 bind / 零长 / 出参容量不足——拒时必须零芯片事务)、"队列未绑定"与"队列满"两条失败出口不许漏槽、`net_udp_set_peer` 轻/重两种回服务器模式(重开丢缓冲 vs 不重开)、挂起 send 由 `net_send_pending` 结算且登记时不许抢发第二份回执、`net_udp_tick` 的会话作废与 stale-readable 兜底、重 bind 必须把还在等包的调用方按 -8 结算、`bind` 遇满队列必须 BUSY 且不给 fd 投毒、**芯片 `open` 回 `ERR_BUSY` 时精确码必须沿通知值回到等待方**(真芯片会这么拒)、**队列里"别人的失败"排在我这条之前时不许污染我的回执**(同 545 在 TCP 侧的钉子,UDP bind 侧补一条)、`net_recvfrom_to` 的"真失败"与"超时"两条出口分开、**netTask 直调路径(板级用例走的那条)的 `set_peer` / `recvfrom` 正例与三种拒绝**、**UDP-24 = 240 轮定种子(`20261104`)随机长跑**:每轮随机载荷长度 1~112(112 = 假件收包流 128 B 上限减掉 8 字节信息头,不是魔数)、随机源端口与源 IP、随机 SERVER/CLIENT 形态、随机"显式对端 `sendto`"与"按记账对端 `sendto`",每 17 轮 `close` + 重 bind 一次;断言收在六族不变量上(读到的长度必须等于芯片宣布值、载荷既不吞信息头也不丢真字节、源地址按形态分别取自表头/对端记账、`udp_mode` 与命令严格一致、`close`/重 bind 后出参槽与挂起槽都不残留、每次 `sendto` 交给芯片的字节数恰好 = `len`),并要求 ≥90% 轮次端到端干净(实测 240/240)——**形态切换 × 挂起结算 × 关闭重建这三件事的交叉面,逐条用例排不出组合,只能靠随机扫**;改写 TC-NETSOCK-440(补 bind 前置 + 新增 440p~440u)、555(表头逐字节按形态分路)、560 组("拒在 netTask 外"→"经队列后非 netTask 仍零 SPI")、500h-i / 506 / 545。全套 **654/654 绿**(改造前 470)。 - **假件新增(一律可加不可改)**:`g_fx.wlen[]` + `g_fx.last_write[]` 捕获 `ch395f_write_send_buf` 的**真实字节**(此前假件忽略 `p_data`,UDP 截断与排空无从断言);`ch395f_fake_set_write_delay(fd, n)` 让 SENBUF_FREE 第 n 次才到(钉"排空必须在下一次写之前完成");`g_chip.read_calls` 供"读了几个 scratch 块"断言。`g_fake` / `g_chip` 既有字段一字未动。 - **两条教训(都是被测码真 bug,不是测法自误)**:① **执行侧自等守卫不能复用调用侧谓词**——`net_udp_recvfrom_exec` 用 `net_ctx_is_owner_task()` 判自等,而它在 netTask 内恒真 → 每一条经队列投进来的正常 recvfrom 都被拒;正确判据是 `caller == xTaskGetCurrentTaskHandle()`(对齐 `net_pending.c` 的 `caller == s_owner_task`)。抓它的是 UDP-8a。② **芯片长度只能读一次并传参**——`net_udp_datagram_read` 内部又调一次 `ch395f_get_recv_len`,于是"剥几字节"与"读几字节"不同源;假件的 recv_len 队列**每次调用弹一个值**,这条设计当场把双读暴露出来。现由调用方读一次 `avail` 传入三个调用点。 - **补测阶段又抓到两条(同一批缺口分析顺手挖的)**:③ **槽指针必须在 fd 合法性检查之后取**——`net_udp_recvfrom_exec` 原先先算 `&s_net_udp[sockfd]` 再判 `p_sock == NULL`,越界 fd 立刻得到一个越界槽指针(哪怕当下没解引用,后面登记就是野写)。④ **作废不等于可以不吭声**——`net_udp_bind_exec` 重开芯片 Socket 会丢掉缓冲里的包,却只 `net_udp_clear()`:A 任务正阻塞在 `net_recvfrom`、B 任务重 bind 同一个 fd,A 就永远睡死。现在重开之前先按 `NET_ERR_CONNRESET` 结算并通知(UDP-20 钉住)。教训同 `net_pending`:**任何"取消挂起"的动作都必须走 complete 那条路,clear 只是归零**。 +- **随机长跑把两条"测法自误"逼了出来(不是被测码 bug,但只有长跑才逼得出)**:⑤ **连续 `sendto` 必须先把 SENBUF_FREE 钉成电平**——不注入 SENBUF_FREE 时,第二次发送会**合法地**转挂起(`net_send_locked` 自旋 2 个 tick 就 `net_send_pending_register`),而本套的挂起 send 替身从不完成,于是 `FATAL: blocking wait still got no receipt after 50 rounds` 直接掐死整个套件、连汇总行都没有(正确姿势照 440i/440j:`ch395f_fake_set_sint_sticky(fd, CH395F_SINT_STAT_SENBUF_FREE)` + `push_glob(1<<(fd+4))` + `g_ns_int_pin_set=0`,注释里点名 TRAP-NET-04,并写明"背靠背发送的真机面留给 TC-NET-1609")。⑥ **假件的 `ch395f_get_recv_len` 是"按调用序号取、耗尽后重复最后一个"的脚本且容量有限**——240 轮只追加不清空,序号撞满容量后每轮都读到同一个陈旧长度(症状:`len` 在随机而 `n` 恒为 28,前 24 轮却全绿,最容易误判成产品 bug)。补了加法假件助手 `ch395f_fake_reset_recv_len(sock)`(只把该 sock 的脚本长度与序号归零,既有单轮用例不调用它、行为一字未变),soak 每轮开头调一次。教训:**长跑用例对假件的第一要求是"状态可重排",不是"能追加"**。 - **本轮覆盖率**(收尾那次全仓跑数,`--update-baseline` 已把数字连同本轮提交一起落盘):`net_udp.c` 237/239 行(99.2%)、函数 **14/14**;`net_socket.c` 966/969(99.7%)、函数 **59/59**;`net_pending.c` 173/199(86.9%);全仓 line 8372/9290(90.1%)、47/47 套绿,无回退。剩两处未覆盖:`net_udp.c` 的槽越界兜底(调用方已挡,属防御性代码)与 `net_socket.c` 错误码槽重查竞态(真并发才走得到,改造前后同为那 2 行)。 - **台架必验(本轮无板子,主机测替代不了)**:① 两端真机互通,服务器形态收发 + 客户端形态往返,重点看 48 B 这类小应答**载荷完整**且源地址正确;② `sendto` 切形态后 `net_udp_set_peer(fd, NULL)` 能否真恢复收广播——手册只写 DesIP 语义,没写切换时机;③ 截断包排空后下一个包是否完整(scratch 循环在真芯片读指针上的行为);④ 挂起 recv 等待期间的 IWDG 余量;⑤ bind 在 open **之前**写 DesIP 的顺序在真芯片上的实际效果(TRAP-NET-14 的 UDP 侧对照)。 - **这 5 条已落成可执行的板级阶段**:阶段 16(`docs/测试说明/板级测试/CH395F_Test_Guide.md` §12.6,板端 `test/board/net_test_task.c` 的 `net_layer_udp_bench_test_main`,PC 侧 `test/host/udp_queue_bench.py`),TC-NET-1601~1609 与 ①~⑤ 的对应关系写在表里。**真机只跑了个开头**:2026-11-03 夜第一次上台架,① 的上半段已见证据——`TC-NET-1601` 建 socket/bind `:6001`/形态回 SERVER、`TC-NET-1602` 服务端形态收到 20 B 且 `addrlen`/`sin_family`/源端口全对、回显逐字节一致并把形态翻成 CLIENT(剥 8 字节信息头与源地址回填在真芯片上成立);②③④⑤ 未测,板子当晚两次失电(SWD `No target connected`、ping 不通,19:19 与 21:0x 各一次),两趟还各暴露一个**台架**缺陷(peer 被 Windows 的 ICMP 10054 打死;单步预算 8 s < 板端 25 s park 导致整条梯子错位),修法与守卫见上面那份指南。PR #74 的合并前提仍是这条回归跑完整;跑完请把结果回填本条并把上面「台架必验」改写成实测结论。 diff --git a/test/host/common/ch395f_fake.c b/test/host/common/ch395f_fake.c index 8ca5d337..d3ac4de2 100644 --- a/test/host/common/ch395f_fake.c +++ b/test/host/common/ch395f_fake.c @@ -169,6 +169,19 @@ void ch395f_fake_set_sint_sticky(uint8_t sock, uint8_t mask) } } +/* 重排本轮的"芯片收到 N 字节"脚本:把调用序号退回起点并清空旧值。 + * 既有脚本语义是"按调用序号取、耗尽后重复最后一个"且容量有限;一条要跑 + * 几百轮的随机化用例若每轮只追加不清空,序号会撞满容量,之后每轮读到的 + * 都是同一个陈旧长度(UDP 队列化的 soak 用例就是这么踩到的)。 + * 既有单轮用例不调用它,逐字节行为不变。 */ +void ch395f_fake_reset_recv_len(uint8_t sock) +{ + if (sock < CH395F_FAKE_SOCK_MAX) { + g_fake.recv_len_n[sock] = 0U; + g_fake.recv_len_i[sock] = 0U; + } +} + uint16_t ch395f_get_recv_len(uint8_t sock) { uint16_t v; diff --git a/test/host/common/ch395f_fake.h b/test/host/common/ch395f_fake.h index d2b91148..706739d7 100644 --- a/test/host/common/ch395f_fake.h +++ b/test/host/common/ch395f_fake.h @@ -192,6 +192,9 @@ extern ch395f_fake_obs_t g_chip; void ch395f_fake_reset(void); /* 全清并把所有 socket 设为"已关闭" */ void ch395f_fake_push_recv_len(uint8_t sock, uint16_t v); +/* 清空并重排该 sock 的收包长度脚本(长跑/多轮用例每轮开头调一次; + * 既有单轮用例不需要调,行为不变) */ +void ch395f_fake_reset_recv_len(uint8_t sock); void ch395f_fake_set_listening(uint8_t sock); void ch395f_fake_set_peer(uint8_t sock, uint8_t b0, uint8_t b1, uint8_t b2, uint8_t b3, uint16_t peer_port); /* 预置对端六元组 */ /* 缓存:open!=CLOSED 且 tcp=LISTEN */ diff --git a/test/host/net_socket/ns_s7_udp.c b/test/host/net_socket/ns_s7_udp.c index aaf2058c..edadcf56 100644 --- a/test/host/net_socket/ns_s7_udp.c +++ b/test/host/net_socket/ns_s7_udp.c @@ -1254,6 +1254,208 @@ static void tc_udp23_recvfrom_direct_ctx(void) reset_all(); } +/* ---------- UDP-24:随机化 soak(纯软件,不需要板子) ---------- */ +/* + * 函数功能:用固定种子的同余序列生成 240 轮随机 UDP 交互,每轮随机「载荷长度 + * 1..112 / 源端口 / 源 IP / 本轮形态 SERVER 或 CLIENT / 回显是否带显式 + * dest_addr / 是否顺带做一次 close+重开」,并断言一组**不变量**。 + * 函数说明:UDP-1..23 是一条条钉死的具体契约,本组补的是另一件事——任意合法 + * 序列跑完之后系统仍停在合法状态。这类断言最能暴露"某条出口忘了清槽 / + * 剥头判定被上一条命令带跑 / 长度取错源"的缺陷,而它们恰好在真机上最难 + * 复现(本次改造修掉的 DesIP 形态缺陷就是这一类)。 + * 种子固定 = 失败可原样复现(LCG 参数 k=1664525、c=1013904223)。 + * 断言按"族"计数并打印第一个坏轮次,不逐轮 CHECK:240 轮 × 6 条会把 + * 套件的用例计数彻底淹没,而失败信息一条也不会少。 + * 112 = 假芯片可读流上限 128 B 减去服务器模式的 8 字节信息头。 + */ +#define UDP_SOAK_ROUNDS 240 +#define UDP_SOAK_MAX_DATA 112 + +static uint32_t udp_soak_rand(uint32_t *st) +{ + *st = (*st * 1664525U) + 1013904223U; + return *st; +} + +static void tc_udp24_random_soak(void) +{ + uint32_t st = 20261104U; + uint8_t payload[UDP_SOAK_MAX_DATA]; + uint8_t rx[UDP_SOAK_MAX_DATA + 8]; + struct net_sockaddr_in peer; + struct net_sockaddr_in from; + const char *ips[2]; + int bad_len = 0, bad_bytes = 0, bad_src = 0, bad_mode = 0, bad_slot = 0, bad_send = 0; + int first_bad[6]; + int i; + int fd; + uint32_t w_bytes_before; + uint32_t w_calls_before; + int round_ok = 0; + char nm[192]; + + ips[0] = "10.20.30.40"; + ips[1] = "192.168.7.9"; + first_bad[0] = first_bad[1] = first_bad[2] = -1; + first_bad[3] = first_bad[4] = first_bad[5] = -1; + + bring_up(); + fd = udp_bind(6001); + if (fd < 0) { + CHECK(0, "UDP-24 soak could not bind a socket"); + reset_all(); + return; + } + ch395f_fake_enable_write_capture(); + /* 真芯片每收完一包 WRITE_SEND_BUF 会拉 SENBUF_FREE(TRAP-NET-04:不等这个事件 + * 就再写,写会被丢)。本套件的挂起发送是 ns_peers 的替身、不会自己结算,所以 + * 按 440i/440j 的既有做法把它钉成电平:让每轮 sendto 都走同步写路径, + * "连续两包背靠背发"这件事交给阶段 16 的真机 TC-NET-1609 去证。 */ + ch395f_fake_set_sint_sticky(fd, CH395F_SINT_STAT_SENBUF_FREE); + ch395f_fake_push_glob((uint32_t)1U << (fd + 4)); + g_ns_int_pin_set = 0; + + (void)memset(&peer, 0, sizeof(peer)); + peer.sin_family = NET_AF_INET; + peer.sin_addr.s_addr = net_inet_addr("119.28.220.214"); + peer.sin_port = net_htons(123U); + + for (i = 0; i < UDP_SOAK_ROUNDS; i++) { + int len = 1 + (int)(udp_soak_rand(&st) % (uint32_t)UDP_SOAK_MAX_DATA); + uint16_t sport = (uint16_t)(1024U + (udp_soak_rand(&st) % 60000U)); + const char *sip = ips[udp_soak_rand(&st) & 1U]; + int as_client = ((int)(udp_soak_rand(&st) % 3U) == 0); + int j; + int n; + int alen; + int clean = 1; + + for (j = 0; j < len; j++) { + payload[j] = (uint8_t)((j * 7 + i * 3 + 0x11) & 0xFF); + } + + /* 形态由本轮决定,不沿用上轮遗留:SERVER 用 set_peer(NULL) 轻切回, + * CLIENT 显式指对端。剥不剥 8 字节必须只由这个判定决定。 */ + if (net_udp_set_peer(fd, as_client ? &peer : NULL) != 0) { + if (first_bad[3] < 0) { first_bad[3] = i; } + bad_mode++; + continue; + } + if (net_udp_get_mode(fd) != (as_client ? NET_UDP_MODE_CLIENT : NET_UDP_MODE_SERVER)) { + if (first_bad[3] < 0) { first_bad[3] = i; } + bad_mode++; + continue; + } + + (void)memset(rx, 0xA5, sizeof(rx)); + alen = (int)sizeof(from); + /* 每轮重排长度脚本:假件的收包长度是"按调用序号取"的脚本,240 轮只追加 + * 不清空会让序号撞满容量、之后每轮都读到同一个陈旧长度。 */ + ch395f_fake_reset_recv_len(0); + if (as_client) { + /* 客户端模式:芯片不再前置信息头,整段就是载荷 */ + ch395f_fake_set_recv_stream(payload, (uint16_t)len); + ch395f_fake_push_recv_len(0, (uint16_t)len); + } else { + udp_script_server_datagram((const char *)payload, len, sport, sip); + } + + n = net_recvfrom_to(fd, rx, len, 800U, (struct net_sockaddr *)&from, &alen); + if (n != len) { + if (first_bad[0] < 0) { first_bad[0] = i; } + bad_len++; + clean = 0; + } else if (memcmp(rx, payload, (size_t)len) != 0) { + if (first_bad[1] < 0) { first_bad[1] = i; } + bad_bytes++; + clean = 0; + } else if (alen != (int)sizeof(from) || + from.sin_family != NET_AF_INET) { + if (first_bad[2] < 0) { first_bad[2] = i; } + bad_src++; + clean = 0; + } else { + uint32_t want_ip = as_client ? peer.sin_addr.s_addr : net_inet_addr(sip); + uint16_t want_port = as_client ? net_ntohs(peer.sin_port) : sport; + + if (from.sin_addr.s_addr != want_ip || net_ntohs(from.sin_port) != want_port) { + if (first_bad[2] < 0) { first_bad[2] = i; } + bad_src++; + clean = 0; + } + } + + /* 出参槽与挂起槽必须在每条出口后归零:泄漏 = 这个 fd 以后永久 BUSY */ + if (net_udp_armed(fd) != 0 || net_udp_pending(fd) != 0) { + if (first_bad[4] < 0) { first_bad[4] = i; } + bad_slot++; + clean = 0; + } + + /* 回显:SERVER 轮必须带显式 dest_addr(BSD 语义),CLIENT 轮走对端记账, + * 两条发送路径都被随机覆盖到。 */ + w_bytes_before = g_fx.write_bytes; + w_calls_before = g_chip.write_calls; + if (as_client) { + n = net_sendto(fd, payload, len, 0, NULL, 0); + } else { + n = net_sendto(fd, payload, len, 0, (const struct net_sockaddr *)&peer, + (int)sizeof(peer)); + } + if (n != len || g_fx.write_bytes - w_bytes_before != (uint32_t)len || + g_chip.write_calls <= w_calls_before || + net_udp_armed(fd) != 0 || net_udp_pending(fd) != 0) { + if (first_bad[5] < 0) { first_bad[5] = i; } + bad_send++; + clean = 0; + } + + /* 每 17 轮做一次 close + 重开:把 net_close_locked / net_socket 上的 + * net_udp_settle_close / net_udp_clear 挂进随机序列里,重开后必须还能收。 */ + if ((i % 17) == 16) { + (void)net_close(fd); + fd = udp_bind(6001); + if (fd < 0 || net_udp_armed(fd) != 0 || net_udp_pending(fd) != 0 || + net_udp_get_mode(fd) != NET_UDP_MODE_SERVER) { + if (first_bad[4] < 0) { first_bad[4] = i; } + bad_slot++; + clean = 0; + } + if (fd < 0) { + break; + } + } + if (clean != 0) { + round_ok++; + } + } + + /* CHECK 只收 (条件, 名字):数字用 snprintf 拼进名字,失败时一眼看到第几轮坏的 */ + (void)snprintf(nm, sizeof(nm), "UDP-24a soak recv length always equals the chip-announced length (bad=%d, first round=%d, rounds=%d)", + bad_len, first_bad[0], UDP_SOAK_ROUNDS); + CHECK(bad_len == 0, nm); + (void)snprintf(nm, sizeof(nm), "UDP-24b payload bytes neither lose the 8-byte info header nor lose real payload: SERVER strips, CLIENT never strips (bad=%d, first round=%d)", + bad_bytes, first_bad[1]); + CHECK(bad_bytes == 0, nm); + (void)snprintf(nm, sizeof(nm), "UDP-24c source address is right in both forms - header-derived in SERVER, peer-derived in CLIENT (bad=%d, first round=%d)", + bad_src, first_bad[2]); + CHECK(bad_src == 0, nm); + (void)snprintf(nm, sizeof(nm), "UDP-24d set_peer keeps the mode exactly as commanded (bad=%d, first round=%d)", + bad_mode, first_bad[3]); + CHECK(bad_mode == 0, nm); + (void)snprintf(nm, sizeof(nm), "UDP-24e neither the out-param slot nor the pending slot is ever left behind, including after close and rebind (bad=%d, first round=%d)", + bad_slot, first_bad[4]); + CHECK(bad_slot == 0, nm); + (void)snprintf(nm, sizeof(nm), "UDP-24f every sendto hands the chip exactly len bytes, explicit-dest and peer-bookkeeping alike (bad=%d, first round=%d)", + bad_send, first_bad[5]); + CHECK(bad_send == 0, nm); + (void)snprintf(nm, sizeof(nm), "UDP-24g at least 90 percent of the random rounds are clean end to end (clean=%d/%d)", + round_ok, UDP_SOAK_ROUNDS); + CHECK(round_ok * 10 >= UDP_SOAK_ROUNDS * 9, nm); + net_close(fd); + reset_all(); +} + /* ===================== 本分片入口 ===================== */ static void ns_run_s7(void) @@ -1281,4 +1483,5 @@ static void ns_run_s7(void) tc_udp21_bind_and_recvfrom_to_exits(); tc_udp22_set_peer_direct_ctx(); tc_udp23_recvfrom_direct_ctx(); + tc_udp24_random_soak(); }