From 2fc7f3d6728b1188252ef4ee514bc11b010f4638 Mon Sep 17 00:00:00 2001 From: Jabber Developer Date: Fri, 1 May 2026 17:03:47 +0000 Subject: [PATCH] fix(ai): validate aiwin pointer to prevent UAF when window closed Prevent use-after-free in the AI request worker thread when the user closes the AI window during the 60-second HTTP request timeout. Without this fix, the worker may dereference a dangling pointer and crash or exhibit undefined behavior. The _ai_request_thread function in ai_client.c previously cast user_data to ProfAiWin* and used it directly without verifying the window was still alive. Added wins_ai_exists() to iterate through all AI windows and check if the pointer is still valid before use. Safety: - wins_ai_exists() iterates all AI windows (handles multiple AI windows) - Worker skips UI update if window was freed, just cleans up - Logs warning when dangling pointer is detected --- src/ai/ai_client.c | 52 ++++++++++++++++++++++++++++++++------------ src/ui/window_list.c | 21 ++++++++++++++++++ src/ui/window_list.h | 1 + 3 files changed, 60 insertions(+), 14 deletions(-) diff --git a/src/ai/ai_client.c b/src/ai/ai_client.c index 48f14ce1..9bc6b176 100644 --- a/src/ai/ai_client.c +++ b/src/ai/ai_client.c @@ -17,6 +17,7 @@ #include "profanity.h" #include "tools/autocomplete.h" #include "ui/ui.h" +#include "ui/window_list.h" #include #include @@ -71,16 +72,38 @@ _write_callback(void* ptr, size_t size, size_t nmemb, void* userdata) * Thread-safe UI display helpers * ======================================================================== */ +/** + * Validate aiwin pointer by checking if it still exists in the window list. + * Detects if the original window was freed (TOCTOU protection). + * Returns the validated pointer if valid, NULL if window was closed. + */ +static ProfAiWin* +_aiwin_validate(gpointer user_data) +{ + if (!user_data) { + return NULL; + } + + if (!wins_ai_exists((ProfAiWin*)user_data)) { + log_warning("[AI-THREAD] aiwin=%p no longer exists — window was closed", (void*)user_data); + return NULL; + } + + /* Pointer is valid */ + return (ProfAiWin*)user_data; +} + /** * Display an error message in the AI window (thread-safe). - * @param aiwin The AI window (may be NULL) + * @param user_data The original user_data pointer (may be NULL) * @param error_msg The error message to display */ static void -_aiwin_display_error(ProfAiWin* aiwin, const gchar* error_msg) +_aiwin_display_error(gpointer user_data, const gchar* error_msg) { - if (!aiwin || aiwin->memcheck != PROFAIWIN_MEMCHECK) { - log_warning("[AI-THREAD] Cannot display error: aiwin is NULL or invalid (msg: %s)", error_msg); + ProfAiWin* aiwin = _aiwin_validate(user_data); + if (!aiwin) { + log_warning("[AI-THREAD] Cannot display error: aiwin is invalid or window was closed (msg: %s)", error_msg); return; } pthread_mutex_lock(&lock); @@ -90,14 +113,15 @@ _aiwin_display_error(ProfAiWin* aiwin, const gchar* error_msg) /** * Display a response message in the AI window (thread-safe). - * @param aiwin The AI window (may be NULL) + * @param user_data The original user_data pointer (may be NULL) * @param response The response message to display */ static void -_aiwin_display_response(ProfAiWin* aiwin, const gchar* response) +_aiwin_display_response(gpointer user_data, const gchar* response) { - if (!aiwin || aiwin->memcheck != PROFAIWIN_MEMCHECK) { - log_warning("[AI-THREAD] Cannot display response: aiwin is NULL or invalid"); + ProfAiWin* aiwin = _aiwin_validate(user_data); + if (!aiwin) { + log_warning("[AI-THREAD] Cannot display response: aiwin is invalid or window was closed"); return; } pthread_mutex_lock(&lock); @@ -653,7 +677,7 @@ _ai_request_thread(gpointer data) auto_gchar gchar* error_msg = g_strdup_printf("No API key set for provider '%s'. Use '/ai set token %s ' to configure.", session->provider_name, session->provider_name); log_error("AI request failed for %s/%s: %s", session->provider_name, session->model, error_msg); - _aiwin_display_error((ProfAiWin*)user_data, error_msg); + _aiwin_display_error(user_data, error_msg); g_free(args); return NULL; } @@ -663,7 +687,7 @@ _ai_request_thread(gpointer data) if (!curl) { log_error("AI request failed for %s/%s: Failed to initialize curl", session->provider_name, session->model); - _aiwin_display_error((ProfAiWin*)user_data, "Failed to initialize curl."); + _aiwin_display_error(user_data, "Failed to initialize curl."); g_free(args); return NULL; } @@ -718,7 +742,7 @@ _ai_request_thread(gpointer data) if (res != CURLE_OK) { auto_gchar gchar* error_msg = g_strdup(curl_easy_strerror(res)); log_error("AI request failed for %s/%s: %s", session->provider_name, session->model, error_msg); - _aiwin_display_error((ProfAiWin*)user_data, error_msg); + _aiwin_display_error(user_data, error_msg); } else if (http_code >= 400) { /* Handle HTTP errors */ log_debug("[AI-THREAD] HTTP error response body (%zu bytes): %s", @@ -726,7 +750,7 @@ _ai_request_thread(gpointer data) auto_gchar gchar* error_msg = g_strdup_printf("HTTP %ld: %s", http_code, response.data ? response.data : "Unknown error"); log_error("AI request failed for %s/%s: %s", session->provider_name, session->model, error_msg); - _aiwin_display_error((ProfAiWin*)user_data, error_msg); + _aiwin_display_error(user_data, error_msg); } else { /* Parse response - transfer ownership to auto_gchar for cleanup */ log_debug("[AI-THREAD] Raw API response (%zu bytes): %s", response.size, response.data ? response.data : "NULL"); @@ -736,11 +760,11 @@ _ai_request_thread(gpointer data) if (content) { /* Add assistant response to history */ ai_session_add_message(session, "assistant", content); - _aiwin_display_response((ProfAiWin*)user_data, content); + _aiwin_display_response(user_data, content); } else { log_error("AI response parse failed for %s/%s: %.200s...", session->provider_name, session->model, response_data); - _aiwin_display_error((ProfAiWin*)user_data, "Failed to parse AI response."); + _aiwin_display_error(user_data, "Failed to parse AI response."); } } diff --git a/src/ui/window_list.c b/src/ui/window_list.c index d7383e27..ecf1e3e6 100644 --- a/src/ui/window_list.c +++ b/src/ui/window_list.c @@ -885,6 +885,27 @@ wins_get_ai(void) return NULL; } +gboolean +wins_ai_exists(ProfAiWin* const aiwin) +{ + if (!aiwin) { + return FALSE; + } + + GList* curr = values; + while (curr) { + ProfWin* window = curr->data; + if (window->type == WIN_AI) { + if ((ProfAiWin*)window == aiwin) { + return TRUE; + } + } + curr = g_list_next(curr); + } + + return FALSE; +} + void wins_lost_connection(void) { diff --git a/src/ui/window_list.h b/src/ui/window_list.h index 4cb155c0..ef9efe86 100644 --- a/src/ui/window_list.h +++ b/src/ui/window_list.h @@ -64,6 +64,7 @@ ProfPluginWin* wins_get_plugin(const char* const tag); ProfXMLWin* wins_get_xmlconsole(void); ProfVcardWin* wins_get_vcard(void); ProfAiWin* wins_get_ai(void); +gboolean wins_ai_exists(ProfAiWin* const aiwin); void wins_close_plugin(char* tag);