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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7d72d5c6e7a8d135e93d02d64c8966cf6cd4b7dcbf17d02bb65d074a963a977c
|
|
4
|
+
data.tar.gz: cde0f1e7c9d8b8270a98dd333e7e49709d24bd237ee2c7a6c671b8913f1b3393
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5c1ceaecff30c64f9fd7f138c75acf080202468f56859b613a52d7ef482f8da32b82c4bf0f4191a50e0804e0938fa62c4ceff1b771b8df4cd0d1e10b06a19c81
|
|
7
|
+
data.tar.gz: 4a817a61508dd57b041c56b351be03d6dfc317d1a84079c9e0049e9dde374583252afe91522fdb7c61fbce547d87dc326d4c3bb2aaa77fb27b69e68669907b69
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,115 @@ wraps, and the README's Versioning section keeps the full gem→runtime map.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.17.4] - 2026-10-14
|
|
12
|
+
|
|
13
|
+
Gem-only fix release; the embedded runtime stays at **`v0.6.18`**. Fork safety
|
|
14
|
+
now also survives pid reuse (a process is identified by its pid and fork
|
|
15
|
+
generation together), `install`/`setup` in a forked child no longer touch the
|
|
16
|
+
backend, a `Sandbox` subclass's `initialize` no longer runs with interrupts
|
|
17
|
+
masked, and the macOS limit that a forked child must not build network clients
|
|
18
|
+
is documented. It also picks up dependency updates: magnus 0.9, tokio 1.53,
|
|
19
|
+
rb-sys 0.9.130, and rustls 0.23.46 for four TLS advisories (see Security).
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **A `Sandbox` subclass's `initialize` no longer runs with interrupts
|
|
24
|
+
masked.** `Sandbox.create`, `.connect_or_create` and `.start` (and
|
|
25
|
+
`PullSession#sandbox`) hold a queued `Thread#raise`/`Timeout`/`Thread#kill`
|
|
26
|
+
back only while the native call runs; the wrapper is now built after that,
|
|
27
|
+
so a subclass whose constructor blocks or runs long no longer delays a
|
|
28
|
+
`Timeout.timeout` or `Thread#kill` until the constructor returns. The
|
|
29
|
+
best-effort interrupt cleanup moved to the native handle with it: an
|
|
30
|
+
interrupt that lands before or during `initialize`, or an exception
|
|
31
|
+
`initialize` raises, still stops the sandbox when the call owns it and
|
|
32
|
+
leaves one it merely connected to running.
|
|
33
|
+
Behaviour of `Sandbox` itself is unchanged.
|
|
34
|
+
|
|
35
|
+
- **`Microsandbox.install` and `Microsandbox.setup` no longer take over the
|
|
36
|
+
backend in a forked child.** They went through the same entry as every
|
|
37
|
+
other native call, so in a child they first rebuilt the backend inherited
|
|
38
|
+
from the parent — which they never use — and failed with the configuration
|
|
39
|
+
error when the parent's selection no longer resolved in the child (say its
|
|
40
|
+
environment selected the cloud backend without an API key). They now leave
|
|
41
|
+
the backend alone; the child's first call that uses it takes it over, as
|
|
42
|
+
before.
|
|
43
|
+
|
|
44
|
+
- **Fork safety no longer identifies a process by its pid alone.** A forked
|
|
45
|
+
process that the kernel had given the pid of an ancestor whose SDK state it
|
|
46
|
+
inherited (the pid counter wrapped onto exactly that value, with no SDK call
|
|
47
|
+
in between) was taken for that ancestor: it would have used the inherited
|
|
48
|
+
runtime, backend and handles instead of rebuilding them or raising
|
|
49
|
+
`ForkedHandleError`. The extension now counts forks with a `pthread_atfork`
|
|
50
|
+
child handler and identifies a process by its pid and that count together.
|
|
51
|
+
`ForkedHandleError`'s message no longer names one pid as two processes when
|
|
52
|
+
the pids are equal.
|
|
53
|
+
|
|
54
|
+
### Security
|
|
55
|
+
|
|
56
|
+
- **rustls 0.23.45 → 0.23.46** (a transitive dependency, through the network
|
|
57
|
+
stack's DNS client and the HTTP client). Fixes RUSTSEC-2026-0346 (`Debug`
|
|
58
|
+
output of client session values exposed TLS secrets), RUSTSEC-2026-0347
|
|
59
|
+
(buffered application data sent to the outer server after ECH rejection),
|
|
60
|
+
RUSTSEC-2026-0348 (Brotli certificate decompression could allocate up to
|
|
61
|
+
1 GiB before authentication) and RUSTSEC-2026-0349 (an unauthenticated
|
|
62
|
+
ServerHello could downgrade later connections to classical key exchange).
|
|
63
|
+
|
|
64
|
+
### Docs
|
|
65
|
+
|
|
66
|
+
- **Known limitation: on macOS a forked child must not build network
|
|
67
|
+
clients.** A child forked (without `exec`) from a Ruby process on macOS
|
|
68
|
+
crashes — `SIGSEGV` or `SIGABRT`, which cannot be rescued — when the SDK
|
|
69
|
+
builds an HTTP client or reads the Keychain there: any cloud backend use,
|
|
70
|
+
`Sandbox.create` with an image not yet cached (or `pull_policy: :always`),
|
|
71
|
+
`Image.load`, and runtime install/setup. The macOS system frameworks behind
|
|
72
|
+
the proxy lookup and the Keychain are not usable after `fork`. Do that work
|
|
73
|
+
in the parent before forking, or in a process started with `Process.spawn`.
|
|
74
|
+
On Linux the cloud backend works in a forked child; new fork specs cover it
|
|
75
|
+
there (they are skipped on macOS). See [runtime.md](docs/runtime.md#macos-no-network-clients-in-a-forked-child).
|
|
76
|
+
|
|
77
|
+
## [0.17.3] - 2026-10-13
|
|
78
|
+
|
|
79
|
+
Gem-only fix release; the embedded runtime stays at **`v0.6.18`**. A process
|
|
80
|
+
forked after the SDK was used — a Puma, Resque or Solid Queue worker — used to
|
|
81
|
+
hang for 30 s on Linux, or die with `SIGTRAP` on macOS, on its first SDK call.
|
|
82
|
+
The child's first SDK call now builds a fresh backend, and a handle created
|
|
83
|
+
before the fork raises the new `ForkedHandleError` there instead of being used.
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
|
|
87
|
+
- **Forking a process that has used the SDK no longer hangs or kills the
|
|
88
|
+
child.** A forked child inherited the parent's default backend, whose SQLite
|
|
89
|
+
worker threads (local) or pooled connections (cloud) do not exist in it: the
|
|
90
|
+
child's first catalog call waited out a 30 s pool timeout on Linux and then
|
|
91
|
+
raised a plain `Microsandbox::Error`, and on macOS the child died with
|
|
92
|
+
`SIGTRAP` — as it did when it merely selected a backend, because destroying
|
|
93
|
+
the inherited one traps too. The first SDK call in a child now builds a
|
|
94
|
+
fresh backend the same way the parent's was chosen (`set_default_backend`,
|
|
95
|
+
an enclosing `with_backend`, or the environment) and leaks the inherited one
|
|
96
|
+
instead of destroying it; a `with_backend` block entered before the fork can
|
|
97
|
+
be left in the child, and selecting a backend there works even when the
|
|
98
|
+
inherited choice no longer resolves. If the backend such a block should
|
|
99
|
+
restore cannot be chosen again in the child, the block's temporary selection
|
|
100
|
+
stays in effect and an exception raised by the block is no longer replaced
|
|
101
|
+
by the restore failure (which is warned about instead). The tokio runtime, already rebuilt after a fork, now
|
|
102
|
+
keeps its pointer and pid in one atomic cell, without the mutex a fork could
|
|
103
|
+
capture locked. See the new "Forking" section of
|
|
104
|
+
[docs/runtime.md](docs/runtime.md#forking-puma-resque-solid-queue) for what
|
|
105
|
+
is supported and what is not.
|
|
106
|
+
- A handle created before a fork (`Sandbox`, `SandboxHandle`, `ExecHandle`,
|
|
107
|
+
streams, SSH and agent clients, `VolumeFs`, ...) is refused in the child
|
|
108
|
+
with `ForkedHandleError` instead of being driven on the parent's dead
|
|
109
|
+
runtime, and a child that garbage-collects one — at the latest when it
|
|
110
|
+
exits — no longer runs its destructor. `Sandbox#owns_lifecycle?` is refused
|
|
111
|
+
the same way; `name`, `id` and the `SandboxHandle` snapshot fields stay
|
|
112
|
+
readable.
|
|
113
|
+
|
|
114
|
+
### Added
|
|
115
|
+
|
|
116
|
+
- `Microsandbox::ForkedHandleError` (`#code` `"forked-handle"`), raised when a
|
|
117
|
+
handle is used in a process forked from the one that created it. The message
|
|
118
|
+
names both pids.
|
|
119
|
+
|
|
11
120
|
## [0.17.2] - 2026-10-12
|
|
12
121
|
|
|
13
122
|
Gem-only fix and release-pipeline hardening release; the embedded runtime
|
|
@@ -1470,7 +1579,9 @@ microsandbox runtime, aligned with the official Python/Node/Go SDKs.
|
|
|
1470
1579
|
core crate has Apple-native deps). Until precompiled gems are published,
|
|
1471
1580
|
installing from source requires a Rust toolchain (stable >= 1.91).
|
|
1472
1581
|
|
|
1473
|
-
[Unreleased]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.17.
|
|
1582
|
+
[Unreleased]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.17.4...HEAD
|
|
1583
|
+
[0.17.4]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.17.3...v0.17.4
|
|
1584
|
+
[0.17.3]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.17.2...v0.17.3
|
|
1474
1585
|
[0.17.2]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.17.1...v0.17.2
|
|
1475
1586
|
[0.17.1]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.17.0...v0.17.1
|
|
1476
1587
|
[0.17.0]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.16.0...v0.17.0
|
data/Cargo.lock
CHANGED
|
@@ -971,9 +971,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
|
|
971
971
|
|
|
972
972
|
[[package]]
|
|
973
973
|
name = "chacha20"
|
|
974
|
-
version = "0.10.
|
|
974
|
+
version = "0.10.2"
|
|
975
975
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
976
|
-
checksum = "
|
|
976
|
+
checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
|
|
977
977
|
dependencies = [
|
|
978
978
|
"cfg-if",
|
|
979
979
|
"cipher 0.5.2",
|
|
@@ -1597,9 +1597,9 @@ dependencies = [
|
|
|
1597
1597
|
|
|
1598
1598
|
[[package]]
|
|
1599
1599
|
name = "der"
|
|
1600
|
-
version = "0.8.
|
|
1600
|
+
version = "0.8.2"
|
|
1601
1601
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1602
|
-
checksum = "
|
|
1602
|
+
checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a"
|
|
1603
1603
|
dependencies = [
|
|
1604
1604
|
"const-oid",
|
|
1605
1605
|
"pem-rfc7468",
|
|
@@ -1923,7 +1923,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
|
1923
1923
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
|
1924
1924
|
dependencies = [
|
|
1925
1925
|
"libc",
|
|
1926
|
-
"windows-sys 0.
|
|
1926
|
+
"windows-sys 0.61.2",
|
|
1927
1927
|
]
|
|
1928
1928
|
|
|
1929
1929
|
[[package]]
|
|
@@ -2051,7 +2051,7 @@ checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a"
|
|
|
2051
2051
|
dependencies = [
|
|
2052
2052
|
"io-lifetimes 2.0.4",
|
|
2053
2053
|
"rustix",
|
|
2054
|
-
"windows-sys 0.
|
|
2054
|
+
"windows-sys 0.59.0",
|
|
2055
2055
|
]
|
|
2056
2056
|
|
|
2057
2057
|
[[package]]
|
|
@@ -2858,7 +2858,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
|
2858
2858
|
checksum = "20fd6de4ccfcc187e38bc21cfa543cb5a302cb86a8b114eb7f0bf0dc9f8ac00f"
|
|
2859
2859
|
dependencies = [
|
|
2860
2860
|
"io-lifetimes 3.0.1",
|
|
2861
|
-
"windows-sys 0.
|
|
2861
|
+
"windows-sys 0.60.2",
|
|
2862
2862
|
]
|
|
2863
2863
|
|
|
2864
2864
|
[[package]]
|
|
@@ -3289,9 +3289,9 @@ dependencies = [
|
|
|
3289
3289
|
|
|
3290
3290
|
[[package]]
|
|
3291
3291
|
name = "magnus"
|
|
3292
|
-
version = "0.
|
|
3292
|
+
version = "0.9.2"
|
|
3293
3293
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
3294
|
-
checksum = "
|
|
3294
|
+
checksum = "b9068710e00d21be762edca59b9c4c8946dbdb29820d4f76ad559afea07982c9"
|
|
3295
3295
|
dependencies = [
|
|
3296
3296
|
"magnus-macros",
|
|
3297
3297
|
"rb-sys",
|
|
@@ -3301,9 +3301,9 @@ dependencies = [
|
|
|
3301
3301
|
|
|
3302
3302
|
[[package]]
|
|
3303
3303
|
name = "magnus-macros"
|
|
3304
|
-
version = "0.
|
|
3304
|
+
version = "0.9.1"
|
|
3305
3305
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
3306
|
-
checksum = "
|
|
3306
|
+
checksum = "9dafde2eeb66f090e0f326112323eb8324b12cb32b5d1210e541d3df6b0ce77e"
|
|
3307
3307
|
dependencies = [
|
|
3308
3308
|
"proc-macro2",
|
|
3309
3309
|
"quote",
|
|
@@ -3660,11 +3660,12 @@ dependencies = [
|
|
|
3660
3660
|
|
|
3661
3661
|
[[package]]
|
|
3662
3662
|
name = "microsandbox_rb"
|
|
3663
|
-
version = "0.17.
|
|
3663
|
+
version = "0.17.4"
|
|
3664
3664
|
dependencies = [
|
|
3665
3665
|
"chrono",
|
|
3666
3666
|
"futures",
|
|
3667
3667
|
"ipnetwork",
|
|
3668
|
+
"libc",
|
|
3668
3669
|
"magnus",
|
|
3669
3670
|
"microsandbox",
|
|
3670
3671
|
"microsandbox-network",
|
|
@@ -4312,9 +4313,9 @@ dependencies = [
|
|
|
4312
4313
|
|
|
4313
4314
|
[[package]]
|
|
4314
4315
|
name = "pageant"
|
|
4315
|
-
version = "0.2.
|
|
4316
|
+
version = "0.2.4"
|
|
4316
4317
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
4317
|
-
checksum = "
|
|
4318
|
+
checksum = "f0786409b5aaa884655c9b4065681db0835db0cc185a6fb19fb17a5d38656866"
|
|
4318
4319
|
dependencies = [
|
|
4319
4320
|
"base16ct",
|
|
4320
4321
|
"byteorder",
|
|
@@ -4733,7 +4734,7 @@ dependencies = [
|
|
|
4733
4734
|
"once_cell",
|
|
4734
4735
|
"socket2",
|
|
4735
4736
|
"tracing",
|
|
4736
|
-
"windows-sys 0.
|
|
4737
|
+
"windows-sys 0.60.2",
|
|
4737
4738
|
]
|
|
4738
4739
|
|
|
4739
4740
|
[[package]]
|
|
@@ -4870,18 +4871,18 @@ dependencies = [
|
|
|
4870
4871
|
|
|
4871
4872
|
[[package]]
|
|
4872
4873
|
name = "rb-sys"
|
|
4873
|
-
version = "0.9.
|
|
4874
|
+
version = "0.9.130"
|
|
4874
4875
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
4875
|
-
checksum = "
|
|
4876
|
+
checksum = "02faf625bb10ba893e3ae620f19c9fb1b5f8fcae0fe4eb86bb3f2230fad75edb"
|
|
4876
4877
|
dependencies = [
|
|
4877
4878
|
"rb-sys-build",
|
|
4878
4879
|
]
|
|
4879
4880
|
|
|
4880
4881
|
[[package]]
|
|
4881
4882
|
name = "rb-sys-build"
|
|
4882
|
-
version = "0.9.
|
|
4883
|
+
version = "0.9.130"
|
|
4883
4884
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
4884
|
-
checksum = "
|
|
4885
|
+
checksum = "05be6f9c86fe5482808162826f6c047d093b3670582296c770de1d94f8066694"
|
|
4885
4886
|
dependencies = [
|
|
4886
4887
|
"bindgen",
|
|
4887
4888
|
"lazy_static",
|
|
@@ -5270,7 +5271,7 @@ dependencies = [
|
|
|
5270
5271
|
"errno",
|
|
5271
5272
|
"libc",
|
|
5272
5273
|
"linux-raw-sys",
|
|
5273
|
-
"windows-sys 0.
|
|
5274
|
+
"windows-sys 0.61.2",
|
|
5274
5275
|
]
|
|
5275
5276
|
|
|
5276
5277
|
[[package]]
|
|
@@ -5285,9 +5286,9 @@ dependencies = [
|
|
|
5285
5286
|
|
|
5286
5287
|
[[package]]
|
|
5287
5288
|
name = "rustls"
|
|
5288
|
-
version = "0.23.
|
|
5289
|
+
version = "0.23.46"
|
|
5289
5290
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
5290
|
-
checksum = "
|
|
5291
|
+
checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf"
|
|
5291
5292
|
dependencies = [
|
|
5292
5293
|
"aws-lc-rs",
|
|
5293
5294
|
"log",
|
|
@@ -5339,7 +5340,7 @@ dependencies = [
|
|
|
5339
5340
|
"security-framework 3.7.0",
|
|
5340
5341
|
"security-framework-sys",
|
|
5341
5342
|
"webpki-root-certs",
|
|
5342
|
-
"windows-sys 0.
|
|
5343
|
+
"windows-sys 0.61.2",
|
|
5343
5344
|
]
|
|
5344
5345
|
|
|
5345
5346
|
[[package]]
|
|
@@ -5969,9 +5970,9 @@ dependencies = [
|
|
|
5969
5970
|
|
|
5970
5971
|
[[package]]
|
|
5971
5972
|
name = "spin"
|
|
5972
|
-
version = "0.9.
|
|
5973
|
+
version = "0.9.9"
|
|
5973
5974
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
5974
|
-
checksum = "
|
|
5975
|
+
checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e"
|
|
5975
5976
|
dependencies = [
|
|
5976
5977
|
"lock_api",
|
|
5977
5978
|
]
|
|
@@ -6413,7 +6414,7 @@ dependencies = [
|
|
|
6413
6414
|
"getrandom 0.4.2",
|
|
6414
6415
|
"once_cell",
|
|
6415
6416
|
"rustix",
|
|
6416
|
-
"windows-sys 0.
|
|
6417
|
+
"windows-sys 0.61.2",
|
|
6417
6418
|
]
|
|
6418
6419
|
|
|
6419
6420
|
[[package]]
|
|
@@ -6531,9 +6532,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|
|
6531
6532
|
|
|
6532
6533
|
[[package]]
|
|
6533
6534
|
name = "tokio"
|
|
6534
|
-
version = "1.
|
|
6535
|
+
version = "1.53.2"
|
|
6535
6536
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
6536
|
-
checksum = "
|
|
6537
|
+
checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044"
|
|
6537
6538
|
dependencies = [
|
|
6538
6539
|
"bytes",
|
|
6539
6540
|
"libc",
|
|
@@ -7210,7 +7211,7 @@ version = "0.1.11"
|
|
|
7210
7211
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
7211
7212
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
|
7212
7213
|
dependencies = [
|
|
7213
|
-
"windows-sys 0.
|
|
7214
|
+
"windows-sys 0.61.2",
|
|
7214
7215
|
]
|
|
7215
7216
|
|
|
7216
7217
|
[[package]]
|
|
@@ -7521,7 +7522,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
|
7521
7522
|
checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d"
|
|
7522
7523
|
dependencies = [
|
|
7523
7524
|
"bitflags 2.13.0",
|
|
7524
|
-
"windows-sys 0.
|
|
7525
|
+
"windows-sys 0.59.0",
|
|
7525
7526
|
]
|
|
7526
7527
|
|
|
7527
7528
|
[[package]]
|
|
@@ -7620,13 +7621,14 @@ dependencies = [
|
|
|
7620
7621
|
|
|
7621
7622
|
[[package]]
|
|
7622
7623
|
name = "wnaf"
|
|
7623
|
-
version = "0.14.
|
|
7624
|
+
version = "0.14.1"
|
|
7624
7625
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
7625
|
-
checksum = "
|
|
7626
|
+
checksum = "795ca18b3fdb5e62bf982199278341ddcf7ebf7d32e25e212ad05d496e95f6fa"
|
|
7626
7627
|
dependencies = [
|
|
7627
7628
|
"ff",
|
|
7628
7629
|
"group",
|
|
7629
7630
|
"hybrid-array",
|
|
7631
|
+
"primefield",
|
|
7630
7632
|
]
|
|
7631
7633
|
|
|
7632
7634
|
[[package]]
|
data/DESIGN.md
CHANGED
|
@@ -105,13 +105,27 @@ reached once the result is a Ruby object. What happens to it then:
|
|
|
105
105
|
|
|
106
106
|
- **`Sandbox.create`, `Sandbox.connect_or_create`, `Sandbox.start`, and
|
|
107
107
|
`PullSession#sandbox`** mask interrupts (`Thread.handle_interrupt(Object =>
|
|
108
|
-
:never)`) around the native call
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
108
|
+
:never)`) around the native call only, so the bare `Native::Sandbox` is
|
|
109
|
+
bound to a local before a queued exception or kill can land; an `ensure`
|
|
110
|
+
then **stops the sandbox** when one cuts in before the caller has the
|
|
111
|
+
object — but only a sandbox the call owns (`owns_lifecycle`, the same
|
|
112
|
+
predicate as `connect_or_create`'s block form): one that was already
|
|
113
|
+
running for someone else is left running. The Ruby wrapper is constructed
|
|
114
|
+
*after* the mask, with normal interrupt handling, because `new` runs a
|
|
115
|
+
subclass's `initialize`: a constructor that blocks or runs long must not
|
|
116
|
+
hold a `Timeout.timeout` or `Thread#kill` back until it returns. So the
|
|
117
|
+
cleanup works on the native handle (its `owns_lifecycle` and `stop`), never
|
|
118
|
+
on the wrapper, and covers the window from the native handle's assignment
|
|
119
|
+
through the wrapper's construction: an interrupt delivered as the mask ends
|
|
120
|
+
(before `new` runs), one delivered inside a subclass's `initialize`, and an
|
|
121
|
+
exception that `initialize` raises itself. That cleanup is best-effort, not
|
|
122
|
+
a guarantee for every lost return: a *further* interrupt that arrives during
|
|
123
|
+
the cleanup itself, before the native `stop` has started, or one delivered
|
|
124
|
+
at the method's final return boundary, after the wrapper has been handed
|
|
125
|
+
off but before the caller has assigned it, leaves the teardown to GC (as it
|
|
126
|
+
did before construction moved out of the mask). A `SIGINT` cannot be masked
|
|
127
|
+
this way; it can arrive before the native handle is assigned, and then the
|
|
128
|
+
next case applies.
|
|
115
129
|
- **Every other wrapper** (`ExecHandle`, streams, SSH clients, write sinks, …)
|
|
116
130
|
and every plain result simply becomes garbage; GC frees the wrapper and the
|
|
117
131
|
Rust value's `Drop` closes the session or connection.
|
|
@@ -129,12 +143,189 @@ for the thread's next blocking operation. The four acquisition methods are
|
|
|
129
143
|
the exception: their own `Object => :never` mask sits on top of the caller's,
|
|
130
144
|
so a deferred exception is not raised at the native call's entry there — the
|
|
131
145
|
sandbox is returned and the exception is delivered at the next blocking
|
|
132
|
-
operation after the acquisition.
|
|
146
|
+
operation after the acquisition. (If that blocking operation is inside a
|
|
147
|
+
subclass's `initialize`, the caller does not have the object yet, so the
|
|
148
|
+
sandbox is stopped as above.)
|
|
133
149
|
|
|
134
150
|
`spec/fake_runtime/interrupt_safety_spec.rb` pins all of this against the real
|
|
135
151
|
extension, driving it with a fake `msb` (`spec/support/fake_runtime/`) whose
|
|
136
152
|
log shows whether a sandbox was stopped, dropped, or left attached.
|
|
137
153
|
|
|
154
|
+
### Fork safety
|
|
155
|
+
|
|
156
|
+
`fork(2)` copies only the calling thread. A child of a process that has used
|
|
157
|
+
the SDK therefore inherits native state whose threads are gone: the tokio
|
|
158
|
+
runtime (workers, I/O driver), the core's process-wide default backend (each
|
|
159
|
+
SQLite connection of a `LocalBackend` is served by its own worker thread; a
|
|
160
|
+
`CloudBackend`'s pooled connections belong to the parent's runtime), and every
|
|
161
|
+
wrapped handle built on top of them. Using that state hangs or crashes — the
|
|
162
|
+
child's first catalog read used to wait out the pool's 30 s acquire timeout on
|
|
163
|
+
Linux and die with `SIGTRAP` on macOS — and so does *destroying* it, because
|
|
164
|
+
the destructors wake the same missing threads (on macOS, replacing an
|
|
165
|
+
inherited `LocalBackend` was enough to kill the child). One rule covers all of
|
|
166
|
+
it: **inherited native state is never used and never dropped in the child. It
|
|
167
|
+
is leaked, once per fork, and replaced by state built in the child.**
|
|
168
|
+
|
|
169
|
+
That needs a reliable answer to "was this built by the process I am?". The
|
|
170
|
+
binding's answer is a **process identity** (`runtime::ProcessIdentity`): the
|
|
171
|
+
pid together with a *fork generation*. At extension load the binding
|
|
172
|
+
registers a `pthread_atfork` child handler whose only work is one atomic
|
|
173
|
+
increment of a counter; the child inherits its parent's counter as plain
|
|
174
|
+
memory, and the handler, running in the child before `fork` returns there,
|
|
175
|
+
makes it one higher. `Kernel#fork` calls libc's `fork`, which runs atfork
|
|
176
|
+
handlers on Linux and macOS, so a child, grandchild, … of the loading process
|
|
177
|
+
has generation 1, 2, …, whatever its pid. Everything the binding builds is
|
|
178
|
+
tagged with the identity of the process that built it, and two identities
|
|
179
|
+
are equal only when both halves are:
|
|
180
|
+
|
|
181
|
+
- the generation tells apart a descendant that was handed an ancestor's pid
|
|
182
|
+
again by the kernel (the pid counter wrapped onto exactly that value)
|
|
183
|
+
without any SDK call in between — pid alone would take it for the
|
|
184
|
+
ancestor;
|
|
185
|
+
- the pid tells apart a process created without running atfork handlers
|
|
186
|
+
(`vfork`, a raw `clone`), whose generation equals its parent's.
|
|
187
|
+
|
|
188
|
+
The handler is async-signal-safe (a lock-free atomic add, no allocation, no
|
|
189
|
+
lock, no Ruby), as a child handler must be; `Relaxed` ordering suffices
|
|
190
|
+
because the forked child is single-threaded until it starts threads, which
|
|
191
|
+
see the counter through the spawn's own synchronization. Handlers cannot be
|
|
192
|
+
unregistered, and Ruby never unloads an extension, so the handler stays
|
|
193
|
+
valid for the life of the process. Should registration fail (`ENOMEM`), the
|
|
194
|
+
generation stays 0 and the identity is effectively the pid alone.
|
|
195
|
+
Three spec-only diagnostics on `Native` expose it: `fork_generation` returns
|
|
196
|
+
the counter, and `fork_identity_equal` / `fork_identity_guard` run the real
|
|
197
|
+
comparisons (`==`, the packed backend word, a handle's ownership check) on
|
|
198
|
+
identities a spec chooses — the only way to pin that both halves decide,
|
|
199
|
+
since a real fork changes both and pid reuse cannot be staged
|
|
200
|
+
(`spec/unit/fork_identity_spec.rb`).
|
|
201
|
+
|
|
202
|
+
Three pieces use the identity:
|
|
203
|
+
|
|
204
|
+
- **Runtime cell** (`runtime.rs`). The runtime and the identity of the process
|
|
205
|
+
that built it live in one heap cell behind one `AtomicPtr`. `runtime()`
|
|
206
|
+
returns the cell's runtime when that identity is ours; otherwise it builds a
|
|
207
|
+
new cell and publishes it with a compare-exchange (a loser of that race in
|
|
208
|
+
the same process drops its own, never-published runtime). There is no mutex,
|
|
209
|
+
so nothing can be inherited locked or poisoned, and no window in which
|
|
210
|
+
pointer and identity disagree. A cell built by a parent process is leaked:
|
|
211
|
+
`Runtime`'s `Drop` joins worker threads that do not exist.
|
|
212
|
+
- **Backend record** (`backend.rs`). The binding records how the default
|
|
213
|
+
backend was chosen — the core's own ladder (environment, profile, config
|
|
214
|
+
file), or an explicit `set_default_backend`/`with_backend` selection — and
|
|
215
|
+
the identity of the process it belongs to (one `AtomicU64`, pid and
|
|
216
|
+
generation, written and read whole). `ensure_this_process_backend` runs,
|
|
217
|
+
with the GVL held, at the start of every `block_on` and of every synchronous
|
|
218
|
+
entry that reads the backend. The one exception is the runtime installer
|
|
219
|
+
(`Native.install`, `Native.setup`): it never reads the backend, so it runs
|
|
220
|
+
through `block_on_runtime_install`, which skips the check, and a child that
|
|
221
|
+
installs leaves the record alone for its first call that does use the
|
|
222
|
+
backend. In a process whose identity differs from the recorded one it
|
|
223
|
+
builds a fresh backend from the record, swaps it into the core and
|
|
224
|
+
`mem::forget`s the inherited one, along with the backends that open
|
|
225
|
+
`with_backend` scopes had saved for their restore. A scope the parent opened
|
|
226
|
+
can still be closed in the child: closing it installs a backend rebuilt from
|
|
227
|
+
how the saved one had been chosen. For the ladder this means the child
|
|
228
|
+
resolves it again, from its own environment, at its first native call; if
|
|
229
|
+
that fails, the call raises the configuration error, nothing is changed, and
|
|
230
|
+
the next call tries again. `set_default_backend` and a `with_backend` push
|
|
231
|
+
are the exception: they are about to replace the default anyway, so in a
|
|
232
|
+
child they take over with the backend they were asked for and never rebuild
|
|
233
|
+
the inherited selection — an explicit selection always gets a child out of
|
|
234
|
+
an inherited one that no longer resolves. Closing a scope works the same
|
|
235
|
+
way: a child whose first use of the backend is leaving a block its parent
|
|
236
|
+
entered takes over directly with what the scope restores, so it never needs
|
|
237
|
+
the scope's temporary backend (which it may be unable to rebuild). Closing a
|
|
238
|
+
scope always consumes it: when the backend it should restore cannot be
|
|
239
|
+
rebuilt in the child, the scope's temporary selection stays in effect and
|
|
240
|
+
the error is reported (by `with_backend` as a warning when the block itself
|
|
241
|
+
raised, so that the block's exception is the one that propagates).
|
|
242
|
+
- **`ForkGuarded<T>`** (`runtime.rs`). The payload of every `#[magnus::wrap]`
|
|
243
|
+
type sits in one, tagged with the identity of the creating process. `get()`
|
|
244
|
+
is the only way to it for anything that acts on the value, and in another
|
|
245
|
+
process it returns `Microsandbox::ForkedHandleError` (there is no `Deref`,
|
|
246
|
+
so an unchecked access does not compile). The message names the creating pid
|
|
247
|
+
and the current one; when they are equal — a reused pid — it says so rather
|
|
248
|
+
than naming the same pid twice as two processes. Its `Drop` runs the
|
|
249
|
+
payload's destructor only in the creating process; a child that
|
|
250
|
+
garbage-collects an inherited handle — at the latest when it exits, since
|
|
251
|
+
Ruby frees every wrapped object then — leaks it. Accessors that only copy
|
|
252
|
+
plain data out of the value (`name`, `id`, the `SandboxHandle` snapshot
|
|
253
|
+
fields, `ExecHandle#id`) skip the check through `peek()`, so a child can
|
|
254
|
+
still read the name it needs to open a handle of its own.
|
|
255
|
+
|
|
256
|
+
What a child leaks is bounded: one runtime; once it uses the backend, the
|
|
257
|
+
inherited default backend and the saved backend of each open scope; each
|
|
258
|
+
inherited handle it collects; plus one backend for each parent-opened scope it
|
|
259
|
+
closes. A child that only installs the runtime builds no backend.
|
|
260
|
+
|
|
261
|
+
The mutexes involved (`backend.rs`) are only taken with the GVL held, and
|
|
262
|
+
nothing that runs under them calls into Ruby or releases the GVL — Ruby
|
|
263
|
+
exceptions are built after they are released. A Ruby-level `fork` needs the
|
|
264
|
+
GVL, so a child never inherits one of them locked.
|
|
265
|
+
|
|
266
|
+
Limits, by design:
|
|
267
|
+
|
|
268
|
+
- **Inherited file descriptors.** A child inherits the parent's open fds,
|
|
269
|
+
including the parent-watchdog pipe of every sandbox the parent created.
|
|
270
|
+
While such a child lives, the runtime sees no EOF on that pipe when the
|
|
271
|
+
parent exits, so the sandbox is not torn down until the child exits too.
|
|
272
|
+
- **Ownership.** A sandbox created before the fork belongs to the parent. The
|
|
273
|
+
child cannot act on it through the inherited handle; it can open its own by
|
|
274
|
+
name, with the usual ownership rules.
|
|
275
|
+
- **Threads mid-call.** A thread that is inside a native call when another
|
|
276
|
+
thread forks (the GVL is released there) may hold a lock in the core at
|
|
277
|
+
that instant — for example the read guard of the core's default-backend
|
|
278
|
+
`RwLock`, for the few instructions of an `Arc` clone — and the child's swap
|
|
279
|
+
then waits on that inherited lock forever. The binding cannot close this
|
|
280
|
+
window. Forking while another thread is inside a call is unsupported, as
|
|
281
|
+
with any threaded native library.
|
|
282
|
+
- **Other libraries.** The SDK no longer touches inherited state; code that
|
|
283
|
+
reaches it some other way can still hang or trap.
|
|
284
|
+
- **macOS system frameworks.** On macOS a forked child that has not called
|
|
285
|
+
`exec` cannot use the system frameworks the core reaches for network and
|
|
286
|
+
credential work: CoreFoundation and libobjc are not fork-safe, and the
|
|
287
|
+
child dies — `SIGSEGV`, or `SIGABRT` from libobjc's fork check — inside the
|
|
288
|
+
framework. It is a crash, not an exception; nothing can rescue it. Two such
|
|
289
|
+
calls sit on SDK paths. Every HTTP client the core builds (reqwest, with the
|
|
290
|
+
`system-proxy` feature that the core crate enables; a downstream crate
|
|
291
|
+
cannot turn a Cargo feature off) looks up the system proxy settings through
|
|
292
|
+
SystemConfiguration (`SCDynamicStoreCopyProxies`) while it is built; and a
|
|
293
|
+
pull without explicit `registry_auth:` first looks for registry credentials
|
|
294
|
+
in the Keychain (`keyring`). So in a forked child on macOS, each of these
|
|
295
|
+
kills the child:
|
|
296
|
+
- any use of a cloud backend, however it is chosen (`set_default_backend`,
|
|
297
|
+
`with_backend`, `MSB_BACKEND`/`MSB_API_KEY`, a profile) — the child's
|
|
298
|
+
rebuild of the backend described above builds a client;
|
|
299
|
+
- `Sandbox.create` with an image that is not in the image cache yet, or
|
|
300
|
+
with `pull_policy: :always` (the Keychain lookup, then the registry
|
|
301
|
+
client; with `registry_auth:` only the latter);
|
|
302
|
+
- `Image.load` (it builds a registry client even for a local archive);
|
|
303
|
+
- runtime provisioning: `Microsandbox.install`, `Microsandbox.setup`, and
|
|
304
|
+
the first-use install in `ensure_runtime!`.
|
|
305
|
+
|
|
306
|
+
Whether the parent used the SDK makes no difference: a child of a parent
|
|
307
|
+
that only required the gem dies the same way, and
|
|
308
|
+
`OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES` does not prevent it. Calls that
|
|
309
|
+
stay on the local catalog and runtime are unaffected, including
|
|
310
|
+
`Sandbox.create` with an image already in the cache. On Linux the cloud
|
|
311
|
+
case is exercised by `spec/unit/fork_safety_spec.rb` and works (those
|
|
312
|
+
examples are skipped on macOS). On macOS, do this work in a process that
|
|
313
|
+
was not forked — the parent before it forks, or a fresh one
|
|
314
|
+
(`Process.spawn`, or `fork` followed by `exec`) — for example, pull the
|
|
315
|
+
images the workers need in the master before forking them.
|
|
316
|
+
- **Forks that skip atfork handlers.** A process created with `vfork` or a
|
|
317
|
+
raw `clone` keeps its parent's generation, so only its pid identifies it.
|
|
318
|
+
Ruby itself never forks that way — `Kernel#fork` runs the handlers, and
|
|
319
|
+
`spawn`/`system` `exec` at once, which discards all of this state — so
|
|
320
|
+
mistaking such a process for its ancestor needs a native library that
|
|
321
|
+
clones without the handlers and then runs Ruby, together with exact pid
|
|
322
|
+
reuse.
|
|
323
|
+
|
|
324
|
+
`spec/unit/fork_safety_spec.rb` (catalog only, no runtime),
|
|
325
|
+
`spec/fake_runtime/fork_safety_spec.rb` (a live handle over the fake `msb`)
|
|
326
|
+
and `spec/integration/fork_safety_spec.rb` (a real microVM) pin the behaviour;
|
|
327
|
+
each forks from a probe process of its own.
|
|
328
|
+
|
|
138
329
|
## Error mapping
|
|
139
330
|
|
|
140
331
|
The core returns one big `MicrosandboxError` enum. `error.rs` maps each variant
|
data/README.md
CHANGED
|
@@ -167,7 +167,7 @@ See [docs/observability.md](docs/observability.md).
|
|
|
167
167
|
| Guest filesystem, streaming reads/writes, host copies | [docs/filesystem.md](docs/filesystem.md) |
|
|
168
168
|
| Metrics, logs, `ping`/`touch`, live `modify` plans | [docs/observability.md](docs/observability.md) |
|
|
169
169
|
| Image cache, private registries, snapshots | [docs/images.md](docs/images.md) |
|
|
170
|
-
| Runtime binaries gem, `msb` resolution order, backends, environment variables | [docs/runtime.md](docs/runtime.md) |
|
|
170
|
+
| Runtime binaries gem, `msb` resolution order, backends, forking, environment variables | [docs/runtime.md](docs/runtime.md) |
|
|
171
171
|
| Error classes and stable codes | [docs/errors.md](docs/errors.md) |
|
|
172
172
|
| Complete supported surface and what is not yet exposed | [docs/surface.md](docs/surface.md) |
|
|
173
173
|
| Release process, release preflight, binaries gems | [docs/releasing.md](docs/releasing.md) |
|
|
@@ -209,7 +209,7 @@ share the upstream tag, but the numbers have diverged since and the gem version
|
|
|
209
209
|
is **not** an indicator of the embedded runtime. Ask the build instead:
|
|
210
210
|
|
|
211
211
|
```ruby
|
|
212
|
-
Microsandbox::VERSION # => "0.17.
|
|
212
|
+
Microsandbox::VERSION # => "0.17.4" (the gem's own version)
|
|
213
213
|
Microsandbox.runtime_version # => "v0.6.18" (the embedded upstream runtime tag)
|
|
214
214
|
```
|
|
215
215
|
|
|
@@ -247,6 +247,8 @@ constants against this gem's. Every release records its runtime in
|
|
|
247
247
|
| `0.17.0` | `v0.6.18` | adopts upstream `v0.6.17`+`v0.6.18` step by step: outbound SOCKS4/SOCKS5 proxies (SOCKS5 UDP + credentials), migration-order fix for databases last opened by a `v0.6.15` `msb`, security hardening — plain-HTTP `Host`/`:authority` now checked against domain-rule policies even in non-strict mode, plus fail-closed strict hostname mode. Parity: `proxy:` (`Microsandbox::OutboundProxy` / `SecretSource`), `strict:` |
|
|
248
248
|
| `0.17.1` | `v0.6.18` | gem-only security and hardening release: relative host paths pinned at create time (GHSA-mcw9-9rf6-p4rm), `Volume.fs` path validation, `Cargo.lock` advisory updates, argument-guard and cleanup fixes, `SECURITY.md` |
|
|
249
249
|
| `0.17.2` | `v0.6.18` | gem-only fix and release-pipeline hardening release: an interrupted native call no longer loses its result (a sandbox the call owns is stopped on `Timeout`/`Thread#kill`), release preflight, SHA-pinned workflow actions and `cargo deny` advisory checks, RBS and docs contract specs |
|
|
250
|
+
| `0.17.3` | `v0.6.18` | gem-only fix release, fork safety: a forked child (Puma, Resque, Solid Queue) rebuilds its backend and runtime on its first SDK call instead of hanging or crashing, a handle created before the fork raises `ForkedHandleError` in the child, and nothing inherited from the parent is dropped there |
|
|
251
|
+
| `0.17.4` | `v0.6.18` | gem-only fix release: fork safety identifies a process by pid and fork generation (survives pid reuse), `install`/`setup` leave the backend alone in a forked child, a `Sandbox` subclass's `initialize` runs outside the interrupt mask, and the macOS limit that a forked child must not build network clients is documented; rustls 0.23.46 fixes four TLS advisories (RUSTSEC-2026-0346 to -0349) |
|
|
250
252
|
|
|
251
253
|
## Development
|
|
252
254
|
|
data/docs/errors.md
CHANGED
|
@@ -56,6 +56,7 @@ options) raises a plain `ArgumentError` before the native layer is reached.
|
|
|
56
56
|
| `UnsupportedOperationError` | `unsupported-operation` | The runtime version does not support the operation |
|
|
57
57
|
| `CloudHttpError` | `cloud-http` | Cloud backend HTTP failure |
|
|
58
58
|
| `UnsupportedError` | `unsupported` | The selected backend does not implement the operation; carries `#operation` (e.g. `"sandbox.kill"`) and `#hint` |
|
|
59
|
+
| `ForkedHandleError` | `forked-handle` | A handle was used in a process forked from the one that created it; open a new one in the child (see [runtime.md](runtime.md#forking-puma-resque-solid-queue)) |
|
|
59
60
|
|
|
60
61
|
The snapshot classes go beyond the Python SDK (which collapses them into its
|
|
61
62
|
base error) and match the Go SDK's per-variant coverage, so callers can rescue
|