This document is the threat model and security contract for path_jail. It
exists because every security library needs one: callers can only use the
library correctly if they know what it defends against and what it doesn't.
If you find a vulnerability, see Reporting a vulnerability below.
path_jail is designed to defend against an attacker who supplies path
strings to your application — for example:
- A web client uploading a file with a chosen filename
- A user-controlled config value naming a path inside a sandbox directory
- A workflow step naming a file inside a CI working directory
The attacker can supply:
- Arbitrary bytes in the path (including
.., leading/, null bytes, magic link prefixes like/proc/self/fd/N) - A pre-existing symlink inside the jail that points outside the jail
- A pre-existing hard link inside the jail that points to sensitive content
- Concurrent filesystem activity attempting to swap paths between validation and open (TOCTOU)
The attacker is assumed to not have:
- Privileges to mount filesystems, run as root inside the jail's filesystem,
call
ptrace, or otherwise escape the OS sandbox the application runs in - The ability to modify the running process's memory
- A working kernel exploit
If your attacker can do any of the above, no userspace library can help —
you need a process-level sandbox (seccomp, landlock, containers, VMs).
path_jail ships three API layers with different security/ergonomics
tradeoffs. Pick the strongest one your environment supports.
| Threat | Jail (default) |
secure-open |
guard (Linux 5.6+) |
|---|---|---|---|
Path traversal via .. |
✅ | ✅ | ✅ |
Absolute path injection (/etc/passwd) |
✅ | ✅ | ✅ |
| Null-byte injection | ✅ | ✅ | ✅ |
| Symlink target outside the jail | ✅ | ✅ | ✅ |
| Broken symlinks (cannot verify target) | ✅ | ✅ | ✅ |
| Symlink swap on final component (TOCTOU) | ❌ | ✅ | ✅ |
| Symlink swap on intermediate directories | ❌ | ❌ | ✅ |
| Concurrent rename of jail root mid-operation | ❌ | ❌ | ✅¹ |
Magic links (/proc/self/fd, /proc/self/root) |
❌ | ❌ | ✅ |
| Hard link to sensitive content (detect) | ❌² | ❌² | ✅³ |
| Bind-mount escape (opt-in) | ❌ | ❌ | ✅⁴ |
| Atomic open with kernel-enforced containment | ❌ | ❌ | ✅ |
| Signed attestation of the open event | ❌ | ❌ | ✅⁵ |
Footnotes:
guard::FdJailpins anO_DIRECTORY | O_NOFOLLOW | O_CLOEXECfd to the jail root at construction time. Subsequent renames or replacements of the root path do not affect the jail — all operations remain scoped to the original directory inode.- Hard links are not detectable in user space before open. The path-based
APIs do not stat the opened file and so cannot surface
nlink. guard::JailFile::has_hard_links()exposesnlink > 1from the post-openfstat. Policy is the caller's responsibility — a content-addressed store may legitimately use hard links. If your policy rejects hard links, checkhas_hard_links()before reading or writing.- Opt in with
OpenOptions::no_xdev(true)(maps toRESOLVE_NO_XDEV). Off by default to preserve directory-tree containment semantics, which are what most callers want. - Opt in by implementing the
Signertrait.path_jailships no crypto; bring your own (ed25519-dalek,ring, HSM client, KMS, etc.).
These threats are documented as not defended by any API:
- Privileged local attackers. A process with root or
CAP_SYS_ADMINon the host can mount, bind-mount, orptracearound any user-space check. - Kernel and filesystem exploits. A kernel-level bug, a FUSE filesystem
misbehaving, or an
openat2semantic regression in a specific kernel version are outside our control. We pin to documented kernel ABI. - Side-channel attacks. Timing, cache, filesystem-metadata leaks.
- Directory iteration (
read_dir) and recursive walks. Iterating jail contents has its own TOCTOU surface (rename-during-walk) that we do not currently address. If you walk a directory tree, treat it as untrusted input on every iteration. - Windows. No Windows-specific protections are implemented.
Jailandsecure-opencompile on Windows but provide no defenses beyond the cross-platform path-string checks;guardis Linux-only. - Unicode normalization. Paths are accepted byte-for-byte. We do not
normalize NFC/NFD on macOS or fold case on Windows/macOS. If your storage
layer is case-insensitive, treat
Report.PDFandreport.pdfas potentially the same file. - Resource exhaustion. Very long paths, deep symlink chains, etc. are
rejected by the kernel (
ENAMETOOLONG,ELOOP) butpath_jaildoes not impose its own limits.
┌──────────────────────────────────┐
You only need a validated │ Use `Jail::join` / `join_typed`. │
path (e.g., for logging) → │ Cheap, portable. │
└──────────────────────────────────┘
┌──────────────────────────────────┐
You open the file in │ Use `secure-open`. │
process, on Unix, and need │ Protects final-component swaps. │
final-component TOCTOU → └──────────────────────────────────┘
┌──────────────────────────────────┐
Security-critical opens on │ Use `guard` (Linux 5.6+). │
Linux, attestation needed, │ Kernel-enforced; signable. │
or hostile multi-tenant → └──────────────────────────────────┘
guard is the strongest. Use it on Linux where you can.
- We follow Semantic Versioning. In the
0.y.zpre-release series, minor-version bumps (0.4 → 0.5) may include breaking API changes; patch bumps (0.4.0 → 0.4.1) will not. Starting with1.0.0, the standard semver contract applies: only major bumps (1.x → 2.0) may break the public API. - Security fixes are issued on the latest minor line. We do not currently
backport to older
0.xlines; after1.0.0we will evaluate backports case-by-case for high-severity findings. - The MSRV (currently 1.85) may be bumped in any minor release in the
0.xseries. After1.0.0, MSRV bumps will be treated as minor-version changes and documented in the changelog.
Do not open a public GitHub issue for security bugs.
Use GitHub Private Vulnerability Reporting on the
tenuo-ai/path_jail repository
(Security tab → "Report a vulnerability"), or email security@tenuo.ai
with:
- A minimal reproducer (Rust code that demonstrates the issue)
- The affected
path_jailversion and feature flags - The platform (OS, kernel version, architecture)
- Your assessment of the impact (information disclosure / write outside jail / etc.)
We aim to acknowledge within 5 business days and to ship a fix within 30 days for high-severity findings (escape from a documented containment guarantee). Lower-severity findings (e.g., a missing defense for a documented out-of-scope threat) will be triaged on the open repository.
The README quick-start uses Jail::new and Jail::join — the path-based
API. That API is not TOCTOU-safe. It validates a path string and returns
a PathBuf; whatever the caller does with that PathBuf is a separate
operation with its own race window.
This is documented but easy to miss. If you operate in any of these
environments, strongly prefer guard over the path-based API:
- Multi-tenant systems where another local process can manipulate the filesystem
- File-upload paths where the same directory is also writable by other workers
- Anywhere "the file you validated" and "the file you opened" need to be the same file with certainty
We may make guard the default in 1.0.0 or a future major release. For now,
choose explicitly.