Documentation
¶
Overview ¶
Package rootpath resolves paths against a container rootfs the way a chroot would, through the kernel (openat2 RESOLVE_IN_ROOT). It is the guest's one containment boundary for image-controlled paths.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ErrPathEscapes = errors.New("path escapes rootfs")
ErrPathEscapes is a path that resolves, or is spelled, outside the root.
Functions ¶
func ProcFDPath ¶
ProcFDPath is the magic link naming fd: handed to a syscall that takes a path (mount, connect), it reaches exactly the node fd holds, with no lookup left for a symlink to redirect.
Types ¶
type Root ¶
type Root struct {
// contains filtered or unexported fields
}
Root resolves archive- and image-supplied paths against a root the way a chroot would, and is the ONLY containment boundary for those paths.
The rule it enforces is the kernel's: RESOLVE_IN_ROOT makes openat2 treat the root fd as "/" for the whole walk, so an absolute symlink ("/dev") lands inside the root, and ".." at the root stays at the root. A lexical check cannot do this — it validates the string, while the kernel resolves the symlinks, and those two disagree exactly when an intermediate component is a symlink. That gap was a real escape: an image shipping "evil -> /run" plus a regular file "evil/x" wrote x outside the rootfs entirely.
Symlinks are FOLLOWED, not refused. Refusing them would be a stricter-looking but wrong policy: usrmerge images ship "lib -> usr/lib" and a later entry "lib/libc.so" must legitimately resolve into usr/lib. Containment, not avoidance, is the property we need.
func (*Root) Dir ¶
Dir resolves name as a directory inside the root, creating any missing component, and returns an O_PATH fd to it, which the caller closes. Unlike Resolve, the last component is followed too, inside the root: the fd is a place to operate on (a mount target through ProcFDPath, a parent for *at calls), so a symlink there must land inside the rootfs, not be replaced. Nothing is created through a symlink: one that dangles is an error.
func (*Root) Forget ¶
Forget drops cached directory resolutions at or under an archive name. The caller invokes it whenever extraction REPLACES an existing node (the collision-recovery path): the node may have been a directory, or a symlink some cached resolution walked through, so anything cached at or below that name is suspect. Fast-path creations need no invalidation — they only succeed where nothing existed, so nothing can have been resolved through the name. Collisions are close to nonexistent in practice, which is what keeps the sweep off the hot path.
func (*Root) OpenPath ¶
OpenPath returns an O_PATH fd to the existing node name inside the root, symlinks followed and kept inside it, the last component included; the caller closes it. Reached through ProcFDPath, the node is the one resolved here, whatever the path becomes in between.
func (*Root) Resolve ¶
Resolve maps an archive path to the resolved path to operate on, creating any missing parent directories inside the root. It returns "" for a name that denotes the root itself.
Only the PARENT is resolved. The final component is deliberately left to the caller, which must not follow it: tar semantics replace whatever sits at that name (writeEntry creates exclusively and replaces on collision), and metadata uses the L* variants so a symlink's own attributes are set. Resolving the last component here would reintroduce exactly the redirection this type exists to stop.