Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions crates/capsec-core/src/has.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ macro_rules! impl_ambient {
}
}

// If you add a permission here, also add it to the
// `ambient_covers_all_permissions` test at the bottom of this file.
impl_ambient!(
FsRead, FsWrite, FsAll, NetConnect, NetBind, NetAll, EnvRead, EnvWrite, Spawn
);
Expand Down Expand Up @@ -256,4 +258,27 @@ mod tests {
use std::mem::size_of;
assert_eq!(size_of::<Cap<(FsRead, NetConnect)>>(), 0);
}

/// Compile-time proof that Cap<Ambient> satisfies Has<P> for every permission.
/// If a new permission is added to permission.rs but not to impl_ambient!,
/// this test fails to compile.
#[test]
fn ambient_covers_all_permissions() {
fn assert_ambient_has<P: Permission>()
where
Cap<Ambient>: Has<P>,
{
}

assert_ambient_has::<FsRead>();
assert_ambient_has::<FsWrite>();
assert_ambient_has::<FsAll>();
assert_ambient_has::<NetConnect>();
assert_ambient_has::<NetBind>();
assert_ambient_has::<NetAll>();
assert_ambient_has::<EnvRead>();
assert_ambient_has::<EnvWrite>();
assert_ambient_has::<Spawn>();
// Ambient itself is covered by the direct impl<P> Has<P> for Cap<P>
}
}
67 changes: 67 additions & 0 deletions crates/capsec-std/src/file.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//! Restricted file handles that enforce capability boundaries.
//!
//! Unlike `std::fs::File` which implements both `Read` and `Write`,
//! these wrappers only expose the I/O trait matching the permission
//! used to obtain them.
//!
//! - [`ReadFile`] — returned by [`open()`](crate::fs::open), implements `Read` + `Seek`
//! - [`WriteFile`] — returned by [`create()`](crate::fs::create), implements `Write` + `Seek`

use std::fs::File;
use std::io::{self, Read, Seek, Write};

/// A file handle that only supports reading.
///
/// Returned by [`capsec::fs::open()`](crate::fs::open). Implements `Read`
/// and `Seek`, but NOT `Write`.
///
/// Wraps a `std::fs::File` internally. Zero overhead beyond the File itself.
pub struct ReadFile(File);

impl ReadFile {
pub(crate) fn new(file: File) -> Self {
Self(file)
}
}

impl Read for ReadFile {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.0.read(buf)
}
}

impl Seek for ReadFile {
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
self.0.seek(pos)
}
}

/// A file handle that only supports writing.
///
/// Returned by [`capsec::fs::create()`](crate::fs::create). Implements `Write`
/// and `Seek`, but NOT `Read`.
///
/// Wraps a `std::fs::File` internally. Zero overhead beyond the File itself.
pub struct WriteFile(File);

impl WriteFile {
pub(crate) fn new(file: File) -> Self {
Self(file)
}
}

impl Write for WriteFile {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.write(buf)
}

fn flush(&mut self) -> io::Result<()> {
self.0.flush()
}
}

impl Seek for WriteFile {
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
self.0.seek(pos)
}
}
18 changes: 9 additions & 9 deletions crates/capsec-std/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
//! let data = capsec_std::fs::read("/tmp/data.bin", &cap).unwrap();
//! ```

use crate::file::{ReadFile, WriteFile};
use capsec_core::cap::Cap;
use capsec_core::error::CapSecError;
use capsec_core::has::Has;
Expand Down Expand Up @@ -113,19 +114,18 @@ pub fn copy(
Ok(std::fs::copy(from, to)?)
}

/// Opens a file for reading. Returns a `std::fs::File`.
/// Opens a file for reading. Returns a [`ReadFile`] that implements `Read` + `Seek`
/// but NOT `Write`, enforcing the capability boundary beyond the function call.
/// Requires [`FsRead`] permission.
pub fn open(path: impl AsRef<Path>, cap: &impl Has<FsRead>) -> Result<std::fs::File, CapSecError> {
pub fn open(path: impl AsRef<Path>, cap: &impl Has<FsRead>) -> Result<ReadFile, CapSecError> {
let _proof: Cap<FsRead> = cap.cap_ref();
Ok(std::fs::File::open(path)?)
Ok(ReadFile::new(std::fs::File::open(path)?))
}

/// Creates or truncates a file for writing. Returns a `std::fs::File`.
/// Creates or truncates a file for writing. Returns a [`WriteFile`] that implements
/// `Write` + `Seek` but NOT `Read`, enforcing the capability boundary beyond the function call.
/// Requires [`FsWrite`] permission.
pub fn create(
path: impl AsRef<Path>,
cap: &impl Has<FsWrite>,
) -> Result<std::fs::File, CapSecError> {
pub fn create(path: impl AsRef<Path>, cap: &impl Has<FsWrite>) -> Result<WriteFile, CapSecError> {
let _proof: Cap<FsWrite> = cap.cap_ref();
Ok(std::fs::File::create(path)?)
Ok(WriteFile::new(std::fs::File::create(path)?))
}
1 change: 1 addition & 0 deletions crates/capsec-std/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
//! the capabilities it declares.

pub mod env;
pub mod file;
pub mod fs;
pub mod net;
pub mod process;
23 changes: 23 additions & 0 deletions crates/capsec-tests/tests/type_system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -409,3 +409,26 @@ fn requires_impl_still_works() {
let cap = root.fs_read();
fn_with_requires_impl(&cap);
}

// ============================================================================
// K. RESTRICTED FILE HANDLES
// ============================================================================

#[test]
fn open_returns_readable_file() {
let root = test_root();
let cap = root.fs_read();
let mut file = capsec_std::fs::open("/dev/null", &cap).unwrap();
let mut buf = Vec::new();
std::io::Read::read_to_end(&mut file, &mut buf).unwrap();
}

#[test]
fn create_returns_writable_file() {
let root = test_root();
let cap = root.fs_write();
let path = std::env::temp_dir().join("capsec-test-create");
let mut file = capsec_std::fs::create(&path, &cap).unwrap();
std::io::Write::write_all(&mut file, b"test").unwrap();
std::fs::remove_file(&path).ok();
}
1 change: 1 addition & 0 deletions crates/capsec/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ pub use capsec_macro::{context, deny, main, requires};

/// Capability-gated filesystem operations. See [`capsec_std::fs`].
pub mod fs {
pub use capsec_std::file::{ReadFile, WriteFile};
pub use capsec_std::fs::*;
}

Expand Down
10 changes: 10 additions & 0 deletions crates/capsec/tests/compile_fail/create_cannot_read.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/// capsec::fs::create() returns WriteFile, which does not implement Read.
use std::io::Read;

fn main() {
let root = capsec::root();
let cap = root.fs_write();
let mut file = capsec::fs::create("/tmp/test.txt", &cap).unwrap();
let mut buf = Vec::new();
file.read_to_end(&mut buf).unwrap();
}
5 changes: 5 additions & 0 deletions crates/capsec/tests/compile_fail/create_cannot_read.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error[E0599]: no method named `read_to_end` found for struct `WriteFile` in the current scope
--> tests/compile_fail/create_cannot_read.rs:9:10
|
9 | file.read_to_end(&mut buf).unwrap();
| ^^^^^^^^^^^ method not found in `WriteFile`
9 changes: 9 additions & 0 deletions crates/capsec/tests/compile_fail/open_cannot_write.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/// capsec::fs::open() returns ReadFile, which does not implement Write.
use std::io::Write;

fn main() {
let root = capsec::root();
let cap = root.fs_read();
let mut file = capsec::fs::open("/tmp/test.txt", &cap).unwrap();
file.write_all(b"nope").unwrap();
}
5 changes: 5 additions & 0 deletions crates/capsec/tests/compile_fail/open_cannot_write.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error[E0599]: no method named `write_all` found for struct `ReadFile` in the current scope
--> tests/compile_fail/open_cannot_write.rs:8:10
|
8 | file.write_all(b"nope").unwrap();
| ^^^^^^^^^ method not found in `ReadFile`
Loading