chansrv: Protect bus lifetime across threads, detect stale fast path
Three correctness gaps found in code review of the private-loop rewrite, all in how chansrv's thread and the IBus thread interact around `bus`: - xrdp_input_enable() read the shared `bus` pointer with no synchronization at all, despite the IBus thread being free to null it out (disconnect, teardown) at any time - a real use-after-free risk, not just a formality, since this function actively calls methods on it rather than just checking it's non-NULL. Fixed by taking a ref under state_mutex before touching it, and using that local reference for the rest of the call; bus's creation and teardown in xrdp_input_main_loop() now publish/clear the shared pointer under the same lock so the ref-under-lock pattern actually synchronizes against something. - xrdp_input_unicode_init()'s fast path trusted ibus_ready without re-checking the connection. ibus_ready and connection liveness normally change together, but there's a window between the underlying socket actually dying and the "disconnected" signal being dispatched on the IBus thread. A client that reconnects into that window would get a false "unicode input is supported" advertisement it could never actually use. Now double-checks ibus_bus_is_connected() before taking the fast path. - That staleness check can't just flip ibus_ready and fall through to spawning a new thread - the old one may still be alive and about to touch bus/g_engine itself, racing the new thread's own setup. Now asks the stale thread to shut down (g_main_loop_quit, safe to call cross-thread) and waits on the exit condvar before proceeding, so there's never more than one IBus thread alive at a time. Verified: 20 back-to-back xrdp_input_unicode_init() calls on a healthy connection all take the fast path correctly (no spurious thread restarts); 5 kill/restart cycles still show no thread leak; an 18-char rapid-fire send against a real focused GTK entry still lands every character in order with zero drops after these changes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+85
-17
@@ -167,28 +167,40 @@ xrdp_input_ibus_create_engine(IBusFactory *factory,
|
|||||||
* with "Set global engine failed: Timeout was reached". Calling this
|
* with "Set global engine failed: Timeout was reached". Calling this
|
||||||
* from chansrv's thread instead leaves the IBus thread free to answer
|
* from chansrv's thread instead leaves the IBus thread free to answer
|
||||||
* the nested call while this blocks. g_dbus_connection sync calls are
|
* the nested call while this blocks. g_dbus_connection sync calls are
|
||||||
* documented thread-safe to issue from any thread, so this is fine
|
* documented thread-safe to issue from any thread, so that part is
|
||||||
* despite `bus` otherwise being owned by the IBus thread. */
|
* fine despite `bus` otherwise being owned by the IBus thread - but
|
||||||
|
* the `bus` pointer itself is not: the IBus thread can null it out
|
||||||
|
* (disconnect, teardown) at any time. Take our own reference under
|
||||||
|
* state_mutex before touching it, so a concurrent teardown drops the
|
||||||
|
* shared pointer without pulling the object out from under us. */
|
||||||
static gboolean
|
static gboolean
|
||||||
xrdp_input_enable(void)
|
xrdp_input_enable(void)
|
||||||
{
|
{
|
||||||
IBusEngineDesc *desc;
|
IBusEngineDesc *desc;
|
||||||
const gchar *name;
|
const gchar *name;
|
||||||
|
IBusBus *local_bus;
|
||||||
|
gboolean result;
|
||||||
|
|
||||||
if (bus == NULL || !ibus_bus_is_connected(bus))
|
g_mutex_lock(&state_mutex);
|
||||||
|
local_bus = (bus != NULL) ? g_object_ref(bus) : NULL;
|
||||||
|
g_mutex_unlock(&state_mutex);
|
||||||
|
|
||||||
|
if (local_bus == NULL || !ibus_bus_is_connected(local_bus))
|
||||||
{
|
{
|
||||||
LOG(LOG_LEVEL_ERROR,
|
LOG(LOG_LEVEL_ERROR,
|
||||||
"xrdp_input_enable: IBus is not connected");
|
"xrdp_input_enable: IBus is not connected");
|
||||||
|
g_clear_object(&local_bus);
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
desc = ibus_bus_get_global_engine(bus);
|
desc = ibus_bus_get_global_engine(local_bus);
|
||||||
name = desc != NULL ? ibus_engine_desc_get_name(desc) : NULL;
|
name = desc != NULL ? ibus_engine_desc_get_name(desc) : NULL;
|
||||||
|
|
||||||
if (name != NULL && g_ascii_strcasecmp(name, "XrdpIme") == 0 &&
|
if (name != NULL && g_ascii_strcasecmp(name, "XrdpIme") == 0 &&
|
||||||
g_engine != NULL)
|
g_engine != NULL)
|
||||||
{
|
{
|
||||||
g_object_unref(desc);
|
g_object_unref(desc);
|
||||||
|
g_object_unref(local_bus);
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,14 +218,15 @@ xrdp_input_enable(void)
|
|||||||
g_object_unref(desc);
|
g_object_unref(desc);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ibus_bus_set_global_engine(bus, "XrdpIme"))
|
result = ibus_bus_set_global_engine(local_bus, "XrdpIme");
|
||||||
|
if (!result)
|
||||||
{
|
{
|
||||||
LOG(LOG_LEVEL_ERROR,
|
LOG(LOG_LEVEL_ERROR,
|
||||||
"xrdp_input_enable: failed to switch global engine to XrdpIme");
|
"xrdp_input_enable: failed to switch global engine to XrdpIme");
|
||||||
return FALSE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return TRUE;
|
g_object_unref(local_bus);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*****************************************************************************/
|
/*****************************************************************************/
|
||||||
@@ -410,14 +423,23 @@ xrdp_input_main_loop(void *in_val)
|
|||||||
g_main_context_push_thread_default(ibus_context);
|
g_main_context_push_thread_default(ibus_context);
|
||||||
thread_default_pushed = TRUE;
|
thread_default_pushed = TRUE;
|
||||||
|
|
||||||
bus = ibus_bus_new();
|
|
||||||
|
|
||||||
if (bus == NULL)
|
|
||||||
{
|
{
|
||||||
LOG(LOG_LEVEL_ERROR, "xrdp_input_main_loop: ibus_bus_new failed");
|
IBusBus *new_bus = ibus_bus_new();
|
||||||
goto thread_cleanup;
|
|
||||||
|
if (new_bus == NULL)
|
||||||
|
{
|
||||||
|
LOG(LOG_LEVEL_ERROR, "xrdp_input_main_loop: ibus_bus_new failed");
|
||||||
|
goto thread_cleanup;
|
||||||
|
}
|
||||||
|
g_object_ref_sink(new_bus);
|
||||||
|
|
||||||
|
/* Published under lock so xrdp_input_enable(), running on
|
||||||
|
* chansrv's thread, never observes a partially-constructed
|
||||||
|
* `bus`. */
|
||||||
|
g_mutex_lock(&state_mutex);
|
||||||
|
bus = new_bus;
|
||||||
|
g_mutex_unlock(&state_mutex);
|
||||||
}
|
}
|
||||||
g_object_ref_sink(bus);
|
|
||||||
|
|
||||||
if (!ibus_bus_is_connected(bus))
|
if (!ibus_bus_is_connected(bus))
|
||||||
{
|
{
|
||||||
@@ -518,10 +540,22 @@ thread_cleanup:
|
|||||||
g_object_unref(factory);
|
g_object_unref(factory);
|
||||||
factory = NULL;
|
factory = NULL;
|
||||||
}
|
}
|
||||||
if (bus != NULL)
|
|
||||||
{
|
{
|
||||||
g_object_unref(bus);
|
/* Clear the shared pointer under lock before dropping the
|
||||||
|
* reference, so a concurrent xrdp_input_enable() ref (taken
|
||||||
|
* under the same lock) always sees either the live object or
|
||||||
|
* NULL, never a dangling one. */
|
||||||
|
IBusBus *old_bus;
|
||||||
|
|
||||||
|
g_mutex_lock(&state_mutex);
|
||||||
|
old_bus = bus;
|
||||||
bus = NULL;
|
bus = NULL;
|
||||||
|
g_mutex_unlock(&state_mutex);
|
||||||
|
|
||||||
|
if (old_bus != NULL)
|
||||||
|
{
|
||||||
|
g_object_unref(old_bus);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (ibus_loop != NULL)
|
if (ibus_loop != NULL)
|
||||||
{
|
{
|
||||||
@@ -633,8 +667,42 @@ xrdp_input_unicode_init(void)
|
|||||||
g_mutex_lock(&state_mutex);
|
g_mutex_lock(&state_mutex);
|
||||||
if (ibus_ready)
|
if (ibus_ready)
|
||||||
{
|
{
|
||||||
g_mutex_unlock(&state_mutex);
|
/* ibus_ready normally tracks connection liveness exactly,
|
||||||
return 0;
|
* since the "disconnected" handler and thread_exit update it
|
||||||
|
* together - but there's a window between the socket actually
|
||||||
|
* dying and that signal being dispatched on the IBus thread.
|
||||||
|
* Double-check here rather than trust a possibly-stale flag,
|
||||||
|
* so a client that reconnects right into that window gets a
|
||||||
|
* clean failure (and a real retry next time) instead of a
|
||||||
|
* false "unicode input is supported" advertisement it can
|
||||||
|
* never actually use. */
|
||||||
|
IBusBus *check_bus = (bus != NULL) ? g_object_ref(bus) : NULL;
|
||||||
|
gboolean connected = (check_bus != NULL) &&
|
||||||
|
ibus_bus_is_connected(check_bus);
|
||||||
|
g_clear_object(&check_bus);
|
||||||
|
|
||||||
|
if (connected)
|
||||||
|
{
|
||||||
|
g_mutex_unlock(&state_mutex);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stale: the IBus thread hasn't noticed its connection is dead
|
||||||
|
* yet. Ask it to shut down and wait for it to fully exit
|
||||||
|
* before starting a new one below - starting a second thread
|
||||||
|
* while the first is still alive would race both of them over
|
||||||
|
* bus/g_engine. */
|
||||||
|
LOG(LOG_LEVEL_WARNING,
|
||||||
|
"xrdp_input_unicode_init: stale IBus connection, "
|
||||||
|
"restarting IBus thread");
|
||||||
|
if (ibus_loop != NULL)
|
||||||
|
{
|
||||||
|
g_main_loop_quit(ibus_loop);
|
||||||
|
}
|
||||||
|
while (!ibus_thread_exited)
|
||||||
|
{
|
||||||
|
g_cond_wait(&state_cond, &state_mutex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ibus_shutting_down = FALSE;
|
ibus_shutting_down = FALSE;
|
||||||
ibus_thread_exited = FALSE;
|
ibus_thread_exited = FALSE;
|
||||||
|
|||||||
Reference in New Issue
Block a user