minijail: Add a way to allow arbitrary fd redirects

This change allows for the parent to redirect arbtitrary file
descriptors into the child, in a way that works even when the
minijail_close_open_fds() is used.

This can be used to pass in additional pipes to the jailed process.

Bug: 63904978
Test: make tests
Change-Id: Ia47eec575c92a08eb5380cc15dc4561572a209b3
diff --git a/libminijail.c b/libminijail.c
index ac76faf..89fb609 100644
--- a/libminijail.c
+++ b/libminijail.c
@@ -77,6 +77,8 @@
 
 #define MAX_RLIMITS 32 /* Currently there are 15 supported by Linux. */
 
+#define MAX_PRESERVED_FDS 10
+
 /* Keyctl commands. */
 #define KEYCTL_JOIN_SESSION_KEYRING 1
 
@@ -103,6 +105,11 @@
 	struct hook *next;
 };
 
+struct preserved_fd {
+	int parent_fd;
+	int child_fd;
+};
+
 struct minijail {
 	/*
 	 * WARNING: if you add a flag here you need to make sure it's
@@ -176,6 +183,8 @@
 	uint64_t securebits_skip_mask;
 	struct hook *hooks_head;
 	struct hook *hooks_tail;
+	struct preserved_fd preserved_fds[MAX_PRESERVED_FDS];
+	size_t preserved_fd_count;
 };
 
 /*
@@ -788,6 +797,18 @@
 	return 0;
 }
 
+int API minijail_preserve_fd(struct minijail *j, int parent_fd, int child_fd)
+{
+	if (parent_fd < 0 || child_fd < 0)
+		return -EINVAL;
+	if (j->preserved_fd_count >= MAX_PRESERVED_FDS)
+		return -ENOMEM;
+	j->preserved_fds[j->preserved_fd_count].parent_fd = parent_fd;
+	j->preserved_fds[j->preserved_fd_count].child_fd = child_fd;
+	j->preserved_fd_count++;
+	return 0;
+}
+
 static void clear_seccomp_options(struct minijail *j)
 {
 	j->flags.seccomp_filter = 0;
@@ -2010,6 +2031,32 @@
 	return 0;
 }
 
+static int redirect_fds(struct minijail *j)
+{
+	size_t i, i2;
+	int closeable;
+	for (i = 0; i < j->preserved_fd_count; i++) {
+		if (dup2(j->preserved_fds[i].parent_fd,
+			 j->preserved_fds[i].child_fd) == -1) {
+			return -1;
+		}
+	}
+	/*
+	 * After all fds have been duped, we are now free to close all parent
+	 * fds that are *not* child fds.
+	 */
+	for (i = 0; i < j->preserved_fd_count; i++) {
+		closeable = true;
+		for (i2 = 0; i2 < j->preserved_fd_count; i2++) {
+			closeable &= j->preserved_fds[i].parent_fd !=
+				     j->preserved_fds[i2].child_fd;
+		}
+		if (closeable)
+			close(j->preserved_fds[i].parent_fd);
+	}
+	return 0;
+}
+
 int minijail_run_internal(struct minijail *j, const char *filename,
 			  char *const argv[], pid_t *pchild_pid,
 			  int *pstdin_fd, int *pstdout_fd, int *pstderr_fd,
@@ -2311,9 +2358,10 @@
 	}
 
 	if (j->flags.close_open_fds) {
-		const size_t kMaxInheritableFdsSize = 10;
+		const size_t kMaxInheritableFdsSize = 10 + MAX_PRESERVED_FDS;
 		int inheritable_fds[kMaxInheritableFdsSize];
 		size_t size = 0;
+		size_t i;
 		if (use_preload) {
 			inheritable_fds[size++] = pipe_fds[0];
 			inheritable_fds[size++] = pipe_fds[1];
@@ -2334,11 +2382,21 @@
 			inheritable_fds[size++] = stderr_fds[0];
 			inheritable_fds[size++] = stderr_fds[1];
 		}
+		for (i = 0; i < j->preserved_fd_count; i++) {
+			/*
+			 * Preserve all parent_fds. They will be dup2(2)-ed in
+			 * the child later.
+			 */
+			inheritable_fds[size++] = j->preserved_fds[i].parent_fd;
+		}
 
 		if (close_open_fds(inheritable_fds, size) < 0)
 			die("failed to close open file descriptors");
 	}
 
+	if (redirect_fds(j))
+		die("failed to set up fd redirections");
+
 	if (sync_child)
 		wait_for_parent_setup(child_sync_pipe_fds);
 
diff --git a/libminijail.h b/libminijail.h
index 372c1a4..a783067 100644
--- a/libminijail.h
+++ b/libminijail.h
@@ -238,6 +238,17 @@
 		      minijail_hook_event_t event);
 
 /*
+ * minijail_preserve_fd: preserves @parent_fd and makes it available as
+ * @child_fd in the child process. @parent_fd will be closed if no other
+ * redirect has claimed it as a @child_fd.  This works even if
+ * minijail_close_open_fds() is invoked.
+ * @j         minijail to add the fd to
+ * @parent_fd the fd in the parent process
+ * @child_fd  the fd that will be available in the child process
+ */
+int minijail_preserve_fd(struct minijail *j, int parent_fd, int child_fd);
+
+/*
  * Lock this process into the given minijail. Note that this procedure cannot
  * fail, since there is no way to undo privilege-dropping; therefore, if any
  * part of the privilege-drop fails, minijail_enter() will abort the entire
diff --git a/libminijail_unittest.cc b/libminijail_unittest.cc
index caaa138..77775b5 100644
--- a/libminijail_unittest.cc
+++ b/libminijail_unittest.cc
@@ -308,6 +308,56 @@
   minijail_destroy(j);
 }
 
+TEST(Test, test_minijail_preserve_fd) {
+  int mj_run_ret;
+  int status;
+#if defined(__ANDROID__)
+  char filename[] = "/system/bin/cat";
+#else
+  char filename[] = "/bin/cat";
+#endif
+  char *argv[2];
+  char teststr[] = "test\n";
+  size_t teststr_len = strlen(teststr);
+  int read_pipe[2];
+  int write_pipe[2];
+  char buf[1024];
+
+  struct minijail *j = minijail_new();
+
+  status = pipe(read_pipe);
+  ASSERT_EQ(status, 0);
+  status = pipe(write_pipe);
+  ASSERT_EQ(status, 0);
+
+  status = minijail_preserve_fd(j, write_pipe[0], STDIN_FILENO);
+  ASSERT_EQ(status, 0);
+  status = minijail_preserve_fd(j, read_pipe[1], STDOUT_FILENO);
+  ASSERT_EQ(status, 0);
+  minijail_close_open_fds(j);
+
+  argv[0] = filename;
+  argv[1] = NULL;
+  mj_run_ret = minijail_run_no_preload(j, argv[0], argv);
+  EXPECT_EQ(mj_run_ret, 0);
+
+  close(write_pipe[0]);
+  status = write(write_pipe[1], teststr, teststr_len);
+  EXPECT_EQ(status, (int)teststr_len);
+  close(write_pipe[1]);
+
+  close(read_pipe[1]);
+  status = read(read_pipe[0], buf, 8);
+  EXPECT_EQ(status, (int)teststr_len);
+  buf[teststr_len] = 0;
+  EXPECT_EQ(strcmp(buf, teststr), 0);
+
+  status = minijail_wait(j);
+  EXPECT_EQ(status, 0);
+
+  minijail_destroy(j);
+}
+
 TEST(Test, parse_size) {
   size_t size;