From c29b7281539ac3a58b886c93fa8ce5f88a4d80b5 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 22 Sep 2026 01:28:10 +0800 Subject: [PATCH] =?UTF-8?q?E-01=E4=BF=AE=E5=A4=8D=EF=BC=9Apoll=E8=B6=85?= =?UTF-8?q?=E9=95=BF=E5=AE=88=E5=8D=AB=E5=89=8D=E7=BD=AEst.size=E6=A0=B9?= =?UTF-8?q?=E6=B2=BB=E6=AD=BB=E7=A0=81=EF=BC=8Csave=E8=BE=93=E5=87=BA?= =?UTF-8?q?=E4=B8=8A=E9=99=90=E6=94=B6=E7=B4=A74096=EF=BC=8C=E5=86=99?= =?UTF-8?q?=E7=9B=98=E9=97=B8=E6=8B=A6=E6=88=AA=E4=B8=8E=E8=87=AA=E6=A3=80?= =?UTF-8?q?=E7=95=99=E7=97=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/cfg_file.c | 63 ++++++++++++++++++++++++++++++++++++------------ App/cfg_file.h | 18 +++++++++++--- simulator/main.c | 59 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 20 deletions(-) diff --git a/App/cfg_file.c b/App/cfg_file.c index 4c888808..c59433ef 100644 --- a/App/cfg_file.c +++ b/App/cfg_file.c @@ -12,6 +12,12 @@ * 移除对 C 库 malloc 的全部依赖(MicroLIB C 堆仅 512B, * 原 4 处 KB 级 malloc 真机必然失败);增编译期堆峰值预算 * 校验 + 失败上报自检记录 + * v1.2 2026-09-21 王建锋 E-01(错误码传播审计 P0):超长文件守卫根治——poll 判定 + * 前置到 cfg_store_stat 真实 st.size(旧检查位于截断后的 + * len 上恒假=死码,超长文件被截断当有效解析 → 界外静默 + * 回工厂默认 → 基线粘住 → 用户配置销毁链);save 输出上限 + * 由 CFG_OUT_MAX 收紧至 CFG_FILE_MAX_LEN;s_write_doc 写盘 + * 唯一闸口拒写超长文档;load/poll 超长处置补自检留痕 * 函数说明:引擎状态非重入——调用方须收敛同一任务(defaultTask:poll/save,见 P1-3; * 启动期 load 先于任务竞争);FS 卷并发由 FatFS REENTRANT 卷锁保证 * (见《配置文件设计说明》D15 A+ 方案) @@ -513,16 +519,24 @@ static int s_load_file(uint8_t **p_pbuf, size_t *p_len) { } /* - * 函数功能:经后端原子写配置文档 + * 函数功能:经后端原子写配置文档(写盘唯一闸口:模板/外科手术同闸) * 入口参数:p_buf - 文档数据 const uint8_t * * len - 数据长度 size_t - * 返回值:0 - 成功;-1 - 写失败 int - * 限定条件:p_buf 非空 + * 返回值:0 - 成功;-1 - 写失败或超长拒写 int + * 限定条件:p_buf 非空;len ≤ CFG_FILE_MAX_LEN + * 函数说明:E-01 纵深防御——超长文档一旦落盘,下次开机 load 守卫即判损坏、 + * 整份用户配置连同注释/未知段销毁,故在唯一出口硬性拦截,使 + * "引擎永不产出超长 config.ini"成为结构性质(调用者新增路径免疫) */ static int s_write_doc(const uint8_t *p_buf, size_t len) { if (p_buf == NULL) { return -1; } + if (len > CFG_FILE_MAX_LEN) { + DBG_ERROR("doc: %luB > %uB limit, write rejected", + (unsigned long)len, (unsigned)CFG_FILE_MAX_LEN); + return -1; + } return cfg_store_write_atomic(cfg_store_config_path(), p_buf, len); } @@ -982,13 +996,15 @@ int cfg_file_load(void) { } if (st.size > CFG_FILE_MAX_LEN) { - /* 文件超长 = 损坏:留工厂默认 + 重建模板 */ + /* 文件超长 = 损坏:留工厂默认 + 重建模板(E-01:补自检留痕——销毁 + * 用户文件属重大事件,仅串口 DBG_ERROR 在现场无调试器时不可追溯) */ if (s_write_template() != 0) { DBG_ERROR("load: corrupt file, template write FAIL"); return -1; } DBG_ERROR("load: file %luB > %uB, corrupt, factory template regenerated", (unsigned long)st.size, (unsigned)CFG_FILE_MAX_LEN); + syslog_post(SYSLOG_SELFTEST, "cfg load oversized"); return 0; } @@ -1081,12 +1097,17 @@ int cfg_file_save(void) { return -1; } - r = s_surgical((char *)p_in, in_len, p_out, CFG_OUT_MAX, &out_len); + /* E-01:输出上限由 CFG_OUT_MAX(5120) 收紧至 CFG_FILE_MAX_LEN(4096)—— + * 旧上限允许把 >4096B 文档合法写盘(= 下一颗雷:下次开机 load 守卫 + * 判损坏、整份用户文件连同注释/未知段一起销毁)。超限走下方既有 + * 溢出中止路径:原文件不动 + 自检上报,RAM 副本保留(可修复后重试) */ + r = s_surgical((char *)p_in, in_len, p_out, CFG_FILE_MAX_LEN, &out_len); cfg_store_free(p_in); if (r != 0) { cfg_store_free(p_out); - DBG_ERROR("save: output overflow, aborted (file untouched)"); + DBG_ERROR("save: output > %uB file limit, aborted (file untouched)", + (unsigned)CFG_FILE_MAX_LEN); syslog_post(SYSLOG_SELFTEST, "cfg save overflow"); return -1; } @@ -1140,16 +1161,19 @@ void cfg_file_poll(void) { /* 已沉降:重载配置文件 */ { - uint8_t *p_buf = NULL; - size_t len = 0u; int i; - if (s_load_file(&p_buf, &len) != 0) { - return; /* 读失败:保留,下次 poll 重试 */ - } - - if (len > CFG_FILE_MAX_LEN) { - cfg_store_free(p_buf); + /* E-01(2026-09-21 审计 P0):超长守卫必须基于 cfg_store_stat 的真实 + * st.size 且先于 s_load_file——s_load_file 读长钉死 CFG_FILE_MAX_LEN + * (nread ≤ 4096 恒成立),旧检查写在读后的 len 上恒假(死码): + * 超长文件被截断当有效解析 → 界外段/键静默回工厂默认 → 基线粘住 → + * 后续 save 按截断视图重写 → 界外内容永久丢失 → 下次开机 load 守卫 + * 判损坏销毁整份用户文件。本分支对齐 load 契约(cfg_file.h 第 4 条): + * 超长 = 损坏 → 工厂默认 + 重建模板 + 自检留痕。 + * 刻意不触发段回调:保留现有网络/在线通道(用户仍可经 FTP 修复); + * RAM=工厂 与硬件现状的分歧在下次保存/重启时收敛(重启必然收敛: + * load 读到的已是重建后的合法模板) */ + if (st.size > CFG_FILE_MAX_LEN) { for (i = 0; i < (int)CFG_FILE_MAX_SECTIONS; i++) { if (s_sections[i].used != 0) { (void)memcpy(s_sections[i].p_data, s_sections[i].p_defaults, @@ -1157,10 +1181,17 @@ void cfg_file_poll(void) { } } (void)s_write_template(); - DBG_ERROR("poll: file corrupt (> %uB), factory template regenerated", - (unsigned)CFG_FILE_MAX_LEN); + DBG_ERROR("poll: file %luB > %uB, corrupt, factory template regenerated", + (unsigned long)st.size, (unsigned)CFG_FILE_MAX_LEN); + syslog_post(SYSLOG_SELFTEST, "cfg poll oversized"); } else { + uint8_t *p_buf = NULL; + size_t len = 0u; + + if (s_load_file(&p_buf, &len) != 0) { + return; /* 读失败:保留,下次 poll 重试 */ + } for (i = 0; i < (int)CFG_FILE_MAX_SECTIONS; i++) { const cfg_sec_t *p_sec = &s_sections[i]; diff --git a/App/cfg_file.h b/App/cfg_file.h index c32dae5d..26480f51 100644 --- a/App/cfg_file.h +++ b/App/cfg_file.h @@ -9,6 +9,10 @@ * 修改记录: * v1.0 2026-09-14 王建锋 创建初始版本(P3:段键表引擎 + net_cfg 集成) * v1.1 2026-09-15 王建锋 属主任务由 netTask 改为 defaultTask(配置引擎单一属主,P1-3) + * v1.2 2026-09-21 王建锋 E-01(错误码传播审计 P0):超长守卫契约同步——poll 沉降 + * 后先按真实 st.size 判超长(旧实现查截断后读长恒假)、 + * save 输出上限收紧至 CFG_FILE_MAX_LEN、写盘唯一闸口拒写 + * 超长;超长处置补自检留痕 */ #ifndef CFG_FILE_H @@ -22,7 +26,8 @@ extern "C" { #include -/* 配置文件长度上限(字节);超过 = 文件损坏,按出厂模板重建 */ +/* 配置文件长度上限(字节);超过 = 文件损坏,按出厂模板重建 + * (load 与 poll 同判,E-01:poll 在读取前基于真实 st.size 判定) */ #define CFG_FILE_MAX_LEN 4096u /* 引擎可注册段数上限 */ @@ -86,6 +91,7 @@ int cfg_file_register(const cfg_section_reg_t *p_reg); * 2. 文件缺失 → 按当前 RAM 副本生成出厂模板写盘(首启建文件) * 3. 文件存在 → 解析:有效键写入 RAM 副本;无效键留工厂默认并留痕 * 4. 文件超长(> CFG_FILE_MAX_LEN)= 损坏 → 工厂默认 + 重建模板 + * + 自检留痕(poll 检出运行期超长文件执行同一契约,E-01) */ int cfg_file_load(void); @@ -96,7 +102,9 @@ int cfg_file_load(void); * 限定条件:至少注册一个段 * 函数说明:文件存在 → 逐行保留注释/未知段/未知键,受管段/键按 RAM 副本 * 值原位改写,缺失段/键补写;文件缺失/损坏 → 按 RAM 副本重建 - * 完整文档(含文件头注释);写盘 = 临时文件 + 原子改名 + * 完整文档(含文件头注释);写盘 = 临时文件 + 原子改名;输出超 + * CFG_FILE_MAX_LEN → 拒绝保存返回 -1(原文件不动 + 自检上报, + * 引擎写盘闸口同拒,E-01) */ int cfg_file_save(void); @@ -107,8 +115,10 @@ int cfg_file_save(void); * 限定条件:由配置引擎属主任务周期调用(建议 2s;MCU = defaultTask, * 模拟器 = 主循环);引擎静态状态无跨任务保护,属主唯一即无竞态 * 函数说明:(size, mtime 变更令牌) 相对基线变化且连续 CFG_FILE_SETTLE_CNT - * 次轮询稳定 → 重载全部段(各段复位工厂默认后解析);挂回调段 - * 有差异 → 调用其回调(如触发网络重建);重载后刷新基线 + * 次轮询稳定 → 先查 st.size 超 CFG_FILE_MAX_LEN = 损坏 → 工厂默认 + * + 重建模板 + 自检留痕(不触发段回调:保留在线修复通道,重启 + * 收敛,E-01);否则重载全部段(各段复位工厂默认后解析);挂回调 + * 段有差异 → 调用其回调(如触发网络重建);重载后刷新基线 * (回调触发的写盘不引发二次重载) */ void cfg_file_poll(void); diff --git a/simulator/main.c b/simulator/main.c index 3888644b..d30c0931 100644 --- a/simulator/main.c +++ b/simulator/main.c @@ -302,6 +302,65 @@ static int run_cfgtest(void) } } printf("[cfgtest] OK surgical save (comment kept, value intact)\n"); + + /* E-01 场景 1:伪造超长文件(> CFG_FILE_MAX_LEN)→ poll 必须判损坏 + * (与 load/MCU 同一契约):RAM 回工厂默认 + 文件重建为出厂模板, + * 而非截断解析残留 10.0.0.1(修复前死码走的是截断分支) */ + { + uint8_t big[CFG_FILE_MAX_LEN + 64u]; + cfg_file_stat_t st_chk; + + (void)memset(big, ';', sizeof(big)); /* 纯注释:内容无关,尺寸超即触发 */ + if (cfg_store_write_atomic(cfg_store_config_path(), big, sizeof(big)) != 0) { + printf("[cfgtest] FAIL forge oversized write\n"); + return 1; + } + cfg_file_poll(); /* 检出变更(沉降计数 1) */ + cfg_file_poll(); /* 沉降完成 → 损坏分支:工厂默认 + 重建模板 */ + + if (cfg_store_stat(cfg_store_config_path(), &st_chk) != 0 || + st_chk.size == 0u || st_chk.size > CFG_FILE_MAX_LEN) { + printf("[cfgtest] FAIL oversized file not rebuilt (size=%lu)\n", + (unsigned long)st_chk.size); + return 1; + } + p_cfg = net_cfg_get(); + if (p_cfg->ip[0] != 192u || p_cfg->ip[1] != 168u || + p_cfg->ip[2] != 1u || p_cfg->ip[3] != 100u) { + printf("[cfgtest] FAIL ip not factory after oversized: %u.%u.%u.%u\n", + (unsigned)p_cfg->ip[0], (unsigned)p_cfg->ip[1], + (unsigned)p_cfg->ip[2], (unsigned)p_cfg->ip[3]); + return 1; + } + } + printf("[cfgtest] OK oversized file rebuilt to factory template\n"); + + /* E-01 场景 2:save 拒写"雷"文件——伪造合法界内纯注释文件(4000B,无 + * [network] 段),外科手术保存文末补写受管段 → 输出必然超 + * CFG_FILE_MAX_LEN:必须拒绝(net_cfg_save 返 -1)且原文件不动; + * 修复前此处会成功写出 ~4200B 超长文件 = 下次开机被销毁的雷 */ + { + uint8_t pad[4000u]; + cfg_file_stat_t st_chk; + + (void)memset(pad, ';', sizeof(pad)); + if (cfg_store_write_atomic(cfg_store_config_path(), pad, sizeof(pad)) != 0) { + printf("[cfgtest] FAIL forge padding write\n"); + return 1; + } + if (net_cfg_save() == 0) { + printf("[cfgtest] FAIL oversized document was written (landmine)\n"); + return 1; + } + (void)cfg_store_stat(cfg_store_config_path(), &st_chk); + if (st_chk.size != 4000u) { + printf("[cfgtest] FAIL file modified by rejected save (size=%lu)\n", + (unsigned long)st_chk.size); + return 1; + } + } + printf("[cfgtest] OK save rejected oversized document (file untouched)\n"); + printf("[cfgtest] PASS\n"); return 0; }