blob: 222aa68a48104da9726b23c6b9437fa38d052407 [file] [log] [blame] [edit]
// Copyright 2020 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! The client binary for interacting with ManaTEE from command line on Chrome OS.
#![deny(unsafe_op_in_unsafe_fn)]
use std::env;
use std::fs::File;
use std::io::copy;
use std::io::stdin;
use std::io::stdout;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Read;
use std::mem::replace;
use std::os::unix::io::AsRawFd;
use std::os::unix::io::FromRawFd;
use std::path::Path;
use std::path::PathBuf;
use std::process::exit;
use std::process::ChildStderr;
use std::process::Command;
use std::process::Stdio;
use std::str::FromStr;
use std::sync::Mutex;
use std::thread::sleep;
use std::thread::spawn;
use std::thread::JoinHandle;
use std::time::Duration;
use std::time::Instant;
use anyhow::anyhow;
use anyhow::bail;
use anyhow::Context;
use anyhow::Error;
use anyhow::Result;
use dbus::arg::OwnedFd;
use dbus::blocking::Connection;
use dbus::blocking::Proxy;
use getopts::Options;
use libchromeos::chromeos::is_dev_mode;
use libchromeos::panic_handler::install_memfd_handler;
use libchromeos::sys::handle_eintr;
use libchromeos::sys::unix::vsock::SocketAddr as VsockAddr;
use libchromeos::sys::unix::vsock::VsockCid;
use libchromeos::sys::unix::wait_for_interrupt;
use libchromeos::sys::unix::KillOnDrop;
use libsirenia::build_info::BUILD_TIMESTAMP;
use libsirenia::cli::TransportTypeOption;
use libsirenia::cli::VerbosityOption;
use libsirenia::cli::DEFAULT_TRANSPORT_TYPE_LONG_NAME;
use libsirenia::cli::DEFAULT_TRANSPORT_TYPE_SHORT_NAME;
use libsirenia::communication::trichechus;
use libsirenia::communication::trichechus::AppInfo;
use libsirenia::communication::trichechus::Trichechus;
use libsirenia::communication::trichechus::TrichechusClient;
use libsirenia::linux::events::CopyFdEventSource;
use libsirenia::linux::events::EventMultiplexer;
use libsirenia::sys;
use libsirenia::sys::dup;
use libsirenia::sys::is_a_tty;
use libsirenia::transport::Transport;
use libsirenia::transport::TransportType;
use libsirenia::transport::DEFAULT_CLIENT_PORT;
use libsirenia::transport::DEFAULT_SERVER_PORT;
use libsirenia::transport::LOOPBACK_DEFAULT;
use log::debug;
use log::error;
use log::info;
use manatee_client::client::OrgChromiumManaTEEInterface;
const DEFAULT_DBUS_TIMEOUT: Duration = Duration::from_secs(25);
const DEVELOPER_SHELL_APP_ID: &str = "shell";
const MINIJAIL_NAME: &str = "minijail0";
const CRONISTA_NAME: &str = "cronista";
const TRICHECHUS_NAME: &str = "trichechus";
const DUGONG_NAME: &str = "dugong";
const CRONISTA_USER: &str = "cronista";
const DUGONG_USER: &str = "dugong";
fn to_dbus_error(err: Error) -> dbus::Error {
dbus::Error::new_failed(&format!("{}", err))
}
/// Implementation of the D-Bus interface over a direct Vsock connection.
struct Passthrough {
client: Mutex<TrichechusClient>,
uri: TransportType,
}
impl Passthrough {
fn new(trichechus_uri: Option<TransportType>, bind_timeout: Option<Duration>) -> Result<Self> {
let uri = trichechus_uri.unwrap_or(TransportType::VsockConnection(VsockAddr {
cid: VsockCid::Host,
port: DEFAULT_SERVER_PORT,
}));
info!("Opening connection to trichechus");
// Adjust the source port when connecting to a non-standard port to facilitate testing.
let bind_port = match uri.get_port().context("failed to get port")? {
DEFAULT_SERVER_PORT => DEFAULT_CLIENT_PORT,
port => port + 1,
};
let start = Instant::now();
// Duration::default() is zero (i.e. no timeout).
let bind_timeout = bind_timeout.unwrap_or_default();
let mut transport = loop {
match uri.try_into_client(Some(bind_port)) {
Ok(t) => break t,
Err(err) => {
if start.elapsed() >= bind_timeout {
return Err(err).context("failed to get client for transport");
}
sleep(Duration::from_millis(100));
}
}
};
let transport = transport.connect().map_err(|e| {
error!("transport connect failed: {}", e);
anyhow!("transport connect failed: {}", e)
})?;
Ok(Passthrough {
client: Mutex::new(TrichechusClient::new(transport)),
uri,
})
}
fn start_teeapplication_impl(
&self,
app_id: &str,
args: Vec<&str>,
) -> Result<(i32, OwnedFd, OwnedFd)> {
info!("Setting up app vsock.");
let mut app_transport = self
.uri
.try_into_client(None)
.context("failed to get client for transport")?;
let addr = app_transport.bind().context("failed to bind to socket")?;
let app_info = AppInfo {
app_id: app_id.to_string(),
port_number: addr.get_port().context("failed to get port")?,
};
info!("Starting rpc.");
self.client
.lock()
.unwrap()
.start_session(app_info, args.iter().map(|s| s.to_string()).collect())
.context("start_session rpc failed")?;
info!("Starting TEE application: {}", app_id);
let Transport { r, w, id: _ } = app_transport
.connect()
.context("failed to connect to socket")?;
info!("Forwarding stdio.");
// Safe because ownership of the file descriptors is transferred.
Ok((
0, /* error_code */
unsafe { OwnedFd::new(r.into_raw_fd()) },
unsafe { OwnedFd::new(w.into_raw_fd()) },
))
}
}
impl OrgChromiumManaTEEInterface for Passthrough {
fn start_teeapplication(
&self,
app_id: &str,
args: Vec<&str>,
_allow_unverified: bool,
) -> std::result::Result<(i32, OwnedFd, OwnedFd), dbus::Error> {
self.start_teeapplication_impl(app_id, args)
.map_err(to_dbus_error)
}
fn system_event(&self, event: &str) -> std::result::Result<String, dbus::Error> {
match self.client.lock().unwrap().system_event(
event
.parse()
.map_err(|err: String| dbus::Error::new_failed(&err))?,
) {
Ok(()) => Ok(String::new()),
Err(err) => match err.downcast::<trichechus::Error>() {
Ok(err) => Ok(err.to_string()),
Err(err) => Err(dbus::Error::new_failed(&err.to_string())),
},
}
}
fn get_manatee_memory_service_socket(&self) -> std::result::Result<OwnedFd, dbus::Error> {
Err(to_dbus_error(anyhow!(
"GetManateeMemeoryServiceSocket not supported"
)))
}
}
fn connect_to_dugong<'a>(c: &'a Connection) -> Result<Proxy<'a, &Connection>> {
Ok(c.with_proxy(
"org.chromium.ManaTEE",
"/org/chromium/ManaTEE1",
DEFAULT_DBUS_TIMEOUT,
))
}
fn handle_app_fds_interactive(input: File, output: File) -> Result<()> {
let mut ctx = EventMultiplexer::new().unwrap();
let raw = sys::ScopedRaw::new().map_err(|_| anyhow!("failed to put stdin in raw mode"))?;
let copy_in = CopyFdEventSource::new(Box::new(input), Box::new(dup::<File>(1)?))?;
ctx.add_event(Box::new(copy_in.0))?;
ctx.add_event(Box::new(copy_in.1))?;
let copy_out = CopyFdEventSource::new(Box::new(dup::<File>(0)?), Box::new(output))?;
ctx.add_event(Box::new(copy_out.0))?;
ctx.add_event(Box::new(copy_out.1))?;
let start = ctx.len();
while ctx.len() == start {
ctx.run_once()?;
}
drop(raw);
Ok(())
}
fn handle_app_fds(mut input: File, mut output: File) -> Result<()> {
let output_thread_handle = spawn(move || -> Result<()> {
handle_eintr!(copy(&mut input, &mut stdout())).context("failed to copy to stdout")?;
// Once stdout is closed, stdin is invalid and it is time to exit.
exit(0);
});
handle_eintr!(copy(&mut stdin(), &mut output)).context("failed to copy from stdin")?;
output_thread_handle
.join()
.map_err(|boxed_err| *boxed_err.downcast::<Error>().unwrap())?
}
fn start_manatee_app(
api: &dyn OrgChromiumManaTEEInterface,
app_id: &str,
args: Vec<&str>,
handler: &dyn Fn(File, File) -> Result<()>,
allow_unverified: bool,
) -> Result<()> {
info!("Starting TEE application: {}", app_id);
let (fd_in, fd_out) = match api
.start_teeapplication(app_id, args, allow_unverified)
.context("failed to call start_teeapplication D-Bus method")?
{
(0, fd_in, fd_out) => (fd_in, fd_out),
(code, _, _) => bail!("start app failed with code: {}", code),
};
info!("Forwarding stdio.");
// Safe because ownership of the file descriptor is transferred.
let file_in = unsafe { File::from_raw_fd(fd_in.into_fd()) };
// Safe because ownership of the file descriptor is transferred.
let file_out = unsafe { File::from_raw_fd(fd_out.into_fd()) };
handler(file_in, file_out)
}
fn system_event(trichechus_uri: Option<TransportType>, event: &str) -> Result<()> {
let timeout = if trichechus_uri.is_none() {
info!("Connecting to D-Bus.");
let connection = Connection::new_system().context("failed to get D-Bus connection")?;
let conn_path = connect_to_dugong(&connection)?;
match conn_path.system_event(event) {
Ok(v) => {
return if v.is_empty() {
Ok(())
} else {
Err(anyhow!("failed to invoke system event: {}", v))
}
}
Err(err) => {
error!("D-Bus call failed: {}", err);
info!("Falling back to default vsock interface.");
}
}
Some(Duration::from_secs(10))
} else {
None
};
let passthrough = Passthrough::new(trichechus_uri, timeout)?;
let err = passthrough
.system_event(event)
.context("system_event D-Bus call failed")?;
if err.is_empty() {
Ok(())
} else {
Err(anyhow!("system_event failed with: {}", err))
}
}
fn dbus_start_manatee_app(
app_id: &str,
args: Vec<&str>,
handler: &dyn Fn(File, File) -> Result<()>,
allow_unverified: bool,
) -> Result<()> {
info!("Connecting to D-Bus.");
let connection = Connection::new_system().context("failed to get D-Bus connection")?;
let conn_path = connect_to_dugong(&connection)?;
start_manatee_app(&conn_path, app_id, args, handler, allow_unverified)
}
fn direct_start_manatee_app(
trichechus_uri: TransportType,
app_id: &str,
args: Vec<&str>,
elf: Option<Vec<u8>>,
handler: &dyn Fn(File, File) -> Result<()>,
allow_unverified: bool,
) -> Result<()> {
let passthrough = Passthrough::new(Some(trichechus_uri), None)?;
if allow_unverified && !is_dev_mode().unwrap_or(false) {
bail!("allow_unverified is only respected in developer mode");
}
if let Some(elf) = elf {
info!("Transmitting TEE app.");
passthrough
.client
.lock()
.unwrap()
.load_app(app_id.to_string(), elf, allow_unverified)
.context("load_app rpc failed")?;
}
// Allow loading unverified apps when developer mode is enabled to ease testing use cases.
start_manatee_app(&passthrough, app_id, args, handler, allow_unverified)
}
fn locate_command(name: &str) -> Result<PathBuf> {
which::which(name).with_context(|| format!("failed to locate command '{}'", name))
}
fn read_line<R: Read>(job_name: &str, reader: &mut BufReader<R>, line: &mut String) -> Result<()> {
line.clear();
reader
.read_line(line)
.with_context(|| format!("failed to read stderr of {}", job_name))?;
eprint!("{}", &line);
Ok(())
}
fn get_listening_port<R: Read + Send + 'static, C: Fn(&str) -> bool>(
job_name: &'static str,
read: R,
conditions: &[C],
) -> Result<(u32, JoinHandle<()>)> {
let mut reader = BufReader::new(read);
let mut line = String::new();
read_line(job_name, &mut reader, &mut line)?;
for condition in conditions {
if condition(&line) {
read_line(job_name, &mut reader, &mut line)?;
}
}
if !line.contains("waiting for connection at: ip://127.0.0.1:") {
bail!(
"failed to locate listening URI for {0:}; last line: '{1:}'",
job_name.to_string(),
line
);
}
let port = u32::from_str(&line[line.rfind(':').unwrap() + 1..line.len() - 1])
.with_context(|| format!("failed to parse port number from line '{0:}'", line))?;
let join_handle =
spawn(
move || {
while read_line(job_name, &mut reader, &mut line).is_ok() && !line.is_empty() {}
},
);
Ok((port, join_handle))
}
fn run_test_environment() -> Result<()> {
let minijail_path = locate_command(MINIJAIL_NAME)?;
let cronista_path = locate_command(CRONISTA_NAME)?;
let trichechus_path = locate_command(TRICHECHUS_NAME)?;
let dugong_path = locate_command(DUGONG_NAME)?;
// Cronista.
let mut cronista = KillOnDrop::from(
Command::new(&minijail_path)
.args(&[
"-u",
CRONISTA_USER,
"--",
cronista_path.to_str().unwrap(),
"-U",
"ip://127.0.0.1:0",
])
.stderr(Stdio::piped())
.spawn()
.with_context(|| format!("failed to start command '{}'", CRONISTA_NAME))?,
);
let (cronista_port, cronista_stderr_print) = get_listening_port(
CRONISTA_NAME,
replace(&mut cronista.as_mut().stderr, Option::<ChildStderr>::None).unwrap(),
&[|l: &str| l.ends_with("starting cronista\n")],
)?;
// Trichechus.
let mut trichechus = KillOnDrop::from(
Command::new(trichechus_path)
.args(&[
"-U",
"ip://127.0.0.1:0",
"-C",
&format!("ip://127.0.0.1:{}", cronista_port),
])
.stderr(Stdio::piped())
.spawn()
.with_context(|| format!("failed to start command '{}'", TRICHECHUS_NAME))?,
);
let conditions = [
|l: &str| l == "Syslog exists.\n" || l == "Creating syslog.\n",
|l: &str| l.contains("starting trichechus:"),
|l: &str| l.contains("Unable to start new process group:"),
];
let (trichechus_port, trichechus_stderr_print) = get_listening_port(
TRICHECHUS_NAME,
replace(&mut trichechus.as_mut().stderr, Option::<ChildStderr>::None).unwrap(),
&conditions,
)?;
// Dugong.
let dugong = KillOnDrop::from(
Command::new(&minijail_path)
.args(&[
"-u",
DUGONG_USER,
"--",
dugong_path.to_str().unwrap(),
"-U",
&format!("ip://127.0.0.1:{}", trichechus_port),
])
.spawn()
.with_context(|| format!("failed to start command '{}'", DUGONG_NAME))?,
);
println!("*** Press Ctrl-C to continue. ***");
wait_for_interrupt().ok();
drop(dugong);
drop(trichechus);
drop(cronista);
trichechus_stderr_print.join().unwrap();
cronista_stderr_print.join().unwrap();
Ok(())
}
fn split_args<I: IntoIterator<Item = String>>(into_iter: I) -> (Vec<String>, Vec<String>) {
let mut opts = Vec::new();
let mut args = Vec::new();
let mut found_delimiter = false;
for value in into_iter {
if !found_delimiter {
if value != "--" {
opts.push(value)
} else {
found_delimiter = true;
}
} else {
args.push(value);
}
}
(opts, args)
}
fn get_usage() -> String {
format!("[-h] [-r | -a <name> [-X <path>] [-i true|false] | --halt | --poweroff | --reboot] [-- ...]\nversion: {}", BUILD_TIMESTAMP)
}
fn main() -> Result<()> {
const HELP_SHORT_NAME: &str = "h";
const RUN_SERVICES_LOCALLY_SHORT_NAME: &str = "r";
const APP_ID_SHORT_NAME: &str = "a";
const APP_ELF_SHORT_NAME: &str = "X";
const INTERACTIVE_SHORT_NAME: &str = "i";
const HALT_LONG_NAME: &str = "halt";
const POWEROFF_LONG_NAME: &str = "poweroff";
const REBOOT_LONG_NAME: &str = "reboot";
const ALLOW_UNVERIFIED_LONG_NAME: &str = "allow-unverified";
install_memfd_handler();
let mut options = Options::new();
options.optflag(HELP_SHORT_NAME, "help", "Show this help string.");
options.optflag(
RUN_SERVICES_LOCALLY_SHORT_NAME,
"run-services-locally",
"Run a test sirenia environment locally.",
);
options.optopt(
APP_ID_SHORT_NAME,
"app-id",
"Specify the app ID to invoke.",
"demo_app",
);
options.optopt(
APP_ELF_SHORT_NAME,
"app-elf",
"Specify the app elf file to load.",
"/bin/bash",
);
options.optopt(
INTERACTIVE_SHORT_NAME,
"interactive",
"Enable or disable readline support. Defaults to false except for 'shell'",
"true|false",
);
options.optflag(
"",
ALLOW_UNVERIFIED_LONG_NAME,
"allow app to load if hash doesn't match",
);
options.optflag("", HALT_LONG_NAME, "Send a halt command to the hypervisor.");
options.optflag(
"",
POWEROFF_LONG_NAME,
"Send a poweroff command to the hypervisor.",
);
options.optflag(
"",
REBOOT_LONG_NAME,
"Send a reboot command to the hypervisor.",
);
let trichechus_uri_opt = TransportTypeOption::new(
DEFAULT_TRANSPORT_TYPE_SHORT_NAME,
DEFAULT_TRANSPORT_TYPE_LONG_NAME,
"trichechus URI (set to bypass dugong D-Bus)",
LOOPBACK_DEFAULT,
&mut options,
);
let verbosity_opt = VerbosityOption::default(&mut options);
let (opts, args) = split_args(env::args());
let matches = options.parse(&opts[1..]).map_err(|err| {
eprintln!("{}", options.usage(&get_usage()));
anyhow!("failed parse command line options: {}", err)
})?;
let verbosity = verbosity_opt.from_matches(&matches);
stderrlog::new().verbosity(verbosity).init().unwrap();
debug!("Verbosity: {}", verbosity);
if matches.opt_present(HELP_SHORT_NAME) {
println!("{}", options.usage(&get_usage()));
return Ok(());
}
// Validate options, by counting mutually exclusive groups of options.
let mut opts = Vec::<String>::new();
let mut mutually_exclusive_opts = 0;
if matches.opt_present(RUN_SERVICES_LOCALLY_SHORT_NAME) {
mutually_exclusive_opts += 1;
opts.push(format!("-{}", RUN_SERVICES_LOCALLY_SHORT_NAME));
}
if matches.opt_present(APP_ID_SHORT_NAME)
|| matches.opt_present(APP_ELF_SHORT_NAME)
|| matches.opt_present(INTERACTIVE_SHORT_NAME)
{
mutually_exclusive_opts += 1;
if matches.opt_present(APP_ID_SHORT_NAME) {
opts.push(format!("-{}", APP_ID_SHORT_NAME));
} else if matches.opt_present(APP_ELF_SHORT_NAME) {
opts.push(format!("-{}", APP_ELF_SHORT_NAME));
} else {
opts.push(format!("-{}", INTERACTIVE_SHORT_NAME));
}
}
for long_name in &[HALT_LONG_NAME, POWEROFF_LONG_NAME, REBOOT_LONG_NAME] {
if matches.opt_present(long_name) {
mutually_exclusive_opts += 1;
opts.push(format!("--{}", long_name));
}
}
if mutually_exclusive_opts > 1 {
eprintln!("{}", options.usage(&get_usage()));
bail!("incompatible options set : {0:?}", opts);
}
let trichechus_uri = trichechus_uri_opt.from_matches(&matches).map_err(|err| {
eprintln!("{}", options.usage(&get_usage()));
anyhow!("failed to get transport type option: {}", err)
})?;
let interactive: Option<bool> = if matches.opt_present(INTERACTIVE_SHORT_NAME) {
Some(
matches
.opt_get::<String>(INTERACTIVE_SHORT_NAME)
.unwrap()
.unwrap()
.parse()
.map_err(|err| {
eprintln!("{}", options.usage(&get_usage()));
anyhow!("invalid value for -i: {}", err)
})?,
)
} else {
None
};
if matches.opt_present(RUN_SERVICES_LOCALLY_SHORT_NAME) {
return run_test_environment();
}
if matches.opt_present(HALT_LONG_NAME) {
return system_event(trichechus_uri, "halt");
}
if matches.opt_present(POWEROFF_LONG_NAME) {
return system_event(trichechus_uri, "poweroff");
}
if matches.opt_present(REBOOT_LONG_NAME) {
return system_event(trichechus_uri, "reboot");
}
let app_id = matches
.opt_get(APP_ID_SHORT_NAME)
.unwrap()
.unwrap_or_else(|| DEVELOPER_SHELL_APP_ID.to_string());
let elf = if let Some(elf_path) = matches.opt_get::<String>(APP_ELF_SHORT_NAME).unwrap() {
let mut data = Vec::<u8>::new();
File::open(Path::new(&elf_path))
.context("open failed")?
.read_to_end(&mut data)
.context("read failed")?;
Some(data)
} else {
None
};
let handler: &dyn Fn(File, File) -> Result<()> = if is_a_tty(stdin().as_raw_fd())
&& interactive.unwrap_or_else(|| app_id.as_str() == DEVELOPER_SHELL_APP_ID)
{
&handle_app_fds_interactive
} else {
&handle_app_fds
};
let args_ref = args.iter().map(AsRef::<str>::as_ref).collect();
let allow_unverified = matches.opt_present(ALLOW_UNVERIFIED_LONG_NAME);
match trichechus_uri {
None => dbus_start_manatee_app(&app_id, args_ref, handler, allow_unverified),
Some(trichechus_uri) => direct_start_manatee_app(
trichechus_uri,
&app_id,
args_ref,
elf,
handler,
allow_unverified,
),
}
}