Files
dagx/SECURITY.md
T

8.1 KiB

Security Policy

Overview

dagx is a task execution library designed for safety and predictability. This document outlines security considerations, best practices, and limitations you should be aware of when using dagx in security-sensitive contexts.

Supported Versions

Version Supported
0.1.x ✅

Security Model

What dagx Provides

  • Compile-time type safety: Dependencies are validated at compile time, preventing type errors
  • Cycle detection: Runtime detection of circular dependencies
  • Memory safety: Rust's ownership system prevents memory corruption
  • Thread safety: Interior mutability uses parking_lot::Mutex for safe concurrent access

What dagx Does NOT Provide

  • Panic isolation: Panics in one task will propagate and may affect other tasks (see Panic Handling below)
  • Resource limits: No built-in timeout, memory limits, or CPU throttling
  • Sandboxing: Tasks run with full process privileges
  • Authentication/Authorization: No access control between tasks
  • Input validation: Tasks must validate their own inputs

Security Considerations

1. Panic Handling

Current Behavior: dagx does NOT isolate panics between tasks. If a task panics:

  • The panic will propagate through the executor
  • Other tasks in the same execution layer may or may not complete (timing-dependent)
  • The entire DAG execution will terminate

Recommendation:

  • Use std::panic::catch_unwind in tasks that may panic
  • Return Result<T, E> from tasks and handle errors explicitly
  • Test panic scenarios thoroughly
use dagx::{task, Task};
use std::panic;

struct SafeTask;

#[task]
impl SafeTask {
    async fn run(&mut self) -> Result<i32, String> {
        // Catch panics and convert to Result
        panic::catch_unwind(|| {
            // Potentially panicking code
            42
        })
        .map_err(|_| "Task panicked".to_string())
    }
}

2. Resource Limits

dagx does NOT enforce:

  • Execution timeouts
  • Memory limits
  • CPU usage limits
  • Task queue depth limits

Recommendation: Implement resource limits at the task level using your async runtime's facilities:

use dagx::{task, Task};
use tokio::time::{timeout, Duration};

struct TimeLimitedTask;

#[task]
impl TimeLimitedTask {
    async fn run(&mut self) -> Result<i32, String> {
        timeout(Duration::from_secs(30), async {
            // Your task logic here
            expensive_computation().await
        })
        .await
        .map_err(|_| "Task timed out".to_string())
    }
}

async fn expensive_computation() -> i32 {
    // Actual work
    42
}

3. Untrusted Input

If your DAG processes untrusted input:

  • Validate all inputs at task boundaries
  • Sanitize outputs before using in security-sensitive contexts
  • Limit resource consumption (see Resource Limits above)
  • Use strongly-typed inputs to leverage compile-time safety
use dagx::{task, Task};

struct ValidatedInput {
    max_size: usize,
}

#[task]
impl ValidatedInput {
    async fn run(&mut self, input: &String) -> Result<String, String> {
        // Validate input size
        if input.len() > self.max_size {
            return Err(format!("Input exceeds maximum size of {}", self.max_size));
        }

        // Validate input format
        if !input.chars().all(|c| c.is_alphanumeric()) {
            return Err("Input contains invalid characters".to_string());
        }

        Ok(input.clone())
    }
}

4. Dependency Injection

Tasks receive dependencies as references, which:

  • Prevents modification of upstream results (read-only access)
  • Ensures consistency across multiple dependent tasks
  • Avoids data races through Rust's borrow checker

However, if your task output contains interior mutability (e.g., Arc<Mutex<T>>), be aware that:

  • Multiple downstream tasks can mutate shared state
  • This is intentional but requires careful synchronization

5. State Management

Tasks with mutable state (&mut self):

  • Execute exactly once per DAG run
  • Have exclusive access to their internal state during execution
  • Are not reentrant (cannot execute concurrently with themselves)

Safe: Each task instance maintains isolated state Unsafe: Sharing mutable state between tasks via global variables or Arc<Mutex<T>> requires manual synchronization

Best Practices

1. Error Handling

Use Result<T, E> for all fallible operations:

use dagx::{task, Task};

struct FallibleTask;

#[task]
impl FallibleTask {
    async fn run(&mut self, input: &String) -> Result<i32, String> {
        input.parse::<i32>()
            .map_err(|e| format!("Parse error: {}", e))
    }
}

2. Timeouts

Implement timeouts for long-running or potentially-hanging operations:

use tokio::time::{timeout, Duration};

#[task]
impl MyTask {
    async fn run(&mut self) -> Result<String, String> {
        timeout(Duration::from_secs(10), async {
            // Network request or other potentially-slow operation
            fetch_data().await
        })
        .await
        .map_err(|_| "Operation timed out".to_string())?
    }
}

async fn fetch_data() -> Result<String, String> {
    Ok("data".to_string())
}

3. Resource Cleanup

Use RAII patterns and Drop implementations for resource cleanup:

struct FileTask {
    path: String,
}

#[task]
impl FileTask {
    async fn run(&mut self) -> Result<String, String> {
        let content = tokio::fs::read_to_string(&self.path)
            .await
            .map_err(|e| format!("File read error: {}", e))?;
        Ok(content)
    }
}
// File handle automatically closed when task completes

4. Logging and Monitoring

Add logging to tasks for observability:

use dagx::{task, Task};

struct MonitoredTask;

#[task]
impl MonitoredTask {
    async fn run(&mut self, input: &i32) -> i32 {
        println!("Task started with input: {}", input);
        let result = input * 2;
        println!("Task completed with result: {}", result);
        result
    }
}

5. Minimize Privilege

  • Run dagx applications with minimum necessary privileges
  • Avoid running as root or with elevated permissions
  • Use operating system security features (containers, namespaces, etc.)

Security Limitations

Known Limitations

  1. No panic recovery: Task panics will crash the DAG execution
  2. No resource limits: Tasks can consume unlimited CPU, memory, or time
  3. No isolation: All tasks run in the same process with same privileges
  4. Shared executor: Tasks compete for executor resources

Not Security Features

dagx is not designed to:

  • Run untrusted code safely
  • Enforce resource quotas
  • Provide sandboxing
  • Implement authentication or authorization
  • Prevent side-channel attacks

If you need these features, consider:

  • WebAssembly sandboxing (wasmtime, wasmer)
  • Container isolation (Docker, podman)
  • Operating system isolation (VMs, seccomp, AppArmor)
  • Dedicated security runtimes (gVisor, Firecracker)

Reporting a Vulnerability

If you discover a security vulnerability in dagx:

  1. DO NOT open a public GitHub issue
  2. Email the maintainer directly: steve@waits.net
  3. Include:
    • Description of the vulnerability
    • Steps to reproduce
    • Potential impact
    • Suggested fix (if available)

You should receive a response within 48 hours. If the vulnerability is confirmed:

  • A fix will be developed privately
  • A security advisory will be published
  • Credit will be given (unless you prefer anonymity)

Security Updates

Security updates will be:

  • Released as patch versions (e.g., 0.1.1)
  • Announced in GitHub releases
  • Published to crates.io immediately
  • Documented in CHANGELOG.md

Additional Resources

License

This security policy is part of the dagx project and is licensed under the MIT License.