Symlink-race-safe variant of do_chmod() for receiver-side use. Threat model: on a daemon running with "use chroot = no" (the prerequisite for CVE-2026-29518), a local attacker can race a symlink swap of one of the parent directory components of a path the receiver is about to chmod. Because chmod() resolves symlinks at every component, the swap redirects the chmod outside the receiver's
| 831 | component, where there is nothing to protect against. |
| 832 | */ |
| 833 | int do_chmod_at(const char *fname, mode_t mode) |
| 834 | { |
| 835 | #ifdef AT_FDCWD |
| 836 | extern int am_daemon, am_chrooted; |
| 837 | char dirpath[MAXPATHLEN]; |
| 838 | const char *bname; |
| 839 | const char *slash; |
| 840 | int dfd, ret, e; |
| 841 | size_t dlen; |
| 842 | |
| 843 | if (dry_run) return 0; |
| 844 | RETURN_ERROR_IF_RO_OR_LO; |
| 845 | |
| 846 | /* Only the daemon-without-chroot case is exposed to the symlink- |
| 847 | * race attack: a chroot already confines the receiver, and a |
| 848 | * non-daemon rsync runs with the user's own authority so a |
| 849 | * symlink they planted can only redirect to files they could |
| 850 | * already access. Everywhere else, fall through to plain |
| 851 | * do_chmod() to avoid the dirfd-open overhead on every call. */ |
| 852 | if (!am_daemon || am_chrooted) |
| 853 | return do_chmod(fname, mode); |
| 854 | |
| 855 | if (!fname || !*fname || *fname == '/' || S_ISLNK(mode)) |
| 856 | return do_chmod(fname, mode); |
| 857 | |
| 858 | slash = strrchr(fname, '/'); |
| 859 | if (!slash) |
| 860 | return do_chmod(fname, mode); |
| 861 | |
| 862 | dlen = slash - fname; |
| 863 | if (dlen >= sizeof dirpath) { |
| 864 | errno = ENAMETOOLONG; |
| 865 | return -1; |
| 866 | } |
| 867 | memcpy(dirpath, fname, dlen); |
| 868 | dirpath[dlen] = '\0'; |
| 869 | bname = slash + 1; |
| 870 | |
| 871 | dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); |
| 872 | if (dfd < 0) |
| 873 | return -1; |
| 874 | |
| 875 | ret = fchmodat(dfd, bname, mode, 0); |
| 876 | e = errno; |
| 877 | close(dfd); |
| 878 | errno = e; |
| 879 | return ret; |
| 880 | #else |
| 881 | return do_chmod(fname, mode); |
| 882 | #endif |
| 883 | } |
| 884 | #endif |
| 885 | |
| 886 | int do_rename(const char *old_path, const char *new_path) |
no test coverage detected