#include <stdio.h>
#include <time.h>
#include <grp.h>
+#include <ctype.h>
#if HAVE_UNSHARE && HAVE_STATVFS && HAVE_SYS_MOUNT_H
#include <sched.h>
#include <sys/mount.h>
#endif
-#if HAVE_LINUX_FS_H
-#include <linux/fs.h>
#include <sys/ioctl.h>
#include <errno.h>
-#endif
#include <sqlite3.h>
if (S_ISLNK(st.st_mode))
throw Error(format(
"the path `%1%' is a symlink; "
- "this is not allowed for the Nix store and its parent directories")
+ "this is not allowed for the store and its parent directories")
% path);
path = dirOf(path);
}
LocalStore::LocalStore(bool reserveSpace)
- : didSetSubstituterEnv(false)
{
schemaPath = settings.nixDBPath + "/schema";
Path perUserDir = profilesDir + "/per-user";
createDirs(perUserDir);
- if (chmod(perUserDir.c_str(), 01777) == -1)
- throw SysError(format("could not set permissions on '%1%' to 1777") % perUserDir);
+ if (chmod(perUserDir.c_str(), 0755) == -1)
+ throw SysError(format("could not set permissions on '%1%' to 755")
+ % perUserDir);
mode_t perm = 01775;
}
if (!lockFile(globalLock, ltRead, false)) {
- printMsg(lvlError, "waiting for the big Nix store lock...");
+ printMsg(lvlError, "waiting for the big store lock...");
lockFile(globalLock, ltRead, true);
}
upgrade. */
int curSchema = getSchema();
if (curSchema > nixSchemaVersion)
- throw Error(format("current Nix store schema is version %1%, but I only support %2%")
+ throw Error(format("current store schema is version %1%, but I only support %2%")
% curSchema % nixSchemaVersion);
else if (curSchema == 0) { /* new store */
}
else if (curSchema < nixSchemaVersion) {
- if (curSchema < 5)
- throw Error(
- "Your Nix store has a database in Berkeley DB format,\n"
- "which is no longer supported. To convert to the new format,\n"
- "please upgrade Nix to version 0.12 first.");
-
- if (!lockFile(globalLock, ltWrite, false)) {
- printMsg(lvlError, "waiting for exclusive access to the Nix store...");
- lockFile(globalLock, ltWrite, true);
- }
-
- /* Get the schema version again, because another process may
- have performed the upgrade already. */
- curSchema = getSchema();
-
- if (curSchema < 6) upgradeStore6();
- else if (curSchema < 7) { upgradeStore7(); openDB(true); }
-
- writeFile(schemaPath, (format("%1%") % nixSchemaVersion).str());
-
- lockFile(globalLock, ltRead, true);
+ /* Guix always used version 7 of the schema. */
+ throw Error(
+ format("Your store database uses an implausibly old schema, version %1%.")
+ % curSchema);
}
else openDB(false);
LocalStore::~LocalStore()
{
- try {
- foreach (RunningSubstituters::iterator, i, runningSubstituters) {
- if (i->second.disabled) continue;
- i->second.to.close();
- i->second.from.close();
- i->second.error.close();
- if (i->second.pid != -1)
- i->second.pid.wait(true);
- }
- } catch (...) {
- ignoreException();
- }
-
try {
if (fdTempRoots != -1) {
fdTempRoots.close();
void LocalStore::openDB(bool create)
{
if (access(settings.nixDBPath.c_str(), R_OK | W_OK))
- throw SysError(format("Nix database directory `%1%' is not writable") % settings.nixDBPath);
+ throw SysError(format("store database directory `%1%' is not writable") % settings.nixDBPath);
- /* Open the Nix database. */
+ /* Open the store database. */
string dbPath = settings.nixDBPath + "/db.sqlite";
if (sqlite3_open_v2(dbPath.c_str(), &db.db,
SQLITE_OPEN_READWRITE | (create ? SQLITE_OPEN_CREATE : 0), 0) != SQLITE_OK)
- throw Error(format("cannot open Nix database `%1%'") % dbPath);
+ throw Error(format("cannot open store database `%1%'") % dbPath);
if (sqlite3_busy_timeout(db, 60 * 60 * 1000) != SQLITE_OK)
throwSQLiteError(db, "setting timeout");
}
-/* To improve purity, users may want to make the Nix store a read-only
- bind mount. So make the Nix store writable for this process. */
+/* To improve purity, users may want to make the store a read-only
+ bind mount. So make the store writable for this process. */
void LocalStore::makeStoreWritable()
{
#if HAVE_UNSHARE && HAVE_STATVFS && HAVE_SYS_MOUNT_H && defined(MS_BIND) && defined(MS_REMOUNT)
/* Check if /nix/store is on a read-only mount. */
struct statvfs stat;
if (statvfs(settings.nixStore.c_str(), &stat) != 0)
- throw SysError("getting info about the Nix store mount point");
+ throw SysError("getting info about the store mount point");
if (stat.f_flag & ST_RDONLY) {
if (unshare(CLONE_NEWNS) == -1)
lchown if available, otherwise don't bother. Wrong ownership
of a symlink doesn't matter, since the owning user can't change
the symlink and can't delete it because the directory is not
- writable. The only exception is top-level paths in the Nix
- store (since that directory is group-writable for the Nix build
+ writable. The only exception is top-level paths in the
+ store (since that directory is group-writable for the build
users group); we check for this case below. */
if (st.st_uid != geteuid()) {
#if HAVE_LCHOWN
});
}
-
-void LocalStore::setSubstituterEnv()
-{
- if (didSetSubstituterEnv) return;
-
- /* Pass configuration options (including those overridden with
- --option) to substituters. */
- setenv("_NIX_OPTIONS", settings.pack().c_str(), 1);
-
- didSetSubstituterEnv = true;
-}
-
-
-void LocalStore::startSubstituter(const Path & substituter, RunningSubstituter & run)
-{
- if (run.disabled || run.pid != -1) return;
-
- debug(format("starting substituter program `%1%'") % substituter);
-
- Pipe toPipe, fromPipe, errorPipe;
-
- toPipe.create();
- fromPipe.create();
- errorPipe.create();
-
- setSubstituterEnv();
-
- run.pid = startProcess([&]() {
- if (dup2(toPipe.readSide, STDIN_FILENO) == -1)
- throw SysError("dupping stdin");
- if (dup2(fromPipe.writeSide, STDOUT_FILENO) == -1)
- throw SysError("dupping stdout");
- if (dup2(errorPipe.writeSide, STDERR_FILENO) == -1)
- throw SysError("dupping stderr");
- execl(substituter.c_str(), substituter.c_str(), "--query", NULL);
- throw SysError(format("executing `%1%'") % substituter);
- });
-
- run.program = baseNameOf(substituter);
- run.to = toPipe.writeSide.borrow();
- run.from = run.fromBuf.fd = fromPipe.readSide.borrow();
- run.error = errorPipe.readSide.borrow();
-
- toPipe.readSide.close();
- fromPipe.writeSide.close();
- errorPipe.writeSide.close();
-
- /* The substituter may exit right away if it's disabled in any way
- (e.g. copy-from-other-stores.pl will exit if no other stores
- are configured). */
- try {
- getLineFromSubstituter(run);
- } catch (EndOfFile & e) {
- run.to.close();
- run.from.close();
- run.error.close();
- run.disabled = true;
- if (run.pid.wait(true) != 0) throw;
- }
-}
-
-
-/* Read a line from the substituter's stdout, while also processing
- its stderr. */
-string LocalStore::getLineFromSubstituter(RunningSubstituter & run)
+/* Read a line from the substituter's reply file descriptor, while also
+ processing its stderr. */
+string LocalStore::getLineFromSubstituter(Agent & run)
{
string res, err;
- /* We might have stdout data left over from the last time. */
- if (run.fromBuf.hasData()) goto haveData;
-
while (1) {
checkInterrupt();
fd_set fds;
FD_ZERO(&fds);
- FD_SET(run.from, &fds);
- FD_SET(run.error, &fds);
+ FD_SET(run.fromAgent.readSide, &fds);
+ FD_SET(run.builderOut.readSide, &fds);
/* Wait for data to appear on the substituter's stdout or
stderr. */
- if (select(run.from > run.error ? run.from + 1 : run.error + 1, &fds, 0, 0, 0) == -1) {
+ if (select(std::max(run.fromAgent.readSide, run.builderOut.readSide) + 1, &fds, 0, 0, 0) == -1) {
if (errno == EINTR) continue;
throw SysError("waiting for input from the substituter");
}
/* Completely drain stderr before dealing with stdout. */
- if (FD_ISSET(run.error, &fds)) {
+ if (FD_ISSET(run.fromAgent.readSide, &fds)) {
char buf[4096];
- ssize_t n = read(run.error, (unsigned char *) buf, sizeof(buf));
+ ssize_t n = read(run.fromAgent.readSide, (unsigned char *) buf, sizeof(buf));
if (n == -1) {
if (errno == EINTR) continue;
throw SysError("reading from substituter's stderr");
}
- if (n == 0) throw EndOfFile(format("substituter `%1%' died unexpectedly") % run.program);
+ if (n == 0) throw EndOfFile(format("`%1% substitute' died unexpectedly")
+ % settings.guixProgram);
err.append(buf, n);
string::size_type p;
while (((p = err.find('\n')) != string::npos)
|| ((p = err.find('\r')) != string::npos)) {
string thing(err, 0, p + 1);
- writeToStderr(run.program + ": " + thing);
+ writeToStderr("substitute: " + thing);
err = string(err, p + 1);
}
}
/* Read from stdout until we get a newline or the buffer is empty. */
- else if (run.fromBuf.hasData() || FD_ISSET(run.from, &fds)) {
- haveData:
- do {
- unsigned char c;
- run.fromBuf(&c, 1);
- if (c == '\n') {
- if (!err.empty()) printMsg(lvlError, run.program + ": " + err);
- return res;
- }
- res += c;
- } while (run.fromBuf.hasData());
+ else if (FD_ISSET(run.builderOut.readSide, &fds)) {
+ unsigned char c;
+ readFull(run.builderOut.readSide, (unsigned char *) &c, 1);
+ if (c == '\n') {
+ if (!err.empty()) printMsg(lvlError, "substitute: " + err);
+ return res;
+ }
+ res += c;
}
}
}
-template<class T> T LocalStore::getIntLineFromSubstituter(RunningSubstituter & run)
+template<class T> T LocalStore::getIntLineFromSubstituter(Agent & run)
{
string s = getLineFromSubstituter(run);
T res;
{
PathSet res;
- if (!settings.useSubstitutes) return res;
-
- foreach (Paths::iterator, i, settings.substituters) {
- if (res.size() == paths.size()) break;
- RunningSubstituter & run(runningSubstituters[*i]);
- startSubstituter(*i, run);
- if (run.disabled) continue;
- string s = "have ";
- foreach (PathSet::const_iterator, j, paths)
- if (res.find(*j) == res.end()) { s += *j; s += " "; }
- writeLine(run.to, s);
- while (true) {
- /* FIXME: we only read stderr when an error occurs, so
- substituters should only write (short) messages to
- stderr when they fail. I.e. they shouldn't write debug
- output. */
- Path path = getLineFromSubstituter(run);
- if (path == "") break;
- res.insert(path);
- }
+ if (!settings.useSubstitutes || paths.empty()) return res;
+
+ Agent & run = *substituter();
+
+ string s = "have ";
+ foreach (PathSet::const_iterator, j, paths)
+ if (res.find(*j) == res.end()) { s += *j; s += " "; }
+ writeLine(run.toAgent.writeSide, s);
+ while (true) {
+ /* FIXME: we only read stderr when an error occurs, so
+ substituters should only write (short) messages to
+ stderr when they fail. I.e. they shouldn't write debug
+ output. */
+ Path path = getLineFromSubstituter(run);
+ if (path == "") break;
+ res.insert(path);
}
+
return res;
}
-void LocalStore::querySubstitutablePathInfos(const Path & substituter,
- PathSet & paths, SubstitutablePathInfos & infos)
+std::shared_ptr<Agent> LocalStore::substituter()
+{
+ if (!runningSubstituter) {
+ const Strings args = { "substitute", "--query" };
+ const std::map<string, string> env = { { "_NIX_OPTIONS", settings.pack() } };
+ runningSubstituter = std::make_shared<Agent>(settings.guixProgram, args, env);
+ }
+
+ return runningSubstituter;
+}
+
+void LocalStore::querySubstitutablePathInfos(PathSet & paths, SubstitutablePathInfos & infos)
{
if (!settings.useSubstitutes) return;
- RunningSubstituter & run(runningSubstituters[substituter]);
- startSubstituter(substituter, run);
- if (run.disabled) return;
+ Agent & run = *substituter();
string s = "info ";
foreach (PathSet::const_iterator, i, paths)
if (infos.find(*i) == infos.end()) { s += *i; s += " "; }
- writeLine(run.to, s);
+ writeLine(run.toAgent.writeSide, s);
while (true) {
Path path = getLineFromSubstituter(run);
assertStorePath(p);
info.references.insert(p);
}
- info.downloadSize = getIntLineFromSubstituter<long long>(run);
- info.narSize = getIntLineFromSubstituter<long long>(run);
+ info.downloadSize = getIntLineFromSubstituter<unsigned long long>(run);
+ info.narSize = getIntLineFromSubstituter<unsigned long long>(run);
}
}
void LocalStore::querySubstitutablePathInfos(const PathSet & paths,
SubstitutablePathInfos & infos)
{
- PathSet todo = paths;
- foreach (Paths::iterator, i, settings.substituters) {
- if (todo.empty()) break;
- querySubstitutablePathInfos(*i, todo, infos);
+ if (!paths.empty()) {
+ PathSet todo = paths;
+ querySubstitutablePathInfos(todo, infos);
}
}
}
+/* Return the authentication agent, a "guix authenticate" process started
+ lazily. */
+static std::shared_ptr<Agent> authenticationAgent()
+{
+ static std::shared_ptr<Agent> agent;
+
+ if (!agent) {
+ Strings args = { "authenticate" };
+ agent = std::make_shared<Agent>(settings.guixProgram, args);
+ }
+
+ return agent;
+}
+
+/* Read an integer and the byte that immediately follows it from FD. Return
+ the integer. */
+static int readInteger(int fd)
+{
+ string str;
+
+ while (1) {
+ char ch;
+ ssize_t rd = read(fd, &ch, 1);
+ if (rd == -1) {
+ if (errno != EINTR)
+ throw SysError("reading an integer");
+ } else if (rd == 0)
+ throw EndOfFile("unexpected EOF reading an integer");
+ else {
+ if (isdigit(ch)) {
+ str += ch;
+ } else {
+ break;
+ }
+ }
+ }
+
+ return stoi(str);
+}
+
+/* Read from FD a reply coming from 'guix authenticate'. The reply has the
+ form "CODE LEN:STR". CODE is an integer, where zero indicates success.
+ LEN specifies the length in bytes of the string that immediately
+ follows. */
+static std::string readAuthenticateReply(int fd)
+{
+ int code = readInteger(fd);
+ int len = readInteger(fd);
+
+ string str;
+ str.resize(len);
+ readFull(fd, (unsigned char *) &str[0], len);
+
+ if (code == 0)
+ return str;
+ else
+ throw Error(str);
+}
+
+/* Sign HASH with the key stored in file SECRETKEY. Return the signature as a
+ string, or raise an exception upon error. */
+static std::string signHash(const string &secretKey, const Hash &hash)
+{
+ auto agent = authenticationAgent();
+ auto hexHash = printHash(hash);
+
+ writeLine(agent->toAgent.writeSide,
+ (format("sign %1%:%2% %3%:%4%")
+ % secretKey.size() % secretKey
+ % hexHash.size() % hexHash).str());
+
+ return readAuthenticateReply(agent->fromAgent.readSide);
+}
+
+/* Verify SIGNATURE and return the base16-encoded hash over which it was
+ computed. */
+static std::string verifySignature(const string &signature)
+{
+ auto agent = authenticationAgent();
+
+ writeLine(agent->toAgent.writeSide,
+ (format("verify %1%:%2%")
+ % signature.size() % signature).str());
+
+ return readAuthenticateReply(agent->fromAgent.readSide);
+}
+
void LocalStore::exportPath(const Path & path, bool sign,
Sink & sink)
{
writeInt(1, hashAndWriteSink);
- Path tmpDir = createTempDir();
- AutoDelete delTmp(tmpDir);
- Path hashFile = tmpDir + "/hash";
- writeFile(hashFile, printHash(hash));
-
Path secretKey = settings.nixConfDir + "/signing-key.sec";
checkSecrecy(secretKey);
- Strings args;
- args.push_back("rsautl");
- args.push_back("-sign");
- args.push_back("-inkey");
- args.push_back(secretKey);
- args.push_back("-in");
- args.push_back(hashFile);
- string signature = runProgram(OPENSSL_PATH, true, args);
+ string signature = signHash(secretKey, hash);
writeString(signature, hashAndWriteSink);
unsigned int magic = readInt(hashAndReadSource);
if (magic != EXPORT_MAGIC)
- throw Error("Nix archive cannot be imported; wrong format");
+ throw Error("normalized archive cannot be imported; wrong format");
Path dstPath = readStorePath(hashAndReadSource);
string signature = readString(hashAndReadSource);
if (requireSignature) {
- Path sigFile = tmpDir + "/sig";
- writeFile(sigFile, signature);
-
- Strings args;
- args.push_back("rsautl");
- args.push_back("-verify");
- args.push_back("-inkey");
- args.push_back(settings.nixConfDir + "/signing-key.pub");
- args.push_back("-pubin");
- args.push_back("-in");
- args.push_back(sigFile);
- string hash2 = runProgram(OPENSSL_PATH, true, args);
+ string hash2 = verifySignature(signature);
/* Note: runProgram() throws an exception if the signature
is invalid. */
bool LocalStore::verifyStore(bool checkContents, bool repair)
{
- printMsg(lvlError, format("reading the Nix store..."));
+ printMsg(lvlError, format("reading the store..."));
bool errors = false;
done.insert(path);
if (!isStorePath(path)) {
- printMsg(lvlError, format("path `%1%' is not in the Nix store") % path);
+ printMsg(lvlError, format("path `%1%' is not in the store") % path);
invalidatePath(path);
return;
}
}
-/* Functions for upgrading from the pre-SQLite database. */
-
-PathSet LocalStore::queryValidPathsOld()
-{
- PathSet paths;
- for (auto & i : readDirectory(settings.nixDBPath + "/info"))
- if (i.name.at(0) != '.') paths.insert(settings.nixStore + "/" + i.name);
- return paths;
-}
-
-
-ValidPathInfo LocalStore::queryPathInfoOld(const Path & path)
-{
- ValidPathInfo res;
- res.path = path;
-
- /* Read the info file. */
- string baseName = baseNameOf(path);
- Path infoFile = (format("%1%/info/%2%") % settings.nixDBPath % baseName).str();
- if (!pathExists(infoFile))
- throw Error(format("path `%1%' is not valid") % path);
- string info = readFile(infoFile);
-
- /* Parse it. */
- Strings lines = tokenizeString<Strings>(info, "\n");
-
- foreach (Strings::iterator, i, lines) {
- string::size_type p = i->find(':');
- if (p == string::npos)
- throw Error(format("corrupt line in `%1%': %2%") % infoFile % *i);
- string name(*i, 0, p);
- string value(*i, p + 2);
- if (name == "References") {
- Strings refs = tokenizeString<Strings>(value, " ");
- res.references = PathSet(refs.begin(), refs.end());
- } else if (name == "Deriver") {
- res.deriver = value;
- } else if (name == "Hash") {
- res.hash = parseHashField(path, value);
- } else if (name == "Registered-At") {
- int n = 0;
- string2Int(value, n);
- res.registrationTime = n;
- }
- }
-
- return res;
-}
-
-
-/* Upgrade from schema 5 (Nix 0.12) to schema 6 (Nix >= 0.15). */
-void LocalStore::upgradeStore6()
-{
- printMsg(lvlError, "upgrading Nix store to new schema (this may take a while)...");
-
- openDB(true);
-
- PathSet validPaths = queryValidPathsOld();
-
- SQLiteTxn txn(db);
-
- foreach (PathSet::iterator, i, validPaths) {
- addValidPath(queryPathInfoOld(*i), false);
- std::cerr << ".";
- }
-
- std::cerr << "|";
-
- foreach (PathSet::iterator, i, validPaths) {
- ValidPathInfo info = queryPathInfoOld(*i);
- unsigned long long referrer = queryValidPathId(*i);
- foreach (PathSet::iterator, j, info.references)
- addReference(referrer, queryValidPathId(*j));
- std::cerr << ".";
- }
-
- std::cerr << "\n";
-
- txn.commit();
-}
-
-
-#if defined(FS_IOC_SETFLAGS) && defined(FS_IOC_GETFLAGS) && defined(FS_IMMUTABLE_FL)
-
-static void makeMutable(const Path & path)
-{
- checkInterrupt();
-
- struct stat st = lstat(path);
-
- if (!S_ISDIR(st.st_mode) && !S_ISREG(st.st_mode)) return;
-
- if (S_ISDIR(st.st_mode)) {
- for (auto & i : readDirectory(path))
- makeMutable(path + "/" + i.name);
- }
-
- /* The O_NOFOLLOW is important to prevent us from changing the
- mutable bit on the target of a symlink (which would be a
- security hole). */
- AutoCloseFD fd = open(path.c_str(), O_RDONLY | O_NOFOLLOW);
- if (fd == -1) {
- if (errno == ELOOP) return; // it's a symlink
- throw SysError(format("opening file `%1%'") % path);
- }
-
- unsigned int flags = 0, old;
-
- /* Silently ignore errors getting/setting the immutable flag so
- that we work correctly on filesystems that don't support it. */
- if (ioctl(fd, FS_IOC_GETFLAGS, &flags)) return;
- old = flags;
- flags &= ~FS_IMMUTABLE_FL;
- if (old == flags) return;
- if (ioctl(fd, FS_IOC_SETFLAGS, &flags)) return;
-}
-
-/* Upgrade from schema 6 (Nix 0.15) to schema 7 (Nix >= 1.3). */
-void LocalStore::upgradeStore7()
+void LocalStore::vacuumDB()
{
- if (getuid() != 0) return;
- printMsg(lvlError, "removing immutable bits from the Nix store (this may take a while)...");
- makeMutable(settings.nixStore);
+ if (sqlite3_exec(db, "vacuum;", 0, 0, 0) != SQLITE_OK)
+ throwSQLiteError(db, "vacuuming SQLite database");
}
-#else
-void LocalStore::upgradeStore7()
+void LocalStore::createUser(const std::string & userName, uid_t userId)
{
-}
-
-#endif
-
+ auto dir = settings.nixStateDir + "/profiles/per-user/" + userName;
-void LocalStore::vacuumDB()
-{
- if (sqlite3_exec(db, "vacuum;", 0, 0, 0) != SQLITE_OK)
- throwSQLiteError(db, "vacuuming SQLite database");
+ createDirs(dir);
+ if (chmod(dir.c_str(), 0755) == -1)
+ throw SysError(format("changing permissions of directory '%s'") % dir);
+ if (chown(dir.c_str(), userId, -1) == -1)
+ throw SysError(format("changing owner of directory '%s'") % dir);
}