From be3b3b1085f586dc6d5a54ba013b9a9913c125cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Salvador=20Fandi=C3=B1o?= Date: Thu, 4 Jun 2026 13:45:48 +0200 Subject: [PATCH 1/2] Fix connection cache eviction --- MANIFEST | 1 + lib/Net/OpenSSH/ConnectionCache.pm | 18 ++++++------ t/connection-cache.t | 46 ++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 9 deletions(-) create mode 100644 t/connection-cache.t diff --git a/MANIFEST b/MANIFEST index 7def16c..ec9eda7 100644 --- a/MANIFEST +++ b/MANIFEST @@ -18,6 +18,7 @@ lib/Net/OpenSSH/ShellQuoter/MSCmd.pm lib/Net/OpenSSH/ShellQuoter/Chain.pm lib/Net/OpenSSH/ObjectRemote.pm t/common.pm +t/connection-cache.t t/test_server_key t/test_server_key.pub t/test_user_key diff --git a/lib/Net/OpenSSH/ConnectionCache.pm b/lib/Net/OpenSSH/ConnectionCache.pm index e6130f0..3011388 100644 --- a/lib/Net/OpenSSH/ConnectionCache.pm +++ b/lib/Net/OpenSSH/ConnectionCache.pm @@ -27,15 +27,15 @@ sub _factory { } } if ($MAX_SIZE <= keys %cache) { - for (keys %cache) { - $ssh = $cache{$_}; - $ssh or $ssh->error != OSSH_MASTER_FAILED or delete $cache{$_} - } - for (keys %cache) { - last if ($MAX_SIZE <= keys %cache); - weaken $cache{$_}; - if (defined $cache{$_}) { - $cache{$_} = $cache{$_}; # unweaken + for (keys %cache) { + $ssh = $cache{$_}; + delete $cache{$_} unless $ssh and $ssh->error != OSSH_MASTER_FAILED; + } + for (keys %cache) { + last if (keys %cache < $MAX_SIZE); + weaken $cache{$_}; + if (defined $cache{$_}) { + $cache{$_} = $cache{$_}; # unweaken } else { delete $cache{$_}; diff --git a/t/connection-cache.t b/t/connection-cache.t new file mode 100644 index 0000000..e71cafa --- /dev/null +++ b/t/connection-cache.t @@ -0,0 +1,46 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use Test::More tests => 7; +use Net::OpenSSH::Constants qw(OSSH_MASTER_FAILED); +use Net::OpenSSH::ConnectionCache; + +{ + package Test::Net::OpenSSH::ConnectionCache::SSH; + + sub new { + my $class = shift; + return bless { @_ }, $class; + } + + sub error { shift->{error} || 0 } + sub wait_for_master { 1 } +} + +local $Net::OpenSSH::ConnectionCache::MAX_SIZE = 2; +local %Net::OpenSSH::ConnectionCache::cache = ( + live => Test::Net::OpenSSH::ConnectionCache::SSH->new(error => 0), + failed => Test::Net::OpenSSH::ConnectionCache::SSH->new(error => OSSH_MASTER_FAILED), + empty => undef, +); + +my $ssh; +eval { + $ssh = Net::OpenSSH::ConnectionCache::_factory( + 'Test::Net::OpenSSH::ConnectionCache::SSH', + host => 'cache-test-host' + ); +}; + +is($@, '', 'cache cleanup does not die on empty entries'); +ok($ssh, 'factory returns a new object'); +ok(!exists $Net::OpenSSH::ConnectionCache::cache{failed}, 'failed master entry is removed'); +ok(!exists $Net::OpenSSH::ConnectionCache::cache{empty}, 'empty cache entry is removed'); +ok(exists $Net::OpenSSH::ConnectionCache::cache{live}, 'live cache entry is retained'); +ok((keys %Net::OpenSSH::ConnectionCache::cache) <= $Net::OpenSSH::ConnectionCache::MAX_SIZE, + 'cache is reduced below the configured maximum'); + +Net::OpenSSH::ConnectionCache::clean_cache(); +is(scalar keys %Net::OpenSSH::ConnectionCache::cache, 0, 'clean_cache clears cache'); From 195af616105e5b8dc11cea21593256d59d9344f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Salvador=20Fandi=C3=B1o?= Date: Thu, 4 Jun 2026 16:33:55 +0200 Subject: [PATCH 2/2] Address connection cache review comments --- lib/Net/OpenSSH/ConnectionCache.pm | 18 +++++++++--------- t/connection-cache.t | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/Net/OpenSSH/ConnectionCache.pm b/lib/Net/OpenSSH/ConnectionCache.pm index 3011388..551a379 100644 --- a/lib/Net/OpenSSH/ConnectionCache.pm +++ b/lib/Net/OpenSSH/ConnectionCache.pm @@ -27,15 +27,15 @@ sub _factory { } } if ($MAX_SIZE <= keys %cache) { - for (keys %cache) { - $ssh = $cache{$_}; - delete $cache{$_} unless $ssh and $ssh->error != OSSH_MASTER_FAILED; - } - for (keys %cache) { - last if (keys %cache < $MAX_SIZE); - weaken $cache{$_}; - if (defined $cache{$_}) { - $cache{$_} = $cache{$_}; # unweaken + for (keys %cache) { + $ssh = $cache{$_}; + delete $cache{$_} unless $ssh and $ssh->error != OSSH_MASTER_FAILED; + } + for (keys %cache) { + last if (keys %cache < $MAX_SIZE); + weaken $cache{$_}; + if (defined $cache{$_}) { + $cache{$_} = $cache{$_}; # unweaken } else { delete $cache{$_}; diff --git a/t/connection-cache.t b/t/connection-cache.t index e71cafa..52b5360 100644 --- a/t/connection-cache.t +++ b/t/connection-cache.t @@ -40,7 +40,7 @@ ok(!exists $Net::OpenSSH::ConnectionCache::cache{failed}, 'failed master entry i ok(!exists $Net::OpenSSH::ConnectionCache::cache{empty}, 'empty cache entry is removed'); ok(exists $Net::OpenSSH::ConnectionCache::cache{live}, 'live cache entry is retained'); ok((keys %Net::OpenSSH::ConnectionCache::cache) <= $Net::OpenSSH::ConnectionCache::MAX_SIZE, - 'cache is reduced below the configured maximum'); + 'cache is reduced to the configured maximum or below'); Net::OpenSSH::ConnectionCache::clean_cache(); is(scalar keys %Net::OpenSSH::ConnectionCache::cache, 0, 'clean_cache clears cache');