* [PATCH 0/2] Fix memory leak with .debug_types
@ 2025-10-23 17:17 Tom Tromey
2025-10-23 17:17 ` [PATCH 1/2] Two bug fixes in mdict_free Tom Tromey
2025-10-23 17:17 ` [PATCH 2/2] Free multidicts from blockvector Tom Tromey
0 siblings, 2 replies; 11+ messages in thread
From: Tom Tromey @ 2025-10-23 17:17 UTC (permalink / raw)
To: gdb-patches; +Cc: Tom Tromey
I noticed a while ago that nothing in the tree ever calls mdict_free,
and looking deeper I found that .debug_types has a longstanding memory
leak.
Now that blockvectors are heap-allocated, this is relatively easy to
fix, which is what this series does.
Regression tested on x86-64 Fedora 40.
Signed-off-by: Tom Tromey <tom@tromey.com>
---
Tom Tromey (2):
Two bug fixes in mdict_free
Free multidicts from blockvector
gdb/block.c | 6 ++++++
gdb/block.h | 2 ++
gdb/dictionary.c | 13 +++++++++----
3 files changed, 17 insertions(+), 4 deletions(-)
---
base-commit: 8eeb52e3c7701f03711d51a2209c39d85d109500
change-id: 20251023-mdict-free-47eec1d7635d
Best regards,
--
Tom Tromey <tom@tromey.com>
^ permalink raw reply [flat|nested] 11+ messages in thread* [PATCH 1/2] Two bug fixes in mdict_free 2025-10-23 17:17 [PATCH 0/2] Fix memory leak with .debug_types Tom Tromey @ 2025-10-23 17:17 ` Tom Tromey 2025-10-23 18:26 ` Simon Marchi 2025-10-23 17:17 ` [PATCH 2/2] Free multidicts from blockvector Tom Tromey 1 sibling, 1 reply; 11+ messages in thread From: Tom Tromey @ 2025-10-23 17:17 UTC (permalink / raw) To: gdb-patches; +Cc: Tom Tromey A heap-allocated multidictionary should be freed by calling mdict_free. However, while this function does free the contents of the dictionary, it neglects to free the dictionary itself. There's also a second bug, which is that if a multidictionary is created with no dictionaries, gdb will crash on the first line of mdict_free: enum dict_type type = mdict->dictionaries[0]->vector->type; So, this patch also adds the type to struct multidictionary, avoiding this problem. Note that this does not increase the structure size on x86-64, because the new member fits into the padding. --- gdb/dictionary.c | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/gdb/dictionary.c b/gdb/dictionary.c index f435ad5a47e817dc59087ac2ff5637415a9256d6..9dec7ccb2af5fad1e0cb62b92dc79e41e2fbbc15 100644 --- a/gdb/dictionary.c +++ b/gdb/dictionary.c @@ -911,6 +911,9 @@ struct multidictionary /* The number of language dictionaries currently allocated. Only used for expandable dictionaries. */ unsigned short n_allocated_dictionaries; + + /* The type of dictionary. */ + enum dict_type type; }; /* A helper function to collate symbols on the pending list by language. */ @@ -948,6 +951,7 @@ mdict_create_hashed (struct obstack *obstack, retval->dictionaries = XOBNEWVEC (obstack, struct dictionary *, nsyms.size ()); retval->n_allocated_dictionaries = nsyms.size (); + retval->type = DICT_HASHED; int idx = 0; for (const auto &[language, symlist] : nsyms) @@ -969,6 +973,7 @@ mdict_create_hashed_expandable (enum language language) retval->n_allocated_dictionaries = 1; retval->dictionaries = XNEW (struct dictionary *); retval->dictionaries[0] = dict_create_hashed_expandable (language); + retval->type = DICT_HASHED_EXPANDABLE; return retval; } @@ -988,6 +993,7 @@ mdict_create_linear (struct obstack *obstack, retval->dictionaries = XOBNEWVEC (obstack, struct dictionary *, nsyms.size ()); retval->n_allocated_dictionaries = nsyms.size (); + retval->type = DICT_LINEAR; int idx = 0; for (const auto &[language, symlist] : nsyms) @@ -1009,6 +1015,7 @@ mdict_create_linear_expandable (enum language language) retval->n_allocated_dictionaries = 1; retval->dictionaries = XNEW (struct dictionary *); retval->dictionaries[0] = dict_create_linear_expandable (language); + retval->type = DICT_LINEAR_EXPANDABLE; return retval; } @@ -1018,15 +1025,12 @@ mdict_create_linear_expandable (enum language language) void mdict_free (struct multidictionary *mdict) { - /* Grab the type of dictionary being used. */ - enum dict_type type = mdict->dictionaries[0]->vector->type; - /* Loop over all dictionaries and free them. */ for (unsigned short idx = 0; idx < mdict->n_allocated_dictionaries; ++idx) dict_free (mdict->dictionaries[idx]); /* Free the dictionary list, if needed. */ - switch (type) + switch (mdict->type) { case DICT_HASHED: case DICT_LINEAR: @@ -1036,6 +1040,7 @@ mdict_free (struct multidictionary *mdict) case DICT_HASHED_EXPANDABLE: case DICT_LINEAR_EXPANDABLE: xfree (mdict->dictionaries); + xfree (mdict); break; } } -- 2.49.0 ^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH 1/2] Two bug fixes in mdict_free 2025-10-23 17:17 ` [PATCH 1/2] Two bug fixes in mdict_free Tom Tromey @ 2025-10-23 18:26 ` Simon Marchi 2025-10-23 18:32 ` Simon Marchi 0 siblings, 1 reply; 11+ messages in thread From: Simon Marchi @ 2025-10-23 18:26 UTC (permalink / raw) To: Tom Tromey, gdb-patches On 10/23/25 1:17 PM, Tom Tromey wrote: > A heap-allocated multidictionary should be freed by calling > mdict_free. However, while this function does free the contents of > the dictionary, it neglects to free the dictionary itself. > > There's also a second bug, which is that if a multidictionary is > created with no dictionaries, gdb will crash on the first line of > mdict_free: > > enum dict_type type = mdict->dictionaries[0]->vector->type; > > So, this patch also adds the type to struct multidictionary, avoiding > this problem. Note that this does not increase the structure size on > x86-64, because the new member fits into the padding. I think you could just remove dict_vector::type and use multidictionary::type at the single spot where dict_vector::type is still used. Simon ^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH 1/2] Two bug fixes in mdict_free 2025-10-23 18:26 ` Simon Marchi @ 2025-10-23 18:32 ` Simon Marchi 2025-10-23 18:49 ` Tom Tromey 0 siblings, 1 reply; 11+ messages in thread From: Simon Marchi @ 2025-10-23 18:32 UTC (permalink / raw) To: Tom Tromey, gdb-patches On 10/23/25 2:26 PM, Simon Marchi wrote: > On 10/23/25 1:17 PM, Tom Tromey wrote: >> A heap-allocated multidictionary should be freed by calling >> mdict_free. However, while this function does free the contents of >> the dictionary, it neglects to free the dictionary itself. >> >> There's also a second bug, which is that if a multidictionary is >> created with no dictionaries, gdb will crash on the first line of >> mdict_free: >> >> enum dict_type type = mdict->dictionaries[0]->vector->type; >> >> So, this patch also adds the type to struct multidictionary, avoiding >> this problem. Note that this does not increase the structure size on >> x86-64, because the new member fits into the padding. > > I think you could just remove dict_vector::type and use > multidictionary::type at the single spot where dict_vector::type is > still used. > > Simon Btw, your change LGTM with or without my suggestion. Approved-By: Simon Marchi <simon.marchi@efficios.com> Simon ^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH 1/2] Two bug fixes in mdict_free 2025-10-23 18:32 ` Simon Marchi @ 2025-10-23 18:49 ` Tom Tromey 0 siblings, 0 replies; 11+ messages in thread From: Tom Tromey @ 2025-10-23 18:49 UTC (permalink / raw) To: Simon Marchi; +Cc: Tom Tromey, gdb-patches >> I think you could just remove dict_vector::type and use >> multidictionary::type at the single spot where dict_vector::type is >> still used. Simon> Btw, your change LGTM with or without my suggestion. I made the change. Tom ^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH 2/2] Free multidicts from blockvector 2025-10-23 17:17 [PATCH 0/2] Fix memory leak with .debug_types Tom Tromey 2025-10-23 17:17 ` [PATCH 1/2] Two bug fixes in mdict_free Tom Tromey @ 2025-10-23 17:17 ` Tom Tromey 2025-10-23 18:31 ` Simon Marchi 1 sibling, 1 reply; 11+ messages in thread From: Tom Tromey @ 2025-10-23 17:17 UTC (permalink / raw) To: gdb-patches; +Cc: Tom Tromey Currently, nothing in the tree ever calls mdict_free. However, code does heap-allocate some multidicts. A simple way to see this is to use valgrind, run "gdb -readnow" on the executable created by gdb.dwarf2/struct-with-sig.exp, and then use "file" to clear the objfile list. This yields: ==1522843== 144 (16 direct, 128 indirect) bytes in 1 blocks are definitely lost in loss record 905 of 3,005 ==1522843== at 0x4843866: malloc (vg_replace_malloc.c:446) ==1522843== by 0x48E397: xmalloc (alloc.c:52) ==1522843== by 0x59DE66: multidictionary* xnew<multidictionary>() (poison.h:102) ==1522843== by 0x59CFF4: mdict_create_hashed_expandable(language) (dictionary.c:965) ==1522843== by 0x50A269: buildsym_compunit::finish_block_internal(symbol*, pending**, pending_block*, dynamic_prop const*, unsigned long, unsigned long, int, int) (buildsym.c:221) ==1522843== by 0x50AE04: buildsym_compunit::end_compunit_symtab_get_static_block(unsigned long, int, int) (buildsym.c:818) ==1522843== by 0x50C4CF: buildsym_compunit::end_expandable_symtab(unsigned long) (buildsym.c:1037) ==1522843== by 0x61DBC6: process_full_type_unit (read.c:4970) This patch fixes the leaks by calling mdict_free when a blockvector is destroyed. --- gdb/block.c | 6 ++++++ gdb/block.h | 2 ++ 2 files changed, 8 insertions(+) diff --git a/gdb/block.c b/gdb/block.c index 02e6d0e4be2bab2c1b3750a2f396e4bc97b7bc5d..2d8d40ec4eb94a1c891a3f8b48972eff3050e66d 100644 --- a/gdb/block.c +++ b/gdb/block.c @@ -842,6 +842,12 @@ blockvector::append_block (struct block *block) m_blocks.push_back (block); } +blockvector::~blockvector () +{ + for (struct block *bl : m_blocks) + mdict_free (bl->multidict ()); +} + /* Implement 'maint info blocks' command. If passed an argument then print a list of all blocks at the given address. With no arguments then list all blocks at the current address of the current inferior. */ diff --git a/gdb/block.h b/gdb/block.h index 61070619ca7daa7b3310a9c5cd8d6ca86c044ada..98ce5a3839dbb8419c9e3a918d27b3b58485fea4 100644 --- a/gdb/block.h +++ b/gdb/block.h @@ -424,6 +424,8 @@ struct blockvector : m_blocks (nblocks, nullptr) {} + ~blockvector (); + /* Return a view on the blocks of this blockvector. */ gdb::array_view<struct block *> blocks () { -- 2.49.0 ^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH 2/2] Free multidicts from blockvector 2025-10-23 17:17 ` [PATCH 2/2] Free multidicts from blockvector Tom Tromey @ 2025-10-23 18:31 ` Simon Marchi 2025-10-23 18:52 ` Tom Tromey 0 siblings, 1 reply; 11+ messages in thread From: Simon Marchi @ 2025-10-23 18:31 UTC (permalink / raw) To: Tom Tromey, gdb-patches On 10/23/25 1:17 PM, Tom Tromey wrote: > Currently, nothing in the tree ever calls mdict_free. However, code > does heap-allocate some multidicts. A simple way to see this is to > use valgrind, run "gdb -readnow" on the executable created by > gdb.dwarf2/struct-with-sig.exp, and then use "file" to clear the > objfile list. This yields: > > ==1522843== 144 (16 direct, 128 indirect) bytes in 1 blocks are definitely lost in loss record 905 of 3,005 > ==1522843== at 0x4843866: malloc (vg_replace_malloc.c:446) > ==1522843== by 0x48E397: xmalloc (alloc.c:52) > ==1522843== by 0x59DE66: multidictionary* xnew<multidictionary>() (poison.h:102) > ==1522843== by 0x59CFF4: mdict_create_hashed_expandable(language) (dictionary.c:965) > ==1522843== by 0x50A269: buildsym_compunit::finish_block_internal(symbol*, pending**, pending_block*, dynamic_prop const*, unsigned long, unsigned long, int, int) (buildsym.c:221) > ==1522843== by 0x50AE04: buildsym_compunit::end_compunit_symtab_get_static_block(unsigned long, int, int) (buildsym.c:818) > ==1522843== by 0x50C4CF: buildsym_compunit::end_expandable_symtab(unsigned long) (buildsym.c:1037) > ==1522843== by 0x61DBC6: process_full_type_unit (read.c:4970) > > This patch fixes the leaks by calling mdict_free when a blockvector is > destroyed. > --- > gdb/block.c | 6 ++++++ > gdb/block.h | 2 ++ > 2 files changed, 8 insertions(+) > > diff --git a/gdb/block.c b/gdb/block.c > index 02e6d0e4be2bab2c1b3750a2f396e4bc97b7bc5d..2d8d40ec4eb94a1c891a3f8b48972eff3050e66d 100644 > --- a/gdb/block.c > +++ b/gdb/block.c > @@ -842,6 +842,12 @@ blockvector::append_block (struct block *block) > m_blocks.push_back (block); > } > > +blockvector::~blockvector () > +{ > + for (struct block *bl : m_blocks) > + mdict_free (bl->multidict ()); > +} Ideally it would be struct block's just to call that. You could add a destructor to struct block and call it explicitly here. It would be the same result, but would be more correct encapsulation-wise. But I guess this is fine too for the time being. > + > /* Implement 'maint info blocks' command. If passed an argument then > print a list of all blocks at the given address. With no arguments > then list all blocks at the current address of the current inferior. */ > diff --git a/gdb/block.h b/gdb/block.h > index 61070619ca7daa7b3310a9c5cd8d6ca86c044ada..98ce5a3839dbb8419c9e3a918d27b3b58485fea4 100644 > --- a/gdb/block.h > +++ b/gdb/block.h > @@ -424,6 +424,8 @@ struct blockvector > : m_blocks (nblocks, nullptr) > {} > > + ~blockvector (); Since there is a user-defined destructor, I would add a DISABLE_COPY_AND_ASSIGN. Otherwise, LGTM. Approved-By: Simon Marchi <simon.marchi@efficios.com> Simon ^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH 2/2] Free multidicts from blockvector 2025-10-23 18:31 ` Simon Marchi @ 2025-10-23 18:52 ` Tom Tromey 2025-10-24 14:08 ` Tom Tromey 0 siblings, 1 reply; 11+ messages in thread From: Tom Tromey @ 2025-10-23 18:52 UTC (permalink / raw) To: Simon Marchi; +Cc: Tom Tromey, gdb-patches >>>>> "Simon" == Simon Marchi <simark@simark.ca> writes: >> + for (struct block *bl : m_blocks) >> + mdict_free (bl->multidict ()); >> +} Simon> Ideally it would be struct block's just to call that. You could add a Simon> destructor to struct block and call it explicitly here. It would be the Simon> same result, but would be more correct encapsulation-wise. But I guess Simon> this is fine too for the time being. Yeah. I have kind of an aversion to explicit destructor calls. Though maybe not for any good reason. Simon> Since there is a user-defined destructor, I would add a Simon> DISABLE_COPY_AND_ASSIGN. Otherwise, LGTM. I did this. Tom ^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH 2/2] Free multidicts from blockvector 2025-10-23 18:52 ` Tom Tromey @ 2025-10-24 14:08 ` Tom Tromey 2025-10-24 16:25 ` Simon Marchi 0 siblings, 1 reply; 11+ messages in thread From: Tom Tromey @ 2025-10-24 14:08 UTC (permalink / raw) To: Tom Tromey; +Cc: Simon Marchi, gdb-patches Simon> Ideally it would be struct block's just to call that. You could add a Simon> destructor to struct block and call it explicitly here. It would be the Simon> same result, but would be more correct encapsulation-wise. But I guess Simon> this is fine too for the time being. Tom> Yeah. I have kind of an aversion to explicit destructor calls. Tom> Though maybe not for any good reason. I was thinking about this last night and realized there's a bigger issue, which is that blocks are allocated on an obstack: struct block : public allocate_on_obstack<block> and block = new (&m_objfile->objfile_obstack) struct block; but allocate_on_obstack requires that the object be trivially destructible. Maybe this could be remedied by resurrecting Jan's obstack allocator; making an "obstack_ptr" class that explicitly calls the destructor but not 'delete'; and finally changing blockvector to hold these. Tom ^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH 2/2] Free multidicts from blockvector 2025-10-24 14:08 ` Tom Tromey @ 2025-10-24 16:25 ` Simon Marchi 2025-10-24 18:04 ` Tom Tromey 0 siblings, 1 reply; 11+ messages in thread From: Simon Marchi @ 2025-10-24 16:25 UTC (permalink / raw) To: Tom Tromey; +Cc: gdb-patches On 10/24/25 10:08 AM, Tom Tromey wrote: > Simon> Ideally it would be struct block's just to call that. You could add a > Simon> destructor to struct block and call it explicitly here. It would be the > Simon> same result, but would be more correct encapsulation-wise. But I guess > Simon> this is fine too for the time being. > > Tom> Yeah. I have kind of an aversion to explicit destructor calls. > Tom> Though maybe not for any good reason. > > I was thinking about this last night and realized there's a bigger > issue, which is that blocks are allocated on an obstack: > > struct block : public allocate_on_obstack<block> > > and > > block = new (&m_objfile->objfile_obstack) struct block; > > but allocate_on_obstack requires that the object be trivially > destructible. > > Maybe this could be remedied by resurrecting Jan's obstack allocator; > making an "obstack_ptr" class that explicitly calls the destructor but > not 'delete'; and finally changing blockvector to hold these. I guess that this restriction of "objects allocated on obstack must be trivially destructible" is a bit artificial and arbitrary? It is technically fine to allocate on obstack an object having a destructor, as long as you ensure the destructor is called at some point. I guess the restriction was put because it's easy to forget and error prone. This obstack_ptr you talk about (obstack_unique_ptr?) sounds like a good way to automate it. I was wondering if we needed to keep allocating these on an obstack, but my understanding is that obstack is a good choice here because: - We can't use a vector<block> to store them, since the blocks themselves can't move, as that would break the parent (superblock) relationship - There is a ton of blocks created, so it's nice to be able to free their memory in O(1). Unfortunately, having to free block::m_multidict means that part is O(n), but the less we have to do in O(n) the better I guess? Simon ^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH 2/2] Free multidicts from blockvector 2025-10-24 16:25 ` Simon Marchi @ 2025-10-24 18:04 ` Tom Tromey 0 siblings, 0 replies; 11+ messages in thread From: Tom Tromey @ 2025-10-24 18:04 UTC (permalink / raw) To: Simon Marchi; +Cc: Tom Tromey, gdb-patches Simon> I guess that this restriction of "objects allocated on obstack must be Simon> trivially destructible" is a bit artificial and arbitrary? It is Simon> technically fine to allocate on obstack an object having a destructor, Simon> as long as you ensure the destructor is called at some point. I guess Simon> the restriction was put because it's easy to forget and error prone. I think the specific difference from ordinary 'new' is that valgrind will detect a new-based memory leak, but not an obstack-based leak. So, in the heap case, the leak will also point out that the destructor isn't being run; whereas in the obstack case it could go completely unnoticed. Simon> - There is a ton of blocks created, so it's nice to be able to free Simon> their memory in O(1). Unfortunately, having to free Simon> block::m_multidict means that part is O(n), but the less we have to Simon> do in O(n) the better I guess? We could also track the ownership of the dictionaries separately, e.g. with unordered_set<mdict_unique_ptr> or similar. This would reduce the cost if it matters. Tom ^ permalink raw reply [flat|nested] 11+ messages in thread
end of thread, other threads:[~2025-10-24 18:05 UTC | newest] Thread overview: 11+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2025-10-23 17:17 [PATCH 0/2] Fix memory leak with .debug_types Tom Tromey 2025-10-23 17:17 ` [PATCH 1/2] Two bug fixes in mdict_free Tom Tromey 2025-10-23 18:26 ` Simon Marchi 2025-10-23 18:32 ` Simon Marchi 2025-10-23 18:49 ` Tom Tromey 2025-10-23 17:17 ` [PATCH 2/2] Free multidicts from blockvector Tom Tromey 2025-10-23 18:31 ` Simon Marchi 2025-10-23 18:52 ` Tom Tromey 2025-10-24 14:08 ` Tom Tromey 2025-10-24 16:25 ` Simon Marchi 2025-10-24 18:04 ` Tom Tromey
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox