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(())
+}