osroot

package
v0.10.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 7, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package osroot provides traversal-resistant file I/O helpers built on os.Root (Go 1.24+). These helpers ensure that file operations cannot escape a scoped directory, preventing symlink attacks and TOCTOU races at the kernel level.

These wrappers predate Go 1.25, which added native ReadFile/WriteFile/MkdirAll (etc.) on *os.Root; they remain as the codebase's stable, consistent helper surface and delegate to the native methods where those now exist.

Errors from these functions are returned unwrapped so that callers can use os.IsNotExist() and errors.Is() directly without losing the original sentinel.

Index

Constants

This section is empty.

Variables

View Source
var ErrNotRegularFile = errors.New("path is not a regular file")

ErrNotRegularFile reports a directory, FIFO, socket or device where a file was required. Callers match it with errors.Is.

Deliberately not classified as os.ErrNotExist, though several callers reach these helpers to decide "is there a config here?": a path occupied by the wrong kind of object is a broken repository, and answering "absent" would have Entire write a fresh file over whatever is there.

View Source
var ErrReplacedDuringOpen = errors.New("file was replaced while it was being opened")

ErrReplacedDuringOpen reports that name resolved to one file when it was opened and a different one by the time the open was validated: something replaced it in between. Callers match it with errors.Is.

It is a race, not a refusal. A caller contending for a file that is expected to be replaced under it - a lock file being reaped and recreated - can retry against the new file. A caller that expected a stable name should treat it as the failure it is.

View Source
var ErrSymlinkedPath = errors.New("path component is a symlink")

ErrSymlinkedPath reports a symlink where a real path component was required. Callers match it with errors.Is.

View Source
var ErrWalkRootNotDirectory = errors.New("path is not a directory")

ErrWalkRootNotDirectory reports a walk root that exists but is not a directory. It is deliberately NOT ErrSymlinkedPath: "replace this link" and "this is a regular file" are different remedies, and conflating them is how a user gets told to fix the wrong thing.

Functions

func Forget added in v0.10.4

func Forget(dir string)

Forget closes and drops the cached root for one directory, leaving the rest of the registry alone.

Call it immediately before deleting or replacing a single rooted directory. ResetShared is the wrong tool there: it clears every anchor, and a caller rebuilding one cache directory has no business invalidating the handles other packages hold on .entire or .git. The plugin index cache is the case this exists for — its directory is removed and recreated by `git clone` while the process runs, so a root cached across that would be a handle to an unlinked inode.

A directory that was never opened is not an error: forgetting is idempotent.

func LstatNoSymlinks(root *os.Root, name string) (os.FileInfo, error)

LstatNoSymlinks lstats the leaf while rejecting and pinning every parent directory component. The leaf itself is returned as-is, including when it is a symlink, so callers can choose whether to reject or unlink it.

func MkdirAll added in v0.7.5

func MkdirAll(root *os.Root, name string, perm os.FileMode) error

MkdirAll creates the directory named by name, along with any necessary parents, relative to root. The kernel enforces containment: a name that escapes root (absolute, or climbing above it via "..") is rejected. Already- existing directories are tolerated, like os.MkdirAll. This thin wrapper keeps the package's os.Root helper surface (alongside ReadFile/WriteFile/Remove) consistent at call sites.

func MkdirAllNoSymlink(root *os.Root, name string, perm os.FileMode) error

MkdirAllNoSymlink is MkdirAll with one added refusal: if any component of name already exists as a symlink, it returns an error wrapping ErrSymlinkedPath instead of creating anything.

os.Root alone is not enough here. It refuses to follow a symlink that escapes the root, but a symlink pointing elsewhere *inside* the root is followed silently, and an escaping one fails later with whatever errno the first open happens to hit — which surfaces to the user as an unexplained I/O failure far from the cause. Checking at the point the directory is established turns both into one named error while the caller still has the context to report it.

Each component is opened relative to its already-pinned parent, including components created by this call. That makes creation and validation one descriptor-relative sequence rather than a check followed by MkdirAll.

func NoSymlinkedParent added in v0.10.4

func NoSymlinkedParent(root *os.Root, name string) error

NoSymlinkedParent reports ErrSymlinkedPath when any directory component of name is a symlink. The leaf is deliberately not examined. This is a diagnostic predicate; an operation that follows it must still use OpenParentNoSymlinks so the checked parent remains pinned through the use.

A component that does not exist is not an error: the caller is about to fail on the missing file, with a better message than this could give.

func OpenChild added in v0.10.4

func OpenChild(parent *os.Root, name string) (*os.Root, error)

OpenChild opens a root for the directory name inside parent, WITHOUT memoizing it: the caller owns the returned root and must Close it.

It is SharedChild's short-lived counterpart, and it exists so that opening a subdirectory of an anchor is never a bare parent.OpenRoot(name). os.Root refuses a symlink that escapes parent but follows one pointing elsewhere INSIDE it, so a bare OpenRoot silently accepts a redirected .git/entire-sessions. The Lstat before and the SameFile after are the same pair SharedChild uses, and they close the Lstat/OpenRoot race: once the identity is confirmed, a later replacement cannot redirect the handle.

A missing directory is returned unwrapped so callers can classify it with os.IsNotExist.

func OpenDirNoSymlinks(root *os.Root, dir string) (*os.Root, func(), error)

OpenDirNoSymlinks opens dir one component at a time. Every child is lstat'd, opened relative to its already-pinned parent, and identity-checked, so a symlink or a replacement racing the open is rejected. The caller must invoke the returned close function.

func OpenFileNoFollow added in v0.10.4

func OpenFileNoFollow(root *os.Root, name string, flag int, perm os.FileMode) (*os.File, error)

OpenFileNoFollow opens or creates a file without following any symlink. It is intended for append-style writers: O_TRUNC is rejected because truncating Entire-owned writes should use an atomic temp-file-and-rename operation instead. Unix builds add O_NOFOLLOW to close the write-before- validation race; other platforms still reject pre-existing links and verify the opened object before returning it.

func OpenNoFollow added in v0.10.4

func OpenNoFollow(root *os.Root, name string) (*os.File, error)

OpenNoFollow opens an existing file without following any symlink component. Parent directories are opened and pinned one at a time; the leaf's second check closes its Lstat/Open race. Once verified, later replacements cannot redirect the open descriptor.

func OpenParentNoSymlinks(root *os.Root, name string) (parent *os.Root, leaf string, closeParent func(), err error)

OpenParentNoSymlinks opens and pins the parent directory of name while rejecting a symlink in every parent component. The returned close function must be called; it is a no-op when name is directly beneath root.

Operations that must not follow parent symlinks should act on the returned root using leaf, rather than validating and then resolving name again from the original root. Holding the parent descriptor closes that check/use gap.

func ReadDir added in v0.10.4

func ReadDir(root *os.Root, name string) ([]os.DirEntry, error)

ReadDir reads the named directory relative to root, returning its entries sorted by filename, like os.ReadDir. os.Root has no ReadDir method of its own, so this is the sanctioned way to list a directory without falling back to an unconfined os.ReadDir on a joined path.

func ReadDirNoSymlinks(root *os.Root, name string) ([]os.DirEntry, error)

ReadDirNoSymlinks reads a directory after opening every component beneath root as a real directory. Unlike ReadDir, an in-root symlink is rejected rather than followed.

func ReadFile

func ReadFile(root *os.Root, name string) ([]byte, error)

ReadFile reads the named file relative to root using os.Root for traversal-resistant access. The kernel enforces that the read cannot escape the root directory, preventing symlink and TOCTOU attacks.

func ReadFileNoFollow added in v0.10.4

func ReadFileNoFollow(root *os.Root, name string) ([]byte, error)

ReadFileNoFollow reads name while refusing a symbolic link in any component. os.Root already prevents a link from escaping root, but follows links whose targets remain inside it. Entire-owned trees require the stronger property: every component must identify the object stored at that name.

func Remove

func Remove(root *os.Root, name string) error

Remove removes the named file relative to root using os.Root for traversal-resistant access. Returns nil if the file doesn't exist.

func RemoveAllNoSymlinks(root *os.Root, name string) error

RemoveAllNoSymlinks removes name and everything beneath it, rejecting a symlink in every parent directory component. A missing name is not an error.

os.Root.RemoveAll on its own is not enough, and the reason is the same one that makes OpenChild necessary next to a bare Root.OpenRoot: it refuses a symlink that would take the descent OUT of the root, and it unlinks a symlinked leaf rather than deleting whatever is at the far end, but it says nothing about the components ABOVE the leaf. A repository shipping `.pi -> /home/victim/notes` therefore had `.pi/extensions/entire` removed from inside /home/victim, with every component Root examined still nominally inside the worktree.

The leaf is unlinked rather than followed, which is os.RemoveAll's behaviour too, so a symlink there costs the link and not its target.

func RemoveNoSymlinks(root *os.Root, name string) error

RemoveNoSymlinks removes leaf without following it and rejects a symlink in every parent directory component. A missing leaf is not an error.

func ResetShared added in v0.10.4

func ResetShared()

ResetShared closes and forgets every root Shared handed out. Call it after deleting or replacing a directory a root was held on: a root that outlives its directory is a handle to an unlinked inode, so writes through it succeed and land nowhere.

func Shared added in v0.10.4

func Shared(dir string) (*os.Root, error)

Shared returns a process-wide *os.Root for dir, opening it at most once. The returned root is owned by the registry and shared with every other caller: do not close it.

dir must be absolute. A relative key would name different directories as the process moved, which is the whole failure mode these roots exist to remove.

A missing directory is returned unwrapped so callers can classify it with os.IsNotExist as well as errors.Is.

func SharedChild added in v0.10.4

func SharedChild(parent *os.Root, dir, name string) (*os.Root, error)

SharedChild returns a shared root for name beneath parent. Unlike opening the assembled absolute path, this keeps name inside an already-trusted root and refuses a symlink at the child boundary. The post-open identity check closes the Lstat/OpenRoot race.

func SymlinkPaths added in v0.10.4

func SymlinkPaths(root *os.Root, dir string) ([]string, error)

SymlinkPaths walks dir within root and returns the names of every symlink it finds, in the root's coordinates. It never follows one, so a symlinked directory is reported and not descended into.

This is doctor's reporter, so unlike WalkDirNoSymlinks it does not stop at the first one — the whole point is to list them. It does share the walk-root blindness fix: a symlinked dir is reported as itself rather than followed and its target's contents enumerated as if they were Entire's.

A missing dir yields no names and no error: nothing there is nothing wrong.

func WalkDirNoSymlinks(root *os.Root, dir string, fn fs.WalkDirFunc) error

WalkDirNoSymlinks walks dir within root, refusing a symlink anywhere it goes: at the walk root, and at every entry beneath it.

Refusing rather than skipping is the point. These walks copy an Entire-owned tree into a checkpoint, or read the rules that decide what gets redacted out of one, and Entire never puts a symlink in either. So a link there was planted, arrived with a checkout, or is a genuine mistake — and the two quiet answers are both wrong. Following it stores some other tree's contents under Entire's names; skipping it drops the session's content out of the checkpoint with nobody told. The caller stops and says which path.

A missing dir is reported unwrapped, so callers can keep classifying it with os.IsNotExist.

func WriteFile

func WriteFile(root *os.Root, name string, data []byte, perm os.FileMode) (retErr error)

WriteFile writes data to the named file relative to root using os.Root for traversal-resistant access. Creates the file if it doesn't exist, truncates it if it does. The kernel enforces that the write cannot escape the root directory.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL