python/notmuch2: do not destroy messages owned by a query

Any messages retrieved from a query - either directly via
search_messages() or indirectly via thread objects - are owned by that
query. Retrieving the same message (i.e. corresponding to the same
message ID / database object) several times will always yield the same
C object.

The caller is allowed to destroy message objects owned by a query before
the query itself - which can save memory for long-lived queries.
However, that message must then never be retrieved again from that
query.

The python-notmuch2 bindings will currently destroy every message object
in Message._destroy(), which will lead to an invalid free if the same
message is then retrieved again. E.g. the following python program leads
to libtalloc abort()ing:

import notmuch2
db   = notmuch2.Database(mode = notmuch2.Database.MODE.READ_ONLY)
t    = next(db.threads('*'))
msgs = list(zip(t.toplevel(), t.toplevel()))
msgs = list(zip(t.toplevel(), t.toplevel()))

Fix this issue by creating a subclass of Message, which is used for
"standalone" message which have to be freed by the caller. Message class
is then used only for messages descended from a query, which do not need
to be freed by the caller.
This commit is contained in:
Anton Khirnov 2020-06-15 22:58:49 +02:00 committed by David Bremner
parent 1bca41698a
commit 1317579079
2 changed files with 30 additions and 18 deletions

View file

@ -400,7 +400,7 @@ class Database(base.NotmuchObject):
capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID] capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID]
if ret not in ok: if ret not in ok:
raise errors.NotmuchError(ret) raise errors.NotmuchError(ret)
msg = message.Message(self, msg_pp[0], db=self) msg = message.StandaloneMessage(self, msg_pp[0], db=self)
if sync_flags: if sync_flags:
msg.tags.from_maildir_flags() msg.tags.from_maildir_flags()
return self.AddedMessage( return self.AddedMessage(
@ -469,7 +469,7 @@ class Database(base.NotmuchObject):
msg_p = msg_pp[0] msg_p = msg_pp[0]
if msg_p == capi.ffi.NULL: if msg_p == capi.ffi.NULL:
raise LookupError raise LookupError
msg = message.Message(self, msg_p, db=self) msg = message.StandaloneMessage(self, msg_p, db=self)
return msg return msg
def get(self, filename): def get(self, filename):
@ -502,7 +502,7 @@ class Database(base.NotmuchObject):
msg_p = msg_pp[0] msg_p = msg_pp[0]
if msg_p == capi.ffi.NULL: if msg_p == capi.ffi.NULL:
raise LookupError raise LookupError
msg = message.Message(self, msg_p, db=self) msg = message.StandaloneMessage(self, msg_p, db=self)
return msg return msg
@property @property

View file

@ -14,7 +14,7 @@ __all__ = ['Message']
class Message(base.NotmuchObject): class Message(base.NotmuchObject):
"""An email message stored in the notmuch database. """An email message stored in the notmuch database retrieved via a query.
This should not be directly created, instead it will be returned This should not be directly created, instead it will be returned
by calling methods on :class:`Database`. A message keeps a by calling methods on :class:`Database`. A message keeps a
@ -61,22 +61,10 @@ class Message(base.NotmuchObject):
@property @property
def alive(self): def alive(self):
if not self._parent.alive: return self._parent.alive
return False
try:
self._msg_p
except errors.ObjectDestroyedError:
return False
else:
return True
def __del__(self):
self._destroy()
def _destroy(self): def _destroy(self):
if self.alive: pass
capi.lib.notmuch_message_destroy(self._msg_p)
self._msg_p = None
@property @property
def messageid(self): def messageid(self):
@ -375,6 +363,30 @@ class Message(base.NotmuchObject):
if isinstance(other, self.__class__): if isinstance(other, self.__class__):
return self.messageid == other.messageid return self.messageid == other.messageid
class StandaloneMessage(Message):
"""An email message stored in the notmuch database.
This subclass of Message is used for messages that are retrieved from the
database directly and are not owned by a query.
"""
@property
def alive(self):
if not self._parent.alive:
return False
try:
self._msg_p
except errors.ObjectDestroyedError:
return False
else:
return True
def __del__(self):
self._destroy()
def _destroy(self):
if self.alive:
capi.lib.notmuch_message_destroy(self._msg_p)
self._msg_p = None
class FilenamesIter(base.NotmuchIter): class FilenamesIter(base.NotmuchIter):
"""Iterator for binary filenames objects.""" """Iterator for binary filenames objects."""