From: Wolfgang Bumiller <w.bumiller@proxmox.com>
To: Kefu Chai <k.chai@proxmox.com>
Cc: pve-devel@lists.proxmox.com
Subject: Re: [PATCH common 1/2] cmd: run: do not cancel the caller's alarm
Date: Thu, 5 Nov 2026 14:20:55 +0100 [thread overview]
Message-ID: <cgsng52kirt4qcaehzqlb75kqttxcgjirdgum6ndwp7ym6ag2d@p254xv6gnjv4> (raw)
In-Reply-To: <20261021110126.4024539-2-k.chai@proxmox.com>
On Wed, Oct 21, 2026 at 07:01:25PM +0800, Kefu Chai wrote:
> run() calls alarm(0) when done, so a caller's pending alarm is lost,
> or pushed back if run had a timeout of its own. The cfs lock relies on
> such an alarm: on shared LVM, 'vgs' and 'lvcreate' run under the
> storage lock, and after 'vgs' nothing limits 'lvcreate' anymore.
>
> Only touch the alarm if run has its own timeout, and afterwards give
> the caller's alarm back what is left of it.
>
> Signed-off-by: Kefu Chai <k.chai@proxmox.com>
> ---
> src/PVE/Cmd.pm | 16 ++++++++++++----
> test/cmd-test.pl | 28 ++++++++++++++++++++++++++++
> 2 files changed, 40 insertions(+), 4 deletions(-)
>
> diff --git a/src/PVE/Cmd.pm b/src/PVE/Cmd.pm
> index 27c9193..a6bb17d 100644
> --- a/src/PVE/Cmd.pm
> +++ b/src/PVE/Cmd.pm
> @@ -100,6 +100,7 @@ sub run($cmd, %param) {
> my $laststderr;
> my $timeout;
> my $oldtimeout;
> + my $armed_at;
> my $pid;
> my $exitcode = -1;
>
> @@ -195,7 +196,10 @@ sub run($cmd, %param) {
>
> local $SIG{ALRM} = sub { die "got timeout\n"; }
> if $timeout;
There is technically a race here where at *this* point the outer alarm
can trigger.
I suggest placing
$oldtimeout = alarm(0) if $timeout;
*above* the `local $SIG{ALRM}` line above first, so that if a timeout is
created, we first disable the alarm, then establish the signal handler,
*then* the new alarm (without re-assigning to $oldtimeout again).
> - $oldtimeout = alarm($timeout) if $timeout;
> + if ($timeout) {
> + $oldtimeout = alarm($timeout);
> + $armed_at = Time::HiRes::time();
> + }
>
> &$afterfork() if $afterfork;
>
> @@ -295,12 +299,12 @@ sub run($cmd, %param) {
> }
> }
>
> - alarm(0);
> + alarm(0) if $timeout; # while our handler is still in place
> };
>
> my $err = $@;
>
> - alarm(0);
> + alarm(0) if $armed_at;
This now means an external timer can interrupt `$errfunc` which it could
not previously.
I think it makes more sense to `eval` the `$errfunc` call and do
something like:
if ($err eq "got timeout\n") {
warn $errfunc_error;
} else {
$err = "$errfunc_error\n$err";
}
then…
>
> if ($errmsg && $laststderr) {
> &$errfunc(undef); # flush laststderr
> @@ -308,7 +312,11 @@ sub run($cmd, %param) {
>
> umask($old_umask) if defined($old_umask);
>
> - alarm($oldtimeout) if $oldtimeout;
> + # give the caller's alarm back what is left of it, a lock may depend on it
> + if ($oldtimeout) {
> + my $left = $oldtimeout - (Time::HiRes::time() - $armed_at);
> + alarm($left > 0 ? $left : 0.001);
> + }
…then move this hunk to after the kill/wait code below, replacing its
`die`s with appending the error similar to the above code, so the
external timer also cannot prevent the kill/wait.
This might then also make the 2nd patch unnecessary?
>
> if ($err) {
> if ($pid && ($err eq "got timeout\n")) {
> diff --git a/test/cmd-test.pl b/test/cmd-test.pl
> index c2f7ae7..191f9f5 100755
> --- a/test/cmd-test.pl
> +++ b/test/cmd-test.pl
> @@ -11,6 +11,7 @@ use lib '../src';
> use PVE::Cmd;
>
> use Test::More;
> +use Time::HiRes ();
>
> # shell_quote: quote a single argument for the shell
> #
> @@ -67,6 +68,33 @@ ok($@, 'run dies on a failing command without noerr');
>
> is(PVE::Cmd::run_command(['true']), 0, 'run_command works as a wrapper for run');
>
> +# run must leave a pending alarm of its caller alone, like the time limit of a lock
> +#
> +# name - how run is called
> +# cmd - command to run, sleeps for a second by default
> +# param - parameters for run
> +my $caller_alarm_tests = [
> + { name => 'without a timeout', param => {} },
> + { name => 'with a timeout', param => { timeout => 5 } },
> + {
> + name => 'with a timeout on a command that cannot start',
> + cmd => ['/nonexistent/command'],
> + param => { timeout => 5 },
> + },
> +];
> +for my $test ($caller_alarm_tests->@*) {
> + my $fired;
> + local $SIG{ALRM} = sub { $fired = Time::HiRes::time() };
> + my $start = Time::HiRes::time();
> + Time::HiRes::alarm(2);
> + eval { PVE::Cmd::run($test->{cmd} // ['sleep', '1'], $test->{param}->%*) };
> + Time::HiRes::sleep(0.1) until $fired || Time::HiRes::time() - $start > 5;
> + Time::HiRes::alarm(0);
> + ok($fired, "the caller's alarm still fires after run $test->{name}");
> + my $after = defined($fired) ? $fired - $start : 99;
> + cmp_ok($after, '<', 2.5, "run $test->{name} does not delay the caller's alarm");
> +}
> +
> # TODO:
> # - cover pipe_socket (needs a socket peer)
> # - cover timeouts and the errfunc/logfunc callbacks
> --
> 2.47.3
>
>
>
>
>
--
next prev parent reply other threads:[~2026-11-05 13:21 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-21 11:01 [PATCH common 0/2] cmd: run: do not leave the command behind on a caller's timeout Kefu Chai
2026-10-21 11:01 ` [PATCH common 1/2] cmd: run: do not cancel the caller's alarm Kefu Chai
2026-11-05 13:20 ` Wolfgang Bumiller [this message]
2026-10-21 11:01 ` [PATCH common 2/2] cmd: run: kill the command when run dies Kefu Chai
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=cgsng52kirt4qcaehzqlb75kqttxcgjirdgum6ndwp7ym6ag2d@p254xv6gnjv4 \
--to=w.bumiller@proxmox.com \
--cc=k.chai@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox