GD32C103CB(Cortex-M4,128 KB Flash / 32 KB SRAM,120 MHz HXTAL PLL)上的 CAN-FD Bootloader,C++(AC6 + microlib,CMSIS csolution 构建)。功能:上电判定标志位 → 直接跳 APP / 10 s 延迟窗口监听 OTA / 从备份区恢复 / 停留 OTA 模式;OTA 采用 AES-128 握手派生会话密钥,48 字节/包加密传输,每包带密钥 CRC32;另含深度睡眠与 PB8 EXTI 唤醒。
全仓(含历史提交)未发现任何 .md/.html 评审、审计或报告文件;Gitea issue 因访问令牌无 read:issue 权限无法读取。本报告是该仓库的第一份评审记录,因此没有可修订的原评审文件。作为替代,第 5 节逐条核对了提交信息中声明的修复/功能在当前代码中是否成立。
| 编号 | 严重度 | 领域 | 文件:行号 | 现象 | 风险 | 修改建议 |
| F-01 | 致命 | 安全 | Robot_V2/sys/config.h:70-72;sys/OTA.cpp:531-560 | AES-128 主密钥 K_master 以明文常量写在头文件并提交进仓库,所有设备共用同一密钥;会话密钥 K_session = AES(K_master, nonce_server) 完全由上位机提供的随机数决定,设备侧不贡献随机性。 | 任何拿到仓库或任一固件镜像的人即可伪造合法上位机;设备无法区分新会话与录制的旧会话,可重放旧握手与旧固件(降级攻击)。 | 主密钥移出源码(读保护的 OTP/选项字节或按设备 UID 派生的设备密钥);握手加入设备侧随机数(双向 nonce);开启 Flash 读保护(SPC)。 |
| F-02 | 致命 | 安全/升级可靠性 | sys/OTA.cpp:423-478 | 固件没有整体签名或整镜像校验:仅有每包 4 字节“带密钥 CRC32”,CRC32 是线性函数,不是 MAC;AES-ECB 逐块加密,相同明文块产生相同密文,可被剪切/拼接/重排。 | 知道密钥即可构造任意固件;即便不知道密钥,ECB 也允许在包内替换整块。被篡改或损坏的镜像会被当成合法固件写入并激活。 | 改为“整镜像 SHA-256 + ECDSA/Ed25519 签名”或至少 AES-CMAC/HMAC;加密改为 AES-CTR/GCM 并带包序号作 nonce;结束时校验总长度与整体摘要后才置可用标志。 |
| F-03 | 致命 | 升级可靠性 | sys/OTA.cpp:369-373, 453, 466-471 | ① writeApplicationData 写 Flash 失败只 printf,不返回错误、不上报,随后照常回 ACK;② CRC 校验失败的包仍会继续检查尾帧标志 receive_IFEND,若尾帧本身 CRC 错误也照样置 FINISHED 并跳转;③ 结束时不校验 received_size 与 expected_size(received_size 从未累加)。 | 丢包、写失败、尾帧损坏都可能导致残缺镜像被 jumpToApplication 置为“可用+激活”(OTA.cpp:232-233),只做向量表两字检查就跳转,设备可能变砖或运行半截固件。 | 写失败要回错误帧并让上位机重发;尾帧必须 CRC 通过;累加已写字节并在结束时比对 expected_size 与整镜像校验值,全部通过后才写可用标志。 |
| S-01 | 严重 | Flash/断电安全 | sys/OTA.cpp:41-124(OTA_SET) | 6 个 OTA 标志共用 0x0801E000 一页。把某位从 0 改回 1 时整页擦除后再逐项重写,无双页/日志结构,重写结果不检查。 | 擦除后、重写前掉电,所有标志(包括备份可用标志)全部变回 0xFFFFFFFF:主区与备份都被判无效,设备只能停在 OTA 模式等待上位机。 | 改为双页乒乓或追加式记录(序号+CRC),先写新记录再作废旧记录;写后回读校验。 |
| S-02 | 严重 | Flash 驱动用法 | sys/OTA.cpp:18-27, 54, 86, 98, 307-309;sys/initial.cpp:208 | write_ADDRESS 每次写完都 fmc_lock()。OTA_SET 内先调用 write_ADDRESS(会上锁)再 fmc_page_erase;eraseApplicationArea 连续调用 3 次 OTA_SET,第 2、3 次进入时 FMC 已被上锁;EXTI 唤醒中断里调用 OTA_SET 前根本没有解锁。GD32 库在锁定时写 FMC_CTL 无效而 fmc_ready_wait 仍返回 READY。 | 页擦除可能被静默跳过却返回成功,标志位停留在旧值(例如“激活”标志清不掉),状态机与实际 Flash 状态不一致。 | 由 OTA_SET 自己负责一次 unlock/lock,write_ADDRESS 不再加解锁;擦除后回读确认 0xFFFFFFFF;所有 Flash 操作统一入口。 |
| S-03 | 严重 | 通信协议 | sys/OTA.cpp:623-634 | getDeviceSerialNumber 把寄存器地址常量(0x1FFFF7E8 等)本身当作 UID 数值,没有解引用。 | 所有设备上报的“UID”完全相同,UID 在握手中失去意义,无法做设备绑定或按设备派生密钥。 | 改为 *(volatile uint32_t*)DEVICE_SERIAL_ADDR_xxx 读取。 |
| S-04 | 严重 | 通信协议 | sys/config.h:49-53;device/Can.cpp:105-119, 196-222 | 协议没有节点地址:所有设备使用相同的固定 CAN ID(0x100-0x103),硬件过滤器全通(mask=0)。 | 同一总线上多块处于 BOOT 的板子会同时响应握手、同时擦写、同时回 ACK,ACK 冲突/交错,升级结果不可预测;也无法单独升级某一节点。 | 在 ID 或负载中加入节点号(可由 UID 派生),过滤器只收本节点与广播 ID。 |
| S-05 | 严重 | CAN 接收/实时性 | device/Can.cpp:196-222, 150-163(communication_check) | 接收只有单个 receive_message 缓冲,ISR 每收一帧整体覆盖,计数器 can0_receive_count 只是计数;主循环取出时读到的永远是最后一帧,且计数自减在主循环、自增在 ISR,无临界区。ISR 还接收 ACK/WAIT ID——其他 BOOT 节点每 10 ms 发送的 WAIT 心跳会覆盖本该处理的数据帧。 | 连续两帧到达(握手应答+数据、多节点心跳)就会丢帧或同一帧被处理两次;提交 26cf5af 声称的“修复BOOT丢帧”并未根治。 | 改为环形接收队列(深度≥4,ISR 入队/主循环出队,单生产者单消费者),ISR 只接收 REQ/DATA/CANCEL。 |
| S-06 | 严重 | CAN 接收 | device/Can.cpp:203-212 | temp_message 是未初始化的栈变量,can_message_receive 只在扩展帧时写 rx_efid;收到标准帧时 rx_efid 为栈上残值。CANFD_OTA_CANCEL_ID = 0。 | 总线上任意标准帧都可能被判成某个 OTA ID,残值为 0 时会被当成“取消 OTA”,中途终止升级。 | temp_message 先清零或调用 can_struct_para_init;先判断 rx_ff==CAN_FF_EXTENDED 再比较 ID;取消命令不要用 ID 0。 |
| S-07 | 严重 | 故障保护 | 整个工程(无 fwdgt/wwdgt 使用);device/Can.cpp:73;RTE/.../gd32c10x_it.c:54-98 | 没有启用任何看门狗;CAN auto_bus_off_recovery = DISABLE 且无软件恢复;HardFault/BusFault 等全部是死循环。 | 任何卡死(总线 bus-off、Flash 等待、异常)后设备永久停机,只能人工断电;BOOT 是最后一道防线,不能没有自恢复。 | 启用独立看门狗并在主循环喂狗(跳 APP 前保持启用,APP 接管);开启 bus-off 自动恢复;Fault 处理改为记录原因后系统复位。 |
| S-08 | 严重 | 调试代码进入发布版 | sys/config.h:17;sys/OTA.cpp:311-337, 324;253-280 | DEBUG_OTA=1 是当前发布配置。① eraseApplicationArea 在 __disable_irq() 后擦除失败分支调用 delay_1ms(5000),该延时依赖 SysTick 中断 → 永久死等;② 擦除循环每页在关中断状态下 printf;③ jumpToApplication 关闭 USART0 时钟后仍 printf(jumping3/jumping5),fputc 轮询 TBE。 | 擦除失败时设备永久挂死(无看门狗兜底);跳转路径存在挂在 fputc 的风险(与外设时钟关闭后寄存器读值有关,需实测);关中断时间长导致 CAN 溢出。 | 发布版置 DEBUG_OTA=0;关中断区段内禁止 printf/延时;跳转前先输出日志再关时钟。 |
| S-09 | 严重 | 备份恢复 | sys/OTA.cpp:657-742;sys/flash.cpp:25 | move_app_data 用硬件 CRC 校验拷贝结果,但 RCU_CRC 时钟只在 FlashEEPROM::init() 中开启,而 flash.cpp 不在构建清单、init() 从未被调用;main.cpp:88 也忽略 move_app_data 返回值,直接跳转。 | CRC 外设无时钟时读回值恒定,src_crc==dst_crc 恒成立,校验形同虚设;拷贝失败也会跳转到半截镜像。另外拷贝前先擦除主区,拷贝中掉电则主区损坏,只能依赖备份再次恢复。 | 调用前 rcu_periph_clock_enable(RCU_CRC);检查返回值,失败时进入 OTA 模式而不是跳转;备份区自身应有长度+CRC/签名头。 |
| G-01 | 一般 | 升级流程 | sys/OTA.cpp:140-185;config.h:74 | 超时计数:非 IDLE 状态每次 process() 只 delay_1us(100) 后 ota_time++,OTA_TIMEOUT=10000 → 实际约 1 s,与注释“10 秒”不符;delay_1us 基于空循环估算,随编译优化变化。 | 上位机准备/擦除较慢时容易误超时;行为依赖优化级别。 | 超时统一基于 SysTick 毫秒计数(记录时间戳比较)。 |
| G-02 | 一般 | 升级流程 | sys/OTA.cpp:142-153, 319-329 | 页擦除失败只发错误帧并 break,process() 仍把状态置为 RECEIVING_FIRMWARE。 | 在擦除不完整的区域继续接收写入,产生 PGERR 或混合镜像。 | eraseApplicationArea 返回结果,失败则回到 IDLE。 |
| G-03 | 一般 | 升级流程 | sys/OTA.cpp:456-457 | offset = receive_packet_num * 48 为 u32 乘法,可溢出回绕后通过 offset+48 ≤ APP_MAX_SIZE 检查。 | 异常包序号会写入意外偏移。 | 先检查 receive_packet_num < APP_MAX_SIZE/48 再计算偏移;同时要求序号单调连续。 |
| G-04 | 一般 | A/B 设计 | main.cpp:24-101;sys/OTA.cpp:221-234 | 所谓 A/B 实为“主区 + 恢复区”:BOOT 从不写备份区;新固件的 CHECK 标志只能由 APP 写入(隐含约定,无文档)。若 APP 未写 CHECK,下次复位会走备份分支,用备份覆盖刚升级的新固件。 | 升级后“回退”行为取决于 APP 是否遵守未文档化约定,容易出现“升级后重启又变回旧版”。 | 书面定义 BOOT/APP 标志握手协议(试运行→确认→回滚计数);或真正双槽并只切换启动槽。 |
| G-05 | 一般 | 跳转 | sys/OTA.cpp:250-271 | 切换到 IRC8M 后未等待 SCSS 生效,也未关闭 PLL/HXTAL、未复位 RCU 寄存器;SysTick 关闭后未清除 SCB->ICSR 挂起位。 | APP 的 SystemInit 一般能兜底,但时钟残留状态与 APP 预期不一致时会出现难查的启动问题。 | 调用 rcu_deinit() 或按 SystemInit 逆序复位时钟,清 PendSV/SysTick 挂起。 |
| G-06 | 一般 | CAN 发送 | device/Can.cpp:376-409;Can.h:5-43;Can.cpp:122 | CAN0_TX_IRQHandler 未在 extern "C" 中声明,C++ 名字修饰后不会覆盖启动文件的弱符号(map 中无该函数);NVIC 也从未使能 CAN0_TX_IRQn。 | 发送队列只入队不出队:邮箱满时消息进入队列后永远不会发送,提交 d019b41 的“发送队列机制”实际不工作。 | 在 extern "C" 中声明 TX 中断函数并 nvic_irq_enable(CAN0_TX_IRQn);或在主循环轮询出队。 |
| G-07 | 一般 | CAN-FD | device/Can.cpp:315-334 | DLC 长度映射有缺档:分支为 21-23→24、29-32→32,长度恰为 24 以及 25-28 没有任何分支,直接返回失败(应为 21-24→24、25-32→32)。 | 发送 24~28 字节报文会被静默丢弃(当前协议最大 36/64 字节,未来扩展易踩坑)。 | 用查表把长度向上取整到 {12,16,20,24,32,48,64}。 |
| G-08 | 一般 | 睡眠/唤醒 | sys/initial.cpp:147-177, 199-255 | 唤醒中断中执行 Flash 擦写与 OTA_SET(未解锁,见 S-02);地址循环用 address >= SLEEP_CONFIG_START 递减,起点为 0x0801E400 时边界写法易错;enter_deep_sleep 返回后无处理(醒来靠 EXTI 软复位)。 | ISR 内长时间 Flash 操作;唤醒后标志写入可能失败。 | ISR 只置标志,Flash 操作放主流程;睡眠标志使用与 OTA 标志相同的可靠记录格式。 |
| G-09 | 一般 | 代码质量 | sys/OTA.h:12;OTA.h:18 | 成员 u32 ota_stateota_time(明显拼接笔误,未使用);isActive() 返回 ota_state 非零,WAITING_RANDOM 等早期状态即被视为“进入 OTA”。 | 可读性差;延迟启动期间收到一帧握手就永久停在 OTA 循环,即使之后超时回 IDLE 也不会再跳 APP(main.cpp:61-67)。 | 删除笔误成员;OTA 循环中 IDLE 持续超时后重新走启动判定。 |
| G-10 | 一般 | 代码质量 | sys/OTA.cpp:18(inline)与 sys/initial.cpp:183 | write_ADDRESS 在两个翻译单元各定义一次(一个 inline、一个外部链接),违反 ODR。 | 链接器选择哪份实现不确定,后续改动其中一份会产生隐蔽差异。 | 只保留一份,放到 flash 模块并在头文件声明。 |
| G-11 | 一般 | 代码质量 | sys/OTA.cpp:676, 709, 735 与 config.h:17 | move_app_data 内用 OTA_DEBUG,而全工程定义的是 DEBUG_OTA,这些日志永远不会输出。 | 恢复失败时没有任何诊断信息。 | 统一宏名。 |
| A-01 | 建议 | 构建 | sys/flash.h:5-14 / sys/flash.cpp(未加入 cproject.yml) | flash.cpp/flash.h 未参与构建,且 FLASH_EEPROM_OK 枚举与 config.h 的 constexpr 同名冲突;EEPROM 地址 0x0801F800 与其他区无文档说明。 | 死代码误导维护者,一旦加入构建即编译失败。 | 删除或整合进统一 Flash 存储模块。 |
| A-02 | 建议 | 内存配置 | RTE/.../startup_gd32c10x.s:42;regions_GD32C103CB.h:96-97;map | 启动文件 Stack 0x800 + scatter ARM_LIB_STACK 0x200 + HEAP 0xC00 三套配置并存(map 中均占用 RAM),microlib 下实际 MSP 来自向量表(2 KB)。 | 浪费 ~3.5 KB RAM,且栈大小以哪处为准不明确。 | 只保留一处栈定义;BOOT 不用 malloc 时把 HEAP 设为 0。 |
| A-03 | 建议 | 构建配置 | Robot_V2.csolution.yml:42-47 | Debug 为 optimize none,Release 为 size;ROM 使用 0x3398/0x6000(约 54%),余量可接受。BOOT 版本号写死在 config.h,未写入镜像固定位置。 | 上位机无法读取 BOOT 版本;Debug/Release 时序差异(空循环延时)大。 | 在固定偏移放置版本/构建信息结构;延时不依赖优化级别。 |
| A-04 | 建议 | 仓库卫生 | .gitignore;out/、tmp/(65 个文件) | out/、tmp/ 构建产物(axf/hex/map/.o/CMake 缓存等)在 .gitignore 生效前已被跟踪,至今仍在库中,每次提交都产生大量噪声 diff;compile_commands.json 含本机路径 G:/Robot_v2/BOOT。 | 仓库膨胀、评审困难、泄露本机目录结构。 | git rm -r --cached out tmp(由维护者执行);发布固件改用 Release 附件。 |
| A-05 | 建议 | 仓库卫生 | Robot_V2.cbuild-idx.yml、*.cbuild-pack.yml、.vscode/*.json、.continue/mcpServers/new-mcp-server.yaml、RTE/*.base@1.0.0 | 生成文件与 IDE 本地配置被跟踪(.gitignore 已声明忽略但未从索引移除);.continue 下是未填写的模板。 | 多人协作时频繁冲突。 | 从索引移除并保持忽略;保留 cbuild-pack.yml(锁定包版本)即可。 |
| A-06 | 建议 | 文档/测试 | 仓库根目录 | 无 README、无协议文档(帧格式、状态机、标志位定义、BOOT/APP 约定)、无主机侧工具说明、无任何测试(AES 可用 FIPS-197 向量做单元测试)。 | 知识只在作者脑中,协议变更无法验证。 | 补 README + docs/protocol.md(状态机图、帧表、Flash 分区表);为 AES/CRC/标志模块加主机侧单元测试。 |