public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
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





  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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal