Skip to content

Commit

Permalink
Merge pull request #559 from yuanxingyang/sandbox
Browse files Browse the repository at this point in the history
Let sandbox supports custom user information settings
  • Loading branch information
jkloetzke authored Apr 22, 2024
2 parents 6df0f92 + d8b36da commit cc29acc
Show file tree
Hide file tree
Showing 10 changed files with 85 additions and 23 deletions.
27 changes: 22 additions & 5 deletions doc/manual/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1750,6 +1750,13 @@ variables. See :ref:`configuration-principle-subst` for the available
substations. The mount paths are also subject to an additional variable
expansion when a step using the sandbox *is actually executed*. This can be
useful e.g. to expand variables that are only available on the build server.

By default, the user ID inside the sandbox is ``nobody``. The optional ``user``
key allows to use two other identities: ``root`` or ``$USER``. Note that using
``root`` does not provide any more privileges. It merely maps the current user
ID to the root user ID inside the sandbox. The ``$USER`` option keeps the
current user ID when entering the sandbox. No other values are allowed.

Example::

provideSandbox:
Expand All @@ -1761,6 +1768,7 @@ Example::
- ["\\$SSH_AUTH_SOCK", "\\SSH_AUTH_SOCK", [nofail, nojenkins]]
environment:
AUTOCONF_BUILD: "x86_64-linux-gnu"
user: nobody

The example assumes that the variable ``MYREPO`` was set somewhere in the
recipes. On the other hand ``$HOME`` is expanded later at build time. This is
Expand All @@ -1777,7 +1785,8 @@ in the :ref:`configuration-config-whitelist` to be available to the shell.
packages) nor will binary artifacts be re-fetched.

The user might amend the mount and search paths in ``default.yaml`` by a
:ref:`configuration-config-sandbox` entry.
:ref:`configuration-config-sandbox` entry. The user identity can be overridden
too.

.. _configuration-recipes-relocatable:

Expand Down Expand Up @@ -2390,10 +2399,10 @@ sandbox

Type: Sandbox-Dictionary

The default paths and mounts of a sandbox are defined by the
The default paths, mounts and user identity inside a sandbox are defined by the
:ref:`configuration-recipes-provideSandbox` keyword. The ``sandbox`` section in
the user configuration allows to specify additional mounts and additional
search paths. The format of the settings is the same as in the
the user configuration allows to specify additional mounts, search paths or
override the user identity. The format of the settings is the same as in the
:ref:`configuration-recipes-provideSandbox` keyword.

Example::
Expand All @@ -2403,17 +2412,25 @@ Example::
- [ "$HOME/bin", "/mnt" ]
paths:
- /mnt
user: "$USER"

The search paths from ``paths`` are added to ``$PATH`` in reverse order so that
later entries have a higher precedence. In contrast to ``provideSandbox`` *no*
variable substitution is possible for the mounts. The mount paths are still subject to
shell variable expansion when a step using the sandbox *is actually executed*,
though.

The ``user`` key allows to override the user identity inside the sandbox. It
takes precedence over the value specified in
:ref:`configuration-recipes-provideSandbox`. The default is ``nobody`` if
neither setting is given. Other possible values are ``root`` and ``$USER``.
The latter is replaced by the current user ID. No other values are allowed.

The example above will mount the ``bin`` directory of the users home directory
as ``/mnt`` inside the sandbox. The ``/mnt`` directory will be in ``$PATH``
before any other search directory of the sandbox but still after any used tool
(if any).
(if any). Additionally, the user identity inside the sandbox will be the same
as the current user.

.. _configuration-config-scmDefaults:

Expand Down
19 changes: 17 additions & 2 deletions pym/bob/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,7 @@ def getEnvironment(self):

class CoreSandbox(CoreItem):
__slots__ = ("coreStep", "enabled", "paths", "mounts", "environment",
"resultId")
"resultId", "user")

def __init__(self, coreStep, env, enabled, spec):
recipeSet = coreStep.corePackage.recipe.getRecipeSet()
Expand All @@ -685,6 +685,7 @@ def __init__(self, coreStep, env, enabled, spec):
k : env.substitute(v, "providedSandbox::environment")
for (k, v) in spec.get('environment', {}).items()
}
self.user = recipeSet.getSandboxUser() or spec.get('user', "nobody")

# Calculate a "resultId" so that only identical sandboxes match
h = hashlib.sha1()
Expand All @@ -701,6 +702,7 @@ def __init__(self, coreStep, env, enabled, spec):
for (key, val) in sorted(self.environment.items()):
h.update(struct.pack("<II", len(key), len(val)))
h.update((key+val).encode('utf8'))
h.update(self.user.encode('utf8'))
self.resultId = h.digest()

def __eq__(self, other):
Expand All @@ -709,7 +711,8 @@ def __eq__(self, other):
(self.enabled == other.enabled) and \
(self.paths == other.paths) and \
(self.mounts == other.mounts) and \
(self.environment == other.environment)
(self.environment == other.environment) and \
(self.user == other.user)

def refDeref(self, stack, inputTools, inputSandbox, pathFormatter, cache=None):
step = self.coreStep.refDeref(stack, inputTools, inputSandbox, pathFormatter)
Expand Down Expand Up @@ -757,6 +760,13 @@ def isEnabled(self):
"""Return True if the sandbox is used in the current build configuration."""
return self.coreSandbox.enabled

def getUser(self):
"""Get user identity in sandbox.
Returns one of 'nobody', 'root' or '$USER'.
"""
return self.coreSandbox.user


class CoreStep(CoreItem):
__slots__ = ( "corePackage", "digestEnv", "env", "args",
Expand Down Expand Up @@ -3070,6 +3080,7 @@ def removeWhiteList(x):
schema.Schema({
schema.Optional('mount') : schema.Schema([ MountValidator() ]),
schema.Optional('paths') : [str],
schema.Optional('user') : schema.Or("nobody", "root", "$USER"),
}),
lambda x: updateDicRecursive(self.__sandboxOpts, x),
True
Expand Down Expand Up @@ -3335,6 +3346,9 @@ def getSandboxMounts(self):
def getSandboxPaths(self):
return list(reversed(self.__sandboxOpts.get("paths", [])))

def getSandboxUser(self):
return self.__sandboxOpts.get("user")

def loadBinary(self, path):
return self.__cache.loadBinary(path)

Expand Down Expand Up @@ -3626,6 +3640,7 @@ def __createSchemas(self):
schema.Optional('mount') : schema.Schema([ MountValidator() ],
error="provideSandbox: invalid 'mount' property"),
schema.Optional('environment') : VarDefineValidator("provideSandbox::environment"),
schema.Optional('user') : schema.Or("nobody", "root", "$USER"),
}),
schema.Optional('root') : schema.Or(bool, str, IfExpression),
schema.Optional('shared') : bool,
Expand Down
4 changes: 4 additions & 0 deletions pym/bob/intermediate.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,7 @@ def fromSandbox(cls, sandbox, graph):
self.__data['step'] = graph.addStep(sandbox.getStep(), True)
self.__data['paths'] = sandbox.getPaths()
self.__data['mounts'] = sandbox.getMounts()
self.__data['user'] = sandbox.getUser()
return self

@classmethod
Expand All @@ -468,6 +469,9 @@ def getPaths(self):
def getMounts(self):
return self.__data['mounts']

def getUser(self):
return self.__data['user']

class ToolIR(AbstractIR):
@classmethod
def fromTool(cls, tool, graph):
Expand Down
7 changes: 7 additions & 0 deletions pym/bob/invoker.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,13 @@ def __getSandboxCmds(self, tmpDir):
elif hostPath != sndbxPath:
cmdArgs.extend(["-m", sndbxPath])

if self.__spec.sandboxUser == "root":
cmdArgs.append("-r")
elif self.__spec.sandboxUser == "$USER":
cmdArgs.append("-i")
else:
assert self.__spec.sandboxUser == "nobody"

return cmdArgs

async def executeStep(self, mode, clean=False, keepSandbox=False):
Expand Down
5 changes: 5 additions & 0 deletions pym/bob/languages.py
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,7 @@ def fromStep(cls, step, envFile=None, envWhiteList=[], logFile=None, isJenkins=F
'root' : step.getSandbox().getStep().getStoragePath(),
'paths' : step.getSandbox().getPaths(),
'hostMounts' : step.getSandbox().getMounts(),
'user' : step.getSandbox().getUser(),
'netAccess' : step.hasNetAccess(),
'depMounts' : [
(dep.getStoragePath(), dep.getExecPath(step))
Expand Down Expand Up @@ -804,6 +805,10 @@ def sandboxHostMounts(self):
def sandboxPaths(self):
return self.__data['sandbox']['paths']

@property
def sandboxUser(self):
return self.__data['sandbox']['user']

@property
def envWhiteList(self):
return set(self.__data['envWhiteList'])
Expand Down
35 changes: 20 additions & 15 deletions src/namespace-sandbox/namespace-sandbox.c
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ struct Options {
int num_mounts; // How many mounts were specified
char **create_dirs; // empty dirs to create (-d)
int num_create_dirs; // How many empty dirs to create were specified
int fake_root; // Pretend to be root inside the namespace.
uid_t uid; // User id in namespace
uid_t gid; // Group id in namespace
int create_netns; // If 1, create a new network namespace.
const char *host_name; // Host name (-H)
};
Expand Down Expand Up @@ -136,6 +137,7 @@ static void Usage(int argc, char *const *argv, const char *fmt, ...) {
"specifies where to\n"
" mount it in the sandbox.\n"
" -n if set, a new network namespace will be created\n"
" -i if set, keep the uid/gid\n"
" -r if set, make the uid/gid be root, otherwise use nobody\n"
" -H <name> set host name\n"
" -D if set, debug info will be printed\n"
Expand Down Expand Up @@ -247,7 +249,7 @@ static void ParseCommandLine(int argc, char *const *argv, struct Options *opt) {
extern int optind, optopt;
int c;

while ((c = getopt(argc, argv, ":CDd:l:L:m:M:nrS:W:w:H:")) != -1) {
while ((c = getopt(argc, argv, ":CDd:il:L:m:M:nrS:W:w:H:")) != -1) {
switch (c) {
case 'C':
// Shortcut for the "does this system support sandboxing" check.
Expand Down Expand Up @@ -284,6 +286,10 @@ static void ParseCommandLine(int argc, char *const *argv, struct Options *opt) {
}
opt->create_dirs[opt->num_create_dirs++] = optarg;
break;
case 'i':
opt->uid = getuid();
opt->gid = getgid();
break;
case 'M':
if (optarg[0] != '/') {
Usage(argc, argv,
Expand Down Expand Up @@ -319,7 +325,8 @@ static void ParseCommandLine(int argc, char *const *argv, struct Options *opt) {
opt->create_netns = 1;
break;
case 'r':
opt->fake_root = 1;
opt->uid = 0;
opt->gid = 0;
break;
case 'H':
opt->host_name = optarg;
Expand Down Expand Up @@ -580,9 +587,10 @@ static void SetupDirectories(struct Options *opt, uid_t uid) {
fclose(passwd);

if (entry == NULL) {
DIE("User not found in /etc/passwd of sandbox\n");
homedir = getenv("HOME");
} else {
homedir = entry->pw_dir;
}
homedir = entry->pw_dir;
} else {
PRINT_DEBUG("/etc/passwd not found/readable in sandbox! Falling back to $HOME\n");
homedir = getenv("HOME");
Expand Down Expand Up @@ -618,7 +626,7 @@ static int WriteFile(const char *filename, const char *fmt, ...) {
return r;
}

static void SetupUserNamespace(int uid, int gid, int new_uid, int new_gid) {
static void SetupUserNamespace(uid_t uid, uid_t gid, uid_t new_uid, uid_t new_gid) {
// Disable needs for CAP_SETGID
int r = WriteFile("/proc/self/setgroups", "deny");
if (r < 0 && errno != ENOENT) {
Expand Down Expand Up @@ -679,6 +687,8 @@ static void ExecCommand(char *const *argv) {
int main(int argc, char *const argv[]) {
struct Options opt;
memset(&opt, 0, sizeof(opt));
opt.uid = kNobodyUid;
opt.gid = kNobodyGid;
opt.mount_sources = calloc(argc, sizeof(char *));
opt.mount_targets = calloc(argc, sizeof(char *));
opt.mount_rw = calloc(argc, sizeof(bool));
Expand All @@ -693,8 +703,8 @@ int main(int argc, char *const argv[]) {
Usage(argc, argv, "Sandbox root (-S) must be specified");
}

int uid = SwitchToEuid();
int gid = SwitchToEgid();
uid_t uid = SwitchToEuid();
uid_t gid = SwitchToEgid();

RedirectStdout(opt.stdout_path);
RedirectStderr(opt.stderr_path);
Expand All @@ -714,13 +724,8 @@ int main(int argc, char *const argv[]) {
// outside environment.
CHECK_CALL(mount("none", "/", NULL, MS_REC | MS_PRIVATE, NULL));

if (opt.fake_root) {
SetupDirectories(&opt, 0);
SetupUserNamespace(uid, gid, 0, 0);
} else {
SetupDirectories(&opt, kNobodyUid);
SetupUserNamespace(uid, gid, kNobodyUid, kNobodyGid);
}
SetupDirectories(&opt, opt.uid);
SetupUserNamespace(uid, gid, opt.uid, opt.gid);
if (opt.host_name) {
CHECK_CALL(sethostname(opt.host_name, strlen(opt.host_name)));
}
Expand Down
2 changes: 2 additions & 0 deletions test/black-box/sandbox/as-root.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
sandbox:
user: "root"
2 changes: 2 additions & 0 deletions test/black-box/sandbox/as-self.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
sandbox:
user: "$USER"
3 changes: 2 additions & 1 deletion test/black-box/sandbox/recipes/root.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ checkoutScript: |
echo ok
buildScript: |
echo ok
packageVars: [FOO]
packageVars: [FOO, EXPECT_UID]
packageScript: |
test $FOO = bar
test ${EXPECT_UID:-65534} = $UID
echo ok
4 changes: 4 additions & 0 deletions test/black-box/sandbox/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ cleanup

run_bob build -DFOO=bar root
run_bob dev --sandbox -E root

# Check that we can keep our UID or even be root inside sandbox
run_bob dev --sandbox -c as-self -DEXPECT_UID=$UID root
run_bob dev --sandbox -c as-root -DEXPECT_UID=0 root

0 comments on commit cc29acc

Please sign in to comment.