Compare commits

..

34 Commits

Author SHA1 Message Date
5b16d240ea rpcs
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-05-27 04:38:07 +01:00
2ef3d46a5b renamed package to void orchestrator
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-05-26 10:48:04 +01:00
efbf6be520 signal handling in spawners
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-05-24 22:19:07 +01:00
983cd53f13 added ability to be interrupted to spawners
All checks were successful
continuous-integration/drone/push Build is passing
2022-05-24 21:46:24 +01:00
afecbd9cd8 tcp listener ctrl-c handler
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-05-24 17:22:47 +01:00
29fec1466e http input reading correction
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-05-24 12:48:29 +01:00
5d707bdde4 streaming http server 2022-05-24 12:47:20 +01:00
44380ba5be implement tls clean close 2022-05-24 12:28:59 +01:00
71888d40e7 added tls processing
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-05-23 19:43:27 +01:00
20c2f8d5f7 forward tls handler 2022-05-23 19:43:22 +01:00
a2cbe9dc54 no-op tls handler
All checks were successful
continuous-integration/drone/push Build is passing
2022-05-23 17:40:52 +01:00
5bad0766fd simplified main function
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-05-23 06:43:47 +01:00
5d1076f975 removed useless tls binary name
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-05-23 03:25:00 +01:00
2017d7c688 removed unnecessary arg privilege
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-05-22 19:23:46 +01:00
7bf1b6600d fib decl one line
All checks were successful
continuous-integration/drone/push Build is passing
2022-05-22 19:15:16 +01:00
8f0b1a8083 http server
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-05-21 17:20:15 +01:00
b7abf4fa07 removed incorrect closure drop 2022-05-21 16:40:03 +01:00
72e2b31629 tcp connection listener
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-05-21 16:27:27 +01:00
8244e4ebf7 fixed missing dev null
All checks were successful
continuous-integration/drone/push Build is passing
2022-05-21 16:24:26 +01:00
d365508cba added stdout and stderr to spawners
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-05-21 16:05:20 +01:00
3069f12da0 extracted prepare spawner
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-05-21 14:47:00 +01:00
9fe48a7749 fib example
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-05-20 18:09:51 +01:00
258ccc09b0 return correct exit code 2022-05-20 17:48:39 +01:00
95d3c06b31 std file descriptor protection
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-05-20 17:34:40 +01:00
8943d56a46 forwarded necessary fds in spawners
Some checks are pending
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is running
2022-05-19 21:44:39 +01:00
01f8f096c6 allowing multiple processes to share socket
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-05-19 20:30:51 +01:00
4f0f74b859 fixed broken waitpid and switched to waitid
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-05-19 17:19:52 +01:00
491fb46cb9 updated nix to 0.24.1 2022-05-19 17:18:05 +01:00
ccdb1f3e7e wait for children and report return code
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-05-19 16:45:02 +01:00
60c7888937 increased logging and accounted for missing /tmp
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-05-17 15:34:16 +01:00
c07d6df163 mount proc 2022-05-17 15:34:12 +01:00
0c77cd47f7 file argument binding
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-05-17 12:12:43 +01:00
00ace078b7 extract common spawner behaviour
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-05-17 11:48:13 +01:00
7da0e02235 added missing cmsg space
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-05-10 23:10:40 +01:00
20 changed files with 1582 additions and 405 deletions

231
Cargo.lock generated
View File

@ -2,23 +2,6 @@
# It is not intended for manual editing.
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]]
name = "aho-corasick"
version = "0.7.18"
@ -28,6 +11,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "anyhow"
version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
[[package]]
name = "atty"
version = "0.2.14"
@ -46,13 +35,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bincode"
version = "1.3.3"
name = "base64"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "bitflags"
@ -89,9 +75,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.0.72"
version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee"
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
[[package]]
name = "cfg-if"
@ -134,27 +120,6 @@ dependencies = [
"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]]
name = "close_fds"
version = "0.3.2"
@ -165,15 +130,6 @@ dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
dependencies = [
"cfg-if",
]
[[package]]
name = "criterion"
version = "0.3.5"
@ -311,29 +267,6 @@ dependencies = [
"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]]
name = "half"
version = "1.8.2"
@ -345,9 +278,6 @@ name = "hashbrown"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
dependencies = [
"ahash",
]
[[package]]
name = "hermit-abi"
@ -358,6 +288,12 @@ dependencies = [
"libc",
]
[[package]]
name = "httparse"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c"
[[package]]
name = "humantime"
version = "2.1.0"
@ -430,9 +366,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.117"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c"
checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
[[package]]
name = "log"
@ -458,23 +394,13 @@ dependencies = [
"autocfg",
]
[[package]]
name = "miniz_oxide"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082"
dependencies = [
"adler",
]
[[package]]
name = "nix"
version = "0.23.1"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6"
checksum = "8f17df307904acd05aa8e32e97bb20f2a0df1728bbc2d771ae8f9a90463441e9"
dependencies = [
"bitflags",
"cc",
"cfg-if",
"libc",
"memoffset",
@ -499,24 +425,11 @@ dependencies = [
"libc",
]
[[package]]
name = "object"
version = "0.28.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40bec70ba014595f99f7aa110b84331ffe1ee9aece7fe6f387cc7e3ecda4d456"
dependencies = [
"crc32fast",
"flate2",
"hashbrown",
"indexmap",
"memchr",
]
[[package]]
name = "once_cell"
version = "1.10.0"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9"
checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225"
[[package]]
name = "oorandom"
@ -642,6 +555,21 @@ dependencies = [
"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]]
name = "rustc_version"
version = "0.4.0"
@ -651,6 +579,27 @@ dependencies = [
"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]]
name = "ryu"
version = "1.0.9"
@ -672,6 +621,16 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "semver"
version = "1.0.6"
@ -719,6 +678,12 @@ dependencies = [
"serde",
]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "strsim"
version = "0.10.0"
@ -817,10 +782,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "version_check"
version = "0.9.4"
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[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]]
name = "walkdir"
@ -833,12 +822,6 @@ dependencies = [
"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]]
name = "wasm-bindgen"
version = "0.2.79"
@ -903,6 +886,16 @@ dependencies = [
"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]]
name = "winapi"
version = "0.3.9"

View File

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

View File

@ -2,6 +2,16 @@
## 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
The basic example instructs the shim to spawn two processes, each of which writes "hello from main{1,2}!" to stdout.
@ -22,6 +32,16 @@ To run this example:
cargo build --example 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
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 clone_shim::clone::{clone3, CloneArgs, CloneFlags};
use void_orchestrator::clone::{clone3, CloneArgs, CloneFlags};
use criterion::{criterion_group, criterion_main, Criterion};

15
examples/fib/main.rs Normal file
View File

@ -0,0 +1,15 @@
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
}

27
examples/fib/spec.json Normal file
View File

@ -0,0 +1,27 @@
{
"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"
}
}
]
}
}
}

85
examples/tls/http.rs Normal file
View File

@ -0,0 +1,85 @@
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)
}

55
examples/tls/listener.rs Normal file
View File

@ -0,0 +1,55 @@
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);
}

149
examples/tls/main.rs Normal file
View File

@ -0,0 +1,149 @@
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));
}

114
examples/tls/spec.json Normal file
View File

@ -0,0 +1,114 @@
{
"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"
}
}
]
}
}
}

176
examples/tls/tls.rs Normal file
View File

@ -0,0 +1,176 @@
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,25 +15,13 @@ pub enum Error {
#[error("json: {0}")]
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}")]
BadPipe(String),
#[error("bad socket specification: a socket must have exactly one reader and one writer: {0}")]
#[error("bad socket specification: a socket must have exactly one reader and one or more writers: {0}")]
BadFileSocket(String),
#[error("no specification provided")]
NoSpecification,
#[error("bad specification type: only json files are supported")]
#[error("bad specification type: only .json files are supported")]
BadSpecType,
#[error("bad trigger argument: this entrypoint is not triggered by something with arguments")]

View File

@ -2,51 +2,39 @@ use log::{debug, info};
pub mod clone;
mod error;
mod pack;
mod spawner;
mod specification;
mod void;
use error::{Error, Result};
use spawner::Spawner;
use specification::Specification;
use specification::{Environment, Specification};
use std::collections::HashMap;
use std::fs::File;
use std::os::unix::io::FromRawFd;
use std::os::unix::io::{AsRawFd, FromRawFd};
use std::path::Path;
use nix::fcntl::OFlag;
use nix::sys::socket;
use nix::sys::wait::{waitid, Id, WaitPidFlag, WaitStatus};
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 spec: Option<&'a Path>,
pub debug: bool,
pub daemon: bool,
pub stdout: bool,
pub stderr: bool,
pub binary: &'a Path,
pub binary_args: Vec<&'a str>,
}
pub fn run(args: &RunArgs) -> Result<()> {
pub fn run(args: &RunArgs) -> Result<i32> {
// parse the specification
let spec: Specification = if let Some(m) = args.spec {
let mut spec: Specification = if let Some(m) = args.spec {
if m.extension().map(|e| e == "json") == Some(true) {
let f = std::fs::File::open(m)?;
Ok(serde_json::from_reader(f)?)
@ -54,17 +42,26 @@ pub fn run(args: &RunArgs) -> Result<()> {
Err(Error::BadSpecType)
}
} else {
let spec = pack::extract_specification(args.binary)?;
if let Some(s) = spec {
Ok(s)
} else {
Err(Error::NoSpecification)
}
unimplemented!("reading spec from the elf is unimplemented")
}?;
debug!("specification read: {:?}", &spec);
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
let (pipes, _) = spec.pipes();
let pipes = create_pipes(pipes)?;
@ -84,7 +81,41 @@ pub fn run(args: &RunArgs) -> Result<()> {
}
.spawn()?;
Ok(())
if args.daemon {
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>> {
@ -121,10 +152,11 @@ impl PipePair {
src: e,
})?;
// safe to create files given the successful return of pipe(2)
Ok(PipePair {
name: name.to_string(),
// SAFETY: valid new fd as pipe2(2) returned successfully
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) }),
})
}
@ -146,7 +178,7 @@ pub struct SocketPair {
name: String,
read: Option<File>,
write: Option<File>,
write: File,
}
impl SocketPair {
@ -162,23 +194,30 @@ impl SocketPair {
src: e,
})?;
// safe to create files given the successful return of socketpair(2)
Ok(SocketPair {
name: name.to_string(),
// SAFETY: valid new fd as socketpair(2) returned successfully
read: Some(unsafe { File::from_raw_fd(read) }),
write: Some(unsafe { File::from_raw_fd(write) }),
// SAFETY: valid new fd as socketpair(2) returned successfully
write: unsafe { File::from_raw_fd(write) },
})
}
fn take_read(&mut self) -> Result<File> {
self.read
.take()
.ok_or_else(|| Error::BadPipe(self.name.to_string()))
.ok_or_else(|| Error::BadFileSocket(self.name.to_string()))
}
fn take_write(&mut self) -> Result<File> {
self.write
.take()
.ok_or_else(|| Error::BadPipe(self.name.to_string()))
fn write(&self) -> Result<File> {
let dup_fd = nix::unistd::dup(self.write.as_raw_fd())
.map_err(|e| Error::Nix { msg: "dup", src: e })?;
// 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, info};
use log::error;
use clone_shim::{pack, run, PackArgs, RunArgs};
use void_orchestrator::{run, RunArgs};
use std::path::Path;
@ -12,39 +12,12 @@ fn main() {
.version(env!("GIT_HASH"))
.author("Jake Hillion <jake@hillion.co.uk>")
.about("Launch a void process application.")
.subcommand_negates_reqs(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::new("spec")
.long("specification")
.short('s')
.help("Provide the specification to launch as an external JSON file.")
.help("Provide the specification as an external JSON file.")
.takes_value(true),
)
.arg(
@ -61,6 +34,25 @@ fn main() {
.help("Stop each spawned application process so that it can be attached to.")
.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::new("binary")
.index(1)
@ -82,30 +74,8 @@ fn main() {
env_logger::init_from_env(env);
// launch process
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
// execute shimmed process
std::process::exit({
let (binary, binary_args) = {
let mut argv = matches.values_of("binary").unwrap();
@ -118,21 +88,21 @@ fn main() {
let args = RunArgs {
spec: matches.value_of("spec").map(Path::new),
debug: matches.is_present("debug"),
daemon: matches.is_present("daemon"),
stdout: matches.is_present("stdout"),
stderr: matches.is_present("stderr"),
binary,
binary_args,
};
match run(&args) {
Ok(_) => {
info!("launched successfully");
exitcode::OK
}
Ok(code) => code,
Err(e) => {
error!("error: {}", e);
-1
}
}
};
std::process::exit(code);
})
}

View File

@ -1,75 +0,0 @@
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,4 +1,6 @@
use super::{Spawner, TriggerData};
use log::info;
use super::{RpcHandler, Spawner, TriggerData};
use crate::specification::{Arg, FileSocket, Pipe};
use crate::void::VoidBuilder;
use crate::{Error, Result};
@ -7,7 +9,10 @@ use std::ffi::CString;
use std::fs::File;
use std::net::TcpListener;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::io::IntoRawFd;
use std::os::unix::io::{FromRawFd, IntoRawFd};
use nix::sys::socket;
use nix::unistd::{fork, ForkResult};
pub struct PreparedArgs(Vec<PreparedArg>);
@ -37,11 +42,15 @@ impl PreparedArgs {
* for things like network sockets. update the builder
* with newly passed fds.
*/
pub fn prepare_ambient(builder: &mut VoidBuilder, args: &[Arg]) -> Result<Self> {
pub fn prepare_ambient(
spawner: &Spawner,
builder: &mut VoidBuilder,
args: &[Arg],
) -> Result<Self> {
let mut v = Vec::with_capacity(args.len());
for arg in args {
v.push(PreparedArg::prepare_ambient(builder, arg)?);
v.push(PreparedArg::prepare_ambient(spawner, builder, arg)?);
}
Ok(PreparedArgs(v))
@ -85,6 +94,9 @@ enum PreparedArg {
/// A TCP Listener
TcpListener { socket: TcpListener },
/// RPC
Rpc { socket: File },
/// The rest of argv[1..], 0 or more arguments
Trailing,
}
@ -113,24 +125,28 @@ impl PreparedArg {
PreparedArg::Pipe(pipe)
}
Arg::FileSocket(s) => {
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(),
}?;
Arg::FileSocket(FileSocket::Rx(s)) => {
let socket = spawner.sockets.get_mut(s).unwrap().take_read()?;
builder.keep_fd(&socket);
PreparedArg::FileSocket(socket)
}
arg => Self::prepare_ambient(builder, arg)?,
arg => Self::prepare_ambient(spawner, builder, arg)?,
})
}
fn prepare_ambient(builder: &mut VoidBuilder, arg: &Arg) -> Result<Self> {
fn prepare_ambient(spawner: &Spawner, builder: &mut VoidBuilder, arg: &Arg) -> Result<Self> {
Ok(match arg {
Arg::Pipe(p) => return Err(Error::BadPipe(p.get_name().to_string())),
Arg::FileSocket(s) => return Err(Error::BadFileSocket(s.get_name().to_string())),
Arg::FileSocket(FileSocket::Rx(s)) => return Err(Error::BadFileSocket(s.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) => {
let fd = File::open(path)?;
@ -146,6 +162,45 @@ impl PreparedArg {
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::Entrypoint => PreparedArg::Entrypoint,
Arg::Trigger => PreparedArg::Trigger,
@ -183,6 +238,10 @@ impl PreparedArg {
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
.binary_args
.iter()

View File

@ -1,10 +1,12 @@
use log::{debug, error, info};
mod args;
mod rpc;
use args::PreparedArgs;
use rpc::RpcHandler;
use crate::specification::{Entrypoint, Environment, Specification, Trigger};
use crate::specification::{Arg, Entrypoint, Environment, Specification, Trigger};
use crate::void::VoidBuilder;
use crate::{Error, Result};
use crate::{PipePair, SocketPair};
@ -13,14 +15,17 @@ use std::collections::HashMap;
use std::ffi::CString;
use std::fs::File;
use std::io::Read;
use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd};
use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd, RawFd};
use std::path::{Path, PathBuf};
use nix::sys::signal::{kill, Signal};
use nix::sys::socket::{recvmsg, ControlMessageOwned, MsgFlags};
use nix::unistd::{self, Pid};
use nix::sys::wait::{waitid, Id, WaitPidFlag, WaitStatus};
use nix::unistd::{self, fork, ForkResult, Pid};
use nix::Error as NixError;
const BUFFER_SIZE: usize = 1024;
const MAX_FILE_DESCRIPTORS: usize = 16;
pub struct Spawner<'a> {
pub spec: &'a Specification,
@ -64,10 +69,7 @@ impl<'a> Spawner<'a> {
match &entrypoint.trigger {
Trigger::Startup => {
let mut builder = VoidBuilder::new();
let binary = PathBuf::from(self.binary).canonicalize()?;
builder.mount(binary, "/entrypoint");
self.mount_entrypoint(&mut builder, self.binary)?;
self.prepare_env(&mut builder, &entrypoint.environment);
let args =
@ -100,70 +102,46 @@ impl<'a> Spawner<'a> {
}
Trigger::Pipe(s) => {
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");
self.prepare_spawner(&mut builder, &entrypoint.environment, &entrypoint.args)?;
let pipe = self.pipes.get_mut(s).unwrap().take_read()?;
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) {
Ok(()) => std::process::exit(exitcode::OK),
Ok(()) => exitcode::OK,
Err(e) => {
error!("error in pipe_trigger: {}", e);
std::process::exit(1)
1
}
};
let void = builder.spawn(closure)?;
info!(
"prepared pipe trigger for entrypoint `{}` as {}",
"spawned pipe trigger for entrypoint `{}` as {}",
name.as_str(),
void
);
}
Trigger::FileSocket(s) => {
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");
self.prepare_spawner(&mut builder, &entrypoint.environment, &entrypoint.args)?;
let socket = self.sockets.get_mut(s).unwrap().take_read()?;
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) {
Ok(()) => std::process::exit(exitcode::OK),
Ok(()) => exitcode::OK,
Err(e) => {
error!("error in file_socket_trigger: {}", e);
std::process::exit(1)
1
}
};
let void = builder.spawn(closure)?;
info!(
"prepared socket trigger for entrypoint `{}` as {}",
"spawned socket trigger for entrypoint `{}` as {}",
name.as_str(),
void
);
@ -175,9 +153,21 @@ impl<'a> Spawner<'a> {
}
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];
loop {
let read_bytes = pipe.read(&mut buf)?;
let read_bytes = match 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 {
return Ok(());
}
@ -189,7 +179,7 @@ impl<'a> Spawner<'a> {
self.prepare_env(&mut builder, &spec.environment);
let args = PreparedArgs::prepare_ambient(&mut builder, &spec.args)?;
let args = PreparedArgs::prepare_ambient(self, &mut builder, &spec.args)?;
let closure =
|| {
@ -216,18 +206,36 @@ impl<'a> Spawner<'a> {
}
};
builder.spawn(closure)?;
let void = builder.spawn(closure)?;
info!("spawned entrypoint `{}` as {}", name, void);
}
}
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 {
let msg = recvmsg(socket.as_raw_fd(), &[], None, MsgFlags::empty()).map_err(|e| {
Error::Nix {
msg: "recvmsg",
src: e,
let msg = match recvmsg::<()>(
socket.as_raw_fd(),
&mut [],
Some(&mut cmsg_buf),
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");
@ -247,7 +255,7 @@ impl<'a> Spawner<'a> {
self.prepare_env(&mut builder, &spec.environment);
let args = PreparedArgs::prepare_ambient(&mut builder, &spec.args)?;
let args = PreparedArgs::prepare_ambient(self, &mut builder, &spec.args)?;
let closure = || {
if self.debug {
@ -273,7 +281,8 @@ impl<'a> Spawner<'a> {
}
};
builder.spawn(closure)?;
let void = builder.spawn(closure)?;
info!("spawned entrypoint `{}` as {}", name, void);
}
_ => unimplemented!(),
}
@ -281,11 +290,39 @@ 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<()> {
let pid = Pid::this();
info!("stopping process `{}`", name);
kill(pid, Signal::SIGSTOP).map_err(|e| Error::Nix {
kill(Pid::this(), Signal::SIGSTOP).map_err(|e| Error::Nix {
msg: "kill",
src: e,
})?;
@ -294,6 +331,67 @@ impl<'a> Spawner<'a> {
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>(
&self,
builder: &mut VoidBuilder,
@ -314,6 +412,20 @@ impl<'a> Spawner<'a> {
Environment::DomainName(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);
}
}
}
}

300
src/spawner/rpc.rs Normal file
View File

@ -0,0 +1,300 @@
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,6 +68,9 @@ pub enum Arg {
/// A TCP Listener
TcpListener { addr: SocketAddr },
/// An RPC socket that accepts specified commands
Rpc(Vec<RpcSpecification>),
/// The rest of argv[1..], 0 or more arguments
Trailing,
}
@ -78,6 +81,38 @@ 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)]
pub enum Pipe {
Rx(String),
@ -117,6 +152,12 @@ pub enum Environment {
Hostname(String),
DomainName(String),
Procfs,
Stdin,
Stdout,
Stderr,
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Debug)]
@ -204,6 +245,31 @@ impl Specification {
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
for entrypoint in self.entrypoints.values() {
if entrypoint.args.contains(&Arg::Trigger) {

View File

@ -1,11 +1,12 @@
use log::{debug, error};
use log::{debug, error, info, trace};
use crate::clone::{clone3, CloneArgs, CloneFlags};
use crate::{Error, Result};
use std::collections::{HashMap, HashSet};
use std::env;
use std::fmt;
use std::fs;
use std::fs::{self, File};
use std::io::Write;
use std::os::unix::io::{AsRawFd, IntoRawFd, RawFd};
use std::path::{Path, PathBuf};
@ -13,7 +14,7 @@ use std::path::{Path, PathBuf};
use nix::fcntl::{FcntlArg, FdFlag};
use nix::mount::{mount, umount2, MntFlags, MsFlags};
use nix::sys::signal::{signal, SigHandler, Signal};
use nix::unistd::{close, getgid, getuid, pivot_root, sethostname, Gid, Pid, Uid};
use nix::unistd::{close, dup2, getgid, getuid, pivot_root, sethostname, Gid, Pid, Uid};
use close_fds::CloseFdsBuilder;
@ -33,6 +34,8 @@ pub struct VoidBuilder {
mounts: HashMap<PathBuf, PathBuf>,
fds: HashSet<RawFd>,
remount_proc: bool,
}
impl VoidBuilder {
@ -42,6 +45,7 @@ impl VoidBuilder {
domain_name: None,
mounts: HashMap::new(),
fds: HashSet::new(),
remount_proc: false,
}
}
@ -65,6 +69,11 @@ impl VoidBuilder {
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> {
let mut args = CloneArgs::new(
CloneFlags::CLONE_NEWCGROUP
@ -94,15 +103,23 @@ impl VoidBuilder {
})?;
let result = {
debug!("voiding user namespace...");
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()?;
debug!("voiding ipc namespace...");
self.void_ipc_namespace()?;
debug!("voiding uts namespace...");
self.void_uts_namespace()?;
debug!("voiding network namespace...");
self.void_network_namespace()?;
debug!("voiding pid namespace...");
self.void_pid_namespace()?;
self.void_mount_namespace()?;
debug!("voiding cgroup namespace...");
self.void_cgroup_namespace()?;
Ok::<(), Error>(())
@ -112,19 +129,12 @@ impl VoidBuilder {
error!("error preparing void: {}", e);
std::process::exit(-1)
} else {
info!("successfully prepared void");
std::process::exit(child_fn())
}
}
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 })
}
@ -180,7 +190,7 @@ impl VoidBuilder {
* unavailable after unmounting the old root.
*/
fn void_mount_namespace(&self) -> Result<()> {
// change the propagation type of the old root to private
trace!("changing the propagation type of the old root to private");
mount(
Option::<&str>::None,
"/",
@ -193,10 +203,21 @@ impl VoidBuilder {
src: e,
})?;
// create and consume a tmpdir to mount a tmpfs into
let new_root = tempfile::tempdir()?.into_path();
trace!("creating tmpdir for new root");
let tmp_base = {
let env_dir = env::temp_dir();
if env_dir.exists() {
env_dir
} else {
debug!("env_dir does not exist, assuming `/` as the base");
"/".into()
}
};
// mount a tmpfs as the new root
// consume so it does not attempt to delete a folder which no longer exists
let new_root = tempfile::tempdir_in(tmp_base)?.into_path();
trace!("mounting a new root tmpfs at `{:?}`", &new_root);
mount(
Some("tmpfs"),
&new_root,
@ -209,13 +230,12 @@ impl VoidBuilder {
src: e,
})?;
// prepare a subdirectory to pivot the old root into
let old_root = new_root.join("old_root/");
debug!("new_root: {:?}; put_old: {:?}", &new_root, &old_root);
fs::create_dir(&old_root)?;
let put_old = new_root.join("old_root/");
debug!("new_root: {:?}; put_old: {:?}", &new_root, &put_old);
fs::create_dir_all(&put_old)?;
// pivot the old root into a subdirectory of the new root
pivot_root(&new_root, &old_root).map_err(|e| Error::Nix {
trace!("pivoting old root into a subdirectory of new root");
pivot_root(&new_root, &put_old).map_err(|e| Error::Nix {
msg: "pivot_root",
src: e,
})?;
@ -223,11 +243,22 @@ impl VoidBuilder {
let new_root = PathBuf::from("/");
let old_root = PathBuf::from("/old_root/");
// chdir after
trace!("changing root directory to new root");
std::env::set_current_dir(&new_root)?;
// mount paths before unmounting old_root
for (src, dst) in &self.mounts {
trace!("creating bind mounts before unmounting");
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 dst = new_root.join(dst.strip_prefix("/").unwrap_or(dst));
@ -252,12 +283,29 @@ impl VoidBuilder {
fs::write(&dst, b"")?;
}
// bind mount
// rbind mount
mount(
Some(&src),
&dst,
Option::<&str>::None,
MsFlags::MS_BIND,
MsFlags::MS_BIND | MsFlags::MS_REC,
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,
)
.map_err(|e| Error::Nix {
@ -347,6 +395,37 @@ impl VoidBuilder {
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() {
let mut flags = FdFlag::from_bits_truncate(
nix::fcntl::fcntl(*fd, FcntlArg::F_GETFD).map_err(|e| Error::Nix {