Skip to content

Commit 97c779b

Browse files
committed
sandbox-paths: rewrite read-only paths to use json config format
Signed-off-by: Samuli Thomasson <[email protected]>
1 parent 39e28a3 commit 97c779b

File tree

4 files changed

+243
-53
lines changed

4 files changed

+243
-53
lines changed

src/libstore/globals.cc

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,12 @@ Settings::Settings()
8686
}
8787

8888
#if (defined(__linux__) || defined(__FreeBSD__)) && defined(SANDBOX_SHELL)
89-
sandboxPaths = tokenizeString<StringSet>("/bin/sh=" SANDBOX_SHELL);
89+
sandboxPaths = SandboxPaths { { "/bin/sh", SandboxPath(SANDBOX_SHELL) } };
9090
#endif
9191

9292
/* chroot-like behavior from Apple's sandbox */
9393
#ifdef __APPLE__
94-
sandboxPaths = tokenizeString<StringSet>("/System/Library/Frameworks /System/Library/PrivateFrameworks /bin/sh /bin/bash /private/tmp /private/var/tmp /usr/lib");
94+
sandboxPaths.setDefault("/System/Library/Frameworks /System/Library/PrivateFrameworks /bin/sh /bin/bash /private/tmp /private/var/tmp /usr/lib");
9595
allowedImpureHostPrefixes = tokenizeString<StringSet>("/System/Library /usr/lib /dev /bin/sh");
9696
#endif
9797
}
@@ -296,6 +296,94 @@ template<> void BaseSetting<SandboxMode>::convertToArg(Args & args, const std::s
296296
});
297297
}
298298

299+
NLOHMANN_JSON_SERIALIZE_ENUM(SandboxPath::MountOpt, {
300+
{SandboxPath::MountOpt::ro, "ro"},
301+
#ifdef __linux__
302+
{SandboxPath::MountOpt::nodev, "nodev"},
303+
{SandboxPath::MountOpt::noexec, "noexec"},
304+
{SandboxPath::MountOpt::nosuid, "nosuid"},
305+
{SandboxPath::MountOpt::noatime, "noatime"},
306+
{SandboxPath::MountOpt::nodiratime, "nodiratime"},
307+
{SandboxPath::MountOpt::relatime, "relatime"},
308+
{SandboxPath::MountOpt::strictatime, "strictatime"},
309+
{SandboxPath::MountOpt::private_, "private"},
310+
{SandboxPath::MountOpt::slave, "slave"},
311+
{SandboxPath::MountOpt::unbindable, "unbindable"},
312+
#endif
313+
});
314+
315+
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(SandboxPath, source, optional, readOnly, options);
316+
317+
/**
318+
* Parses either old (strings) or new (json object) format sandbox-paths.
319+
*/
320+
SandboxPaths SandboxPath::parse(const std::string_view & str, const std::string & ctx)
321+
{
322+
SandboxPaths res;
323+
324+
auto add = [&](std::string target, SandboxPath v) {
325+
if (target == "")
326+
throw UsageError("setting '%s' is an object whose keys are paths and paths cannot be empty", ctx);
327+
target = canonPath(std::move(target));
328+
if (v.source == "") v.source = target;
329+
if (!res.try_emplace(target, std::move(v)).second)
330+
throw UsageError("Sandbox path declared twice in '%s': %s", ctx, target);
331+
};
332+
333+
if (str.starts_with('{')) {
334+
for (auto & [k, v] : nlohmann::json::parse(str, nullptr, false, true).template get<SandboxPaths>())
335+
add(k, std::move(v));
336+
} else {
337+
/* Parses legacy format sandbox-path e.g. "path[=source][?]".
338+
* This format supports only a subset of options available with JSON format. */
339+
for (std::string_view s : tokenizeString<Strings>(str)) {
340+
bool optional = s.ends_with('?');
341+
if (optional) s.remove_suffix(1);
342+
if (size_t eq = s.find('='); eq != s.npos) {
343+
add(std::string(s, 0, eq), { std::string(s.data() + eq + 1, s.size() - eq - 1), optional });
344+
} else
345+
add(std::string(s), { "", optional });
346+
}
347+
}
348+
return res;
349+
}
350+
351+
template<> SandboxPaths BaseSetting<SandboxPaths>::parse(const std::string & str) const
352+
{
353+
return SandboxPath().parse(str, this->name);
354+
}
355+
356+
template<> struct BaseSetting<SandboxPaths>::trait
357+
{
358+
static constexpr bool appendable = true;
359+
};
360+
361+
/* Omits keys that are set to their default values. */
362+
template<> std::string BaseSetting<SandboxPaths>::to_string() const
363+
{
364+
if (value.empty())
365+
return "";
366+
nlohmann::json res = nlohmann::json::object();
367+
for (const auto & [k, v] : value) {
368+
auto po = nlohmann::json::object();
369+
if (v.source != "" && v.source != k) po.emplace("source", v.source);
370+
if (v.optional) po.emplace("optional", v.optional);
371+
if (v.readOnly) po.emplace("readOnly", v.readOnly);
372+
if (!v.options.empty()) po.emplace("options", v.options);
373+
res.emplace(k, std::move(po));
374+
}
375+
return res.dump();
376+
}
377+
378+
template<> void BaseSetting<SandboxPaths>::appendOrSet(SandboxPaths newValue, bool append)
379+
{
380+
if (!append) value.clear();
381+
for (auto & [k, v] : newValue)
382+
value.insert_or_assign(std::move(k), std::move(v));
383+
}
384+
385+
template class BaseSetting<SandboxPaths>;
386+
299387
unsigned int MaxBuildJobsSetting::parse(const std::string & str) const
300388
{
301389
if (str == "auto") return std::max(1U, std::thread::hardware_concurrency());

src/libstore/include/nix/store/globals.hh

Lines changed: 119 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
#include <limits>
66

77
#include <sys/types.h>
8+
#ifdef __linux__
9+
#include <sys/mount.h>
10+
#endif
811

912
#include "nix/util/types.hh"
1013
#include "nix/util/configuration.hh"
@@ -18,6 +21,58 @@ namespace nix {
1821

1922
typedef enum { smEnabled, smRelaxed, smDisabled } SandboxMode;
2023

24+
struct SandboxPath;
25+
using SandboxPaths = std::map<Path, SandboxPath, std::less<>>;
26+
struct SandboxPath
27+
{
28+
typedef enum {
29+
#ifdef __linux__
30+
ro = MS_RDONLY,
31+
nodev = MS_NODEV,
32+
noexec = MS_NOEXEC,
33+
nosuid = MS_NOSUID,
34+
noatime = MS_NOATIME,
35+
nodiratime = MS_NODIRATIME,
36+
relatime = MS_RELATIME,
37+
strictatime = MS_STRICTATIME, /* overrides any atime/relatime */
38+
private_ = MS_PRIVATE,
39+
slave = MS_SLAVE,
40+
unbindable = MS_UNBINDABLE
41+
#else
42+
ro // FIXME: do any options make sense on other that linux?
43+
#endif
44+
} MountOpt;
45+
46+
#ifdef __linux__
47+
/* Options to set when readOnly=true */
48+
static constexpr std::array<MountOpt, 5> readOnlyDefaults = {
49+
ro, nodev, noexec, nosuid, noatime
50+
};
51+
#endif
52+
53+
Path source;
54+
55+
/**
56+
* Ignore path if source is missing.
57+
*/
58+
bool optional;
59+
60+
/**
61+
* Enables MS_RDONLY, NODEV, NOSUID, NOEXEC and NOATIME. You can get finer
62+
* control with 'options' instead.
63+
* */
64+
bool readOnly;
65+
66+
std::vector<MountOpt> options;
67+
68+
SandboxPath(const Path & source = "",
69+
bool optional = false, bool readOnly = false, const std::vector<MountOpt> & options = { })
70+
: source(source), optional(optional), readOnly(readOnly), options(options) { };
71+
SandboxPath(const char * source) : SandboxPath(Path(source)) { };
72+
73+
static SandboxPaths parse(const std::string_view & str, const std::string& = "(unknown)");
74+
};
75+
2176
struct MaxBuildJobsSetting : public BaseSetting<unsigned int>
2277
{
2378
MaxBuildJobsSetting(Config * options,
@@ -629,24 +684,76 @@ public:
629684
)",
630685
{"build-use-chroot", "build-use-sandbox"}};
631686

632-
Setting<PathSet> sandboxPaths{
687+
Setting<SandboxPaths> sandboxPaths{
633688
this, {}, "sandbox-paths",
634689
R"(
635-
A list of paths bind-mounted into Nix sandbox environments. Use the
636-
syntax `target[=source][:ro][?]` to control the mount:
690+
Paths to bind-mount into Nix sandbox environments.
691+
Two syntaxes can be used:
692+
693+
1. Original (old) syntax: Strings separated by whitespace. Entries
694+
are parsed as `TARGET[=SOURCE][?]`. Only the `TARGET` path is
695+
required.
696+
697+
`SOURCE` can be set following an equals sign (`=`) to specify a
698+
different source path (the value of `TARGET` is used by default
699+
for the source path as well). For instance, `/bin=/nix-bin` would
700+
mount path `/nix-bin` in `/bin` inside the sandbox.
701+
702+
A `?` suffix can be used to make it not an error if the `SOURCE`
703+
path does not exist. Without it an error is raised for an
704+
unavailable path. For instance, `/dev/nvidiactl?` specifies that
705+
`/dev/nvidiactl` will only be mounted in the sandbox if it exists
706+
in the host filesystem.
707+
708+
2. JSON syntax (new): Using this form more configurable settings
709+
become available. All paths are specified in a single JSON object
710+
so that every key is a target path inside the sandbox and the
711+
corresponding values can contain additional (platform-specific)
712+
settings.
713+
714+
For instance:
715+
716+
```nix
717+
sandbox-paths = {"/bin/sh":{}} # /bin/sh
718+
sandbox-paths = {"/bin/sh":{"source":"/usr/bin/bash"}} # /bin/sh=/usr/bin/bash
719+
sandbox-paths = {"/etc/nix/netrc":{"optional":true}} # /etc/nix/netrc?
720+
```
721+
722+
Additional per-path options are available on Linux:
723+
724+
- `readOnly` (boolean)
637725
638-
- `=source` will mount a different path at target location; for
639-
instance, `/bin=/nix-bin` will mount the path `/nix-bin` as `/bin`
640-
inside the sandbox.
726+
When this is `true`, the bind-mount is made read-only and
727+
additional mount-point flags are enabled. In particular these
728+
options are enabled by this flag: `ro`, `nosuid`, `nodev`,
729+
`noexec` and `noatime`.
641730
642-
- `:ro` makes the mount read-only (Linux only).
731+
- `options` (string array)
643732
644-
- `?` makes it not an error if *source* does not exist; for example,
645-
`/dev/nvidiactl?` specifies that `/dev/nvidiactl` will only be
646-
mounted in the sandbox if it exists in the host filesystem.
733+
This setting can be used to add/modify (some) mount(-point) flags
734+
directly. In addition to flags used by `readOnly` the following
735+
flags can also be used: `nodiratime`, `relatime`, `strictatime`,
736+
`unbindable`, `private`, `slave`.
647737
648-
If the source is in the Nix store, then its closure will be added to
649-
the sandbox as well.
738+
Full example:
739+
740+
```nix
741+
sandbox-paths = {
742+
"/path/to" : {
743+
"source" : "/path/from", # ()
744+
"optional" : true, # (false)
745+
"readOnly" : true, # (false)
746+
"options" : [ "optionA", "optionB", ... ], # ()
747+
},
748+
749+
# ...
750+
}
751+
```
752+
753+
> **Note:**
754+
>
755+
> If the source is in the Nix store, then its closure will
756+
> be added to the sandbox as well.
650757
651758
Depending on how Nix was built, the default value for this option
652759
may be empty or provide `/bin/sh` as a bind-mount of `bash`.

src/libstore/unix/build/derivation-builder.cc

Lines changed: 8 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -105,15 +105,8 @@ class DerivationBuilderImpl : public DerivationBuilder, public DerivationBuilder
105105
/**
106106
* Stuff we need to pass to initChild().
107107
*/
108-
struct ChrootPath {
109-
Path source;
110-
bool optional;
111-
bool rdonly;
112-
ChrootPath(Path source = "", bool optional = false, bool rdonly = false)
113-
: source(source), optional(optional), rdonly(rdonly)
114-
{ }
115-
};
116-
typedef std::map<Path, ChrootPath> PathsInChroot; // maps target path to source path
108+
109+
typedef SandboxPaths PathsInChroot; // maps target path to source path
117110

118111
typedef StringMap Environment;
119112
Environment env;
@@ -848,35 +841,16 @@ DerivationBuilderImpl::PathsInChroot DerivationBuilderImpl::getPathsInSandbox()
848841
{
849842
PathsInChroot pathsInChroot;
850843

851-
auto addPathWithOptions = [&](std::string s) {
852-
if (s.empty()) return;
853-
bool optional = false;
854-
bool rdonly = false;
855-
if (s[s.size() - 1] == '?') {
856-
optional = true;
857-
s.pop_back();
858-
}
859-
if (s.size() > 3 && s.substr(s.size() - 3) == ":ro") {
860-
rdonly = true;
861-
s.resize(s.size() - 3);
862-
}
863-
size_t p = s.find('=');
864-
if (p == std::string::npos)
865-
pathsInChroot[s] = {s, optional, rdonly};
866-
else
867-
pathsInChroot[s.substr(0, p)] = {s.substr(p + 1), optional, rdonly};
868-
};
869-
870844
/* Allow a user-configurable set of directories from the
871845
host file system. */
872-
for (auto i : settings.sandboxPaths.get()) {
873-
addPathWithOptions(i);
874-
}
846+
for (const auto & [k, v] : settings.sandboxPaths.get())
847+
pathsInChroot.insert_or_assign(k, v);
848+
875849
if (hasPrefix(store.storeDir, tmpDirInSandbox()))
876850
{
877851
throw Error("`sandbox-build-dir` must not contain the storeDir");
878852
}
879-
pathsInChroot[tmpDirInSandbox()] = tmpDir;
853+
pathsInChroot.insert_or_assign(tmpDirInSandbox(), tmpDir);
880854

881855
/* Add the closure of store paths to the chroot. */
882856
StorePathSet closure;
@@ -946,7 +920,8 @@ DerivationBuilderImpl::PathsInChroot DerivationBuilderImpl::getPathsInSandbox()
946920
if (line == "") {
947921
state = stBegin;
948922
} else {
949-
addPathWithOptions(line);
923+
for (const auto & [k, v] : SandboxPath().parse(line))
924+
pathsInChroot.try_emplace(k, v);
950925
}
951926
}
952927
}

src/libstore/unix/build/linux-derivation-builder.cc

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,18 +121,38 @@ static void setupSeccomp()
121121
# endif
122122
}
123123

124-
static void doBind(const Path & source, const Path & target, bool optional = false, bool rdonly = false)
124+
static auto combineMountOpts(auto init, auto iter)
125125
{
126+
return std::transform_reduce(iter.cbegin(), iter.cend(), init,
127+
[](unsigned long a, unsigned long b) {
128+
if (b & (MS_NOATIME | MS_RELATIME | MS_NODIRATIME | MS_STRICTATIME)) {
129+
return (a & ~(MS_NOATIME | MS_NODIRATIME | MS_RELATIME | MS_STRICTATIME)) | b;
130+
}
131+
return a | b;
132+
},
133+
[](const SandboxPath::MountOpt & o) { return static_cast<unsigned long>(o); });
134+
};
135+
136+
137+
static void doBind(const SandboxPath & pval, const Path & target)
138+
{
139+
auto source = pval.source;
140+
auto optional = pval.optional;
126141
debug("bind mounting '%1%' to '%2%'", source, target);
127142

128143
auto bindMount = [&]() {
129144
if (mount(source.c_str(), target.c_str(), "", MS_BIND | MS_REC, 0) == -1)
130145
throw SysError("bind mount from '%1%' to '%2%' failed", source, target);
131146

132-
if (rdonly)
147+
// set extra options when wanted
148+
auto flags = pval.readOnly ? combineMountOpts(0, pval.readOnlyDefaults) : 0;
149+
flags = combineMountOpts(flags, pval.options);
150+
if (flags != 0) {
133151
// initial mount wouldn't respect MS_RDONLY, must remount
134-
if (mount("", target.c_str(), "", MS_REMOUNT | MS_BIND | MS_RDONLY, 0) == -1)
135-
throw (SysError("making bind mount '%s' read-only failed", target));
152+
debug("remounting '%s' with flags: %d", target, flags);
153+
if (mount("", target.c_str(), "", MS_REMOUNT | MS_BIND | flags, 0) == -1)
154+
throw SysError("mount: updating bind-mount flags of '%s' failed", target);
155+
}
136156
};
137157

138158
auto maybeSt = maybeLstat(source);
@@ -677,7 +697,7 @@ struct ChrootLinuxDerivationBuilder : LinuxDerivationBuilder
677697
// For backwards-compatibility, resolve all the symlinks in the
678698
// chroot paths.
679699
auto canonicalPath = canonPath(i, true);
680-
pathsInChroot.emplace(i, canonicalPath);
700+
pathsInChroot.try_emplace(i, canonicalPath);
681701
}
682702

683703
/* Bind-mount all the directories from the "host"
@@ -699,7 +719,7 @@ struct ChrootLinuxDerivationBuilder : LinuxDerivationBuilder
699719
} else
700720
# endif
701721
{
702-
doBind(i.second.source, chrootRootDir + i.first, i.second.optional);
722+
doBind(i.second, chrootRootDir + i.first);
703723
}
704724
}
705725

0 commit comments

Comments
 (0)