libutil: Add openNewFileForWrite helper function, wrap callsites

This is purely a fix to use CreateFileW in mingw builds. Also adds some
FIXMEs for suspicious symlink following on truncation that can probably
be tightened down without any problems (other than nix-channel), but for
now this is a no-op change other than consistently using O_CLOEXEC, which
is harmless.
This commit is contained in:
Sergei Zimmerman
2026-02-04 02:07:31 +03:00
parent d5ce1a79ec
commit 47f261cc19
7 changed files with 74 additions and 14 deletions

View File

@@ -133,11 +133,17 @@ class SampleStack : public EvalProfiler
FrameInfo getPrimOpFrameInfo(const PrimOp & primOp, std::span<Value *> args, PosIdx pos);
public:
SampleStack(EvalState & state, std::filesystem::path profileFile, std::chrono::nanoseconds period)
SampleStack(EvalState & state, const std::filesystem::path & profileFile, std::chrono::nanoseconds period)
: state(state)
, sampleInterval(period)
, profileFd([&]() {
AutoCloseFD fd = toDescriptor(open(profileFile.string().c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0660));
AutoCloseFD fd = openNewFileForWrite(
profileFile,
0660,
{
.truncateExisting = true,
.followSymlinksOnTruncate = true, /* FIXME: Probably shouldn't follow symlinks. */
});
if (!fd)
throw SysError("opening file %s", PathFmt(profileFile));
return fd;

View File

@@ -1035,14 +1035,13 @@ LogFile::LogFile(Store & store, const StorePath & drvPath, const LogFileSettings
Path logFileName = fmt("%s/%s%s", dir, baseName.substr(2), logSettings.compressLog ? ".bz2" : "");
fd = toDescriptor(open(
logFileName.c_str(),
O_CREAT | O_WRONLY | O_TRUNC
#ifndef _WIN32
| O_CLOEXEC
#endif
,
0666));
fd = openNewFileForWrite(
logFileName,
0666,
{
.truncateExisting = true,
.followSymlinksOnTruncate = true, /* FIXME: Probably shouldn't follow symlinks. */
});
if (!fd)
throw SysError("creating log file '%1%'", logFileName);

View File

@@ -198,6 +198,29 @@ Descriptor openDirectory(const std::filesystem::path & path);
*/
Descriptor openFileReadonly(const std::filesystem::path & path);
struct OpenNewFileForWriteParams
{
/**
* Whether to truncate an existing file.
*/
bool truncateExisting:1 = false;
/**
* Whether to follow symlinks if @ref truncateExisting is true.
*/
bool followSymlinksOnTruncate:1 = false;
};
/**
* Open a `Descriptor` for write access or create it if it doesn't exist or truncate existing depending on @ref
* truncateExisting.
*
* @param mode POSIX permission bits. Ignored on Windows.
* @throws Nothing.
*
* @todo Reparse points on Windows.
*/
Descriptor openNewFileForWrite(const std::filesystem::path & path, mode_t mode, OpenNewFileForWriteParams params);
/**
* Read the contents of a file into a string.
*/

View File

@@ -32,6 +32,19 @@ Descriptor openFileReadonly(const std::filesystem::path & path)
return open(path.c_str(), O_RDONLY | O_CLOEXEC);
}
Descriptor openNewFileForWrite(const std::filesystem::path & path, mode_t mode, OpenNewFileForWriteParams params)
{
auto flags = O_WRONLY | O_CREAT | O_CLOEXEC;
if (params.truncateExisting) {
flags |= O_TRUNC;
if (!params.followSymlinksOnTruncate)
flags |= O_NOFOLLOW;
} else {
flags |= O_EXCL; /* O_CREAT | O_EXCL already ensures that symlinks are not followed. */
}
return open(path.c_str(), flags, mode);
}
std::filesystem::path defaultTempDir()
{
return getEnvNonEmpty("TMPDIR").value_or("/tmp");

View File

@@ -41,6 +41,19 @@ Descriptor openFileReadonly(const std::filesystem::path & path)
/*hTemplateFile=*/nullptr);
}
Descriptor
openNewFileForWrite(const std::filesystem::path & path, [[maybe_unused]] mode_t mode, OpenNewFileForWriteParams params)
{
return CreateFileW(
path.c_str(),
GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_DELETE,
/*lpSecurityAttributes=*/nullptr,
params.truncateExisting ? CREATE_ALWAYS : CREATE_NEW, /* TODO: Reparse points. */
FILE_ATTRIBUTE_NORMAL,
/*hTemplateFile=*/nullptr);
}
std::filesystem::path defaultTempDir()
{
wchar_t buf[MAX_PATH + 1];

View File

@@ -44,7 +44,13 @@ static void readChannels()
// Writes the list of channels.
static void writeChannels()
{
auto channelsFD = AutoCloseFD{open(channelsList.c_str(), O_WRONLY | O_CLOEXEC | O_CREAT | O_TRUNC, 0644)};
AutoCloseFD channelsFD = openNewFileForWrite(
channelsList,
0644,
{
.truncateExisting = true,
.followSymlinksOnTruncate = true, /* Back-compat. */
});
if (!channelsFD)
throw SysError("opening '%1%' for writing", channelsList.string());
for (const auto & channel : channels)

View File

@@ -112,7 +112,7 @@ std::tuple<StorePath, Hash> prefetchFile(
if (executable)
mode = 0700;
AutoCloseFD fd = toDescriptor(open(tmpFile.string().c_str(), O_WRONLY | O_CREAT | O_EXCL, mode));
AutoCloseFD fd = openNewFileForWrite(tmpFile, mode, {.truncateExisting = false});
if (!fd)
throw SysError("creating temporary file %s", PathFmt(tmpFile));
@@ -126,9 +126,9 @@ std::tuple<StorePath, Hash> prefetchFile(
/* Optionally unpack the file. */
if (unpack) {
Activity act(*logger, lvlChatty, actUnknown, fmt("unpacking '%s'", url.to_string()));
auto unpacked = (tmpDir.path() / "unpacked").string();
auto unpacked = tmpDir.path() / "unpacked";
createDirs(unpacked);
unpackTarfile(tmpFile.string(), unpacked);
unpackTarfile(tmpFile, unpacked);
auto entries = DirectoryIterator{unpacked};
/* If the archive unpacks to a single file/directory, then use