E-01修复:poll超长守卫前置st.size根治死码,save输出上限收紧4096,写盘闸拦截与自检留痕

This commit is contained in:
2026-09-22 01:28:10 +08:00
parent c3f424abc0
commit c29b728153
3 changed files with 120 additions and 20 deletions
+47 -16
View File
@@ -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];
+14 -4
View File
@@ -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 <stddef.h>
/* 配置文件长度上限(字节);超过 = 文件损坏,按出厂模板重建 */
/* 配置文件长度上限(字节);超过 = 文件损坏,按出厂模板重建
* (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);
+59
View File
@@ -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;
}