blob: 983a7f5155c6166a16d611e44370710ffb5f0c7c [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
anyhow::Context,
fidl::endpoints::ControlHandle,
fidl::endpoints::ServerEnd,
fidl_fuchsia_io as fio,
fidl_fuchsia_ldsvc::{LoaderMarker, LoaderRequest},
fidl_fuchsia_test_runner as ftestrunner,
ftestrunner::LibraryLoaderCacheMarker,
fuchsia_async as fasync, fuchsia_zircon as zx,
futures::lock::Mutex as FutMutex,
futures::prelude::*,
std::collections::HashMap,
std::sync::{Arc, Weak},
tracing::warn,
};
/// maps vmo key with vmo result.
type VmoKeyMap = HashMap<String, (i32, Option<zx::Vmo>)>;
#[derive(Debug)]
pub struct LibraryLoaderCache {
/// Proxy to /pkg/lib
lib_proxy: Arc<fio::DirectoryProxy>,
/// Mapping of config key with loaded VMOs map.
load_response_map: FutMutex<HashMap<String, Arc<FutMutex<VmoKeyMap>>>>,
}
impl LibraryLoaderCache {
pub fn new(lib_proxy: Arc<fio::DirectoryProxy>) -> Arc<Self> {
return Arc::new(Self { lib_proxy, load_response_map: FutMutex::new(HashMap::new()) });
}
}
pub async fn serve_cache(
cache: Arc<LibraryLoaderCache>,
server_end: ServerEnd<LibraryLoaderCacheMarker>,
) -> Result<(), anyhow::Error> {
let mut stream = server_end.into_stream()?;
while let Some(event) = stream.try_next().await? {
match event {
ftestrunner::LibraryLoaderCacheRequest::Serve { loader, .. } => {
let cache = Arc::downgrade(&cache);
serve_lib_loader(loader, cache).detach();
}
}
}
Ok(())
}
fn vmo_create_child(vmo: &zx::Vmo) -> Result<zx::Vmo, anyhow::Error> {
let size = vmo.get_size().context("Cannot get vmo size.")?;
vmo.create_child(
zx::VmoChildOptions::SNAPSHOT_AT_LEAST_ON_WRITE | zx::VmoChildOptions::NO_WRITE,
0,
size,
)
.context("cannot create child vmo")
}
fn duplicate_vmo(vmo: &Option<zx::Vmo>) -> Result<Option<zx::Vmo>, anyhow::Error> {
Ok(match &vmo {
// create child instead of duplicating so that our debugger tools don't break.
// Also vmo created using create child is non-writable, but debugger is able to write to it
// as it has special permissions.
Some(vmo) => vmo_create_child(&vmo)?.into(),
None => None,
})
}
/// Serve a custom lib loader which caches request to load VMOs.
fn serve_lib_loader(
loader: ServerEnd<LoaderMarker>,
lib_loader_cache: Weak<LibraryLoaderCache>,
) -> fasync::Task<()> {
fasync::Task::spawn(
async move {
let mut stream = loader.into_stream()?;
let (mut search_dirs, mut current_response_map) = match lib_loader_cache.upgrade() {
Some(obj) => (
vec![obj.lib_proxy.clone()],
obj.load_response_map.lock().await.entry("".to_string()).or_default().clone(),
),
None => return Ok(()),
};
while let Some(req) = stream.try_next().await? {
let lib_loader_cache = match lib_loader_cache.upgrade() {
Some(obj) => obj,
None => break,
};
match req {
LoaderRequest::Done { control_handle } => {
control_handle.shutdown();
}
LoaderRequest::LoadObject { object_name, responder } => {
if let Some((rv, vmo)) = current_response_map.lock().await.get(&object_name)
{
responder.send(rv.clone(), duplicate_vmo(vmo)?)?;
continue;
}
let (vmo, rv) =
match library_loader::load_object(&search_dirs, &object_name).await {
Ok(b) => (b.into(), zx::sys::ZX_OK),
Err(e) => {
warn!("failed to load object: {:?}", e);
(None, zx::sys::ZX_ERR_NOT_FOUND)
}
};
let vmo_clone = duplicate_vmo(&vmo)?;
current_response_map.lock().await.insert(object_name, (rv, vmo));
responder.send(rv, vmo_clone)?;
}
LoaderRequest::Config { config, responder } => {
match library_loader::parse_config_string(
&vec![lib_loader_cache.lib_proxy.clone()],
&config,
) {
Ok(new_search_path) => {
search_dirs = new_search_path;
current_response_map = lib_loader_cache
.load_response_map
.lock()
.await
.entry(config)
.or_default()
.clone();
responder.send(zx::sys::ZX_OK)?;
}
Err(e) => {
warn!("failed to parse config: {}", e);
responder.send(zx::sys::ZX_ERR_INVALID_ARGS)?;
}
}
}
LoaderRequest::Clone { loader, responder } => {
serve_lib_loader(loader, Arc::downgrade(&lib_loader_cache)).detach();
responder.send(zx::sys::ZX_OK)?;
}
}
}
Ok(())
}
.unwrap_or_else(|e: anyhow::Error| warn!("couldn't run library loader service: {:?}", e)),
)
}
#[cfg(test)]
mod tests {
use {super::*, anyhow::Error, assert_matches::assert_matches, std::path::Path};
async fn list_directory<'a>(root_proxy: &'a fio::DirectoryProxy) -> Vec<String> {
let dir = fuchsia_fs::clone_directory(&root_proxy, fio::OpenFlags::CLONE_SAME_RIGHTS)
.expect("Failed to clone DirectoryProxy");
let entries = fuchsia_fs::directory::readdir(&dir).await.expect("readdir failed");
entries.iter().map(|entry| entry.name.clone()).collect::<Vec<String>>()
}
#[fasync::run_singlethreaded(test)]
async fn load_objects_test() -> Result<(), Error> {
// Open this test's real /pkg/lib directory to use for this test, and then check to see
// whether an asan subdirectory is present, and use it instead if so.
// TODO(fxbug.dev/37534): Use a synthetic /pkg/lib in this test so it doesn't depend on the
// package layout (like whether sanitizers are in use) once Rust vfs supports
// OPEN_RIGHT_EXECUTABLE
let rights = fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_EXECUTABLE;
let mut pkg_lib = fuchsia_fs::open_directory_in_namespace("/pkg/lib", rights)?;
let entries = list_directory(&pkg_lib).await;
if entries.iter().any(|f| &f as &str == "asan-ubsan") {
pkg_lib = fuchsia_fs::open_directory(&pkg_lib, &Path::new("asan-ubsan"), rights)?;
} else if entries.iter().any(|f| &f as &str == "asan") {
pkg_lib = fuchsia_fs::open_directory(&pkg_lib, &Path::new("asan"), rights)?;
} else if entries.iter().any(|f| &f as &str == "coverage") {
pkg_lib = fuchsia_fs::open_directory(&pkg_lib, &Path::new("coverage"), rights)?;
} else if entries.iter().any(|f| &f as &str == "coverage-rust") {
pkg_lib = fuchsia_fs::open_directory(&pkg_lib, &Path::new("coverage-rust"), rights)?;
} else if entries.iter().any(|f| &f as &str == "coverage-cts") {
pkg_lib = fuchsia_fs::open_directory(&pkg_lib, &Path::new("coverage-cts"), rights)?;
} else if entries.iter().any(|f| &f as &str == "profile") {
pkg_lib = fuchsia_fs::open_directory(&pkg_lib, &Path::new("profile"), rights)?;
}
let (loader_proxy, loader_service) = fidl::endpoints::create_proxy::<LoaderMarker>()?;
let cache = Arc::new(LibraryLoaderCache {
lib_proxy: pkg_lib.into(),
load_response_map: FutMutex::new(HashMap::new()),
});
serve_lib_loader(loader_service, Arc::downgrade(&cache)).detach();
let tests = vec![
// Should be able to access lib/ld.so.1
("ld.so.1", true),
// Should be able to access lib/libfdio.so
("libfdio.so", true),
// Should not be able to access lib/lib/ld.so.1
("lib/ld.so.1", false),
// Should not be able to access lib/../lib/ld.so.1
("../lib/ld.so.1", false),
// Should not be able to access lib/bin/test-runner-unit-tests
("bin/test-runner-unit-tests", false),
// Should not be able to access bin/test-runner-unit-tests
("../bin/test-runner-unit-tests", false),
// Should not be able to access meta/test-runner-unit-tests.cm
("../meta/test-runner-unit-tests.cm", false),
];
for &(obj_name, should_succeed) in &tests {
let (res, o_vmo) = loader_proxy.load_object(obj_name).await?;
let map = cache.load_response_map.lock().await.get("").unwrap().clone();
assert!(map.lock().await.contains_key(obj_name));
if should_succeed {
assert_eq!(zx::sys::ZX_OK, res, "loading {} did not succeed", obj_name);
assert!(o_vmo.is_some());
assert_matches!(map.lock().await.get(obj_name).unwrap().1, Some(_));
} else {
assert_ne!(zx::sys::ZX_OK, res, "loading {} did not fail", obj_name);
assert!(o_vmo.is_none());
assert_eq!(map.lock().await.get(obj_name).unwrap().1, None);
}
}
// also test clone
let (loader_proxy2, loader_service) = fidl::endpoints::create_proxy::<LoaderMarker>()?;
assert_eq!(zx::sys::ZX_OK, loader_proxy.clone(loader_service).await?);
for (obj_name, should_succeed) in tests {
let (res, o_vmo) = loader_proxy2.load_object(obj_name).await?;
if should_succeed {
assert_eq!(zx::sys::ZX_OK, res, "loading {} did not succeed", obj_name);
assert!(o_vmo.is_some());
} else {
assert_ne!(zx::sys::ZX_OK, res, "loading {} did not fail", obj_name);
assert!(o_vmo.is_none());
}
}
// test done
loader_proxy2.done().expect("done should not fail");
let err = loader_proxy2
.load_object("some_name")
.await
.expect_err("Should fail with PEER_CLOSED.");
let status = assert_matches!(err, fidl::Error::ClientChannelClosed{status, ..} => status);
assert_eq!(status, zx::Status::PEER_CLOSED);
Ok(())
}
#[fasync::run_singlethreaded(test)]
async fn config_test() -> Result<(), Error> {
// This /pkg/lib/config_test/ directory is added by the build rules for this test package,
// since we need a directory that supports OPEN_RIGHT_EXECUTABLE. It contains a file 'foo'
// which contains 'hippos' and a file 'bar/baz' (that is, baz in a subdirectory bar) which
// contains 'rule'.
// TODO(fxbug.dev/37534): Use a synthetic /pkg/lib in this test so it doesn't depend on the
// package layout once Rust vfs supports OPEN_RIGHT_EXECUTABLE
let pkg_lib = fuchsia_fs::open_directory_in_namespace(
"/pkg/lib/config_test/",
fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_EXECUTABLE,
)?;
let (loader_proxy, loader_service) = fidl::endpoints::create_proxy::<LoaderMarker>()?;
let cache = Arc::new(LibraryLoaderCache {
lib_proxy: pkg_lib.into(),
load_response_map: FutMutex::new(HashMap::new()),
});
serve_lib_loader(loader_service, Arc::downgrade(&cache)).detach();
// Attempt to access things with different configurations
for (obj_name, config, expected_result) in vec![
// Should be able to load foo
("foo", None, Some("hippos")),
// Should not be able to load bar (it's a directory)
("bar", None, None),
// Should not be able to load baz (it's in a sub directory)
("baz", None, None),
// Should be able to load baz with config "bar!" (only look in sub directory bar)
("baz", Some("bar!"), Some("rule")),
// Should not be able to load foo with config "bar!" (only look in sub directory bar)
("foo", Some("bar!"), None),
// Should be able to load foo with config "bar" (also look in sub directory bar)
("foo", Some("bar"), Some("hippos")),
// Should be able to load baz with config "bar" (also look in sub directory bar)
("baz", Some("bar"), Some("rule")),
] {
if let Some(config) = config {
assert_eq!(zx::sys::ZX_OK, loader_proxy.config(config).await?);
}
let (res, o_vmo) = loader_proxy.load_object(obj_name).await?;
let map = cache
.load_response_map
.lock()
.await
.get(config.unwrap_or_default())
.unwrap()
.clone();
if let Some(expected_result) = expected_result {
assert_eq!(zx::sys::ZX_OK, res);
let mut buf = vec![0; expected_result.len()];
o_vmo.expect("missing vmo").read(&mut buf, 0)?;
assert_eq!(expected_result.as_bytes(), buf.as_slice());
assert_matches!(map.lock().await.get(obj_name).unwrap().1, Some(_));
} else {
assert_ne!(zx::sys::ZX_OK, res);
assert!(o_vmo.is_none());
assert_eq!(map.lock().await.get(obj_name).unwrap().1, None);
}
}
Ok(())
}
}