Delete experimental code

Summary: This reduces Reverie's maintenance burden going forward. This code isn't used and is in the commit history if it needs to be referenced.

Reviewed By: VladimirMakaev

Differential Revision: D56889383

fbshipit-source-id: 6fe2ca3a945a69a84624f545102283bda0285233
This commit is contained in:
Jason White 2024-05-03 04:36:51 -07:00 committed by Facebook GitHub Bot
parent 374247cc0a
commit 407899245d
45 changed files with 0 additions and 5869 deletions

View file

@ -1,12 +0,0 @@
# @generated by autocargo from //hermetic_infra/reverie/experimental:nostd-print
[package]
name = "nostd-print"
version = "0.1.0"
authors = ["Meta Platforms"]
edition = "2021"
repository = "https://github.com/facebookexperimental/reverie"
license = "BSD-2-Clause"
[dependencies]
syscalls = { version = "0.6.7", features = ["serde"] }

View file

@ -1,130 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
//! Provides helpers for printing formatted messages to stdout/stderr without
//! relying on std.
use core::fmt;
use core::fmt::Write;
use syscalls::syscall3;
use syscalls::Errno;
use syscalls::Sysno;
#[inline(always)]
fn sys_write(fd: i32, buf: &[u8]) -> Result<usize, Errno> {
unsafe { syscall3(Sysno::write, fd as usize, buf.as_ptr() as usize, buf.len()) }
}
fn sys_write_all(fd: i32, mut buf: &[u8]) -> Result<(), Errno> {
while !buf.is_empty() {
match sys_write(fd, buf) {
Ok(n) => buf = &buf[n..],
Err(Errno::EINTR) => continue,
Err(errno) => return Err(errno),
}
}
Ok(())
}
struct Stdio<const N: usize = 4096> {
fd: i32,
buf: [u8; N],
len: usize,
}
impl<const N: usize> Stdio<N> {
pub fn new(fd: i32) -> Self {
Self {
fd,
buf: [0; N],
len: 0,
}
}
pub fn write_all(&mut self, mut buf: &[u8]) -> Result<(), Errno> {
while !buf.is_empty() {
if self.buf[self.len..].is_empty() {
self.flush()?;
}
let remaining = &mut self.buf[self.len..];
let count = remaining.len().min(buf.len());
remaining[0..count].copy_from_slice(&buf[0..count]);
self.len += count;
buf = &buf[count..];
}
Ok(())
}
/// Flushes the buffered writes to the file descriptor.
pub fn flush(&mut self) -> Result<(), Errno> {
sys_write_all(self.fd, &self.buf[0..self.len])?;
self.len = 0;
Ok(())
}
}
impl<const N: usize> Drop for Stdio<N> {
fn drop(&mut self) {
let _ = self.flush();
}
}
impl fmt::Write for Stdio {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.write_all(s.as_bytes()).map_err(|_| fmt::Error)
}
}
fn _inner_print(fd: i32, args: fmt::Arguments<'_>, newline: bool) -> fmt::Result {
let mut f = Stdio::new(fd);
f.write_fmt(args)?;
if newline {
f.write_str("\n")?;
}
Ok(())
}
#[doc(hidden)]
pub fn _print(fd: i32, args: fmt::Arguments<'_>, newline: bool) {
// Ignore the error.
let _ = _inner_print(fd, args, newline);
}
#[macro_export]
macro_rules! print {
($($arg:tt)*) => ($crate::_print(1, ::core::format_args!($($arg)*), false));
}
#[macro_export]
macro_rules! eprint {
($($arg:tt)*) => ($crate::_print(2, ::core::format_args!($($arg)*), false));
}
#[macro_export]
macro_rules! println {
() => ($crate::print!("\n"));
($($arg:tt)*) => ({
// Purposefully avoiding format_args_nl because it requires a nightly
// feature.
$crate::_print(1, ::core::format_args!($($arg)*), true);
})
}
#[macro_export]
macro_rules! eprintln {
() => ($crate::eprint!("\n"));
($($arg:tt)*) => ({
// Purposefully avoiding format_args_nl because it requires a nightly
// feature.
$crate::_print(2, ::core::format_args!($($arg)*), true);
})
}

View file

@ -1,18 +0,0 @@
# @generated by autocargo from //hermetic_infra/reverie/experimental:reverie-host
[package]
name = "reverie-host"
version = "0.1.0"
authors = ["Meta Platforms"]
edition = "2021"
repository = "https://github.com/facebookexperimental/reverie"
license = "BSD-2-Clause"
[dependencies]
anyhow = "1.0.75"
dirs = "2.0"
reverie-process = { version = "0.1.0", path = "../../reverie-process" }
reverie-rpc = { version = "0.1.0", path = "../reverie-rpc" }
serde = { version = "1.0.185", features = ["derive", "rc"] }
tempfile = "3.8"
tokio = { version = "1.37.0", features = ["full", "test-util", "tracing"] }

View file

@ -1,43 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
use core::marker::Unpin;
use std::io;
use serde::Deserialize;
use serde::Serialize;
use tokio::io::AsyncRead;
use tokio::io::AsyncReadExt;
use tokio::io::AsyncWrite;
use tokio::io::AsyncWriteExt;
pub async fn read<'a, T, S>(stream: &mut S, buf: &'a mut Vec<u8>) -> io::Result<T>
where
T: Deserialize<'a>,
S: AsyncRead + Unpin,
{
let len = stream.read_u32().await? as usize;
buf.resize(len, 0);
stream.read_exact(buf).await?;
reverie_rpc::decode_frame(buf)
}
pub async fn write<T, S>(stream: &mut S, buf: &mut Vec<u8>, item: T) -> io::Result<()>
where
T: Serialize,
S: AsyncWrite + Unpin,
{
buf.clear();
reverie_rpc::encode(&item, buf)?;
stream.write_all(buf).await
}

View file

@ -1,18 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
//! This crate does two main things:
//! * Handles launching the root child process.
//! * Provides an interface for managing global state for in-guest backends.
mod codec;
mod server;
mod tracer;
pub use server::*;
pub use tracer::*;

View file

@ -1,76 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
use std::path::Path;
use std::path::PathBuf;
use anyhow::Result;
use reverie_rpc::Service;
use tempfile::NamedTempFile;
use tokio::net::UnixListener;
use super::codec;
/// A global state server.
pub struct Server {
socket: NamedTempFile<UnixListener>,
}
impl Server {
/// Creates the server, but does not yet listen for incoming connections.
pub fn new() -> Result<Self> {
let sock_dir = dirs::runtime_dir().unwrap_or_else(|| PathBuf::from("/tmp"));
let prefix = format!("reverie-{}-", std::process::id());
let socket = tempfile::Builder::new()
.prefix(&prefix)
.suffix(".sock")
.make_in(sock_dir, |path| UnixListener::bind(path))?;
Ok(Self { socket })
}
/// Returns the path to the socket.
pub fn sock_path(&self) -> &Path {
self.socket.path()
}
/// Accepts new socket connections and processes them.
pub async fn serve<S>(&self, service: S) -> !
where
S: Service + Clone + Send + Sync + 'static,
{
loop {
match self.socket.as_file().accept().await {
Ok((mut stream, _addr)) => {
let service = service.clone();
tokio::spawn(async move {
let mut reader_buf = Vec::with_capacity(1024);
let mut writer_buf = Vec::with_capacity(1024);
while let Ok(request) = codec::read(&mut stream, &mut reader_buf).await {
if let Some(response) = service.call(request).await {
// Only send back a response if this request has
// an associated response. This lets us have
// "send-only" messages, which are useful for
// accumulating state.
codec::write(&mut stream, &mut writer_buf, response)
.await
.unwrap();
}
}
});
}
Err(e) => {
eprintln!("connection failed: {}", e);
}
}
}
}
}

View file

@ -1,153 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
use std::env;
use std::ffi::OsStr;
use std::io;
use std::path::PathBuf;
use anyhow::anyhow;
use anyhow::Context;
use anyhow::Result;
use reverie_process::Child;
use reverie_process::Command;
use reverie_rpc::Service;
use super::server::Server;
pub struct TracerBuilder<S> {
command: Command,
sabre: Option<PathBuf>,
plugin: Option<PathBuf>,
service: S,
}
impl TracerBuilder<()> {
pub fn new(command: Command) -> Self {
Self {
command,
sabre: None,
plugin: None,
service: (),
}
}
}
impl<S> TracerBuilder<S> {
/// Sets the path to the plugin's DSO. If this is not set, the
/// `SABRE_PLUGIN` environment variable is used instead.
pub fn plugin<P: Into<Option<PathBuf>>>(mut self, path: P) -> Self {
self.plugin = path.into();
self
}
/// Sets the path to the sabre binary. If this is not set, the
/// `SABRE_BINARY` environment variable is used instead.
pub fn sabre<P: Into<Option<PathBuf>>>(mut self, path: P) -> Self {
self.sabre = path.into();
self
}
/// Set the global state service. The service is started when the child
/// process is spawned.
pub fn global_state<T>(self, service: T) -> TracerBuilder<T> {
TracerBuilder {
command: self.command,
sabre: self.sabre,
plugin: self.plugin,
service,
}
}
/// Spawns the root guest process.
pub fn spawn(self) -> Result<Child>
where
S: Service + Clone + Send + Sync + 'static,
{
let sabre = self
.sabre
.or_else(|| std::env::var_os("SABRE_BINARY").map(PathBuf::from))
.map_or_else(find_sabre, |x| Ok(Some(x)))?;
let sabre = sabre.ok_or_else(|| anyhow!("Could not find sabre executable"))?;
let plugin = self
.plugin
.or_else(|| std::env::var_os("SABRE_PLUGIN").map(PathBuf::from))
.map_or_else(find_plugin, |x| Ok(Some(x)))?;
let plugin = plugin.ok_or_else(|| anyhow!("Could not sabre plugin"))?;
let mut command = into_sabre(self.command, sabre.as_ref(), plugin.as_ref())?;
let server = Server::new()?;
command.env("REVERIE_SOCK", server.sock_path());
let service = self.service;
tokio::spawn(async move { server.serve(service).await });
let child = command
.spawn()
.with_context(|| format!("Failed to spawn: {:?}", command.get_program()))?;
Ok(child)
}
}
fn into_sabre(mut command: Command, sabre: &OsStr, plugin: &OsStr) -> Result<Command> {
let program = command
.find_program()
.with_context(|| format!("Could not find program: {:?}", command.get_program()))?;
command.prepend_args([plugin, "--".as_ref(), program.as_ref()]);
// Change the program that we're launching. This also changes arg0 to match.
command.program(sabre);
// Ensure that SABRE_BINARY and SABRE_PLUGIN are not inherited by the child
// process.
command.env_remove("SABRE_BINARY");
command.env_remove("SABRE_PLUGIN");
Ok(command)
}
/// Tries to find the path to the `sabre` executable based on the path to the
/// current executablbe. This should be the case when using dotslash.
fn find_sabre() -> Result<Option<PathBuf>, io::Error> {
let mut path = env::current_exe()?;
path.pop();
path.push("sabre");
if path.is_file() {
Ok(Some(path))
} else {
Ok(None)
}
}
/// Tries to find the plugin based on the path to the current executable. This
/// should be the case when using dotslash.
fn find_plugin() -> Result<Option<PathBuf>, io::Error> {
let mut path = env::current_exe()?;
if let Some(exe_name) = path.file_name() {
let mut name = exe_name.to_os_string();
name.push("_plugin.so");
// Search for the plugin in the same directory.
path.set_file_name(name);
if path.is_file() {
return Ok(Some(path));
}
}
Ok(None)
}

View file

@ -1,21 +0,0 @@
# @generated by autocargo from //hermetic_infra/reverie/experimental:reverie-rpc-macros
[package]
name = "reverie-rpc-macros"
version = "0.1.0"
authors = ["Meta Platforms"]
edition = "2021"
repository = "https://github.com/facebookexperimental/reverie"
license = "BSD-2-Clause"
[lib]
test = false
doctest = false
proc-macro = true
[dependencies]
darling = "0.14.0"
heck = "0.3.1"
proc-macro2 = { version = "1.0.70", features = ["span-locations"] }
quote = "1.0.29"
syn = { version = "1.0.109", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] }

View file

@ -1,322 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
use proc_macro2::TokenStream;
use quote::quote;
use quote::ToTokens;
use quote::TokenStreamExt;
use syn::spanned::Spanned;
use crate::Method;
use crate::Service;
impl ToTokens for Service {
fn to_tokens(&self, tokens: &mut TokenStream) {
tokens.append_all(&[
self.expand_trait(),
self.expand_server(),
self.expand_request_enum(),
self.expand_response_enum(),
self.expand_client(),
]);
}
}
impl Service {
/// Expands the trait that the server needs to implement.
fn expand_trait(&self) -> TokenStream {
let attrs = &self.attrs;
let vis = &self.vis;
let ident = &self.ident;
let server_ident = &self.server_ident;
let methods = self.methods.iter().map(
|Method {
attrs,
ident,
args,
output,
..
}| {
quote! {
#( #attrs )*
async fn #ident(&self, #( #args ),*) #output;
}
},
);
quote! {
#( #attrs )*
#[::reverie_rpc::async_trait::async_trait]
#vis trait #ident: ::core::marker::Sync + Sized {
#( #methods )*
/// Returns a type that can be used to serve this service.
fn serve(self) -> #server_ident<Self> {
#server_ident { service: self }
}
}
}
}
/// Expands `struct ServeMyService`
/// Expands `impl<S> Service for ServeMyService<S>`
fn expand_server(&self) -> TokenStream {
let vis = &self.vis;
let ident = &self.ident;
let request_ident = &self.request_ident;
let response_ident = &self.response_ident;
let server_ident = &self.server_ident;
let service_requests = self.methods.iter().map(|method| {
let attrs = &method.attrs;
let ident = &method.ident;
let camel_ident = &method.camel_ident;
let arg_pats = method.args.iter().map(|pat_type| &pat_type.pat).collect::<Vec<_>>();
if method.method_attrs.no_response {
quote! {
#( #attrs )*
#[allow(unused_doc_comments)]
#request_ident::#camel_ident { #( #arg_pats ),* } => {
self.service.#ident(#( #arg_pats ),*).await;
None
},
}
} else {
quote! {
#( #attrs )*
#[allow(unused_doc_comments)]
#request_ident::#camel_ident { #( #arg_pats ),* } => {
Some(#response_ident::#camel_ident(self.service.#ident(#( #arg_pats ),*).await))
},
}
}
});
let request_lifetime: Option<syn::Generics> = self
.request_generics
.as_ref()
.map(|_| syn::parse_quote!(<'r>));
// FIXME: Avoid requiring `Send` if possible.
quote! {
/// A helper for serving the service.
#[derive(Clone)]
#vis struct #server_ident<S> {
service: S,
}
impl<S> #server_ident<S> {
pub fn into_inner(self) -> S {
self.service
}
}
impl<S> core::ops::Deref for #server_ident<S> {
type Target = S;
fn deref(&self) -> &Self::Target {
&self.service
}
}
impl<S> core::ops::DerefMut for #server_ident<S> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.service
}
}
impl<S> ::reverie_rpc::Service for #server_ident<S>
where
S: #ident + Send,
{
type Request<'r> where S: 'r = #request_ident #request_lifetime;
type Response = #response_ident;
type Future<'a> where S: 'a = ::reverie_rpc::BoxFuture<'a, Option<Self::Response>>;
fn call<'a>(
&'a self,
req: Self::Request<'a>,
) -> Self::Future<'a> {
Box::pin(async move {
match req {
#( #service_requests )*
}
})
}
}
}
}
/// Expands the associated request type for each method's arguments.
fn expand_request_enum(&self) -> TokenStream {
let variants = self.methods.iter().map(|method| {
let attrs = &method.attrs;
let ident = &method.camel_ident;
let args = method.args.iter().cloned().map(|mut arg| {
// If we have an argument with a reference, change it to our
// 'req lifetime that is declared on the enum.
if let syn::Type::Reference(r) = arg.ty.as_mut() {
r.lifetime = Some(syn::Lifetime::new("'req", r.and_token.span()));
}
arg
});
quote! {
#( #attrs )*
#ident { #( #args ),* },
}
});
let vis = &self.vis;
let ident = &self.request_ident;
let generics = self.request_generics.as_ref();
quote! {
#[allow(missing_docs)]
#[derive(Debug)]
#[derive(::reverie_rpc::serde::Serialize, ::reverie_rpc::serde::Deserialize)]
#[serde(crate = "reverie_rpc::serde")]
#vis enum #ident #generics {
#( #variants )*
}
}
}
/// Expands the associated response type for each method's return type.
fn expand_response_enum(&self) -> TokenStream {
let variants = self.methods.iter().filter_map(|method| {
if method.method_attrs.no_response {
// Don't expand this variant if it's a send-only method.
return None;
}
let attrs = &method.attrs;
let ident = &method.camel_ident;
let output = match &method.output {
syn::ReturnType::Default => quote!(()),
syn::ReturnType::Type(_, ret) => quote!(#ret),
};
Some(quote! {
#( #attrs )*
#ident(#output),
})
});
let vis = &self.vis;
let ident = &self.response_ident;
quote! {
#[allow(missing_docs)]
#[derive(Debug)]
#[derive(::reverie_rpc::serde::Serialize, ::reverie_rpc::serde::Deserialize)]
#[serde(crate = "reverie_rpc::serde")]
#vis enum #ident {
#( #variants )*
}
}
}
fn expand_client(&self) -> TokenStream {
let vis = &self.vis;
let attrs = &self.attrs;
let client_ident = &self.client_ident;
let request = &self.request_ident;
let response = &self.response_ident;
let req_generics: Option<syn::Generics> = self.request_generics.as_ref().map(|_| {
// HACK: The lifetime parameter for the request type shouldn't need
// to force the service client type to also have a lifetime
// parameter because the request itself isn't stored in the service
// client, it's just sent through the channel.
//
// However, we need the channel to have these parameters so that
// `MakeClient` knows the type of the request/response. Thus, in
// order to ensure we aren't leaking the request type's lifetime
// parameter into the service client's generic parameters and
// complicating it's usage, we say that the request type has a
// static lifetime and transmute it just before sending it through
// the channel. This is terrible, but perfectly safe because we
// don't store the request before serializing it. Generic Associated
// Types (GATs) might help with this, but they don't support dyn
// traits which is useful for enabling nesting of channels (and thus
// composition of global state).
syn::parse_quote!(<'static>)
});
let methods = self.methods.iter().map(|method| {
let attrs = &method.attrs;
let ident = &method.ident;
let camel_ident = &method.camel_ident;
let args = &method.args;
let arg_pats = method.args.iter().map(|pat_type| &pat_type.pat);
let output = &method.output;
if method.method_attrs.no_response {
quote! {
#( #attrs )*
pub fn #ident(&self, #( #args ),*) {
use ::reverie_rpc::Channel;
// Transmute is safe because the channel doesn't store
// the request type.
let request: #request #req_generics = unsafe {
::core::mem::transmute(#request::#camel_ident { #( #arg_pats ),* })
};
self.channel.send(&request);
}
}
} else {
quote! {
#( #attrs )*
pub fn #ident(&self, #( #args ),*) #output {
use ::reverie_rpc::Channel;
// Transmute is safe because the channel doesn't store
// the request type.
let request: #request #req_generics = unsafe {
::core::mem::transmute(#request::#camel_ident { #( #arg_pats ),* })
};
match self.channel.call(&request) {
#response::#camel_ident(ret) => ret,
other => panic!("Got unexpected response: {:?}", other),
}
}
}
}
});
quote! {
#( #attrs )*
#vis struct #client_ident {
channel: ::reverie_rpc::BoxChannel<#request #req_generics, #response>,
}
impl ::reverie_rpc::MakeClient for #client_ident {
type Request = #request #req_generics;
type Response = #response;
fn make_client(channel: ::reverie_rpc::BoxChannel<Self::Request, Self::Response>) -> Self {
Self {
channel,
}
}
}
impl #client_ident {
#( #methods )*
}
}
}
}

View file

@ -1,56 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
extern crate proc_macro;
mod expand;
mod parse;
use proc_macro::TokenStream;
use quote::ToTokens;
struct Service {
attrs: Vec<syn::Attribute>,
vis: syn::Visibility,
ident: syn::Ident,
methods: Vec<Method>,
request_ident: syn::Ident,
request_generics: Option<syn::Generics>,
response_ident: syn::Ident,
server_ident: syn::Ident,
client_ident: syn::Ident,
}
#[derive(Default, Debug, darling::FromMeta)]
struct MethodAttrs {
/// True if `#[rpc(no_response)]` was specified.
#[darling(default)]
no_response: bool,
}
struct Method {
/// Attributes that should get expanded.
attrs: Vec<syn::Attribute>,
/// Attributes that only we care about (e.g., `#[rpc(no_response = true)]`)
method_attrs: MethodAttrs,
/// The method name.
ident: syn::Ident,
/// The camel-case version of the method name. Used for generating the
/// Request and Response enum variants.
camel_ident: syn::Ident,
// NOTE: We expect all methods to take &self implicitly.
args: Vec<syn::PatType>,
/// Return type of the method.
output: syn::ReturnType,
}
#[proc_macro_attribute]
pub fn service(_args: TokenStream, input: TokenStream) -> TokenStream {
let service = syn::parse_macro_input!(input as Service);
service.into_token_stream().into()
}

View file

@ -1,143 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
use darling::FromMeta;
use heck::CamelCase;
use quote::format_ident;
use syn::parse::Parse;
use syn::parse::ParseStream;
use syn::spanned::Spanned;
use crate::Method;
use crate::MethodAttrs;
use crate::Service;
fn parse_method(method: syn::TraitItemMethod, has_ref: &mut bool) -> syn::Result<Method> {
if !method.sig.generics.params.is_empty() {
return Err(syn::Error::new(
method.sig.generics.span(),
"RPC methods cannot have generic parameters",
));
}
let ident = method.sig.ident;
let camel_ident = syn::Ident::new(&ident.to_string().to_camel_case(), ident.span());
// Search through the attributes and find any with the `rpc`
// path. We need to exclude these from getting passed through
// and expanded.
let mut custom_attrs = None;
#[allow(clippy::unnecessary_filter_map)]
let attrs: Vec<_> = method
.attrs
.into_iter()
.filter_map(|attrs| {
// Clippy complains about this `filter_map` being
// equivalent to `filter`, but it's not because `attrs`
// needs to be passed by value to the closure so we can
// move it out.
if attrs.path.is_ident("rpc") {
custom_attrs = Some(attrs);
None
} else {
Some(attrs)
}
})
.collect();
let method_attrs = match custom_attrs {
Some(custom_attrs) => {
let meta = custom_attrs.parse_meta()?;
MethodAttrs::from_meta(&meta)?
}
None => MethodAttrs::default(),
};
if method_attrs.no_response {
if method.sig.output != syn::ReturnType::Default {
return Err(syn::Error::new(
method.sig.output.span(),
"#[rpc(no_response)] methods cannot have a return type",
));
}
}
let args = method
.sig
.inputs
.iter()
.filter_map(|arg| match arg {
syn::FnArg::Receiver(_) => None,
syn::FnArg::Typed(t) => {
if let syn::Type::Reference(_) = t.ty.as_ref() {
*has_ref = true;
}
Some(t.clone())
}
})
.collect();
Ok(Method {
attrs,
method_attrs,
ident,
camel_ident,
args,
output: method.sig.output,
})
}
impl Parse for Service {
fn parse(input: ParseStream) -> syn::Result<Self> {
let t: syn::ItemTrait = input.parse()?;
let attrs = t.attrs;
let vis = t.vis;
let ident = t.ident;
let mut has_ref = false;
let mut methods = Vec::new();
for inner in t.items {
if let syn::TraitItem::Method(method) = inner {
if method.sig.ident == "serve" {
return Err(syn::Error::new(
ident.span(),
format!("method conflicts with generated fn {}::serve", ident),
));
}
methods.push(parse_method(method, &mut has_ref)?);
}
}
let request_ident = format_ident!("{}Request", ident, span = ident.span());
let response_ident = format_ident!("{}Response", ident, span = ident.span());
let server_ident = format_ident!("Serve{}", ident, span = ident.span());
let client_ident = format_ident!("{}Client", ident, span = ident.span());
let request_generics = if has_ref {
Some(syn::parse_quote!(<'req>))
} else {
None
};
Ok(Self {
attrs,
vis,
ident,
methods,
request_ident,
request_generics,
response_ident,
server_ident,
client_ident,
})
}
}

View file

@ -1,15 +0,0 @@
# @generated by autocargo from //hermetic_infra/reverie/experimental:reverie-rpc
[package]
name = "reverie-rpc"
version = "0.1.0"
authors = ["Meta Platforms"]
edition = "2021"
repository = "https://github.com/facebookexperimental/reverie"
license = "BSD-2-Clause"
[dependencies]
async-trait = "0.1.71"
bincode = "1.3.3"
reverie-rpc-macros = { version = "0.1.0", path = "../reverie-rpc-macros" }
serde = { version = "1.0.185", features = ["derive", "rc"] }

View file

@ -1,57 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
use serde::Deserialize;
use serde::Serialize;
/// Represents a bidirectional stream of messages.
pub trait Channel<Req, Res>
where
Req: Serialize,
Res: for<'a> Deserialize<'a>,
{
/// Sends a request, but does not expect a response. This is useful when
/// some requests don't have an associated response.
fn send(&self, item: &Req);
/// Sends a request and waits for a response from the server.
fn call(&self, item: &Req) -> Res;
}
pub type BoxChannel<Req, Res> = Box<dyn Channel<Req, Res> + Send + Sync + 'static>;
pub trait MakeClient {
type Request: Serialize;
type Response: for<'a> Deserialize<'a>;
fn make_client(channel: BoxChannel<Self::Request, Self::Response>) -> Self;
}
// Dummy impl for (), so we can easily use this for tools that don't use global
// state.
impl MakeClient for () {
type Request = ();
type Response = ();
fn make_client(_channel: BoxChannel<Self::Request, Self::Response>) -> Self {}
}
impl<T, Req, Res> Channel<Req, Res> for Box<T>
where
T: Channel<Req, Res> + ?Sized,
Req: Serialize,
Res: for<'a> Deserialize<'a>,
{
fn send(&self, item: &Req) {
self.as_ref().send(item)
}
fn call(&self, item: &Req) -> Res {
self.as_ref().call(item)
}
}

View file

@ -1,86 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
use std::io;
use std::io::Write;
use bincode::Options;
use serde::Deserialize;
use serde::Serialize;
fn bincode_options() -> impl bincode::Options {
// NOTE: Both the server and client must agree on these bincode options.
// Otherwise, we'll get deserialization errors.
bincode::DefaultOptions::new().with_limit(16 * (1 << 20) /* 16MB */)
}
pub fn encode<T>(item: &T, buf: &mut Vec<u8>) -> io::Result<()>
where
T: Serialize,
{
let mut cursor = io::Cursor::new(buf);
// Reserve 4 bytes at the beginning of the buffer so we can fill it in
// with the length of the payload once we know what it is.
cursor.write_all(&[0, 0, 0, 0])?;
// Serialize into our buffer.
encode_frame_into(&mut cursor, item)?;
let buf = cursor.into_inner();
// Fill in the actual size now that we know what it is.
let size = buf[4..].len() as u32;
buf[0..4].copy_from_slice(&size.to_be_bytes());
Ok(())
}
/// Encodes a length-delimited frame.
pub fn encode_frame_into<W, T>(writer: W, item: &T) -> io::Result<()>
where
T: Serialize + ?Sized,
W: Write,
{
bincode_options().serialize_into(writer, item).map_err(|e| {
io::Error::new(
io::ErrorKind::Other,
format!("failed to encode frame: {}", e),
)
})
}
/// Decodes a length-delimited frame.
pub fn decode_frame<'a, T>(frame: &'a [u8]) -> io::Result<T>
where
T: Deserialize<'a>,
{
bincode_options().deserialize(frame).map_err(|e| {
io::Error::new(
io::ErrorKind::Other,
format!("failed to decode frame: {}", e),
)
})
}
pub fn decode_from<'a, T, R>(mut reader: R, buf: &'a mut Vec<u8>) -> io::Result<T>
where
T: Deserialize<'a>,
R: io::Read,
{
let mut head = [0u8; 4];
reader.read_exact(&mut head)?;
let len = u32::from_be_bytes(head) as usize;
buf.resize(len, 0);
reader.read_exact(buf)?;
decode_frame(buf)
}

View file

@ -1,28 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
//! This crate provides the protocol that is to be used when communicating with
//! global state. This crate is meant to be shared between the guest and host
//! processes.
//!
//! The RPC protocol is simply a mapping between a Request and Response. That
//! is, for each item in the Request enum, there is a corresponding item in the
//! Response enum.
mod channel;
mod codec;
mod service;
#[doc(hidden)]
pub use async_trait;
pub use channel::*;
pub use codec::*;
pub use reverie_rpc_macros::service;
#[doc(hidden)]
pub use serde;
pub use service::*;

View file

@ -1,54 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
use core::future::Future;
use core::pin::Pin;
use std::sync::Arc;
use serde::Deserialize;
use serde::Serialize;
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
pub trait Service {
type Request<'r>: Deserialize<'r> + Unpin + Send
where
Self: 'r;
type Response: Serialize + Send + Unpin;
type Future<'a>: Future<Output = Option<Self::Response>> + Send + 'a
where
Self: 'a;
/// Makes a "call" to our service. Returns `None` if the service has no
/// response for the client (i.e., it was a send-only request).
fn call<'a>(&'a self, req: Self::Request<'a>) -> Self::Future<'a>;
}
impl<S> Service for Arc<S>
where
S: Service,
{
type Request<'r> = S::Request<'r> where S: 'r;
type Response = S::Response;
type Future<'a>
= S::Future<'a> where S: 'a;
fn call<'a>(&'a self, req: Self::Request<'a>) -> Self::Future<'a> {
self.as_ref().call(req)
}
}
impl Service for () {
type Request<'r> = ();
type Response = ();
type Future<'a> = core::future::Ready<Option<()>>;
fn call<'a>(&'a self, _req: ()) -> Self::Future<'a> {
core::future::ready(Some(()))
}
}

View file

@ -1,21 +0,0 @@
# @generated by autocargo from //hermetic_infra/reverie/experimental:reverie-sabre-macros
[package]
name = "reverie-sabre-macros"
version = "0.1.0"
authors = ["Meta Platforms"]
edition = "2021"
repository = "https://github.com/facebookexperimental/reverie"
license = "BSD-2-Clause"
[lib]
test = false
doctest = false
proc-macro = true
[dependencies]
darling = "0.14.0"
heck = "0.3.1"
proc-macro2 = { version = "1.0.70", features = ["span-locations"] }
quote = "1.0.29"
syn = { version = "1.0.109", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] }

View file

@ -1,155 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
use proc_macro2::TokenStream;
use quote::quote;
use quote::ToTokens;
use crate::Tool;
impl ToTokens for Tool {
fn to_tokens(&self, tokens: &mut TokenStream) {
let mut item_impl = self.outer_item.clone();
let ty = &item_impl.self_ty;
// Implement ToolGlobal so that we can access it from any of the
// callbacks specified by sbr_init.
tokens.extend(quote! {
impl ::reverie_sabre::ToolGlobal for #ty {
type Target = #ty;
fn global() -> &'static Self::Target {
use ::reverie_sabre::internal::OnceCell;
static __TOOL_INSTANCE: OnceCell<#ty> = OnceCell::new();
__TOOL_INSTANCE.get_or_init(::reverie_sabre::internal::init_tool)
}
}
});
let mut fn_icept_structs = quote! {};
let mut type_impl = quote! {};
// Traversing "detoured" methods declarations and preparing the plumbing
// to hook up to sabre we need a callback, a field to hold a pointer to
// original function, a stub pointer, DETOURS strucure, etc
for method in &self.detoured_methods {
let field_ident = &method.undetoured_field_name;
let callback_ident = &method.callback_name;
let stub_ident = &method.stub_name;
let undetoured_method_name = &method.undetoured_method_name;
let args = &method.outer_item.sig.inputs;
let arg_pats = args
.iter()
.filter_map(|pat_type| match pat_type {
syn::FnArg::Typed(pat) => Some(&pat.pat),
_ => None,
})
.collect::<Vec<_>>();
let output = &method.outer_item.sig.output;
let function_type_name = &method.detoured_function_type_name;
let original_marked_method = &method.outer_item;
let detoured_definition_name = &method.detoured_definition_name;
let lib_name_c_str = syn::LitStr::new(
format!("{0}\0", method.attrs.lib).as_str(),
proc_macro2::Span::call_site(),
);
let func_name_c_str = syn::LitStr::new(
format!("{0}\0", method.attrs.func).as_str(),
proc_macro2::Span::call_site(),
);
fn_icept_structs.extend(quote! {
sabre::ffi::fn_icept {
lib_name: #lib_name_c_str.as_ptr() as *const i8,
fn_name: #func_name_c_str.as_ptr() as *const i8,
icept_callback: #callback_ident,
},
});
type_impl.extend(quote! {
fn #undetoured_method_name(#args) #output {
unsafe {
if let Some(f) = #field_ident {
return f(#(#arg_pats),*);
}
panic!("original function wasn't captured");
}
}
#original_marked_method
});
tokens.extend(quote! {
type #function_type_name = fn(#args) #output;
static mut #field_ident: Option<#function_type_name> = None;
unsafe extern "C" fn #stub_ident(#args) #output {
#ty::#detoured_definition_name(#(#arg_pats),*)
}
extern "C" fn #callback_ident(func: sabre::ffi::void_void_fn) -> sabre::ffi::void_void_fn {
unsafe {
#field_ident = Some(std::mem::transmute(func));
std::mem::transmute(#stub_ident as *const())
}
}
});
}
tokens.extend(quote! {
impl #ty {
#type_impl
}
});
item_impl.items.push(syn::ImplItem::Method(
syn::parse2(quote! {
fn detours() -> &'static [sabre::ffi::fn_icept] {
static DETOURS: &[sabre::ffi::fn_icept] = &[
#fn_icept_structs
];
DETOURS
}
})
.unwrap(),
));
// Expand the original `impl Tool for MyTool` block.
item_impl.to_tokens(tokens);
// Implement the entry point for our plugin.
tokens.extend(quote! {
#[no_mangle]
pub extern "C" fn sbr_init(
argc: *mut i32,
argv: *mut *mut *mut libc::c_char,
fn_icept_reg: sabre::ffi::icept_reg_fn,
vdso_callback: *mut Option<sabre::ffi::handle_vdso_fn>,
syscall_handler: *mut Option<sabre::ffi::handle_syscall_fn>,
rdtsc_handler: *mut Option<sabre::ffi::handle_rdtsc_fn>,
post_load: *mut Option<sabre::ffi::post_load_fn>,
sabre_path: *const libc::c_char,
plugin_path: *const libc::c_char,
) {
::reverie_sabre::internal::sbr_init::<#ty>(
argc,
argv,
fn_icept_reg,
vdso_callback,
syscall_handler,
rdtsc_handler,
post_load,
sabre_path,
plugin_path,
)
}
});
}
}

View file

@ -1,43 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
extern crate proc_macro;
mod expand;
mod parse;
use proc_macro::TokenStream;
use quote::ToTokens;
struct Tool {
outer_item: syn::ItemImpl,
detoured_methods: Vec<Detour>,
}
#[derive(Default, Debug, darling::FromMeta)]
struct DetourAttrs {
func: String,
lib: String,
}
struct Detour {
callback_name: syn::Ident,
stub_name: syn::Ident,
undetoured_field_name: syn::Ident,
undetoured_method_name: syn::Ident,
detoured_definition_name: syn::Ident,
detoured_function_type_name: syn::Ident,
attrs: DetourAttrs,
outer_item: syn::ImplItemMethod,
}
#[proc_macro_attribute]
pub fn tool(_args: TokenStream, input: TokenStream) -> TokenStream {
let service = syn::parse_macro_input!(input as Tool);
service.into_token_stream().into()
}

View file

@ -1,85 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
use darling::FromMeta;
use heck::CamelCase;
use quote::format_ident;
use syn::parse::Parse;
use syn::parse::ParseStream;
use crate::Detour;
use crate::DetourAttrs;
use crate::Tool;
fn parse_method(method: &syn::ImplItemMethod, attribute: &syn::Attribute) -> syn::Result<Detour> {
let meta = attribute.parse_meta()?;
let method_attrs = DetourAttrs::from_meta(&meta)?;
let mut outer_item = method.clone();
outer_item.attrs.retain(|a| !a.path.is_ident("detour"));
Ok(Detour {
outer_item,
callback_name: format_ident!("{}_{}_callback", method_attrs.lib, method_attrs.func),
stub_name: format_ident!("{}_{}_stub", method_attrs.lib, method_attrs.func),
detoured_definition_name: format_ident!("{}", method.sig.ident.to_string()),
undetoured_field_name: format_ident!(
"{}_{}_UNDETOURED",
method_attrs.lib.clone().to_uppercase(),
method_attrs.func.clone().to_uppercase()
),
undetoured_method_name: format_ident!(
"{}_{}_undetoured",
method_attrs.lib,
method_attrs.func
),
detoured_function_type_name: syn::Ident::new(
format!(
"{}{}Func",
method_attrs.lib.clone().to_uppercase(),
method_attrs.func.clone().to_uppercase()
)
.as_str()
.to_camel_case()
.as_str(),
proc_macro2::Span::call_site(),
),
attrs: method_attrs,
})
}
fn filter_map_attribute<'a>(
method: &'a syn::ImplItemMethod,
attribute: &str,
) -> Option<&'a syn::Attribute> {
method.attrs.iter().find(|a| a.path.is_ident(attribute))
}
impl Parse for Tool {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut impl_node: syn::ItemImpl = input.parse()?;
let detour_methods: Result<Vec<_>, _> = impl_node
.items
.iter()
.filter_map(|n| match n {
syn::ImplItem::Method(method_impl) => filter_map_attribute(method_impl, "detour")
.map(|a| parse_method(method_impl, a)),
_ => None,
})
.collect();
impl_node.items.retain(|m| match m {
syn::ImplItem::Method(method) => filter_map_attribute(method, "detour").is_none(),
_ => true,
});
Ok(Self {
outer_item: impl_node,
detoured_methods: detour_methods?,
})
}
}

View file

@ -1,25 +0,0 @@
# @generated by autocargo from //hermetic_infra/reverie/experimental:reverie-sabre
[package]
name = "reverie-sabre"
version = "0.1.0"
authors = ["Meta Platforms"]
edition = "2021"
repository = "https://github.com/facebookexperimental/reverie"
license = "BSD-2-Clause"
[dependencies]
array-macro = "1.0.5"
atomic = "0.5.1"
heapless = "0.8.0"
lazy_static = "1.4"
libc = "0.2.139"
mimalloc = { version = "0.1", default-features = false }
nostd-print = { version = "0.1.0", path = "../nostd-print" }
once_cell = "1.12"
parking_lot = { version = "0.12.1", features = ["send_guard"] }
reverie-rpc = { version = "0.1.0", path = "../reverie-rpc" }
reverie-sabre-macros = { version = "0.1.0", path = "../reverie-sabre-macros" }
reverie-syscalls = { version = "0.1.0", path = "../../reverie-syscalls" }
serde = { version = "1.0.185", features = ["derive", "rc"] }
syscalls = { version = "0.6.7", features = ["serde"] }

View file

@ -1,282 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
use reverie_syscalls::LocalMemory;
use reverie_syscalls::Syscall;
use syscalls::syscall;
use syscalls::SyscallArgs;
use syscalls::Sysno;
use super::ffi;
use super::thread;
use super::thread::GuestTransitionErr;
use super::thread::PidTid;
use super::thread::Thread;
use super::tool::Tool;
use super::tool::ToolGlobal;
use super::utils;
use super::vdso;
use crate::signal::guard;
pub const CONTROLLED_EXIT_SIGNAL: libc::c_int = libc::SIGSTKFLT;
/// Implement the thread notifier trait for any global tools
impl<T> thread::EventSink for T
where
T: ToolGlobal,
{
#[inline]
fn on_new_thread(pid_tid: PidTid) {
T::global().on_thread_start(pid_tid.tid);
}
fn on_thread_exit(pid_tid: PidTid) {
T::global().on_thread_exit(pid_tid.tid);
}
}
pub extern "C" fn handle_syscall<T: ToolGlobal>(
syscall: isize,
arg1: usize,
arg2: usize,
arg3: usize,
arg4: usize,
arg5: usize,
arg6: usize,
wrapper_sp: *mut ffi::syscall_stackframe,
) -> usize {
let mut thread = if let Some(thread) = Thread::<T>::current() {
thread
} else {
terminate(1);
};
match handle_syscall_with_thread::<T>(
&mut thread,
syscall,
arg1,
arg2,
arg3,
arg4,
arg5,
arg6,
wrapper_sp,
) {
Ok(return_code) => return_code,
Err(GuestTransitionErr::ExitNow) => terminate(0),
Err(GuestTransitionErr::ExitingElsewhere) => 0,
}
}
/// Handle the critical section for the given system call on the given thread
#[allow(clippy::if_same_then_else)]
fn handle_syscall_with_thread<T: ToolGlobal>(
thread: &mut Thread<T>,
syscall: isize,
arg1: usize,
arg2: usize,
arg3: usize,
arg4: usize,
arg5: usize,
arg6: usize,
wrapper_sp: *mut ffi::syscall_stackframe,
) -> Result<usize, GuestTransitionErr> {
let _guard = guard::enter_signal_exclusion_zone();
thread.leave_guest_execution()?;
let sys_no = Sysno::from(syscall as i32);
let result = if sys_no == Sysno::clone && arg2 != 0 {
thread.maybe_fork_as_guest(|| unsafe {
ffi::clone_syscall(
arg1,
arg2 as *mut libc::c_void,
arg3 as *mut i32,
arg4 as *mut i32,
arg5,
(*wrapper_sp).ret,
)
})?
} else if sys_no == Sysno::clone {
thread.maybe_fork_as_guest(|| {
let args = SyscallArgs::new(arg1, arg2, arg3, arg4, arg5, arg6);
let syscall = Syscall::from_raw(sys_no, args);
T::global()
.syscall(syscall, &LocalMemory::new())
.map_or_else(|e| -e.into_raw() as usize, |x| x as usize)
})?
} else if utils::is_vfork(sys_no, arg1) {
thread.maybe_fork_as_guest(|| unsafe {
let pid = ffi::vfork_syscall();
if pid == 0 {
// Child
// Even though this function doesn't return, this is
// safe because the thread is in `Guest` and that state
// will be correct in the child application when the
// jmp takes it there
ffi::vfork_return_from_child(wrapper_sp)
} else {
// parent
pid
}
})?
} else if sys_no == Sysno::clone3 {
let cl_args = unsafe { &*(arg1 as *const ffi::clone_args) };
if cl_args.stack == 0 {
thread.maybe_fork_as_guest(|| unsafe {
syscall!(sys_no, arg1, arg2, arg3, arg4, arg5, arg6)
.map_or_else(|e| -e.into_raw() as usize, |x| x as usize)
})?
} else {
thread.maybe_fork_as_guest(|| unsafe {
ffi::clone3_syscall(arg1, arg2, arg3, 0, arg5, (*wrapper_sp).ret)
})?
}
} else if sys_no == Sysno::exit {
// intercept the exit_group syscall and signal all the threads to exit
// in a predictable and trackable way
if thread.try_exit() {
terminate(arg1);
}
0
} else if sys_no == Sysno::exit_group {
// intercept the exit_group syscall and signal all the threads to exit
// in a predictable and trackable way
exit_group_with_thread(thread, arg1)
} else {
let args = SyscallArgs::new(arg1, arg2, arg3, arg4, arg5, arg6);
let syscall = Syscall::from_raw(sys_no, args);
thread.execute_as_guest(|| {
T::global()
.syscall(syscall, &LocalMemory::new())
.map_or_else(|e| -e.into_raw() as usize, |x| x as usize)
})?
};
thread.enter_guest_execution()?;
Ok(result)
}
/// Terminate this thread with no notifications
fn terminate(exit_code: usize) -> ! {
unsafe {
syscalls::syscall1(Sysno::exit, exit_code).expect("Exit should succeed");
}
unreachable!("The thread should have ended by now");
}
/// Perform and exit group with the current thread
fn exit_group_with_thread<T: ToolGlobal>(thread: &mut Thread<T>, exit_code: usize) -> usize {
thread.try_exit();
if let Some(exiting_pid) = thread::exit_all(|_, process_and_thread_id| unsafe {
syscalls::syscall3(
Sysno::tgkill,
process_and_thread_id.pid as usize,
process_and_thread_id.tid as usize,
CONTROLLED_EXIT_SIGNAL as usize,
)
.expect("Signaling thread failed");
}) {
if !thread::wait_for_all_to_exit(exiting_pid, T::global().get_exit_timeout()) {
T::global().on_exit_timeout()
} else {
terminate(exit_code)
}
} else {
0
}
}
pub fn exit_group<T: ToolGlobal>(exit_code: usize) -> usize {
if let Some(mut thread) = Thread::<T>::current() {
exit_group_with_thread(&mut thread, exit_code)
} else {
0
}
}
/// If any thread receives the exit signal call, this handler will gracefully
/// exit that thread
pub extern "C" fn handle_exit_signal<T: ToolGlobal>(
_: libc::c_int,
_: *const libc::siginfo_t,
_: *const libc::c_void,
) {
let mut thread = if let Some(thread) = Thread::<T>::current() {
thread
} else {
terminate(0);
};
if thread.try_exit() {
terminate(0);
}
}
extern "C" fn handle_vdso_clock_gettime<T: ToolGlobal>(
clockid: libc::clockid_t,
tp: *mut libc::timespec,
) -> i32 {
T::global().vdso_clock_gettime(clockid, tp)
}
extern "C" fn handle_vdso_getcpu<T: ToolGlobal>(
cpu: *mut u32,
node: *mut u32,
_unused: usize,
) -> i32 {
T::global().vdso_getcpu(cpu, node, _unused)
}
extern "C" fn handle_vdso_gettimeofday<T: ToolGlobal>(
tv: *mut libc::timeval,
tz: *mut libc::timezone,
) -> i32 {
T::global().vdso_gettimeofday(tv, tz)
}
extern "C" fn handle_vdso_time<T: ToolGlobal>(tloc: *mut libc::time_t) -> i32 {
T::global().vdso_time(tloc)
}
pub extern "C" fn handle_vdso<T: ToolGlobal>(
syscall: isize,
actual_fn: ffi::void_void_fn,
) -> Option<ffi::void_void_fn> {
use core::mem::transmute;
unsafe {
match Sysno::from(syscall as i32) {
Sysno::clock_gettime => {
vdso::clock_gettime = transmute(actual_fn as *const ());
transmute(handle_vdso_clock_gettime::<T> as *const ())
}
Sysno::getcpu => {
vdso::getcpu = transmute(actual_fn as *const ());
transmute(handle_vdso_getcpu::<T> as *const ())
}
Sysno::gettimeofday => {
vdso::gettimeofday = transmute(actual_fn as *const ());
transmute(handle_vdso_gettimeofday::<T> as *const ())
}
Sysno::time => {
vdso::time = transmute(actual_fn as *const ());
transmute(handle_vdso_time::<T> as *const ())
}
_ => None,
}
}
}
pub extern "C" fn handle_rdtsc<T: ToolGlobal>() -> u64 {
T::global().rdtsc()
}

View file

@ -1,292 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
use syscalls::Sysno;
use super::syscall_stackframe;
extern "C" {
// ffi_returns_twice is required here due to a miscompilation bug in release
// mode. Otherwise, stack variables of the parent can get corrupted due to
// compiler optimizations. Because of this, vfork *must* be implemented in
// raw assembly. It can't be safely implemented in Rust inline asm. For more
// information, see: https://github.com/rust-lang/libc/issues/1596
pub fn vfork_syscall() -> usize;
}
pub unsafe fn clone_syscall(
clone_flags: usize, // rdi
child_stack: *mut libc::c_void, // rsi
parent_tidptr: *mut i32, // rdx
child_tidptr: *mut i32, // rcx
tls: usize, // r8
ret_addr: *const libc::c_void, // r9
) -> usize {
let mut ret: usize = Sysno::clone as usize;
core::arch::asm! {
"syscall",
// Both child and parent return here.
"test rax, rax",
"jnz 1f",
// Child
"push rdi",
"push rsi",
"push rdx",
"push r10", // rcx
"push r8",
"push r9",
"call qword ptr [rip + exit_plugin@GOTPCREL]",
"pop r9",
"pop r8",
"pop r10",
"pop rdx",
"pop rsi",
"pop rdi",
// The child always returns 0
"mov rax, 0",
// Add redzone to our stack because jumping back to the trampoline
// removes it.
"sub rsp, 0x80",
// Jump back to our trampoline.
"jmp r9",
// Parent
"1:",
inlateout("rax") ret,
in("rdi") clone_flags,
in("rsi") child_stack,
in("rdx") parent_tidptr,
in("r10") child_tidptr,
in("r8") tls,
in("r9") ret_addr,
// syscall instructions clobber rcx and r11
lateout("rcx") _,
lateout("r11") _,
}
ret
}
pub unsafe fn clone3_syscall(
arg1: usize, // rdi
arg2: usize, // rsi
arg3: usize, // rdx
unused: usize, // rcx
arg5: usize, // r8
ret_addr: *mut libc::c_void, // r9
) -> usize {
let mut ret: usize = Sysno::clone3 as usize;
core::arch::asm! {
"syscall",
// Both child and parent return here.
"test rax, rax",
"jnz 1f",
// Child
"push rdi",
"push rsi",
"push rdx",
"push r8",
"push r9",
"call qword ptr [rip + exit_plugin@GOTPCREL]",
"pop r9",
"pop r8",
"pop rdx",
"pop rsi",
"pop rdi",
// The child always returns 0
"mov rax, 0",
// Add redzone to our stack because jumping back to the trampoline
// removes it.
"sub rsp, 0x80",
// Jump back to our trampoline.
"jmp r9",
// Parent
"1:",
inlateout("rax") ret,
in("rdi") arg1,
in("rsi") arg2,
in("rdx") arg3,
in("r10") unused,
in("r8") arg5,
in("r9") ret_addr,
// syscall instructions clobber rcx and r11
lateout("rcx") _,
lateout("r11") _,
}
ret
}
/// This restores the stack frame pointer, restores the registers from when the
/// syscall was first intercepted, and finally jumps back to the next
/// instruction after the syscall.
///
/// This function never actually returns from the perspective of the caller.
pub unsafe extern "C" fn vfork_return_from_child(wrapper_sp: *const syscall_stackframe) -> ! {
super::exit_plugin();
core::arch::asm! {
// Load registers from the syscall_stackframe struct. These are all
// offsets into the struct.
//
// FIXME: Don't hard code these struct field offsets.
"mov r15, qword ptr [rdi + 0x8]",
"mov r14, qword ptr [rdi + 0x10]",
"mov r13, qword ptr [rdi + 0x18]",
"mov r12, qword ptr [rdi + 0x20]",
"mov r11, qword ptr [rdi + 0x28]",
"mov r10, qword ptr [rdi + 0x30]",
"mov r9, qword ptr [rdi + 0x38]",
"mov r8, qword ptr [rdi + 0x40]",
// Skip rdi because we are reading it for the pointer offset.
"mov rsi, qword ptr [rdi + 0x50]",
"mov rdx, qword ptr [rdi + 0x58]",
"mov rcx, qword ptr [rdi + 0x60]",
"mov rbx, qword ptr [rdi + 0x68]",
"mov rbp, qword ptr [rdi + 0x70]",
// Its safe to clobber r11 to load *ret.
"mov r11, qword ptr [rdi + 0x80]",
// Finally, set rdi.
"mov rdi, qword ptr [rdi + 0x48]",
// The child always returns 0.
"mov rax, 0",
"sub rsp, 0x80",
// Jump back to the client.
"jmp r11",
in("rdi") wrapper_sp,
options(noreturn),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vfork() {
// The libc::vfork function has miscompilation problems. See
// https://github.com/rust-lang/libc/issues/1596
//
// Test to see if our own vfork has the same issue or not.
use core::hint::black_box;
use core::ptr::read_volatile;
unsafe {
let a0 = read_volatile(&1234);
let a1 = read_volatile(&1234);
let a2 = read_volatile(&1234);
let a3 = read_volatile(&1234);
let a4 = read_volatile(&1234);
let a5 = read_volatile(&1234);
let a6 = read_volatile(&1234);
let a7 = read_volatile(&1234);
let a8 = read_volatile(&1234);
let a9 = read_volatile(&1234);
let a10 = read_volatile(&1234);
let a11 = read_volatile(&1234);
let a12 = read_volatile(&1234);
let a13 = read_volatile(&1234);
let a14 = read_volatile(&1234);
let a15 = read_volatile(&1234);
let a16 = read_volatile(&1234);
let a17 = read_volatile(&1234);
let a18 = read_volatile(&1234);
let a19 = read_volatile(&1234);
if vfork_syscall() == 0 {
let b0 = read_volatile(&5678);
let b1 = read_volatile(&5678);
let b2 = read_volatile(&5678);
let b3 = read_volatile(&5678);
let b4 = read_volatile(&5678);
let b5 = read_volatile(&5678);
let b6 = read_volatile(&5678);
let b7 = read_volatile(&5678);
let b8 = read_volatile(&5678);
let b9 = read_volatile(&5678);
let b10 = read_volatile(&5678);
let b11 = read_volatile(&5678);
let b12 = read_volatile(&5678);
let b13 = read_volatile(&5678);
let b14 = read_volatile(&5678);
let b15 = read_volatile(&5678);
let b16 = read_volatile(&5678);
let b17 = read_volatile(&5678);
let b18 = read_volatile(&5678);
let b19 = read_volatile(&5678);
black_box(b0);
black_box(b1);
black_box(b2);
black_box(b3);
black_box(b4);
black_box(b5);
black_box(b6);
black_box(b7);
black_box(b8);
black_box(b9);
black_box(b10);
black_box(b11);
black_box(b12);
black_box(b13);
black_box(b14);
black_box(b15);
black_box(b16);
black_box(b17);
black_box(b18);
black_box(b19);
// When the vforked child exits, the parent can resume.
libc::_exit(0);
}
// None of the items pushed onto the child stack should have leaked into the
// parent stack.
assert_eq!(a0, 1234);
assert_eq!(a1, 1234);
assert_eq!(a2, 1234);
assert_eq!(a3, 1234);
assert_eq!(a4, 1234);
assert_eq!(a5, 1234);
assert_eq!(a6, 1234);
assert_eq!(a7, 1234);
assert_eq!(a8, 1234);
assert_eq!(a9, 1234);
assert_eq!(a10, 1234);
assert_eq!(a11, 1234);
assert_eq!(a12, 1234);
assert_eq!(a13, 1234);
assert_eq!(a14, 1234);
assert_eq!(a15, 1234);
assert_eq!(a16, 1234);
assert_eq!(a17, 1234);
assert_eq!(a18, 1234);
assert_eq!(a19, 1234);
}
}
}

View file

@ -1,139 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
#![allow(non_camel_case_types)]
mod clone;
pub use clone::clone3_syscall;
pub use clone::clone_syscall;
pub use clone::vfork_return_from_child;
pub use clone::vfork_syscall;
extern "C" {
pub fn calling_from_plugin() -> bool;
pub fn enter_plugin();
pub fn exit_plugin();
pub fn is_vdso_ready() -> bool;
}
pub type vdso_clock_gettime_fn =
extern "C" fn(clockid: libc::clockid_t, tp: *mut libc::timespec) -> i32;
pub type vdso_getcpu_fn = extern "C" fn(cpu: *mut u32, node: *mut u32, _unused: usize) -> i32;
pub type vdso_gettimeofday_fn =
extern "C" fn(tv: *mut libc::timeval, tz: *mut libc::timezone) -> i32;
pub type vdso_time_fn = extern "C" fn(tloc: *mut libc::time_t) -> i32;
pub extern "C" fn vdso_clock_gettime_stub(
_clockid: libc::clockid_t,
_tp: *mut libc::timespec,
) -> i32 {
// HACK: These are never called, but referencing these functions ensures
// they get linked into our binary. These are actually used by the loader.
unsafe { calling_from_plugin() };
unsafe { enter_plugin() };
unsafe { exit_plugin() };
unsafe { is_vdso_ready() };
-libc::EFAULT
}
pub extern "C" fn vdso_getcpu_stub(_cpu: *mut u32, _node: *mut u32, _unused: usize) -> i32 {
-libc::EFAULT
}
pub extern "C" fn vdso_gettimeofday_stub(_tv: *mut libc::timeval, _tz: *mut libc::timezone) -> i32 {
-libc::EFAULT
}
pub extern "C" fn vdso_time_stub(_tloc: *mut libc::time_t) -> i32 {
-libc::EFAULT
}
pub type void_void_fn = unsafe extern "C" fn() -> *mut libc::c_void;
#[repr(C)]
pub struct fn_icept {
pub lib_name: *const libc::c_char,
pub fn_name: *const libc::c_char,
pub icept_callback: extern "C" fn(void_void_fn) -> void_void_fn,
}
pub type icept_reg_fn = extern "C" fn(*const fn_icept);
unsafe impl Send for fn_icept {}
unsafe impl Sync for fn_icept {}
#[repr(C)]
pub struct syscall_stackframe {
pub rbp_stackalign: *mut libc::c_void,
pub r15: *mut libc::c_void,
pub r14: *mut libc::c_void,
pub r13: *mut libc::c_void,
pub r12: *mut libc::c_void,
pub r11: *mut libc::c_void,
pub r10: *mut libc::c_void,
pub r9: *mut libc::c_void,
pub r8: *mut libc::c_void,
pub rdi: *mut libc::c_void,
pub rsi: *mut libc::c_void,
pub rdx: *mut libc::c_void,
pub rcx: *mut libc::c_void,
pub rbx: *mut libc::c_void,
pub rbp_prologue: *mut libc::c_void,
// trampoline
pub fake_ret: *mut libc::c_void,
/// Syscall return address. This is where execution should continue after a
/// syscall has been handled.
pub ret: *mut libc::c_void,
}
pub type handle_syscall_fn = extern "C" fn(
syscall: isize,
arg1: usize,
arg2: usize,
arg3: usize,
arg4: usize,
arg5: usize,
arg6: usize,
wrapper_sp: *mut syscall_stackframe,
) -> usize;
pub type handle_vdso_fn =
extern "C" fn(syscall: isize, actual_fn: void_void_fn) -> Option<void_void_fn>;
pub type handle_rdtsc_fn = extern "C" fn() -> u64;
pub type post_load_fn = extern "C" fn(bool);
/// A struct of arguments for the clone3 syscall.
#[derive(Debug)]
#[repr(C)]
pub struct clone_args {
// Flags bit mask
pub flags: u64,
// Where to store PID file descriptor (int *)
pub pidfd: u64,
// Where to store child TID, in child's memory (pid_t *)
pub child_tid: u64,
// Where to store child TID, in parent's memory (pid_t *)
pub parent_tid: u64,
// Signal to deliver to parent on child termination
pub exit_signal: u64,
// Pointer to lowest byte of stack
pub stack: u64,
// Size of stack
pub stack_size: u64,
// Location of new TLS
pub tls: u64,
// Pointer to a pid_t array (since Linux 5.5)
pub set_tid: u64,
// Number of elements in set_tid (since Linux 5.5)
pub set_tid_size: u64,
// File descriptor for target cgroup of child (since Linux 5.7)
pub cgroup: u64,
}

View file

@ -1 +0,0 @@
../../../../../../../third-party/sabre/plugin_api/recursion_protector.c

View file

@ -1 +0,0 @@
../../../../../../../third-party/sabre/plugin_api/arch/x86_64/vfork_syscall.s

View file

@ -1,72 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
//! Everything defined here shouldn't be used directly. However, they must be
//! exposed so that code generated by the proc macros can use them.
#![doc(hidden)]
pub use ::once_cell::sync::OnceCell;
use reverie_rpc::MakeClient;
use super::callbacks;
use super::ffi;
use super::paths;
use super::rpc;
use super::signal;
use super::tool::Tool;
use super::tool::ToolGlobal;
/// Creates an instance of a Tool. This is called when `ToolGlobal::global` is
/// called for the first time.
pub fn init_tool<T: Tool>() -> T {
// Create the base transport channel. This transport layer can be wrapped
// potentially many times by nested tools. If this fails (i.e., it failed to
// connect to the socket), there isn't anything we can do except panic. A
// client without a connection to the global state isn't very useful.
let channel = rpc::BaseChannel::new().unwrap();
T::new(MakeClient::make_client(Box::new(channel)))
}
fn register_detours<T: ToolGlobal>(fn_icept_reg: ffi::icept_reg_fn) {
for detour_func in <<T as ToolGlobal>::Target>::detours() {
fn_icept_reg(detour_func);
}
}
pub fn sbr_init<T: ToolGlobal>(
argc: *mut i32,
argv: *mut *mut *mut libc::c_char,
fn_icept_reg: ffi::icept_reg_fn,
vdso_callback: *mut Option<ffi::handle_vdso_fn>,
syscall_handler: *mut Option<ffi::handle_syscall_fn>,
rdtsc_handler: *mut Option<ffi::handle_rdtsc_fn>,
_post_load: *mut Option<ffi::post_load_fn>,
sabre_path: *const libc::c_char,
client_path: *const libc::c_char,
) {
unsafe {
*vdso_callback = Some(callbacks::handle_vdso::<T>);
*syscall_handler = Some(callbacks::handle_syscall::<T>);
*rdtsc_handler = Some(callbacks::handle_rdtsc::<T>);
paths::set_sabre_path(sabre_path);
paths::set_client_path(client_path);
// The plugin path is the first argument.
paths::set_plugin_path(**argv);
signal::register_central_handler::<T>();
*argc -= 1;
*argv = (*argv).wrapping_add(1);
// Setting up function detours
register_detours::<T>(fn_icept_reg);
}
}

View file

@ -1,38 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
//! This library provides an ergonomic interface writing SaBRe plugins with
//! Rust.
mod callbacks;
pub mod ffi;
#[doc(hidden)]
pub mod internal;
mod paths;
mod protected_files;
mod rpc;
mod signal;
mod slot_map;
mod thread;
mod tool;
mod utils;
pub mod vdso;
pub use nostd_print::*;
pub use paths::*;
pub use reverie_sabre_macros::tool;
pub use tool::*;
// Tracing programs that use jemalloc will hang if we allocate when jemalloc
// calls readlinkat. Using a different allocator works around this problem.
//
// NOTE: Even though we set the global allocator here, anything that depends on
// this library will use this global allocator. Thus, it will apply to all
// tools/plugins automatically.
#[global_allocator]
static GLOBAL_ALLOCATOR: mimalloc::MiMalloc = mimalloc::MiMalloc;

View file

@ -1,54 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
use std::ffi::CStr;
/// Path to the sabre executable. Needed for intercepting syscalls after execve.
static mut SABRE_PATH: *const libc::c_char = core::ptr::null();
/// Path to this plugin. Needed for intercepting syscalls after execve.
static mut PLUGIN_PATH: *const libc::c_char = core::ptr::null();
/// Path to the client binary.
static mut CLIENT_PATH: *const libc::c_char = core::ptr::null();
/// Sets the global path to the sabre binary.
#[doc(hidden)]
#[inline]
pub(super) unsafe fn set_sabre_path(path: *const libc::c_char) {
SABRE_PATH = path;
}
/// Sets the global path to the plugin (aka tool).
#[doc(hidden)]
#[inline]
pub(super) unsafe fn set_plugin_path(path: *const libc::c_char) {
PLUGIN_PATH = path;
}
/// Sets the global path to the client binary.
#[doc(hidden)]
#[inline]
pub(super) unsafe fn set_client_path(path: *const libc::c_char) {
CLIENT_PATH = path;
}
/// Returns the path to the sabre binary.
pub fn sabre_path() -> &'static CStr {
unsafe { CStr::from_ptr(SABRE_PATH) }
}
/// Returns the path to the plugin.
pub fn plugin_path() -> &'static CStr {
unsafe { CStr::from_ptr(PLUGIN_PATH) }
}
/// Returns the path to the client binary.
pub fn client_path() -> &'static CStr {
unsafe { CStr::from_ptr(CLIENT_PATH) }
}

View file

@ -1,210 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
//! Protects a set of file descriptors from getting closed.
use std::os::unix::io::AsRawFd;
use std::os::unix::io::RawFd;
use parking_lot::Mutex;
use syscalls::Sysno;
use syscalls::SysnoSet;
// TODO: Remove this lazy_static after upgrading to parking_lot >= 0.12.1.
// Mutex::new is a const fn in newer versions.
lazy_static::lazy_static! {
/// A set of file descriptors that should not get closed.
static ref PROTECTED_FILES: Mutex<ProtectedFiles> = Mutex::new(ProtectedFiles::new());
}
struct ProtectedFiles {
// We have to use Vec here to ensure `new` can be a const fn, which is
// required for global static variables. This should be fine, since we don't
// expect to be protecting more than a handful of file descriptors.
files: Vec<RawFd>,
}
impl ProtectedFiles {
pub const fn new() -> Self {
Self { files: Vec::new() }
}
pub fn contains<Fd: AsRawFd>(&self, fd: &Fd) -> bool {
self.files.contains(&fd.as_raw_fd())
}
pub fn insert<Fd: AsRawFd>(&mut self, fd: &Fd) -> bool {
if self.contains(fd) {
true
} else {
self.files.push(fd.as_raw_fd());
false
}
}
pub fn remove<Fd: AsRawFd>(&mut self, fd: &Fd) -> bool {
let fd = fd.as_raw_fd();
if let Some(index) = self.files.iter().position(|item| item == &fd) {
self.files.swap_remove(index);
true
} else {
false
}
}
}
/// A file descriptor that is internal to the plugin and not visible to the
/// client. These file descriptors cannot be closed by the client.
pub struct ProtectedFd<T: AsRawFd>(T);
impl<T: AsRawFd> Drop for ProtectedFd<T> {
fn drop(&mut self) {
PROTECTED_FILES.lock().remove(&self.0);
}
}
impl<T: AsRawFd> AsRef<T> for ProtectedFd<T> {
fn as_ref(&self) -> &T {
&self.0
}
}
impl<T: AsRawFd> AsMut<T> for ProtectedFd<T> {
fn as_mut(&mut self) -> &mut T {
&mut self.0
}
}
/// Takes a closure `f` that creates and returns a file descriptor. The file
/// descriptor that is returned is protected from getting closed. This is safe
/// even if another thread is trying to close this same file descriptor.
pub fn protect_with<F, T, E>(f: F) -> Result<ProtectedFd<T>, E>
where
F: FnOnce() -> Result<T, E>,
T: AsRawFd,
{
let mut protected_files = PROTECTED_FILES.lock();
f().map(|fd| {
protected_files.insert(&fd);
ProtectedFd(fd)
})
}
/// Returns true if a file descriptor is protected and shouldn't be closed.
pub fn is_protected<Fd: AsRawFd>(fd: &Fd) -> bool {
PROTECTED_FILES.lock().contains(fd)
}
/// All of these syscalls take the input file descriptor as the first argument.
/// Some syscalls, like mmap, don't conform to this pattern and need to be
/// handled in a special way.
static FD_ARG0_SYSCALLS: SysnoSet = SysnoSet::new(&[
Sysno::close,
Sysno::dup,
Sysno::dup2,
Sysno::openat,
Sysno::fstat,
Sysno::read,
Sysno::write,
Sysno::lseek,
Sysno::ioctl,
Sysno::pread64,
Sysno::pwrite64,
Sysno::readv,
Sysno::writev,
Sysno::connect,
Sysno::accept,
Sysno::sendto,
Sysno::recvfrom,
Sysno::sendmsg,
Sysno::recvmsg,
Sysno::shutdown,
Sysno::bind,
Sysno::listen,
Sysno::getsockname,
Sysno::getpeername,
Sysno::getsockopt,
Sysno::fcntl,
Sysno::flock,
Sysno::fsync,
Sysno::fdatasync,
Sysno::ftruncate,
Sysno::getdents,
Sysno::getdents64,
Sysno::fchdir,
Sysno::fchmod,
Sysno::fchown,
Sysno::fstatfs,
Sysno::readahead,
Sysno::fsetxattr,
Sysno::fgetxattr,
Sysno::flistxattr,
Sysno::fremovexattr,
Sysno::fadvise64,
Sysno::epoll_wait,
Sysno::epoll_ctl,
Sysno::inotify_add_watch,
Sysno::inotify_rm_watch,
Sysno::mkdirat,
Sysno::mknodat,
Sysno::fchownat,
Sysno::futimesat,
Sysno::newfstatat,
Sysno::unlinkat,
Sysno::renameat,
Sysno::linkat,
Sysno::readlinkat,
Sysno::fchmodat,
Sysno::faccessat,
Sysno::sync_file_range,
Sysno::vmsplice,
Sysno::utimensat,
Sysno::epoll_pwait,
Sysno::signalfd,
Sysno::fallocate,
Sysno::timerfd_settime,
Sysno::timerfd_gettime,
Sysno::accept4,
Sysno::signalfd4,
Sysno::dup3,
Sysno::preadv,
Sysno::pwritev,
Sysno::recvmmsg,
Sysno::fanotify_mark,
Sysno::name_to_handle_at,
Sysno::open_by_handle_at,
Sysno::syncfs,
Sysno::sendmmsg,
Sysno::setns,
Sysno::finit_module,
Sysno::renameat2,
Sysno::kexec_file_load,
Sysno::execveat,
Sysno::preadv2,
Sysno::pwritev2,
Sysno::statx,
Sysno::pidfd_send_signal,
Sysno::io_uring_enter,
Sysno::io_uring_register,
Sysno::open_tree,
Sysno::move_mount,
Sysno::fsconfig,
Sysno::fsmount,
Sysno::fspick,
Sysno::openat2,
Sysno::pidfd_getfd,
]);
static FD_ARG1_SYSCALLS: SysnoSet = SysnoSet::new(&[Sysno::dup2, Sysno::dup3]);
/// Returns true if the given syscall operates on a protected file descriptor.
pub fn uses_protected_fd(sysno: Sysno, arg0: usize, arg1: usize) -> bool {
(FD_ARG0_SYSCALLS.contains(sysno) && is_protected(&(arg0 as i32)))
|| (FD_ARG1_SYSCALLS.contains(sysno) && is_protected(&(arg1 as i32)))
}

View file

@ -1,105 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
use std::io;
use std::io::Write;
use std::os::unix::io::AsRawFd;
use std::os::unix::io::FromRawFd;
use std::os::unix::net::UnixStream;
use std::sync::Mutex;
use reverie_rpc::Channel;
use serde::Deserialize;
use serde::Serialize;
use syscalls::Errno;
use super::protected_files::protect_with;
use super::protected_files::ProtectedFd;
/// The file descriptor that our RPC socket connection should use. We use 100
/// here because many programs or tests expect to use the early file
/// descriptors. Using file descriptor 100 also makes this easier to debug.
const SOCKET_FD: i32 = 100;
struct Inner {
stream: ProtectedFd<UnixStream>,
}
/// Implements a channel using a UNIX domain socket.
pub struct BaseChannel {
inner: Mutex<Inner>,
}
impl BaseChannel {
/// Connects to the global state RPC server.
pub fn new() -> io::Result<Self> {
// FIXME: We can't rely on this environment variable existing. Instead,
// the host should use seccomp-unotify to listen for a special syscall
// that returns a file descriptor to the socket connection.
let sock_path = std::env::var_os("REVERIE_SOCK")
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "$REVERIE_SOCK does not exist!"))?;
let stream = protect_with(|| -> Result<_, io::Error> {
let sock = UnixStream::connect(sock_path)?;
// Move the socket to our desired file descriptor and make sure it
// gets closed when execve is called.
let fd =
Errno::result(unsafe { libc::dup3(sock.as_raw_fd(), SOCKET_FD, libc::O_CLOEXEC) })?;
// Close the old socket file descriptor.
drop(sock);
Ok(unsafe { UnixStream::from_raw_fd(fd) })
})?;
Ok(Self {
inner: Mutex::new(Inner { stream }),
})
}
}
impl Inner {
fn try_send<T>(&mut self, item: &T) -> io::Result<()>
where
T: Serialize,
{
let mut buf = Vec::with_capacity(1024);
reverie_rpc::encode(item, &mut buf)?;
self.stream.as_mut().write_all(&buf)?;
Ok(())
}
fn try_recv<T>(&mut self) -> io::Result<T>
where
T: for<'a> Deserialize<'a>,
{
let mut buf = Vec::with_capacity(1024);
reverie_rpc::decode_from(self.stream.as_mut(), &mut buf)
}
}
impl<Req, Res> Channel<Req, Res> for BaseChannel
where
Req: Serialize,
Res: for<'a> Deserialize<'a>,
{
fn send(&self, item: &Req) {
let mut inner = self.inner.lock().unwrap();
inner.try_send(item).expect("Failed to send RPC");
}
fn call(&self, item: &Req) -> Res {
let mut inner = self.inner.lock().unwrap();
inner.try_send(item).expect("Failed to send RPC");
inner.try_recv().expect("Failed to recv RPC")
}
}

View file

@ -1,539 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
use core::cell::UnsafeCell;
use core::marker::PhantomData;
use core::mem;
use core::sync::atomic::AtomicU64;
use core::sync::atomic::Ordering::*;
use heapless::mpmc::Q64;
/// These constants describe the format of the combined queued and guard
/// counter. These counters are coupled to
/// 1. Allow for single atomic writes when creating and dropping signal guards
/// 2. Ensuring consistency between the counters at any given time. This
/// simplifies the logic in proving the correctness of the operations
///
/// The format of the counters is:
///
/// |<---------------------- 64 Bits ---------------------->|
/// |<-= 32 Bits Queued Count ->|<-- 32 Bits Guard Count -->|
const GUARD_COUNT_BITS: u8 = 32;
const GUARD_COUNT_UNIT: u64 = 1;
const GUARD_COUNT_MASK: u64 = (1 << GUARD_COUNT_BITS) - 1;
const QUEUED_COUNT_SHIFT: u8 = GUARD_COUNT_BITS;
const QUEUED_COUNT_UNIT: u64 = 1 << QUEUED_COUNT_SHIFT;
const QUEUED_COUNT_MASK: u64 = !GUARD_COUNT_MASK;
type Invocation<T> = (fn(T), T);
pub type SignalHandlerInput = libc::siginfo_t;
pub type SignalGuard = SequencerGuard<'static, SignalHandlerInput>;
pub type SignalAntiGuard = SequencerAntiGuard<'static, SignalHandlerInput>;
thread_local! {
pub(crate) static SIGNAL_HANLDER_SEQUENCER: GuardedSequencer<SignalHandlerInput>
= GuardedSequencer::with_initial_guard_count(1);
}
/// Marker struct that triggers "run-on-drop" behavior for defered invocations.
/// The phantom data here is to make the guard `!Send`
pub struct SequencerGuard<'a, T> {
owner: &'a GuardedSequencer<T>,
_phantom: PhantomData<UnsafeCell<()>>,
}
/// Marker struct that is the like the particle opposite of the signal guard.
/// When it is created, the count of guards decreases by one. If the new guard
/// count is zero, defered execitions will be evaluated and signals will not be
/// blocked. When this struct is dropped, the count of guards will be increased
/// by one guarding against signal interuptions again. The phantom data here is
/// to make the anti guard `!Send`
pub struct SequencerAntiGuard<'a, T> {
owner: &'a GuardedSequencer<T>,
_phantom: PhantomData<UnsafeCell<()>>,
}
impl<'a, T> SequencerGuard<'a, T> {
fn new(owner: &'a GuardedSequencer<T>) -> Self {
SequencerGuard {
owner,
_phantom: Default::default(),
}
}
}
impl<'a, T> SequencerAntiGuard<'a, T> {
fn new(owner: &'a GuardedSequencer<T>) -> Self {
SequencerAntiGuard {
owner,
_phantom: Default::default(),
}
}
}
impl<'a, T> Drop for SequencerGuard<'a, T> {
/// When the guard is dropped, we decrement the counter for guards, and if
/// this was the last one, we run any invocations that were added while the
/// guard(s) were active
fn drop(&mut self) {
self.owner.decrement_guard_count()
}
}
impl<'a, T> Drop for SequencerAntiGuard<'a, T> {
/// When an anti guard is dropped, we increment the guard count to return
/// the thread to a state that cannot be interrupted
fn drop(&mut self) {
self.owner.increment_guard_count()
}
}
pub(crate) struct GuardedSequencer<T> {
queue: Q64<Invocation<T>>,
guard_state: AtomicU64,
}
impl<T> GuardedSequencer<T> {
const fn with_initial_guard_count(guard_count: u32) -> Self {
GuardedSequencer {
queue: Q64::new(),
guard_state: AtomicU64::new(guard_count as u64),
}
}
// Enqueue the given invocation to be run when no guards are active.
fn enqueue_invocation(&self, invocation: Invocation<T>) {
self.queue.enqueue(invocation).ok().expect("Buffer full");
}
/// Drain the invocation. When a invocation is "drained", it is:
/// 1. Removed from the buffer
/// 2. Evaluated
/// 3. Counted as handled in the queued count
fn drain_one_invocation(&self) -> bool {
if let Some((handler, argument)) = self.queue.dequeue() {
handler(argument);
// Decrement the queued count stored with guard count
self.guard_state.fetch_sub(QUEUED_COUNT_UNIT, SeqCst);
true
} else {
false
}
}
/// Execute the invocations that are currently stored in the queue.
fn drain_invocations(&self) {
// While invocations are successfully being drained, keep draining. If
// draining one invocation is not successful, either there are no more
// or drain was interrupted and completed elsewhere. Either way there's
// nothing more to do here.
while self.drain_one_invocation() {}
}
/// Execute the given handler with the given argument when the current
/// thread has no active signal guards. If there are no guards, this
/// invocation will be run synchronously, otherwise, the invocation will be
/// stored and run asynchronously when guard count reaches zero
pub fn invoke(&self, handler: fn(T), argument: T) {
// Increment the guard and queued count in one atomic step
self.guard_state
.fetch_add(QUEUED_COUNT_UNIT + GUARD_COUNT_UNIT, SeqCst);
self.enqueue_invocation((handler, argument));
// decrementing the guard count here will either run the invocation
// that was just added or defer it depending respectively on whether
// this was the only guard or not
self.decrement_guard_count();
}
/// Increment the number of active guards by one
fn increment_guard_count(&self) {
self.guard_state.fetch_add(GUARD_COUNT_UNIT, Acquire);
}
/// Decrement the counter for guards, and if the count goes to zero, we run
/// any invocations that were added while the guard(s) were active
fn decrement_guard_count(&self) {
let prev_guard_state = self.guard_state.fetch_sub(GUARD_COUNT_UNIT, Release);
let prev_guard_count = prev_guard_state & GUARD_COUNT_MASK;
assert!(
prev_guard_count > 0,
"Signal guard count went negative indicating a bug"
);
// If this wasn't the last guard or if there were no invocations added,
// we are done
if prev_guard_count > 1 || prev_guard_state & QUEUED_COUNT_MASK == 0 {
return;
}
// Now it's our responsibility to execute all invocations
self.drain_invocations();
}
/// Create a guard on this sequencer. Invocations performed on this
/// sequencer will be deferred until the returned guard (and any others)
/// are dropped
fn guard<'a>(&'a self) -> SequencerGuard<'a, T> {
self.increment_guard_count();
SequencerGuard::new(self)
}
/// Create a guard on this sequencer without incrementing the guard count.
/// Think of this as taking ownership of a guard that someone else forgot.
/// Invocations performed on this sequencer will be deferred until the
/// returned guard (and any others) are dropped.
fn implicit_guard<'a>(&'a self) -> SequencerGuard<'a, T> {
assert!(
self.guard_state.load(Acquire) > 0,
"No implicit signal guard in place"
);
SequencerGuard::new(self)
}
/// Create a an anti guard on this sequencer. Until it is dropped, the
/// returned anti guard cancels out exactly one guard meaning if a single
/// guard exists and has defered invocations, those invocations will be
/// executed as soon as this anti guard is created, and any subsequent
/// invocations will be run immediately
fn anti_guard<'a>(&'a self) -> SequencerAntiGuard<'a, T> {
self.decrement_guard_count();
SequencerAntiGuard::new(self)
}
}
/// Get a static pointer to the the guarded sequencer for this thread
fn signal_handler_sequencer() -> &'static GuardedSequencer<SignalHandlerInput> {
// We are using some unsafe code here to convert the lifetime provided for
// the thread-local `.with` function into a static lifetime. This is safe
// because we are not passing the returned value to any other thread
SIGNAL_HANLDER_SEQUENCER.with(|sequencer| unsafe { mem::transmute::<_, &'static _>(sequencer) })
}
/// Execute the given handler with the given argument when the current
/// thread has no active signal guards. If there are no guards, this
/// invocation will be run synchronously, otherwise, the invocation will be
/// stored and run asynchronously when guard count reaches zero
pub fn invoke_guarded(handler: fn(SignalHandlerInput), siginfo: SignalHandlerInput) {
signal_handler_sequencer().invoke(handler, siginfo);
}
/// Enter a region where signals cannot interrupt invocation of the current
/// thread. This operation should be thought to have atomic-aquire ordering.
/// The exclusion zone will last until the returned guard is dropped
#[must_use]
pub fn enter_signal_exclusion_zone() -> SignalGuard {
signal_handler_sequencer().guard()
}
/// Enter an already-exiting region where signals cannot interrupt execution of
/// the current thread. The exclusion zone will last until the returned guard is
/// dropped
#[must_use]
pub fn enter_implicit_signal_exclusion_zone() -> SignalGuard {
signal_handler_sequencer().implicit_guard()
}
/// Re-enter an execution phase where signals can interrupt the current thread.
/// When the returned anti guard is dropped, a signal exclusion zone will be
/// resumed
#[must_use]
pub fn reenter_signal_inclusion_zone() -> SignalAntiGuard {
signal_handler_sequencer().anti_guard()
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use std::mem;
use std::rc::Rc;
use super::*;
macro_rules! assert_interrupts_eq {
($received:ident, [$($v:ident),*]) => {
{
let to_compare : Vec<&'static str> = vec![$(stringify!($v)),*];
assert_eq!(&*$received.borrow(), &to_compare);
}
}
}
macro_rules! make_handlers {
($($handler:ident),*$(,)?) => {
$(
fn $handler(input: HandlerInput) {
let HandlerInput(_, log) = input;
log.borrow_mut().push(stringify!($handler));
}
macro_rules! $handler {
($sched:ident, $log:ident) => {
$sched.invoke($handler, HandlerInput($sched.clone(), $log.clone()))
}
}
)*
}
}
// Define the handlers we are goning to call
make_handlers! {
h1,
h2,
h3,
h4,
h5,
h6,
}
type InvocationLog = Rc<RefCell<Vec<&'static str>>>;
#[derive(Clone)]
struct HandlerInput(Rc<GuardedSequencer<HandlerInput>>, InvocationLog);
/// Test wrapper to do the cleanup we need and run each test in serial
fn run_guarded_sequencer_test<T>(t: T)
where
T: FnOnce(Rc<GuardedSequencer<HandlerInput>>, InvocationLog),
{
let handler_log = Rc::new(RefCell::new(Vec::new()));
let sequencer = Rc::new(GuardedSequencer::with_initial_guard_count(0));
t(sequencer.clone(), handler_log);
// Make sure if the test exits normally that all handlers were run
// and the guard/handler counts are returned to zero
assert_eq!(0, sequencer.guard_state.load(SeqCst));
assert!(sequencer.queue.dequeue().is_none());
}
#[test]
fn test_basic_handler_defer() {
run_guarded_sequencer_test(|sequencer, log| {
assert_interrupts_eq!(log, []);
// Running ungaurded should work immediately
h1!(sequencer, log);
assert_interrupts_eq!(log, [h1]);
// Running with one guard should defer the handler
{
let _g1 = sequencer.guard();
h2!(sequencer, log);
assert_interrupts_eq!(log, [h1]);
}
// until after the guard goes out of scope
assert_interrupts_eq!(log, [h1, h2]);
});
}
#[test]
fn test_defer_with_multiple_guards() {
run_guarded_sequencer_test(|sequencer, log| {
// Running with one guard should defer the handler
{
let _g1 = sequencer.guard();
h1!(sequencer, log);
assert_interrupts_eq!(log, []);
// Running with another guard should do the same
{
let _g2 = sequencer.guard();
h2!(sequencer, log);
assert_interrupts_eq!(log, []);
}
// Nothing should change when the first guard is dropped
assert_interrupts_eq!(log, []);
}
// When both guards are dropped, the handlers should run in the
// order they were received
assert_interrupts_eq!(log, [h1, h2]);
});
}
#[test]
fn test_defer_with_anti_guard() {
run_guarded_sequencer_test(|sequencer, log| {
// Running with one guard should defer the handler
{
let _g1 = sequencer.guard();
h1!(sequencer, log);
assert_interrupts_eq!(log, []);
// Running with an anti guard allows defered signals to run,
// and allow new new handlers to run immediately
{
let _ag = sequencer.anti_guard();
assert_interrupts_eq!(log, [h1]);
h2!(sequencer, log);
assert_interrupts_eq!(log, [h1, h2]);
}
// When the anti guard is gone, we return to a state where
// handlers are defered
h3!(sequencer, log);
assert_interrupts_eq!(log, [h1, h2]);
}
// When the guard is dropped, the handlers should run in the
// order they were received
assert_interrupts_eq!(log, [h1, h2, h3]);
});
}
#[test]
fn test_defer_with_implicit_guard() {
run_guarded_sequencer_test(|sequencer, log| {
// Create a guard and drop it without calling the destructor;
mem::forget(sequencer.guard());
h1!(sequencer, log);
assert_interrupts_eq!(log, []);
//To release that implicit guard, we enter an implicitly guarded
// zone
{
let _implicit_guard = sequencer.implicit_guard();
// Handlers are still deferred
h2!(sequencer, log);
assert_interrupts_eq!(log, []);
}
// Once the guard is dropped, deferred signals are run
assert_interrupts_eq!(log, [h1, h2]);
//and new handlers are run immediately
h3!(sequencer, log);
assert_interrupts_eq!(log, [h1, h2, h3]);
});
}
#[test]
fn test_nested_handling() {
run_guarded_sequencer_test(|sequencer, log| {
// Nothing prevents guards from being created and dropped within
// handler functions
fn nested_1(input_1: HandlerInput) {
let HandlerInput(sequencer, log) = input_1.clone();
h2!(sequencer, log);
let _g = sequencer.guard();
h3!(sequencer, log);
{
let _ag = sequencer.anti_guard();
h4!(sequencer, log);
}
sequencer.invoke(nested_2, input_1);
h5!(sequencer, log);
assert_interrupts_eq!(log, [h1, h2, h3, h4]);
}
fn nested_2(input_2: HandlerInput) {
let HandlerInput(sequencer, log) = input_2;
h6!(sequencer, log);
}
{
let _g = sequencer.guard();
h1!(sequencer, log);
sequencer.invoke(nested_1, HandlerInput(sequencer.clone(), log.clone()));
}
assert_interrupts_eq!(log, [h1, h2, h3, h4, h5, h6]);
});
}
#[test]
#[should_panic]
fn test_invalid_implicit_guard() {
run_guarded_sequencer_test(|sequencer, _| {
// Entering an implicit guard when one doesn't exist is an error
let _g = sequencer.implicit_guard();
})
}
#[test]
#[should_panic]
fn test_anti_guard_without_guard() {
run_guarded_sequencer_test(|sequencer, _| {
// Creating an anti guard outside of a guard is an error
let _ag = sequencer.anti_guard();
})
}
thread_local! {
static INVOKE_COUNT: AtomicU64 = AtomicU64::new(0);
}
fn test_signal_handler(_: SignalHandlerInput) {
INVOKE_COUNT.with(|counter| counter.fetch_add(1, SeqCst));
}
pub(crate) const DUMMY_SIGINFO: SignalHandlerInput = unsafe {
mem::transmute::<_, SignalHandlerInput>([0u8; mem::size_of::<SignalHandlerInput>()])
};
#[test]
fn test_signal_handling_guard() {
SIGNAL_HANLDER_SEQUENCER.with(|sequencer| {
// Reinitialize the guard state and queue just in case
sequencer.guard_state.store(1, SeqCst);
while sequencer.queue.dequeue().is_some() {}
INVOKE_COUNT.with(|counter| counter.store(0, SeqCst));
// Run through the functions just to check that we have our sanity
invoke_guarded(test_signal_handler, DUMMY_SIGINFO);
// We initialized the sequencer with an implicit guard, so
assert_eq!(0, INVOKE_COUNT.with(|count| count.load(SeqCst)));
{
let _g1 = enter_implicit_signal_exclusion_zone();
invoke_guarded(test_signal_handler, DUMMY_SIGINFO);
assert_eq!(0, INVOKE_COUNT.with(|count| count.load(SeqCst)));
}
assert_eq!(2, INVOKE_COUNT.with(|count| count.load(SeqCst)));
{
let _g2 = enter_signal_exclusion_zone();
invoke_guarded(test_signal_handler, DUMMY_SIGINFO);
assert_eq!(2, INVOKE_COUNT.with(|count| count.load(SeqCst)));
{
let _ag1 = reenter_signal_inclusion_zone();
assert_eq!(3, INVOKE_COUNT.with(|count| count.load(SeqCst)));
invoke_guarded(test_signal_handler, DUMMY_SIGINFO);
assert_eq!(4, INVOKE_COUNT.with(|count| count.load(SeqCst)));
}
invoke_guarded(test_signal_handler, DUMMY_SIGINFO);
assert_eq!(4, INVOKE_COUNT.with(|count| count.load(SeqCst)));
}
assert_eq!(5, INVOKE_COUNT.with(|count| count.load(SeqCst)));
})
}
}

View file

@ -1,437 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
use core::mem::transmute;
use core::mem::MaybeUninit;
use core::ptr;
use core::sync::atomic::fence;
use core::sync::atomic::Ordering::*;
use atomic::Atomic;
use lazy_static::lazy_static;
use crate::callbacks;
use crate::signal;
use crate::slot_map::SlotKey;
use crate::slot_map::SlotMap;
use crate::tool::Tool;
use crate::tool::ToolGlobal;
pub mod guard;
pub type HandlerInput = libc::siginfo_t;
/// We need to keep track of the sigaction that the user specified or what was
/// originally provided as a default separately from what we execute directly as
/// a signal handler.
#[derive(Debug, Clone, Copy)]
struct SigActionPair {
/// Prisinte sigaction provided by the user or os
guest_facing_action: libc::sigaction,
/// The actual sigaction we are using
internal_action: libc::sigaction,
}
impl SigActionPair {
/// Create a new SigActionPair from the original sig action and an override
/// for the default handler. The created pair will contain the original
/// action, and a synthetic action with the handler replaced if an override
/// is provided or if the the sa_sigaction is one of the non-function-
/// pointer values (`SI_DFL`, `SI_ERR`, `SI_IGN`)
fn new(original: libc::sigaction, override_handler: Option<libc::sighandler_t>) -> Self {
let mut internal_action = original.clone();
// This is safe because it is only reading from a mut static that is
// guaranteed to have been completely set before this function
// is called
internal_action.sa_sigaction = unsafe {
match (original.sa_sigaction, override_handler) {
(_, Some(override_handler)) => override_handler,
(libc::SIG_DFL, _) => DEFAULT_EXIT_HANDLER
.expect("Default handlers should be set before registering actions"),
(libc::SIG_IGN, _) => DEFAULT_IGNORE_HANDLER
.expect("Default handlers should be set before registering actions"),
(libc::SIG_ERR, _) => DEFAULT_ERROR_HANDLER
.expect("Default handlers should be set before registering actions"),
(default_action, None) => default_action,
}
};
SigActionPair {
guest_facing_action: original,
internal_action,
}
}
}
lazy_static! {
/// This is where we are storing the registered actions for each signal.
/// We have to store them as Options for now because our slot map requires
/// its stored type to implement default
static ref HANDLER_SLOT_MAP: SlotMap<Option<SigActionPair>> = SlotMap::new();
}
// The sighandler_t type has some values that aren't pointers that are still
// valid. They aren't executable, so we need an executable version that we
// control for each. Those are below
/// Storage of our default handler for the libc::SIG_DFL
static mut DEFAULT_EXIT_HANDLER: Option<libc::sighandler_t> = None;
/// Storage of our default handler for the libc::SIG_IGN
static mut DEFAULT_IGNORE_HANDLER: Option<libc::sighandler_t> = None;
/// Storage of our default handler for the libc::SIG_ERR
static mut DEFAULT_ERROR_HANDLER: Option<libc::sighandler_t> = None;
/// This function invokes the function specified by the given sigaction directly
/// with the given signal value or siginfo as arguments depending on whether
/// the sigaction's flags indicate it is expecting a sigaction or siginfo.
/// Note. In the case that the action is requesting sigaction, the 3rd argument
/// to the handler will always be null. The specifications for sigaction say the
/// third argument is a pointer to the context for the signal being raised, but
/// we cannot guarantee that context will be valid with the handler function is
/// executed. It also seems like that argument's use is rare, so we are omitting
/// it for the time being. When T122210155, we should be able to provide the ctx
/// argument without introducing unsafety.
unsafe fn invoke_signal_handler(
signal_val: libc::c_int,
action: &libc::sigaction,
sig_info: libc::siginfo_t,
) {
if action.sa_flags & libc::SA_SIGINFO > 0 {
let to_run: extern "C" fn(libc::c_int, *const libc::siginfo_t, *const libc::c_void) =
transmute(action.sa_sigaction as *const libc::c_void);
to_run(
signal_val,
&sig_info as *const libc::siginfo_t,
ptr::null::<libc::c_void>(),
);
} else {
let to_run: extern "C" fn(libc::c_int) =
transmute(action.sa_sigaction as *const libc::c_void);
to_run(signal_val);
}
}
/// Register the given sigaction as the default. Optionally an override function
/// can be passed in that will us to change the default handler for an action
fn insert_action(
sigaction: libc::sigaction,
override_default_handler: Option<libc::sighandler_t>,
) -> SlotKey {
HANDLER_SLOT_MAP.insert(Some(SigActionPair::new(
sigaction,
override_default_handler,
)))
}
/// Register a signal handler for the guest and return the sigaction currently
/// registered for the specified signal
#[allow(dead_code)]
pub fn register_guest_handler(signal_value: i32, new_action: libc::sigaction) -> libc::sigaction {
register_guest_handler_impl(signal_value, new_action, false)
.expect("All signals should have pre-registered guest handlers before now")
}
/// This is our replacement for default handlers where
/// `libc::sighandler_t = libc::SIG_DFL` which is the default handler
/// value for almost all signals. This function will stop all threads in order
/// to raise thread-exit events for each
pub extern "C" fn default_exit_handler<T: ToolGlobal>(
_signal_value: libc::c_int,
_siginfo: *const libc::siginfo_t,
_ctx: *const libc::c_void,
) {
callbacks::exit_group::<T>(0);
}
/// This is our replacement for default handlers where
/// `libc::sighandler_t = libc::SIG_IGN` which is the default handler
/// value for lots of signals. This function does nothing, but allows uniform
/// treatment of function pointers in signal handlers (instead of checking for)
///specific values of sighandler_t before calling
pub extern "C" fn default_ignore_handler<T: ToolGlobal>(
_signal_value: libc::c_int,
_siginfo: *const libc::siginfo_t,
_ctx: *const libc::c_void,
) {
}
/// This is our replacement for default handlers where
/// `libc::sighandler_t = libc::SIG_ERR` which is the default handler
/// value for signals representing unrecoverable errors (SIGILL, SIGSEGV, etc).
/// This function will stop all threads in order to raise thread-exit events
/// for each, but the error code will be non-zero
pub extern "C" fn default_error_handler<T: ToolGlobal>(
_signal_value: libc::c_int,
_siginfo: *const libc::siginfo_t,
_ctx: *const libc::c_void,
) {
callbacks::exit_group::<T>(1);
}
/// This macro defines the functions and constants and api for signals based on
/// an input set of signal. There should only be one invocation of the macro,
/// and it is below. It allows us to express the list of signals we are
/// supporting with properties on each to deal with edge cases
macro_rules! generate_signal_handlers {
(
default_exit_handler: $default_exit_handler_fn:expr,
default_ignore_handler: $default_ignore_handler_fn:expr,
default_error_handler: $default_error_handler_fn:expr,
signals: [$($signal_name:ident $({
$(override_default = $override_default_handler:expr;)?
$(guest_handler_allowed = $guest_handler_allowed:expr;)?
})?),+$(,)?]) => {
/// All signal values as i32
mod signal_values {
$(
pub const $signal_name: i32 = libc::$signal_name as i32;
)+
}
/// Storage for the slot keys that point to the handlers for each signal
mod handler_keys {
use super::*;
$(
pub static $signal_name: Atomic<Option<SlotKey>> = Atomic::new(None);
)+
}
/// Handler functions for each signal
mod reverie_handlers {
use super::*;
$(
#[allow(non_snake_case)]
pub fn $signal_name(handler_input: HandlerInput) {
if let Some(Some(SigActionPair {
internal_action,
..
})) = handler_keys::$signal_name
.load(Relaxed)
.and_then(|key| HANDLER_SLOT_MAP.get(key))
{
unsafe {
invoke_signal_handler(
signal_values::$signal_name as libc::c_int,
internal_action,
handler_input,
);
}
}
}
)+
}
/// This is the function that will be registered for all signals.
/// guest and default handlers for each signal will be dispatched from
/// here using the global sequencer to prevent signals from interfering
/// with reverie or its tool's state
pub extern "C" fn central_handler<T: ToolGlobal>(
real_signal_value: i32,
sig_info_ptr: *const libc::siginfo_t,
_ctx: *const libc::c_void,
) {
let wrapped_handler = match real_signal_value {
$(
signal_values::$signal_name => reverie_handlers::$signal_name,
)+
_ => panic!("Invalid signal {}", real_signal_value)
};
let sig_info = unsafe { *sig_info_ptr };
T::global().handle_signal_event(real_signal_value);
signal::guard::invoke_guarded(wrapped_handler, sig_info);
}
/// This is the funtion that needs to be called to initialize all the
/// signal handling machinery. This will register our central handler
/// for all signals
pub fn register_central_handler<T: ToolGlobal>() {
// Register the default handler functions that correspond to the
// scalar sighandler_t behaviors. This is safe because this will
// only be done before the first syscall is handled, and only
// one thread will be active.
unsafe {
DEFAULT_EXIT_HANDLER = Some($default_exit_handler_fn
as *const libc::c_void
as libc::sighandler_t);
DEFAULT_IGNORE_HANDLER = Some($default_ignore_handler_fn
as *const libc::c_void
as libc::sighandler_t);
DEFAULT_ERROR_HANDLER = Some($default_error_handler_fn
as *const libc::c_void
as libc::sighandler_t);
}
// To make sure handlers are set before continuing
fence(SeqCst);
$( unsafe {
let sa_sigaction = central_handler::<T>
as extern "C" fn(libc::c_int, *const libc::siginfo_t, *const libc::c_void)
as *mut libc::c_void
as libc::sighandler_t;
let mut sa_mask = MaybeUninit::<libc::sigset_t>::uninit();
assert_eq!(0, libc::sigemptyset(sa_mask.as_mut_ptr()), "Failed to create sigset");
libc::sigaddset(sa_mask.as_mut_ptr(), signal_values::$signal_name);
let action = libc::sigaction {
sa_sigaction,
sa_mask: sa_mask.assume_init(),
sa_flags: 0x14000000,
sa_restorer: None,
};
let mut original_action : MaybeUninit<libc::sigaction>
= MaybeUninit::uninit();
assert_eq!(0, libc::sigaction(
signal_values::$signal_name as libc::c_int,
&action as *const libc::sigaction,
original_action.as_mut_ptr(),
), "Failed to register central handler for {}", stringify!($signal_name));
let override_default_handler = None $($(
.or(Some(
$override_default_handler as *const libc::c_void as libc::sighandler_t)
)
)?)?;
let handler_key = insert_action(
original_action.assume_init(),
override_default_handler,
);
handler_keys::$signal_name.store(Some(handler_key), SeqCst);
} )+
}
/// Register the given action for the given signal. The force-allow
/// flag means that the handler will be registered even if guest
/// handlers are disallowed for the given signal. Return a copy of the
/// sigaction that was previously associated with the given signal
fn register_guest_handler_impl(
signal_value: i32,
new_action: libc::sigaction,
force_allow: bool
) -> Option<libc::sigaction> {
let (handler_key, guest_handler_allowed, signal_name) = match signal_value {
$(
signal_values::$signal_name => {
let allowed = force_allow || (true $($( && $guest_handler_allowed)?)?);
let signal_name = stringify!($signal_name);
(&handler_keys::$signal_name, allowed, signal_name)
},
)+
_ => panic!("Invalid signal {}", signal_value)
};
if !guest_handler_allowed {
panic!("Guest handler registration for {} is not supported", signal_name);
}
let new_action_key = insert_action(new_action, None);
let old_action_key_opt = handler_key.swap(Some(new_action_key), Relaxed);
// The first time this function is called, there won't be a stored
// key for every signal action, but if there is return it. It is
// safe because the key being used must have come from the same
// map, and because no elements are deleted, the get operation
// will always succeed
old_action_key_opt.map(|old_action_key| unsafe {
HANDLER_SLOT_MAP.get_unchecked(old_action_key).unwrap().guest_facing_action
})
}
/// Get the sigaction registered for the given signal if there is one.
/// The returned sigaction will either be the original default sigaction
/// set by default for the application or the unaltered sigaction
/// registered by the user
#[allow(dead_code)]
pub fn get_registered_guest_handler(
signal_value: i32
) -> libc::sigaction {
let current_action_key = match signal_value {
$(
signal_values::$signal_name => {
handler_keys::$signal_name
.load(Relaxed)
.expect("All signals should have guest handlers before now")
}
)+
_ => panic!("Invalid signal {}", signal_value)
};
// This is safe because the key being used must have come from the
// same map, and because no elements are deleted, the get operation
// will always succeed
unsafe {
HANDLER_SLOT_MAP.get_unchecked(current_action_key)
.unwrap().guest_facing_action
}
}
};
}
generate_signal_handlers! {
default_exit_handler: default_exit_handler::<T>,
default_ignore_handler: default_ignore_handler::<T>,
default_error_handler: default_error_handler::<T>,
signals: [
SIGHUP,
SIGINT,
SIGQUIT,
// SIGILL, <- needs special synchronous handling Todo(T129735993)
SIGTRAP,
SIGABRT,
SIGBUS,
SIGFPE,
// SIGKILL, <- cannot be handled directly Todo(T129348205)
SIGUSR1,
// SIGSEGV, <- needs special synchronous handling Todo(T129735993)
SIGUSR2,
SIGPIPE,
SIGALRM,
SIGTERM,
SIGSTKFLT {
// This is our controlled exit signal. If the guest tries to
// register a handler for it, we will panic rather than chancining
// undefined behavior
override_default = crate::callbacks::handle_exit_signal::<T>;
guest_handler_allowed = false;
},
// SIGCHLD, <- Causing problems in test_rr_syscallbuf_sigstop T128095829
SIGCONT,
// SIGSTOP, <- cannot be handled directly Todo(T129348205)
SIGTSTP,
SIGTTIN,
SIGTTOU,
SIGURG,
SIGXCPU,
SIGXFSZ,
SIGVTALRM,
SIGPROF,
SIGWINCH,
SIGIO,
SIGPWR,
SIGSYS,
]
}

View file

@ -1,492 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
#![allow(invalid_reference_casting)]
use core::iter::repeat;
use core::mem::MaybeUninit;
use core::sync::atomic::AtomicU32;
use core::sync::atomic::AtomicUsize;
use core::sync::atomic::Ordering::*;
use array_macro::array;
/// This is the key for addressing specific values from the slot map. A slot's
/// address is made up of three parts and those parts are encoded into this one
/// unsigned integer as follows:
///
/// |<-------------------------- 32 Bits Total ----------------------------->|
/// |<-12 Bits: Generation->|<-10 Bits: Chunk Index->|<-10 Bits: Slot Index->|
///
/// Generation - Number of times the slot has been written (including deletes)
/// Chunk Index - The index of the chunk containing the slot
/// Slot Index - The slot's index within its chunk
///
/// Defining generation this way has the nice property that any slot with an
/// even generation is empty. Each slot's generation starts at zero and is
/// incremented by one after its first write
#[repr(transparent)]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SlotKey(u32);
/// These constants define the bit pattern above.
const INDEX_IN_CHUNK_BITS: u8 = 10;
const CHUNK_INDEX_BITS: u8 = 10;
const GENERATION_BITS: u8 = 32 - CHUNK_INDEX_BITS - INDEX_IN_CHUNK_BITS;
const INDEX_IN_CHUNK_MASK: u32 = (0x1 << INDEX_IN_CHUNK_BITS) - 1;
const CHUNK_INDEX_SHIFT: u8 = INDEX_IN_CHUNK_BITS;
const CHUNK_INDEX_MASK: u32 = ((0x1 << CHUNK_INDEX_BITS) - 1) << CHUNK_INDEX_SHIFT;
const GENERATION_SHIFT: u8 = CHUNK_INDEX_SHIFT + CHUNK_INDEX_BITS;
const GENERATION_MASK: u32 = ((0x1 << GENERATION_BITS) - 1) << GENERATION_SHIFT;
// These are some useful constants related to the slotkey's bit pattern
const MAX_CHUNK_COUNT: usize = 1 << CHUNK_INDEX_BITS;
const ONE_GENERATION: u32 = 1 << GENERATION_SHIFT;
const SLOTS_PER_CHUNK: usize = 1 << CHUNK_INDEX_BITS;
/// Each slot needs to know what its generation is and if the slot is empty, it
/// needs to point to the next empty slot
type SlotMetaData = AtomicU32;
type Slot<T> = (SlotMetaData, T);
type Chunk<T> = [Slot<T>; SLOTS_PER_CHUNK];
type ChunkPointer<T> = MaybeUninit<Box<Chunk<T>>>;
/// Allocate a new chunk and return it
fn new_chunk<T: ?Sized + Default>() -> ChunkPointer<T> {
MaybeUninit::new(Box::new(array![Default::default(); SLOTS_PER_CHUNK]))
}
impl SlotKey {
/// Construct a slot key from its parts
fn new_from_parts(chunk_index: usize, slot_index: usize, generation: u32) -> Self {
SlotKey::new(
(((chunk_index as u32) << CHUNK_INDEX_SHIFT) & CHUNK_INDEX_MASK)
+ ((generation << GENERATION_SHIFT) & GENERATION_MASK)
+ ((slot_index as u32) & INDEX_IN_CHUNK_MASK),
)
}
pub fn new<S>(int_value: S) -> Self
where
S: Into<u32>,
{
Self(int_value.into())
}
/// Get the index of the slot encoded in the slot key
fn slot_index(self) -> usize {
(self.0 & INDEX_IN_CHUNK_MASK) as usize
}
/// Get the index of the chunk encoded in the slot key
fn chunk_index(self) -> usize {
((self.0 & CHUNK_INDEX_MASK) >> CHUNK_INDEX_SHIFT) as usize
}
/// Get the generation encoded in the slot key
fn generation(self) -> u32 {
(self.0 & GENERATION_MASK) >> GENERATION_SHIFT
}
}
/// Enumeration of the types of errors that can occur
#[derive(Debug, Clone, Copy)]
pub enum InsertError {
/// Indicates that inserts with the partition were stopped before trying to
/// insert the value. This means the map was not altered during the
/// operation
InsertsDisallowedBeforeInsert,
/// Indicates that inserts for the partition were disallowed during the
/// process of inserting the value. This means the map might have been
/// temporarily updated to contain the given value, so the caller is
/// needs to take action to correct any side effects of the value being
/// temporarily present to other consumers of this map.
///
/// Note. This does not indicate any corruption in the map.
InsertsDisallowedDuringInsert,
}
/// This is a specialized, concurrent, lock-free slotmap that allows for
/// wait-free reads and guarantees safety and well-defined behavior with only a
/// few caveats.
///
/// 1. Some operations will wait by spinning if their is operational contentions:
/// - When a new chunk needs to be allocated only one thread can do the allocation,
/// and all the others have to wait
/// 2. Currently deletes are not supported <- Todo(T117692439)
pub struct SlotMap<T> {
chunk_count_for_reads: AtomicUsize,
chunk_count_for_writes: AtomicUsize,
next_slot_key: AtomicU32,
disallowed_partition_value: AtomicU32,
/// This array stores pointers to all the allocated chunks indexed by chunk
/// id. Initially all values in the array are uninitiallized.
chunks: [ChunkPointer<T>; MAX_CHUNK_COUNT],
}
impl<T> SlotMap<T>
where
T: 'static + ?Sized + Default,
{
pub fn new() -> Self {
SlotMap {
chunk_count_for_reads: Default::default(),
chunk_count_for_writes: Default::default(),
next_slot_key: Default::default(),
disallowed_partition_value: AtomicU32::new(u32::MAX),
chunks: array![MaybeUninit::uninit(); MAX_CHUNK_COUNT],
}
}
/// Stop this map from accepting new insertions. Any insertsions in progress
/// when this is called will fail. This method can be called multiple times,
/// but only the first caller (that actually changes the state) will receive
/// true as the return value. All other calls will receive false.
pub fn stop_inserts_for_partition(&self, partition: u32) -> bool {
self.disallowed_partition_value.swap(partition, SeqCst) != partition
}
/// checks to see if inserts are allowed for the given partition
pub fn inserts_allowed_for_partition(&self, partition: u32) -> bool {
self.disallowed_partition_value.load(Acquire) != partition
}
/// Insert the given value without checking whether partitions are allowed
pub fn insert(&self, value: T) -> SlotKey {
if let Ok(key) = self.insert_impl(None, value) {
key
} else {
unreachable!("Inserts without partition are infalible");
}
}
/// Attempt to insert the given value into the slotmap with the given
/// partiion. If inserts are allowed on the given partition, the insert will
/// succeed, but if inserts are disallowed on the partition, then the insert
/// will fail.
///
/// Note. Partition is purely about exluding inserts. It has no bearing on
/// how values are stored in the slot map
pub fn try_insert(&self, partition: u32, value: T) -> Result<SlotKey, InsertError> {
self.insert_impl(Some(partition), value)
}
/// Attempt to insert the given value into the slotmap with the given
/// optional partiion. If inserts are allowed on the given partition or if
/// no partition is specified, the insert will succeed, but if inserts are
/// disallowed on the partition, then the insert will fail.
///
/// Note. Partition is purely about exluding inserts. It has no bearing on
/// how values are stored in the slot map
fn insert_impl(&self, partition_opt: Option<u32>, value: T) -> Result<SlotKey, InsertError> {
if let Some(partition) = partition_opt {
if !self.inserts_allowed_for_partition(partition) {
return Err(InsertError::InsertsDisallowedBeforeInsert);
}
}
let result = SlotKey::new(self.next_slot_key.fetch_add(1, Relaxed) + ONE_GENERATION);
let chunk_index = result.chunk_index();
// We are preallocating the space for all the possible pointers to
// chunks, so if we run out of space for chunks, we can't get more :(
assert!(chunk_index < MAX_CHUNK_COUNT, "Maximum map size exceeded");
// Check to see if a chunk has been allocated for this chunk_id
loop {
let chunk_count = self.chunk_count_for_reads.load(Acquire);
if chunk_count > chunk_index {
break;
}
if self
.chunk_count_for_writes
.compare_exchange(chunk_count, chunk_count + 1, SeqCst, Relaxed)
.is_ok()
{
let next_chunk = new_chunk();
// Allocate the next chunk. This is safe because
// 1. `chunk_count` < MAX_CHUNK_COUNT
// 2. We will be writing a valid pointer to the chunk array
// before dereferencing.
// 3. We are inside a spin lock that ensures each entry in the
// chunks array will only be written to once.
//
// TODO: remove #![allow(invalid_reference_casting)] and replace with
// UnsafeCell
unsafe {
let chunk_pointer_pointer =
self.chunks.get_unchecked(chunk_count as usize) as *const ChunkPointer<T>;
let chunk_pointer_writeable =
&mut *(chunk_pointer_pointer as *mut ChunkPointer<T>);
*chunk_pointer_writeable = next_chunk;
}
self.chunk_count_for_reads.store(chunk_count + 1, Release);
break;
}
}
// This is safe because
// 1. The ChunkPointer at the current index is guaranteed to be valid
// because of the above.
// 2. The Chunk pointed to by the chunk pointer is guaranteed to be
// allocated.
// 3. The size of the allocated chunk is greater than index in the
// slot.
unsafe {
let slot = (self.get_unchecked_slot(result) as *const Slot<T>) as *mut Slot<T>;
(*slot).1 = value;
(*slot).0.fetch_add(ONE_GENERATION, SeqCst);
// Last chance to check if inserts were blocked was called during
// insertion. If it was, then we increase the generation on
// the slot again and return None indicating the insert failed
if let Some(partition) = partition_opt {
if !self.inserts_allowed_for_partition(partition) {
(*slot).0.fetch_add(ONE_GENERATION, SeqCst);
return Err(InsertError::InsertsDisallowedDuringInsert);
}
}
}
Ok(result)
}
/// Gets a reference to the slot at the given slotkey, but with no checks to
/// ensure the slot exists and is not deleted.
unsafe fn get_unchecked_slot(&self, slot_key: SlotKey) -> &Slot<T> {
let chunk_index = slot_key.chunk_index();
let index_in_chunk = slot_key.slot_index();
let chunk = self.chunks.get_unchecked(chunk_index).assume_init_ref();
chunk.get_unchecked(index_in_chunk)
}
/// Gets a reference to the value at the given slotkey, but with no checks
/// to ensure the slot exists and is not deleted.
pub unsafe fn get_unchecked(&self, slot_key: SlotKey) -> &T {
&self.get_unchecked_slot(slot_key).1
}
/// Gets the value of the slot at the given key if the slot exists and if the
/// generation of the key matches the generation in the slot.
pub fn get(&self, slot_key: SlotKey) -> Option<&T> {
let chunk_index = slot_key.chunk_index();
let index_in_chunk = slot_key.slot_index();
let key_generation = slot_key.generation();
let chunk_count = self.chunk_count_for_reads.load(Acquire) as usize;
(chunk_count > chunk_index)
.then(|| {
// This is safe because the chunk_index is less than the number
// of chunks that have been allocated
unsafe { self.chunks.get_unchecked(chunk_index).assume_init_ref() }
})
.map(|chunk| {
// This is safe because we know the chunk will have been
// allocated, and the index in that chunk is guaranteed to be
// less than the size of the chunk.
unsafe { (*chunk).get_unchecked(index_in_chunk) }
})
.filter(|(slot_data, _)| {
SlotKey::new(slot_data.load(Relaxed)).generation() == key_generation
})
.map(|(_, v)| v)
}
/// get an iterator of all the entries in the slotmap.
pub fn entries(&self) -> impl Iterator<Item = (SlotKey, &T)> {
(0..self.chunk_count_for_reads.load(Relaxed))
.map(|chunk_index| {
// This is safe because the range we are iterating over is
// limitted to the range where we can guarantee the chunks have
// been allocated.
let chunk = unsafe { self.chunks.get_unchecked(chunk_index).assume_init_ref() };
(chunk_index, chunk)
})
.flat_map(|(chunk_idx, chunk)| repeat(chunk_idx).zip(chunk.iter().enumerate()))
.map(|(chunk_idx, (slot_idx, (slot_metadata, value)))| {
let generation = SlotKey::new(slot_metadata.load(Relaxed)).generation();
let slot_key = SlotKey::new_from_parts(chunk_idx, slot_idx, generation);
(slot_key, value)
})
.filter(|(slot_key, _)| slot_key.generation() % 2 == 1)
}
}
impl<T> Drop for SlotMap<T> {
fn drop(&mut self) {
for chunk_index in 0..self.chunk_count_for_reads.load(Relaxed) {
// This is safe because the range we are iterating over is limitted
// to the range where we can guarantee the chunks have been
// allocated.
unsafe {
self.chunks
.get_unchecked_mut(chunk_index)
.assume_init_drop();
}
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::thread;
use super::*;
fn new_slot_map() -> SlotMap<AtomicU32> {
SlotMap::new()
}
#[test]
fn test_happy_path() {
let map = new_slot_map();
let i_value_1 = 42;
let i_value_2 = 101;
// test writing
let key1 = map
.try_insert(0, AtomicU32::new(i_value_1))
.expect("Insert failed");
let key2 = map
.try_insert(0, AtomicU32::new(i_value_2))
.expect("Insert failed");
// Test reading
let v1 = map.get(key1).expect("This was just added");
let v2 = map.get(key2).expect("This was just added");
assert_eq!(v1.load(Relaxed), i_value_1);
assert_eq!(v2.load(Relaxed), i_value_2);
// Test iterating
for (key, v) in map.entries() {
if key == key1 {
assert_eq!(v.load(Relaxed), i_value_1);
} else if key == key2 {
assert_eq!(v.load(Relaxed), i_value_2);
}
}
// Test mutating internal state while iterating
for (_, v) in map.entries() {
v.fetch_add(1, Relaxed);
}
// Test reading after mutating
let v1 = map.get(key1).expect("This was just added");
let v2 = map.get(key2).expect("This was just added");
assert_eq!(v1.load(Relaxed), i_value_1 + 1);
assert_eq!(v2.load(Relaxed), i_value_2 + 1);
}
#[derive(Default, Debug)]
struct TestDroppable(usize, Option<Arc<AtomicUsize>>);
impl Drop for TestDroppable {
fn drop(&mut self) {
if let Some(drop_count) = &self.1 {
drop_count.fetch_add(1, Relaxed);
}
}
}
#[test]
fn test_drop() {
let drop_counter = Arc::new(AtomicUsize::default());
let to_drop_count = 100;
{
let map: SlotMap<TestDroppable> = SlotMap::new();
for i in 0..to_drop_count {
let _ = map
.try_insert(0, TestDroppable(i, Some(Arc::clone(&drop_counter))))
.expect("insert failed");
}
}
assert_eq!(to_drop_count, drop_counter.load(Relaxed));
}
#[test]
fn test_thread_safety() {
let thread_count = 100;
let insert_count = 10000;
let drop_counter = Arc::new(AtomicUsize::default());
// Test basic thread safety by inserting a bunch of values on different
// threads and ensure that the number dropped equals the number inserted.
{
let map: Arc<SlotMap<TestDroppable>> = Arc::new(SlotMap::new());
(0..thread_count)
.map(|thread_num| {
let map_clone = Arc::clone(&map);
let dc_clone = Arc::clone(&drop_counter);
thread::spawn(move || {
(0..insert_count)
.map(|i| {
// Do some inserting
map_clone
.try_insert(
0,
TestDroppable(
thread_num * insert_count + i,
Some(Arc::clone(&dc_clone)),
),
)
.expect("Insert failed")
})
.enumerate()
.for_each(|(i, key)| {
// And some reading
assert_eq!(
Some(thread_num * insert_count + i),
map_clone.get(key).map(|v| v.0)
);
})
})
})
.collect::<Vec<_>>()
.into_iter()
.for_each(|t| t.join().expect("Failed to join"));
}
assert_eq!(thread_count * insert_count, drop_counter.load(SeqCst));
}
#[test]
fn test_insert_after_disable() {
let map = new_slot_map();
assert!(map.try_insert(0, AtomicU32::new(1)).is_ok());
map.stop_inserts_for_partition(0);
assert!(map.try_insert(0, AtomicU32::new(2)).is_err());
// Inserting without a partition should still be allowed
let key = map.insert(AtomicU32::new(3));
assert_eq!(map.get(key).unwrap().load(SeqCst), 3);
}
}

View file

@ -1,826 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
use core::marker::PhantomData;
use core::sync::atomic::AtomicU8;
use core::sync::atomic::Ordering;
use core::sync::atomic::Ordering::*;
use core::time::Duration;
use std::time::Instant;
use atomic::Atomic;
use lazy_static::lazy_static;
use syscalls::raw::syscall0;
use syscalls::Sysno;
use crate::signal::guard;
use crate::slot_map::SlotKey;
use crate::slot_map::SlotMap;
lazy_static! {
static ref SLOT_MAP: SlotMap<ThreadRepr> = SlotMap::new();
}
thread_local! {
pub static THREAD_SLOT_KEY: Option<SlotKey> = generate_thread_and_slot_key();
}
/// We are serializing the state information directly to the bits of an unsigned
/// integer type. The layout of the bits is
///
/// 0b_xxFEDCBA
/// 543210
///
/// A => New <- 1 means this is the first time this thread was seen
/// B => 1 indicates Guest state and 0 indicates Handler state
/// C => Needs to exit
/// D => Exiting (Supersedes A)
/// E => Exited (Supersedes A & B)
/// F => Forking - Indicates or clonging or vforking (Only valid in Guest state)
///
/// This pattern enables lets us update the thread's state (in the allowed ways)
/// and the thread's `needs_to_exit` flag independently and atomically
type StateRepr = u8;
type AtomicStateRepr = AtomicU8;
type ThreadRepr = (Atomic<PidTid>, AtomicStateRepr);
// We want to make sure PidTid actually fits into the the Atomic type, so here
// we check its size and that its alignment divides evenly into 8
const _: () = assert!(core::mem::size_of::<PidTid>() == 8);
const _: () = assert!(8 % core::mem::align_of::<PidTid>() == 0);
// Constants to describe the bit pattern above
const NEW_SHIFT: u8 = 0;
const NEW_MASK: u8 = 1 << NEW_SHIFT;
const HANDLER_GUEST_SHIFT: u8 = 1;
const HANDLER_GUEST_MASK: u8 = 1 << HANDLER_GUEST_SHIFT;
const NEEDS_TO_EXIT_SHIFT: u8 = 2;
const NEEDS_TO_EXIT_MASK: u8 = 1 << NEEDS_TO_EXIT_SHIFT;
const EXITED_SHIFT: u8 = 3;
const EXITED_MASK: u8 = 1 << EXITED_SHIFT;
const EXITING_SHIFT: u8 = 4;
const EXITING_MASK: u8 = 1 << EXITING_SHIFT;
const FORKING_SHIFT: u8 = 5;
const FORKING_MASK: u8 = 1 << FORKING_SHIFT;
/// Gets the value of the `needs_to_exit` flag from the thread representation.
const fn needs_to_exit(thread_repr: StateRepr) -> bool {
(thread_repr & NEEDS_TO_EXIT_MASK) > 0
}
/// Gets the inverted value of the `not_new` flag from the thread
/// representation.
const fn is_new(thread_repr: StateRepr) -> bool {
(thread_repr & NEW_MASK) > 0
}
/// Sets the `needs_to_exit` flag in the given representation to true.
fn store_needs_to_exit(atomic_repr: &AtomicStateRepr, ordering: Ordering) -> StateRepr {
atomic_repr.fetch_or(NEEDS_TO_EXIT_MASK, ordering) | NEEDS_TO_EXIT_MASK
}
/// Constructs a representation of a thread from its components.
const fn build_repr(thread_state: ThreadState, needs_to_exit: bool) -> StateRepr {
thread_state.as_u8() + (((needs_to_exit as u8) << NEEDS_TO_EXIT_SHIFT) & NEEDS_TO_EXIT_MASK)
}
/// Return the value of the forking flag in the given state
const fn is_forking(thread_rep: StateRepr) -> bool {
thread_rep & FORKING_MASK > 0 && thread_rep & HANDLER_GUEST_MASK > 0
}
/// Set the forking flag to false in the atomic repr and return the result
fn clear_forking_flag(atomic_repr: &AtomicStateRepr, order: Ordering) -> StateRepr {
atomic_repr.fetch_and(!FORKING_MASK, order) & !FORKING_MASK
}
/// Convenience function for getting the current thread id via a syscall
fn thread_id_syscall() -> u32 {
// Unsafe unwrap is okay here because this syscall is guaranteed not to fail
unsafe { syscall0(Sysno::gettid) as u32 }
}
/// Convenience function for getting the current process id via a syscall
fn process_id_syscall() -> u32 {
// Unsafe unwrap is okay here because this syscall is guaranteed not to fail
unsafe { syscall0(Sysno::getpid) as u32 }
}
/// Called by the `thread_local` macro (during TLS initialization) to add the
/// calling thread's metadata to the static map of all threads and store its key
/// as a thread-local variable. If the thread metadata slotmap has had inputs
/// disabled, the slotkey stored in this variable will be None
fn generate_thread_and_slot_key() -> Option<SlotKey> {
let pid_tid = PidTid::current();
SLOT_MAP
.try_insert(
pid_tid.pid,
(
Atomic::new(pid_tid),
AtomicStateRepr::new(NEW_MASK | HANDLER_GUEST_MASK),
),
)
.ok()
}
#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq)]
pub struct PidTid {
pub pid: u32,
pub tid: u32,
}
impl PidTid {
fn current() -> Self {
PidTid {
pid: process_id_syscall(),
tid: thread_id_syscall(),
}
}
}
/// Possible error responses during a transition to/from guest execution.
#[derive(Debug, Clone, Copy)]
pub enum GuestTransitionErr {
/// This thread needs to exit, and the caller is responsible.
ExitNow,
/// This thread is exiting/exited but the caller is not responsible.
ExitingElsewhere,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum ThreadState {
Guest(bool),
Handler,
Exiting,
Exited,
}
impl From<StateRepr> for ThreadState {
fn from(state_repr: StateRepr) -> Self {
if (state_repr & EXITED_MASK) > 0 {
ThreadState::Exited
} else if (state_repr & EXITING_MASK) > 0 {
ThreadState::Exiting
} else if (state_repr & HANDLER_GUEST_MASK) > 0 {
ThreadState::Guest(is_forking(state_repr))
} else {
ThreadState::Handler
}
}
}
impl ThreadState {
const fn as_u8(&self) -> u8 {
use ThreadState::*;
match self {
Guest(false) => HANDLER_GUEST_MASK,
Guest(true) => HANDLER_GUEST_MASK | FORKING_MASK,
Handler => 0,
Exiting => EXITING_MASK,
Exited => EXITED_MASK,
}
}
/// Writes this thread state to the given atomic thread representation and
/// return the resulting representation.
fn store(&self, atomic_repr: &AtomicStateRepr, ordering: Ordering) -> StateRepr {
if *self == ThreadState::Handler {
let handle_mask = !(HANDLER_GUEST_MASK | FORKING_MASK);
atomic_repr.fetch_and(handle_mask, ordering) & handle_mask
} else {
let state_u8 = self.as_u8();
atomic_repr.fetch_or(state_u8, ordering) | state_u8
}
}
}
/// Trait representing the notifications that can be raised from a thread
/// during its lifecycle
pub trait EventSink {
fn on_new_thread(_pid_tid: PidTid) {}
fn on_thread_exit(_pid_tid: PidTid) {}
}
#[derive(Debug, Clone, Copy)]
pub struct Thread<E: EventSink> {
slot_key: SlotKey,
/// True if this is the first time the thread was seen.
new: bool,
forking: bool,
/// Raw representation of the thread as a unsigned integer.
repr: StateRepr,
/// All the fields contained in the repr.
state: ThreadState,
needs_to_exit: bool,
/// Funny phantom here to let the drop checker know it doesn't need to
/// worry about `E`
_phantom: PhantomData<dyn Fn(E) + Send + Sync>,
}
fn get_threads_for_process<'a>(
process_id: u32,
) -> impl Iterator<Item = (SlotKey, PidTid, &'a AtomicStateRepr)> {
SLOT_MAP
.entries()
.map(|(slot_key, (atomic_pid_tid, atomic_repr))| {
(slot_key, atomic_pid_tid.load(Relaxed), atomic_repr)
})
.filter(move |(_, pid_tid, _)| pid_tid.pid == process_id)
}
/// Indicates to all threads that they need to exit, and wait for all
/// threads to confirm they are exited. The closure given is a way to signal
/// to a thread by id to trigger a critical section transition which will
/// observe and handle the need to exit.
pub fn exit_all<F>(signal_guest_thread: F) -> Option<u32>
where
F: Fn(SlotKey, PidTid),
{
let exiting_pid = process_id_syscall();
// Only go through the motions of exiting all threads if the disallow
// flag is successfully set
disallow_new_threads().then(|| {
// Iterate through all the slots and mark each thread as
// `needs_to_exit`. Collect the PidTids of threads that aren't
// already exited, so we "exit_waiters" can wait for the
get_threads_for_process(exiting_pid)
.map(|(slot_key, pid_tid, atomic_repr)| {
// Read each thread's state and mark it as needing to exit.
// Marking this flag is safe even on threads that have
// already exited because the exiting and exited flages
// take precenence
let state: ThreadState = store_needs_to_exit(atomic_repr, SeqCst).into();
(slot_key, pid_tid, state)
})
.for_each(|(slot_key, pid_tid, state)| {
// Signal any threads in guest state in case they are in a
// blocking syscall, so they can exit
if state == ThreadState::Guest(false) {
signal_guest_thread(slot_key, pid_tid)
}
});
exiting_pid
})
}
/// Waits for all threads to exit or for the given optional timeout to
/// expire. The boolean returned will be true if all threads exited before
/// the timeout and false if the timeout elapsed before all threads exited
pub fn wait_for_all_to_exit(process_id: u32, timeout_opt: Option<Duration>) -> bool {
let start_time = Instant::now();
// Spin until all the given keyed threads have exited or timeout has expired
loop {
if !get_threads_for_process(process_id)
.map(|(_, _, atomic_repr)| atomic_repr.load(Relaxed))
.map(ThreadState::from)
.any(|state| state != ThreadState::Exited)
{
return true;
}
if let Some(timeout) = timeout_opt {
if timeout > start_time.elapsed() {
return false;
}
}
}
}
/// Checks the slotmap to see if new inserts are allowed. This is done as an
/// atomic operation with `Acquire` ordering
fn new_threads_allowed() -> bool {
SLOT_MAP.inserts_allowed_for_partition(process_id_syscall())
}
/// Set the slotmap to stop allowing inserts. This will instantly stop
/// new threads from being inserted into the slot map. The returned boolean
/// indicates whether the property was changed by this call or not
fn disallow_new_threads() -> bool {
SLOT_MAP.stop_inserts_for_partition(process_id_syscall())
}
impl<E: EventSink> Thread<E> {
/// Gets the thread data associated with the current thread. If no data is
/// associated with the current thread, then a new instance will be created
/// and returned associated with the current thread's id
pub fn current() -> Option<Thread<E>> {
let thread_slot_key = THREAD_SLOT_KEY
.try_with(|v| *v)
.expect("Slot key should always be readable in TLS (Even if it's None)")?;
let (_, atomic_thread_repr) = SLOT_MAP.get(thread_slot_key)?;
let mut result = Thread::new_with_repr(thread_slot_key, atomic_thread_repr.load(Acquire));
// If the thread is marked as new, we need to mark the repr as no longer
// new but keep the new field marked in the returned instance.
if result.new {
let _guard = guard::enter_implicit_signal_exclusion_zone();
E::on_new_thread(PidTid::current());
// Unset the `new` flag in the atomic repr, so no other calls to
// current can return a "new" thread
let mut final_repr = atomic_thread_repr.fetch_and(!NEW_MASK, Relaxed) & !NEW_MASK;
// Lastly check the flag that indicates whether we are allowing
// new threads. If it is true, we need to mark this new thread
// as `needs_to_exit`.
if !new_threads_allowed() {
final_repr = store_needs_to_exit(atomic_thread_repr, Release);
}
result.update_from_repr(final_repr);
} else if result.forking {
let _guard = guard::enter_implicit_signal_exclusion_zone();
// Read the actual pid-tid through syscalls
let actual_pid_tid = PidTid::current();
let stored_pid_tid = result.get_process_and_thread_ids();
E::on_new_thread(actual_pid_tid);
// Fix the stored state for this thread to match the current thread.
// A new pid-tid here means the thread (and likely the process) is
// new
if actual_pid_tid != stored_pid_tid {
result.fix_stored_state_after_fork(actual_pid_tid);
result.new = true;
}
clear_forking_flag(atomic_thread_repr, SeqCst);
result.forking = false;
}
Some(result)
}
/// Creates a new thread with the given id and representation.
fn new_with_repr(slot_key: SlotKey, repr: StateRepr) -> Self {
Thread {
slot_key,
new: is_new(repr),
forking: is_forking(repr),
state: repr.into(),
needs_to_exit: needs_to_exit(repr),
repr,
_phantom: Default::default(),
}
}
/// Get the process and thread ids associated with this thread
pub fn get_process_and_thread_ids(&self) -> PidTid {
unsafe { SLOT_MAP.get_unchecked(self.slot_key).0.load(Relaxed) }
}
fn get_atomic_repr(&self) -> &AtomicStateRepr {
// This is safe because we are using the slot key for the thread that
// must have been created by inserting into the slotmap.
unsafe { &SLOT_MAP.get_unchecked(self.slot_key).1 }
}
/// Updates the thread state to the give value in both this object and the
/// storage map.
///
/// NOTE: This function has the side effect of updating the other fields in
/// this instance associated with the storage representation. Specifically
/// if the `needs_to_exit` flag gets set externally, that change will be
/// reflected in this thread
fn set_state(&mut self, new_thread_state: ThreadState, ordering: Ordering) -> ThreadState {
self.update_from_repr(new_thread_state.store(self.get_atomic_repr(), ordering))
}
/// Attempts to set the whole representation of the thread at once, but only
/// if the existing repr matches the one in this instance. Returns true if
/// the cas succeeds.
///
/// NOTE: The fields for this instance will be updated based on the new repr
/// state regardless of whether the compare and swap succeeds or not.
fn compare_and_swap_repr(&mut self, new_repr: StateRepr, ordering: Ordering) -> bool {
let cas_result = self
.get_atomic_repr()
.compare_exchange(self.repr, new_repr, ordering, Relaxed);
// Regardless of what the final state was, update this instance to match it
match cas_result {
Ok(new_repr) | Err(new_repr) => self.update_from_repr(new_repr),
};
cas_result.is_ok()
}
/// Updates the fields in this thread instance based on the given
/// representation.
fn update_from_repr(&mut self, new_repr: StateRepr) -> ThreadState {
self.repr = new_repr;
self.forking = is_forking(new_repr);
self.state = new_repr.into();
self.needs_to_exit = needs_to_exit(new_repr);
self.state
}
/// Set the thread's state to indicate it is leaving the guest's execution and
/// entering Reverie's.
/// Note - If at any point the `need_to_exit` flag is set, this function will
/// start the thread exit progress
pub fn leave_guest_execution(&mut self) -> Result<(), GuestTransitionErr> {
self.checked_state_transition(ThreadState::Handler)
}
/// Set the thread's state to indicate it is leaving Reverie's control and re-entering
/// the guest's execution
/// Note - If at any point the `need_to_exit` flag is set, this function will
/// start the thread exit progress
pub fn enter_guest_execution(&mut self) -> Result<(), GuestTransitionErr> {
self.checked_state_transition(ThreadState::Guest(false))
}
/// Set the thread's state atomically to the given state, and if the needs_to_exit flag
/// has been set before or after the change, attempt to exit and return and Result::Err
/// indicating if the exit was successful
fn checked_state_transition(
&mut self,
new_state: ThreadState,
) -> Result<(), GuestTransitionErr> {
if self.needs_to_exit {
Err(self.try_exit_during_transistion())
} else {
self.set_state(new_state, Release);
if self.needs_to_exit {
Err(self.try_exit_during_transistion())
} else {
Ok(())
}
}
}
/// Run the given closure with this thread's state set to guest by switching
/// in and out of guest execution before and after running the closure
pub fn execute_as_guest<F, R>(&mut self, to_run: F) -> Result<R, GuestTransitionErr>
where
F: FnOnce() -> R,
{
let _anti_guard = guard::reenter_signal_inclusion_zone();
self.enter_guest_execution()?;
let result = to_run();
self.leave_guest_execution()?;
Ok(result)
}
/// Convenience method that calls try_exit, but wraps the resulting boolean in
/// the correct error response based on whether the state was successfully set
/// to `Exited`
fn try_exit_during_transistion(&mut self) -> GuestTransitionErr {
if self.try_exit() {
GuestTransitionErr::ExitNow
} else {
GuestTransitionErr::ExitingElsewhere
}
}
/// Attempts to start the exiting process by
/// 1. Verify ownership of the thread by being the first set the state to
/// `Exiting`.
/// 2. Set the state to Exited.
///
/// Returning `true` informs the caller that this operation successfully put
/// the thread into `Exited` state; it's then the caller's responsibility to
/// actually exit the thread.
pub fn try_exit(&mut self) -> bool {
let exiting_state = build_repr(ThreadState::Exiting, true);
if self.compare_and_swap_repr(exiting_state, SeqCst) {
let _guard = guard::enter_signal_exclusion_zone();
self.set_state(ThreadState::Exited, Acquire);
E::on_thread_exit(self.get_process_and_thread_ids());
true
} else {
false
}
}
/// Fix the stored pid-tid and thread states after a fork which could have
/// corrupted both (from the point of view of this process).
fn fix_stored_state_after_fork(&self, actual_pid_tid: PidTid) {
// This is safe because a thread cannot have an invalid slot key
let (pid_tid, atomic_repr) = unsafe { SLOT_MAP.get_unchecked(self.slot_key) };
// Override whatever the guest state was with a forking guest state
let new_state = ThreadState::Guest(true).as_u8();
atomic_repr.store(new_state, SeqCst);
// Override the pid-tid with the actual pid-tid is
pid_tid.store(actual_pid_tid, SeqCst);
// If new threads are not allowed in this process, it means this thread
// needs to exit. If this flag was orignally set in the parent, then
// we overwrote it above, and need to set it to exit manually
if !new_threads_allowed() {
store_needs_to_exit(atomic_repr, SeqCst);
}
}
/// This function is designed to wrap a function that might fork the current
/// thread into a new thread in a new process. It indicates that it is in
/// the process of forking, and special care should be taken not to assume
/// the thread's stored id is valid.
///
/// It is valid for the given function not to return in the child's
/// execution context. Any cleanup that needs to happen in the child's
/// execution related to forking state will be cleaned up on the child's
/// first syscall.
pub fn maybe_fork_as_guest<F>(
&mut self,
maybe_forking_fn: F,
) -> Result<usize, GuestTransitionErr>
where
F: FnOnce() -> usize,
{
if new_threads_allowed() {
// Keep track of the starting thread id here. There is the
// possibility with something like vfork that the thread id could be
// changed, but when we change it back below, we don't want to tell
// the user it's a new thread
let starting_pid_tid = self.get_process_and_thread_ids();
self.checked_state_transition(ThreadState::Guest(true))?;
// It is possible that this function might not return, but that's ok
let fork_result = maybe_forking_fn();
// If there was no fork or this is the parent of the fork, we need
// to make sure the thread id is still correct
// Get the pid-tid stored in the slotmap for this thread
let ending_pid_tid = self.get_process_and_thread_ids();
// Read the actual pid-tid through syscalls
let actual_pid_tid = PidTid::current();
// If the pid-tid combo changed during start and finish, it means
// there was a fork or a vfork and we need to fix the stored ids
if ending_pid_tid != actual_pid_tid {
self.fix_stored_state_after_fork(actual_pid_tid);
// If the actual pid is not the same as the starting pid, then
// likely we are in the child of a fork. That means the thread
// is new.
if starting_pid_tid != actual_pid_tid {
E::on_new_thread(actual_pid_tid);
}
}
// Leave the guest state which, and clear the forking flag
self.leave_guest_execution()?;
Ok(fork_result)
} else {
Ok(0)
}
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use std::mem;
use std::ptr;
use std::sync::atomic::fence;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use std::sync::Mutex;
use std::thread::spawn;
use super::*;
/// Each of these unit tests is mutating the global slotmap, so they
/// can't interfere with each other
static UNIT_TEST_LOCK: Mutex<()> = Mutex::new(());
lazy_static! {
/// Keep track of the process and thread ids raised in start and exit
/// events
static ref THREADS_STARTED: Mutex<HashSet<PidTid>> = Mutex::new(HashSet::default());
static ref THREADS_EXITED: Mutex<HashSet<PidTid>> = Mutex::new(HashSet::default());
}
struct TestEventSink;
impl EventSink for TestEventSink {
fn on_new_thread(pid_tid: PidTid) {
assert!(
THREADS_STARTED.lock().unwrap().insert(pid_tid),
"Already started {:?}",
pid_tid
);
}
fn on_thread_exit(pid_tid: PidTid) {
assert!(
THREADS_EXITED.lock().unwrap().insert(pid_tid),
"Already exited {:?}",
pid_tid
);
}
}
fn current_test_thread() -> Option<Thread<TestEventSink>> {
Thread::<TestEventSink>::current()
}
pub fn run_test_in_new_thread<T>(t: T)
where
T: 'static + Send + FnOnce(),
{
let guard = UNIT_TEST_LOCK.lock().unwrap();
// Here we are replacing the global slot map with a new one for every
// test run. This is safe because each test is running inside a single
// mutex, so only one test will be accessing the static variable at once
unsafe {
// FIXME(JakobDegen): This is UB
let slot_map_mut = ptr::addr_of!(*SLOT_MAP).cast_mut();
*slot_map_mut = SlotMap::<ThreadRepr>::new();
}
THREADS_STARTED.lock().unwrap().clear();
THREADS_EXITED.lock().unwrap().clear();
fence(SeqCst);
let test_result = spawn(t).join();
mem::drop(guard);
// A test failure inside the closure won't cause the actual test to
// fail, so we panic here to propogate the error
if let Err(e) = test_result {
panic!("This test failed - {:?}", e);
}
}
/// Panic if the given thread's id wasn't provided in a notification as a
/// thread that exited
fn assert_exit_signal_received<E: EventSink>(thread: &Thread<E>) {
assert!(
THREADS_EXITED
.lock()
.unwrap()
.contains(&thread.get_process_and_thread_ids())
);
}
/// Panic if the given thread's id wasn't provided in a notification as a
/// thread that started
fn assert_start_signal_received<E: EventSink>(thread: &Thread<E>) {
assert!(
THREADS_STARTED
.lock()
.unwrap()
.contains(&thread.get_process_and_thread_ids())
);
}
#[test]
pub fn test_thread_lifecycle() {
run_test_in_new_thread(|| {
let mut thread = current_test_thread().expect("A thread should have been created here");
assert!(thread.new);
let _guard = guard::enter_signal_exclusion_zone();
assert_start_signal_received(&thread);
// Threads should always be in guest state upon loading.
assert_eq!(thread.state, ThreadState::Guest(false));
assert!(thread.leave_guest_execution().is_ok());
// Threads should always be in guest state upon loading.
assert_eq!(thread.state, ThreadState::Handler);
// Make sure executing as guest has the right state in and out of
// execution.
assert!(
thread
.execute_as_guest(|| {
assert_eq!(
current_test_thread()
.expect("Should be able to get thread more than once")
.state,
ThreadState::Guest(false)
);
})
.is_ok()
);
assert_eq!(thread.state, ThreadState::Handler);
store_needs_to_exit(thread.get_atomic_repr(), SeqCst);
// Once needs to exit is set, transitions should fail and cause the
// thread to go to an error state.
assert!(matches!(
thread.enter_guest_execution(),
Err(GuestTransitionErr::ExitNow)
));
assert_eq!(thread.state, ThreadState::Exited);
assert_exit_signal_received(&thread);
// Once exited a thread cannot return to guest or handler state even
// if you set the state directly (which clients can't).
thread.set_state(ThreadState::Guest(false), Acquire);
assert_eq!(thread.state, ThreadState::Exited);
thread.set_state(ThreadState::Handler, Acquire);
assert_eq!(thread.state, ThreadState::Exited);
thread.set_state(ThreadState::Exiting, Acquire);
assert_eq!(thread.state, ThreadState::Exited);
})
}
#[test]
fn test_new_thread_beahvior() {
run_test_in_new_thread(|| {
// This thread should be new
let thread = current_test_thread().expect("A thread should have been created here");
assert!(thread.new);
assert_start_signal_received(&thread);
// But now it's shouldn't be
let thread = current_test_thread().expect("A thread should have been created here");
assert!(!thread.new);
})
}
#[test]
fn test_disallow_new_threads() {
run_test_in_new_thread(|| {
// Check that the flag is not set at first
assert!(
spawn(|| current_test_thread().is_some())
.join()
.expect("Join error")
);
// This will exit all the threads (we don't actually know how many)
exit_all(|_, _| {});
// Now new threads should not be allowed
assert!(
spawn(|| current_test_thread().is_none())
.join()
.expect("Join error")
);
})
}
#[test]
fn test_exit_all() {
run_test_in_new_thread(|| {
let guest = current_test_thread().expect("This thread should be present");
let mut handler = spawn(|| current_test_thread().expect("This one too"))
.join()
.expect("Failed to join");
assert_start_signal_received(&guest);
assert_start_signal_received(&handler);
assert!(handler.leave_guest_execution().is_ok());
let guest_notified = Arc::new(AtomicBool::new(false));
let gn_clone = Arc::clone(&guest_notified);
let handler_notified = Arc::new(AtomicBool::new(false));
let hn_clone = Arc::clone(&handler_notified);
// This thread is new and should be in guest state, so it should get
// notified.
exit_all(move |slot_key, _| {
if slot_key == guest.slot_key {
gn_clone.store(true, SeqCst);
} else if slot_key == handler.slot_key {
hn_clone.store(true, SeqCst);
}
});
// But only the guest should get notified
assert!(guest_notified.load(Acquire));
assert!(!handler_notified.load(Acquire));
// Both threads should be marked as needs to exit
assert!(needs_to_exit(guest.get_atomic_repr().load(Relaxed)));
assert!(needs_to_exit(handler.get_atomic_repr().load(Relaxed)));
})
}
}

View file

@ -1,190 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
use core::time::Duration;
use reverie_rpc::MakeClient;
use reverie_syscalls::LocalMemory;
use reverie_syscalls::Syscall;
use syscalls::syscall;
use syscalls::Errno;
use syscalls::Sysno;
use super::protected_files::uses_protected_fd;
use super::utils;
use super::vdso;
use crate::ffi::fn_icept;
/// A trait that every Reverie *tool* must implement. The primary function of the
/// tool specifies how syscalls and signals are handled.
///
/// The type that a `Tool` is implemented for represents the process-level state.
/// That is, one runtime instance of this type will be created for each guest
/// process. This type is in turn a factory for *thread level states*, which are
/// allocated dynamically upon guest thread creation. Instances of the thread
/// state are also managed by Reverie.
pub trait Tool {
/// The client used to make global state RPC calls.
///
/// This can be set to the unit type `()` if the tool does not require
/// global state.
type Client: MakeClient;
/// Called when the process first starts. The global state RPC client is
/// passed in and may be used to send/recv messages to/from the global
/// state.
///
/// The point at which this is called in the lifetime of the process is
/// undefined. It may not be called until *after* libc is loaded.
fn new(client: Self::Client) -> Self;
/// This is called in place of a system call. For example, if the program
/// called the `open` syscall, this callback would be called instead. By
/// default, the real syscall is simply called.
#[inline]
fn syscall(&self, syscall: Syscall, _memory: &LocalMemory) -> Result<usize, Errno> {
unsafe { syscall.call() }
}
/// Called when a thread first starts.
#[inline]
fn on_thread_start(&self, _thread_id: u32) {}
/// Called just before the thread exits. A thread may exit due a variety of
/// reasons:
/// - The thread called `exit(2)`.
/// - This, or another, thread called `exit_group(2)`.
/// - This thread was killed by a signal.
///
/// NOTE: From the tool's persective, it is possible for this api method to
/// be called without the accompanying call to `on_thread_start` for the
/// same thread id. This is very unlikely, but can happen if a thread is
/// signaled to exit before `on_thread_start` is called.
#[inline]
fn on_thread_exit(&self, _thread_id: u32) {}
/// Called whenever the `rdtsc` instruction was executed. This should return
/// the RDTSC timestamp counter.
///
/// By default, this returns the result of the `rdtsc` instruction.
#[inline]
fn rdtsc(&self) -> u64 {
unsafe { core::arch::x86_64::_rdtsc() }
}
/// Called whenever the VDSO function `clock_gettime` was called. By
/// default, the original `clock_gettime` VDSO function is called.
#[inline]
fn vdso_clock_gettime(&self, clockid: libc::clockid_t, tp: *mut libc::timespec) -> i32 {
unsafe { vdso::clock_gettime(clockid, tp) }
}
/// Called whenever the VDSO function `getcpu` was called. By default, the
/// original `getcpu` VDSO function is called.
#[inline]
fn vdso_getcpu(&self, cpu: *mut u32, node: *mut u32, _unused: usize) -> i32 {
unsafe { vdso::getcpu(cpu, node, _unused) }
}
/// Called whenever the VDSO function `gettimeofday` was called. By default,
/// the original `gettimeofday` VDSO function is called.
#[inline]
fn vdso_gettimeofday(&self, tv: *mut libc::timeval, tz: *mut libc::timezone) -> i32 {
unsafe { vdso::gettimeofday(tv, tz) }
}
/// Called whenever the VDSO function `time` was called. By default, the
/// original `time` VDSO function is called.
#[inline]
fn vdso_time(&self, tloc: *mut libc::time_t) -> i32 {
unsafe { vdso::time(tloc) }
}
/// Returns the time limit for waiting for all threads to exit when an
/// `exit_group` syscall is observed. By default, `None` is returned, which
/// indicates no timeout. That is, the default behavior is to wait
/// indefinitely for all threads to exit.
fn get_exit_timeout(&self) -> Option<Duration> {
None
}
/// Called when the timeout duration provided by `get_exit_timeout` elapses
/// before all threads in the guest application exit. The default behavior
/// in this case is to issue an un-intercepted `exit_group(1)` syscall.
fn on_exit_timeout(&self) -> usize {
let _ = unsafe { syscalls::syscall1(Sysno::exit_group, 1) };
unreachable!("All threads will exit before this is called")
}
/// Called when a signal of the given value is received before any handlers
/// for that signal are evaluated
fn handle_signal_event(&self, _signal: i32) {}
/// This is called early on from sbr_init to get a list of functions we want
/// to be detoured. Because this is called very early on, this function
/// should not be doing *any* allocations or library calls.
fn detours() -> &'static [fn_icept] {
&[]
}
}
pub trait ToolGlobal {
type Target: Tool + 'static;
/// Returns a reference to the global (process-level) instance of this tool.
fn global() -> &'static Self::Target;
}
/// Helper methods for syscalls.
pub trait SyscallExt {
/// Executes the syscall, returning the result.
unsafe fn call(self) -> Result<usize, Errno>;
}
impl SyscallExt for Syscall {
unsafe fn call(self) -> Result<usize, Errno> {
use reverie_syscalls::SyscallInfo;
let (sysno, args) = self.into_parts();
// Some syscalls need to be handled in a special way.
if sysno == Sysno::readlink {
utils::sys_readlink(
args.arg0 as *const libc::c_char,
args.arg1 as *mut libc::c_char,
args.arg2 as usize,
)
} else if sysno == Sysno::execve {
utils::sys_execve(
args.arg0 as *const libc::c_char,
args.arg1 as *const *const libc::c_char,
args.arg2 as *const *const libc::c_char,
)
} else if sysno == Sysno::rt_sigprocmask {
utils::sys_rt_sigprocmask(
args.arg0 as libc::c_int,
args.arg1 as *const _,
args.arg2 as *mut _,
args.arg3 as usize,
)
} else if sysno == Sysno::close && args.arg0 == libc::STDERR_FILENO as usize {
// Prevent stderr from getting closed. We need this for logging
// purposes.
// FIXME: All logging should go through the global state instead.
Ok(0)
} else if uses_protected_fd(sysno, args.arg0, args.arg1) {
// If this syscall operates on a protected file descriptor, we
// should return EBADF to indicate that the file descriptor isn't
// opened (even if it really is).
Err(Errno::EBADF)
} else {
syscall!(
sysno, args.arg0, args.arg1, args.arg2, args.arg3, args.arg4, args.arg5
)
}
}
}

View file

@ -1,178 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
use std::ffi::CStr;
use syscalls::syscall3;
use syscalls::Errno;
use syscalls::Sysno;
use super::paths;
use crate::callbacks::CONTROLLED_EXIT_SIGNAL;
/// `readlink` needs to be handled in a special way. If we're trying to read
/// `/proc/self/exe`, then we can't return the path to the sabre executable. We
/// need to replace it with the path to the real binary.
///
/// NOTE: This doesn't handle numerous other cases such as:
/// 1. Using `readlinkat(-100, "/proc/self/exe", ...)`
/// 2. Using `readlinkat(dir_fd, "exe", ...)`
/// 3. Using `readlink("/proc/{pid}/exe", ...)`
pub fn sys_readlink(
path: *const libc::c_char,
buf: *mut libc::c_char,
bufsize: usize,
) -> Result<usize, Errno> {
if unsafe { CStr::from_ptr(path) }.to_bytes() == b"/proc/self/exe" {
if buf.is_null() {
return Err(Errno::EFAULT);
}
let client_path = paths::client_path();
let len = client_path.to_bytes().len().min(bufsize);
unsafe { core::ptr::copy_nonoverlapping(client_path.as_ptr(), buf, len) };
Ok(len)
} else {
unsafe {
syscall3(
Sysno::readlink,
path as usize,
buf as usize,
bufsize as usize,
)
}
}
}
/// `execve` needs to be handled in a special way because, in order to trace
/// child processes after they call execve, we need to run the child process as
/// `sabre plugin.so -- child` instead.
pub fn sys_execve(
filename: *const libc::c_char,
argv: *const *const libc::c_char,
envp: *const *const libc::c_char,
) -> Result<usize, Errno> {
// FIXME: This is subject to race conditions!
if unsafe { libc::access(filename, libc::F_OK) } != 0 {
return Err(Errno::ENOENT);
}
// Count the number of arguments so we only need to do one allocation.
let mut argc = 0;
while !(unsafe { *argv.add(argc) }).is_null() {
argc += 1;
}
let sabre = paths::sabre_path().as_ptr();
let mut new_argv = Vec::with_capacity(argc + 4);
new_argv.push(sabre);
new_argv.push(paths::plugin_path().as_ptr());
new_argv.push(b"--\0".as_ptr() as *const libc::c_char);
// FIXME: Overwrite arg0 so it contains an absolute path. Sabre can only
// take absolute paths at the moment.
new_argv.push(filename);
// Append the original argv (except arg0)
for i in 1..argc {
new_argv.push(unsafe { *argv.add(i) });
}
new_argv.push(core::ptr::null());
// Never returns if successful. Thus, it doesn't matter if our Vec is
// dropped.
unsafe {
syscall3(
Sysno::execve,
sabre as usize,
new_argv.as_ptr() as usize,
envp as usize,
)
}
}
/// glibc defines this to be much larger than what the kernel accepts. Since we
/// have to make a direct syscall to `rt_sigaction`, we must use the same sigset
/// as the kernel does.
///
/// The kernel currently uses 64 bits for the sigset. See:
/// https://elixir.bootlin.com/linux/v5.17.5/source/arch/x86/include/uapi/asm/signal.h#L17
#[derive(Copy, Clone, Default)]
#[repr(C)]
pub struct KernelSigset(u64);
impl KernelSigset {
/// Check if the sigset contains a signal.
#[allow(unused)]
pub fn contains(&self, sig: libc::c_int) -> bool {
let mask = sigmask(sig);
(self.0 & mask) == mask
}
/// Removes the given signal from the sigset.
pub fn remove(&mut self, sig: libc::c_int) {
let mask = sigmask(sig);
self.0 &= !(mask as u64)
}
}
#[inline]
fn sigmask(sig: libc::c_int) -> u64 {
// wrapping_sub is safe because signal numbers start at 1.
1 << (sig as u64).wrapping_sub(1)
}
/// rt_sigprocmask needs special handling because if the guest tries to set a
/// signal mask that prevents our control signal from being received by a
/// thread, we are going to create and pass our own sigset that only differs
/// from the client's in that it does not suppress our control signal
pub fn sys_rt_sigprocmask(
operation: libc::c_int,
sigset_ptr: *const KernelSigset,
prev_sigset_ptr: *mut KernelSigset,
// Should always 8 for x86_64
sigset_size: usize,
) -> Result<usize, Errno> {
if sigset_ptr.is_null() {
return unsafe {
syscalls::syscall4(
Sysno::rt_sigprocmask,
operation as usize,
sigset_ptr as usize,
prev_sigset_ptr as usize,
sigset_size as usize,
)
};
}
let mut new_sigset = unsafe { *sigset_ptr };
if matches!(operation, libc::SIG_SETMASK | libc::SIG_BLOCK) {
new_sigset.remove(CONTROLLED_EXIT_SIGNAL);
}
unsafe {
syscalls::syscall4(
Sysno::rt_sigprocmask,
operation as usize,
&new_sigset as *const _ as usize,
prev_sigset_ptr as usize,
sigset_size as usize,
)
}
}
#[inline]
pub fn is_vfork(sys_no: Sysno, arg1: usize) -> bool {
const VFORK_FLAGS: usize = (libc::CLONE_VM | libc::CLONE_VFORK | libc::SIGCHLD) as usize;
sys_no == Sysno::vfork || (sys_no == Sysno::clone && (arg1 & VFORK_FLAGS == VFORK_FLAGS))
}

View file

@ -1,21 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
use super::ffi;
#[allow(non_upper_case_globals)]
pub static mut clock_gettime: ffi::vdso_clock_gettime_fn = ffi::vdso_clock_gettime_stub;
#[allow(non_upper_case_globals)]
pub static mut getcpu: ffi::vdso_getcpu_fn = ffi::vdso_getcpu_stub;
#[allow(non_upper_case_globals)]
pub static mut gettimeofday: ffi::vdso_gettimeofday_fn = ffi::vdso_gettimeofday_stub;
#[allow(non_upper_case_globals)]
pub static mut time: ffi::vdso_time_fn = ffi::vdso_time_stub;

View file

@ -1,20 +0,0 @@
# @generated by autocargo from //hermetic_infra/reverie/experimental/riptrace:main
[package]
name = "main"
version = "0.1.0"
authors = ["Meta Platforms"]
edition = "2021"
repository = "https://github.com/facebookexperimental/reverie"
license = "BSD-2-Clause"
[dependencies]
anyhow = "1.0.75"
async-trait = "0.1.71"
clap = { version = "3.2.25", features = ["derive", "env", "regex", "unicode", "wrap_help"] }
colored = "2.1.0"
reverie-host = { version = "0.1.0", path = "../reverie-host" }
reverie-process = { version = "0.1.0", path = "../../reverie-process" }
riptrace-rpc = { version = "0.1.0", path = "rpc" }
syscalls = { version = "0.6.7", features = ["serde"] }
tokio = { version = "1.37.0", features = ["full", "test-util", "tracing"] }

View file

@ -1,14 +0,0 @@
# @generated by autocargo from //hermetic_infra/reverie/experimental/riptrace:riptrace-rpc
[package]
name = "riptrace-rpc"
version = "0.1.0"
authors = ["Meta Platforms"]
edition = "2021"
repository = "https://github.com/facebookexperimental/reverie"
license = "BSD-2-Clause"
[dependencies]
reverie-rpc = { version = "0.1.0", path = "../../reverie-rpc" }
serde = { version = "1.0.185", features = ["derive", "rc"] }
syscalls = { version = "0.6.7", features = ["serde"] }

View file

@ -1,39 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
//! This contains the RPC protocol for the guest and host. That is, how the host
//! and guest should talk to each other.
use serde::Deserialize;
use serde::Serialize;
use syscalls::Errno;
/// Configuration options that adjust the behavior of the tool.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Config {
/// Only log syscalls that failed.
pub only_failures: bool,
/// Don't print anything.
pub quiet: bool,
}
/// Our service definition. The request and response enums are derived from this
/// interface. This also derives the client implementation.
#[reverie_rpc::service]
pub trait MyService {
/// Get the current configuration.
fn config() -> Config;
/// Increment the count of syscalls.
#[rpc(no_response)]
fn count(count: usize);
#[rpc(no_response)]
fn pretty_print(thread_id: u32, pretty: &str, result: Option<Result<usize, Errno>>);
}

View file

@ -1,120 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
use core::sync::atomic::AtomicUsize;
use core::sync::atomic::Ordering;
use std::fs;
use std::io;
use std::io::Write;
use std::sync::Mutex;
use colored::Colorize;
use reverie_process::Pid;
use riptrace_rpc::Config;
use riptrace_rpc::MyService;
use syscalls::Errno;
pub struct GlobalState {
/// The configuration.
pub config: Config,
/// The number of syscalls we've seen so far.
pub count: AtomicUsize,
output: Output,
}
pub enum Output {
Stderr(io::Stderr),
File(Mutex<io::BufWriter<fs::File>>),
}
#[async_trait::async_trait]
impl MyService for GlobalState {
async fn config(&self) -> Config {
self.config.clone()
}
async fn count(&self, count: usize) {
self.count.fetch_add(count, Ordering::Relaxed);
}
async fn pretty_print(
&self,
thread_id: u32,
pretty: &str,
result: Option<Result<usize, Errno>>,
) {
let thread_id = Pid::from_raw(thread_id as i32);
match &self.output {
Output::Stderr(stderr) => {
let mut stderr = stderr.lock();
match result {
Some(Ok(value)) => {
writeln!(
stderr,
"[{}] {} {} {}",
thread_id.colored(),
pretty,
"->".bold(),
value.to_string().green()
)
.unwrap();
}
Some(Err(errno)) => {
writeln!(
stderr,
"[{}] {} {} {}",
thread_id.colored(),
pretty,
"->".bold(),
errno.to_string().bold().red()
)
.unwrap();
}
None => {
writeln!(stderr, "[{}] {}", thread_id.colored(), pretty).unwrap();
}
}
}
Output::File(file) => {
let mut f = file.lock().unwrap();
match result {
Some(Ok(value)) => {
writeln!(f, "[{}] {} -> {}", thread_id, pretty, value).unwrap();
}
Some(Err(errno)) => {
writeln!(f, "[{}] {} -> {}", thread_id, pretty, errno).unwrap();
}
None => {
writeln!(f, "[{}] {}", thread_id, pretty).unwrap();
}
}
}
}
}
}
impl GlobalState {
pub fn new(config: Config) -> Self {
Self {
config,
count: AtomicUsize::new(0),
output: Output::Stderr(io::stderr()),
}
}
pub fn with_output(&mut self, f: fs::File) -> &mut Self {
self.output = Output::File(Mutex::new(io::BufWriter::new(f)));
self
}
}

View file

@ -1,112 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
mod global_state;
use std::fs;
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::Result;
use clap::Parser;
use global_state::GlobalState;
use reverie_process::Command;
use reverie_process::ExitStatus;
use riptrace_rpc::Config;
use riptrace_rpc::MyService;
/// A super fast strace.
#[derive(Parser)]
#[clap(trailing_var_arg = true)]
struct Args {
/// Count the number of calls for each system call and report a summary on
/// program exit.
#[clap(long, short = 'c')]
summary: bool,
/// Only log syscalls that failed.
#[clap(long)]
only_failures: bool,
/// Don't log anything.
#[clap(long, short)]
quiet: bool,
/// Output to this file instead of stderr.
#[clap(long, short)]
output: Option<PathBuf>,
/// Path to the sabre binary used to launch the plugin.
#[clap(long, env = "SABRE_PATH")]
sabre: Option<PathBuf>,
/// Path to the plugin.
#[clap(long, env = "SABRE_PLUGIN")]
plugin: Option<PathBuf>,
/// The program and arguments.
#[clap(required = true, multiple_values = true)]
command: Vec<String>,
}
impl Args {
async fn run(self) -> Result<ExitStatus> {
let mut command = Command::new(&self.command[0]);
command.args(&self.command[1..]);
let config = Config {
only_failures: self.only_failures,
quiet: self.quiet,
};
let mut global_state = GlobalState::new(config);
if let Some(path) = self.output {
global_state.with_output(fs::File::create(path)?);
}
let global_state = Arc::new(global_state.serve());
let mut child = reverie_host::TracerBuilder::new(command)
.plugin(self.plugin)
.sabre(self.sabre)
.global_state(global_state.clone())
.spawn()?;
let exit_status = child.wait().await?;
if self.summary {
let count = global_state
.count
.load(core::sync::atomic::Ordering::Relaxed);
eprintln!("Saw {} syscalls", count);
}
Ok(exit_status)
}
}
fn main() {
#[tokio::main]
async fn _main() -> ExitStatus {
match Args::parse().run().await {
Ok(exit_status) => exit_status,
Err(err) => {
eprintln!("{:?}", err);
ExitStatus::Exited(1)
}
}
}
// Make sure the tokio runtime exits before propagating the exit status.
// This ensures that any Drop code gets a chance to run.
//
// TODO: Add a proc macro that does this instead.
_main().raise_or_exit()
}

View file

@ -1,16 +0,0 @@
# @generated by autocargo from //hermetic_infra/reverie/experimental/riptrace:riptrace-tool
[package]
name = "riptrace-tool"
version = "0.1.0"
authors = ["Meta Platforms"]
edition = "2021"
repository = "https://github.com/facebookexperimental/reverie"
license = "BSD-2-Clause"
[dependencies]
libc = "0.2.139"
reverie-sabre = { version = "0.1.0", path = "../../reverie-sabre" }
reverie-syscalls = { version = "0.1.0", path = "../../../reverie-syscalls" }
riptrace-rpc = { version = "0.1.0", path = "../rpc" }
syscalls = { version = "0.6.7", features = ["serde"] }

View file

@ -1,110 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
//! An strace tool meant to be injected and ran by SaBRe.
use core::sync::atomic::AtomicU64;
use core::sync::atomic::Ordering;
use reverie_sabre as sabre;
use reverie_syscalls::Displayable;
use reverie_syscalls::LocalMemory;
use reverie_syscalls::Syscall;
use riptrace_rpc::Config;
use riptrace_rpc::MyServiceClient;
use sabre::SyscallExt;
use sabre::Tool;
use syscalls::Errno;
use syscalls::Sysno;
struct Riptrace {
/// Count of syscalls we've seen so far.
count: AtomicU64,
#[allow(dead_code)]
client: MyServiceClient,
config: Config,
}
#[sabre::tool]
impl Tool for Riptrace {
type Client = MyServiceClient;
#[detour(lib = "libc", func = "malloc")]
fn malloc(_size: usize) -> *mut libc::c_void {
todo!()
}
#[detour(lib = "libc", func = "free")]
fn free(_ptr: *mut libc::c_void) {
todo!()
}
fn new(client: Self::Client) -> Self {
let config = client.config();
Self {
count: AtomicU64::new(0),
client,
config,
}
}
fn syscall(&self, syscall: Syscall, memory: &LocalMemory) -> Result<usize, Errno> {
self.count.fetch_add(1, Ordering::Relaxed);
match syscall {
Syscall::Execve(_) | Syscall::Execveat(_) => {
if !self.config.quiet {
self.client.print_syscall(&syscall, memory, None);
}
// NOTE: execve does not return upon success
let errno = unsafe { syscall.call() }.unwrap_err();
self.client
.print_syscall(&syscall, memory, Some(Err(errno)));
Err(errno)
}
syscall => {
let ret = unsafe { syscall.call() };
if !self.config.quiet && (!self.config.only_failures || ret.is_err()) {
self.client.print_syscall(&syscall, memory, Some(ret));
}
ret
}
}
}
}
trait MyServiceClientExt {
fn print_syscall(
&self,
syscall: &Syscall,
memory: &LocalMemory,
result: Option<Result<usize, Errno>>,
);
}
impl MyServiceClientExt for MyServiceClient {
fn print_syscall(
&self,
syscall: &Syscall,
memory: &LocalMemory,
result: Option<Result<usize, Errno>>,
) {
// TODO: Use a thread-local to avoid this extra syscall.
let tid = unsafe { syscalls::raw_syscall!(Sysno::gettid) } as u32;
// TODO: Use a smallvec to allocate this instead.
let pretty = syscall.display_with_outputs(memory).to_string();
self.pretty_print(tid, &pretty, result)
}
}