v0.0.59 - Fix MCP segfault: drop redundant g_main_context_acquire/release in nested JS-eval/snapshot loops (was corrupting owner_count); allow ws:// WebSocket from https:// pages by registering ws as a secure scheme

This commit is contained in:
Laan Tungir
2026-07-28 15:53:05 -04:00
parent ae43decd4d
commit e0607fe045
5 changed files with 61 additions and 28 deletions
+1 -1
View File
@@ -1 +1 @@
0.0.58
0.0.59
+32 -12
View File
@@ -12,10 +12,31 @@
#include <stdlib.h>
/* ── 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;
}
+16 -12
View File
@@ -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);
+2 -2
View File
@@ -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 */
+10 -1
View File
@@ -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). */