diff --git a/VERSION b/VERSION index 9ebecc7..1e7c774 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.58 +0.0.59 diff --git a/src/agent_snapshot.c b/src/agent_snapshot.c index 5636d6c..e26f670 100644 --- a/src/agent_snapshot.c +++ b/src/agent_snapshot.c @@ -12,10 +12,31 @@ #include /* ── Synchronous JS evaluation ─────────────────────────────────────── * - * WebKitGTK's evaluate_javascript is async. We use a nested GMainLoop - * on the default context to wait for the result. The key is to acquire - * the context before creating the loop so that the WebKit IPC sources - * are properly dispatched. + * WebKitGTK's evaluate_javascript is async. We run a nested GMainLoop + * on the default main context until the JS callback (or a timeout) + * quits it. + * + * This function is routinely called from within a Soup server source + * callback (the MCP HTTP handler) that is already dispatching on the + * default main context. Running a nested GMainLoop on the same context + * is the standard GTK modal pattern (gtk_dialog_run does the same) and + * is safe: g_main_loop_run() manages context ownership internally for + * the nested run, and the outer loop's dispatch is suspended until the + * inner loop quits. + * + * The PREVIOUS code additionally called g_main_context_acquire()/ + * g_main_context_release() around the loop. That was the bug: the + * default context is already owned by the outer dispatch, so acquire() + * fails silently (owner_count is NOT incremented), yet release() then + * decrements it unconditionally — producing + * "g_main_context_release_unlocked: assertion 'context->owner_count > 0'" + * and, combined with the recursive check, a segfault. The fix is simply + * to drop the explicit acquire/release and let g_main_loop_run() handle + * ownership. We must NOT use a g_main_context_iteration() pump instead, + * because that would re-dispatch unrelated ready sources (other Soup + * requests, WebKit load events, GTK UI events) re-entrantly while a + * tool call is in progress, which can deadlock when an MCP tool runs + * during page loading. */ typedef struct { @@ -48,6 +69,7 @@ static gboolean on_js_timeout(gpointer user_data) { js_eval_ctx_t *ctx = (js_eval_ctx_t *)user_data; if (!ctx->done) { g_printerr("[agent] JS evaluation timed out\n"); + ctx->done = TRUE; if (ctx->loop && g_main_loop_is_running(ctx->loop)) { g_main_loop_quit(ctx->loop); } @@ -61,26 +83,24 @@ char *agent_js_eval_sync(WebKitWebView *webview, const char *script, js_eval_ctx_t ctx = {0}; - /* Use the default main context for the nested loop. */ - GMainContext *main_ctx = g_main_context_default(); - g_main_context_acquire(main_ctx); - - ctx.loop = g_main_loop_new(main_ctx, FALSE); + /* Nested loop on the default context. Do NOT acquire/release the + * context here — see the block comment above. */ + ctx.loop = g_main_loop_new(g_main_context_default(), FALSE); /* Add a timeout source to the same context. */ guint timeout_id = g_timeout_add(timeout_ms, on_js_timeout, &ctx); - /* Start async evaluation. */ + /* Start async evaluation. The completion callback fires on the + * default main context (where WebKit schedules its async work). */ webkit_web_view_evaluate_javascript(webview, script, -1, NULL, NULL, NULL, on_js_finished, &ctx); - /* Run the nested loop until JS completes or timeout. */ + /* Run the nested loop until JS completes or the timeout quits it. */ g_main_loop_run(ctx.loop); /* Cleanup. */ g_source_remove(timeout_id); g_main_loop_unref(ctx.loop); - g_main_context_release(main_ctx); return ctx.result; } diff --git a/src/agent_tools.c b/src/agent_tools.c index fc5ba7a..c248242 100644 --- a/src/agent_tools.c +++ b/src/agent_tools.c @@ -1005,8 +1005,16 @@ static cJSON *tool_eval(cJSON *params) { /* ── Screenshot tool ──────────────────────────────────────────────── */ /* Context for the async webkit_web_view_snapshot() call. We use a - * nested GMainLoop to turn the async API into a synchronous one, - * matching the pattern in agent_js_eval_sync(). */ + * async API into a synchronous one. + * + * We run a nested GMainLoop on the default main context (the standard + * GTK modal pattern) but do NOT call g_main_context_acquire()/ + * g_main_context_release() around it — the default context is already + * owned by the outer dispatch (these tools are invoked from the MCP + * HTTP handler), so an explicit acquire fails silently and a matching + * release corrupts the owner_count, causing a segfault. The nested + * g_main_loop_run() handles ownership internally. See the block comment + * in agent_js_eval_sync() for the full rationale. */ typedef struct { cairo_surface_t *surface; gboolean done; @@ -1034,6 +1042,7 @@ static gboolean snapshot_timeout_cb(gpointer user_data) { snapshot_ctx_t *ctx = (snapshot_ctx_t *)user_data; if (!ctx->done) { g_printerr("[agent] screenshot timed out\n"); + ctx->done = TRUE; if (ctx->loop && g_main_loop_is_running(ctx->loop)) { g_main_loop_quit(ctx->loop); } @@ -1054,11 +1063,10 @@ static cJSON *tool_screenshot(cJSON *params) { WebKitWebView *wv = get_active_webview(); if (!wv) return make_error("NO_TAB", "No active tab"); - /* Set up the nested GMainLoop to wait for the async snapshot. */ + /* Nested GMainLoop on the default context. No acquire/release — + * see the note on snapshot_ctx_t above. */ snapshot_ctx_t ctx = {0}; - GMainContext *main_ctx = g_main_context_default(); - g_main_context_acquire(main_ctx); - ctx.loop = g_main_loop_new(main_ctx, FALSE); + ctx.loop = g_main_loop_new(g_main_context_default(), FALSE); guint timeout_id = g_timeout_add(10000, snapshot_timeout_cb, &ctx); @@ -1074,7 +1082,6 @@ static cJSON *tool_screenshot(cJSON *params) { g_source_remove(timeout_id); g_main_loop_unref(ctx.loop); - g_main_context_release(main_ctx); if (!ctx.done || ctx.surface == NULL) { if (ctx.surface) cairo_surface_destroy(ctx.surface); @@ -4036,11 +4043,9 @@ static cJSON *tool_screenshot_annotated(cJSON *params) { /* We don't strictly need the result, but wait for it to complete. */ g_free(ann_result); - /* Step 3: Take the WebKit snapshot (same pattern as tool_screenshot). */ + /* Step 3: Take the WebKit snapshot (same nested-loop pattern as tool_screenshot). */ snapshot_ctx_t ctx = {0}; - GMainContext *main_ctx = g_main_context_default(); - g_main_context_acquire(main_ctx); - ctx.loop = g_main_loop_new(main_ctx, FALSE); + ctx.loop = g_main_loop_new(g_main_context_default(), FALSE); guint timeout_id = g_timeout_add(10000, snapshot_timeout_cb, &ctx); @@ -4053,7 +4058,6 @@ static cJSON *tool_screenshot_annotated(cJSON *params) { g_source_remove(timeout_id); g_main_loop_unref(ctx.loop); - g_main_context_release(main_ctx); if (!ctx.done || ctx.surface == NULL) { if (ctx.surface) cairo_surface_destroy(ctx.surface); diff --git a/src/version.h b/src/version.h index cac6546..24d299a 100644 --- a/src/version.h +++ b/src/version.h @@ -11,9 +11,9 @@ #ifndef SOVEREIGN_BROWSER_VERSION_H #define SOVEREIGN_BROWSER_VERSION_H -#define SB_VERSION "v0.0.58" +#define SB_VERSION "v0.0.59" #define SB_VERSION_MAJOR 0 #define SB_VERSION_MINOR 0 -#define SB_VERSION_PATCH 58 +#define SB_VERSION_PATCH 59 #endif /* SOVEREIGN_BROWSER_VERSION_H */ diff --git a/src/web_context.c b/src/web_context.c index 1a14984..4ca3cb2 100644 --- a/src/web_context.c +++ b/src/web_context.c @@ -27,12 +27,21 @@ static void configure_context(WebKitWebContext *ctx) { /* Security strip: register sovereign://, tor://, file:// as secure * (no CORS enforcement), and file:// as local. Same as the original - * main.c configuration. */ + * main.c configuration. + * + * Also register ws (insecure WebSocket) as secure. Without this, + * WebKitGTK blocks ws:// connections opened from https:// pages as + * mixed active content (a secure page may not connect to an + * insecure origin). This browser is intentionally open and should + * be able to connect to anywhere, including local plaintext + * WebSocket services, so we treat ws:// as a secure scheme — the + * same status wss:// already has by default. */ WebKitSecurityManager *sec_mgr = webkit_web_context_get_security_manager(ctx); webkit_security_manager_register_uri_scheme_as_secure(sec_mgr, "sovereign"); webkit_security_manager_register_uri_scheme_as_secure(sec_mgr, "tor"); webkit_security_manager_register_uri_scheme_as_secure(sec_mgr, "file"); + webkit_security_manager_register_uri_scheme_as_secure(sec_mgr, "ws"); webkit_security_manager_register_uri_scheme_as_local(sec_mgr, "file"); /* Accept any TLS certificate (FIPS uses Noise IK, not TLS CAs). */