Percent-decode the request path once. Reject NUL, backslash, decoded / inside a
segment (%2f), and any .. segment. Collapse . and empty segments.
Deny any segment starting with . except the first segment .well-known.
Linux: open the result with openat2(RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS)
relative to a directory fd of the root opened at prepare time, with
O_NONBLOCK | O_NOCTTY | O_CLOEXEC. The kernel guarantees confinement for the whole
resolution, so a concurrently swapped symlink cannot escape (closes the TOCTOU window
of the canonicalise approach), and opening a FIFO cannot block. File type and size
come from fstat on the opened fd.
Fallback (other OSes; kernels without openat2; seccomp policies that reject it):
canonicalise root/rel and require it to stay beneath the canonical root.
Directories serve configured index files (same lookup); never list.
Consequences
With RESOLVE_BENEATH, absolute symlinks are refused even if they point inside the
root; relative symlinks that stay inside the root work. This is stricter than the
fallback and is documented for operators.
Non-blocking hot path (Linux): the lookup is first attempted on the async worker with
RESOLVE_CACHED (5.12+; EAGAIN unless every component is in the dentry cache) and
small files are read with preadv2(RWF_NOWAIT) (EAGAIN unless in the page cache).
Anything that would block is redone on the blocking pool. Large files stream in 256 KiB
positional reads, inline when cached, otherwise one blocking-pool hop per chunk; no
thread is held between chunks, so slow clients cannot pin blocking threads.
On overlayfs (e.g. a container’s writable layer) RESOLVE_CACHED typically returns
EAGAIN, so the blocking path is used: correct but slower. Serve document roots from a
real filesystem (ext4/xfs, or a volume in containers).