util1: confine operator paths in the robust_rename EXDEV fallback

The cross-filesystem fallback copied to the dest and unlinked the source without
operator-path confinement, so an absolute --temp-dir/--partial-dir on another
filesystem was opened/unlinked via plain libc -- a raced parent symlink could
redirect the dest-write or source-unlink out of the module.  The source READ was
already confined; flip operator_path_resolve for an absolute (operator) path
around the copy_file dest-open and the do_unlink_at, leaving relative in-module
paths on the secure_relative_open arm.

This is the EXDEV-fallback backstop for the same absolute --partial-dir /
foreign-owned-parent-symlink escape that operator-path-partial-dir_test.py
already exercises at the handle_partial_dir() layer (which confines the staging
dir before this code runs).  A dedicated test for the EXDEV copy_file path itself
would need the main tmp->final rename to fail first and then hit EXDEV on a
foreign-owned raced parent -- a nested, timing-dependent trigger.

Reported-by: Leonid Bugaev
This commit is contained in:
Andrew Tridgell committed 2026-07-20 14:05:32 +10:00
1 parent ad64e99590
commit fd4c75116b
1 file changed
+19 -2
+19 -2
View File
@@ -628,16 +628,33 @@ int robust_rename(const char *from, const char *to, const char *partialptr,
errno = ETXTBSY;
break;
#endif
case EXDEV:
case EXDEV: {
int save = operator_path_resolve, rc;
if (partialptr) {
if (!handle_partial_dir(partialptr,PDIR_CREATE))
return -2;
to = partialptr;
}
if (copy_file(from, to, -1, mode) != 0)
/* Cross-fs fallback: copy then unlink. An absolute --temp-dir
* source / --partial-dir dest is an operator path whose parents
* do_open_at()/do_unlink_at() would otherwise follow via plain libc
* -- confine them through the ownership walk so a raced parent
* symlink can't redirect the dest-write or the source-unlink out of
* the module. copy_file already confines the source READ; a
* relative in-module path stays on the secure_relative_open arm, so
* only flip the flag for an absolute (operator) path. */
if (to && *to == '/')
operator_path_resolve = 1;
rc = copy_file(from, to, -1, mode);
operator_path_resolve = save;
if (rc != 0)
return -2;
if (from && *from == '/')
operator_path_resolve = 1;
do_unlink_at(from);
operator_path_resolve = save;
return 1;
}
default:
return -1;
}