Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion pypdf/generic/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ def _reference_clone(
if ind is not None:
if id(ind.pdf) not in pdf_dest._id_translated:
pdf_dest._id_translated[id(ind.pdf)] = {}
pdf_dest._id_translated[id(ind.pdf)]["PreventGC"] = ind.pdf # type: ignore
pdf_dest._id_translated[id(ind.pdf)]["PreventGC"] = ind.pdf # type: ignore[index]
if (
not force_duplicate
and ind.idnum in pdf_dest._id_translated[id(ind.pdf)]
Expand Down Expand Up @@ -346,6 +346,7 @@ def clone(
return self
if id(self.pdf) not in pdf_dest._id_translated:
pdf_dest._id_translated[id(self.pdf)] = {}
pdf_dest._id_translated[id(self.pdf)]["PreventGC"] = self.pdf # type: ignore[index]

if self.idnum in pdf_dest._id_translated[id(self.pdf)]:
dup = pdf_dest.get_object(pdf_dest._id_translated[id(self.pdf)][self.idnum])
Expand Down
59 changes: 59 additions & 0 deletions tests/test_generic.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Test the pypdf.generic module."""

import codecs
import gc
import weakref
from base64 import a85encode
from copy import deepcopy
from io import BytesIO
Expand Down Expand Up @@ -916,6 +918,63 @@ def test_cloning(caplog):
assert isinstance(obj21.get("/Test2"), IndirectObject)


def test_cloning_indirect_obj_keeps_hard_reference():
"""
Reported in #3450

Ensure that cloning an IndirectObject keeps a hard reference to
the underlying object, preventing its deallocation, which could allow
`id(obj)` to return the same value for different objects.
"""
writer1 = PdfWriter()
indirect_object = IndirectObject(1, 0, writer1)

# Create a weak reference to the underlying object to test later
# if it is still alive in memory or not
obj_weakref = weakref.ref(indirect_object.pdf)
assert obj_weakref() is not None

writer2 = PdfWriter()
indirect_object.clone(writer2)

# Mimic indirect_object/writer1 going out of scope and being
# garbage collected. Clone should have kept a hard reference to
# it, preventing its deallocation.
del indirect_object
del writer1
gc.collect()
assert obj_weakref() is not None


def test_cloning_null_obj_keeps_hard_reference():
"""
Ensure that cloning a NullObject keeps a hard reference to
the underlying object, preventing its deallocation, which could allow
`id(obj)` to return the same value for different objects.
"""
writer1 = PdfWriter()
indirect_object = IndirectObject(1, 0, writer1)
null_obj = NullObject()
null_obj.indirect_reference = indirect_object

# Create a weak reference to the underlying object to test later
# if it is still alive in memory or not
obj_weakref = weakref.ref(indirect_object.pdf)
assert obj_weakref() is not None

writer2 = PdfWriter()
null_obj.clone(writer2)

# Mimic indirect_object/writer1 going out of scope and being
# garbage collected. Clone should have kept a hard reference to
# it, preventing its deallocation.
del indirect_object
del writer1
del null_obj
gc.collect()
assert obj_weakref() is not None


@pytest.mark.enable_socket
def test_append_with_indirectobject_not_pointing(caplog):
"""
Expand Down
Loading