fix(ai): validate aiwin pointer to prevent UAF when window closed
Some checks failed
CI Code / Check spelling (pull_request) Successful in 53s
CI Code / Check coding style (pull_request) Successful in 1m11s
CI Code / Linux (debian) (pull_request) Failing after 2m58s
CI Code / Linux (arch) (pull_request) Failing after 3m11s
CI Code / Linux (ubuntu) (pull_request) Failing after 3m10s
CI Code / Code Coverage (pull_request) Successful in 2m46s

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
This commit is contained in:
2026-05-01 17:03:47 +00:00
parent fe0a46da58
commit 2fc7f3d672
3 changed files with 60 additions and 14 deletions

View File

@@ -17,6 +17,7 @@
#include "profanity.h"
#include "tools/autocomplete.h"
#include "ui/ui.h"
#include "ui/window_list.h"
#include <curl/curl.h>
#include <glib.h>
@@ -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 <key>' 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.");
}
}

View File

@@ -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)
{

View File

@@ -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);