mirror of
https://git.jabber.space/devs/cproof.git
synced 2026-07-30 11:26:21 +00:00
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
This commit is contained in:
@@ -17,6 +17,7 @@
|
|||||||
#include "profanity.h"
|
#include "profanity.h"
|
||||||
#include "tools/autocomplete.h"
|
#include "tools/autocomplete.h"
|
||||||
#include "ui/ui.h"
|
#include "ui/ui.h"
|
||||||
|
#include "ui/window_list.h"
|
||||||
|
|
||||||
#include <curl/curl.h>
|
#include <curl/curl.h>
|
||||||
#include <glib.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
|
* 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).
|
* 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
|
* @param error_msg The error message to display
|
||||||
*/
|
*/
|
||||||
static void
|
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) {
|
ProfAiWin* aiwin = _aiwin_validate(user_data);
|
||||||
log_warning("[AI-THREAD] Cannot display error: aiwin is NULL or invalid (msg: %s)", error_msg);
|
if (!aiwin) {
|
||||||
|
log_warning("[AI-THREAD] Cannot display error: aiwin is invalid or window was closed (msg: %s)", error_msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pthread_mutex_lock(&lock);
|
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).
|
* 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
|
* @param response The response message to display
|
||||||
*/
|
*/
|
||||||
static void
|
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) {
|
ProfAiWin* aiwin = _aiwin_validate(user_data);
|
||||||
log_warning("[AI-THREAD] Cannot display response: aiwin is NULL or invalid");
|
if (!aiwin) {
|
||||||
|
log_warning("[AI-THREAD] Cannot display response: aiwin is invalid or window was closed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pthread_mutex_lock(&lock);
|
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.",
|
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);
|
session->provider_name, session->provider_name);
|
||||||
log_error("AI request failed for %s/%s: %s", session->provider_name, session->model, error_msg);
|
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);
|
g_free(args);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
@@ -663,7 +687,7 @@ _ai_request_thread(gpointer data)
|
|||||||
if (!curl) {
|
if (!curl) {
|
||||||
log_error("AI request failed for %s/%s: Failed to initialize curl",
|
log_error("AI request failed for %s/%s: Failed to initialize curl",
|
||||||
session->provider_name, session->model);
|
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);
|
g_free(args);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
@@ -718,7 +742,7 @@ _ai_request_thread(gpointer data)
|
|||||||
if (res != CURLE_OK) {
|
if (res != CURLE_OK) {
|
||||||
auto_gchar gchar* error_msg = g_strdup(curl_easy_strerror(res));
|
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);
|
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) {
|
} else if (http_code >= 400) {
|
||||||
/* Handle HTTP errors */
|
/* Handle HTTP errors */
|
||||||
log_debug("[AI-THREAD] HTTP error response body (%zu bytes): %s",
|
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,
|
auto_gchar gchar* error_msg = g_strdup_printf("HTTP %ld: %s", http_code,
|
||||||
response.data ? response.data : "Unknown error");
|
response.data ? response.data : "Unknown error");
|
||||||
log_error("AI request failed for %s/%s: %s", session->provider_name, session->model, error_msg);
|
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 {
|
} else {
|
||||||
/* Parse response - transfer ownership to auto_gchar for cleanup */
|
/* 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");
|
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) {
|
if (content) {
|
||||||
/* Add assistant response to history */
|
/* Add assistant response to history */
|
||||||
ai_session_add_message(session, "assistant", content);
|
ai_session_add_message(session, "assistant", content);
|
||||||
_aiwin_display_response((ProfAiWin*)user_data, content);
|
_aiwin_display_response(user_data, content);
|
||||||
} else {
|
} else {
|
||||||
log_error("AI response parse failed for %s/%s: %.200s...",
|
log_error("AI response parse failed for %s/%s: %.200s...",
|
||||||
session->provider_name, session->model, response_data);
|
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.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -885,6 +885,27 @@ wins_get_ai(void)
|
|||||||
return NULL;
|
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
|
void
|
||||||
wins_lost_connection(void)
|
wins_lost_connection(void)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ ProfPluginWin* wins_get_plugin(const char* const tag);
|
|||||||
ProfXMLWin* wins_get_xmlconsole(void);
|
ProfXMLWin* wins_get_xmlconsole(void);
|
||||||
ProfVcardWin* wins_get_vcard(void);
|
ProfVcardWin* wins_get_vcard(void);
|
||||||
ProfAiWin* wins_get_ai(void);
|
ProfAiWin* wins_get_ai(void);
|
||||||
|
gboolean wins_ai_exists(ProfAiWin* const aiwin);
|
||||||
|
|
||||||
void wins_close_plugin(char* tag);
|
void wins_close_plugin(char* tag);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user