std::permissions
What the manifest allows, as data the program carries.
[permissions] in khora.toml says which paths a package may read and
write. The answer itself lives in std::permissions::grants, which
khora build writes; this module is the part that decides what an answer
means, and std::fs’s real handlers consult it before touching a file.
The grants are compiled in, not read at run time. A file the program consults for its own permissions is a file an attacker edits; a list the compiler wrote into the binary is one nothing at run time can widen.
Why this is a file you can read rather than a check inside the compiler
Section titled “Why this is a file you can read rather than a check inside the compiler”The compiler contributes data. Everything that decides whether a path is
allowed is here, in Khora, and can be read, tested and stepped through –
which is the whole reason the enforcement is not a pass in khora-types
that nobody outside the compiler can see. The design document is explicit
about it: “ordinary Khora in a module you can read, not a second
enforcement engine hidden in the compiler”.
What it says when nothing is configured
Section titled “What it says when nothing is configured”Everything. A missing [permissions] table grants everything, which is
opt-in tightening rather than a tax on starting – the same bargain Rust
makes with unsafe. The default below is the one a program with no
manifest, or no [permissions.fs], compiles with, and khora build
narrows it only when the manifest asks.
API at a glance
Section titled “API at a glance”Functions: granted · granted_name · granted_host · normalized
Functions
Section titled “Functions”granted
Section titled “granted”pub fn granted(grants: List<String>, path: String) -> BoolWhether any grant covers path.
The glob dialect is .gitignore’s, because it is the one everybody already
has in their fingers:
*matches within one path segment and stops at a separator.**matches across separators, sodata/**coversdata/a/b.txt– though notdataitself, which is.gitignore’s reading too.- Everything else matches itself.
Separators are normalized first, so a grant written with / covers a
Windows path spelled with \\. granted_path in khora-manifest answers
the same question for the compiler and the two have to agree; the tests
name the cases where agreement is not obvious.
A path with a .. segment is refused, whatever the grants say, and it
is refused here rather than left to the matcher, because the matcher gets
it wrong. ** spans separators, so the ./logs/** the capabilities guide
teaches for granting a tree matches logs/../secret.txt literally: the
** swallows ../secret.txt, and a grant that reads like one directory
covers the disk. A program that joins a path it was handed – an argument,
a config entry, a request path – under a granted prefix was passing that
reach on to whoever handed it the path. normalized says why the .. is
refused rather than resolved away.
granted_name
Section titled “granted_name”pub fn granted_name(grants: List<String>, name: String) -> BoolWhether any grant covers name.
For things that are not paths – an environment variable, a command –
where there are no segments and so * spans everything. DATABASE_*
covers DATABASE_URL, and * covers all of them.
granted_name in khora-manifest answers the same question for the
compiler, and the two are tested against one table.
granted_host
Section titled “granted_host”pub fn granted_host(grants: List<String>, host: String) -> BoolWhether any grant covers host, which is name or name:port.
Two rules, and both are the reading that costs a newcomer least:
*spans dots, so*.internalcoversdb.eu.internaland not onlydb.internal. That is what a Content-Security-Policy origin means by it; the one-label reading belongs to TLS certificates, and surprising somebody into a denied connection is the worse failure.- A grant with no port covers every port, which is what Deno’s
--allow-net=example.comdoes.:*says the same thing explicitly.
normalized
Section titled “normalized”pub fn normalized(path: String) -> StringOne spelling of a path, so a grant and a request can be compared.
Two things are levelled, and both were reported as traps by somebody who hit them:
\\becomes/, so a grant written with forward slashes covers a path Windows spelled with back ones.- A
.segment is dropped, so./data/**grantsdata/foo.txtanddata/**grants./data/foo.txt. Before this, the./was significant on both sides: two readers ofcapabilities.mdwrote the example’sread = ["./data/**"]into a manifest, openeddata/foo.txt, and were refused by a grant that looks like it says yes.
.. is neither resolved nor levelled here; granted refuses the path
outright. Both of the other answers widen a grant, which is the one
direction a permission check must never be wrong in.
Resolving a/../b to b is only correct if a is a real directory. Where
a is a symlink, a/.. is the parent of what a points at, so the open
lands on the target’s neighbour b while the check approved the b beside
a – it would be answering about a file the filesystem does not open.
Telling those apart needs the filesystem, and this function has only the
string.
Comparing the .. as written is not the safe half it looks like. **
crosses separators, so ./logs/** matches logs/../secret.txt as text and
grants it. No spelling of the comparison is right for both logs/a.txt and
logs/../secret.txt, so a path with a .. segment is not compared at all.
A caller that means a/../b can pass b; one that cannot is asking a
question only the filesystem can answer.
Nor is anything resolved against a working directory or a package root. A grant is matched against the path the program passes, so a program that builds its paths from its own arguments needs a grant wide enough to cover what its users will type. The permissions design note records why that is the boundary and what it costs a CLI.
Khora 0.2.0built frombd964738afb6on 2026-10-17 20:41ZVersioningSecurityContributingReleasesSource