Compare commits

..

2 Commits

Author SHA1 Message Date
bd0d114145 wip: elf packing
All checks were successful
continuous-integration/drone/push Build is passing
2022-05-10 22:56:30 +01:00
f07dab1b4b wip: bin packing 2022-05-10 22:56:30 +01:00
20 changed files with 407 additions and 1584 deletions

235
Cargo.lock generated
View File

@ -2,6 +2,23 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 3
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ahash"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [
"getrandom",
"once_cell",
"version_check",
]
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "0.7.18" version = "0.7.18"
@ -11,12 +28,6 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "anyhow"
version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
[[package]] [[package]]
name = "atty" name = "atty"
version = "0.2.14" version = "0.2.14"
@ -35,10 +46,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]] [[package]]
name = "base64" name = "bincode"
version = "0.13.0" version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "bitflags" name = "bitflags"
@ -75,9 +89,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.73" version = "1.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
@ -120,6 +134,27 @@ dependencies = [
"os_str_bytes", "os_str_bytes",
] ]
[[package]]
name = "clone-shim"
version = "0.1.0"
dependencies = [
"bincode",
"clap 3.1.15",
"close_fds",
"criterion",
"env_logger",
"exitcode",
"ipnetwork",
"libc",
"log",
"nix",
"object",
"serde",
"serde_json",
"tempfile",
"thiserror",
]
[[package]] [[package]]
name = "close_fds" name = "close_fds"
version = "0.3.2" version = "0.3.2"
@ -130,6 +165,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "crc32fast"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "criterion" name = "criterion"
version = "0.3.5" version = "0.3.5"
@ -267,6 +311,29 @@ dependencies = [
"instant", "instant",
] ]
[[package]]
name = "flate2"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39522e96686d38f4bc984b9198e3a0613264abaebaff2c5c918bfa6b6da09af"
dependencies = [
"cfg-if",
"crc32fast",
"libc",
"miniz_oxide",
]
[[package]]
name = "getrandom"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]] [[package]]
name = "half" name = "half"
version = "1.8.2" version = "1.8.2"
@ -278,6 +345,9 @@ name = "hashbrown"
version = "0.11.2" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
dependencies = [
"ahash",
]
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
@ -288,12 +358,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "httparse"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c"
[[package]] [[package]]
name = "humantime" name = "humantime"
version = "2.1.0" version = "2.1.0"
@ -366,9 +430,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.126" version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c"
[[package]] [[package]]
name = "log" name = "log"
@ -395,12 +459,22 @@ dependencies = [
] ]
[[package]] [[package]]
name = "nix" name = "miniz_oxide"
version = "0.24.1" version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f17df307904acd05aa8e32e97bb20f2a0df1728bbc2d771ae8f9a90463441e9" checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082"
dependencies = [
"adler",
]
[[package]]
name = "nix"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"cc",
"cfg-if", "cfg-if",
"libc", "libc",
"memoffset", "memoffset",
@ -426,10 +500,23 @@ dependencies = [
] ]
[[package]] [[package]]
name = "once_cell" name = "object"
version = "1.12.0" version = "0.28.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" checksum = "40bec70ba014595f99f7aa110b84331ffe1ee9aece7fe6f387cc7e3ecda4d456"
dependencies = [
"crc32fast",
"flate2",
"hashbrown",
"indexmap",
"memchr",
]
[[package]]
name = "once_cell"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9"
[[package]] [[package]]
name = "oorandom" name = "oorandom"
@ -555,21 +642,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "ring"
version = "0.16.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
dependencies = [
"cc",
"libc",
"once_cell",
"spin",
"untrusted",
"web-sys",
"winapi",
]
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.4.0" version = "0.4.0"
@ -579,27 +651,6 @@ dependencies = [
"semver", "semver",
] ]
[[package]]
name = "rustls"
version = "0.20.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033"
dependencies = [
"log",
"ring",
"sct",
"webpki",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7522c9de787ff061458fe9a829dc790a3f5b22dc571694fc5883f448b94d9a9"
dependencies = [
"base64",
]
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.9" version = "1.0.9"
@ -621,16 +672,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "sct"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
dependencies = [
"ring",
"untrusted",
]
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.6" version = "1.0.6"
@ -678,12 +719,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.10.0" version = "0.10.0"
@ -782,34 +817,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]] [[package]]
name = "untrusted" name = "version_check"
version = "0.7.1" version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "void-orchestrator"
version = "0.1.0"
dependencies = [
"anyhow",
"clap 3.1.15",
"close_fds",
"criterion",
"env_logger",
"exitcode",
"httparse",
"ipnetwork",
"lazy_static",
"libc",
"log",
"nix",
"rustls",
"rustls-pemfile",
"serde",
"serde_json",
"tempfile",
"thiserror",
]
[[package]] [[package]]
name = "walkdir" name = "walkdir"
@ -822,6 +833,12 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.79" version = "0.2.79"
@ -886,16 +903,6 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "webpki"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
dependencies = [
"ring",
"untrusted",
]
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"

View File

@ -1,5 +1,5 @@
[package] [package]
name = "void-orchestrator" name = "clone-shim"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
@ -14,23 +14,18 @@ exitcode = "1"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
bincode = "1.3"
ipnetwork = "0.18" ipnetwork = "0.18"
libc = "0.2.117" libc = "0.2.117"
nix = "0.24.1" nix = "0.23.1"
close_fds = "0.3.2" close_fds = "0.3.2"
tempfile = "3.3" tempfile = "3.3"
object = { version = "0.28", features = ["write_core"] }
[dev-dependencies] [dev-dependencies]
criterion = "0.3" criterion = "0.3"
anyhow = "1"
# examples/tls
httparse = "1"
rustls = "0.20"
rustls-pemfile = "1"
lazy_static = "1"
[[bench]] [[bench]]
name = "clone3" name = "clone3"

View File

@ -2,16 +2,6 @@
## Running the examples ## Running the examples
### examples/fib
The fib example performs fibonacci trivially on a fixed number. It is the most basic example of a process that requires no privilege, excluding `Stdout` to print the result.
To run this example:
cargo build
cargo build --example fib
target/debug/clone-shim -s examples/fib/spec.json target/debug/examples/fib
### examples/basic ### examples/basic
The basic example instructs the shim to spawn two processes, each of which writes "hello from main{1,2}!" to stdout. The basic example instructs the shim to spawn two processes, each of which writes "hello from main{1,2}!" to stdout.
@ -32,16 +22,6 @@ To run this example:
cargo build --example pipes cargo build --example pipes
target/debug/clone-shim -s examples/pipes/spec.json target/debug/examples/pipes target/debug/clone-shim -s examples/pipes/spec.json target/debug/examples/pipes
### examples/pipes
The pipes example shows some of the power of the shim by using pipes. The process "pipe_sender" sends two messages down a pipe that it's given by the shim. These two messages each spawn a completely isolated process, "pipe_receiver", that receives that message.
To run this example:
cargo build
cargo build --example tls
target/debug/clone-shim -s examples/tls/spec.json target/debug/examples/tls
## Debugging the shim ## Debugging the shim
The shim can be debugged as with most processes, but it is exceptionally forky. Breaking before a clone in `rust-gdb` then running `set follow-fork-mode child` is often necessary. The best approach is to go in with a plan of attack. The shim can be debugged as with most processes, but it is exceptionally forky. Breaking before a clone in `rust-gdb` then running `set follow-fork-mode child` is often necessary. The best approach is to go in with a plan of attack.

View File

@ -1,4 +1,4 @@
use void_orchestrator::clone::{clone3, CloneArgs, CloneFlags}; use clone_shim::clone::{clone3, CloneArgs, CloneFlags};
use criterion::{criterion_group, criterion_main, Criterion}; use criterion::{criterion_group, criterion_main, Criterion};

View File

@ -1,15 +0,0 @@
fn main() {
println!("fib(1) = {}", fib(1));
println!("fib(7) = {}", fib(7));
println!("fib(19) = {}", fib(19));
}
fn fib(i: u64) -> u64 {
let (mut a, mut b) = (0, 1);
for _ in 0..i {
(a, b) = (b, a + b);
}
a
}

View File

@ -1,27 +0,0 @@
{
"entrypoints": {
"fib": {
"environment": [
"Stdout",
{
"Filesystem": {
"host_path": "/lib/x86_64-linux-gnu/libgcc_s.so.1",
"environment_path": "/lib/libgcc_s.so.1"
}
},
{
"Filesystem": {
"host_path": "/lib/x86_64-linux-gnu/libc.so.6",
"environment_path": "/lib/libc.so.6"
}
},
{
"Filesystem": {
"host_path": "/lib64/ld-linux-x86-64.so.2",
"environment_path": "/lib64/ld-linux-x86-64.so.2"
}
}
]
}
}
}

View File

@ -1,85 +0,0 @@
use std::fs::OpenOptions;
use std::io::{self, ErrorKind, Read, Write};
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
pub(super) fn handler(mut stream: UnixStream) -> i32 {
println!("entered http handler");
let mut buf = Vec::new();
let mut buf_len = 0;
loop {
buf.resize_with(buf_len + 4096, Default::default);
let read_bytes = stream.read(&mut buf[buf_len..]).unwrap();
buf_len += read_bytes;
if read_bytes == 0 {
break;
}
let mut headers = [httparse::EMPTY_HEADER; 64];
let mut req = httparse::Request::new(&mut headers);
let result = req.parse(&buf).unwrap();
if result.is_partial() {
continue;
}
let filename = if req.method != Some("GET") {
None
} else {
req.path
};
if let Some(filename) = filename {
if try_serve_file(&mut stream, filename).unwrap() {
return exitcode::OK;
}
}
let status_line = "HTTP/1.1 404 NOT FOUND";
let contents = "file not found\n";
let response = format!(
"{}\r\nContent-Length: {}\r\n\r\n{}",
status_line,
contents.len(),
contents
);
stream.write_all(response.as_bytes()).unwrap();
break;
}
exitcode::OK
}
fn try_serve_file(stream: &mut impl io::Write, filename: &str) -> io::Result<bool> {
let mut fd = match OpenOptions::new()
.read(true)
.open(PathBuf::from("/var/www/html/").join(filename.strip_prefix('/').unwrap_or(filename)))
{
Ok(fd) => fd,
Err(e) => {
if e.kind() == ErrorKind::NotFound {
return Ok(false);
}
return Err(e);
}
};
let status_line = "HTTP/1.1 200 OK";
let response_header = format!(
"{}\r\nContent-Length: {}\r\n\r\n",
status_line,
fd.metadata()?.len(),
);
stream.write_all(response_header.as_bytes())?;
io::copy(&mut fd, stream)?;
Ok(true)
}

View File

@ -1,55 +0,0 @@
use std::fs::File;
use std::io::ErrorKind;
use std::net::TcpListener;
use std::os::unix::io::AsRawFd;
use std::sync::atomic::{AtomicBool, Ordering};
use nix::poll::{poll, PollFd, PollFlags};
use nix::sys::signal::{signal, SigHandler, Signal};
use nix::Error as NixError;
use lazy_static::lazy_static;
lazy_static! {
static ref RUNNING: AtomicBool = AtomicBool::new(true);
}
pub(crate) fn handler(tls_handler_trigger: File, listener: TcpListener) -> i32 {
println!("connection_listener entered");
// SAFETY: only unsafe if you use the result
unsafe { signal(Signal::SIGINT, SigHandler::Handler(handle_sigint)) }.unwrap();
listener.set_nonblocking(true).unwrap();
let mut to_poll = [PollFd::new(listener.as_raw_fd(), PollFlags::POLLIN)];
while RUNNING.load(Ordering::Relaxed) {
if let Err(e) = poll(&mut to_poll, 1000) {
if e == NixError::EINTR {
continue; // timed out
}
Err(e).unwrap()
}
let stream = match listener.accept() {
Ok(s) => s,
Err(e) => {
if e.kind() != ErrorKind::WouldBlock {
Err(e).unwrap()
} else {
continue;
}
}
};
println!("received a new connection");
super::tls_handler(&tls_handler_trigger, stream.0);
}
exitcode::OK
}
extern "C" fn handle_sigint(signal: libc::c_int) {
let signal = Signal::try_from(signal).unwrap();
RUNNING.store(signal != Signal::SIGINT, Ordering::Relaxed);
}

View File

@ -1,149 +0,0 @@
mod http;
mod listener;
mod tls;
use std::fs::File;
use std::net::{TcpListener, TcpStream};
use std::os::unix::net::UnixStream;
fn main() {
match std::env::args().next() {
Some(s) => match s.as_str() {
"connection_listener" => connection_listener_entrypoint(),
"tls_handler" => tls_handler_entrypoint(),
"http_handler" => http_handler_entrypoint(),
_ => unimplemented!(),
},
None => unimplemented!(),
}
}
fn connection_listener_entrypoint() {
// imports
use std::os::unix::io::{FromRawFd, RawFd};
// argument parsing
let mut args = std::env::args();
let _entrypoint = args.next();
let tls_handler_trigger = args.next();
let tls_handler_trigger: RawFd = tls_handler_trigger
.expect("tls handler trigger required")
.parse()
.expect("tls handler trigger should be a file descriptor");
let tls_handler_trigger = unsafe { File::from_raw_fd(tls_handler_trigger) };
let tcp_listener = args.next();
let tcp_listener: RawFd = tcp_listener
.expect("tcp listener required")
.parse()
.expect("tcp listener should be a file descriptor");
let tcp_listener = unsafe { TcpListener::from_raw_fd(tcp_listener) };
// run function
std::process::exit(listener::handler(tls_handler_trigger, tcp_listener));
}
fn tls_handler(trigger_socket: &File, stream: TcpStream) {
// imports
use nix::sys::socket::{sendmsg, ControlMessage, MsgFlags};
use std::os::unix::io::AsRawFd;
// send file descriptor(s)
let sockfd = trigger_socket.as_raw_fd();
let fds = [stream.as_raw_fd()];
sendmsg::<()>(
sockfd,
&[],
&[ControlMessage::ScmRights(&fds)],
MsgFlags::empty(),
None,
)
.unwrap();
}
fn tls_handler_entrypoint() {
// imports
use std::os::unix::io::{FromRawFd, RawFd};
// argument parsing
let mut args = std::env::args();
let _entrypoint = args.next();
let http_handler_trigger = args.next();
let http_handler_trigger: RawFd = http_handler_trigger
.expect("http handler trigger required")
.parse()
.expect("http handler trigger should be a file descriptor");
let http_handler_trigger = unsafe { File::from_raw_fd(http_handler_trigger) };
let tls_cert_file = args.next();
let tls_cert_file: RawFd = tls_cert_file
.expect("tls cert file required")
.parse()
.expect("tls cert file should be a file descriptor");
let tls_cert_file = unsafe { File::from_raw_fd(tls_cert_file) };
let tls_key_file = args.next();
let tls_key_file: RawFd = tls_key_file
.expect("tls key file required")
.parse()
.expect("tls key file should be a file descriptor");
let tls_key_file = unsafe { File::from_raw_fd(tls_key_file) };
let stream = args.next();
let stream: RawFd = stream
.expect("request stream required")
.parse()
.expect("request stream should be a file descriptor");
let stream = unsafe { TcpStream::from_raw_fd(stream) };
std::process::exit(tls::handler(
http_handler_trigger,
tls_cert_file,
tls_key_file,
stream,
));
}
fn http_handler(trigger_socket: &File, stream: UnixStream) {
// imports
use nix::sys::socket::{sendmsg, ControlMessage, MsgFlags};
use std::os::unix::io::AsRawFd;
// send file descriptor(s)
let sockfd = trigger_socket.as_raw_fd();
let fds = [stream.as_raw_fd()];
sendmsg::<()>(
sockfd,
&[],
&[ControlMessage::ScmRights(&fds)],
MsgFlags::empty(),
None,
)
.unwrap();
}
fn http_handler_entrypoint() {
// imports
use std::os::unix::io::{FromRawFd, RawFd};
// argument parsing
let mut args = std::env::args();
let _entrypoint = args.next();
let stream = args.next();
let stream: RawFd = stream
.expect("request stream required")
.parse()
.expect("request stream should be a file descriptor");
let stream = unsafe { UnixStream::from_raw_fd(stream) };
std::process::exit(http::handler(stream));
}

View File

@ -1,114 +0,0 @@
{
"entrypoints": {
"connection_listener": {
"args": [
"Entrypoint",
{
"FileSocket": {
"Tx": "tls"
}
},
{
"TcpListener": {
"addr": "0.0.0.0:8443"
}
}
],
"environment": [
{
"Filesystem": {
"host_path": "/lib/x86_64-linux-gnu/libgcc_s.so.1",
"environment_path": "/lib/libgcc_s.so.1"
}
},
{
"Filesystem": {
"host_path": "/lib/x86_64-linux-gnu/libc.so.6",
"environment_path": "/lib/libc.so.6"
}
},
{
"Filesystem": {
"host_path": "/lib64/ld-linux-x86-64.so.2",
"environment_path": "/lib64/ld-linux-x86-64.so.2"
}
}
]
},
"tls_handler": {
"trigger": {
"FileSocket": "tls"
},
"args": [
"Entrypoint",
{
"FileSocket": {
"Tx": "http"
}
},
{
"File": "/etc/ssl/certs/example.com.pem"
},
{
"File": "/etc/ssl/private/example.com.key"
},
"Trigger"
],
"environment": [
{
"Filesystem": {
"host_path": "/lib/x86_64-linux-gnu/libgcc_s.so.1",
"environment_path": "/lib/libgcc_s.so.1"
}
},
{
"Filesystem": {
"host_path": "/lib/x86_64-linux-gnu/libc.so.6",
"environment_path": "/lib/libc.so.6"
}
},
{
"Filesystem": {
"host_path": "/lib64/ld-linux-x86-64.so.2",
"environment_path": "/lib64/ld-linux-x86-64.so.2"
}
}
]
},
"http_handler": {
"trigger": {
"FileSocket": "http"
},
"args": [
"Entrypoint",
"Trigger"
],
"environment": [
{
"Filesystem": {
"host_path": "/var/www/html",
"environment_path": "/var/www/html"
}
},
{
"Filesystem": {
"host_path": "/lib/x86_64-linux-gnu/libgcc_s.so.1",
"environment_path": "/lib/libgcc_s.so.1"
}
},
{
"Filesystem": {
"host_path": "/lib/x86_64-linux-gnu/libc.so.6",
"environment_path": "/lib/libc.so.6"
}
},
{
"Filesystem": {
"host_path": "/lib64/ld-linux-x86-64.so.2",
"environment_path": "/lib64/ld-linux-x86-64.so.2"
}
}
]
}
}
}

View File

@ -1,176 +0,0 @@
use std::fs::File;
use std::io::{self, BufReader, ErrorKind, Read, Write};
use std::net::TcpStream;
use std::os::unix::io::AsRawFd;
use std::os::unix::net::UnixStream;
use std::sync::Arc;
use nix::poll::{poll, PollFd, PollFlags};
use rustls::ServerConnection;
use anyhow::Context;
const BUFFER_SIZE: usize = 4096;
pub(crate) fn handler(
http_trigger_socket: File,
cert: File,
key: File,
mut stream: TcpStream,
) -> i32 {
let (mut socket, far_socket) = UnixStream::pair().unwrap();
let config = make_config(cert, key);
let mut tls_conn = rustls::ServerConnection::new(config).unwrap();
super::http_handler(&http_trigger_socket, far_socket);
stream.set_nonblocking(true).unwrap();
socket.set_nonblocking(true).unwrap();
let mut to_poll = [
PollFd::new(stream.as_raw_fd(), PollFlags::POLLIN),
PollFd::new(socket.as_raw_fd(), PollFlags::POLLIN),
];
loop {
println!("starting polling");
poll(&mut to_poll, -1).unwrap();
if let Some(events) = to_poll[0].revents() {
if events.contains(PollFlags::POLLIN) {
handle_encrypted_data(&mut tls_conn, &mut stream, &mut socket).unwrap();
}
}
if let Some(events) = to_poll[1].revents() {
if events.contains(PollFlags::POLLIN) {
handle_new_data(&mut tls_conn, &mut socket, &mut stream).unwrap();
}
if events.contains(PollFlags::POLLHUP) {
println!("response writer hung up, exiting");
break;
}
}
}
tls_conn.send_close_notify();
tls_conn.write_tls(&mut stream).unwrap();
exitcode::OK
}
fn handle_encrypted_data(
tls_conn: &mut ServerConnection,
stream: &mut (impl Read + Write),
socket: &mut impl Write,
) -> anyhow::Result<()> {
println!("handling newly received encrypted data");
loop {
let read = match tls_conn.read_tls(stream) {
Err(e) => {
if e.kind() == ErrorKind::WouldBlock {
0
} else {
return Err(e).context("io error reading from stream");
}
}
Ok(n) => n,
};
if read == 0 {
return Ok(());
}
let process_result = tls_conn.process_new_packets();
let write_tls_result = tls_conn.write_tls(stream);
let io_state = process_result.context("tls processing failure")?;
write_tls_result.context("tls write failure")?;
if io_state.plaintext_bytes_to_read() > 0 {
let mut reader = tls_conn
.reader()
.take(io_state.plaintext_bytes_to_read() as u64);
std::io::copy(&mut reader, socket)?;
}
}
}
fn handle_new_data(
tls_conn: &mut ServerConnection,
socket: &mut impl Read,
stream: &mut impl Write,
) -> anyhow::Result<()> {
println!("handling new data to encrypt");
let mut buf = [0_u8; BUFFER_SIZE];
loop {
let read = non_blocking_read(socket, &mut buf)?;
if read == 0 {
return Ok(());
}
tls_conn.writer().write_all(&buf[0..read])?;
tls_conn.write_tls(stream)?;
}
}
fn non_blocking_read(reader: &mut impl io::Read, buf: &mut [u8]) -> io::Result<usize> {
match reader.read(buf) {
Err(e) => {
if e.kind() == ErrorKind::WouldBlock {
Ok(0)
} else {
Err(e)
}
}
Ok(n) => Ok(n),
}
}
fn make_config(cert: File, key: File) -> Arc<rustls::ServerConfig> {
let certs = load_certs(cert);
let privkey = load_private_key(key);
let config = rustls::ServerConfig::builder()
.with_safe_default_cipher_suites()
.with_safe_default_kx_groups()
.with_safe_default_protocol_versions()
.expect("inconsistent cipher-suites/versions specified")
.with_no_client_auth()
.with_single_cert(certs, privkey)
.expect("bad certificates/private key");
Arc::new(config)
}
fn load_certs(certfile: File) -> Vec<rustls::Certificate> {
let mut reader = BufReader::new(certfile);
rustls_pemfile::certs(&mut reader)
.unwrap()
.iter()
.map(|v| rustls::Certificate(v.clone()))
.collect()
}
fn load_private_key(keyfile: File) -> rustls::PrivateKey {
let mut reader = BufReader::new(keyfile);
loop {
match rustls_pemfile::read_one(&mut reader).expect("cannot parse private key .pem file") {
Some(rustls_pemfile::Item::RSAKey(key)) => return rustls::PrivateKey(key),
Some(rustls_pemfile::Item::PKCS8Key(key)) => return rustls::PrivateKey(key),
Some(rustls_pemfile::Item::ECKey(key)) => return rustls::PrivateKey(key),
None => break,
_ => {}
}
}
panic!("no keys found (encrypted keys not supported)");
}

View File

@ -15,13 +15,25 @@ pub enum Error {
#[error("json: {0}")] #[error("json: {0}")]
Json(#[from] serde_json::Error), Json(#[from] serde_json::Error),
#[error("bincode: {0}")]
Bincode(#[from] bincode::Error),
#[error("elf: read: {0}")]
ElfRead(#[from] object::read::Error),
#[error("elf: write: {0}")]
ElfWrite(#[from] object::write::Error),
#[error("bad pipe specification: a pipe must have exactly one reader and one writer: {0}")] #[error("bad pipe specification: a pipe must have exactly one reader and one writer: {0}")]
BadPipe(String), BadPipe(String),
#[error("bad socket specification: a socket must have exactly one reader and one or more writers: {0}")] #[error("bad socket specification: a socket must have exactly one reader and one writer: {0}")]
BadFileSocket(String), BadFileSocket(String),
#[error("bad specification type: only .json files are supported")] #[error("no specification provided")]
NoSpecification,
#[error("bad specification type: only json files are supported")]
BadSpecType, BadSpecType,
#[error("bad trigger argument: this entrypoint is not triggered by something with arguments")] #[error("bad trigger argument: this entrypoint is not triggered by something with arguments")]

View File

@ -2,39 +2,51 @@ use log::{debug, info};
pub mod clone; pub mod clone;
mod error; mod error;
mod pack;
mod spawner; mod spawner;
mod specification; mod specification;
mod void; mod void;
use error::{Error, Result}; use error::{Error, Result};
use spawner::Spawner; use spawner::Spawner;
use specification::{Environment, Specification}; use specification::Specification;
use std::collections::HashMap; use std::collections::HashMap;
use std::fs::File; use std::fs::File;
use std::os::unix::io::{AsRawFd, FromRawFd}; use std::os::unix::io::FromRawFd;
use std::path::Path; use std::path::Path;
use nix::fcntl::OFlag; use nix::fcntl::OFlag;
use nix::sys::socket; use nix::sys::socket;
use nix::sys::wait::{waitid, Id, WaitPidFlag, WaitStatus};
use nix::unistd; use nix::unistd;
pub struct PackArgs<'a> {
pub spec: &'a Path,
pub binary: &'a Path,
pub output: &'a Path,
}
pub fn pack(args: &PackArgs) -> Result<()> {
let spec: Specification = if args.spec.extension().map(|e| e == "json") == Some(true) {
let f = std::fs::File::open(args.spec)?;
Ok(serde_json::from_reader(f)?)
} else {
Err(Error::BadSpecType)
}?;
pack::pack_binary(args.binary, &spec, args.output)
}
pub struct RunArgs<'a> { pub struct RunArgs<'a> {
pub spec: Option<&'a Path>, pub spec: Option<&'a Path>,
pub debug: bool, pub debug: bool,
pub daemon: bool,
pub stdout: bool,
pub stderr: bool,
pub binary: &'a Path, pub binary: &'a Path,
pub binary_args: Vec<&'a str>, pub binary_args: Vec<&'a str>,
} }
pub fn run(args: &RunArgs) -> Result<i32> { pub fn run(args: &RunArgs) -> Result<()> {
// parse the specification // parse the specification
let mut spec: Specification = if let Some(m) = args.spec { let spec: Specification = if let Some(m) = args.spec {
if m.extension().map(|e| e == "json") == Some(true) { if m.extension().map(|e| e == "json") == Some(true) {
let f = std::fs::File::open(m)?; let f = std::fs::File::open(m)?;
Ok(serde_json::from_reader(f)?) Ok(serde_json::from_reader(f)?)
@ -42,26 +54,17 @@ pub fn run(args: &RunArgs) -> Result<i32> {
Err(Error::BadSpecType) Err(Error::BadSpecType)
} }
} else { } else {
unimplemented!("reading spec from the elf is unimplemented") let spec = pack::extract_specification(args.binary)?;
if let Some(s) = spec {
Ok(s)
} else {
Err(Error::NoSpecification)
}
}?; }?;
debug!("specification read: {:?}", &spec); debug!("specification read: {:?}", &spec);
spec.validate()?; spec.validate()?;
if args.stdout {
debug!("forwarding stdout");
for entrypoint in &mut spec.entrypoints.values_mut() {
entrypoint.environment.insert(Environment::Stdout);
}
}
if args.stderr {
debug!("forwarding stderr");
for entrypoint in &mut spec.entrypoints.values_mut() {
entrypoint.environment.insert(Environment::Stderr);
}
}
// create all the pipes // create all the pipes
let (pipes, _) = spec.pipes(); let (pipes, _) = spec.pipes();
let pipes = create_pipes(pipes)?; let pipes = create_pipes(pipes)?;
@ -81,41 +84,7 @@ pub fn run(args: &RunArgs) -> Result<i32> {
} }
.spawn()?; .spawn()?;
if args.daemon { Ok(())
return Ok(exitcode::OK);
}
info!("spawned successfully, awaiting children exiting...");
let mut exit_code = exitcode::OK;
loop {
let status = match waitid(Id::All, WaitPidFlag::WEXITED) {
Ok(v) => Ok(v),
Err(nix::Error::ECHILD) => {
info!("all child processes have exited, exiting...");
break;
}
Err(e) => Err(Error::Nix {
msg: "waitpid",
src: e,
}),
}?;
match status {
WaitStatus::Exited(pid, code) => {
if code != exitcode::OK {
exit_code = code;
}
debug!("child {} exited with code {}", pid, code);
}
WaitStatus::Signaled(pid, sig, _coredump) => {
debug!("child {} was terminated with signal {}", pid, sig);
}
_ => unreachable!(),
}
}
Ok(exit_code)
} }
fn create_pipes(names: Vec<&str>) -> Result<HashMap<String, PipePair>> { fn create_pipes(names: Vec<&str>) -> Result<HashMap<String, PipePair>> {
@ -152,11 +121,10 @@ impl PipePair {
src: e, src: e,
})?; })?;
// safe to create files given the successful return of pipe(2)
Ok(PipePair { Ok(PipePair {
name: name.to_string(), name: name.to_string(),
// SAFETY: valid new fd as pipe2(2) returned successfully
read: Some(unsafe { File::from_raw_fd(read) }), read: Some(unsafe { File::from_raw_fd(read) }),
// SAFETY: valid new fd as pipe2(2) returned successfully
write: Some(unsafe { File::from_raw_fd(write) }), write: Some(unsafe { File::from_raw_fd(write) }),
}) })
} }
@ -178,7 +146,7 @@ pub struct SocketPair {
name: String, name: String,
read: Option<File>, read: Option<File>,
write: File, write: Option<File>,
} }
impl SocketPair { impl SocketPair {
@ -194,30 +162,23 @@ impl SocketPair {
src: e, src: e,
})?; })?;
// safe to create files given the successful return of socketpair(2)
Ok(SocketPair { Ok(SocketPair {
name: name.to_string(), name: name.to_string(),
// SAFETY: valid new fd as socketpair(2) returned successfully
read: Some(unsafe { File::from_raw_fd(read) }), read: Some(unsafe { File::from_raw_fd(read) }),
// SAFETY: valid new fd as socketpair(2) returned successfully write: Some(unsafe { File::from_raw_fd(write) }),
write: unsafe { File::from_raw_fd(write) },
}) })
} }
fn take_read(&mut self) -> Result<File> { fn take_read(&mut self) -> Result<File> {
self.read self.read
.take() .take()
.ok_or_else(|| Error::BadFileSocket(self.name.to_string())) .ok_or_else(|| Error::BadPipe(self.name.to_string()))
} }
fn write(&self) -> Result<File> { fn take_write(&mut self) -> Result<File> {
let dup_fd = nix::unistd::dup(self.write.as_raw_fd()) self.write
.map_err(|e| Error::Nix { msg: "dup", src: e })?; .take()
.ok_or_else(|| Error::BadPipe(self.name.to_string()))
// SAFETY: valid new fd as dup(2) returned successfully
Ok(unsafe { File::from_raw_fd(dup_fd) })
}
fn write_ref(&self) -> &File {
&self.write
} }
} }

View File

@ -1,6 +1,6 @@
use log::error; use log::{error, info};
use void_orchestrator::{run, RunArgs}; use clone_shim::{pack, run, PackArgs, RunArgs};
use std::path::Path; use std::path::Path;
@ -12,12 +12,39 @@ fn main() {
.version(env!("GIT_HASH")) .version(env!("GIT_HASH"))
.author("Jake Hillion <jake@hillion.co.uk>") .author("Jake Hillion <jake@hillion.co.uk>")
.about("Launch a void process application.") .about("Launch a void process application.")
.subcommand_negates_reqs(true)
.trailing_var_arg(true) .trailing_var_arg(true)
.subcommand(
Command::new("pack")
.arg(
Arg::new("spec")
.long("specification")
.short('s')
.help("Provide the specification to pack as an external JSON file.")
.takes_value(true)
.required(true),
)
.arg(
Arg::new("binary")
.long("binary")
.short('b')
.help("Provide the binary to pack.")
.takes_value(true)
.required(true),
)
.arg(
Arg::new("output")
.long("out")
.short('o')
.help("Location of the output file")
.takes_value(true),
),
)
.arg( .arg(
Arg::new("spec") Arg::new("spec")
.long("specification") .long("specification")
.short('s') .short('s')
.help("Provide the specification as an external JSON file.") .help("Provide the specification to launch as an external JSON file.")
.takes_value(true), .takes_value(true),
) )
.arg( .arg(
@ -34,25 +61,6 @@ fn main() {
.help("Stop each spawned application process so that it can be attached to.") .help("Stop each spawned application process so that it can be attached to.")
.takes_value(false), .takes_value(false),
) )
.arg(
Arg::new("daemon")
.long("daemon")
.short('D')
.help("Detach the shim from all child processes and exit immediately.")
.takes_value(false),
)
.arg(
Arg::new("stdout")
.long("stdout")
.help("Allow all spawned processes access to stdout (useful for debugging).")
.takes_value(false),
)
.arg(
Arg::new("stderr")
.long("stderr")
.help("Allow all spawned processes access to stderr (useful for debugging).")
.takes_value(false),
)
.arg( .arg(
Arg::new("binary") Arg::new("binary")
.index(1) .index(1)
@ -74,8 +82,30 @@ fn main() {
env_logger::init_from_env(env); env_logger::init_from_env(env);
// launch process // launch process
// execute shimmed process
std::process::exit({ let code = if let Some(matches) = matches.subcommand_matches("pack") {
// execute binary packing procedure
let args = PackArgs {
spec: Path::new(matches.value_of("spec").expect("spec required")),
binary: Path::new(matches.value_of("binary").expect("binary required")),
output: matches
.value_of("output")
.map(Path::new)
.unwrap_or_else(|| Path::new("a.out")),
};
match pack(&args) {
Ok(_) => {
info!("binary packed successfully");
exitcode::OK
}
Err(e) => {
error!("error packing binary: {}", e);
1
}
}
} else {
// execute shimmed process
let (binary, binary_args) = { let (binary, binary_args) = {
let mut argv = matches.values_of("binary").unwrap(); let mut argv = matches.values_of("binary").unwrap();
@ -88,21 +118,21 @@ fn main() {
let args = RunArgs { let args = RunArgs {
spec: matches.value_of("spec").map(Path::new), spec: matches.value_of("spec").map(Path::new),
debug: matches.is_present("debug"), debug: matches.is_present("debug"),
daemon: matches.is_present("daemon"),
stdout: matches.is_present("stdout"),
stderr: matches.is_present("stderr"),
binary, binary,
binary_args, binary_args,
}; };
match run(&args) { match run(&args) {
Ok(code) => code, Ok(_) => {
info!("launched successfully");
exitcode::OK
}
Err(e) => { Err(e) => {
error!("error: {}", e); error!("error: {}", e);
-1 -1
} }
} }
}) };
std::process::exit(code);
} }

75
src/pack.rs Normal file
View File

@ -0,0 +1,75 @@
use crate::{Result, Specification};
use std::fs::File;
use std::path::Path;
use bincode::Options;
use object::endian::Endianness;
use object::read::ReadCache;
use object::read::{Object, ObjectSection};
use object::write::{StandardSegment, StreamingBuffer};
use object::SectionKind;
const SPECIFICATION_SECTION_NAME: &str = "void_specification";
pub(crate) fn pack_binary(binary: &Path, spec: &Specification, output: &Path) -> Result<()> {
let binary = File::open(binary)?;
let binary = ReadCache::new(binary);
let output = File::create(output)?;
let mut output = StreamingBuffer::new(output);
let input_object = object::File::parse(&binary)?;
let format = input_object.format();
let architecture = input_object.architecture();
let endianness = if input_object.is_little_endian() {
Endianness::Little
} else {
Endianness::Big
};
let mut output_object = object::write::Object::new(format, architecture, endianness);
for input_section in input_object.sections() {
let output_section = output_object.add_section(
input_section.segment_name_bytes()?.unwrap_or(&[]).to_vec(),
input_section.name_bytes()?.to_vec(),
input_section.kind(),
);
output_object.set_section_data(output_section, input_section.data()?, 0);
}
let spec_section = output_object.add_section(
output_object.segment_name(StandardSegment::Debug).to_vec(),
SPECIFICATION_SECTION_NAME.to_string().into(),
SectionKind::Other,
);
let spec = bincode_options().serialize(spec)?;
output_object.set_section_data(spec_section, spec, 0);
output_object.emit(&mut output)?;
Ok(())
}
pub(crate) fn extract_specification(binary: &Path) -> Result<Option<Specification>> {
let binary = File::open(binary)?;
let binary = ReadCache::new(binary);
let input_object = object::File::parse(&binary)?;
let spec_section = if let Some(s) = input_object.section_by_name(SPECIFICATION_SECTION_NAME) {
s
} else {
return Ok(None);
};
let spec_data = spec_section.data()?;
Ok(Some(bincode_options().deserialize(spec_data)?))
}
fn bincode_options() -> impl bincode::Options {
bincode::DefaultOptions::new()
}

View File

@ -1,6 +1,4 @@
use log::info; use super::{Spawner, TriggerData};
use super::{RpcHandler, Spawner, TriggerData};
use crate::specification::{Arg, FileSocket, Pipe}; use crate::specification::{Arg, FileSocket, Pipe};
use crate::void::VoidBuilder; use crate::void::VoidBuilder;
use crate::{Error, Result}; use crate::{Error, Result};
@ -9,10 +7,7 @@ use std::ffi::CString;
use std::fs::File; use std::fs::File;
use std::net::TcpListener; use std::net::TcpListener;
use std::os::unix::ffi::OsStrExt; use std::os::unix::ffi::OsStrExt;
use std::os::unix::io::{FromRawFd, IntoRawFd}; use std::os::unix::io::IntoRawFd;
use nix::sys::socket;
use nix::unistd::{fork, ForkResult};
pub struct PreparedArgs(Vec<PreparedArg>); pub struct PreparedArgs(Vec<PreparedArg>);
@ -42,15 +37,11 @@ impl PreparedArgs {
* for things like network sockets. update the builder * for things like network sockets. update the builder
* with newly passed fds. * with newly passed fds.
*/ */
pub fn prepare_ambient( pub fn prepare_ambient(builder: &mut VoidBuilder, args: &[Arg]) -> Result<Self> {
spawner: &Spawner,
builder: &mut VoidBuilder,
args: &[Arg],
) -> Result<Self> {
let mut v = Vec::with_capacity(args.len()); let mut v = Vec::with_capacity(args.len());
for arg in args { for arg in args {
v.push(PreparedArg::prepare_ambient(spawner, builder, arg)?); v.push(PreparedArg::prepare_ambient(builder, arg)?);
} }
Ok(PreparedArgs(v)) Ok(PreparedArgs(v))
@ -94,9 +85,6 @@ enum PreparedArg {
/// A TCP Listener /// A TCP Listener
TcpListener { socket: TcpListener }, TcpListener { socket: TcpListener },
/// RPC
Rpc { socket: File },
/// The rest of argv[1..], 0 or more arguments /// The rest of argv[1..], 0 or more arguments
Trailing, Trailing,
} }
@ -125,28 +113,24 @@ impl PreparedArg {
PreparedArg::Pipe(pipe) PreparedArg::Pipe(pipe)
} }
Arg::FileSocket(FileSocket::Rx(s)) => { Arg::FileSocket(s) => {
let socket = spawner.sockets.get_mut(s).unwrap().take_read()?; let socket = match s {
FileSocket::Rx(s) => spawner.sockets.get_mut(s).unwrap().take_read(),
FileSocket::Tx(s) => spawner.sockets.get_mut(s).unwrap().take_write(),
}?;
builder.keep_fd(&socket); builder.keep_fd(&socket);
PreparedArg::FileSocket(socket) PreparedArg::FileSocket(socket)
} }
arg => Self::prepare_ambient(spawner, builder, arg)?, arg => Self::prepare_ambient(builder, arg)?,
}) })
} }
fn prepare_ambient(spawner: &Spawner, builder: &mut VoidBuilder, arg: &Arg) -> Result<Self> { fn prepare_ambient(builder: &mut VoidBuilder, arg: &Arg) -> Result<Self> {
Ok(match arg { Ok(match arg {
Arg::Pipe(p) => return Err(Error::BadPipe(p.get_name().to_string())), Arg::Pipe(p) => return Err(Error::BadPipe(p.get_name().to_string())),
Arg::FileSocket(FileSocket::Rx(s)) => return Err(Error::BadFileSocket(s.to_string())), Arg::FileSocket(s) => return Err(Error::BadFileSocket(s.get_name().to_string())),
Arg::FileSocket(FileSocket::Tx(s)) => {
let socket = spawner.sockets.get(s).unwrap().write()?;
builder.keep_fd(&socket);
PreparedArg::FileSocket(socket)
}
Arg::File(path) => { Arg::File(path) => {
let fd = File::open(path)?; let fd = File::open(path)?;
@ -162,45 +146,6 @@ impl PreparedArg {
PreparedArg::TcpListener { socket } PreparedArg::TcpListener { socket }
} }
Arg::Rpc(specs) => {
let (ambient, void) = socket::socketpair(
socket::AddressFamily::Unix,
socket::SockType::Datagram,
None,
socket::SockFlag::empty(),
)
.map_err(|e| Error::Nix {
msg: "socketpair",
src: e,
})?;
// SAFETY: valid new fd as socketpair(2) returned successfully
let ambient = unsafe { File::from_raw_fd(ambient) };
// SAFETY: valid new fd as socketpair(2) returned successfully
let void = unsafe { File::from_raw_fd(void) };
// spawn this child with ambient authority
// necessary as no void ever has outgoing network capability
// SAFETY: this program is single-threaded so no safety issue
let child = unsafe { fork() }.map_err(|e| Error::Nix {
msg: "fork",
src: e,
})?;
match child {
ForkResult::Child => {
let handler = RpcHandler::new(specs);
handler.handle(ambient).unwrap();
}
ForkResult::Parent { child } => {
info!("spawned rpc handler with pid {}", child);
}
};
// SAFETY: safe as socketpair returned successfully
PreparedArg::Rpc { socket: void }
}
Arg::BinaryName => PreparedArg::BinaryName, Arg::BinaryName => PreparedArg::BinaryName,
Arg::Entrypoint => PreparedArg::Entrypoint, Arg::Entrypoint => PreparedArg::Entrypoint,
Arg::Trigger => PreparedArg::Trigger, Arg::Trigger => PreparedArg::Trigger,
@ -238,10 +183,6 @@ impl PreparedArg {
Ok(vec![CString::new(socket.into_raw_fd().to_string()).unwrap()]) Ok(vec![CString::new(socket.into_raw_fd().to_string()).unwrap()])
} }
PreparedArg::Rpc { socket } => {
Ok(vec![CString::new(socket.into_raw_fd().to_string()).unwrap()])
}
PreparedArg::Trailing => Ok(spawner PreparedArg::Trailing => Ok(spawner
.binary_args .binary_args
.iter() .iter()

View File

@ -1,12 +1,10 @@
use log::{debug, error, info}; use log::{debug, error, info};
mod args; mod args;
mod rpc;
use args::PreparedArgs; use args::PreparedArgs;
use rpc::RpcHandler;
use crate::specification::{Arg, Entrypoint, Environment, Specification, Trigger}; use crate::specification::{Entrypoint, Environment, Specification, Trigger};
use crate::void::VoidBuilder; use crate::void::VoidBuilder;
use crate::{Error, Result}; use crate::{Error, Result};
use crate::{PipePair, SocketPair}; use crate::{PipePair, SocketPair};
@ -15,17 +13,14 @@ use std::collections::HashMap;
use std::ffi::CString; use std::ffi::CString;
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd, RawFd}; use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use nix::sys::signal::{kill, Signal}; use nix::sys::signal::{kill, Signal};
use nix::sys::socket::{recvmsg, ControlMessageOwned, MsgFlags}; use nix::sys::socket::{recvmsg, ControlMessageOwned, MsgFlags};
use nix::sys::wait::{waitid, Id, WaitPidFlag, WaitStatus}; use nix::unistd::{self, Pid};
use nix::unistd::{self, fork, ForkResult, Pid};
use nix::Error as NixError;
const BUFFER_SIZE: usize = 1024; const BUFFER_SIZE: usize = 1024;
const MAX_FILE_DESCRIPTORS: usize = 16;
pub struct Spawner<'a> { pub struct Spawner<'a> {
pub spec: &'a Specification, pub spec: &'a Specification,
@ -69,7 +64,10 @@ impl<'a> Spawner<'a> {
match &entrypoint.trigger { match &entrypoint.trigger {
Trigger::Startup => { Trigger::Startup => {
let mut builder = VoidBuilder::new(); let mut builder = VoidBuilder::new();
self.mount_entrypoint(&mut builder, self.binary)?;
let binary = PathBuf::from(self.binary).canonicalize()?;
builder.mount(binary, "/entrypoint");
self.prepare_env(&mut builder, &entrypoint.environment); self.prepare_env(&mut builder, &entrypoint.environment);
let args = let args =
@ -102,46 +100,70 @@ impl<'a> Spawner<'a> {
} }
Trigger::Pipe(s) => { Trigger::Pipe(s) => {
let mut builder = VoidBuilder::new();
self.prepare_spawner(&mut builder, &entrypoint.environment, &entrypoint.args)?;
let pipe = self.pipes.get_mut(s).unwrap().take_read()?; let pipe = self.pipes.get_mut(s).unwrap().take_read()?;
let binary = PathBuf::from(self.binary).canonicalize()?;
let mut builder = VoidBuilder::new();
builder.mount(binary, "/entrypoint");
builder.keep_fd(&pipe); builder.keep_fd(&pipe);
self.prepare_env(&mut builder, &entrypoint.environment);
for env in &entrypoint.environment {
if let Environment::Filesystem {
host_path,
environment_path: _,
} = env
{
builder.mount(host_path, host_path);
}
}
let closure = || match self.pipe_trigger(pipe, entrypoint, name) { let closure = || match self.pipe_trigger(pipe, entrypoint, name) {
Ok(()) => exitcode::OK, Ok(()) => std::process::exit(exitcode::OK),
Err(e) => { Err(e) => {
error!("error in pipe_trigger: {}", e); error!("error in pipe_trigger: {}", e);
1 std::process::exit(1)
} }
}; };
let void = builder.spawn(closure)?; let void = builder.spawn(closure)?;
info!( info!(
"spawned pipe trigger for entrypoint `{}` as {}", "prepared pipe trigger for entrypoint `{}` as {}",
name.as_str(), name.as_str(),
void void
); );
} }
Trigger::FileSocket(s) => { Trigger::FileSocket(s) => {
let mut builder = VoidBuilder::new();
self.prepare_spawner(&mut builder, &entrypoint.environment, &entrypoint.args)?;
let socket = self.sockets.get_mut(s).unwrap().take_read()?; let socket = self.sockets.get_mut(s).unwrap().take_read()?;
let binary = PathBuf::from(self.binary).canonicalize()?;
let mut builder = VoidBuilder::new();
builder.mount(binary, "/entrypoint");
builder.keep_fd(&socket); builder.keep_fd(&socket);
for env in &entrypoint.environment {
if let Environment::Filesystem {
host_path,
environment_path: _,
} = env
{
builder.mount(host_path, host_path);
}
}
let closure = || match self.file_socket_trigger(socket, entrypoint, name) { let closure = || match self.file_socket_trigger(socket, entrypoint, name) {
Ok(()) => exitcode::OK, Ok(()) => std::process::exit(exitcode::OK),
Err(e) => { Err(e) => {
error!("error in file_socket_trigger: {}", e); error!("error in file_socket_trigger: {}", e);
1 std::process::exit(1)
} }
}; };
let void = builder.spawn(closure)?; let void = builder.spawn(closure)?;
info!( info!(
"spawned socket trigger for entrypoint `{}` as {}", "prepared socket trigger for entrypoint `{}` as {}",
name.as_str(), name.as_str(),
void void
); );
@ -153,21 +175,9 @@ impl<'a> Spawner<'a> {
} }
fn pipe_trigger(&self, mut pipe: File, spec: &Entrypoint, name: &str) -> Result<()> { fn pipe_trigger(&self, mut pipe: File, spec: &Entrypoint, name: &str) -> Result<()> {
// put the work in a forked process that can handle signals
Self::fork_for_trigger()?;
let mut buf = [0_u8; BUFFER_SIZE]; let mut buf = [0_u8; BUFFER_SIZE];
loop { loop {
let read_bytes = match pipe.read(&mut buf) { let read_bytes = pipe.read(&mut buf)?;
Ok(n) => Ok(n),
Err(e) => {
if e.kind() == std::io::ErrorKind::Interrupted {
return Ok(());
}
Err(e)
}
}?;
if read_bytes == 0 { if read_bytes == 0 {
return Ok(()); return Ok(());
} }
@ -179,7 +189,7 @@ impl<'a> Spawner<'a> {
self.prepare_env(&mut builder, &spec.environment); self.prepare_env(&mut builder, &spec.environment);
let args = PreparedArgs::prepare_ambient(self, &mut builder, &spec.args)?; let args = PreparedArgs::prepare_ambient(&mut builder, &spec.args)?;
let closure = let closure =
|| { || {
@ -206,36 +216,18 @@ impl<'a> Spawner<'a> {
} }
}; };
let void = builder.spawn(closure)?; builder.spawn(closure)?;
info!("spawned entrypoint `{}` as {}", name, void);
} }
} }
fn file_socket_trigger(&self, socket: File, spec: &Entrypoint, name: &str) -> Result<()> { fn file_socket_trigger(&self, socket: File, spec: &Entrypoint, name: &str) -> Result<()> {
// put the work in a forked process that can handle signals
Self::fork_for_trigger()?;
let mut cmsg_buf = nix::cmsg_space!([RawFd; MAX_FILE_DESCRIPTORS]);
loop { loop {
let msg = match recvmsg::<()>( let msg = recvmsg(socket.as_raw_fd(), &[], None, MsgFlags::empty()).map_err(|e| {
socket.as_raw_fd(), Error::Nix {
&mut [], msg: "recvmsg",
Some(&mut cmsg_buf), src: e,
MsgFlags::empty(),
) {
Ok(m) => Ok(m),
Err(e) => {
if e == NixError::EINTR {
return Ok(());
}
Err(Error::Nix {
msg: "recvmsg",
src: e,
})
} }
}?; })?;
debug!("triggering from socket recvmsg"); debug!("triggering from socket recvmsg");
@ -255,7 +247,7 @@ impl<'a> Spawner<'a> {
self.prepare_env(&mut builder, &spec.environment); self.prepare_env(&mut builder, &spec.environment);
let args = PreparedArgs::prepare_ambient(self, &mut builder, &spec.args)?; let args = PreparedArgs::prepare_ambient(&mut builder, &spec.args)?;
let closure = || { let closure = || {
if self.debug { if self.debug {
@ -281,8 +273,7 @@ impl<'a> Spawner<'a> {
} }
}; };
let void = builder.spawn(closure)?; builder.spawn(closure)?;
info!("spawned entrypoint `{}` as {}", name, void);
} }
_ => unimplemented!(), _ => unimplemented!(),
} }
@ -290,39 +281,11 @@ impl<'a> Spawner<'a> {
} }
} }
fn fork_for_trigger() -> Result<()> {
// SAFETY: only unsafe in a multi-threaded program
if let ForkResult::Parent { child: _pid } = unsafe { fork() }.map_err(|e| Error::Nix {
msg: "fork",
src: e,
})? {
let status = waitid(Id::All, WaitPidFlag::WEXITED).map_err(|e| Error::Nix {
msg: "waitpid",
src: e,
})?;
match status {
WaitStatus::Exited(_pid, code) => {
std::process::exit(code);
}
WaitStatus::Signaled(pid, sig, _coredump) => {
debug!(
"trigger: forked child {} was terminated with signal {}",
pid, sig
);
std::process::exit(-1);
}
_ => unreachable!(),
}
}
Ok(())
}
fn stop_self(name: &str) -> Result<()> { fn stop_self(name: &str) -> Result<()> {
let pid = Pid::this();
info!("stopping process `{}`", name); info!("stopping process `{}`", name);
kill(Pid::this(), Signal::SIGSTOP).map_err(|e| Error::Nix { kill(pid, Signal::SIGSTOP).map_err(|e| Error::Nix {
msg: "kill", msg: "kill",
src: e, src: e,
})?; })?;
@ -331,67 +294,6 @@ impl<'a> Spawner<'a> {
Ok(()) Ok(())
} }
fn mount_entrypoint(&self, builder: &mut VoidBuilder, binary: &Path) -> Result<()> {
let binary = PathBuf::from(binary).canonicalize()?;
builder.mount(binary, "/entrypoint");
Ok(())
}
fn forward_mounts<'b>(
&self,
builder: &mut VoidBuilder,
environment: impl IntoIterator<Item = &'b Environment>,
arguments: impl IntoIterator<Item = &'b Arg>,
) {
for env in environment {
if let Environment::Filesystem {
host_path,
environment_path: _,
} = env
{
builder.mount(host_path, host_path);
}
}
for arg in arguments {
if let Arg::File(host_path) = arg {
builder.mount(host_path, host_path);
}
}
}
fn forward_files<'b>(
&self,
builder: &mut VoidBuilder,
arguments: impl IntoIterator<Item = &'b Arg>,
) {
for arg in arguments {
if let Arg::FileSocket(socket) = arg {
builder.keep_fd(self.sockets.get(socket.get_name()).unwrap().write_ref());
}
}
}
fn prepare_spawner<'b>(
&self,
builder: &mut VoidBuilder,
environment: impl IntoIterator<Item = &'b Environment>,
args: impl IntoIterator<Item = &'b Arg> + Copy,
) -> Result<()> {
self.mount_entrypoint(builder, self.binary)?;
self.forward_mounts(builder, environment, args);
self.forward_files(builder, args);
builder.mount("/dev/null", "/dev/null");
builder.mount("/proc", "/proc").remount_proc();
builder.keep_fd(&1);
builder.keep_fd(&2);
Ok(())
}
fn prepare_env<'b>( fn prepare_env<'b>(
&self, &self,
builder: &mut VoidBuilder, builder: &mut VoidBuilder,
@ -412,20 +314,6 @@ impl<'a> Spawner<'a> {
Environment::DomainName(name) => { Environment::DomainName(name) => {
builder.set_domain_name(name); builder.set_domain_name(name);
} }
Environment::Procfs => {
builder.mount("/proc", "/proc").remount_proc();
}
Environment::Stdin => {
builder.keep_fd(&0);
}
Environment::Stdout => {
builder.keep_fd(&1);
}
Environment::Stderr => {
builder.keep_fd(&2);
}
} }
} }
} }

View File

@ -1,300 +0,0 @@
use log::{debug, error};
use crate::specification::{AddressFamily as SpecAddressFamily, RpcSpecification};
use crate::Error;
use std::ffi::CStr;
use std::fs::File;
use std::net::{TcpStream, UdpSocket};
use std::os::raw::c_char;
use std::os::unix::io::AsRawFd;
use nix::sys::socket::AddressFamily;
use nix::sys::socket::{recv, send, sendmsg, ControlMessage, MsgFlags};
const MAX_MSG_LENGTH: usize = 4096;
pub struct RpcHandler<'a> {
permitted_rpcs: &'a [RpcSpecification],
}
impl<'a> RpcHandler<'a> {
pub(super) fn new(permitted_rpcs: &'a [RpcSpecification]) -> Self {
Self { permitted_rpcs }
}
pub(super) fn handle(&self, socket: File) -> Result<(), Error> {
let mut buf = vec![0; MAX_MSG_LENGTH];
loop {
let read_bytes =
recv(socket.as_raw_fd(), &mut buf, MsgFlags::empty()).map_err(|e| Error::Nix {
msg: "recvmsg",
src: e,
})?;
debug!("handling rpc");
if read_bytes < 4 {
error!("received rpc too short");
continue;
}
// SAFETY: safe as the enum repr is non_exhaustive so any value is valid and the buffer is long enough
let kind = unsafe { *(buf.as_ptr() as *const RpcKind) };
let fds = Vec::new();
if kind.num_fds() > 0 {
// get any fds to go alongside the message
// nothing which requires this currently exists
unimplemented!()
}
let resp = handle_rpc(self.permitted_rpcs, kind, &buf[4..], &fds);
let (msg, fds) = RpcResultSend::new(resp);
// sendmsg first so its there when listening for the send
if !fds.is_empty() {
let fds: Box<[i32]> = fds.iter().map(|f| f.as_raw_fd()).collect();
sendmsg::<()>(
socket.as_raw_fd(),
&[],
&[ControlMessage::ScmRights(&fds)],
MsgFlags::empty(),
None,
)
.map_err(|e| Error::Nix {
msg: "sendmsg",
src: e,
})?;
}
// SAFETY: safe as msg is of fixed size
let msg = unsafe {
std::slice::from_raw_parts(
&msg as *const RpcResultSend as *const u8,
std::mem::size_of_val(&msg),
)
};
send(socket.as_raw_fd(), msg, MsgFlags::empty()).map_err(|e| Error::Nix {
msg: "send",
src: e,
})?;
}
}
}
#[repr(u32)]
#[non_exhaustive]
#[allow(dead_code)]
#[derive(Clone, Copy)]
pub enum RpcKind {
OpenTcpSocket,
OpenUdpSocket,
}
impl RpcKind {
fn num_fds(&self) -> usize {
match self {
RpcKind::OpenTcpSocket => 0,
RpcKind::OpenUdpSocket => 0,
}
}
}
pub struct OpenSocket {
pub family: AddressFamily,
pub port: u16,
pub host: [c_char],
}
pub enum RpcResult {
OpenTcpSocket { socket: TcpStream },
OpenUdpSocket { socket: UdpSocket },
Error { error: RpcError },
}
pub enum RpcResultSend {
OpenTcpSocket,
OpenUdpSocket,
Error { error: RpcError },
}
impl RpcResultSend {
fn new(from: RpcResult) -> (Self, Vec<Box<dyn AsRawFd>>) {
match from {
RpcResult::OpenTcpSocket { socket } => (Self::OpenTcpSocket, vec![Box::new(socket)]),
RpcResult::OpenUdpSocket { socket } => (Self::OpenUdpSocket, vec![Box::new(socket)]),
RpcResult::Error { error } => (Self::Error { error }, vec![]),
}
}
}
#[repr(C)]
pub enum RpcError {
BadlyFormedRequest,
OperationNotPermitted,
Io { errno: i32 },
}
fn handle_rpc(
permitted_rpcs: &[RpcSpecification],
kind: RpcKind,
data: &[u8],
_fds: &[File],
) -> RpcResult {
fn inner(
permitted_rpcs: &[RpcSpecification],
kind: RpcKind,
data: &[u8],
) -> Result<RpcResult, RpcError> {
match kind {
RpcKind::OpenTcpSocket => {
let data = unsafe { &*(data as *const [u8] as *const OpenSocket) };
if !validate_open_tcp_socket(permitted_rpcs, data)? {
Ok(RpcResult::Error {
error: RpcError::OperationNotPermitted,
})
} else {
handle_open_tcp_socket(data)
}
}
RpcKind::OpenUdpSocket => {
let data = unsafe { &*(data as *const [u8] as *const OpenSocket) };
if !validate_open_udp_socket(permitted_rpcs, data)? {
Ok(RpcResult::Error {
error: RpcError::OperationNotPermitted,
})
} else {
handle_open_udp_socket(data)
}
}
}
}
match inner(permitted_rpcs, kind, data) {
Ok(o) => o,
Err(e) => RpcResult::Error { error: e },
}
}
fn validate_open_tcp_socket(
permitted_rpcs: &[RpcSpecification],
req: &OpenSocket,
) -> Result<bool, RpcError> {
for each in permitted_rpcs {
if let RpcSpecification::OpenTcpSocket { family, port, host } = each {
let mut allowed = true;
allowed &= match family {
None => true,
Some(fam) => match req.family {
AddressFamily::Inet => *fam == SpecAddressFamily::Inet,
AddressFamily::Inet6 => *fam == SpecAddressFamily::Inet6,
_ => false,
},
};
allowed &= match port {
None => true,
Some(p) => req.port == *p,
};
allowed &= match host {
None => true,
Some(h) => {
CStr::from_bytes_with_nul(as_u8_slice(&req.host))
.map_err(|_| RpcError::BadlyFormedRequest)?
.to_string_lossy()
.as_ref()
== h
}
};
if allowed {
return Ok(true);
}
}
}
Ok(false)
}
fn handle_open_tcp_socket(req: &OpenSocket) -> Result<RpcResult, RpcError> {
let host = CStr::from_bytes_with_nul(as_u8_slice(&req.host))
.map_err(|_| RpcError::BadlyFormedRequest)?;
let host = host.to_str().map_err(|_| RpcError::BadlyFormedRequest)?;
let socket = TcpStream::connect(host).map_err(|e| RpcError::Io {
errno: e.raw_os_error().unwrap(),
})?;
Ok(RpcResult::OpenTcpSocket { socket })
}
fn validate_open_udp_socket(
permitted_rpcs: &[RpcSpecification],
req: &OpenSocket,
) -> Result<bool, RpcError> {
for each in permitted_rpcs {
if let RpcSpecification::OpenUdpSocket { family, port, host } = each {
let mut allowed = true;
allowed &= match family {
None => true,
Some(fam) => match req.family {
AddressFamily::Inet => *fam == SpecAddressFamily::Inet,
AddressFamily::Inet6 => *fam == SpecAddressFamily::Inet6,
_ => false,
},
};
allowed &= match port {
None => true,
Some(p) => req.port == *p,
};
allowed &= match host {
None => true,
Some(h) => {
CStr::from_bytes_with_nul(as_u8_slice(&req.host))
.map_err(|_| RpcError::BadlyFormedRequest)?
.to_string_lossy()
.as_ref()
== h
}
};
if allowed {
return Ok(true);
}
}
}
Ok(false)
}
fn handle_open_udp_socket(req: &OpenSocket) -> Result<RpcResult, RpcError> {
let host = CStr::from_bytes_with_nul(as_u8_slice(&req.host))
.map_err(|_| RpcError::BadlyFormedRequest)?;
let host = host.to_str().map_err(|_| RpcError::BadlyFormedRequest)?;
let socket = UdpSocket::bind("0.0.0.0:0").map_err(|e| RpcError::Io {
errno: e.raw_os_error().unwrap(),
})?;
socket.connect(host).map_err(|e| RpcError::Io {
errno: e.raw_os_error().unwrap(),
})?;
Ok(RpcResult::OpenUdpSocket { socket })
}
fn as_u8_slice(s: &[c_char]) -> &[u8] {
unsafe { std::slice::from_raw_parts(s.as_ptr() as *const u8, s.len()) }
}

View File

@ -68,9 +68,6 @@ pub enum Arg {
/// A TCP Listener /// A TCP Listener
TcpListener { addr: SocketAddr }, TcpListener { addr: SocketAddr },
/// An RPC socket that accepts specified commands
Rpc(Vec<RpcSpecification>),
/// The rest of argv[1..], 0 or more arguments /// The rest of argv[1..], 0 or more arguments
Trailing, Trailing,
} }
@ -81,38 +78,6 @@ impl Arg {
} }
} }
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub enum RpcSpecification {
/// Open a TCP socket
///
/// None for each value means that any value is allowed in the call.
/// A specified value restricts to exactly that.
OpenTcpSocket {
family: Option<AddressFamily>,
port: Option<u16>,
host: Option<String>,
},
/// Open a UDP socket
///
/// None for each value means that any value is allowed in the call.
/// A specified value restricts to exactly that.
OpenUdpSocket {
family: Option<AddressFamily>,
port: Option<u16>,
host: Option<String>,
},
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub enum AddressFamily {
/// IPv4 address
Inet,
/// IPv6 address
Inet6,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
pub enum Pipe { pub enum Pipe {
Rx(String), Rx(String),
@ -152,12 +117,6 @@ pub enum Environment {
Hostname(String), Hostname(String),
DomainName(String), DomainName(String),
Procfs,
Stdin,
Stdout,
Stderr,
} }
#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Debug)] #[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Debug)]
@ -245,31 +204,6 @@ impl Specification {
return Err(Error::BadPipe(pipe.to_string())); return Err(Error::BadPipe(pipe.to_string()));
} }
// validate sockets match
let (read, write) = self.sockets();
let mut read_set = HashSet::with_capacity(read.len());
for socket in read {
if !read_set.insert(socket) {
return Err(Error::BadFileSocket(socket.to_string()));
}
}
let mut write_set = HashSet::with_capacity(write.len());
for socket in write {
write_set.insert(socket);
}
for socket in &read_set {
if !write_set.contains(socket) {
return Err(Error::BadFileSocket(socket.to_string()));
}
}
if let Some(socket) = (&write_set - &read_set).into_iter().next() {
return Err(Error::BadFileSocket(socket.to_string()));
}
// validate trigger arguments make sense // validate trigger arguments make sense
for entrypoint in self.entrypoints.values() { for entrypoint in self.entrypoints.values() {
if entrypoint.args.contains(&Arg::Trigger) { if entrypoint.args.contains(&Arg::Trigger) {

View File

@ -1,12 +1,11 @@
use log::{debug, error, info, trace}; use log::{debug, error};
use crate::clone::{clone3, CloneArgs, CloneFlags}; use crate::clone::{clone3, CloneArgs, CloneFlags};
use crate::{Error, Result}; use crate::{Error, Result};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::env;
use std::fmt; use std::fmt;
use std::fs::{self, File}; use std::fs;
use std::io::Write; use std::io::Write;
use std::os::unix::io::{AsRawFd, IntoRawFd, RawFd}; use std::os::unix::io::{AsRawFd, IntoRawFd, RawFd};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -14,7 +13,7 @@ use std::path::{Path, PathBuf};
use nix::fcntl::{FcntlArg, FdFlag}; use nix::fcntl::{FcntlArg, FdFlag};
use nix::mount::{mount, umount2, MntFlags, MsFlags}; use nix::mount::{mount, umount2, MntFlags, MsFlags};
use nix::sys::signal::{signal, SigHandler, Signal}; use nix::sys::signal::{signal, SigHandler, Signal};
use nix::unistd::{close, dup2, getgid, getuid, pivot_root, sethostname, Gid, Pid, Uid}; use nix::unistd::{close, getgid, getuid, pivot_root, sethostname, Gid, Pid, Uid};
use close_fds::CloseFdsBuilder; use close_fds::CloseFdsBuilder;
@ -34,8 +33,6 @@ pub struct VoidBuilder {
mounts: HashMap<PathBuf, PathBuf>, mounts: HashMap<PathBuf, PathBuf>,
fds: HashSet<RawFd>, fds: HashSet<RawFd>,
remount_proc: bool,
} }
impl VoidBuilder { impl VoidBuilder {
@ -45,7 +42,6 @@ impl VoidBuilder {
domain_name: None, domain_name: None,
mounts: HashMap::new(), mounts: HashMap::new(),
fds: HashSet::new(), fds: HashSet::new(),
remount_proc: false,
} }
} }
@ -69,11 +65,6 @@ impl VoidBuilder {
self self
} }
pub fn remount_proc(&mut self) -> &mut Self {
self.remount_proc = true;
self
}
pub fn spawn(&mut self, child_fn: impl FnOnce() -> i32) -> Result<VoidHandle> { pub fn spawn(&mut self, child_fn: impl FnOnce() -> i32) -> Result<VoidHandle> {
let mut args = CloneArgs::new( let mut args = CloneArgs::new(
CloneFlags::CLONE_NEWCGROUP CloneFlags::CLONE_NEWCGROUP
@ -103,23 +94,15 @@ impl VoidBuilder {
})?; })?;
let result = { let result = {
debug!("voiding user namespace...");
self.void_user_namespace(parent_uid, parent_gid)?; // first to regain full capabilities self.void_user_namespace(parent_uid, parent_gid)?; // first to regain full capabilities
debug!("voiding mount namespace...");
self.void_mount_namespace()?;
debug!("voiding file descriptors..."); // occur after mount to unmount /dev/null
self.void_file_descriptors()?; self.void_file_descriptors()?;
debug!("voiding ipc namespace...");
self.void_ipc_namespace()?; self.void_ipc_namespace()?;
debug!("voiding uts namespace...");
self.void_uts_namespace()?; self.void_uts_namespace()?;
debug!("voiding network namespace...");
self.void_network_namespace()?; self.void_network_namespace()?;
debug!("voiding pid namespace...");
self.void_pid_namespace()?; self.void_pid_namespace()?;
debug!("voiding cgroup namespace..."); self.void_mount_namespace()?;
self.void_cgroup_namespace()?; self.void_cgroup_namespace()?;
Ok::<(), Error>(()) Ok::<(), Error>(())
@ -129,12 +112,19 @@ impl VoidBuilder {
error!("error preparing void: {}", e); error!("error preparing void: {}", e);
std::process::exit(-1) std::process::exit(-1)
} else { } else {
info!("successfully prepared void");
std::process::exit(child_fn()) std::process::exit(child_fn())
} }
} }
debug!("cloned child: {}", child); debug!("cloned child: {}", child);
// Leak the child function's resources in the parent process.
// This avoids closing files that have been "moved" into the child.
// It is also an over-approximation, and may cause actual memory leaks.
// As the spawning process is normally short lived, this shouldn't
// be a problem.
std::mem::forget(child_fn);
Ok(VoidHandle { pid: child }) Ok(VoidHandle { pid: child })
} }
@ -190,7 +180,7 @@ impl VoidBuilder {
* unavailable after unmounting the old root. * unavailable after unmounting the old root.
*/ */
fn void_mount_namespace(&self) -> Result<()> { fn void_mount_namespace(&self) -> Result<()> {
trace!("changing the propagation type of the old root to private"); // change the propagation type of the old root to private
mount( mount(
Option::<&str>::None, Option::<&str>::None,
"/", "/",
@ -203,21 +193,10 @@ impl VoidBuilder {
src: e, src: e,
})?; })?;
trace!("creating tmpdir for new root"); // create and consume a tmpdir to mount a tmpfs into
let tmp_base = { let new_root = tempfile::tempdir()?.into_path();
let env_dir = env::temp_dir();
if env_dir.exists() {
env_dir
} else {
debug!("env_dir does not exist, assuming `/` as the base");
"/".into()
}
};
// consume so it does not attempt to delete a folder which no longer exists // mount a tmpfs as the new root
let new_root = tempfile::tempdir_in(tmp_base)?.into_path();
trace!("mounting a new root tmpfs at `{:?}`", &new_root);
mount( mount(
Some("tmpfs"), Some("tmpfs"),
&new_root, &new_root,
@ -230,12 +209,13 @@ impl VoidBuilder {
src: e, src: e,
})?; })?;
let put_old = new_root.join("old_root/"); // prepare a subdirectory to pivot the old root into
debug!("new_root: {:?}; put_old: {:?}", &new_root, &put_old); let old_root = new_root.join("old_root/");
fs::create_dir_all(&put_old)?; debug!("new_root: {:?}; put_old: {:?}", &new_root, &old_root);
fs::create_dir(&old_root)?;
trace!("pivoting old root into a subdirectory of new root"); // pivot the old root into a subdirectory of the new root
pivot_root(&new_root, &put_old).map_err(|e| Error::Nix { pivot_root(&new_root, &old_root).map_err(|e| Error::Nix {
msg: "pivot_root", msg: "pivot_root",
src: e, src: e,
})?; })?;
@ -243,22 +223,11 @@ impl VoidBuilder {
let new_root = PathBuf::from("/"); let new_root = PathBuf::from("/");
let old_root = PathBuf::from("/old_root/"); let old_root = PathBuf::from("/old_root/");
trace!("changing root directory to new root"); // chdir after
std::env::set_current_dir(&new_root)?; std::env::set_current_dir(&new_root)?;
trace!("creating bind mounts before unmounting"); // mount paths before unmounting old_root
for (src, dst) in &self.mounts {
let standard_dev_null = if self.mounts.contains_key(&PathBuf::from("/dev/null")) {
None
} else {
Some((PathBuf::from("/dev/null"), PathBuf::from("/dev/null")))
};
for (src, dst) in self
.mounts
.iter()
.chain(standard_dev_null.as_ref().map(|(x, y)| (x, y)))
{
let mut src = old_root.join(src.strip_prefix("/").unwrap_or(src)); let mut src = old_root.join(src.strip_prefix("/").unwrap_or(src));
let dst = new_root.join(dst.strip_prefix("/").unwrap_or(dst)); let dst = new_root.join(dst.strip_prefix("/").unwrap_or(dst));
@ -283,29 +252,12 @@ impl VoidBuilder {
fs::write(&dst, b"")?; fs::write(&dst, b"")?;
} }
// rbind mount // bind mount
mount( mount(
Some(&src), Some(&src),
&dst, &dst,
Option::<&str>::None, Option::<&str>::None,
MsFlags::MS_BIND | MsFlags::MS_REC, MsFlags::MS_BIND,
Option::<&str>::None,
)
.map_err(|e| Error::Nix {
msg: "mount",
src: e,
})?;
}
// remount proc
if self.remount_proc {
debug!("remounting /proc`");
mount(
Some("proc"),
"/proc",
Some("proc"),
MsFlags::empty(),
Option::<&str>::None, Option::<&str>::None,
) )
.map_err(|e| Error::Nix { .map_err(|e| Error::Nix {
@ -395,37 +347,6 @@ impl VoidBuilder {
closer.closefrom(3); closer.closefrom(3);
} }
// overwrite stdin/stdout/stderr without closing
{
let mut nullfd: Option<File> = None;
for stdfd in &[0, 1, 2] {
if !keep.contains(stdfd) {
trace!("voiding stdfd {}", stdfd);
let fd = nullfd
.take()
.map(Ok)
.unwrap_or_else(|| File::open("/dev/null"))?;
dup2(fd.as_raw_fd(), *stdfd).map_err(|e| Error::Nix {
msg: "dup2",
src: e,
})?;
nullfd = Some(fd);
}
}
}
if !self.mounts.contains_key(&PathBuf::from("/dev/null")) {
debug!("unmount /dev/null after voiding file descriptors");
umount2("/dev/null", MntFlags::MNT_DETACH).map_err(|e| Error::Nix {
msg: "umount2",
src: e,
})?;
}
for fd in keep.as_ref() { for fd in keep.as_ref() {
let mut flags = FdFlag::from_bits_truncate( let mut flags = FdFlag::from_bits_truncate(
nix::fcntl::fcntl(*fd, FcntlArg::F_GETFD).map_err(|e| Error::Nix { nix::fcntl::fcntl(*fd, FcntlArg::F_GETFD).map_err(|e| Error::Nix {