diff --git a/.envrc b/.envrc index ff5954f..8392d15 100644 --- a/.envrc +++ b/.envrc @@ -1 +1 @@ -use flake . --impure \ No newline at end of file +use flake \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..03197b2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Nix + uses: cachix/install-nix-action@v24 + with: + extra_nix_config: | + experimental-features = nix-command flakes + + - name: Check flake + run: nix flake check + + - name: Format check + run: nix develop -c cargo fmt -- --check + + - name: Clippy + run: nix develop -c cargo clippy --all-targets --all-features -- -D warnings + + - name: Test + run: nix develop -c cargo test --workspace + + - name: Build + run: nix build .#leeward-x86_64 + + - name: Security audit + run: nix develop -c cargo audit \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d3d8391 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,112 @@ +name: Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + tag: + description: 'Release tag (e.g., v0.1.0)' + required: true + type: string + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Nix + uses: cachix/install-nix-action@v24 + with: + extra_nix_config: | + experimental-features = nix-command flakes + accept-flake-config = true + + - name: Setup Cachix + uses: cachix/cachix-action@v14 + with: + name: leeward + skipPush: true + + - name: Build all packages + run: | + # Build all outputs + nix build .#leeward-x86_64 -L + nix build .#leeward-aarch64 -L + nix build .#leeward-static -L + nix build .#leeward-deb -L + + # Copy artifacts + mkdir -p artifacts + cp -L result*/* artifacts/ || true + + - name: Create release archives + run: | + cd artifacts + + # Create tarballs for each architecture + for arch in x86_64 aarch64; do + if [ -d "leeward-$arch" ]; then + tar czf "leeward-${arch}-linux.tar.gz" "leeward-$arch" + fi + done + + # Create checksums + sha256sum *.tar.gz *.deb > checksums.txt || true + + cd .. + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: release-artifacts + path: artifacts/* + + release: + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: release-artifacts + path: artifacts + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ github.event.inputs.tag || github.ref_name }} + name: Release ${{ github.event.inputs.tag || github.ref_name }} + draft: false + prerelease: false + files: | + artifacts/*.tar.gz + artifacts/*.deb + artifacts/checksums.txt + body: | + ## Installation + + ### Debian/Ubuntu + ```bash + wget https://github.com/vektia/leeward/releases/latest/download/leeward_*_amd64.deb + sudo dpkg -i leeward_*.deb + ``` + + ### Binary (x86_64) + ```bash + curl -L https://github.com/vektia/leeward/releases/latest/download/leeward-x86_64-linux.tar.gz | tar xz + sudo mv leeward-x86_64/* /usr/local/bin/ + ``` + + ### Nix + ```nix + nix run github:vektia/leeward/${{ github.event.inputs.tag || github.ref_name }} + ``` + + See [CHANGELOG.md](https://github.com/vektia/leeward/blob/main/CHANGELOG.md) for changes. \ No newline at end of file diff --git a/.gitignore b/.gitignore index e07339f..d10f70d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +# Python __pycache__/ .pytest_cache/ .ruff_cache @@ -9,19 +10,22 @@ __pycache__/ dist/ build/ *.egg-info/ +*.whl +# Rust target/ -Cargo.lock - -*.whl *.so +# Development tools .direnv/ .devenv/ .pre-commit-config.yaml - -/target - .cargo-home/ -include/ \ No newline at end of file +# Nix build results +result +result-* + +# Runtime files +*.sock +.leeward.sock \ No newline at end of file diff --git a/ADOPTION.md b/ADOPTION.md deleted file mode 100644 index 7a1d47e..0000000 --- a/ADOPTION.md +++ /dev/null @@ -1,24 +0,0 @@ -# Using leeward? - -We'd love to hear from you. Knowing who uses leeward helps us: - -- πŸ“Š Demonstrate impact to sponsors -- 🎯 Prioritize features that matter -- πŸ’ͺ Keep the project alive and maintained - -## How to let us know - -**Option 1: Public** - -Open an issue with the `adoption` label including: -- Company name (or "confidential") -- Use case (one line) -- Approximate executions/month - -**Option 2: Private** - -Email hello@vektia.com.br - ---- - -Thank you for strengthening open source πŸ™ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d49d528 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,39 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- Initial release of leeward +- Pre-fork worker pool architecture for ~0.5ms latency +- Linux namespace isolation (pid, mount, net, ipc, uts) +- Seccomp syscall filtering +- Landlock filesystem restrictions +- Unix socket IPC between daemon and CLI +- MessagePack protocol for communication +- Systemd service files (system and user) +- Nix flake support with overlay +- Debian package generation +- Static musl binary builds +- Multi-architecture support (x86_64, aarch64) + +### Architecture +- `leeward-core`: Core isolation primitives +- `leeward-daemon`: Persistent daemon with worker pool +- `leeward-cli`: Command-line interface +- `leeward-ffi`: C FFI library for language bindings + +### Security Features +- No root required (uses user namespaces) +- Defense in depth with 3 isolation layers +- Zero network access by default +- Restricted filesystem access via Landlock +- Minimal syscall whitelist via seccomp + +## [0.1.0] - TBD + +Initial public release. \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index d2f426e..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,265 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -**leeward** is a Linux-native sandbox for running untrusted code (currently Python) with extremely low latency (~0.5ms vs Docker's 300-500ms). It's designed for AI agent code execution using native Linux primitives instead of containers or VMs. - -**Status:** Work in progress. Implementing advanced pre-fork pool with io_uring and shared memory. - -## Architecture - -### Pre-Fork Execution Model -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” io_uring/shm β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Client β”‚ ◄───────────────► β”‚ leeward daemon β”‚ -β”‚ (any lang) β”‚ zero-copy IPC β”‚ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ - β”‚ β”‚ Pre-warmed Worker Pool β”‚ β”‚ - Python, Go, β”‚ β”‚ β”‚ β”‚ - Node, Rust β”‚ β”‚ [W1] Python idle ──pipe β”‚ β”‚ - via C FFI β”‚ β”‚ [W2] Python idle ──pipe β”‚ β”‚ - β”‚ β”‚ [W3] Python idle ──pipe β”‚ β”‚ - β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - -- **leeward-daemon**: Persistent daemon maintaining pre-forked worker pool -- **leeward-cli**: Command-line interface for executing code -- **leeward-ffi**: C FFI library for multi-language support -- **leeward-core**: Core isolation primitives and worker management - -### Performance Architecture (4 Levels) - -**Level 1: Pre-fork Pool (~0.5ms)** -- Workers created at daemon startup using `clone3` with `CLONE_INTO_CGROUP` -- Python interpreter already loaded and idle -- Code sent via pipe, execution happens immediately -- No fork/exec overhead on each request - -**Level 2: io_uring IPC (~0.2ms savings)** -- Zero-copy async I/O using io_uring submission queues -- 1 syscall per batch vs 4 syscalls per request (write/read/write/read) -- Batched request processing - -**Level 3: Shared Memory (~0.1ms savings)** -- Results written to `memfd_create` + `mmap` shared region -- Client and daemon map same file descriptor -- Eliminates 2 kernel copies (workerβ†’daemonβ†’client becomes workerβ†’shared memory) -- Request/response arenas in shared memory - -**Level 4: SECCOMP_USER_NOTIF (no worker recycling)** -- Blocked syscalls notify supervisor instead of killing process -- Supervisor returns `EACCES` to continue execution -- Workers don't die on denied syscalls, no recycling needed -- Graceful degradation vs fatal termination - -### Isolation Layers (Defense in Depth) -1. **Linux namespaces** via `clone3` (user, pid, mount, net, ipc, uts) -2. **seccomp user notifications** (`SECCOMP_USER_NOTIF`) - supervisor-mediated syscall filtering -3. **Landlock** filesystem restrictions (whitelist-based) -4. **cgroups v2** resource limits via `CLONE_INTO_CGROUP` (256MB RAM, 100% CPU, 32 PIDs, 30s timeout) - -### Communication Protocol -- **Client ↔ Daemon**: io_uring submission queue or Unix socket fallback -- **Daemon ↔ Worker**: Anonymous pipes for code delivery -- **Results**: Shared memory region (memfd) mapped by both daemon and client -- **Serialization**: MessagePack for control messages -- **Worker lifecycle**: Long-lived, survives denied syscalls (SECCOMP_USER_NOTIF) - -## Workspace Structure - -``` -crates/ -β”œβ”€β”€ leeward-core/ # Core isolation primitives -β”‚ └── src/ -β”‚ β”œβ”€β”€ config.rs # SandboxConfig with resource limits -β”‚ β”œβ”€β”€ worker.rs # Worker process management -β”‚ β”œβ”€β”€ result.rs # ExecutionResult, ExecutionMetrics -β”‚ β”œβ”€β”€ error.rs # Error types -β”‚ └── isolation/ # Isolation mechanisms -β”‚ β”œβ”€β”€ namespace.rs # setup_namespaces() -β”‚ β”œβ”€β”€ seccomp.rs # setup_seccomp() -β”‚ β”œβ”€β”€ landlock.rs # setup_landlock() -β”‚ β”œβ”€β”€ cgroups.rs # setup_cgroups() -β”‚ └── mounts.rs # setup_mounts() -β”œβ”€β”€ leeward-daemon/ # Persistent daemon -β”‚ └── src/ -β”‚ β”œβ”€β”€ main.rs # Entry point, signal handling -β”‚ β”œβ”€β”€ config.rs # DaemonConfig (pool size, socket path) -β”‚ β”œβ”€β”€ pool.rs # WorkerPool management -β”‚ β”œβ”€β”€ server.rs # UnixServer for client connections -β”‚ └── protocol.rs # Request/Response types -β”œβ”€β”€ leeward-cli/ # CLI interface -β”‚ └── src/ -β”‚ └── main.rs # Commands: exec, status, ping, run -└── leeward-ffi/ # C FFI bindings - └── src/ - └── lib.rs # leeward_execute(), leeward_free_result() -``` - -## Build System - -### Cargo (Primary) -```bash -# Development -cargo build -cargo test -cargo check -cargo clippy - -# Aliases (from .cargo/config.toml) -cargo b # build -cargo br # build --release -cargo t # test -cargo c # check -cargo cl # clippy -cargo daemon # run daemon in release mode -cargo cli # run CLI in release mode - -# Release build (with LTO, single codegen unit, stripped) -cargo build --release - -# Test specific crate -cargo test -p leeward-core -cargo test --workspace -``` - -### Nix (Optional, for reproducible builds) -```bash -# Build specific targets -nix build .#cli # CLI binary -nix build .#daemon # Daemon binary -nix build .#ffi # C FFI library (.so and .a) -nix build # Default: all targets - -# Development shell (includes Rust, mold linker, all deps) -nix develop -``` - -### Build Configuration -- **Rust version:** 1.85 (edition 2024) -- **Linker:** mold (via clang) for faster builds -- **Release optimizations:** LTO=fat, codegen-units=1, strip=true, opt-level=3 -- **Dependencies optimized** at opt-level=3 even in dev builds (for package.*) - -## Development Commands - -### Running the daemon -```bash -cargo daemon -# Or with args: -cargo run --release --bin leeward-daemon -- --pool-size 10 -``` - -### Using the CLI -```bash -cargo cli exec "print('hello world')" -cargo cli status -cargo cli ping -``` - -### Testing -```bash -cargo test # All tests -cargo test --workspace # Explicit workspace tests -cargo test -p leeward-core # Single crate -``` - -Test locations: -- `tests/integration/` - Integration tests -- `tests/escapes/` - Security/escape tests -- Unit tests within each crate's `src/` files - -## Key Technical Details - -### Requirements -- Linux >= 5.13 (Landlock support required) -- User namespaces enabled (check: `unshare -U whoami`) -- No root required - -### Default Resource Limits -```rust -// See crates/leeward-core/src/config.rs -memory_limit: 256MB -cpu_quota: 100% -max_processes: 32 -timeout: 30s -``` - -### Seccomp Syscall Whitelist -Only essential Python syscalls allowed. See `crates/leeward-core/src/isolation/seccomp.rs` for the full list (currently marked TODO). - -### Multi-Language Support -The C FFI library (`leeward-ffi`) enables bindings for any language with C interop: -- Python (planned in `bindings/python/`) -- Go, Node.js, Rust - all can use the FFI -- Generated header file via cbindgen -- Both shared (`.so`) and static (`.a`) libraries built - -## Important Implementation Notes - -### Currently Marked TODO -Implementing new execution paradigm: -- `crates/leeward-core/src/isolation/*.rs` - All setup functions -- Pre-fork pool with `clone3` + `CLONE_INTO_CGROUP` -- Pipe-based code delivery to idle workers -- io_uring integration for IPC -- Shared memory (memfd + mmap) for results -- SECCOMP_USER_NOTIF for syscall supervision - -When implementing, maintain the defense-in-depth approach: all 4 isolation layers must work together. - -### Performance Implementation Details - -**Pre-fork Pool:** -- Use `clone3` syscall with `CLONE_INTO_CGROUP` flag -- Workers created at daemon startup, not on-demand -- Each worker loads Python interpreter and enters idle loop -- Communication via anonymous pipes (one per worker) -- Workers never exit unless recycled (every N executions or on error) - -**io_uring Integration:** -- Daemon maintains io_uring instance for client communication -- Submission queue batches multiple requests -- Zero-copy buffers using registered buffers -- Completion queue processed asynchronously -- Fallback to Unix socket if io_uring unavailable - -**Shared Memory:** -- Create with `memfd_create("leeward_results", MFD_CLOEXEC)` -- Request arena: fixed-size slots for incoming code -- Response arena: variable-size slots for stdout/stderr -- Client maps read-only, daemon/workers map read-write -- Ring buffer or slab allocator for slot management - -**SECCOMP_USER_NOTIF:** -- Set up seccomp filter with `SECCOMP_RET_USER_NOTIF` for blocked syscalls -- Supervisor thread polls seccomp notification fd -- On notification: log syscall, return `EACCES` to continue -- Worker continues execution instead of dying -- Reduces worker churn dramatically - -### Security Considerations -- No network access by default (isolated network namespace) -- Filesystem access via Landlock whitelist only -- Resource limits enforced via cgroups v2 -- Syscalls mediated by supervisor (SECCOMP_USER_NOTIF) -- Workers are long-lived but fully isolated -- Shared memory regions are per-request, isolated between executions - -### Code Style -- Workspace uses Rust 2024 edition -- Lints: clippy::all, clippy::pedantic, clippy::nursery enabled -- Unsafe code generates warnings (required for isolation primitives) -- Use `thiserror` for error types -- Use `tracing` for logging, not `println!` - -## License and Support - -- **License:** MIT (previously Apache-2.0, changed in recent commits) -- **Repository:** https://github.com/vektia/leeward -- **Production usage:** See ADOPTION.md to report usage -- **Enterprise support:** hello@vektia.com.br -- **Sponsors:** github.com/sponsors/vektia diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3ddd17b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,277 @@ +# Contributing to Leeward + +Thank you for your interest in contributing to Leeward! This document provides guidelines and information for contributors. + +## πŸš€ Quick Start + +### Development Setup + +#### With Nix (Recommended) +```bash +# Clone the repository +git clone https://github.com/vektia/leeward +cd leeward + +# Enter development shell (auto-loads all dependencies) +nix develop + +# Or with direnv (automatic) +direnv allow +``` + +#### Without Nix +```bash +# Install Rust 1.85+ +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + +# Clone and build +git clone https://github.com/vektia/leeward +cd leeward +cargo build --release +``` + +### Running Locally + +```bash +# Start the daemon +./target/release/leeward-daemon & + +# Or with cargo alias +cargo daemon & + +# Execute code +./target/release/leeward exec "print('Hello, World!')" + +# Or with cargo alias +cargo cli exec "print('Hello, World!')" +``` + +## πŸ—οΈ Architecture Overview + +Leeward uses a pre-fork pool architecture for ultra-low latency (~0.5ms): + +``` +Client (any language) ←→ Unix Socket ←→ Daemon ←→ Pre-warmed Workers + β”œβ”€ Python ready + β”œβ”€ Isolated + └─ Waiting +``` + +### Key Components + +- **leeward-core**: Core isolation primitives (namespaces, seccomp, landlock) +- **leeward-daemon**: Persistent daemon managing worker pool +- **leeward-cli**: Command-line interface +- **leeward-ffi**: C FFI for language bindings + +### Isolation Layers + +1. **Linux Namespaces** - Process, network, mount isolation +2. **Seccomp** - Syscall filtering +3. **Landlock** - Filesystem access control + +## πŸ“ Code Style + +### Rust Guidelines + +- Use Rust 2024 edition +- Enable clippy lints: `clippy::all`, `clippy::pedantic`, `clippy::nursery` +- Use `thiserror` for error types +- Use `tracing` for logging (not `println!`) +- Document public APIs with examples + +### Running Checks + +```bash +# Format code +cargo fmt + +# Run clippy +cargo clippy --all-targets --all-features + +# Run tests +cargo test --workspace + +# Check everything +cargo check && cargo clippy && cargo test +``` + +## πŸ§ͺ Testing + +### Unit Tests +Place unit tests in the same file as the code: +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_something() { + // ... + } +} +``` + +### Integration Tests +Place integration tests in `tests/integration/`: +```rust +// tests/integration/basic_execution.rs +#[test] +fn test_python_execution() { + // ... +} +``` + +### Security Tests +Security/escape tests go in `tests/escapes/`: +```rust +// tests/escapes/network_escape.rs +#[test] +fn test_network_isolation() { + // Should fail - no network in sandbox +} +``` + +## πŸ”§ Making Changes + +### 1. Fork and Branch +```bash +git checkout -b feature/your-feature +# or +git checkout -b fix/your-bugfix +``` + +### 2. Make Your Changes +- Write clean, documented code +- Add tests for new functionality +- Update documentation if needed + +### 3. Commit Guidelines +Follow conventional commits: +``` +feat: add new feature +fix: fix bug in worker pool +docs: update README +test: add security tests +refactor: simplify isolation code +perf: optimize worker spawning +``` + +### 4. Submit PR +- Fill out the PR template +- Ensure CI passes +- Wait for review + +## 🚒 Release Process + +### Building Release Artifacts + +#### Local Build with Nix +```bash +# Build all components +nix build .#daemon +nix build .#cli + +# Build Debian package +nix build -f nix/deb.nix + +# Create static binary +cargo build --release --target x86_64-unknown-linux-musl +``` + +#### GitHub Actions +Releases are automatically built when pushing tags: +```bash +git tag v0.1.0 +git push origin v0.1.0 +``` + +This creates: +- Linux binaries (x86_64, aarch64) +- Debian packages (.deb) +- Static musl binaries +- Nix bundles +- SHA256 checksums + +### Using the Nix Overlay + +```nix +# In your flake.nix +{ + inputs.leeward.url = "github:vektia/leeward"; + + outputs = { self, nixpkgs, leeward, ... }: { + nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { + modules = [ + { + nixpkgs.overlays = [ leeward.overlays.default ]; + environment.systemPackages = [ pkgs.leeward ]; + } + ]; + }; + }; +} +``` + +## πŸ“‹ TODO for v1.0 + +High priority items for the v1.0 release: + +- [ ] Implement io_uring IPC for zero-copy communication +- [ ] Add shared memory support (memfd + mmap) +- [ ] Implement SECCOMP_USER_NOTIF for syscall supervision +- [ ] Complete Python bindings +- [ ] Add timeout handling in workers +- [ ] Implement worker recycling after N executions +- [ ] Add metrics/monitoring +- [ ] Complete FFI for other languages + +## πŸ› Debugging + +### Enable Debug Logging +```bash +RUST_LOG=debug ./target/release/leeward-daemon +``` + +### Check Worker Status +```bash +./target/release/leeward status +``` + +### Common Issues + +**"Operation not permitted" on namespaces** +```bash +# Check if user namespaces are enabled +cat /proc/sys/kernel/unprivileged_userns_clone +# Should be 1 +``` + +**Landlock not available** +```bash +# Need Linux >= 5.13 +uname -r +``` + +## πŸ“š Resources + +- [Linux Namespaces](https://man7.org/linux/man-pages/man7/namespaces.7.html) +- [Seccomp](https://man7.org/linux/man-pages/man2/seccomp.2.html) +- [Landlock](https://docs.kernel.org/userspace-api/landlock.html) +- [io_uring](https://kernel.dk/io_uring.pdf) + +## πŸ“„ License + +By contributing, you agree that your contributions will be licensed under the Apache-2.0 License. + +## 🀝 Code of Conduct + +Be respectful, inclusive, and constructive. We're building secure software that protects users - let's do it together professionally. + +## πŸ’¬ Questions? + +- Open an issue for bugs/features +- Start a discussion for questions +- Email: hello@vektia.com.br + +Happy coding! πŸš€ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..c13eec1 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1030 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "caps" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd1ddba47aba30b6a889298ad0109c3b8dcb0e8fc993b459daa7067d46f865e0" +dependencies = [ + "libc", +] + +[[package]] +name = "cbindgen" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadd868a2ce9ca38de7eeafdcec9c7065ef89b42b32f0839278d55f35c54d1ff" +dependencies = [ + "clap", + "heck 0.4.1", + "indexmap", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn", + "tempfile", + "toml", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "clap" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "io-uring" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd7bddefd0a8833b88a4b68f90dae22c7450d11b354198baee3874fd811b344" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "landlock" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49fefd6652c57d68aaa32544a4c0e642929725bdc1fd929367cdeb673ab81088" +dependencies = [ + "enumflags2", + "libc", + "thiserror", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leeward-cli" +version = "0.0.1" +dependencies = [ + "clap", + "leeward-core", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "leeward-core" +version = "0.0.1" +dependencies = [ + "caps", + "landlock", + "libc", + "memfd", + "nix", + "rmp-serde", + "seccompiler", + "serde", + "thiserror", + "tracing", +] + +[[package]] +name = "leeward-daemon" +version = "0.0.1" +dependencies = [ + "anyhow", + "io-uring", + "leeward-core", + "memfd", + "parking_lot", + "rmp-serde", + "serde", + "serde_json", + "signal-hook", + "signal-hook-tokio", + "thiserror", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "leeward-ffi" +version = "0.0.1" +dependencies = [ + "cbindgen", + "leeward-core", + "libc", + "once_cell", + "thiserror", +] + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "proc-macro2" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "seccompiler" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "345a3e4dddf721a478089d4697b83c6c0a8f5bf16086f6c13397e4534eb6e2e5" +dependencies = [ + "libc", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.148" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signal-hook-tokio" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213241f76fb1e37e27de3b6aa1b068a2c333233b59cca6634f634b80a27ecf1e" +dependencies = [ + "futures-core", + "libc", + "signal-hook", + "tokio", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zmij" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d6085d62852e35540689d1f97ad663e3971fc19cf5eceab364d62c646ea167" diff --git a/Cargo.toml b/Cargo.toml index 315b0db..2e74435 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,9 +3,9 @@ resolver = "2" members = ["crates/*"] [workspace.package] -version = "0.0.1" +version = "0.1.0" edition = "2024" -license = "MIT" +license = "Apache-2.0" repository = "https://github.com/vektia/leeward" authors = ["Vektia"] rust-version = "1.85" diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..969a42c --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,325 @@ +# Installation Guide + +## Quick Install + +### From Release (Binary) + +Download the latest release from [GitHub Releases](https://github.com/vektia/leeward/releases): + +```bash +# Linux x86_64 +curl -L https://github.com/vektia/leeward/releases/latest/download/leeward-amd64-linux.tar.gz | tar xz +sudo mv leeward/leeward* /usr/local/bin/ + +# Linux ARM64 +curl -L https://github.com/vektia/leeward/releases/latest/download/leeward-arm64-linux.tar.gz | tar xz +sudo mv leeward/leeward* /usr/local/bin/ +``` + +### Debian/Ubuntu + +```bash +# Download and install the .deb package +wget https://github.com/vektia/leeward/releases/latest/download/leeward_*_amd64.deb +sudo dpkg -i leeward_*.deb + +# The daemon will be installed as a systemd service +sudo systemctl start leeward +sudo systemctl enable leeward +``` + +### Nix (Flakes) + +Add to your `flake.nix`: + +```nix +{ + inputs = { + leeward.url = "github:vektia/leeward"; + }; + + outputs = { self, nixpkgs, leeward, ... }: { + # Use as overlay + nixpkgs.overlays = [ leeward.overlays.default ]; + + # Or add to system packages + environment.systemPackages = [ leeward.packages.${system}.default ]; + }; +} +``` + +Direct installation: + +```bash +# Install CLI and daemon +nix profile install github:vektia/leeward#cli +nix profile install github:vektia/leeward#daemon + +# Or run directly +nix run github:vektia/leeward#daemon +nix run github:vektia/leeward#cli -- exec "print('hello')" +``` + +### From Source + +```bash +# Clone repository +git clone https://github.com/vektia/leeward +cd leeward + +# Build with Cargo +cargo build --release + +# Install binaries +sudo cp target/release/leeward-daemon /usr/local/bin/ +sudo cp target/release/leeward /usr/local/bin/ + +# Or build with Nix +nix build .#daemon +nix build .#cli +``` + +## Platform-Specific Instructions + +### NixOS + +Add to your `configuration.nix`: + +```nix +{ config, pkgs, ... }: +{ + # Import the module + imports = [ + "${builtins.fetchTarball "https://github.com/vektia/leeward/archive/main.tar.gz"}/nix/module.nix" + ]; + + # Enable the service + services.leeward = { + enable = true; + workers = 4; # Number of pre-forked workers + }; + + # Enable user namespaces + boot.kernel.sysctl."kernel.unprivileged_userns_clone" = 1; +} +``` + +### Ubuntu/Debian + +```bash +# Enable user namespaces (if not already enabled) +echo 'kernel.unprivileged_userns_clone=1' | sudo tee /etc/sysctl.d/99-userns.conf +sudo sysctl --system + +# Install from .deb +wget https://github.com/vektia/leeward/releases/latest/download/leeward_*_amd64.deb +sudo dpkg -i leeward_*.deb + +# Start service +sudo systemctl enable --now leeward +``` + +### Fedora/RHEL/Rocky + +```bash +# Enable user namespaces (if disabled) +sudo sysctl -w kernel.unprivileged_userns_clone=1 +echo 'kernel.unprivileged_userns_clone=1' | sudo tee /etc/sysctl.d/99-userns.conf + +# Install from tarball +curl -L https://github.com/vektia/leeward/releases/latest/download/leeward-amd64-linux.tar.gz | tar xz +sudo mv leeward/* /usr/local/bin/ + +# Create systemd service +sudo cp leeward/leeward.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable --now leeward +``` + +### Arch Linux + +```bash +# User namespaces are enabled by default + +# Install from AUR (when available) +yay -S leeward + +# Or from tarball +curl -L https://github.com/vektia/leeward/releases/latest/download/leeward-amd64-linux.tar.gz | tar xz +sudo mv leeward/* /usr/local/bin/ +``` + +### Alpine Linux (Static Binary) + +```bash +# Download static musl binary +wget https://github.com/vektia/leeward/releases/latest/download/leeward-static-linux-amd64.tar.gz +tar xzf leeward-static-linux-amd64.tar.gz +sudo mv leeward-static/* /usr/local/bin/ +``` + +## Running as a Service + +### Systemd (System Service) + +```bash +# Copy the service file +sudo cp contrib/leeward.system.service /etc/systemd/system/leeward.service + +# Create leeward user +sudo useradd -r -s /usr/sbin/nologin -d /nonexistent leeward + +# Enable and start +sudo systemctl daemon-reload +sudo systemctl enable --now leeward + +# Check status +sudo systemctl status leeward +journalctl -u leeward -f +``` + +### Systemd (User Service) + +```bash +# Copy user service file +mkdir -p ~/.config/systemd/user/ +cp contrib/leeward.user.service ~/.config/systemd/user/leeward.service + +# Start user service +systemctl --user daemon-reload +systemctl --user enable --now leeward + +# Check status +systemctl --user status leeward +``` + +### Docker (Alternative) + +If you prefer containerized deployment: + +```bash +# Using the official image (when available) +docker run -d \ + --privileged \ + --name leeward \ + -v /run/leeward:/run/leeward \ + ghcr.io/vektia/leeward:latest + +# Or build from source +docker build -t leeward . +docker run -d --privileged --name leeward leeward +``` + +## Verification + +After installation, verify everything works: + +```bash +# Check daemon is running +leeward status + +# Execute test code +leeward exec "print('Hello from Leeward!')" + +# Test isolation (should fail) +leeward exec "import socket; socket.socket()" # No network +leeward exec "open('/etc/passwd', 'r')" # No filesystem access +``` + +## Building Packages + +### Build Debian Package + +With Nix: +```bash +nix build -f nix/deb.nix +# Output: result/leeward_0.1.0_amd64.deb +``` + +### Build Static Binary + +```bash +# Install musl target +rustup target add x86_64-unknown-linux-musl + +# Build static binary +cargo build --release --target x86_64-unknown-linux-musl +``` + +### Build All Release Artifacts + +```bash +# With Nix (recommended) +nix build .#daemon +nix build .#cli +nix build -f nix/deb.nix + +# With Cargo +cargo build --release +cargo build --release --target x86_64-unknown-linux-musl +``` + +## Troubleshooting + +### "Operation not permitted" on namespaces + +Check if user namespaces are enabled: +```bash +cat /proc/sys/kernel/unprivileged_userns_clone +# Should output: 1 + +# If not, enable: +sudo sysctl -w kernel.unprivileged_userns_clone=1 +``` + +### Landlock not available + +Requires Linux kernel >= 5.13: +```bash +uname -r # Check kernel version + +# Check if Landlock is enabled +cat /sys/kernel/security/lsm | grep landlock +``` + +### Socket permission denied + +Ensure the socket directory exists and has correct permissions: +```bash +# For system service +sudo mkdir -p /run/leeward +sudo chown leeward:leeward /run/leeward + +# For user service +mkdir -p $XDG_RUNTIME_DIR/leeward +``` + +## Uninstall + +### From Package Manager +```bash +# Debian/Ubuntu +sudo apt remove leeward + +# With systemctl +sudo systemctl stop leeward +sudo systemctl disable leeward +``` + +### Manual Uninstall +```bash +# Stop service +sudo systemctl stop leeward + +# Remove binaries +sudo rm /usr/local/bin/leeward* + +# Remove service files +sudo rm /etc/systemd/system/leeward.service + +# Remove user (if created) +sudo userdel leeward + +# Remove runtime directory +sudo rm -rf /run/leeward +``` \ No newline at end of file diff --git a/README.md b/README.md index b0f462e..c88b5e4 100644 --- a/README.md +++ b/README.md @@ -1,91 +1,36 @@ # leeward -> Linux-native sandbox for running untrusted code. No containers. No VMs. Fast. - -⚠️ **Work in progress** β€” Core isolation primitives are being implemented. - -## Why - -AI agents need to execute code. Current options suck: - -| Solution | Problem | -|----------|---------| -| Docker | 300-500ms startup, heavy | -| E2B/Modal | Cloud-only, expensive | -| WASM | No native libs, limited | -| Firecracker | Overkill for most cases | - -leeward gives you **~0.5ms** execution latency using native Linux primitives. - -## How -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” io_uring/shm β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Client β”‚ ◄───────────────► β”‚ leeward daemon β”‚ -β”‚ (any lang) β”‚ zero-copy IPC β”‚ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ - β”‚ β”‚ Pre-warmed Worker Pool β”‚ β”‚ - Python, Go, β”‚ β”‚ β”‚ β”‚ - Node, Rust β”‚ β”‚ [W1] Python idle ──pipe β”‚ β”‚ - via C FFI β”‚ β”‚ [W2] Python idle ──pipe β”‚ β”‚ - β”‚ β”‚ [W3] Python idle ──pipe β”‚ β”‚ - β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - -Each worker is isolated with: -- Linux namespaces (user, pid, mount, net, ipc) via clone3 -- seccomp user notifications (supervisor decides on blocked syscalls) -- Landlock filesystem restrictions -- cgroups v2 resource limits (CLONE_INTO_CGROUP) +Linux-native sandbox for running untrusted code with ~0.5ms latency. ## Usage + ```python from leeward import Leeward with Leeward() as sandbox: - result = sandbox.execute("print(sum(range(100)))") - print(result.stdout) # "4950" -``` -```bash -# Or via CLI -leeward exec "print('hello')" + result = sandbox.execute("print('Hello, World!')") + print(result.stdout) # "Hello, World!" ``` +## Features + +- **Fast**: ~0.5ms execution latency (vs Docker's 300-500ms) +- **Secure**: Linux namespaces + seccomp + Landlock +- **Simple**: No containers or VMs needed +- **Lightweight**: Pre-forked Python workers + ## Requirements -- Linux >= 5.13 (Landlock support) +- Linux >= 5.13 - User namespaces enabled - No root required -## Development - -```bash -# With direnv (auto-loads environment) -direnv allow - -# Or manually -nix develop - -# Build and run -cargo build --release -./target/release/leeward-daemon & # runs in background -./target/release/leeward exec "print('hello')" -``` - -Environment sets `LEEWARD_SOCKET=$DEVENV_STATE/leeward.sock` for isolation between clones. - -## Status - -Building the core. Not ready for production. - -## Support - -leeward is free and open source under [Apache-2.0](LICENSE.md). +## Documentation -- **Using in production?** [Let us know](ADOPTION.md) β€” it helps the project -- **Sponsors** β€” [github.com/sponsors/vektia](https://github.com/sponsors/vektia) -- **Enterprise support** β€” hello@vektia.com.br +- [Installation](INSTALL.md) +- [Contributing](CONTRIBUTING.md) +- [Changelog](CHANGELOG.md) ## License -[Apache-2.0](LICENSE.md) \ No newline at end of file +Apache-2.0 \ No newline at end of file diff --git a/contrib/leeward.system.service b/contrib/leeward.system.service new file mode 100644 index 0000000..c1950f4 --- /dev/null +++ b/contrib/leeward.system.service @@ -0,0 +1,26 @@ +[Unit] +Description=Leeward Sandbox Daemon +After=network.target + +[Service] +Type=simple +ExecStart=/usr/local/bin/leeward-daemon +Restart=always +RestartSec=5 +User=leeward +Group=leeward + +# Security hardening +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +PrivateTmp=true +RuntimeDirectory=leeward +RuntimeDirectoryMode=0755 + +# Resource limits +MemoryMax=1G +CPUQuota=200% + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/contrib/leeward.user.service b/contrib/leeward.user.service new file mode 100644 index 0000000..bebd595 --- /dev/null +++ b/contrib/leeward.user.service @@ -0,0 +1,14 @@ +[Unit] +Description=Leeward Sandbox Daemon (User) +After=default.target + +[Service] +Type=simple +ExecStart=%h/.local/bin/leeward-daemon +Restart=always +RestartSec=5 +RuntimeDirectory=leeward +RuntimeDirectoryMode=0700 + +[Install] +WantedBy=default.target \ No newline at end of file diff --git a/crates/leeward-cli/src/main.rs b/crates/leeward-cli/src/main.rs index 71db126..26eef33 100644 --- a/crates/leeward-cli/src/main.rs +++ b/crates/leeward-cli/src/main.rs @@ -3,6 +3,41 @@ use clap::{Parser, Subcommand}; use leeward_core::config::default_socket_path; use std::path::PathBuf; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::UnixStream; + +/// Send request to daemon and receive response +async fn send_request( + socket_path: &PathBuf, + request: &leeward_core::protocol::Request, +) -> Result> { + // Connect to daemon + let mut stream = UnixStream::connect(socket_path).await?; + + // Encode request + let request_bytes = leeward_core::protocol::encode(request)?; + + // Send length prefix (4 bytes, big-endian) + let len = request_bytes.len() as u32; + stream.write_all(&len.to_be_bytes()).await?; + + // Send request + stream.write_all(&request_bytes).await?; + + // Read response length + let mut len_buf = [0u8; 4]; + stream.read_exact(&mut len_buf).await?; + let response_len = u32::from_be_bytes(len_buf) as usize; + + // Read response + let mut response_buf = vec![0u8; response_len]; + stream.read_exact(&mut response_buf).await?; + + // Decode response + let response = leeward_core::protocol::decode(&response_buf)?; + + Ok(response) +} #[derive(Parser)] #[command(name = "leeward")] @@ -26,10 +61,6 @@ enum Commands { /// Timeout in seconds #[arg(short, long, default_value = "30")] timeout: u64, - - /// Memory limit in MB - #[arg(short, long, default_value = "256")] - memory: u64, }, /// Get daemon status @@ -55,10 +86,6 @@ enum Commands { #[arg(short, long, default_value = "30")] timeout: u64, - /// Memory limit in MB - #[arg(short, long, default_value = "256")] - memory: u64, - /// Allow network access #[arg(long)] network: bool, @@ -81,44 +108,96 @@ async fn main() -> Result<(), Box> { code, socket, timeout, - memory, } => { let socket = socket.unwrap_or_else(default_socket_path); - println!("Executing via daemon at {:?}", socket); - println!("Code: {}", code); - println!("Timeout: {}s, Memory: {}MB", timeout, memory); - // TODO: Connect to daemon and execute + + let request = leeward_core::protocol::Request::Execute( + leeward_core::protocol::ExecuteRequest { + code: Some(code), + shm_slot_id: None, + timeout: Some(std::time::Duration::from_secs(timeout)), + memory_limit: None, + files: Vec::new(), + } + ); + + match send_request(&socket, &request).await? { + leeward_core::protocol::Response::Execute(resp) => { + if resp.success { + if let Some(result) = resp.result { + print!("{}", String::from_utf8_lossy(&result.stdout)); + eprint!("{}", String::from_utf8_lossy(&result.stderr)); + std::process::exit(result.exit_code); + } + } else { + eprintln!("Error: {}", resp.error.unwrap_or_else(|| "Unknown error".into())); + std::process::exit(1); + } + } + leeward_core::protocol::Response::Error { message } => { + eprintln!("Error: {}", message); + std::process::exit(1); + } + _ => { + eprintln!("Unexpected response"); + std::process::exit(1); + } + } } Commands::Status { socket } => { let socket = socket.unwrap_or_else(default_socket_path); - println!("Getting status from {:?}", socket); - // TODO: Connect to daemon and get status + let request = leeward_core::protocol::Request::Status; + + match send_request(&socket, &request).await? { + leeward_core::protocol::Response::Status { total, idle, busy } => { + println!("Workers: {} total, {} idle, {} busy", total, idle, busy); + } + leeward_core::protocol::Response::Error { message } => { + eprintln!("Error: {}", message); + std::process::exit(1); + } + _ => { + eprintln!("Unexpected response"); + std::process::exit(1); + } + } } Commands::Ping { socket } => { let socket = socket.unwrap_or_else(default_socket_path); - println!("Pinging daemon at {:?}", socket); - // TODO: Connect and ping + let request = leeward_core::protocol::Request::Ping; + + match send_request(&socket, &request).await? { + leeward_core::protocol::Response::Pong => { + println!("Pong!"); + } + leeward_core::protocol::Response::Error { message } => { + eprintln!("Error: {}", message); + std::process::exit(1); + } + _ => { + eprintln!("Unexpected response"); + std::process::exit(1); + } + } } Commands::Run { code, timeout, - memory, network, } => { println!("Running directly (no daemon)"); println!("Code: {}", code); println!( - "Timeout: {}s, Memory: {}MB, Network: {}", - timeout, memory, network + "Timeout: {}s, Network: {}", + timeout, network ); // TODO: Use leeward_core directly to execute let _config = leeward_core::SandboxConfig::builder() .timeout_secs(timeout) - .memory_limit_mb(memory) .allow_network(network) .build(); } diff --git a/crates/leeward-core/Cargo.toml b/crates/leeward-core/Cargo.toml index a2dd861..24ae038 100644 --- a/crates/leeward-core/Cargo.toml +++ b/crates/leeward-core/Cargo.toml @@ -16,6 +16,7 @@ caps = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } serde = { workspace = true } +rmp-serde = { workspace = true } libc = { workspace = true } memfd = { workspace = true } diff --git a/crates/leeward-core/src/config.rs b/crates/leeward-core/src/config.rs index 5d39f14..4fa6ce9 100644 --- a/crates/leeward-core/src/config.rs +++ b/crates/leeward-core/src/config.rs @@ -16,18 +16,9 @@ pub struct SandboxConfig { /// Paths to bind mount read-write pub rw_binds: Vec, - /// Memory limit in bytes - pub memory_limit: u64, - - /// CPU limit as percentage (0-100) - pub cpu_limit: u32, - /// Maximum execution time pub timeout: Duration, - /// Maximum number of processes/threads - pub max_pids: u32, - /// Allow network access pub allow_network: bool, @@ -41,17 +32,14 @@ pub struct SandboxConfig { impl Default for SandboxConfig { fn default() -> Self { Self { - python_path: PathBuf::from("/usr/bin/python3"), + python_path: find_python(), ro_binds: vec![ PathBuf::from("/usr"), PathBuf::from("/lib"), PathBuf::from("/lib64"), ], rw_binds: vec![], - memory_limit: 256 * 1024 * 1024, // 256MB - cpu_limit: 100, timeout: Duration::from_secs(30), - max_pids: 32, allow_network: false, workdir: PathBuf::from("/home/sandbox"), env: vec![ @@ -84,23 +72,6 @@ impl SandboxConfigBuilder { self } - #[must_use] - pub fn memory_limit(mut self, bytes: u64) -> Self { - self.config.memory_limit = bytes; - self - } - - #[must_use] - pub fn memory_limit_mb(self, mb: u64) -> Self { - self.memory_limit(mb * 1024 * 1024) - } - - #[must_use] - pub fn cpu_limit(mut self, percent: u32) -> Self { - self.config.cpu_limit = percent.min(100); - self - } - #[must_use] pub fn timeout(mut self, duration: Duration) -> Self { self.config.timeout = duration; @@ -142,6 +113,21 @@ impl SandboxConfigBuilder { } } +/// Find Python executable in PATH +fn find_python() -> PathBuf { + if let Ok(path_var) = std::env::var("PATH") { + for dir in path_var.split(':') { + for name in &["python3", "python"] { + let candidate = PathBuf::from(dir).join(name); + if candidate.exists() { + return candidate; + } + } + } + } + PathBuf::from("python3") +} + /// Get default socket path from LEEWARD_SOCKET env var or system default /// /// Returns: diff --git a/crates/leeward-core/src/error.rs b/crates/leeward-core/src/error.rs index dc28022..cdad496 100644 --- a/crates/leeward-core/src/error.rs +++ b/crates/leeward-core/src/error.rs @@ -13,9 +13,6 @@ pub enum LeewardError { #[error("landlock error: {0}")] Landlock(String), - #[error("cgroups error: {0}")] - Cgroups(String), - #[error("mount error: {0}")] Mount(String), diff --git a/crates/leeward-core/src/isolation/cgroups.rs b/crates/leeward-core/src/isolation/cgroups.rs deleted file mode 100644 index 14c7f1a..0000000 --- a/crates/leeward-core/src/isolation/cgroups.rs +++ /dev/null @@ -1,86 +0,0 @@ -//! Cgroups v2 resource limits - -use crate::Result; - -/// Configuration for cgroups v2 resource limits -#[derive(Debug, Clone)] -pub struct CgroupsConfig { - /// Memory limit in bytes (memory.max) - pub memory_max: u64, - /// CPU quota as percentage (cpu.max) - pub cpu_percent: u32, - /// Maximum number of processes (pids.max) - pub pids_max: u32, - /// Enable memory swap (memory.swap.max) - pub allow_swap: bool, -} - -impl Default for CgroupsConfig { - fn default() -> Self { - Self { - memory_max: 256 * 1024 * 1024, // 256MB - cpu_percent: 100, - pids_max: 32, - allow_swap: false, - } - } -} - -impl CgroupsConfig { - /// Create a new cgroup for a sandbox - pub fn create_cgroup(&self, name: &str) -> Result { - // TODO: Create cgroup under /sys/fs/cgroup/leeward/{name} - tracing::debug!( - name, - memory = self.memory_max, - cpu = self.cpu_percent, - pids = self.pids_max, - "creating cgroup" - ); - Ok(CgroupHandle { - name: name.to_string(), - path: format!("/sys/fs/cgroup/leeward/{name}"), - }) - } -} - -/// Handle to a cgroup -#[derive(Debug)] -pub struct CgroupHandle { - name: String, - path: String, -} - -impl CgroupHandle { - /// Add a process to this cgroup - pub fn add_process(&self, pid: u32) -> Result<()> { - // TODO: Write pid to cgroup.procs - tracing::debug!(cgroup = %self.name, pid, "adding process to cgroup"); - Ok(()) - } - - /// Get current memory usage - pub fn memory_current(&self) -> Result { - // TODO: Read memory.current - Ok(0) - } - - /// Get peak memory usage - pub fn memory_peak(&self) -> Result { - // TODO: Read memory.peak - Ok(0) - } - - /// Check if OOM killed - pub fn was_oom_killed(&self) -> Result { - // TODO: Read memory.events for oom_kill - Ok(false) - } - - /// Destroy the cgroup - pub fn destroy(self) -> Result<()> { - // TODO: rmdir the cgroup - tracing::debug!(cgroup = %self.name, "destroying cgroup"); - Ok(()) - } -} diff --git a/crates/leeward-core/src/isolation/clone3.rs b/crates/leeward-core/src/isolation/clone3.rs index b379215..a453f94 100644 --- a/crates/leeward-core/src/isolation/clone3.rs +++ b/crates/leeward-core/src/isolation/clone3.rs @@ -1,8 +1,7 @@ -//! clone3 syscall wrapper with CLONE_INTO_CGROUP support +//! clone3 syscall wrapper for process creation use crate::{LeewardError, Result}; use libc::pid_t; -use std::os::unix::io::RawFd; /// clone3 clone_args structure (from linux/sched.h) #[repr(C)] @@ -24,16 +23,11 @@ pub struct CloneArgs { pub set_tid: u64, /// Size of set_tid array pub set_tid_size: u64, - /// File descriptor for cgroup - pub cgroup: u64, } /// clone3 syscall number const SYS_CLONE3: i64 = 435; -/// CLONE_INTO_CGROUP flag (requires Linux >= 5.7) -pub const CLONE_INTO_CGROUP: u64 = 0x200000000; - /// Wrapper around the clone3 syscall /// /// # Safety @@ -58,23 +52,14 @@ pub unsafe fn clone3(args: &CloneArgs) -> Result { Ok(ret as pid_t) } -/// Helper to create a pre-forked worker with namespaces and cgroup +/// Helper to create a pre-forked worker with namespaces pub fn clone_worker( - cgroup_fd: RawFd, namespace_flags: u64, child_fn: impl FnOnce() -> Result<()>, ) -> Result { - let mut flags = namespace_flags; - - // Only use CLONE_INTO_CGROUP if we have a valid fd - if cgroup_fd >= 0 { - flags |= CLONE_INTO_CGROUP; - } - let args = CloneArgs { - flags, + flags: namespace_flags, exit_signal: libc::SIGCHLD as u64, - cgroup: if cgroup_fd >= 0 { cgroup_fd as u64 } else { 0 }, ..Default::default() }; diff --git a/crates/leeward-core/src/isolation/landlock.rs b/crates/leeward-core/src/isolation/landlock.rs index 202a79e..0ec3bb5 100644 --- a/crates/leeward-core/src/isolation/landlock.rs +++ b/crates/leeward-core/src/isolation/landlock.rs @@ -1,7 +1,11 @@ //! Landlock filesystem sandboxing -use crate::{LeewardError, Result}; +use crate::Result; use std::path::PathBuf; +use landlock::{ + Access, AccessFs, Ruleset, RulesetAttr, RulesetCreatedAttr, + RulesetStatus, ABI +}; /// Configuration for Landlock filesystem restrictions #[derive(Debug, Clone, Default)] @@ -38,13 +42,101 @@ impl LandlockConfig { /// Apply Landlock restrictions to the current process pub fn apply(&self) -> Result<()> { - // TODO: Implement using landlock crate tracing::debug!( ro = self.ro_paths.len(), rw = self.rw_paths.len(), exec = self.exec_paths.len(), "applying landlock rules" ); + + // Check if Landlock is supported - use V2 for now (Linux 5.19+) + let abi = ABI::V2; + tracing::debug!("Using Landlock ABI version: {:?}", abi); + + // Create ruleset with all filesystem access flags we want to control + let mut ruleset = Ruleset::default() + .handle_access(AccessFs::from_all(abi)) + .map_err(|e| crate::LeewardError::Landlock(format!("failed to create ruleset: {e}")))? + .create() + .map_err(|e| crate::LeewardError::Landlock(format!("failed to create ruleset: {e}")))?; + + // Add read-only paths + let ro_access = AccessFs::ReadFile | AccessFs::ReadDir; + for path in &self.ro_paths { + if path.exists() { + let file = std::fs::File::open(path) + .map_err(|e| crate::LeewardError::Landlock(format!("failed to open {}: {e}", path.display())))?; + ruleset = ruleset + .add_rule(landlock::PathBeneath::new(file, ro_access)) + .map_err(|e| crate::LeewardError::Landlock(format!( + "failed to add ro rule for {}: {e}", + path.display() + )))?; + tracing::debug!("added read-only access for {}", path.display()); + } + } + + // Add read-write paths + let rw_access = AccessFs::ReadFile + | AccessFs::WriteFile + | AccessFs::ReadDir + | AccessFs::RemoveDir + | AccessFs::RemoveFile + | AccessFs::MakeChar + | AccessFs::MakeDir + | AccessFs::MakeReg + | AccessFs::MakeSock + | AccessFs::MakeFifo + | AccessFs::MakeBlock + | AccessFs::MakeSym; + + for path in &self.rw_paths { + if path.exists() { + let file = std::fs::File::open(path) + .map_err(|e| crate::LeewardError::Landlock(format!("failed to open {}: {e}", path.display())))?; + ruleset = ruleset + .add_rule(landlock::PathBeneath::new(file, rw_access)) + .map_err(|e| crate::LeewardError::Landlock(format!( + "failed to add rw rule for {}: {e}", + path.display() + )))?; + tracing::debug!("added read-write access for {}", path.display()); + } + } + + // Add execute paths + let exec_access = AccessFs::Execute | AccessFs::ReadFile; + for path in &self.exec_paths { + if path.exists() { + let file = std::fs::File::open(path) + .map_err(|e| crate::LeewardError::Landlock(format!("failed to open {}: {e}", path.display())))?; + ruleset = ruleset + .add_rule(landlock::PathBeneath::new(file, exec_access)) + .map_err(|e| crate::LeewardError::Landlock(format!( + "failed to add exec rule for {}: {e}", + path.display() + )))?; + tracing::debug!("added execute access for {}", path.display()); + } + } + + // Enforce the ruleset + let status = ruleset + .restrict_self() + .map_err(|e| crate::LeewardError::Landlock(format!("failed to enforce landlock: {e}")))?; + + match status.ruleset { + RulesetStatus::NotEnforced => { + tracing::warn!("Landlock ruleset could not be enforced"); + } + RulesetStatus::PartiallyEnforced => { + tracing::info!("Landlock ruleset partially enforced"); + } + RulesetStatus::FullyEnforced => { + tracing::info!("Landlock ruleset fully enforced"); + } + } + Ok(()) } } diff --git a/crates/leeward-core/src/isolation/mod.rs b/crates/leeward-core/src/isolation/mod.rs index d55e6c3..145f5ff 100644 --- a/crates/leeward-core/src/isolation/mod.rs +++ b/crates/leeward-core/src/isolation/mod.rs @@ -1,21 +1,18 @@ //! Linux isolation primitives //! //! This module contains the core isolation mechanisms: -//! - `clone3` - clone3 syscall with CLONE_INTO_CGROUP support +//! - `clone3` - clone3 syscall for process creation //! - `namespace` - Linux namespaces (user, pid, mount, net, ipc) //! - `seccomp` - syscall filtering with SECCOMP_USER_NOTIF //! - `landlock` - filesystem access control -//! - `cgroups` - resource limits //! - `mounts` - filesystem setup with bind mounts and tmpfs -pub mod cgroups; pub mod clone3; pub mod landlock; pub mod mounts; pub mod namespace; pub mod seccomp; -pub use self::cgroups::CgroupsConfig; pub use self::landlock::LandlockConfig; pub use self::mounts::MountConfig; pub use self::namespace::NamespaceConfig; diff --git a/crates/leeward-core/src/isolation/mounts.rs b/crates/leeward-core/src/isolation/mounts.rs index b0af559..3c2de14 100644 --- a/crates/leeward-core/src/isolation/mounts.rs +++ b/crates/leeward-core/src/isolation/mounts.rs @@ -2,6 +2,8 @@ use crate::{LeewardError, Result}; use std::path::PathBuf; +use std::ffi::CString; +use std::os::unix::ffi::OsStrExt; /// Configuration for filesystem mounts #[derive(Debug, Clone, Default)] @@ -48,19 +50,55 @@ impl MountConfig { } fn setup_root(&self) -> Result<()> { - // TODO: Create new root directory structure tracing::debug!(root = ?self.new_root, "setting up root"); + + // Create new root if it doesn't exist + if self.new_root != PathBuf::new() { + std::fs::create_dir_all(&self.new_root) + .map_err(|e| LeewardError::Mount(format!("failed to create new root: {e}")))?; + + // Create essential directories + for dir in &["proc", "sys", "dev", "tmp", "home", "home/sandbox"] { + let path = self.new_root.join(dir); + std::fs::create_dir_all(&path) + .map_err(|e| LeewardError::Mount(format!("failed to create {}: {e}", dir)))?; + } + } + Ok(()) } fn setup_binds(&self) -> Result<()> { for (src, dst) in &self.ro_binds { tracing::debug!(?src, ?dst, "ro bind mount"); - // TODO: mount --bind, then remount ro + + if src.exists() { + // Ensure destination exists + if let Some(parent) = dst.parent() { + std::fs::create_dir_all(parent) + .map_err(|e| LeewardError::Mount(format!("failed to create mount point: {e}")))?; + } + + // Bind mount + mount_bind(src, dst)?; + // Remount read-only + mount_remount_ro(dst)?; + } } + for (src, dst) in &self.rw_binds { tracing::debug!(?src, ?dst, "rw bind mount"); - // TODO: mount --bind + + if src.exists() { + // Ensure destination exists + if let Some(parent) = dst.parent() { + std::fs::create_dir_all(parent) + .map_err(|e| LeewardError::Mount(format!("failed to create mount point: {e}")))?; + } + + // Bind mount + mount_bind(src, dst)?; + } } Ok(()) } @@ -68,15 +106,172 @@ impl MountConfig { fn setup_tmpfs(&self) -> Result<()> { for (path, size) in &self.tmpfs { tracing::debug!(?path, size, "tmpfs mount"); - // TODO: mount -t tmpfs -o size={size} + + // Ensure mount point exists + std::fs::create_dir_all(path) + .map_err(|e| LeewardError::Mount(format!("failed to create tmpfs mount point: {e}")))?; + + mount_tmpfs(path, *size)?; } Ok(()) } fn do_pivot_root(&self) -> Result<()> { - // TODO: pivot_root(new_root, put_old) - // Then unmount and remove put_old tracing::debug!(root = ?self.new_root, "pivot_root"); + + if self.new_root == PathBuf::new() { + return Ok(()); // Skip pivot_root if no new root specified + } + + let put_old = self.new_root.join("put_old"); + std::fs::create_dir_all(&put_old) + .map_err(|e| LeewardError::Mount(format!("failed to create put_old: {e}")))?; + + pivot_root(&self.new_root, &put_old)?; + + // Change to new root + std::env::set_current_dir("/") + .map_err(|e| LeewardError::Mount(format!("failed to chdir to /: {e}")))?; + + // Unmount old root + umount2(&PathBuf::from("/put_old"), libc::MNT_DETACH)?; + + // Remove put_old directory + std::fs::remove_dir("/put_old") + .map_err(|e| LeewardError::Mount(format!("failed to remove put_old: {e}")))?; + Ok(()) } } + +// Helper functions for mount operations + +fn path_to_cstring(path: &std::path::Path) -> Result { + CString::new(path.as_os_str().as_bytes()) + .map_err(|e| LeewardError::Mount(format!("invalid path {}: {}", path.display(), e))) +} + +fn mount_bind(src: &std::path::Path, dst: &std::path::Path) -> Result<()> { + let src_c = path_to_cstring(src)?; + let dst_c = path_to_cstring(dst)?; + + // SAFETY: mount syscall with bind flag + let ret = unsafe { + libc::mount( + src_c.as_ptr(), + dst_c.as_ptr(), + std::ptr::null(), + libc::MS_BIND | libc::MS_REC, + std::ptr::null(), + ) + }; + + if ret != 0 { + return Err(LeewardError::Mount(format!( + "failed to bind mount {} to {}: {}", + src.display(), + dst.display(), + std::io::Error::last_os_error() + ))); + } + + Ok(()) +} + +fn mount_remount_ro(path: &std::path::Path) -> Result<()> { + let path_c = path_to_cstring(path)?; + + // SAFETY: mount syscall to remount read-only + let ret = unsafe { + libc::mount( + std::ptr::null(), + path_c.as_ptr(), + std::ptr::null(), + libc::MS_BIND | libc::MS_REMOUNT | libc::MS_RDONLY, + std::ptr::null(), + ) + }; + + if ret != 0 { + return Err(LeewardError::Mount(format!( + "failed to remount {} read-only: {}", + path.display(), + std::io::Error::last_os_error() + ))); + } + + Ok(()) +} + +fn mount_tmpfs(path: &std::path::Path, size: u64) -> Result<()> { + let path_c = path_to_cstring(path)?; + let fstype = CString::new("tmpfs") + .map_err(|e| LeewardError::Mount(format!("invalid fstype: {e}")))?; + + let size_mb = size / (1024 * 1024); + let options = CString::new(format!("size={}M", size_mb)) + .map_err(|e| LeewardError::Mount(format!("invalid options: {e}")))?; + + // SAFETY: mount syscall with tmpfs + let ret = unsafe { + libc::mount( + fstype.as_ptr(), + path_c.as_ptr(), + fstype.as_ptr(), + 0, + options.as_ptr() as *const libc::c_void, + ) + }; + + if ret != 0 { + return Err(LeewardError::Mount(format!( + "failed to mount tmpfs at {}: {}", + path.display(), + std::io::Error::last_os_error() + ))); + } + + Ok(()) +} + +fn pivot_root(new_root: &std::path::Path, put_old: &std::path::Path) -> Result<()> { + let new_root_c = path_to_cstring(new_root)?; + let put_old_c = path_to_cstring(put_old)?; + + // SAFETY: pivot_root syscall + let ret = unsafe { + libc::syscall( + libc::SYS_pivot_root, + new_root_c.as_ptr(), + put_old_c.as_ptr(), + ) + }; + + if ret != 0 { + return Err(LeewardError::Mount(format!( + "pivot_root failed: {}", + std::io::Error::last_os_error() + ))); + } + + Ok(()) +} + +fn umount2(path: &std::path::Path, flags: i32) -> Result<()> { + let path_c = path_to_cstring(path)?; + + // SAFETY: umount2 syscall + let ret = unsafe { + libc::umount2(path_c.as_ptr(), flags) + }; + + if ret != 0 { + return Err(LeewardError::Mount(format!( + "umount2 failed for {}: {}", + path.display(), + std::io::Error::last_os_error() + ))); + } + + Ok(()) +} diff --git a/crates/leeward-core/src/isolation/seccomp.rs b/crates/leeward-core/src/isolation/seccomp.rs index 353d867..d0ab59b 100644 --- a/crates/leeward-core/src/isolation/seccomp.rs +++ b/crates/leeward-core/src/isolation/seccomp.rs @@ -1,7 +1,11 @@ //! Seccomp-BPF syscall filtering with SECCOMP_USER_NOTIF support use crate::{LeewardError, Result}; -use std::os::unix::io::{AsRawFd, RawFd}; +use std::os::unix::io::RawFd; +use std::collections::BTreeMap; +use seccompiler::{ + SeccompAction, SeccompFilter, SeccompRule, TargetArch +}; /// Configuration for seccomp filtering #[derive(Debug, Clone)] @@ -31,25 +35,68 @@ impl SeccompConfig { /// seccomp notifications. The supervisor can poll this fd and decide /// what to do with blocked syscalls. pub fn apply(&self) -> Result> { - // TODO: Build and apply BPF filter using seccompiler - // For notify mode: - // 1. Build filter with SECCOMP_RET_USER_NOTIF for blocked syscalls - // 2. Use seccomp(SECCOMP_SET_MODE_FILTER, ...) to apply - // 3. Get notification fd from seccomp(SECCOMP_GET_NOTIF_SIZES, ...) - tracing::debug!( notify = self.notify_mode, syscalls = self.allowed_syscalls.len(), "applying seccomp filter" ); - if self.notify_mode { - // TODO: Return actual notification fd - // For now, return None as placeholder - Ok(None) - } else { - Ok(None) + // Build the filter + let filter = self.build_filter()?; + + // Apply the filter + // Note: SECCOMP_USER_NOTIF requires kernel 5.0+ and special handling + // For now, we'll use basic filtering with KILL action for denied syscalls + // Convert filter to BPF program and apply it + let bpf_prog: seccompiler::BpfProgram = filter + .try_into() + .map_err(|e| LeewardError::Seccomp(format!("failed to compile filter to BPF: {e}")))?; + + seccompiler::apply_filter(&bpf_prog) + .map_err(|e| LeewardError::Seccomp(format!("failed to apply seccomp filter: {e}")))?; + + tracing::info!("seccomp filter applied with {} allowed syscalls", self.allowed_syscalls.len()); + + // SECCOMP_USER_NOTIF would require: + // 1. Using raw seccomp() syscall with SECCOMP_FILTER_FLAG_NEW_LISTENER + // 2. Getting notification fd from kernel + // 3. Setting up notification handler thread + // For now, return None as we're using basic filtering + Ok(None) + } + + /// Build the seccomp filter + fn build_filter(&self) -> Result { + let mut rules = BTreeMap::new(); + + // For each allowed syscall, create a rule with Allow action + // SeccompRule::new only takes conditions, the action is Allow by default for matched rules + for &syscall_num in &self.allowed_syscalls { + rules.insert( + syscall_num, + vec![SeccompRule::new(vec![]) + .map_err(|e| LeewardError::Seccomp(format!("failed to create rule: {e}")))?], + ); } + + // Default action for unmatched syscalls + let default_action = if self.log_denials { + SeccompAction::Log // Log and deny + } else { + SeccompAction::KillThread // Kill the thread + }; + + // Get current architecture + let arch = get_arch(); + + // Create the filter + SeccompFilter::new( + rules, + default_action, + SeccompAction::Allow, // Bad architecture action + arch, + ) + .map_err(|e| LeewardError::Seccomp(format!("failed to create seccomp filter: {e}"))) } } @@ -154,6 +201,18 @@ impl Drop for SeccompNotifyFd { } } +/// Get the current architecture for seccomp +fn get_arch() -> TargetArch { + #[cfg(target_arch = "x86_64")] + return TargetArch::x86_64; + + #[cfg(target_arch = "aarch64")] + return TargetArch::aarch64; + + #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] + compile_error!("Unsupported architecture for seccomp"); +} + /// Default syscalls needed for Python to run fn default_python_syscalls() -> Vec { vec![ diff --git a/crates/leeward-core/src/lib.rs b/crates/leeward-core/src/lib.rs index e5018b0..0ec0543 100644 --- a/crates/leeward-core/src/lib.rs +++ b/crates/leeward-core/src/lib.rs @@ -6,7 +6,6 @@ //! - Linux namespaces via clone3 (user, pid, mount, net, ipc) //! - seccomp user notifications (SECCOMP_USER_NOTIF) //! - Landlock filesystem restrictions -//! - cgroups v2 resource limits (CLONE_INTO_CGROUP) //! - Shared memory for zero-copy results (memfd + mmap) //! - Pipe-based code delivery to pre-forked workers @@ -17,6 +16,7 @@ pub mod config; pub mod error; pub mod isolation; pub mod pipe; +pub mod protocol; pub mod result; pub mod shm; pub mod worker; diff --git a/crates/leeward-daemon/src/protocol.rs b/crates/leeward-core/src/protocol.rs similarity index 97% rename from crates/leeward-daemon/src/protocol.rs rename to crates/leeward-core/src/protocol.rs index 3ca1bab..08e5c5a 100644 --- a/crates/leeward-daemon/src/protocol.rs +++ b/crates/leeward-core/src/protocol.rs @@ -2,9 +2,8 @@ //! //! Supports both traditional msgpack and zero-copy shared memory modes -use leeward_core::ExecutionResult; +use crate::ExecutionResult; use serde::{Deserialize, Serialize}; -use std::os::unix::io::RawFd; use std::time::Duration; /// Request to execute code diff --git a/crates/leeward-core/src/worker.rs b/crates/leeward-core/src/worker.rs index c2d6ca3..58504b2 100644 --- a/crates/leeward-core/src/worker.rs +++ b/crates/leeward-core/src/worker.rs @@ -1,9 +1,5 @@ -//! Sandbox worker process management - use crate::{pipe::ParentPipe, ExecutionResult, LeewardError, Result, SandboxConfig}; -use std::os::unix::io::RawFd; -/// State of a worker in the pool #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WorkerState { /// Ready to accept work @@ -16,33 +12,17 @@ pub enum WorkerState { Dead, } -/// A pre-forked sandboxed worker process -/// -/// In the new paradigm: -/// - Workers are created at daemon startup with clone3 + CLONE_INTO_CGROUP -/// - Python interpreter is already loaded and idle -/// - Code is sent via pipe, execution is immediate (~0.5ms) -/// - Workers survive denied syscalls (SECCOMP_USER_NOTIF) #[derive(Debug)] pub struct Worker { - /// Unique worker ID pub id: u32, - /// Current state pub state: WorkerState, - /// Process ID (if running) pub pid: Option, - /// Number of executions completed pub execution_count: u64, - /// Configuration for this worker config: SandboxConfig, - /// Communication pipe with the worker pipe: Option, - /// Cgroup file descriptor (for CLONE_INTO_CGROUP) - cgroup_fd: Option, } impl Worker { - /// Create a new worker with the given config pub fn new(id: u32, config: SandboxConfig) -> Self { Self { id, @@ -51,44 +31,29 @@ impl Worker { execution_count: 0, config, pipe: None, - cgroup_fd: None, } } - /// Spawn the worker process using pre-fork model - /// - /// This uses clone3 with CLONE_INTO_CGROUP to create a fully isolated - /// worker process with Python already loaded. pub fn spawn(&mut self) -> Result<()> { use crate::isolation::clone3; use crate::pipe::WorkerPipe; tracing::info!(worker_id = self.id, "spawning pre-forked worker"); - // Create communication pipes + // Create pipes for communication let worker_pipe = WorkerPipe::new()?; let (parent_pipe, child_pipe) = worker_pipe.split(); - // TODO: Create cgroup for this worker and get fd - // For now, use -1 as placeholder (will be implemented with cgroups) - let cgroup_fd = -1; - - // Get namespace flags from config - let namespace_flags = self.config_to_namespace_flags(); - - // Clone config for child process + // Get namespace flags (but don't include them in clone3, we'll set them inside) + let namespace_flags = 0; // We'll enter namespaces from inside the worker let config = self.config.clone(); - // Clone the worker process with full isolation - let pid = clone3::clone_worker(cgroup_fd, namespace_flags, move || { - // Child process: Set up isolation and load Python + let pid = clone3::clone_worker(namespace_flags, move || { worker_main(child_pipe, &config) })?; - // Parent process: Store worker info self.pid = Some(pid); self.pipe = Some(parent_pipe); - self.cgroup_fd = Some(cgroup_fd); self.state = WorkerState::Idle; tracing::info!( @@ -100,7 +65,6 @@ impl Worker { Ok(()) } - /// Execute code in this worker via pipe pub fn execute(&mut self, code: &str) -> Result { if self.state != WorkerState::Idle { return Err(LeewardError::Execution(format!( @@ -117,14 +81,12 @@ impl Worker { self.state = WorkerState::Busy; tracing::debug!(worker_id = self.id, code_len = code.len(), "sending code to worker"); - // Send code via pipe pipe.send_code(code.as_bytes())?; + let result_bytes = pipe.recv_result()?; - // Receive result via pipe - let _result_bytes = pipe.recv_result()?; - - // TODO: Deserialize result from MessagePack - let result = ExecutionResult::default(); + // MessagePack deserialization + let result: ExecutionResult = rmp_serde::from_slice(&result_bytes) + .map_err(|e| LeewardError::Execution(format!("failed to deserialize result: {}", e)))?; self.execution_count += 1; self.state = WorkerState::Idle; @@ -138,36 +100,28 @@ impl Worker { Ok(result) } - /// Kill and recycle this worker pub fn recycle(&mut self) -> Result<()> { tracing::info!(worker_id = self.id, "recycling worker"); self.state = WorkerState::Recycling; - // Kill existing process if any if let Some(pid) = self.pid { unsafe { libc::kill(pid, libc::SIGKILL); } } - // Close pipe self.pipe = None; - - // Reset state self.pid = None; self.execution_count = 0; - // Spawn new worker self.spawn() } - /// Check if worker should be recycled based on execution count #[must_use] pub fn should_recycle(&self, max_executions: u64) -> bool { self.execution_count >= max_executions } - /// Convert config to namespace flags fn config_to_namespace_flags(&self) -> u64 { use nix::sched::CloneFlags; @@ -185,26 +139,63 @@ impl Worker { } } -/// Worker main function (runs in child process) -fn worker_main(mut pipe: crate::pipe::ChildPipe, _config: &SandboxConfig) -> Result<()> { - use crate::isolation::{LandlockConfig, SeccompConfig}; +fn worker_main(mut pipe: crate::pipe::ChildPipe, config: &SandboxConfig) -> Result<()> { + use crate::isolation::{LandlockConfig, SeccompConfig, NamespaceConfig}; - tracing::debug!("worker process starting, setting up isolation"); + tracing::debug!("worker process starting isolation setup"); - // Set up Landlock filesystem restrictions - LandlockConfig::default().apply()?; + // Step 1: Setup namespaces (critical for security) + let namespace_config = NamespaceConfig { + user: false, // User namespace needs UID mapping setup + pid: true, // Isolate process tree + mount: true, // Isolate filesystem + net: !config.allow_network, // Network isolation + ipc: true, // IPC isolation + uts: true, // Hostname isolation + }; - // Set up seccomp with NOTIFY mode - let _notify_fd = SeccompConfig::default().apply()?; + namespace_config.enter()?; + tracing::info!("namespaces configured"); - // TODO: Load Python interpreter - tracing::info!("worker isolation complete, loading Python"); + // Step 2: Apply Landlock filesystem restrictions (if available) + // Landlock requires Linux 5.13+, but that's okay - we try it + let mut landlock = LandlockConfig::default(); - // Enter idle loop, waiting for code via pipe - loop { - tracing::debug!("worker waiting for code"); + // Add Python path and libraries as executable + if let Some(python_dir) = config.python_path.parent() { + landlock = landlock.exec(python_dir).ro(python_dir); + } - // Receive code from daemon + // Add read-only paths + for path in &config.ro_binds { + landlock = landlock.ro(path); + } + + // Add read-write paths + for path in &config.rw_binds { + landlock = landlock.rw(path); + } + + // Add /tmp as read-write + landlock = landlock.rw("/tmp"); + + match landlock.apply() { + Ok(_) => tracing::info!("landlock restrictions applied"), + Err(e) => { + // Landlock is nice to have but not critical if we have seccomp + namespaces + tracing::warn!("landlock not available (kernel < 5.13?): {}", e); + } + } + + // Step 3: Apply seccomp filter (critical for security) + let seccomp = SeccompConfig::default(); + seccomp.apply()?; + tracing::info!("seccomp filter applied"); + + tracing::info!("worker fully isolated, entering main loop"); + + // Main worker loop + loop { let code = match pipe.recv_code() { Ok(code) => code, Err(e) => { @@ -213,20 +204,65 @@ fn worker_main(mut pipe: crate::pipe::ChildPipe, _config: &SandboxConfig) -> Res } }; - tracing::debug!(code_len = code.len(), "received code, executing"); + let exec_result = execute_python(&code, config); - // TODO: Execute code in Python - // For now, just echo back - let result = code; + let result_bytes = match rmp_serde::to_vec(&exec_result) { + Ok(bytes) => bytes, + Err(e) => { + tracing::error!("failed to serialize result: {}", e); + break; + } + }; - // Send result back to daemon - if let Err(e) = pipe.send_result(&result) { + if let Err(e) = pipe.send_result(&result_bytes) { tracing::error!("failed to send result: {}", e); break; } - - tracing::debug!("result sent, waiting for next code"); } Ok(()) } + +fn execute_python(code: &[u8], config: &SandboxConfig) -> ExecutionResult { + use std::process::{Command, Stdio}; + use std::time::Instant; + + let code_str = String::from_utf8_lossy(code); + let start = Instant::now(); + + let output = match Command::new(&config.python_path) + .arg("-c") + .arg(code_str.as_ref()) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + { + Ok(output) => output, + Err(e) => { + return ExecutionResult { + exit_code: -1, + stdout: Vec::new(), + stderr: format!("Failed to execute Python: {}", e).into_bytes(), + duration: start.elapsed(), + memory_peak: 0, + cpu_time_us: 0, + timed_out: false, + oom_killed: false, + }; + } + }; + + let duration = start.elapsed(); + + ExecutionResult { + exit_code: output.status.code().unwrap_or(-1), + stdout: output.stdout, + stderr: output.stderr, + duration, + memory_peak: 0, // TODO: Get from cgroup memory.peak + cpu_time_us: 0, // TODO: Get from /proc/[pid]/stat + timed_out: false, // TODO: Implement timeout handling + oom_killed: false, // TODO: Detect from cgroup events + } +} diff --git a/crates/leeward-daemon/src/main.rs b/crates/leeward-daemon/src/main.rs index cd17914..d1a6964 100644 --- a/crates/leeward-daemon/src/main.rs +++ b/crates/leeward-daemon/src/main.rs @@ -13,7 +13,6 @@ use tracing_subscriber::EnvFilter; mod config; mod iouring; mod pool; -mod protocol; mod server; use config::DaemonConfig; @@ -43,6 +42,29 @@ async fn main() -> Result<()> { // Remove existing socket let _ = std::fs::remove_file(&config.socket_path); + // Validate Python + let python_path = &config.sandbox_config.python_path; + + match std::process::Command::new(python_path) + .arg("--version") + .output() + { + Ok(output) => { + let version = String::from_utf8_lossy(&output.stdout) + .trim() + .to_string(); + let version = if version.is_empty() { + String::from_utf8_lossy(&output.stderr).trim().to_string() + } else { + version + }; + tracing::info!(python = ?python_path, version = %version, "Python ready"); + } + Err(e) => { + anyhow::bail!("Python not found or not executable: {}", e); + } + } + // Bind socket let listener = UnixListener::bind(&config.socket_path)?; tracing::info!(socket = ?config.socket_path, "listening"); diff --git a/crates/leeward-daemon/src/server.rs b/crates/leeward-daemon/src/server.rs index f21afd3..6e10506 100644 --- a/crates/leeward-daemon/src/server.rs +++ b/crates/leeward-daemon/src/server.rs @@ -1,6 +1,7 @@ //! Unix socket server -use crate::{config::DaemonConfig, pool::WorkerPool, protocol::{self, Request, Response}}; +use crate::{config::DaemonConfig, pool::WorkerPool}; +use leeward_core::protocol::{self, Request, Response}; use std::sync::Arc; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, diff --git a/flake.lock b/flake.lock index 88df767..c9ff4de 100644 --- a/flake.lock +++ b/flake.lock @@ -1,114 +1,5 @@ { "nodes": { - "cachix": { - "inputs": { - "devenv": [ - "devenv" - ], - "flake-compat": [ - "devenv", - "flake-compat" - ], - "git-hooks": [ - "devenv", - "git-hooks" - ], - "nixpkgs": [ - "devenv", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1760971495, - "narHash": "sha256-IwnNtbNVrlZIHh7h4Wz6VP0Furxg9Hh0ycighvL5cZc=", - "owner": "cachix", - "repo": "cachix", - "rev": "c5bfd933d1033672f51a863c47303fc0e093c2d2", - "type": "github" - }, - "original": { - "owner": "cachix", - "ref": "latest", - "repo": "cachix", - "type": "github" - } - }, - "devenv": { - "inputs": { - "cachix": "cachix", - "flake-compat": "flake-compat", - "flake-parts": "flake-parts", - "git-hooks": "git-hooks", - "nix": "nix", - "nixd": "nixd", - "nixpkgs": "nixpkgs" - }, - "locked": { - "lastModified": 1767288951, - "narHash": "sha256-160ZiJhibIkePTQ3wLjLcPgxseP78sF59psTTm5oLCQ=", - "owner": "cachix", - "repo": "devenv", - "rev": "7f7e03392c9ce626a9ef412d42b3bef2f7f8625e", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "devenv", - "type": "github" - } - }, - "flake-compat": { - "flake": false, - "locked": { - "lastModified": 1761588595, - "narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, - "flake-parts": { - "inputs": { - "nixpkgs-lib": [ - "devenv", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1760948891, - "narHash": "sha256-TmWcdiUUaWk8J4lpjzu4gCGxWY6/Ok7mOK4fIFfBuU4=", - "owner": "hercules-ci", - "repo": "flake-parts", - "rev": "864599284fc7c0ba6357ed89ed5e2cd5040f0c04", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "flake-parts", - "type": "github" - } - }, - "flake-root": { - "locked": { - "lastModified": 1723604017, - "narHash": "sha256-rBtQ8gg+Dn4Sx/s+pvjdq3CB2wQNzx9XGFq/JVGCB6k=", - "owner": "srid", - "repo": "flake-root", - "rev": "b759a56851e10cb13f6b8e5698af7b59c44be26e", - "type": "github" - }, - "original": { - "owner": "srid", - "repo": "flake-root", - "type": "github" - } - }, "flake-utils": { "inputs": { "systems": "systems" @@ -127,144 +18,13 @@ "type": "github" } }, - "git-hooks": { - "inputs": { - "flake-compat": [ - "devenv", - "flake-compat" - ], - "gitignore": "gitignore", - "nixpkgs": [ - "devenv", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1760663237, - "narHash": "sha256-BflA6U4AM1bzuRMR8QqzPXqh8sWVCNDzOdsxXEguJIc=", - "owner": "cachix", - "repo": "git-hooks.nix", - "rev": "ca5b894d3e3e151ffc1db040b6ce4dcc75d31c37", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "git-hooks.nix", - "type": "github" - } - }, - "gitignore": { - "inputs": { - "nixpkgs": [ - "devenv", - "git-hooks", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1709087332, - "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", - "owner": "hercules-ci", - "repo": "gitignore.nix", - "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "gitignore.nix", - "type": "github" - } - }, - "nix": { - "inputs": { - "flake-compat": [ - "devenv", - "flake-compat" - ], - "flake-parts": [ - "devenv", - "flake-parts" - ], - "git-hooks-nix": [ - "devenv", - "git-hooks" - ], - "nixpkgs": [ - "devenv", - "nixpkgs" - ], - "nixpkgs-23-11": [ - "devenv" - ], - "nixpkgs-regression": [ - "devenv" - ] - }, - "locked": { - "lastModified": 1766922625, - "narHash": "sha256-O0wExzdYqSNqbPYCQhUWeoKlDa7q6wxhuWiHolxqdl8=", - "owner": "cachix", - "repo": "nix", - "rev": "c62c4bdb6673871ae5cdc51c498df6292d5169aa", - "type": "github" - }, - "original": { - "owner": "cachix", - "ref": "devenv-2.32", - "repo": "nix", - "type": "github" - } - }, - "nixd": { - "inputs": { - "flake-parts": [ - "devenv", - "flake-parts" - ], - "flake-root": "flake-root", - "nixpkgs": [ - "devenv", - "nixpkgs" - ], - "treefmt-nix": "treefmt-nix" - }, - "locked": { - "lastModified": 1763964548, - "narHash": "sha256-JTRoaEWvPsVIMFJWeS4G2isPo15wqXY/otsiHPN0zww=", - "owner": "nix-community", - "repo": "nixd", - "rev": "d4bf15e56540422e2acc7bc26b20b0a0934e3f5e", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "nixd", - "type": "github" - } - }, "nixpkgs": { "locked": { - "lastModified": 1761313199, - "narHash": "sha256-wCIACXbNtXAlwvQUo1Ed++loFALPjYUA3dpcUJiXO44=", - "owner": "cachix", - "repo": "devenv-nixpkgs", - "rev": "d1c30452ebecfc55185ae6d1c983c09da0c274ff", - "type": "github" - }, - "original": { - "owner": "cachix", - "ref": "rolling", - "repo": "devenv-nixpkgs", - "type": "github" - } - }, - "nixpkgs_2": { - "locked": { - "lastModified": 1761114652, - "narHash": "sha256-f/QCJM/YhrV/lavyCVz8iU3rlZun6d+dAiC3H+CDle4=", + "lastModified": 1767116409, + "narHash": "sha256-5vKw92l1GyTnjoLzEagJy5V5mDFck72LiQWZSOnSicw=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "01f116e4df6a15f4ccdffb1bcd41096869fb385c", + "rev": "cad22e7d996aea55ecab064e84834289143e44a0", "type": "github" }, "original": { @@ -274,40 +34,25 @@ "type": "github" } }, - "nixpkgs_3": { - "locked": { - "lastModified": 1744536153, - "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, "root": { "inputs": { - "devenv": "devenv", "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs_2", + "nixpkgs": "nixpkgs", "rust-overlay": "rust-overlay" } }, "rust-overlay": { "inputs": { - "nixpkgs": "nixpkgs_3" + "nixpkgs": [ + "nixpkgs" + ] }, "locked": { - "lastModified": 1766890375, - "narHash": "sha256-0Zi7ChAtjq/efwQYmp7kOJPcSt6ya9ynSUe6ppgZhsQ=", + "lastModified": 1767322002, + "narHash": "sha256-yHKXXw2OWfIFsyTjduB4EyFwR0SYYF0hK8xI9z4NIn0=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "91e1f7a0017065360f447622d11b7ce6ed04772f", + "rev": "03c6e38661c02a27ca006a284813afdc461e9f7e", "type": "github" }, "original": { @@ -330,28 +75,6 @@ "repo": "default", "type": "github" } - }, - "treefmt-nix": { - "inputs": { - "nixpkgs": [ - "devenv", - "nixd", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1734704479, - "narHash": "sha256-MMi74+WckoyEWBRcg/oaGRvXC9BVVxDZNRMpL+72wBI=", - "owner": "numtide", - "repo": "treefmt-nix", - "rev": "65712f5af67234dad91a5a4baee986a8b62dbf8f", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "treefmt-nix", - "type": "github" - } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 6f62a17..31b3cff 100644 --- a/flake.nix +++ b/flake.nix @@ -1,36 +1,235 @@ { - description = "Run untrusted Python code safely with native Linux isolation"; + description = "Linux-native sandbox for running untrusted code"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; flake-utils.url = "github:numtide/flake-utils"; - rust-overlay.url = "github:oxalica/rust-overlay"; - devenv.url = "github:cachix/devenv"; }; - outputs = inputs@{ self, nixpkgs, flake-utils, rust-overlay, devenv }: + outputs = { self, nixpkgs, rust-overlay, flake-utils }: let - nixosModules.default = import ./nix/module.nix; - in - flake-utils.lib.eachDefaultSystem (system: - let - overlays = [ (import rust-overlay) ]; - pkgs = import nixpkgs { inherit system overlays; }; - lib = import ./nix/lib.nix { inherit pkgs; }; - packages = import ./nix/packages.nix { inherit pkgs lib; }; - in - { - packages = { - default = packages.leeward-all; - cli = packages.leeward-cli; - daemon = packages.leeward-daemon; - ffi = packages.leeward-ffi; + supportedSystems = [ "x86_64-linux" "aarch64-linux" ]; + + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + + nixpkgsFor = forAllSystems (system: import nixpkgs { + inherit system; + overlays = [ rust-overlay.overlays.default ]; + }); + + # Read version from Cargo.toml + cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml); + version = cargoToml.workspace.package.version; + + buildLeeward = { pkgs, target ? null, static ? false }: + let + rustToolchain = if static then + pkgs.rust-bin.stable.latest.default.override { + targets = [ "x86_64-unknown-linux-musl" ]; + } + else + pkgs.rust-bin.stable.latest.default; + + buildInputs = with pkgs; [ + python3 + ] ++ lib.optionals static [ + pkgs.pkgsStatic.stdenv.cc + ]; + + nativeBuildInputs = with pkgs; [ + rustToolchain + pkg-config + ]; + + in pkgs.rustPlatform.buildRustPackage { + pname = "leeward"; + inherit version; + + src = pkgs.lib.cleanSource ./.; + + cargoLock.lockFile = ./Cargo.lock; + + inherit buildInputs nativeBuildInputs; + + CARGO_BUILD_TARGET = if static then "x86_64-unknown-linux-musl" else null; + CARGO_BUILD_RUSTFLAGS = if static then "-C target-feature=+crt-static" else ""; + + postInstall = '' + mkdir -p $out/bin + + if [ -f target/*/release/leeward-daemon ]; then + mv target/*/release/leeward-daemon $out/bin/ + mv target/*/release/leeward $out/bin/ + else + mv target/release/leeward-daemon $out/bin/ || true + mv target/release/leeward $out/bin/ || true + fi + + ${if static then "${pkgs.binutils}/bin/strip $out/bin/*" else ""} + + mkdir -p $out/lib/systemd/{system,user} + cp contrib/leeward.system.service $out/lib/systemd/system/leeward.service + cp contrib/leeward.user.service $out/lib/systemd/user/leeward.service + ''; + + meta = with pkgs.lib; { + description = "Linux-native sandbox for running untrusted code"; + homepage = "https://github.com/vektia/leeward"; + license = licenses.asl20; + platforms = [ "x86_64-linux" "aarch64-linux" ]; + mainProgram = "leeward"; + }; }; - devShells.default = devenv.lib.mkShell { - inherit inputs pkgs; - modules = [ (import ./nix/shell.nix) ]; + buildDeb = { pkgs, leeward }: + let + arch = if pkgs.stdenv.hostPlatform.system == "x86_64-linux" then "amd64" + else if pkgs.stdenv.hostPlatform.system == "aarch64-linux" then "arm64" + else throw "Unsupported architecture"; + in pkgs.stdenv.mkDerivation { + pname = "leeward-deb"; + inherit version; + + dontUnpack = true; + dontBuild = true; + + nativeBuildInputs = [ pkgs.dpkg ]; + + installPhase = '' + mkdir -p $out + mkdir -p deb/DEBIAN deb/usr/bin deb/usr/lib/systemd/{system,user} + + cp ${leeward}/bin/* deb/usr/bin/ + cp ${leeward}/lib/systemd/system/*.service deb/usr/lib/systemd/system/ + cp ${leeward}/lib/systemd/user/*.service deb/usr/lib/systemd/user/ + cat > deb/DEBIAN/control < + Description: Linux-native sandbox for running untrusted code + Homepage: https://github.com/vektia/leeward + Section: devel + Priority: optional + Depends: python3 + EOF + + cat > deb/DEBIAN/postinst <<'EOF' + #!/bin/sh + set -e + if ! getent passwd leeward >/dev/null; then + useradd -r -s /usr/sbin/nologin -d /nonexistent leeward + fi + mkdir -p /run/leeward + chown leeward:leeward /run/leeward + if [ -d /run/systemd/system ]; then + systemctl daemon-reload + fi + EOF + chmod 755 deb/DEBIAN/postinst + + dpkg-deb --build deb $out/leeward_${version}_${arch}.deb + ''; + }; + + in { + packages = forAllSystems (system: + let + pkgs = nixpkgsFor.${system}; + leeward = buildLeeward { inherit pkgs; }; + + in { + default = leeward; + leeward-static = if system == "x86_64-linux" + then buildLeeward { inherit pkgs; static = true; } + else null; + leeward-deb = buildDeb { inherit pkgs leeward; }; + } // pkgs.lib.optionalAttrs (system == "x86_64-linux") { + leeward-x86_64 = leeward; + } // pkgs.lib.optionalAttrs (system == "aarch64-linux") { + leeward-aarch64 = leeward; + }); + + devShells = forAllSystems (system: + let + pkgs = nixpkgsFor.${system}; + rustToolchain = pkgs.rust-bin.stable.latest.default.override { + extensions = [ "rust-src" "rust-analyzer" ]; + }; + in { + default = pkgs.mkShell { + buildInputs = with pkgs; [ + rustToolchain + cargo-watch + cargo-audit + pkg-config + python3 + mold + clang + ]; + + RUST_SRC_PATH = "${rustToolchain}/lib/rustlib/src/rust/library"; + LEEWARD_SOCKET = "$PWD/.leeward.sock"; + + shellHook = ""; + }; + }); + + nixosModules.default = { config, lib, pkgs, ... }: + with lib; + let + cfg = config.services.leeward; + in { + options.services.leeward = { + enable = mkEnableOption "Leeward sandbox daemon"; + + workers = mkOption { + type = types.int; + default = 4; + description = "Number of pre-forked workers"; + }; + + package = mkOption { + type = types.package; + default = self.packages.${pkgs.system}.default; + description = "Leeward package to use"; + }; + }; + + config = mkIf cfg.enable { + systemd.services.leeward = { + description = "Leeward Sandbox Daemon"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + serviceConfig = { + Type = "simple"; + ExecStart = "${cfg.package}/bin/leeward-daemon --workers ${toString cfg.workers}"; + Restart = "always"; + RestartSec = 5; + User = "leeward"; + Group = "leeward"; + NoNewPrivileges = true; + ProtectSystem = "strict"; + ProtectHome = true; + PrivateTmp = true; + RuntimeDirectory = "leeward"; + RuntimeDirectoryMode = "0755"; + }; + }; + + users.users.leeward = { + isSystemUser = true; + group = "leeward"; + description = "Leeward daemon user"; + }; + + users.groups.leeward = {}; + }; }; - } - ) // { inherit nixosModules; }; + }; } \ No newline at end of file diff --git a/nix/lib.nix b/nix/lib.nix deleted file mode 100644 index 398fd95..0000000 --- a/nix/lib.nix +++ /dev/null @@ -1,14 +0,0 @@ -{ pkgs }: - -{ - cargoVersion = path: - let - cargoToml = builtins.fromTOML (builtins.readFile path); - in - cargoToml.workspace.package.version or cargoToml.package.version; - - buildDeps = with pkgs; [ - pkg-config - libseccomp - ]; -} \ No newline at end of file diff --git a/nix/module.nix b/nix/module.nix deleted file mode 100644 index 1df18fc..0000000 --- a/nix/module.nix +++ /dev/null @@ -1,71 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; - -let - cfg = config.services.leeward; - - leeward = pkgs.callPackage ./packages.nix { inherit pkgs; }; -in { - options.services.leeward = { - enable = mkEnableOption "leeward sandbox daemon"; - - package = mkOption { - type = types.package; - default = leeward.leeward-daemon; - description = "The leeward package to use."; - }; - - numWorkers = mkOption { - type = types.int; - default = 4; - description = "Number of worker processes in the pool."; - }; - - recycleAfter = mkOption { - type = types.int; - default = 100; - description = "Recycle workers after this many executions."; - }; - }; - - config = mkIf cfg.enable { - systemd.services.leeward = { - description = "Leeward sandbox daemon"; - documentation = [ "https://github.com/vektia/leeward" ]; - after = [ "network.target" ]; - wantedBy = [ "multi-user.target" ]; - - serviceConfig = { - Type = "simple"; - ExecStart = "${cfg.package}/bin/leeward-daemon"; - Restart = "on-failure"; - RestartSec = "5s"; - - # Security hardening - NoNewPrivileges = true; - PrivateTmp = true; - ProtectSystem = "strict"; - ProtectHome = true; - ReadWritePaths = [ "/run/leeward" ]; - - # Runtime directory - RuntimeDirectory = "leeward"; - RuntimeDirectoryMode = "0755"; - - # Logging - StandardOutput = "journal"; - StandardError = "journal"; - SyslogIdentifier = "leeward-daemon"; - }; - }; - - # Create group for socket access - users.groups.leeward = {}; - - # Ensure runtime directory permissions - systemd.tmpfiles.rules = [ - "d /run/leeward 0755 root leeward - -" - ]; - }; -} diff --git a/nix/packages.nix b/nix/packages.nix deleted file mode 100644 index faa7c79..0000000 --- a/nix/packages.nix +++ /dev/null @@ -1,52 +0,0 @@ -{ pkgs, lib }: - -let - version = lib.cargoVersion ../Cargo.toml; - buildDeps = lib.buildDeps; - src = pkgs.lib.cleanSource ../.; - - rustBuild = { - inherit version src; - cargoLock.lockFile = ../Cargo.lock; - nativeBuildInputs = with pkgs; [ clang mold ] ++ buildDeps; - buildInputs = buildDeps; - RUSTFLAGS = "-C link-arg=-fuse-ld=mold"; - }; - - leeward-cli = pkgs.rustPlatform.buildRustPackage (rustBuild // { - pname = "leeward-cli"; - cargoBuildFlags = [ "-p" "leeward-cli" ]; - cargoTestFlags = [ "-p" "leeward-cli" ]; - }); - - leeward-daemon = pkgs.rustPlatform.buildRustPackage (rustBuild // { - pname = "leeward-daemon"; - cargoBuildFlags = [ "-p" "leeward-daemon" ]; - cargoTestFlags = [ "-p" "leeward-daemon" ]; - }); - - leeward-ffi = pkgs.rustPlatform.buildRustPackage (rustBuild // { - pname = "leeward-ffi"; - cargoBuildFlags = [ "-p" "leeward-ffi" ]; - cargoTestFlags = [ "-p" "leeward-ffi" ]; - nativeBuildInputs = with pkgs; [ clang mold cbindgen ] ++ buildDeps; - - postInstall = '' - mkdir -p $out/lib $out/include - cp target/release/libleeward.so $out/lib/ 2>/dev/null || true - cp target/release/libleeward.a $out/lib/ 2>/dev/null || true - if [ -f include/leeward.h ]; then - cp include/leeward.h $out/include/ - fi - ''; - }); - -in -{ - inherit leeward-cli leeward-daemon leeward-ffi; - - leeward-all = pkgs.symlinkJoin { - name = "leeward-${version}"; - paths = [ leeward-cli leeward-daemon leeward-ffi ]; - }; -} \ No newline at end of file diff --git a/nix/shell.nix b/nix/shell.nix deleted file mode 100644 index 5254adc..0000000 --- a/nix/shell.nix +++ /dev/null @@ -1,19 +0,0 @@ -{ pkgs, config, ... }: - -{ - packages = with pkgs; [ - cargo-watch - pkg-config - libseccomp - mold - ]; - - languages.rust.enable = true; - - env = { - LIBSECCOMP_LINK_TYPE = "dylib"; - LIBSECCOMP_LIB_PATH = "${pkgs.libseccomp}/lib"; - PKG_CONFIG_PATH = "${pkgs.libseccomp}/lib/pkgconfig"; - LEEWARD_SOCKET = "${config.env.DEVENV_STATE}/leeward.sock"; - }; -} \ No newline at end of file