Library / SDK
sunfishcode/cap-std avatar
sunfishcode/cap-std

cap-std: replacing Rust ambient authority with a directory handle

Capability-oriented version of the Rust standard library

834 stars60 forksRustNOASSERTION

At a glance

What is it?
cap-std is a family of Rust crates that provide capability-based versions of the standard library, where a program opens files through a Dir it was handed rather than by naming an absolute path. It defends against path traversal and symlink escapes, and its own documentation is explicit that it is not a sandbox for untrusted Rust code.
Who is it for?
cap-std suits Rust programs that take paths from users, plugins or configuration and want to bound what those paths can reach without changing the host process or adding a sandbox layer. It is the wrong choice for untrusted Rust code, since the documentation states that such code can use unsafe or the unsandboxed std::fs APIs and escape entirely, and the wrong choice if you want a general security boundary, because cap-std constrains intent rather than confining a process.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 7 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Ambient authority replaced by a handle you were given

Ordinary Rust code has what the cap-std documentation calls ambient authority. The operating system gives programs resource handles, such as file descriptors, but a typical program can request any file or network handle simply by providing its name or address:

rust
let file = File::open("/anything/you/want.txt")?;

Access control may still exist around that, in the form of access control lists, namespaces, firewalls or virtualization, but the documentation's point is that those mechanisms are coarse grained and configured outside the application.

Capability based security removes that ambient authority. To open a file you need a Dir, which represents an open directory, and you open the file relative to it:

rust
let file = dir.open("the/thing.txt")?;

The security argument is not that the process becomes safe, it is that the authority becomes smaller. A program handed a Dir can only reach what is under that Dir, and the program that owns the Dir decides what to hand out. That is why the documentation says this lets application logic configure its own access without changing the behaviour of the whole host process, setting up a separate host process, or requiring external configuration.

For most Rust code this is a mechanical change at the boundary where a path string enters the program, and then a gradual replacement of path-taking calls with Dir-taking calls.

Traversal and symlink escapes end in PermissionDenied

The concrete win is that hostile path names stop working, and they stop working the same way regardless of the operating system.

The documentation gives two cases. Opening a path that escapes the directory:

rust
let hidden = dir.open("../hidden.txt")?;

and the more subtle one, where the escape is hidden behind a symlink created inside the directory:

rust
dir.symlink("/hidden.txt", "misdirection.txt")?;
let secret = dir.open("misdirection.txt")?;

Both return PermissionDenied errors. The second case is the one that matters most in practice, because a naive check that the requested string does not start with .. says nothing about where the path actually resolves, while a Dir based open resolves within the directory and refuses to leave it.

This is the vulnerability the project names in its own description: CWE-22, improper limitation of a pathname to a restricted directory, path traversal, which it notes is number eight in the 2021 CWE Top 25 Most Dangerous Software Weaknesses. The same mechanism is described as a way to prevent untrusted input from inducing a program to open /proc/self/mem on Linux.

The shape of the fix is worth noting. A Dir automatically protects against paths that might include .., symlinks, or absolute paths that might lead outside of that Dir, so the protection is a property of the type rather than a check you have to remember to write.

Where the first Dir comes from, and the one escape hatch

There is an obvious question if every resource needs another resource to obtain it, and the documentation answers it directly: how does one obtain the first resource?

There are three main ways. The cap-directories crate creates Dirs for config, cache and other data directories. The cap-tempfile crate creates Dirs for temporary directories. And Dir::open_ambient_dir opens a plain path.

The third is the one to look at closely, because the documentation flags it: this function is not sandboxed and may open any file the host process has access to.

So the trust boundary of the whole design sits at the point where you choose the first Dir. A program that derives its Dir from a directory the operating system or the user already trusts, such as the config or cache location from cap-directories, carries that trust into everything it does afterwards. A program that calls open_ambient_dir on a path that came from the network has simply moved the problem, and any Dir derived from that path inherits the exposure.

The pattern the documentation implies is that open_ambient_dir is a legitimate but pointed tool. It is how a command line program opens the directory the user named on the command line, and everything below that point is constrained. Using it repeatedly on untrusted input, or deriving several Dirs from a path an attacker controls, undoes the model.

A family of crates around one filesystem type

The project is organised around the eponymous cap-std crate, but it is really a workspace of capability oriented libraries, and each one has a distinct job.

cap-std itself provides capability based versions of the std APIs. cap-directories provides capability based access to standard application directories, and cap-tempfile to temporary directories. cap-fs-ext adds filesystem features beyond what is available in std, cap-time-ext adds time features beyond std, cap-rand gives capability based access to random number generators, and cap-net-ext adds network features beyond std.

There is also a cap-std-ext crate, noted as maintained independently, which includes further extension APIs for both filesystem APIs, including atomic create and replace on Linux specifically, and for passing file descriptors to child processes.

That last item is the one to notice. Passing a file descriptor to a child process is exactly the operation that breaks the containment of a capability, because a descriptor handed to a child is authority the child can reuse. Handling it inside the capability model, rather than around it, is why the extension crate matters.

The workspace layout in the repository mirrors the crate list, with cap-primitives as the shared foundation directory, and it also carries benches, fuzz, tests, examples and media directories. The workspace package itself is named cap-std-workspace, carries publish = false, and the individual crates are what get published.

What cap-std explicitly is not

The documentation is unusually clear about the limit, and it is worth quoting the shape of it. The project states that cap-std is not a sandbox for untrusted Rust code, and that among other things, untrusted Rust code could use unsafe or the unsandboxed APIs in std::fs.

That is the honest boundary. cap-std constrains code that opts into it. It cannot constrain code that does not, and in Rust a dependency can simply call std::fs::open on an absolute path. The same applies to unsafe blocks, which can do anything the platform allows.

The framing the documentation uses is that cap-std allows code to declare its intent and to opt in to protection from malicious path names. Code which takes a Dir from which to open files, rather than taking bare filenames, declares its intent to only open files underneath that Dir.

That is a narrower and more achievable goal than sandboxing, and it fits how Rust programs are usually structured: the path handling happens in a few identifiable places, and the rest of the program deals in Dirs. It also means the protection is only as complete as the migration. A single remaining call to std::fs with a user supplied path is a hole, and nothing in the type system marks it for you.

Real uses: serving static files, test directories and WASI

The documentation lists several examples of cap-std in use, and they differ in what they consider the primary benefit.

As a sandbox, there is a port of tide that uses cap-std to access static files, where it prevents path resolution from following symlinks outside the designated root directory. The referenced diff shows the kinds of changes needed to use the API, which is the most useful thing here: it shows what a migration actually looks like in a real web server.

As a general purpose Dir type for working with directories, the io-streams crate uses cap-tempdir to create temporary directories for unit tests. Here the documentation is candid that the main benefit is convenience, letting tests say dir.open(...) instead of using open(path.join(...)) or dealing with chdir and global mutable state, and that the fact that it also sandboxes the unit tests is a nice side effect. That is a good description of how cap-std behaves when adopted for ergonomics rather than security.

As an application data store, the kv-cli example is a program using cap-directories and cap-std APIs to store application specific data, and the repository ships that example alongside a std_fs_misc one.

And the largest deployment is infrastructural. cap-std is a foundation for the WASI implementation in Wasmtime, providing sandboxing and support for Linux, macOS, Windows and more. That is the case with the most users, and it is also the one where the type-level discipline is enforced across a whole runtime rather than adopted by choice.

Versions, licence terms and what you get from crates.io

The workspace is at version 4.0.3, edition 2021, with rust-version 1.70, and the root package is not itself published, since the crates are what you depend on. There are no GitHub releases, so the version you use is the version on crates.io, and the last push to the repository was on 2026-10-19.

The feature flags are worth knowing because they change behaviour rather than adding functionality. The default feature set is empty, and the two optional flags are fs_utf8, which enables UTF-8 path handling in the filesystem crates, and arf_strings, which enables arbitrary Rust strings in the filesystem, time and tempfile crates. The default being empty means the safe behaviour is what you get without asking for anything.

The licence is a custom combination that GitHub cannot classify, which is why the repository description flags it that way. The workspace manifest states it as a three way choice:

toml
license = "Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT"
rust-version = "1.70"

The files at the root match: LICENSE-APACHE, LICENSE-Apache-2.0_WITH_LLVM-exception and LICENSE-MIT, alongside a COPYRIGHT file. The Apache 2.0 with the LLVM exception variant exists for compatibility with the Rust compiler ecosystem, and the MIT option is the permissive alternative if a downstream dependency requires it. Which of the three applies to your build depends on your own dependency tree rather than on cap-std alone, so this is a question for your counsel rather than a conclusion, though the structure itself is unusual and worth reading directly.

Editorial conclusion

cap-std suits Rust programs that take paths from users, plugins or configuration and want to bound what those paths can reach without changing the host process or adding a sandbox layer. It is the wrong choice for untrusted Rust code, since the documentation states that such code can use unsafe or the unsandboxed std::fs APIs and escape entirely, and the wrong choice if you want a general security boundary, because cap-std constrains intent rather than confining a process. Verify three things before you depend on it: that every path your program opens comes through a Dir rather than through std::fs, that your entry point for the first Dir is one of the three documented ways rather than a blanket open_ambient_dir call, and that the licence combination you are relying on, Apache-2.0 with the LLVM exception or Apache-2.0 or MIT, matches your distribution.

Frequently asked questions

What is cap-std used for in Rust?

It provides capability-based versions of the standard library APIs, so a program opens files through a Dir it was given instead of naming an absolute path. That removes ambient authority and lets application code configure its own access limits.

How do I get the first Dir in cap-std?

The documentation lists three ways: the cap-directories crate for config, cache and other data directories, the cap-tempfile crate for temporary directories, and Dir::open_ambient_dir for a plain path, which is not sandboxed and may open any file the host process can reach.

Does cap-std stop path traversal attacks?

Yes. The project states that cap-std provides protection against CWE-22, path traversal, which it notes is number eight in the 2021 CWE Top 25, and that attempts to open a path escaping the Dir, directly or through a symlink, return PermissionDenied.

Is cap-std a sandbox for untrusted Rust code?

No. The documentation states plainly that cap-std is not a sandbox for untrusted Rust code, since such code could use unsafe or the unsandboxed APIs in std::fs. It works for code that opts in to it.

Which Rust version and licence does cap-std target?

The workspace manifest sets edition 2021 and rust-version 1.70, and the licence is offered as Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT, with the matching licence files at the root of the repository.

Official sources

  1. Issues
  2. README
  3. sunfishcode/cap-std on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/sunfishcode-cap-std.svg)](https://hysenlabs.com/projects/sunfishcode-cap-std)

Was this useful?