From 407899245d16364c7dffd4506572f2624d000ea1 Mon Sep 17 00:00:00 2001 From: Jason White Date: Fri, 3 May 2024 04:36:51 -0700 Subject: [PATCH] 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 --- experimental/nostd-print/Cargo.toml | 12 - experimental/nostd-print/src/lib.rs | 130 --- experimental/reverie-host/Cargo.toml | 18 - experimental/reverie-host/src/codec.rs | 43 - experimental/reverie-host/src/lib.rs | 18 - experimental/reverie-host/src/server.rs | 76 -- experimental/reverie-host/src/tracer.rs | 153 ---- experimental/reverie-rpc-macros/Cargo.toml | 21 - experimental/reverie-rpc-macros/src/expand.rs | 322 ------- experimental/reverie-rpc-macros/src/lib.rs | 56 -- experimental/reverie-rpc-macros/src/parse.rs | 143 --- experimental/reverie-rpc/Cargo.toml | 15 - experimental/reverie-rpc/src/channel.rs | 57 -- experimental/reverie-rpc/src/codec.rs | 86 -- experimental/reverie-rpc/src/lib.rs | 28 - experimental/reverie-rpc/src/service.rs | 54 -- experimental/reverie-sabre-macros/Cargo.toml | 21 - .../reverie-sabre-macros/src/expand.rs | 155 ---- experimental/reverie-sabre-macros/src/lib.rs | 43 - .../reverie-sabre-macros/src/parse.rs | 85 -- experimental/reverie-sabre/Cargo.toml | 25 - experimental/reverie-sabre/src/callbacks.rs | 282 ------ experimental/reverie-sabre/src/ffi/clone.rs | 292 ------- experimental/reverie-sabre/src/ffi/mod.rs | 139 --- .../src/ffi/recursion_protector.c | 1 - .../reverie-sabre/src/ffi/vfork_syscall.S | 1 - experimental/reverie-sabre/src/internal.rs | 72 -- experimental/reverie-sabre/src/lib.rs | 38 - experimental/reverie-sabre/src/paths.rs | 54 -- .../reverie-sabre/src/protected_files.rs | 210 ----- experimental/reverie-sabre/src/rpc.rs | 105 --- .../reverie-sabre/src/signal/guard.rs | 539 ------------ experimental/reverie-sabre/src/signal/mod.rs | 437 --------- experimental/reverie-sabre/src/slot_map.rs | 492 ----------- experimental/reverie-sabre/src/thread.rs | 826 ------------------ experimental/reverie-sabre/src/tool.rs | 190 ---- experimental/reverie-sabre/src/utils.rs | 178 ---- experimental/reverie-sabre/src/vdso.rs | 21 - experimental/riptrace/Cargo.toml | 20 - experimental/riptrace/rpc/Cargo.toml | 14 - experimental/riptrace/rpc/src/lib.rs | 39 - experimental/riptrace/src/global_state.rs | 120 --- experimental/riptrace/src/main.rs | 112 --- experimental/riptrace/tool/Cargo.toml | 16 - experimental/riptrace/tool/src/lib.rs | 110 --- 45 files changed, 5869 deletions(-) delete mode 100644 experimental/nostd-print/Cargo.toml delete mode 100644 experimental/nostd-print/src/lib.rs delete mode 100644 experimental/reverie-host/Cargo.toml delete mode 100644 experimental/reverie-host/src/codec.rs delete mode 100644 experimental/reverie-host/src/lib.rs delete mode 100644 experimental/reverie-host/src/server.rs delete mode 100644 experimental/reverie-host/src/tracer.rs delete mode 100644 experimental/reverie-rpc-macros/Cargo.toml delete mode 100644 experimental/reverie-rpc-macros/src/expand.rs delete mode 100644 experimental/reverie-rpc-macros/src/lib.rs delete mode 100644 experimental/reverie-rpc-macros/src/parse.rs delete mode 100644 experimental/reverie-rpc/Cargo.toml delete mode 100644 experimental/reverie-rpc/src/channel.rs delete mode 100644 experimental/reverie-rpc/src/codec.rs delete mode 100644 experimental/reverie-rpc/src/lib.rs delete mode 100644 experimental/reverie-rpc/src/service.rs delete mode 100644 experimental/reverie-sabre-macros/Cargo.toml delete mode 100644 experimental/reverie-sabre-macros/src/expand.rs delete mode 100644 experimental/reverie-sabre-macros/src/lib.rs delete mode 100644 experimental/reverie-sabre-macros/src/parse.rs delete mode 100644 experimental/reverie-sabre/Cargo.toml delete mode 100644 experimental/reverie-sabre/src/callbacks.rs delete mode 100644 experimental/reverie-sabre/src/ffi/clone.rs delete mode 100644 experimental/reverie-sabre/src/ffi/mod.rs delete mode 120000 experimental/reverie-sabre/src/ffi/recursion_protector.c delete mode 120000 experimental/reverie-sabre/src/ffi/vfork_syscall.S delete mode 100644 experimental/reverie-sabre/src/internal.rs delete mode 100644 experimental/reverie-sabre/src/lib.rs delete mode 100644 experimental/reverie-sabre/src/paths.rs delete mode 100644 experimental/reverie-sabre/src/protected_files.rs delete mode 100644 experimental/reverie-sabre/src/rpc.rs delete mode 100644 experimental/reverie-sabre/src/signal/guard.rs delete mode 100644 experimental/reverie-sabre/src/signal/mod.rs delete mode 100644 experimental/reverie-sabre/src/slot_map.rs delete mode 100644 experimental/reverie-sabre/src/thread.rs delete mode 100644 experimental/reverie-sabre/src/tool.rs delete mode 100644 experimental/reverie-sabre/src/utils.rs delete mode 100644 experimental/reverie-sabre/src/vdso.rs delete mode 100644 experimental/riptrace/Cargo.toml delete mode 100644 experimental/riptrace/rpc/Cargo.toml delete mode 100644 experimental/riptrace/rpc/src/lib.rs delete mode 100644 experimental/riptrace/src/global_state.rs delete mode 100644 experimental/riptrace/src/main.rs delete mode 100644 experimental/riptrace/tool/Cargo.toml delete mode 100644 experimental/riptrace/tool/src/lib.rs diff --git a/experimental/nostd-print/Cargo.toml b/experimental/nostd-print/Cargo.toml deleted file mode 100644 index 835bb61..0000000 --- a/experimental/nostd-print/Cargo.toml +++ /dev/null @@ -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"] } diff --git a/experimental/nostd-print/src/lib.rs b/experimental/nostd-print/src/lib.rs deleted file mode 100644 index acacd91..0000000 --- a/experimental/nostd-print/src/lib.rs +++ /dev/null @@ -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 { - 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 { - fd: i32, - buf: [u8; N], - len: usize, -} - -impl Stdio { - 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 Drop for Stdio { - 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); - }) -} diff --git a/experimental/reverie-host/Cargo.toml b/experimental/reverie-host/Cargo.toml deleted file mode 100644 index 48a447c..0000000 --- a/experimental/reverie-host/Cargo.toml +++ /dev/null @@ -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"] } diff --git a/experimental/reverie-host/src/codec.rs b/experimental/reverie-host/src/codec.rs deleted file mode 100644 index c369dfb..0000000 --- a/experimental/reverie-host/src/codec.rs +++ /dev/null @@ -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) -> io::Result -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(stream: &mut S, buf: &mut Vec, item: T) -> io::Result<()> -where - T: Serialize, - S: AsyncWrite + Unpin, -{ - buf.clear(); - - reverie_rpc::encode(&item, buf)?; - - stream.write_all(buf).await -} diff --git a/experimental/reverie-host/src/lib.rs b/experimental/reverie-host/src/lib.rs deleted file mode 100644 index ba3fb76..0000000 --- a/experimental/reverie-host/src/lib.rs +++ /dev/null @@ -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::*; diff --git a/experimental/reverie-host/src/server.rs b/experimental/reverie-host/src/server.rs deleted file mode 100644 index 0b9162f..0000000 --- a/experimental/reverie-host/src/server.rs +++ /dev/null @@ -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, -} - -impl Server { - /// Creates the server, but does not yet listen for incoming connections. - pub fn new() -> Result { - 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(&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); - } - } - } - } -} diff --git a/experimental/reverie-host/src/tracer.rs b/experimental/reverie-host/src/tracer.rs deleted file mode 100644 index 3a0eeb6..0000000 --- a/experimental/reverie-host/src/tracer.rs +++ /dev/null @@ -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 { - command: Command, - sabre: Option, - plugin: Option, - service: S, -} - -impl TracerBuilder<()> { - pub fn new(command: Command) -> Self { - Self { - command, - sabre: None, - plugin: None, - service: (), - } - } -} - -impl TracerBuilder { - /// Sets the path to the plugin's DSO. If this is not set, the - /// `SABRE_PLUGIN` environment variable is used instead. - pub fn plugin>>(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>>(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(self, service: T) -> TracerBuilder { - TracerBuilder { - command: self.command, - sabre: self.sabre, - plugin: self.plugin, - service, - } - } - - /// Spawns the root guest process. - pub fn spawn(self) -> Result - 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 { - 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, 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, 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) -} diff --git a/experimental/reverie-rpc-macros/Cargo.toml b/experimental/reverie-rpc-macros/Cargo.toml deleted file mode 100644 index 08fde8c..0000000 --- a/experimental/reverie-rpc-macros/Cargo.toml +++ /dev/null @@ -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"] } diff --git a/experimental/reverie-rpc-macros/src/expand.rs b/experimental/reverie-rpc-macros/src/expand.rs deleted file mode 100644 index 4c5aa0b..0000000 --- a/experimental/reverie-rpc-macros/src/expand.rs +++ /dev/null @@ -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 { - #server_ident { service: self } - } - } - } - } - - /// Expands `struct ServeMyService` - /// Expands `impl Service for ServeMyService` - 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::>(); - - 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 = 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 { - service: S, - } - - impl #server_ident { - pub fn into_inner(self) -> S { - self.service - } - } - - impl core::ops::Deref for #server_ident { - type Target = S; - - fn deref(&self) -> &Self::Target { - &self.service - } - } - - impl core::ops::DerefMut for #server_ident { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.service - } - } - - impl ::reverie_rpc::Service for #server_ident - 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>; - - 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 = 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 { - Self { - channel, - } - } - } - - impl #client_ident { - #( #methods )* - } - } - } -} diff --git a/experimental/reverie-rpc-macros/src/lib.rs b/experimental/reverie-rpc-macros/src/lib.rs deleted file mode 100644 index 04712b0..0000000 --- a/experimental/reverie-rpc-macros/src/lib.rs +++ /dev/null @@ -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, - vis: syn::Visibility, - ident: syn::Ident, - methods: Vec, - request_ident: syn::Ident, - request_generics: Option, - 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, - /// 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, - /// 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() -} diff --git a/experimental/reverie-rpc-macros/src/parse.rs b/experimental/reverie-rpc-macros/src/parse.rs deleted file mode 100644 index bfe737a..0000000 --- a/experimental/reverie-rpc-macros/src/parse.rs +++ /dev/null @@ -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 { - 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 { - 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, - }) - } -} diff --git a/experimental/reverie-rpc/Cargo.toml b/experimental/reverie-rpc/Cargo.toml deleted file mode 100644 index db9cc0c..0000000 --- a/experimental/reverie-rpc/Cargo.toml +++ /dev/null @@ -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"] } diff --git a/experimental/reverie-rpc/src/channel.rs b/experimental/reverie-rpc/src/channel.rs deleted file mode 100644 index fb236a5..0000000 --- a/experimental/reverie-rpc/src/channel.rs +++ /dev/null @@ -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 -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 = Box + Send + Sync + 'static>; - -pub trait MakeClient { - type Request: Serialize; - type Response: for<'a> Deserialize<'a>; - - fn make_client(channel: BoxChannel) -> 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 {} -} - -impl Channel for Box -where - T: Channel + ?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) - } -} diff --git a/experimental/reverie-rpc/src/codec.rs b/experimental/reverie-rpc/src/codec.rs deleted file mode 100644 index 6ba99a7..0000000 --- a/experimental/reverie-rpc/src/codec.rs +++ /dev/null @@ -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(item: &T, buf: &mut Vec) -> 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(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 -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) -> io::Result -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) -} diff --git a/experimental/reverie-rpc/src/lib.rs b/experimental/reverie-rpc/src/lib.rs deleted file mode 100644 index 3ed8098..0000000 --- a/experimental/reverie-rpc/src/lib.rs +++ /dev/null @@ -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::*; diff --git a/experimental/reverie-rpc/src/service.rs b/experimental/reverie-rpc/src/service.rs deleted file mode 100644 index 25e69db..0000000 --- a/experimental/reverie-rpc/src/service.rs +++ /dev/null @@ -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 + Send + 'a>>; - -pub trait Service { - type Request<'r>: Deserialize<'r> + Unpin + Send - where - Self: 'r; - type Response: Serialize + Send + Unpin; - type Future<'a>: Future> + 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 Service for Arc -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>; - - fn call<'a>(&'a self, _req: ()) -> Self::Future<'a> { - core::future::ready(Some(())) - } -} diff --git a/experimental/reverie-sabre-macros/Cargo.toml b/experimental/reverie-sabre-macros/Cargo.toml deleted file mode 100644 index eba8752..0000000 --- a/experimental/reverie-sabre-macros/Cargo.toml +++ /dev/null @@ -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"] } diff --git a/experimental/reverie-sabre-macros/src/expand.rs b/experimental/reverie-sabre-macros/src/expand.rs deleted file mode 100644 index 24872c9..0000000 --- a/experimental/reverie-sabre-macros/src/expand.rs +++ /dev/null @@ -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::>(); - 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, - syscall_handler: *mut Option, - rdtsc_handler: *mut Option, - post_load: *mut Option, - 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, - ) - } - }); - } -} diff --git a/experimental/reverie-sabre-macros/src/lib.rs b/experimental/reverie-sabre-macros/src/lib.rs deleted file mode 100644 index 93eeec4..0000000 --- a/experimental/reverie-sabre-macros/src/lib.rs +++ /dev/null @@ -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, -} - -#[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() -} diff --git a/experimental/reverie-sabre-macros/src/parse.rs b/experimental/reverie-sabre-macros/src/parse.rs deleted file mode 100644 index ae46fbd..0000000 --- a/experimental/reverie-sabre-macros/src/parse.rs +++ /dev/null @@ -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 { - 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 { - let mut impl_node: syn::ItemImpl = input.parse()?; - - let detour_methods: Result, _> = 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?, - }) - } -} diff --git a/experimental/reverie-sabre/Cargo.toml b/experimental/reverie-sabre/Cargo.toml deleted file mode 100644 index e40a857..0000000 --- a/experimental/reverie-sabre/Cargo.toml +++ /dev/null @@ -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"] } diff --git a/experimental/reverie-sabre/src/callbacks.rs b/experimental/reverie-sabre/src/callbacks.rs deleted file mode 100644 index a535355..0000000 --- a/experimental/reverie-sabre/src/callbacks.rs +++ /dev/null @@ -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 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( - 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::::current() { - thread - } else { - terminate(1); - }; - - match handle_syscall_with_thread::( - &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( - thread: &mut Thread, - syscall: isize, - arg1: usize, - arg2: usize, - arg3: usize, - arg4: usize, - arg5: usize, - arg6: usize, - wrapper_sp: *mut ffi::syscall_stackframe, -) -> Result { - 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(thread: &mut Thread, 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(exit_code: usize) -> usize { - if let Some(mut thread) = Thread::::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( - _: libc::c_int, - _: *const libc::siginfo_t, - _: *const libc::c_void, -) { - let mut thread = if let Some(thread) = Thread::::current() { - thread - } else { - terminate(0); - }; - - if thread.try_exit() { - terminate(0); - } -} - -extern "C" fn handle_vdso_clock_gettime( - clockid: libc::clockid_t, - tp: *mut libc::timespec, -) -> i32 { - T::global().vdso_clock_gettime(clockid, tp) -} - -extern "C" fn handle_vdso_getcpu( - cpu: *mut u32, - node: *mut u32, - _unused: usize, -) -> i32 { - T::global().vdso_getcpu(cpu, node, _unused) -} - -extern "C" fn handle_vdso_gettimeofday( - tv: *mut libc::timeval, - tz: *mut libc::timezone, -) -> i32 { - T::global().vdso_gettimeofday(tv, tz) -} - -extern "C" fn handle_vdso_time(tloc: *mut libc::time_t) -> i32 { - T::global().vdso_time(tloc) -} - -pub extern "C" fn handle_vdso( - syscall: isize, - actual_fn: ffi::void_void_fn, -) -> Option { - 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:: as *const ()) - } - Sysno::getcpu => { - vdso::getcpu = transmute(actual_fn as *const ()); - transmute(handle_vdso_getcpu:: as *const ()) - } - Sysno::gettimeofday => { - vdso::gettimeofday = transmute(actual_fn as *const ()); - transmute(handle_vdso_gettimeofday:: as *const ()) - } - Sysno::time => { - vdso::time = transmute(actual_fn as *const ()); - transmute(handle_vdso_time:: as *const ()) - } - _ => None, - } - } -} - -pub extern "C" fn handle_rdtsc() -> u64 { - T::global().rdtsc() -} diff --git a/experimental/reverie-sabre/src/ffi/clone.rs b/experimental/reverie-sabre/src/ffi/clone.rs deleted file mode 100644 index 40a8c97..0000000 --- a/experimental/reverie-sabre/src/ffi/clone.rs +++ /dev/null @@ -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); - } - } -} diff --git a/experimental/reverie-sabre/src/ffi/mod.rs b/experimental/reverie-sabre/src/ffi/mod.rs deleted file mode 100644 index d60b2e4..0000000 --- a/experimental/reverie-sabre/src/ffi/mod.rs +++ /dev/null @@ -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; - -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, -} diff --git a/experimental/reverie-sabre/src/ffi/recursion_protector.c b/experimental/reverie-sabre/src/ffi/recursion_protector.c deleted file mode 120000 index e44acaa..0000000 --- a/experimental/reverie-sabre/src/ffi/recursion_protector.c +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../third-party/sabre/plugin_api/recursion_protector.c \ No newline at end of file diff --git a/experimental/reverie-sabre/src/ffi/vfork_syscall.S b/experimental/reverie-sabre/src/ffi/vfork_syscall.S deleted file mode 120000 index eeddf6f..0000000 --- a/experimental/reverie-sabre/src/ffi/vfork_syscall.S +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../third-party/sabre/plugin_api/arch/x86_64/vfork_syscall.s \ No newline at end of file diff --git a/experimental/reverie-sabre/src/internal.rs b/experimental/reverie-sabre/src/internal.rs deleted file mode 100644 index a173dd1..0000000 --- a/experimental/reverie-sabre/src/internal.rs +++ /dev/null @@ -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 { - // 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(fn_icept_reg: ffi::icept_reg_fn) { - for detour_func in <::Target>::detours() { - fn_icept_reg(detour_func); - } -} - -pub fn sbr_init( - argc: *mut i32, - argv: *mut *mut *mut libc::c_char, - fn_icept_reg: ffi::icept_reg_fn, - vdso_callback: *mut Option, - syscall_handler: *mut Option, - rdtsc_handler: *mut Option, - _post_load: *mut Option, - sabre_path: *const libc::c_char, - client_path: *const libc::c_char, -) { - unsafe { - *vdso_callback = Some(callbacks::handle_vdso::); - *syscall_handler = Some(callbacks::handle_syscall::); - *rdtsc_handler = Some(callbacks::handle_rdtsc::); - - 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::(); - - *argc -= 1; - *argv = (*argv).wrapping_add(1); - - // Setting up function detours - register_detours::(fn_icept_reg); - } -} diff --git a/experimental/reverie-sabre/src/lib.rs b/experimental/reverie-sabre/src/lib.rs deleted file mode 100644 index a13363d..0000000 --- a/experimental/reverie-sabre/src/lib.rs +++ /dev/null @@ -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; diff --git a/experimental/reverie-sabre/src/paths.rs b/experimental/reverie-sabre/src/paths.rs deleted file mode 100644 index 6177965..0000000 --- a/experimental/reverie-sabre/src/paths.rs +++ /dev/null @@ -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) } -} diff --git a/experimental/reverie-sabre/src/protected_files.rs b/experimental/reverie-sabre/src/protected_files.rs deleted file mode 100644 index 0dd437f..0000000 --- a/experimental/reverie-sabre/src/protected_files.rs +++ /dev/null @@ -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 = 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, -} - -impl ProtectedFiles { - pub const fn new() -> Self { - Self { files: Vec::new() } - } - - pub fn contains(&self, fd: &Fd) -> bool { - self.files.contains(&fd.as_raw_fd()) - } - - pub fn insert(&mut self, fd: &Fd) -> bool { - if self.contains(fd) { - true - } else { - self.files.push(fd.as_raw_fd()); - false - } - } - - pub fn remove(&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); - -impl Drop for ProtectedFd { - fn drop(&mut self) { - PROTECTED_FILES.lock().remove(&self.0); - } -} - -impl AsRef for ProtectedFd { - fn as_ref(&self) -> &T { - &self.0 - } -} - -impl AsMut for ProtectedFd { - 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: F) -> Result, E> -where - F: FnOnce() -> Result, - 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: &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))) -} diff --git a/experimental/reverie-sabre/src/rpc.rs b/experimental/reverie-sabre/src/rpc.rs deleted file mode 100644 index a2b6d3e..0000000 --- a/experimental/reverie-sabre/src/rpc.rs +++ /dev/null @@ -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, -} - -/// Implements a channel using a UNIX domain socket. -pub struct BaseChannel { - inner: Mutex, -} - -impl BaseChannel { - /// Connects to the global state RPC server. - pub fn new() -> io::Result { - // 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(&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(&mut self) -> io::Result - where - T: for<'a> Deserialize<'a>, - { - let mut buf = Vec::with_capacity(1024); - reverie_rpc::decode_from(self.stream.as_mut(), &mut buf) - } -} - -impl Channel 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") - } -} diff --git a/experimental/reverie-sabre/src/signal/guard.rs b/experimental/reverie-sabre/src/signal/guard.rs deleted file mode 100644 index 12bcfca..0000000 --- a/experimental/reverie-sabre/src/signal/guard.rs +++ /dev/null @@ -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 = (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 - = 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, - _phantom: PhantomData>, -} - -/// 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, - _phantom: PhantomData>, -} - -impl<'a, T> SequencerGuard<'a, T> { - fn new(owner: &'a GuardedSequencer) -> Self { - SequencerGuard { - owner, - _phantom: Default::default(), - } - } -} - -impl<'a, T> SequencerAntiGuard<'a, T> { - fn new(owner: &'a GuardedSequencer) -> 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 { - queue: Q64>, - guard_state: AtomicU64, -} - -impl GuardedSequencer { - 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) { - 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 { - // 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>>; - - #[derive(Clone)] - struct HandlerInput(Rc>, InvocationLog); - - /// Test wrapper to do the cleanup we need and run each test in serial - fn run_guarded_sequencer_test(t: T) - where - T: FnOnce(Rc>, 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::()]) - }; - - #[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))); - }) - } -} diff --git a/experimental/reverie-sabre/src/signal/mod.rs b/experimental/reverie-sabre/src/signal/mod.rs deleted file mode 100644 index 68778fc..0000000 --- a/experimental/reverie-sabre/src/signal/mod.rs +++ /dev/null @@ -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) -> 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> = 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 = None; - -/// Storage of our default handler for the libc::SIG_IGN -static mut DEFAULT_IGNORE_HANDLER: Option = None; - -/// Storage of our default handler for the libc::SIG_ERR -static mut DEFAULT_ERROR_HANDLER: Option = 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::(), - ); - } 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, -) -> 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( - _signal_value: libc::c_int, - _siginfo: *const libc::siginfo_t, - _ctx: *const libc::c_void, -) { - callbacks::exit_group::(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( - _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( - _signal_value: libc::c_int, - _siginfo: *const libc::siginfo_t, - _ctx: *const libc::c_void, -) { - callbacks::exit_group::(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> = 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( - 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() { - - // 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:: - 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::::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 - = 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 { - - 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::, - default_ignore_handler: default_ignore_handler::, - default_error_handler: default_error_handler::, - - 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::; - 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, - ] -} diff --git a/experimental/reverie-sabre/src/slot_map.rs b/experimental/reverie-sabre/src/slot_map.rs deleted file mode 100644 index 20da7e6..0000000 --- a/experimental/reverie-sabre/src/slot_map.rs +++ /dev/null @@ -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 = (SlotMetaData, T); -type Chunk = [Slot; SLOTS_PER_CHUNK]; -type ChunkPointer = MaybeUninit>>; - -/// Allocate a new chunk and return it -fn new_chunk() -> ChunkPointer { - 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(int_value: S) -> Self - where - S: Into, - { - 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 { - 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; MAX_CHUNK_COUNT], -} - -impl SlotMap -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 { - 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, value: T) -> Result { - 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; - - let chunk_pointer_writeable = - &mut *(chunk_pointer_pointer as *mut ChunkPointer); - - *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) as *mut Slot; - (*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 { - 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 { - (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 Drop for SlotMap { - 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 { - 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>); - - 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 = 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> = 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::>() - .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); - } -} diff --git a/experimental/reverie-sabre/src/thread.rs b/experimental/reverie-sabre/src/thread.rs deleted file mode 100644 index 2d4e8e1..0000000 --- a/experimental/reverie-sabre/src/thread.rs +++ /dev/null @@ -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 = SlotMap::new(); -} - -thread_local! { - pub static THREAD_SLOT_KEY: Option = 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, 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::() == 8); -const _: () = assert!(8 % core::mem::align_of::() == 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 { - 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 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 { - 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, -} - -fn get_threads_for_process<'a>( - process_id: u32, -) -> impl Iterator { - 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(signal_guest_thread: F) -> Option -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) -> 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 Thread { - /// 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> { - 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(&mut self, to_run: F) -> Result - 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( - &mut self, - maybe_forking_fn: F, - ) -> Result - 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> = Mutex::new(HashSet::default()); - static ref THREADS_EXITED: Mutex> = 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::::current() - } - - pub fn run_test_in_new_thread(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::::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(thread: &Thread) { - 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(thread: &Thread) { - 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))); - }) - } -} diff --git a/experimental/reverie-sabre/src/tool.rs b/experimental/reverie-sabre/src/tool.rs deleted file mode 100644 index 0c573e3..0000000 --- a/experimental/reverie-sabre/src/tool.rs +++ /dev/null @@ -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 { - 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 { - 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; -} - -impl SyscallExt for Syscall { - unsafe fn call(self) -> Result { - 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 - ) - } - } -} diff --git a/experimental/reverie-sabre/src/utils.rs b/experimental/reverie-sabre/src/utils.rs deleted file mode 100644 index d9a23c2..0000000 --- a/experimental/reverie-sabre/src/utils.rs +++ /dev/null @@ -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 { - 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 { - // 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 { - 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)) -} diff --git a/experimental/reverie-sabre/src/vdso.rs b/experimental/reverie-sabre/src/vdso.rs deleted file mode 100644 index e8af5be..0000000 --- a/experimental/reverie-sabre/src/vdso.rs +++ /dev/null @@ -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; diff --git a/experimental/riptrace/Cargo.toml b/experimental/riptrace/Cargo.toml deleted file mode 100644 index 01a7e9a..0000000 --- a/experimental/riptrace/Cargo.toml +++ /dev/null @@ -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"] } diff --git a/experimental/riptrace/rpc/Cargo.toml b/experimental/riptrace/rpc/Cargo.toml deleted file mode 100644 index 2d03d9b..0000000 --- a/experimental/riptrace/rpc/Cargo.toml +++ /dev/null @@ -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"] } diff --git a/experimental/riptrace/rpc/src/lib.rs b/experimental/riptrace/rpc/src/lib.rs deleted file mode 100644 index 1a6317e..0000000 --- a/experimental/riptrace/rpc/src/lib.rs +++ /dev/null @@ -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>); -} diff --git a/experimental/riptrace/src/global_state.rs b/experimental/riptrace/src/global_state.rs deleted file mode 100644 index 03d7730..0000000 --- a/experimental/riptrace/src/global_state.rs +++ /dev/null @@ -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>), -} - -#[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>, - ) { - 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 - } -} diff --git a/experimental/riptrace/src/main.rs b/experimental/riptrace/src/main.rs deleted file mode 100644 index 3b517cd..0000000 --- a/experimental/riptrace/src/main.rs +++ /dev/null @@ -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, - - /// Path to the sabre binary used to launch the plugin. - #[clap(long, env = "SABRE_PATH")] - sabre: Option, - - /// Path to the plugin. - #[clap(long, env = "SABRE_PLUGIN")] - plugin: Option, - - /// The program and arguments. - #[clap(required = true, multiple_values = true)] - command: Vec, -} - -impl Args { - async fn run(self) -> Result { - 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() -} diff --git a/experimental/riptrace/tool/Cargo.toml b/experimental/riptrace/tool/Cargo.toml deleted file mode 100644 index 45983a4..0000000 --- a/experimental/riptrace/tool/Cargo.toml +++ /dev/null @@ -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"] } diff --git a/experimental/riptrace/tool/src/lib.rs b/experimental/riptrace/tool/src/lib.rs deleted file mode 100644 index 58697e5..0000000 --- a/experimental/riptrace/tool/src/lib.rs +++ /dev/null @@ -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 { - 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>, - ); -} - -impl MyServiceClientExt for MyServiceClient { - fn print_syscall( - &self, - syscall: &Syscall, - memory: &LocalMemory, - result: Option>, - ) { - // 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) - } -}