`BlockData` for lambda encounter reference and release error
Found in the development of my personal CLI tool: diff of commit 6ec9b86 and 0185534.
At present, I have only successfully reproduced it in the case of ThreadPool.with_owned_data
, but it may also affect other cases.
If the lambda passed to ThreadPool.with_owned_data
need to access a BlockData
, we need to pass it by block_data_ref (_data_)
in C code instead of directly pass _data_
. Otherwise the reference count of this BlockData
will be incorrect, possibly leading to release in advance. However, in the program I mentioned before and the following example, the C code generated by the compiler is wrong.
A minimal example:
[Compact (opaque = true)]
class Foo {
public Foo () {
}
}
class Bar {
string baz;
ThreadPool<Foo> pool;
Mutex mutex = Mutex ();
public Bar (string s, bool c) {
this.baz = s;
try {
pool = new ThreadPool<Foo>.with_owned_data ((foo) => {
if (c) {
mutex.lock ();
baz += "Hello";
print ("%s\n", baz);
mutex.unlock ();
}
}, 1, false);
} catch {}
}
public void run () {
try {
pool.add (new Foo ());
} catch {}
}
}
void main () {
var bar = new Bar ("Hello", true);
bar.run ();
}
Compile and run it. It will fail with:
'./test-lambda-block-lifespan.bin' terminated by signal SIGSEGV (Address boundary error)
Check the C code:
Bar*
bar_construct (GType object_type,
const gchar* s,
gboolean c)
{
Bar* self = NULL;
Block1Data* _data1_;
gchar* _tmp0_;
GError* _inner_error0_ = NULL;
g_return_val_if_fail (s != NULL, NULL);
self = (Bar*) g_type_create_instance (object_type);
_data1_ = g_slice_new0 (Block1Data);
_data1_->_ref_count_ = 1;
_data1_->self = bar_ref (self);
_data1_->c = c;
_tmp0_ = g_strdup (s);
_g_free0 (self->priv->baz);
self->priv->baz = _tmp0_;
{
GThreadPool* _tmp1_ = NULL;
GThreadPool* _tmp2_;
GThreadPool* _tmp3_;
_tmp2_ = g_thread_pool_new (____lambda4__gfunc, _data1_, 1, FALSE, &_inner_error0_);
_tmp1_ = _tmp2_;
if (G_UNLIKELY (_inner_error0_ != NULL)) {
goto __catch0_g_error;
}
_tmp3_ = _tmp1_;
_tmp1_ = NULL;
_g_thread_pool_free0 (self->priv->pool);
self->priv->pool = _tmp3_;
_g_thread_pool_free0 (_tmp1_);
}
goto __finally0;
__catch0_g_error:
{
g_clear_error (&_inner_error0_);
}
__finally0:
if (G_UNLIKELY (_inner_error0_ != NULL)) {
block1_data_unref (_data1_);
_data1_ = NULL;
g_critical ("file %s: line %d: uncaught error: %s (%s, %d)", __FILE__, __LINE__, _inner_error0_->message, g_quark_to_string (_inner_error0_->domain), _inner_error0_->code);
g_clear_error (&_inner_error0_);
return NULL;
}
block1_data_unref (_data1_);
_data1_ = NULL;
return self;
}
Line _tmp2_ = g_thread_pool_new (____lambda4__gfunc, _data1_, 1, FALSE, &_inner_error0_);
just passed _data1_
without increase the refcount, and at the end of the construct func, the memory of _data1_
was released by block1_data_unref (_data1_);
so when calling bar.run ();
it can no more be accessed.
However, I've also tried to write an example without the use of ThreadPool.with_owned_data
, but it works fine:
public delegate void Test ();
public class Foo {
string data;
Bar bar;
public Foo (string data, bool c) {
this.data = data;
bar = new Bar (() => {
if (c) {
this.data += ", World!";
print ("%s\n", this.data);
}
});
print ("%s\n", this.data);
}
public void run () {
bar.run ();
}
}
[Compact (opaque = true)]
public class Bar {
Test test;
public Bar (owned Test t) {
test = (owned) t;
}
public void run () {
test ();
}
}
void main () {
var foo = new Foo ("Hello", true);
foo.run ();
}
And check its C code; it correctly used block1_data_ref (_data1_)
to handle the refcount: _tmp1_ = bar_new (___lambda4__test, block1_data_ref (_data1_), block1_data_unref);
.
I haven't figured out what are the necessary conditions for this problem to occur since these very similar examples showed different behaviors.