1
0
Fork 0
mirror of https://github.com/NixOS/nix synced 2025-06-25 10:41:16 +02:00

sandbox-paths: rewrite read-only paths to use json config format

Signed-off-by: Samuli Thomasson <samuli.thomasson@pm.me>
This commit is contained in:
Samuli Thomasson 2025-06-13 17:07:08 +02:00
parent 6041531716
commit c9651a0cbd
No known key found for this signature in database
GPG key ID: 6B8903D2645A5B48
4 changed files with 243 additions and 53 deletions

View file

@ -86,12 +86,12 @@ Settings::Settings()
} }
#if (defined(__linux__) || defined(__FreeBSD__)) && defined(SANDBOX_SHELL) #if (defined(__linux__) || defined(__FreeBSD__)) && defined(SANDBOX_SHELL)
sandboxPaths = tokenizeString<StringSet>("/bin/sh=" SANDBOX_SHELL); sandboxPaths = SandboxPaths { { "/bin/sh", SandboxPath(SANDBOX_SHELL) } };
#endif #endif
/* chroot-like behavior from Apple's sandbox */ /* chroot-like behavior from Apple's sandbox */
#ifdef __APPLE__ #ifdef __APPLE__
sandboxPaths = tokenizeString<StringSet>("/System/Library/Frameworks /System/Library/PrivateFrameworks /bin/sh /bin/bash /private/tmp /private/var/tmp /usr/lib"); sandboxPaths.setDefault("/System/Library/Frameworks /System/Library/PrivateFrameworks /bin/sh /bin/bash /private/tmp /private/var/tmp /usr/lib");
allowedImpureHostPrefixes = tokenizeString<StringSet>("/System/Library /usr/lib /dev /bin/sh"); allowedImpureHostPrefixes = tokenizeString<StringSet>("/System/Library /usr/lib /dev /bin/sh");
#endif #endif
} }
@ -296,6 +296,94 @@ template<> void BaseSetting<SandboxMode>::convertToArg(Args & args, const std::s
}); });
} }
NLOHMANN_JSON_SERIALIZE_ENUM(SandboxPath::MountOpt, {
{SandboxPath::MountOpt::ro, "ro"},
#ifdef __linux__
{SandboxPath::MountOpt::nodev, "nodev"},
{SandboxPath::MountOpt::noexec, "noexec"},
{SandboxPath::MountOpt::nosuid, "nosuid"},
{SandboxPath::MountOpt::noatime, "noatime"},
{SandboxPath::MountOpt::nodiratime, "nodiratime"},
{SandboxPath::MountOpt::relatime, "relatime"},
{SandboxPath::MountOpt::strictatime, "strictatime"},
{SandboxPath::MountOpt::private_, "private"},
{SandboxPath::MountOpt::slave, "slave"},
{SandboxPath::MountOpt::unbindable, "unbindable"},
#endif
});
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(SandboxPath, source, optional, readOnly, options);
/**
* Parses either old (strings) or new (json object) format sandbox-paths.
*/
SandboxPaths SandboxPath::parse(const std::string_view & str, const std::string & ctx)
{
SandboxPaths res;
auto add = [&](std::string target, SandboxPath v) {
if (target == "")
throw UsageError("setting '%s' is an object whose keys are paths and paths cannot be empty", ctx);
target = canonPath(std::move(target));
if (v.source == "") v.source = target;
if (!res.try_emplace(target, std::move(v)).second)
throw UsageError("Sandbox path declared twice in '%s': %s", ctx, target);
};
if (str.starts_with('{')) {
for (auto & [k, v] : nlohmann::json::parse(str, nullptr, false, true).template get<SandboxPaths>())
add(k, std::move(v));
} else {
/* Parses legacy format sandbox-path e.g. "path[=source][?]".
* This format supports only a subset of options available with JSON format. */
for (std::string_view s : tokenizeString<Strings>(str)) {
bool optional = s.ends_with('?');
if (optional) s.remove_suffix(1);
if (size_t eq = s.find('='); eq != s.npos) {
add(std::string(s, 0, eq), { std::string(s.data() + eq + 1, s.size() - eq - 1), optional });
} else
add(std::string(s), { "", optional });
}
}
return res;
}
template<> SandboxPaths BaseSetting<SandboxPaths>::parse(const std::string & str) const
{
return SandboxPath().parse(str, this->name);
}
template<> struct BaseSetting<SandboxPaths>::trait
{
static constexpr bool appendable = true;
};
/* Omits keys that are set to their default values. */
template<> std::string BaseSetting<SandboxPaths>::to_string() const
{
if (value.empty())
return "";
nlohmann::json res = nlohmann::json::object();
for (const auto & [k, v] : value) {
auto po = nlohmann::json::object();
if (v.source != "" && v.source != k) po.emplace("source", v.source);
if (v.optional) po.emplace("optional", v.optional);
if (v.readOnly) po.emplace("readOnly", v.readOnly);
if (!v.options.empty()) po.emplace("options", v.options);
res.emplace(k, std::move(po));
}
return res.dump();
}
template<> void BaseSetting<SandboxPaths>::appendOrSet(SandboxPaths newValue, bool append)
{
if (!append) value.clear();
for (auto & [k, v] : newValue)
value.insert_or_assign(std::move(k), std::move(v));
}
template class BaseSetting<SandboxPaths>;
unsigned int MaxBuildJobsSetting::parse(const std::string & str) const unsigned int MaxBuildJobsSetting::parse(const std::string & str) const
{ {
if (str == "auto") return std::max(1U, std::thread::hardware_concurrency()); if (str == "auto") return std::max(1U, std::thread::hardware_concurrency());

View file

@ -5,6 +5,9 @@
#include <limits> #include <limits>
#include <sys/types.h> #include <sys/types.h>
#ifdef __linux__
#include <sys/mount.h>
#endif
#include "nix/util/types.hh" #include "nix/util/types.hh"
#include "nix/util/configuration.hh" #include "nix/util/configuration.hh"
@ -18,6 +21,58 @@ namespace nix {
typedef enum { smEnabled, smRelaxed, smDisabled } SandboxMode; typedef enum { smEnabled, smRelaxed, smDisabled } SandboxMode;
struct SandboxPath;
using SandboxPaths = std::map<Path, SandboxPath, std::less<>>;
struct SandboxPath
{
typedef enum {
#ifdef __linux__
ro = MS_RDONLY,
nodev = MS_NODEV,
noexec = MS_NOEXEC,
nosuid = MS_NOSUID,
noatime = MS_NOATIME,
nodiratime = MS_NODIRATIME,
relatime = MS_RELATIME,
strictatime = MS_STRICTATIME, /* overrides any atime/relatime */
private_ = MS_PRIVATE,
slave = MS_SLAVE,
unbindable = MS_UNBINDABLE
#else
ro // FIXME: do any options make sense on other that linux?
#endif
} MountOpt;
#ifdef __linux__
/* Options to set when readOnly=true */
static constexpr std::array<MountOpt, 5> readOnlyDefaults = {
ro, nodev, noexec, nosuid, noatime
};
#endif
Path source;
/**
* Ignore path if source is missing.
*/
bool optional;
/**
* Enables MS_RDONLY, NODEV, NOSUID, NOEXEC and NOATIME. You can get finer
* control with 'options' instead.
* */
bool readOnly;
std::vector<MountOpt> options;
SandboxPath(const Path & source = "",
bool optional = false, bool readOnly = false, const std::vector<MountOpt> & options = { })
: source(source), optional(optional), readOnly(readOnly), options(options) { };
SandboxPath(const char * source) : SandboxPath(Path(source)) { };
static SandboxPaths parse(const std::string_view & str, const std::string& = "(unknown)");
};
struct MaxBuildJobsSetting : public BaseSetting<unsigned int> struct MaxBuildJobsSetting : public BaseSetting<unsigned int>
{ {
MaxBuildJobsSetting(Config * options, MaxBuildJobsSetting(Config * options,
@ -629,24 +684,76 @@ public:
)", )",
{"build-use-chroot", "build-use-sandbox"}}; {"build-use-chroot", "build-use-sandbox"}};
Setting<PathSet> sandboxPaths{ Setting<SandboxPaths> sandboxPaths{
this, {}, "sandbox-paths", this, {}, "sandbox-paths",
R"( R"(
A list of paths bind-mounted into Nix sandbox environments. Use the Paths to bind-mount into Nix sandbox environments.
syntax `target[=source][:ro][?]` to control the mount: Two syntaxes can be used:
- `=source` will mount a different path at target location; for 1. Original (old) syntax: Strings separated by whitespace. Entries
instance, `/bin=/nix-bin` will mount the path `/nix-bin` as `/bin` are parsed as `TARGET[=SOURCE][?]`. Only the `TARGET` path is
inside the sandbox. required.
- `:ro` makes the mount read-only (Linux only). `SOURCE` can be set following an equals sign (`=`) to specify a
different source path (the value of `TARGET` is used by default
for the source path as well). For instance, `/bin=/nix-bin` would
mount path `/nix-bin` in `/bin` inside the sandbox.
- `?` makes it not an error if *source* does not exist; for example, A `?` suffix can be used to make it not an error if the `SOURCE`
`/dev/nvidiactl?` specifies that `/dev/nvidiactl` will only be path does not exist. Without it an error is raised for an
mounted in the sandbox if it exists in the host filesystem. unavailable path. For instance, `/dev/nvidiactl?` specifies that
`/dev/nvidiactl` will only be mounted in the sandbox if it exists
in the host filesystem.
If the source is in the Nix store, then its closure will be added to 2. JSON syntax (new): Using this form more configurable settings
the sandbox as well. become available. All paths are specified in a single JSON object
so that every key is a target path inside the sandbox and the
corresponding values can contain additional (platform-specific)
settings.
For instance:
```nix
sandbox-paths = {"/bin/sh":{}} # /bin/sh
sandbox-paths = {"/bin/sh":{"source":"/usr/bin/bash"}} # /bin/sh=/usr/bin/bash
sandbox-paths = {"/etc/nix/netrc":{"optional":true}} # /etc/nix/netrc?
```
Additional per-path options are available on Linux:
- `readOnly` (boolean)
When this is `true`, the bind-mount is made read-only and
additional mount-point flags are enabled. In particular these
options are enabled by this flag: `ro`, `nosuid`, `nodev`,
`noexec` and `noatime`.
- `options` (string array)
This setting can be used to add/modify (some) mount(-point) flags
directly. In addition to flags used by `readOnly` the following
flags can also be used: `nodiratime`, `relatime`, `strictatime`,
`unbindable`, `private`, `slave`.
Full example:
```nix
sandbox-paths = {
"/path/to" : {
"source" : "/path/from", # ()
"optional" : true, # (false)
"readOnly" : true, # (false)
"options" : [ "optionA", "optionB", ... ], # ()
},
# ...
}
```
> **Note:**
>
> If the source is in the Nix store, then its closure will
> be added to the sandbox as well.
Depending on how Nix was built, the default value for this option Depending on how Nix was built, the default value for this option
may be empty or provide `/bin/sh` as a bind-mount of `bash`. may be empty or provide `/bin/sh` as a bind-mount of `bash`.

View file

@ -105,15 +105,8 @@ protected:
/** /**
* Stuff we need to pass to initChild(). * Stuff we need to pass to initChild().
*/ */
struct ChrootPath {
Path source; typedef SandboxPaths PathsInChroot; // maps target path to source path
bool optional;
bool rdonly;
ChrootPath(Path source = "", bool optional = false, bool rdonly = false)
: source(source), optional(optional), rdonly(rdonly)
{ }
};
typedef std::map<Path, ChrootPath> PathsInChroot; // maps target path to source path
typedef StringMap Environment; typedef StringMap Environment;
Environment env; Environment env;
@ -848,35 +841,16 @@ DerivationBuilderImpl::PathsInChroot DerivationBuilderImpl::getPathsInSandbox()
{ {
PathsInChroot pathsInChroot; PathsInChroot pathsInChroot;
auto addPathWithOptions = [&](std::string s) {
if (s.empty()) return;
bool optional = false;
bool rdonly = false;
if (s[s.size() - 1] == '?') {
optional = true;
s.pop_back();
}
if (s.size() > 3 && s.substr(s.size() - 3) == ":ro") {
rdonly = true;
s.resize(s.size() - 3);
}
size_t p = s.find('=');
if (p == std::string::npos)
pathsInChroot[s] = {s, optional, rdonly};
else
pathsInChroot[s.substr(0, p)] = {s.substr(p + 1), optional, rdonly};
};
/* Allow a user-configurable set of directories from the /* Allow a user-configurable set of directories from the
host file system. */ host file system. */
for (auto i : settings.sandboxPaths.get()) { for (const auto & [k, v] : settings.sandboxPaths.get())
addPathWithOptions(i); pathsInChroot.insert_or_assign(k, v);
}
if (hasPrefix(store.storeDir, tmpDirInSandbox())) if (hasPrefix(store.storeDir, tmpDirInSandbox()))
{ {
throw Error("`sandbox-build-dir` must not contain the storeDir"); throw Error("`sandbox-build-dir` must not contain the storeDir");
} }
pathsInChroot[tmpDirInSandbox()] = tmpDir; pathsInChroot.insert_or_assign(tmpDirInSandbox(), tmpDir);
/* Add the closure of store paths to the chroot. */ /* Add the closure of store paths to the chroot. */
StorePathSet closure; StorePathSet closure;
@ -946,7 +920,8 @@ DerivationBuilderImpl::PathsInChroot DerivationBuilderImpl::getPathsInSandbox()
if (line == "") { if (line == "") {
state = stBegin; state = stBegin;
} else { } else {
addPathWithOptions(line); for (const auto & [k, v] : SandboxPath().parse(line))
pathsInChroot.try_emplace(k, v);
} }
} }
} }

View file

@ -121,18 +121,38 @@ static void setupSeccomp()
# endif # endif
} }
static void doBind(const Path & source, const Path & target, bool optional = false, bool rdonly = false) static auto combineMountOpts(auto init, auto iter)
{ {
return std::transform_reduce(iter.cbegin(), iter.cend(), init,
[](unsigned long a, unsigned long b) {
if (b & (MS_NOATIME | MS_RELATIME | MS_NODIRATIME | MS_STRICTATIME)) {
return (a & ~(MS_NOATIME | MS_NODIRATIME | MS_RELATIME | MS_STRICTATIME)) | b;
}
return a | b;
},
[](const SandboxPath::MountOpt & o) { return static_cast<unsigned long>(o); });
};
static void doBind(const SandboxPath & pval, const Path & target)
{
auto source = pval.source;
auto optional = pval.optional;
debug("bind mounting '%1%' to '%2%'", source, target); debug("bind mounting '%1%' to '%2%'", source, target);
auto bindMount = [&]() { auto bindMount = [&]() {
if (mount(source.c_str(), target.c_str(), "", MS_BIND | MS_REC, 0) == -1) if (mount(source.c_str(), target.c_str(), "", MS_BIND | MS_REC, 0) == -1)
throw SysError("bind mount from '%1%' to '%2%' failed", source, target); throw SysError("bind mount from '%1%' to '%2%' failed", source, target);
if (rdonly) // set extra options when wanted
auto flags = pval.readOnly ? combineMountOpts(0, pval.readOnlyDefaults) : 0;
flags = combineMountOpts(flags, pval.options);
if (flags != 0) {
// initial mount wouldn't respect MS_RDONLY, must remount // initial mount wouldn't respect MS_RDONLY, must remount
if (mount("", target.c_str(), "", MS_REMOUNT | MS_BIND | MS_RDONLY, 0) == -1) debug("remounting '%s' with flags: %d", target, flags);
throw (SysError("making bind mount '%s' read-only failed", target)); if (mount("", target.c_str(), "", MS_REMOUNT | MS_BIND | flags, 0) == -1)
throw SysError("mount: updating bind-mount flags of '%s' failed", target);
}
}; };
auto maybeSt = maybeLstat(source); auto maybeSt = maybeLstat(source);
@ -677,7 +697,7 @@ struct ChrootLinuxDerivationBuilder : LinuxDerivationBuilder
// For backwards-compatibility, resolve all the symlinks in the // For backwards-compatibility, resolve all the symlinks in the
// chroot paths. // chroot paths.
auto canonicalPath = canonPath(i, true); auto canonicalPath = canonPath(i, true);
pathsInChroot.emplace(i, canonicalPath); pathsInChroot.try_emplace(i, canonicalPath);
} }
/* Bind-mount all the directories from the "host" /* Bind-mount all the directories from the "host"
@ -699,7 +719,7 @@ struct ChrootLinuxDerivationBuilder : LinuxDerivationBuilder
} else } else
# endif # endif
{ {
doBind(i.second.source, chrootRootDir + i.first, i.second.optional); doBind(i.second, chrootRootDir + i.first);
} }
} }