microsandbox-rb 0.17.3 → 0.17.5
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 +120 -1
- data/Cargo.lock +81 -61
- data/DESIGN.md +134 -41
- data/README.md +4 -2
- data/SECURITY.md +8 -0
- data/docs/filesystem.md +2 -1
- data/docs/runtime.md +33 -3
- data/docs/surface.md +4 -1
- data/ext/microsandbox/Cargo.toml +9 -2
- data/ext/microsandbox/src/backend.rs +28 -24
- data/ext/microsandbox/src/error.rs +37 -37
- data/ext/microsandbox/src/lib.rs +50 -2
- data/ext/microsandbox/src/runtime.rs +173 -27
- data/lib/microsandbox/sandbox.rb +39 -20
- data/lib/microsandbox/version.rb +1 -1
- metadata +1 -1
|
@@ -9,6 +9,8 @@ use magnus::{value::ReprValue, Error, ExceptionClass, Module, RClass, RModule, R
|
|
|
9
9
|
use microsandbox::{AgentClientError, MicrosandboxError, Operation, UnsupportedReason};
|
|
10
10
|
use microsandbox_network::policy::BuildError;
|
|
11
11
|
|
|
12
|
+
use crate::runtime::ruby;
|
|
13
|
+
|
|
12
14
|
/// The Ruby class (relative to the `Microsandbox` module) for a core error.
|
|
13
15
|
/// `"Error"` is the base class; anything else is a named subclass.
|
|
14
16
|
fn class_name(err: &MicrosandboxError) -> &'static str {
|
|
@@ -88,16 +90,16 @@ fn exception_class(ruby: &Ruby, name: &str) -> Option<ExceptionClass> {
|
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
/// Convert a core error into a Ruby exception, preserving the typed class.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
///
|
|
94
|
+
/// Every constructor in this module builds a Ruby exception class, so it must
|
|
95
|
+
/// run on a Ruby thread holding the GVL: in a bound method, never inside a
|
|
96
|
+
/// `block_on` future or a `nogvl` callback (those return the core error and
|
|
97
|
+
/// get mapped after `block_on` returns). There is no exception class to build
|
|
98
|
+
/// without the GVL, so [`ruby`] panics instead of handing out a handle that
|
|
99
|
+
/// isn't backed by one.
|
|
94
100
|
pub fn to_ruby(err: MicrosandboxError) -> Error {
|
|
95
101
|
let message = err.to_string();
|
|
96
|
-
let ruby =
|
|
97
|
-
Ok(ruby) => ruby,
|
|
98
|
-
// Not on a Ruby thread (should never happen from a bound method).
|
|
99
|
-
Err(_) => return Error::new(magnus::exception::runtime_error(), message),
|
|
100
|
-
};
|
|
102
|
+
let ruby = ruby();
|
|
101
103
|
|
|
102
104
|
// `Unsupported` gets a Ruby-idiom message (`sandbox.kill` instead of
|
|
103
105
|
// `Sandbox::kill`) plus structured `operation` / `hint` attributes on the
|
|
@@ -116,15 +118,8 @@ pub fn to_ruby(err: MicrosandboxError) -> Error {
|
|
|
116
118
|
/// backend but have no SDK [`Operation`] (Ruby-only diagnostic hooks such as
|
|
117
119
|
/// `Microsandbox.runtime_path`). `name` is the Ruby-facing API name. Mirrors
|
|
118
120
|
/// the Python SDK's name-based `local_only` helper.
|
|
119
|
-
#[allow(deprecated)]
|
|
120
121
|
pub fn local_only(name: &str) -> Error {
|
|
121
|
-
|
|
122
|
-
Ok(ruby) => unsupported_error(&ruby, name, "use a local backend"),
|
|
123
|
-
Err(_) => Error::new(
|
|
124
|
-
magnus::exception::runtime_error(),
|
|
125
|
-
format!("{name} is not supported by this backend: use a local backend"),
|
|
126
|
-
),
|
|
127
|
-
}
|
|
122
|
+
unsupported_error(&ruby(), name, "use a local backend")
|
|
128
123
|
}
|
|
129
124
|
|
|
130
125
|
/// Build a `Microsandbox::UnsupportedError` carrying the rendered message and
|
|
@@ -191,32 +186,37 @@ fn camel_to_snake(name: &str) -> String {
|
|
|
191
186
|
|
|
192
187
|
/// A plain `Microsandbox::Error` (base) with a custom message — used for
|
|
193
188
|
/// binding-level validation errors that have no core counterpart.
|
|
194
|
-
#[allow(deprecated)]
|
|
195
189
|
pub fn base_error(message: impl Into<String>) -> Error {
|
|
196
190
|
let message = message.into();
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
},
|
|
202
|
-
Err(_) => Error::new(magnus::exception::runtime_error(), message),
|
|
191
|
+
let ruby = ruby();
|
|
192
|
+
match exception_class(&ruby, "Error") {
|
|
193
|
+
Some(class) => Error::new(class, message),
|
|
194
|
+
None => Error::new(ruby.exception_runtime_error(), message),
|
|
203
195
|
}
|
|
204
196
|
}
|
|
205
197
|
|
|
206
|
-
/// `Microsandbox::ForkedHandleError`: a handle created in process
|
|
207
|
-
/// used in
|
|
208
|
-
/// `class` is the Ruby class users know the handle
|
|
209
|
-
|
|
198
|
+
/// `Microsandbox::ForkedHandleError`: a handle created in the process with pid
|
|
199
|
+
/// `born` was used in a fork descendant with pid `cur` (see
|
|
200
|
+
/// `runtime::ForkGuarded`). `class` is the Ruby class users know the handle
|
|
201
|
+
/// by. The two pids are equal when the descendant was given its ancestor's
|
|
202
|
+
/// pid again (they differ in fork generation, which is not shown): that
|
|
203
|
+
/// ancestor has exited, so the message must not claim two processes or point
|
|
204
|
+
/// at the original.
|
|
210
205
|
pub fn forked_handle(class: &str, born: u32, cur: u32) -> Error {
|
|
211
|
-
let message =
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
206
|
+
let message = if born == cur {
|
|
207
|
+
format!(
|
|
208
|
+
"{class} was created in an earlier process that also had pid {born} and cannot be \
|
|
209
|
+
used in this forked process; create a new handle after fork"
|
|
210
|
+
)
|
|
211
|
+
} else {
|
|
212
|
+
format!(
|
|
213
|
+
"{class} was created in process {born} and cannot be used in forked process {cur}; \
|
|
214
|
+
create a new handle after fork (the original keeps working in process {born})"
|
|
215
|
+
)
|
|
216
|
+
};
|
|
217
|
+
let ruby = ruby();
|
|
218
|
+
match exception_class(&ruby, "ForkedHandleError") {
|
|
219
|
+
Some(class) => Error::new(class, message),
|
|
220
|
+
None => Error::new(ruby.exception_runtime_error(), message),
|
|
221
221
|
}
|
|
222
222
|
}
|
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.
|
|
@@ -119,10 +122,52 @@ fn resolved_msb_path() -> Result<String, Error> {
|
|
|
119
122
|
Ok(path.to_string_lossy().into_owned())
|
|
120
123
|
}
|
|
121
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
|
+
|
|
122
163
|
/// magnus entry point. RubyGems loads this via `require "microsandbox/microsandbox_rb"`,
|
|
123
164
|
/// which calls `Init_microsandbox_rb` (matching the cdylib `[lib] name`).
|
|
124
165
|
#[magnus::init]
|
|
125
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
|
+
|
|
126
171
|
let module = ruby.define_module("Microsandbox")?;
|
|
127
172
|
let native = module.define_module("Native")?;
|
|
128
173
|
|
|
@@ -137,6 +182,9 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
137
182
|
)?;
|
|
138
183
|
native.define_singleton_method("resolved_msb_path", function!(resolved_msb_path, 0))?;
|
|
139
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))?;
|
|
140
188
|
|
|
141
189
|
backend::define(ruby, &native)?;
|
|
142
190
|
sandbox::define(ruby, &native)?;
|
|
@@ -43,28 +43,123 @@ use std::ffi::c_void;
|
|
|
43
43
|
use std::future::Future;
|
|
44
44
|
use std::mem::ManuallyDrop;
|
|
45
45
|
use std::panic::{catch_unwind, AssertUnwindSafe};
|
|
46
|
-
use std::sync::atomic::{AtomicPtr, Ordering};
|
|
46
|
+
use std::sync::atomic::{AtomicPtr, AtomicU32, Ordering};
|
|
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")
|
|
58
62
|
}
|
|
59
63
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
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
|
+
}
|
|
65
159
|
}
|
|
66
160
|
|
|
67
|
-
// The process-wide tokio runtime, tagged with the
|
|
161
|
+
// The process-wide tokio runtime, tagged with the identity of the process it
|
|
162
|
+
// was built under (see `ProcessIdentity`). A
|
|
68
163
|
// multi-threaded runtime owns worker + I/O-driver threads, and `fork(2)` copies
|
|
69
164
|
// ONLY the calling thread — so a child that inherits this runtime has a runtime
|
|
70
165
|
// whose threads are gone: `block_on` can still drive the current thread, but the
|
|
@@ -72,12 +167,12 @@ pub fn current_pid() -> u32 {
|
|
|
72
167
|
// connections stall/drop mid-use. This is hit in practice by forking job servers
|
|
73
168
|
// (Solid Queue, Resque) and clustered web servers (Puma workers).
|
|
74
169
|
//
|
|
75
|
-
// The runtime and its
|
|
76
|
-
// reader sees either a whole cell or none: there is no window in
|
|
77
|
-
// pointer is new and the
|
|
78
|
-
// could capture locked or that a panic could poison.
|
|
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.
|
|
79
174
|
struct RuntimeCell {
|
|
80
|
-
|
|
175
|
+
owner: ProcessIdentity,
|
|
81
176
|
rt: Runtime,
|
|
82
177
|
}
|
|
83
178
|
|
|
@@ -94,13 +189,13 @@ fn build_runtime() -> Runtime {
|
|
|
94
189
|
/// The multi-threaded tokio runtime for THIS process, built on first use and
|
|
95
190
|
/// rebuilt after a `fork(2)` (see the note above).
|
|
96
191
|
pub fn runtime() -> &'static Runtime {
|
|
97
|
-
let cur =
|
|
192
|
+
let cur = ProcessIdentity::current();
|
|
98
193
|
let mut observed = RUNTIME.load(Ordering::Acquire);
|
|
99
194
|
// SAFETY (every dereference below): a non-null pointer in `RUNTIME` came
|
|
100
195
|
// from `Box::into_raw` and is never freed once published, so it is valid
|
|
101
196
|
// for `'static`.
|
|
102
197
|
if let Some(cell) = unsafe { observed.as_ref() } {
|
|
103
|
-
if cell.
|
|
198
|
+
if cell.owner == cur {
|
|
104
199
|
// Built in this process: its threads are alive here.
|
|
105
200
|
return &cell.rt;
|
|
106
201
|
}
|
|
@@ -109,7 +204,7 @@ pub fn runtime() -> &'static Runtime {
|
|
|
109
204
|
// Slow path: no runtime yet, or the one we see was built by a parent
|
|
110
205
|
// process. Build ours and publish it with a compare-exchange.
|
|
111
206
|
let fresh = Box::into_raw(Box::new(RuntimeCell {
|
|
112
|
-
|
|
207
|
+
owner: cur,
|
|
113
208
|
rt: build_runtime(),
|
|
114
209
|
}));
|
|
115
210
|
loop {
|
|
@@ -117,12 +212,12 @@ pub fn runtime() -> &'static Runtime {
|
|
|
117
212
|
// Published. The cell we replaced (if any) belonged to a parent
|
|
118
213
|
// process and is LEAKED on purpose, never dropped: its worker
|
|
119
214
|
// threads did not survive the fork, and `Runtime`'s `Drop` joins
|
|
120
|
-
// them. One runtime is leaked per
|
|
215
|
+
// them. One runtime is leaked per forked process that uses the
|
|
121
216
|
// SDK.
|
|
122
217
|
Ok(_) => return unsafe { &(*fresh).rt },
|
|
123
218
|
Err(winner) => {
|
|
124
219
|
if let Some(cell) = unsafe { winner.as_ref() } {
|
|
125
|
-
if cell.
|
|
220
|
+
if cell.owner == cur {
|
|
126
221
|
// Another thread of THIS process won the race. Our
|
|
127
222
|
// cell was never published and its threads are alive
|
|
128
223
|
// here, so dropping it is safe (and joins them).
|
|
@@ -153,7 +248,7 @@ pub fn runtime() -> &'static Runtime {
|
|
|
153
248
|
/// exits: Ruby frees every `T_DATA` object in `rb_objspace_call_finalizer` —
|
|
154
249
|
/// leaks it instead.
|
|
155
250
|
pub struct ForkGuarded<T> {
|
|
156
|
-
|
|
251
|
+
owner: ProcessIdentity,
|
|
157
252
|
/// The Ruby class users know the handle by, for the error message.
|
|
158
253
|
class: &'static str,
|
|
159
254
|
inner: ManuallyDrop<T>,
|
|
@@ -162,7 +257,18 @@ pub struct ForkGuarded<T> {
|
|
|
162
257
|
impl<T> ForkGuarded<T> {
|
|
163
258
|
pub fn new(class: &'static str, inner: T) -> Self {
|
|
164
259
|
Self {
|
|
165
|
-
|
|
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,
|
|
166
272
|
class,
|
|
167
273
|
inner: ManuallyDrop::new(inner),
|
|
168
274
|
}
|
|
@@ -171,11 +277,15 @@ impl<T> ForkGuarded<T> {
|
|
|
171
277
|
/// The payload, or `ForkedHandleError` when this is not the process that
|
|
172
278
|
/// created it.
|
|
173
279
|
pub fn get(&self) -> Result<&T, Error> {
|
|
174
|
-
|
|
175
|
-
|
|
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 {
|
|
176
286
|
Ok(&self.inner)
|
|
177
287
|
} else {
|
|
178
|
-
Err(error::forked_handle(self.class, self.pid, cur))
|
|
288
|
+
Err(error::forked_handle(self.class, self.owner.pid, cur.pid))
|
|
179
289
|
}
|
|
180
290
|
}
|
|
181
291
|
|
|
@@ -191,7 +301,7 @@ impl<T> ForkGuarded<T> {
|
|
|
191
301
|
|
|
192
302
|
impl<T> Drop for ForkGuarded<T> {
|
|
193
303
|
fn drop(&mut self) {
|
|
194
|
-
if self.
|
|
304
|
+
if self.owner == ProcessIdentity::current() {
|
|
195
305
|
// SAFETY: dropped exactly once, here; `inner` is not used again.
|
|
196
306
|
unsafe { ManuallyDrop::drop(&mut self.inner) };
|
|
197
307
|
}
|
|
@@ -209,6 +319,13 @@ impl<T> Drop for ForkGuarded<T> {
|
|
|
209
319
|
/// Every module's `define` calls this for each type it wraps, right after
|
|
210
320
|
/// defining the class, so wrapping a value afterwards is `TypedData::class`
|
|
211
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.
|
|
212
329
|
pub fn preload_class<T: TypedData>(ruby: &Ruby) {
|
|
213
330
|
let _ = T::class(ruby);
|
|
214
331
|
}
|
|
@@ -390,6 +507,9 @@ where
|
|
|
390
507
|
/// `Err` means the future was never polled (see [`nogvl`], and the fork check
|
|
391
508
|
/// below); propagate it with `?` before looking at the future's own output:
|
|
392
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`].
|
|
393
513
|
pub fn block_on<F>(fut: F) -> Result<F::Output, Error>
|
|
394
514
|
where
|
|
395
515
|
F: Future,
|
|
@@ -401,3 +521,29 @@ where
|
|
|
401
521
|
crate::backend::ensure_this_process_backend()?;
|
|
402
522
|
nogvl(|| runtime().block_on(fut))
|
|
403
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>
|
|
545
|
+
where
|
|
546
|
+
F: Future,
|
|
547
|
+
{
|
|
548
|
+
nogvl(|| runtime().block_on(fut))
|
|
549
|
+
}
|
data/lib/microsandbox/sandbox.rb
CHANGED
|
@@ -497,7 +497,7 @@ module Microsandbox
|
|
|
497
497
|
opts = build_create_opts(**kwargs)
|
|
498
498
|
Microsandbox.ensure_runtime!
|
|
499
499
|
# Everything Ruby-level happens BEFORE `acquire`: its interrupt mask must
|
|
500
|
-
# cover only the native call
|
|
500
|
+
# cover only the native call, never caller-supplied code.
|
|
501
501
|
sandbox_name = name.to_s
|
|
502
502
|
sandbox = acquire { Native::Sandbox.create(sandbox_name, opts) }
|
|
503
503
|
return sandbox unless block_given?
|
|
@@ -603,17 +603,34 @@ module Microsandbox
|
|
|
603
603
|
# sandbox would be garbage and only GC would stop it, "eventually"; for
|
|
604
604
|
# `Timeout.timeout { Sandbox.create(...) }` that is not good enough. So:
|
|
605
605
|
#
|
|
606
|
-
# - the
|
|
607
|
-
# which holds a queued exception (or kill) back until the
|
|
608
|
-
# assigned. The mask covers only
|
|
609
|
-
#
|
|
610
|
-
# ({Microsandbox.ensure_runtime!})
|
|
611
|
-
#
|
|
612
|
-
#
|
|
613
|
-
#
|
|
614
|
-
#
|
|
615
|
-
#
|
|
616
|
-
#
|
|
606
|
+
# - the native call runs under `Thread.handle_interrupt(Object => :never)`,
|
|
607
|
+
# which holds a queued exception (or kill) back until the bare
|
|
608
|
+
# {Native::Sandbox} is assigned to a local. The mask covers only that
|
|
609
|
+
# uninterruptible call, so it changes no observable timing; runtime
|
|
610
|
+
# provisioning ({Microsandbox.ensure_runtime!}), the wrapper's
|
|
611
|
+
# construction and the block forms' `yield` stay outside it, with
|
|
612
|
+
# normal interrupt handling. For the same reason the block passed here
|
|
613
|
+
# must contain NOTHING but the native call: argument conversion
|
|
614
|
+
# (`name.to_s`, option Hashes) is the caller's own code and has to run
|
|
615
|
+
# before `acquire`, where it stays interruptible;
|
|
616
|
+
# - the wrapper is built after the mask (`new` may run a subclass's
|
|
617
|
+
# `initialize`, which must not hold a `Timeout`/`Thread#kill` back), so
|
|
618
|
+
# a held-back interrupt is delivered as the mask ends, before `new`.
|
|
619
|
+
# Until `new` has returned, the cleanup therefore works on the bare
|
|
620
|
+
# native handle, never on the wrapper: an interrupt at that point, one
|
|
621
|
+
# that lands inside a subclass's `initialize`, and an exception that
|
|
622
|
+
# `initialize` raises itself all leave the caller without a reference,
|
|
623
|
+
# so all three stop the sandbox (when owned, see below);
|
|
624
|
+
# - the `ensure` plus the `handed_off` flag — not `rescue Exception;
|
|
625
|
+
# raise` — is what also covers `Thread#kill`, which no `rescue` can
|
|
626
|
+
# catch.
|
|
627
|
+
#
|
|
628
|
+
# The protected window is the native handle's assignment through the
|
|
629
|
+
# wrapper's construction, and the cleanup in it is best-effort: a
|
|
630
|
+
# *further* interrupt that lands during the cleanup, before the native
|
|
631
|
+
# `stop` has started, or one delivered at this method's return boundary
|
|
632
|
+
# (after `handed_off` is set, before the caller has assigned the result),
|
|
633
|
+
# leaves the teardown to GC.
|
|
617
634
|
#
|
|
618
635
|
# A SIGINT (Ctrl-C on the main thread) is the exception: it is not queued
|
|
619
636
|
# like those two and cannot be masked. One that lands during the native
|
|
@@ -625,30 +642,32 @@ module Microsandbox
|
|
|
625
642
|
# Ownership: {connect_or_create} can hand back a sandbox that was already
|
|
626
643
|
# running for someone else, and {start} restarts a persisted one. The
|
|
627
644
|
# cleanup therefore applies EXACTLY the predicate the block form of
|
|
628
|
-
# {connect_or_create} uses —
|
|
645
|
+
# {connect_or_create} uses — the lifecycle ownership behind
|
|
646
|
+
# {Sandbox#owns_lifecycle?}, read from the native handle — in all three
|
|
629
647
|
# callers, {create} included: a sandbox this call does not own is never
|
|
630
648
|
# stopped here. Its reference is just let go (GC closes the handle, the
|
|
631
649
|
# sandbox keeps running).
|
|
632
650
|
# @yieldreturn [Native::Sandbox]
|
|
633
651
|
# @return [Sandbox]
|
|
634
652
|
def acquire
|
|
635
|
-
|
|
636
|
-
|
|
653
|
+
native = nil
|
|
654
|
+
handed_off = false
|
|
637
655
|
begin
|
|
638
656
|
Thread.handle_interrupt(Object => :never) do
|
|
639
|
-
|
|
657
|
+
native = yield
|
|
640
658
|
end
|
|
641
|
-
|
|
659
|
+
sandbox = new(native)
|
|
660
|
+
handed_off = true
|
|
661
|
+
sandbox
|
|
642
662
|
ensure
|
|
643
|
-
if
|
|
663
|
+
if native && !handed_off
|
|
644
664
|
begin
|
|
645
|
-
|
|
665
|
+
native.stop if native.owns_lifecycle
|
|
646
666
|
rescue Microsandbox::Error
|
|
647
667
|
# best-effort cleanup; the interrupt is what the caller must see
|
|
648
668
|
end
|
|
649
669
|
end
|
|
650
670
|
end
|
|
651
|
-
sandbox
|
|
652
671
|
end
|
|
653
672
|
|
|
654
673
|
# @api private
|
data/lib/microsandbox/version.rb
CHANGED
|
@@ -8,7 +8,7 @@ module Microsandbox
|
|
|
8
8
|
# Versioning section of the README for the full gem-to-runtime map. Must equal
|
|
9
9
|
# the native ext's Cargo crate version (`Native.version`), enforced by
|
|
10
10
|
# spec/unit/version_spec.rb.
|
|
11
|
-
VERSION = "0.17.
|
|
11
|
+
VERSION = "0.17.5"
|
|
12
12
|
|
|
13
13
|
# The upstream microsandbox runtime release this gem build embeds — the `tag`
|
|
14
14
|
# pinned on the `microsandbox`/`microsandbox-network` git deps in
|