microsandbox-rb 0.17.2 → 0.17.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +45 -1
- data/Cargo.lock +1 -1
- data/DESIGN.md +98 -0
- data/README.md +3 -2
- data/docs/errors.md +1 -0
- data/docs/runtime.md +85 -0
- data/docs/surface.md +3 -0
- data/ext/microsandbox/Cargo.toml +1 -1
- data/ext/microsandbox/src/agent.rs +13 -10
- data/ext/microsandbox/src/backend.rs +290 -33
- data/ext/microsandbox/src/error.rs +18 -0
- data/ext/microsandbox/src/exec.rs +32 -18
- data/ext/microsandbox/src/fs_stream.rs +11 -8
- data/ext/microsandbox/src/host_path.rs +4 -3
- data/ext/microsandbox/src/lib.rs +1 -0
- data/ext/microsandbox/src/runtime.rs +127 -36
- data/ext/microsandbox/src/sandbox.rs +175 -91
- data/ext/microsandbox/src/snapshot.rs +7 -4
- data/ext/microsandbox/src/ssh.rs +26 -23
- data/ext/microsandbox/src/stream.rs +13 -7
- data/ext/microsandbox/src/volume.rs +22 -16
- data/lib/microsandbox/errors.rb +8 -0
- data/lib/microsandbox/version.rb +1 -1
- data/lib/microsandbox.rb +20 -1
- data/sig/microsandbox.rbs +1 -0
- metadata +1 -1
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
|
|
42
42
|
use std::ffi::c_void;
|
|
43
43
|
use std::future::Future;
|
|
44
|
+
use std::mem::ManuallyDrop;
|
|
44
45
|
use std::panic::{catch_unwind, AssertUnwindSafe};
|
|
45
|
-
use std::sync::atomic::{AtomicPtr,
|
|
46
|
-
use std::sync::Mutex;
|
|
46
|
+
use std::sync::atomic::{AtomicPtr, Ordering};
|
|
47
47
|
|
|
48
48
|
use magnus::{Error, Ruby, TypedData};
|
|
49
49
|
use tokio::runtime::Runtime;
|
|
@@ -57,7 +57,14 @@ pub fn ruby() -> Ruby {
|
|
|
57
57
|
Ruby::get().expect("microsandbox: not on a Ruby thread")
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
/// This process's id. The single `std::process::id()` call site: everything
|
|
61
|
+
/// fork-aware in the extension (the runtime cell below, the backend generation
|
|
62
|
+
/// in `backend.rs`, [`ForkGuarded`]) compares against this.
|
|
63
|
+
pub fn current_pid() -> u32 {
|
|
64
|
+
std::process::id()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// The process-wide tokio runtime, tagged with the pid it was built under. A
|
|
61
68
|
// multi-threaded runtime owns worker + I/O-driver threads, and `fork(2)` copies
|
|
62
69
|
// ONLY the calling thread — so a child that inherits this runtime has a runtime
|
|
63
70
|
// whose threads are gone: `block_on` can still drive the current thread, but the
|
|
@@ -65,16 +72,16 @@ pub fn ruby() -> Ruby {
|
|
|
65
72
|
// connections stall/drop mid-use. This is hit in practice by forking job servers
|
|
66
73
|
// (Solid Queue, Resque) and clustered web servers (Puma workers).
|
|
67
74
|
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
static
|
|
75
|
+
// The runtime and its pid live in ONE heap cell behind ONE atomic pointer, so a
|
|
76
|
+
// reader sees either a whole cell or none: there is no window in which the
|
|
77
|
+
// pointer is new and the pid old (or the reverse), and no mutex that a fork
|
|
78
|
+
// could capture locked or that a panic could poison.
|
|
79
|
+
struct RuntimeCell {
|
|
80
|
+
pid: u32,
|
|
81
|
+
rt: Runtime,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
static RUNTIME: AtomicPtr<RuntimeCell> = AtomicPtr::new(std::ptr::null_mut());
|
|
78
85
|
|
|
79
86
|
fn build_runtime() -> Runtime {
|
|
80
87
|
tokio::runtime::Builder::new_multi_thread()
|
|
@@ -85,32 +92,111 @@ fn build_runtime() -> Runtime {
|
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
/// The multi-threaded tokio runtime for THIS process, built on first use and
|
|
88
|
-
/// rebuilt after a `fork(2)` (
|
|
95
|
+
/// rebuilt after a `fork(2)` (see the note above).
|
|
89
96
|
pub fn runtime() -> &'static Runtime {
|
|
90
|
-
let cur =
|
|
91
|
-
let
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
97
|
+
let cur = current_pid();
|
|
98
|
+
let mut observed = RUNTIME.load(Ordering::Acquire);
|
|
99
|
+
// SAFETY (every dereference below): a non-null pointer in `RUNTIME` came
|
|
100
|
+
// from `Box::into_raw` and is never freed once published, so it is valid
|
|
101
|
+
// for `'static`.
|
|
102
|
+
if let Some(cell) = unsafe { observed.as_ref() } {
|
|
103
|
+
if cell.pid == cur {
|
|
104
|
+
// Built in this process: its threads are alive here.
|
|
105
|
+
return &cell.rt;
|
|
106
|
+
}
|
|
96
107
|
}
|
|
97
108
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
109
|
+
// Slow path: no runtime yet, or the one we see was built by a parent
|
|
110
|
+
// process. Build ours and publish it with a compare-exchange.
|
|
111
|
+
let fresh = Box::into_raw(Box::new(RuntimeCell {
|
|
112
|
+
pid: cur,
|
|
113
|
+
rt: build_runtime(),
|
|
114
|
+
}));
|
|
115
|
+
loop {
|
|
116
|
+
match RUNTIME.compare_exchange(observed, fresh, Ordering::AcqRel, Ordering::Acquire) {
|
|
117
|
+
// Published. The cell we replaced (if any) belonged to a parent
|
|
118
|
+
// process and is LEAKED on purpose, never dropped: its worker
|
|
119
|
+
// threads did not survive the fork, and `Runtime`'s `Drop` joins
|
|
120
|
+
// them. One runtime is leaked per fork generation that uses the
|
|
121
|
+
// SDK.
|
|
122
|
+
Ok(_) => return unsafe { &(*fresh).rt },
|
|
123
|
+
Err(winner) => {
|
|
124
|
+
if let Some(cell) = unsafe { winner.as_ref() } {
|
|
125
|
+
if cell.pid == cur {
|
|
126
|
+
// Another thread of THIS process won the race. Our
|
|
127
|
+
// cell was never published and its threads are alive
|
|
128
|
+
// here, so dropping it is safe (and joins them).
|
|
129
|
+
drop(unsafe { Box::from_raw(fresh) });
|
|
130
|
+
return &cell.rt;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
observed = winner;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
105
136
|
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/// The Rust payload of a wrapped Ruby object, bound to the process that
|
|
140
|
+
/// created it.
|
|
141
|
+
///
|
|
142
|
+
/// A handle that crosses a `fork(2)` is useless in the child and dangerous to
|
|
143
|
+
/// destroy there: it owns tasks, channels and connections of the parent's
|
|
144
|
+
/// runtime, clones of the parent's backend, the parent-watchdog pipe of a
|
|
145
|
+
/// sandbox. So every `#[magnus::wrap]` type keeps its payload in one of these:
|
|
146
|
+
///
|
|
147
|
+
/// * [`get`](Self::get) — the only way to the payload for anything that acts
|
|
148
|
+
/// on it — fails with `Microsandbox::ForkedHandleError` in any process other
|
|
149
|
+
/// than the one that built the value. `Deref` is deliberately not
|
|
150
|
+
/// implemented: an access that skips the check should not compile.
|
|
151
|
+
/// * `Drop` runs the payload's destructor only in that same process. A forked
|
|
152
|
+
/// child that garbage-collects an inherited handle — at the latest when it
|
|
153
|
+
/// exits: Ruby frees every `T_DATA` object in `rb_objspace_call_finalizer` —
|
|
154
|
+
/// leaks it instead.
|
|
155
|
+
pub struct ForkGuarded<T> {
|
|
156
|
+
pid: u32,
|
|
157
|
+
/// The Ruby class users know the handle by, for the error message.
|
|
158
|
+
class: &'static str,
|
|
159
|
+
inner: ManuallyDrop<T>,
|
|
160
|
+
}
|
|
106
161
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
162
|
+
impl<T> ForkGuarded<T> {
|
|
163
|
+
pub fn new(class: &'static str, inner: T) -> Self {
|
|
164
|
+
Self {
|
|
165
|
+
pid: current_pid(),
|
|
166
|
+
class,
|
|
167
|
+
inner: ManuallyDrop::new(inner),
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// The payload, or `ForkedHandleError` when this is not the process that
|
|
172
|
+
/// created it.
|
|
173
|
+
pub fn get(&self) -> Result<&T, Error> {
|
|
174
|
+
let cur = current_pid();
|
|
175
|
+
if self.pid == cur {
|
|
176
|
+
Ok(&self.inner)
|
|
177
|
+
} else {
|
|
178
|
+
Err(error::forked_handle(self.class, self.pid, cur))
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/// The payload without the process check — ONLY for accessors that copy
|
|
183
|
+
/// plain data out of the value's own memory (a name, an id, a cached
|
|
184
|
+
/// status). Those stay readable in a forked child, which needs e.g. the
|
|
185
|
+
/// name to open its own handle. Never use this for anything that takes a
|
|
186
|
+
/// lock, touches a file descriptor, or reaches the runtime or the backend.
|
|
187
|
+
pub fn peek(&self) -> &T {
|
|
188
|
+
&self.inner
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
impl<T> Drop for ForkGuarded<T> {
|
|
193
|
+
fn drop(&mut self) {
|
|
194
|
+
if self.pid == current_pid() {
|
|
195
|
+
// SAFETY: dropped exactly once, here; `inner` is not used again.
|
|
196
|
+
unsafe { ManuallyDrop::drop(&mut self.inner) };
|
|
197
|
+
}
|
|
198
|
+
// Otherwise: inherited across a fork. Leak it (see the type docs).
|
|
199
|
+
}
|
|
114
200
|
}
|
|
115
201
|
|
|
116
202
|
/// Resolve the Ruby class of a `#[magnus::wrap]` type now, at extension load.
|
|
@@ -301,12 +387,17 @@ where
|
|
|
301
387
|
|
|
302
388
|
/// Drive a future to completion on the shared runtime with the GVL released.
|
|
303
389
|
///
|
|
304
|
-
/// `Err` means the future was never polled (see [`nogvl`]
|
|
305
|
-
/// `?` before looking at the future's own output:
|
|
390
|
+
/// `Err` means the future was never polled (see [`nogvl`], and the fork check
|
|
391
|
+
/// below); propagate it with `?` before looking at the future's own output:
|
|
306
392
|
/// `block_on(fut)?.map_err(error::to_ruby)?`.
|
|
307
393
|
pub fn block_on<F>(fut: F) -> Result<F::Output, Error>
|
|
308
394
|
where
|
|
309
395
|
F: Future,
|
|
310
396
|
{
|
|
397
|
+
// Many core operations resolve the process-wide default backend while they
|
|
398
|
+
// run. After a fork that backend is the parent's; replace it now, while we
|
|
399
|
+
// still hold the GVL (the check takes a mutex and may build a Ruby
|
|
400
|
+
// exception — neither is allowed once the GVL is released).
|
|
401
|
+
crate::backend::ensure_this_process_backend()?;
|
|
311
402
|
nogvl(|| runtime().block_on(fut))
|
|
312
403
|
}
|