microsandbox-rb 0.17.3 → 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 +68 -1
- data/Cargo.lock +34 -32
- data/DESIGN.md +134 -41
- data/README.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
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,72 @@ 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
|
+
|
|
11
77
|
## [0.17.3] - 2026-10-13
|
|
12
78
|
|
|
13
79
|
Gem-only fix release; the embedded runtime stays at **`v0.6.18`**. A process
|
|
@@ -1513,7 +1579,8 @@ microsandbox runtime, aligned with the official Python/Node/Go SDKs.
|
|
|
1513
1579
|
core crate has Apple-native deps). Until precompiled gems are published,
|
|
1514
1580
|
installing from source requires a Rust toolchain (stable >= 1.91).
|
|
1515
1581
|
|
|
1516
|
-
[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
|
|
1517
1584
|
[0.17.3]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.17.2...v0.17.3
|
|
1518
1585
|
[0.17.2]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.17.1...v0.17.2
|
|
1519
1586
|
[0.17.1]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.17.0...v0.17.1
|
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,7 +143,9 @@ 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
|
|
@@ -148,23 +164,62 @@ Linux and die with `SIGTRAP` on macOS — and so does *destroying* it, because
|
|
|
148
164
|
the destructors wake the same missing threads (on macOS, replacing an
|
|
149
165
|
inherited `LocalBackend` was enough to kill the child). One rule covers all of
|
|
150
166
|
it: **inherited native state is never used and never dropped in the child. It
|
|
151
|
-
is leaked, once per fork, and replaced by state built in the child.**
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
163
213
|
backend was chosen — the core's own ladder (environment, profile, config
|
|
164
214
|
file), or an explicit `set_default_backend`/`with_backend` selection — and
|
|
165
|
-
the
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
168
223
|
builds a fresh backend from the record, swaps it into the core and
|
|
169
224
|
`mem::forget`s the inherited one, along with the backends that open
|
|
170
225
|
`with_backend` scopes had saved for their restore. A scope the parent opened
|
|
@@ -185,20 +240,23 @@ pieces implement it, all keyed on the pid (`runtime::current_pid`):
|
|
|
185
240
|
the error is reported (by `with_backend` as a warning when the block itself
|
|
186
241
|
raised, so that the block's exception is the one that propagates).
|
|
187
242
|
- **`ForkGuarded<T>`** (`runtime.rs`). The payload of every `#[magnus::wrap]`
|
|
188
|
-
type sits in one, tagged with the creating
|
|
189
|
-
it for anything that acts on the value, and in another
|
|
190
|
-
`Microsandbox::ForkedHandleError` (there is no `Deref`,
|
|
191
|
-
access does not compile).
|
|
192
|
-
the
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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.
|
|
202
260
|
|
|
203
261
|
The mutexes involved (`backend.rs`) are only taken with the GVL held, and
|
|
204
262
|
nothing that runs under them calls into Ruby or releases the GVL — Ruby
|
|
@@ -223,10 +281,45 @@ Limits, by design:
|
|
|
223
281
|
with any threaded native library.
|
|
224
282
|
- **Other libraries.** The SDK no longer touches inherited state; code that
|
|
225
283
|
reaches it some other way can still hang or trap.
|
|
226
|
-
- **
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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.
|
|
230
323
|
|
|
231
324
|
`spec/unit/fork_safety_spec.rb` (catalog only, no runtime),
|
|
232
325
|
`spec/fake_runtime/fork_safety_spec.rb` (a live handle over the fake `msb`)
|
data/README.md
CHANGED
|
@@ -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
|
|
|
@@ -248,6 +248,7 @@ constants against this gem's. Every release records its runtime in
|
|
|
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
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) |
|
|
251
252
|
|
|
252
253
|
## Development
|
|
253
254
|
|
data/docs/runtime.md
CHANGED
|
@@ -114,7 +114,9 @@ backend), chosen the same way as the parent's: an explicit
|
|
|
114
114
|
`set_default_backend`, an enclosing `with_backend` block, or — when neither
|
|
115
115
|
was used — the environment and config file as the child sees them at that
|
|
116
116
|
call. Everything addressed by name works in the child from then on:
|
|
117
|
-
`Sandbox.create`/`get`/`list`/`start`, `Volume`, `Image`, `Snapshot
|
|
117
|
+
`Sandbox.create`/`get`/`list`/`start`, `Volume`, `Image`, `Snapshot` — except
|
|
118
|
+
that on macOS a forked child must not do anything that builds a network
|
|
119
|
+
client (see [macOS](#macos-no-network-clients-in-a-forked-child) below).
|
|
118
120
|
|
|
119
121
|
If the parent's choice cannot be made again in the child (say the child's
|
|
120
122
|
environment now selects the cloud backend without credentials), SDK calls
|
|
@@ -126,6 +128,7 @@ does not depend on the inherited choice and works regardless.
|
|
|
126
128
|
Microsandbox::Sandbox.list # the parent has used the SDK
|
|
127
129
|
|
|
128
130
|
pid = fork do
|
|
131
|
+
# On macOS "alpine" must already be in the image cache (see below).
|
|
129
132
|
Microsandbox::Sandbox.create("job-#{Process.pid}", image: "alpine") do |sb|
|
|
130
133
|
puts sb.exec("echo", ["hello from the worker"]).stdout
|
|
131
134
|
end
|
|
@@ -137,8 +140,8 @@ What does **not** cross a fork is an object created before it. A `Sandbox`,
|
|
|
137
140
|
`SandboxHandle`, `ExecHandle`, `ExecStdin`, `PullSession`, log/metrics/file
|
|
138
141
|
stream, `SshClient`/`SftpClient`/`SshServer`, `AgentClient` or `VolumeFs`
|
|
139
142
|
belongs to the process that created it. In a forked child, any call that acts
|
|
140
|
-
on one raises `Microsandbox::ForkedHandleError` (the message names
|
|
141
|
-
pids); the original keeps working in the parent. Reading `name` or `id` — and
|
|
143
|
+
on one raises `Microsandbox::ForkedHandleError` (the message names the
|
|
144
|
+
pids involved); the original keeps working in the parent. Reading `name` or `id` — and
|
|
142
145
|
the snapshot fields of a `SandboxHandle` — still works, so a child can open
|
|
143
146
|
its own handle:
|
|
144
147
|
|
|
@@ -189,6 +192,33 @@ Things to know:
|
|
|
189
192
|
parent's runtime, backend and whichever inherited handles it collects)
|
|
190
193
|
instead of destroying it — destroying it is what used to crash.
|
|
191
194
|
|
|
195
|
+
### macOS: no network clients in a forked child
|
|
196
|
+
|
|
197
|
+
On macOS, a forked child that has not called `exec` dies — `SIGSEGV` or
|
|
198
|
+
`SIGABRT`, not an exception, so it cannot be rescued — as soon as the SDK
|
|
199
|
+
builds a network client or reads registry credentials from the Keychain
|
|
200
|
+
there. macOS's system frameworks do not support being used after `fork`
|
|
201
|
+
without `exec`, and every HTTP client the SDK builds looks up the system proxy
|
|
202
|
+
settings through them. In a forked child on macOS, do not:
|
|
203
|
+
|
|
204
|
+
- use a cloud backend at all, however it is selected (`set_default_backend`,
|
|
205
|
+
`with_backend`, `MSB_BACKEND`/`MSB_API_KEY`, a profile);
|
|
206
|
+
- `Sandbox.create` with an image that is not in the image cache yet, or with
|
|
207
|
+
`pull_policy: :always`;
|
|
208
|
+
- `Image.load`;
|
|
209
|
+
- install the runtime: `Microsandbox.install`, `Microsandbox.setup`, or the
|
|
210
|
+
first-use install `ensure_runtime!` performs.
|
|
211
|
+
|
|
212
|
+
It makes no difference whether the parent used the SDK, and
|
|
213
|
+
`OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES` does not help. Local work is
|
|
214
|
+
unaffected, including `Sandbox.create` with an image that is already cached.
|
|
215
|
+
On Linux the cloud backend works in a forked child.
|
|
216
|
+
|
|
217
|
+
Do that work in a process that was not forked: in the parent before it forks
|
|
218
|
+
(install the runtime and pull the images the workers will use — creating a
|
|
219
|
+
sandbox from the image pulls it), or in a fresh process started with
|
|
220
|
+
`Process.spawn` (or `fork` followed by `exec`).
|
|
221
|
+
|
|
192
222
|
## One runtime line per home
|
|
193
223
|
|
|
194
224
|
The runtime home (`MSB_HOME`, default `~/.microsandbox`) belongs to one
|
data/docs/surface.md
CHANGED
|
@@ -59,7 +59,10 @@ counterpart, and [DESIGN.md](../DESIGN.md) explains how the layers fit.
|
|
|
59
59
|
- **Errors** — the typed hierarchy in [errors.md](errors.md).
|
|
60
60
|
- **Process model** — the SDK may be used before and after `fork`; objects
|
|
61
61
|
created before a fork are refused in the child with `ForkedHandleError`
|
|
62
|
-
(see [runtime.md](runtime.md#forking-puma-resque-solid-queue)).
|
|
62
|
+
(see [runtime.md](runtime.md#forking-puma-resque-solid-queue)). On macOS a
|
|
63
|
+
forked child must not build network clients — cloud backends, image pulls,
|
|
64
|
+
`Image.load`, runtime install — or it crashes (see
|
|
65
|
+
[runtime.md](runtime.md#macos-no-network-clients-in-a-forked-child)).
|
|
63
66
|
|
|
64
67
|
Create options span resources (`cpus`/`memory`/`max_cpus`/`max_memory`),
|
|
65
68
|
`env`/`workdir`/`labels`/`user`/`hostname`/`shell`, named `scripts`, the image
|