daemon: Protect ‘copyFileRecursively’ from race conditions.

Previously, if an attacker managed to introduce a hard link or a symlink
on one of the destination file names before it is opened,
‘copyFileRecursively’ would overwrite the symlink’s target or the hard
link’s content.

This kind of attack could be carried out while guix-daemon is copying
the output or the chroot directory of a failed fixed-output derivation
build, possibly allowing the attacker to escalate to the privileges of
the build user.

* nix/libutil/util.cc (copyFileRecursively): In the ‘S_ISREG’ case, open
‘destination’ with O_NOFOLLOW | O_EXCL.  In the ‘S_ISDIR’ case, open
‘destination’ with O_NOFOLLOW.

Reported-by: Reepca Russelstein <reepca@russelstein.xyz>
Change-Id: I94273efe4e92c1a4270a98c5ec47bd098e9227c9
Signed-off-by: John Kehayias <john.kehayias@protonmail.com>
This commit is contained in:
Ludovic Courtès 2025-04-15 14:46:45 +02:00 committed by John Kehayias
parent c659f977bb
commit 0e79d5b655
No known key found for this signature in database
GPG key ID: 499097AE5EA815D9

View file

@ -473,7 +473,8 @@ static void copyFileRecursively(int sourceroot, const Path &source,
if (sourceFd == -1) throw SysError(format("opening `%1%'") % source); if (sourceFd == -1) throw SysError(format("opening `%1%'") % source);
AutoCloseFD destinationFd = openat(destinationroot, destination.c_str(), AutoCloseFD destinationFd = openat(destinationroot, destination.c_str(),
O_CLOEXEC | O_CREAT | O_WRONLY | O_TRUNC, O_CLOEXEC | O_CREAT | O_WRONLY | O_TRUNC
| O_NOFOLLOW | O_EXCL,
st.st_mode); st.st_mode);
if (destinationFd == -1) throw SysError(format("opening `%1%'") % source); if (destinationFd == -1) throw SysError(format("opening `%1%'") % source);
@ -495,7 +496,8 @@ static void copyFileRecursively(int sourceroot, const Path &source,
throw SysError(format("creating directory `%1%'") % destination); throw SysError(format("creating directory `%1%'") % destination);
AutoCloseFD destinationFd = openat(destinationroot, destination.c_str(), AutoCloseFD destinationFd = openat(destinationroot, destination.c_str(),
O_CLOEXEC | O_RDONLY | O_DIRECTORY); O_CLOEXEC | O_RDONLY | O_DIRECTORY
| O_NOFOLLOW);
if (err != 0) if (err != 0)
throw SysError(format("opening directory `%1%'") % destination); throw SysError(format("opening directory `%1%'") % destination);
@ -505,7 +507,7 @@ static void copyFileRecursively(int sourceroot, const Path &source,
throw SysError(format("opening `%1%'") % source); throw SysError(format("opening `%1%'") % source);
if (deleteSource && !(st.st_mode & S_IWUSR)) { if (deleteSource && !(st.st_mode & S_IWUSR)) {
/* Ensure the directory writable so files within it can be /* Ensure the directory is writable so files within it can be
deleted. */ deleted. */
if (fchmod(sourceFd, st.st_mode | S_IWUSR) == -1) if (fchmod(sourceFd, st.st_mode | S_IWUSR) == -1)
throw SysError(format("making `%1%' directory writable") % source); throw SysError(format("making `%1%' directory writable") % source);