mirror of
https://github.com/facebookexperimental/reverie.git
synced 2024-11-24 04:09:38 +00:00
Delete experimental code
Summary: This reduces Reverie's maintenance burden going forward. This code isn't used and is in the commit history if it needs to be referenced. Reviewed By: VladimirMakaev Differential Revision: D56889383 fbshipit-source-id: 6fe2ca3a945a69a84624f545102283bda0285233
This commit is contained in:
parent
374247cc0a
commit
407899245d
45 changed files with 0 additions and 5869 deletions
|
@ -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"] }
|
|
@ -1,130 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
//! Provides helpers for printing formatted messages to stdout/stderr without
|
||||
//! relying on std.
|
||||
|
||||
use core::fmt;
|
||||
use core::fmt::Write;
|
||||
|
||||
use syscalls::syscall3;
|
||||
use syscalls::Errno;
|
||||
use syscalls::Sysno;
|
||||
|
||||
#[inline(always)]
|
||||
fn sys_write(fd: i32, buf: &[u8]) -> Result<usize, Errno> {
|
||||
unsafe { syscall3(Sysno::write, fd as usize, buf.as_ptr() as usize, buf.len()) }
|
||||
}
|
||||
|
||||
fn sys_write_all(fd: i32, mut buf: &[u8]) -> Result<(), Errno> {
|
||||
while !buf.is_empty() {
|
||||
match sys_write(fd, buf) {
|
||||
Ok(n) => buf = &buf[n..],
|
||||
Err(Errno::EINTR) => continue,
|
||||
Err(errno) => return Err(errno),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct Stdio<const N: usize = 4096> {
|
||||
fd: i32,
|
||||
buf: [u8; N],
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl<const N: usize> Stdio<N> {
|
||||
pub fn new(fd: i32) -> Self {
|
||||
Self {
|
||||
fd,
|
||||
buf: [0; N],
|
||||
len: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_all(&mut self, mut buf: &[u8]) -> Result<(), Errno> {
|
||||
while !buf.is_empty() {
|
||||
if self.buf[self.len..].is_empty() {
|
||||
self.flush()?;
|
||||
}
|
||||
let remaining = &mut self.buf[self.len..];
|
||||
let count = remaining.len().min(buf.len());
|
||||
remaining[0..count].copy_from_slice(&buf[0..count]);
|
||||
self.len += count;
|
||||
buf = &buf[count..];
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Flushes the buffered writes to the file descriptor.
|
||||
pub fn flush(&mut self) -> Result<(), Errno> {
|
||||
sys_write_all(self.fd, &self.buf[0..self.len])?;
|
||||
self.len = 0;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> Drop for Stdio<N> {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.flush();
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Write for Stdio {
|
||||
fn write_str(&mut self, s: &str) -> fmt::Result {
|
||||
self.write_all(s.as_bytes()).map_err(|_| fmt::Error)
|
||||
}
|
||||
}
|
||||
|
||||
fn _inner_print(fd: i32, args: fmt::Arguments<'_>, newline: bool) -> fmt::Result {
|
||||
let mut f = Stdio::new(fd);
|
||||
f.write_fmt(args)?;
|
||||
|
||||
if newline {
|
||||
f.write_str("\n")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn _print(fd: i32, args: fmt::Arguments<'_>, newline: bool) {
|
||||
// Ignore the error.
|
||||
let _ = _inner_print(fd, args, newline);
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! print {
|
||||
($($arg:tt)*) => ($crate::_print(1, ::core::format_args!($($arg)*), false));
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! eprint {
|
||||
($($arg:tt)*) => ($crate::_print(2, ::core::format_args!($($arg)*), false));
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! println {
|
||||
() => ($crate::print!("\n"));
|
||||
($($arg:tt)*) => ({
|
||||
// Purposefully avoiding format_args_nl because it requires a nightly
|
||||
// feature.
|
||||
$crate::_print(1, ::core::format_args!($($arg)*), true);
|
||||
})
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! eprintln {
|
||||
() => ($crate::eprint!("\n"));
|
||||
($($arg:tt)*) => ({
|
||||
// Purposefully avoiding format_args_nl because it requires a nightly
|
||||
// feature.
|
||||
$crate::_print(2, ::core::format_args!($($arg)*), true);
|
||||
})
|
||||
}
|
|
@ -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"] }
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
use core::marker::Unpin;
|
||||
use std::io;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use tokio::io::AsyncRead;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::io::AsyncWrite;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
pub async fn read<'a, T, S>(stream: &mut S, buf: &'a mut Vec<u8>) -> io::Result<T>
|
||||
where
|
||||
T: Deserialize<'a>,
|
||||
S: AsyncRead + Unpin,
|
||||
{
|
||||
let len = stream.read_u32().await? as usize;
|
||||
|
||||
buf.resize(len, 0);
|
||||
|
||||
stream.read_exact(buf).await?;
|
||||
|
||||
reverie_rpc::decode_frame(buf)
|
||||
}
|
||||
|
||||
pub async fn write<T, S>(stream: &mut S, buf: &mut Vec<u8>, item: T) -> io::Result<()>
|
||||
where
|
||||
T: Serialize,
|
||||
S: AsyncWrite + Unpin,
|
||||
{
|
||||
buf.clear();
|
||||
|
||||
reverie_rpc::encode(&item, buf)?;
|
||||
|
||||
stream.write_all(buf).await
|
||||
}
|
|
@ -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::*;
|
|
@ -1,76 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use reverie_rpc::Service;
|
||||
use tempfile::NamedTempFile;
|
||||
use tokio::net::UnixListener;
|
||||
|
||||
use super::codec;
|
||||
|
||||
/// A global state server.
|
||||
pub struct Server {
|
||||
socket: NamedTempFile<UnixListener>,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
/// Creates the server, but does not yet listen for incoming connections.
|
||||
pub fn new() -> Result<Self> {
|
||||
let sock_dir = dirs::runtime_dir().unwrap_or_else(|| PathBuf::from("/tmp"));
|
||||
|
||||
let prefix = format!("reverie-{}-", std::process::id());
|
||||
let socket = tempfile::Builder::new()
|
||||
.prefix(&prefix)
|
||||
.suffix(".sock")
|
||||
.make_in(sock_dir, |path| UnixListener::bind(path))?;
|
||||
|
||||
Ok(Self { socket })
|
||||
}
|
||||
|
||||
/// Returns the path to the socket.
|
||||
pub fn sock_path(&self) -> &Path {
|
||||
self.socket.path()
|
||||
}
|
||||
|
||||
/// Accepts new socket connections and processes them.
|
||||
pub async fn serve<S>(&self, service: S) -> !
|
||||
where
|
||||
S: Service + Clone + Send + Sync + 'static,
|
||||
{
|
||||
loop {
|
||||
match self.socket.as_file().accept().await {
|
||||
Ok((mut stream, _addr)) => {
|
||||
let service = service.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut reader_buf = Vec::with_capacity(1024);
|
||||
let mut writer_buf = Vec::with_capacity(1024);
|
||||
|
||||
while let Ok(request) = codec::read(&mut stream, &mut reader_buf).await {
|
||||
if let Some(response) = service.call(request).await {
|
||||
// Only send back a response if this request has
|
||||
// an associated response. This lets us have
|
||||
// "send-only" messages, which are useful for
|
||||
// accumulating state.
|
||||
codec::write(&mut stream, &mut writer_buf, response)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("connection failed: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,153 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
use std::env;
|
||||
use std::ffi::OsStr;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use reverie_process::Child;
|
||||
use reverie_process::Command;
|
||||
use reverie_rpc::Service;
|
||||
|
||||
use super::server::Server;
|
||||
|
||||
pub struct TracerBuilder<S> {
|
||||
command: Command,
|
||||
sabre: Option<PathBuf>,
|
||||
plugin: Option<PathBuf>,
|
||||
service: S,
|
||||
}
|
||||
|
||||
impl TracerBuilder<()> {
|
||||
pub fn new(command: Command) -> Self {
|
||||
Self {
|
||||
command,
|
||||
sabre: None,
|
||||
plugin: None,
|
||||
service: (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> TracerBuilder<S> {
|
||||
/// Sets the path to the plugin's DSO. If this is not set, the
|
||||
/// `SABRE_PLUGIN` environment variable is used instead.
|
||||
pub fn plugin<P: Into<Option<PathBuf>>>(mut self, path: P) -> Self {
|
||||
self.plugin = path.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the path to the sabre binary. If this is not set, the
|
||||
/// `SABRE_BINARY` environment variable is used instead.
|
||||
pub fn sabre<P: Into<Option<PathBuf>>>(mut self, path: P) -> Self {
|
||||
self.sabre = path.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the global state service. The service is started when the child
|
||||
/// process is spawned.
|
||||
pub fn global_state<T>(self, service: T) -> TracerBuilder<T> {
|
||||
TracerBuilder {
|
||||
command: self.command,
|
||||
sabre: self.sabre,
|
||||
plugin: self.plugin,
|
||||
service,
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns the root guest process.
|
||||
pub fn spawn(self) -> Result<Child>
|
||||
where
|
||||
S: Service + Clone + Send + Sync + 'static,
|
||||
{
|
||||
let sabre = self
|
||||
.sabre
|
||||
.or_else(|| std::env::var_os("SABRE_BINARY").map(PathBuf::from))
|
||||
.map_or_else(find_sabre, |x| Ok(Some(x)))?;
|
||||
let sabre = sabre.ok_or_else(|| anyhow!("Could not find sabre executable"))?;
|
||||
|
||||
let plugin = self
|
||||
.plugin
|
||||
.or_else(|| std::env::var_os("SABRE_PLUGIN").map(PathBuf::from))
|
||||
.map_or_else(find_plugin, |x| Ok(Some(x)))?;
|
||||
let plugin = plugin.ok_or_else(|| anyhow!("Could not sabre plugin"))?;
|
||||
|
||||
let mut command = into_sabre(self.command, sabre.as_ref(), plugin.as_ref())?;
|
||||
|
||||
let server = Server::new()?;
|
||||
|
||||
command.env("REVERIE_SOCK", server.sock_path());
|
||||
|
||||
let service = self.service;
|
||||
|
||||
tokio::spawn(async move { server.serve(service).await });
|
||||
|
||||
let child = command
|
||||
.spawn()
|
||||
.with_context(|| format!("Failed to spawn: {:?}", command.get_program()))?;
|
||||
|
||||
Ok(child)
|
||||
}
|
||||
}
|
||||
|
||||
fn into_sabre(mut command: Command, sabre: &OsStr, plugin: &OsStr) -> Result<Command> {
|
||||
let program = command
|
||||
.find_program()
|
||||
.with_context(|| format!("Could not find program: {:?}", command.get_program()))?;
|
||||
|
||||
command.prepend_args([plugin, "--".as_ref(), program.as_ref()]);
|
||||
|
||||
// Change the program that we're launching. This also changes arg0 to match.
|
||||
command.program(sabre);
|
||||
|
||||
// Ensure that SABRE_BINARY and SABRE_PLUGIN are not inherited by the child
|
||||
// process.
|
||||
command.env_remove("SABRE_BINARY");
|
||||
command.env_remove("SABRE_PLUGIN");
|
||||
|
||||
Ok(command)
|
||||
}
|
||||
|
||||
/// Tries to find the path to the `sabre` executable based on the path to the
|
||||
/// current executablbe. This should be the case when using dotslash.
|
||||
fn find_sabre() -> Result<Option<PathBuf>, io::Error> {
|
||||
let mut path = env::current_exe()?;
|
||||
|
||||
path.pop();
|
||||
path.push("sabre");
|
||||
|
||||
if path.is_file() {
|
||||
Ok(Some(path))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to find the plugin based on the path to the current executable. This
|
||||
/// should be the case when using dotslash.
|
||||
fn find_plugin() -> Result<Option<PathBuf>, io::Error> {
|
||||
let mut path = env::current_exe()?;
|
||||
|
||||
if let Some(exe_name) = path.file_name() {
|
||||
let mut name = exe_name.to_os_string();
|
||||
name.push("_plugin.so");
|
||||
|
||||
// Search for the plugin in the same directory.
|
||||
path.set_file_name(name);
|
||||
|
||||
if path.is_file() {
|
||||
return Ok(Some(path));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
|
@ -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"] }
|
|
@ -1,322 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use quote::ToTokens;
|
||||
use quote::TokenStreamExt;
|
||||
use syn::spanned::Spanned;
|
||||
|
||||
use crate::Method;
|
||||
use crate::Service;
|
||||
|
||||
impl ToTokens for Service {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
tokens.append_all(&[
|
||||
self.expand_trait(),
|
||||
self.expand_server(),
|
||||
self.expand_request_enum(),
|
||||
self.expand_response_enum(),
|
||||
self.expand_client(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
impl Service {
|
||||
/// Expands the trait that the server needs to implement.
|
||||
fn expand_trait(&self) -> TokenStream {
|
||||
let attrs = &self.attrs;
|
||||
let vis = &self.vis;
|
||||
let ident = &self.ident;
|
||||
let server_ident = &self.server_ident;
|
||||
|
||||
let methods = self.methods.iter().map(
|
||||
|Method {
|
||||
attrs,
|
||||
ident,
|
||||
args,
|
||||
output,
|
||||
..
|
||||
}| {
|
||||
quote! {
|
||||
#( #attrs )*
|
||||
async fn #ident(&self, #( #args ),*) #output;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
quote! {
|
||||
#( #attrs )*
|
||||
#[::reverie_rpc::async_trait::async_trait]
|
||||
#vis trait #ident: ::core::marker::Sync + Sized {
|
||||
#( #methods )*
|
||||
|
||||
/// Returns a type that can be used to serve this service.
|
||||
fn serve(self) -> #server_ident<Self> {
|
||||
#server_ident { service: self }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Expands `struct ServeMyService`
|
||||
/// Expands `impl<S> Service for ServeMyService<S>`
|
||||
fn expand_server(&self) -> TokenStream {
|
||||
let vis = &self.vis;
|
||||
let ident = &self.ident;
|
||||
let request_ident = &self.request_ident;
|
||||
let response_ident = &self.response_ident;
|
||||
let server_ident = &self.server_ident;
|
||||
|
||||
let service_requests = self.methods.iter().map(|method| {
|
||||
let attrs = &method.attrs;
|
||||
let ident = &method.ident;
|
||||
let camel_ident = &method.camel_ident;
|
||||
let arg_pats = method.args.iter().map(|pat_type| &pat_type.pat).collect::<Vec<_>>();
|
||||
|
||||
if method.method_attrs.no_response {
|
||||
quote! {
|
||||
#( #attrs )*
|
||||
#[allow(unused_doc_comments)]
|
||||
#request_ident::#camel_ident { #( #arg_pats ),* } => {
|
||||
self.service.#ident(#( #arg_pats ),*).await;
|
||||
None
|
||||
},
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
#( #attrs )*
|
||||
#[allow(unused_doc_comments)]
|
||||
#request_ident::#camel_ident { #( #arg_pats ),* } => {
|
||||
Some(#response_ident::#camel_ident(self.service.#ident(#( #arg_pats ),*).await))
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let request_lifetime: Option<syn::Generics> = self
|
||||
.request_generics
|
||||
.as_ref()
|
||||
.map(|_| syn::parse_quote!(<'r>));
|
||||
|
||||
// FIXME: Avoid requiring `Send` if possible.
|
||||
quote! {
|
||||
/// A helper for serving the service.
|
||||
#[derive(Clone)]
|
||||
#vis struct #server_ident<S> {
|
||||
service: S,
|
||||
}
|
||||
|
||||
impl<S> #server_ident<S> {
|
||||
pub fn into_inner(self) -> S {
|
||||
self.service
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> core::ops::Deref for #server_ident<S> {
|
||||
type Target = S;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.service
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> core::ops::DerefMut for #server_ident<S> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.service
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> ::reverie_rpc::Service for #server_ident<S>
|
||||
where
|
||||
S: #ident + Send,
|
||||
{
|
||||
type Request<'r> where S: 'r = #request_ident #request_lifetime;
|
||||
type Response = #response_ident;
|
||||
type Future<'a> where S: 'a = ::reverie_rpc::BoxFuture<'a, Option<Self::Response>>;
|
||||
|
||||
fn call<'a>(
|
||||
&'a self,
|
||||
req: Self::Request<'a>,
|
||||
) -> Self::Future<'a> {
|
||||
Box::pin(async move {
|
||||
match req {
|
||||
#( #service_requests )*
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Expands the associated request type for each method's arguments.
|
||||
fn expand_request_enum(&self) -> TokenStream {
|
||||
let variants = self.methods.iter().map(|method| {
|
||||
let attrs = &method.attrs;
|
||||
let ident = &method.camel_ident;
|
||||
let args = method.args.iter().cloned().map(|mut arg| {
|
||||
// If we have an argument with a reference, change it to our
|
||||
// 'req lifetime that is declared on the enum.
|
||||
if let syn::Type::Reference(r) = arg.ty.as_mut() {
|
||||
r.lifetime = Some(syn::Lifetime::new("'req", r.and_token.span()));
|
||||
}
|
||||
|
||||
arg
|
||||
});
|
||||
|
||||
quote! {
|
||||
#( #attrs )*
|
||||
#ident { #( #args ),* },
|
||||
}
|
||||
});
|
||||
|
||||
let vis = &self.vis;
|
||||
let ident = &self.request_ident;
|
||||
let generics = self.request_generics.as_ref();
|
||||
|
||||
quote! {
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug)]
|
||||
#[derive(::reverie_rpc::serde::Serialize, ::reverie_rpc::serde::Deserialize)]
|
||||
#[serde(crate = "reverie_rpc::serde")]
|
||||
#vis enum #ident #generics {
|
||||
#( #variants )*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Expands the associated response type for each method's return type.
|
||||
fn expand_response_enum(&self) -> TokenStream {
|
||||
let variants = self.methods.iter().filter_map(|method| {
|
||||
if method.method_attrs.no_response {
|
||||
// Don't expand this variant if it's a send-only method.
|
||||
return None;
|
||||
}
|
||||
|
||||
let attrs = &method.attrs;
|
||||
let ident = &method.camel_ident;
|
||||
|
||||
let output = match &method.output {
|
||||
syn::ReturnType::Default => quote!(()),
|
||||
syn::ReturnType::Type(_, ret) => quote!(#ret),
|
||||
};
|
||||
|
||||
Some(quote! {
|
||||
#( #attrs )*
|
||||
#ident(#output),
|
||||
})
|
||||
});
|
||||
|
||||
let vis = &self.vis;
|
||||
let ident = &self.response_ident;
|
||||
|
||||
quote! {
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug)]
|
||||
#[derive(::reverie_rpc::serde::Serialize, ::reverie_rpc::serde::Deserialize)]
|
||||
#[serde(crate = "reverie_rpc::serde")]
|
||||
#vis enum #ident {
|
||||
#( #variants )*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn expand_client(&self) -> TokenStream {
|
||||
let vis = &self.vis;
|
||||
let attrs = &self.attrs;
|
||||
let client_ident = &self.client_ident;
|
||||
let request = &self.request_ident;
|
||||
let response = &self.response_ident;
|
||||
|
||||
let req_generics: Option<syn::Generics> = self.request_generics.as_ref().map(|_| {
|
||||
// HACK: The lifetime parameter for the request type shouldn't need
|
||||
// to force the service client type to also have a lifetime
|
||||
// parameter because the request itself isn't stored in the service
|
||||
// client, it's just sent through the channel.
|
||||
//
|
||||
// However, we need the channel to have these parameters so that
|
||||
// `MakeClient` knows the type of the request/response. Thus, in
|
||||
// order to ensure we aren't leaking the request type's lifetime
|
||||
// parameter into the service client's generic parameters and
|
||||
// complicating it's usage, we say that the request type has a
|
||||
// static lifetime and transmute it just before sending it through
|
||||
// the channel. This is terrible, but perfectly safe because we
|
||||
// don't store the request before serializing it. Generic Associated
|
||||
// Types (GATs) might help with this, but they don't support dyn
|
||||
// traits which is useful for enabling nesting of channels (and thus
|
||||
// composition of global state).
|
||||
syn::parse_quote!(<'static>)
|
||||
});
|
||||
|
||||
let methods = self.methods.iter().map(|method| {
|
||||
let attrs = &method.attrs;
|
||||
let ident = &method.ident;
|
||||
let camel_ident = &method.camel_ident;
|
||||
let args = &method.args;
|
||||
let arg_pats = method.args.iter().map(|pat_type| &pat_type.pat);
|
||||
let output = &method.output;
|
||||
|
||||
if method.method_attrs.no_response {
|
||||
quote! {
|
||||
#( #attrs )*
|
||||
pub fn #ident(&self, #( #args ),*) {
|
||||
use ::reverie_rpc::Channel;
|
||||
|
||||
// Transmute is safe because the channel doesn't store
|
||||
// the request type.
|
||||
let request: #request #req_generics = unsafe {
|
||||
::core::mem::transmute(#request::#camel_ident { #( #arg_pats ),* })
|
||||
};
|
||||
|
||||
self.channel.send(&request);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
#( #attrs )*
|
||||
pub fn #ident(&self, #( #args ),*) #output {
|
||||
use ::reverie_rpc::Channel;
|
||||
|
||||
// Transmute is safe because the channel doesn't store
|
||||
// the request type.
|
||||
let request: #request #req_generics = unsafe {
|
||||
::core::mem::transmute(#request::#camel_ident { #( #arg_pats ),* })
|
||||
};
|
||||
|
||||
match self.channel.call(&request) {
|
||||
#response::#camel_ident(ret) => ret,
|
||||
other => panic!("Got unexpected response: {:?}", other),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
#( #attrs )*
|
||||
#vis struct #client_ident {
|
||||
channel: ::reverie_rpc::BoxChannel<#request #req_generics, #response>,
|
||||
}
|
||||
|
||||
impl ::reverie_rpc::MakeClient for #client_ident {
|
||||
type Request = #request #req_generics;
|
||||
type Response = #response;
|
||||
|
||||
fn make_client(channel: ::reverie_rpc::BoxChannel<Self::Request, Self::Response>) -> Self {
|
||||
Self {
|
||||
channel,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl #client_ident {
|
||||
#( #methods )*
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
extern crate proc_macro;
|
||||
|
||||
mod expand;
|
||||
mod parse;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::ToTokens;
|
||||
|
||||
struct Service {
|
||||
attrs: Vec<syn::Attribute>,
|
||||
vis: syn::Visibility,
|
||||
ident: syn::Ident,
|
||||
methods: Vec<Method>,
|
||||
request_ident: syn::Ident,
|
||||
request_generics: Option<syn::Generics>,
|
||||
response_ident: syn::Ident,
|
||||
server_ident: syn::Ident,
|
||||
client_ident: syn::Ident,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, darling::FromMeta)]
|
||||
struct MethodAttrs {
|
||||
/// True if `#[rpc(no_response)]` was specified.
|
||||
#[darling(default)]
|
||||
no_response: bool,
|
||||
}
|
||||
|
||||
struct Method {
|
||||
/// Attributes that should get expanded.
|
||||
attrs: Vec<syn::Attribute>,
|
||||
/// Attributes that only we care about (e.g., `#[rpc(no_response = true)]`)
|
||||
method_attrs: MethodAttrs,
|
||||
/// The method name.
|
||||
ident: syn::Ident,
|
||||
/// The camel-case version of the method name. Used for generating the
|
||||
/// Request and Response enum variants.
|
||||
camel_ident: syn::Ident,
|
||||
// NOTE: We expect all methods to take &self implicitly.
|
||||
args: Vec<syn::PatType>,
|
||||
/// Return type of the method.
|
||||
output: syn::ReturnType,
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn service(_args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
let service = syn::parse_macro_input!(input as Service);
|
||||
service.into_token_stream().into()
|
||||
}
|
|
@ -1,143 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
use darling::FromMeta;
|
||||
use heck::CamelCase;
|
||||
use quote::format_ident;
|
||||
use syn::parse::Parse;
|
||||
use syn::parse::ParseStream;
|
||||
use syn::spanned::Spanned;
|
||||
|
||||
use crate::Method;
|
||||
use crate::MethodAttrs;
|
||||
use crate::Service;
|
||||
|
||||
fn parse_method(method: syn::TraitItemMethod, has_ref: &mut bool) -> syn::Result<Method> {
|
||||
if !method.sig.generics.params.is_empty() {
|
||||
return Err(syn::Error::new(
|
||||
method.sig.generics.span(),
|
||||
"RPC methods cannot have generic parameters",
|
||||
));
|
||||
}
|
||||
|
||||
let ident = method.sig.ident;
|
||||
|
||||
let camel_ident = syn::Ident::new(&ident.to_string().to_camel_case(), ident.span());
|
||||
|
||||
// Search through the attributes and find any with the `rpc`
|
||||
// path. We need to exclude these from getting passed through
|
||||
// and expanded.
|
||||
let mut custom_attrs = None;
|
||||
#[allow(clippy::unnecessary_filter_map)]
|
||||
let attrs: Vec<_> = method
|
||||
.attrs
|
||||
.into_iter()
|
||||
.filter_map(|attrs| {
|
||||
// Clippy complains about this `filter_map` being
|
||||
// equivalent to `filter`, but it's not because `attrs`
|
||||
// needs to be passed by value to the closure so we can
|
||||
// move it out.
|
||||
if attrs.path.is_ident("rpc") {
|
||||
custom_attrs = Some(attrs);
|
||||
None
|
||||
} else {
|
||||
Some(attrs)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let method_attrs = match custom_attrs {
|
||||
Some(custom_attrs) => {
|
||||
let meta = custom_attrs.parse_meta()?;
|
||||
MethodAttrs::from_meta(&meta)?
|
||||
}
|
||||
None => MethodAttrs::default(),
|
||||
};
|
||||
|
||||
if method_attrs.no_response {
|
||||
if method.sig.output != syn::ReturnType::Default {
|
||||
return Err(syn::Error::new(
|
||||
method.sig.output.span(),
|
||||
"#[rpc(no_response)] methods cannot have a return type",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let args = method
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.filter_map(|arg| match arg {
|
||||
syn::FnArg::Receiver(_) => None,
|
||||
syn::FnArg::Typed(t) => {
|
||||
if let syn::Type::Reference(_) = t.ty.as_ref() {
|
||||
*has_ref = true;
|
||||
}
|
||||
Some(t.clone())
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Method {
|
||||
attrs,
|
||||
method_attrs,
|
||||
ident,
|
||||
camel_ident,
|
||||
args,
|
||||
output: method.sig.output,
|
||||
})
|
||||
}
|
||||
|
||||
impl Parse for Service {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let t: syn::ItemTrait = input.parse()?;
|
||||
|
||||
let attrs = t.attrs;
|
||||
let vis = t.vis;
|
||||
let ident = t.ident;
|
||||
|
||||
let mut has_ref = false;
|
||||
let mut methods = Vec::new();
|
||||
|
||||
for inner in t.items {
|
||||
if let syn::TraitItem::Method(method) = inner {
|
||||
if method.sig.ident == "serve" {
|
||||
return Err(syn::Error::new(
|
||||
ident.span(),
|
||||
format!("method conflicts with generated fn {}::serve", ident),
|
||||
));
|
||||
}
|
||||
|
||||
methods.push(parse_method(method, &mut has_ref)?);
|
||||
}
|
||||
}
|
||||
|
||||
let request_ident = format_ident!("{}Request", ident, span = ident.span());
|
||||
let response_ident = format_ident!("{}Response", ident, span = ident.span());
|
||||
let server_ident = format_ident!("Serve{}", ident, span = ident.span());
|
||||
let client_ident = format_ident!("{}Client", ident, span = ident.span());
|
||||
|
||||
let request_generics = if has_ref {
|
||||
Some(syn::parse_quote!(<'req>))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
attrs,
|
||||
vis,
|
||||
ident,
|
||||
methods,
|
||||
request_ident,
|
||||
request_generics,
|
||||
response_ident,
|
||||
server_ident,
|
||||
client_ident,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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"] }
|
|
@ -1,57 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
/// Represents a bidirectional stream of messages.
|
||||
pub trait Channel<Req, Res>
|
||||
where
|
||||
Req: Serialize,
|
||||
Res: for<'a> Deserialize<'a>,
|
||||
{
|
||||
/// Sends a request, but does not expect a response. This is useful when
|
||||
/// some requests don't have an associated response.
|
||||
fn send(&self, item: &Req);
|
||||
|
||||
/// Sends a request and waits for a response from the server.
|
||||
fn call(&self, item: &Req) -> Res;
|
||||
}
|
||||
|
||||
pub type BoxChannel<Req, Res> = Box<dyn Channel<Req, Res> + Send + Sync + 'static>;
|
||||
|
||||
pub trait MakeClient {
|
||||
type Request: Serialize;
|
||||
type Response: for<'a> Deserialize<'a>;
|
||||
|
||||
fn make_client(channel: BoxChannel<Self::Request, Self::Response>) -> Self;
|
||||
}
|
||||
|
||||
// Dummy impl for (), so we can easily use this for tools that don't use global
|
||||
// state.
|
||||
impl MakeClient for () {
|
||||
type Request = ();
|
||||
type Response = ();
|
||||
|
||||
fn make_client(_channel: BoxChannel<Self::Request, Self::Response>) -> Self {}
|
||||
}
|
||||
|
||||
impl<T, Req, Res> Channel<Req, Res> for Box<T>
|
||||
where
|
||||
T: Channel<Req, Res> + ?Sized,
|
||||
Req: Serialize,
|
||||
Res: for<'a> Deserialize<'a>,
|
||||
{
|
||||
fn send(&self, item: &Req) {
|
||||
self.as_ref().send(item)
|
||||
}
|
||||
|
||||
fn call(&self, item: &Req) -> Res {
|
||||
self.as_ref().call(item)
|
||||
}
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
|
||||
use bincode::Options;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
fn bincode_options() -> impl bincode::Options {
|
||||
// NOTE: Both the server and client must agree on these bincode options.
|
||||
// Otherwise, we'll get deserialization errors.
|
||||
bincode::DefaultOptions::new().with_limit(16 * (1 << 20) /* 16MB */)
|
||||
}
|
||||
|
||||
pub fn encode<T>(item: &T, buf: &mut Vec<u8>) -> io::Result<()>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
let mut cursor = io::Cursor::new(buf);
|
||||
|
||||
// Reserve 4 bytes at the beginning of the buffer so we can fill it in
|
||||
// with the length of the payload once we know what it is.
|
||||
cursor.write_all(&[0, 0, 0, 0])?;
|
||||
|
||||
// Serialize into our buffer.
|
||||
encode_frame_into(&mut cursor, item)?;
|
||||
|
||||
let buf = cursor.into_inner();
|
||||
|
||||
// Fill in the actual size now that we know what it is.
|
||||
let size = buf[4..].len() as u32;
|
||||
buf[0..4].copy_from_slice(&size.to_be_bytes());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Encodes a length-delimited frame.
|
||||
pub fn encode_frame_into<W, T>(writer: W, item: &T) -> io::Result<()>
|
||||
where
|
||||
T: Serialize + ?Sized,
|
||||
W: Write,
|
||||
{
|
||||
bincode_options().serialize_into(writer, item).map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("failed to encode frame: {}", e),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Decodes a length-delimited frame.
|
||||
pub fn decode_frame<'a, T>(frame: &'a [u8]) -> io::Result<T>
|
||||
where
|
||||
T: Deserialize<'a>,
|
||||
{
|
||||
bincode_options().deserialize(frame).map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("failed to decode frame: {}", e),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn decode_from<'a, T, R>(mut reader: R, buf: &'a mut Vec<u8>) -> io::Result<T>
|
||||
where
|
||||
T: Deserialize<'a>,
|
||||
R: io::Read,
|
||||
{
|
||||
let mut head = [0u8; 4];
|
||||
reader.read_exact(&mut head)?;
|
||||
|
||||
let len = u32::from_be_bytes(head) as usize;
|
||||
|
||||
buf.resize(len, 0);
|
||||
|
||||
reader.read_exact(buf)?;
|
||||
|
||||
decode_frame(buf)
|
||||
}
|
|
@ -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::*;
|
|
@ -1,54 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
use core::future::Future;
|
||||
use core::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
|
||||
|
||||
pub trait Service {
|
||||
type Request<'r>: Deserialize<'r> + Unpin + Send
|
||||
where
|
||||
Self: 'r;
|
||||
type Response: Serialize + Send + Unpin;
|
||||
type Future<'a>: Future<Output = Option<Self::Response>> + Send + 'a
|
||||
where
|
||||
Self: 'a;
|
||||
|
||||
/// Makes a "call" to our service. Returns `None` if the service has no
|
||||
/// response for the client (i.e., it was a send-only request).
|
||||
fn call<'a>(&'a self, req: Self::Request<'a>) -> Self::Future<'a>;
|
||||
}
|
||||
|
||||
impl<S> Service for Arc<S>
|
||||
where
|
||||
S: Service,
|
||||
{
|
||||
type Request<'r> = S::Request<'r> where S: 'r;
|
||||
type Response = S::Response;
|
||||
type Future<'a>
|
||||
= S::Future<'a> where S: 'a;
|
||||
|
||||
fn call<'a>(&'a self, req: Self::Request<'a>) -> Self::Future<'a> {
|
||||
self.as_ref().call(req)
|
||||
}
|
||||
}
|
||||
|
||||
impl Service for () {
|
||||
type Request<'r> = ();
|
||||
type Response = ();
|
||||
type Future<'a> = core::future::Ready<Option<()>>;
|
||||
|
||||
fn call<'a>(&'a self, _req: ()) -> Self::Future<'a> {
|
||||
core::future::ready(Some(()))
|
||||
}
|
||||
}
|
|
@ -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"] }
|
|
@ -1,155 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use quote::ToTokens;
|
||||
|
||||
use crate::Tool;
|
||||
|
||||
impl ToTokens for Tool {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let mut item_impl = self.outer_item.clone();
|
||||
|
||||
let ty = &item_impl.self_ty;
|
||||
// Implement ToolGlobal so that we can access it from any of the
|
||||
// callbacks specified by sbr_init.
|
||||
tokens.extend(quote! {
|
||||
impl ::reverie_sabre::ToolGlobal for #ty {
|
||||
type Target = #ty;
|
||||
|
||||
fn global() -> &'static Self::Target {
|
||||
use ::reverie_sabre::internal::OnceCell;
|
||||
static __TOOL_INSTANCE: OnceCell<#ty> = OnceCell::new();
|
||||
__TOOL_INSTANCE.get_or_init(::reverie_sabre::internal::init_tool)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut fn_icept_structs = quote! {};
|
||||
|
||||
let mut type_impl = quote! {};
|
||||
// Traversing "detoured" methods declarations and preparing the plumbing
|
||||
// to hook up to sabre we need a callback, a field to hold a pointer to
|
||||
// original function, a stub pointer, DETOURS strucure, etc
|
||||
for method in &self.detoured_methods {
|
||||
let field_ident = &method.undetoured_field_name;
|
||||
let callback_ident = &method.callback_name;
|
||||
let stub_ident = &method.stub_name;
|
||||
let undetoured_method_name = &method.undetoured_method_name;
|
||||
let args = &method.outer_item.sig.inputs;
|
||||
let arg_pats = args
|
||||
.iter()
|
||||
.filter_map(|pat_type| match pat_type {
|
||||
syn::FnArg::Typed(pat) => Some(&pat.pat),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let output = &method.outer_item.sig.output;
|
||||
let function_type_name = &method.detoured_function_type_name;
|
||||
let original_marked_method = &method.outer_item;
|
||||
let detoured_definition_name = &method.detoured_definition_name;
|
||||
|
||||
let lib_name_c_str = syn::LitStr::new(
|
||||
format!("{0}\0", method.attrs.lib).as_str(),
|
||||
proc_macro2::Span::call_site(),
|
||||
);
|
||||
|
||||
let func_name_c_str = syn::LitStr::new(
|
||||
format!("{0}\0", method.attrs.func).as_str(),
|
||||
proc_macro2::Span::call_site(),
|
||||
);
|
||||
|
||||
fn_icept_structs.extend(quote! {
|
||||
sabre::ffi::fn_icept {
|
||||
lib_name: #lib_name_c_str.as_ptr() as *const i8,
|
||||
fn_name: #func_name_c_str.as_ptr() as *const i8,
|
||||
icept_callback: #callback_ident,
|
||||
},
|
||||
});
|
||||
|
||||
type_impl.extend(quote! {
|
||||
fn #undetoured_method_name(#args) #output {
|
||||
unsafe {
|
||||
if let Some(f) = #field_ident {
|
||||
return f(#(#arg_pats),*);
|
||||
}
|
||||
panic!("original function wasn't captured");
|
||||
}
|
||||
}
|
||||
|
||||
#original_marked_method
|
||||
});
|
||||
|
||||
tokens.extend(quote! {
|
||||
type #function_type_name = fn(#args) #output;
|
||||
static mut #field_ident: Option<#function_type_name> = None;
|
||||
|
||||
unsafe extern "C" fn #stub_ident(#args) #output {
|
||||
#ty::#detoured_definition_name(#(#arg_pats),*)
|
||||
}
|
||||
|
||||
extern "C" fn #callback_ident(func: sabre::ffi::void_void_fn) -> sabre::ffi::void_void_fn {
|
||||
unsafe {
|
||||
#field_ident = Some(std::mem::transmute(func));
|
||||
std::mem::transmute(#stub_ident as *const())
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
tokens.extend(quote! {
|
||||
impl #ty {
|
||||
#type_impl
|
||||
}
|
||||
});
|
||||
|
||||
item_impl.items.push(syn::ImplItem::Method(
|
||||
syn::parse2(quote! {
|
||||
fn detours() -> &'static [sabre::ffi::fn_icept] {
|
||||
static DETOURS: &[sabre::ffi::fn_icept] = &[
|
||||
#fn_icept_structs
|
||||
];
|
||||
DETOURS
|
||||
}
|
||||
})
|
||||
.unwrap(),
|
||||
));
|
||||
|
||||
// Expand the original `impl Tool for MyTool` block.
|
||||
item_impl.to_tokens(tokens);
|
||||
|
||||
// Implement the entry point for our plugin.
|
||||
tokens.extend(quote! {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sbr_init(
|
||||
argc: *mut i32,
|
||||
argv: *mut *mut *mut libc::c_char,
|
||||
fn_icept_reg: sabre::ffi::icept_reg_fn,
|
||||
vdso_callback: *mut Option<sabre::ffi::handle_vdso_fn>,
|
||||
syscall_handler: *mut Option<sabre::ffi::handle_syscall_fn>,
|
||||
rdtsc_handler: *mut Option<sabre::ffi::handle_rdtsc_fn>,
|
||||
post_load: *mut Option<sabre::ffi::post_load_fn>,
|
||||
sabre_path: *const libc::c_char,
|
||||
plugin_path: *const libc::c_char,
|
||||
) {
|
||||
::reverie_sabre::internal::sbr_init::<#ty>(
|
||||
argc,
|
||||
argv,
|
||||
fn_icept_reg,
|
||||
vdso_callback,
|
||||
syscall_handler,
|
||||
rdtsc_handler,
|
||||
post_load,
|
||||
sabre_path,
|
||||
plugin_path,
|
||||
)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
extern crate proc_macro;
|
||||
|
||||
mod expand;
|
||||
mod parse;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::ToTokens;
|
||||
|
||||
struct Tool {
|
||||
outer_item: syn::ItemImpl,
|
||||
detoured_methods: Vec<Detour>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, darling::FromMeta)]
|
||||
struct DetourAttrs {
|
||||
func: String,
|
||||
lib: String,
|
||||
}
|
||||
|
||||
struct Detour {
|
||||
callback_name: syn::Ident,
|
||||
stub_name: syn::Ident,
|
||||
undetoured_field_name: syn::Ident,
|
||||
undetoured_method_name: syn::Ident,
|
||||
detoured_definition_name: syn::Ident,
|
||||
detoured_function_type_name: syn::Ident,
|
||||
attrs: DetourAttrs,
|
||||
outer_item: syn::ImplItemMethod,
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn tool(_args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
let service = syn::parse_macro_input!(input as Tool);
|
||||
service.into_token_stream().into()
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
use darling::FromMeta;
|
||||
use heck::CamelCase;
|
||||
use quote::format_ident;
|
||||
use syn::parse::Parse;
|
||||
use syn::parse::ParseStream;
|
||||
|
||||
use crate::Detour;
|
||||
use crate::DetourAttrs;
|
||||
use crate::Tool;
|
||||
|
||||
fn parse_method(method: &syn::ImplItemMethod, attribute: &syn::Attribute) -> syn::Result<Detour> {
|
||||
let meta = attribute.parse_meta()?;
|
||||
let method_attrs = DetourAttrs::from_meta(&meta)?;
|
||||
let mut outer_item = method.clone();
|
||||
outer_item.attrs.retain(|a| !a.path.is_ident("detour"));
|
||||
Ok(Detour {
|
||||
outer_item,
|
||||
callback_name: format_ident!("{}_{}_callback", method_attrs.lib, method_attrs.func),
|
||||
stub_name: format_ident!("{}_{}_stub", method_attrs.lib, method_attrs.func),
|
||||
detoured_definition_name: format_ident!("{}", method.sig.ident.to_string()),
|
||||
undetoured_field_name: format_ident!(
|
||||
"{}_{}_UNDETOURED",
|
||||
method_attrs.lib.clone().to_uppercase(),
|
||||
method_attrs.func.clone().to_uppercase()
|
||||
),
|
||||
undetoured_method_name: format_ident!(
|
||||
"{}_{}_undetoured",
|
||||
method_attrs.lib,
|
||||
method_attrs.func
|
||||
),
|
||||
detoured_function_type_name: syn::Ident::new(
|
||||
format!(
|
||||
"{}{}Func",
|
||||
method_attrs.lib.clone().to_uppercase(),
|
||||
method_attrs.func.clone().to_uppercase()
|
||||
)
|
||||
.as_str()
|
||||
.to_camel_case()
|
||||
.as_str(),
|
||||
proc_macro2::Span::call_site(),
|
||||
),
|
||||
attrs: method_attrs,
|
||||
})
|
||||
}
|
||||
|
||||
fn filter_map_attribute<'a>(
|
||||
method: &'a syn::ImplItemMethod,
|
||||
attribute: &str,
|
||||
) -> Option<&'a syn::Attribute> {
|
||||
method.attrs.iter().find(|a| a.path.is_ident(attribute))
|
||||
}
|
||||
|
||||
impl Parse for Tool {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let mut impl_node: syn::ItemImpl = input.parse()?;
|
||||
|
||||
let detour_methods: Result<Vec<_>, _> = impl_node
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|n| match n {
|
||||
syn::ImplItem::Method(method_impl) => filter_map_attribute(method_impl, "detour")
|
||||
.map(|a| parse_method(method_impl, a)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
impl_node.items.retain(|m| match m {
|
||||
syn::ImplItem::Method(method) => filter_map_attribute(method, "detour").is_none(),
|
||||
_ => true,
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
outer_item: impl_node,
|
||||
detoured_methods: detour_methods?,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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"] }
|
|
@ -1,282 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
use reverie_syscalls::LocalMemory;
|
||||
use reverie_syscalls::Syscall;
|
||||
use syscalls::syscall;
|
||||
use syscalls::SyscallArgs;
|
||||
use syscalls::Sysno;
|
||||
|
||||
use super::ffi;
|
||||
use super::thread;
|
||||
use super::thread::GuestTransitionErr;
|
||||
use super::thread::PidTid;
|
||||
use super::thread::Thread;
|
||||
use super::tool::Tool;
|
||||
use super::tool::ToolGlobal;
|
||||
use super::utils;
|
||||
use super::vdso;
|
||||
use crate::signal::guard;
|
||||
|
||||
pub const CONTROLLED_EXIT_SIGNAL: libc::c_int = libc::SIGSTKFLT;
|
||||
|
||||
/// Implement the thread notifier trait for any global tools
|
||||
impl<T> thread::EventSink for T
|
||||
where
|
||||
T: ToolGlobal,
|
||||
{
|
||||
#[inline]
|
||||
fn on_new_thread(pid_tid: PidTid) {
|
||||
T::global().on_thread_start(pid_tid.tid);
|
||||
}
|
||||
|
||||
fn on_thread_exit(pid_tid: PidTid) {
|
||||
T::global().on_thread_exit(pid_tid.tid);
|
||||
}
|
||||
}
|
||||
|
||||
pub extern "C" fn handle_syscall<T: ToolGlobal>(
|
||||
syscall: isize,
|
||||
arg1: usize,
|
||||
arg2: usize,
|
||||
arg3: usize,
|
||||
arg4: usize,
|
||||
arg5: usize,
|
||||
arg6: usize,
|
||||
wrapper_sp: *mut ffi::syscall_stackframe,
|
||||
) -> usize {
|
||||
let mut thread = if let Some(thread) = Thread::<T>::current() {
|
||||
thread
|
||||
} else {
|
||||
terminate(1);
|
||||
};
|
||||
|
||||
match handle_syscall_with_thread::<T>(
|
||||
&mut thread,
|
||||
syscall,
|
||||
arg1,
|
||||
arg2,
|
||||
arg3,
|
||||
arg4,
|
||||
arg5,
|
||||
arg6,
|
||||
wrapper_sp,
|
||||
) {
|
||||
Ok(return_code) => return_code,
|
||||
Err(GuestTransitionErr::ExitNow) => terminate(0),
|
||||
Err(GuestTransitionErr::ExitingElsewhere) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle the critical section for the given system call on the given thread
|
||||
#[allow(clippy::if_same_then_else)]
|
||||
fn handle_syscall_with_thread<T: ToolGlobal>(
|
||||
thread: &mut Thread<T>,
|
||||
syscall: isize,
|
||||
arg1: usize,
|
||||
arg2: usize,
|
||||
arg3: usize,
|
||||
arg4: usize,
|
||||
arg5: usize,
|
||||
arg6: usize,
|
||||
wrapper_sp: *mut ffi::syscall_stackframe,
|
||||
) -> Result<usize, GuestTransitionErr> {
|
||||
let _guard = guard::enter_signal_exclusion_zone();
|
||||
thread.leave_guest_execution()?;
|
||||
|
||||
let sys_no = Sysno::from(syscall as i32);
|
||||
|
||||
let result = if sys_no == Sysno::clone && arg2 != 0 {
|
||||
thread.maybe_fork_as_guest(|| unsafe {
|
||||
ffi::clone_syscall(
|
||||
arg1,
|
||||
arg2 as *mut libc::c_void,
|
||||
arg3 as *mut i32,
|
||||
arg4 as *mut i32,
|
||||
arg5,
|
||||
(*wrapper_sp).ret,
|
||||
)
|
||||
})?
|
||||
} else if sys_no == Sysno::clone {
|
||||
thread.maybe_fork_as_guest(|| {
|
||||
let args = SyscallArgs::new(arg1, arg2, arg3, arg4, arg5, arg6);
|
||||
let syscall = Syscall::from_raw(sys_no, args);
|
||||
|
||||
T::global()
|
||||
.syscall(syscall, &LocalMemory::new())
|
||||
.map_or_else(|e| -e.into_raw() as usize, |x| x as usize)
|
||||
})?
|
||||
} else if utils::is_vfork(sys_no, arg1) {
|
||||
thread.maybe_fork_as_guest(|| unsafe {
|
||||
let pid = ffi::vfork_syscall();
|
||||
if pid == 0 {
|
||||
// Child
|
||||
|
||||
// Even though this function doesn't return, this is
|
||||
// safe because the thread is in `Guest` and that state
|
||||
// will be correct in the child application when the
|
||||
// jmp takes it there
|
||||
ffi::vfork_return_from_child(wrapper_sp)
|
||||
} else {
|
||||
// parent
|
||||
pid
|
||||
}
|
||||
})?
|
||||
} else if sys_no == Sysno::clone3 {
|
||||
let cl_args = unsafe { &*(arg1 as *const ffi::clone_args) };
|
||||
if cl_args.stack == 0 {
|
||||
thread.maybe_fork_as_guest(|| unsafe {
|
||||
syscall!(sys_no, arg1, arg2, arg3, arg4, arg5, arg6)
|
||||
.map_or_else(|e| -e.into_raw() as usize, |x| x as usize)
|
||||
})?
|
||||
} else {
|
||||
thread.maybe_fork_as_guest(|| unsafe {
|
||||
ffi::clone3_syscall(arg1, arg2, arg3, 0, arg5, (*wrapper_sp).ret)
|
||||
})?
|
||||
}
|
||||
} else if sys_no == Sysno::exit {
|
||||
// intercept the exit_group syscall and signal all the threads to exit
|
||||
// in a predictable and trackable way
|
||||
if thread.try_exit() {
|
||||
terminate(arg1);
|
||||
}
|
||||
0
|
||||
} else if sys_no == Sysno::exit_group {
|
||||
// intercept the exit_group syscall and signal all the threads to exit
|
||||
// in a predictable and trackable way
|
||||
exit_group_with_thread(thread, arg1)
|
||||
} else {
|
||||
let args = SyscallArgs::new(arg1, arg2, arg3, arg4, arg5, arg6);
|
||||
let syscall = Syscall::from_raw(sys_no, args);
|
||||
|
||||
thread.execute_as_guest(|| {
|
||||
T::global()
|
||||
.syscall(syscall, &LocalMemory::new())
|
||||
.map_or_else(|e| -e.into_raw() as usize, |x| x as usize)
|
||||
})?
|
||||
};
|
||||
|
||||
thread.enter_guest_execution()?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Terminate this thread with no notifications
|
||||
fn terminate(exit_code: usize) -> ! {
|
||||
unsafe {
|
||||
syscalls::syscall1(Sysno::exit, exit_code).expect("Exit should succeed");
|
||||
}
|
||||
unreachable!("The thread should have ended by now");
|
||||
}
|
||||
|
||||
/// Perform and exit group with the current thread
|
||||
fn exit_group_with_thread<T: ToolGlobal>(thread: &mut Thread<T>, exit_code: usize) -> usize {
|
||||
thread.try_exit();
|
||||
if let Some(exiting_pid) = thread::exit_all(|_, process_and_thread_id| unsafe {
|
||||
syscalls::syscall3(
|
||||
Sysno::tgkill,
|
||||
process_and_thread_id.pid as usize,
|
||||
process_and_thread_id.tid as usize,
|
||||
CONTROLLED_EXIT_SIGNAL as usize,
|
||||
)
|
||||
.expect("Signaling thread failed");
|
||||
}) {
|
||||
if !thread::wait_for_all_to_exit(exiting_pid, T::global().get_exit_timeout()) {
|
||||
T::global().on_exit_timeout()
|
||||
} else {
|
||||
terminate(exit_code)
|
||||
}
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn exit_group<T: ToolGlobal>(exit_code: usize) -> usize {
|
||||
if let Some(mut thread) = Thread::<T>::current() {
|
||||
exit_group_with_thread(&mut thread, exit_code)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// If any thread receives the exit signal call, this handler will gracefully
|
||||
/// exit that thread
|
||||
pub extern "C" fn handle_exit_signal<T: ToolGlobal>(
|
||||
_: libc::c_int,
|
||||
_: *const libc::siginfo_t,
|
||||
_: *const libc::c_void,
|
||||
) {
|
||||
let mut thread = if let Some(thread) = Thread::<T>::current() {
|
||||
thread
|
||||
} else {
|
||||
terminate(0);
|
||||
};
|
||||
|
||||
if thread.try_exit() {
|
||||
terminate(0);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn handle_vdso_clock_gettime<T: ToolGlobal>(
|
||||
clockid: libc::clockid_t,
|
||||
tp: *mut libc::timespec,
|
||||
) -> i32 {
|
||||
T::global().vdso_clock_gettime(clockid, tp)
|
||||
}
|
||||
|
||||
extern "C" fn handle_vdso_getcpu<T: ToolGlobal>(
|
||||
cpu: *mut u32,
|
||||
node: *mut u32,
|
||||
_unused: usize,
|
||||
) -> i32 {
|
||||
T::global().vdso_getcpu(cpu, node, _unused)
|
||||
}
|
||||
|
||||
extern "C" fn handle_vdso_gettimeofday<T: ToolGlobal>(
|
||||
tv: *mut libc::timeval,
|
||||
tz: *mut libc::timezone,
|
||||
) -> i32 {
|
||||
T::global().vdso_gettimeofday(tv, tz)
|
||||
}
|
||||
|
||||
extern "C" fn handle_vdso_time<T: ToolGlobal>(tloc: *mut libc::time_t) -> i32 {
|
||||
T::global().vdso_time(tloc)
|
||||
}
|
||||
|
||||
pub extern "C" fn handle_vdso<T: ToolGlobal>(
|
||||
syscall: isize,
|
||||
actual_fn: ffi::void_void_fn,
|
||||
) -> Option<ffi::void_void_fn> {
|
||||
use core::mem::transmute;
|
||||
|
||||
unsafe {
|
||||
match Sysno::from(syscall as i32) {
|
||||
Sysno::clock_gettime => {
|
||||
vdso::clock_gettime = transmute(actual_fn as *const ());
|
||||
transmute(handle_vdso_clock_gettime::<T> as *const ())
|
||||
}
|
||||
Sysno::getcpu => {
|
||||
vdso::getcpu = transmute(actual_fn as *const ());
|
||||
transmute(handle_vdso_getcpu::<T> as *const ())
|
||||
}
|
||||
Sysno::gettimeofday => {
|
||||
vdso::gettimeofday = transmute(actual_fn as *const ());
|
||||
transmute(handle_vdso_gettimeofday::<T> as *const ())
|
||||
}
|
||||
Sysno::time => {
|
||||
vdso::time = transmute(actual_fn as *const ());
|
||||
transmute(handle_vdso_time::<T> as *const ())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub extern "C" fn handle_rdtsc<T: ToolGlobal>() -> u64 {
|
||||
T::global().rdtsc()
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,139 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#![allow(non_camel_case_types)]
|
||||
|
||||
mod clone;
|
||||
|
||||
pub use clone::clone3_syscall;
|
||||
pub use clone::clone_syscall;
|
||||
pub use clone::vfork_return_from_child;
|
||||
pub use clone::vfork_syscall;
|
||||
|
||||
extern "C" {
|
||||
pub fn calling_from_plugin() -> bool;
|
||||
pub fn enter_plugin();
|
||||
pub fn exit_plugin();
|
||||
pub fn is_vdso_ready() -> bool;
|
||||
}
|
||||
|
||||
pub type vdso_clock_gettime_fn =
|
||||
extern "C" fn(clockid: libc::clockid_t, tp: *mut libc::timespec) -> i32;
|
||||
pub type vdso_getcpu_fn = extern "C" fn(cpu: *mut u32, node: *mut u32, _unused: usize) -> i32;
|
||||
pub type vdso_gettimeofday_fn =
|
||||
extern "C" fn(tv: *mut libc::timeval, tz: *mut libc::timezone) -> i32;
|
||||
pub type vdso_time_fn = extern "C" fn(tloc: *mut libc::time_t) -> i32;
|
||||
|
||||
pub extern "C" fn vdso_clock_gettime_stub(
|
||||
_clockid: libc::clockid_t,
|
||||
_tp: *mut libc::timespec,
|
||||
) -> i32 {
|
||||
// HACK: These are never called, but referencing these functions ensures
|
||||
// they get linked into our binary. These are actually used by the loader.
|
||||
unsafe { calling_from_plugin() };
|
||||
unsafe { enter_plugin() };
|
||||
unsafe { exit_plugin() };
|
||||
unsafe { is_vdso_ready() };
|
||||
|
||||
-libc::EFAULT
|
||||
}
|
||||
|
||||
pub extern "C" fn vdso_getcpu_stub(_cpu: *mut u32, _node: *mut u32, _unused: usize) -> i32 {
|
||||
-libc::EFAULT
|
||||
}
|
||||
|
||||
pub extern "C" fn vdso_gettimeofday_stub(_tv: *mut libc::timeval, _tz: *mut libc::timezone) -> i32 {
|
||||
-libc::EFAULT
|
||||
}
|
||||
|
||||
pub extern "C" fn vdso_time_stub(_tloc: *mut libc::time_t) -> i32 {
|
||||
-libc::EFAULT
|
||||
}
|
||||
|
||||
pub type void_void_fn = unsafe extern "C" fn() -> *mut libc::c_void;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct fn_icept {
|
||||
pub lib_name: *const libc::c_char,
|
||||
pub fn_name: *const libc::c_char,
|
||||
pub icept_callback: extern "C" fn(void_void_fn) -> void_void_fn,
|
||||
}
|
||||
|
||||
pub type icept_reg_fn = extern "C" fn(*const fn_icept);
|
||||
|
||||
unsafe impl Send for fn_icept {}
|
||||
unsafe impl Sync for fn_icept {}
|
||||
#[repr(C)]
|
||||
pub struct syscall_stackframe {
|
||||
pub rbp_stackalign: *mut libc::c_void,
|
||||
pub r15: *mut libc::c_void,
|
||||
pub r14: *mut libc::c_void,
|
||||
pub r13: *mut libc::c_void,
|
||||
pub r12: *mut libc::c_void,
|
||||
pub r11: *mut libc::c_void,
|
||||
pub r10: *mut libc::c_void,
|
||||
pub r9: *mut libc::c_void,
|
||||
pub r8: *mut libc::c_void,
|
||||
pub rdi: *mut libc::c_void,
|
||||
pub rsi: *mut libc::c_void,
|
||||
pub rdx: *mut libc::c_void,
|
||||
pub rcx: *mut libc::c_void,
|
||||
pub rbx: *mut libc::c_void,
|
||||
pub rbp_prologue: *mut libc::c_void,
|
||||
// trampoline
|
||||
pub fake_ret: *mut libc::c_void,
|
||||
/// Syscall return address. This is where execution should continue after a
|
||||
/// syscall has been handled.
|
||||
pub ret: *mut libc::c_void,
|
||||
}
|
||||
|
||||
pub type handle_syscall_fn = extern "C" fn(
|
||||
syscall: isize,
|
||||
arg1: usize,
|
||||
arg2: usize,
|
||||
arg3: usize,
|
||||
arg4: usize,
|
||||
arg5: usize,
|
||||
arg6: usize,
|
||||
wrapper_sp: *mut syscall_stackframe,
|
||||
) -> usize;
|
||||
|
||||
pub type handle_vdso_fn =
|
||||
extern "C" fn(syscall: isize, actual_fn: void_void_fn) -> Option<void_void_fn>;
|
||||
|
||||
pub type handle_rdtsc_fn = extern "C" fn() -> u64;
|
||||
|
||||
pub type post_load_fn = extern "C" fn(bool);
|
||||
|
||||
/// A struct of arguments for the clone3 syscall.
|
||||
#[derive(Debug)]
|
||||
#[repr(C)]
|
||||
pub struct clone_args {
|
||||
// Flags bit mask
|
||||
pub flags: u64,
|
||||
// Where to store PID file descriptor (int *)
|
||||
pub pidfd: u64,
|
||||
// Where to store child TID, in child's memory (pid_t *)
|
||||
pub child_tid: u64,
|
||||
// Where to store child TID, in parent's memory (pid_t *)
|
||||
pub parent_tid: u64,
|
||||
// Signal to deliver to parent on child termination
|
||||
pub exit_signal: u64,
|
||||
// Pointer to lowest byte of stack
|
||||
pub stack: u64,
|
||||
// Size of stack
|
||||
pub stack_size: u64,
|
||||
// Location of new TLS
|
||||
pub tls: u64,
|
||||
// Pointer to a pid_t array (since Linux 5.5)
|
||||
pub set_tid: u64,
|
||||
// Number of elements in set_tid (since Linux 5.5)
|
||||
pub set_tid_size: u64,
|
||||
// File descriptor for target cgroup of child (since Linux 5.7)
|
||||
pub cgroup: u64,
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
../../../../../../../third-party/sabre/plugin_api/recursion_protector.c
|
|
@ -1 +0,0 @@
|
|||
../../../../../../../third-party/sabre/plugin_api/arch/x86_64/vfork_syscall.s
|
|
@ -1,72 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
//! Everything defined here shouldn't be used directly. However, they must be
|
||||
//! exposed so that code generated by the proc macros can use them.
|
||||
|
||||
#![doc(hidden)]
|
||||
pub use ::once_cell::sync::OnceCell;
|
||||
use reverie_rpc::MakeClient;
|
||||
|
||||
use super::callbacks;
|
||||
use super::ffi;
|
||||
use super::paths;
|
||||
use super::rpc;
|
||||
use super::signal;
|
||||
use super::tool::Tool;
|
||||
use super::tool::ToolGlobal;
|
||||
|
||||
/// Creates an instance of a Tool. This is called when `ToolGlobal::global` is
|
||||
/// called for the first time.
|
||||
pub fn init_tool<T: Tool>() -> T {
|
||||
// Create the base transport channel. This transport layer can be wrapped
|
||||
// potentially many times by nested tools. If this fails (i.e., it failed to
|
||||
// connect to the socket), there isn't anything we can do except panic. A
|
||||
// client without a connection to the global state isn't very useful.
|
||||
let channel = rpc::BaseChannel::new().unwrap();
|
||||
|
||||
T::new(MakeClient::make_client(Box::new(channel)))
|
||||
}
|
||||
|
||||
fn register_detours<T: ToolGlobal>(fn_icept_reg: ffi::icept_reg_fn) {
|
||||
for detour_func in <<T as ToolGlobal>::Target>::detours() {
|
||||
fn_icept_reg(detour_func);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sbr_init<T: ToolGlobal>(
|
||||
argc: *mut i32,
|
||||
argv: *mut *mut *mut libc::c_char,
|
||||
fn_icept_reg: ffi::icept_reg_fn,
|
||||
vdso_callback: *mut Option<ffi::handle_vdso_fn>,
|
||||
syscall_handler: *mut Option<ffi::handle_syscall_fn>,
|
||||
rdtsc_handler: *mut Option<ffi::handle_rdtsc_fn>,
|
||||
_post_load: *mut Option<ffi::post_load_fn>,
|
||||
sabre_path: *const libc::c_char,
|
||||
client_path: *const libc::c_char,
|
||||
) {
|
||||
unsafe {
|
||||
*vdso_callback = Some(callbacks::handle_vdso::<T>);
|
||||
*syscall_handler = Some(callbacks::handle_syscall::<T>);
|
||||
*rdtsc_handler = Some(callbacks::handle_rdtsc::<T>);
|
||||
|
||||
paths::set_sabre_path(sabre_path);
|
||||
paths::set_client_path(client_path);
|
||||
|
||||
// The plugin path is the first argument.
|
||||
paths::set_plugin_path(**argv);
|
||||
|
||||
signal::register_central_handler::<T>();
|
||||
|
||||
*argc -= 1;
|
||||
*argv = (*argv).wrapping_add(1);
|
||||
|
||||
// Setting up function detours
|
||||
register_detours::<T>(fn_icept_reg);
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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) }
|
||||
}
|
|
@ -1,210 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
//! Protects a set of file descriptors from getting closed.
|
||||
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::os::unix::io::RawFd;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use syscalls::Sysno;
|
||||
use syscalls::SysnoSet;
|
||||
|
||||
// TODO: Remove this lazy_static after upgrading to parking_lot >= 0.12.1.
|
||||
// Mutex::new is a const fn in newer versions.
|
||||
lazy_static::lazy_static! {
|
||||
/// A set of file descriptors that should not get closed.
|
||||
static ref PROTECTED_FILES: Mutex<ProtectedFiles> = Mutex::new(ProtectedFiles::new());
|
||||
}
|
||||
|
||||
struct ProtectedFiles {
|
||||
// We have to use Vec here to ensure `new` can be a const fn, which is
|
||||
// required for global static variables. This should be fine, since we don't
|
||||
// expect to be protecting more than a handful of file descriptors.
|
||||
files: Vec<RawFd>,
|
||||
}
|
||||
|
||||
impl ProtectedFiles {
|
||||
pub const fn new() -> Self {
|
||||
Self { files: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn contains<Fd: AsRawFd>(&self, fd: &Fd) -> bool {
|
||||
self.files.contains(&fd.as_raw_fd())
|
||||
}
|
||||
|
||||
pub fn insert<Fd: AsRawFd>(&mut self, fd: &Fd) -> bool {
|
||||
if self.contains(fd) {
|
||||
true
|
||||
} else {
|
||||
self.files.push(fd.as_raw_fd());
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove<Fd: AsRawFd>(&mut self, fd: &Fd) -> bool {
|
||||
let fd = fd.as_raw_fd();
|
||||
if let Some(index) = self.files.iter().position(|item| item == &fd) {
|
||||
self.files.swap_remove(index);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A file descriptor that is internal to the plugin and not visible to the
|
||||
/// client. These file descriptors cannot be closed by the client.
|
||||
pub struct ProtectedFd<T: AsRawFd>(T);
|
||||
|
||||
impl<T: AsRawFd> Drop for ProtectedFd<T> {
|
||||
fn drop(&mut self) {
|
||||
PROTECTED_FILES.lock().remove(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRawFd> AsRef<T> for ProtectedFd<T> {
|
||||
fn as_ref(&self) -> &T {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRawFd> AsMut<T> for ProtectedFd<T> {
|
||||
fn as_mut(&mut self) -> &mut T {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes a closure `f` that creates and returns a file descriptor. The file
|
||||
/// descriptor that is returned is protected from getting closed. This is safe
|
||||
/// even if another thread is trying to close this same file descriptor.
|
||||
pub fn protect_with<F, T, E>(f: F) -> Result<ProtectedFd<T>, E>
|
||||
where
|
||||
F: FnOnce() -> Result<T, E>,
|
||||
T: AsRawFd,
|
||||
{
|
||||
let mut protected_files = PROTECTED_FILES.lock();
|
||||
|
||||
f().map(|fd| {
|
||||
protected_files.insert(&fd);
|
||||
ProtectedFd(fd)
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns true if a file descriptor is protected and shouldn't be closed.
|
||||
pub fn is_protected<Fd: AsRawFd>(fd: &Fd) -> bool {
|
||||
PROTECTED_FILES.lock().contains(fd)
|
||||
}
|
||||
|
||||
/// All of these syscalls take the input file descriptor as the first argument.
|
||||
/// Some syscalls, like mmap, don't conform to this pattern and need to be
|
||||
/// handled in a special way.
|
||||
static FD_ARG0_SYSCALLS: SysnoSet = SysnoSet::new(&[
|
||||
Sysno::close,
|
||||
Sysno::dup,
|
||||
Sysno::dup2,
|
||||
Sysno::openat,
|
||||
Sysno::fstat,
|
||||
Sysno::read,
|
||||
Sysno::write,
|
||||
Sysno::lseek,
|
||||
Sysno::ioctl,
|
||||
Sysno::pread64,
|
||||
Sysno::pwrite64,
|
||||
Sysno::readv,
|
||||
Sysno::writev,
|
||||
Sysno::connect,
|
||||
Sysno::accept,
|
||||
Sysno::sendto,
|
||||
Sysno::recvfrom,
|
||||
Sysno::sendmsg,
|
||||
Sysno::recvmsg,
|
||||
Sysno::shutdown,
|
||||
Sysno::bind,
|
||||
Sysno::listen,
|
||||
Sysno::getsockname,
|
||||
Sysno::getpeername,
|
||||
Sysno::getsockopt,
|
||||
Sysno::fcntl,
|
||||
Sysno::flock,
|
||||
Sysno::fsync,
|
||||
Sysno::fdatasync,
|
||||
Sysno::ftruncate,
|
||||
Sysno::getdents,
|
||||
Sysno::getdents64,
|
||||
Sysno::fchdir,
|
||||
Sysno::fchmod,
|
||||
Sysno::fchown,
|
||||
Sysno::fstatfs,
|
||||
Sysno::readahead,
|
||||
Sysno::fsetxattr,
|
||||
Sysno::fgetxattr,
|
||||
Sysno::flistxattr,
|
||||
Sysno::fremovexattr,
|
||||
Sysno::fadvise64,
|
||||
Sysno::epoll_wait,
|
||||
Sysno::epoll_ctl,
|
||||
Sysno::inotify_add_watch,
|
||||
Sysno::inotify_rm_watch,
|
||||
Sysno::mkdirat,
|
||||
Sysno::mknodat,
|
||||
Sysno::fchownat,
|
||||
Sysno::futimesat,
|
||||
Sysno::newfstatat,
|
||||
Sysno::unlinkat,
|
||||
Sysno::renameat,
|
||||
Sysno::linkat,
|
||||
Sysno::readlinkat,
|
||||
Sysno::fchmodat,
|
||||
Sysno::faccessat,
|
||||
Sysno::sync_file_range,
|
||||
Sysno::vmsplice,
|
||||
Sysno::utimensat,
|
||||
Sysno::epoll_pwait,
|
||||
Sysno::signalfd,
|
||||
Sysno::fallocate,
|
||||
Sysno::timerfd_settime,
|
||||
Sysno::timerfd_gettime,
|
||||
Sysno::accept4,
|
||||
Sysno::signalfd4,
|
||||
Sysno::dup3,
|
||||
Sysno::preadv,
|
||||
Sysno::pwritev,
|
||||
Sysno::recvmmsg,
|
||||
Sysno::fanotify_mark,
|
||||
Sysno::name_to_handle_at,
|
||||
Sysno::open_by_handle_at,
|
||||
Sysno::syncfs,
|
||||
Sysno::sendmmsg,
|
||||
Sysno::setns,
|
||||
Sysno::finit_module,
|
||||
Sysno::renameat2,
|
||||
Sysno::kexec_file_load,
|
||||
Sysno::execveat,
|
||||
Sysno::preadv2,
|
||||
Sysno::pwritev2,
|
||||
Sysno::statx,
|
||||
Sysno::pidfd_send_signal,
|
||||
Sysno::io_uring_enter,
|
||||
Sysno::io_uring_register,
|
||||
Sysno::open_tree,
|
||||
Sysno::move_mount,
|
||||
Sysno::fsconfig,
|
||||
Sysno::fsmount,
|
||||
Sysno::fspick,
|
||||
Sysno::openat2,
|
||||
Sysno::pidfd_getfd,
|
||||
]);
|
||||
|
||||
static FD_ARG1_SYSCALLS: SysnoSet = SysnoSet::new(&[Sysno::dup2, Sysno::dup3]);
|
||||
|
||||
/// Returns true if the given syscall operates on a protected file descriptor.
|
||||
pub fn uses_protected_fd(sysno: Sysno, arg0: usize, arg1: usize) -> bool {
|
||||
(FD_ARG0_SYSCALLS.contains(sysno) && is_protected(&(arg0 as i32)))
|
||||
|| (FD_ARG1_SYSCALLS.contains(sysno) && is_protected(&(arg1 as i32)))
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::os::unix::io::FromRawFd;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use reverie_rpc::Channel;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use syscalls::Errno;
|
||||
|
||||
use super::protected_files::protect_with;
|
||||
use super::protected_files::ProtectedFd;
|
||||
|
||||
/// The file descriptor that our RPC socket connection should use. We use 100
|
||||
/// here because many programs or tests expect to use the early file
|
||||
/// descriptors. Using file descriptor 100 also makes this easier to debug.
|
||||
const SOCKET_FD: i32 = 100;
|
||||
|
||||
struct Inner {
|
||||
stream: ProtectedFd<UnixStream>,
|
||||
}
|
||||
|
||||
/// Implements a channel using a UNIX domain socket.
|
||||
pub struct BaseChannel {
|
||||
inner: Mutex<Inner>,
|
||||
}
|
||||
|
||||
impl BaseChannel {
|
||||
/// Connects to the global state RPC server.
|
||||
pub fn new() -> io::Result<Self> {
|
||||
// FIXME: We can't rely on this environment variable existing. Instead,
|
||||
// the host should use seccomp-unotify to listen for a special syscall
|
||||
// that returns a file descriptor to the socket connection.
|
||||
let sock_path = std::env::var_os("REVERIE_SOCK")
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "$REVERIE_SOCK does not exist!"))?;
|
||||
|
||||
let stream = protect_with(|| -> Result<_, io::Error> {
|
||||
let sock = UnixStream::connect(sock_path)?;
|
||||
|
||||
// Move the socket to our desired file descriptor and make sure it
|
||||
// gets closed when execve is called.
|
||||
let fd =
|
||||
Errno::result(unsafe { libc::dup3(sock.as_raw_fd(), SOCKET_FD, libc::O_CLOEXEC) })?;
|
||||
|
||||
// Close the old socket file descriptor.
|
||||
drop(sock);
|
||||
|
||||
Ok(unsafe { UnixStream::from_raw_fd(fd) })
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
inner: Mutex::new(Inner { stream }),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Inner {
|
||||
fn try_send<T>(&mut self, item: &T) -> io::Result<()>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
let mut buf = Vec::with_capacity(1024);
|
||||
|
||||
reverie_rpc::encode(item, &mut buf)?;
|
||||
|
||||
self.stream.as_mut().write_all(&buf)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_recv<T>(&mut self) -> io::Result<T>
|
||||
where
|
||||
T: for<'a> Deserialize<'a>,
|
||||
{
|
||||
let mut buf = Vec::with_capacity(1024);
|
||||
reverie_rpc::decode_from(self.stream.as_mut(), &mut buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Req, Res> Channel<Req, Res> for BaseChannel
|
||||
where
|
||||
Req: Serialize,
|
||||
Res: for<'a> Deserialize<'a>,
|
||||
{
|
||||
fn send(&self, item: &Req) {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.try_send(item).expect("Failed to send RPC");
|
||||
}
|
||||
|
||||
fn call(&self, item: &Req) -> Res {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.try_send(item).expect("Failed to send RPC");
|
||||
inner.try_recv().expect("Failed to recv RPC")
|
||||
}
|
||||
}
|
|
@ -1,539 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
use core::cell::UnsafeCell;
|
||||
use core::marker::PhantomData;
|
||||
use core::mem;
|
||||
use core::sync::atomic::AtomicU64;
|
||||
use core::sync::atomic::Ordering::*;
|
||||
|
||||
use heapless::mpmc::Q64;
|
||||
|
||||
/// These constants describe the format of the combined queued and guard
|
||||
/// counter. These counters are coupled to
|
||||
/// 1. Allow for single atomic writes when creating and dropping signal guards
|
||||
/// 2. Ensuring consistency between the counters at any given time. This
|
||||
/// simplifies the logic in proving the correctness of the operations
|
||||
///
|
||||
/// The format of the counters is:
|
||||
///
|
||||
/// |<---------------------- 64 Bits ---------------------->|
|
||||
/// |<-= 32 Bits Queued Count ->|<-- 32 Bits Guard Count -->|
|
||||
const GUARD_COUNT_BITS: u8 = 32;
|
||||
const GUARD_COUNT_UNIT: u64 = 1;
|
||||
const GUARD_COUNT_MASK: u64 = (1 << GUARD_COUNT_BITS) - 1;
|
||||
const QUEUED_COUNT_SHIFT: u8 = GUARD_COUNT_BITS;
|
||||
const QUEUED_COUNT_UNIT: u64 = 1 << QUEUED_COUNT_SHIFT;
|
||||
const QUEUED_COUNT_MASK: u64 = !GUARD_COUNT_MASK;
|
||||
|
||||
type Invocation<T> = (fn(T), T);
|
||||
pub type SignalHandlerInput = libc::siginfo_t;
|
||||
|
||||
pub type SignalGuard = SequencerGuard<'static, SignalHandlerInput>;
|
||||
pub type SignalAntiGuard = SequencerAntiGuard<'static, SignalHandlerInput>;
|
||||
|
||||
thread_local! {
|
||||
pub(crate) static SIGNAL_HANLDER_SEQUENCER: GuardedSequencer<SignalHandlerInput>
|
||||
= GuardedSequencer::with_initial_guard_count(1);
|
||||
}
|
||||
|
||||
/// Marker struct that triggers "run-on-drop" behavior for defered invocations.
|
||||
/// The phantom data here is to make the guard `!Send`
|
||||
pub struct SequencerGuard<'a, T> {
|
||||
owner: &'a GuardedSequencer<T>,
|
||||
_phantom: PhantomData<UnsafeCell<()>>,
|
||||
}
|
||||
|
||||
/// Marker struct that is the like the particle opposite of the signal guard.
|
||||
/// When it is created, the count of guards decreases by one. If the new guard
|
||||
/// count is zero, defered execitions will be evaluated and signals will not be
|
||||
/// blocked. When this struct is dropped, the count of guards will be increased
|
||||
/// by one guarding against signal interuptions again. The phantom data here is
|
||||
/// to make the anti guard `!Send`
|
||||
pub struct SequencerAntiGuard<'a, T> {
|
||||
owner: &'a GuardedSequencer<T>,
|
||||
_phantom: PhantomData<UnsafeCell<()>>,
|
||||
}
|
||||
|
||||
impl<'a, T> SequencerGuard<'a, T> {
|
||||
fn new(owner: &'a GuardedSequencer<T>) -> Self {
|
||||
SequencerGuard {
|
||||
owner,
|
||||
_phantom: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> SequencerAntiGuard<'a, T> {
|
||||
fn new(owner: &'a GuardedSequencer<T>) -> Self {
|
||||
SequencerAntiGuard {
|
||||
owner,
|
||||
_phantom: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Drop for SequencerGuard<'a, T> {
|
||||
/// When the guard is dropped, we decrement the counter for guards, and if
|
||||
/// this was the last one, we run any invocations that were added while the
|
||||
/// guard(s) were active
|
||||
fn drop(&mut self) {
|
||||
self.owner.decrement_guard_count()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Drop for SequencerAntiGuard<'a, T> {
|
||||
/// When an anti guard is dropped, we increment the guard count to return
|
||||
/// the thread to a state that cannot be interrupted
|
||||
fn drop(&mut self) {
|
||||
self.owner.increment_guard_count()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct GuardedSequencer<T> {
|
||||
queue: Q64<Invocation<T>>,
|
||||
guard_state: AtomicU64,
|
||||
}
|
||||
|
||||
impl<T> GuardedSequencer<T> {
|
||||
const fn with_initial_guard_count(guard_count: u32) -> Self {
|
||||
GuardedSequencer {
|
||||
queue: Q64::new(),
|
||||
guard_state: AtomicU64::new(guard_count as u64),
|
||||
}
|
||||
}
|
||||
|
||||
// Enqueue the given invocation to be run when no guards are active.
|
||||
fn enqueue_invocation(&self, invocation: Invocation<T>) {
|
||||
self.queue.enqueue(invocation).ok().expect("Buffer full");
|
||||
}
|
||||
|
||||
/// Drain the invocation. When a invocation is "drained", it is:
|
||||
/// 1. Removed from the buffer
|
||||
/// 2. Evaluated
|
||||
/// 3. Counted as handled in the queued count
|
||||
fn drain_one_invocation(&self) -> bool {
|
||||
if let Some((handler, argument)) = self.queue.dequeue() {
|
||||
handler(argument);
|
||||
|
||||
// Decrement the queued count stored with guard count
|
||||
self.guard_state.fetch_sub(QUEUED_COUNT_UNIT, SeqCst);
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute the invocations that are currently stored in the queue.
|
||||
fn drain_invocations(&self) {
|
||||
// While invocations are successfully being drained, keep draining. If
|
||||
// draining one invocation is not successful, either there are no more
|
||||
// or drain was interrupted and completed elsewhere. Either way there's
|
||||
// nothing more to do here.
|
||||
while self.drain_one_invocation() {}
|
||||
}
|
||||
|
||||
/// Execute the given handler with the given argument when the current
|
||||
/// thread has no active signal guards. If there are no guards, this
|
||||
/// invocation will be run synchronously, otherwise, the invocation will be
|
||||
/// stored and run asynchronously when guard count reaches zero
|
||||
pub fn invoke(&self, handler: fn(T), argument: T) {
|
||||
// Increment the guard and queued count in one atomic step
|
||||
self.guard_state
|
||||
.fetch_add(QUEUED_COUNT_UNIT + GUARD_COUNT_UNIT, SeqCst);
|
||||
|
||||
self.enqueue_invocation((handler, argument));
|
||||
|
||||
// decrementing the guard count here will either run the invocation
|
||||
// that was just added or defer it depending respectively on whether
|
||||
// this was the only guard or not
|
||||
self.decrement_guard_count();
|
||||
}
|
||||
|
||||
/// Increment the number of active guards by one
|
||||
fn increment_guard_count(&self) {
|
||||
self.guard_state.fetch_add(GUARD_COUNT_UNIT, Acquire);
|
||||
}
|
||||
|
||||
/// Decrement the counter for guards, and if the count goes to zero, we run
|
||||
/// any invocations that were added while the guard(s) were active
|
||||
fn decrement_guard_count(&self) {
|
||||
let prev_guard_state = self.guard_state.fetch_sub(GUARD_COUNT_UNIT, Release);
|
||||
|
||||
let prev_guard_count = prev_guard_state & GUARD_COUNT_MASK;
|
||||
|
||||
assert!(
|
||||
prev_guard_count > 0,
|
||||
"Signal guard count went negative indicating a bug"
|
||||
);
|
||||
|
||||
// If this wasn't the last guard or if there were no invocations added,
|
||||
// we are done
|
||||
if prev_guard_count > 1 || prev_guard_state & QUEUED_COUNT_MASK == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Now it's our responsibility to execute all invocations
|
||||
self.drain_invocations();
|
||||
}
|
||||
|
||||
/// Create a guard on this sequencer. Invocations performed on this
|
||||
/// sequencer will be deferred until the returned guard (and any others)
|
||||
/// are dropped
|
||||
fn guard<'a>(&'a self) -> SequencerGuard<'a, T> {
|
||||
self.increment_guard_count();
|
||||
SequencerGuard::new(self)
|
||||
}
|
||||
|
||||
/// Create a guard on this sequencer without incrementing the guard count.
|
||||
/// Think of this as taking ownership of a guard that someone else forgot.
|
||||
/// Invocations performed on this sequencer will be deferred until the
|
||||
/// returned guard (and any others) are dropped.
|
||||
fn implicit_guard<'a>(&'a self) -> SequencerGuard<'a, T> {
|
||||
assert!(
|
||||
self.guard_state.load(Acquire) > 0,
|
||||
"No implicit signal guard in place"
|
||||
);
|
||||
SequencerGuard::new(self)
|
||||
}
|
||||
|
||||
/// Create a an anti guard on this sequencer. Until it is dropped, the
|
||||
/// returned anti guard cancels out exactly one guard meaning if a single
|
||||
/// guard exists and has defered invocations, those invocations will be
|
||||
/// executed as soon as this anti guard is created, and any subsequent
|
||||
/// invocations will be run immediately
|
||||
fn anti_guard<'a>(&'a self) -> SequencerAntiGuard<'a, T> {
|
||||
self.decrement_guard_count();
|
||||
SequencerAntiGuard::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a static pointer to the the guarded sequencer for this thread
|
||||
fn signal_handler_sequencer() -> &'static GuardedSequencer<SignalHandlerInput> {
|
||||
// We are using some unsafe code here to convert the lifetime provided for
|
||||
// the thread-local `.with` function into a static lifetime. This is safe
|
||||
// because we are not passing the returned value to any other thread
|
||||
SIGNAL_HANLDER_SEQUENCER.with(|sequencer| unsafe { mem::transmute::<_, &'static _>(sequencer) })
|
||||
}
|
||||
|
||||
/// Execute the given handler with the given argument when the current
|
||||
/// thread has no active signal guards. If there are no guards, this
|
||||
/// invocation will be run synchronously, otherwise, the invocation will be
|
||||
/// stored and run asynchronously when guard count reaches zero
|
||||
pub fn invoke_guarded(handler: fn(SignalHandlerInput), siginfo: SignalHandlerInput) {
|
||||
signal_handler_sequencer().invoke(handler, siginfo);
|
||||
}
|
||||
|
||||
/// Enter a region where signals cannot interrupt invocation of the current
|
||||
/// thread. This operation should be thought to have atomic-aquire ordering.
|
||||
/// The exclusion zone will last until the returned guard is dropped
|
||||
#[must_use]
|
||||
pub fn enter_signal_exclusion_zone() -> SignalGuard {
|
||||
signal_handler_sequencer().guard()
|
||||
}
|
||||
|
||||
/// Enter an already-exiting region where signals cannot interrupt execution of
|
||||
/// the current thread. The exclusion zone will last until the returned guard is
|
||||
/// dropped
|
||||
#[must_use]
|
||||
pub fn enter_implicit_signal_exclusion_zone() -> SignalGuard {
|
||||
signal_handler_sequencer().implicit_guard()
|
||||
}
|
||||
|
||||
/// Re-enter an execution phase where signals can interrupt the current thread.
|
||||
/// When the returned anti guard is dropped, a signal exclusion zone will be
|
||||
/// resumed
|
||||
#[must_use]
|
||||
pub fn reenter_signal_inclusion_zone() -> SignalAntiGuard {
|
||||
signal_handler_sequencer().anti_guard()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::cell::RefCell;
|
||||
use std::mem;
|
||||
use std::rc::Rc;
|
||||
|
||||
use super::*;
|
||||
|
||||
macro_rules! assert_interrupts_eq {
|
||||
($received:ident, [$($v:ident),*]) => {
|
||||
{
|
||||
let to_compare : Vec<&'static str> = vec![$(stringify!($v)),*];
|
||||
assert_eq!(&*$received.borrow(), &to_compare);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! make_handlers {
|
||||
($($handler:ident),*$(,)?) => {
|
||||
$(
|
||||
fn $handler(input: HandlerInput) {
|
||||
let HandlerInput(_, log) = input;
|
||||
log.borrow_mut().push(stringify!($handler));
|
||||
}
|
||||
|
||||
macro_rules! $handler {
|
||||
($sched:ident, $log:ident) => {
|
||||
$sched.invoke($handler, HandlerInput($sched.clone(), $log.clone()))
|
||||
}
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
// Define the handlers we are goning to call
|
||||
make_handlers! {
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
}
|
||||
|
||||
type InvocationLog = Rc<RefCell<Vec<&'static str>>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct HandlerInput(Rc<GuardedSequencer<HandlerInput>>, InvocationLog);
|
||||
|
||||
/// Test wrapper to do the cleanup we need and run each test in serial
|
||||
fn run_guarded_sequencer_test<T>(t: T)
|
||||
where
|
||||
T: FnOnce(Rc<GuardedSequencer<HandlerInput>>, InvocationLog),
|
||||
{
|
||||
let handler_log = Rc::new(RefCell::new(Vec::new()));
|
||||
let sequencer = Rc::new(GuardedSequencer::with_initial_guard_count(0));
|
||||
|
||||
t(sequencer.clone(), handler_log);
|
||||
|
||||
// Make sure if the test exits normally that all handlers were run
|
||||
// and the guard/handler counts are returned to zero
|
||||
assert_eq!(0, sequencer.guard_state.load(SeqCst));
|
||||
assert!(sequencer.queue.dequeue().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_handler_defer() {
|
||||
run_guarded_sequencer_test(|sequencer, log| {
|
||||
assert_interrupts_eq!(log, []);
|
||||
|
||||
// Running ungaurded should work immediately
|
||||
h1!(sequencer, log);
|
||||
assert_interrupts_eq!(log, [h1]);
|
||||
|
||||
// Running with one guard should defer the handler
|
||||
{
|
||||
let _g1 = sequencer.guard();
|
||||
h2!(sequencer, log);
|
||||
assert_interrupts_eq!(log, [h1]);
|
||||
}
|
||||
|
||||
// until after the guard goes out of scope
|
||||
assert_interrupts_eq!(log, [h1, h2]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_defer_with_multiple_guards() {
|
||||
run_guarded_sequencer_test(|sequencer, log| {
|
||||
// Running with one guard should defer the handler
|
||||
{
|
||||
let _g1 = sequencer.guard();
|
||||
h1!(sequencer, log);
|
||||
assert_interrupts_eq!(log, []);
|
||||
|
||||
// Running with another guard should do the same
|
||||
{
|
||||
let _g2 = sequencer.guard();
|
||||
h2!(sequencer, log);
|
||||
assert_interrupts_eq!(log, []);
|
||||
}
|
||||
|
||||
// Nothing should change when the first guard is dropped
|
||||
assert_interrupts_eq!(log, []);
|
||||
}
|
||||
|
||||
// When both guards are dropped, the handlers should run in the
|
||||
// order they were received
|
||||
assert_interrupts_eq!(log, [h1, h2]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_defer_with_anti_guard() {
|
||||
run_guarded_sequencer_test(|sequencer, log| {
|
||||
// Running with one guard should defer the handler
|
||||
{
|
||||
let _g1 = sequencer.guard();
|
||||
h1!(sequencer, log);
|
||||
assert_interrupts_eq!(log, []);
|
||||
|
||||
// Running with an anti guard allows defered signals to run,
|
||||
// and allow new new handlers to run immediately
|
||||
{
|
||||
let _ag = sequencer.anti_guard();
|
||||
|
||||
assert_interrupts_eq!(log, [h1]);
|
||||
h2!(sequencer, log);
|
||||
assert_interrupts_eq!(log, [h1, h2]);
|
||||
}
|
||||
|
||||
// When the anti guard is gone, we return to a state where
|
||||
// handlers are defered
|
||||
h3!(sequencer, log);
|
||||
assert_interrupts_eq!(log, [h1, h2]);
|
||||
}
|
||||
|
||||
// When the guard is dropped, the handlers should run in the
|
||||
// order they were received
|
||||
assert_interrupts_eq!(log, [h1, h2, h3]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_defer_with_implicit_guard() {
|
||||
run_guarded_sequencer_test(|sequencer, log| {
|
||||
// Create a guard and drop it without calling the destructor;
|
||||
mem::forget(sequencer.guard());
|
||||
|
||||
h1!(sequencer, log);
|
||||
assert_interrupts_eq!(log, []);
|
||||
|
||||
//To release that implicit guard, we enter an implicitly guarded
|
||||
// zone
|
||||
{
|
||||
let _implicit_guard = sequencer.implicit_guard();
|
||||
|
||||
// Handlers are still deferred
|
||||
h2!(sequencer, log);
|
||||
assert_interrupts_eq!(log, []);
|
||||
}
|
||||
|
||||
// Once the guard is dropped, deferred signals are run
|
||||
assert_interrupts_eq!(log, [h1, h2]);
|
||||
|
||||
//and new handlers are run immediately
|
||||
h3!(sequencer, log);
|
||||
assert_interrupts_eq!(log, [h1, h2, h3]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_handling() {
|
||||
run_guarded_sequencer_test(|sequencer, log| {
|
||||
// Nothing prevents guards from being created and dropped within
|
||||
// handler functions
|
||||
|
||||
fn nested_1(input_1: HandlerInput) {
|
||||
let HandlerInput(sequencer, log) = input_1.clone();
|
||||
|
||||
h2!(sequencer, log);
|
||||
let _g = sequencer.guard();
|
||||
h3!(sequencer, log);
|
||||
{
|
||||
let _ag = sequencer.anti_guard();
|
||||
h4!(sequencer, log);
|
||||
}
|
||||
|
||||
sequencer.invoke(nested_2, input_1);
|
||||
|
||||
h5!(sequencer, log);
|
||||
assert_interrupts_eq!(log, [h1, h2, h3, h4]);
|
||||
}
|
||||
|
||||
fn nested_2(input_2: HandlerInput) {
|
||||
let HandlerInput(sequencer, log) = input_2;
|
||||
|
||||
h6!(sequencer, log);
|
||||
}
|
||||
|
||||
{
|
||||
let _g = sequencer.guard();
|
||||
h1!(sequencer, log);
|
||||
sequencer.invoke(nested_1, HandlerInput(sequencer.clone(), log.clone()));
|
||||
}
|
||||
|
||||
assert_interrupts_eq!(log, [h1, h2, h3, h4, h5, h6]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_invalid_implicit_guard() {
|
||||
run_guarded_sequencer_test(|sequencer, _| {
|
||||
// Entering an implicit guard when one doesn't exist is an error
|
||||
let _g = sequencer.implicit_guard();
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_anti_guard_without_guard() {
|
||||
run_guarded_sequencer_test(|sequencer, _| {
|
||||
// Creating an anti guard outside of a guard is an error
|
||||
let _ag = sequencer.anti_guard();
|
||||
})
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static INVOKE_COUNT: AtomicU64 = AtomicU64::new(0);
|
||||
}
|
||||
|
||||
fn test_signal_handler(_: SignalHandlerInput) {
|
||||
INVOKE_COUNT.with(|counter| counter.fetch_add(1, SeqCst));
|
||||
}
|
||||
|
||||
pub(crate) const DUMMY_SIGINFO: SignalHandlerInput = unsafe {
|
||||
mem::transmute::<_, SignalHandlerInput>([0u8; mem::size_of::<SignalHandlerInput>()])
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_signal_handling_guard() {
|
||||
SIGNAL_HANLDER_SEQUENCER.with(|sequencer| {
|
||||
// Reinitialize the guard state and queue just in case
|
||||
sequencer.guard_state.store(1, SeqCst);
|
||||
while sequencer.queue.dequeue().is_some() {}
|
||||
|
||||
INVOKE_COUNT.with(|counter| counter.store(0, SeqCst));
|
||||
|
||||
// Run through the functions just to check that we have our sanity
|
||||
invoke_guarded(test_signal_handler, DUMMY_SIGINFO);
|
||||
|
||||
// We initialized the sequencer with an implicit guard, so
|
||||
assert_eq!(0, INVOKE_COUNT.with(|count| count.load(SeqCst)));
|
||||
|
||||
{
|
||||
let _g1 = enter_implicit_signal_exclusion_zone();
|
||||
invoke_guarded(test_signal_handler, DUMMY_SIGINFO);
|
||||
assert_eq!(0, INVOKE_COUNT.with(|count| count.load(SeqCst)));
|
||||
}
|
||||
|
||||
assert_eq!(2, INVOKE_COUNT.with(|count| count.load(SeqCst)));
|
||||
|
||||
{
|
||||
let _g2 = enter_signal_exclusion_zone();
|
||||
invoke_guarded(test_signal_handler, DUMMY_SIGINFO);
|
||||
assert_eq!(2, INVOKE_COUNT.with(|count| count.load(SeqCst)));
|
||||
|
||||
{
|
||||
let _ag1 = reenter_signal_inclusion_zone();
|
||||
assert_eq!(3, INVOKE_COUNT.with(|count| count.load(SeqCst)));
|
||||
invoke_guarded(test_signal_handler, DUMMY_SIGINFO);
|
||||
assert_eq!(4, INVOKE_COUNT.with(|count| count.load(SeqCst)));
|
||||
}
|
||||
|
||||
invoke_guarded(test_signal_handler, DUMMY_SIGINFO);
|
||||
assert_eq!(4, INVOKE_COUNT.with(|count| count.load(SeqCst)));
|
||||
}
|
||||
|
||||
assert_eq!(5, INVOKE_COUNT.with(|count| count.load(SeqCst)));
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,437 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
use core::mem::transmute;
|
||||
use core::mem::MaybeUninit;
|
||||
use core::ptr;
|
||||
use core::sync::atomic::fence;
|
||||
use core::sync::atomic::Ordering::*;
|
||||
|
||||
use atomic::Atomic;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use crate::callbacks;
|
||||
use crate::signal;
|
||||
use crate::slot_map::SlotKey;
|
||||
use crate::slot_map::SlotMap;
|
||||
use crate::tool::Tool;
|
||||
use crate::tool::ToolGlobal;
|
||||
|
||||
pub mod guard;
|
||||
|
||||
pub type HandlerInput = libc::siginfo_t;
|
||||
|
||||
/// We need to keep track of the sigaction that the user specified or what was
|
||||
/// originally provided as a default separately from what we execute directly as
|
||||
/// a signal handler.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct SigActionPair {
|
||||
/// Prisinte sigaction provided by the user or os
|
||||
guest_facing_action: libc::sigaction,
|
||||
|
||||
/// The actual sigaction we are using
|
||||
internal_action: libc::sigaction,
|
||||
}
|
||||
|
||||
impl SigActionPair {
|
||||
/// Create a new SigActionPair from the original sig action and an override
|
||||
/// for the default handler. The created pair will contain the original
|
||||
/// action, and a synthetic action with the handler replaced if an override
|
||||
/// is provided or if the the sa_sigaction is one of the non-function-
|
||||
/// pointer values (`SI_DFL`, `SI_ERR`, `SI_IGN`)
|
||||
fn new(original: libc::sigaction, override_handler: Option<libc::sighandler_t>) -> Self {
|
||||
let mut internal_action = original.clone();
|
||||
|
||||
// This is safe because it is only reading from a mut static that is
|
||||
// guaranteed to have been completely set before this function
|
||||
// is called
|
||||
internal_action.sa_sigaction = unsafe {
|
||||
match (original.sa_sigaction, override_handler) {
|
||||
(_, Some(override_handler)) => override_handler,
|
||||
(libc::SIG_DFL, _) => DEFAULT_EXIT_HANDLER
|
||||
.expect("Default handlers should be set before registering actions"),
|
||||
(libc::SIG_IGN, _) => DEFAULT_IGNORE_HANDLER
|
||||
.expect("Default handlers should be set before registering actions"),
|
||||
(libc::SIG_ERR, _) => DEFAULT_ERROR_HANDLER
|
||||
.expect("Default handlers should be set before registering actions"),
|
||||
(default_action, None) => default_action,
|
||||
}
|
||||
};
|
||||
|
||||
SigActionPair {
|
||||
guest_facing_action: original,
|
||||
internal_action,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
/// This is where we are storing the registered actions for each signal.
|
||||
/// We have to store them as Options for now because our slot map requires
|
||||
/// its stored type to implement default
|
||||
static ref HANDLER_SLOT_MAP: SlotMap<Option<SigActionPair>> = SlotMap::new();
|
||||
}
|
||||
|
||||
// The sighandler_t type has some values that aren't pointers that are still
|
||||
// valid. They aren't executable, so we need an executable version that we
|
||||
// control for each. Those are below
|
||||
|
||||
/// Storage of our default handler for the libc::SIG_DFL
|
||||
static mut DEFAULT_EXIT_HANDLER: Option<libc::sighandler_t> = None;
|
||||
|
||||
/// Storage of our default handler for the libc::SIG_IGN
|
||||
static mut DEFAULT_IGNORE_HANDLER: Option<libc::sighandler_t> = None;
|
||||
|
||||
/// Storage of our default handler for the libc::SIG_ERR
|
||||
static mut DEFAULT_ERROR_HANDLER: Option<libc::sighandler_t> = None;
|
||||
|
||||
/// This function invokes the function specified by the given sigaction directly
|
||||
/// with the given signal value or siginfo as arguments depending on whether
|
||||
/// the sigaction's flags indicate it is expecting a sigaction or siginfo.
|
||||
/// Note. In the case that the action is requesting sigaction, the 3rd argument
|
||||
/// to the handler will always be null. The specifications for sigaction say the
|
||||
/// third argument is a pointer to the context for the signal being raised, but
|
||||
/// we cannot guarantee that context will be valid with the handler function is
|
||||
/// executed. It also seems like that argument's use is rare, so we are omitting
|
||||
/// it for the time being. When T122210155, we should be able to provide the ctx
|
||||
/// argument without introducing unsafety.
|
||||
unsafe fn invoke_signal_handler(
|
||||
signal_val: libc::c_int,
|
||||
action: &libc::sigaction,
|
||||
sig_info: libc::siginfo_t,
|
||||
) {
|
||||
if action.sa_flags & libc::SA_SIGINFO > 0 {
|
||||
let to_run: extern "C" fn(libc::c_int, *const libc::siginfo_t, *const libc::c_void) =
|
||||
transmute(action.sa_sigaction as *const libc::c_void);
|
||||
to_run(
|
||||
signal_val,
|
||||
&sig_info as *const libc::siginfo_t,
|
||||
ptr::null::<libc::c_void>(),
|
||||
);
|
||||
} else {
|
||||
let to_run: extern "C" fn(libc::c_int) =
|
||||
transmute(action.sa_sigaction as *const libc::c_void);
|
||||
to_run(signal_val);
|
||||
}
|
||||
}
|
||||
|
||||
/// Register the given sigaction as the default. Optionally an override function
|
||||
/// can be passed in that will us to change the default handler for an action
|
||||
fn insert_action(
|
||||
sigaction: libc::sigaction,
|
||||
override_default_handler: Option<libc::sighandler_t>,
|
||||
) -> SlotKey {
|
||||
HANDLER_SLOT_MAP.insert(Some(SigActionPair::new(
|
||||
sigaction,
|
||||
override_default_handler,
|
||||
)))
|
||||
}
|
||||
|
||||
/// Register a signal handler for the guest and return the sigaction currently
|
||||
/// registered for the specified signal
|
||||
#[allow(dead_code)]
|
||||
pub fn register_guest_handler(signal_value: i32, new_action: libc::sigaction) -> libc::sigaction {
|
||||
register_guest_handler_impl(signal_value, new_action, false)
|
||||
.expect("All signals should have pre-registered guest handlers before now")
|
||||
}
|
||||
|
||||
/// This is our replacement for default handlers where
|
||||
/// `libc::sighandler_t = libc::SIG_DFL` which is the default handler
|
||||
/// value for almost all signals. This function will stop all threads in order
|
||||
/// to raise thread-exit events for each
|
||||
pub extern "C" fn default_exit_handler<T: ToolGlobal>(
|
||||
_signal_value: libc::c_int,
|
||||
_siginfo: *const libc::siginfo_t,
|
||||
_ctx: *const libc::c_void,
|
||||
) {
|
||||
callbacks::exit_group::<T>(0);
|
||||
}
|
||||
|
||||
/// This is our replacement for default handlers where
|
||||
/// `libc::sighandler_t = libc::SIG_IGN` which is the default handler
|
||||
/// value for lots of signals. This function does nothing, but allows uniform
|
||||
/// treatment of function pointers in signal handlers (instead of checking for)
|
||||
///specific values of sighandler_t before calling
|
||||
pub extern "C" fn default_ignore_handler<T: ToolGlobal>(
|
||||
_signal_value: libc::c_int,
|
||||
_siginfo: *const libc::siginfo_t,
|
||||
_ctx: *const libc::c_void,
|
||||
) {
|
||||
}
|
||||
|
||||
/// This is our replacement for default handlers where
|
||||
/// `libc::sighandler_t = libc::SIG_ERR` which is the default handler
|
||||
/// value for signals representing unrecoverable errors (SIGILL, SIGSEGV, etc).
|
||||
/// This function will stop all threads in order to raise thread-exit events
|
||||
/// for each, but the error code will be non-zero
|
||||
pub extern "C" fn default_error_handler<T: ToolGlobal>(
|
||||
_signal_value: libc::c_int,
|
||||
_siginfo: *const libc::siginfo_t,
|
||||
_ctx: *const libc::c_void,
|
||||
) {
|
||||
callbacks::exit_group::<T>(1);
|
||||
}
|
||||
|
||||
/// This macro defines the functions and constants and api for signals based on
|
||||
/// an input set of signal. There should only be one invocation of the macro,
|
||||
/// and it is below. It allows us to express the list of signals we are
|
||||
/// supporting with properties on each to deal with edge cases
|
||||
macro_rules! generate_signal_handlers {
|
||||
(
|
||||
default_exit_handler: $default_exit_handler_fn:expr,
|
||||
default_ignore_handler: $default_ignore_handler_fn:expr,
|
||||
default_error_handler: $default_error_handler_fn:expr,
|
||||
signals: [$($signal_name:ident $({
|
||||
$(override_default = $override_default_handler:expr;)?
|
||||
$(guest_handler_allowed = $guest_handler_allowed:expr;)?
|
||||
})?),+$(,)?]) => {
|
||||
|
||||
/// All signal values as i32
|
||||
mod signal_values {
|
||||
$(
|
||||
pub const $signal_name: i32 = libc::$signal_name as i32;
|
||||
)+
|
||||
}
|
||||
|
||||
/// Storage for the slot keys that point to the handlers for each signal
|
||||
mod handler_keys {
|
||||
use super::*;
|
||||
|
||||
$(
|
||||
pub static $signal_name: Atomic<Option<SlotKey>> = Atomic::new(None);
|
||||
)+
|
||||
}
|
||||
|
||||
/// Handler functions for each signal
|
||||
mod reverie_handlers {
|
||||
use super::*;
|
||||
|
||||
$(
|
||||
#[allow(non_snake_case)]
|
||||
pub fn $signal_name(handler_input: HandlerInput) {
|
||||
|
||||
if let Some(Some(SigActionPair {
|
||||
internal_action,
|
||||
..
|
||||
})) = handler_keys::$signal_name
|
||||
.load(Relaxed)
|
||||
.and_then(|key| HANDLER_SLOT_MAP.get(key))
|
||||
{
|
||||
|
||||
unsafe {
|
||||
invoke_signal_handler(
|
||||
signal_values::$signal_name as libc::c_int,
|
||||
internal_action,
|
||||
handler_input,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
)+
|
||||
}
|
||||
|
||||
/// This is the function that will be registered for all signals.
|
||||
/// guest and default handlers for each signal will be dispatched from
|
||||
/// here using the global sequencer to prevent signals from interfering
|
||||
/// with reverie or its tool's state
|
||||
pub extern "C" fn central_handler<T: ToolGlobal>(
|
||||
real_signal_value: i32,
|
||||
sig_info_ptr: *const libc::siginfo_t,
|
||||
_ctx: *const libc::c_void,
|
||||
) {
|
||||
let wrapped_handler = match real_signal_value {
|
||||
$(
|
||||
signal_values::$signal_name => reverie_handlers::$signal_name,
|
||||
)+
|
||||
_ => panic!("Invalid signal {}", real_signal_value)
|
||||
};
|
||||
|
||||
let sig_info = unsafe { *sig_info_ptr };
|
||||
T::global().handle_signal_event(real_signal_value);
|
||||
signal::guard::invoke_guarded(wrapped_handler, sig_info);
|
||||
}
|
||||
|
||||
/// This is the funtion that needs to be called to initialize all the
|
||||
/// signal handling machinery. This will register our central handler
|
||||
/// for all signals
|
||||
pub fn register_central_handler<T: ToolGlobal>() {
|
||||
|
||||
// Register the default handler functions that correspond to the
|
||||
// scalar sighandler_t behaviors. This is safe because this will
|
||||
// only be done before the first syscall is handled, and only
|
||||
// one thread will be active.
|
||||
unsafe {
|
||||
DEFAULT_EXIT_HANDLER = Some($default_exit_handler_fn
|
||||
as *const libc::c_void
|
||||
as libc::sighandler_t);
|
||||
DEFAULT_IGNORE_HANDLER = Some($default_ignore_handler_fn
|
||||
as *const libc::c_void
|
||||
as libc::sighandler_t);
|
||||
DEFAULT_ERROR_HANDLER = Some($default_error_handler_fn
|
||||
as *const libc::c_void
|
||||
as libc::sighandler_t);
|
||||
}
|
||||
|
||||
// To make sure handlers are set before continuing
|
||||
fence(SeqCst);
|
||||
|
||||
$( unsafe {
|
||||
|
||||
let sa_sigaction = central_handler::<T>
|
||||
as extern "C" fn(libc::c_int, *const libc::siginfo_t, *const libc::c_void)
|
||||
as *mut libc::c_void
|
||||
as libc::sighandler_t;
|
||||
|
||||
let mut sa_mask = MaybeUninit::<libc::sigset_t>::uninit();
|
||||
assert_eq!(0, libc::sigemptyset(sa_mask.as_mut_ptr()), "Failed to create sigset");
|
||||
libc::sigaddset(sa_mask.as_mut_ptr(), signal_values::$signal_name);
|
||||
|
||||
let action = libc::sigaction {
|
||||
sa_sigaction,
|
||||
sa_mask: sa_mask.assume_init(),
|
||||
sa_flags: 0x14000000,
|
||||
sa_restorer: None,
|
||||
};
|
||||
|
||||
let mut original_action : MaybeUninit<libc::sigaction>
|
||||
= MaybeUninit::uninit();
|
||||
|
||||
assert_eq!(0, libc::sigaction(
|
||||
signal_values::$signal_name as libc::c_int,
|
||||
&action as *const libc::sigaction,
|
||||
original_action.as_mut_ptr(),
|
||||
), "Failed to register central handler for {}", stringify!($signal_name));
|
||||
|
||||
let override_default_handler = None $($(
|
||||
.or(Some(
|
||||
$override_default_handler as *const libc::c_void as libc::sighandler_t)
|
||||
)
|
||||
)?)?;
|
||||
|
||||
let handler_key = insert_action(
|
||||
original_action.assume_init(),
|
||||
override_default_handler,
|
||||
);
|
||||
|
||||
handler_keys::$signal_name.store(Some(handler_key), SeqCst);
|
||||
} )+
|
||||
}
|
||||
|
||||
/// Register the given action for the given signal. The force-allow
|
||||
/// flag means that the handler will be registered even if guest
|
||||
/// handlers are disallowed for the given signal. Return a copy of the
|
||||
/// sigaction that was previously associated with the given signal
|
||||
fn register_guest_handler_impl(
|
||||
signal_value: i32,
|
||||
new_action: libc::sigaction,
|
||||
force_allow: bool
|
||||
) -> Option<libc::sigaction> {
|
||||
|
||||
let (handler_key, guest_handler_allowed, signal_name) = match signal_value {
|
||||
$(
|
||||
signal_values::$signal_name => {
|
||||
let allowed = force_allow || (true $($( && $guest_handler_allowed)?)?);
|
||||
let signal_name = stringify!($signal_name);
|
||||
(&handler_keys::$signal_name, allowed, signal_name)
|
||||
},
|
||||
)+
|
||||
_ => panic!("Invalid signal {}", signal_value)
|
||||
};
|
||||
|
||||
if !guest_handler_allowed {
|
||||
panic!("Guest handler registration for {} is not supported", signal_name);
|
||||
}
|
||||
|
||||
let new_action_key = insert_action(new_action, None);
|
||||
let old_action_key_opt = handler_key.swap(Some(new_action_key), Relaxed);
|
||||
|
||||
// The first time this function is called, there won't be a stored
|
||||
// key for every signal action, but if there is return it. It is
|
||||
// safe because the key being used must have come from the same
|
||||
// map, and because no elements are deleted, the get operation
|
||||
// will always succeed
|
||||
old_action_key_opt.map(|old_action_key| unsafe {
|
||||
HANDLER_SLOT_MAP.get_unchecked(old_action_key).unwrap().guest_facing_action
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the sigaction registered for the given signal if there is one.
|
||||
/// The returned sigaction will either be the original default sigaction
|
||||
/// set by default for the application or the unaltered sigaction
|
||||
/// registered by the user
|
||||
#[allow(dead_code)]
|
||||
pub fn get_registered_guest_handler(
|
||||
signal_value: i32
|
||||
) -> libc::sigaction {
|
||||
let current_action_key = match signal_value {
|
||||
$(
|
||||
signal_values::$signal_name => {
|
||||
handler_keys::$signal_name
|
||||
.load(Relaxed)
|
||||
.expect("All signals should have guest handlers before now")
|
||||
}
|
||||
)+
|
||||
_ => panic!("Invalid signal {}", signal_value)
|
||||
};
|
||||
|
||||
// This is safe because the key being used must have come from the
|
||||
// same map, and because no elements are deleted, the get operation
|
||||
// will always succeed
|
||||
unsafe {
|
||||
HANDLER_SLOT_MAP.get_unchecked(current_action_key)
|
||||
.unwrap().guest_facing_action
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
generate_signal_handlers! {
|
||||
default_exit_handler: default_exit_handler::<T>,
|
||||
default_ignore_handler: default_ignore_handler::<T>,
|
||||
default_error_handler: default_error_handler::<T>,
|
||||
|
||||
signals: [
|
||||
SIGHUP,
|
||||
SIGINT,
|
||||
SIGQUIT,
|
||||
// SIGILL, <- needs special synchronous handling Todo(T129735993)
|
||||
SIGTRAP,
|
||||
SIGABRT,
|
||||
SIGBUS,
|
||||
SIGFPE,
|
||||
// SIGKILL, <- cannot be handled directly Todo(T129348205)
|
||||
SIGUSR1,
|
||||
// SIGSEGV, <- needs special synchronous handling Todo(T129735993)
|
||||
SIGUSR2,
|
||||
SIGPIPE,
|
||||
SIGALRM,
|
||||
SIGTERM,
|
||||
SIGSTKFLT {
|
||||
// This is our controlled exit signal. If the guest tries to
|
||||
// register a handler for it, we will panic rather than chancining
|
||||
// undefined behavior
|
||||
override_default = crate::callbacks::handle_exit_signal::<T>;
|
||||
guest_handler_allowed = false;
|
||||
},
|
||||
// SIGCHLD, <- Causing problems in test_rr_syscallbuf_sigstop T128095829
|
||||
SIGCONT,
|
||||
// SIGSTOP, <- cannot be handled directly Todo(T129348205)
|
||||
SIGTSTP,
|
||||
SIGTTIN,
|
||||
SIGTTOU,
|
||||
SIGURG,
|
||||
SIGXCPU,
|
||||
SIGXFSZ,
|
||||
SIGVTALRM,
|
||||
SIGPROF,
|
||||
SIGWINCH,
|
||||
SIGIO,
|
||||
SIGPWR,
|
||||
SIGSYS,
|
||||
]
|
||||
}
|
|
@ -1,492 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#![allow(invalid_reference_casting)]
|
||||
|
||||
use core::iter::repeat;
|
||||
use core::mem::MaybeUninit;
|
||||
use core::sync::atomic::AtomicU32;
|
||||
use core::sync::atomic::AtomicUsize;
|
||||
use core::sync::atomic::Ordering::*;
|
||||
|
||||
use array_macro::array;
|
||||
|
||||
/// This is the key for addressing specific values from the slot map. A slot's
|
||||
/// address is made up of three parts and those parts are encoded into this one
|
||||
/// unsigned integer as follows:
|
||||
///
|
||||
/// |<-------------------------- 32 Bits Total ----------------------------->|
|
||||
/// |<-12 Bits: Generation->|<-10 Bits: Chunk Index->|<-10 Bits: Slot Index->|
|
||||
///
|
||||
/// Generation - Number of times the slot has been written (including deletes)
|
||||
/// Chunk Index - The index of the chunk containing the slot
|
||||
/// Slot Index - The slot's index within its chunk
|
||||
///
|
||||
/// Defining generation this way has the nice property that any slot with an
|
||||
/// even generation is empty. Each slot's generation starts at zero and is
|
||||
/// incremented by one after its first write
|
||||
#[repr(transparent)]
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct SlotKey(u32);
|
||||
|
||||
/// These constants define the bit pattern above.
|
||||
const INDEX_IN_CHUNK_BITS: u8 = 10;
|
||||
const CHUNK_INDEX_BITS: u8 = 10;
|
||||
const GENERATION_BITS: u8 = 32 - CHUNK_INDEX_BITS - INDEX_IN_CHUNK_BITS;
|
||||
const INDEX_IN_CHUNK_MASK: u32 = (0x1 << INDEX_IN_CHUNK_BITS) - 1;
|
||||
const CHUNK_INDEX_SHIFT: u8 = INDEX_IN_CHUNK_BITS;
|
||||
const CHUNK_INDEX_MASK: u32 = ((0x1 << CHUNK_INDEX_BITS) - 1) << CHUNK_INDEX_SHIFT;
|
||||
const GENERATION_SHIFT: u8 = CHUNK_INDEX_SHIFT + CHUNK_INDEX_BITS;
|
||||
const GENERATION_MASK: u32 = ((0x1 << GENERATION_BITS) - 1) << GENERATION_SHIFT;
|
||||
|
||||
// These are some useful constants related to the slotkey's bit pattern
|
||||
const MAX_CHUNK_COUNT: usize = 1 << CHUNK_INDEX_BITS;
|
||||
const ONE_GENERATION: u32 = 1 << GENERATION_SHIFT;
|
||||
const SLOTS_PER_CHUNK: usize = 1 << CHUNK_INDEX_BITS;
|
||||
|
||||
/// Each slot needs to know what its generation is and if the slot is empty, it
|
||||
/// needs to point to the next empty slot
|
||||
type SlotMetaData = AtomicU32;
|
||||
type Slot<T> = (SlotMetaData, T);
|
||||
type Chunk<T> = [Slot<T>; SLOTS_PER_CHUNK];
|
||||
type ChunkPointer<T> = MaybeUninit<Box<Chunk<T>>>;
|
||||
|
||||
/// Allocate a new chunk and return it
|
||||
fn new_chunk<T: ?Sized + Default>() -> ChunkPointer<T> {
|
||||
MaybeUninit::new(Box::new(array![Default::default(); SLOTS_PER_CHUNK]))
|
||||
}
|
||||
|
||||
impl SlotKey {
|
||||
/// Construct a slot key from its parts
|
||||
fn new_from_parts(chunk_index: usize, slot_index: usize, generation: u32) -> Self {
|
||||
SlotKey::new(
|
||||
(((chunk_index as u32) << CHUNK_INDEX_SHIFT) & CHUNK_INDEX_MASK)
|
||||
+ ((generation << GENERATION_SHIFT) & GENERATION_MASK)
|
||||
+ ((slot_index as u32) & INDEX_IN_CHUNK_MASK),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new<S>(int_value: S) -> Self
|
||||
where
|
||||
S: Into<u32>,
|
||||
{
|
||||
Self(int_value.into())
|
||||
}
|
||||
|
||||
/// Get the index of the slot encoded in the slot key
|
||||
fn slot_index(self) -> usize {
|
||||
(self.0 & INDEX_IN_CHUNK_MASK) as usize
|
||||
}
|
||||
|
||||
/// Get the index of the chunk encoded in the slot key
|
||||
fn chunk_index(self) -> usize {
|
||||
((self.0 & CHUNK_INDEX_MASK) >> CHUNK_INDEX_SHIFT) as usize
|
||||
}
|
||||
|
||||
/// Get the generation encoded in the slot key
|
||||
fn generation(self) -> u32 {
|
||||
(self.0 & GENERATION_MASK) >> GENERATION_SHIFT
|
||||
}
|
||||
}
|
||||
|
||||
/// Enumeration of the types of errors that can occur
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum InsertError {
|
||||
/// Indicates that inserts with the partition were stopped before trying to
|
||||
/// insert the value. This means the map was not altered during the
|
||||
/// operation
|
||||
InsertsDisallowedBeforeInsert,
|
||||
|
||||
/// Indicates that inserts for the partition were disallowed during the
|
||||
/// process of inserting the value. This means the map might have been
|
||||
/// temporarily updated to contain the given value, so the caller is
|
||||
/// needs to take action to correct any side effects of the value being
|
||||
/// temporarily present to other consumers of this map.
|
||||
///
|
||||
/// Note. This does not indicate any corruption in the map.
|
||||
InsertsDisallowedDuringInsert,
|
||||
}
|
||||
|
||||
/// This is a specialized, concurrent, lock-free slotmap that allows for
|
||||
/// wait-free reads and guarantees safety and well-defined behavior with only a
|
||||
/// few caveats.
|
||||
///
|
||||
/// 1. Some operations will wait by spinning if their is operational contentions:
|
||||
/// - When a new chunk needs to be allocated only one thread can do the allocation,
|
||||
/// and all the others have to wait
|
||||
/// 2. Currently deletes are not supported <- Todo(T117692439)
|
||||
pub struct SlotMap<T> {
|
||||
chunk_count_for_reads: AtomicUsize,
|
||||
chunk_count_for_writes: AtomicUsize,
|
||||
next_slot_key: AtomicU32,
|
||||
|
||||
disallowed_partition_value: AtomicU32,
|
||||
|
||||
/// This array stores pointers to all the allocated chunks indexed by chunk
|
||||
/// id. Initially all values in the array are uninitiallized.
|
||||
chunks: [ChunkPointer<T>; MAX_CHUNK_COUNT],
|
||||
}
|
||||
|
||||
impl<T> SlotMap<T>
|
||||
where
|
||||
T: 'static + ?Sized + Default,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
SlotMap {
|
||||
chunk_count_for_reads: Default::default(),
|
||||
chunk_count_for_writes: Default::default(),
|
||||
next_slot_key: Default::default(),
|
||||
disallowed_partition_value: AtomicU32::new(u32::MAX),
|
||||
chunks: array![MaybeUninit::uninit(); MAX_CHUNK_COUNT],
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop this map from accepting new insertions. Any insertsions in progress
|
||||
/// when this is called will fail. This method can be called multiple times,
|
||||
/// but only the first caller (that actually changes the state) will receive
|
||||
/// true as the return value. All other calls will receive false.
|
||||
pub fn stop_inserts_for_partition(&self, partition: u32) -> bool {
|
||||
self.disallowed_partition_value.swap(partition, SeqCst) != partition
|
||||
}
|
||||
|
||||
/// checks to see if inserts are allowed for the given partition
|
||||
pub fn inserts_allowed_for_partition(&self, partition: u32) -> bool {
|
||||
self.disallowed_partition_value.load(Acquire) != partition
|
||||
}
|
||||
|
||||
/// Insert the given value without checking whether partitions are allowed
|
||||
pub fn insert(&self, value: T) -> SlotKey {
|
||||
if let Ok(key) = self.insert_impl(None, value) {
|
||||
key
|
||||
} else {
|
||||
unreachable!("Inserts without partition are infalible");
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to insert the given value into the slotmap with the given
|
||||
/// partiion. If inserts are allowed on the given partition, the insert will
|
||||
/// succeed, but if inserts are disallowed on the partition, then the insert
|
||||
/// will fail.
|
||||
///
|
||||
/// Note. Partition is purely about exluding inserts. It has no bearing on
|
||||
/// how values are stored in the slot map
|
||||
pub fn try_insert(&self, partition: u32, value: T) -> Result<SlotKey, InsertError> {
|
||||
self.insert_impl(Some(partition), value)
|
||||
}
|
||||
|
||||
/// Attempt to insert the given value into the slotmap with the given
|
||||
/// optional partiion. If inserts are allowed on the given partition or if
|
||||
/// no partition is specified, the insert will succeed, but if inserts are
|
||||
/// disallowed on the partition, then the insert will fail.
|
||||
///
|
||||
/// Note. Partition is purely about exluding inserts. It has no bearing on
|
||||
/// how values are stored in the slot map
|
||||
fn insert_impl(&self, partition_opt: Option<u32>, value: T) -> Result<SlotKey, InsertError> {
|
||||
if let Some(partition) = partition_opt {
|
||||
if !self.inserts_allowed_for_partition(partition) {
|
||||
return Err(InsertError::InsertsDisallowedBeforeInsert);
|
||||
}
|
||||
}
|
||||
|
||||
let result = SlotKey::new(self.next_slot_key.fetch_add(1, Relaxed) + ONE_GENERATION);
|
||||
|
||||
let chunk_index = result.chunk_index();
|
||||
|
||||
// We are preallocating the space for all the possible pointers to
|
||||
// chunks, so if we run out of space for chunks, we can't get more :(
|
||||
assert!(chunk_index < MAX_CHUNK_COUNT, "Maximum map size exceeded");
|
||||
|
||||
// Check to see if a chunk has been allocated for this chunk_id
|
||||
loop {
|
||||
let chunk_count = self.chunk_count_for_reads.load(Acquire);
|
||||
if chunk_count > chunk_index {
|
||||
break;
|
||||
}
|
||||
|
||||
if self
|
||||
.chunk_count_for_writes
|
||||
.compare_exchange(chunk_count, chunk_count + 1, SeqCst, Relaxed)
|
||||
.is_ok()
|
||||
{
|
||||
let next_chunk = new_chunk();
|
||||
|
||||
// Allocate the next chunk. This is safe because
|
||||
// 1. `chunk_count` < MAX_CHUNK_COUNT
|
||||
// 2. We will be writing a valid pointer to the chunk array
|
||||
// before dereferencing.
|
||||
// 3. We are inside a spin lock that ensures each entry in the
|
||||
// chunks array will only be written to once.
|
||||
//
|
||||
// TODO: remove #![allow(invalid_reference_casting)] and replace with
|
||||
// UnsafeCell
|
||||
unsafe {
|
||||
let chunk_pointer_pointer =
|
||||
self.chunks.get_unchecked(chunk_count as usize) as *const ChunkPointer<T>;
|
||||
|
||||
let chunk_pointer_writeable =
|
||||
&mut *(chunk_pointer_pointer as *mut ChunkPointer<T>);
|
||||
|
||||
*chunk_pointer_writeable = next_chunk;
|
||||
}
|
||||
|
||||
self.chunk_count_for_reads.store(chunk_count + 1, Release);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// This is safe because
|
||||
// 1. The ChunkPointer at the current index is guaranteed to be valid
|
||||
// because of the above.
|
||||
// 2. The Chunk pointed to by the chunk pointer is guaranteed to be
|
||||
// allocated.
|
||||
// 3. The size of the allocated chunk is greater than index in the
|
||||
// slot.
|
||||
unsafe {
|
||||
let slot = (self.get_unchecked_slot(result) as *const Slot<T>) as *mut Slot<T>;
|
||||
(*slot).1 = value;
|
||||
(*slot).0.fetch_add(ONE_GENERATION, SeqCst);
|
||||
|
||||
// Last chance to check if inserts were blocked was called during
|
||||
// insertion. If it was, then we increase the generation on
|
||||
// the slot again and return None indicating the insert failed
|
||||
if let Some(partition) = partition_opt {
|
||||
if !self.inserts_allowed_for_partition(partition) {
|
||||
(*slot).0.fetch_add(ONE_GENERATION, SeqCst);
|
||||
|
||||
return Err(InsertError::InsertsDisallowedDuringInsert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Gets a reference to the slot at the given slotkey, but with no checks to
|
||||
/// ensure the slot exists and is not deleted.
|
||||
unsafe fn get_unchecked_slot(&self, slot_key: SlotKey) -> &Slot<T> {
|
||||
let chunk_index = slot_key.chunk_index();
|
||||
let index_in_chunk = slot_key.slot_index();
|
||||
|
||||
let chunk = self.chunks.get_unchecked(chunk_index).assume_init_ref();
|
||||
|
||||
chunk.get_unchecked(index_in_chunk)
|
||||
}
|
||||
|
||||
/// Gets a reference to the value at the given slotkey, but with no checks
|
||||
/// to ensure the slot exists and is not deleted.
|
||||
pub unsafe fn get_unchecked(&self, slot_key: SlotKey) -> &T {
|
||||
&self.get_unchecked_slot(slot_key).1
|
||||
}
|
||||
|
||||
/// Gets the value of the slot at the given key if the slot exists and if the
|
||||
/// generation of the key matches the generation in the slot.
|
||||
pub fn get(&self, slot_key: SlotKey) -> Option<&T> {
|
||||
let chunk_index = slot_key.chunk_index();
|
||||
let index_in_chunk = slot_key.slot_index();
|
||||
let key_generation = slot_key.generation();
|
||||
|
||||
let chunk_count = self.chunk_count_for_reads.load(Acquire) as usize;
|
||||
|
||||
(chunk_count > chunk_index)
|
||||
.then(|| {
|
||||
// This is safe because the chunk_index is less than the number
|
||||
// of chunks that have been allocated
|
||||
unsafe { self.chunks.get_unchecked(chunk_index).assume_init_ref() }
|
||||
})
|
||||
.map(|chunk| {
|
||||
// This is safe because we know the chunk will have been
|
||||
// allocated, and the index in that chunk is guaranteed to be
|
||||
// less than the size of the chunk.
|
||||
unsafe { (*chunk).get_unchecked(index_in_chunk) }
|
||||
})
|
||||
.filter(|(slot_data, _)| {
|
||||
SlotKey::new(slot_data.load(Relaxed)).generation() == key_generation
|
||||
})
|
||||
.map(|(_, v)| v)
|
||||
}
|
||||
|
||||
/// get an iterator of all the entries in the slotmap.
|
||||
pub fn entries(&self) -> impl Iterator<Item = (SlotKey, &T)> {
|
||||
(0..self.chunk_count_for_reads.load(Relaxed))
|
||||
.map(|chunk_index| {
|
||||
// This is safe because the range we are iterating over is
|
||||
// limitted to the range where we can guarantee the chunks have
|
||||
// been allocated.
|
||||
let chunk = unsafe { self.chunks.get_unchecked(chunk_index).assume_init_ref() };
|
||||
(chunk_index, chunk)
|
||||
})
|
||||
.flat_map(|(chunk_idx, chunk)| repeat(chunk_idx).zip(chunk.iter().enumerate()))
|
||||
.map(|(chunk_idx, (slot_idx, (slot_metadata, value)))| {
|
||||
let generation = SlotKey::new(slot_metadata.load(Relaxed)).generation();
|
||||
let slot_key = SlotKey::new_from_parts(chunk_idx, slot_idx, generation);
|
||||
(slot_key, value)
|
||||
})
|
||||
.filter(|(slot_key, _)| slot_key.generation() % 2 == 1)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Drop for SlotMap<T> {
|
||||
fn drop(&mut self) {
|
||||
for chunk_index in 0..self.chunk_count_for_reads.load(Relaxed) {
|
||||
// This is safe because the range we are iterating over is limitted
|
||||
// to the range where we can guarantee the chunks have been
|
||||
// allocated.
|
||||
unsafe {
|
||||
self.chunks
|
||||
.get_unchecked_mut(chunk_index)
|
||||
.assume_init_drop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn new_slot_map() -> SlotMap<AtomicU32> {
|
||||
SlotMap::new()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_happy_path() {
|
||||
let map = new_slot_map();
|
||||
|
||||
let i_value_1 = 42;
|
||||
let i_value_2 = 101;
|
||||
|
||||
// test writing
|
||||
let key1 = map
|
||||
.try_insert(0, AtomicU32::new(i_value_1))
|
||||
.expect("Insert failed");
|
||||
let key2 = map
|
||||
.try_insert(0, AtomicU32::new(i_value_2))
|
||||
.expect("Insert failed");
|
||||
|
||||
// Test reading
|
||||
let v1 = map.get(key1).expect("This was just added");
|
||||
let v2 = map.get(key2).expect("This was just added");
|
||||
assert_eq!(v1.load(Relaxed), i_value_1);
|
||||
assert_eq!(v2.load(Relaxed), i_value_2);
|
||||
|
||||
// Test iterating
|
||||
for (key, v) in map.entries() {
|
||||
if key == key1 {
|
||||
assert_eq!(v.load(Relaxed), i_value_1);
|
||||
} else if key == key2 {
|
||||
assert_eq!(v.load(Relaxed), i_value_2);
|
||||
}
|
||||
}
|
||||
|
||||
// Test mutating internal state while iterating
|
||||
for (_, v) in map.entries() {
|
||||
v.fetch_add(1, Relaxed);
|
||||
}
|
||||
|
||||
// Test reading after mutating
|
||||
let v1 = map.get(key1).expect("This was just added");
|
||||
let v2 = map.get(key2).expect("This was just added");
|
||||
assert_eq!(v1.load(Relaxed), i_value_1 + 1);
|
||||
assert_eq!(v2.load(Relaxed), i_value_2 + 1);
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
struct TestDroppable(usize, Option<Arc<AtomicUsize>>);
|
||||
|
||||
impl Drop for TestDroppable {
|
||||
fn drop(&mut self) {
|
||||
if let Some(drop_count) = &self.1 {
|
||||
drop_count.fetch_add(1, Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_drop() {
|
||||
let drop_counter = Arc::new(AtomicUsize::default());
|
||||
let to_drop_count = 100;
|
||||
{
|
||||
let map: SlotMap<TestDroppable> = SlotMap::new();
|
||||
|
||||
for i in 0..to_drop_count {
|
||||
let _ = map
|
||||
.try_insert(0, TestDroppable(i, Some(Arc::clone(&drop_counter))))
|
||||
.expect("insert failed");
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(to_drop_count, drop_counter.load(Relaxed));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_thread_safety() {
|
||||
let thread_count = 100;
|
||||
let insert_count = 10000;
|
||||
|
||||
let drop_counter = Arc::new(AtomicUsize::default());
|
||||
|
||||
// Test basic thread safety by inserting a bunch of values on different
|
||||
// threads and ensure that the number dropped equals the number inserted.
|
||||
{
|
||||
let map: Arc<SlotMap<TestDroppable>> = Arc::new(SlotMap::new());
|
||||
|
||||
(0..thread_count)
|
||||
.map(|thread_num| {
|
||||
let map_clone = Arc::clone(&map);
|
||||
let dc_clone = Arc::clone(&drop_counter);
|
||||
thread::spawn(move || {
|
||||
(0..insert_count)
|
||||
.map(|i| {
|
||||
// Do some inserting
|
||||
map_clone
|
||||
.try_insert(
|
||||
0,
|
||||
TestDroppable(
|
||||
thread_num * insert_count + i,
|
||||
Some(Arc::clone(&dc_clone)),
|
||||
),
|
||||
)
|
||||
.expect("Insert failed")
|
||||
})
|
||||
.enumerate()
|
||||
.for_each(|(i, key)| {
|
||||
// And some reading
|
||||
assert_eq!(
|
||||
Some(thread_num * insert_count + i),
|
||||
map_clone.get(key).map(|v| v.0)
|
||||
);
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.for_each(|t| t.join().expect("Failed to join"));
|
||||
}
|
||||
|
||||
assert_eq!(thread_count * insert_count, drop_counter.load(SeqCst));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_after_disable() {
|
||||
let map = new_slot_map();
|
||||
|
||||
assert!(map.try_insert(0, AtomicU32::new(1)).is_ok());
|
||||
|
||||
map.stop_inserts_for_partition(0);
|
||||
|
||||
assert!(map.try_insert(0, AtomicU32::new(2)).is_err());
|
||||
|
||||
// Inserting without a partition should still be allowed
|
||||
let key = map.insert(AtomicU32::new(3));
|
||||
assert_eq!(map.get(key).unwrap().load(SeqCst), 3);
|
||||
}
|
||||
}
|
|
@ -1,826 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
use core::marker::PhantomData;
|
||||
use core::sync::atomic::AtomicU8;
|
||||
use core::sync::atomic::Ordering;
|
||||
use core::sync::atomic::Ordering::*;
|
||||
use core::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use atomic::Atomic;
|
||||
use lazy_static::lazy_static;
|
||||
use syscalls::raw::syscall0;
|
||||
use syscalls::Sysno;
|
||||
|
||||
use crate::signal::guard;
|
||||
use crate::slot_map::SlotKey;
|
||||
use crate::slot_map::SlotMap;
|
||||
|
||||
lazy_static! {
|
||||
static ref SLOT_MAP: SlotMap<ThreadRepr> = SlotMap::new();
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
pub static THREAD_SLOT_KEY: Option<SlotKey> = generate_thread_and_slot_key();
|
||||
}
|
||||
|
||||
/// We are serializing the state information directly to the bits of an unsigned
|
||||
/// integer type. The layout of the bits is
|
||||
///
|
||||
/// 0b_xxFEDCBA
|
||||
/// 543210
|
||||
///
|
||||
/// A => New <- 1 means this is the first time this thread was seen
|
||||
/// B => 1 indicates Guest state and 0 indicates Handler state
|
||||
/// C => Needs to exit
|
||||
/// D => Exiting (Supersedes A)
|
||||
/// E => Exited (Supersedes A & B)
|
||||
/// F => Forking - Indicates or clonging or vforking (Only valid in Guest state)
|
||||
///
|
||||
/// This pattern enables lets us update the thread's state (in the allowed ways)
|
||||
/// and the thread's `needs_to_exit` flag independently and atomically
|
||||
type StateRepr = u8;
|
||||
type AtomicStateRepr = AtomicU8;
|
||||
type ThreadRepr = (Atomic<PidTid>, AtomicStateRepr);
|
||||
|
||||
// We want to make sure PidTid actually fits into the the Atomic type, so here
|
||||
// we check its size and that its alignment divides evenly into 8
|
||||
const _: () = assert!(core::mem::size_of::<PidTid>() == 8);
|
||||
const _: () = assert!(8 % core::mem::align_of::<PidTid>() == 0);
|
||||
|
||||
// Constants to describe the bit pattern above
|
||||
const NEW_SHIFT: u8 = 0;
|
||||
const NEW_MASK: u8 = 1 << NEW_SHIFT;
|
||||
const HANDLER_GUEST_SHIFT: u8 = 1;
|
||||
const HANDLER_GUEST_MASK: u8 = 1 << HANDLER_GUEST_SHIFT;
|
||||
const NEEDS_TO_EXIT_SHIFT: u8 = 2;
|
||||
const NEEDS_TO_EXIT_MASK: u8 = 1 << NEEDS_TO_EXIT_SHIFT;
|
||||
const EXITED_SHIFT: u8 = 3;
|
||||
const EXITED_MASK: u8 = 1 << EXITED_SHIFT;
|
||||
const EXITING_SHIFT: u8 = 4;
|
||||
const EXITING_MASK: u8 = 1 << EXITING_SHIFT;
|
||||
const FORKING_SHIFT: u8 = 5;
|
||||
const FORKING_MASK: u8 = 1 << FORKING_SHIFT;
|
||||
|
||||
/// Gets the value of the `needs_to_exit` flag from the thread representation.
|
||||
const fn needs_to_exit(thread_repr: StateRepr) -> bool {
|
||||
(thread_repr & NEEDS_TO_EXIT_MASK) > 0
|
||||
}
|
||||
|
||||
/// Gets the inverted value of the `not_new` flag from the thread
|
||||
/// representation.
|
||||
const fn is_new(thread_repr: StateRepr) -> bool {
|
||||
(thread_repr & NEW_MASK) > 0
|
||||
}
|
||||
|
||||
/// Sets the `needs_to_exit` flag in the given representation to true.
|
||||
fn store_needs_to_exit(atomic_repr: &AtomicStateRepr, ordering: Ordering) -> StateRepr {
|
||||
atomic_repr.fetch_or(NEEDS_TO_EXIT_MASK, ordering) | NEEDS_TO_EXIT_MASK
|
||||
}
|
||||
|
||||
/// Constructs a representation of a thread from its components.
|
||||
const fn build_repr(thread_state: ThreadState, needs_to_exit: bool) -> StateRepr {
|
||||
thread_state.as_u8() + (((needs_to_exit as u8) << NEEDS_TO_EXIT_SHIFT) & NEEDS_TO_EXIT_MASK)
|
||||
}
|
||||
|
||||
/// Return the value of the forking flag in the given state
|
||||
const fn is_forking(thread_rep: StateRepr) -> bool {
|
||||
thread_rep & FORKING_MASK > 0 && thread_rep & HANDLER_GUEST_MASK > 0
|
||||
}
|
||||
|
||||
/// Set the forking flag to false in the atomic repr and return the result
|
||||
fn clear_forking_flag(atomic_repr: &AtomicStateRepr, order: Ordering) -> StateRepr {
|
||||
atomic_repr.fetch_and(!FORKING_MASK, order) & !FORKING_MASK
|
||||
}
|
||||
|
||||
/// Convenience function for getting the current thread id via a syscall
|
||||
fn thread_id_syscall() -> u32 {
|
||||
// Unsafe unwrap is okay here because this syscall is guaranteed not to fail
|
||||
unsafe { syscall0(Sysno::gettid) as u32 }
|
||||
}
|
||||
|
||||
/// Convenience function for getting the current process id via a syscall
|
||||
fn process_id_syscall() -> u32 {
|
||||
// Unsafe unwrap is okay here because this syscall is guaranteed not to fail
|
||||
unsafe { syscall0(Sysno::getpid) as u32 }
|
||||
}
|
||||
|
||||
/// Called by the `thread_local` macro (during TLS initialization) to add the
|
||||
/// calling thread's metadata to the static map of all threads and store its key
|
||||
/// as a thread-local variable. If the thread metadata slotmap has had inputs
|
||||
/// disabled, the slotkey stored in this variable will be None
|
||||
fn generate_thread_and_slot_key() -> Option<SlotKey> {
|
||||
let pid_tid = PidTid::current();
|
||||
SLOT_MAP
|
||||
.try_insert(
|
||||
pid_tid.pid,
|
||||
(
|
||||
Atomic::new(pid_tid),
|
||||
AtomicStateRepr::new(NEW_MASK | HANDLER_GUEST_MASK),
|
||||
),
|
||||
)
|
||||
.ok()
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
pub struct PidTid {
|
||||
pub pid: u32,
|
||||
pub tid: u32,
|
||||
}
|
||||
|
||||
impl PidTid {
|
||||
fn current() -> Self {
|
||||
PidTid {
|
||||
pid: process_id_syscall(),
|
||||
tid: thread_id_syscall(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Possible error responses during a transition to/from guest execution.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum GuestTransitionErr {
|
||||
/// This thread needs to exit, and the caller is responsible.
|
||||
ExitNow,
|
||||
|
||||
/// This thread is exiting/exited but the caller is not responsible.
|
||||
ExitingElsewhere,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ThreadState {
|
||||
Guest(bool),
|
||||
Handler,
|
||||
Exiting,
|
||||
Exited,
|
||||
}
|
||||
|
||||
impl From<StateRepr> for ThreadState {
|
||||
fn from(state_repr: StateRepr) -> Self {
|
||||
if (state_repr & EXITED_MASK) > 0 {
|
||||
ThreadState::Exited
|
||||
} else if (state_repr & EXITING_MASK) > 0 {
|
||||
ThreadState::Exiting
|
||||
} else if (state_repr & HANDLER_GUEST_MASK) > 0 {
|
||||
ThreadState::Guest(is_forking(state_repr))
|
||||
} else {
|
||||
ThreadState::Handler
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ThreadState {
|
||||
const fn as_u8(&self) -> u8 {
|
||||
use ThreadState::*;
|
||||
match self {
|
||||
Guest(false) => HANDLER_GUEST_MASK,
|
||||
Guest(true) => HANDLER_GUEST_MASK | FORKING_MASK,
|
||||
Handler => 0,
|
||||
Exiting => EXITING_MASK,
|
||||
Exited => EXITED_MASK,
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes this thread state to the given atomic thread representation and
|
||||
/// return the resulting representation.
|
||||
fn store(&self, atomic_repr: &AtomicStateRepr, ordering: Ordering) -> StateRepr {
|
||||
if *self == ThreadState::Handler {
|
||||
let handle_mask = !(HANDLER_GUEST_MASK | FORKING_MASK);
|
||||
atomic_repr.fetch_and(handle_mask, ordering) & handle_mask
|
||||
} else {
|
||||
let state_u8 = self.as_u8();
|
||||
atomic_repr.fetch_or(state_u8, ordering) | state_u8
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait representing the notifications that can be raised from a thread
|
||||
/// during its lifecycle
|
||||
pub trait EventSink {
|
||||
fn on_new_thread(_pid_tid: PidTid) {}
|
||||
|
||||
fn on_thread_exit(_pid_tid: PidTid) {}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Thread<E: EventSink> {
|
||||
slot_key: SlotKey,
|
||||
|
||||
/// True if this is the first time the thread was seen.
|
||||
new: bool,
|
||||
|
||||
forking: bool,
|
||||
|
||||
/// Raw representation of the thread as a unsigned integer.
|
||||
repr: StateRepr,
|
||||
|
||||
/// All the fields contained in the repr.
|
||||
state: ThreadState,
|
||||
needs_to_exit: bool,
|
||||
|
||||
/// Funny phantom here to let the drop checker know it doesn't need to
|
||||
/// worry about `E`
|
||||
_phantom: PhantomData<dyn Fn(E) + Send + Sync>,
|
||||
}
|
||||
|
||||
fn get_threads_for_process<'a>(
|
||||
process_id: u32,
|
||||
) -> impl Iterator<Item = (SlotKey, PidTid, &'a AtomicStateRepr)> {
|
||||
SLOT_MAP
|
||||
.entries()
|
||||
.map(|(slot_key, (atomic_pid_tid, atomic_repr))| {
|
||||
(slot_key, atomic_pid_tid.load(Relaxed), atomic_repr)
|
||||
})
|
||||
.filter(move |(_, pid_tid, _)| pid_tid.pid == process_id)
|
||||
}
|
||||
|
||||
/// Indicates to all threads that they need to exit, and wait for all
|
||||
/// threads to confirm they are exited. The closure given is a way to signal
|
||||
/// to a thread by id to trigger a critical section transition which will
|
||||
/// observe and handle the need to exit.
|
||||
pub fn exit_all<F>(signal_guest_thread: F) -> Option<u32>
|
||||
where
|
||||
F: Fn(SlotKey, PidTid),
|
||||
{
|
||||
let exiting_pid = process_id_syscall();
|
||||
|
||||
// Only go through the motions of exiting all threads if the disallow
|
||||
// flag is successfully set
|
||||
disallow_new_threads().then(|| {
|
||||
// Iterate through all the slots and mark each thread as
|
||||
// `needs_to_exit`. Collect the PidTids of threads that aren't
|
||||
// already exited, so we "exit_waiters" can wait for the
|
||||
get_threads_for_process(exiting_pid)
|
||||
.map(|(slot_key, pid_tid, atomic_repr)| {
|
||||
// Read each thread's state and mark it as needing to exit.
|
||||
// Marking this flag is safe even on threads that have
|
||||
// already exited because the exiting and exited flages
|
||||
// take precenence
|
||||
let state: ThreadState = store_needs_to_exit(atomic_repr, SeqCst).into();
|
||||
(slot_key, pid_tid, state)
|
||||
})
|
||||
.for_each(|(slot_key, pid_tid, state)| {
|
||||
// Signal any threads in guest state in case they are in a
|
||||
// blocking syscall, so they can exit
|
||||
if state == ThreadState::Guest(false) {
|
||||
signal_guest_thread(slot_key, pid_tid)
|
||||
}
|
||||
});
|
||||
|
||||
exiting_pid
|
||||
})
|
||||
}
|
||||
|
||||
/// Waits for all threads to exit or for the given optional timeout to
|
||||
/// expire. The boolean returned will be true if all threads exited before
|
||||
/// the timeout and false if the timeout elapsed before all threads exited
|
||||
pub fn wait_for_all_to_exit(process_id: u32, timeout_opt: Option<Duration>) -> bool {
|
||||
let start_time = Instant::now();
|
||||
|
||||
// Spin until all the given keyed threads have exited or timeout has expired
|
||||
loop {
|
||||
if !get_threads_for_process(process_id)
|
||||
.map(|(_, _, atomic_repr)| atomic_repr.load(Relaxed))
|
||||
.map(ThreadState::from)
|
||||
.any(|state| state != ThreadState::Exited)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(timeout) = timeout_opt {
|
||||
if timeout > start_time.elapsed() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks the slotmap to see if new inserts are allowed. This is done as an
|
||||
/// atomic operation with `Acquire` ordering
|
||||
fn new_threads_allowed() -> bool {
|
||||
SLOT_MAP.inserts_allowed_for_partition(process_id_syscall())
|
||||
}
|
||||
|
||||
/// Set the slotmap to stop allowing inserts. This will instantly stop
|
||||
/// new threads from being inserted into the slot map. The returned boolean
|
||||
/// indicates whether the property was changed by this call or not
|
||||
fn disallow_new_threads() -> bool {
|
||||
SLOT_MAP.stop_inserts_for_partition(process_id_syscall())
|
||||
}
|
||||
|
||||
impl<E: EventSink> Thread<E> {
|
||||
/// Gets the thread data associated with the current thread. If no data is
|
||||
/// associated with the current thread, then a new instance will be created
|
||||
/// and returned associated with the current thread's id
|
||||
pub fn current() -> Option<Thread<E>> {
|
||||
let thread_slot_key = THREAD_SLOT_KEY
|
||||
.try_with(|v| *v)
|
||||
.expect("Slot key should always be readable in TLS (Even if it's None)")?;
|
||||
|
||||
let (_, atomic_thread_repr) = SLOT_MAP.get(thread_slot_key)?;
|
||||
|
||||
let mut result = Thread::new_with_repr(thread_slot_key, atomic_thread_repr.load(Acquire));
|
||||
|
||||
// If the thread is marked as new, we need to mark the repr as no longer
|
||||
// new but keep the new field marked in the returned instance.
|
||||
if result.new {
|
||||
let _guard = guard::enter_implicit_signal_exclusion_zone();
|
||||
E::on_new_thread(PidTid::current());
|
||||
|
||||
// Unset the `new` flag in the atomic repr, so no other calls to
|
||||
// current can return a "new" thread
|
||||
let mut final_repr = atomic_thread_repr.fetch_and(!NEW_MASK, Relaxed) & !NEW_MASK;
|
||||
|
||||
// Lastly check the flag that indicates whether we are allowing
|
||||
// new threads. If it is true, we need to mark this new thread
|
||||
// as `needs_to_exit`.
|
||||
if !new_threads_allowed() {
|
||||
final_repr = store_needs_to_exit(atomic_thread_repr, Release);
|
||||
}
|
||||
|
||||
result.update_from_repr(final_repr);
|
||||
} else if result.forking {
|
||||
let _guard = guard::enter_implicit_signal_exclusion_zone();
|
||||
// Read the actual pid-tid through syscalls
|
||||
let actual_pid_tid = PidTid::current();
|
||||
let stored_pid_tid = result.get_process_and_thread_ids();
|
||||
|
||||
E::on_new_thread(actual_pid_tid);
|
||||
|
||||
// Fix the stored state for this thread to match the current thread.
|
||||
// A new pid-tid here means the thread (and likely the process) is
|
||||
// new
|
||||
if actual_pid_tid != stored_pid_tid {
|
||||
result.fix_stored_state_after_fork(actual_pid_tid);
|
||||
result.new = true;
|
||||
}
|
||||
|
||||
clear_forking_flag(atomic_thread_repr, SeqCst);
|
||||
result.forking = false;
|
||||
}
|
||||
|
||||
Some(result)
|
||||
}
|
||||
|
||||
/// Creates a new thread with the given id and representation.
|
||||
fn new_with_repr(slot_key: SlotKey, repr: StateRepr) -> Self {
|
||||
Thread {
|
||||
slot_key,
|
||||
new: is_new(repr),
|
||||
forking: is_forking(repr),
|
||||
state: repr.into(),
|
||||
needs_to_exit: needs_to_exit(repr),
|
||||
repr,
|
||||
_phantom: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the process and thread ids associated with this thread
|
||||
pub fn get_process_and_thread_ids(&self) -> PidTid {
|
||||
unsafe { SLOT_MAP.get_unchecked(self.slot_key).0.load(Relaxed) }
|
||||
}
|
||||
|
||||
fn get_atomic_repr(&self) -> &AtomicStateRepr {
|
||||
// This is safe because we are using the slot key for the thread that
|
||||
// must have been created by inserting into the slotmap.
|
||||
unsafe { &SLOT_MAP.get_unchecked(self.slot_key).1 }
|
||||
}
|
||||
|
||||
/// Updates the thread state to the give value in both this object and the
|
||||
/// storage map.
|
||||
///
|
||||
/// NOTE: This function has the side effect of updating the other fields in
|
||||
/// this instance associated with the storage representation. Specifically
|
||||
/// if the `needs_to_exit` flag gets set externally, that change will be
|
||||
/// reflected in this thread
|
||||
fn set_state(&mut self, new_thread_state: ThreadState, ordering: Ordering) -> ThreadState {
|
||||
self.update_from_repr(new_thread_state.store(self.get_atomic_repr(), ordering))
|
||||
}
|
||||
|
||||
/// Attempts to set the whole representation of the thread at once, but only
|
||||
/// if the existing repr matches the one in this instance. Returns true if
|
||||
/// the cas succeeds.
|
||||
///
|
||||
/// NOTE: The fields for this instance will be updated based on the new repr
|
||||
/// state regardless of whether the compare and swap succeeds or not.
|
||||
fn compare_and_swap_repr(&mut self, new_repr: StateRepr, ordering: Ordering) -> bool {
|
||||
let cas_result = self
|
||||
.get_atomic_repr()
|
||||
.compare_exchange(self.repr, new_repr, ordering, Relaxed);
|
||||
|
||||
// Regardless of what the final state was, update this instance to match it
|
||||
match cas_result {
|
||||
Ok(new_repr) | Err(new_repr) => self.update_from_repr(new_repr),
|
||||
};
|
||||
|
||||
cas_result.is_ok()
|
||||
}
|
||||
|
||||
/// Updates the fields in this thread instance based on the given
|
||||
/// representation.
|
||||
fn update_from_repr(&mut self, new_repr: StateRepr) -> ThreadState {
|
||||
self.repr = new_repr;
|
||||
self.forking = is_forking(new_repr);
|
||||
self.state = new_repr.into();
|
||||
self.needs_to_exit = needs_to_exit(new_repr);
|
||||
self.state
|
||||
}
|
||||
|
||||
/// Set the thread's state to indicate it is leaving the guest's execution and
|
||||
/// entering Reverie's.
|
||||
/// Note - If at any point the `need_to_exit` flag is set, this function will
|
||||
/// start the thread exit progress
|
||||
pub fn leave_guest_execution(&mut self) -> Result<(), GuestTransitionErr> {
|
||||
self.checked_state_transition(ThreadState::Handler)
|
||||
}
|
||||
|
||||
/// Set the thread's state to indicate it is leaving Reverie's control and re-entering
|
||||
/// the guest's execution
|
||||
/// Note - If at any point the `need_to_exit` flag is set, this function will
|
||||
/// start the thread exit progress
|
||||
pub fn enter_guest_execution(&mut self) -> Result<(), GuestTransitionErr> {
|
||||
self.checked_state_transition(ThreadState::Guest(false))
|
||||
}
|
||||
|
||||
/// Set the thread's state atomically to the given state, and if the needs_to_exit flag
|
||||
/// has been set before or after the change, attempt to exit and return and Result::Err
|
||||
/// indicating if the exit was successful
|
||||
fn checked_state_transition(
|
||||
&mut self,
|
||||
new_state: ThreadState,
|
||||
) -> Result<(), GuestTransitionErr> {
|
||||
if self.needs_to_exit {
|
||||
Err(self.try_exit_during_transistion())
|
||||
} else {
|
||||
self.set_state(new_state, Release);
|
||||
if self.needs_to_exit {
|
||||
Err(self.try_exit_during_transistion())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the given closure with this thread's state set to guest by switching
|
||||
/// in and out of guest execution before and after running the closure
|
||||
pub fn execute_as_guest<F, R>(&mut self, to_run: F) -> Result<R, GuestTransitionErr>
|
||||
where
|
||||
F: FnOnce() -> R,
|
||||
{
|
||||
let _anti_guard = guard::reenter_signal_inclusion_zone();
|
||||
self.enter_guest_execution()?;
|
||||
let result = to_run();
|
||||
self.leave_guest_execution()?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Convenience method that calls try_exit, but wraps the resulting boolean in
|
||||
/// the correct error response based on whether the state was successfully set
|
||||
/// to `Exited`
|
||||
fn try_exit_during_transistion(&mut self) -> GuestTransitionErr {
|
||||
if self.try_exit() {
|
||||
GuestTransitionErr::ExitNow
|
||||
} else {
|
||||
GuestTransitionErr::ExitingElsewhere
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to start the exiting process by
|
||||
/// 1. Verify ownership of the thread by being the first set the state to
|
||||
/// `Exiting`.
|
||||
/// 2. Set the state to Exited.
|
||||
///
|
||||
/// Returning `true` informs the caller that this operation successfully put
|
||||
/// the thread into `Exited` state; it's then the caller's responsibility to
|
||||
/// actually exit the thread.
|
||||
pub fn try_exit(&mut self) -> bool {
|
||||
let exiting_state = build_repr(ThreadState::Exiting, true);
|
||||
if self.compare_and_swap_repr(exiting_state, SeqCst) {
|
||||
let _guard = guard::enter_signal_exclusion_zone();
|
||||
self.set_state(ThreadState::Exited, Acquire);
|
||||
E::on_thread_exit(self.get_process_and_thread_ids());
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Fix the stored pid-tid and thread states after a fork which could have
|
||||
/// corrupted both (from the point of view of this process).
|
||||
fn fix_stored_state_after_fork(&self, actual_pid_tid: PidTid) {
|
||||
// This is safe because a thread cannot have an invalid slot key
|
||||
let (pid_tid, atomic_repr) = unsafe { SLOT_MAP.get_unchecked(self.slot_key) };
|
||||
|
||||
// Override whatever the guest state was with a forking guest state
|
||||
let new_state = ThreadState::Guest(true).as_u8();
|
||||
|
||||
atomic_repr.store(new_state, SeqCst);
|
||||
|
||||
// Override the pid-tid with the actual pid-tid is
|
||||
pid_tid.store(actual_pid_tid, SeqCst);
|
||||
|
||||
// If new threads are not allowed in this process, it means this thread
|
||||
// needs to exit. If this flag was orignally set in the parent, then
|
||||
// we overwrote it above, and need to set it to exit manually
|
||||
if !new_threads_allowed() {
|
||||
store_needs_to_exit(atomic_repr, SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
/// This function is designed to wrap a function that might fork the current
|
||||
/// thread into a new thread in a new process. It indicates that it is in
|
||||
/// the process of forking, and special care should be taken not to assume
|
||||
/// the thread's stored id is valid.
|
||||
///
|
||||
/// It is valid for the given function not to return in the child's
|
||||
/// execution context. Any cleanup that needs to happen in the child's
|
||||
/// execution related to forking state will be cleaned up on the child's
|
||||
/// first syscall.
|
||||
pub fn maybe_fork_as_guest<F>(
|
||||
&mut self,
|
||||
maybe_forking_fn: F,
|
||||
) -> Result<usize, GuestTransitionErr>
|
||||
where
|
||||
F: FnOnce() -> usize,
|
||||
{
|
||||
if new_threads_allowed() {
|
||||
// Keep track of the starting thread id here. There is the
|
||||
// possibility with something like vfork that the thread id could be
|
||||
// changed, but when we change it back below, we don't want to tell
|
||||
// the user it's a new thread
|
||||
let starting_pid_tid = self.get_process_and_thread_ids();
|
||||
|
||||
self.checked_state_transition(ThreadState::Guest(true))?;
|
||||
|
||||
// It is possible that this function might not return, but that's ok
|
||||
let fork_result = maybe_forking_fn();
|
||||
|
||||
// If there was no fork or this is the parent of the fork, we need
|
||||
// to make sure the thread id is still correct
|
||||
|
||||
// Get the pid-tid stored in the slotmap for this thread
|
||||
let ending_pid_tid = self.get_process_and_thread_ids();
|
||||
// Read the actual pid-tid through syscalls
|
||||
let actual_pid_tid = PidTid::current();
|
||||
|
||||
// If the pid-tid combo changed during start and finish, it means
|
||||
// there was a fork or a vfork and we need to fix the stored ids
|
||||
if ending_pid_tid != actual_pid_tid {
|
||||
self.fix_stored_state_after_fork(actual_pid_tid);
|
||||
|
||||
// If the actual pid is not the same as the starting pid, then
|
||||
// likely we are in the child of a fork. That means the thread
|
||||
// is new.
|
||||
if starting_pid_tid != actual_pid_tid {
|
||||
E::on_new_thread(actual_pid_tid);
|
||||
}
|
||||
}
|
||||
|
||||
// Leave the guest state which, and clear the forking flag
|
||||
self.leave_guest_execution()?;
|
||||
Ok(fork_result)
|
||||
} else {
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashSet;
|
||||
use std::mem;
|
||||
use std::ptr;
|
||||
use std::sync::atomic::fence;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::thread::spawn;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Each of these unit tests is mutating the global slotmap, so they
|
||||
/// can't interfere with each other
|
||||
static UNIT_TEST_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
lazy_static! {
|
||||
/// Keep track of the process and thread ids raised in start and exit
|
||||
/// events
|
||||
static ref THREADS_STARTED: Mutex<HashSet<PidTid>> = Mutex::new(HashSet::default());
|
||||
static ref THREADS_EXITED: Mutex<HashSet<PidTid>> = Mutex::new(HashSet::default());
|
||||
}
|
||||
|
||||
struct TestEventSink;
|
||||
|
||||
impl EventSink for TestEventSink {
|
||||
fn on_new_thread(pid_tid: PidTid) {
|
||||
assert!(
|
||||
THREADS_STARTED.lock().unwrap().insert(pid_tid),
|
||||
"Already started {:?}",
|
||||
pid_tid
|
||||
);
|
||||
}
|
||||
|
||||
fn on_thread_exit(pid_tid: PidTid) {
|
||||
assert!(
|
||||
THREADS_EXITED.lock().unwrap().insert(pid_tid),
|
||||
"Already exited {:?}",
|
||||
pid_tid
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn current_test_thread() -> Option<Thread<TestEventSink>> {
|
||||
Thread::<TestEventSink>::current()
|
||||
}
|
||||
|
||||
pub fn run_test_in_new_thread<T>(t: T)
|
||||
where
|
||||
T: 'static + Send + FnOnce(),
|
||||
{
|
||||
let guard = UNIT_TEST_LOCK.lock().unwrap();
|
||||
|
||||
// Here we are replacing the global slot map with a new one for every
|
||||
// test run. This is safe because each test is running inside a single
|
||||
// mutex, so only one test will be accessing the static variable at once
|
||||
unsafe {
|
||||
// FIXME(JakobDegen): This is UB
|
||||
let slot_map_mut = ptr::addr_of!(*SLOT_MAP).cast_mut();
|
||||
|
||||
*slot_map_mut = SlotMap::<ThreadRepr>::new();
|
||||
}
|
||||
|
||||
THREADS_STARTED.lock().unwrap().clear();
|
||||
THREADS_EXITED.lock().unwrap().clear();
|
||||
|
||||
fence(SeqCst);
|
||||
|
||||
let test_result = spawn(t).join();
|
||||
mem::drop(guard);
|
||||
|
||||
// A test failure inside the closure won't cause the actual test to
|
||||
// fail, so we panic here to propogate the error
|
||||
if let Err(e) = test_result {
|
||||
panic!("This test failed - {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Panic if the given thread's id wasn't provided in a notification as a
|
||||
/// thread that exited
|
||||
fn assert_exit_signal_received<E: EventSink>(thread: &Thread<E>) {
|
||||
assert!(
|
||||
THREADS_EXITED
|
||||
.lock()
|
||||
.unwrap()
|
||||
.contains(&thread.get_process_and_thread_ids())
|
||||
);
|
||||
}
|
||||
|
||||
/// Panic if the given thread's id wasn't provided in a notification as a
|
||||
/// thread that started
|
||||
fn assert_start_signal_received<E: EventSink>(thread: &Thread<E>) {
|
||||
assert!(
|
||||
THREADS_STARTED
|
||||
.lock()
|
||||
.unwrap()
|
||||
.contains(&thread.get_process_and_thread_ids())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_thread_lifecycle() {
|
||||
run_test_in_new_thread(|| {
|
||||
let mut thread = current_test_thread().expect("A thread should have been created here");
|
||||
assert!(thread.new);
|
||||
|
||||
let _guard = guard::enter_signal_exclusion_zone();
|
||||
|
||||
assert_start_signal_received(&thread);
|
||||
|
||||
// Threads should always be in guest state upon loading.
|
||||
assert_eq!(thread.state, ThreadState::Guest(false));
|
||||
|
||||
assert!(thread.leave_guest_execution().is_ok());
|
||||
|
||||
// Threads should always be in guest state upon loading.
|
||||
assert_eq!(thread.state, ThreadState::Handler);
|
||||
|
||||
// Make sure executing as guest has the right state in and out of
|
||||
// execution.
|
||||
assert!(
|
||||
thread
|
||||
.execute_as_guest(|| {
|
||||
assert_eq!(
|
||||
current_test_thread()
|
||||
.expect("Should be able to get thread more than once")
|
||||
.state,
|
||||
ThreadState::Guest(false)
|
||||
);
|
||||
})
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
assert_eq!(thread.state, ThreadState::Handler);
|
||||
|
||||
store_needs_to_exit(thread.get_atomic_repr(), SeqCst);
|
||||
|
||||
// Once needs to exit is set, transitions should fail and cause the
|
||||
// thread to go to an error state.
|
||||
assert!(matches!(
|
||||
thread.enter_guest_execution(),
|
||||
Err(GuestTransitionErr::ExitNow)
|
||||
));
|
||||
|
||||
assert_eq!(thread.state, ThreadState::Exited);
|
||||
assert_exit_signal_received(&thread);
|
||||
|
||||
// Once exited a thread cannot return to guest or handler state even
|
||||
// if you set the state directly (which clients can't).
|
||||
thread.set_state(ThreadState::Guest(false), Acquire);
|
||||
assert_eq!(thread.state, ThreadState::Exited);
|
||||
thread.set_state(ThreadState::Handler, Acquire);
|
||||
assert_eq!(thread.state, ThreadState::Exited);
|
||||
thread.set_state(ThreadState::Exiting, Acquire);
|
||||
assert_eq!(thread.state, ThreadState::Exited);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_thread_beahvior() {
|
||||
run_test_in_new_thread(|| {
|
||||
// This thread should be new
|
||||
let thread = current_test_thread().expect("A thread should have been created here");
|
||||
assert!(thread.new);
|
||||
assert_start_signal_received(&thread);
|
||||
|
||||
// But now it's shouldn't be
|
||||
let thread = current_test_thread().expect("A thread should have been created here");
|
||||
assert!(!thread.new);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disallow_new_threads() {
|
||||
run_test_in_new_thread(|| {
|
||||
// Check that the flag is not set at first
|
||||
assert!(
|
||||
spawn(|| current_test_thread().is_some())
|
||||
.join()
|
||||
.expect("Join error")
|
||||
);
|
||||
|
||||
// This will exit all the threads (we don't actually know how many)
|
||||
exit_all(|_, _| {});
|
||||
|
||||
// Now new threads should not be allowed
|
||||
assert!(
|
||||
spawn(|| current_test_thread().is_none())
|
||||
.join()
|
||||
.expect("Join error")
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exit_all() {
|
||||
run_test_in_new_thread(|| {
|
||||
let guest = current_test_thread().expect("This thread should be present");
|
||||
let mut handler = spawn(|| current_test_thread().expect("This one too"))
|
||||
.join()
|
||||
.expect("Failed to join");
|
||||
assert_start_signal_received(&guest);
|
||||
assert_start_signal_received(&handler);
|
||||
|
||||
assert!(handler.leave_guest_execution().is_ok());
|
||||
|
||||
let guest_notified = Arc::new(AtomicBool::new(false));
|
||||
let gn_clone = Arc::clone(&guest_notified);
|
||||
|
||||
let handler_notified = Arc::new(AtomicBool::new(false));
|
||||
let hn_clone = Arc::clone(&handler_notified);
|
||||
|
||||
// This thread is new and should be in guest state, so it should get
|
||||
// notified.
|
||||
exit_all(move |slot_key, _| {
|
||||
if slot_key == guest.slot_key {
|
||||
gn_clone.store(true, SeqCst);
|
||||
} else if slot_key == handler.slot_key {
|
||||
hn_clone.store(true, SeqCst);
|
||||
}
|
||||
});
|
||||
|
||||
// But only the guest should get notified
|
||||
assert!(guest_notified.load(Acquire));
|
||||
assert!(!handler_notified.load(Acquire));
|
||||
|
||||
// Both threads should be marked as needs to exit
|
||||
assert!(needs_to_exit(guest.get_atomic_repr().load(Relaxed)));
|
||||
assert!(needs_to_exit(handler.get_atomic_repr().load(Relaxed)));
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,190 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
use core::time::Duration;
|
||||
|
||||
use reverie_rpc::MakeClient;
|
||||
use reverie_syscalls::LocalMemory;
|
||||
use reverie_syscalls::Syscall;
|
||||
use syscalls::syscall;
|
||||
use syscalls::Errno;
|
||||
use syscalls::Sysno;
|
||||
|
||||
use super::protected_files::uses_protected_fd;
|
||||
use super::utils;
|
||||
use super::vdso;
|
||||
use crate::ffi::fn_icept;
|
||||
|
||||
/// A trait that every Reverie *tool* must implement. The primary function of the
|
||||
/// tool specifies how syscalls and signals are handled.
|
||||
///
|
||||
/// The type that a `Tool` is implemented for represents the process-level state.
|
||||
/// That is, one runtime instance of this type will be created for each guest
|
||||
/// process. This type is in turn a factory for *thread level states*, which are
|
||||
/// allocated dynamically upon guest thread creation. Instances of the thread
|
||||
/// state are also managed by Reverie.
|
||||
pub trait Tool {
|
||||
/// The client used to make global state RPC calls.
|
||||
///
|
||||
/// This can be set to the unit type `()` if the tool does not require
|
||||
/// global state.
|
||||
type Client: MakeClient;
|
||||
|
||||
/// Called when the process first starts. The global state RPC client is
|
||||
/// passed in and may be used to send/recv messages to/from the global
|
||||
/// state.
|
||||
///
|
||||
/// The point at which this is called in the lifetime of the process is
|
||||
/// undefined. It may not be called until *after* libc is loaded.
|
||||
fn new(client: Self::Client) -> Self;
|
||||
|
||||
/// This is called in place of a system call. For example, if the program
|
||||
/// called the `open` syscall, this callback would be called instead. By
|
||||
/// default, the real syscall is simply called.
|
||||
#[inline]
|
||||
fn syscall(&self, syscall: Syscall, _memory: &LocalMemory) -> Result<usize, Errno> {
|
||||
unsafe { syscall.call() }
|
||||
}
|
||||
|
||||
/// Called when a thread first starts.
|
||||
#[inline]
|
||||
fn on_thread_start(&self, _thread_id: u32) {}
|
||||
|
||||
/// Called just before the thread exits. A thread may exit due a variety of
|
||||
/// reasons:
|
||||
/// - The thread called `exit(2)`.
|
||||
/// - This, or another, thread called `exit_group(2)`.
|
||||
/// - This thread was killed by a signal.
|
||||
///
|
||||
/// NOTE: From the tool's persective, it is possible for this api method to
|
||||
/// be called without the accompanying call to `on_thread_start` for the
|
||||
/// same thread id. This is very unlikely, but can happen if a thread is
|
||||
/// signaled to exit before `on_thread_start` is called.
|
||||
#[inline]
|
||||
fn on_thread_exit(&self, _thread_id: u32) {}
|
||||
|
||||
/// Called whenever the `rdtsc` instruction was executed. This should return
|
||||
/// the RDTSC timestamp counter.
|
||||
///
|
||||
/// By default, this returns the result of the `rdtsc` instruction.
|
||||
#[inline]
|
||||
fn rdtsc(&self) -> u64 {
|
||||
unsafe { core::arch::x86_64::_rdtsc() }
|
||||
}
|
||||
|
||||
/// Called whenever the VDSO function `clock_gettime` was called. By
|
||||
/// default, the original `clock_gettime` VDSO function is called.
|
||||
#[inline]
|
||||
fn vdso_clock_gettime(&self, clockid: libc::clockid_t, tp: *mut libc::timespec) -> i32 {
|
||||
unsafe { vdso::clock_gettime(clockid, tp) }
|
||||
}
|
||||
|
||||
/// Called whenever the VDSO function `getcpu` was called. By default, the
|
||||
/// original `getcpu` VDSO function is called.
|
||||
#[inline]
|
||||
fn vdso_getcpu(&self, cpu: *mut u32, node: *mut u32, _unused: usize) -> i32 {
|
||||
unsafe { vdso::getcpu(cpu, node, _unused) }
|
||||
}
|
||||
|
||||
/// Called whenever the VDSO function `gettimeofday` was called. By default,
|
||||
/// the original `gettimeofday` VDSO function is called.
|
||||
#[inline]
|
||||
fn vdso_gettimeofday(&self, tv: *mut libc::timeval, tz: *mut libc::timezone) -> i32 {
|
||||
unsafe { vdso::gettimeofday(tv, tz) }
|
||||
}
|
||||
|
||||
/// Called whenever the VDSO function `time` was called. By default, the
|
||||
/// original `time` VDSO function is called.
|
||||
#[inline]
|
||||
fn vdso_time(&self, tloc: *mut libc::time_t) -> i32 {
|
||||
unsafe { vdso::time(tloc) }
|
||||
}
|
||||
|
||||
/// Returns the time limit for waiting for all threads to exit when an
|
||||
/// `exit_group` syscall is observed. By default, `None` is returned, which
|
||||
/// indicates no timeout. That is, the default behavior is to wait
|
||||
/// indefinitely for all threads to exit.
|
||||
fn get_exit_timeout(&self) -> Option<Duration> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Called when the timeout duration provided by `get_exit_timeout` elapses
|
||||
/// before all threads in the guest application exit. The default behavior
|
||||
/// in this case is to issue an un-intercepted `exit_group(1)` syscall.
|
||||
fn on_exit_timeout(&self) -> usize {
|
||||
let _ = unsafe { syscalls::syscall1(Sysno::exit_group, 1) };
|
||||
unreachable!("All threads will exit before this is called")
|
||||
}
|
||||
|
||||
/// Called when a signal of the given value is received before any handlers
|
||||
/// for that signal are evaluated
|
||||
fn handle_signal_event(&self, _signal: i32) {}
|
||||
/// This is called early on from sbr_init to get a list of functions we want
|
||||
/// to be detoured. Because this is called very early on, this function
|
||||
/// should not be doing *any* allocations or library calls.
|
||||
fn detours() -> &'static [fn_icept] {
|
||||
&[]
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ToolGlobal {
|
||||
type Target: Tool + 'static;
|
||||
|
||||
/// Returns a reference to the global (process-level) instance of this tool.
|
||||
fn global() -> &'static Self::Target;
|
||||
}
|
||||
|
||||
/// Helper methods for syscalls.
|
||||
pub trait SyscallExt {
|
||||
/// Executes the syscall, returning the result.
|
||||
unsafe fn call(self) -> Result<usize, Errno>;
|
||||
}
|
||||
|
||||
impl SyscallExt for Syscall {
|
||||
unsafe fn call(self) -> Result<usize, Errno> {
|
||||
use reverie_syscalls::SyscallInfo;
|
||||
|
||||
let (sysno, args) = self.into_parts();
|
||||
|
||||
// Some syscalls need to be handled in a special way.
|
||||
if sysno == Sysno::readlink {
|
||||
utils::sys_readlink(
|
||||
args.arg0 as *const libc::c_char,
|
||||
args.arg1 as *mut libc::c_char,
|
||||
args.arg2 as usize,
|
||||
)
|
||||
} else if sysno == Sysno::execve {
|
||||
utils::sys_execve(
|
||||
args.arg0 as *const libc::c_char,
|
||||
args.arg1 as *const *const libc::c_char,
|
||||
args.arg2 as *const *const libc::c_char,
|
||||
)
|
||||
} else if sysno == Sysno::rt_sigprocmask {
|
||||
utils::sys_rt_sigprocmask(
|
||||
args.arg0 as libc::c_int,
|
||||
args.arg1 as *const _,
|
||||
args.arg2 as *mut _,
|
||||
args.arg3 as usize,
|
||||
)
|
||||
} else if sysno == Sysno::close && args.arg0 == libc::STDERR_FILENO as usize {
|
||||
// Prevent stderr from getting closed. We need this for logging
|
||||
// purposes.
|
||||
// FIXME: All logging should go through the global state instead.
|
||||
Ok(0)
|
||||
} else if uses_protected_fd(sysno, args.arg0, args.arg1) {
|
||||
// If this syscall operates on a protected file descriptor, we
|
||||
// should return EBADF to indicate that the file descriptor isn't
|
||||
// opened (even if it really is).
|
||||
Err(Errno::EBADF)
|
||||
} else {
|
||||
syscall!(
|
||||
sysno, args.arg0, args.arg1, args.arg2, args.arg3, args.arg4, args.arg5
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,178 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
use std::ffi::CStr;
|
||||
|
||||
use syscalls::syscall3;
|
||||
use syscalls::Errno;
|
||||
use syscalls::Sysno;
|
||||
|
||||
use super::paths;
|
||||
use crate::callbacks::CONTROLLED_EXIT_SIGNAL;
|
||||
|
||||
/// `readlink` needs to be handled in a special way. If we're trying to read
|
||||
/// `/proc/self/exe`, then we can't return the path to the sabre executable. We
|
||||
/// need to replace it with the path to the real binary.
|
||||
///
|
||||
/// NOTE: This doesn't handle numerous other cases such as:
|
||||
/// 1. Using `readlinkat(-100, "/proc/self/exe", ...)`
|
||||
/// 2. Using `readlinkat(dir_fd, "exe", ...)`
|
||||
/// 3. Using `readlink("/proc/{pid}/exe", ...)`
|
||||
pub fn sys_readlink(
|
||||
path: *const libc::c_char,
|
||||
buf: *mut libc::c_char,
|
||||
bufsize: usize,
|
||||
) -> Result<usize, Errno> {
|
||||
if unsafe { CStr::from_ptr(path) }.to_bytes() == b"/proc/self/exe" {
|
||||
if buf.is_null() {
|
||||
return Err(Errno::EFAULT);
|
||||
}
|
||||
|
||||
let client_path = paths::client_path();
|
||||
let len = client_path.to_bytes().len().min(bufsize);
|
||||
|
||||
unsafe { core::ptr::copy_nonoverlapping(client_path.as_ptr(), buf, len) };
|
||||
|
||||
Ok(len)
|
||||
} else {
|
||||
unsafe {
|
||||
syscall3(
|
||||
Sysno::readlink,
|
||||
path as usize,
|
||||
buf as usize,
|
||||
bufsize as usize,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `execve` needs to be handled in a special way because, in order to trace
|
||||
/// child processes after they call execve, we need to run the child process as
|
||||
/// `sabre plugin.so -- child` instead.
|
||||
pub fn sys_execve(
|
||||
filename: *const libc::c_char,
|
||||
argv: *const *const libc::c_char,
|
||||
envp: *const *const libc::c_char,
|
||||
) -> Result<usize, Errno> {
|
||||
// FIXME: This is subject to race conditions!
|
||||
if unsafe { libc::access(filename, libc::F_OK) } != 0 {
|
||||
return Err(Errno::ENOENT);
|
||||
}
|
||||
|
||||
// Count the number of arguments so we only need to do one allocation.
|
||||
let mut argc = 0;
|
||||
while !(unsafe { *argv.add(argc) }).is_null() {
|
||||
argc += 1;
|
||||
}
|
||||
|
||||
let sabre = paths::sabre_path().as_ptr();
|
||||
|
||||
let mut new_argv = Vec::with_capacity(argc + 4);
|
||||
new_argv.push(sabre);
|
||||
new_argv.push(paths::plugin_path().as_ptr());
|
||||
new_argv.push(b"--\0".as_ptr() as *const libc::c_char);
|
||||
|
||||
// FIXME: Overwrite arg0 so it contains an absolute path. Sabre can only
|
||||
// take absolute paths at the moment.
|
||||
new_argv.push(filename);
|
||||
|
||||
// Append the original argv (except arg0)
|
||||
for i in 1..argc {
|
||||
new_argv.push(unsafe { *argv.add(i) });
|
||||
}
|
||||
|
||||
new_argv.push(core::ptr::null());
|
||||
|
||||
// Never returns if successful. Thus, it doesn't matter if our Vec is
|
||||
// dropped.
|
||||
unsafe {
|
||||
syscall3(
|
||||
Sysno::execve,
|
||||
sabre as usize,
|
||||
new_argv.as_ptr() as usize,
|
||||
envp as usize,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// glibc defines this to be much larger than what the kernel accepts. Since we
|
||||
/// have to make a direct syscall to `rt_sigaction`, we must use the same sigset
|
||||
/// as the kernel does.
|
||||
///
|
||||
/// The kernel currently uses 64 bits for the sigset. See:
|
||||
/// https://elixir.bootlin.com/linux/v5.17.5/source/arch/x86/include/uapi/asm/signal.h#L17
|
||||
#[derive(Copy, Clone, Default)]
|
||||
#[repr(C)]
|
||||
pub struct KernelSigset(u64);
|
||||
|
||||
impl KernelSigset {
|
||||
/// Check if the sigset contains a signal.
|
||||
#[allow(unused)]
|
||||
pub fn contains(&self, sig: libc::c_int) -> bool {
|
||||
let mask = sigmask(sig);
|
||||
(self.0 & mask) == mask
|
||||
}
|
||||
|
||||
/// Removes the given signal from the sigset.
|
||||
pub fn remove(&mut self, sig: libc::c_int) {
|
||||
let mask = sigmask(sig);
|
||||
self.0 &= !(mask as u64)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn sigmask(sig: libc::c_int) -> u64 {
|
||||
// wrapping_sub is safe because signal numbers start at 1.
|
||||
1 << (sig as u64).wrapping_sub(1)
|
||||
}
|
||||
|
||||
/// rt_sigprocmask needs special handling because if the guest tries to set a
|
||||
/// signal mask that prevents our control signal from being received by a
|
||||
/// thread, we are going to create and pass our own sigset that only differs
|
||||
/// from the client's in that it does not suppress our control signal
|
||||
pub fn sys_rt_sigprocmask(
|
||||
operation: libc::c_int,
|
||||
sigset_ptr: *const KernelSigset,
|
||||
prev_sigset_ptr: *mut KernelSigset,
|
||||
// Should always 8 for x86_64
|
||||
sigset_size: usize,
|
||||
) -> Result<usize, Errno> {
|
||||
if sigset_ptr.is_null() {
|
||||
return unsafe {
|
||||
syscalls::syscall4(
|
||||
Sysno::rt_sigprocmask,
|
||||
operation as usize,
|
||||
sigset_ptr as usize,
|
||||
prev_sigset_ptr as usize,
|
||||
sigset_size as usize,
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
let mut new_sigset = unsafe { *sigset_ptr };
|
||||
|
||||
if matches!(operation, libc::SIG_SETMASK | libc::SIG_BLOCK) {
|
||||
new_sigset.remove(CONTROLLED_EXIT_SIGNAL);
|
||||
}
|
||||
|
||||
unsafe {
|
||||
syscalls::syscall4(
|
||||
Sysno::rt_sigprocmask,
|
||||
operation as usize,
|
||||
&new_sigset as *const _ as usize,
|
||||
prev_sigset_ptr as usize,
|
||||
sigset_size as usize,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_vfork(sys_no: Sysno, arg1: usize) -> bool {
|
||||
const VFORK_FLAGS: usize = (libc::CLONE_VM | libc::CLONE_VFORK | libc::SIGCHLD) as usize;
|
||||
sys_no == Sysno::vfork || (sys_no == Sysno::clone && (arg1 & VFORK_FLAGS == VFORK_FLAGS))
|
||||
}
|
|
@ -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;
|
|
@ -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"] }
|
|
@ -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"] }
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
//! This contains the RPC protocol for the guest and host. That is, how the host
|
||||
//! and guest should talk to each other.
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use syscalls::Errno;
|
||||
|
||||
/// Configuration options that adjust the behavior of the tool.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
/// Only log syscalls that failed.
|
||||
pub only_failures: bool,
|
||||
|
||||
/// Don't print anything.
|
||||
pub quiet: bool,
|
||||
}
|
||||
|
||||
/// Our service definition. The request and response enums are derived from this
|
||||
/// interface. This also derives the client implementation.
|
||||
#[reverie_rpc::service]
|
||||
pub trait MyService {
|
||||
/// Get the current configuration.
|
||||
fn config() -> Config;
|
||||
|
||||
/// Increment the count of syscalls.
|
||||
#[rpc(no_response)]
|
||||
fn count(count: usize);
|
||||
|
||||
#[rpc(no_response)]
|
||||
fn pretty_print(thread_id: u32, pretty: &str, result: Option<Result<usize, Errno>>);
|
||||
}
|
|
@ -1,120 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
use core::sync::atomic::AtomicUsize;
|
||||
use core::sync::atomic::Ordering;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use colored::Colorize;
|
||||
use reverie_process::Pid;
|
||||
use riptrace_rpc::Config;
|
||||
use riptrace_rpc::MyService;
|
||||
use syscalls::Errno;
|
||||
|
||||
pub struct GlobalState {
|
||||
/// The configuration.
|
||||
pub config: Config,
|
||||
|
||||
/// The number of syscalls we've seen so far.
|
||||
pub count: AtomicUsize,
|
||||
|
||||
output: Output,
|
||||
}
|
||||
|
||||
pub enum Output {
|
||||
Stderr(io::Stderr),
|
||||
File(Mutex<io::BufWriter<fs::File>>),
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MyService for GlobalState {
|
||||
async fn config(&self) -> Config {
|
||||
self.config.clone()
|
||||
}
|
||||
|
||||
async fn count(&self, count: usize) {
|
||||
self.count.fetch_add(count, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
async fn pretty_print(
|
||||
&self,
|
||||
thread_id: u32,
|
||||
pretty: &str,
|
||||
result: Option<Result<usize, Errno>>,
|
||||
) {
|
||||
let thread_id = Pid::from_raw(thread_id as i32);
|
||||
|
||||
match &self.output {
|
||||
Output::Stderr(stderr) => {
|
||||
let mut stderr = stderr.lock();
|
||||
|
||||
match result {
|
||||
Some(Ok(value)) => {
|
||||
writeln!(
|
||||
stderr,
|
||||
"[{}] {} {} {}",
|
||||
thread_id.colored(),
|
||||
pretty,
|
||||
"->".bold(),
|
||||
value.to_string().green()
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
Some(Err(errno)) => {
|
||||
writeln!(
|
||||
stderr,
|
||||
"[{}] {} {} {}",
|
||||
thread_id.colored(),
|
||||
pretty,
|
||||
"->".bold(),
|
||||
errno.to_string().bold().red()
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
None => {
|
||||
writeln!(stderr, "[{}] {}", thread_id.colored(), pretty).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
Output::File(file) => {
|
||||
let mut f = file.lock().unwrap();
|
||||
|
||||
match result {
|
||||
Some(Ok(value)) => {
|
||||
writeln!(f, "[{}] {} -> {}", thread_id, pretty, value).unwrap();
|
||||
}
|
||||
Some(Err(errno)) => {
|
||||
writeln!(f, "[{}] {} -> {}", thread_id, pretty, errno).unwrap();
|
||||
}
|
||||
None => {
|
||||
writeln!(f, "[{}] {}", thread_id, pretty).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GlobalState {
|
||||
pub fn new(config: Config) -> Self {
|
||||
Self {
|
||||
config,
|
||||
count: AtomicUsize::new(0),
|
||||
output: Output::Stderr(io::stderr()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_output(&mut self, f: fs::File) -> &mut Self {
|
||||
self.output = Output::File(Mutex::new(io::BufWriter::new(f)));
|
||||
|
||||
self
|
||||
}
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
mod global_state;
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use global_state::GlobalState;
|
||||
use reverie_process::Command;
|
||||
use reverie_process::ExitStatus;
|
||||
use riptrace_rpc::Config;
|
||||
use riptrace_rpc::MyService;
|
||||
|
||||
/// A super fast strace.
|
||||
#[derive(Parser)]
|
||||
#[clap(trailing_var_arg = true)]
|
||||
struct Args {
|
||||
/// Count the number of calls for each system call and report a summary on
|
||||
/// program exit.
|
||||
#[clap(long, short = 'c')]
|
||||
summary: bool,
|
||||
|
||||
/// Only log syscalls that failed.
|
||||
#[clap(long)]
|
||||
only_failures: bool,
|
||||
|
||||
/// Don't log anything.
|
||||
#[clap(long, short)]
|
||||
quiet: bool,
|
||||
|
||||
/// Output to this file instead of stderr.
|
||||
#[clap(long, short)]
|
||||
output: Option<PathBuf>,
|
||||
|
||||
/// Path to the sabre binary used to launch the plugin.
|
||||
#[clap(long, env = "SABRE_PATH")]
|
||||
sabre: Option<PathBuf>,
|
||||
|
||||
/// Path to the plugin.
|
||||
#[clap(long, env = "SABRE_PLUGIN")]
|
||||
plugin: Option<PathBuf>,
|
||||
|
||||
/// The program and arguments.
|
||||
#[clap(required = true, multiple_values = true)]
|
||||
command: Vec<String>,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
async fn run(self) -> Result<ExitStatus> {
|
||||
let mut command = Command::new(&self.command[0]);
|
||||
command.args(&self.command[1..]);
|
||||
|
||||
let config = Config {
|
||||
only_failures: self.only_failures,
|
||||
quiet: self.quiet,
|
||||
};
|
||||
|
||||
let mut global_state = GlobalState::new(config);
|
||||
|
||||
if let Some(path) = self.output {
|
||||
global_state.with_output(fs::File::create(path)?);
|
||||
}
|
||||
|
||||
let global_state = Arc::new(global_state.serve());
|
||||
|
||||
let mut child = reverie_host::TracerBuilder::new(command)
|
||||
.plugin(self.plugin)
|
||||
.sabre(self.sabre)
|
||||
.global_state(global_state.clone())
|
||||
.spawn()?;
|
||||
|
||||
let exit_status = child.wait().await?;
|
||||
|
||||
if self.summary {
|
||||
let count = global_state
|
||||
.count
|
||||
.load(core::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
eprintln!("Saw {} syscalls", count);
|
||||
}
|
||||
|
||||
Ok(exit_status)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[tokio::main]
|
||||
async fn _main() -> ExitStatus {
|
||||
match Args::parse().run().await {
|
||||
Ok(exit_status) => exit_status,
|
||||
Err(err) => {
|
||||
eprintln!("{:?}", err);
|
||||
ExitStatus::Exited(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the tokio runtime exits before propagating the exit status.
|
||||
// This ensures that any Drop code gets a chance to run.
|
||||
//
|
||||
// TODO: Add a proc macro that does this instead.
|
||||
_main().raise_or_exit()
|
||||
}
|
|
@ -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"] }
|
|
@ -1,110 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
//! An strace tool meant to be injected and ran by SaBRe.
|
||||
|
||||
use core::sync::atomic::AtomicU64;
|
||||
use core::sync::atomic::Ordering;
|
||||
|
||||
use reverie_sabre as sabre;
|
||||
use reverie_syscalls::Displayable;
|
||||
use reverie_syscalls::LocalMemory;
|
||||
use reverie_syscalls::Syscall;
|
||||
use riptrace_rpc::Config;
|
||||
use riptrace_rpc::MyServiceClient;
|
||||
use sabre::SyscallExt;
|
||||
use sabre::Tool;
|
||||
use syscalls::Errno;
|
||||
use syscalls::Sysno;
|
||||
|
||||
struct Riptrace {
|
||||
/// Count of syscalls we've seen so far.
|
||||
count: AtomicU64,
|
||||
#[allow(dead_code)]
|
||||
client: MyServiceClient,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
#[sabre::tool]
|
||||
impl Tool for Riptrace {
|
||||
type Client = MyServiceClient;
|
||||
|
||||
#[detour(lib = "libc", func = "malloc")]
|
||||
fn malloc(_size: usize) -> *mut libc::c_void {
|
||||
todo!()
|
||||
}
|
||||
|
||||
#[detour(lib = "libc", func = "free")]
|
||||
fn free(_ptr: *mut libc::c_void) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn new(client: Self::Client) -> Self {
|
||||
let config = client.config();
|
||||
|
||||
Self {
|
||||
count: AtomicU64::new(0),
|
||||
client,
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
fn syscall(&self, syscall: Syscall, memory: &LocalMemory) -> Result<usize, Errno> {
|
||||
self.count.fetch_add(1, Ordering::Relaxed);
|
||||
match syscall {
|
||||
Syscall::Execve(_) | Syscall::Execveat(_) => {
|
||||
if !self.config.quiet {
|
||||
self.client.print_syscall(&syscall, memory, None);
|
||||
}
|
||||
|
||||
// NOTE: execve does not return upon success
|
||||
let errno = unsafe { syscall.call() }.unwrap_err();
|
||||
|
||||
self.client
|
||||
.print_syscall(&syscall, memory, Some(Err(errno)));
|
||||
|
||||
Err(errno)
|
||||
}
|
||||
syscall => {
|
||||
let ret = unsafe { syscall.call() };
|
||||
|
||||
if !self.config.quiet && (!self.config.only_failures || ret.is_err()) {
|
||||
self.client.print_syscall(&syscall, memory, Some(ret));
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait MyServiceClientExt {
|
||||
fn print_syscall(
|
||||
&self,
|
||||
syscall: &Syscall,
|
||||
memory: &LocalMemory,
|
||||
result: Option<Result<usize, Errno>>,
|
||||
);
|
||||
}
|
||||
|
||||
impl MyServiceClientExt for MyServiceClient {
|
||||
fn print_syscall(
|
||||
&self,
|
||||
syscall: &Syscall,
|
||||
memory: &LocalMemory,
|
||||
result: Option<Result<usize, Errno>>,
|
||||
) {
|
||||
// TODO: Use a thread-local to avoid this extra syscall.
|
||||
let tid = unsafe { syscalls::raw_syscall!(Sysno::gettid) } as u32;
|
||||
|
||||
// TODO: Use a smallvec to allocate this instead.
|
||||
let pretty = syscall.display_with_outputs(memory).to_string();
|
||||
|
||||
self.pretty_print(tid, &pretty, result)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue