Closed p5pRT closed 13 years ago
When debuging a core dump in some XS code i reduced it to what seems to indicate a long standing bug in deleting elements in a HASH that is being iterated over.
Internally a perl HASH is an array of single linked chains of entries. Deleting an element means removing the correct chain entry by replacing the pointer to the removed entry with a pointer to the next entry and then freeing the deleted entry
However\, if the deleted element is the current entry the deleted entry is kept after removing it from the chain and the LAZYDEL flag is set. Only on the next iteration is the element actually removed and the iterator is set to the next entry.
However\, if you delete the current iterator and then delete the next element in the same chain the "next" pointer of the iterator is not updated because the iterator is not on the chain anymore. That means that when the next iteration looks up the iterator next pointer it will point to the freed memory of the second element.
This is easily demonstrated with the next program. It first uses Devel::Peek to find a chain with at least two elements\, then iterates there and deletes these two elements. The next iteration then causes a freed memory warning (in my real code more had happened with the memory at that point and I ended up with a core dump).
======================================================== #!/usr/bin/perl -w use strict; use Devel::Peek; use File::Temp qw(tempdir);
my %hash = ("a".."z");
my $tmp_dir = tempdir(CLEANUP => 1);
sub riter { local *OLDERR; open(OLDERR\, ">&STDERR") || die "Can't dup STDERR: $!"; open(STDERR\, ">"\, "$tmp_dir/dump") || die "Could not open '$tmp_dir/dump' for write: $^E"; Dump(\%hash); open(STDERR\, ">&OLDERR") || die "Can't dup OLDERR: $!"; open(my $fh\, "\<"\, "$tmp_dir/dump") || die "Could not open '$tmp_dir/dump' for read: $^E"; local $/; my $dump = \<$fh>; my ($riter) = $dump =~ /^\s*RITER\s*=\s*(\d+)/m or die "No plain RITER in dump '$dump'"; return $riter; }
my @riters; while (my $key = each %hash) { push @{$riters[riter()]}\, $key; }
my ($first_key\, $second_key); my $riter = 0; for my $chain (@riters) { if ($chain && @$chain >= 2) { $first_key = $chain->[0]; $second_key = $chain->[1]; last; } $riter++; } $first_key || die "No 2 element chains. Please retry with a different intial HASH"; $| = 1;
# Ok all preparation is done print \<\<"EOF" Found keys '$first_key' and '$second_key' on chain $riter Will now iterato to key '$first_key' then delete '$first_key' and '$second_key'. EOF ; 1 until $first_key eq each %hash; delete $hash{$first_key}; delete $hash{$second_key};
Most of that code above is to analyze the HASH chains. But once you know the keys on a 2-element (or more) chain the problem can be demonstrated directly too. E.g. on my system the HASH function works out so that the following code:
perl -wle '%a=("a".."z"); each %a; delete $a{w}; delete $a{e}; 1 for each %a'
will give:
Use of freed value in iteration at -e line 1
The fix is not so clear. Ideas would be: - Don't remove lazydel elements from the entry chain. Then its next pointer would always be upto date. But all iteration code would have to be fixed to skip the deleted iterator and getting the PL_sv_placeholder code right could be a bit tricky for restricted hashes. Still\, I think this would be the cleanest fix\, but it will break existing code that thinks it knows howe to iterate hash entries by itself - Fix hv_free_ent This is good because it would put all the fixup in one place. But it would implicitely assume that by the time an entry is passed to hv_free_ent its next pointer should still be valid. It also means doing the extra test even though the place it gets called is one where the lazydel problem can't happen (which is quite common) - Fix the places where the delete is done Drawback is that you may never forget to do the lazydel fixup in at any place where the entry chain gets shortened Anyways\, this is what I used in my sample fix. - Always delete the current entry but if it is the one in the iterator\, replace EITER by the next address \, and LAZYDEL just means that the EITER poiner is already increased. That still needs code to update the EITER pointer if THAT entry is deleted\, but it still leads to simpler code needing less HeNEXT()
Looking at the delete code I see another strange thing. If the delete of the current iterator is done with the G_DISCARD flag\, the corresponding value is not freed and survives until the lazy deleted entry gets removed on the next hash iteration. This is easily demonstrated like this:
perl -wle ' sub DESTROY { print "DESTROY" } %a=(a=>bless[]); each %a; delete $a{a}; print "END" '
This prints:
END DESTROY
notice the difference with:
perl -wle ' sub DESTROY { print "DESTROY" } %hash = (a => bless[]); each %hash; $dummy = delete $hash{a}; $dummy = 0; print "END" '
This prints:
DESTROY END
This is easily solved by always replacing the deleted entry value with &PL_sv_placeholder. It actually simplifies the code a bit except for the fact that the mro_method_changed from free_hash_ent now has to be done in hv_delete
A further note: while debugging this issue it was annoying that Devel::Peek::Dump doesb't actually dump the HASH elements when an iterator is active. Also added is a patch that does the iteration to dump the HASH contents by iterating over it by hand (not disturbing any active iterator). With that it also doesn't activate hash magic during iteration\, which I think is a feature
Next a few proposed patches solving all this. They are for perl 5.10.1\, (a quick look at blead indicates the code there seems unchanged\, so I think all this is still relevant)
============================================================
============================================================
On Sun Feb 27 09:53:44 2011\, perl@ton.iguana.be wrote:
Patch for deleting the element after a delete iterator
Thank you. I’ve applied your first patch to the sprout/5.14.1-85026 branch. I’ll look at the others later.
The RT System itself - Status changed from 'new' to 'open'
On Mon May 16 18:09:46 2011\, sprout wrote:
On Sun Feb 27 09:53:44 2011\, perl@ton.iguana.be wrote:
Patch for deleting the element after a delete iterator
Thank you. I’ve applied your first patch to the sprout/5.14.1-85026 branch. I’ll look at the others later.
I’ve now applied it to blead as ae19993915.
I’ve also applied your second patch as f50383f5.
@cpansprout - Status changed from 'open' to 'resolved'
@cpansprout - Status changed from 'resolved' to 'open'
Oops. I didn’t mean to resolve that yet.
On Thu May 19 19:42:06 2011\, sprout wrote:
On Mon May 16 18:09:46 2011\, sprout wrote:
On Sun Feb 27 09:53:44 2011\, perl@ton.iguana.be wrote:
Patch for deleting the element after a delete iterator
Thank you. I’ve applied your first patch to the sprout/5.14.1-85026 branch. I’ll look at the others later.
I’ve now applied it to blead as ae19993915.
I’ve also applied your second patch as f50383f5.
I forgot to mention that I changed HvNAME_get to HvENAME_get.
On Sun Feb 27 09:53:44 2011\, perl@ton.iguana.be wrote:
A further note: while debugging this issue it was annoying that Devel::Peek::Dump doesb't actually dump the HASH elements when an iterator is active. Also added is a patch that does the iteration to dump the HASH contents by iterating over it by hand (not disturbing any active iterator). With that it also doesn't activate hash magic during iteration\, which I think is a feature
...
Patch to iterate a hash by hand during do_sv_dump:
--- /home/ton/src/perl-5.10.1/dump.c 2009-07-09 17:00:51.000000000 +0200 +++ dump.c 2011-02-27 14:59:18.516122042 +0100
Thank you. Applied as b56985536ef. This also fixes all the to-do tests in ext/Devel-Peek/t/Peek.t\, so I’ve removed the TODO markers with commit f3ce8053656a.
@cpansprout - Status changed from 'open' to 'resolved'
Migrated from rt.perl.org#85026 (status was 'resolved')
Searchable as RT85026$