Skip to content

Commit c323f69

Browse files
authored
Merge pull request #31 from bordumb/dev-tokioSpawnWrapper
feat: add spawn_with wrapper and tokio guide to capsec-tokio
2 parents 8268f03 + df4d534 commit c323f69

6 files changed

Lines changed: 202 additions & 6 deletions

File tree

crates/capsec-tokio/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ keywords = ["security", "capability", "tokio", "async"]
99
categories = ["asynchronous", "rust-patterns"]
1010

1111
[features]
12+
rt = ["dep:tokio", "tokio/rt"]
1213
fs = ["dep:tokio", "tokio/fs"]
1314
net = ["dep:tokio", "tokio/net"]
1415
process = ["dep:tokio", "tokio/process"]
1516
io-util = ["dep:tokio", "tokio/io-util"]
16-
full = ["fs", "net", "process", "io-util"]
17+
full = ["rt", "fs", "net", "process", "io-util"]
1718

1819
[dependencies]
1920
capsec-core.workspace = true

crates/capsec-tokio/README.md

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,52 @@ You probably want to depend on the `capsec` facade crate with the `tokio` featur
1010
capsec = { version = "0.1", features = ["tokio"] }
1111
```
1212

13-
## Example
13+
## Capsec + Tokio Guide
14+
15+
There are three common patterns for using capabilities in async code.
16+
17+
### Pattern 1: Scoped proof (no spawn)
18+
19+
When your async code runs on the current task (no `tokio::spawn`), pass capabilities by reference. The scope-before-await pattern inside each wrapper keeps futures `Send` automatically.
20+
21+
```rust,ignore
22+
async fn handle_request(cap: &impl Has<FsRead>) {
23+
let data = capsec::tokio::fs::read("/tmp/config.toml", cap).await?;
24+
// use data...
25+
}
26+
```
27+
28+
### Pattern 2: `spawn_with` (single capability)
29+
30+
When spawning a task that needs one capability, use `capsec_tokio::task::spawn_with`. It converts `Cap<P>` to `SendCap<P>` for you — no need to call `.make_send()` manually.
1431

1532
```rust,ignore
16-
use capsec::prelude::*;
33+
let cap = root.grant::<FsRead>();
1734
35+
let handle = capsec::tokio::task::spawn_with(cap, |cap| async move {
36+
capsec::tokio::fs::read("/tmp/data.bin", &cap).await.unwrap()
37+
});
38+
39+
let data = handle.await?;
40+
```
41+
42+
### Pattern 3: `Arc` context (multiple capabilities)
43+
44+
When a spawned task needs multiple capabilities, use `#[capsec::context(send)]` to create a `Send`-safe context struct, wrap it in `Arc`, and clone for each task.
45+
46+
```rust,ignore
1847
#[capsec::context(send)]
1948
struct AppCtx {
2049
fs: FsRead,
50+
net: NetConnect,
2151
}
2252
23-
async fn load_config(ctx: &AppCtx) -> Result<String, CapSecError> {
24-
capsec::tokio::fs::read_to_string("/etc/app/config.toml", ctx).await
25-
}
53+
let ctx = Arc::new(AppCtx::new(&root));
54+
55+
let ctx2 = Arc::clone(&ctx);
56+
tokio::spawn(async move {
57+
let data = capsec::tokio::fs::read("/tmp/data", &*ctx2).await?;
58+
});
2659
```
2760

2861
## Modules
@@ -31,6 +64,7 @@ Enable via feature flags (`full` enables all):
3164

3265
| Module | Feature | Permission required | What it wraps |
3366
|--------|---------|-------------------|---------------|
67+
| `task` | `rt` || `tokio::spawn` — capability-aware task spawning |
3468
| `fs` | `fs` | `FsRead` / `FsWrite` | `tokio::fs` — read, write, delete, rename, copy, open, create |
3569
| `net` | `net` | `NetConnect` / `NetBind` | `tokio::net` — TCP connect, TCP/UDP bind |
3670
| `process` | `process` | `Spawn` | `tokio::process` — Command::new, run |

crates/capsec-tokio/src/lib.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,84 @@
1111
//!
1212
//! Enable via feature flags:
1313
//!
14+
//! - `rt` — task spawning with capability transfer ([`task::spawn_with`])
1415
//! - `fs` — async filesystem operations (`tokio::fs`)
1516
//! - `net` — async network operations (`tokio::net`)
1617
//! - `process` — async subprocess execution (`tokio::process`)
1718
//! - `full` — all of the above
19+
//!
20+
//! # Capsec + Tokio Guide
21+
//!
22+
//! There are three common patterns for using capabilities in async code,
23+
//! depending on whether you're spawning tasks and how many capabilities
24+
//! you need.
25+
//!
26+
//! ## Pattern 1: Scoped proof (no spawn)
27+
//!
28+
//! When your async code runs on the current task (no `tokio::spawn`), pass
29+
//! capabilities by reference. The scope-before-await pattern inside each
30+
//! wrapper keeps futures `Send` automatically.
31+
//!
32+
//! ```no_run
33+
//! use capsec_core::has::Has;
34+
//! use capsec_core::permission::FsRead;
35+
//!
36+
//! async fn handle_request(cap: &impl Has<FsRead>) {
37+
//! let data = capsec_tokio::fs::read("/tmp/config.toml", cap).await.unwrap();
38+
//! // ...
39+
//! }
40+
//! ```
41+
//!
42+
//! ## Pattern 2: `spawn_with` (single capability)
43+
//!
44+
//! When spawning a task that needs one capability, use
45+
//! [`task::spawn_with`]. It converts `Cap<P>` to `SendCap<P>` for you —
46+
//! no need to call `.make_send()` manually.
47+
//!
48+
//! ```no_run
49+
//! use capsec_core::permission::FsRead;
50+
//!
51+
//! # async fn example(root: capsec_core::root::CapRoot) {
52+
//! let cap = root.grant::<FsRead>();
53+
//!
54+
//! let handle = capsec_tokio::task::spawn_with(cap, |cap| async move {
55+
//! capsec_tokio::fs::read("/tmp/data.bin", &cap).await.unwrap()
56+
//! });
57+
//!
58+
//! let data = handle.await.unwrap();
59+
//! # }
60+
//! ```
61+
//!
62+
//! ## Pattern 3: `Arc` context (multiple capabilities)
63+
//!
64+
//! When a spawned task needs multiple capabilities, use
65+
//! `#[capsec::context(send)]` to create a `Send`-safe context struct,
66+
//! wrap it in `Arc`, and clone for each task.
67+
//!
68+
//! ```no_run
69+
//! use std::sync::Arc;
70+
//!
71+
//! // The `send` flag generates SendCap<P> fields instead of Cap<P>
72+
//! #[capsec_macro::context(send)]
73+
//! struct AppCtx {
74+
//! fs: capsec_core::permission::FsRead,
75+
//! net: capsec_core::permission::NetConnect,
76+
//! }
77+
//!
78+
//! # async fn example(root: capsec_core::root::CapRoot) {
79+
//! let ctx = Arc::new(AppCtx::new(&root));
80+
//!
81+
//! let ctx2 = Arc::clone(&ctx);
82+
//! tokio::spawn(async move {
83+
//! let data = capsec_tokio::fs::read("/tmp/data", &*ctx2).await.unwrap();
84+
//! });
85+
//!
86+
//! let ctx3 = Arc::clone(&ctx);
87+
//! tokio::spawn(async move {
88+
//! let stream = capsec_tokio::net::tcp_connect("127.0.0.1:8080", &*ctx3).await.unwrap();
89+
//! });
90+
//! # }
91+
//! ```
1892
1993
#[cfg(feature = "fs")]
2094
pub mod file;
@@ -24,3 +98,5 @@ pub mod fs;
2498
pub mod net;
2599
#[cfg(feature = "process")]
26100
pub mod process;
101+
#[cfg(feature = "rt")]
102+
pub mod task;

crates/capsec-tokio/src/task.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
//! Task spawning with capability transfer.
2+
//!
3+
//! [`spawn_with`] is a convenience wrapper around [`tokio::spawn`] that handles
4+
//! the `Cap<P>` → `SendCap<P>` conversion automatically. Without it, users
5+
//! must manually call `.make_send()` before spawning — a common source of
6+
//! confusing `!Send` compiler errors.
7+
//!
8+
//! # Example
9+
//!
10+
//! ```no_run
11+
//! use capsec_core::permission::FsRead;
12+
//!
13+
//! # async fn example(root: capsec_core::root::CapRoot) {
14+
//! let cap = root.grant::<FsRead>();
15+
//!
16+
//! let handle = capsec_tokio::task::spawn_with(cap, |cap| async move {
17+
//! capsec_tokio::fs::read("/tmp/data.bin", &cap).await.unwrap()
18+
//! });
19+
//!
20+
//! let data = handle.await.unwrap();
21+
//! # }
22+
//! ```
23+
24+
use capsec_core::cap::{Cap, SendCap};
25+
use capsec_core::permission::Permission;
26+
use std::future::Future;
27+
28+
/// Spawns a tokio task with a capability, handling the `Send` conversion.
29+
///
30+
/// Takes a `Cap<P>` (which is `!Send`), converts it to a `SendCap<P>`, and
31+
/// passes it into the closure that produces the spawned future. This removes
32+
/// the need to manually call `.make_send()`.
33+
///
34+
/// # Example
35+
///
36+
/// ```no_run
37+
/// use capsec_core::permission::FsRead;
38+
///
39+
/// # async fn example(root: capsec_core::root::CapRoot) {
40+
/// let cap = root.grant::<FsRead>();
41+
///
42+
/// let handle = capsec_tokio::task::spawn_with(cap, |cap| async move {
43+
/// capsec_tokio::fs::read("/tmp/data.bin", &cap).await.unwrap()
44+
/// });
45+
///
46+
/// let data = handle.await.unwrap();
47+
/// # }
48+
/// ```
49+
pub fn spawn_with<P, F, Fut, T>(cap: Cap<P>, f: F) -> tokio::task::JoinHandle<T>
50+
where
51+
P: Permission,
52+
F: FnOnce(SendCap<P>) -> Fut + Send + 'static,
53+
Fut: Future<Output = T> + Send + 'static,
54+
T: Send + 'static,
55+
{
56+
let send_cap = cap.make_send();
57+
tokio::spawn(f(send_cap))
58+
}

crates/capsec-tokio/tests/async_io.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,26 @@ async fn context_struct_works_with_async_wrappers() {
144144
let data = capsec_tokio::fs::read("/dev/null", &ctx).await.unwrap();
145145
assert!(data.is_empty());
146146
}
147+
148+
#[tokio::test]
149+
async fn spawn_with_single_cap() {
150+
let root = test_root();
151+
let cap = root.fs_read();
152+
let handle = capsec_tokio::task::spawn_with(cap, |cap| async move {
153+
capsec_tokio::fs::read("/dev/null", &cap).await.unwrap()
154+
});
155+
let data = handle.await.unwrap();
156+
assert!(data.is_empty());
157+
}
158+
159+
#[tokio::test]
160+
async fn spawn_with_future_is_send() {
161+
fn assert_send<T: Send>(_: &T) {}
162+
let root = test_root();
163+
let cap = root.fs_read();
164+
let handle = capsec_tokio::task::spawn_with(cap, |cap| async move {
165+
capsec_tokio::fs::read("/dev/null", &cap).await.unwrap()
166+
});
167+
assert_send(&handle);
168+
let _ = handle.await.unwrap();
169+
}

crates/capsec/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ pub mod process {
9090
/// ```
9191
#[cfg(feature = "tokio")]
9292
pub mod tokio {
93+
/// Task spawning with capability transfer.
94+
pub mod task {
95+
pub use capsec_tokio::task::*;
96+
}
9397
/// Async capability-gated filesystem operations.
9498
pub mod fs {
9599
pub use capsec_tokio::file::{AsyncReadFile, AsyncWriteFile};

0 commit comments

Comments
 (0)