Net-LibSSH
view release on metacpan or search on metacpan
t/08-disconnect-invalidates.t view on Meta::CPAN
use strict;
use warnings;
use lib 't/lib';
use Test::More;
use TestSSHD;
use Net::LibSSH;
use POSIX ();
# ssh_disconnect() walks session->channels and frees every one of them inside
# libssh. disconnect() used to hand that straight through, so after it every
# live Net::LibSSH::Channel held a dangling ssh_channel and every
# Net::LibSSH::SFTP an sftp_session whose channel was gone.
#
# nlss_channel_check_open() could not catch it: self->channel is non-NULL, it
# just points at freed memory. The three measured crashes were
#
# $ssh->disconnect; $ch->exec(...) -> SIGSEGV
# $ssh->disconnect; undef $ch -> SIGSEGV (svt_free -> send_eof)
# $ssh->disconnect; $sftp->stat(...) -> SIGSEGV
#
# The second is the worse one: it crashes over the GC path without the caller
# ever touching the channel again, so a program that disconnects and then
# simply ends segfaults while cleaning up.
#
# The session now carries a generation counter that disconnect() bumps and
# that every channel and sftp session copies at construction. A mismatch means
# libssh has already freed the C object underneath: methods croak naming the
# session, and svt_free skips the C teardown while still releasing the session
# reference and the struct.
#
# Every scenario runs in a forked child, so a regression is a failing test
# instead of `prove` going 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;
}
# Run $code in a forked child and check the one line it writes back against
# $expect (a string compared for equality, or a Regexp matched against it).
#
# With global_destruct => 1 the child leaves through a normal exit() so Perl's
# global destruction runs -- that is the "program calls disconnect() and then
# ends" case. Everywhere else the child uses POSIX::_exit to skip it, because
# $srv was inherited across fork() and shares the parent's sshd pid.
sub run_in_child {
my ($name, $code, $expect, %opt) = @_;
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|' . _oneline($@) . "\n" unless $ok;
close $wr;
if ($opt{global_destruct}) {
# Let global destruction run, but keep this child from taking the
# parent's sshd down with it and from emitting a second TAP plan.
$srv->{pid} = undef;
Test::Builder->new->no_ending(1);
exit($ok ? 0 : 1);
}
POSIX::_exit($ok ? 0 : 1);
}
close $wr;
my $line = <$rd>;
close $rd;
waitpid($pid, 0);
my $status = $?;
my $signal = $status & 127;
if ($signal) {
my $signame = $signal == 11 ? 'SIGSEGV'
: $signal == 6 ? 'SIGABRT'
: "signal $signal";
fail($name);
diag("child was killed by $signame -- disconnect() left a dangling "
. "libssh object behind and it was used or freed afterwards.");
return;
}
my $got = defined $line ? do { my $l = $line; chomp $l; $l }
: '(undef -- child produced no output on the pipe)';
if (ref $expect eq 'Regexp') {
like $got, $expect, $name;
}
else {
( run in 0.470 second using v1.01-cache-2.11-cpan-7f9471e7e0a )