From: "Michael Köppl" <m.koeppl@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH common 1/3] tools: lock file: add helper to close inherited lock handles
Date: Thu, 5 Nov 2026 17:36:35 +0100 [thread overview]
Message-ID: <20261105163637.1318072-2-m.koeppl@proxmox.com> (raw)
In-Reply-To: <20261105163637.1318072-1-m.koeppl@proxmox.com>
A child forked while its parent holds a lock from lock_file_full()
inherits the locked file handle. The lock then is held until the child
exits too, even after the parent released it.
Keep the locked handle instead of a flag in the scalar that
$lock_handles weakly references. Add a helper that lets a child close
its copies of the handles locked by its ancestors. The ancestor keeps
holding the lock, since its own handle stays open.
Signed-off-by: Michael Köppl <m.koeppl@proxmox.com>
---
src/PVE/Tools.pm | 23 +++++++++++++++-----
test/lock_file.pl | 54 +++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 72 insertions(+), 5 deletions(-)
diff --git a/src/PVE/Tools.pm b/src/PVE/Tools.pm
index 9176b358..2f2123c1 100644
--- a/src/PVE/Tools.pm
+++ b/src/PVE/Tools.pm
@@ -198,7 +198,8 @@ sub run_with_timeout {
# Since this is the only way we lock now and we don't have the old
# 'lock(); code(); unlock();' pattern anymore we do not actually need to
# count how deep we're nesting. Therefore this hash now stores a weak reference
-# to a boolean telling us whether we already have a lock.
+# to a scalar holding the locked file handle. If the lock has not been acquired
+# yet, it holds a false value.
my $lock_handles = {};
@@ -243,7 +244,7 @@ sub lock_file_full {
if (!$checkptr || !$$checkptr) {
# We cannot create a weak reference in a single atomic step, so we first
# create a false-value, then create a reference to it, then weaken it,
- # and after successfully locking the file we change the boolean value.
+ # and after successfully locking the file we store the handle in it.
#
# The reason for this is that if an outer SIGALRM throws an exception
# between creating the reference and weakening it, a subsequent call to
@@ -253,8 +254,8 @@ sub lock_file_full {
#
# An externally triggered exception elsewhere in the code will cause the
# weak reference to become 'undef', and since the file handle is only
- # stored in the local scope in $local_fh, the file will be closed by
- # perl's cleanup routines as well.
+ # stored in the local scope in $local_fh and $check, the file will be
+ # closed by perl's cleanup routines as well.
#
# This still assumes that an IO::File handle can properly deal with such
# exceptions thrown during its own destruction, but that's up to perls
@@ -266,7 +267,7 @@ sub lock_file_full {
$@ = "can't lock file '$filename' - $@";
return undef;
}
- $check = 1;
+ $check = $local_fh;
}
$res = eval { &$code(@param); };
return undef if $@;
@@ -279,6 +280,18 @@ sub lock_file {
return lock_file_full($filename, $timeout, 0, $code, @param);
}
+# Close this process's copies of lock handles inherited from its ancestors, so
+# it does not keep their locks held after they released them.
+sub close_inherited_lock_handles {
+ for my $pid (keys %$lock_handles) {
+ next if $pid == $$;
+ for my $checkptr (values $lock_handles->{$pid}->%*) {
+ close($$checkptr) if $checkptr && $$checkptr;
+ }
+ delete $lock_handles->{$pid};
+ }
+}
+
# file_ helper for backward compat, prefer PVE::File directly for new usage.
# TODO: remove with PVE 10 / Debian Forky releases.
sub file_set_contents {
diff --git a/test/lock_file.pl b/test/lock_file.pl
index 83965c55..ffa2404a 100755
--- a/test/lock_file.pl
+++ b/test/lock_file.pl
@@ -5,6 +5,7 @@ use strict;
use warnings;
use Socket;
+use Fcntl qw(:flock);
use POSIX (); # don't import assert()
use PVE::Tools 'lock_file_full';
@@ -91,6 +92,54 @@ sub forktest1($) {
close($fmain);
}
+sub lock_is_free {
+ open(my $fh, '>>', $name) or die "open(): $!\n";
+ return flock($fh, LOCK_EX | LOCK_NB);
+}
+
+sub inherited_lock_test {
+ my ($close_in_child) = @_;
+
+ socketpair(my $fparent, my $fchild, AF_UNIX, SOCK_STREAM, PF_UNSPEC)
+ or die "socketpair(): $!\n";
+
+ my $pid;
+ lock_file_full(
+ $name,
+ 10,
+ 0,
+ sub {
+ $pid = fork() // die "fork(): $!\n";
+ if (!$pid) {
+ close($fparent);
+ PVE::Tools::close_inherited_lock_handles() if $close_in_child;
+ print {$fchild} "1\n";
+ $fchild->flush();
+ <$fchild>; # wait for parent to finish checks
+ POSIX::_exit(0);
+ }
+ close($fchild);
+ my $line = <$fparent>;
+ die "child failed to start\n" if !$line || $line ne "1\n";
+ die "lock released while the parent still holds it\n" if lock_is_free();
+ },
+ );
+
+ my $err = $@;
+ my $free = !$err && lock_is_free();
+ print {$fparent} "2\n";
+ $fparent->flush();
+ waitpid($pid, 0) if $pid;
+ close($fparent);
+ die $err if $err;
+
+ if ($close_in_child) {
+ die "child kept the inherited lock held\n" if !$free;
+ } else {
+ die "child did not inherit the lock\n" if $free;
+ }
+}
+
eval {
# Regular lock:
new();
@@ -143,6 +192,11 @@ eval {
# Can we get two simultaneous shared locks? (shared=1)
forktest1(0);
forktest1(1);
+
+ # Lock held by child after parent released it?
+ inherited_lock_test(0);
+ # Lock free once child closed inherited copies?
+ inherited_lock_test(1);
};
my $err = $@;
system("rm $name*");
--
2.47.3
next prev parent reply other threads:[~2026-11-05 16:37 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-05 16:36 [PATCH common/storage 0/3] fix #8286: do not hold inherited locks in volume cleanup workers Michael Köppl
2026-11-05 16:36 ` Michael Köppl [this message]
2026-11-05 16:36 ` [PATCH storage 2/3] fix #8286: vdisk free: do not hold inherited locks in cleanup worker Michael Köppl
2026-11-05 16:36 ` [PATCH storage 3/3] lvm plugin: do not hold inherited locks in cleanup workers Michael Köppl
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=20261105163637.1318072-2-m.koeppl@proxmox.com \
--to=m.koeppl@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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.