+# In scalar mode: returns a file handle to the deepest directory node.
+# In list context: returns a list of:
+# * the deepest directory node
+# * the 2nd deepest directory (parent of the above)
+# * directory name of the last directory
+# So that the path $2/$3 should lead to $1 afterwards.
+sub walk_tree_nofollow($$$) {
+ my ($start, $subdir, $mkdir) = @_;
+
+ # splitdir() returns '' for empty components including the leading /
+ my @comps = grep { length($_)>0 } File::Spec->splitdir($subdir);
+
+ sysopen(my $fd, $start, O_PATH | O_DIRECTORY)
+ or die "failed to open start directory $start: $!\n";
+
+ my $dir = $start;
+ my $last_component = undef;
+ my $second = $fd;
+ foreach my $component (@comps) {
+ $dir .= "/$component";
+ my $next = PVE::Tools::openat(fileno($fd), $component, O_NOFOLLOW | O_DIRECTORY);
+
+ if (!$next) {
+ # failed, check for symlinks and try to create the path
+ die "symlink encountered at: $dir\n" if $! == ELOOP;
+ die "cannot open directory $dir: $!\n" if !$mkdir;
+
+ # We don't check for errors on mkdirat() here and just try to
+ # openat() again, since at least one error (EEXIST) is an
+ # expected possibility if multiple containers start
+ # simultaneously. If someone else injects a symlink now then
+ # the subsequent openat() will fail due to O_NOFOLLOW anyway.
+ PVE::Tools::mkdirat(fileno($fd), $component, 0755);
+
+ $next = PVE::Tools::openat(fileno($fd), $component, O_NOFOLLOW | O_DIRECTORY);
+ die "failed to create path: $dir: $!\n" if !$next;
+ }
+
+ close $second if defined($last_component);
+ $last_component = $component;
+ $second = $fd;
+ $fd = $next;
+ }
+
+ return ($fd, defined($last_component) && $second, $last_component) if wantarray;
+ close $second if defined($last_component);
+ return $fd;
+}
+