Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make initLibStore a viable alternative #7732

Merged
2 changes: 1 addition & 1 deletion configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ fi

# Look for OpenSSL, a required dependency. FIXME: this is only (maybe)
# used by S3BinaryCacheStore.
PKG_CHECK_MODULES([OPENSSL], [libcrypto], [CXXFLAGS="$OPENSSL_CFLAGS $CXXFLAGS"])
PKG_CHECK_MODULES([OPENSSL], [libcrypto >= 1.1.1], [CXXFLAGS="$OPENSSL_CFLAGS $CXXFLAGS"])


# Look for libarchive.
Expand Down
2 changes: 0 additions & 2 deletions perl/lib/Nix/Store.xs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ static ref<Store> store()
if (!_store) {
try {
initLibStore();
loadConfFile();
settings.lockCPU = false;
_store = openStore();
} catch (Error & e) {
croak("%s", e.what());
Expand Down
85 changes: 5 additions & 80 deletions src/libmain/shared.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
#include <cctype>
#include <exception>
#include <iostream>
#include <mutex>

#include <cstdlib>
#include <sys/time.h>
Expand All @@ -20,16 +19,9 @@
#ifdef __linux__
#include <features.h>
#endif
#ifdef __GLIBC__
#include <gnu/lib-names.h>
#include <nss.h>
#include <dlfcn.h>
#endif

#include <openssl/crypto.h>

#include <sodium.h>


namespace nix {

Expand Down Expand Up @@ -115,57 +107,6 @@ std::string getArg(const std::string & opt,
return *i;
}


#if OPENSSL_VERSION_NUMBER < 0x10101000L
/* OpenSSL is not thread-safe by default - it will randomly crash
unless the user supplies a mutex locking function. So let's do
that. */
static std::vector<std::mutex> opensslLocks;

static void opensslLockCallback(int mode, int type, const char * file, int line)
{
if (mode & CRYPTO_LOCK)
opensslLocks[type].lock();
else
opensslLocks[type].unlock();
}
#endif

static std::once_flag dns_resolve_flag;

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. */
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 sigHandler(int signo) { }


Expand All @@ -177,16 +118,7 @@ void initNix()
std::cerr.rdbuf()->pubsetbuf(buf, sizeof(buf));
#endif

#if OPENSSL_VERSION_NUMBER < 0x10101000L
/* Initialise OpenSSL locking. */
opensslLocks = std::vector<std::mutex>(CRYPTO_num_locks());
CRYPTO_set_locking_callback(opensslLockCallback);
#endif

if (sodium_init() == -1)
throw Error("could not initialise libsodium");

loadConfFile();
initLibStore();

startSignalHandlerThread();

Expand Down Expand Up @@ -223,7 +155,10 @@ void initNix()
if (sigaction(SIGTRAP, &act, 0)) throw SysError("handling SIGTRAP");
#endif

/* Register a SIGSEGV handler to detect stack overflows. */
/* Register a SIGSEGV handler to detect stack overflows.
Why not initLibExpr()? initGC() is essentially that, but
detectStackOverflow is not an instance of the init function concept, as
it may have to be invoked more than once per process. */
detectStackOverflow();

/* There is no privacy in the Nix system ;-) At least not for
Expand All @@ -236,16 +171,6 @@ void initNix()
gettimeofday(&tv, 0);
srandom(tv.tv_usec);

/* 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 __APPLE__
if (hasPrefix(getEnv("TMPDIR").value_or("/tmp"), "/var/folders/"))
unsetenv("TMPDIR");
#endif

preloadNSS();
initLibStore();
}


Expand Down
63 changes: 62 additions & 1 deletion src/libstore/globals.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,20 @@

#include <algorithm>
#include <map>
#include <mutex>
#include <thread>
#include <dlfcn.h>
#include <sys/utsname.h>

#include <nlohmann/json.hpp>

#include <sodium/core.h>

#ifdef __GLIBC__
#include <gnu/lib-names.h>
#include <nss.h>
#include <dlfcn.h>
#endif

namespace nix {

Expand Down Expand Up @@ -41,7 +49,6 @@ Settings::Settings()
, nixDaemonSocketFile(canonPath(getEnvNonEmpty("NIX_DAEMON_SOCKET_PATH").value_or(nixStateDir + DEFAULT_SOCKET_PATH)))
{
buildUsersGroup = getuid() == 0 ? "nixbld" : "";
lockCPU = getEnv("NIX_AFFINITY_HACK") == "1";
allowSymlinkedStore = getEnv("NIX_IGNORE_SYMLINK_STORE") == "1";

auto sslOverride = getEnv("NIX_SSL_CERT_FILE").value_or(getEnv("SSL_CERT_FILE").value_or(""));
Expand Down Expand Up @@ -281,6 +288,42 @@ void initPlugins()
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 bool initLibStoreDone = false;

void assertLibStoreInitialized() {
Expand All @@ -291,6 +334,24 @@ void assertLibStoreInitialized() {
}

void initLibStore() {

initLibUtil();

if (sodium_init() == -1)
throw Error("could not initialise libsodium");

loadConfFile();

preloadNSS();

/* 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 __APPLE__
if (hasPrefix(getEnv("TMPDIR").value_or("/tmp"), "/var/folders/"))
unsetenv("TMPDIR");
#endif
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this strictly worse than having it in initNix()? I don't think users of libstore would expect their environment to be messed with in this way.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This behavior exists for a reason, and for a libstore reason, so it's not strictly worse.

I've opened #7731 so this can be improved later. Until then I think we should stick to the current behavior, so that store users who should move from initNix to initLibStore don't regress because of this.


initLibStoreDone = true;
}

Expand Down
5 changes: 0 additions & 5 deletions src/libstore/globals.hh
Original file line number Diff line number Diff line change
Expand Up @@ -478,11 +478,6 @@ public:
)",
{"env-keep-derivations"}};

/**
* Whether to lock the Nix client and worker to the same CPU.
*/
bool lockCPU;

Setting<SandboxMode> sandboxMode{
this,
#if __linux__
Expand Down
2 changes: 1 addition & 1 deletion src/libutil/hash.cc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include <iostream>
#include <cstring>

#include <openssl/crypto.h>
#include <openssl/md5.h>
#include <openssl/sha.h>

Expand All @@ -16,7 +17,6 @@

namespace nix {


static size_t regularHashSize(HashType type) {
switch (type) {
case htMD5: return md5HashSize;
Expand Down
47 changes: 45 additions & 2 deletions src/libutil/util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ extern char * * environ __attribute__((weak));

namespace nix {

void initLibUtil() {
}
Comment on lines +50 to +51
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is odd for now, but allows libutil to initialize a dependency if need be in the future.


std::optional<std::string> getEnv(const std::string & key)
{
char * value = getenv(key.c_str());
Expand Down Expand Up @@ -1744,14 +1747,40 @@ void triggerInterrupt()
}

static sigset_t savedSignalMask;
static bool savedSignalMaskIsSet = false;

void startSignalHandlerThread()
void setChildSignalMask(sigset_t * sigs)
{
updateWindowSize();
assert(sigs); // C style function, but think of sigs as a reference

#if _POSIX_C_SOURCE >= 1 || _XOPEN_SOURCE || _POSIX_SOURCE
sigemptyset(&savedSignalMask);
// There's no "assign" or "copy" function, so we rely on (math) idempotence
// of the or operator: a or a = a.
sigorset(&savedSignalMask, sigs, sigs);
#else
// Without sigorset, our best bet is to assume that sigset_t is a type that
// can be assigned directly, such as is the case for a sigset_t defined as
// an integer type.
savedSignalMask = *sigs;
#endif

savedSignalMaskIsSet = true;
}

void saveSignalMask() {
if (sigprocmask(SIG_BLOCK, nullptr, &savedSignalMask))
throw SysError("querying signal mask");

savedSignalMaskIsSet = true;
}

void startSignalHandlerThread()
{
updateWindowSize();

saveSignalMask();

sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
Expand All @@ -1767,6 +1796,20 @@ void startSignalHandlerThread()

static void restoreSignals()
{
// If startSignalHandlerThread wasn't called, that means we're not running
// in a proper libmain process, but a process that presumably manages its
// own signal handlers. Such a process should call either
// - initNix(), to be a proper libmain process
// - startSignalHandlerThread(), to resemble libmain regarding signal
// handling only
// - saveSignalMask(), for processes that define their own signal handling
// thread
// TODO: Warn about this? Have a default signal mask? The latter depends on
// whether we should generally inherit signal masks from the caller.
// I don't know what the larger unix ecosystem expects from us here.
if (!savedSignalMaskIsSet)
return;
Comment on lines +1810 to +1811
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@edolstra, would it make sense to define a default signal mask for child processes, or is a unix process like nix expected to inherit the signal mask of its caller and pass that mask on to its children (such as git, tar, ...)?

If you think it's sensible to unblock all signals in child processes, I would do this to make the library a bit more robust / work better out of the box.

Suggested change
if (!savedSignalMaskIsSet)
return;
if (!savedSignalMaskIsSet)
sigemptyset(&savedSignalMaskIsSet);

Copy link
Member Author

@roberth roberth Feb 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you don't know or aren't confident whether it's acceptable unixing to unblock all signals in child processes, that's ok too; we'd just stick to the current, conservative approach.


if (sigprocmask(SIG_SETMASK, &savedSignalMask, nullptr))
throw SysError("restoring signals");
}
Expand Down
20 changes: 20 additions & 0 deletions src/libutil/util.hh
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ namespace nix {
struct Sink;
struct Source;

void initLibUtil();

/**
* The system for which Nix is compiled.
Expand Down Expand Up @@ -445,6 +446,8 @@ void setStackSize(size_t stackSize);
/**
* Restore the original inherited Unix process context (such as signal
* masks, stack size).

* See startSignalHandlerThread(), saveSignalMask().
*/
void restoreProcessContext(bool restoreMounts = true);

Expand Down Expand Up @@ -814,9 +817,26 @@ class Callback;
/**
* Start a thread that handles various signals. Also block those signals
* on the current thread (and thus any threads created by it).
* Saves the signal mask before changing the mask to block those signals.
* See saveSignalMask().
*/
void startSignalHandlerThread();

/**
* Saves the signal mask, which is the signal mask that nix will restore
* before creating child processes.
* See setChildSignalMask() to set an arbitrary signal mask instead of the
* current mask.
*/
void saveSignalMask();

/**
* Sets the signal mask. Like saveSignalMask() but for a signal set that doesn't
* necessarily match the current thread's mask.
* See saveSignalMask() to set the saved mask to the current mask.
*/
void setChildSignalMask(sigset_t *sigs);

struct InterruptCallback
{
virtual ~InterruptCallback() { };
Expand Down