Inconsistent GLib.Source behaviour triggering invalid writes/reads
I found this while working on a pytest test suite for a program that uses dbus-next. dbus-next uses a custom class for a source during the initial connection. This results in invalid reads, but it's a bit of a heisenbug because it's hard to trigger reliably. The reproducer below (attached as file too) illustrates the problem but the summary is:
if the custom source's dispatch()
returns GLib.SOURCE_REMOVE
, the caller must also explicitly call source.destroy()
before the program quits. The exception is (option d
below) to drop the last python reference to the source, after the last dispatch()
call and before mainloop.quit()
. The above is not needed for GLib.SOURCE_CONTINUE
.
In the reproducer, any of the option a
to option d
can be used to fix the issue, without them, we get:
...
(process:75303): GLib-CRITICAL **: 22:03:29.601: source_remove_from_context: assertion 'source_list != NULL' failed
...
==75303== Invalid read of size 4
==75303== at 0x1351550F: g_source_unref_internal (gmain.c:2337)
==75303== by 0x1347F3AA: ??? (in /usr/lib64/python3.10/site-packages/gi/_gi.cpython-310-x86_64-linux-gnu.so)
#!/usr/bin/python3
# Run this with valgrind --tool=memcheck /usr/bin/python3 thisfile.py
from gi.repository import GLib
class MySource(GLib.Source):
def __init__(self):
pass
def dispatch(self, callback, user_data):
print("dispatch invoked")
callback(self)
# If we reply with GLib.SOURCE_REMOVE, the source must be explicitly
# destroyed, either here or in the callback() or before
# mainloop.quit(). Note that we must call source.destroy() and not
# rely on the unref, i.e. del source does *not* work.
# Option a - call destroy here
# self.destroy()
# If we return GLib.SOURCE_CONTINUE, destroying the source is not
# required.
return GLib.SOURCE_REMOVE
def check(self):
return False
def prepare(self):
return (False, -1)
source = MySource()
def cb(src):
print("callback invoked")
# Option b - call destroy here
# src.destroy()
# This does NOT work, same valgrind errors as if we didn't do anything
# global source
# del source
main_context = GLib.main_context_default()
source.set_callback(cb)
source.add_unix_fd(open("/dev/zero").fileno(), GLib.IO_IN)
source.attach(main_context)
# Deleting doesn't work because of the python wrappers - they call destroy()
# even if there are multiple refs left
# del source
loop = GLib.MainLoop()
def quit():
global source
print("quitting")
# Option c - call destroy here
# source.destroy()
# Option d - we can unref the source here and it will work
del source
# Just calling loop.quit() without destroying the source however ends up
# in a bunch of valgrind issue.
loop.quit()
GLib.timeout_add(1000, quit)
loop.run()
Above as attachment: test.py