microsandbox-rb 0.17.0 → 0.17.2
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 +229 -8
- data/Cargo.lock +45 -46
- data/DESIGN.md +90 -72
- data/README.md +178 -669
- data/SECURITY.md +65 -0
- data/docs/configuration.md +154 -0
- data/docs/errors.md +74 -0
- data/docs/execution.md +108 -0
- data/docs/filesystem.md +45 -0
- data/docs/images.md +65 -0
- data/docs/lifecycle.md +141 -0
- data/docs/observability.md +99 -0
- data/docs/releasing.md +117 -0
- data/docs/runtime.md +154 -0
- data/docs/surface.md +75 -0
- data/ext/microsandbox/Cargo.toml +9 -5
- data/ext/microsandbox/src/agent.rs +13 -11
- data/ext/microsandbox/src/backend.rs +1 -1
- data/ext/microsandbox/src/exec.rs +12 -8
- data/ext/microsandbox/src/fs_stream.rs +19 -7
- data/ext/microsandbox/src/host_path.rs +108 -0
- data/ext/microsandbox/src/image.rs +2 -2
- data/ext/microsandbox/src/lib.rs +3 -2
- data/ext/microsandbox/src/runtime.rs +189 -13
- data/ext/microsandbox/src/sandbox.rs +149 -102
- data/ext/microsandbox/src/snapshot.rs +17 -11
- data/ext/microsandbox/src/ssh.rs +22 -16
- data/ext/microsandbox/src/stream.rs +6 -2
- data/ext/microsandbox/src/volume.rs +50 -17
- data/lib/microsandbox/fs.rb +4 -8
- data/lib/microsandbox/image.rb +2 -2
- data/lib/microsandbox/log_entry.rb +1 -1
- data/lib/microsandbox/metrics.rb +1 -1
- data/lib/microsandbox/outbound_proxy.rb +8 -0
- data/lib/microsandbox/sandbox.rb +128 -13
- data/lib/microsandbox/snapshot.rb +1 -1
- data/lib/microsandbox/ssh.rb +2 -10
- data/lib/microsandbox/streams.rb +8 -5
- data/lib/microsandbox/version.rb +1 -1
- data/lib/microsandbox/volume.rb +31 -14
- data/lib/microsandbox-rb.rb +7 -0
- data/lib/microsandbox.rb +88 -31
- data/sig/microsandbox.rbs +24 -0
- metadata +14 -1
data/DESIGN.md
CHANGED
|
@@ -50,7 +50,7 @@ multi-threaded tokio runtime via `runtime.block_on(fut)`.
|
|
|
50
50
|
|
|
51
51
|
### GVL release
|
|
52
52
|
|
|
53
|
-
`block_on` is wrapped in `nogvl` (`
|
|
53
|
+
`block_on` is wrapped in `nogvl` (`rb_thread_call_without_gvl2`) so the Ruby
|
|
54
54
|
Global VM Lock is **released** while a sandbox operation is in flight. Without
|
|
55
55
|
this, a long-running `exec` would freeze every other Ruby thread/fiber in the
|
|
56
56
|
process (a real problem under Rails/Puma). The `nogvl` closure runs pure Rust
|
|
@@ -59,21 +59,81 @@ captured and re-raised *after* the GVL is re-acquired rather than unwinding
|
|
|
59
59
|
across the C frame (which would be UB).
|
|
60
60
|
|
|
61
61
|
**The *calling* thread is not preemptible during the call.** `nogvl` passes a
|
|
62
|
-
null unblock-function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
`
|
|
70
|
-
|
|
71
|
-
`
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
62
|
+
null unblock-function, so while a native call blocks, the thread that issued it
|
|
63
|
+
cannot be interrupted: `Timeout::timeout` (which relies on `Thread#raise`),
|
|
64
|
+
`Thread#kill`, and `SIGINT` (Ctrl-C) are all deferred until the call returns on
|
|
65
|
+
its own. The GVL release keeps *other* threads live; it does **not** make the
|
|
66
|
+
calling thread cancelable. This is harmless for the bounded calls
|
|
67
|
+
(`exec`/`shell` with a `timeout:`, `AgentClient` with a `timeout:`) because the
|
|
68
|
+
deadline fires inside the future, but the unbounded/streaming paths —
|
|
69
|
+
`exec`/`shell` with `timeout: nil`, `ExecHandle#recv`/`#wait`,
|
|
70
|
+
`log_stream`/`metrics_stream` `recv` (especially `follow: true`),
|
|
71
|
+
`Sandbox#wait`/`SandboxHandle#wait_until_stopped`, and `AgentClient#request`
|
|
72
|
+
with no timeout — can block their caller indefinitely if the guest wedges or a
|
|
73
|
+
relay drops. **Bound such calls with the explicit `timeout:` knobs rather than
|
|
74
|
+
wrapping them in `Timeout::timeout`**, which will not fire while the native
|
|
75
|
+
call is in flight.
|
|
76
|
+
|
|
77
|
+
### Interrupts never lose a result
|
|
78
|
+
|
|
79
|
+
An interrupt that arrives during a native call is delivered **after the call
|
|
80
|
+
returns**, and the call's result is not lost. Ruby delivers `Thread#raise`,
|
|
81
|
+
`Thread#kill` and `SIGINT` with a `longjmp`, which skips Rust frames without
|
|
82
|
+
running destructors; if that happened while the value a call had just produced
|
|
83
|
+
(a booted sandbox, a guest exec session, megabytes of output) lived only in a
|
|
84
|
+
Rust frame, it would be neither returned to Ruby nor dropped. The plain
|
|
85
|
+
`rb_thread_call_without_gvl` does exactly that — it checks interrupts once more
|
|
86
|
+
after the callback, before returning — which is why it is not used. Two rules
|
|
87
|
+
in `runtime.rs` replace it:
|
|
88
|
+
|
|
89
|
+
1. `nogvl` checks interrupts itself, under `rb_protect`, **only before the work
|
|
90
|
+
starts**: once up front, and again whenever `rb_thread_call_without_gvl2`
|
|
91
|
+
declined to run the callback because an interrupt was pending at entry. A
|
|
92
|
+
raise or kill there is an ordinary `Err` that unwinds the Rust frames
|
|
93
|
+
normally — nothing has happened yet. After the callback has run, nothing is
|
|
94
|
+
checked. (`nogvl` and `block_on` therefore return `Result`, and every call
|
|
95
|
+
site propagates it with `?`. A bound method that needs two awaits does both
|
|
96
|
+
in one `block_on`, so no value sits between two entry checks.)
|
|
97
|
+
2. Between `block_on` returning and the bound method returning its value, the
|
|
98
|
+
binding does not call into Ruby: no `funcall` (the VM checks interrupts
|
|
99
|
+
after every one), only C-level Hash/Array/String construction and
|
|
100
|
+
`TypedData` wrapping. The class lookup magnus would otherwise do with
|
|
101
|
+
`const_get` on a type's first wrap is resolved at extension load instead.
|
|
102
|
+
|
|
103
|
+
So the next interrupt checkpoint after a native call is the Ruby VM's own,
|
|
104
|
+
reached once the result is a Ruby object. What happens to it then:
|
|
105
|
+
|
|
106
|
+
- **`Sandbox.create`, `Sandbox.connect_or_create`, `Sandbox.start`, and
|
|
107
|
+
`PullSession#sandbox`** mask interrupts (`Thread.handle_interrupt(Object =>
|
|
108
|
+
:never)`) around the native call plus the wrap, and an `ensure` **stops the
|
|
109
|
+
sandbox** when an exception or a kill cuts in before the caller has the
|
|
110
|
+
object — but only a sandbox the call owns (`Sandbox#owns_lifecycle?`, the
|
|
111
|
+
same predicate as `connect_or_create`'s block form): one that was already
|
|
112
|
+
running for someone else is left running. A `SIGINT` cannot be masked this
|
|
113
|
+
way; it can arrive before the wrapper is assigned, and then the next case
|
|
114
|
+
applies.
|
|
115
|
+
- **Every other wrapper** (`ExecHandle`, streams, SSH clients, write sinks, …)
|
|
116
|
+
and every plain result simply becomes garbage; GC frees the wrapper and the
|
|
117
|
+
Rust value's `Drop` closes the session or connection.
|
|
118
|
+
|
|
119
|
+
The calls themselves remain uninterruptible, so `Timeout.timeout { Sandbox.create(...) }`
|
|
120
|
+
still waits for the create to finish — and now also for the stop that cleans
|
|
121
|
+
it up — before `Timeout::Error` surfaces. That stop is best-effort (a failure
|
|
122
|
+
is swallowed so the interrupt is what the caller sees; GC then tears the
|
|
123
|
+
sandbox down).
|
|
124
|
+
|
|
125
|
+
`Thread.handle_interrupt(X => :on_blocking)` treats a native call as the
|
|
126
|
+
blocking point at its *start*: the bridge raises an exception that is already
|
|
127
|
+
deferred before the call begins, and one that arrives during the call waits
|
|
128
|
+
for the thread's next blocking operation. The four acquisition methods are
|
|
129
|
+
the exception: their own `Object => :never` mask sits on top of the caller's,
|
|
130
|
+
so a deferred exception is not raised at the native call's entry there — the
|
|
131
|
+
sandbox is returned and the exception is delivered at the next blocking
|
|
132
|
+
operation after the acquisition.
|
|
133
|
+
|
|
134
|
+
`spec/fake_runtime/interrupt_safety_spec.rb` pins all of this against the real
|
|
135
|
+
extension, driving it with a fake `msb` (`spec/support/fake_runtime/`) whose
|
|
136
|
+
log shows whether a sandbox was stopped, dropped, or left attached.
|
|
77
137
|
|
|
78
138
|
## Error mapping
|
|
79
139
|
|
|
@@ -185,7 +245,7 @@ extension.
|
|
|
185
245
|
`ext/microsandbox/Cargo.toml` depends on the core crate via a **pinned git tag**
|
|
186
246
|
(`microsandbox` / `microsandbox-network`, pinned to the same tag as
|
|
187
247
|
`Microsandbox::RUNTIME_VERSION` — currently `v0.6.18`), so the gem builds anywhere
|
|
188
|
-
— CI
|
|
248
|
+
— CI and end-user source installs —
|
|
189
249
|
without an adjacent checkout. For fast local development against a sibling
|
|
190
250
|
microsandbox checkout, copy `.cargo/config.toml.example` to `.cargo/config.toml`
|
|
191
251
|
(gitignored); its `paths` override builds against the local crates instead of
|
|
@@ -195,25 +255,13 @@ git. The override must never be committed — it would break container builds.
|
|
|
195
255
|
|
|
196
256
|
* **Source gem**: compiles the extension via `extconf.rb` (rb-sys
|
|
197
257
|
`create_rust_makefile`); requires a Rust toolchain (MSRV below).
|
|
198
|
-
* **Precompiled platform gems**: built
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
host runtime comes from the `microsandbox-rb-binaries` gem — or, without it, is
|
|
204
|
-
fetched on first use (see above). The guest `agentd` is baked into
|
|
205
|
-
the extension by *target* arch (`filesystem/build.rs` uses
|
|
206
|
-
`CARGO_CFG_TARGET_ARCH` + `include_bytes!`), so it cross-compiles correctly;
|
|
207
|
-
the real cross work is linking the *target* native libs — `libcap-ng` on Linux
|
|
208
|
-
(via Debian multiarch for `aarch64-linux`) and the Hypervisor + Security
|
|
209
|
-
frameworks on macOS (via osxcross; `arm64-darwin` is the platform still to
|
|
210
|
-
confirm — if osxcross can't link them, move it to a native `macos-14` runner).
|
|
211
|
-
The job is gated to `workflow_dispatch` and **not** auto-published on tags:
|
|
212
|
-
since CI can't boot a microVM to prove a built gem actually works, gems are
|
|
213
|
-
promoted to the publish path manually after per-platform validation. Published
|
|
214
|
-
to RubyGems via Trusted Publishing (OIDC). See [Releasing](README.md#releasing).
|
|
258
|
+
* **Precompiled platform gems**: not built or published. Every platform
|
|
259
|
+
installs the source gem, which compiles the extension via `rb_sys` at install
|
|
260
|
+
time; the host runtime comes from the `microsandbox-rb-binaries` gem — or,
|
|
261
|
+
without it, is fetched on first use (see above). The companion binaries gems
|
|
262
|
+
below are unaffected. See [docs/releasing.md](docs/releasing.md).
|
|
215
263
|
* **Runtime binaries gems** (`microsandbox-rb-binaries`): a distinct artifact
|
|
216
|
-
from the
|
|
264
|
+
from the SDK gem — no Ruby code beyond a small
|
|
217
265
|
locator module, just the verified upstream `msb` + `libkrunfw` for one
|
|
218
266
|
platform. CI's `binaries` job vendors and builds all three platforms on every
|
|
219
267
|
run and smoke-tests the host one; the `package` job installs the source gem
|
|
@@ -232,42 +280,11 @@ git. The override must never be committed — it would break container builds.
|
|
|
232
280
|
|
|
233
281
|
## Implemented surface (v1) vs roadmap
|
|
234
282
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
`wait_until_stopped` (→ `SandboxStopResult`) controls — the v0.5.8 live-vs-handle
|
|
241
|
-
split that mirrors the official SDKs), backend routing (`set_default_backend`/
|
|
242
|
-
`with_backend`/`default_backend_kind`), block form), `exec`/`shell` with collected `ExecOutput`,
|
|
243
|
-
**streaming** `exec_stream`/`shell_stream` (`ExecHandle` is `Enumerable` over
|
|
244
|
-
`ExecEvent`s, with stdin sink + signal/kill/resize), the full guest filesystem
|
|
245
|
-
API (`fs.read`/`write`/`list`/`mkdir`/`remove`/`stat`/…), `metrics`,
|
|
246
|
-
`Microsandbox.all_sandbox_metrics`, **streaming `metrics_stream`/`log_stream`**
|
|
247
|
-
(`Enumerable` over `Metrics`/`LogEntry`), `logs`,
|
|
248
|
-
**OCI image-cache management** (`Image.get`/`list`/`inspect`/`remove`/`prune`),
|
|
249
|
-
**named volumes** (`Volume.create`/`get`/`list`/`remove` + `volumes:` mounts),
|
|
250
|
-
**snapshots** (`Snapshot.create`/`get`/`list`/`remove`/`verify`/`export`/`import`
|
|
251
|
-
+ `from_snapshot:` boot), **rootfs patches** (`Patch.text`/`file`/`append`/
|
|
252
|
-
`copy_file`/`copy_dir`/`symlink`/`mkdir`/`remove` via `create(patches:)`),
|
|
253
|
-
**custom per-rule network policies** (`NetworkPolicy`/`Rule`/`Destination` —
|
|
254
|
-
CIDR/IP/domain/suffix/group allow-deny rules with per-direction defaults and
|
|
255
|
-
bulk domain denials, alongside the presets), interactive **`attach`/
|
|
256
|
-
`attach_shell`** (host-TTY coupled — raw mode + SIGWINCH), **SSH**
|
|
257
|
-
(`Sandbox#ssh` → `SshClient`/`SftpClient`/`SshServer`), the **raw agent client**
|
|
258
|
-
(`AgentClient` → `AgentStream`/`AgentFrame`),
|
|
259
|
-
`version`/`install`/`installed?`/`ensure_runtime!`, **registry auth**
|
|
260
|
-
(`registry_auth`/`registry_insecure`/`registry_ca_certs` on `create`, for
|
|
261
|
-
private/authenticated registries), and the typed error hierarchy.
|
|
262
|
-
|
|
263
|
-
Create options now cover `image`, `cpus`, `memory`, `oci_upper_size`, `env`,
|
|
264
|
-
`workdir`, `shell`, `user`, `hostname`, `labels`, `scripts`, `entrypoint`,
|
|
265
|
-
`ports`/`ports_udp`, `volumes`, `patches`, `network` (policy presets
|
|
266
|
-
`public_only`/`none`/`allow_all`/`non_local`, or a custom `NetworkPolicy`/Hash),
|
|
267
|
-
`log_level`, `quiet_logs`, `security`, `max_duration`, `idle_timeout`, `rlimits`,
|
|
268
|
-
`pull_policy`, `registry_auth`/`registry_insecure`/`registry_ca_certs`,
|
|
269
|
-
`secrets`, `from_snapshot`, `detached`, and `replace`/`replace_with_timeout`.
|
|
270
|
-
`exec`/`shell` add per-call `rlimits`.
|
|
283
|
+
The authoritative, up-to-date scope statement — every implemented API family,
|
|
284
|
+
the full create-option list, and the handful of upstream knobs not yet exposed —
|
|
285
|
+
lives in [docs/surface.md](docs/surface.md), with the typed counterpart in
|
|
286
|
+
`sig/microsandbox.rbs`. It is maintained there (and only there) so this file
|
|
287
|
+
and the README cannot drift from each other.
|
|
271
288
|
|
|
272
289
|
## Verification
|
|
273
290
|
|
|
@@ -300,7 +317,8 @@ create options, full mount options (tmpfs/disk + stat-virtualization/
|
|
|
300
317
|
host-permissions), and snapshot inspection (`open`/`list_dir`/`reindex`).
|
|
301
318
|
|
|
302
319
|
A few **secondary** upstream knobs remain unexposed (a genuine binding gap, not
|
|
303
|
-
upstream-gated — they exist at the pinned
|
|
320
|
+
upstream-gated — they exist at the pinned runtime,
|
|
321
|
+
`Microsandbox::RUNTIME_VERSION`): per-published-port host
|
|
304
322
|
**bind address** (ports always bind loopback), network **interface overrides**,
|
|
305
323
|
and inline **named-volume create-mode** (pre-create with `Volume.create`, then
|
|
306
324
|
mount with `{ named: }`). These slot in module-by-module exactly as the existing
|