gcancellable: Fix minor race between GCancellable and GCancellableSource

There’s a minor race condition between cancellation of a `GCancellable`,
and disposal/finalisation of a `GCancellableSource` in another thread.

Thread A                               Thread B
 g_cancellable_cancel(C)
 →cancellable_source_cancelled(C, S)
                                       g_source_unref(S)
                                       cancellable_source_dispose(S)
 →→g_source_ref(S)
 →→# S is invalid at this point; crash

Thankfully, the `GCancellable` sets `cancelled_running` while it’s
emitting the `cancelled` signal, so if `cancellable_source_dispose()` is
called while that’s high, we know that the thread which is doing the
cancellation has already started (or is committed to starting) calling
`cancellable_source_cancelled()`.

Fix the race by resurrecting the `GCancellableSource` in
`cancellable_source_dispose()`, and signalling this using
`GCancellableSource.resurrected_during_cancellation`. Check for that
flag in `cancellable_source_cancelled()` and ignore cancellation if it’s
set.

The modifications to `resurrected_during_cancellation` and the
cancellable source’s refcount have to be done with `cancellable_mutex`
held so that they are seen atomically by each thread. This should not
affect performance too much, as it only happens during cancellation or
disposal of a `GCancellableSource`.

Signed-off-by: Philip Withnall <withnall@endlessm.com>

Fixes: #1841
This commit is contained in:
Philip Withnall
2020-02-21 14:44:44 +00:00
parent 4093321c9a
commit e4a690f5dd
2 changed files with 120 additions and 0 deletions

View File

@@ -643,6 +643,8 @@ typedef struct {
GCancellable *cancellable;
gulong cancelled_handler;
/* Protected by cancellable_mutex: */
gboolean resurrected_during_cancellation;
} GCancellableSource;
/*
@@ -661,8 +663,24 @@ cancellable_source_cancelled (GCancellable *cancellable,
gpointer user_data)
{
GSource *source = user_data;
GCancellableSource *cancellable_source = (GCancellableSource *) source;
g_mutex_lock (&cancellable_mutex);
/* Drop the reference added in cancellable_source_dispose(); see the comment there.
* The reference must be dropped after unlocking @cancellable_mutex since
* it could be the final reference, and the dispose function takes
* @cancellable_mutex. */
if (cancellable_source->resurrected_during_cancellation)
{
cancellable_source->resurrected_during_cancellation = FALSE;
g_mutex_unlock (&cancellable_mutex);
g_source_unref (source);
return;
}
g_source_ref (source);
g_mutex_unlock (&cancellable_mutex);
g_source_set_ready_time (source, 0);
g_source_unref (source);
}
@@ -684,12 +702,37 @@ cancellable_source_dispose (GSource *source)
{
GCancellableSource *cancellable_source = (GCancellableSource *)source;
g_mutex_lock (&cancellable_mutex);
if (cancellable_source->cancellable)
{
if (cancellable_source->cancellable->priv->cancelled_running)
{
/* There can be a race here: if thread A has called
* g_cancellable_cancel() and has got as far as committing to call
* cancellable_source_cancelled(), then thread B drops the final
* ref on the GCancellableSource before g_source_ref() is called in
* cancellable_source_cancelled(), then cancellable_source_dispose()
* will run through and the GCancellableSource will be finalised
* before cancellable_source_cancelled() gets to g_source_ref(). It
* will then be left in a state where its committed to using a
* dangling GCancellableSource pointer.
*
* Eliminate that race by resurrecting the #GSource temporarily, and
* then dropping that reference in cancellable_source_cancelled(),
* which should be guaranteed to fire because were inside a
* @cancelled_running block.
*/
g_source_ref (source);
cancellable_source->resurrected_during_cancellation = TRUE;
}
g_clear_signal_handler (&cancellable_source->cancelled_handler,
cancellable_source->cancellable);
g_clear_object (&cancellable_source->cancellable);
}
g_mutex_unlock (&cancellable_mutex);
}
static gboolean