├── .github └── workflows │ ├── build.yml │ └── release-plz.yml ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── benches └── benchmarks.rs ├── examples ├── blocking-signals-demonstration.rs ├── blocking-signals-minimal.rs ├── error-handling-basic.rs ├── error-handling-complete.rs ├── memory-leak.rs └── tracing.rs ├── release-plz.toml └── src ├── c.rs ├── c └── mock.rs ├── errors.rs ├── lib.rs ├── macros.rs └── tests.rs /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: ["main"] 5 | pull_request: 6 | branches: ["main"] 7 | env: 8 | CARGO_TERM_COLOR: always 9 | jobs: 10 | build: 11 | name: Build & Test (${{ matrix.os }}) 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, macos-latest] 16 | steps: 17 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 18 | 19 | # selecting a toolchain either by action or manual `rustup` calls should 20 | # happen before the cache plugin, as the cache uses the current rustc 21 | # version as its cache key 22 | - name: Install Rust Toolchain 23 | run: | 24 | rustup toolchain install stable --profile minimal 25 | 26 | - name: Rust Cache 27 | uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2 28 | with: 29 | workspaces: | 30 | . -> target 31 | 32 | # Manages cargo installs with proper caching support 33 | - name: Install cargo-hack 34 | uses: taiki-e/install-action@f1390fd0d8205ae79e5e57b1d1e300dceeb4163e # v2 35 | with: 36 | # An arbitrary version SemVer constrained to prevent breaking CI as 37 | # time goes on. Feel free to bump. 38 | tool: cargo-hack@0.6 39 | 40 | - name: Remove Cargo.lock 41 | # Because this is a library crate, it is best to not use a lockfile so 42 | # we are more quickly alerted to breaking changes if a dependency 43 | # violates semver. 44 | run: rm Cargo.lock 45 | 46 | # Fail fast 47 | - name: Check 48 | run: cargo hack check --feature-powerset --no-dev-deps 49 | 50 | # Catch link-time errors missed by check 51 | - name: Build 52 | run: cargo build --all-features 53 | 54 | # Proper exhaustive testing 55 | - name: Test 56 | # Test can't be run with --no-dev-deps 57 | run: cargo hack test --feature-powerset -- --test-threads=1 58 | 59 | # Run clippy treating warnings as errors 60 | - name: Clippy 61 | run: cargo hack clippy --feature-powerset --no-dev-deps -- -D warnings 62 | 63 | # Check formatting 64 | - name: Format 65 | run: cargo fmt --all --check 66 | -------------------------------------------------------------------------------- /.github/workflows/release-plz.yml: -------------------------------------------------------------------------------- 1 | name: Release-plz 2 | 3 | permissions: 4 | pull-requests: write 5 | contents: write 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | # Release unpublished packages. 14 | release-plz-release: 15 | name: Release-plz release 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: write 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4 22 | with: 23 | fetch-depth: 0 24 | - name: Install Rust toolchain 25 | uses: dtolnay/rust-toolchain@stable 26 | - name: Run release-plz 27 | uses: release-plz/action@7419a2cb1535b9c0e852b4dec626967baf65c022 # v0.5.102 28 | with: 29 | command: release 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 33 | 34 | # Create a PR with the new versions and changelog, preparing the next release. 35 | release-plz-pr: 36 | name: Release-plz PR 37 | runs-on: ubuntu-latest 38 | permissions: 39 | contents: write 40 | pull-requests: write 41 | concurrency: 42 | group: release-plz-${{ github.ref }} 43 | cancel-in-progress: false 44 | steps: 45 | - name: Checkout repository 46 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4 47 | with: 48 | fetch-depth: 0 49 | - name: Install Rust toolchain 50 | uses: dtolnay/rust-toolchain@stable 51 | - name: Run release-plz 52 | uses: release-plz/action@7419a2cb1535b9c0e852b4dec626967baf65c022 # v0.5.102 53 | with: 54 | command: release-pr 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | tarpaulin-report.html 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.cargo.features": ["full"] 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.1.6](https://github.com/brannondorsey/mem-isolate/compare/v0.1.5...v0.1.6) - 2025-04-24 11 | 12 | ### Added 13 | 14 | - *(tests)* Run tests on MacOS in addition to Ubuntu ([#60](https://github.com/brannondorsey/mem-isolate/pull/60)) 15 | 16 | ### Fixed 17 | 18 | - Ensure large results are handled correctly ([#56](https://github.com/brannondorsey/mem-isolate/pull/56)) 19 | 20 | ### Other 21 | 22 | - Add empty test and TODO comment ([#59](https://github.com/brannondorsey/mem-isolate/pull/59)) 23 | - Add test that covers the case when the user-defined callable panics ([#57](https://github.com/brannondorsey/mem-isolate/pull/57)) 24 | 25 | ## [0.1.5](https://github.com/brannondorsey/mem-isolate/compare/v0.1.4...v0.1.5) - 2025-04-20 26 | 27 | ### Fixed 28 | 29 | - Run tests with one thread only as a fix for flaky tests that hang indefinitely ([#49](https://github.com/brannondorsey/mem-isolate/pull/49)). See [#52](https://github.com/brannondorsey/mem-isolate/pull/52) for how we tested this fix. 30 | 31 | ### Other 32 | 33 | - Add 1 second default timeouts to all tests ([#50](https://github.com/brannondorsey/mem-isolate/pull/50)) 34 | - Bump test timeouts for two flaky tests ([#53](https://github.com/brannondorsey/mem-isolate/pull/53)) 35 | - Error on clippy warnings and check formatting in CI ([#48](https://github.com/brannondorsey/mem-isolate/pull/48)) 36 | 37 | ## [0.1.4](https://github.com/brannondorsey/mem-isolate/compare/v0.1.3...v0.1.4) - 2025-04-15 38 | 39 | ### Other 40 | 41 | - Include discussion section in README with links to Hacker News and Reddit posts ([#47](https://github.com/brannondorsey/mem-isolate/pull/47)) 42 | - Clarify safety claims and stress limitations in README and module documentation ([#44](https://github.com/brannondorsey/mem-isolate/pull/44)) 43 | - Add examples illustrating how to block and restore signals around mem-isolate calls ([#45](https://github.com/brannondorsey/mem-isolate/pull/45)) 44 | 45 | ## [0.1.3](https://github.com/brannondorsey/mem-isolate/compare/v0.1.2...v0.1.3) - 2025-04-05 46 | 47 | ### Fixed 48 | 49 | - Handle syscall interruption by retrying `EINTR` errors encountered during `waitpid()` ([#35](https://github.com/brannondorsey/mem-isolate/pull/35)) 50 | 51 | ### Other 52 | 53 | - Cargo update ([#39](https://github.com/brannondorsey/mem-isolate/pull/39)) 54 | - Simplify MockableSystemFunctions Debug impl ([#37](https://github.com/brannondorsey/mem-isolate/pull/37)) 55 | - Add optional tracing to tests ([#36](https://github.com/brannondorsey/mem-isolate/pull/36)) 56 | - Refactor `fork()` into a subroutine ([#29](https://github.com/brannondorsey/mem-isolate/pull/29)) 57 | 58 | ## [0.1.2](https://github.com/brannondorsey/mem-isolate/compare/v0.1.1...v0.1.2) - 2025-03-16 59 | 60 | ### Added 61 | 62 | - Add `tracing` feature ([#24](https://github.com/brannondorsey/mem-isolate/pull/24)) 63 | 64 | ### Other 65 | 66 | - Cut a new release only when the release PR is merged ([#28](https://github.com/brannondorsey/mem-isolate/pull/28)) 67 | - Clarify function purity definition in README and module description ([#26](https://github.com/brannondorsey/mem-isolate/pull/26)) 68 | - Add a benchmark indicating how fast a `fork()` could be without a `wait()` afterwards ([#23](https://github.com/brannondorsey/mem-isolate/pull/23)) 69 | - Use `cargo-hack` in CI to test all feature combinations ([#25](https://github.com/brannondorsey/mem-isolate/pull/25)) 70 | - Refactor for readability ([#19](https://github.com/brannondorsey/mem-isolate/pull/19)) 71 | 72 | ## [0.1.1](https://github.com/brannondorsey/mem-isolate/compare/v0.1.0...v0.1.1) - 2025-03-14 73 | 74 | ### Other 75 | 76 | - Split LICENSE into LICENSE-MIT and LICENSE-APACHE ([#16](https://github.com/brannondorsey/mem-isolate/pull/16)) 77 | - Pin to release-plz action version for security ([#15](https://github.com/brannondorsey/mem-isolate/pull/15)) 78 | - Run clippy with --all-targets via CI ([#14](https://github.com/brannondorsey/mem-isolate/pull/14)) 79 | - Enable and conform to more strict clippy lints ([#13](https://github.com/brannondorsey/mem-isolate/pull/13)) 80 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anes" 16 | version = "0.1.6" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" 19 | 20 | [[package]] 21 | name = "anstyle" 22 | version = "1.0.10" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 25 | 26 | [[package]] 27 | name = "autocfg" 28 | version = "1.4.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 31 | 32 | [[package]] 33 | name = "bincode" 34 | version = "1.3.3" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" 37 | dependencies = [ 38 | "serde", 39 | ] 40 | 41 | [[package]] 42 | name = "bitflags" 43 | version = "2.9.0" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 46 | 47 | [[package]] 48 | name = "bumpalo" 49 | version = "3.17.0" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 52 | 53 | [[package]] 54 | name = "cast" 55 | version = "0.3.0" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" 58 | 59 | [[package]] 60 | name = "cfg-if" 61 | version = "1.0.0" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 64 | 65 | [[package]] 66 | name = "cfg_aliases" 67 | version = "0.2.1" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 70 | 71 | [[package]] 72 | name = "ciborium" 73 | version = "0.2.2" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" 76 | dependencies = [ 77 | "ciborium-io", 78 | "ciborium-ll", 79 | "serde", 80 | ] 81 | 82 | [[package]] 83 | name = "ciborium-io" 84 | version = "0.2.2" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" 87 | 88 | [[package]] 89 | name = "ciborium-ll" 90 | version = "0.2.2" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" 93 | dependencies = [ 94 | "ciborium-io", 95 | "half", 96 | ] 97 | 98 | [[package]] 99 | name = "clap" 100 | version = "4.5.35" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" 103 | dependencies = [ 104 | "clap_builder", 105 | ] 106 | 107 | [[package]] 108 | name = "clap_builder" 109 | version = "4.5.35" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" 112 | dependencies = [ 113 | "anstyle", 114 | "clap_lex", 115 | ] 116 | 117 | [[package]] 118 | name = "clap_lex" 119 | version = "0.7.4" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 122 | 123 | [[package]] 124 | name = "criterion" 125 | version = "0.5.1" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" 128 | dependencies = [ 129 | "anes", 130 | "cast", 131 | "ciborium", 132 | "clap", 133 | "criterion-plot", 134 | "is-terminal", 135 | "itertools", 136 | "num-traits", 137 | "once_cell", 138 | "oorandom", 139 | "plotters", 140 | "rayon", 141 | "regex", 142 | "serde", 143 | "serde_derive", 144 | "serde_json", 145 | "tinytemplate", 146 | "walkdir", 147 | ] 148 | 149 | [[package]] 150 | name = "criterion-plot" 151 | version = "0.5.0" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" 154 | dependencies = [ 155 | "cast", 156 | "itertools", 157 | ] 158 | 159 | [[package]] 160 | name = "crossbeam-deque" 161 | version = "0.8.6" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 164 | dependencies = [ 165 | "crossbeam-epoch", 166 | "crossbeam-utils", 167 | ] 168 | 169 | [[package]] 170 | name = "crossbeam-epoch" 171 | version = "0.9.18" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 174 | dependencies = [ 175 | "crossbeam-utils", 176 | ] 177 | 178 | [[package]] 179 | name = "crossbeam-utils" 180 | version = "0.8.21" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 183 | 184 | [[package]] 185 | name = "crunchy" 186 | version = "0.2.3" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" 189 | 190 | [[package]] 191 | name = "ctor" 192 | version = "0.4.1" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "07e9666f4a9a948d4f1dff0c08a4512b0f7c86414b23960104c243c10d79f4c3" 195 | dependencies = [ 196 | "ctor-proc-macro", 197 | "dtor", 198 | ] 199 | 200 | [[package]] 201 | name = "ctor-proc-macro" 202 | version = "0.0.5" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "4f211af61d8efdd104f96e57adf5e426ba1bc3ed7a4ead616e15e5881fd79c4d" 205 | 206 | [[package]] 207 | name = "dtor" 208 | version = "0.0.5" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "222ef136a1c687d4aa0395c175f2c4586e379924c352fd02f7870cf7de783c23" 211 | dependencies = [ 212 | "dtor-proc-macro", 213 | ] 214 | 215 | [[package]] 216 | name = "dtor-proc-macro" 217 | version = "0.0.5" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "7454e41ff9012c00d53cf7f475c5e3afa3b91b7c90568495495e8d9bf47a1055" 220 | 221 | [[package]] 222 | name = "either" 223 | version = "1.15.0" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 226 | 227 | [[package]] 228 | name = "errno" 229 | version = "0.3.11" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" 232 | dependencies = [ 233 | "libc", 234 | "windows-sys", 235 | ] 236 | 237 | [[package]] 238 | name = "fastrand" 239 | version = "2.3.0" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 242 | 243 | [[package]] 244 | name = "getrandom" 245 | version = "0.3.2" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" 248 | dependencies = [ 249 | "cfg-if", 250 | "libc", 251 | "r-efi", 252 | "wasi", 253 | ] 254 | 255 | [[package]] 256 | name = "glob" 257 | version = "0.3.2" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" 260 | 261 | [[package]] 262 | name = "half" 263 | version = "2.5.0" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "7db2ff139bba50379da6aa0766b52fdcb62cb5b263009b09ed58ba604e14bbd1" 266 | dependencies = [ 267 | "cfg-if", 268 | "crunchy", 269 | ] 270 | 271 | [[package]] 272 | name = "hermit-abi" 273 | version = "0.5.0" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" 276 | 277 | [[package]] 278 | name = "is-terminal" 279 | version = "0.4.16" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" 282 | dependencies = [ 283 | "hermit-abi", 284 | "libc", 285 | "windows-sys", 286 | ] 287 | 288 | [[package]] 289 | name = "itertools" 290 | version = "0.10.5" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 293 | dependencies = [ 294 | "either", 295 | ] 296 | 297 | [[package]] 298 | name = "itoa" 299 | version = "1.0.15" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 302 | 303 | [[package]] 304 | name = "js-sys" 305 | version = "0.3.77" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 308 | dependencies = [ 309 | "once_cell", 310 | "wasm-bindgen", 311 | ] 312 | 313 | [[package]] 314 | name = "lazy_static" 315 | version = "1.5.0" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 318 | 319 | [[package]] 320 | name = "libc" 321 | version = "0.2.171" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" 324 | 325 | [[package]] 326 | name = "linux-raw-sys" 327 | version = "0.9.3" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" 330 | 331 | [[package]] 332 | name = "log" 333 | version = "0.4.27" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 336 | 337 | [[package]] 338 | name = "matchers" 339 | version = "0.1.0" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 342 | dependencies = [ 343 | "regex-automata 0.1.10", 344 | ] 345 | 346 | [[package]] 347 | name = "mem-isolate" 348 | version = "0.1.6" 349 | dependencies = [ 350 | "bincode", 351 | "criterion", 352 | "ctor", 353 | "libc", 354 | "nix", 355 | "rand", 356 | "rstest", 357 | "serde", 358 | "tempfile", 359 | "thiserror", 360 | "tracing", 361 | "tracing-subscriber", 362 | ] 363 | 364 | [[package]] 365 | name = "memchr" 366 | version = "2.7.4" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 369 | 370 | [[package]] 371 | name = "nix" 372 | version = "0.29.0" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" 375 | dependencies = [ 376 | "bitflags", 377 | "cfg-if", 378 | "cfg_aliases", 379 | "libc", 380 | ] 381 | 382 | [[package]] 383 | name = "nu-ansi-term" 384 | version = "0.46.0" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 387 | dependencies = [ 388 | "overload", 389 | "winapi", 390 | ] 391 | 392 | [[package]] 393 | name = "num-traits" 394 | version = "0.2.19" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 397 | dependencies = [ 398 | "autocfg", 399 | ] 400 | 401 | [[package]] 402 | name = "once_cell" 403 | version = "1.21.3" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 406 | 407 | [[package]] 408 | name = "oorandom" 409 | version = "11.1.5" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" 412 | 413 | [[package]] 414 | name = "overload" 415 | version = "0.1.1" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 418 | 419 | [[package]] 420 | name = "pin-project-lite" 421 | version = "0.2.16" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 424 | 425 | [[package]] 426 | name = "plotters" 427 | version = "0.3.7" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" 430 | dependencies = [ 431 | "num-traits", 432 | "plotters-backend", 433 | "plotters-svg", 434 | "wasm-bindgen", 435 | "web-sys", 436 | ] 437 | 438 | [[package]] 439 | name = "plotters-backend" 440 | version = "0.3.7" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" 443 | 444 | [[package]] 445 | name = "plotters-svg" 446 | version = "0.3.7" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" 449 | dependencies = [ 450 | "plotters-backend", 451 | ] 452 | 453 | [[package]] 454 | name = "ppv-lite86" 455 | version = "0.2.21" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 458 | dependencies = [ 459 | "zerocopy", 460 | ] 461 | 462 | [[package]] 463 | name = "proc-macro2" 464 | version = "1.0.94" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 467 | dependencies = [ 468 | "unicode-ident", 469 | ] 470 | 471 | [[package]] 472 | name = "quote" 473 | version = "1.0.40" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 476 | dependencies = [ 477 | "proc-macro2", 478 | ] 479 | 480 | [[package]] 481 | name = "r-efi" 482 | version = "5.2.0" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 485 | 486 | [[package]] 487 | name = "rand" 488 | version = "0.9.0" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" 491 | dependencies = [ 492 | "rand_chacha", 493 | "rand_core", 494 | "zerocopy", 495 | ] 496 | 497 | [[package]] 498 | name = "rand_chacha" 499 | version = "0.9.0" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 502 | dependencies = [ 503 | "ppv-lite86", 504 | "rand_core", 505 | ] 506 | 507 | [[package]] 508 | name = "rand_core" 509 | version = "0.9.3" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 512 | dependencies = [ 513 | "getrandom", 514 | ] 515 | 516 | [[package]] 517 | name = "rayon" 518 | version = "1.10.0" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 521 | dependencies = [ 522 | "either", 523 | "rayon-core", 524 | ] 525 | 526 | [[package]] 527 | name = "rayon-core" 528 | version = "1.12.1" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 531 | dependencies = [ 532 | "crossbeam-deque", 533 | "crossbeam-utils", 534 | ] 535 | 536 | [[package]] 537 | name = "regex" 538 | version = "1.11.1" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 541 | dependencies = [ 542 | "aho-corasick", 543 | "memchr", 544 | "regex-automata 0.4.9", 545 | "regex-syntax 0.8.5", 546 | ] 547 | 548 | [[package]] 549 | name = "regex-automata" 550 | version = "0.1.10" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 553 | dependencies = [ 554 | "regex-syntax 0.6.29", 555 | ] 556 | 557 | [[package]] 558 | name = "regex-automata" 559 | version = "0.4.9" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 562 | dependencies = [ 563 | "aho-corasick", 564 | "memchr", 565 | "regex-syntax 0.8.5", 566 | ] 567 | 568 | [[package]] 569 | name = "regex-syntax" 570 | version = "0.6.29" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 573 | 574 | [[package]] 575 | name = "regex-syntax" 576 | version = "0.8.5" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 579 | 580 | [[package]] 581 | name = "relative-path" 582 | version = "1.9.3" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" 585 | 586 | [[package]] 587 | name = "rstest" 588 | version = "0.25.0" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "6fc39292f8613e913f7df8fa892b8944ceb47c247b78e1b1ae2f09e019be789d" 591 | dependencies = [ 592 | "rstest_macros", 593 | "rustc_version", 594 | ] 595 | 596 | [[package]] 597 | name = "rstest_macros" 598 | version = "0.25.0" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "1f168d99749d307be9de54d23fd226628d99768225ef08f6ffb52e0182a27746" 601 | dependencies = [ 602 | "cfg-if", 603 | "glob", 604 | "proc-macro2", 605 | "quote", 606 | "regex", 607 | "relative-path", 608 | "rustc_version", 609 | "syn", 610 | "unicode-ident", 611 | ] 612 | 613 | [[package]] 614 | name = "rustc_version" 615 | version = "0.4.1" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 618 | dependencies = [ 619 | "semver", 620 | ] 621 | 622 | [[package]] 623 | name = "rustix" 624 | version = "1.0.5" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" 627 | dependencies = [ 628 | "bitflags", 629 | "errno", 630 | "libc", 631 | "linux-raw-sys", 632 | "windows-sys", 633 | ] 634 | 635 | [[package]] 636 | name = "rustversion" 637 | version = "1.0.20" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 640 | 641 | [[package]] 642 | name = "ryu" 643 | version = "1.0.20" 644 | source = "registry+https://github.com/rust-lang/crates.io-index" 645 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 646 | 647 | [[package]] 648 | name = "same-file" 649 | version = "1.0.6" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 652 | dependencies = [ 653 | "winapi-util", 654 | ] 655 | 656 | [[package]] 657 | name = "semver" 658 | version = "1.0.26" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" 661 | 662 | [[package]] 663 | name = "serde" 664 | version = "1.0.219" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 667 | dependencies = [ 668 | "serde_derive", 669 | ] 670 | 671 | [[package]] 672 | name = "serde_derive" 673 | version = "1.0.219" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 676 | dependencies = [ 677 | "proc-macro2", 678 | "quote", 679 | "syn", 680 | ] 681 | 682 | [[package]] 683 | name = "serde_json" 684 | version = "1.0.140" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 687 | dependencies = [ 688 | "itoa", 689 | "memchr", 690 | "ryu", 691 | "serde", 692 | ] 693 | 694 | [[package]] 695 | name = "sharded-slab" 696 | version = "0.1.7" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 699 | dependencies = [ 700 | "lazy_static", 701 | ] 702 | 703 | [[package]] 704 | name = "smallvec" 705 | version = "1.15.0" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 708 | 709 | [[package]] 710 | name = "syn" 711 | version = "2.0.100" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 714 | dependencies = [ 715 | "proc-macro2", 716 | "quote", 717 | "unicode-ident", 718 | ] 719 | 720 | [[package]] 721 | name = "tempfile" 722 | version = "3.19.1" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" 725 | dependencies = [ 726 | "fastrand", 727 | "getrandom", 728 | "once_cell", 729 | "rustix", 730 | "windows-sys", 731 | ] 732 | 733 | [[package]] 734 | name = "thiserror" 735 | version = "2.0.12" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 738 | dependencies = [ 739 | "thiserror-impl", 740 | ] 741 | 742 | [[package]] 743 | name = "thiserror-impl" 744 | version = "2.0.12" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 747 | dependencies = [ 748 | "proc-macro2", 749 | "quote", 750 | "syn", 751 | ] 752 | 753 | [[package]] 754 | name = "thread_local" 755 | version = "1.1.8" 756 | source = "registry+https://github.com/rust-lang/crates.io-index" 757 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 758 | dependencies = [ 759 | "cfg-if", 760 | "once_cell", 761 | ] 762 | 763 | [[package]] 764 | name = "tinytemplate" 765 | version = "1.2.1" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" 768 | dependencies = [ 769 | "serde", 770 | "serde_json", 771 | ] 772 | 773 | [[package]] 774 | name = "tracing" 775 | version = "0.1.41" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 778 | dependencies = [ 779 | "pin-project-lite", 780 | "tracing-attributes", 781 | "tracing-core", 782 | ] 783 | 784 | [[package]] 785 | name = "tracing-attributes" 786 | version = "0.1.28" 787 | source = "registry+https://github.com/rust-lang/crates.io-index" 788 | checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" 789 | dependencies = [ 790 | "proc-macro2", 791 | "quote", 792 | "syn", 793 | ] 794 | 795 | [[package]] 796 | name = "tracing-core" 797 | version = "0.1.33" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 800 | dependencies = [ 801 | "once_cell", 802 | "valuable", 803 | ] 804 | 805 | [[package]] 806 | name = "tracing-log" 807 | version = "0.2.0" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 810 | dependencies = [ 811 | "log", 812 | "once_cell", 813 | "tracing-core", 814 | ] 815 | 816 | [[package]] 817 | name = "tracing-subscriber" 818 | version = "0.3.19" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" 821 | dependencies = [ 822 | "matchers", 823 | "nu-ansi-term", 824 | "once_cell", 825 | "regex", 826 | "sharded-slab", 827 | "smallvec", 828 | "thread_local", 829 | "tracing", 830 | "tracing-core", 831 | "tracing-log", 832 | ] 833 | 834 | [[package]] 835 | name = "unicode-ident" 836 | version = "1.0.18" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 839 | 840 | [[package]] 841 | name = "valuable" 842 | version = "0.1.1" 843 | source = "registry+https://github.com/rust-lang/crates.io-index" 844 | checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 845 | 846 | [[package]] 847 | name = "walkdir" 848 | version = "2.5.0" 849 | source = "registry+https://github.com/rust-lang/crates.io-index" 850 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 851 | dependencies = [ 852 | "same-file", 853 | "winapi-util", 854 | ] 855 | 856 | [[package]] 857 | name = "wasi" 858 | version = "0.14.2+wasi-0.2.4" 859 | source = "registry+https://github.com/rust-lang/crates.io-index" 860 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 861 | dependencies = [ 862 | "wit-bindgen-rt", 863 | ] 864 | 865 | [[package]] 866 | name = "wasm-bindgen" 867 | version = "0.2.100" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 870 | dependencies = [ 871 | "cfg-if", 872 | "once_cell", 873 | "rustversion", 874 | "wasm-bindgen-macro", 875 | ] 876 | 877 | [[package]] 878 | name = "wasm-bindgen-backend" 879 | version = "0.2.100" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 882 | dependencies = [ 883 | "bumpalo", 884 | "log", 885 | "proc-macro2", 886 | "quote", 887 | "syn", 888 | "wasm-bindgen-shared", 889 | ] 890 | 891 | [[package]] 892 | name = "wasm-bindgen-macro" 893 | version = "0.2.100" 894 | source = "registry+https://github.com/rust-lang/crates.io-index" 895 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 896 | dependencies = [ 897 | "quote", 898 | "wasm-bindgen-macro-support", 899 | ] 900 | 901 | [[package]] 902 | name = "wasm-bindgen-macro-support" 903 | version = "0.2.100" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 906 | dependencies = [ 907 | "proc-macro2", 908 | "quote", 909 | "syn", 910 | "wasm-bindgen-backend", 911 | "wasm-bindgen-shared", 912 | ] 913 | 914 | [[package]] 915 | name = "wasm-bindgen-shared" 916 | version = "0.2.100" 917 | source = "registry+https://github.com/rust-lang/crates.io-index" 918 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 919 | dependencies = [ 920 | "unicode-ident", 921 | ] 922 | 923 | [[package]] 924 | name = "web-sys" 925 | version = "0.3.77" 926 | source = "registry+https://github.com/rust-lang/crates.io-index" 927 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 928 | dependencies = [ 929 | "js-sys", 930 | "wasm-bindgen", 931 | ] 932 | 933 | [[package]] 934 | name = "winapi" 935 | version = "0.3.9" 936 | source = "registry+https://github.com/rust-lang/crates.io-index" 937 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 938 | dependencies = [ 939 | "winapi-i686-pc-windows-gnu", 940 | "winapi-x86_64-pc-windows-gnu", 941 | ] 942 | 943 | [[package]] 944 | name = "winapi-i686-pc-windows-gnu" 945 | version = "0.4.0" 946 | source = "registry+https://github.com/rust-lang/crates.io-index" 947 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 948 | 949 | [[package]] 950 | name = "winapi-util" 951 | version = "0.1.9" 952 | source = "registry+https://github.com/rust-lang/crates.io-index" 953 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 954 | dependencies = [ 955 | "windows-sys", 956 | ] 957 | 958 | [[package]] 959 | name = "winapi-x86_64-pc-windows-gnu" 960 | version = "0.4.0" 961 | source = "registry+https://github.com/rust-lang/crates.io-index" 962 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 963 | 964 | [[package]] 965 | name = "windows-sys" 966 | version = "0.59.0" 967 | source = "registry+https://github.com/rust-lang/crates.io-index" 968 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 969 | dependencies = [ 970 | "windows-targets", 971 | ] 972 | 973 | [[package]] 974 | name = "windows-targets" 975 | version = "0.52.6" 976 | source = "registry+https://github.com/rust-lang/crates.io-index" 977 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 978 | dependencies = [ 979 | "windows_aarch64_gnullvm", 980 | "windows_aarch64_msvc", 981 | "windows_i686_gnu", 982 | "windows_i686_gnullvm", 983 | "windows_i686_msvc", 984 | "windows_x86_64_gnu", 985 | "windows_x86_64_gnullvm", 986 | "windows_x86_64_msvc", 987 | ] 988 | 989 | [[package]] 990 | name = "windows_aarch64_gnullvm" 991 | version = "0.52.6" 992 | source = "registry+https://github.com/rust-lang/crates.io-index" 993 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 994 | 995 | [[package]] 996 | name = "windows_aarch64_msvc" 997 | version = "0.52.6" 998 | source = "registry+https://github.com/rust-lang/crates.io-index" 999 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1000 | 1001 | [[package]] 1002 | name = "windows_i686_gnu" 1003 | version = "0.52.6" 1004 | source = "registry+https://github.com/rust-lang/crates.io-index" 1005 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1006 | 1007 | [[package]] 1008 | name = "windows_i686_gnullvm" 1009 | version = "0.52.6" 1010 | source = "registry+https://github.com/rust-lang/crates.io-index" 1011 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1012 | 1013 | [[package]] 1014 | name = "windows_i686_msvc" 1015 | version = "0.52.6" 1016 | source = "registry+https://github.com/rust-lang/crates.io-index" 1017 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1018 | 1019 | [[package]] 1020 | name = "windows_x86_64_gnu" 1021 | version = "0.52.6" 1022 | source = "registry+https://github.com/rust-lang/crates.io-index" 1023 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1024 | 1025 | [[package]] 1026 | name = "windows_x86_64_gnullvm" 1027 | version = "0.52.6" 1028 | source = "registry+https://github.com/rust-lang/crates.io-index" 1029 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1030 | 1031 | [[package]] 1032 | name = "windows_x86_64_msvc" 1033 | version = "0.52.6" 1034 | source = "registry+https://github.com/rust-lang/crates.io-index" 1035 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1036 | 1037 | [[package]] 1038 | name = "wit-bindgen-rt" 1039 | version = "0.39.0" 1040 | source = "registry+https://github.com/rust-lang/crates.io-index" 1041 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 1042 | dependencies = [ 1043 | "bitflags", 1044 | ] 1045 | 1046 | [[package]] 1047 | name = "zerocopy" 1048 | version = "0.8.24" 1049 | source = "registry+https://github.com/rust-lang/crates.io-index" 1050 | checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" 1051 | dependencies = [ 1052 | "zerocopy-derive", 1053 | ] 1054 | 1055 | [[package]] 1056 | name = "zerocopy-derive" 1057 | version = "0.8.24" 1058 | source = "registry+https://github.com/rust-lang/crates.io-index" 1059 | checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" 1060 | dependencies = [ 1061 | "proc-macro2", 1062 | "quote", 1063 | "syn", 1064 | ] 1065 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mem-isolate" 3 | version = "0.1.6" 4 | description = "Contain memory leaks and fragmentation" 5 | edition = "2024" 6 | authors = ["Brannon Dorsey "] 7 | license = "MIT OR Apache-2.0" 8 | readme = "README.md" 9 | repository = "https://github.com/brannondorsey/mem-isolate" 10 | keywords = ["memory", "isolation", "sandbox", "unix"] 11 | categories = ["memory-management", "os::unix-apis", "rust-patterns"] 12 | exclude = [".github/*"] 13 | 14 | [features] 15 | # Include no features by default 16 | default = [] 17 | 18 | # Include all features 19 | full = ["tracing"] 20 | 21 | # Actual features 22 | tracing = ["dep:tracing"] 23 | 24 | [dependencies] 25 | bincode = "1" 26 | libc = "0.2" 27 | serde = { version = "1", features = ["derive"] } 28 | thiserror = "2" 29 | tracing = { version = "0.1.41", optional = true } 30 | 31 | [dev-dependencies] 32 | criterion = "0.5.1" 33 | ctor = "0.4.1" 34 | nix = { version = "0.29.0", features = ["signal"] } 35 | rand = "0.9.0" 36 | rstest = { version = "0.25.0", default-features = false } 37 | tempfile = "3.18.0" 38 | tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } 39 | 40 | [[bench]] 41 | name = "benchmarks" 42 | harness = false 43 | 44 | [[example]] 45 | name = "error-handling-basic" 46 | test = true 47 | 48 | [[example]] 49 | name = "error-handling-complete" 50 | test = true 51 | 52 | [[example]] 53 | name = "tracing" 54 | required-features = ["tracing"] 55 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Brannon Dorsey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `mem-isolate`: *Contain memory leaks and fragmentation* 2 | 3 | [![Build Status](https://github.com/brannondorsey/mem-isolate/actions/workflows/build.yml/badge.svg)](https://github.com/brannondorsey/mem-isolate/actions/workflows/build.yml) 4 | 5 | `mem-isolate` runs your function via a `fork()`, waits for the result, and returns it. 6 | 7 | This grants your code access to an exact copy of memory and state at the time just before the call, but guarantees that the function will not affect the parent process's memory footprint in any way. 8 | 9 | It forces functions to be *memory pure* (pure with respect to memory), even if they aren't. 10 | 11 | ```rust 12 | use mem_isolate::execute_in_isolated_process; 13 | 14 | // No heap, stack, or program memory out here... 15 | let result = mem_isolate::execute_in_isolated_process(|| { 16 | // ...Can be affected by anything in here 17 | unsafe { 18 | gnarly_cpp_bindings::potentially_leaking_function(); 19 | unstable_ffi::segfault_prone_function(); 20 | heap_fragmenting_operation(); 21 | something_that_panics_in_a_way_you_could_recover_from(); 22 | } 23 | }); 24 | ``` 25 | 26 | Example use cases: 27 | 28 | * Run code with a known memory leak 29 | * Run code that fragments the heap 30 | * Erase memory mutations before continuing with your program 31 | * Run your code 1ms slower (*har har* 😉, see [limitations](#limitations)) 32 | 33 | > NOTE: Because of its heavy use of POSIX system calls, this crate only supports Unix-like operating systems (e.g., Linux, macOS, BSD). Windows and wasm support are not planned at this time. 34 | 35 | See the [examples/](examples/) for more uses, especially [the basic error handling example](examples/error-handling-basic.rs). 36 | 37 | ## How it works 38 | 39 | POSIX systems use the `fork()` system call to create a new child process that is a copy of the parent. On modern systems, this is relatively cheap (~1ms) even if the parent process is using a lot of memory at the time of the call. This is because the OS uses copy-on-write memory techniques to avoid duplicating the entire memory of the parent process. At the time `fork()` is called, the parent and child all share the same physical pages in memory. Only when one of them modifies a page is it copied to a new location. 40 | 41 | `mem-isolate` uses this implementation detail as a nifty hack to provide a `callable` function with a temporary and isolated memory space. You can think of this isolation almost like a snapshot is taken of your program's memory at the time `execute_in_isolated_process()` is called, which will be restored once the user-supplied `callable` function has finished executing. 42 | 43 | When `execute_in_isolated_process()` is called, the process will: 44 | 45 | 1. Create a `pipe()` for inter-process communication between the process *it* has been invoked in (the "parent") and the new child process that will be created to isolate and run your `callable` 46 | 1. `fork()` a new child process 47 | 1. Execute the user-supplied `callable` in the child process and deliver its result back to the parent process through the pipe 48 | 1. Wait for the child process to finish with `waitpid()` 49 | 1. Return the result to the parent process 50 | 51 | We call this trick the "fork and free" pattern. It's pretty nifty. 🫰 52 | 53 | ## Limitations 54 | 55 | There are plenty of reasons not to use `mem-isolate` in your project. 56 | 57 | ### Performance & Usability 58 | 59 | * Works only on POSIX systems (Linux, macOS, BSD) 60 | * Data returned from the `callable` function must be serialized to and from the child process (using `serde`), which can be expensive for large data. 61 | * Excluding serialization/deserialization cost, `execute_in_isolated_process()` introduces runtime overhead on the order of ~1ms compared to a direct invocation of the `callable`. 62 | 63 | In performance-critical systems, these overheads can be no joke. However, for many use cases, this is an affordable trade-off for the memory safety and snapshotting behavior that `mem-isolate` provides. 64 | 65 | ### Safety & Correctness 66 | 67 | The use of `fork()`, which this crate uses under the hood, has a slew of potentially dangerous side effects and surprises if you're not careful. 68 | 69 | * For **single-threaded use only:** It is generally unsound to `fork()` in multi-threaded environments, especially when mutexes are involved. Only the thread that calls `fork()` will be cloned and live on in the new process. This can easily lead to deadlocks and hung child processes if other threads are holding resource locks that the child process expects to acquire. 70 | * **Signals** delivered to the parent process won't be automatically forwarded to the child process running your `callable` during its execution. See one of the `examples/blocking-signals-*` files for [an example](examples/blocking-signals-minimal.rs) of how to handle this. 71 | * **[Channels](https://doc.rust-lang.org/std/sync/mpsc/fn.channel.html)** can't be used to communicate between the parent and child processes. Consider using shared mmaps, pipes, or the filesystem instead. 72 | * **Shared mmaps** break the isolation guarantees of this crate. The child process will be able to mutate `mmap(..., MAP_SHARED, ...)` regions created by the parent process. 73 | * **Panics** in your `callable` won't panic the rest of your program, as they would without `mem-isolate`. That's as useful as it is harmful, depending on your use case, but it's worth noting. 74 | * **Mutable references, static variables, and raw pointers** accessible to your `callable` won't be modified as you would expect them to. That's kind of the whole point of this crate... ;) 75 | 76 | Failing to understand or respect these limitations will make your code more susceptible to both undefined behavior (UB) and heap corruption, not less. 77 | 78 | ## Benchmarks 79 | 80 | In a [simple benchmark](benches/benchmarks.rs), raw function calls are ~1.5ns, `fork()` + wait is ~1.7ms, and `execute_in_isolated_process()` is about 1.9ms. That's very slow by comparison, but tolerable for many use cases where memory safety is paramount. 81 | 82 | ```txt 83 | cargo bench 84 | Finished `bench` profile [optimized] target(s) in 0.07s 85 | Running unittests src/lib.rs (target/release/deps/mem_isolate-d96fcfa5f2fd31c0) 86 | 87 | running 3 tests 88 | test tests::simple_example ... ignored 89 | test tests::test_static_memory_mutation_with_isolation ... ignored 90 | test tests::test_static_memory_mutation_without_isolation ... ignored 91 | 92 | test result: ok. 0 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 0.00s 93 | 94 | Running benches/benchmarks.rs (target/release/deps/benchmarks-25c74db99f107a73) 95 | Overhead/direct_function_call 96 | time: [1.4347 ns 1.4357 ns 1.4370 ns] 97 | change: [-1.4983% +0.6412% +3.4486%] (p = 0.55 > 0.05) 98 | No change in performance detected. 99 | Found 11 outliers among 100 measurements (11.00%) 100 | 4 (4.00%) high mild 101 | 7 (7.00%) high severe 102 | Overhead/fork_alone time: [1.6893 ms 1.6975 ms 1.7062 ms] 103 | change: [+1.3025% +3.8968% +5.7914%] (p = 0.01 < 0.05) 104 | Performance has regressed. 105 | Found 2 outliers among 100 measurements (2.00%) 106 | 1 (1.00%) high mild 107 | 1 (1.00%) high severe 108 | Overhead/execute_in_isolated_process 109 | time: [1.8769 ms 1.9007 ms 1.9226 ms] 110 | change: [-7.6229% -5.7657% -3.7073%] (p = 0.00 < 0.05) 111 | Performance has improved. 112 | Found 1 outliers among 100 measurements (1.00%) 113 | 1 (1.00%) low severe 114 | ``` 115 | 116 | All benchmarks were run on a ThinkPad T14 Gen 4 AMD (14″) laptop with a AMD Ryzen 5 PRO 7540U CPU @ 3.2Ghz max clock speed and 32 GB of RAM. The OS used was Debian 13 with Linux kernel 6.12. 117 | 118 | ## License 119 | 120 | `mem-isolate` is dual-licensed under either of: 121 | 122 | * [MIT license](https://opensource.org/license/mit) 123 | * [Apache License, Version 2.0](https://opensource.org/license/apache-2-0) 124 | 125 | at your option. 126 | 127 | ### Contribution 128 | 129 | Unless you explicitly state otherwise, any contribution intentionally submitted 130 | for inclusion in the work by you shall be dual licensed as above, without any 131 | additional terms or conditions. 132 | 133 | ## Discussion 134 | 135 | This crate was previously discussed on [Hacker News](https://news.ycombinator.com/item?id=43601301) and [Reddit](https://www.reddit.com/r/rust/comments/1jsu2i2/run_unsafe_code_safely_using_memisolate/) 🧵 136 | 137 | Version [0.1.4](https://github.com/brannondorsey/mem-isolate/releases/tag/v0.1.4) included changes to the documentation and messaging around the limitations and appropriate use cases for this crate based on feedback from these discussions. Thanks everyone! 138 | -------------------------------------------------------------------------------- /benches/benchmarks.rs: -------------------------------------------------------------------------------- 1 | use criterion::{Criterion, black_box, criterion_group, criterion_main}; 2 | 3 | use mem_isolate::execute_in_isolated_process; 4 | use std::time::Duration; 5 | 6 | pub fn criterion_benchmark(c: &mut Criterion) { 7 | pub fn times_ten(x: u32) -> u32 { 8 | x * 10 9 | } 10 | let mut group = c.benchmark_group("Overhead"); 11 | 12 | group 13 | .measurement_time(Duration::from_secs(1)) 14 | .warm_up_time(Duration::from_secs(1)); 15 | 16 | group.bench_function("direct_function_call", |b| { 17 | b.iter(|| times_ten(black_box(1))) 18 | }); 19 | 20 | // An indication of how much cpu time could be yielded back with an async 21 | // version of execute_in_isolated_process() 22 | group.bench_function("fork_alone", |b| { 23 | b.iter(|| { 24 | match unsafe { libc::fork() } { 25 | -1 => panic!("Fork failed"), 26 | // Child process 27 | 0 => { 28 | times_ten(black_box(1)); 29 | unsafe { libc::_exit(0) }; 30 | } 31 | // Parent process 32 | _ => { 33 | // Not calling wait() here will result in zombie processes 34 | // but once they are adopted by init they will eventually 35 | // be reaped 36 | } 37 | } 38 | }) 39 | }); 40 | 41 | group.bench_function("fork_with_wait", |b| { 42 | b.iter(|| { 43 | match unsafe { libc::fork() } { 44 | -1 => panic!("Fork failed"), 45 | // Child process 46 | 0 => { 47 | times_ten(black_box(1)); 48 | unsafe { libc::_exit(0) }; 49 | } 50 | // Parent process 51 | pid => { 52 | let mut status = 0; 53 | unsafe { 54 | libc::waitpid(pid, &mut status, 0); 55 | } 56 | } 57 | } 58 | }) 59 | }); 60 | 61 | group.bench_function("execute_in_isolated_process", |b| { 62 | b.iter(|| execute_in_isolated_process(|| times_ten(black_box(1)))) 63 | }); 64 | group.finish(); 65 | } 66 | 67 | criterion_group!(benches, criterion_benchmark); 68 | criterion_main!(benches); 69 | -------------------------------------------------------------------------------- /examples/blocking-signals-demonstration.rs: -------------------------------------------------------------------------------- 1 | //! This example demonstrates how to block signals in a parent process while 2 | //! executing code in an isolated process. 3 | //! 4 | //! This is useful if: 5 | //! 6 | //! 1. You want to simulate the expected behavior of running your code without 7 | //! `mem_isolate::execute_in_isolated_process()`, which would be guaranteed 8 | //! to treat signals the same both inside and outside of the `callable()` 9 | //! function. 10 | //! 2. You want to prevent either process from being interrupted by signals 11 | //! while your `callable()` is running. 12 | //! 3. You want to ensure that the parent process is not killed while the 13 | //! isolated process is running, leaving an orphaned child process. 14 | //! 15 | //! It is important to remember that `mem_isolate::execute_in_isolated_process()` 16 | //! uses `fork()` under the hood, which creates a child process that will not 17 | //! receive signals sent to the main process as your `callable()` would otherwise. 18 | //! 19 | //! Run this example with `cargo run --example blocking-signals-demonstration` 20 | //! 21 | //! This example is great for illustrating how signal blocking works, but if you 22 | //! want to just copy and paste some code, see `blocking-signals-minimal.rs` 23 | //! 24 | //! NOTE: Because both SIGKILL and SIGSTOP are unblockable, nothing can be done 25 | //! to prevent them from killing the parent process or the child process. 26 | //! 27 | //! WARNING: Do not expect signal handling to work as you might think in a 28 | //! multi-threaded program. It is not recommended to use `mem_isolate` in a 29 | //! multi-threaded program anyway, so that's generally OK. 30 | 31 | use mem_isolate::{MemIsolateError, execute_in_isolated_process}; 32 | use nix::errno::Errno; 33 | use nix::sys::signal::{SigSet, SigmaskHow, sigprocmask}; 34 | use nix::unistd::Pid; 35 | use std::process::{Child, Command, Stdio}; 36 | use std::thread; 37 | use std::time::Duration; 38 | 39 | fn main() -> Result<(), MemIsolateError> { 40 | let parent_pid = Pid::this(); 41 | println!("Parent PID: {}", parent_pid); 42 | let wait_time_for_readability = Duration::from_secs(5); 43 | 44 | // Get closures for blocking and restoring signals 45 | let (block_signals, restore_signals) = get_block_and_restore_signal_closures(); 46 | 47 | // Block all signals before calling `mem_isolate::execute_in_isolated_process()` 48 | // This ensures the main program won't be killed leaving an orphaned child process 49 | println!("Parent: Blocking all signals"); 50 | block_signals().expect("Failed to block signals"); 51 | 52 | // Kick-off a subprocess that will send a SIGTERM to this process 53 | let sigterm_sender_proc = send_sigterm_to_parent(Duration::from_secs(1)); 54 | 55 | // Run your code in an isolated process. NOTE: The child process created by 56 | // `fork()` inside `execute_in_isolated_process()` will inherit the signal 57 | // mask set by main process just above. 58 | let result = execute_in_isolated_process(move || { 59 | println!( 60 | "\nChild: I've started executing a user-defined callable. I'll wait for {} seconds before exiting...", 61 | wait_time_for_readability.as_secs() 62 | ); 63 | thread::sleep(wait_time_for_readability); 64 | println!("Child: I'm all done now, exiting\n"); 65 | }); 66 | 67 | reap_child_process_so_it_doesnt_become_a_zombie(sigterm_sender_proc); 68 | 69 | println!( 70 | "Parent: Notice that the SIGTERM is pending and didn't interrupt this process or the child process. Unblocking signals in {} seconds...", 71 | wait_time_for_readability.as_secs() 72 | ); 73 | thread::sleep(wait_time_for_readability); 74 | println!("Parent: Restoring signals, expect the parent to now recieve the SIGTERM"); 75 | restore_signals().expect("Failed to restore signals"); 76 | // WARNING: Don't expect code to ever reach this point, because the pending SIGTERM will kill the parent process 77 | // as soon as we unblock the SIGTERM that has been pending this whole time 78 | println!("Parent: Notice how I never ran"); 79 | result 80 | } 81 | 82 | fn get_block_and_restore_signal_closures() -> ( 83 | impl FnOnce() -> Result<(), Errno>, 84 | impl FnOnce() -> Result<(), Errno>, 85 | ) { 86 | let all_signals = SigSet::all(); 87 | let mut old_signals = SigSet::empty(); 88 | 89 | let block_signals = move || { 90 | sigprocmask( 91 | SigmaskHow::SIG_SETMASK, 92 | Some(&all_signals), 93 | Some(&mut old_signals), 94 | ) 95 | }; 96 | 97 | let restore_signals = move || sigprocmask(SigmaskHow::SIG_SETMASK, Some(&old_signals), None); 98 | 99 | (block_signals, restore_signals) 100 | } 101 | 102 | fn send_sigterm_to_parent(wait_time: Duration) -> Child { 103 | // We do this via a subprocess instead of a thread because the latter will 104 | // break the signal mask that we have set. `mem_isolate::execute_in_isolated_process()` 105 | // also SHOULD NOT be used in a multi-threaded program (see limitations in README) 106 | println!( 107 | "Parent: Sending SIGTERM to self in {} seconds. NOTE: This signal will be sent to the parent while the child is executing the user-defined callable", 108 | wait_time.as_secs() 109 | ); 110 | Command::new("sh") 111 | .arg("-c") 112 | .arg(format!( 113 | "sleep {} && kill -s TERM {}", 114 | wait_time.as_secs(), 115 | Pid::this() 116 | )) 117 | .stdout(Stdio::null()) 118 | .stderr(Stdio::null()) 119 | .spawn() 120 | .expect("Failed to spawn delayed kill command") 121 | } 122 | 123 | fn reap_child_process_so_it_doesnt_become_a_zombie(mut child: Child) { 124 | let exit_status = child.wait().expect("Failed to wait for child process"); 125 | if !exit_status.success() { 126 | panic!("Other process: failed to send SIGTERM to parent process"); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /examples/blocking-signals-minimal.rs: -------------------------------------------------------------------------------- 1 | //! See `blocking-signals-demonstration.rs` for a more detailed example of 2 | //! how signal blocking works at runtime, and why you might want to do it. 3 | //! 4 | //! Use this example for copy + paste code snippets. 5 | 6 | use mem_isolate::{MemIsolateError, execute_in_isolated_process}; 7 | use nix::errno::Errno; 8 | use nix::sys::signal::{SigSet, SigmaskHow, sigprocmask}; 9 | 10 | fn main() -> Result<(), MemIsolateError> { 11 | // Block all signals right before calling `mem_isolate::execute_in_isolated_process()` 12 | // This ensures the main program won't be killed leaving an orphaned child process 13 | let (block_signals, restore_signals) = get_block_and_restore_signal_closures(); 14 | block_signals().expect("Failed to block signals"); 15 | 16 | // Run your code in an isolated process. NOTE: The child process created by 17 | // `fork()` inside `execute_in_isolated_process()` will inherit the signal 18 | // mask set by main process just above. 19 | let result = execute_in_isolated_process(|| ()); 20 | 21 | // Restore the signal mask, unblocking all signals 22 | restore_signals().expect("Failed to restore signals"); 23 | result 24 | } 25 | 26 | fn get_block_and_restore_signal_closures() -> ( 27 | impl FnOnce() -> Result<(), Errno>, 28 | impl FnOnce() -> Result<(), Errno>, 29 | ) { 30 | let all_signals = SigSet::all(); 31 | let mut old_signals = SigSet::empty(); 32 | 33 | let block_signals = move || { 34 | sigprocmask( 35 | SigmaskHow::SIG_SETMASK, 36 | Some(&all_signals), 37 | Some(&mut old_signals), 38 | ) 39 | }; 40 | 41 | let restore_signals = move || sigprocmask(SigmaskHow::SIG_SETMASK, Some(&old_signals), None); 42 | 43 | (block_signals, restore_signals) 44 | } 45 | -------------------------------------------------------------------------------- /examples/error-handling-basic.rs: -------------------------------------------------------------------------------- 1 | use mem_isolate::{MemIsolateError, execute_in_isolated_process}; 2 | 3 | // Keep reading, this will soon make sense... 4 | const MY_FUNCTION_IS_IDEMPOTENT: bool = true; 5 | const PREFER_RETRIES_WITH_MEM_ISOLATE: bool = true; 6 | 7 | // Your function can be fallible, so feel free to return a Result. 8 | // Heck, you can return _anything_ that's serializable. 9 | fn my_function() -> Result { 10 | use rand::Rng; 11 | let mut rng = rand::rng(); 12 | if rng.random::() > 0.5 { 13 | return Err("Your supplied function non-deterministically failed".to_string()); 14 | } 15 | 16 | Ok("Your function executed without a hitch!".to_string()) 17 | } 18 | 19 | fn main() -> Result<(), MemIsolateError> { 20 | // This function wraps your function's return type T in a Result, 21 | // where MemIsolateError indiates something went wrong on the mem-isolate side of things. 22 | let mem_isolate_result = execute_in_isolated_process(my_function); 23 | 24 | // Your function's return value can be accessed by unwrapping the mem-isolate result 25 | let my_function_result = match mem_isolate_result { 26 | // Ok() signifies that the callable executed without a mem-isolate issue 27 | Ok(my_funct_result) => my_funct_result, 28 | Err(MemIsolateError::CallableExecuted(ref _err)) => { 29 | // The callable executed, but something went wrong afterwords. 30 | // For instance, maybe the data it returned failed serialization 31 | mem_isolate_result? 32 | } 33 | Err(MemIsolateError::CallableDidNotExecute(ref _err)) => { 34 | // Something went wrong before the callable was executed, you could 35 | // retry with or without mem_isolate 36 | 37 | // Because we know that the my_function never executed, it's 38 | // harmless to retry (even if !MY_FUNCTION_IS_IDEMPOTENT) 39 | if PREFER_RETRIES_WITH_MEM_ISOLATE { 40 | // You could naively retry with mem_isolate... 41 | execute_in_isolated_process(my_function)? 42 | } else { 43 | // ...or by directly calling the function without mem_isolate 44 | my_function() 45 | } 46 | } 47 | Err(MemIsolateError::CallableStatusUnknown(ref _err)) => { 48 | // Uh oh, something went wrong in a way that's impossible to know the 49 | // status of the callable 50 | if MY_FUNCTION_IS_IDEMPOTENT { 51 | // If the function is idempotent, you could retry without 52 | // mem_isolate 53 | my_function() 54 | } else { 55 | // Ruh, roh 56 | panic!("Callable is not idempotent, and we don't know the status of the callable"); 57 | } 58 | } 59 | }; 60 | 61 | // At last, we can handle the result of the function 62 | match my_function_result { 63 | Ok(result) => { 64 | println!("{result}"); 65 | } 66 | Err(e) => { 67 | eprintln!("{e}"); 68 | } 69 | } 70 | 71 | Ok(()) 72 | } 73 | 74 | // Add a smoke test to make sure this example stays working 75 | #[cfg(test)] 76 | mod example_error_handling_basic { 77 | use super::*; 78 | 79 | #[test] 80 | fn execute_main() { 81 | main().unwrap(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /examples/error-handling-complete.rs: -------------------------------------------------------------------------------- 1 | //! This example is meant to show you all of the possible errors that can occur 2 | //! when using `execute_in_isolated_process()`. 3 | //! 4 | //! It doesn't actually do anything meaningful 5 | use MemIsolateError::*; 6 | use mem_isolate::errors::CallableDidNotExecuteError::*; 7 | use mem_isolate::errors::CallableExecutedError::*; 8 | use mem_isolate::errors::CallableStatusUnknownError::*; 9 | use mem_isolate::{MemIsolateError, execute_in_isolated_process}; 10 | 11 | fn main() { 12 | let result = execute_in_isolated_process(|| {}); 13 | match result { 14 | Ok(_) => {} 15 | 16 | Err(CallableExecuted(SerializationFailed(_string))) => {} 17 | Err(CallableExecuted(DeserializationFailed(_string))) => {} 18 | Err(CallableExecuted(ChildPipeWriteFailed(_opt_err))) => {} 19 | 20 | Err(CallableDidNotExecute(PipeCreationFailed(_err))) => {} 21 | Err(CallableDidNotExecute(ChildPipeCloseFailed(_opt_err))) => {} 22 | Err(CallableDidNotExecute(ForkFailed(_err))) => {} 23 | 24 | Err(CallableStatusUnknown(ParentPipeCloseFailed(_err))) => {} 25 | Err(CallableStatusUnknown(WaitFailed(_err))) => {} 26 | Err(CallableStatusUnknown(ParentPipeReadFailed(_err))) => {} 27 | Err(CallableStatusUnknown(CallableProcessDiedDuringExecution)) => {} 28 | Err(CallableStatusUnknown(UnexpectedChildExitStatus(_status))) => {} 29 | Err(CallableStatusUnknown(ChildProcessKilledBySignal(_signal))) => {} 30 | Err(CallableStatusUnknown(UnexpectedWaitpidReturnValue(_val))) => {} 31 | }; 32 | } 33 | 34 | // This test does two things: 35 | // 1. It ensures our errors are being exposed publicly by the crate 36 | // 2. It ensures our error handling is exhaustive and will fail if a change is made (flagging a semver breaking change) 37 | #[cfg(test)] 38 | mod example_error_handling_complete { 39 | use super::*; 40 | 41 | #[test] 42 | fn execute_main() { 43 | main(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/memory-leak.rs: -------------------------------------------------------------------------------- 1 | use mem_isolate::execute_in_isolated_process; 2 | 3 | fn main() { 4 | let leaky_fn = || { 5 | // Leak 1KiB of memory 6 | let data: Vec = vec![42; 1024]; 7 | let data = Box::new(data); 8 | let uh_oh = Box::leak(data); 9 | let leaked_ptr = format!("{:p}", uh_oh); 10 | assert!( 11 | check_memory_exists_and_holds_vec_data(&leaked_ptr), 12 | "The memory should exist in `leaky_fn()` where it was leaked" 13 | ); 14 | leaked_ptr 15 | }; 16 | 17 | let leaked_ptr: String = execute_in_isolated_process(leaky_fn).unwrap(); 18 | assert!( 19 | !check_memory_exists_and_holds_vec_data(&leaked_ptr), 20 | "The leaked memory doesn't exist out here though" 21 | ); 22 | println!( 23 | "Success, the memory leak in `leaky_fn()` was contained to the ephemeral child process it executed inside of" 24 | ); 25 | } 26 | 27 | fn check_memory_exists_and_holds_vec_data(ptr_str: &str) -> bool { 28 | let addr = usize::from_str_radix(ptr_str.trim_start_matches("0x"), 16).unwrap(); 29 | let vec_ptr = addr as *const Vec; 30 | !vec_ptr.is_null() && unsafe { std::ptr::read_volatile(vec_ptr) }.capacity() > 0 31 | } 32 | -------------------------------------------------------------------------------- /examples/tracing.rs: -------------------------------------------------------------------------------- 1 | // Compile error if tracing is not enabled 2 | #[cfg(not(feature = "tracing"))] 3 | compile_error!( 4 | "This example requires the 'tracing' feature to be enabled. Run with: cargo run --example tracing --features tracing" 5 | ); 6 | 7 | use mem_isolate::MemIsolateError; 8 | use tracing::{Level, info, instrument}; 9 | use tracing_subscriber::EnvFilter; 10 | 11 | #[instrument] 12 | fn main() -> Result<(), MemIsolateError> { 13 | init_tracing_subscriber(); 14 | 15 | mem_isolate::execute_in_isolated_process(|| { 16 | info!("look ma, I'm in the callable!"); 17 | 42 18 | })?; 19 | 20 | Ok(()) 21 | } 22 | 23 | /// Defaults to TRACE but can be overridden by the RUST_LOG environment variable 24 | fn init_tracing_subscriber() { 25 | let env_filter = EnvFilter::builder() 26 | .with_default_directive(Level::TRACE.into()) 27 | .from_env_lossy(); 28 | tracing_subscriber::fmt().with_env_filter(env_filter).init(); 29 | } 30 | -------------------------------------------------------------------------------- /release-plz.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | # Release only when release PR is merged 3 | # https://release-plz.dev/docs/config#the-release_always-field 4 | release_always = false 5 | -------------------------------------------------------------------------------- /src/c.rs: -------------------------------------------------------------------------------- 1 | use libc::c_int; 2 | use std::io; 3 | 4 | // For test builds, use our mocking framework 5 | #[cfg(test)] 6 | pub mod mock; 7 | 8 | #[derive(Debug, Clone, PartialEq, Eq)] 9 | pub enum ForkReturn { 10 | Parent(i32), 11 | Child, 12 | } 13 | 14 | #[derive(Debug, Clone, PartialEq, Eq)] 15 | pub struct PipeFds { 16 | pub read_fd: c_int, 17 | pub write_fd: c_int, 18 | } 19 | 20 | // Define the core trait for system functions 21 | pub trait SystemFunctions: std::fmt::Debug { 22 | fn fork(&self) -> Result; 23 | fn pipe(&self) -> Result; 24 | fn close(&self, fd: c_int) -> Result<(), io::Error>; 25 | fn waitpid(&self, pid: c_int) -> Result; 26 | fn _exit(&self, status: c_int) -> !; 27 | } 28 | 29 | // The real implementation that calls system functions directly 30 | #[derive(Debug, Clone)] 31 | pub struct RealSystemFunctions; 32 | 33 | impl SystemFunctions for RealSystemFunctions { 34 | fn fork(&self) -> Result { 35 | const FORK_FAILED: i32 = -1; 36 | const FORK_CHILD: i32 = 0; 37 | 38 | let ret = unsafe { libc::fork() }; 39 | match ret { 40 | FORK_FAILED => Err(io::Error::last_os_error()), 41 | FORK_CHILD => Ok(ForkReturn::Child), 42 | _ => { 43 | let child_pid = ret; 44 | Ok(ForkReturn::Parent(child_pid)) 45 | } 46 | } 47 | } 48 | 49 | fn pipe(&self) -> Result { 50 | let mut pipe_fds: [c_int; 2] = [0; 2]; 51 | let ret = unsafe { libc::pipe(pipe_fds.as_mut_ptr()) }; 52 | if ret == -1 { 53 | Err(io::Error::last_os_error()) 54 | } else { 55 | Ok(PipeFds { 56 | read_fd: pipe_fds[0], 57 | write_fd: pipe_fds[1], 58 | }) 59 | } 60 | } 61 | 62 | fn close(&self, fd: c_int) -> Result<(), io::Error> { 63 | let ret = unsafe { libc::close(fd) }; 64 | if ret == -1 { 65 | Err(io::Error::last_os_error()) 66 | } else { 67 | Ok(()) 68 | } 69 | } 70 | 71 | fn waitpid(&self, pid: c_int) -> Result { 72 | let mut status: c_int = 0; 73 | let ret = unsafe { libc::waitpid(pid, &raw mut status, 0) }; 74 | if ret == -1 { 75 | Err(io::Error::last_os_error()) 76 | } else { 77 | Ok(status) 78 | } 79 | } 80 | 81 | fn _exit(&self, status: c_int) -> ! { 82 | unsafe { libc::_exit(status) } 83 | } 84 | } 85 | 86 | pub type WaitpidStatus = libc::c_int; 87 | pub type ExitStatus = libc::c_int; 88 | pub type Signal = libc::c_int; 89 | 90 | #[inline] 91 | pub fn child_process_exited_on_its_own(waitpid_status: WaitpidStatus) -> Option { 92 | if libc::WIFEXITED(waitpid_status) { 93 | Some(libc::WEXITSTATUS(waitpid_status)) 94 | } else { 95 | None 96 | } 97 | } 98 | 99 | #[inline] 100 | pub fn child_process_killed_by_signal(waitpid_status: WaitpidStatus) -> Option { 101 | if libc::WIFSIGNALED(waitpid_status) { 102 | Some(libc::WTERMSIG(waitpid_status)) 103 | } else { 104 | None 105 | } 106 | } 107 | 108 | // For test builds, these functions will be provided by the mock module 109 | #[cfg(test)] 110 | mod tests { 111 | #![allow(clippy::unwrap_used)] 112 | 113 | use super::*; 114 | use crate::tests::TEST_TIMEOUT; 115 | use mock::*; 116 | use rstest::*; 117 | use std::io; 118 | 119 | #[rstest] 120 | #[timeout(TEST_TIMEOUT)] 121 | fn mock_enabling_disabling() { 122 | assert!(!is_mocking_enabled()); 123 | 124 | let mock = MockableSystemFunctions::with_fallback(); 125 | enable_mocking(&mock); 126 | assert!(is_mocking_enabled()); 127 | 128 | disable_mocking(); 129 | assert!(!is_mocking_enabled()); 130 | } 131 | 132 | #[rstest] 133 | #[timeout(TEST_TIMEOUT)] 134 | 135 | fn with_mock_system_helper() { 136 | assert!(!is_mocking_enabled()); 137 | with_mock_system(MockConfig::Fallback, |_| { 138 | assert!(is_mocking_enabled()); 139 | }); 140 | assert!(!is_mocking_enabled()); 141 | } 142 | 143 | #[rstest] 144 | #[timeout(TEST_TIMEOUT)] 145 | #[should_panic(expected = "No mock behavior configured for fork() and fallback is disabled")] 146 | fn stict_mocking_panics_when_no_mock_is_configured() { 147 | let mock = MockableSystemFunctions::strict(); 148 | enable_mocking(&mock); 149 | let _ = mock.fork(); 150 | disable_mocking(); 151 | } 152 | 153 | #[rstest] 154 | #[timeout(TEST_TIMEOUT)] 155 | #[should_panic(expected = "No mock behavior configured for fork() and fallback is disabled")] 156 | fn strict_mocking_panics_when_no_mock_is_configured_with_system_helper() { 157 | assert!(!is_mocking_enabled()); 158 | with_mock_system(MockConfig::Strict, |mock| { 159 | let _ = mock.fork(); 160 | }); 161 | assert!(!is_mocking_enabled()); 162 | } 163 | 164 | #[rstest] 165 | #[timeout(TEST_TIMEOUT)] 166 | #[should_panic(expected = "No mock behavior configured for fork() and fallback is disabled")] 167 | fn strict_mocking_panics_when_no_mock_is_configured_with_system_helper_configured_strict() { 168 | assert!(!is_mocking_enabled()); 169 | with_mock_system(configured_strict(|_| {}), |mock| { 170 | let _ = mock.fork(); 171 | }); 172 | assert!(!is_mocking_enabled()); 173 | } 174 | 175 | #[rstest] 176 | #[timeout(TEST_TIMEOUT)] 177 | fn fork_mocking() { 178 | use CallBehavior::Mock; 179 | 180 | let mock = MockableSystemFunctions::strict(); 181 | mock.expect_fork(Mock(Ok(ForkReturn::Parent(123)))); 182 | mock.expect_fork(Mock(Ok(ForkReturn::Child))); 183 | 184 | enable_mocking(&mock); 185 | 186 | let result1 = mock.fork().expect("Fork should succeed"); 187 | assert!(matches!(result1, ForkReturn::Parent(123))); 188 | let result2 = mock.fork().expect("Fork should succeed"); 189 | assert!(matches!(result2, ForkReturn::Child)); 190 | 191 | disable_mocking(); 192 | } 193 | 194 | #[rstest] 195 | #[timeout(TEST_TIMEOUT)] 196 | fn pipe_mocking() { 197 | with_mock_system( 198 | configured_strict(|mock| { 199 | mock.expect_pipe(CallBehavior::Mock(Ok(PipeFds { 200 | read_fd: 1000, 201 | write_fd: 1001, 202 | }))) 203 | .expect_close(CallBehavior::Mock(Ok(()))) 204 | .expect_close(CallBehavior::Mock(Ok(()))); 205 | }), 206 | |mock| { 207 | let pipe_fds = mock.pipe().expect("Pipe should succeed"); 208 | 209 | assert_eq!(pipe_fds.read_fd, 1000); 210 | assert_eq!(pipe_fds.write_fd, 1001); 211 | 212 | mock.close(pipe_fds.read_fd).expect("Close should succeed"); 213 | mock.close(pipe_fds.write_fd).expect("Close should succeed"); 214 | }, 215 | ); 216 | } 217 | 218 | #[rstest] 219 | #[timeout(TEST_TIMEOUT)] 220 | fn close_mocking() { 221 | let mock = MockableSystemFunctions::strict(); 222 | mock.expect_close(CallBehavior::Mock(Ok(()))); 223 | 224 | enable_mocking(&mock); 225 | 226 | let result = mock.close(999); 227 | assert!(result.is_ok()); 228 | 229 | disable_mocking(); 230 | } 231 | 232 | #[rstest] 233 | #[timeout(TEST_TIMEOUT)] 234 | fn waitpid_mocking() { 235 | let mock = MockableSystemFunctions::strict(); 236 | mock.expect_waitpid(CallBehavior::Mock(Ok(42))); 237 | 238 | enable_mocking(&mock); 239 | 240 | let status = mock.waitpid(123).expect("Waitpid should succeed"); 241 | assert_eq!(status, 42); 242 | 243 | disable_mocking(); 244 | } 245 | 246 | #[rstest] 247 | #[timeout(TEST_TIMEOUT)] 248 | fn error_conditions() { 249 | use CallBehavior::Mock; 250 | 251 | let mock = MockableSystemFunctions::strict(); 252 | 253 | // Set up various error conditions 254 | mock.expect_fork(Mock(Err(io::Error::from_raw_os_error(libc::EAGAIN)))); 255 | mock.expect_pipe(Mock(Err(io::Error::from_raw_os_error(libc::EMFILE)))); 256 | mock.expect_close(Mock(Err(io::Error::from_raw_os_error(libc::EBADF)))); 257 | mock.expect_waitpid(Mock(Err(io::Error::from_raw_os_error(libc::ECHILD)))); 258 | 259 | enable_mocking(&mock); 260 | 261 | // Test fork error 262 | let fork_result = mock.fork(); 263 | assert!(fork_result.is_err()); 264 | assert_eq!(fork_result.unwrap_err().raw_os_error(), Some(libc::EAGAIN)); 265 | 266 | // Test pipe error 267 | let pipe_result = mock.pipe(); 268 | assert!(pipe_result.is_err()); 269 | assert_eq!(pipe_result.unwrap_err().raw_os_error(), Some(libc::EMFILE)); 270 | 271 | // Test close error 272 | let close_result = mock.close(1); 273 | assert!(close_result.is_err()); 274 | assert_eq!(close_result.unwrap_err().raw_os_error(), Some(libc::EBADF)); 275 | 276 | // Test waitpid error 277 | let waitpid_result = mock.waitpid(1); 278 | assert!(waitpid_result.is_err()); 279 | assert_eq!( 280 | waitpid_result.unwrap_err().raw_os_error(), 281 | Some(libc::ECHILD) 282 | ); 283 | 284 | disable_mocking(); 285 | } 286 | 287 | #[rstest] 288 | #[timeout(TEST_TIMEOUT)] 289 | #[should_panic(expected = "No mock behavior configured for fork()")] 290 | fn missing_fork_expectation() { 291 | let mock = MockableSystemFunctions::with_fallback(); 292 | mock.disable_fallback(); 293 | enable_mocking(&mock); 294 | 295 | // This should panic because we didn't set an expectation 296 | let _ = mock.fork(); 297 | 298 | // We won't reach this, but it's good practice to clean up 299 | disable_mocking(); 300 | } 301 | 302 | #[rstest] 303 | #[timeout(TEST_TIMEOUT)] 304 | #[should_panic(expected = "No mock behavior configured for pipe()")] 305 | fn missing_pipe_expectation() { 306 | let mock = MockableSystemFunctions::with_fallback(); 307 | mock.disable_fallback(); 308 | enable_mocking(&mock); 309 | 310 | let _ = mock.pipe(); 311 | 312 | disable_mocking(); 313 | } 314 | 315 | #[rstest] 316 | #[timeout(TEST_TIMEOUT)] 317 | #[should_panic(expected = "No mock behavior configured for close()")] 318 | fn missing_close_expectation() { 319 | let mock = MockableSystemFunctions::with_fallback(); 320 | mock.disable_fallback(); 321 | enable_mocking(&mock); 322 | 323 | let _ = mock.close(1); 324 | 325 | disable_mocking(); 326 | } 327 | 328 | #[rstest] 329 | #[timeout(TEST_TIMEOUT)] 330 | #[should_panic(expected = "No mock behavior configured for waitpid()")] 331 | fn missing_waitpid_expectation() { 332 | let mock = MockableSystemFunctions::with_fallback(); 333 | mock.disable_fallback(); 334 | enable_mocking(&mock); 335 | 336 | let _ = mock.waitpid(1); 337 | 338 | disable_mocking(); 339 | } 340 | 341 | #[rstest] 342 | #[timeout(TEST_TIMEOUT)] 343 | #[should_panic(expected = "_exit(0) called in mock context")] 344 | fn exit_in_mock_context() { 345 | let mock = MockableSystemFunctions::with_fallback(); 346 | mock.disable_fallback(); 347 | enable_mocking(&mock); 348 | 349 | // This should panic with a specific message 350 | #[allow(clippy::used_underscore_items)] 351 | mock._exit(0); 352 | 353 | // WARNING: No disable_mocking() here because its unreachable 354 | } 355 | 356 | #[rstest] 357 | #[timeout(TEST_TIMEOUT)] 358 | fn multiple_expectations() { 359 | let mock = MockableSystemFunctions::with_fallback(); 360 | 361 | // Set up a sequence of expectations 362 | mock.expect_fork(CallBehavior::Mock(Ok(ForkReturn::Parent(1)))) 363 | .expect_fork(CallBehavior::Mock(Ok(ForkReturn::Parent(2)))) 364 | .expect_fork(CallBehavior::Mock(Ok(ForkReturn::Parent(3)))); 365 | 366 | enable_mocking(&mock); 367 | 368 | // Verify calls are processed in order 369 | assert!(matches!(mock.fork().unwrap(), ForkReturn::Parent(1))); 370 | assert!(matches!(mock.fork().unwrap(), ForkReturn::Parent(2))); 371 | assert!(matches!(mock.fork().unwrap(), ForkReturn::Parent(3))); 372 | 373 | disable_mocking(); 374 | } 375 | 376 | #[rstest] 377 | #[timeout(TEST_TIMEOUT)] 378 | fn non_os_error_handling() { 379 | // Create a special io::Error that's not an OS error 380 | let custom_error = io::Error::new(io::ErrorKind::Other, "Custom error"); 381 | 382 | let mock = MockableSystemFunctions::with_fallback(); 383 | 384 | // For MockResult::from_result, this should convert to EIO 385 | mock.expect_fork(CallBehavior::Mock(Err(custom_error))); 386 | 387 | enable_mocking(&mock); 388 | 389 | let result = mock.fork(); 390 | assert!(result.is_err()); 391 | // Should be converted to EIO 392 | assert_eq!(result.unwrap_err().raw_os_error(), Some(libc::EIO)); 393 | 394 | disable_mocking(); 395 | } 396 | 397 | #[rstest] 398 | #[timeout(TEST_TIMEOUT)] 399 | fn mixed_real_and_mock_calls() { 400 | with_mock_system( 401 | configured_with_fallback(|mock| { 402 | // Configure a mix of real and mock calls 403 | mock.expect_fork(CallBehavior::Real) 404 | .expect_fork(CallBehavior::Mock(Ok(ForkReturn::Parent(123)))) 405 | .expect_fork(CallBehavior::Real); 406 | }), 407 | |sys| { 408 | // Test function with the configured mock 409 | println!("First call: Real implementation"); 410 | let _result1 = sys.fork(); 411 | 412 | println!("Second call: Mock implementation"); 413 | let result2 = sys.fork().expect("Mock fork should succeed"); 414 | assert_eq!(result2, ForkReturn::Parent(123)); 415 | 416 | println!("Third call: Real implementation"); 417 | let _result3 = sys.fork(); 418 | }, 419 | ); 420 | } 421 | } 422 | -------------------------------------------------------------------------------- /src/c/mock.rs: -------------------------------------------------------------------------------- 1 | use crate::c::{ForkReturn, PipeFds, RealSystemFunctions, SystemFunctions}; 2 | use libc::c_int; 3 | use std::cell::{Cell, RefCell}; 4 | use std::collections::VecDeque; 5 | use std::io; 6 | use std::thread_local; 7 | 8 | // Type for representing a mock result 9 | #[derive(Clone, Debug)] 10 | enum MockResult { 11 | Ok(T), 12 | Err(i32), // OS error code 13 | } 14 | 15 | impl MockResult { 16 | #[allow(clippy::wrong_self_convention)] 17 | fn to_result(self) -> Result { 18 | match self { 19 | MockResult::Ok(val) => Ok(val), 20 | MockResult::Err(code) => Err(io::Error::from_raw_os_error(code)), 21 | } 22 | } 23 | 24 | fn from_result(result: Result) -> Self { 25 | match result { 26 | Ok(val) => MockResult::Ok(val), 27 | Err(err) => { 28 | if let Some(code) = err.raw_os_error() { 29 | MockResult::Err(code) 30 | } else { 31 | // Default to a generic error code if it's not an OS error 32 | MockResult::Err(libc::EIO) // I/O error 33 | } 34 | } 35 | } 36 | } 37 | } 38 | 39 | /// Defines how a call should be implemented - real or mocked 40 | #[derive(Clone, Debug)] 41 | enum CallImplementation { 42 | Real, // Use real implementation 43 | Mock(MockResult), // Use mocked result 44 | } 45 | 46 | /// Public API for specifying call behavior 47 | pub enum CallBehavior { 48 | Real, // Use the real system implementation 49 | Mock(Result), // Use a mock result 50 | } 51 | 52 | /// A generic queue of call implementations 53 | #[derive(Clone, Debug)] 54 | struct CallQueue { 55 | queue: RefCell>>, 56 | name: &'static str, // For better error messages 57 | } 58 | 59 | impl CallQueue { 60 | fn new(name: &'static str) -> Self { 61 | Self { 62 | queue: RefCell::new(VecDeque::new()), 63 | name, 64 | } 65 | } 66 | 67 | fn push(&self, behavior: CallBehavior) { 68 | let mut queue = self.queue.borrow_mut(); 69 | match behavior { 70 | CallBehavior::Real => queue.push_back(CallImplementation::Real), 71 | CallBehavior::Mock(result) => { 72 | queue.push_back(CallImplementation::Mock(MockResult::from_result(result))); 73 | } 74 | } 75 | } 76 | 77 | fn execute_next(&self, real_impl: F, fallback_enabled: bool) -> Result 78 | where 79 | F: FnOnce() -> Result, 80 | { 81 | // Get explicit reference to make the borrow checker happy 82 | let mut queue = self.queue.borrow_mut(); 83 | match queue.pop_front() { 84 | Some(CallImplementation::Real) => real_impl(), 85 | Some(CallImplementation::Mock(result)) => result.to_result(), 86 | None if fallback_enabled => real_impl(), 87 | None => panic!( 88 | "No mock behavior configured for {}() and fallback is disabled", 89 | self.name 90 | ), 91 | } 92 | } 93 | } 94 | 95 | /// Mock implementation that returns predefined values and can fall back to real implementation 96 | #[derive(Clone)] 97 | pub struct MockableSystemFunctions { 98 | real_impl: RealSystemFunctions, 99 | fallback_enabled: Cell, 100 | fork_queue: CallQueue, 101 | pipe_queue: CallQueue, 102 | close_queue: CallQueue<()>, 103 | waitpid_queue: CallQueue, 104 | } 105 | 106 | // The derived Debug impl is quite verbose 107 | impl std::fmt::Debug for MockableSystemFunctions { 108 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 109 | write!(f, "MockableSystemFunctions") 110 | } 111 | } 112 | 113 | impl Default for MockableSystemFunctions { 114 | fn default() -> Self { 115 | Self { 116 | real_impl: RealSystemFunctions, 117 | fallback_enabled: Cell::new(true), // Enable fallback by default 118 | fork_queue: CallQueue::new("fork"), 119 | pipe_queue: CallQueue::new("pipe"), 120 | close_queue: CallQueue::new("close"), 121 | waitpid_queue: CallQueue::new("waitpid"), 122 | } 123 | } 124 | } 125 | 126 | impl MockableSystemFunctions { 127 | /// Enable fallback to real implementations when no mock is configured 128 | pub fn with_fallback() -> Self { 129 | let mock = Self::default(); 130 | mock.enable_fallback(); 131 | mock 132 | } 133 | 134 | /// Disable fallback to real implementations (strict mocking mode) 135 | pub fn strict() -> Self { 136 | let mock = Self::default(); 137 | mock.disable_fallback(); 138 | mock 139 | } 140 | 141 | /// Enable fallback to real implementations when no mock is configured 142 | pub fn enable_fallback(&self) -> &Self { 143 | self.fallback_enabled.set(true); 144 | self 145 | } 146 | 147 | /// Disable fallback to real implementations (strict mocking mode) 148 | pub fn disable_fallback(&self) -> &Self { 149 | self.fallback_enabled.set(false); 150 | self 151 | } 152 | 153 | /// Check if fallback is enabled 154 | pub fn is_fallback_enabled(&self) -> bool { 155 | self.fallback_enabled.get() 156 | } 157 | 158 | // Generic methods for specifying behavior 159 | 160 | pub fn expect_fork(&self, behavior: CallBehavior) -> &Self { 161 | self.fork_queue.push(behavior); 162 | self 163 | } 164 | 165 | pub fn expect_pipe(&self, behavior: CallBehavior) -> &Self { 166 | self.pipe_queue.push(behavior); 167 | self 168 | } 169 | 170 | pub fn expect_close(&self, behavior: CallBehavior<()>) -> &Self { 171 | self.close_queue.push(behavior); 172 | self 173 | } 174 | 175 | pub fn expect_waitpid(&self, behavior: CallBehavior) -> &Self { 176 | self.waitpid_queue.push(behavior); 177 | self 178 | } 179 | } 180 | 181 | impl SystemFunctions for MockableSystemFunctions { 182 | fn fork(&self) -> Result { 183 | self.fork_queue 184 | .execute_next(|| self.real_impl.fork(), self.is_fallback_enabled()) 185 | } 186 | 187 | fn pipe(&self) -> Result { 188 | self.pipe_queue 189 | .execute_next(|| self.real_impl.pipe(), self.is_fallback_enabled()) 190 | } 191 | 192 | fn close(&self, fd: c_int) -> Result<(), io::Error> { 193 | self.close_queue 194 | .execute_next(|| self.real_impl.close(fd), self.is_fallback_enabled()) 195 | } 196 | 197 | fn waitpid(&self, pid: c_int) -> Result { 198 | self.waitpid_queue 199 | .execute_next(|| self.real_impl.waitpid(pid), self.is_fallback_enabled()) 200 | } 201 | 202 | fn _exit(&self, status: c_int) -> ! { 203 | if is_mocking_enabled() { 204 | // In mock context, we panic instead of exiting 205 | panic!("_exit({status}) called in mock context"); 206 | } else { 207 | // Otherwise, use the real implementation 208 | #[allow(clippy::used_underscore_items)] 209 | self.real_impl._exit(status) 210 | } 211 | } 212 | } 213 | 214 | // Thread-local storage for the current mocking state 215 | thread_local! { 216 | static CURRENT_MOCK: RefCell> = const { RefCell::new(None) }; 217 | static IS_MOCKING_ENABLED: Cell = const { Cell::new(false) }; 218 | } 219 | 220 | /// Enable mocking for the current thread with the specified mock configuration 221 | pub fn enable_mocking(mock: &MockableSystemFunctions) { 222 | CURRENT_MOCK.with(|m| { 223 | *m.borrow_mut() = Some(mock.clone()); 224 | }); 225 | IS_MOCKING_ENABLED.with(|e| e.set(true)); 226 | } 227 | 228 | /// Disable mocking for the current thread 229 | pub fn disable_mocking() { 230 | CURRENT_MOCK.with(|m| { 231 | *m.borrow_mut() = None; 232 | }); 233 | IS_MOCKING_ENABLED.with(|e| e.set(false)); 234 | } 235 | 236 | /// Returns true if mocking is currently enabled for the current thread 237 | pub fn is_mocking_enabled() -> bool { 238 | IS_MOCKING_ENABLED.with(std::cell::Cell::get) 239 | } 240 | 241 | /// Get the current mock from thread-local storage 242 | pub fn get_current_mock() -> MockableSystemFunctions { 243 | CURRENT_MOCK.with(|m| { 244 | m.borrow() 245 | .clone() 246 | .expect("No mock available in thread-local storage") 247 | }) 248 | } 249 | 250 | /// Configuration options for the mock system 251 | pub enum MockConfig { 252 | /// Use fallback mode (real implementations when no mock is configured) 253 | Fallback, 254 | 255 | /// Use strict mode (panic when no mock is configured) 256 | Strict, 257 | 258 | /// Configure the mock with a function, using fallback mode 259 | ConfiguredWithFallback(Box), 260 | 261 | /// Configure the mock with a function, using strict mode 262 | ConfiguredStrict(Box), 263 | } 264 | 265 | /// Set up mocking environment and execute a test function 266 | pub fn with_mock_system( 267 | config: MockConfig, 268 | test_fn: impl FnOnce(&MockableSystemFunctions) -> R, 269 | ) -> R { 270 | let mock = MockableSystemFunctions::with_fallback(); 271 | 272 | // Configure based on the enum variant 273 | match config { 274 | MockConfig::Fallback => {} 275 | MockConfig::Strict => { 276 | mock.disable_fallback(); 277 | } 278 | MockConfig::ConfiguredWithFallback(configure_fn) => { 279 | configure_fn(&mock); 280 | } 281 | MockConfig::ConfiguredStrict(configure_fn) => { 282 | mock.disable_fallback(); 283 | configure_fn(&mock); 284 | } 285 | } 286 | 287 | // Enable mocking with the configured mock 288 | enable_mocking(&mock); 289 | 290 | // Run the test function with mocking enabled 291 | let result = test_fn(&mock); 292 | 293 | // Disable mocking 294 | disable_mocking(); 295 | 296 | result 297 | } 298 | 299 | /// Helper to create a `ConfiguredWithFallback` variant 300 | pub fn configured_with_fallback(configure_fn: F) -> MockConfig 301 | where 302 | F: FnOnce(&MockableSystemFunctions) + 'static, 303 | { 304 | MockConfig::ConfiguredWithFallback(Box::new(configure_fn)) 305 | } 306 | 307 | /// Helper to create a `ConfiguredStrict` variant 308 | pub fn configured_strict(configure_fn: F) -> MockConfig 309 | where 310 | F: FnOnce(&MockableSystemFunctions) + 'static, 311 | { 312 | MockConfig::ConfiguredStrict(Box::new(configure_fn)) 313 | } 314 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | //! Error handling is an important part of the `mem-isolate` crate. If something 2 | //! went wrong, we want to give the caller as much context as possible about how 3 | //! that error affected their `callable`, so they are well-equipped to know what 4 | //! to do about it. 5 | //! 6 | //! The primary error type is [`MemIsolateError`], which is returned by 7 | //! [`crate::execute_in_isolated_process`]. 8 | //! 9 | //! Philosophically, error handling in `mem-isolate` is organized into three 10 | //! levels of error wrapping: 11 | //! 12 | //! 1. The first level describes the effect of the error on the `callable` (e.g. 13 | //! did your callable function execute or not) 14 | //! 2. The second level describes what `mem-isolate` operation caused the error 15 | //! (e.g. did serialization fail) 16 | //! 3. The third level is the underlying OS error if it is available (e.g. an 17 | //! `io::Error`) 18 | //! 19 | //! For most applications, you'll care only about the first level. For an 20 | //! example of common error handling dealing only with first level errors, see 21 | //! [`examples/error-handling-basic.rs`](https://github.com/brannondorsey/mem-isolate/blob/main/examples/error-handling-basic.rs). 22 | //! 23 | //! Levels two and three are useful if you want to know more about what 24 | //! **exactly** went wrong and expose internals about how `mem-isolate` works. 25 | //! 26 | //! Note: These errors all describe things that went wrong with a `mem-isolate` 27 | //! operation. They have nothing to do with the `callable` you passed to 28 | //! [`crate::execute_in_isolated_process`], which can define its own errors and 29 | //! maybe values by returning a [`Result`] or [`Option`] type. 30 | 31 | use serde::Deserialize; 32 | use serde::Serialize; 33 | use std::io; 34 | use thiserror::Error; 35 | 36 | /// [`MemIsolateError`] is the **primary error type returned by the crate**. The 37 | /// goal is to give the caller context about what happened to their callable if 38 | /// something went wrong. 39 | /// 40 | /// For basic usage, and an introduction of how you should think about error 41 | /// handling with this crate, see 42 | /// [`examples/error-handling-basic.rs`](https://github.com/brannondorsey/mem-isolate/blob/main/examples/error-handling-basic.rs) 43 | /// 44 | /// For an exhaustive look of all possible error variants, see [`examples/error-handling-complete.rs`](https://github.com/brannondorsey/mem-isolate/blob/main/examples/error-handling-complete.rs) 45 | // TODO: Consider making this Send + Sync 46 | #[derive(Error, Debug, Serialize, Deserialize)] 47 | pub enum MemIsolateError { 48 | /// Indicates something went wrong before the callable was executed. Because 49 | /// the callable never executed, it should be safe to naively retry the 50 | /// callable with or without mem-isolate, even if the function is not 51 | /// idempotent. 52 | #[error("an error occurred before the callable was executed: {0}")] 53 | CallableDidNotExecute(#[source] CallableDidNotExecuteError), 54 | 55 | /// Indicates something went wrong after the callable was executed. **Do not** 56 | /// retry execution of the callable unless it is idempotent. 57 | #[error("an error occurred after the callable was executed: {0}")] 58 | CallableExecuted(#[source] CallableExecutedError), 59 | 60 | /// Indicates something went wrong, but it is unknown whether the callable was 61 | /// executed. **You should retry the callable only if it is idempotent.** 62 | #[error("the callable process exited with an unknown status: {0}")] 63 | CallableStatusUnknown(#[source] CallableStatusUnknownError), 64 | } 65 | 66 | /// An error indicating something went wrong **after** the user-supplied callable was executed 67 | /// 68 | /// You should only retry the callable if it is idempotent. 69 | #[derive(Error, Debug, Serialize, Deserialize)] 70 | pub enum CallableExecutedError { 71 | /// An error occurred while serializing the result of the callable inside the child process 72 | #[error( 73 | "an error occurred while serializing the result of the callable inside the child process: {0}" 74 | )] 75 | SerializationFailed(String), 76 | 77 | /// An error occurred while deserializing the result of the callable in the parent process 78 | #[error( 79 | "an error occurred while deserializing the result of the callable in the parent process: {0}" 80 | )] 81 | DeserializationFailed(String), 82 | 83 | /// A system error occurred while writing the child process's result to the pipe. 84 | #[serde( 85 | serialize_with = "serialize_option_os_error", 86 | deserialize_with = "deserialize_option_os_error" 87 | )] 88 | #[error("system error encountered writing the child process's result to the pipe: {}", format_option_error(.0))] 89 | ChildPipeWriteFailed(#[source] Option), 90 | } 91 | 92 | /// An error indicating something went wrong **before** the user-supplied 93 | /// callable was executed 94 | /// 95 | /// It is harmless to retry the callable. 96 | #[derive(Error, Debug, Serialize, Deserialize)] 97 | pub enum CallableDidNotExecuteError { 98 | // TODO: Consider making these io::Errors be RawOsError typedefs instead. 99 | // That rules out a ton of overloaded io::Error posibilities. It's more 100 | // precise. WARNING: Serialization will fail if this is not an OS error. 101 | // 102 | /// A system error occurred while creating the pipe used to communicate with 103 | /// the child process 104 | #[serde( 105 | serialize_with = "serialize_os_error", 106 | deserialize_with = "deserialize_os_error" 107 | )] 108 | #[error( 109 | "system error encountered creating the pipe used to communicate with the child process: {0}" 110 | )] 111 | PipeCreationFailed(#[source] io::Error), 112 | 113 | /// A system error occurred while closing the child process's copy of the 114 | /// pipe's read end 115 | #[serde( 116 | serialize_with = "serialize_option_os_error", 117 | deserialize_with = "deserialize_option_os_error" 118 | )] 119 | #[error("system error encountered closing the child's copy of the pipe's read end: {}", format_option_error(.0))] 120 | ChildPipeCloseFailed(#[source] Option), 121 | 122 | /// A system error occurred while forking the child process which is used to 123 | /// execute user-supplied callable 124 | #[serde( 125 | serialize_with = "serialize_os_error", 126 | deserialize_with = "deserialize_os_error" 127 | )] 128 | #[error("system error encountered forking the child process: {0}")] 129 | ForkFailed(#[source] io::Error), 130 | } 131 | 132 | /// An error indicating that something went wrong in a way where it is difficult 133 | /// or impossible to determine whether the user-supplied callable was executed 134 | /// `¯\_(ツ)_/¯` 135 | /// 136 | /// You should only retry the callable if it is idempotent. 137 | #[derive(Error, Debug, Serialize, Deserialize)] 138 | pub enum CallableStatusUnknownError { 139 | /// A system error occurred while closing the parent's copy of the pipe's 140 | /// write end 141 | #[serde( 142 | serialize_with = "serialize_os_error", 143 | deserialize_with = "deserialize_os_error" 144 | )] 145 | #[error("system error encountered closing the parent's copy of the pipe's write end: {0}")] 146 | ParentPipeCloseFailed(#[source] io::Error), 147 | 148 | /// A system error occurred while waiting for the child process to exit 149 | #[serde( 150 | serialize_with = "serialize_os_error", 151 | deserialize_with = "deserialize_os_error" 152 | )] 153 | #[error("system error encountered waiting for the child process: {0}")] 154 | WaitFailed(#[source] io::Error), 155 | 156 | /// A system error occurred while reading the child's result from the pipe 157 | #[serde( 158 | serialize_with = "serialize_os_error", 159 | deserialize_with = "deserialize_os_error" 160 | )] 161 | #[error("system error encountered reading the child's result from the pipe: {0}")] 162 | ParentPipeReadFailed(#[source] io::Error), 163 | 164 | /// The callable process died while executing the user-supplied callable 165 | #[error("the callable process died during execution")] 166 | CallableProcessDiedDuringExecution, 167 | 168 | /// The child process responsible for executing the user-supplied callable 169 | /// exited with an unexpected status 170 | /// 171 | /// Note this does not represent some sort of exit code or return value 172 | /// indicating the success or failure of the user-supplied callable itself. 173 | #[error("the callable process exited with an unexpected status: {0}")] 174 | UnexpectedChildExitStatus(i32), 175 | 176 | /// The child process responsible for executing the user-supplied callable 177 | /// was killed by a signal 178 | #[error("the callable process was killed by a signal: {0}")] 179 | ChildProcessKilledBySignal(i32), 180 | 181 | /// Waitpid returned an unexpected value 182 | #[error("waitpid returned an unexpected value: {0}")] 183 | UnexpectedWaitpidReturnValue(i32), 184 | } 185 | 186 | fn serialize_os_error(error: &io::Error, serializer: S) -> Result 187 | where 188 | S: serde::Serializer, 189 | { 190 | if let Some(raw_os_error) = error.raw_os_error() { 191 | serializer.serialize_i32(raw_os_error) 192 | } else { 193 | Err(serde::ser::Error::custom("not an os error")) 194 | } 195 | } 196 | 197 | fn deserialize_os_error<'de, D>(deserializer: D) -> Result 198 | where 199 | D: serde::Deserializer<'de>, 200 | { 201 | let s: i32 = i32::deserialize(deserializer)?; 202 | Ok(io::Error::from_raw_os_error(s)) 203 | } 204 | 205 | #[allow(clippy::ref_option)] 206 | fn serialize_option_os_error(error: &Option, serializer: S) -> Result 207 | where 208 | S: serde::Serializer, 209 | { 210 | if let Some(error) = error { 211 | serialize_os_error(error, serializer) 212 | } else { 213 | serializer.serialize_none() 214 | } 215 | } 216 | 217 | fn deserialize_option_os_error<'de, D>(deserializer: D) -> Result, D::Error> 218 | where 219 | D: serde::Deserializer<'de>, 220 | { 221 | let s: Option = Option::deserialize(deserializer)?; 222 | match s { 223 | Some(s) => Ok(Some(io::Error::from_raw_os_error(s))), 224 | None => Ok(None), 225 | } 226 | } 227 | 228 | #[allow(clippy::ref_option)] 229 | fn format_option_error(err: &Option) -> String { 230 | match err { 231 | Some(e) => e.to_string(), 232 | None => "None".to_string(), 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # `mem-isolate`: *Contain memory leaks and fragmentation* 2 | //! 3 | //! It runs your function via a `fork()`, waits for the result, and returns it. 4 | //! 5 | //! This grants your code access to an exact copy of memory and state at the 6 | //! time just before the call, but guarantees that the function will not affect 7 | //! the parent process's memory footprint in any way. 8 | //! 9 | //! It forces functions to be *memory pure* (pure with respect to memory), even 10 | //! if they aren't. 11 | //! 12 | //! ``` 13 | //! use mem_isolate::execute_in_isolated_process; 14 | //! 15 | //! // No heap, stack, or program memory out here... 16 | //! let result = mem_isolate::execute_in_isolated_process(|| { 17 | //! // ...Can be affected by anything in here 18 | //! Box::leak(Box::new(vec![42; 1024])); 19 | //! }); 20 | //! ``` 21 | //! 22 | //! To keep things simple, this crate exposes only two public interfaces: 23 | //! 24 | //! * [`execute_in_isolated_process`] - The function that executes your code in 25 | //! an isolated process. 26 | //! * [`MemIsolateError`] - The error type that function returns ☝️ 27 | //! 28 | //! For more code examples, see 29 | //! [`examples/`](https://github.com/brannondorsey/mem-isolate/tree/main/examples). 30 | //! [This 31 | //! one](https://github.com/brannondorsey/mem-isolate/blob/main/examples/error-handling-basic.rs) 32 | //! in particular shows how you should think about error handling. 33 | //! 34 | //! For more information, see the 35 | //! [README](https://github.com/brannondorsey/mem-isolate). 36 | //! 37 | //! ## Limitations 38 | //! 39 | //! #### Performance & Usability 40 | //! 41 | //! * Works only on POSIX systems (Linux, macOS, BSD) 42 | //! * Data returned from the `callable` function must be serialized to and from 43 | //! the child process (using `serde`), which can be expensive for large data. 44 | //! * Excluding serialization/deserialization cost, 45 | //! `execute_in_isolated_process()` introduces runtime overhead on the order 46 | //! of ~1ms compared to a direct invocation of the `callable`. 47 | //! 48 | //! In performance-critical systems, these overheads can be no joke. However, 49 | //! for many use cases, this is an affordable trade-off for the memory safety 50 | //! and snapshotting behavior that `mem-isolate` provides. 51 | //! 52 | //! #### Safety & Correctness 53 | //! 54 | //! The use of `fork()`, which this crate uses under the hood, has a slew of 55 | //! potentially dangerous side effects and surprises if you're not careful. 56 | //! 57 | //! * For **single-threaded use only:** It is generally unsound to `fork()` in 58 | //! multi-threaded environments, especially when mutexes are involved. Only 59 | //! the thread that calls `fork()` will be cloned and live on in the new 60 | //! process. This can easily lead to deadlocks and hung child processes if 61 | //! other threads are holding resource locks that the child process expects to 62 | //! acquire. 63 | //! * **Signals** delivered to the parent process won't be automatically 64 | //! forwarded to the child process running your `callable` during its 65 | //! execution. See one of the `examples/blocking-signals-*` files for [an 66 | //! example](https://github.com/brannondorsey/mem-isolate/blob/main/examples/blocking-signals-minimal.rs) 67 | //! of how to handle this. 68 | //! * **[Channels](https://doc.rust-lang.org/std/sync/mpsc/fn.channel.html)** 69 | //! can't be used to communicate between the parent and child processes. 70 | //! Consider using shared mmaps, pipes, or the filesystem instead. 71 | //! * **Shared mmaps** break the isolation guarantees of this crate. The child 72 | //! process will be able to mutate `mmap(..., MAP_SHARED, ...)` regions 73 | //! created by the parent process. 74 | //! * **Panics** in your `callable` won't panic the rest of your program, as 75 | //! they would without `mem-isolate`. That's as useful as it is harmful, 76 | //! depending on your use case, but it's worth noting. 77 | //! * **Mutable references, static variables, and raw pointers** accessible to 78 | //! your `callable` won't be modified as you would expect them to. That's kind 79 | //! of the whole point of this crate... ;) 80 | //! 81 | //! Failing to understand or respect these limitations will make your code more 82 | //! susceptible to both undefined behavior (UB) and heap corruption, not less. 83 | //! 84 | //! ## Feature Flags 85 | //! 86 | //! The following crate [feature 87 | //! flags](https://doc.rust-lang.org/cargo/reference/manifest.html#the-features-section) 88 | //! are available: 89 | //! 90 | //! * `tracing`: Enable [tracing](https://docs.rs/tracing) instrumentation. 91 | //! Instruments all high-level functions in 92 | //! [`lib.rs`](https://github.com/brannondorsey/mem-isolate/blob/main/src/lib.rs) 93 | //! and creates spans for child and parent processes in 94 | //! [`execute_in_isolated_process`]. Events are mostly `debug!` and `error!` 95 | //! level. See 96 | //! [`examples/tracing.rs`](https://github.com/brannondorsey/mem-isolate/blob/main/examples/tracing.rs) 97 | //! for an example. 98 | //! 99 | //! By default, no additional features are enabled. 100 | #![warn(missing_docs)] 101 | #![warn(clippy::pedantic, clippy::unwrap_used)] 102 | #![warn(missing_debug_implementations)] 103 | 104 | #[cfg(not(any(target_family = "unix")))] 105 | compile_error!( 106 | "Because of its heavy use of POSIX system calls, this crate only supports Unix-like operating systems (e.g. Linux, macOS, BSD)" 107 | ); 108 | 109 | mod macros; 110 | use macros::{debug, error}; 111 | #[cfg(feature = "tracing")] 112 | // Don't import event macros like debug, error, etc. directly to avoid conflicts 113 | // with our macros (see just above^) 114 | use tracing::{Level, instrument, span}; 115 | 116 | use libc::{EINTR, c_int}; 117 | use std::fmt::Debug; 118 | use std::fs::File; 119 | use std::io::{Read, Write}; 120 | use std::os::unix::io::FromRawFd; 121 | 122 | #[cfg(test)] 123 | mod tests; 124 | 125 | mod c; 126 | use c::{ 127 | ForkReturn, PipeFds, SystemFunctions, WaitpidStatus, child_process_exited_on_its_own, 128 | child_process_killed_by_signal, 129 | }; 130 | 131 | pub mod errors; 132 | pub use errors::MemIsolateError; 133 | use errors::{ 134 | CallableDidNotExecuteError::{ChildPipeCloseFailed, ForkFailed, PipeCreationFailed}, 135 | CallableExecutedError::{ChildPipeWriteFailed, DeserializationFailed, SerializationFailed}, 136 | CallableStatusUnknownError::{ 137 | CallableProcessDiedDuringExecution, ChildProcessKilledBySignal, ParentPipeCloseFailed, 138 | ParentPipeReadFailed, UnexpectedChildExitStatus, UnexpectedWaitpidReturnValue, WaitFailed, 139 | }, 140 | }; 141 | 142 | use MemIsolateError::{CallableDidNotExecute, CallableExecuted, CallableStatusUnknown}; 143 | 144 | // Re-export the serde traits our public API depends on 145 | pub use serde::{Serialize, de::DeserializeOwned}; 146 | 147 | // Child process exit status codes 148 | const CHILD_EXIT_HAPPY: i32 = 0; 149 | const CHILD_EXIT_IF_READ_CLOSE_FAILED: i32 = 3; 150 | const CHILD_EXIT_IF_WRITE_FAILED: i32 = 4; 151 | 152 | #[cfg(feature = "tracing")] 153 | const HIGHEST_LEVEL: Level = Level::ERROR; 154 | 155 | /// Executes a user-supplied `callable` in a forked child process so that any 156 | /// memory changes during execution do not affect the parent. The child 157 | /// serializes its result (using bincode) and writes it through a pipe, which 158 | /// the parent reads and deserializes. 159 | /// 160 | /// # Example 161 | /// 162 | /// ```rust 163 | /// use mem_isolate::execute_in_isolated_process; 164 | /// 165 | /// let leaky_fn = || { 166 | /// // Leak 1KiB of memory 167 | /// let data: Vec = Vec::with_capacity(1024); 168 | /// let data = Box::new(data); 169 | /// Box::leak(data); 170 | /// }; 171 | /// 172 | /// let _ = execute_in_isolated_process(leaky_fn); 173 | /// // However, the memory is not leaked in the parent process here 174 | /// ``` 175 | /// 176 | /// # Errors 177 | /// 178 | /// Error handling is organized into three levels: 179 | /// 180 | /// 1. The first level describes the effect of the error on the `callable` (e.g. 181 | /// did your callable function execute or not) 182 | /// 2. The second level describes what `mem-isolate` operation caused the error 183 | /// (e.g. did serialization fail) 184 | /// 3. The third level is the underlying OS error if it is available (e.g. an 185 | /// `io::Error`) 186 | /// 187 | /// For most applications, you'll care only about the first level: 188 | /// 189 | /// ```rust 190 | /// use mem_isolate::{execute_in_isolated_process, MemIsolateError}; 191 | /// 192 | /// // Function that might cause memory issues 193 | /// let result = execute_in_isolated_process(|| { 194 | /// // Some operation 195 | /// "Success!".to_string() 196 | /// }); 197 | /// 198 | /// match result { 199 | /// Ok(value) => println!("Callable succeeded: {}", value), 200 | /// Err(MemIsolateError::CallableDidNotExecute(_)) => { 201 | /// // Safe to retry, callable never executed 202 | /// println!("Callable did not execute, can safely retry"); 203 | /// }, 204 | /// Err(MemIsolateError::CallableExecuted(_)) => { 205 | /// // Do not retry unless idempotent 206 | /// println!("Callable executed but result couldn't be returned"); 207 | /// }, 208 | /// Err(MemIsolateError::CallableStatusUnknown(_)) => { 209 | /// // Retry only if idempotent 210 | /// println!("Unknown if callable executed, retry only if idempotent"); 211 | /// } 212 | /// } 213 | /// ``` 214 | /// 215 | /// For a more detailed look at error handling, see the documentation in the 216 | /// [`errors`] module. 217 | /// 218 | /// # Important Note on Closures 219 | /// 220 | /// When using closures that capture and mutate variables from their 221 | /// environment, these mutations **only occur in the isolated child process** 222 | /// and do not affect the parent process's memory. For example, it may seem 223 | /// surprising that the following code will leave the parent's `counter` 224 | /// variable unchanged: 225 | /// 226 | /// ```rust 227 | /// use mem_isolate::execute_in_isolated_process; 228 | /// 229 | /// let mut counter = 0; 230 | /// let result = execute_in_isolated_process(|| { 231 | /// counter += 1; // This increment only happens in the child process 232 | /// counter // Returns 1 233 | /// }); 234 | /// assert_eq!(counter, 0); // Parent's counter remains unchanged 235 | /// ``` 236 | /// 237 | /// This is the intended behavior as the function's purpose is to isolate all 238 | /// memory effects of the callable. However, this can be surprising, especially 239 | /// for [`FnMut`] or [`FnOnce`] closures. 240 | /// 241 | /// # Limitations 242 | /// 243 | /// #### Performance & Usability 244 | /// 245 | /// * Works only on POSIX systems (Linux, macOS, BSD) 246 | /// * Data returned from the `callable` function must be serialized to and from 247 | /// the child process (using `serde`), which can be expensive for large data. 248 | /// * Excluding serialization/deserialization cost, 249 | /// `execute_in_isolated_process()` introduces runtime overhead on the order 250 | /// of ~1ms compared to a direct invocation of the `callable`. 251 | /// 252 | /// In performance-critical systems, these overheads can be no joke. However, 253 | /// for many use cases, this is an affordable trade-off for the memory safety 254 | /// and snapshotting behavior that `mem-isolate` provides. 255 | /// 256 | /// #### Safety & Correctness 257 | /// 258 | /// The use of `fork()`, which this crate uses under the hood, has a slew of 259 | /// potentially dangerous side effects and surprises if you're not careful. 260 | /// 261 | /// * For **single-threaded use only:** It is generally unsound to `fork()` in 262 | /// multi-threaded environments, especially when mutexes are involved. Only 263 | /// the thread that calls `fork()` will be cloned and live on in the new 264 | /// process. This can easily lead to deadlocks and hung child processes if 265 | /// other threads are holding resource locks that the child process expects to 266 | /// acquire. 267 | /// * **Signals** delivered to the parent process won't be automatically 268 | /// forwarded to the child process running your `callable` during its 269 | /// execution. See one of the `examples/blocking-signals-*` files for [an 270 | /// example](https://github.com/brannondorsey/mem-isolate/blob/main/) of how 271 | /// to handle this. 272 | /// * **[Channels](https://doc.rust-lang.org/std/sync/mpsc/fn.channel.html)** 273 | /// can't be used to communicate between the parent and child processes. 274 | /// Consider using shared mmaps, pipes, or the filesystem instead. 275 | /// * **Shared mmaps** break the isolation guarantees of this crate. The child 276 | /// process will be able to mutate `mmap(..., MAP_SHARED, ...)` regions 277 | /// created by the parent process. 278 | /// * **Panics** in your `callable` won't panic the rest of your program, as 279 | /// they would without `mem-isolate`. That's as useful as it is harmful, 280 | /// depending on your use case, but it's worth noting. 281 | /// * **Mutable references, static variables, and raw pointers** accessible to 282 | /// your `callable` won't be modified as you would expect them to. That's kind 283 | /// of the whole point of this crate... ;) 284 | /// 285 | /// Failing to understand or respect these limitations will make your code more 286 | /// susceptible to both undefined behavior (UB) and heap corruption, not less. 287 | #[cfg_attr(feature = "tracing", instrument(skip(callable)))] 288 | pub fn execute_in_isolated_process(callable: F) -> Result 289 | where 290 | F: FnOnce() -> T, 291 | T: Serialize + DeserializeOwned, 292 | { 293 | #[cfg(feature = "tracing")] 294 | let parent_span = span!(HIGHEST_LEVEL, "parent").entered(); 295 | 296 | let sys = get_system_functions(); 297 | let PipeFds { read_fd, write_fd } = create_pipe(&sys)?; 298 | 299 | match fork(&sys)? { 300 | ForkReturn::Child => { 301 | #[cfg(feature = "tracing")] 302 | let _child_span = { 303 | std::mem::drop(parent_span); 304 | span!(HIGHEST_LEVEL, "child").entered() 305 | }; 306 | 307 | #[cfg(test)] 308 | // Disable mocking in the child process, fixes test failures on macOS 309 | c::mock::disable_mocking(); 310 | 311 | // NOTE: Fallible actions in the child must either serialize 312 | // and send their error over the pipe, or exit with a code 313 | // that can be inerpreted by the parent. 314 | // TODO: Consider removing the serializations and just 315 | // using exit codes as the only way to communicate errors. 316 | // TODO: Get rid of all of the .expect()s 317 | 318 | let mut writer = unsafe { File::from_raw_fd(write_fd) }; 319 | close_read_end_of_pipe_in_child_or_exit(&sys, &mut writer, read_fd); 320 | 321 | let result = execute_callable(callable); 322 | let encoded = serialize_result_or_error_value(result); 323 | write_and_flush_or_exit(&sys, &mut writer, &encoded); 324 | exit_happy(&sys) 325 | } 326 | ForkReturn::Parent(child_pid) => { 327 | close_write_end_of_pipe_in_parent(&sys, write_fd)?; 328 | 329 | // Read the data from the pipe before waiting for the child to exit 330 | // to prevent deadlocks. Don't bubble up errors before the waitpid 331 | // to avoid creating zombie processes. 332 | let pipe_result = read_all_of_child_result_pipe(read_fd); 333 | 334 | let waitpid_bespoke_status = wait_for_child(&sys, child_pid)?; 335 | error_if_child_unhappy(waitpid_bespoke_status)?; 336 | 337 | // Defer the buffer emptiness check until after the waitpid to preserve 338 | // the behavior that child processes that panic will result in a 339 | // CallableProcessDiedDuringExecution error. 340 | let buffer = pipe_result?; 341 | error_if_buffer_is_empty(&buffer)?; 342 | deserialize_result(&buffer) 343 | } 344 | } 345 | } 346 | 347 | #[must_use] 348 | #[cfg_attr(feature = "tracing", instrument)] 349 | fn get_system_functions() -> impl SystemFunctions { 350 | // Use the appropriate implementation based on build config 351 | #[cfg(not(test))] 352 | let sys = c::RealSystemFunctions; 353 | 354 | #[cfg(test)] 355 | let sys = if c::mock::is_mocking_enabled() { 356 | // Use the mock from thread-local storage 357 | c::mock::get_current_mock() 358 | } else { 359 | // Create a new fallback mock if no mock is active 360 | c::mock::MockableSystemFunctions::with_fallback() 361 | }; 362 | 363 | debug!("using {:?}", sys); 364 | sys 365 | } 366 | 367 | #[cfg_attr(feature = "tracing", instrument)] 368 | fn create_pipe(sys: &S) -> Result { 369 | let pipe_fds = match sys.pipe() { 370 | Ok(pipe_fds) => pipe_fds, 371 | Err(err) => { 372 | let err = CallableDidNotExecute(PipeCreationFailed(err)); 373 | error!("error creating pipe, propagating {:?}", err); 374 | return Err(err); 375 | } 376 | }; 377 | debug!("pipe created: {:?}", pipe_fds); 378 | Ok(pipe_fds) 379 | } 380 | 381 | #[cfg_attr(feature = "tracing", instrument)] 382 | fn fork(sys: &S) -> Result { 383 | match sys.fork() { 384 | Ok(result) => Ok(result), 385 | Err(err) => { 386 | let err = CallableDidNotExecute(ForkFailed(err)); 387 | error!("error forking, propagating {:?}", err); 388 | Err(err) 389 | } 390 | } 391 | } 392 | 393 | #[cfg_attr(feature = "tracing", instrument(skip(callable)))] 394 | fn execute_callable(callable: F) -> T 395 | where 396 | F: FnOnce() -> T, 397 | { 398 | debug!("starting execution of user-supplied callable"); 399 | #[allow(clippy::let_and_return)] 400 | let result = { 401 | #[cfg(feature = "tracing")] 402 | let _span = span!(HIGHEST_LEVEL, "inside_callable").entered(); 403 | callable() 404 | }; 405 | debug!("finished execution of user-supplied callable"); 406 | result 407 | } 408 | 409 | #[cfg_attr(feature = "tracing", instrument)] 410 | fn wait_for_child( 411 | sys: &S, 412 | child_pid: c_int, 413 | ) -> Result { 414 | debug!("waiting for child process"); 415 | let waitpid_bespoke_status = loop { 416 | match sys.waitpid(child_pid) { 417 | Ok(status) => break status, 418 | Err(wait_err) => { 419 | if wait_err.raw_os_error() == Some(EINTR) { 420 | debug!("waitpid interrupted with EINTR, retrying"); 421 | continue; 422 | } 423 | let err = CallableStatusUnknown(WaitFailed(wait_err)); 424 | error!("error waiting for child process, propagating {:?}", err); 425 | return Err(err); 426 | } 427 | } 428 | }; 429 | 430 | debug!( 431 | "wait completed, received status: {:?}", 432 | waitpid_bespoke_status 433 | ); 434 | Ok(waitpid_bespoke_status) 435 | } 436 | 437 | #[cfg_attr(feature = "tracing", instrument)] 438 | fn error_if_child_unhappy(waitpid_bespoke_status: WaitpidStatus) -> Result<(), MemIsolateError> { 439 | let result = if let Some(exit_status) = child_process_exited_on_its_own(waitpid_bespoke_status) 440 | { 441 | debug!("child process exited with status: {}", exit_status); 442 | match exit_status { 443 | CHILD_EXIT_HAPPY => Ok(()), 444 | CHILD_EXIT_IF_READ_CLOSE_FAILED => { 445 | Err(CallableDidNotExecute(ChildPipeCloseFailed(None))) 446 | } 447 | CHILD_EXIT_IF_WRITE_FAILED => Err(CallableExecuted(ChildPipeWriteFailed(None))), 448 | unhandled_status => Err(CallableStatusUnknown(UnexpectedChildExitStatus( 449 | unhandled_status, 450 | ))), 451 | } 452 | } else if let Some(signal) = child_process_killed_by_signal(waitpid_bespoke_status) { 453 | Err(CallableStatusUnknown(ChildProcessKilledBySignal(signal))) 454 | } else { 455 | Err(CallableStatusUnknown(UnexpectedWaitpidReturnValue( 456 | waitpid_bespoke_status, 457 | ))) 458 | }; 459 | 460 | if let Ok(()) = result { 461 | debug!("child process exited happily on its own"); 462 | } else { 463 | error!("child process signaled an error, propagating {:?}", result); 464 | } 465 | 466 | result 467 | } 468 | 469 | #[cfg_attr(feature = "tracing", instrument)] 470 | fn deserialize_result(buffer: &[u8]) -> Result { 471 | match bincode::deserialize::>(buffer) { 472 | Ok(Ok(result)) => { 473 | debug!("successfully deserialized happy result"); 474 | Ok(result) 475 | } 476 | Ok(Err(err)) => { 477 | debug!("successfully deserialized error result: {:?}", err); 478 | Err(err) 479 | } 480 | Err(err) => { 481 | let err = CallableExecuted(DeserializationFailed(err.to_string())); 482 | error!("failed to deserialize result, propagating {:?}", err); 483 | Err(err) 484 | } 485 | } 486 | } 487 | 488 | /// Doesn't matter if the value is an error or not, we just want to serialize it either way 489 | /// 490 | /// # Panics 491 | /// 492 | /// Panics if the serialization of a [`MemIsolateError`] fails 493 | #[cfg_attr(feature = "tracing", instrument(skip(result)))] 494 | fn serialize_result_or_error_value(result: T) -> Vec { 495 | match bincode::serialize(&Ok::(result)) { 496 | Ok(encoded) => { 497 | debug!( 498 | "serialization successful, resulted in {} bytes", 499 | encoded.len() 500 | ); 501 | encoded 502 | } 503 | Err(err) => { 504 | let err = CallableExecuted(SerializationFailed(err.to_string())); 505 | error!( 506 | "serialization failed, now attempting to serialize error: {:?}", 507 | err 508 | ); 509 | #[allow(clippy::let_and_return)] 510 | let encoded = bincode::serialize(&Err::(err)) 511 | .expect("failed to serialize error"); 512 | debug!( 513 | "serialization of error successful, resulting in {} bytes", 514 | encoded.len() 515 | ); 516 | encoded 517 | } 518 | } 519 | } 520 | 521 | #[cfg_attr(feature = "tracing", instrument)] 522 | fn write_and_flush_or_exit(sys: &S, writer: &mut W, buffer: &[u8]) 523 | where 524 | S: SystemFunctions, 525 | W: Write + Debug, 526 | { 527 | let result = writer.write_all(buffer).and_then(|()| writer.flush()); 528 | #[allow(unused_variables)] 529 | if let Err(err) = result { 530 | error!("error writing to pipe: {:?}", err); 531 | // If we can't write to the pipe, we can't communicate the error either 532 | // so we rely on the parent correctly interpreting the exit code 533 | let exit_code = CHILD_EXIT_IF_WRITE_FAILED; 534 | debug!("exiting child process with exit code: {}", exit_code); 535 | #[allow(clippy::used_underscore_items)] 536 | sys._exit(exit_code); 537 | } else { 538 | debug!("wrote and flushed to pipe successfully"); 539 | } 540 | } 541 | 542 | fn exit_happy(sys: &S) -> ! { 543 | // NOTE: We don't wrap this in #[cfg_attr(feature = "tracing", instrument)] 544 | // because doing so results in a compiler error because of the `!` return type 545 | // No idea why its usage is fine without the cfg_addr... 546 | #[cfg(feature = "tracing")] 547 | let _span = { 548 | const FN_NAME: &str = stringify!(exit_happy); 549 | span!(HIGHEST_LEVEL, FN_NAME).entered() 550 | }; 551 | 552 | let exit_code = CHILD_EXIT_HAPPY; 553 | debug!("exiting child process with exit code: {}", exit_code); 554 | 555 | #[allow(clippy::used_underscore_items)] 556 | sys._exit(exit_code); 557 | } 558 | 559 | #[cfg_attr(feature = "tracing", instrument)] 560 | fn read_all_of_child_result_pipe(read_fd: c_int) -> Result, MemIsolateError> { 561 | // Read from the pipe by wrapping the read fd as a File 562 | let mut buffer = Vec::new(); 563 | { 564 | let mut reader = unsafe { File::from_raw_fd(read_fd) }; 565 | if let Err(err) = reader.read_to_end(&mut buffer) { 566 | let err = CallableStatusUnknown(ParentPipeReadFailed(err)); 567 | error!("error reading from pipe, propagating {:?}", err); 568 | return Err(err); 569 | } 570 | } // The read_fd will automatically be closed when the File is dropped 571 | debug!("successfully read {} bytes from pipe", buffer.len()); 572 | Ok(buffer) 573 | } 574 | 575 | #[cfg_attr(feature = "tracing", instrument)] 576 | fn error_if_buffer_is_empty(buffer: &[u8]) -> Result<(), MemIsolateError> { 577 | if buffer.is_empty() { 578 | let err = CallableStatusUnknown(CallableProcessDiedDuringExecution); 579 | error!("buffer unexpectedly empty, propagating {:?}", err); 580 | return Err(err); 581 | } 582 | Ok(()) 583 | } 584 | 585 | #[cfg_attr(feature = "tracing", instrument)] 586 | fn close_write_end_of_pipe_in_parent( 587 | sys: &S, 588 | write_fd: c_int, 589 | ) -> Result<(), MemIsolateError> { 590 | if let Err(err) = sys.close(write_fd) { 591 | let err = CallableStatusUnknown(ParentPipeCloseFailed(err)); 592 | error!("error closing write end of pipe, propagating {:?}", err); 593 | return Err(err); 594 | } 595 | debug!("write end of pipe closed successfully"); 596 | Ok(()) 597 | } 598 | 599 | #[cfg_attr(feature = "tracing", instrument)] 600 | fn close_read_end_of_pipe_in_child_or_exit( 601 | sys: &S, 602 | writer: &mut (impl Write + Debug), 603 | read_fd: c_int, 604 | ) { 605 | if let Err(close_err) = sys.close(read_fd) { 606 | let err = CallableDidNotExecute(ChildPipeCloseFailed(Some(close_err))); 607 | error!( 608 | "error closing read end of pipe, now attempting to serialize error: {:?}", 609 | err 610 | ); 611 | 612 | let encoded = bincode::serialize(&err).expect("failed to serialize error"); 613 | writer 614 | .write_all(&encoded) 615 | .expect("failed to write error to pipe"); 616 | writer.flush().expect("failed to flush error to pipe"); 617 | 618 | let exit_code = CHILD_EXIT_IF_READ_CLOSE_FAILED; 619 | error!("exiting child process with exit code: {}", exit_code); 620 | #[allow(clippy::used_underscore_items)] 621 | sys._exit(exit_code); 622 | } else { 623 | debug!("read end of pipe closed successfully"); 624 | } 625 | } 626 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | //! Private macros module for conditional tracing 2 | //! 3 | //! This module provides macros that simplify conditional tracing by 4 | //! squashing the two-line pattern: 5 | //! 6 | //! ``` 7 | //! #[cfg(feature = "tracing")] 8 | //! tracing::debug!("message"); 9 | //! ``` 10 | //! 11 | //! Into a `debug!` macro call. 12 | //! 13 | //! These macros have the same names as the tracing crate's macros, 14 | //! but they are conditionally compiled based on the "tracing" feature. 15 | #![allow(unused_imports)] 16 | #![allow(unused_macros)] 17 | 18 | /// Conditionally emits a trace-level log message when the "tracing" feature is enabled. 19 | /// 20 | /// This macro does nothing when the "tracing" feature is disabled. 21 | #[macro_export] 22 | macro_rules! trace { 23 | ($($arg:tt)*) => { 24 | #[cfg(feature = "tracing")] 25 | tracing::trace!($($arg)*); 26 | }; 27 | } 28 | 29 | /// Conditionally emits a debug-level log message when the "tracing" feature is enabled. 30 | /// 31 | /// This macro does nothing when the "tracing" feature is disabled. 32 | macro_rules! debug { 33 | ($($arg:tt)*) => { 34 | #[cfg(feature = "tracing")] 35 | tracing::debug!($($arg)*); 36 | }; 37 | } 38 | 39 | /// Conditionally emits an info-level log message when the "tracing" feature is enabled. 40 | /// 41 | /// This macro does nothing when the "tracing" feature is disabled. 42 | macro_rules! info { 43 | ($($arg:tt)*) => { 44 | #[cfg(feature = "tracing")] 45 | tracing::info!($($arg)*); 46 | }; 47 | } 48 | 49 | /// Conditionally emits a warn-level log message when the "tracing" feature is enabled. 50 | /// 51 | /// This macro does nothing when the "tracing" feature is disabled. 52 | macro_rules! warning { 53 | ($($arg:tt)*) => { 54 | #[cfg(feature = "tracing")] 55 | tracing::warn!($($arg)*); 56 | }; 57 | } 58 | 59 | /// Conditionally emits an error-level log message when the "tracing" feature is enabled. 60 | /// 61 | /// This macro does nothing when the "tracing" feature is disabled. 62 | macro_rules! error { 63 | ($($arg:tt)*) => { 64 | #[cfg(feature = "tracing")] 65 | tracing::error!($($arg)*); 66 | }; 67 | } 68 | 69 | pub(crate) use {debug, error, info, trace, warning}; 70 | -------------------------------------------------------------------------------- /src/tests.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::unwrap_used)] 2 | 3 | use super::*; 4 | use crate::c::mock::{ 5 | CallBehavior, MockConfig, configured_strict, configured_with_fallback, is_mocking_enabled, 6 | with_mock_system, 7 | }; 8 | use errors::CallableDidNotExecuteError; 9 | use errors::CallableExecutedError; 10 | use errors::CallableStatusUnknownError; 11 | use rstest::*; 12 | use serde::{Deserialize, Serialize}; 13 | use std::error::Error; 14 | use std::fs; 15 | use std::io; 16 | use std::path::Path; 17 | use std::process; 18 | use std::sync::mpsc::channel; 19 | use std::thread; 20 | use std::time; 21 | use std::time::Duration; 22 | use tempfile::NamedTempFile; 23 | 24 | pub(crate) const TEST_TIMEOUT: Duration = Duration::from_secs(1); 25 | 26 | #[derive(Serialize, Deserialize, Debug, PartialEq)] 27 | struct MyResult { 28 | value: i32, 29 | } 30 | 31 | fn error_eio() -> io::Error { 32 | io::Error::from_raw_os_error(libc::EIO) 33 | } 34 | 35 | fn error_eagain() -> io::Error { 36 | io::Error::from_raw_os_error(libc::EAGAIN) 37 | } 38 | 39 | fn error_enfile() -> io::Error { 40 | io::Error::from_raw_os_error(libc::ENFILE) 41 | } 42 | 43 | fn error_eintr() -> io::Error { 44 | io::Error::from_raw_os_error(libc::EINTR) 45 | } 46 | 47 | fn write_pid_to_file(path: &Path) -> io::Result<()> { 48 | let pid: u32 = process::id(); 49 | fs::write(path, format!("{pid}\n")) 50 | } 51 | 52 | #[derive(Debug, PartialEq)] 53 | enum WaitForPidfileToPopulateResult { 54 | Success(i32), 55 | Timeout, 56 | Error(std::num::ParseIntError), 57 | } 58 | 59 | fn wait_for_pidfile_to_populate( 60 | path_with_eventual_pid: &Path, 61 | timeout: Duration, 62 | ) -> WaitForPidfileToPopulateResult { 63 | let start = time::Instant::now(); 64 | loop { 65 | if start.elapsed() >= timeout { 66 | return WaitForPidfileToPopulateResult::Timeout; 67 | } 68 | if let Ok(child_pid) = fs::read_to_string(path_with_eventual_pid) { 69 | if child_pid.ends_with('\n') { 70 | match child_pid.trim().parse::() { 71 | Ok(pid) => return WaitForPidfileToPopulateResult::Success(pid), 72 | Err(e) => return WaitForPidfileToPopulateResult::Error(e), 73 | } 74 | } 75 | } 76 | thread::sleep(Duration::from_millis(10)); 77 | } 78 | } 79 | 80 | #[cfg(feature = "tracing")] 81 | #[ctor::ctor] 82 | fn before_tests() { 83 | use tracing_subscriber::EnvFilter; 84 | let env_filter = EnvFilter::builder() 85 | .with_default_directive(Level::INFO.into()) 86 | .from_env_lossy(); 87 | tracing_subscriber::fmt().with_env_filter(env_filter).init(); 88 | } 89 | 90 | #[rstest] 91 | #[timeout(TEST_TIMEOUT)] 92 | fn simple_example() { 93 | let result = execute_in_isolated_process(|| MyResult { value: 42 }).unwrap(); 94 | assert_eq!(result, MyResult { value: 42 }); 95 | } 96 | 97 | #[rstest] 98 | #[timeout(TEST_TIMEOUT)] 99 | #[allow(static_mut_refs)] 100 | fn static_memory_mutation_without_isolation() { 101 | static mut MEMORY: bool = false; 102 | let mutate = || unsafe { MEMORY = true }; 103 | 104 | // Directly modify static memory 105 | mutate(); 106 | 107 | // Verify the change persists 108 | unsafe { 109 | assert!(MEMORY, "Static memory should be modified"); 110 | } 111 | } 112 | 113 | #[rstest] 114 | #[timeout(TEST_TIMEOUT)] 115 | #[allow(static_mut_refs)] 116 | fn static_memory_mutation_with_isolation() { 117 | static mut MEMORY: bool = false; 118 | let mutate = || unsafe { MEMORY = true }; 119 | 120 | // Modify static memory in isolated process 121 | execute_in_isolated_process(mutate).unwrap(); 122 | 123 | // Verify the change does not affect parent process 124 | unsafe { 125 | assert!( 126 | !MEMORY, 127 | "Static memory should remain unmodified in parent process" 128 | ); 129 | } 130 | } 131 | 132 | #[rstest] 133 | #[timeout(TEST_TIMEOUT)] 134 | fn isolate_memory_leak() { 135 | fn check_memory_exists_and_holds_vec_data(ptr_str: &str) -> bool { 136 | let addr = usize::from_str_radix(ptr_str.trim_start_matches("0x"), 16).unwrap(); 137 | let vec_ptr = addr as *const Vec; 138 | 139 | if vec_ptr.is_null() { 140 | return false; 141 | } 142 | 143 | // Use a recovery mechanism to safely check memory 144 | std::panic::catch_unwind(|| { 145 | // Safety: We're verifying if memory exists and has expected properties 146 | unsafe { 147 | let vec = &*vec_ptr; 148 | if vec.capacity() != 1024 || vec.len() != 1024 { 149 | return false; 150 | } 151 | if vec.first() != Some(&42) { 152 | return false; 153 | } 154 | // Memory exists and has the expected properties 155 | true 156 | } 157 | }) 158 | .unwrap_or(false) 159 | } 160 | 161 | let leaky_fn = || { 162 | // Leak 1KiB of memory 163 | let data: Vec = vec![42; 1024]; 164 | let data = Box::new(data); 165 | let uh_oh = Box::leak(data); 166 | let leaked_ptr = format!("{uh_oh:p}"); 167 | assert!( 168 | check_memory_exists_and_holds_vec_data(&leaked_ptr), 169 | "The memory should exist in `leaky_fn()` where it was leaked" 170 | ); 171 | leaked_ptr 172 | }; 173 | 174 | let leaked_ptr: String = execute_in_isolated_process(leaky_fn).unwrap(); 175 | assert!( 176 | !check_memory_exists_and_holds_vec_data(&leaked_ptr), 177 | "The leaked memory doesn't exist out here though" 178 | ); 179 | } 180 | 181 | #[rstest] 182 | #[timeout(TEST_TIMEOUT)] 183 | fn all_function_types() { 184 | // 1. Function pointer (simplest, most explicit) 185 | fn function_pointer() -> MyResult { 186 | MyResult { value: 42 } 187 | } 188 | let result = execute_in_isolated_process(function_pointer).unwrap(); 189 | assert_eq!(result, MyResult { value: 42 }); 190 | 191 | // 2. Fn closure (immutable captures, can be called multiple times) 192 | let fn_closure = || MyResult { value: 42 }; 193 | let result = execute_in_isolated_process(fn_closure).unwrap(); 194 | assert_eq!(result, MyResult { value: 42 }); 195 | 196 | // 3. FnMut closure (mutable captures, can be called multiple times) 197 | let mut counter = 0; 198 | let fn_mut_closure = || { 199 | counter += 1; 200 | MyResult { value: counter } 201 | }; 202 | let result = execute_in_isolated_process(fn_mut_closure).unwrap(); 203 | assert_eq!(result, MyResult { value: 1 }); 204 | // WARNING: This zero is a surprising result if you don't understand that 205 | // the closure is called in a new process. This is the whole point of mem-isolate. 206 | assert_eq!(counter, 0); 207 | 208 | // 4. FnOnce closure (consumes captures, can only be called once) 209 | let value = String::from("hello"); 210 | let fn_once_closure = move || { 211 | // This closure takes ownership of value 212 | MyResult { 213 | #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] 214 | value: value.len() as i32, 215 | } 216 | }; 217 | let result = execute_in_isolated_process(fn_once_closure).unwrap(); 218 | assert_eq!(result, MyResult { value: 5 }); 219 | } 220 | 221 | #[rstest] 222 | #[timeout(TEST_TIMEOUT)] 223 | fn handle_large_result() { 224 | let initial_string = "The quick brown fox jumps over the lazy dog"; 225 | let result = execute_in_isolated_process(|| initial_string.repeat(10000)).unwrap(); 226 | assert_eq!(result.len(), 10000 * initial_string.len()); 227 | } 228 | 229 | #[rstest] 230 | #[timeout(TEST_TIMEOUT)] 231 | fn panic_in_child() { 232 | #[allow(clippy::semicolon_if_nothing_returned)] 233 | let error = execute_in_isolated_process(|| { 234 | // TODO: Figure out why this casues the child process to exit happily on its own 235 | // if buffer.is_empty() check is removed (resulting in a deserialization error) 236 | // Shouldn't the waitpid check result in noticing that the child panicked? 237 | panic!("Panic in child"); 238 | #[allow(clippy::unused_unit)] 239 | #[allow(unreachable_code)] 240 | () 241 | }) 242 | .unwrap_err(); 243 | eprintln!("error: {error:?}"); 244 | if cfg!(target_os = "macos") { 245 | assert!(matches!( 246 | error, 247 | MemIsolateError::CallableStatusUnknown( 248 | CallableStatusUnknownError::ChildProcessKilledBySignal(5), 249 | ) 250 | )); 251 | } else { 252 | assert!(matches!( 253 | error, 254 | MemIsolateError::CallableStatusUnknown( 255 | CallableStatusUnknownError::CallableProcessDiedDuringExecution, 256 | ) 257 | )); 258 | } 259 | } 260 | #[rstest] 261 | #[timeout(TEST_TIMEOUT)] 262 | #[allow(clippy::unit_cmp)] 263 | fn empty_result() { 264 | assert_eq!(execute_in_isolated_process(|| {}).unwrap(), ()); 265 | } 266 | 267 | #[rstest] 268 | #[timeout(TEST_TIMEOUT)] 269 | fn serialization_error() { 270 | // Custom type that implements Serialize but fails during serialization 271 | #[derive(Debug)] 272 | struct CustomIteratorWrapper { 273 | _data: Vec, 274 | } 275 | 276 | impl Serialize for CustomIteratorWrapper { 277 | fn serialize(&self, _serializer: S) -> Result 278 | where 279 | S: serde::Serializer, 280 | { 281 | Err(serde::ser::Error::custom("Fake serialization error")) 282 | } 283 | } 284 | 285 | impl<'de> Deserialize<'de> for CustomIteratorWrapper { 286 | fn deserialize(_deserializer: D) -> Result 287 | where 288 | D: serde::Deserializer<'de>, 289 | { 290 | Ok(CustomIteratorWrapper { _data: vec![] }) 291 | } 292 | } 293 | 294 | // Use our function with a closure that returns the problematic type 295 | let result = execute_in_isolated_process(|| CustomIteratorWrapper { 296 | _data: vec![1, 2, 3], 297 | }); 298 | 299 | // Verify we get the expected serialization error 300 | match result { 301 | Err(MemIsolateError::CallableExecuted(CallableExecutedError::SerializationFailed(err))) => { 302 | assert!( 303 | err.contains("Fake serialization error"), 304 | "Expected error about sequence length, got: {err}" 305 | ); 306 | } 307 | other => panic!("Expected SerializationFailed error, got: {other:?}"), 308 | } 309 | } 310 | 311 | #[rstest] 312 | #[timeout(TEST_TIMEOUT)] 313 | fn deserialization_error() { 314 | // Custom type that successfully serializes but fails during deserialization 315 | #[derive(Debug, PartialEq)] 316 | struct DeserializationFailer { 317 | data: i32, 318 | } 319 | 320 | impl Serialize for DeserializationFailer { 321 | fn serialize(&self, serializer: S) -> Result 322 | where 323 | S: serde::Serializer, 324 | { 325 | // Successfully serialize as a simple integer 326 | self.data.serialize(serializer) 327 | } 328 | } 329 | 330 | impl<'de> Deserialize<'de> for DeserializationFailer { 331 | fn deserialize(_deserializer: D) -> Result 332 | where 333 | D: serde::Deserializer<'de>, 334 | { 335 | // Always fail deserialization with a custom error 336 | Err(serde::de::Error::custom( 337 | "Intentional deserialization failure", 338 | )) 339 | } 340 | } 341 | 342 | // Use our function with a closure that returns the problematic type 343 | let result = execute_in_isolated_process(|| DeserializationFailer { data: 42 }); 344 | 345 | // Verify we get the expected deserialization error 346 | match result { 347 | Err(MemIsolateError::CallableExecuted(CallableExecutedError::DeserializationFailed( 348 | err, 349 | ))) => { 350 | assert!( 351 | err.contains("Intentional deserialization failure"), 352 | "Expected custom deserialization error, got: {err}" 353 | ); 354 | } 355 | other => panic!("Expected DeserializationFailed error, got: {other:?}"), 356 | } 357 | } 358 | 359 | #[rstest] 360 | #[timeout(TEST_TIMEOUT)] 361 | fn with_mock_helper() { 362 | with_mock_system(MockConfig::Fallback, |_| { 363 | // Test with active mocking 364 | // Check that mocking is properly configured 365 | assert!(is_mocking_enabled()); 366 | 367 | // Test code that uses mocked functions 368 | let result = execute_in_isolated_process(|| MyResult { value: 42 }).unwrap(); 369 | assert_eq!(result, MyResult { value: 42 }); 370 | }); 371 | 372 | // After with_mock_system, mocking is disabled automatically 373 | assert!(!is_mocking_enabled()); 374 | } 375 | 376 | #[rstest] 377 | #[timeout(TEST_TIMEOUT)] 378 | fn pipe_error() { 379 | with_mock_system( 380 | // Pipe creation is the first syscall in execute_in_isolated_process so we can afford 381 | // to use strict mode here. 382 | configured_strict(|mock| { 383 | let pipe_creation_error = error_enfile(); 384 | mock.expect_pipe(CallBehavior::Mock(Err(pipe_creation_error))); 385 | }), 386 | |_| { 387 | let result = execute_in_isolated_process(|| MyResult { value: 42 }); 388 | let err = result.unwrap_err(); 389 | matches!( 390 | err, 391 | MemIsolateError::CallableDidNotExecute( 392 | CallableDidNotExecuteError::PipeCreationFailed(_) 393 | ) 394 | ); 395 | 396 | let pipe_creation_error = error_enfile(); 397 | assert_eq!( 398 | err.source().unwrap().source().unwrap().to_string(), 399 | pipe_creation_error.to_string() 400 | ); 401 | }, 402 | ); 403 | } 404 | 405 | #[rstest] 406 | #[timeout(TEST_TIMEOUT)] 407 | fn fork_error() { 408 | with_mock_system( 409 | configured_with_fallback(|mock| { 410 | let fork_error = error_eagain(); 411 | mock.expect_fork(CallBehavior::Mock(Err(fork_error))); 412 | }), 413 | |_| { 414 | let result = execute_in_isolated_process(|| MyResult { value: 42 }); 415 | let err = result.unwrap_err(); 416 | matches!( 417 | err, 418 | MemIsolateError::CallableDidNotExecute(CallableDidNotExecuteError::ForkFailed(_)) 419 | ); 420 | 421 | let fork_error = error_eagain(); 422 | assert_eq!( 423 | err.source().unwrap().source().unwrap().to_string(), 424 | fork_error.to_string() 425 | ); 426 | }, 427 | ); 428 | } 429 | 430 | #[rstest] 431 | #[timeout(TEST_TIMEOUT)] 432 | fn parent_pipe_close_failure() { 433 | with_mock_system( 434 | configured_with_fallback(|mock| { 435 | // Let pipe() and fork() succeed normally 436 | // But make the first call to close() fail 437 | // This will affect the parent's attempt to close the write_fd 438 | let close_error = error_eio(); // I/O error 439 | mock.expect_close(CallBehavior::Mock(Err(close_error))); 440 | }), 441 | |_| { 442 | let result = execute_in_isolated_process(|| MyResult { value: 42 }); 443 | 444 | match result { 445 | Err(MemIsolateError::CallableStatusUnknown( 446 | CallableStatusUnknownError::ParentPipeCloseFailed(err), 447 | )) => { 448 | // Verify the error matches what we configured 449 | let expected_error = error_eio(); 450 | assert_eq!(err.kind(), expected_error.kind()); 451 | assert_eq!(err.raw_os_error(), expected_error.raw_os_error()); 452 | } 453 | other => panic!("Expected ParentPipeCloseFailed error, got: {other:?}"), 454 | } 455 | }, 456 | ); 457 | } 458 | 459 | // // TODO: Come back and fix this test. 460 | // #[rstest] 461 | // #[timeout(TEST_TIMEOUT)] 462 | // fn parent_pipe_reader_invalid() { 463 | // use crate::c::PipeFds; 464 | 465 | // fn error_ebadf() -> io::Error { 466 | // io::Error::from_raw_os_error(libc::EBADF) 467 | // } 468 | 469 | // // Create a real pipe, but only 470 | 471 | // with_mock_system( 472 | // configured_with_fallback(move |mock| { 473 | // let invalid_read_fd = -100; // If this is a -1 we get another problem 474 | 475 | // let sys = c::RealSystemFunctions; 476 | // let PipeFds { 477 | // read_fd: real_read_fd, 478 | // write_fd: real_write_fd, 479 | // } = sys.pipe().expect("pipe should succeed"); 480 | 481 | // // Close the real read_fd since we're replacing it with an invalid one 482 | // sys.close(real_read_fd) 483 | // .expect("closing real read_fd should succeed"); 484 | 485 | // mock.expect_pipe(CallBehavior::Mock(Ok(PipeFds { 486 | // read_fd: invalid_read_fd, 487 | // write_fd: real_write_fd, 488 | // }))); 489 | // }), 490 | // |_| { 491 | // let result = execute_in_isolated_process(|| MyResult { value: 42 }); 492 | 493 | // match result { 494 | // Err(MemIsolateError::CallableStatusUnknown( 495 | // CallableStatusUnknownError::ParentPipeReadFailed(err), 496 | // )) => { 497 | // // Verify the error matches what we configured 498 | // let expected_error = error_ebadf(); 499 | // assert_eq!(err.kind(), expected_error.kind(), "1"); 500 | // assert_eq!(err.raw_os_error(), expected_error.raw_os_error(), "2"); 501 | // } 502 | // other => panic!("Expected ParentPipeReadFailed error, got: {:?}", other), 503 | // } 504 | // }, 505 | // ); 506 | // } 507 | 508 | #[rstest] 509 | #[timeout(TEST_TIMEOUT)] 510 | fn waitpid_child_process_exited_on_its_own() { 511 | // The default case 512 | execute_in_isolated_process(|| {}).unwrap(); 513 | } 514 | 515 | #[rstest] 516 | #[timeout(Duration::from_secs(3))] 517 | #[allow(clippy::semicolon_if_nothing_returned)] 518 | fn waitpid_child_killed_by_signal() { 519 | let tmp_file = NamedTempFile::new().expect("Failed to create temp file"); 520 | let tmp_path_clone = tmp_file.path().to_path_buf().clone(); 521 | 522 | let callable = move || { 523 | write_pid_to_file(&tmp_path_clone).expect("Failed to write pid to temp file"); 524 | // Wait for SIGTERM by parking the thread 525 | loop { 526 | thread::park(); 527 | } 528 | #[allow(unreachable_code)] 529 | () 530 | }; 531 | 532 | let (tx, rx) = channel(); 533 | thread::spawn(move || { 534 | let result = execute_in_isolated_process(callable); 535 | tx.send(result) 536 | }); 537 | 538 | let timeout = Duration::from_secs(2); 539 | if let WaitForPidfileToPopulateResult::Success(child_pid) = 540 | wait_for_pidfile_to_populate(tmp_file.path(), timeout) 541 | { 542 | // SIGTERM the child 543 | unsafe { 544 | libc::kill(child_pid, libc::SIGTERM); 545 | } 546 | } else { 547 | panic!("Failed to retrieve child pid from temp file"); 548 | } 549 | 550 | let result = rx.recv().unwrap(); 551 | assert!(matches!( 552 | result, 553 | Err(MemIsolateError::CallableStatusUnknown( 554 | CallableStatusUnknownError::ChildProcessKilledBySignal(libc::SIGTERM) 555 | )) 556 | )); 557 | } 558 | 559 | #[rstest] 560 | #[timeout(Duration::from_secs(3))] 561 | #[allow(clippy::semicolon_if_nothing_returned)] 562 | fn waitpid_child_killed_by_signal_after_suspension_and_continuation() { 563 | let tmp_file = NamedTempFile::new().expect("Failed to create temp file"); 564 | let tmp_path_clone = tmp_file.path().to_path_buf().clone(); 565 | 566 | let callable = move || { 567 | write_pid_to_file(&tmp_path_clone).expect("Failed to write pid to temp file"); 568 | // Wait for SIGTERM by parking the thread 569 | loop { 570 | thread::park(); 571 | } 572 | #[allow(unreachable_code)] 573 | () 574 | }; 575 | 576 | let (tx, rx) = channel(); 577 | thread::spawn(move || { 578 | let result = execute_in_isolated_process(callable); 579 | tx.send(result) 580 | }); 581 | 582 | let timeout = Duration::from_secs(2); 583 | if let WaitForPidfileToPopulateResult::Success(child_pid) = 584 | wait_for_pidfile_to_populate(tmp_file.path(), timeout) 585 | { 586 | unsafe { 587 | libc::kill(child_pid, libc::SIGSTOP); 588 | } 589 | 590 | thread::sleep(Duration::from_millis(100)); 591 | unsafe { 592 | libc::kill(child_pid, libc::SIGCONT); 593 | } 594 | 595 | thread::sleep(Duration::from_millis(100)); 596 | // No reason to choose SIGKILL here other than we already tested SIGTERM 597 | unsafe { 598 | libc::kill(child_pid, libc::SIGKILL); 599 | } 600 | } else { 601 | panic!("Failed to retrieve child pid from temp file"); 602 | } 603 | 604 | let result = rx.recv().unwrap(); 605 | assert!(matches!( 606 | result, 607 | Err(MemIsolateError::CallableStatusUnknown( 608 | CallableStatusUnknownError::ChildProcessKilledBySignal(libc::SIGKILL) 609 | )) 610 | )); 611 | } 612 | 613 | #[rstest] 614 | #[timeout(TEST_TIMEOUT)] 615 | fn waitpid_interrupted_by_signal_mock() { 616 | with_mock_system( 617 | configured_with_fallback(|mock| { 618 | // waitpid() will return EINTR if a signal is delivered to the parent, 619 | // we want to continue in this case. Here we mock the first three calls to 620 | // waitpid() returning EINTR, then the fourth call will fallback to the 621 | // real syscall. 622 | mock.expect_waitpid(CallBehavior::Mock(Err(error_eintr()))); 623 | mock.expect_waitpid(CallBehavior::Mock(Err(error_eintr()))); 624 | mock.expect_waitpid(CallBehavior::Mock(Err(error_eintr()))); 625 | }), 626 | |_| { 627 | let result = execute_in_isolated_process(|| MyResult { value: 42 }); 628 | assert_eq!(result.unwrap(), MyResult { value: 42 }); 629 | }, 630 | ); 631 | } 632 | --------------------------------------------------------------------------------