perl5 icon indicating copy to clipboard operation
perl5 copied to clipboard

Some closures leak memory in v5.40

Open mauke opened this issue 1 year ago • 10 comments

Description Starting in v5.40, some closures leak memory despite there being no reference cycles.

Steps to Reproduce

#!/usr/bin/env perl
use v5.38;
use Scalar::Util qw(weaken);

my $wref;
{
    my $x;
    my $subject = sub {
        $x = $_[0];

        my $y;
        return sub { $y };
    };
    my $subscriber = {};
    weaken($wref = $subscriber);
    $subscriber->{foo} = $subject->($subscriber);
}
!defined $wref or die "Leak";
$ perl bleak.pl 
Leak at bleak.pl line 18.
$ 

This used to work in v5.38. It is broken in v5.40 and blead.

Expected behavior

$ perl bleak.pl
$ 

mauke avatar Aug 28 '24 10:08 mauke

Bisect says:

386907f061c1812ecaa5f3c88d9f729828408097 is the first bad commit

commit 386907f061c1812ecaa5f3c88d9f729828408097
Author: Tony Cook <[email protected]>
Date:   Thu Sep 14 10:37:42 2023 +1000

    Revert "[perl #89544] Non-eval closures don’t need CvOUTSIDE"
    
    But they do need CvOUTSIDE() for eval-in-package-DB's scope
    magic to work correctly.
    
    This also incidentally fixes a TODO Deparse, since the CvOUTSIDE
    is now available
    
    Fixes #19370 aka rt89544
    
    This breaks #11286, but I don't think that's fixable without breaking
    eval-in-DB's special scope behaviour.
    
    This reverts (most of) commit a0d2bbd5c47035a4f7369e4fddd46b502764d86e.

 cv.h                    | 6 +-----
 dump.c                  | 1 -
 ext/Devel-Peek/t/Peek.t | 8 ++++----
 lib/B/Deparse.t         | 1 -
 pad.c                   | 4 +---
 t/op/closure.t          | 3 ++-
 t/op/eval.t             | 1 -
 7 files changed, 8 insertions(+), 16 deletions(-)

mauke avatar Aug 28 '24 10:08 mauke

Bisect says:

386907f is the first bad commit

Confirmed. It went into blead on Sep 25.

commit 386907f061c1812ecaa5f3c88d9f729828408097
Author:     Tony Cook <[email protected]>
AuthorDate: Thu Sep 14 10:37:42 2023 +1000
Commit:     Tony Cook <[email protected]>
CommitDate: Mon Sep 25 10:24:07 2023 +1000

@tonycoz can you take a look? Thanks.

jkeenan avatar Aug 28 '24 11:08 jkeenan

The script shown here detects a leak on perls before a0d2bbd5c47035a4f7369e4fddd46b502764d86e (5.16).

haarg avatar Aug 29 '24 15:08 haarg

If we can fix, it should be backported to perl-5.40.1.

jkeenan avatar Aug 29 '24 19:08 jkeenan

Yes, my change reverted this to pre-5.18 behaviour, since a0d2bbd broke eval in package DB, which is a documented feature.

I asked for guidance on the list and received no feedback that could resolve the issue.

The only feedback I received on the PR #21489 was to apply the reversion and consider mitigations, of which I didn't see any

Note that if your closure contains a string eval you'll still see the reference loop.

tonycoz avatar Sep 01 '24 23:09 tonycoz

I've been thinking about this. I now think your example:

use strict;
package DB { sub myeval { eval shift or die } }
my $y = 1; # something true so eval in myeval above is true
my $has_outer = sub { $y; my $x; sub { DB::myeval '$y'; eval "";  ++$x; } }->();
$has_outer->(); # has an outside pointer and can resolve $y
my $no_outer  = sub { $y; my $x; sub { DB::myeval '$y';           ++$x; } }->();
$no_outer->(); # dies since we can't see $y

... should die in either case, no matter if there is an eval there or not. The inner sub shouldn't be able to resolve $y at runtime.

Rationale: A closure should only capture variables whose names appear lexically in the sub's body. Capturing more is not just inefficient (using more memory than expected), but can introduce leaks due to hidden reference cycles. Runtime code from eval should see the surrounding lexical scope, but if a lexical variable from an outer context is not directly referenced in the statically visible part of the code, I'd argue that it is not really part of the inner (captured) scope.

It is nice if attempts to access such a variable at runtime work anyway or trigger a Variable "$foo" is not available warning, as with this example from perldiag:

use strict;
use warnings;

sub f {
    my $x;
    sub { eval '$x' }
}
f()->();
# Variable "$x" is not available at (eval 1) line 1.

But I'm totally fine with getting an undeclared variable error from either static (lexically present in the inner sub) or dynamic (via a call to a sub in package DB) eval.

mauke avatar Sep 03 '24 12:09 mauke

What if we add a $^P flag for this?

Leont avatar Sep 03 '24 18:09 Leont

What if we add a $^P flag for this?

Then your code behaves differently in terms of references in the debugger, which makes the debugger not fit for purpose.

tonycoz avatar Sep 03 '24 23:09 tonycoz

But I'm totally fine with getting an undeclared variable error from either static (lexically present in the inner sub) or dynamic (via a call to a sub in package DB) eval.

The same mechanism is used to search for lexical subs, so outer lexical subs would also fail to be found in an eval.

tonycoz avatar Sep 10 '24 04:09 tonycoz

Given the choice between the 5.38 and 5.40 behaviors, I think I would prefer to revert to 5.38. If a closure doesn't capture a lexical, it can't expect it to be available. And this includes lexical subs. Implicitly capturing everything, leading to reference loops and lifetime issues, for the sake of the debugger seems like the wrong approach. And given how long the behavior existed before there were any complaints, it seems like it has a smaller impact.

If there was an option to use weak references, as implied by https://github.com/Perl/perl5/pull/21489#issuecomment-1725443594, that would probably be preferable to either of the behaviors that have been implemented so far.

haarg avatar Sep 30 '24 08:09 haarg

Sorry for the delay, I've added the extra tests I wanted to #22635

tonycoz avatar Nov 18 '24 02:11 tonycoz

This looks fixed.

book avatar Apr 03 '25 15:04 book

Did a perldelta entry get added for this?

karenetheridge avatar Jun 06 '25 19:06 karenetheridge

It is in perldelta

khwilliamson avatar Jun 29 '25 03:06 khwilliamson