- 启用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命令常量定义
971 lines
39 KiB
HTML
971 lines
39 KiB
HTML
<!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>📋 项目: ESP32_COMM (ESP32-C3 OTA Relay)</span>
|
||
<span>📅 日期: 2026-06-13</span>
|
||
<span>🔌 固件版本: v0.3.12</span>
|
||
<span>👥 审查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">📊</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">🚨</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);">✔ 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">⚠ 影响</div>
|
||
帧交错(A任务的请求配对B任务的响应)、GD32状态机混乱、固件传输数据损坏。生产环境高频CAN通信+OTA并发时必现。
|
||
</div>
|
||
<div class="fix-box">
|
||
<div class="fix-label">🔧 修复建议</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);">✔ 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">⚠ 影响</div>
|
||
攻击者可轻易伪造认证握手,向 GD32 注入恶意固件。GD32_CHIP_UID占位值意味着所有设备共享相同"身份"(如果运行时未正确覆盖)。
|
||
</div>
|
||
<div class="fix-box">
|
||
<div class="fix-label">🔧 修复建议</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);">✔ 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">⚠ 影响</div>
|
||
非对齐写入导致 ESP_ERR_INVALID_ARG 或触发 read-modify-write 异常,GD32固件最后一个chunk损坏或丢失。
|
||
</div>
|
||
<div class="fix-box">
|
||
<div class="fix-label">🔧 修复建议</div>
|
||
flashWriteChunk 内部做4字节对齐对齐(不足部分 read-modify-write),或在上层确保只传对齐参数。
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Other CRITICAL Findings -->
|
||
<div class="section">
|
||
<div class="section-title"><span class="icon">🔴</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);">✔ 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">🔧 修复建议</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);">✔ 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 < 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">🔧 修复建议</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);">✔ 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">🔧 修复建议</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);">✔ 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">🔧 修复建议</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);">✔ 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">🔧 修复建议</div>限制 CORS 为本地网络来源,或移除 CORS头并使用同源策略。</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- HIGH Findings -->
|
||
<div class="section">
|
||
<div class="section-title"><span class="icon">🟠</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">🏗</span> 架构级隐患</div>
|
||
|
||
<div class="arch-card">
|
||
<h4>① 全局状态泛滥,无封装无同步</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>② 单通道SPI + 多任务无仲裁</h4>
|
||
<p>SpiMaster 单例是唯一的SPI通信通道,但没有任何仲裁机制。WiFi/BLE/健康监控/OTA中继四个功能模块各自直接调用,协议级操作(请求-响应对)不是原子的。需要从架构层面引入SPI总线仲裁器。</p>
|
||
</div>
|
||
|
||
<div class="arch-card">
|
||
<h4>③ 安全架构不完整</h4>
|
||
<p>认证握手存在但密钥硬编码→形同虚设;固件校验字段存在但未填充→形同虚设;CORS开放→任何人可触发OTA。<strong>安全功能"看起来有"但实际上不提供保护</strong>,这种"安全假象"比没有安全功能更危险,因为它给了虚假的信心。</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Priority Fix Order -->
|
||
<div class="section">
|
||
<div class="section-title"><span class="icon">🛠</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">✅</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>
|