@@ -76,7 +76,7 @@ void _filterx_type_init_methods(FilterXType *type);
76
76
77
77
#define FILTERX_OBJECT_REFCOUNT_FROZEN (G_MAXINT32)
78
78
#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 )
80
80
81
81
82
82
FILTERX_DECLARE_TYPE (object );
@@ -121,9 +121,37 @@ filterx_object_ref(FilterXObject *self)
121
121
if (!self )
122
122
return NULL ;
123
123
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 )
126
126
{
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
+
127
155
g_atomic_counter_inc (& self -> ref_cnt );
128
156
return self ;
129
157
}
@@ -148,16 +176,28 @@ filterx_object_unref(FilterXObject *self)
148
176
if (!self )
149
177
return ;
150
178
151
- gsize r = g_atomic_counter_get (& self -> ref_cnt );
179
+ gint r = g_atomic_counter_get (& self -> ref_cnt );
152
180
if (r == FILTERX_OBJECT_REFCOUNT_FROZEN )
153
181
return ;
154
182
if (r == FILTERX_OBJECT_REFCOUNT_STACK )
155
183
{
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
+
157
197
g_atomic_counter_set (& self -> ref_cnt , 0 );
158
198
return ;
159
199
}
160
- if (r < 0 )
200
+ if (r <= 0 )
161
201
g_assert_not_reached ();
162
202
163
203
if (g_atomic_counter_dec_and_test (& self -> ref_cnt ))
0 commit comments