PerlMemoryLeakSelfExecHack

19th March 2015 at 11:06pm
CodeSnippets Perl

I'm not proud of this, but it's kinda interesting to share regardless. You should probably never do this in real life,.. or in the immortal words of LinusTechTips "do as we say, not as we do"! ;o)

This was a very hacky solution that was put in to a product that had a slow memory leak that was caused by a piece of compiled XS code that we no longer had the source for. I like to think it was a short-term fix, but I wouldn't be surprised if it was still in use today all these years later.

The code essentially does the following:

  • Intentionally eats up more and more memory to simulate the memory leak I described above
  • Monitors its memory usage using Devel::Size
  • When it decides it is using too much memory, it:
    • Serialises its state to an anonymous file on disk, taking care not to close the file handle
    • Replaces itself by re-exec'ing another instance of itself, passing in the file descriptor number of the open file handle to the state file
    • Seeks back to the beginning of the state file and reads back in the serialised state
    • Rise and repeat
#!/usr/bin/perl -w

use strict;
use warnings;
use Storable qw();
use Getopt::Std qw();
use Data::Dumper qw();
use Fcntl qw();
use Devel::Size qw();

BEGIN {
    use Cwd qw();
    use constant CWD => Cwd::cwd();
    use constant ARGV0 => $0;
    use constant ARGVN => @ARGV;
    use constant MAX_BYTES => 1024 * 1024; # 1MB
}

my $state_fh;
my $opts = {};
Getopt::Std::getopts('F:',$opts);

my $state = $opts->{F} ? load_state($opts->{F}) : { age => 0 };
print '$state ',Data::Dumper::Dumper($state);

INPUT: {
    $state->{input} ||= [()];
    local $| = 1;
    print "Give me some state information please: ";
    local $_ = <>; chomp;
    push @{$state->{input}}, $_ if /\S/;

    my $stuff = [];
    do {
        print '.';
        push @{$stuff}, 'X' x 102400;
        sleep 1;
    } while Devel::Size::total_size($stuff) <= MAX_BYTES;

    print "\nWe're using too much memory; respawning ...\n";
    eval { self_exec($state); };
    die "Bugger! $@" if $@; # Do something better
}

print Devel::Size::total_size($state)."\n";
exit;

sub self_exec {
    my $state = shift;
    my $fileno = save_state($state);
    my @ARGV = ARGVN;
    shift(@ARGV),shift(@ARGV) if @ARGV >= 2 && $ARGV[0] eq '-F';
    chdir(CWD) || die sprintf("Unable to chdir to '%s': %s",CWD,$!);
    exec(ARGV0,'-F',$fileno,@ARGV) ||
        die sprintf("Unable to exec '%s': %s",join("','",ARGV0,@ARGV),$1);
}

sub save_state {
    my $state = shift;
    if (ref($state_fh) eq 'GLOB') {
        seek($state_fh,0,0);
        truncate($state_fh,0);
    } else {
        open($state_fh,'+>',undef) ||
            die "Unable to open anonymous state file handle: $!";
    }
    fcntl($state_fh, Fcntl::F_SETFD(), 0) ||
        die "Can't clear close-on-exec flag on temp fh: $!";
    Storable::store_fd($state,$state_fh);
    return fileno($state_fh);
}

sub load_state {
    my $fileno = shift;
    my $state = {};
    if (open($state_fh,'+<&=',$fileno)) {
        seek($state_fh,0,0);
        $state = Storable::fd_retrieve($state_fh);
        $state->{age}++;
    } else {
        warn "Unable to read state from fileno '$fileno': $!";
    }
    return $state;
}

__END__