#![deny(bare_trait_objects)]
#[macro_use]
extern crate clap;
use anyhow::{format_err, Error};
use clap::Arg;
use lucet_runtime::{self, DlModule, Limits, MmapRegion, Module, PublicKey, Region, RunResult};
use lucet_wasi::{self, WasiCtxBuilder, __wasi_exitcode_t};
use std::fs::File;
use std::path::PathBuf;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
struct Config<'a> {
lucet_module: &'a str,
guest_args: Vec<&'a str>,
entrypoint: &'a str,
preopen_dirs: Vec<(File, &'a str)>,
limits: Limits,
timeout: Option<Duration>,
verify: bool,
pk_path: Option<PathBuf>,
}
fn parse_humansized(desc: &str) -> Result<u64, Error> {
use human_size::{Byte, ParsingError, Size, SpecificSize};
match desc.parse::<Size>() {
Ok(s) => {
let bytes: SpecificSize<Byte> = s.into();
Ok(bytes.value() as u64)
}
Err(ParsingError::MissingMultiple) => Ok(desc.parse::<u64>()?),
Err(e) => Err(e)?,
}
}
fn main() {
lucet_runtime::lucet_internal_ensure_linked();
lucet_wasi::export_wasi_funcs();
let matches = app_from_crate!()
.arg(
Arg::with_name("entrypoint")
.long("entrypoint")
.takes_value(true)
.default_value("_start")
.help("Entrypoint to run within the WASI module"),
)
.arg(
Arg::with_name("preopen_dirs")
.required(false)
.long("dir")
.takes_value(true)
.multiple(true)
.number_of_values(1)
.help("A directory to provide to the WASI guest")
.long_help(
"Directories on the host can be provided to the WASI guest as part of a \
virtual filesystem. Each directory is specified as \
--dir `host_path:guest_path`, where `guest_path` specifies the path that will \
correspond to `host_path` for calls like `fopen` in the guest.\
\n\n\
For example, `--dir /home/host_user/wasi_sandbox:/sandbox` will make \
`/home/host_user/wasi_sandbox` available within the guest as `/sandbox`.\
\n\n\
Guests will be able to access any files and directories under the \
`host_path`, but will be unable to access other parts of the host \
filesystem through relative paths (e.g., `/sandbox/../some_other_file`) \
or through symlinks.",
),
)
.arg(
Arg::with_name("lucet_module")
.required(true)
.help("Path to the `lucetc`-compiled WASI module"),
)
.arg(
Arg::with_name("heap_memory_size")
.long("max-heap-size")
.takes_value(true)
.default_value("4 GiB")
.help("Maximum heap size (must be a multiple of 4 KiB)"),
)
.arg(
Arg::with_name("heap_address_space_size")
.long("heap-address-space")
.takes_value(true)
.default_value("8 GiB")
.help("Maximum heap address space size (must be a multiple of 4 KiB, and >= `max-heap-size`)"),
)
.arg(
Arg::with_name("stack_size")
.long("stack-size")
.takes_value(true)
.default_value("8 MiB")
.help("Maximum stack size (must be a multiple of 4 KiB)"),
)
.arg(
Arg::with_name("timeout").long("timeout").takes_value(true).help("Number of milliseconds the instance will be allowed to run")
)
.arg(
Arg::with_name("guest_args")
.required(false)
.multiple(true)
.help("Arguments to the WASI `main` function"),
)
.arg(
Arg::with_name("verify")
.long("--signature-verify")
.takes_value(false)
.help("Verify the signature of the source file")
)
.arg(
Arg::with_name("pk_path")
.long("--signature-pk")
.takes_value(true)
.help("Path to the public key to verify the source code signature")
)
.get_matches();
let entrypoint = matches.value_of("entrypoint").unwrap();
let lucet_module = matches.value_of("lucet_module").unwrap();
let preopen_dirs = matches
.values_of("preopen_dirs")
.map(|vals| {
vals.map(|preopen_dir| {
if let [host_path, guest_path] =
preopen_dir.split(':').collect::<Vec<&str>>().as_slice()
{
let host_dir = File::open(host_path).unwrap();
(host_dir, *guest_path)
} else {
println!("Invalid directory specification: {}", preopen_dir);
println!("{}", matches.usage());
std::process::exit(1);
}
})
.collect()
})
.unwrap_or(vec![]);
let heap_memory_size = matches
.value_of("heap_memory_size")
.ok_or_else(|| format_err!("missing heap memory size"))
.and_then(|v| parse_humansized(v))
.unwrap() as usize;
let heap_address_space_size = matches
.value_of("heap_address_space_size")
.ok_or_else(|| format_err!("missing heap address space size"))
.and_then(|v| parse_humansized(v))
.unwrap() as usize;
if heap_memory_size > heap_address_space_size {
println!("`heap-address-space` must be at least as large as `max-heap-size`");
println!("{}", matches.usage());
std::process::exit(1);
}
let stack_size = matches
.value_of("stack_size")
.ok_or_else(|| format_err!("missing stack size"))
.and_then(|v| parse_humansized(v))
.unwrap() as usize;
let timeout = matches
.value_of("timeout")
.map(|t| Duration::from_millis(t.parse::<u64>().unwrap()));
let limits = Limits {
heap_memory_size,
heap_address_space_size,
stack_size,
globals_size: 0, ..Limits::default()
};
let guest_args = matches
.values_of("guest_args")
.map(|vals| vals.collect())
.unwrap_or(vec![]);
let verify = matches.is_present("verify");
let pk_path = matches.value_of("pk_path").map(PathBuf::from);
let config = Config {
lucet_module,
guest_args,
entrypoint,
preopen_dirs,
limits,
timeout,
verify,
pk_path,
};
run(config)
}
fn run(config: Config<'_>) {
let exitcode = {
let pk = match (config.verify, config.pk_path) {
(false, _) => None,
(true, Some(pk_path)) => {
Some(PublicKey::from_file(pk_path).expect("public key can be loaded"))
}
(true, None) => panic!("signature verification requires a public key"),
};
let module = if let Some(pk) = pk {
DlModule::load_and_verify(&config.lucet_module, pk)
.expect("signed module can be loaded")
} else {
DlModule::load(&config.lucet_module).expect("module can be loaded")
};
let min_globals_size = module.initial_globals_size();
let globals_size = ((min_globals_size + 4096 - 1) / 4096) * 4096;
let region = MmapRegion::create(
1,
&Limits {
globals_size,
..config.limits
},
)
.expect("region can be created");
let args = std::iter::once(config.lucet_module)
.chain(config.guest_args.into_iter())
.collect::<Vec<&str>>();
let mut ctx = WasiCtxBuilder::new()
.args(args.iter())
.inherit_stdio()
.inherit_env();
for (dir, guest_path) in config.preopen_dirs {
ctx = ctx.preopened_dir(dir, guest_path);
}
let mut inst = region
.new_instance_builder(module as Arc<dyn Module>)
.with_embed_ctx(ctx.build().expect("WASI ctx can be created"))
.build()
.expect("instance can be created");
if let Some(timeout) = config.timeout {
let kill_switch = inst.kill_switch();
thread::spawn(move || {
thread::sleep(timeout);
kill_switch.terminate().ok();
});
}
match inst.run(config.entrypoint, &[]) {
Ok(RunResult::Returned(_)) => 0,
Ok(RunResult::Yielded(_)) => panic!("lucet-wasi unexpectedly yielded"),
Err(lucet_runtime::Error::RuntimeTerminated(
lucet_runtime::TerminationDetails::Provided(any),
)) => *any
.downcast_ref::<__wasi_exitcode_t>()
.expect("termination yields an exitcode"),
Err(lucet_runtime::Error::RuntimeTerminated(
lucet_runtime::TerminationDetails::Remote,
)) => {
println!("Terminated via remote kill switch (likely a timeout)");
std::u32::MAX
}
Err(e) => panic!("lucet-wasi runtime error: {}", e),
}
};
std::process::exit(exitcode as i32);
}