#include "environment-variables.hh" #include "globals.hh" #include "file-system.hh" #include "logging.hh" #include "strings.hh" #include "users.hh" #include "args.hh" #include "abstract-setting-to-json.hh" #include "compute-levels.hh" #include "current-process.hh" #include #include #include #include #include #include #include #ifdef __GLIBC__ #include #include #include #endif #include "config-impl.hh" #ifdef __APPLE__ #include "processes.hh" #include #include #endif // All built-in store implementations. #include "dummy-store.hh" #include "http-binary-cache-store.hh" #include "legacy-ssh-store.hh" #include "local-binary-cache-store.hh" #include "local-store.hh" #include "s3-binary-cache-store.hh" #include "ssh-store.hh" #include "uds-remote-store.hh" namespace nix { /* The default location of the daemon socket, relative to nixStateDir. The socket is in a directory to allow you to control access to the Nix daemon by setting the mode/ownership of the directory appropriately. (This wouldn't work on the socket itself since it must be deleted and recreated on startup.) */ #define DEFAULT_SOCKET_PATH "/daemon-socket/socket" Settings settings; static GlobalConfig::Register rSettings(&settings); Settings::Settings() : nixPrefix(NIX_PREFIX) , nixStore(canonPath(getEnvNonEmpty("NIX_STORE_DIR").value_or(getEnvNonEmpty("NIX_STORE").value_or(NIX_STORE_DIR)))) , nixDataDir(canonPath(getEnvNonEmpty("NIX_DATA_DIR").value_or(NIX_DATA_DIR))) , nixLogDir(canonPath(getEnvNonEmpty("NIX_LOG_DIR").value_or(NIX_LOG_DIR))) , nixStateDir(canonPath(getEnvNonEmpty("NIX_STATE_DIR").value_or(NIX_STATE_DIR))) , nixConfDir(canonPath(getEnvNonEmpty("NIX_CONF_DIR").value_or(NIX_CONF_DIR))) , nixUserConfFiles(getUserConfigFiles()) , nixBinDir(canonPath(getEnvNonEmpty("NIX_BIN_DIR").value_or(NIX_BIN_DIR))) , nixManDir(canonPath(NIX_MAN_DIR)) , nixDaemonSocketFile(canonPath(getEnvNonEmpty("NIX_DAEMON_SOCKET_PATH").value_or(nixStateDir + DEFAULT_SOCKET_PATH))) { buildUsersGroup = getuid() == 0 ? "nixbld" : ""; allowSymlinkedStore = getEnv("NIX_IGNORE_SYMLINK_STORE") == "1"; auto sslOverride = getEnv("NIX_SSL_CERT_FILE").value_or(getEnv("SSL_CERT_FILE").value_or("")); if (sslOverride != "") caFile = sslOverride; /* Backwards compatibility. */ auto s = getEnv("NIX_REMOTE_SYSTEMS"); if (s) { Strings ss; for (auto & p : tokenizeString(*s, ":")) ss.push_back("@" + p); builders = concatStringsSep(" ", ss); } #if defined(__linux__) && defined(SANDBOX_SHELL) sandboxPaths = tokenizeString("/bin/sh=" SANDBOX_SHELL); #endif /* chroot-like behavior from Apple's sandbox */ #if __APPLE__ sandboxPaths = tokenizeString("/System/Library/Frameworks /System/Library/PrivateFrameworks /bin/sh /bin/bash /private/tmp /private/var/tmp /usr/lib"); allowedImpureHostPrefixes = tokenizeString("/System/Library /usr/lib /dev /bin/sh"); #endif /* Set the build hook location For builds we perform a self-invocation, so Lix has to be self-aware. That is, it has to know where it is installed. We don't think it's sentient. Normally, nix is installed according to `nixBinDir`, which is set at compile time, but can be overridden. This makes for a great default that works even if this code is linked as a library into some other program whose main is not aware that it might need to be a build remote hook. However, it may not have been installed at all. For example, if it's a static build, there's a good chance that it has been moved out of its installation directory. That makes `nixBinDir` useless. Instead, we'll query the OS for the path to the current executable, using `getSelfExe()`. As a last resort, we resort to `PATH`. Hopefully we find a `nix` there that's compatible. If you're porting Lix to a new platform, that might be good enough for a while, but you'll want to improve `getSelfExe()` to work on your platform. */ std::string nixExePath = nixBinDir + "/nix"; if (!pathExists(nixExePath)) { nixExePath = getSelfExe().value_or("nix"); } buildHook = { nixExePath, "__build-remote", }; } void loadConfFile() { auto applyConfigFile = [&](const ApplyConfigOptions & options) { try { std::string contents = readFile(*options.path); globalConfig.applyConfig(contents, options); } catch (SysError &) { } }; applyConfigFile(ApplyConfigOptions{.path = settings.nixConfDir + "/nix.conf"}); /* We only want to send overrides to the daemon, i.e. stuff from ~/.nix/nix.conf or the command line. */ globalConfig.resetOverridden(); auto files = settings.nixUserConfFiles; auto home = getHome(); for (auto file = files.rbegin(); file != files.rend(); file++) { applyConfigFile(ApplyConfigOptions{.path = *file, .home = home}); } auto nixConfEnv = getEnv("NIX_CONFIG"); if (nixConfEnv.has_value()) { globalConfig.applyConfig(nixConfEnv.value(), ApplyConfigOptions{.fromEnvVar = true}); } } std::vector getUserConfigFiles() { // Use the paths specified in NIX_USER_CONF_FILES if it has been defined auto nixConfFiles = getEnv("NIX_USER_CONF_FILES"); if (nixConfFiles.has_value()) { return tokenizeString>(nixConfFiles.value(), ":"); } // Use the paths specified by the XDG spec std::vector files; auto dirs = getConfigDirs(); for (auto & dir : dirs) { files.insert(files.end(), dir + "/nix/nix.conf"); } return files; } unsigned int Settings::getDefaultCores() { const unsigned int concurrency = std::max(1U, std::thread::hardware_concurrency()); const unsigned int maxCPU = getMaxCPU(); if (maxCPU > 0) return maxCPU; else return concurrency; } #if __APPLE__ static bool hasVirt() { int hasVMM; int hvSupport; size_t size; size = sizeof(hasVMM); if (sysctlbyname("kern.hv_vmm_present", &hasVMM, &size, nullptr, 0) == 0) { if (hasVMM) return false; } // whether the kernel and hardware supports virt size = sizeof(hvSupport); if (sysctlbyname("kern.hv_support", &hvSupport, &size, nullptr, 0) == 0) { return hvSupport == 1; } else { return false; } } #endif StringSet Settings::getDefaultSystemFeatures() { /* For backwards compatibility, accept some "features" that are used in Nixpkgs to route builds to certain machines but don't actually require anything special on the machines. */ StringSet features{"nixos-test", "benchmark", "big-parallel"}; #if __linux__ features.insert("uid-range"); #endif #if __linux__ if (access("/dev/kvm", R_OK | W_OK) == 0) features.insert("kvm"); #endif #if __APPLE__ if (hasVirt()) features.insert("apple-virt"); #endif return features; } StringSet Settings::getDefaultExtraPlatforms() { StringSet extraPlatforms; if (std::string{SYSTEM} == "x86_64-linux" && !isWSL1()) extraPlatforms.insert("i686-linux"); #if __linux__ StringSet levels = computeLevels(); for (auto iter = levels.begin(); iter != levels.end(); ++iter) extraPlatforms.insert(*iter + "-linux"); #elif __APPLE__ // Rosetta 2 emulation layer can run x86_64 binaries on aarch64 // machines. Note that we can’t force processes from executing // x86_64 in aarch64 environments or vice versa since they can // always exec with their own binary preferences. if (std::string{SYSTEM} == "aarch64-darwin" && runProgram(RunOptions {.program = "arch", .args = {"-arch", "x86_64", "/usr/bin/true"}, .mergeStderrToStdout = true}).first == 0) extraPlatforms.insert("x86_64-darwin"); #endif return extraPlatforms; } bool Settings::isWSL1() { struct utsname utsbuf; uname(&utsbuf); // WSL1 uses -Microsoft suffix // WSL2 uses -microsoft-standard suffix return std::string_view(utsbuf.release).ends_with("-Microsoft"); } Path Settings::getDefaultSSLCertFile() { for (auto & fn : {"/etc/ssl/certs/ca-certificates.crt", "/nix/var/nix/profiles/default/etc/ssl/certs/ca-bundle.crt"}) if (pathAccessible(fn)) return fn; return ""; } const std::string nixVersion = PACKAGE_VERSION; void to_json(nlohmann::json & j, const SandboxMode & e) { if (e == SandboxMode::smEnabled) { j = true; } else if (e == SandboxMode::smRelaxed) { j = "relaxed"; } else if (e == SandboxMode::smDisabled) { j = false; } else { abort(); } } void from_json(const nlohmann::json & j, SandboxMode & e) { if (j == true) { e = SandboxMode::smEnabled; } else if (j == "relaxed") { e = SandboxMode::smRelaxed; } else if (j == false) { e = SandboxMode::smDisabled; } else { throw Error("Invalid sandbox mode '%s'", std::string(j)); } } template<> SandboxMode BaseSetting::parse(const std::string & str, const ApplyConfigOptions & options) const { if (str == "true") return smEnabled; else if (str == "relaxed") return smRelaxed; else if (str == "false") return smDisabled; else throw UsageError("option '%s' has invalid value '%s'", name, str); } template<> struct BaseSetting::trait { static constexpr bool appendable = false; }; template<> std::string BaseSetting::to_string() const { if (value == smEnabled) return "true"; else if (value == smRelaxed) return "relaxed"; else if (value == smDisabled) return "false"; else abort(); } template<> void BaseSetting::convertToArg(Args & args, const std::string & category) { args.addFlag({ .longName = name, .description = "Enable sandboxing.", .category = category, .handler = {[this]() { override(smEnabled); }} }); args.addFlag({ .longName = "no-" + name, .description = "Disable sandboxing.", .category = category, .handler = {[this]() { override(smDisabled); }} }); args.addFlag({ .longName = "relaxed-" + name, .description = "Enable sandboxing, but allow builds to disable it.", .category = category, .handler = {[this]() { override(smRelaxed); }} }); } unsigned int MaxBuildJobsSetting::parse(const std::string & str, const ApplyConfigOptions & options) const { if (str == "auto") return std::max(1U, std::thread::hardware_concurrency()); else { if (auto n = string2Int(str)) return *n; else throw UsageError("configuration setting '%s' should be 'auto' or an integer", name); } } Paths PluginFilesSetting::parse(const std::string & str, const ApplyConfigOptions & options) const { if (pluginsLoaded) throw UsageError("plugin-files set after plugins were loaded, you may need to move the flag before the subcommand"); return BaseSetting::parse(str, options); } void initPlugins() { assert(!settings.pluginFiles.pluginsLoaded); for (const auto & pluginFile : settings.pluginFiles.get()) { Paths pluginFiles; try { auto ents = readDirectory(pluginFile); for (const auto & ent : ents) pluginFiles.emplace_back(pluginFile + "/" + ent.name); } catch (SysError & e) { if (e.errNo != ENOTDIR) throw; pluginFiles.emplace_back(pluginFile); } for (const auto & file : pluginFiles) { /* handle is purposefully leaked as there may be state in the DSO needed by the action of the plugin. */ void *handle = dlopen(file.c_str(), RTLD_LAZY | RTLD_LOCAL); if (!handle) warn("could not dynamically open plugin file '%s': %s", file, dlerror()); } } /* Since plugins can add settings, try to re-apply previously unknown settings. */ globalConfig.reapplyUnknownSettings(); globalConfig.warnUnknownSettings(); /* Tell the user if they try to set plugin-files after we've already loaded */ settings.pluginFiles.pluginsLoaded = true; } static void preloadNSS() { /* builtin:fetchurl can trigger a DNS lookup, which with glibc can trigger a dynamic library load of one of the glibc NSS libraries in a sandboxed child, which will fail unless the library's already been loaded in the parent. So we force a lookup of an invalid domain to force the NSS machinery to load its lookup libraries in the parent before any child gets a chance to. */ static std::once_flag dns_resolve_flag; std::call_once(dns_resolve_flag, []() { #ifdef __GLIBC__ /* On linux, glibc will run every lookup through the nss layer. * That means every lookup goes, by default, through nscd, which acts as a local * cache. * Because we run builds in a sandbox, we also remove access to nscd otherwise * lookups would leak into the sandbox. * * But now we have a new problem, we need to make sure the nss_dns backend that * does the dns lookups when nscd is not available is loaded or available. * * We can't make it available without leaking nix's environment, so instead we'll * load the backend, and configure nss so it does not try to run dns lookups * through nscd. * * This is technically only used for builtins:fetch* functions so we only care * about dns. * * All other platforms are unaffected. */ if (!dlopen(LIBNSS_DNS_SO, RTLD_NOW)) warn("unable to load nss_dns backend"); // FIXME: get hosts entry from nsswitch.conf. __nss_configure_lookup("hosts", "files dns"); #endif }); } static void registerStoreImplementations() { registerDummyStore(); registerHttpBinaryCacheStore(); registerLegacySSHStore(); registerLocalBinaryCacheStore(); registerLocalStore(); registerS3BinaryCacheStore(); registerSSHStore(); registerUDSRemoteStore(); } static bool initLibStoreDone = false; void assertLibStoreInitialized() { if (!initLibStoreDone) { printError("The program must call nix::initNix() before calling any libstore library functions."); std::terminate(); }; } void initLibStore() { if (sodium_init() == -1) throw Error("could not initialise libsodium"); loadConfFile(); preloadNSS(); #if __APPLE__ /* Because of an objc quirk[1], calling curl_global_init for the first time after fork() will always result in a crash. Up until now the solution has been to set OBJC_DISABLE_INITIALIZE_FORK_SAFETY for every nix process to ignore that error. Instead of working around that error we address it at the core - by calling curl_global_init here, which should mean curl will already have been initialized by the time we try to do so in a forked process. [1] https://github.com/apple-oss-distributions/objc4/blob/01edf1705fbc3ff78a423cd21e03dfc21eb4d780/runtime/objc-initialize.mm#L614-L636 */ curl_global_init(CURL_GLOBAL_ALL); /* On macOS, don't use the per-session TMPDIR (as set e.g. by sshd). This breaks build users because they don't have access to the TMPDIR, in particular in ‘nix-store --serve’. */ if (defaultTempDir().starts_with("/var/folders/")) unsetenv("TMPDIR"); #endif registerStoreImplementations(); initLibStoreDone = true; } }