Add a remote binder service for executing commands
To summarize, three binaries are involved to run a command remotely:
- pvm_exec: the client executable on the host side to wrap the
executable command with hints of FD passing
- compsvc: listen to requests, spin off and sandbox a worker for
execution setup
- compsvc_worker: set up authfs, prepare the fds and exec the actual
task
Please see the code documentation for details.
Bug: 171316742
Test: [shell 1] adb shell compsvc /system/bin/sleep
[shell 2] adb shell exec 8</dev/zero 7<>/dev/null pvm_exec
--in-fd 8 --out-fd 7 -- sleep 300
# Saw FDs in /proc/${sleep_pid}/fd
Change-Id: I4758a4dc7bc70b6e5cce79e151c84c9990d9bc89
diff --git a/compos/Android.bp b/compos/Android.bp
new file mode 100644
index 0000000..ac69a52
--- /dev/null
+++ b/compos/Android.bp
@@ -0,0 +1,41 @@
+rust_binary {
+ name: "pvm_exec",
+ srcs: ["src/pvm_exec.rs"],
+ rustlibs: [
+ "compos_aidl_interface-rust",
+ "libanyhow",
+ "libclap",
+ "liblibc",
+ "liblog_rust",
+ "libminijail_rust",
+ "libnix",
+ "libscopeguard",
+ ],
+}
+
+rust_binary {
+ name: "compsvc",
+ srcs: ["src/compsvc.rs"],
+ rustlibs: [
+ "compos_aidl_interface-rust",
+ "libandroid_logger",
+ "libanyhow",
+ "libclap",
+ "liblog_rust",
+ "libminijail_rust",
+ ],
+}
+
+rust_binary {
+ name: "compsvc_worker",
+ srcs: ["src/compsvc_worker.rs"],
+ rustlibs: [
+ "libandroid_logger",
+ "libanyhow",
+ "libclap",
+ "liblog_rust",
+ "libminijail_rust",
+ "libnix",
+ "libscopeguard",
+ ],
+}
diff --git a/compos/aidl/Android.bp b/compos/aidl/Android.bp
new file mode 100644
index 0000000..8737d63
--- /dev/null
+++ b/compos/aidl/Android.bp
@@ -0,0 +1,12 @@
+aidl_interface {
+ name: "compos_aidl_interface",
+ unstable: true,
+ srcs: [
+ "com/android/compos/*.aidl",
+ ],
+ backend: {
+ rust: {
+ enabled: true,
+ },
+ },
+}
diff --git a/compos/aidl/com/android/compos/ICompService.aidl b/compos/aidl/com/android/compos/ICompService.aidl
new file mode 100644
index 0000000..0e18442
--- /dev/null
+++ b/compos/aidl/com/android/compos/ICompService.aidl
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compos;
+
+import com.android.compos.Metadata;
+
+/** {@hide} */
+interface ICompService {
+ /**
+ * Execute a command composed of the args, in a context that may be specified in the Metadata,
+ * e.g. with file descriptors pre-opened. The service is responsible to decide what executables
+ * it may run.
+ *
+ * @param args The command line arguments to run. The 0-th args is normally the program name,
+ * which may not be used by the service. The service may be configured to always use
+ * a fixed executable, or possibly use the 0-th args are the executable lookup hint.
+ * @param metadata Additional information of the execution
+ * @return exit code of the program
+ */
+ byte execute(in String[] args, in Metadata metadata);
+}
diff --git a/compos/aidl/com/android/compos/InputFdAnnotation.aidl b/compos/aidl/com/android/compos/InputFdAnnotation.aidl
new file mode 100644
index 0000000..44a5591
--- /dev/null
+++ b/compos/aidl/com/android/compos/InputFdAnnotation.aidl
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compos;
+
+/** {@hide} */
+parcelable InputFdAnnotation {
+ /**
+ * File descriptor number to be passed to the program. This is also the same file descriptor
+ * number used in the backend server.
+ */
+ int fd;
+
+ /** The actual file size in bytes of the backing file to be read. */
+ long file_size;
+}
diff --git a/compos/aidl/com/android/compos/Metadata.aidl b/compos/aidl/com/android/compos/Metadata.aidl
new file mode 100644
index 0000000..a15214d
--- /dev/null
+++ b/compos/aidl/com/android/compos/Metadata.aidl
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compos;
+
+import com.android.compos.InputFdAnnotation;
+import com.android.compos.OutputFdAnnotation;
+
+/** {@hide} */
+parcelable Metadata {
+ InputFdAnnotation[] input_fd_annotations;
+ OutputFdAnnotation[] output_fd_annotations;
+}
diff --git a/compos/aidl/com/android/compos/OutputFdAnnotation.aidl b/compos/aidl/com/android/compos/OutputFdAnnotation.aidl
new file mode 100644
index 0000000..95ce425
--- /dev/null
+++ b/compos/aidl/com/android/compos/OutputFdAnnotation.aidl
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compos;
+
+/** {@hide} */
+parcelable OutputFdAnnotation {
+ /**
+ * File descriptor number to be passed to the program. This is currently assumed to be same as
+ * the file descriptor number used in the backend server.
+ */
+ int fd;
+}
diff --git a/compos/src/compsvc.rs b/compos/src/compsvc.rs
new file mode 100644
index 0000000..e912463
--- /dev/null
+++ b/compos/src/compsvc.rs
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+//! compsvc is a service to run computational tasks in a PVM upon request. It is able to set up
+//! file descriptors backed by fd_server and pass the file descriptors to the actual tasks for
+//! read/write. The service also attempts to sandbox the execution so that one task cannot leak or
+//! impact future tasks.
+//!
+//! Example:
+//! $ compsvc /system/bin/sleep
+//!
+//! The current architecture / process hierarchy looks like:
+//! - compsvc (handle requests)
+//! - compsvc_worker (for environment setup)
+//! - authfs (fd translation)
+//! - actual task
+
+use anyhow::{bail, Context, Result};
+use log::error;
+use minijail::{self, Minijail};
+use std::path::PathBuf;
+
+use compos_aidl_interface::aidl::com::android::compos::ICompService::{
+ BnCompService, ICompService,
+};
+use compos_aidl_interface::aidl::com::android::compos::Metadata::Metadata;
+use compos_aidl_interface::binder::{
+ add_service, BinderFeatures, Interface, ProcessState, Result as BinderResult, Status,
+ StatusCode, Strong,
+};
+
+const SERVICE_NAME: &str = "compsvc";
+// TODO(b/161470604): Move the executable into an apex.
+const WORKER_BIN: &str = "/system/bin/compsvc_worker";
+// TODO: Replace with a valid directory setup in the VM.
+const AUTHFS_MOUNTPOINT: &str = "/data/local/tmp/authfs_mnt";
+
+struct CompService {
+ worker_bin: PathBuf,
+ task_bin: String,
+ debuggable: bool,
+}
+
+impl CompService {
+ pub fn new_binder(service: CompService) -> Strong<dyn ICompService> {
+ BnCompService::new_binder(service, BinderFeatures::default())
+ }
+
+ fn run_worker_in_jail_and_wait(&self, args: &[String]) -> Result<(), minijail::Error> {
+ let mut jail = Minijail::new()?;
+
+ // TODO(b/185175567): New user and uid namespace when supported. Run as nobody.
+ // New mount namespace to isolate the FUSE mount.
+ jail.namespace_vfs();
+
+ let inheritable_fds = if self.debuggable {
+ vec![1, 2] // inherit/redirect stdout/stderr for debugging
+ } else {
+ vec![]
+ };
+ let _pid = jail.run(&self.worker_bin, &inheritable_fds, &args)?;
+ jail.wait()
+ }
+
+ fn build_worker_args(&self, args: &[String], metadata: &Metadata) -> Vec<String> {
+ let mut worker_args = vec![
+ WORKER_BIN.to_string(),
+ "--authfs-root".to_string(),
+ AUTHFS_MOUNTPOINT.to_string(),
+ ];
+ for annotation in &metadata.input_fd_annotations {
+ worker_args.push("--in-fd".to_string());
+ worker_args.push(format!("{}:{}", annotation.fd, annotation.file_size));
+ }
+ for annotation in &metadata.output_fd_annotations {
+ worker_args.push("--out-fd".to_string());
+ worker_args.push(annotation.fd.to_string());
+ }
+ if self.debuggable {
+ worker_args.push("--debug".to_string());
+ }
+ worker_args.push("--".to_string());
+
+ // Do not accept arbitrary code execution. We want to execute some specific task of this
+ // service. Use the associated executable.
+ worker_args.push(self.task_bin.clone());
+ worker_args.extend_from_slice(&args[1..]);
+ worker_args
+ }
+}
+
+impl Interface for CompService {}
+
+impl ICompService for CompService {
+ fn execute(&self, args: &[String], metadata: &Metadata) -> BinderResult<i8> {
+ let worker_args = self.build_worker_args(args, metadata);
+
+ match self.run_worker_in_jail_and_wait(&worker_args) {
+ Ok(_) => Ok(0), // TODO(b/161471326): Sign the output on succeed.
+ Err(minijail::Error::ReturnCode(exit_code)) => {
+ error!("Task failed with exit code {}", exit_code);
+ Err(Status::from(StatusCode::FAILED_TRANSACTION))
+ }
+ Err(e) => {
+ error!("Unexpected error: {}", e);
+ Err(Status::from(StatusCode::UNKNOWN_ERROR))
+ }
+ }
+ }
+}
+
+fn parse_args() -> Result<CompService> {
+ #[rustfmt::skip]
+ let matches = clap::App::new("compsvc")
+ .arg(clap::Arg::with_name("debug")
+ .long("debug"))
+ .arg(clap::Arg::with_name("task_bin")
+ .required(true))
+ .get_matches();
+
+ Ok(CompService {
+ task_bin: matches.value_of("task_bin").unwrap().to_string(),
+ worker_bin: PathBuf::from(WORKER_BIN),
+ debuggable: matches.is_present("debug"),
+ })
+}
+
+fn main() -> Result<()> {
+ android_logger::init_once(
+ android_logger::Config::default().with_tag("compsvc").with_min_level(log::Level::Debug),
+ );
+
+ let service = parse_args()?;
+
+ ProcessState::start_thread_pool();
+ // TODO: switch to remote binder
+ add_service(SERVICE_NAME, CompService::new_binder(service).as_binder())
+ .with_context(|| format!("Failed to register service {}", SERVICE_NAME))?;
+ ProcessState::join_thread_pool();
+ bail!("Unexpected exit after join_thread_pool")
+}
diff --git a/compos/src/compsvc_worker.rs b/compos/src/compsvc_worker.rs
new file mode 100644
index 0000000..cf40f81
--- /dev/null
+++ b/compos/src/compsvc_worker.rs
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+//! This executable works as a child/worker for the main compsvc service. This worker is mainly
+//! responsible for setting up the execution environment, e.g. to create file descriptors for
+//! remote file access via an authfs mount.
+
+use anyhow::{bail, Result};
+use log::warn;
+use minijail::Minijail;
+use nix::sys::statfs::{statfs, FsType};
+use std::fs::{File, OpenOptions};
+use std::io;
+use std::os::unix::io::AsRawFd;
+use std::path::Path;
+use std::process::exit;
+use std::thread::sleep;
+use std::time::{Duration, Instant};
+
+const AUTHFS_BIN: &str = "/apex/com.android.virt/bin/authfs";
+const AUTHFS_SETUP_POLL_INTERVAL_MS: Duration = Duration::from_millis(50);
+const AUTHFS_SETUP_TIMEOUT_SEC: Duration = Duration::from_secs(10);
+const FUSE_SUPER_MAGIC: FsType = FsType(0x65735546);
+
+/// The number that hints the future file descriptor. These are not really file descriptor, but
+/// represents the file descriptor number to pass to the task.
+type PseudoRawFd = i32;
+
+fn is_fuse(path: &str) -> Result<bool> {
+ Ok(statfs(path)?.filesystem_type() == FUSE_SUPER_MAGIC)
+}
+
+fn spawn_authfs(config: &Config) -> Result<Minijail> {
+ // TODO(b/185175567): Run in a more restricted sandbox.
+ let jail = Minijail::new()?;
+
+ let mut args = vec![AUTHFS_BIN.to_string(), config.authfs_root.clone()];
+ for conf in &config.in_fds {
+ // TODO(b/185178698): Many input files need to be signed and verified.
+ // or can we use debug cert for now, which is better than nothing?
+ args.push("--remote-ro-file-unverified".to_string());
+ args.push(format!("{}:{}:{}", conf.fd, conf.fd, conf.file_size));
+ }
+ for conf in &config.out_fds {
+ args.push("--remote-new-rw-file".to_string());
+ args.push(format!("{}:{}", conf.fd, conf.fd));
+ }
+
+ let preserve_fds = if config.debuggable {
+ vec![1, 2] // inherit/redirect stdout/stderr for debugging
+ } else {
+ vec![]
+ };
+
+ let _pid = jail.run(Path::new(AUTHFS_BIN), &preserve_fds, &args)?;
+ Ok(jail)
+}
+
+fn wait_until_authfs_ready(authfs_root: &str) -> Result<()> {
+ let start_time = Instant::now();
+ loop {
+ if is_fuse(authfs_root)? {
+ break;
+ }
+ if start_time.elapsed() > AUTHFS_SETUP_TIMEOUT_SEC {
+ bail!("Time out mounting authfs");
+ }
+ sleep(AUTHFS_SETUP_POLL_INTERVAL_MS);
+ }
+ Ok(())
+}
+
+fn open_authfs_file(authfs_root: &str, basename: PseudoRawFd, writable: bool) -> io::Result<File> {
+ OpenOptions::new().read(true).write(writable).open(format!("{}/{}", authfs_root, basename))
+}
+
+fn open_authfs_files_for_mapping(config: &Config) -> io::Result<Vec<(File, PseudoRawFd)>> {
+ let mut fd_mapping = Vec::with_capacity(config.in_fds.len() + config.out_fds.len());
+
+ let results: io::Result<Vec<_>> = config
+ .in_fds
+ .iter()
+ .map(|conf| Ok((open_authfs_file(&config.authfs_root, conf.fd, false)?, conf.fd)))
+ .collect();
+ fd_mapping.append(&mut results?);
+
+ let results: io::Result<Vec<_>> = config
+ .out_fds
+ .iter()
+ .map(|conf| Ok((open_authfs_file(&config.authfs_root, conf.fd, true)?, conf.fd)))
+ .collect();
+ fd_mapping.append(&mut results?);
+
+ Ok(fd_mapping)
+}
+
+fn spawn_jailed_task(config: &Config, fd_mapping: Vec<(File, PseudoRawFd)>) -> Result<Minijail> {
+ // TODO(b/185175567): Run in a more restricted sandbox.
+ let jail = Minijail::new()?;
+ let mut preserve_fds: Vec<_> = fd_mapping.iter().map(|(f, id)| (f.as_raw_fd(), *id)).collect();
+ if config.debuggable {
+ // inherit/redirect stdout/stderr for debugging
+ preserve_fds.push((1, 1));
+ preserve_fds.push((2, 2));
+ }
+ let _pid =
+ jail.run_remap(&Path::new(&config.args[0]), preserve_fds.as_slice(), &config.args)?;
+ Ok(jail)
+}
+
+struct InFdAnnotation {
+ fd: PseudoRawFd,
+ file_size: u64,
+}
+
+struct OutFdAnnotation {
+ fd: PseudoRawFd,
+}
+
+struct Config {
+ authfs_root: String,
+ in_fds: Vec<InFdAnnotation>,
+ out_fds: Vec<OutFdAnnotation>,
+ args: Vec<String>,
+ debuggable: bool,
+}
+
+fn parse_args() -> Result<Config> {
+ #[rustfmt::skip]
+ let matches = clap::App::new("compsvc_worker")
+ .arg(clap::Arg::with_name("authfs-root")
+ .long("authfs-root")
+ .value_name("DIR")
+ .required(true)
+ .takes_value(true))
+ .arg(clap::Arg::with_name("in-fd")
+ .long("in-fd")
+ .multiple(true)
+ .takes_value(true)
+ .requires("authfs-root"))
+ .arg(clap::Arg::with_name("out-fd")
+ .long("out-fd")
+ .multiple(true)
+ .takes_value(true)
+ .requires("authfs-root"))
+ .arg(clap::Arg::with_name("debug")
+ .long("debug"))
+ .arg(clap::Arg::with_name("args")
+ .last(true)
+ .required(true)
+ .multiple(true))
+ .get_matches();
+
+ // Safe to unwrap since the arg is required by the clap rule
+ let authfs_root = matches.value_of("authfs-root").unwrap().to_string();
+
+ let results: Result<Vec<_>> = matches
+ .values_of("in-fd")
+ .unwrap_or_default()
+ .into_iter()
+ .map(|arg| {
+ if let Some(index) = arg.find(':') {
+ let (fd, size) = arg.split_at(index);
+ Ok(InFdAnnotation { fd: fd.parse()?, file_size: size[1..].parse()? })
+ } else {
+ bail!("Invalid argument: {}", arg);
+ }
+ })
+ .collect();
+ let in_fds = results?;
+
+ let results: Result<Vec<_>> = matches
+ .values_of("out-fd")
+ .unwrap_or_default()
+ .into_iter()
+ .map(|arg| Ok(OutFdAnnotation { fd: arg.parse()? }))
+ .collect();
+ let out_fds = results?;
+
+ let args: Vec<_> = matches.values_of("args").unwrap().map(|s| s.to_string()).collect();
+ let debuggable = matches.is_present("debug");
+
+ Ok(Config { authfs_root, in_fds, out_fds, args, debuggable })
+}
+
+fn main() -> Result<()> {
+ let log_level =
+ if env!("TARGET_BUILD_VARIANT") == "eng" { log::Level::Trace } else { log::Level::Info };
+ android_logger::init_once(
+ android_logger::Config::default().with_tag("compsvc_worker").with_min_level(log_level),
+ );
+
+ let config = parse_args()?;
+
+ let authfs_jail = spawn_authfs(&config)?;
+ let authfs_lifetime = scopeguard::guard(authfs_jail, |authfs_jail| {
+ if let Err(e) = authfs_jail.kill() {
+ if !matches!(e, minijail::Error::Killed(_)) {
+ warn!("Failed to kill authfs: {}", e);
+ }
+ }
+ });
+
+ wait_until_authfs_ready(&config.authfs_root)?;
+ let fd_mapping = open_authfs_files_for_mapping(&config)?;
+
+ let jail = spawn_jailed_task(&config, fd_mapping)?;
+ let jail_result = jail.wait();
+
+ // Be explicit about the lifetime, which should last at least until the task is finished.
+ drop(authfs_lifetime);
+
+ match jail_result {
+ Ok(_) => Ok(()),
+ Err(minijail::Error::ReturnCode(exit_code)) => {
+ exit(exit_code as i32);
+ }
+ Err(e) => {
+ bail!("Unexpected minijail error: {}", e);
+ }
+ }
+}
diff --git a/compos/src/pvm_exec.rs b/compos/src/pvm_exec.rs
new file mode 100644
index 0000000..fcde266
--- /dev/null
+++ b/compos/src/pvm_exec.rs
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+//! pvm_exec is a proxy/wrapper command to run a command remotely. It does not transport the
+//! program and just pass the command line arguments to compsvc to execute. The most important task
+//! for this program is to run a `fd_server` that serves remote file read/write requests.
+//!
+//! Example:
+//! $ adb shell exec 3</dev/zero 4<>/dev/null pvm_exec --in-fd 3 --out-fd 4 -- sleep 10
+//!
+//! Note the immediate argument right after "--" (e.g. "sleep" in the example above) is not really
+//! used. It is only for ergonomics.
+
+use anyhow::{bail, Context, Result};
+use log::{error, warn};
+use minijail::Minijail;
+use nix::fcntl::{fcntl, FcntlArg::F_GETFD};
+use nix::sys::stat::fstat;
+use std::os::unix::io::RawFd;
+use std::path::Path;
+use std::process::exit;
+
+use compos_aidl_interface::aidl::com::android::compos::{
+ ICompService::ICompService, InputFdAnnotation::InputFdAnnotation, Metadata::Metadata,
+ OutputFdAnnotation::OutputFdAnnotation,
+};
+use compos_aidl_interface::binder::Strong;
+
+static SERVICE_NAME: &str = "compsvc";
+static FD_SERVER_BIN: &str = "/apex/com.android.virt/bin/fd_server";
+
+fn get_local_service() -> Strong<dyn ICompService> {
+ compos_aidl_interface::binder::get_interface(SERVICE_NAME).expect("Cannot reach compsvc")
+}
+
+fn spawn_fd_server(metadata: &Metadata, debuggable: bool) -> Result<Minijail> {
+ let mut inheritable_fds = if debuggable {
+ vec![1, 2] // inherit/redirect stdout/stderr for debugging
+ } else {
+ vec![]
+ };
+
+ let mut args = vec![FD_SERVER_BIN.to_string()];
+ for metadata in &metadata.input_fd_annotations {
+ args.push("--ro-fds".to_string());
+ args.push(metadata.fd.to_string());
+ inheritable_fds.push(metadata.fd);
+ }
+ for metadata in &metadata.output_fd_annotations {
+ args.push("--rw-fds".to_string());
+ args.push(metadata.fd.to_string());
+ inheritable_fds.push(metadata.fd);
+ }
+
+ let jail = Minijail::new()?;
+ let _pid = jail.run(Path::new(FD_SERVER_BIN), &inheritable_fds, &args)?;
+ Ok(jail)
+}
+
+fn is_fd_valid(fd: RawFd) -> Result<bool> {
+ let retval = fcntl(fd, F_GETFD)?;
+ Ok(retval >= 0)
+}
+
+fn parse_arg_fd(arg: &str) -> Result<RawFd> {
+ let fd = arg.parse::<RawFd>()?;
+ if !is_fd_valid(fd)? {
+ bail!("Bad FD: {}", fd);
+ }
+ Ok(fd)
+}
+
+struct Config {
+ args: Vec<String>,
+ metadata: Metadata,
+ debuggable: bool,
+}
+
+fn parse_args() -> Result<Config> {
+ #[rustfmt::skip]
+ let matches = clap::App::new("pvm_exec")
+ .arg(clap::Arg::with_name("in-fd")
+ .long("in-fd")
+ .takes_value(true)
+ .multiple(true)
+ .use_delimiter(true))
+ .arg(clap::Arg::with_name("out-fd")
+ .long("out-fd")
+ .takes_value(true)
+ .multiple(true)
+ .use_delimiter(true))
+ .arg(clap::Arg::with_name("debug")
+ .long("debug"))
+ .arg(clap::Arg::with_name("args")
+ .last(true)
+ .required(true)
+ .multiple(true))
+ .get_matches();
+
+ let results: Result<Vec<_>> = matches
+ .values_of("in-fd")
+ .unwrap_or_default()
+ .map(|arg| {
+ let fd = parse_arg_fd(arg)?;
+ let file_size = fstat(fd)?.st_size;
+ Ok(InputFdAnnotation { fd, file_size })
+ })
+ .collect();
+ let input_fd_annotations = results?;
+
+ let results: Result<Vec<_>> = matches
+ .values_of("out-fd")
+ .unwrap_or_default()
+ .map(|arg| {
+ let fd = parse_arg_fd(arg)?;
+ Ok(OutputFdAnnotation { fd })
+ })
+ .collect();
+ let output_fd_annotations = results?;
+
+ let args: Vec<_> = matches.values_of("args").unwrap().map(|s| s.to_string()).collect();
+ let debuggable = matches.is_present("debug");
+
+ Ok(Config {
+ args,
+ metadata: Metadata { input_fd_annotations, output_fd_annotations },
+ debuggable,
+ })
+}
+
+fn main() -> Result<()> {
+ // 1. Parse the command line arguments for collect execution data.
+ let Config { args, metadata, debuggable } = parse_args()?;
+
+ // 2. Spawn and configure a fd_server to serve remote read/write requests.
+ let fd_server_jail = spawn_fd_server(&metadata, debuggable)?;
+ let fd_server_lifetime = scopeguard::guard(fd_server_jail, |fd_server_jail| {
+ if let Err(e) = fd_server_jail.kill() {
+ if !matches!(e, minijail::Error::Killed(_)) {
+ warn!("Failed to kill fd_server: {}", e);
+ }
+ }
+ });
+
+ // 3. Send the command line args to the remote to execute.
+ let exit_code = get_local_service().execute(&args, &metadata).context("Binder call failed")?;
+
+ // Be explicit about the lifetime, which should last at least until the task is finished.
+ drop(fd_server_lifetime);
+
+ if exit_code > 0 {
+ error!("remote execution failed with exit code {}", exit_code);
+ exit(exit_code as i32);
+ }
+ Ok(())
+}