minijail0: implement --env-reset and --env-add

The default behavior of minijail0 is to transmit its own environment
to the program it launches.
These two options enable modification of this standard behavior, by
setting up a specific environment for the target program.

* --env-reset prevents minijail0 to pass its own environment to the
program. Note that if this option is used without any --env-add option,
the program starts with a completely empty environment.

* --env-add allows setting any number of environment variables to be
added to the environment set up for the program. Note that the special
environment variable LD_PRELOAD can also be set this way, without
impacting minijail0's own invocation.

Test: `make tests`

Test: `valgrind ./minijail0 --env-add A=X /usr/bin/env`
Test: `valgrind ./minijail0 --env-add A=X --env-add A=Y /usr/bin/env`
Test: `valgrind ./minijail0 --env-reset /usr/bin/env`
Test: `valgrind ./minijail0 --env-reset --env-reset /usr/bin/env`
Test: `valgrind ./minijail0 --env-reset --env-add A=X /usr/bin/env`
Test: `valgrind ./minijail0 --env-add A=X --env-reset --env-add A=Y /usr/bin/env`
Test: `valgrind ./minijail0 --env-add A=X --env-reset --env-add A=Y --env-reset /usr/bin/env`

Test: `minijail0 --env-reset --env-add LD_PRELOAD="a b" --env-add FOO=bar /usr/bin/env`
"""
ERROR: ld.so: object 'a' from LD_PRELOAD cannot be preloaded (cannot open shared object file): ignored.
ERROR: ld.so: object 'b' from LD_PRELOAD cannot be preloaded (cannot open shared object file): ignored.
LD_PRELOAD=a b
FOO=bar
"""

Change-Id: I2e106003824ff6707b80b6653141657e4ec40f47
diff --git a/libminijail.c b/libminijail.c
index 8ddea7e..ea59404 100644
--- a/libminijail.c
+++ b/libminijail.c
@@ -2590,7 +2590,7 @@
 	const char *preload_path = j->preload_path ?: PRELOADPATH;
 	char *newenv = NULL;
 	int ret = 0;
-	const char *oldenv = getenv(kLdPreloadEnvVar);
+	const char *oldenv = minijail_getenv(*child_env, kLdPreloadEnvVar);
 
 	if (!oldenv)
 		oldenv = "";
@@ -2939,6 +2939,20 @@
 	return minijail_run_config_internal(j, &config);
 }
 
+int API minijail_run_env(struct minijail *j, const char *filename,
+			 char *const argv[], char *const envp[])
+{
+	struct minijail_run_config config = {
+	   .filename = filename,
+	   .elf_fd = -1,
+	   .argv = argv,
+	   .envp = envp,
+	   .use_preload = true,
+	   .exec_in_child = true,
+	};
+	return minijail_run_config_internal(j, &config);
+}
+
 int API minijail_run_pid(struct minijail *j, const char *filename,
 			 char *const argv[], pid_t *pchild_pid)
 {
diff --git a/libminijail.h b/libminijail.h
index bcfd995..d2dce7a 100644
--- a/libminijail.h
+++ b/libminijail.h
@@ -344,6 +344,13 @@
 
 /*
  * Run the specified command in the given minijail, execve(2)-style.
+ * Pass |envp| as the full environment for the child.
+ */
+int minijail_run_env(struct minijail *j, const char *filename,
+		     char *const argv[], char *const envp[]);
+
+/*
+ * Run the specified command in the given minijail, execve(2)-style.
  * If minijail_namespace_pids() or minijail_namespace_user() are used,
  * this or minijail_fork() is required instead of minijail_enter().
  */
diff --git a/minijail0.1 b/minijail0.1
index 47aa803..9258e3f 100644
--- a/minijail0.1
+++ b/minijail0.1
@@ -289,6 +289,16 @@
 Create a new UTS/hostname namespace, and optionally set the hostname in the new
 namespace to \fIhostname\fR.
 .TP
+\fB--env-reset\fR
+Clear the current environment instead of having the program inherit the active
+environment. This is often used to start the program with a minimal
+sanitized environment.
+.TP
+\fB--env-add <NAME=value>\fR
+Adds or replace the specified environment variable \fINAME\fR in the program's
+environment before starting it, and set it to the specified \fIvalue\fR.
+This option can be used several times to set any number of environment variables.
+.TP
 \fB--logging=<system>\fR
 Use \fIsystem\fR as the logging system. \fIsystem\fR must be one of
 \fBauto\fR (the default), \fBsyslog\fR, or \fBstderr\fR.
diff --git a/minijail0.c b/minijail0.c
index 86dff01..9b1fcf3 100644
--- a/minijail0.c
+++ b/minijail0.c
@@ -16,15 +16,17 @@
 #include "minijail0_cli.h"
 #include "util.h"
 
-int main(int argc, char *argv[])
+int main(int argc, char *argv[], char *environ[])
 {
 	struct minijail *j = minijail_new();
 	const char *dl_mesg = NULL;
 	const char *preload_path = PRELOADPATH;
 	int exit_immediately = 0;
 	ElfType elftype = ELFERROR;
-	int consumed = parse_args(j, argc, argv, &exit_immediately, &elftype,
-				  &preload_path);
+	char **envp = NULL;
+	int consumed = parse_args(j, argc, argv, environ,
+				  &exit_immediately, &elftype,
+				  &preload_path, &envp);
 	argc -= consumed;
 	argv += consumed;
 
@@ -61,9 +63,14 @@
 			return 1;
 		}
 		minijail_set_preload_path(j, preload_path);
-		minijail_run(j, argv[0], argv);
-	} else
+		if (envp) {
+			minijail_run_env(j, argv[0], argv, envp);
+		} else {
+			minijail_run(j, argv[0], argv);
+		}
+	} else {
 		errx(1, "Target program '%s' is not a valid ELF file", argv[0]);
+	}
 
 	if (exit_immediately)
 		return 0;
diff --git a/minijail0_cli.c b/minijail0_cli.c
index 54cfdb7..3b9b708 100644
--- a/minijail0_cli.c
+++ b/minijail0_cli.c
@@ -456,7 +456,7 @@
  * bit confusing, and honestly there's no reason to "optimize" here.
  *
  * The long enum values are internal to this file and can freely change at any
- * time without breaking anything.  Don't worry about ordering.
+ * time without breaking anything.  Please keep alphabetically ordered.
  */
 enum {
 	/* Everything after this point only have long options. */
@@ -465,6 +465,8 @@
 	OPT_ALLOW_SPECULATIVE_EXECUTION,
 	OPT_AMBIENT,
 	OPT_CONFIG,
+	OPT_ENV_ADD,
+	OPT_ENV_RESET,
 	OPT_LOGGING,
 	OPT_PRELOAD_LIBRARY,
 	OPT_PROFILE,
@@ -494,6 +496,8 @@
     {"allow-speculative-execution", no_argument, 0,
      OPT_ALLOW_SPECULATIVE_EXECUTION},
     {"config", required_argument, 0, OPT_CONFIG},
+    {"env-add", required_argument, 0, OPT_ENV_ADD},
+    {"env-reset", no_argument, 0, OPT_ENV_RESET},
     {"mount", required_argument, 0, 'k'},
     {"bind-mount", required_argument, 0, 'b'},
     {0, 0, 0, 0},
@@ -599,6 +603,12 @@
 "               This will avoid accessing <program> binary before execve(2).\n"
 "               Type 'static' will avoid preload hooking.\n"
 "  -w           Create and join a new anonymous session keyring.\n"
+"  --env-reset  Clear the current environment instead of having <program>\n"
+"               inherit the active environment. Often used to start <program>\n"
+"               with a minimal sanitized environment.\n"
+"  --env-add <NAME=value>\n"
+"               Sets the specified environment variable <NAME>\n"
+"               in the <program>'s environment before starting it.\n"
 "\n"
 "Uncommon options:\n"
 "  --allow-speculative-execution\n"
@@ -693,9 +703,33 @@
 	return opt;
 }
 
+static void set_child_env(char ***envp, char *arg, char *const environ[])
+{
+	/* We expect VAR=value format for arg. */
+	char *delim = strchr(arg, '=');
+	if (!delim) {
+		errx(1, "Expected an argument of the "
+		        "form VAR=value (got '%s')", arg);
+	}
+	*delim = '\0';
+	const char *env_value = delim + 1;
+	if (!*envp) {
+		/*
+		 * We got our first --env-add. Initialize *envp by
+		 * copying our current env to the future child env.
+		 */
+		*envp = minijail_copy_env(environ);
+		if (!*envp)
+			err(1, "Failed to allocate memory.");
+	}
+	if (minijail_setenv(envp, arg, env_value, 1))
+		err(1, "minijail_setenv() failed.");
+}
+
 int parse_args(struct minijail *j, int argc, char *const argv[],
-	       int *exit_immediately, ElfType *elftype,
-	       const char **preload_path)
+	       char *const environ[], int *exit_immediately,
+	       ElfType *elftype, const char **preload_path,
+	       char ***envp)
 {
 	enum seccomp_type { None, Strict, Filter, BpfBinaryFilter };
 	enum seccomp_type seccomp = None;
@@ -1033,6 +1067,32 @@
 			}
 			break;
 		}
+		case OPT_ENV_ADD:
+			/*
+			 * We either copy our current env to the child env
+			 * then add the requested envvar to it, or just
+			 * add the requested envvar to the already existing
+			 * envp.
+			 */
+			set_child_env(envp, optarg, environ);
+			break;
+		case OPT_ENV_RESET:
+			if (*envp && *envp != environ) {
+				/*
+				 * We already started to initialize the future
+				 * child env, because we got some --env-add
+				 * earlier on the command-line, so first,
+				 * free the memory we allocated.
+				 * If |*envp| happens to point to |environ|,
+				 * don't attempt to free it.
+				 */
+				minijail_free_env(*envp);
+			}
+			/* Allocate an empty environment for the child. */
+			*envp = calloc(1, sizeof(char *));
+			if (!*envp)
+				err(1, "Failed to allocate memory.");
+			break;
 		default:
 			usage(argv[0]);
 			exit(opt == 'h' ? 0 : 1);
@@ -1127,6 +1187,16 @@
 		minijail_mount_tmp_size(j, tmp_size);
 
 	/*
+	 * Copy our current env to the child if its |*envp| has not
+	 * already been initialized from --env-(reset|add) usage.
+	 */
+	if (!*envp) {
+		*envp = minijail_copy_env(environ);
+		if (!*envp)
+			err(1, "Failed to allocate memory.");
+	}
+
+	/*
 	 * There should be at least one additional unparsed argument: the
 	 * executable name.
 	 */
diff --git a/minijail0_cli.h b/minijail0_cli.h
index 583c763..cd504b3 100644
--- a/minijail0_cli.h
+++ b/minijail0_cli.h
@@ -17,8 +17,9 @@
 struct minijail;
 
 int parse_args(struct minijail *j, int argc, char *const argv[],
-	       int *exit_immediately, ElfType *elftype,
-	       const char **preload_path);
+	       char *const environ[], int *exit_immediately,
+	       ElfType *elftype, const char **preload_path,
+	       char ***envp);
 
 #ifdef __cplusplus
 }; /* extern "C" */
diff --git a/minijail0_cli_unittest.cc b/minijail0_cli_unittest.cc
index 151f789..f280a8a 100644
--- a/minijail0_cli_unittest.cc
+++ b/minijail0_cli_unittest.cc
@@ -60,9 +60,10 @@
     testing::internal::CaptureStdout();
 
     const char* preload_path = PRELOADPATH;
+    char **envp = NULL;
     int ret =
         parse_args(j, pargv.size(), const_cast<char* const*>(pargv.data()),
-                   exit_immediately, elftype, &preload_path);
+                   NULL, exit_immediately, elftype, &preload_path, &envp);
     testing::internal::GetCapturedStdout();
 
     minijail_destroy(j);
@@ -544,6 +545,48 @@
   ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
 }
 
+// Valid calls to the clear env option.
+TEST_F(CliTest, valid_clear_env) {
+  std::vector<std::string> argv = {"--env-reset", "/bin/sh"};
+
+  ASSERT_TRUE(parse_args_(argv));
+}
+
+// Valid calls to the set env option.
+TEST_F(CliTest, valid_set_env) {
+  std::vector<std::string> argv1 = {"--env-add", "NAME=value", "/bin/sh"};
+  ASSERT_TRUE(parse_args_(argv1));
+
+  // multiple occurences are allowed.
+  std::vector<std::string> argv2 = {"--env-add", "A=b",
+                                    "--env-add", "b=C=D", "/bin/sh"};
+  ASSERT_TRUE(parse_args_(argv2));
+
+  // --env-reset before any --env-add to not pass our own env.
+  std::vector<std::string> argv3 = {"--env-reset", "--env-add", "A=b", "/bin/sh"};
+  ASSERT_TRUE(parse_args_(argv3));
+
+  // --env-add before an --env-reset doesn't have any effect, but is allowed.
+  std::vector<std::string> argv4 = {"--env-add", "A=b", "--env-reset", "/bin/sh"};
+  ASSERT_TRUE(parse_args_(argv4));
+}
+
+// Invalid calls to the set env options.
+TEST_F(CliTest, invalid_set_env) {
+
+  // invalid env=value arguments.
+  std::vector<std::string> argv2 = {"--env-add", "", "/bin/sh"};
+
+  argv2[1] = "INVALID";
+  ASSERT_EXIT(parse_args_(argv2), testing::ExitedWithCode(1), "");
+
+  argv2[1] = "=";
+  ASSERT_EXIT(parse_args_(argv2), testing::ExitedWithCode(1), "");
+
+  argv2[1] = "=foo";
+  ASSERT_EXIT(parse_args_(argv2), testing::ExitedWithCode(1), "");
+}
+
 // Android unit tests do not support data file yet.
 #if !defined(__ANDROID__)