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.

Reply via email to