From 89ee635de3c1c047de87981d21f3457e26c33885 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Thu, 8 Jan 2026 14:37:57 +0200 Subject: [PATCH 1/4] Fix key order --- sshd/edit_sync.cgi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sshd/edit_sync.cgi b/sshd/edit_sync.cgi index f3c27375e..af45c5d97 100755 --- a/sshd/edit_sync.cgi +++ b/sshd/edit_sync.cgi @@ -25,7 +25,7 @@ print &ui_table_row($text{'sync_pass'}, print &ui_table_row($text{'sync_type'}, &ui_select("type", $config{'sync_type'}, [ [ "", $text{'sync_auto'} ], - [ "rsa" ], [ "dsa" ], [ "rsa1" ], [ "ed25519" ] ])); + [ "ed25519" ], [ "rsa" ], [ "dsa" ], [ "rsa1" ] ])); print &ui_table_end(); print &ui_form_end([ [ undef, $text{'save'} ] ]); From 12dca80535ce544a4d53641549111e00b8b25cf8 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Thu, 8 Jan 2026 16:40:44 +0200 Subject: [PATCH 2/4] Fix to improve fork handling and zombie reaping *Note: Even though the current code generally works, it's better to properly reap child processes with waitpid to avoid infinite timeouts and also clean up inherited FDs. --- fastrpc.cgi | 79 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 30 deletions(-) diff --git a/fastrpc.cgi b/fastrpc.cgi index 99e322927..4b81c33a9 100755 --- a/fastrpc.cgi +++ b/fastrpc.cgi @@ -78,22 +78,30 @@ else { die "No connection on any socket!"; } die "accept failed : $!" if (!$acptaddr); -$oldsel = select(SOCK); +my $oldsel = select(SOCK); $| = 1; select($oldsel); -$rcount = 0; -@childpids = ( ); +my $rcount = 0; +my %xfer_kids; while(1) { + foreach my $p (keys %xfer_kids) { + my $waited_pid = waitpid($p, POSIX::WNOHANG()); + delete($xfer_kids{$p}) if ($waited_pid > 0 || $waited_pid == -1); + } # Wait for the request. Wait longer if this isn't the first one my $rmask; vec($rmask, fileno(SOCK), 1) = 1; - @childpids = grep { kill(0, $_) } @childpids; - my $timeout = @childpids ? undef : - $gconfig{'rpc_timeout'} ? $gconfig{'rpc_timeout'} : - $rcount ? 360 : 60; + my $timeout = $gconfig{'rpc_timeout'} + ? $gconfig{'rpc_timeout'} + : $rcount + ? 360 + : 60; my $sel = select($rmask, undef, undef, $timeout); if ($sel <= 0) { + # Don't kill the control session while a tcpwrite/tcpread is + # running + next if (keys %xfer_kids); print STDERR "fastrpc: session timed out\n" if ($gconfig{'rpcdebug'}); last; @@ -116,7 +124,6 @@ while(1) { # Process it my $rv; - my $tcppid; if ($arg->{'action'} eq 'ping') { # Just respond with an OK print STDERR "fastrpc: ping\n" if ($gconfig{'rpcdebug'}); @@ -157,10 +164,17 @@ while(1) { my $tsock6 = $use_ipv6 ? time().$$."v6" : undef; my $tport = $port + 1; &allocate_socket($tsock, $tsock6, \$tport); - $tcppid = fork(); - if (!$tcppid) { + my $cpid = fork(); + if (!defined($cpid)) { + $rv = { 'status' => 0, 'rv' => "fork() failed : $!" }; + } + elsif ($cpid == 0) { + close(SOCK); + close(MAIN); + close(MAIN6) if ($use_ipv6); # Accept connection in separate process - print STDERR "fastrpc: tcpwrite $file port $tport\n" if ($gconfig{'rpcdebug'}); + print STDERR "fastrpc: tcpwrite $file port $tport\n" + if ($gconfig{'rpcdebug'}); my $rmask; vec($rmask, fileno($tsock), 1) = 1; if ($use_ipv6) { @@ -199,10 +213,14 @@ while(1) { close(TRANS); exit; } + else { + $xfer_kids{$cpid} = 1; + print STDERR "fastrpc: tcpwrite $file started\n" + if ($gconfig{'rpcdebug'}); + $rv = { 'status' => 1, 'rv' => [ $file, $tport ] }; + } close($tsock); - close($tsock6); - print STDERR "fastrpc: tcpwrite $file done\n" if ($gconfig{'rpcdebug'}); - $rv = { 'status' => 1, 'rv' => [ $file, $tport ] }; + close($tsock6) if ($use_ipv6); } elsif ($arg->{'action'} eq 'read') { # Transfer data from a file @@ -232,8 +250,14 @@ while(1) { my $tsock6 = $use_ipv6 ? time().$$."v6" : undef; my $tport = $port + 1; &allocate_socket($tsock, $tsock6, \$tport); - $tcppid = fork(); - if (!$tcppid) { + my $cpid = fork(); + if (!defined($cpid)) { + $rv = { 'status' => 0, 'rv' => "fork() failed : $!" }; + } + elsif ($cpid == 0) { + close(SOCK); + close(MAIN); + close(MAIN6) if ($use_ipv6); # Accept connection in separate process my $rmask; vec($rmask, fileno($tsock), 1) = 1; @@ -256,11 +280,17 @@ while(1) { close(TRANS); exit; } + else { + $xfer_kids{$cpid} = 1; + print STDERR "fastrpc: tcpread $arg->{'file'} ". + "started\n" + if ($gconfig{'rpcdebug'}); + $rv = { 'status' => 1, + 'rv' => [ $arg->{'file'}, $tport ] }; + } close(FILE); close($tsock); - close($tsock6); - print STDERR "fastrpc: tcpread $arg->{'file'} done\n" if ($gconfig{'rpcdebug'}); - $rv = { 'status' => 1, 'rv' => [ $arg->{'file'}, $tport ] }; + close($tsock6) if ($use_ipv6); } } elsif ($arg->{'action'} eq 'require') { @@ -335,17 +365,6 @@ while(1) { # Send back to the client print SOCK length($rawrv),"\n"; print SOCK $rawrv; - - # If a process was forked for a file transfer, wait for it if requested - if ($tcppid) { - if ($arg->{'wait'}) { - waitpid($tcppid, 0); - } - else { - push(@childpids, $tcppid); - } - } - last if ($arg->{'action'} eq 'quit'); $rcount++; } From 013aa5a5c63a079176398e1c3a3c33a392f79a70 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Thu, 8 Jan 2026 18:28:43 +0200 Subject: [PATCH 3/4] Fix to make timeout cleanly configurable --- fastrpc.cgi | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/fastrpc.cgi b/fastrpc.cgi index 4b81c33a9..c83f04a59 100755 --- a/fastrpc.cgi +++ b/fastrpc.cgi @@ -10,6 +10,10 @@ use POSIX; use Socket; $force_lang = $default_lang; &init_config(); + +use constant DEFAULT_RPC_TIMEOUT => 60; +use constant DEFAULT_RPC_IDLE_FACTOR => 6; + print "Content-type: text/plain\n\n"; # Can this user make remote calls? @@ -45,6 +49,16 @@ else { $version = &get_webmin_version(); print "1 $port $sid $version\n"; +# Timeout +my $rpc_idle_factor = $gconfig{'rpc_idle_factor'} || DEFAULT_RPC_IDLE_FACTOR; +if ($rpc_idle_factor !~ /^\d+$/ || $rpc_idle_factor < 1) { + $rpc_idle_factor = DEFAULT_RPC_IDLE_FACTOR; + } +my $config_rpc_timeout = $gconfig{'rpc_timeout'} || DEFAULT_RPC_TIMEOUT; +if ($config_rpc_timeout !~ /^\d+$/ || $config_rpc_timeout < 1) { + $config_rpc_timeout = DEFAULT_RPC_TIMEOUT; + } + # Fork and listen for calls .. $pid = fork(); if ($pid < 0) { @@ -62,7 +76,7 @@ vec($rmask, fileno(MAIN), 1) = 1; if ($use_ipv6) { vec($rmask, fileno(MAIN6), 1) = 1; } -$sel = select($rmask, undef, undef, $gconfig{'rpc_timeout'} || 60); +$sel = select($rmask, undef, undef, $config_rpc_timeout); if ($sel <= 0) { print STDERR "fastrpc: accept timed out\n" if ($gconfig{'rpcdebug'}); @@ -92,11 +106,9 @@ while(1) { # Wait for the request. Wait longer if this isn't the first one my $rmask; vec($rmask, fileno(SOCK), 1) = 1; - my $timeout = $gconfig{'rpc_timeout'} - ? $gconfig{'rpc_timeout'} - : $rcount - ? 360 - : 60; + my $timeout = $rcount + ? $config_rpc_timeout * $rpc_idle_factor + : $config_rpc_timeout; my $sel = select($rmask, undef, undef, $timeout); if ($sel <= 0) { # Don't kill the control session while a tcpwrite/tcpread is From 65ab502176d16eed6af156049c7b010c6655a231 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Thu, 8 Jan 2026 18:30:41 +0200 Subject: [PATCH 4/4] Add an option to configure RPC timeout --- lang/en | 8 ++++++++ webmin/change_session.cgi | 6 ++++++ webmin/edit_session.cgi | 5 +++++ webmin/help/rpc_timeout.html | 6 ++++++ webmin/lang/en | 2 ++ 5 files changed, 27 insertions(+) create mode 100644 webmin/help/rpc_timeout.html diff --git a/lang/en b/lang/en index 92c91da97..6293a4ed6 100644 --- a/lang/en +++ b/lang/en @@ -475,6 +475,14 @@ time_in_mins=In $1 minutes time_in_sec=In $1 second time_in_secs=In $1 seconds time_now=Just now +time_second=second +time_seconds=seconds +time_minute=minute +time_minutes=minutes +time_hour=hour +time_hours=hours +time_day=day +time_days=days defcert_error=Default $1 bundled SSL certificate is being used. It is highly advised to update default $2 certificate before proceeding with login. diff --git a/webmin/change_session.cgi b/webmin/change_session.cgi index b29feca02..8a1164d21 100755 --- a/webmin/change_session.cgi +++ b/webmin/change_session.cgi @@ -180,6 +180,12 @@ $in{'passreset_timeout'} =~ /^\d+$/ && $in{'passreset_timeout'} > 0 || &error($text{'session_epassreset_timeout'}); $gconfig{'passreset_timeout'} = $in{'passreset_timeout'}; +# RPC timeout +my $rpc_timeout = $in{'rpc_timeout'}; +$rpc_timeout =~ /^\d+$/ && $rpc_timeout > 0 || + &error($text{'session_erpc_timeout'}); +$gconfig{'rpc_timeout'} = $rpc_timeout; + &write_file("$config_directory/config", \%gconfig); &unlock_file("$config_directory/config"); diff --git a/webmin/edit_session.cgi b/webmin/edit_session.cgi index 3a54c2490..54847d39e 100755 --- a/webmin/edit_session.cgi +++ b/webmin/edit_session.cgi @@ -174,6 +174,11 @@ print &ui_table_row($text{'session_md5'}, [ 2, $text{'session_sha512'}."
" ], [ 3, $text{'session_yescrypt'} ] ])); +# RPC timeout +print &ui_table_row(&hlink($text{'session_rpc_timeout'}, 'rpc_timeout'), + &ui_textbox("rpc_timeout", $gconfig{'rpc_timeout'} || 60, 3). + " ".$text{'time_seconds'}); + print ui_table_end(); print ui_form_end([ [ "save", $text{'save'} ] ]); diff --git a/webmin/help/rpc_timeout.html b/webmin/help/rpc_timeout.html new file mode 100644 index 000000000..66e5d89c2 --- /dev/null +++ b/webmin/help/rpc_timeout.html @@ -0,0 +1,6 @@ +
RPC session timeout
+ +Sets how many seconds Webmin keeps an RPC session open while it is waiting with +no activity. + +