microsandbox-rb 0.17.2 → 0.17.4
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 +112 -1
- data/Cargo.lock +34 -32
- data/DESIGN.md +199 -8
- data/README.md +4 -2
- data/docs/errors.md +1 -0
- data/docs/runtime.md +115 -0
- data/docs/surface.md +6 -0
- data/ext/microsandbox/Cargo.toml +9 -2
- data/ext/microsandbox/src/agent.rs +13 -10
- data/ext/microsandbox/src/backend.rs +295 -34
- data/ext/microsandbox/src/error.rs +41 -23
- 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 +51 -2
- data/ext/microsandbox/src/runtime.rs +276 -39
- 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/sandbox.rb +39 -20
- data/lib/microsandbox/version.rb +1 -1
- data/lib/microsandbox.rb +20 -1
- data/sig/microsandbox.rbs +1 -0
- metadata +1 -1
|
@@ -16,23 +16,23 @@ use microsandbox::sandbox::{FsReadStream, FsWriteSink};
|
|
|
16
16
|
use tokio::sync::Mutex;
|
|
17
17
|
|
|
18
18
|
use crate::error;
|
|
19
|
-
use crate::runtime::{block_on, ruby};
|
|
19
|
+
use crate::runtime::{block_on, ruby, ForkGuarded};
|
|
20
20
|
|
|
21
21
|
#[magnus::wrap(class = "Microsandbox::Native::FsReadStream", free_immediately, size)]
|
|
22
22
|
pub struct FsReadStreamHandle {
|
|
23
|
-
inner: Arc<Mutex<FsReadStream
|
|
23
|
+
inner: ForkGuarded<Arc<Mutex<FsReadStream>>>,
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
impl FsReadStreamHandle {
|
|
27
27
|
pub fn new(stream: FsReadStream) -> Self {
|
|
28
28
|
Self {
|
|
29
|
-
inner: Arc::new(Mutex::new(stream)),
|
|
29
|
+
inner: ForkGuarded::new("Microsandbox::FsReadStream", Arc::new(Mutex::new(stream))),
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/// Next chunk of bytes (ASCII-8BIT), or nil at end of stream.
|
|
34
34
|
fn recv(&self) -> Result<Option<RString>, Error> {
|
|
35
|
-
let inner = Arc::clone(
|
|
35
|
+
let inner = Arc::clone(self.inner.get()?);
|
|
36
36
|
match block_on(async move { inner.lock().await.recv().await })?.map_err(error::to_ruby)? {
|
|
37
37
|
Some(bytes) => Ok(Some(ruby().str_from_slice(bytes.as_ref()))),
|
|
38
38
|
None => Ok(None),
|
|
@@ -42,13 +42,16 @@ impl FsReadStreamHandle {
|
|
|
42
42
|
|
|
43
43
|
#[magnus::wrap(class = "Microsandbox::Native::FsWriteSink", free_immediately, size)]
|
|
44
44
|
pub struct FsWriteSinkHandle {
|
|
45
|
-
inner: Arc<Mutex<Option<FsWriteSink
|
|
45
|
+
inner: ForkGuarded<Arc<Mutex<Option<FsWriteSink>>>>,
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
impl FsWriteSinkHandle {
|
|
49
49
|
pub fn new(sink: FsWriteSink) -> Self {
|
|
50
50
|
Self {
|
|
51
|
-
inner:
|
|
51
|
+
inner: ForkGuarded::new(
|
|
52
|
+
"Microsandbox::FsWriteSink",
|
|
53
|
+
Arc::new(Mutex::new(Some(sink))),
|
|
54
|
+
),
|
|
52
55
|
}
|
|
53
56
|
}
|
|
54
57
|
|
|
@@ -56,7 +59,7 @@ impl FsWriteSinkHandle {
|
|
|
56
59
|
fn write(&self, data: RString) -> Result<(), Error> {
|
|
57
60
|
// Copy out while the GVL is held (GC.compact could move the buffer).
|
|
58
61
|
let bytes = unsafe { data.as_slice() }.to_vec();
|
|
59
|
-
let inner = Arc::clone(
|
|
62
|
+
let inner = Arc::clone(self.inner.get()?);
|
|
60
63
|
let result = block_on(async move {
|
|
61
64
|
let guard = inner.lock().await;
|
|
62
65
|
match guard.as_ref() {
|
|
@@ -72,7 +75,7 @@ impl FsWriteSinkHandle {
|
|
|
72
75
|
|
|
73
76
|
/// Flush and close the sink. Idempotent.
|
|
74
77
|
fn close(&self) -> Result<(), Error> {
|
|
75
|
-
let inner = Arc::clone(
|
|
78
|
+
let inner = Arc::clone(self.inner.get()?);
|
|
76
79
|
// Take AND close in ONE `block_on`. `block_on` returns `Err` when an
|
|
77
80
|
// interrupt is delivered before it starts; as two calls, an interrupt
|
|
78
81
|
// between them would drop the sink the first one took without ever
|
|
@@ -47,10 +47,11 @@ pub struct HostPaths {
|
|
|
47
47
|
|
|
48
48
|
impl HostPaths {
|
|
49
49
|
/// Capture which backend the create call is about to dispatch to.
|
|
50
|
-
pub fn for_default_backend() -> Self {
|
|
51
|
-
|
|
50
|
+
pub fn for_default_backend() -> Result<Self, Error> {
|
|
51
|
+
crate::backend::ensure_this_process_backend()?;
|
|
52
|
+
Ok(Self {
|
|
52
53
|
local: matches!(microsandbox::default_backend().kind(), BackendKind::Local),
|
|
53
|
-
}
|
|
54
|
+
})
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
/// Anchor a relative host path to the current directory. Absolute paths
|
data/ext/microsandbox/src/lib.rs
CHANGED
|
@@ -46,8 +46,10 @@ fn all_sandbox_metrics() -> Result<RHash, Error> {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/// Download and install the `msb` runtime + `libkrunfw` into `~/.microsandbox`.
|
|
49
|
+
/// Never touches the backend, so a forked child does not take the inherited
|
|
50
|
+
/// one over here (see `runtime::block_on_runtime_install`).
|
|
49
51
|
fn install() -> Result<(), Error> {
|
|
50
|
-
runtime::
|
|
52
|
+
runtime::block_on_runtime_install(microsandbox::setup::install())?.map_err(error::to_ruby)
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
/// Customizable install via the core `Setup` builder. `opts`: base_dir (install
|
|
@@ -84,7 +86,8 @@ fn setup(opts: RHash) -> Result<(), Error> {
|
|
|
84
86
|
.force(force)
|
|
85
87
|
.build(),
|
|
86
88
|
};
|
|
87
|
-
|
|
89
|
+
// Backend-free, like `install`.
|
|
90
|
+
runtime::block_on_runtime_install(setup.install())?.map_err(error::to_ruby)
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
/// Whether the `msb` runtime + `libkrunfw` are installed and resolvable.
|
|
@@ -108,6 +111,7 @@ fn set_runtime_libkrunfw_path(path: String) {
|
|
|
108
111
|
/// no async) — runs on the Ruby thread; resolves the path from the local
|
|
109
112
|
/// backend's config.
|
|
110
113
|
fn resolved_msb_path() -> Result<String, Error> {
|
|
114
|
+
backend::ensure_this_process_backend()?;
|
|
111
115
|
let backend = microsandbox::default_backend();
|
|
112
116
|
// Shim-only entry point with no SDK `Operation` — report the Ruby-facing
|
|
113
117
|
// name (`Microsandbox.runtime_path`), like Python's name-based local_only.
|
|
@@ -118,10 +122,52 @@ fn resolved_msb_path() -> Result<String, Error> {
|
|
|
118
122
|
Ok(path.to_string_lossy().into_owned())
|
|
119
123
|
}
|
|
120
124
|
|
|
125
|
+
/// This process's fork generation (see `runtime::ProcessIdentity`): how many
|
|
126
|
+
/// `fork`s led from the process that loaded the extension to this one. A
|
|
127
|
+
/// diagnostic for the fork specs, not part of the Ruby API (nothing in
|
|
128
|
+
/// `lib/` calls it, and `Native` is the extension's internal namespace).
|
|
129
|
+
fn fork_generation() -> u32 {
|
|
130
|
+
runtime::ProcessIdentity::current().generation()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// Whether two given identities are the same process, by the two comparisons
|
|
134
|
+
/// every ownership decision uses: `PartialEq` (the runtime cell, handles,
|
|
135
|
+
/// `with_backend` scopes) and the packed word (the backend record). Returns
|
|
136
|
+
/// `[equal, same_bits]`. A spec-only diagnostic, like `fork_generation`:
|
|
137
|
+
/// pid reuse cannot be staged on demand, so this is where the specs pin that
|
|
138
|
+
/// both the pid and the generation take part. Pure: no state is read or
|
|
139
|
+
/// changed.
|
|
140
|
+
fn fork_identity_equal(pid_a: u32, gen_a: u32, pid_b: u32, gen_b: u32) -> (bool, bool) {
|
|
141
|
+
let a = runtime::ProcessIdentity::from_parts(pid_a, gen_a);
|
|
142
|
+
let b = runtime::ProcessIdentity::from_parts(pid_b, gen_b);
|
|
143
|
+
(a == b, a.to_bits() == b.to_bits())
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// Run a handle's real ownership check (`ForkGuarded::get_in`) for a handle
|
|
147
|
+
/// created by the process (`born_pid`, `born_gen`) and used in (`cur_pid`,
|
|
148
|
+
/// `cur_gen`): `nil` when it is allowed, the `ForkedHandleError` that `get`
|
|
149
|
+
/// would raise otherwise — including the equal-pid wording. Spec-only and
|
|
150
|
+
/// pure, like `fork_identity_equal`; the guard holds no payload.
|
|
151
|
+
fn fork_identity_guard(
|
|
152
|
+
born_pid: u32,
|
|
153
|
+
born_gen: u32,
|
|
154
|
+
cur_pid: u32,
|
|
155
|
+
cur_gen: u32,
|
|
156
|
+
) -> Result<(), Error> {
|
|
157
|
+
let born = runtime::ProcessIdentity::from_parts(born_pid, born_gen);
|
|
158
|
+
let cur = runtime::ProcessIdentity::from_parts(cur_pid, cur_gen);
|
|
159
|
+
let guard = runtime::ForkGuarded::owned_by(born, "Microsandbox::VolumeFs", ());
|
|
160
|
+
guard.get_in(cur).map(|_| ())
|
|
161
|
+
}
|
|
162
|
+
|
|
121
163
|
/// magnus entry point. RubyGems loads this via `require "microsandbox/microsandbox_rb"`,
|
|
122
164
|
/// which calls `Init_microsandbox_rb` (matching the cdylib `[lib] name`).
|
|
123
165
|
#[magnus::init]
|
|
124
166
|
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
167
|
+
// Before anything can fork: from here on every fork's child gets a fork
|
|
168
|
+
// generation one higher than its parent's (see `runtime::ProcessIdentity`).
|
|
169
|
+
runtime::register_fork_handler();
|
|
170
|
+
|
|
125
171
|
let module = ruby.define_module("Microsandbox")?;
|
|
126
172
|
let native = module.define_module("Native")?;
|
|
127
173
|
|
|
@@ -136,6 +182,9 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
136
182
|
)?;
|
|
137
183
|
native.define_singleton_method("resolved_msb_path", function!(resolved_msb_path, 0))?;
|
|
138
184
|
native.define_singleton_method("all_sandbox_metrics", function!(all_sandbox_metrics, 0))?;
|
|
185
|
+
native.define_singleton_method("fork_generation", function!(fork_generation, 0))?;
|
|
186
|
+
native.define_singleton_method("fork_identity_equal", function!(fork_identity_equal, 4))?;
|
|
187
|
+
native.define_singleton_method("fork_identity_guard", function!(fork_identity_guard, 4))?;
|
|
139
188
|
|
|
140
189
|
backend::define(ruby, &native)?;
|
|
141
190
|
sandbox::define(ruby, &native)?;
|
|
@@ -41,23 +41,125 @@
|
|
|
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
46
|
use std::sync::atomic::{AtomicPtr, AtomicU32, Ordering};
|
|
46
|
-
use std::sync::Mutex;
|
|
47
47
|
|
|
48
48
|
use magnus::{Error, Ruby, TypedData};
|
|
49
49
|
use tokio::runtime::Runtime;
|
|
50
50
|
|
|
51
51
|
use crate::error;
|
|
52
52
|
|
|
53
|
-
/// The current Ruby handle. Safe to call from any bound method
|
|
54
|
-
/// conversion: we always hold the GVL there
|
|
55
|
-
/// returns and re-acquires it).
|
|
53
|
+
/// The current Ruby handle. Safe to call from any bound method, value
|
|
54
|
+
/// conversion or error construction (`error::*`): we always hold the GVL there
|
|
55
|
+
/// (both run after `block_on` returns and re-acquires it).
|
|
56
|
+
///
|
|
57
|
+
/// Panics when the calling thread does not hold the GVL (a `nogvl` callback, a
|
|
58
|
+
/// tokio worker): `Ruby::get` checks that in release builds too, so a misplaced
|
|
59
|
+
/// call fails loudly instead of touching Ruby without the lock.
|
|
56
60
|
pub fn ruby() -> Ruby {
|
|
57
|
-
Ruby::get().expect("microsandbox: not on a Ruby thread")
|
|
61
|
+
Ruby::get().expect("microsandbox: not on a Ruby thread holding the GVL")
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
//--------------------------------------------------------------------------------------------------
|
|
65
|
+
// Process identity
|
|
66
|
+
//--------------------------------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
/// Which process this is, for fork safety: the pid plus a fork generation.
|
|
69
|
+
///
|
|
70
|
+
/// Everything fork-aware in the extension — the runtime cell below, the
|
|
71
|
+
/// backend record in `backend.rs`, [`ForkGuarded`] — tags what it builds with
|
|
72
|
+
/// the identity of the process that built it and compares that against
|
|
73
|
+
/// [`ProcessIdentity::current`] before use. Both halves are compared:
|
|
74
|
+
///
|
|
75
|
+
/// * The pid alone is not enough. A descendant can end up with the pid of an
|
|
76
|
+
/// ancestor whose state it inherited, if the pid counter wraps onto exactly
|
|
77
|
+
/// that value and no SDK call happened in between; it would then pass for
|
|
78
|
+
/// that ancestor.
|
|
79
|
+
/// * The generation alone is not enough either. It counts the `fork`s that
|
|
80
|
+
/// ran this library's atfork child handler, and a process can be created
|
|
81
|
+
/// without running it — `vfork`, a raw `clone` — in which case only the pid
|
|
82
|
+
/// tells the child apart.
|
|
83
|
+
///
|
|
84
|
+
/// Every `fork(2)` that runs atfork handlers (which `Kernel#fork` /
|
|
85
|
+
/// `Process.fork` always do: Ruby calls libc's `fork`) gives the child a
|
|
86
|
+
/// generation one higher than its parent's, so parent and child differ even
|
|
87
|
+
/// when their pids are equal.
|
|
88
|
+
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
89
|
+
pub struct ProcessIdentity {
|
|
90
|
+
pub pid: u32,
|
|
91
|
+
generation: u32,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// How many `fork`s, counted from the process that loaded the extension, led
|
|
95
|
+
/// to this process. Incremented only by [`on_fork_child`], in a child, and
|
|
96
|
+
/// inherited (as plain memory) by every child. 32 bits: it would take four
|
|
97
|
+
/// billion nested forks in one lineage to wrap, and a wrap matters only if
|
|
98
|
+
/// the pid matches as well.
|
|
99
|
+
static FORK_GENERATION: AtomicU32 = AtomicU32::new(0);
|
|
100
|
+
|
|
101
|
+
/// The `pthread_atfork` child handler. It runs in the new child before `fork`
|
|
102
|
+
/// returns there, while the child is single-threaded, and must be
|
|
103
|
+
/// async-signal-safe: one lock-free atomic add, nothing else (no allocation,
|
|
104
|
+
/// no lock, no Ruby). `Relaxed` is enough: the only thread that exists to read
|
|
105
|
+
/// the counter is this one, and threads started later see it through the
|
|
106
|
+
/// spawn's own synchronization. The parent never changes its counter.
|
|
107
|
+
extern "C" fn on_fork_child() {
|
|
108
|
+
FORK_GENERATION.fetch_add(1, Ordering::Relaxed);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// Register [`on_fork_child`]. Called once, from the extension's init.
|
|
112
|
+
///
|
|
113
|
+
/// Ruby never unloads (`dlclose`s) an extension, so the handler's code stays
|
|
114
|
+
/// mapped for the life of the process, as `pthread_atfork` requires — handlers
|
|
115
|
+
/// cannot be unregistered. If registration fails (`ENOMEM`), the generation
|
|
116
|
+
/// stays 0 and identity degrades to the pid alone, which is how the extension
|
|
117
|
+
/// behaved before the generation existed; that is not worth refusing to load
|
|
118
|
+
/// over. Platforms without `pthread_atfork` (the gem ships for macOS and
|
|
119
|
+
/// Linux only) get the same pid-only identity.
|
|
120
|
+
pub fn register_fork_handler() {
|
|
121
|
+
static ONCE: std::sync::Once = std::sync::Once::new();
|
|
122
|
+
ONCE.call_once(|| {
|
|
123
|
+
#[cfg(unix)]
|
|
124
|
+
// SAFETY: `on_fork_child` is an `extern "C" fn()` that stays valid for
|
|
125
|
+
// the life of the process (see above) and is async-signal-safe.
|
|
126
|
+
unsafe {
|
|
127
|
+
let _ = libc::pthread_atfork(None, None, Some(on_fork_child));
|
|
128
|
+
}
|
|
129
|
+
});
|
|
58
130
|
}
|
|
59
131
|
|
|
60
|
-
|
|
132
|
+
impl ProcessIdentity {
|
|
133
|
+
/// This process's identity. The single `std::process::id()` call site.
|
|
134
|
+
pub fn current() -> Self {
|
|
135
|
+
Self {
|
|
136
|
+
pid: std::process::id(),
|
|
137
|
+
generation: FORK_GENERATION.load(Ordering::Relaxed),
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// An identity from its two halves — ONLY for the fork-identity
|
|
142
|
+
/// diagnostics in `lib.rs`, which let the specs compare identities that no
|
|
143
|
+
/// real fork can produce on demand (an equal pid with another generation).
|
|
144
|
+
pub fn from_parts(pid: u32, generation: u32) -> Self {
|
|
145
|
+
Self { pid, generation }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// The fork generation, for `Native.fork_generation` (a diagnostic the
|
|
149
|
+
/// fork specs read; not part of the Ruby API).
|
|
150
|
+
pub fn generation(self) -> u32 {
|
|
151
|
+
self.generation
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// Both halves in one word, for a lock-free atomic record. `0` is never a
|
|
155
|
+
/// real identity (no user process has pid 0), so it can mean "none".
|
|
156
|
+
pub fn to_bits(self) -> u64 {
|
|
157
|
+
(u64::from(self.pid) << 32) | u64::from(self.generation)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// The process-wide tokio runtime, tagged with the identity of the process it
|
|
162
|
+
// was built under (see `ProcessIdentity`). A
|
|
61
163
|
// multi-threaded runtime owns worker + I/O-driver threads, and `fork(2)` copies
|
|
62
164
|
// ONLY the calling thread — so a child that inherits this runtime has a runtime
|
|
63
165
|
// whose threads are gone: `block_on` can still drive the current thread, but the
|
|
@@ -65,16 +167,16 @@ pub fn ruby() -> Ruby {
|
|
|
65
167
|
// connections stall/drop mid-use. This is hit in practice by forking job servers
|
|
66
168
|
// (Solid Queue, Resque) and clustered web servers (Puma workers).
|
|
67
169
|
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
static
|
|
170
|
+
// The runtime and its process identity live in ONE heap cell behind ONE atomic
|
|
171
|
+
// pointer, so a reader sees either a whole cell or none: there is no window in
|
|
172
|
+
// which the pointer is new and the identity old (or the reverse), and no mutex
|
|
173
|
+
// that a fork could capture locked or that a panic could poison.
|
|
174
|
+
struct RuntimeCell {
|
|
175
|
+
owner: ProcessIdentity,
|
|
176
|
+
rt: Runtime,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
static RUNTIME: AtomicPtr<RuntimeCell> = AtomicPtr::new(std::ptr::null_mut());
|
|
78
180
|
|
|
79
181
|
fn build_runtime() -> Runtime {
|
|
80
182
|
tokio::runtime::Builder::new_multi_thread()
|
|
@@ -85,32 +187,126 @@ fn build_runtime() -> Runtime {
|
|
|
85
187
|
}
|
|
86
188
|
|
|
87
189
|
/// The multi-threaded tokio runtime for THIS process, built on first use and
|
|
88
|
-
/// rebuilt after a `fork(2)` (
|
|
190
|
+
/// rebuilt after a `fork(2)` (see the note above).
|
|
89
191
|
pub fn runtime() -> &'static Runtime {
|
|
90
|
-
let cur =
|
|
91
|
-
let
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
192
|
+
let cur = ProcessIdentity::current();
|
|
193
|
+
let mut observed = RUNTIME.load(Ordering::Acquire);
|
|
194
|
+
// SAFETY (every dereference below): a non-null pointer in `RUNTIME` came
|
|
195
|
+
// from `Box::into_raw` and is never freed once published, so it is valid
|
|
196
|
+
// for `'static`.
|
|
197
|
+
if let Some(cell) = unsafe { observed.as_ref() } {
|
|
198
|
+
if cell.owner == cur {
|
|
199
|
+
// Built in this process: its threads are alive here.
|
|
200
|
+
return &cell.rt;
|
|
201
|
+
}
|
|
96
202
|
}
|
|
97
203
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
204
|
+
// Slow path: no runtime yet, or the one we see was built by a parent
|
|
205
|
+
// process. Build ours and publish it with a compare-exchange.
|
|
206
|
+
let fresh = Box::into_raw(Box::new(RuntimeCell {
|
|
207
|
+
owner: cur,
|
|
208
|
+
rt: build_runtime(),
|
|
209
|
+
}));
|
|
210
|
+
loop {
|
|
211
|
+
match RUNTIME.compare_exchange(observed, fresh, Ordering::AcqRel, Ordering::Acquire) {
|
|
212
|
+
// Published. The cell we replaced (if any) belonged to a parent
|
|
213
|
+
// process and is LEAKED on purpose, never dropped: its worker
|
|
214
|
+
// threads did not survive the fork, and `Runtime`'s `Drop` joins
|
|
215
|
+
// them. One runtime is leaked per forked process that uses the
|
|
216
|
+
// SDK.
|
|
217
|
+
Ok(_) => return unsafe { &(*fresh).rt },
|
|
218
|
+
Err(winner) => {
|
|
219
|
+
if let Some(cell) = unsafe { winner.as_ref() } {
|
|
220
|
+
if cell.owner == cur {
|
|
221
|
+
// Another thread of THIS process won the race. Our
|
|
222
|
+
// cell was never published and its threads are alive
|
|
223
|
+
// here, so dropping it is safe (and joins them).
|
|
224
|
+
drop(unsafe { Box::from_raw(fresh) });
|
|
225
|
+
return &cell.rt;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
observed = winner;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
105
231
|
}
|
|
232
|
+
}
|
|
106
233
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
234
|
+
/// The Rust payload of a wrapped Ruby object, bound to the process that
|
|
235
|
+
/// created it.
|
|
236
|
+
///
|
|
237
|
+
/// A handle that crosses a `fork(2)` is useless in the child and dangerous to
|
|
238
|
+
/// destroy there: it owns tasks, channels and connections of the parent's
|
|
239
|
+
/// runtime, clones of the parent's backend, the parent-watchdog pipe of a
|
|
240
|
+
/// sandbox. So every `#[magnus::wrap]` type keeps its payload in one of these:
|
|
241
|
+
///
|
|
242
|
+
/// * [`get`](Self::get) — the only way to the payload for anything that acts
|
|
243
|
+
/// on it — fails with `Microsandbox::ForkedHandleError` in any process other
|
|
244
|
+
/// than the one that built the value. `Deref` is deliberately not
|
|
245
|
+
/// implemented: an access that skips the check should not compile.
|
|
246
|
+
/// * `Drop` runs the payload's destructor only in that same process. A forked
|
|
247
|
+
/// child that garbage-collects an inherited handle — at the latest when it
|
|
248
|
+
/// exits: Ruby frees every `T_DATA` object in `rb_objspace_call_finalizer` —
|
|
249
|
+
/// leaks it instead.
|
|
250
|
+
pub struct ForkGuarded<T> {
|
|
251
|
+
owner: ProcessIdentity,
|
|
252
|
+
/// The Ruby class users know the handle by, for the error message.
|
|
253
|
+
class: &'static str,
|
|
254
|
+
inner: ManuallyDrop<T>,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
impl<T> ForkGuarded<T> {
|
|
258
|
+
pub fn new(class: &'static str, inner: T) -> Self {
|
|
259
|
+
Self {
|
|
260
|
+
owner: ProcessIdentity::current(),
|
|
261
|
+
class,
|
|
262
|
+
inner: ManuallyDrop::new(inner),
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/// A guard owned by `owner` instead of this process — ONLY for the
|
|
267
|
+
/// fork-identity diagnostic in `lib.rs`, which runs the real ownership
|
|
268
|
+
/// decision ([`get_in`](Self::get_in)) on identities given by a spec.
|
|
269
|
+
pub fn owned_by(owner: ProcessIdentity, class: &'static str, inner: T) -> Self {
|
|
270
|
+
Self {
|
|
271
|
+
owner,
|
|
272
|
+
class,
|
|
273
|
+
inner: ManuallyDrop::new(inner),
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/// The payload, or `ForkedHandleError` when this is not the process that
|
|
278
|
+
/// created it.
|
|
279
|
+
pub fn get(&self) -> Result<&T, Error> {
|
|
280
|
+
self.get_in(ProcessIdentity::current())
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/// [`get`](Self::get) as decided in the process identified by `cur`.
|
|
284
|
+
pub fn get_in(&self, cur: ProcessIdentity) -> Result<&T, Error> {
|
|
285
|
+
if self.owner == cur {
|
|
286
|
+
Ok(&self.inner)
|
|
287
|
+
} else {
|
|
288
|
+
Err(error::forked_handle(self.class, self.owner.pid, cur.pid))
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/// The payload without the process check — ONLY for accessors that copy
|
|
293
|
+
/// plain data out of the value's own memory (a name, an id, a cached
|
|
294
|
+
/// status). Those stay readable in a forked child, which needs e.g. the
|
|
295
|
+
/// name to open its own handle. Never use this for anything that takes a
|
|
296
|
+
/// lock, touches a file descriptor, or reaches the runtime or the backend.
|
|
297
|
+
pub fn peek(&self) -> &T {
|
|
298
|
+
&self.inner
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
impl<T> Drop for ForkGuarded<T> {
|
|
303
|
+
fn drop(&mut self) {
|
|
304
|
+
if self.owner == ProcessIdentity::current() {
|
|
305
|
+
// SAFETY: dropped exactly once, here; `inner` is not used again.
|
|
306
|
+
unsafe { ManuallyDrop::drop(&mut self.inner) };
|
|
307
|
+
}
|
|
308
|
+
// Otherwise: inherited across a fork. Leak it (see the type docs).
|
|
309
|
+
}
|
|
114
310
|
}
|
|
115
311
|
|
|
116
312
|
/// Resolve the Ruby class of a `#[magnus::wrap]` type now, at extension load.
|
|
@@ -123,6 +319,13 @@ pub fn runtime() -> &'static Runtime {
|
|
|
123
319
|
/// Every module's `define` calls this for each type it wraps, right after
|
|
124
320
|
/// defining the class, so wrapping a value afterwards is `TypedData::class`
|
|
125
321
|
/// reading a resolved static plus `rb_data_typed_object_wrap`.
|
|
322
|
+
///
|
|
323
|
+
/// Doing it at load also keeps that first resolution away from concurrent
|
|
324
|
+
/// callers, which matters since magnus 0.9: the static is a magnus `Lazy`, an
|
|
325
|
+
/// `OnceLock` whose initializer (the `const_get`) runs while holding the lock.
|
|
326
|
+
/// That `funcall` can switch Ruby threads, and a second thread reaching the
|
|
327
|
+
/// same lock would block on it while holding the GVL the first one needs to
|
|
328
|
+
/// finish.
|
|
126
329
|
pub fn preload_class<T: TypedData>(ruby: &Ruby) {
|
|
127
330
|
let _ = T::class(ruby);
|
|
128
331
|
}
|
|
@@ -301,10 +504,44 @@ where
|
|
|
301
504
|
|
|
302
505
|
/// Drive a future to completion on the shared runtime with the GVL released.
|
|
303
506
|
///
|
|
304
|
-
/// `Err` means the future was never polled (see [`nogvl`]
|
|
305
|
-
/// `?` before looking at the future's own output:
|
|
507
|
+
/// `Err` means the future was never polled (see [`nogvl`], and the fork check
|
|
508
|
+
/// below); propagate it with `?` before looking at the future's own output:
|
|
306
509
|
/// `block_on(fut)?.map_err(error::to_ruby)?`.
|
|
510
|
+
///
|
|
511
|
+
/// Every native call goes through this, with one deliberate exception: the
|
|
512
|
+
/// runtime installer, see [`block_on_runtime_install`].
|
|
307
513
|
pub fn block_on<F>(fut: F) -> Result<F::Output, Error>
|
|
514
|
+
where
|
|
515
|
+
F: Future,
|
|
516
|
+
{
|
|
517
|
+
// Many core operations resolve the process-wide default backend while they
|
|
518
|
+
// run. After a fork that backend is the parent's; replace it now, while we
|
|
519
|
+
// still hold the GVL (the check takes a mutex and may build a Ruby
|
|
520
|
+
// exception — neither is allowed once the GVL is released).
|
|
521
|
+
crate::backend::ensure_this_process_backend()?;
|
|
522
|
+
nogvl(|| runtime().block_on(fut))
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/// [`block_on`] without its backend check, for the runtime installer ONLY:
|
|
526
|
+
/// `Native.install` and `Native.setup` (`microsandbox::setup`).
|
|
527
|
+
///
|
|
528
|
+
/// The check at the top of [`block_on`] is deliberate and stays there: it is
|
|
529
|
+
/// what makes every other native call safe after a fork. This entry is not a
|
|
530
|
+
/// cheaper alternative to it. It exists because the installer never touches
|
|
531
|
+
/// the default backend or the catalog — `Setup::install` creates the
|
|
532
|
+
/// `bin`/`lib` directories, runs the installed `msb --version`, copies a CI
|
|
533
|
+
/// build or downloads and unpacks the release bundle, and checks that the two
|
|
534
|
+
/// files exist, all under the base directory it resolves from `MSB_HOME` —
|
|
535
|
+
/// so in a forked child the check would only rebuild (and leak) a backend
|
|
536
|
+
/// nothing uses, or fail the install when the inherited backend selection no
|
|
537
|
+
/// longer resolves there. Skipping it leaves the backend record as it was:
|
|
538
|
+
/// the child's next call that does reach the backend is the one that takes
|
|
539
|
+
/// it over.
|
|
540
|
+
///
|
|
541
|
+
/// Before adding a caller, trace its core path and confirm it never reaches
|
|
542
|
+
/// `microsandbox::default_backend()` (directly or through a `Sandbox`,
|
|
543
|
+
/// `Volume`, `Image`, `Snapshot` or agent function that resolves it).
|
|
544
|
+
pub fn block_on_runtime_install<F>(fut: F) -> Result<F::Output, Error>
|
|
308
545
|
where
|
|
309
546
|
F: Future,
|
|
310
547
|
{
|