1# Copyright (C) 2021-2023 Free Software Foundation, Inc.
2
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 3 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15
16import tracemalloc
17import gdb
18import re
19
20# A global variable in which we store a reference to the gdb.Inferior
21# object sent to us in the new_inferior event.
22inf = None
23
24# Register the new_inferior event handler.
25def new_inferior_handler(event):
26    global inf
27    inf = event.inferior
28
29
30gdb.events.new_inferior.connect(new_inferior_handler)
31
32# A global filters list, we only care about memory allocations
33# originating from this script.
34filters = [tracemalloc.Filter(True, "*py-inferior-leak.py")]
35
36# Add a new inferior, and return the number of the new inferior.
37def add_inferior():
38    output = gdb.execute("add-inferior", False, True)
39    m = re.search(r"Added inferior (\d+)", output)
40    if m:
41        num = int(m.group(1))
42    else:
43        raise RuntimeError("no match")
44    return num
45
46
47# Run the test.  When CLEAR is True we clear the global INF variable
48# before comparing the before and after memory allocation traces.
49# When CLEAR is False we leave INF set to reference the gdb.Inferior
50# object, thus preventing the gdb.Inferior from being deallocated.
51def test(clear):
52    global filters, inf
53
54    # Start tracing, and take a snapshot of the current allocations.
55    tracemalloc.start()
56    snapshot1 = tracemalloc.take_snapshot()
57
58    # Create an inferior, this triggers the new_inferior event, which
59    # in turn holds a reference to the new gdb.Inferior object in the
60    # global INF variable.
61    num = add_inferior()
62    gdb.execute("remove-inferiors %s" % num)
63
64    # Possibly clear the global INF variable.
65    if clear:
66        inf = None
67
68    # Now grab a second snapshot of memory allocations, and stop
69    # tracing memory allocations.
70    snapshot2 = tracemalloc.take_snapshot()
71    tracemalloc.stop()
72
73    # Filter the snapshots; we only care about allocations originating
74    # from this file.
75    snapshot1 = snapshot1.filter_traces(filters)
76    snapshot2 = snapshot2.filter_traces(filters)
77
78    # Compare the snapshots, this leaves only things that were
79    # allocated, but not deallocated since the first snapshot.
80    stats = snapshot2.compare_to(snapshot1, "traceback")
81
82    # Total up all the deallocated things.
83    total = 0
84    for stat in stats:
85        total += stat.size_diff
86    return total
87
88
89# The first time we run this some global state will be allocated which
90# shows up as memory that is allocated, but not released.  So, run the
91# test once and discard the result.
92test(True)
93
94# Now run the test twice, the first time we clear our global reference
95# to the gdb.Inferior object, which should allow Python to deallocate
96# the object.  The second time we hold onto the global reference,
97# preventing Python from performing the deallocation.
98bytes_with_clear = test(True)
99bytes_without_clear = test(False)
100
101# The bug that used to exist in GDB was that even when we released the
102# global reference the gdb.Inferior object would not be deallocated.
103if bytes_with_clear > 0:
104    raise gdb.GdbError("memory leak when gdb.Inferior should be released")
105if bytes_without_clear == 0:
106    raise gdb.GdbError("gdb.Inferior object is no longer allocated")
107
108# Print a PASS message that the test script can see.
109print("PASS")
110