copy_file: confine an absolute operator source via the ownership walk

copy_file() routed a RELATIVE source through secure_relative_open (parents
confined) but opened an ABSOLUTE source -- an operator basis such as an absolute
--copy-dest -- with bare do_open_nofollow, which refuses only a leaf symlink and
follows every parent.  basis_link_stat() refuses a foreign-owned basis at stat
time, but a parent flipped to a foreign symlink between that stat and this open
redirects the basis read out of tree (an out-of-tree content read-leak into the
destination; RED on 3.4.x, GREEN here).

Resolve an absolute source's parents through owner_walk_parent (foreign-owned
parent symlink refused, operator's own dirs/uid0/euid symlinks followed).
operator_path_resolve is set only across the walk -- so module-exclude is
enforced -- and restored, leaving the caller's value for the dest side; that is
why confining the source here does not re-open the copy_xattrs dest race that
wrapping the whole copy_altdest_file would (copy-xattrs-symlink-race stays green).

(Leonid Bugaev May-2026 re-audit, KI-46.)
This commit is contained in:
Andrew Tridgell committed 2026-07-20 14:05:32 +10:00
1 parent e7c017914f
commit 8d3b7be875
1 file changed
+26 -2
+26 -2
View File
@@ -397,10 +397,34 @@ int copy_file(const char *source, const char *dest, int tmpfilefd, mode_t mode)
* --copy-dest=cd where cd is a symlink to an outside directory) cannot
* redirect the read to a file the attacker should not see. Plain
* do_open_nofollow only refuses a final-component symlink; parents are
* still followed. (An absolute source is operator-trusted -- e.g. an
* absolutized basis dir -- and uses do_open_nofollow.) */
* still followed. An ABSOLUTE source is an operator basis (e.g. an absolute
* --copy-dest): confine its parents via the ownership walk -- a foreign-owned
* parent symlink is refused, the operator's own dirs/uid0/euid symlinks
* followed -- so a flipped parent can't redirect the basis read out of tree.
* operator_path_resolve is set only across the walk (so module-exclude is
* enforced) and restored, leaving the caller's value for the dest side -- this
* is why confining the source here does not re-open the copy_xattrs dest
* race the way wrapping the whole copy_altdest_file would. */
if (secure_relpath_active() && source && *source && source[0] != '/')
ifd = secure_relative_open(NULL, source, O_RDONLY | O_NOFOLLOW, 0);
#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY
else if (secure_relpath_active() && source && source[0] == '/'
&& !symlink_optout_allowed()) {
int save = operator_path_resolve, dfd, e;
const char *leaf;
operator_path_resolve = 1;
dfd = owner_walk_parent(source, &leaf);
operator_path_resolve = save;
if (dfd < 0)
ifd = -1;
else {
ifd = openat(dfd, leaf, O_RDONLY | O_NOFOLLOW);
e = errno;
close(dfd);
errno = e;
}
}
#endif
else
ifd = do_open_nofollow(source, O_RDONLY);
if (ifd < 0) {