{"id":33919444,"url":"https://github.com/corrode/tarnish","last_synced_at":"2025-12-30T16:27:44.817Z","repository":{"id":325545027,"uuid":"1101473701","full_name":"corrode/tarnish","owner":"corrode","description":"Sandbox untrusted Rust code with isolated processes","archived":false,"fork":false,"pushed_at":"2025-11-22T00:01:03.000Z","size":34,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-11-22T00:24:50.411Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/corrode.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-11-21T18:14:38.000Z","updated_at":"2025-11-22T00:01:06.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/corrode/tarnish","commit_stats":null,"previous_names":["corrode/tarnish"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/corrode/tarnish","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/corrode%2Ftarnish","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/corrode%2Ftarnish/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/corrode%2Ftarnish/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/corrode%2Ftarnish/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/corrode","download_url":"https://codeload.github.com/corrode/tarnish/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/corrode%2Ftarnish/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":27679672,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-12-12T02:00:06.775Z","response_time":129,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-12-12T08:51:35.833Z","updated_at":"2025-12-12T08:51:36.542Z","avatar_url":"https://github.com/corrode.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# tarnish\n\nA library for isolating crash-prone code in separate processes with automatic\nrecovery.\n\n## Why?\n\nSometimes you need to run code that might crash your process. Maybe you're\ncalling into a C library through FFI, and somewhere in that library there's a\nnull pointer dereference waiting to happen. Or you're using a third-party\nsys-crate with brittle unsafe code. Or you're experimenting with code that\npanics unpredictably.\n\nRust's type system can't protect you from segfaults in C code. It can't prevent\nan `abort()` call in a dependency. When those things happen, the entire process\nterminates.\n\nThis library provides **crash isolation**: the fragile code runs in a separate\nprocess. If it crashes, the parent process survives and can restart the worker.\n\nThis library was born out of necessity to wrap a brittle FFI binding that would\noccasionally segfault. That specific use case works well. I haven't extensively\ntested it beyond that, so proceed with appropriate caution for your use case.\n\n## Features\n\n- **Crash isolation**: Process-level isolation survives segfaults, panics, and aborts\n- **Automatic recovery**: Workers automatically restart after crashes\n- **Type-safe messaging**: Built-in serialization for process communication\n- **Trait-based API**: Simple, composable design\n- **Cross-platform**: Works anywhere Rust can spawn processes\n- **General-purpose**: Not limited to FFI use cases\n\n## Differences to  `std::panic::catch_unwind`\n\n[`std::panic::catch_unwind`](https://doc.rust-lang.org/std/panic/fn.catch_unwind.html) can only catch Rust panics that use unwinding. \n\n| Feature                     | `catch_unwind`                | `tarnish`                            |\n|-----------------------------|-------------------------------|--------------------------------------|\n| **Isolation Level**         | Thread-level (same process)   | Process-level (separate process)     |\n| **Survives segfaults**      | ❌ No                          | ✅ Yes                               |\n| **Survives C/FFI crashes**  | ❌ No                          | ✅ Yes                               |\n| **Survives `abort()`**      | ❌ No                          | ✅ Yes                               |\n| **Survives panic-abort**    | ❌ No                          | ✅ Yes                               |\n| **Survives stack overflow** | ❌ No*                         | ✅ Yes                               |\n| **Performance**             | Very fast (nanoseconds)       | Slower (process spawn + IPC)         |\n| **Memory overhead**         | Minimal                       | Separate process (~few MB)           |\n| **Trait bounds**            | Requires `UnwindSafe`         | Requires `Serialize` + `Deserialize` |\n| **Data sharing**            | Direct (same address space)   | Serialized (across process boundary) |\n\nThe failures get handled on different levels.\nWhile `catch_unwind` catches panics within the same process, `tarnish` isolates code in a separate process to survive ANY crash.\n\nHere are some examples of what `catch_unwind` *cannot* handle:\n\n```rust,ignore\nuse std::panic;\n\n// ❌ Segfault from unsafe code\npanic::catch_unwind(|| unsafe {\n    let ptr = std::ptr::null_mut::\u003ci32\u003e();\n    *ptr = 42; // SEGFAULT - entire process dies\n});\n\n// ❌ C library crash\npanic::catch_unwind(|| unsafe {\n    libc::abort(); // Terminates entire process\n});\n\n// ❌ Panic with -C panic=abort\n// (Terminates entire process)\n\n// ❌ Stack overflow (usually)\n// (May or may not be caught)\n```\n\nThe cost of `tarnish` is higher latency and memory usage due to process spawning\nand inter-process communication. Use `catch_unwind` for pure Rust code, and\n`tarnish` when calling unsafe FFI or when you need absolute crash isolation. \n\n## How It Works \n\nYou implement a `Task` trait that encapsulates your risky business logic. When\nyou spawn a worker, the library creates a fresh copy of your own binary, but\nwith a special environment variable set. Your `main()` function checks for this\nvariable at startup. If it's there, you know you're the worker subprocess, and\nyou should run the worker loop. If it's not there, you're the parent, and you\ncan spawn workers as needed. \n\nThis is conceptually similar to the classic fork pattern on Unix systems, but it\nworks on any platform that can spawn processes. The parent and worker\ncommunicate over `stdin` and `stdout`, using messages which get serialized with\n[postcard](https://github.com/jamesmunns/postcard), then base64 encoded so they\nplay nice with text streams. The concrete messaging format is an implementation\ndetail that you should not rely on, as it can change in future versions.\n\nSerialization and deserialization happens automatically, so contrary to the Unix\nfork-exec model, you only need to worry about the business logic.\n\nWhen a worker panics or crashes, the parent notices immediately. It spawns a\nfresh worker and retries the operation. If the crash was transient (cosmic ray,\nmemory pressure, who knows), the retry succeeds. If it was deterministic (i.e.,\nthat input will always crash), the retry fails too, and you get an error back.\nEither way, your parent process keeps running. \n\n## Task Trait\n\nThe task trait looks like this:\n\n```rust,ignore\npub trait Task: Default + 'static {\n    type Input: Serialize + Deserialize;\n    type Output: Serialize + Deserialize;\n    type Error: Display;\n\n    fn run(\u0026mut self, input: Self::Input) -\u003e Result\u003cSelf::Output, Self::Error\u003e;\n}\n```\n\nYour input and output types need to derive `Serialize` and `Deserialize`;\neverything else happens behind the scenes. You can also use types from the\nstandard library like `String` for both input and output if that's all you need.\n(There is a blanket implementation for those.)\n\n## Inline Tasks Using the `task!` Macro\n\nFor simple cases, you can use the `task!` macro instead of manually implementing the `Task` trait:\n\n```rust,no_run\nuse tarnish::task;\n\nfn main() {\n    // Simple task with explicit return type\n    let result = task!(calculate: || -\u003e Result\u003ci32, String\u003e {\n        // This code runs in an isolated process\n        Ok(42)\n    });\n\n    // Task with default return type (tarnish::Result\u003c()\u003e)\n    let result = task!(simple: || {\n        // Do something that might crash\n        Ok(())\n    });\n}\n```\n\nThe macro automatically generates the `Task` implementation and handles process spawning. Each task runs in its own isolated process, so if it crashes, your main process survives.\n\nThe label (`calculate`, `simple`) is required to generate a unique type for each task. When a subprocess spawns, it uses this label to identify which task it should run. Each `task!` call in your binary needs a unique label. If this turns out to be a limitation, please open an issue.\n\nThe macro is perfect for quick isolation of crash-prone code blocks without the ceremony of defining a full `Task` struct.\nUse the full `Task` trait when you need persistent workers that handle multiple requests, maintain state between calls, or when you want more control over the lifecycle.\n\n## Example Use-Case: Wrapping Crash-Prone FFI\n\nThe original use-case is isolating FFI calls that might crash, so let's look at\nan example in more detail. \n\n```rust,no_run\nuse tarnish::{Task, Process};\nuse serde::{Serialize, Deserialize};\n\n#[derive(Default)]\nstruct UnsafeFFIWrapper;\n\n// Define your input/output types\n\n#[derive(Serialize, Deserialize)]\nstruct Input {\n    operation: String,\n    data: Vec\u003cu8\u003e,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\nstruct Output {\n    success: bool,\n    data: Vec\u003cu8\u003e,\n}\n\nimpl Task for UnsafeFFIWrapper {\n    type Input = Input;\n    type Output = Output;\n    type Error = String;\n\n    fn run(\u0026mut self, input: Input) -\u003e Result\u003cOutput, String\u003e {\n        // This unsafe block might segfault!\n        // If it does, only this worker process dies, not the parent.\n        unsafe {\n            let result = some_unsafe_c_function(\n                input.data.as_ptr(),\n                input.data.len()\n            );\n\n            if result.is_null() {\n                return Err(\"C function returned null\".to_string());\n            }\n\n            // Process the result...\n            Ok(Output {\n                success: true,\n                data: vec![],\n            })\n        }\n    }\n}\n\nfn main() {\n    tarnish::main::\u003cUnsafeFFIWrapper\u003e(parent_main);\n}\n\nfn parent_main() {\n    let mut process = Process::\u003cUnsafeFFIWrapper\u003e::spawn()\n        .expect(\"Failed to spawn worker\");\n\n    let input = Input {\n        operation: \"transform\".to_string(),\n        data: vec![1, 2, 3, 4],\n    };\n\n    match process.call(input) {\n        Ok(output) =\u003e {\n            println!(\"FFI call succeeded: {:?}\", output);\n        }\n        Err(e) =\u003e {\n            // If the C code segfaulted, we get an error here,\n            // but the parent process is still running\n            eprintln!(\"Worker crashed or returned error: {}\", e);\n        }\n    }\n}\n\n// Your unsafe FFI declaration\nunsafe extern \"C\" {\n    fn some_unsafe_c_function(data: *const u8, len: usize) -\u003e *mut std::ffi::c_void;\n}\n```\n\nNote how `main` just calls `tarnish::main()` with the parent logic function.\nThis handles the check for parent-vs-task context.\n\n## Process Pools\n\nFor concurrent task execution we offer a `ProcessPool` to manage multiple worker processes.\nThis is useful in situations where your tasks are CPU-bound, which is a common problem with `*-sys` crates.\n\n```rust,no_run\nuse std::num::NonZeroUsize;\nuse tarnish::{Task, ProcessPool};\n\n#[derive(Default)]\nstruct HeavyComputation;\n\nimpl Task for HeavyComputation {\n    type Input = Vec\u003cu8\u003e;\n    type Output = u64;\n    type Error = String;\n\n    fn run(\u0026mut self, input: Vec\u003cu8\u003e) -\u003e Result\u003cu64, String\u003e {\n        // Do the expensive computation\n        Ok(input.iter().map(|\u0026x| x as u64).sum())\n    }\n}\n\nfn main() {\n    tarnish::main::\u003cHeavyComputation\u003e(|| {\n        let size = NonZeroUsize::new(4).unwrap();\n        let mut pool = ProcessPool::\u003cHeavyComputation\u003e::new(size)\n            .expect(\"Failed to create pool\");\n\n        // Process tasks across 4 workers\n        for i in 0..100 {\n            let result = pool.call(vec![i; 1000]);\n            println!(\"Result: {:?}\", result);\n        }\n    });\n}\n```\n\nEach pool provides a set of guarantees:\n- Uses round-robin scheduling to distribute work\n- Automatically restarts crashed workers, just like `Process` does\n- Each worker maintains its own isolated process memory\n- Workers persist between calls for efficiency\n\n## Shutdown\n\nWhen you drop a `Process` handle, it sends a shutdown message to the task and\nwaits for up to 5 seconds. If the task doesn't exit cleanly, it gets a `SIGKILL`.\n\n## When Tasks Crash\n\nWhen a task crashes mid-operation, `process.call()` automatically restarts the\ntask and returns an error. The fresh task is ready for the next call.\n\nYou have to retry the operation yourself. \n\n```rust,ignore\n// Try once, retry on failure\nlet result = process.call(input.clone())\n    .or_else(|_| process.call(input));\n```\n\nOr implement more sophisticated retry logic with backoff, limits, etc. The\nlibrary handles keeping a fresh task available, you decide when to retry.\n\n## Serialization format\n\nMessages are serialized with [postcard](https://github.com/jamesmunns/postcard)\nusing [COBS encoding](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing).\nPostcard is a compact binary format (~10-20% the size of JSON), and COBS adds\nonly ~0.4% overhead while providing natural frame delimiters.\n\n**How it works**: COBS encoding transforms binary data to never contain 0x00\nbytes, which we then use as message delimiters.\n\nThis is an implementation detail, however, and may change in future versions.\n\nIf you really don't want the serde dependency, you can disable the default\nfeatures and implement `MessageEncode` and `MessageDecode` manually for your\ntypes. But honestly, you probably want serde.\n\n## Limitations\n\n\u003e [!WARNING]\n\u003e This library provides **crash isolation**, not **security isolation**. It protects\n\u003e your parent process from crashes (segfaults, panics, aborts), but does NOT sandbox\n\u003e malicious code. Worker processes have full access to the filesystem, network, and\n\u003e other system resources. Do not use this to run untrusted code.\n\n**Platform support**: Tested on macOS. Probably works on other\nUnix-like systems. Windows support would require work around process spawning\nand signal handling.\n\n**Requirements**: Tasks must implement `Default` (for spawning fresh workers)\nand be `'static` (no borrowed data across process boundaries).\n\n## Similar Libraries\n\n- **[Sandcrust](https://www.researchgate.net/publication/320748351_Sandcrust_Automatic_Sandboxing_of_Unsafe_Components_in_Rust)** - Academic research project for automatic sandboxing of unsafe components\n- **[rusty-fork](https://crates.io/crates/rusty-fork)** - Process isolation for tests ([GitHub](https://github.com/AltSysrq/rusty-fork))\n- **[Bastion](https://lib.rs/crates/bastion)** - Fault-tolerant runtime with actor-model supervision\n- **[rust_supervisor](https://crates.io/crates/rust_supervisor)** - Erlang/OTP-style supervision for Rust threads\n- **[subprocess](https://crates.io/crates/subprocess)** - Execution and interaction with external processes ([GitHub](https://github.com/hniksic/rust-subprocess))\n- **[async-process](https://crates.io/crates/async-process)** - Asynchronous process handling\n\n| Feature | tarnish | Sandcrust | rusty-fork | Bastion | rust_supervisor | subprocess/async-process |\n|---------|---------|-----------|------------|---------|-----------------|--------------------------|\n| Process isolation | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ |\n| Automatic restart | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ |\n| Survives segfaults | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ |\n| Production ready | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ |\n| Built-in IPC | ✓ | ✓ | ✗ | ✓ | ✗ | ✗ |\n| Trait-based API | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ |\n| FFI focus | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ |\n| External commands | ✓ | ✗ | ✗ | ✗ | ✗ | ✓ |\n| Active maintenance | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ |\n\n## About The Name\n\n[Tarnish](https://en.wikipedia.org/wiki/Tarnish) is the protective layer that\nforms on metal when it's exposed to air. It looks like damage, but it's actually\nprotecting the metal underneath from further corrosion. When you scratch it off,\nit just grows back.\n\nI think you now understand where I'm going with this. This library is similar.\nThe worker process is \"the tarnish.\" It takes the hits so your main process\ndoesn't have to. When it gets damaged, we regenerate it. The protection\ncontinues. This and the Rust pun.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcorrode%2Ftarnish","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcorrode%2Ftarnish","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcorrode%2Ftarnish/lists"}