From fd4c75116bfcd3e7b4eb53eaaf74ca16f5871e15 Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Fri, 3 Jul 2026 14:35:49 +1000 Subject: [PATCH] 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 --- util1.c | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/util1.c b/util1.c index 77f284c2..8c9c0070 100644 --- a/util1.c +++ b/util1.c @@ -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; }