libstore: replace random() calls with atomic counter

random() is not thread-safe, it relies on global state, and calling it
from worker threads can result in multiple threads producing the same
value. It also doesn't guarantee unique values even in single-threaded
use.

Use an atomic counter for the use-case of generating temporary paths,
and switch to a thread-local RNG for the one remaining call.

This will probably fix https://github.com/NixOS/nix/issues/7273 though
I'm not willing to risk corrupting my store to find out.

Change-Id: I4c4c4c9796613573ffefd29cc8efe3d07839facc
This commit is contained in:
Lily Ballard 2024-10-20 20:35:01 -07:00
parent 11950a0a79
commit 69957a971e
8 changed files with 51 additions and 14 deletions

View file

@ -0,0 +1,13 @@
---
synopsis: Fix potential store corruption with auto-optimise-store
issues: [7273]
cls: [2100]
category: Fixes
credits: lilyball
---
Optimising store paths (and other operations involving temporary files) no longer use `random(3)`
to generate filenames. On darwin systems this was observed to potentially cause store corruption
when using [`auto-optimise-store`](@docroot@/command-ref/conf-file.md#conf-auto-optimise-store),
though this corruption was possible on any system whose `random(3)` does not have locking around
the global state.

View file

@ -172,12 +172,6 @@ void initNix()
now. In particular, store objects should be readable by now. In particular, store objects should be readable by
everybody. */ everybody. */
umask(0022); umask(0022);
/* Initialise the PRNG. */
struct timeval tv;
gettimeofday(&tv, 0);
srandom(tv.tv_usec);
} }

View file

@ -1,4 +1,5 @@
#include "derivation-goal.hh" #include "derivation-goal.hh"
#include "file-system.hh"
#include "hook-instance.hh" #include "hook-instance.hh"
#include "worker.hh" #include "worker.hh"
#include "finally.hh" #include "finally.hh"
@ -860,15 +861,22 @@ void replaceValidPath(const Path & storePath, const Path & tmpPath)
tmpPath (the replacement), so we have to move it out of the tmpPath (the replacement), so we have to move it out of the
way first. We'd better not be interrupted here, because if way first. We'd better not be interrupted here, because if
we're repairing (say) Glibc, we end up with a broken system. */ we're repairing (say) Glibc, we end up with a broken system. */
Path oldPath = fmt("%1%.old-%2%-%3%", storePath, getpid(), random()); Path oldPath;
if (pathExists(storePath)) if (pathExists(storePath)) {
do {
oldPath = makeTempPath(storePath, ".old");
// store paths are often directories so we can't just unlink() it
// let's make sure the path doesn't exist before we try to use it
} while (pathExists(oldPath));
movePath(storePath, oldPath); movePath(storePath, oldPath);
}
try { try {
movePath(tmpPath, storePath); movePath(tmpPath, storePath);
} catch (...) { } catch (...) {
try { try {
// attempt to recover // attempt to recover
if (!oldPath.empty())
movePath(oldPath, storePath); movePath(oldPath, storePath);
} catch (...) { } catch (...) {
ignoreExceptionExceptInterrupt(); ignoreExceptionExceptInterrupt();
@ -876,6 +884,7 @@ void replaceValidPath(const Path & storePath, const Path & tmpPath)
throw; throw;
} }
if (!oldPath.empty())
deletePath(oldPath); deletePath(oldPath);
} }

View file

@ -33,7 +33,8 @@ static void makeSymlink(const Path & link, const Path & target)
createDirs(dirOf(link)); createDirs(dirOf(link));
/* Create the new symlink. */ /* Create the new symlink. */
Path tempLink = fmt("%1%.tmp-%2%-%3%", link, getpid(), random()); Path tempLink = makeTempPath(link);
unlink(tempLink.c_str()); // just in case; ignore errors
createSymlink(target, tempLink); createSymlink(target, tempLink);
/* Atomically replace the old one. */ /* Atomically replace the old one. */

View file

@ -214,7 +214,8 @@ void LocalStore::optimisePath_(Activity * act, OptimiseStats & stats,
its timestamp back to 0. */ its timestamp back to 0. */
MakeReadOnly makeReadOnly(mustToggle ? dirOfPath : ""); MakeReadOnly makeReadOnly(mustToggle ? dirOfPath : "");
Path tempLink = fmt("%1%/.tmp-link-%2%-%3%", realStoreDir, getpid(), random()); Path tempLink = makeTempPath(realStoreDir, "/.tmp-link");
unlink(tempLink.c_str()); // just in case; ignore errors
if (link(linkPath.c_str(), tempLink.c_str()) == -1) { if (link(linkPath.c_str(), tempLink.c_str()) == -1) {
if (errno == EMLINK) { if (errno == EMLINK) {
@ -233,7 +234,7 @@ void LocalStore::optimisePath_(Activity * act, OptimiseStats & stats,
renameFile(tempLink, path); renameFile(tempLink, path);
} catch (SysError & e) { } catch (SysError & e) {
if (unlink(tempLink.c_str()) == -1) if (unlink(tempLink.c_str()) == -1)
printError("unable to unlink '%1%'", tempLink); printError("unable to unlink '%1%': %2%", tempLink, strerror(errno));
if (errno == EMLINK) { if (errno == EMLINK) {
/* Some filesystems generate too many links on the rename, /* Some filesystems generate too many links on the rename,
rather than on the original link. (Probably it rather than on the original link. (Probably it

View file

@ -5,6 +5,7 @@
#include "signals.hh" #include "signals.hh"
#include "url.hh" #include "url.hh"
#include <random>
#include <sqlite3.h> #include <sqlite3.h>
@ -265,9 +266,11 @@ void handleSQLiteBusy(const SQLiteBusy & e, time_t & nextWarning)
/* Sleep for a while since retrying the transaction right away /* Sleep for a while since retrying the transaction right away
is likely to fail again. */ is likely to fail again. */
checkInterrupt(); checkInterrupt();
static thread_local std::default_random_engine generator(clock());
std::uniform_int_distribution<long> uniform_dist(0, 100);
struct timespec t; struct timespec t;
t.tv_sec = 0; t.tv_sec = 0;
t.tv_nsec = (random() % 100) * 1000 * 1000; /* <= 0.1s */ t.tv_nsec = uniform_dist(generator) * 1000 * 1000; /* <= 0.1s */
nanosleep(&t, 0); nanosleep(&t, 0);
} }

View file

@ -2,6 +2,7 @@
#include <cstdlib> #include <cstdlib>
#include <filesystem> #include <filesystem>
#include <atomic> #include <atomic>
#include <random>
#include "environment-variables.hh" #include "environment-variables.hh"
#include "file-descriptor.hh" #include "file-descriptor.hh"
@ -595,6 +596,13 @@ std::pair<AutoCloseFD, Path> createTempFile(const Path & prefix)
return {std::move(fd), tmpl}; return {std::move(fd), tmpl};
} }
Path makeTempPath(const Path & root, const Path & suffix)
{
// start the counter at a random value to minimize issues with preexisting temp paths
static std::atomic_uint_fast32_t counter(std::random_device{}());
return fmt("%1%%2%-%3%-%4%", root, suffix, getpid(), counter.fetch_add(1, std::memory_order_relaxed));
}
void createSymlink(const Path & target, const Path & link) void createSymlink(const Path & target, const Path & link)
{ {
if (symlink(target.c_str(), link.c_str())) if (symlink(target.c_str(), link.c_str()))

View file

@ -296,6 +296,14 @@ std::pair<AutoCloseFD, Path> createTempFile(const Path & prefix = "nix");
*/ */
Path defaultTempDir(); Path defaultTempDir();
/**
* Return temporary path constructed by appending a suffix to a root path.
*
* The constructed path looks like `<root><suffix>-<pid>-<unique>`. To create a
* path nested in a directory, provide a suffix starting with `/`.
*/
Path makeTempPath(const Path & root, const Path & suffix = ".tmp");
/** /**
* Used in various places. * Used in various places.
*/ */