From 4c6bb66f7cb6b6d4fce6899d89ca4e295821f080 Mon Sep 17 00:00:00 2001 From: "CSBOOK\\44744" <447449914@qq.com> Date: Sun, 28 Jun 2026 18:57:02 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BA=86=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E9=A1=BA=E5=BA=8F=E5=92=8C=20storage=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E4=B8=8D=E8=A7=84=E8=8C=83=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../runtime/app_composition_default_runtime.c | 22 +- .../control_plane_connectivity_runtime.c | 7 +- components/platform/include/platform.h | 1 + .../src/runtime/platform_core_runtime.c | 11 + .../platform/src/runtime/platform_storage.c | 22 + tq-catn-deep-code-review.md | 756 ------------------ 6 files changed, 46 insertions(+), 773 deletions(-) delete mode 100644 tq-catn-deep-code-review.md diff --git a/components/app_composition/src/runtime/app_composition_default_runtime.c b/components/app_composition/src/runtime/app_composition_default_runtime.c index 53a5862..8b62ea1 100644 --- a/components/app_composition/src/runtime/app_composition_default_runtime.c +++ b/components/app_composition/src/runtime/app_composition_default_runtime.c @@ -26,6 +26,7 @@ static const char *TAG = "app_comp_boot"; typedef enum { APP_COMPOSITION_BOOT_STAGE_POWER_HOLD = 1, + APP_COMPOSITION_BOOT_STAGE_STORAGE_PREPARE, APP_COMPOSITION_BOOT_STAGE_AUDIO_INIT, APP_COMPOSITION_BOOT_STAGE_BOOT_SOUND, APP_COMPOSITION_BOOT_STAGE_AUDIO_READY, @@ -33,7 +34,6 @@ typedef enum { APP_COMPOSITION_BOOT_STAGE_LCD_RENDER_LOOP, APP_COMPOSITION_BOOT_STAGE_PLATFORM_DOMAIN, APP_COMPOSITION_BOOT_STAGE_TURNTABLE_CALIBRATION, - APP_COMPOSITION_BOOT_STAGE_STORAGE_PREPARE, APP_COMPOSITION_BOOT_STAGE_WIFI_BOOTSTRAP, APP_COMPOSITION_BOOT_STAGE_RUNTIME_WORKERS, APP_COMPOSITION_BOOT_STAGE_NETWORK_SERVICES, @@ -70,6 +70,15 @@ static esp_err_t app_composition_run_plan_a_boot_sequence(void) "power_hold", "already_asserted_by_platform_core"); + app_composition_log_stage(APP_COMPOSITION_BOOT_STAGE_STORAGE_PREPARE, + "storage_prepare", + "control_plane_prepare_runtime_storage"); + esp_err_t err = control_plane_prepare_runtime_storage(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Runtime storage prepare failed: %s", esp_err_to_name(err)); + return err; + } + app_composition_log_stage(APP_COMPOSITION_BOOT_STAGE_AUDIO_INIT, "audio_init", "boot_sound_policy_begin"); @@ -124,7 +133,7 @@ static esp_err_t app_composition_run_plan_a_boot_sequence(void) app_composition_log_stage(APP_COMPOSITION_BOOT_STAGE_PLATFORM_DOMAIN, "platform_domain", "control_plane_init"); - esp_err_t err = control_plane_init(); + err = control_plane_init(); if (err != ESP_OK) { ESP_LOGE(TAG, "control_plane_init failed: %s", esp_err_to_name(err)); return err; @@ -157,15 +166,6 @@ static esp_err_t app_composition_run_plan_a_boot_sequence(void) "disabled_by_config"); #endif - app_composition_log_stage(APP_COMPOSITION_BOOT_STAGE_STORAGE_PREPARE, - "storage_prepare", - "control_plane_prepare_runtime_storage"); - err = control_plane_prepare_runtime_storage(); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Runtime storage prepare failed: %s", esp_err_to_name(err)); - return err; - } - app_composition_log_stage(APP_COMPOSITION_BOOT_STAGE_WIFI_BOOTSTRAP, "wifi_bootstrap", "control_plane_bootstrap_connectivity"); diff --git a/components/control_plane/src/runtime/control_plane_connectivity_runtime.c b/components/control_plane/src/runtime/control_plane_connectivity_runtime.c index 45b15f2..ceffba5 100644 --- a/components/control_plane/src/runtime/control_plane_connectivity_runtime.c +++ b/components/control_plane/src/runtime/control_plane_connectivity_runtime.c @@ -184,13 +184,8 @@ esp_err_t control_plane_bootstrap_connectivity(const char *seed_ssid, (seed_ssid != NULL && seed_ssid[0] != '\0') ? seed_ssid : "", connect_timeout_ms); - esp_err_t err = control_plane_prepare_runtime_storage(); - if (err != ESP_OK) { - return err; - } - control_plane_register_wifi_behavior_hooks(); - err = domain_runtime_wifi_portal_init_defaults(seed_ssid, seed_password); + esp_err_t err = domain_runtime_wifi_portal_init_defaults(seed_ssid, seed_password); if (err != ESP_OK) { return err; } diff --git a/components/platform/include/platform.h b/components/platform/include/platform.h index 2c6308c..69e5ef4 100644 --- a/components/platform/include/platform.h +++ b/components/platform/include/platform.h @@ -132,6 +132,7 @@ typedef struct { esp_err_t platform_init(void); esp_err_t platform_prepare_storage(void); +bool platform_storage_is_ready(void); esp_err_t platform_mount_littlefs(void); esp_err_t platform_mount_logfs(void); void platform_read_device_identity(char *device_id, diff --git a/components/platform/src/runtime/platform_core_runtime.c b/components/platform/src/runtime/platform_core_runtime.c index b6294a9..7700676 100644 --- a/components/platform/src/runtime/platform_core_runtime.c +++ b/components/platform/src/runtime/platform_core_runtime.c @@ -18,6 +18,7 @@ static const char *TAG = "platform_core"; static bool s_initialized; +static bool s_storage_ready; static const char *k_identity_nvs_namespace = "ota"; static const char *const s_known_nvs_namespaces[] = { @@ -134,6 +135,10 @@ esp_err_t platform_init(void) esp_err_t platform_prepare_storage(void) { + if (s_storage_ready) { + return ESP_OK; + } + esp_err_t err = nvs_flash_init(); if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { ESP_LOGW(TAG, "Default NVS init failed with %s; erasing default NVS partition", @@ -145,11 +150,17 @@ esp_err_t platform_prepare_storage(void) err = nvs_flash_init(); } if (err == ESP_OK) { + s_storage_ready = true; platform_log_nvs_stats(); } return err; } +bool platform_storage_is_ready(void) +{ + return s_storage_ready; +} + void platform_read_device_identity(char *device_id, size_t device_id_len, char *serial_number, diff --git a/components/platform/src/runtime/platform_storage.c b/components/platform/src/runtime/platform_storage.c index c3ed0ff..56aafe4 100644 --- a/components/platform/src/runtime/platform_storage.c +++ b/components/platform/src/runtime/platform_storage.c @@ -6,11 +6,22 @@ #include "platform.h" +#include "esp_check.h" #include "esp_log.h" #include "nvs.h" static const char *TAG = "platform_storage"; +static esp_err_t ensure_storage_ready(const char *op) +{ + if (platform_storage_is_ready()) { + return ESP_OK; + } + + ESP_LOGW(TAG, "%s rejected because storage is not prepared", op != NULL ? op : "storage operation"); + return ESP_ERR_INVALID_STATE; +} + static esp_err_t open_storage_readonly(const char *ns, nvs_handle_t *out_handle) { if (ns == NULL || out_handle == NULL) { @@ -37,6 +48,7 @@ esp_err_t platform_storage_set_blob(const char *ns, const char *key, const void if (ns == NULL || key == NULL || data == NULL || len == 0U) { return ESP_ERR_INVALID_ARG; } + ESP_RETURN_ON_ERROR(ensure_storage_ready("set_blob"), TAG, "storage not ready"); nvs_handle_t handle = 0; esp_err_t err = nvs_open(ns, NVS_READWRITE, &handle); @@ -61,6 +73,7 @@ esp_err_t platform_storage_set_string(const char *ns, const char *key, const cha if (ns == NULL || key == NULL || value == NULL) { return ESP_ERR_INVALID_ARG; } + ESP_RETURN_ON_ERROR(ensure_storage_ready("set_string"), TAG, "storage not ready"); nvs_handle_t handle = 0; esp_err_t err = nvs_open(ns, NVS_READWRITE, &handle); @@ -85,6 +98,7 @@ esp_err_t platform_storage_set_u8(const char *ns, const char *key, uint8_t value if (ns == NULL || key == NULL) { return ESP_ERR_INVALID_ARG; } + ESP_RETURN_ON_ERROR(ensure_storage_ready("set_u8"), TAG, "storage not ready"); nvs_handle_t handle = 0; esp_err_t err = nvs_open(ns, NVS_READWRITE, &handle); @@ -109,6 +123,7 @@ esp_err_t platform_storage_set_i8(const char *ns, const char *key, int8_t value) if (ns == NULL || key == NULL) { return ESP_ERR_INVALID_ARG; } + ESP_RETURN_ON_ERROR(ensure_storage_ready("set_i8"), TAG, "storage not ready"); nvs_handle_t handle = 0; esp_err_t err = nvs_open(ns, NVS_READWRITE, &handle); @@ -140,6 +155,7 @@ esp_err_t platform_storage_get_blob(const char *ns, if (ns == NULL || key == NULL || out == NULL || out_len == 0U) { return ESP_ERR_INVALID_ARG; } + ESP_RETURN_ON_ERROR(ensure_storage_ready("get_blob"), TAG, "storage not ready"); nvs_handle_t handle = 0; esp_err_t err = open_storage_readonly(ns, &handle); @@ -171,6 +187,7 @@ esp_err_t platform_storage_get_u8(const char *ns, const char *key, uint8_t *out) if (ns == NULL || key == NULL || out == NULL) { return ESP_ERR_INVALID_ARG; } + ESP_RETURN_ON_ERROR(ensure_storage_ready("get_u8"), TAG, "storage not ready"); nvs_handle_t handle = 0; esp_err_t err = open_storage_readonly(ns, &handle); @@ -188,6 +205,7 @@ esp_err_t platform_storage_get_i8(const char *ns, const char *key, int8_t *out) if (ns == NULL || key == NULL || out == NULL) { return ESP_ERR_INVALID_ARG; } + ESP_RETURN_ON_ERROR(ensure_storage_ready("get_i8"), TAG, "storage not ready"); nvs_handle_t handle = 0; esp_err_t err = open_storage_readonly(ns, &handle); @@ -205,6 +223,7 @@ esp_err_t platform_storage_get_u32(const char *ns, const char *key, uint32_t *ou if (ns == NULL || key == NULL || out == NULL) { return ESP_ERR_INVALID_ARG; } + ESP_RETURN_ON_ERROR(ensure_storage_ready("get_u32"), TAG, "storage not ready"); nvs_handle_t handle = 0; esp_err_t err = open_storage_readonly(ns, &handle); @@ -222,6 +241,7 @@ esp_err_t platform_storage_get_u64(const char *ns, const char *key, uint64_t *ou if (ns == NULL || key == NULL || out == NULL) { return ESP_ERR_INVALID_ARG; } + ESP_RETURN_ON_ERROR(ensure_storage_ready("get_u64"), TAG, "storage not ready"); nvs_handle_t handle = 0; esp_err_t err = open_storage_readonly(ns, &handle); @@ -249,6 +269,7 @@ esp_err_t platform_storage_get_string(const char *ns, if (ns == NULL || key == NULL || out == NULL || out_len == 0U) { return ESP_ERR_INVALID_ARG; } + ESP_RETURN_ON_ERROR(ensure_storage_ready("get_string"), TAG, "storage not ready"); nvs_handle_t handle = 0; esp_err_t err = open_storage_readonly(ns, &handle); @@ -280,6 +301,7 @@ esp_err_t platform_storage_erase(const char *ns, const char *key) if (ns == NULL || key == NULL) { return ESP_ERR_INVALID_ARG; } + ESP_RETURN_ON_ERROR(ensure_storage_ready("erase"), TAG, "storage not ready"); nvs_handle_t handle = 0; esp_err_t err = nvs_open(ns, NVS_READWRITE, &handle); diff --git a/tq-catn-deep-code-review.md b/tq-catn-deep-code-review.md deleted file mode 100644 index ea89445..0000000 --- a/tq-catn-deep-code-review.md +++ /dev/null @@ -1,756 +0,0 @@ -# TQ_catN 工程深度代码审查报告(第二版) - -**审查范围**:control_plane / domain / platform / app_composition 全部源文件 -**审查维度**:内存安全、线程安全、状态机、资源管理、FreeRTOS 集成、架构合规 - ---- - -## 一、严重(Critical)— 可能导致崩溃或永久功能丧失 - -### C1. Watchdog 任务自删除导致多模态客户端永久失效 -- **文件**:`components/control_plane/src/multimodal/control_plane_multimodal_runtime.c` -- **函数**:`multimodal_watchdog_task()` / `stop_watchdog_task()` -- **行号**:356-373, 375-435 - -```c -// watchdog 任务检测到空闲超时后: -control_plane_multimodal_stop(); // 内部调用 stop_watchdog_task() - -// stop_watchdog_task: -if (!wait_task_exit(..., 1200)) { - TaskHandle_t task = get_multimodal_task_handle(&s_multimodal_watchdog_task); - if (task != NULL) { - vTaskDelete(task); // 删除自己,控制流不再返回 - } - set_multimodal_task_handle(&s_multimodal_watchdog_task, NULL); // 永远执行不到 -} -``` - -**问题**:watchdog 任务自己调用 `vTaskDelete(task)`(task 即自身),后续清理代码(将句柄置 NULL、重置 stop flag)永远执行不到。`s_multimodal_watchdog_task` 保持为悬空句柄,`s_multimodal_watchdog_stop_requested` 永久为 true。后续 `control_plane_start_multimodal_client()` 因句柄非 NULL 而拒绝重新创建 watchdog,**多模态客户端再也无法恢复**。 - -**修复**:watchdog 任务不应自删除。应通过 `xTaskNotifyGive` 通知外部任务执行删除,或使用 `vTaskDelete(NULL)` 后将清理工作移交给调用者。 - ---- - -### C2. Key Input ISR 对已释放队列的 Use-After-Free -- **文件**:`components/control_plane/src/runtime/control_plane_key_input.c` -- **函数**:`control_plane_key_gpio_isr_handler()` / `control_plane_stop_key_input_task()` -- **行号**:160-178, 493-515 - -```c -// ISR 中: -QueueHandle_t queue = s_gpio_irq_queue; -if (queue == NULL) { return; } -(void)xQueueSendFromISR(queue, &event, &higher_priority_task_woken); - -// 停止流程中: -vQueueDelete(s_gpio_irq_queue); // queue 被释放 -s_gpio_irq_queue = NULL; -``` - -**问题**:`gpio_isr_handler_remove()` 与 `vQueueDelete()` 之间无法保证中断不再触发。ISR 先将 `s_gpio_irq_queue` 读到局部变量 `queue`,随后主任务释放并删除队列,ISR 仍可能向已释放的 Queue 发送事件,导致内存损坏或系统崩溃。 - -**修复**:删除 queue 前应先 `gpio_intr_disable()` 所有引脚并确认 ISR 不再执行,或使用引用计数保护 queue 生命周期。 - ---- - -### C3. 音频下游队列释放后使用(UAF) -- **文件**:`components/platform/src/multimodal/audio_pipeline/audio_pipeline_runtime_impl.c` -- **函数**:`multimodal_audio_handle_downstream_opus()` -- **行号**:2056-2088 - -```c -if (!lock_downstream_playback_queue(pdMS_TO_TICKS(20))) { ... } -QueueHandle_t queue = ctx->downstream_playback_queue; -unlock_downstream_playback_queue(); - -// 解锁后在另一个任务可能销毁 queue 的情况下继续使用 -if (xQueueSend(queue, &packet, pdMS_TO_TICKS(20)) == pdTRUE) { ... } -``` - -**问题**:解锁后、`xQueueSend` 前,若 `stop_downstream_playback_task()` 删除了该 queue,`xQueueSend` 操作已释放内存,导致崩溃。 - -**修复**:`xQueueSend` 必须在持有 `s_downstream_playback_queue_mutex` 的临界区内完成。 - ---- - -### C4. 音频任务强制删除时 TaskHandle 失效(UAF) -- **文件**:`components/platform/src/multimodal/audio_pipeline/audio_pipeline_runtime_impl.c` -- **函数**:`multimodal_audio_deinit()` -- **行号**:929-942 - -```c -audio_snapshot_uplink_tasks(&s_audio, &capture_task, &send_task); -if (capture_task != NULL) { - delete_audio_task_with_caps(capture_task); // 可能已自删 - audio_set_uplink_capture_task(&s_audio, NULL); -} -``` - -**问题**:`uplink_capture_task` / `uplink_send_task` 在退出前会自删。获取句柄后任务可能已完成并自删;随后 `delete_audio_task_with_caps(capture_task)` 对已释放/不存在的 TCB 操作,在 FreeRTOS 中极易触发 `abort()` 或链表损坏。 - -**修复**:使用任务通知 + 超时等待任务自行退出,禁止在外部持有句柄时强制删除。 - ---- - -### C5. 校准服务未持有旋转互斥锁 — 步进电机资源冲突 -- **文件**:`components/domain/src/calibration/domain_calibration_service.c` -- **函数**:`domain_calibrate_with_timeout` -- **行号**:113-231 - -**问题**:校准全程持有 `s_state_mutex`,但**从未**获取 `s_rotation_mutex`。`domain_direction_service.c` 中的 `domain_rotate_to_angle_with_timeout` 则先获取 `s_rotation_mutex` 再获取 `s_state_mutex`。当校准时,其他任务可成功获取 `s_rotation_mutex`,然后在 `s_state_mutex` 上阻塞;一旦校准释放 `s_state_mutex`,该任务立即进入步进电机调用,与校准的下一步电机操作**并发冲突**,导致机械位置失步或硬件损坏。 - -**修复**:校准函数也应按相同顺序获取 `s_rotation_mutex` 和 `s_state_mutex`。 - ---- - -### C6. 角度归一化函数对无穷值死循环 -- **文件**:`components/domain/src/direction/domain_angle_math.c` -- **函数**:`domain_angle_normalize` -- **行号**:9-18 - -```c -while (angle > 180.0f) { - angle -= 360.0f; // 对 +inf 永远为 true,死循环 -} -while (angle <= -180.0f) { - angle += 360.0f; // 对 -inf 永远为 true,死循环 -} -``` - -**问题**:若传入 `INFINITY`、`-INFINITY` 或 `NAN`,`while` 循环永不退出,导致看门狗复位或任务挂死。 - -**修复**:增加前置检查 `if (!isfinite(angle)) return 0.0f;` 或使用 `fmodf` 替代循环。 - ---- - -### C7. WS 发送锁释放后立即删除 -- **文件**:`components/platform/src/multimodal/transport_ws/transport_ws_runtime_impl.c` -- **函数**:`multimodal_ws_stop_sync_until()` -- **行号**:417-423 - -```c -if (send_lock_held && s_ws_send_lock != NULL) { - xSemaphoreGive(s_ws_send_lock); -} -if (send_lock_held && s_ws_send_lock != NULL) { - vSemaphoreDelete(s_ws_send_lock); // 另一任务可能刚被唤醒并持有该锁 - s_ws_send_lock = NULL; -} -``` - -**问题**:`xSemaphoreGive` 后、`vSemaphoreDelete` 前,其他正在等待 `s_ws_send_lock` 的任务可能获得该信号量;随后信号量被删除,导致等待任务使用已释放对象。 - -**修复**:在仍持有锁的状态下直接删除(或改用静态信号量/互斥量避免动态删除)。 - ---- - -### C8. 下游空闲静音 Timer 回调竞态删除 -- **文件**:`components/platform/src/multimodal/audio_pipeline/audio_pipeline_runtime_impl.c` -- **函数**:`stop_downstream_idle_mute_task()` / `downstream_idle_mute_timer_cb()` -- **行号**:1479-1506, 1301-1316 - -```c -esp_timer_stop(timer); -esp_timer_delete(timer); // 若 timer 回调正在另一核执行,删除不安全 -``` - -**问题**:`esp_timer_delete()` **不等待**正在执行的回调结束。若回调正在运行,`timer` 及其 `arg` 被释放后,回调中继续访问 `ctx` 导致 UAF。 - -**修复**:删除前设置原子标志让回调自行短路返回,并确保无活跃回调。 - ---- - -### C9. `multimodal_ws_is_connected()` 对已销毁 client 句柄操作 -- **文件**:`components/platform/src/multimodal/transport_ws/transport_ws.c` -- **函数**:`multimodal_ws_is_connected()` -- **行号**:458-470 - -```c -ws_client_state_snapshot(&client, &connected, &stop_requested); -... -return esp_websocket_client_is_connected(client); // client 可能已被销毁 -``` - -**问题**:退出临界区后调用 `esp_websocket_client_is_connected(client)`,在此期间 `multimodal_ws_stop_sync_until()` 可能已 `destroy(client)` 并将句柄置 NULL。对无效/已释放句柄调用 esp_websocket API 属于 UAF。 - -**修复**:将 `is_connected` 判断完全放在 `s_client_state_lock` 临界区内,或增加引用计数。 - ---- - -### C10. LAN HTTP Server 强制删除任务时 IO Buffer UAF -- **文件**:`components/platform/src/runtime/platform_lan_http_server.c` -- **函数**:`platform_lan_http_server_stop()` / `server_task()` -- **行号**:822-839, 594-727 - -**问题**:强制删除 `server_task` 后,立即调用 `server_release_io_buffers()` 释放 buffer。若被删任务恰好在 `handle_client()` 执行路径中(已读取到局部指针但尚未返回),将继续访问已释放内存。 - -**修复**:在停止任务前通过 `shutdown(s_client_fd, SHUT_RDWR)` 中断阻塞的 IO,等待任务自然退出;仅在超时后才强制删除,且删除后延迟释放 buffer。 - ---- - -## 二、高(High)— 可能导致竞态条件、数据损坏或功能异常 - -### H1. `s_started` 与 `s_transport_available` 不一致导致永久连接失败 -- **文件**:`components/control_plane/src/multimodal/control_plane_multimodal_session_service.c` -- **行号**:1533-1576 -- **问题**:`control_plane_multimodal_start()` 先设置 `s_started = true`,再启动 transport。如果 transport 启动后立即断连,`s_started` 仍为 true,下次调用 `start()` 时幂等检查会跳过重连。 -- **修复**:`s_started` 应在 `TRANSPORT_CONNECTED` 事件回调时才设置。 - ---- - -### H2. Session Service 互斥锁创建非线程安全 -- **文件**:`components/control_plane/src/multimodal/control_plane_multimodal_session_service.c` -- **函数**:`ensure_state_mutex()` -- **行号**:283-298 - -```c -if (s_state_mutex != NULL) { return true; } -s_state_mutex = xSemaphoreCreateMutex(); // 非原子 -``` - -**问题**:若两个线程首次并发进入,可能创建两个 mutex,第二个覆盖第一个导致泄漏;若已有线程获取了第一个 mutex,后续代码使用第二个 mutex,导致对共享状态无保护。 - -**修复**:使用 `portENTER_CRITICAL` 保护创建路径,或采用静态互斥量。 - ---- - -### H3. Device Auth 状态加载失败后被忽略继续初始化 -- **文件**:`components/control_plane/src/multimodal/control_plane_multimodal_device_auth.c` -- **函数**:`control_plane_multimodal_device_auth_init()` -- **行号**:1856-1888 - -```c -esp_err_t err = load_state_from_store_locked(); -if (err != ESP_OK) { - ESP_LOGW(TAG, "Auth store state unavailable, continue with defaults"); -} -// ... 继续初始化,标记 s_auth.initialized = true -``` - -**问题**:当 `load_state_from_store_locked()` 返回 `ESP_ERR_INVALID_SIZE`(持久化数据损坏)时,仅打印 warning 即继续。此时 `s_auth.jwt` 等状态可能包含损坏的残留值,后续 `jwt_still_valid_locked()` 可能基于损坏的过期时间做出错误判断。 - -**修复**:数据损坏时应清除状态为默认值,而非保留残留值。 - ---- - -### H4. PTT 状态机基于过时 Snapshot 的 TOCTOU 竞态 -- **文件**:`components/control_plane/src/runtime/control_plane_runtime_workers.c` -- **函数**:`control_plane_handle_ptt_teardown()` -- **行号**:1193-1243 - -```c -ptt_runtime_t ptt = control_plane_ptt_snapshot(); -if (ptt.state == PTT_STATE_TEARDOWN_LOCAL) { - control_plane_multimodal_stop_uplink(); - control_plane_ptt_set_state(PTT_STATE_TEARDOWN_REMOTE, "uplink_stopped"); - ptt = control_plane_ptt_snapshot(); -} -``` - -**问题**:第一次 snapshot 后判断为 `TEARDOWN_LOCAL`,随即停止 uplink 并推进到 `TEARDOWN_REMOTE`。但若在此期间用户已释放 PTT,`control_plane_set_ptt_active(false)` 已将状态重置为 `IDLE`。随后本函数仍强行将其推进到 `TEARDOWN_REMOTE` 并发送 `StopSpeech`,导致客户端与服务端状态不一致。 - -**修复**:在状态推进前检查 `generation` 是否仍有效,或改用临界区保护整个 teardown 流程。 - ---- - -### H5. PTT foreground_attempts 计数与 Snapshot 不一致 -- **文件**:`components/control_plane/src/runtime/control_plane_runtime_workers.c` -- **函数**:`control_plane_drive_ptt_state_machine()` -- **行号**:1288-1303 - -```c -bool started = control_plane_try_start_multimodal_from_state_machine(...); -portENTER_CRITICAL(&s_runtime_lock); -s_ptt_runtime.foreground_attempts++; -portEXIT_CRITICAL(&s_runtime_lock); -if (!started && - ptt.foreground_attempts + 1U >= CONTROL_PLANE_PTT_FOREGROUND_RECONNECT_MAX_ATTEMPTS) { - control_plane_ptt_set_state(PTT_STATE_DENIED, ...); -} -``` - -**问题**:判断条件使用 snapshot 中的旧 `ptt.foreground_attempts`,而实际值已在临界区内递增。若在此期间有其他代码重置了 `foreground_attempts`,此处仍依据旧值做出 `DENIED` 决策。 - -**修复**:在临界区内同时读取和判断 `foreground_attempts`。 - ---- - -### H6. WS 分片重组全局缓冲区无互斥保护 -- **文件**:`components/platform/src/multimodal/transport_ws/transport_ws_workers_impl.c` -- **函数**:`process_text_fragment()` / `process_binary_fragment()` - -**问题**:`s_text_reassembly_buf` / `s_binary_reassembly_buf` 在 queue 创建失败的 fallback 路径下,`handle_ws_msg_direct()` 会在 WebSocket client 事件任务上下文中直接调用这些函数。同时 `multimodal_ws_stop()` 会在调用者任务中执行 `reset_*_reassembly()`。两者无互斥,构成数据竞态。 - -**修复**:对重组缓冲区操作加锁,或确保 fallback 路径也串行化到同一任务。 - ---- - -### H7. `wait_worker_task_exit()` 对全局句柄无原子/屏障读取 -- **文件**:`components/platform/src/multimodal/transport_ws/transport_ws_workers_impl.c` -- **函数**:`wait_worker_task_exit()` -- **行号**:472-489 - -```c -while (*task_handle != NULL && (xTaskGetTickCount() - start_tick) < timeout_ticks) { - vTaskDelay(pdMS_TO_TICKS(20)); -} -``` - -**问题**:`s_ws_dispatch_task` 由被等待任务在退出时置 NULL,但本函数直接解引用读取,无原子操作或内存屏障。在 ESP32 多核缓存下,可能永远读到旧值导致超时后误判为未退出,进而执行对已自删任务的二次删除。 - -**修复**:使用 `volatile TaskHandle_t` + 适当内存屏障,或通过通知机制同步。 - ---- - -### H8. LCD 渲染任务强制删除导致 Mutex 永久占用 -- **文件**:`components/platform/src/lcd_direct.c` -- **函数**:`lcd_direct_stop_render_loop()` -- **行号**:691-738 - -```c -if (task != xTaskGetCurrentTaskHandle()) { - vTaskDelete(task); // 若 task 正持有 s_lock,Mutex 将永久锁定 - ... -} -``` - -**问题**:渲染循环中会 `xSemaphoreTake(s_lock, ...)`。若任务在持锁期间被强制删除,FreeRTOS 不会自动释放 Mutex,导致后续所有尝试获取 `s_lock` 的操作永远超时。 - -**修复**:避免 `vTaskDelete` 强制终止;使用通知 + 轮询标志让任务自行退出。 - ---- - -### H9. 步进电机旋转长期占用 CPU 不释放 -- **文件**:`components/platform/src/platform_stepper_driver.c` -- **函数**:`rotate_with_accel()` -- **行号**:275-303 - -```c -for (int i = 0; i < steps; i++) { - stepper_advance_phase(clockwise); - precise_delay_us(current_delay_us); // busy-wait -} -``` - -**问题**:以最快 600 us/步、最大 330° ≈ 3784 步计算,最长可连续自旋约 **2.3 秒** 不执行 `taskYIELD`。在此期间其他任务(包括音频采集、WS 保活)被饿死,可能导致音频欠采样、WS 断开。 - -**修复**:在步进循环中每隔一定步数调用 `taskYIELD()`,或将步进控制移到硬件定时器/LEDC。 - ---- - -### H10. Factory CLI 强制删除已自行退出的任务 -- **文件**:`components/control_plane/src/runtime/control_plane_factory_cli.c` -- **函数**:`control_plane_factory_cli_stop()` -- **行号**:526-550 - -**问题**:若 worker 在循环等待期间已自行调用 `vTaskDelete(NULL)`,主线程第二次检查时理论上应返回。但存在竞态窗口:worker 尚未执行 `vTaskDelete(NULL)` 时,主线程仍得到非 NULL 句柄,随后执行 `vTaskDelete(worker_task)` 操作一个已结束任务的 TCB,属于未定义行为。 - -**修复**:使用 `xTaskNotifyWait` 或事件组等待 worker 明确报告退出,而非轮询 + 强制删除。 - ---- - -### H11. System Event / Key Input 任务直接强制删除 -- **文件**:`components/control_plane/src/runtime/control_plane_system_events.c` -- **函数**:`control_plane_stop_system_event_task()` -- **行号**:257-276 - -```c -if (s_system_event_task != NULL) { - vTaskDelete(s_system_event_task); // 任务可能正在持有锁或访问共享资源 - s_system_event_task = NULL; -} -``` - -**问题**:两处均在超时后直接 `vTaskDelete`。FreeRTOS 不保证被删任务持有的互斥锁会被释放。若任务在 `xQueueReceive` 临界区或持有某把锁时被删,将导致后续死锁或资源泄漏。 - -**修复**:使用任务通知 + 超时等待任务自行退出。 - ---- - -### H12. 浮点精度导致方向查询失败 -- **文件**:`components/domain/src/direction/domain_direction_catalog.c` -- **函数**:`domain_direction_catalog_find_by_angle` -- **行号**:149 - -```c -if (entry->angle == angle) { // 精确浮点比较 -``` - -**问题**:`domain_get_current_direction_index()` 将经多次浮点运算后的 `s_current_angle` 传入此函数。浮点运算会引入微小误差(如 `44.999996f` 与 `45.0f`),导致精确相等比较失败。 - -**修复**:引入 epsilon 容差比较,如 `fabsf(entry->angle - angle) < 0.5f`。 - ---- - -### H13. HTTP 响应 JSON 打印失败时缓冲区残留脏数据 -- **文件**:`components/domain/src/fortune/domain_user_config_server.c` -- **函数**:`response_json_object` -- **行号**:130-136 - -```c -if (!cJSON_PrintPreallocated(root, (char *)response->body, - (int)response->body_capacity, false)) { - response->body_len = 0U; - return ESP_ERR_INVALID_SIZE; -} -``` - -**问题**:`cJSON_PrintPreallocated` 在缓冲区不足时可能**写入部分 JSON** 后返回 `false`。此时函数仅置 `body_len = 0` 并返回错误,但 `response->body` 中残留不完整 JSON。若上层调用者忽略返回值直接发送,客户端会收到截断/损坏的 JSON。 - -**修复**:失败前显式将 `response->body[0] = '\0'`。 - ---- - -### H14. 旋转期间状态锁释放导致脏读及角度不一致 -- **文件**:`components/domain/src/direction/domain_direction_service.c` -- **函数**:`domain_rotate_to_angle_with_timeout` -- **行号**:302-364 - -```c -domain_direction_unlock_state(); // 释放 state mutex -// ... 电机运动期间 ... -esp_err_t motor_err = domain_runtime_stepper_rotate_angle_with_accel_timeout(...); -``` - -**问题**:释放 `s_state_mutex` 后、电机运动完成前,其他任务可获取 `s_state_mutex` 并读取 `s_current_angle`(此时仍是旧值),产生不一致快照。此外,若电机实际运动与指令存在偏差,`s_current_angle` 在运动后仅按理论值更新。 - -**修复**:旋转完成后重新获取锁更新角度,或记录旋转目标值供读取者使用。 - ---- - -### H15. HTTP 请求路径/方法 NULL/长度未校验 -- **文件**:`components/domain/src/fortune/domain_user_config_server.c` -- **函数**:`user_config_http_handler` -- **行号**:338-364 - -```c -if (strcmp(request->path, "/") == 0) { // request->path 可能为 NULL -``` - -**问题**:对 `request->path` 直接使用 `strcmp`,未校验 `NULL` 或确保 NUL 终止;`request->method` 也未校验是否为合法枚举值。若平台层传递畸形请求,将导致空指针解引用或越界读取。 - -**修复**:增加 `request->path != NULL` 和 `request->method` 范围校验。 - ---- - -### H16. 启动顺序错误 — 平台总初始化晚于音频/LCD -- **文件**:`components/app_composition/src/runtime/app_composition_default_runtime.c` -- **函数**:`app_composition_run_plan_a_boot_sequence` -- **行号**:67-194 - -**问题**:启动流程中 `control_plane_play_boot_sound()` 和 `control_plane_init_lcd_device()` 在 `control_plane_init()`(内部调用 `domain_runtime_platform_init()` → `platform_init()`)之前执行。若音频或 LCD 的 platform 实现依赖 `platform_init()` 预先配置的 GPIO/DMA/时钟等资源,前 3 步将在平台未就绪时操作硬件。 - -**修复**:将 `platform_init()` 或等价的底层初始化移到 boot sequence 的最前面。 - ---- - -## 三、中(Medium)— 设计缺陷、资源泄漏或边界条件 - -### M1. `local_responding_event_worker` 队列永久泄漏 -- **文件**:`components/control_plane/src/multimodal/control_plane_multimodal_session_service.c` -- **函数**:`stop_local_responding_event_worker()` -- **行号**:612-654 - -```c -if (s_local_responding_event_queue != NULL) { - xQueueReset(s_local_responding_event_queue); // 仅 reset,未删除 -} -``` - -**问题**:队列在 stop 时仅被 `xQueueReset`,从未 `vQueueDelete`。反复启停 multimodal client 将导致队列句柄永久累积。 - -**修复**:`vQueueDelete(s_local_responding_event_queue)` 并将指针置 NULL。 - ---- - -### M2. `send_start` 发送成功后状态被 mark_transport_unavailable 干扰 -- **文件**:`components/control_plane/src/multimodal/control_plane_multimodal_session_service.c` -- **函数**:`control_plane_multimodal_send_start()` -- **行号**:1843-1866 - -```c -err = domain_runtime_multimodal_transport_send_text("start", json); -// ... -if (!s_start_in_progress || strcmp(s_pending_start_task_id, task_id) != 0) { - return ESP_ERR_INVALID_STATE; // Start 已发到服务端,但本地拒绝记录 -} -``` - -**问题**:`send_text` 成功返回后、再次加锁前,若 transport callback 线程调用 `mark_transport_unavailable()` 清空了 `s_start_in_progress`,则函数返回 `ESP_ERR_INVALID_STATE`。此时服务端已创建 task,客户端却无记录,形成 orphaned server task。 - -**修复**:在发送前加锁获取 task_id,发送完成后在锁内更新状态。 - ---- - -### M3. `process_text_json` 中 `is_finished` 跨事件污染 -- **文件**:`components/control_plane/src/multimodal/control_plane_multimodal_session_service.c` -- **函数**:`process_text_json()` -- **行号**:1218, 1412-1423 - -```c -bool is_finished = cJSON_IsBool(finished) ? cJSON_IsTrue(finished) : false; -// ... -if (text_len != s_last_asr_text_len || is_finished) { ... } -``` - -**问题**:`is_finished` 在函数入口处基于 `output.finished` 计算一次。`SpeechContent` 事件(ASR 中间结果)通常不带 `finished` 字段,此时 `is_finished` 保留上一个事件(可能是 TTS `RespondingContent`)的值。若上一个事件 `finished=true`,当前 ASR 文本会被错误标记为 final。 - -**修复**:每个事件分支独立计算 `is_finished`,或使用事件类型判断而非共享变量。 - ---- - -### M4. 生日档案日期校验宽松 — 接受 2 月 31 日等非法日期 -- **文件**:`components/domain/src/fortune/domain_fortune_store.c` -- **函数**:`domain_birth_profile_is_valid` -- **行号**:36-46 - -```c -profile->day >= 1 && profile->day <= 31 // 2月也可为31 -``` - -**问题**:存储层接受所有月份的 day<=31。若数据绕过 Web 校验直接写入 NVS,非法日期将通过校验。 - -**修复**:增加按月份的日期上限校验,或统一使用 `is_valid_date` 函数。 - ---- - -### M5. HTTP 请求体无上限 — DoS/内存耗尽风险 -- **文件**:`components/domain/src/fortune/domain_user_config_server.c` -- **函数**:`parse_profile` -- **行号**:198-212 - -```c -cJSON *root = cJSON_ParseWithLength((const char *)body, body_len); -``` - -**问题**:`body_len` 未设上限直接传入 `cJSON_ParseWithLength`。若攻击者发送数 MB 的畸形 JSON,将耗尽堆内存。 - -**修复**:在 platform HTTP server 层或 domain 层增加 `body_len <= MAX_JSON_BODY_SIZE` 校验。 - ---- - -### M6. 启动失败时资源未清理 -- **文件**:`components/app_composition/src/runtime/app_composition_default_runtime.c` -- **函数**:`app_composition_run_plan_a_boot_sequence` -- **行号**:128-131, 164-167, 183-187 - -**问题**:若 `control_plane_init()`、`control_plane_prepare_runtime_storage()` 或 `control_plane_start_runtime_workers()` 失败,函数直接返回错误。但此前已初始化的音频子系统、LCD 设备、LCD 渲染任务等**没有任何 stop/deinit** 路径。 - -**修复**:在 boot sequence 中增加失败时的逆向清理逻辑(reverse cleanup)。 - ---- - -### M7. 旧版认证迁移写失败导致反复重试迁移 -- **文件**:`components/domain/src/runtime/domain_runtime_adapters.c` -- **函数**:`domain_runtime_auth_store_load_record` -- **行号**:1005-1018 - -```c -esp_err_t save_err = platform_storage_set_blob(ns, key, out, actual); -if (save_err != ESP_OK) { - ESP_LOGW(TAG, "Persist migrated auth record failed"); -} else { - ESP_LOGI(TAG, "Migrated legacy auth record"); -} -return ESP_OK; // 无论 save_err 如何都返回 OK -``` - -**问题**:`save_err != ESP_OK` 时仅打印警告,仍返回 `ESP_OK`。下次加载同一条记录时,将再次执行迁移流程,造成反复读写 NVS。 - -**修复**:迁移写失败时应返回错误,或设置标志避免重复迁移。 - ---- - -### M8. 控制层直接调用 ESP-IDF 堆 API — 架构层旁路 -- **文件**:`components/control_plane/src/runtime/control_plane_fortune_service.c` -- **函数**:`fortune_psram_first_calloc` -- **行号**:46-60 - -```c -void *ptr = heap_caps_calloc(count, size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); -``` - -**问题**:`control_plane` 直接调用 `heap_caps_calloc` 和包含 `soc/soc_caps.h`。根据 `AGENTS.md` 的四层依赖约束,control_plane 不应直接调用 ESP-IDF 内部 API,而应通过 `platform` facade。 - -**修复**:将 PSRAM 分配封装为 `platform_psram_first_calloc()`,由 platform 层暴露给上层。 - ---- - -### M9. `wifi_portal_connect_sta` 持锁忙等 10 秒 -- **文件**:`components/platform/src/wifi_portal_bridge.cpp` -- **函数**:`wifi_portal_connect_sta` -- **行号**:280-291 - -```c -while (esp_timer_get_time() < deadline_us) { - if (manager.IsConnected()) { return ESP_OK; } - vTaskDelay(pdMS_TO_TICKS(100)); -} -``` - -**问题**:在持有 `s_portal_mutex` 递归锁的情况下进行最多 10 秒的轮询。期间所有需要该锁的操作(保存凭证、清除凭证)全部被阻塞。 - -**修复**:释放锁后轮询,或使用事件组/信号量等待连接完成。 - ---- - -### M10. 旋转互斥锁为非递归锁 -- **文件**:`components/domain/src/direction/domain_direction_service.c` -- **函数**:`ensure_rotation_mutex` -- **行号**:99-125 - -```c -SemaphoreHandle_t created_mutex = xSemaphoreCreateMutex(); // 非递归 -``` - -**问题**:`s_rotation_mutex` 是普通互斥锁(非递归)。若同一任务因回调或嵌套调用再次进入 `domain_rotate_to_angle_with_timeout`,将在 `domain_direction_lock_rotation` 死锁。 - -**修复**:改为 `xSemaphoreCreateRecursiveMutex()`。 - ---- - -### M11. `platform_storage_get_blob` / `get_string` 栈上 1.6KB 结构体 -- **文件**:`components/domain/src/runtime/domain_runtime_adapters.c` -- **行号**:778, 870 - -```c -typedef struct { char jwt[1536]; ... } domain_auth_state_legacy_blob_t; -domain_auth_state_legacy_blob_t state = {0}; // 约1.6KB 栈分配 -``` - -**问题**:该结构体分配在栈上,在栈空间受限的任务中可能导致栈溢出。 - -**修复**:改为 PSRAM 动态分配。 - ---- - -### M12. `control_plane_set_ptt_active()` 临界区内逻辑偏多 -- **文件**:`components/control_plane/src/runtime/control_plane_runtime_workers.c` -- **函数**:`control_plane_set_ptt_active()` -- **行号**:293-335 - -**问题**:`portENTER_CRITICAL(&s_runtime_lock)` 内包含大量字段赋值与条件分支。在高频按键场景下会延长另一核心获取自旋锁的等待时间,影响中断延迟。 - -**修复**:将非必要字段移到临界区外,或使用轻量级原子操作替代。 - ---- - -## 四、低(Low)— 代码质量、可维护性和优化建议 - -### L1. `s_server_success_prompt_played` 永不清零 -- **文件**:`components/control_plane/src/multimodal/control_plane_multimodal_runtime.c` -- **行号**:58 -- **问题**:断连重连后成功提示音不再播放。确认是否为有意设计。 - -### L2. 多处 `cJSON_free` 与 `free` / `heap_caps_free` 混用 -- **文件**:`control_plane_multimodal_device_auth.c`、`control_plane_multimodal_auth_service.c` -- **问题**:`cJSON_PrintUnformatted` 的结果使用 `cJSON_free()` 释放,而 PSRAM 分配的内存有时使用 `free()`。ESP-IDF 默认下 `cJSON_free` 等同于 `free`,但一旦 cJSON 库配置为自定义分配器,将导致分配/释放不匹配。 - -### L3. `control_plane_multimodal_generate_uuid()` 使用非加密安全随机数 -- **文件**:`components/control_plane/src/multimodal/control_plane_multimodal_protocol.c` -- **行号**:102-124 -- **问题**:`esp_random()` 基于硬件 RNG,但在某些启动阶段熵可能不足。对于 task_id 等协议标识符虽无安全要求,但若未来用于安全敏感场景需替换。 - -### L4. `audio_debug_dump_impl.c` UDP socket 无错误检查 -- **文件**:`components/platform/src/multimodal/audio_pipeline/audio_debug_dump_impl.c` -- **行号**:95-127 -- **问题**:`sendto` 返回值被忽略。若网络异常导致持续失败,无法及时发现 socket 异常。 - -### L5. `domain_runtime_post_json` 的 auth header 应改用栈缓冲区 -- **文件**:`components/domain/src/runtime/domain_runtime_adapters.c` -- **行号**:477-511 -- **问题**:每次调用都 `malloc` + `snprintf` + `free` 一个 Authorization header 字符串。token 最大 256 字节 + "Bearer " 前缀,总计约 264 字节,完全可以在栈上分配。 - -### L6. `control_plane_delete_task(NULL, task_with_caps)` 语义不清晰 -- **文件**:`components/control_plane/src/runtime/control_plane_runtime_workers.c` -- **行号**:631 -- **问题**:`vTaskDelete(NULL)` 会删除当前任务,但代码意图极不清晰。应加注释说明。 - -### L7. `control_plane_events.h` 的 event union 只有 key 成员 -- **文件**:`components/control_plane/internal/control_plane_events.h` -- **行号**:50-56 -- **问题**:定义了 WIFI、MULTIMODAL、LCD 等多种事件族,但 union 中只有 `key` 成员。若不打算通过事件队列传递其他事件,建议简化并加注释。 - -### L8. `platform_storage.c` NVS handle 频繁 open/close -- **文件**:`components/platform/src/runtime/platform_storage.c` -- **问题**:每次读写操作都调用 `nvs_open` + `nvs_close`,有固定开销。对高频 namespace 考虑缓存 handle。 - -### L9. `transport_ws.c` `s_headers[384]` 缓冲区偏小 -- **文件**:`components/platform/src/multimodal/transport_ws/transport_ws.c` -- **行号**:158-159 -- **问题**:Bearer token 255 字符 + workspace ID + header 格式可能超过 384 字节。建议增大至 512 字节。 - -### L10. `main.c` 忽略启动返回值 -- **文件**:`main/main.c` -- **行号**:9-11 -- **问题**:`(void)app_composition_start_default_runtime();` 启动失败被静默忽略。 - ---- - -## 五、修复优先级建议 - -### P0 — 必须立即修复(崩溃/永久功能丧失风险) - -| 编号 | 问题 | 文件 | -|------|------|------| -| C1 | Watchdog 自删除导致多模态永久失效 | multimodal_runtime.c | -| C2 | Key Input ISR UAF | control_plane_key_input.c | -| C3 | 音频下游队列 UAF | audio_pipeline_runtime_impl.c | -| C4 | 音频任务强制删除 UAF | audio_pipeline_runtime_impl.c | -| C5 | 校准缺旋转锁 | domain_calibration_service.c | -| C6 | 角度归一化死循环 | domain_angle_math.c | -| C7 | WS 发送锁删除竞态 | transport_ws_runtime_impl.c | -| C8 | Timer 回调竞态删除 | audio_pipeline_runtime_impl.c | -| C9 | `is_connected` 对已销毁 client 操作 | transport_ws.c | -| C10 | LAN HTTP Server UAF | platform_lan_http_server.c | -| H1 | `s_started` 与 transport 状态不一致 | session_service.c | -| H2 | Session mutex 创建非线程安全 | session_service.c | - -### P1 — 应尽快修复(竞态/数据损坏/功能异常) - -| 编号 | 问题 | 文件 | -|------|------|------| -| H3 | Device Auth 损坏数据继续初始化 | device_auth.c | -| H4 | PTT teardown TOCTOU | runtime_workers.c | -| H5 | PTT foreground_attempts 不一致 | runtime_workers.c | -| H6 | WS 重组缓冲区无互斥 | transport_ws_workers_impl.c | -| H7 | wait_worker_task_exit 无原子读取 | transport_ws_workers_impl.c | -| H8 | LCD 渲染任务强制删除锁永久占用 | lcd_direct.c | -| H9 | 步进电机旋转饿死其他任务 | platform_stepper_driver.c | -| H10-H11 | 多处强制删除任务 | factory_cli.c / system_events.c | -| H12 | 浮点精确比较失败 | domain_direction_catalog.c | -| H13 | JSON 打印脏数据 | domain_user_config_server.c | -| H16 | 启动顺序错误 | app_composition_default_runtime.c | - -### P2 — 建议优化(资源泄漏、设计缺陷、边界条件) - -| 编号 | 问题 | 文件 | -|------|------|------| -| M1 | local_responding_event 队列泄漏 | session_service.c | -| M2 | send_start orphaned task | session_service.c | -| M3 | is_finished 跨事件污染 | session_service.c | -| M4-M5 | 日期校验/HTTP body 上限 | fortune_store.c / user_config_server.c | -| M6 | 启动失败资源未清理 | app_composition_default_runtime.c | -| M7 | 认证迁移反复重试 | domain_runtime_adapters.c | -| M8 | 架构层旁路 | control_plane_fortune_service.c | -| M9 | Wi-Fi 连接持锁忙等 | wifi_portal_bridge.cpp | -| M10 | 旋转锁非递归 | domain_direction_service.c | - -### P3 — 可后续处理(代码质量、性能、文档) - -L1-L10 及其他低优先级项。 - ---- - -## 六、总结 - -本次深度审查在三个层面共发现 **10 个 Critical、16 个 High、12 个 Medium、10 个 Low** 级别问题。 - -**最危险的发现**: -1. **Watchdog 任务自删除**(C1)会导致设备空闲超时后多模态功能**永久丧失**,除非重启 -2. **多处 Use-After-Free**(C2-C4, C9-C10)在特定时序下会直接导致系统崩溃 -3. **校准缺旋转锁**(C5)会导致步进电机并发冲突,可能损坏机械结构 -4. **角度归一化死循环**(C6)对畸形输入会挂死任务,触发看门狗复位 - -**建议立即处理 P0 列表中的 12 项**,它们覆盖了崩溃、永久功能丧失和硬件损坏风险。P1 中的竞态条件和数据损坏问题应在下一个迭代中跟进。