Net-LibSSH

 view release on metacpan or  search on metacpan

t/07-refcount-chain.t  view on Meta::CPAN

use strict;
use warnings;
use lib 't/lib';
use Test::More;
use TestSSHD;
use Net::LibSSH;
use POSIX ();

# The session refcount chain is what keeps the underlying ssh_session
# alive when the Perl-level session variable goes away while an object
# opened on it -- a channel or an SFTP session -- is still in use.
# NLSS_Channel and NLSS_SFTP each take a reference on the session's
# blessed, magic-bearing SV at construction and release it in their
# svt_free.
#
# There are three ways to lose the session variable, and all three have
# to keep the session alive: it falls out of scope, it is undef-ed, or
# it is assigned something else. Up to 0.002 the reference was taken on
# ST(0) -- the reference scalar itself rather than the referent it
# points at -- so only the scope case survived. The other two dropped
# the referent's refcount to zero, ssh_free()d the session under a live
# channel, and segfaulted on the next call into libssh.
#
# Every scenario runs in a forked child, so a regression is reported as
# a failing test instead of taking `prove` down with a SIGSEGV.

my $srv = TestSSHD->start;
plan skip_all => 'sshd or ssh-keygen not available' unless $srv;

sub connect_session {
    my $ssh = Net::LibSSH->new;
    $ssh->option(host       => $srv->host);
    $ssh->option(port       => $srv->port);
    $ssh->option(user       => scalar getpwuid($<));
    $ssh->option(knownhosts => '/dev/null');
    $ssh->connect
        or die 'connect: ' . ($ssh->error // '') . "\n";
    $ssh->auth_publickey($srv->client_key)
        or die 'auth: ' . ($ssh->error // '') . "\n";
    return $ssh;
}

# Open $what on a fresh session, then lose the session variable the way
# $mode says. In every mode the session variable is gone by the time the
# returned object is handed back -- 'scope' just lets the enclosing block
# end without saying anything about it.
sub open_and_drop {
    my ($what, $mode) = @_;
    return do {
        my $ssh = connect_session();
        my $obj = $what eq 'channel' ? $ssh->channel : $ssh->sftp;
        undef $ssh if $mode eq 'undef';
        $ssh = 42  if $mode eq 'reassign';
        $obj;
    };
}

# Run $code in a forked child and compare the one line it writes back
# with $expect (a string, or a coderef called with the child's pid).
sub run_in_child {
    my ($name, $code, $expect) = @_;
    pipe(my $rd, my $wr) or die "pipe: $!";
    my $pid = fork();
    die "fork: $!" unless defined $pid;

    if ($pid == 0) {
        close $rd;
        my $ok = eval { print {$wr} $code->() . "\n"; 1 };
        print {$wr} "ERROR|$@\n" unless $ok;
        close $wr;
        # POSIX::_exit skips Perl's global destruction. $srv (TestSSHD) was
        # inherited from the parent across fork() and shares the parent's sshd
        # pid; letting this child run $srv's DESTROY too would send a second
        # SIGTERM and waitpid() a process the parent still owns.
        POSIX::_exit($ok ? 0 : 1);
    }

    close $wr;
    my $line = <$rd>;
    close $rd;
    waitpid($pid, 0);
    my $signal = $? & 127;

    if ($signal) {
        my $signame = $signal == 11 ? 'SIGSEGV'
                    : $signal == 6  ? 'SIGABRT'
                    :                 "signal $signal";
        fail($name);
        diag("child was killed by $signame -- the session refcount chain did "
            . "not keep the underlying ssh_session alive while an object "
            . "opened on it was still in use.");
        return;
    }



( run in 0.392 second using v1.01-cache-2.11-cpan-7f9471e7e0a )