Skip to content

Commit f10f3fc

Browse files
bazsialltilla
authored andcommitted
filterx-object: improve filterx_object_ref,unref() assertions
Clarify the races involved and also trigger an abort on use-after-free. Signed-off-by: Balazs Scheidler <balazs.scheidler@axoflow.com>
1 parent 058b467 commit f10f3fc

File tree

1 file changed

+46
-6
lines changed

1 file changed

+46
-6
lines changed

lib/filterx/filterx-object.h

+46-6
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ void _filterx_type_init_methods(FilterXType *type);
7676

7777
#define FILTERX_OBJECT_REFCOUNT_FROZEN (G_MAXINT32)
7878
#define FILTERX_OBJECT_REFCOUNT_STACK (G_MAXINT32-1)
79-
#define FILTERX_OBJECT_REFCOUNT_OFLOW_MARK (G_MAXINT32-2)
79+
#define FILTERX_OBJECT_REFCOUNT_OFLOW_MARK (FILTERX_OBJECT_REFCOUNT_STACK-1024)
8080

8181

8282
FILTERX_DECLARE_TYPE(object);
@@ -121,9 +121,37 @@ filterx_object_ref(FilterXObject *self)
121121
if (!self)
122122
return NULL;
123123

124-
gsize r = g_atomic_counter_get(&self->ref_cnt);
125-
if (r < FILTERX_OBJECT_REFCOUNT_OFLOW_MARK)
124+
gint r = g_atomic_counter_get(&self->ref_cnt);
125+
if (r < FILTERX_OBJECT_REFCOUNT_OFLOW_MARK && r > 0)
126126
{
127+
/* NOTE: getting into this path is racy, as two threads might be
128+
* checking the overflow mark in parallel and then decide we need to
129+
* run this (normal) path. In this case, the race could cause ref_cnt
130+
* to reach FILTERX_OBJECT_REFCOUNT_OFLOW_MARK, without triggering the
131+
* overflow assert below.
132+
*
133+
* To mitigate this, FILTERX_OBJECT_REFCOUNT_OFLOW_MARK is set to 1024
134+
* less than the first value that we handle specially. This means
135+
* that even if the race is lost, we would need 1024 competing CPUs
136+
* concurrently losing the race and incrementing ref_cnt here. And
137+
* even in this case the only issue is that we don't detect an actual
138+
* overflow at runtime that should never occur in the first place.
139+
*
140+
* This is _really_ unlikely, and we will detect ref_cnt overflows in
141+
* non-doom scenarios first, so we can address the actual issue (which
142+
* might be a reference counting bug somewhere).
143+
*
144+
* If less than 1024 CPUs lose the race, then the refcount would end
145+
* up in the range between FILTERX_OBJECT_REFCOUNT_OFLOW_MARK and
146+
* FILTERX_OBJECT_REFCOUNT_STACK, causing the assertion at the end of
147+
* this function to trigger an abort.
148+
*
149+
* The non-racy solution would be to use a
150+
* g_atomic_int_exchange_and_add() call and checking the old_value
151+
* against FILTERX_OBJECT_REFCOUNT_OFLOW_MARK another time, but that's
152+
* an extra conditional in a hot-path.
153+
*/
154+
127155
g_atomic_counter_inc(&self->ref_cnt);
128156
return self;
129157
}
@@ -148,16 +176,28 @@ filterx_object_unref(FilterXObject *self)
148176
if (!self)
149177
return;
150178

151-
gsize r = g_atomic_counter_get(&self->ref_cnt);
179+
gint r = g_atomic_counter_get(&self->ref_cnt);
152180
if (r == FILTERX_OBJECT_REFCOUNT_FROZEN)
153181
return;
154182
if (r == FILTERX_OBJECT_REFCOUNT_STACK)
155183
{
156-
/* we can call exactly one unref for a stack allocation */
184+
/* NOTE: Normally, stack based allocations are only used by a single
185+
* thread. Furthermore, code where we use this object will only have
186+
* a borrowed reference, e.g. it can't cross thread boundaries. With
187+
* that said, even though the condition of this if() statement is
188+
* racy, it's not actually racing, as we only have a single relevant
189+
* thread.
190+
*
191+
* In the rare case where we do want to pass a stack based allocation
192+
* to another thread, we would need to pass a new reference to it, and
193+
* filterx_object_ref() clones a new object in this case, which again
194+
* means, that we won't have actual race here.
195+
*/
196+
157197
g_atomic_counter_set(&self->ref_cnt, 0);
158198
return;
159199
}
160-
if (r < 0)
200+
if (r <= 0)
161201
g_assert_not_reached();
162202

163203
if (g_atomic_counter_dec_and_test(&self->ref_cnt))

0 commit comments

Comments
 (0)