Hi Paul, What happens during the race is the following. At a certain point while mv -f /tmp/nmap /usr/bin/nmap is executing, my program manages to create /tmp/nmap. During this window the application performs its size check, and the file shows a size greater than 100 bytes. After the move operation completes, the file can still be opened successfully. Once this time window occurs and the file is already present at /tmp/nmap, running the vertical privilege escalation script again results in one of the four different outcomes I mentioned earlier, depending on which stage of the operation the race is won. This made me wonder whether the behavior could be considered a design issue. It appears that the file contents are copied first and the permissions are applied afterward (even when using -a). Because of this ordering, an unprivileged user might retain a writable handle while a privileged process performs the operation. My doubt is that if this is the case, then whenever we know a script or privileged program is going to copy a file into a directory, an unprivileged user could try to create the destination file during the operation and potentially write to it while the copy is happening, since the permissions and security checks appear to be enforced later. In theory, this seems like it could lead to vertical or horizontal privilege escalation scenarios. I also noticed that there does not appear to be any explicit locking (for example a try-lock or similar mechanism) around this operation. Could you clarify if this behavior is expected, or if there are safeguards I might be overlooking? Thanks.
[undefined] Warm Regards, Ajay SK Firmware Security Researcher Payatu Security Consulting Pvt Ltd. Reach me on: 7397338492 This email and any files transmitted with it are confidential and intended solely for the use of the individual or entity to whom they are addressed. If you have received this email in error, please notify the system manager. Please note that any views or opinions presented in this email are solely those of the author and do not necessarily represent those of the company. Finally, the recipient should check this email and any attachments for the presence of Malwares. The company accepts no liability for any damage caused by any Malware transmitted by this email. ________________________________ From: Paul Eggert <[email protected]> Sent: 09 March 2026 02:20 To: Collin Funk <[email protected]>; Ajay S.K <[email protected]> Cc: [email protected] <[email protected]> Subject: Re: bug#80572: [BUG] Privilege escalation via cp trying to replace file contents using root privileges [You don't often get email from [email protected]. Learn why this is important at https://aka.ms/LearnAboutSenderIdentification ] CAUTION: This email originated from outside of the organization. Do not click links or open attachments unless you recognize the sender and know the content is safe. On 2026-03-08 01:04, Collin Funk wrote: > The behavior you see is surprising to me. Yes, I'm not understanding it either. Just to double check, he writes that /tmp and /usr/bin are the same file system. In that case, 'cp -f /usr/bin/nmap /tmp/nmap' should do something like this: openat(AT_FDCWD, "/tmp/nmap", O_RDONLY|O_PATH|O_DIRECTORY) = -1 ENOENT (No such file or directory) newfstatat(AT_FDCWD, "/usr/bin/nmap", {st_mode=S_IFREG|0755, st_size=138120, ...}, 0) = 0 openat(AT_FDCWD, "/usr/bin/nmap", O_RDONLY) = 3 fstat(3, {st_mode=S_IFREG|0755, st_size=138120, ...}) = 0 openat(AT_FDCWD, "/tmp/nmap", O_WRONLY|O_CREAT|O_EXCL, 0755) = 4 ioctl(4, BTRFS_IOC_CLONE or FICLONE, 3) = -1 EOPNOTSUPP (Operation not supported) fstat(4, {st_mode=S_IFREG|0755, st_size=0, ...}) = 0 lseek(3, 0, SEEK_DATA) = 0 lseek(3, 0, SEEK_HOLE) = 138120 lseek(3, 0, SEEK_SET) = 0 copy_file_range(3, NULL, 4, NULL, 2146435072, 0) = 138120 copy_file_range(3, NULL, 4, NULL, 2146435072, 0) = 0 close(4) = 0 close(3) = 0 and 'mv -f /tmp/nmap /usr/bin/nmap' should do something like this: renameat2(AT_FDCWD, "/tmp/nmap", AT_FDCWD, "/usr/bin/nmap", RENAME_NOREPLACE) = -1 EEXIST (File exists) openat(AT_FDCWD, "/usr/bin/nmap", O_RDONLY|O_PATH|O_DIRECTORY) = -1 ENOTDIR (Not a directory) newfstatat(AT_FDCWD, "/tmp/nmap", {st_mode=S_IFREG|0755, st_size=138120, ...}, AT_SYMLINK_NOFOLLOW) = 0 newfstatat(AT_FDCWD, "/usr/bin/nmap", {st_mode=S_IFREG|0755, st_size=138120, ...}, AT_SYMLINK_NOFOLLOW) = 0 renameat(AT_FDCWD, "/tmp/nmap", AT_FDCWD, "/usr/bin/nmap") = 0 He can use 'strace' to verify this, and if 'strace' reports anything different we can take it from there. I am assuming bleeding-edge coreutils; if he's using older coreutils perhaps the older version has been fixed and he should upgrade.
