Files
ESP32_COMM/docs/CODE_REVIEW_REPORT_2026-06-13.html
T
小龙虾 2caf10b9de feat(ota): 添加OTA回滚保护和固件完整性校验功能
- 启用CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE和CONFIG_APP_ROLLBACK_ENABLE配置选项
- 在main.cpp中添加esp_ota_mark_app_valid_cancel_rollback()调用,防止意外回滚
- 实现SHA-256固件完整性校验,包括计算、存储和验证功能
- 添加ota_mutex.h头文件用于ESP32 OTA全局互斥锁管理

feat(health_monitor): 实现线程安全的健康状态管理

- 将HealthMonitor类的健康状态改为私有成员并通过mutex保护
- 添加getState()和setState()方法提供线程安全的状态访问
- 修复多任务并发访问健康状态时的竞争条件问题

feat(ota_relay): 增强中继模块的线程安全性和可靠性

- 为OtaRelay类添加静态mutex保护状态变量访问
- 实现StateLockGuard RAII锁管理器确保异常安全
- 添加全局超时机制防止OTA过程无限等待
- 改进flash写入逻辑支持非4字节对齐数据处理

refactor(app_config): 更新版本信息和配置常量

- 更新BUILD_TIME至最新编译时间
- 更新GIT_HASH至最新提交
- 添加BLE_OTA_CTRL_VERIFY_SHA256命令常量定义
2026-06-14 17:10:24 +08:00

971 lines
39 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32_COMM 高风险代码评审报告</title>
<style>
:root {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #21262d;
--bg-card: #1c2128;
--border: #30363d;
--text-primary: #e6edf3;
--text-secondary: #8b949e;
--text-muted: #6e7681;
--critical: #f85149;
--critical-bg: rgba(248,81,73,0.12);
--critical-border: rgba(248,81,73,0.4);
--high: #d29922;
--high-bg: rgba(210,153,34,0.12);
--high-border: rgba(210,153,34,0.4);
--medium: #58a6ff;
--medium-bg: rgba(88,166,255,0.12);
--medium-border: rgba(88,166,255,0.4);
--accent: #58a6ff;
--success: #3fb950;
--success-bg: rgba(63,185,80,0.12);
--code-bg: #0d1117;
--table-stripe: rgba(110,118,129,0.05);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans SC', Helvetica, Arial, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
/* Header */
.report-header {
text-align: center;
padding: 3rem 0;
border-bottom: 1px solid var(--border);
margin-bottom: 3rem;
}
.report-header h1 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 0.5rem;
background: linear-gradient(135deg, #f85149, #d29922, #58a6ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.report-header .subtitle {
color: var(--text-secondary);
font-size: 1rem;
margin-bottom: 1.5rem;
}
.report-meta {
display: flex;
justify-content: center;
gap: 2rem;
flex-wrap: wrap;
font-size: 0.85rem;
color: var(--text-muted);
}
.report-meta span { display: flex; align-items: center; gap: 0.4rem; }
/* Stats Grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 1rem;
margin-bottom: 3rem;
}
.stat-card {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.5rem;
text-align: center;
transition: transform 0.2s, border-color 0.2s;
}
.stat-card:hover { transform: translateY(-2px); }
.stat-card .number {
font-size: 2.5rem;
font-weight: 800;
line-height: 1;
margin-bottom: 0.3rem;
}
.stat-card .label {
font-size: 0.8rem;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.stat-card.critical { border-color: var(--critical-border); }
.stat-card.critical .number { color: var(--critical); }
.stat-card.high { border-color: var(--high-border); }
.stat-card.high .number { color: var(--high); }
.stat-card.medium { border-color: var(--medium-border); }
.stat-card.medium .number { color: var(--medium); }
.stat-card.total { border-color: var(--accent); }
.stat-card.total .number { color: var(--accent); }
.stat-card.verified { border-color: var(--success); }
.stat-card.verified .number { color: var(--success); }
/* Dimension Table */
.section { margin-bottom: 3rem; }
.section-title {
font-size: 1.4rem;
font-weight: 700;
margin-bottom: 1.2rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--border);
display: flex;
align-items: center;
gap: 0.6rem;
}
.section-title .icon { font-size: 1.2rem; }
table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
th, td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid var(--border);
}
th {
background: var(--bg-tertiary);
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.05em;
}
tr:nth-child(even) td { background: var(--table-stripe); }
/* Severity Badges */
.badge {
display: inline-flex;
align-items: center;
padding: 0.15rem 0.6rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
white-space: nowrap;
}
.badge.critical { background: var(--critical-bg); color: var(--critical); border: 1px solid var(--critical-border); }
.badge.high { background: var(--high-bg); color: var(--high); border: 1px solid var(--high-border); }
.badge.medium { background: var(--medium-bg); color: var(--medium); border: 1px solid var(--medium-border); }
/* Finding Cards */
.finding-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
margin-bottom: 1.2rem;
overflow: hidden;
transition: border-color 0.2s;
}
.finding-card:hover { border-color: var(--text-muted); }
.finding-card.critical { border-left: 4px solid var(--critical); }
.finding-card.high { border-left: 4px solid var(--high); }
.finding-header {
padding: 1.2rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
cursor: pointer;
user-select: none;
}
.finding-header:hover { background: var(--bg-tertiary); }
.finding-title-group { flex: 1; }
.finding-number {
font-size: 0.7rem;
color: var(--text-muted);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.2rem;
}
.finding-title {
font-size: 1.05rem;
font-weight: 600;
line-height: 1.4;
}
.finding-badges {
display: flex;
gap: 0.5rem;
flex-shrink: 0;
align-items: center;
flex-wrap: wrap;
justify-content: flex-end;
}
.finding-body {
padding: 0 1.5rem 1.5rem;
display: none;
}
.finding-card.open .finding-body { display: block; }
.finding-body .detail-grid {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.4rem 1rem;
margin-bottom: 1rem;
font-size: 0.85rem;
}
.detail-label {
color: var(--text-muted);
font-weight: 500;
white-space: nowrap;
}
.detail-value { color: var(--text-primary); }
.code-block {
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1rem;
margin: 0.8rem 0;
overflow-x: auto;
font-family: 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
font-size: 0.82rem;
line-height: 1.5;
}
.code-block .comment { color: #6a9955; }
.code-block .keyword { color: #c586c0; }
.code-block .type { color: #4ec9b0; }
.code-block .string { color: #ce9178; }
.code-block .highlight { background: rgba(248,81,73,0.15); display: inline; padding: 0.1em 0.2em; border-radius: 3px; }
.impact-box {
background: rgba(248,81,73,0.06);
border: 1px solid rgba(248,81,73,0.2);
border-radius: 8px;
padding: 0.8rem 1rem;
margin: 0.8rem 0;
font-size: 0.88rem;
}
.impact-box .impact-label {
color: var(--critical);
font-weight: 600;
font-size: 0.8rem;
margin-bottom: 0.3rem;
}
.fix-box {
background: rgba(63,185,80,0.06);
border: 1px solid rgba(63,185,80,0.2);
border-radius: 8px;
padding: 0.8rem 1rem;
margin: 0.8rem 0;
font-size: 0.88rem;
}
.fix-box .fix-label {
color: var(--success);
font-weight: 600;
font-size: 0.8rem;
margin-bottom: 0.3rem;
}
/* Architecture Section */
.arch-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1rem;
}
.arch-card h4 {
font-size: 1rem;
margin-bottom: 0.6rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.arch-card p { font-size: 0.9rem; color: var(--text-secondary); line-height: 1.7; }
/* Priority Table */
.priority-table { margin-top: 1rem; }
.priority-table .p0 { color: var(--critical); font-weight: 700; }
.priority-table .p1 { color: var(--high); font-weight: 600; }
.priority-table .p2 { color: var(--medium); font-weight: 500; }
.priority-table .p3 { color: var(--text-secondary); }
.progress-bar {
width: 100%;
height: 6px;
background: var(--bg-tertiary);
border-radius: 3px;
overflow: hidden;
margin-top: 0.3rem;
}
.progress-bar .fill {
height: 100%;
border-radius: 3px;
transition: width 0.5s ease;
}
.fill.critical-fill { background: var(--critical); }
.fill.high-fill { background: var(--high); }
.fill.medium-fill { background: var(--medium); }
/* Chart */
.chart-container {
display: flex;
align-items: flex-end;
gap: 1.5rem;
height: 200px;
padding: 1rem 0;
justify-content: center;
}
.chart-bar-group {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
flex: 1;
max-width: 100px;
}
.chart-bar-stack {
width: 60px;
display: flex;
flex-direction: column-reverse;
border-radius: 6px 6px 0 0;
overflow: hidden;
}
.chart-bar-stack > div {
width: 100%;
transition: height 0.6s ease;
}
.chart-label {
font-size: 0.72rem;
color: var(--text-muted);
text-align: center;
white-space: nowrap;
}
.chart-value {
font-size: 0.8rem;
font-weight: 600;
color: var(--text-primary);
}
/* Expand/Collapse all */
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.toolbar button {
background: var(--bg-tertiary);
border: 1px solid var(--border);
color: var(--text-secondary);
padding: 0.4rem 1rem;
border-radius: 6px;
cursor: pointer;
font-size: 0.82rem;
transition: all 0.2s;
}
.toolbar button:hover {
background: var(--bg-secondary);
color: var(--text-primary);
border-color: var(--text-muted);
}
/* Footer */
.report-footer {
text-align: center;
padding: 2rem 0;
border-top: 1px solid var(--border);
margin-top: 3rem;
color: var(--text-muted);
font-size: 0.8rem;
}
/* Responsive */
@media (max-width: 768px) {
.container { padding: 1rem; }
.stats-grid { grid-template-columns: repeat(2, 1fr); }
.finding-header { flex-direction: column; }
.finding-badges { justify-content: flex-start; }
.chart-container { gap: 0.8rem; }
.chart-bar-stack { width: 40px; }
}
/* Print */
@media print {
body { background: white; color: black; }
.finding-body { display: block !important; }
.stat-card, .finding-card, .arch-card { break-inside: avoid; }
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<header class="report-header">
<h1>ESP32_COMM 高风险代码评审报告</h1>
<div class="subtitle">多维度并行审查 + 对抗性验证 · 4维度 × 33项确认发现</div>
<div class="report-meta">
<span>&#128203; 项目: ESP32_COMM (ESP32-C3 OTA Relay)</span>
<span>&#128197; 日期: 2026-06-13</span>
<span>&#128268; 固件版本: v0.3.12</span>
<span>&#128101; 审查Agent: 4 Review + 36 Verify + 1 Synthesize</span>
</div>
</header>
<!-- Stats -->
<div class="stats-grid">
<div class="stat-card total">
<div class="number">33</div>
<div class="label">确认发现</div>
</div>
<div class="stat-card critical">
<div class="number">8</div>
<div class="label">CRITICAL</div>
</div>
<div class="stat-card high">
<div class="number">18</div>
<div class="label">HIGH</div>
</div>
<div class="stat-card medium">
<div class="number">4</div>
<div class="label">MEDIUM</div>
</div>
<div class="stat-card verified">
<div class="number">33/36</div>
<div class="label">验证通过率</div>
</div>
<div class="stat-card total">
<div class="number">2</div>
<div class="label">已驳回(误报)</div>
</div>
</div>
<!-- Dimension Chart -->
<div class="section">
<div class="section-title"><span class="icon">&#128202;</span> 各维度发现分布</div>
<div class="chart-container">
<div class="chart-bar-group">
<div class="chart-value">8</div>
<div class="chart-bar-stack" style="height:160px;">
<div style="height:60px;background:var(--critical);" title="2 CRITICAL"></div>
<div style="height:80px;background:var(--high);" title="4 HIGH"></div>
<div style="height:20px;background:var(--medium);" title="1 MEDIUM"></div>
</div>
<div class="chart-label">内存安全</div>
</div>
<div class="chart-bar-group">
<div class="chart-value">10</div>
<div class="chart-bar-stack" style="height:180px;">
<div style="height:54px;background:var(--critical);" title="3 CRITICAL"></div>
<div style="height:108px;background:var(--high);" title="6 HIGH"></div>
<div style="height:18px;background:var(--medium);" title="1 MEDIUM"></div>
</div>
<div class="chart-label">并发/RTOS</div>
</div>
<div class="chart-bar-group">
<div class="chart-value">9</div>
<div class="chart-bar-stack" style="height:170px;">
<div style="height:72px;background:var(--critical);" title="4 CRITICAL"></div>
<div style="height:72px;background:var(--high);" title="4 HIGH"></div>
<div style="height:18px;background:var(--medium);" title="1 MEDIUM"></div>
</div>
<div class="chart-label">OTA/固件</div>
</div>
<div class="chart-bar-group">
<div class="chart-value">7</div>
<div class="chart-bar-stack" style="height:150px;">
<div style="height:43px;background:var(--critical);" title="2 CRITICAL"></div>
<div style="height:86px;background:var(--high);" title="4 HIGH"></div>
<div style="height:14px;background:var(--medium);" title="1 MEDIUM"></div>
</div>
<div class="chart-label">SPI/协议</div>
</div>
</div>
</div>
<!-- TOP 3 Critical -->
<div class="section">
<div class="section-title"><span class="icon">&#128680;</span> TOP 3 最危险 Bug</div>
<!-- Finding 1 -->
<div class="finding-card critical open" id="f1">
<div class="finding-header" onclick="toggleFinding('f1')">
<div class="finding-title-group">
<div class="finding-number">Finding #1 — CRITICAL</div>
<div class="finding-title">SPI 总线无并发保护 — 多任务竞争导致协议损坏</div>
</div>
<div class="finding-badges">
<span class="badge critical">CRITICAL</span>
<span class="badge" style="background:var(--bg-tertiary);color:var(--text-secondary);border:1px solid var(--border);">并发竞态</span>
</div>
</div>
<div class="finding-body">
<div class="detail-grid">
<span class="detail-label">文件:</span>
<span class="detail-value">spi_master.h:73-142, ota_relay.cpp, wifi_ota.cpp</span>
<span class="detail-label">维度:</span>
<span class="detail-value">并发/RTOS</span>
<span class="detail-label">验证:</span>
<span class="detail-value" style="color:var(--success);">&#10004; HIGH置信度 — 源码确认</span>
</div>
<p>SpiMaster 是单例但<strong>没有任何互斥锁</strong>。多个 FreeRTOS 任务直接调用 SPI 方法:ota_relay 任务 (优先级 MAX-2)、wifi_ota 任务 (MAX-3)、health_monitor 任务 (MAX-4)。协议操作是多步非原子的(如 otaTransfer = 发送帧 + 轮询响应,CS在两次传输之间释放),高优先级任务可在协议序列中间抢占。</p>
<div class="code-block"><span class="comment">// spi_master.h — 无任何mutex成员</span>
<span class="keyword">class</span> <span class="type">SpiMaster</span> {
<span class="keyword">private</span>:
<span class="type">spi_device_handle_t</span> m_spiDevice = <span class="keyword">NULL</span>; <span class="comment">// ← 唯一成员,无锁</span>
};</div>
<div class="impact-box">
<div class="impact-label">&#9888; 影响</div>
帧交错(A任务的请求配对B任务的响应)、GD32状态机混乱、固件传输数据损坏。生产环境高频CAN通信+OTA并发时必现。
</div>
<div class="fix-box">
<div class="fix-label">&#128295; 修复建议</div>
在 SpiMaster 中添加 <code>SemaphoreHandle_t m_mutex</code>,所有公共方法入口加锁。对于多步协议操作(otaTransfer等),使用统一的锁粒度保护整个请求-响应序列。
</div>
</div>
</div>
<!-- Finding 2 -->
<div class="finding-card critical" id="f2">
<div class="finding-header" onclick="toggleFinding('f2')">
<div class="finding-title-group">
<div class="finding-number">Finding #2 — CRITICAL</div>
<div class="finding-title">硬编码占位密钥 — 认证体系形同虚设</div>
</div>
<div class="finding-badges">
<span class="badge critical">CRITICAL</span>
<span class="badge" style="background:var(--bg-tertiary);color:var(--text-secondary);border:1px solid var(--border);">安全/OTA</span>
</div>
</div>
<div class="finding-body">
<div class="detail-grid">
<span class="detail-label">文件:</span>
<span class="detail-value">app_config.h:107-115</span>
<span class="detail-label">维度:</span>
<span class="detail-value">OTA/固件完整性</span>
<span class="detail-label">验证:</span>
<span class="detail-value" style="color:var(--success);">&#10004; HIGH置信度 — 源码确认</span>
</div>
<p>MASTER_KEY 使用 NIST AES 测试向量,SCRAMBLE_CONST 使用简单递增序列,GD32_CHIP_UID 使用 0x00-0x0B。注释标注"需填入实际密钥"但从未替换。</p>
<div class="code-block"><span class="comment">// ⚠️ 请填入实际密钥 ← 注释说"需填入"但从未填入</span>
<span class="keyword">inline constexpr</span> <span class="type">uint8_t</span> MASTER_KEY[] = {
<span class="highlight">0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF</span>, <span class="comment">// ← NIST AES测试向量!</span>
<span class="highlight">0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10</span>
};
<span class="keyword">inline constexpr</span> <span class="type">uint8_t</span> SCRAMBLE_CONST[] = {
<span class="highlight">0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88</span>, <span class="comment">// ← 简单递增序列</span>
<span class="highlight">0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00</span>
};
<span class="keyword">inline constexpr</span> <span class="type">uint8_t</span> GD32_CHIP_UID[] = {
<span class="highlight">0x00, 0x01, 0x02, 0x03, 0x04, 0x05</span>, <span class="comment">// ← 0x00-0x0B</span>
<span class="highlight">0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B</span>
};</div>
<div class="impact-box">
<div class="impact-label">&#9888; 影响</div>
攻击者可轻易伪造认证握手,向 GD32 注入恶意固件。GD32_CHIP_UID占位值意味着所有设备共享相同"身份"(如果运行时未正确覆盖)。
</div>
<div class="fix-box">
<div class="fix-label">&#128295; 修复建议</div>
从安全存储(NVS/efuse)读取密钥;移除所有硬编码占位值;添加编译期断言确保非测试密钥;MASTER_KEY和SCRAMBLE_CONST应在首次设备配准时由安全随机数生成。
</div>
</div>
</div>
<!-- Finding 3 -->
<div class="finding-card critical" id="f3">
<div class="finding-header" onclick="toggleFinding('f3')">
<div class="finding-title-group">
<div class="finding-number">Finding #3 — CRITICAL</div>
<div class="finding-title">Flash 写入无对齐检查 — 可能导致固件损坏</div>
</div>
<div class="finding-badges">
<span class="badge critical">CRITICAL</span>
<span class="badge" style="background:var(--bg-tertiary);color:var(--text-secondary);border:1px solid var(--border);">内存安全/OTA</span>
</div>
</div>
<div class="finding-body">
<div class="detail-grid">
<span class="detail-label">文件:</span>
<span class="detail-value">ota_relay.cpp:308-320, wifi_ota.cpp:711/740, ble_ota.cpp:575</span>
<span class="detail-label">维度:</span>
<span class="detail-value">内存安全 + OTA完整性</span>
<span class="detail-label">验证:</span>
<span class="detail-value" style="color:var(--success);">&#10004; HIGH置信度 — 源码确认</span>
</div>
<div class="code-block"><span class="type">esp_err_t</span> OtaRelay::flashWriteChunk(<span class="type">uint32_t</span> offset, <span class="keyword">const</span> <span class="type">uint8_t</span> *data, <span class="type">size_t</span> len)
{
<span class="comment">// ... 无对齐检查 ...</span>
<span class="keyword">return</span> <span class="highlight">esp_partition_write</span>(instance().m_gd32Partition, offset, data, len); <span class="comment">// ← offset/len可能非4字节对齐</span>
}</div>
<p>两个调用路径产生非对齐值:(1) WiFi OTA: gd32_offset 从 httpd_req_recv 返回值累加,最后一个chunk是固件余数;(2) BLE OTA: data_len = recv_len - 11(7字节头+4字节CRC),不保证4字节对齐。esp_partition_write 要求 offset 和 size 均4字节对齐。</p>
<div class="impact-box">
<div class="impact-label">&#9888; 影响</div>
非对齐写入导致 ESP_ERR_INVALID_ARG 或触发 read-modify-write 异常,GD32固件最后一个chunk损坏或丢失。
</div>
<div class="fix-box">
<div class="fix-label">&#128295; 修复建议</div>
flashWriteChunk 内部做4字节对齐对齐(不足部分 read-modify-write),或在上层确保只传对齐参数。
</div>
</div>
</div>
</div>
<!-- Other CRITICAL Findings -->
<div class="section">
<div class="section-title"><span class="icon">&#128308;</span> 其他 CRITICAL 级发现</div>
<div class="finding-card critical" id="f4">
<div class="finding-header" onclick="toggleFinding('f4')">
<div class="finding-title-group">
<div class="finding-number">Finding #4 — CRITICAL</div>
<div class="finding-title">BLE/WiFi 双通道 OTA 同时触发 — ESP32 OTA状态冲突</div>
</div>
<div class="finding-badges"><span class="badge critical">CRITICAL</span></div>
</div>
<div class="finding-body">
<div class="detail-grid">
<span class="detail-label">文件:</span><span class="detail-value">wifi_ota.h:42-46, ble_ota.h</span>
<span class="detail-label">验证:</span><span class="detail-value" style="color:var(--success);">&#10004; HIGH置信度</span>
</div>
<p>BleOta 和 WifiOta 各自维护独立的 m_esp32OtaActive、m_esp32OtaHandle、m_esp32UpdatePartition,但底层 esp_ota_begin/write/end 操作的是同一个OTA分区。两个通道同时触发时:第二个 esp_ota_begin 会失败或覆盖句柄;esp_ota_write 可能交错写入同一分区;导致固件损坏+变砖。</p>
<div class="fix-box"><div class="fix-label">&#128295; 修复建议</div>添加全局 OTA 互斥标志,同一时刻只允许一个OTA源(BLE或WiFi)。</div>
</div>
</div>
<div class="finding-card critical" id="f5">
<div class="finding-header" onclick="toggleFinding('f5')">
<div class="finding-title-group">
<div class="finding-number">Finding #5 — CRITICAL</div>
<div class="finding-title">SPI 协议帧仅用 XOR 伪校验 — 数据损坏不可检测</div>
</div>
<div class="finding-badges"><span class="badge critical">CRITICAL</span></div>
</div>
<div class="finding-body">
<div class="detail-grid">
<span class="detail-label">文件:</span><span class="detail-value">spi_master.cpp:134-141</span>
<span class="detail-label">验证:</span><span class="detail-value" style="color:var(--success);">&#10004; HIGH置信度</span>
</div>
<div class="code-block"><span class="type">uint8_t</span> SpiMaster::otaCrc8(<span class="keyword">const</span> <span class="type">uint8_t</span> *data, <span class="type">size_t</span> len) {
<span class="type">uint8_t</span> crc = <span class="highlight">0</span>;
<span class="keyword">for</span> (<span class="type">size_t</span> i = <span class="highlight">0</span>; i &lt; len; ++i)
<span class="highlight">crc ^= data[i]</span>; <span class="comment">// ← 简单XOR,非CRC8</span>
<span class="keyword">return</span> crc;
}</div>
<p>XOR校验无法检测:偶数个相同位置的位翻转、字节交换、全零段丢失。固件数据的静默损坏几乎不可检测。</p>
<div class="fix-box"><div class="fix-label">&#128295; 修复建议</div>替换为 CRC-8/MAXIM 或 CRC-16/CCITT,与GD32团队协商。</div>
</div>
</div>
<div class="finding-card critical" id="f6">
<div class="finding-header" onclick="toggleFinding('f6')">
<div class="finding-title-group">
<div class="finding-number">Finding #6 — CRITICAL</div>
<div class="finding-title">SPI_OTA_SOF 与 SPI_APP_ENTER_BOOT_ACK_OK 值碰撞 (0xA5)</div>
</div>
<div class="finding-badges"><span class="badge critical">CRITICAL</span></div>
</div>
<div class="finding-body">
<div class="detail-grid">
<span class="detail-label">文件:</span><span class="detail-value">spi_master.h:21,26</span>
<span class="detail-label">验证:</span><span class="detail-value" style="color:var(--success);">&#10004; HIGH置信度</span>
</div>
<div class="code-block"><span class="keyword">#define</span> <span class="highlight">SPI_OTA_SOF</span> (<span class="highlight">0xA5</span>) <span class="comment">// OTA帧起始标记</span>
<span class="keyword">#define</span> <span class="highlight">SPI_APP_ENTER_BOOT_ACK_OK</span> (<span class="highlight">0xA5</span>) <span class="comment">// APP确认进入Bootloader</span></div>
<p>同一值 0xA5 表示两种完全不同的语义。GD32在OTA窗口回复ACK时,ESP32可能将ACK字节误解析为OTA帧起始,导致协议状态机混乱。</p>
<div class="fix-box"><div class="fix-label">&#128295; 修复建议</div>修改其中一个常量值,消除歧义。例如将 SPI_APP_ENTER_BOOT_ACK_OK 改为 0xB5。</div>
</div>
</div>
<div class="finding-card critical" id="f7">
<div class="finding-header" onclick="toggleFinding('f7')">
<div class="finding-title-group">
<div class="finding-number">Finding #7 — CRITICAL</div>
<div class="finding-title">OTA 会话无全局超时 — 可无限挂起</div>
</div>
<div class="finding-badges"><span class="badge critical">CRITICAL</span></div>
</div>
<div class="finding-body">
<div class="detail-grid">
<span class="detail-label">文件:</span><span class="detail-value">ota_relay.cpp:76-91,562; ota_relay.h:59</span>
<span class="detail-label">验证:</span><span class="detail-value" style="color:var(--success);">&#10004; HIGH置信度</span>
</div>
<p>waitUntilNotWait() 每次调用有 max_wait_rounds 上限,但整个 forwardFirmware() 流程没有全局超时。GD32 持续返回 WAIT 时,单个 SEND_SIZE阶段可挂起 ~78秒,完整流程可挂起数分钟。OTA_RESULT_ERR_USER_ABORT 已定义但从未使用。</p>
<div class="fix-box"><div class="fix-label">&#128295; 修复建议</div>在 forwardFirmware() 入口记录开始时间,每个阶段检查全局超时(如5分钟),超时则中止并清理。</div>
</div>
</div>
<div class="finding-card critical" id="f8">
<div class="finding-header" onclick="toggleFinding('f8')">
<div class="finding-title-group">
<div class="finding-number">Finding #8 — CRITICAL</div>
<div class="finding-title">HTTP CORS 允许所有来源 — 任意网页可触发 OTA</div>
</div>
<div class="finding-badges"><span class="badge critical">CRITICAL</span></div>
</div>
<div class="finding-body">
<div class="detail-grid">
<span class="detail-label">文件:</span><span class="detail-value">wifi_ota.cpp:58</span>
<span class="detail-label">验证:</span><span class="detail-value" style="color:var(--success);">&#10004; HIGH置信度</span>
</div>
<div class="code-block">httpd_resp_set_hdr(req, <span class="string">"Access-Control-Allow-Origin"</span>, <span class="highlight">"*"</span>);</div>
<p>任何网页都可以向 ESP32 发起 OTA 请求。结合占位密钥问题,攻击链完整:恶意网页 → 上传伪造固件 → 通过占位密钥认证 → 烧录恶意GD32固件。</p>
<div class="fix-box"><div class="fix-label">&#128295; 修复建议</div>限制 CORS 为本地网络来源,或移除 CORS头并使用同源策略。</div>
</div>
</div>
</div>
<!-- HIGH Findings -->
<div class="section">
<div class="section-title"><span class="icon">&#128992;</span> HIGH 级关键发现</div>
<div class="toolbar">
<span style="color:var(--text-muted);font-size:0.85rem;">共 18 项 HIGH 级发现,点击展开详情</span>
<button onclick="toggleAll('high')">展开/折叠全部</button>
</div>
<table>
<thead>
<tr><th>#</th><th>严重度</th><th>文件</th><th>问题</th><th>影响</th></tr>
</thead>
<tbody>
<tr><td>9</td><td><span class="badge high">HIGH</span></td><td><code>wifi_ota.cpp:121</code></td><td>WiFi事件回调中调用 esp_wifi_connect() 重连</td><td>在系统事件任务中调用可能死锁</td></tr>
<tr><td>10</td><td><span class="badge high">HIGH</span></td><td><code>health_monitor.cpp</code></td><td>healthState 静态变量无锁跨任务读写</td><td>竞态导致健康判断错误</td></tr>
<tr><td>11</td><td><span class="badge high">HIGH</span></td><td><code>ota_relay.h:80-84</code></td><td>5个静态变量(relayState等)无锁跨任务访问</td><td>协议状态不一致</td></tr>
<tr><td>12</td><td><span class="badge high">HIGH</span></td><td><code>wifi_ota.h:33-47</code></td><td>WifiOta 所有成员 public 且无锁</td><td>HTTP回调与WiFi事件回调竞争</td></tr>
<tr><td>13</td><td><span class="badge high">HIGH</span></td><td><code>ble_ota.h</code></td><td>BleOta 成员无锁,BLE GATT回调与主任务竞争</td><td>BLE OTA 数据损坏</td></tr>
<tr><td>14</td><td><span class="badge high">HIGH</span></td><td><code>debug_log.cpp:296</code></td><td>log() 栈分配 msg[512] + log_buf[600] = 1112字节</td><td>低栈任务中栈溢出</td></tr>
<tr><td>15</td><td><span class="badge high">HIGH</span></td><td><code>wifi_ota.cpp:1008</code></td><td>spi_send_handler 栈分配 1728+ 字节</td><td>HTTP server栈仅8KB,接近溢出</td></tr>
<tr><td>16</td><td><span class="badge high">HIGH</span></td><td><code>ota_relay.cpp:456</code></td><td>sendNonceAndAuthenticate() nonce 仅用 esp_random() 一次</td><td>认证会话密钥可预测</td></tr>
<tr><td>17</td><td><span class="badge high">HIGH</span></td><td><code>ota_relay.h:25</code></td><td>ota_meta_t.checksum[32] 从未填充/验证</td><td>固件完整性无保障</td></tr>
<tr><td>18</td><td><span class="badge high">HIGH</span></td><td><code>wifi_ota.cpp:429-444</code></td><td>ESP32 OTA 完成→重启,无回滚验证</td><td>新固件若无法启动将变砖</td></tr>
<tr><td>19</td><td><span class="badge high">HIGH</span></td><td><code>spi_master.cpp:258</code></td><td>syncTransfer/recv 忽略 timeout_ms 参数</td><td>SPI事务可能无限等待</td></tr>
<tr><td>20</td><td><span class="badge high">HIGH</span></td><td><code>wifi_ota.cpp:599-629</code></td><td>esp_ota_write 失败后 break,未调用 esp_ota_abort</td><td>OTA分区处于不一致状态</td></tr>
<tr><td>21</td><td><span class="badge high">HIGH</span></td><td><code>ble_ota.cpp:439-444</code></td><td>BLE OTA 帧预留4字节CRC但减去计算不匹配</td><td>最后几个字节被误截断</td></tr>
<tr><td>22</td><td><span class="badge high">HIGH</span></td><td><code>spi_master.h</code></td><td>SPI仅4线,无硬件流控/就绪信号</td><td>高速传输时GD32来不及处理</td></tr>
</tbody>
</table>
</div>
<!-- Architecture Concerns -->
<div class="section">
<div class="section-title"><span class="icon">&#127959;</span> 架构级隐患</div>
<div class="arch-card">
<h4>&#9312; 全局状态泛滥,无封装无同步</h4>
<p>项目中有 <strong>5个静态全局变量</strong> (relayState, relaySeq, relayTotalPackets, gd32AppAlive, gd32Version) 和 <strong>大量public成员</strong> (WifiOta/BleOta所有状态),跨越3+个FreeRTOS任务和多个ISR/callback上下文访问,<strong>没有任何一个</strong>被mutex/atomic/critical section保护。这是系统性问题,不是个别bug。</p>
</div>
<div class="arch-card">
<h4>&#9313; 单通道SPI + 多任务无仲裁</h4>
<p>SpiMaster 单例是唯一的SPI通信通道,但没有任何仲裁机制。WiFi/BLE/健康监控/OTA中继四个功能模块各自直接调用,协议级操作(请求-响应对)不是原子的。需要从架构层面引入SPI总线仲裁器。</p>
</div>
<div class="arch-card">
<h4>&#9314; 安全架构不完整</h4>
<p>认证握手存在但密钥硬编码→形同虚设;固件校验字段存在但未填充→形同虚设;CORS开放→任何人可触发OTA。<strong>安全功能"看起来有"但实际上不提供保护</strong>,这种"安全假象"比没有安全功能更危险,因为它给了虚假的信心。</p>
</div>
</div>
<!-- Priority Fix Order -->
<div class="section">
<div class="section-title"><span class="icon">&#128736;</span> 优先修复顺序</div>
<table class="priority-table">
<thead>
<tr><th>优先级</th><th>修复项</th><th>工作量</th><th>紧急度</th><th>进度</th></tr>
</thead>
<tbody>
<tr>
<td class="p0">P0</td>
<td>添加 SPI 总线互斥锁</td>
<td>小</td>
<td>极高 — 生产环境必崩</td>
<td><div class="progress-bar"><div class="fill critical-fill" style="width:0%"></div></div></td>
</tr>
<tr>
<td class="p0">P0</td>
<td>替换硬编码密钥为安全存储</td>
<td>中</td>
<td>极高 — 安全漏洞</td>
<td><div class="progress-bar"><div class="fill critical-fill" style="width:0%"></div></div></td>
</tr>
<tr>
<td class="p0">P0</td>
<td>Flash 写入对齐检查</td>
<td>小</td>
<td>极高 — 数据损坏</td>
<td><div class="progress-bar"><div class="fill critical-fill" style="width:0%"></div></div></td>
</tr>
<tr>
<td class="p1">P1</td>
<td>添加全局 OTA 超时+中止机制</td>
<td>中</td>
<td>高 — 设备挂死</td>
<td><div class="progress-bar"><div class="fill high-fill" style="width:0%"></div></div></td>
</tr>
<tr>
<td class="p1">P1</td>
<td>ESP32 OTA 添加回滚验证</td>
<td>中</td>
<td>高 — 变砖风险</td>
<td><div class="progress-bar"><div class="fill high-fill" style="width:0%"></div></div></td>
</tr>
<tr>
<td class="p1">P1</td>
<td>替换 XOR 为 CRC-8/16 校验</td>
<td>小</td>
<td>高 — 固件损坏</td>
<td><div class="progress-bar"><div class="fill high-fill" style="width:0%"></div></div></td>
</tr>
<tr>
<td class="p1">P1</td>
<td>消除 0xA5 值碰撞</td>
<td>极小</td>
<td>高 — 协议混乱</td>
<td><div class="progress-bar"><div class="fill high-fill" style="width:0%"></div></div></td>
</tr>
<tr>
<td class="p2">P2</td>
<td>添加 OTA 单源互斥 (BLE/WiFi)</td>
<td>中</td>
<td>中高</td>
<td><div class="progress-bar"><div class="fill medium-fill" style="width:0%"></div></div></td>
</tr>
<tr>
<td class="p2">P2</td>
<td>为所有跨任务状态添加锁/atomic</td>
<td>大</td>
<td>中高</td>
<td><div class="progress-bar"><div class="fill medium-fill" style="width:0%"></div></div></td>
</tr>
<tr>
<td class="p2">P2</td>
<td>修复 WiFi 事件回调中的重连调用</td>
<td>小</td>
<td>中</td>
<td><div class="progress-bar"><div class="fill medium-fill" style="width:0%"></div></div></td>
</tr>
<tr>
<td class="p3">P3</td>
<td>修复大栈分配问题</td>
<td>小</td>
<td>中</td>
<td><div class="progress-bar"><div class="fill" style="width:0%;background:var(--text-muted);"></div></div></td>
</tr>
<tr>
<td class="p3">P3</td>
<td>限制 CORS 来源</td>
<td>小</td>
<td>中</td>
<td><div class="progress-bar"><div class="fill" style="width:0%;background:var(--text-muted);"></div></div></td>
</tr>
<tr>
<td class="p3">P3</td>
<td>实现 ota_meta_t 校验填充与验证</td>
<td>中</td>
<td>中</td>
<td><div class="progress-bar"><div class="fill" style="width:0%;background:var(--text-muted);"></div></div></td>
</tr>
</tbody>
</table>
</div>
<!-- Dismissed -->
<div class="section">
<div class="section-title"><span class="icon">&#9989;</span> 已驳回发现(误报)</div>
<table>
<thead>
<tr><th>原始严重度</th><th>问题描述</th><th>驳回原因</th></tr>
</thead>
<tbody>
<tr>
<td><span class="badge critical" style="text-decoration:line-through;opacity:0.5;">CRITICAL</span></td>
<td>SPI DMA缓冲区栈分配导致数据损坏/硬Fault</td>
<td>ESP-IDF 6.0 的 spi_master 驱动内置 <code>setup_dma_priv_buffer()</code> 函数自动检测并对齐DMA缓冲区,栈分配仅导致性能损失,不引发数据损坏</td>
</tr>
<tr>
<td><span class="badge high" style="text-decoration:line-through;opacity:0.5;">HIGH</span></td>
<td>TOCTOU: relayState 检查后 reset() 无锁</td>
<td>虽然模式存在,但当前代码流程中检查和重置在同一线程同步执行,实际竞态窗口极小,风险被高估</td>
</tr>
</tbody>
</table>
</div>
<!-- Footer -->
<footer class="report-footer">
<p>ESP32_COMM 高风险代码评审报告 · 由 4维并行审查 + 36项对抗性验证 生成 · 2026-06-13</p>
<p style="margin-top:0.5rem;">审查维度: 内存安全 · 并发/RTOS · OTA/固件完整性 · SPI通信/协议</p>
</footer>
</div>
<script>
function toggleFinding(id) {
const card = document.getElementById(id);
if (card) card.classList.toggle('open');
}
let allHighOpen = false;
function toggleAll() {
allHighOpen = !allHighOpen;
document.querySelectorAll('.finding-card').forEach(card => {
if (allHighOpen) card.classList.add('open');
else card.classList.remove('open');
});
}
</script>
</body>
</html>