gobject: fix race in toggle ref during g_object_ref()

Previously:

   1. old_val = atomic_add(&object->ref_count);
   2. if (old_val == 1 && OBJECT_HAS_TOGGLE_REF (object)) { toggle_notify() }

As old_val was 1, you might think that no other thread can have a valid
reference to object. However, that's not the case. For one, GWeakRef can
be used to create another strong reference. More easily, the single
reference can be shared between multiple threads (as long as the code
takes care that the object lives long enough).

That means, another thread can easily add and drop references (including
toggle references). All between step 1 and 2.

A race here might be hard to hit, and the effect might not be obviously
bad. However, consider old_val is 1 due to a normal reference, and
another thread adds a toggle ref between step 1. and 2. Then we would
notify a toggle from 1->2, although a newly added toggle ref is expected
to always start with a normal and a toggle reference. The first toggle
notification is expected to notify about the loss of other references, not
about getting a second reference.

To handle this properly, when we increase the reference count from 1 to
2, we must do so under a lock and check for the toggle notification.

As we now correctly track the toggle behavior, we can also assert in
toggle_refs_get_notify_unlocked() that n_toggle_refs agrees with the
number of references, that is, that the user did always match
g_object_add_toggle_ref() with g_object_remove_toggle_ref().

The downside is here too, that there is now a case (when increasing the
reference count from 1 to 2)  where we need to take the global lock.
That performance problem should be addresses by using per-object locks
instead of a global lock.
This commit is contained in:
Thomas Haller 2023-12-19 17:03:23 +01:00
parent 408dc69186
commit 9ae43169cf

View File

@ -3587,12 +3587,7 @@ toggle_refs_get_notify_unlocked (GObject *object,
if (tstackptr->n_toggle_refs != 1)
{
/* There are multiple references. We won't notify.
*
* Note that the user MUST pair g_object_add_toggle_ref() with
* g_object_remove_toggle_ref(). In that case, having a
* "n_toggle_refs" larger than one, also means that we have multiple
* references. */
g_critical ("Unexpected number of toggle-refs. g_object_add_toggle_ref() must be paired with g_object_remove_toggle_ref()");
return NULL;
}
@ -3600,21 +3595,6 @@ toggle_refs_get_notify_unlocked (GObject *object,
return tstackptr->toggle_refs[0].notify;
}
static void
toggle_refs_notify (GObject *object,
gboolean is_last_ref)
{
GToggleNotify notify;
gpointer data;
G_LOCK (toggle_refs_mutex);
notify = toggle_refs_get_notify_unlocked (object, &data);
G_UNLOCK (toggle_refs_mutex);
if (notify)
notify (data, object, is_last_ref);
}
/**
* g_object_add_toggle_ref: (skip)
* @object: a #GObject
@ -3781,19 +3761,49 @@ gpointer
(g_object_ref) (gpointer _object)
{
GObject *object = _object;
gint old_val;
gboolean object_already_finalized;
GToggleNotify toggle_notify;
gpointer toggle_data;
gint old_ref;
g_return_val_if_fail (G_IS_OBJECT (object), NULL);
old_val = g_atomic_int_add (&object->ref_count, 1);
object_already_finalized = (old_val <= 0);
old_ref = g_atomic_int_get (&object->ref_count);
retry:
toggle_notify = NULL;
if (old_ref > 1 && old_ref < G_MAXINT)
{
/* Fast-path. We have apparently more than 1 references already. No
* special handling for toggle references, just increment the ref count. */
if (!g_atomic_int_compare_and_exchange_full ((int *) &object->ref_count,
old_ref, old_ref + 1, &old_ref))
goto retry;
}
else if (old_ref == 1)
{
gboolean do_retry;
/* With ref count 1, check whether we need to emit a toggle notification. */
G_LOCK (toggle_refs_mutex);
toggle_notify = toggle_refs_get_notify_unlocked (object, &toggle_data);
do_retry = !g_atomic_int_compare_and_exchange_full ((int *) &object->ref_count,
old_ref, old_ref + 1, &old_ref);
G_UNLOCK (toggle_refs_mutex);
if (do_retry)
goto retry;
}
else
{
gboolean object_already_finalized = TRUE;
g_return_val_if_fail (!object_already_finalized, NULL);
return NULL;
}
if (old_val == 1 && OBJECT_HAS_TOGGLE_REF (object))
toggle_refs_notify (object, FALSE);
TRACE (GOBJECT_OBJECT_REF (object, G_TYPE_FROM_INSTANCE (object), old_ref));
TRACE (GOBJECT_OBJECT_REF (object, G_TYPE_FROM_INSTANCE (object), old_val));
if (toggle_notify)
toggle_notify (toggle_data, object, FALSE);
return object;
}