allowedpaths

package
v0.0.21 Latest Latest
Warning

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

Go to latest
Published: Jun 19, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package allowedpaths implements a filesystem sandbox that restricts access to a set of allowed directories using os.Root (Go 1.24+).

Index

Constants

View Source
const MaxGlobEntries = 10_000

MaxGlobEntries is the maximum number of directory entries read per single glob expansion step. ReadDirForGlob returns an error for directories that exceed this limit to prevent memory exhaustion during pattern matching.

Variables

This section is empty.

Functions

func CollectDirEntries

func CollectDirEntries(readBatch func(n int) ([]fs.DirEntry, error), batchSize, offset, maxRead int) ([]fs.DirEntry, bool, error)

CollectDirEntries reads directory entries in batches using readBatch, skipping the first offset entries and collecting up to maxRead entries. Returns (entries, truncated, lastErr). Entries are sorted by name.

NOTE: We intentionally truncate before reading all entries. For directories larger than maxRead, the returned entries are sorted within the read window but may not be the globally-smallest names. Reading all entries to get globally-correct sorting would defeat the DoS protection — a directory with millions of files would OOM or stall. The truncation warning communicates that output is incomplete.

func FileIdentity

func FileIdentity(_ string, info fs.FileInfo, _ *Sandbox) (uint64, uint64, bool)

FileIdentity extracts canonical file identity (dev+inode) from FileInfo. On Unix, this is extracted directly from Stat_t via info.Sys().

func IsDevNull

func IsDevNull(path string) bool

IsDevNull reports whether path refers to the platform's null device.

func IsErrIsDirectory

func IsErrIsDirectory(err error) bool

IsErrIsDirectory reports whether err is an "is a directory" error.

func PortableErrMsg

func PortableErrMsg(err error) string

PortableErrMsg returns a POSIX-style error message for the given error, normalizing platform-specific syscall messages to consistent strings. This ensures shell error output is identical across Linux, macOS, and Windows.

func PortablePathError

func PortablePathError(err error) error

PortablePathError returns a *os.PathError with a normalized error message. If the error is not a *os.PathError, it is returned as-is. Only the Err field is normalized; the Path and Op fields are preserved as-is.

func WithContextClose added in v0.0.8

func WithContextClose(ctx context.Context, f io.ReadWriteCloser) io.ReadWriteCloser

WithContextClose wraps f so that f.Close() is guaranteed to be called when ctx is done, in addition to any explicit Close calls made by the caller. The underlying file is closed at most once.

A background goroutine waits for either ctx cancellation or an explicit Close call. If the context expires first the file is closed immediately and the goroutine exits. If Close is called first the goroutine is signalled to stop and exits without touching the file again.

Types

type Sandbox

type Sandbox struct {
	// contains filtered or unexported fields
}

Sandbox restricts filesystem access to a set of allowed directories. The restriction is enforced using os.Root (Go 1.24+), which uses openat syscalls for atomic path validation — immune to symlink and ".." traversal attacks.

func New

func New(paths []string) (sb *Sandbox, warnings []byte, err error)

New creates a sandbox from an allowlist of directory paths. Paths that do not exist or cannot be opened are silently skipped — the sandbox operates with whatever paths are available at construction time.

Diagnostic messages about skipped paths are collected into warnings. The caller is responsible for writing them to the appropriate output stream.

func (*Sandbox) Access

func (s *Sandbox) Access(path string, cwd string, mode uint32) error

Access checks whether the resolved path is accessible with the given mode. All operations go through os.Root to stay within the sandbox. Mode: 0x04 = read, 0x02 = write, 0x01 = execute.

On Unix, read permission for regular files is verified by attempting to open through os.Root with O_NONBLOCK (fd-relative openat, respects POSIX ACLs, never blocks on FIFOs). Metadata is obtained from the opened fd via fstat to eliminate TOCTOU between open and stat. For special files where open fails (e.g. sockets), and for write and execute checks, mode-bit inspection is used on the fd-relative Stat result. On Windows, the same OpenFile approach is used for read checks; write and execute checks are not performed.

All operations are fd-relative through os.Root — no filesystem path is re-resolved through the mutable namespace after initial validation.

func (*Sandbox) CanonicalizeRootPrefix added in v0.0.15

func (s *Sandbox) CanonicalizeRootPrefix(absPath string) string

CanonicalizeRootPrefix returns absPath with any matching sandbox-root prefix replaced by that root's canonical (symlink-resolved) form. If absPath is outside every root, or its containing root is not a symlink, the input is returned unchanged.

Use case: builtins like `pwd -P` walk symlinks within the sandbox via callCtx.LstatFile/ReadlinkFile, but the *root itself* may be a symlink (e.g. AllowedPaths=/tmp/link with /tmp/link -> /tmp/real). os.OpenRoot follows the root symlink at open time, so per-component LstatFile cannot detect it. This helper applies the missing translation by mapping the configured-root prefix to the canonical one captured at New() time.

func (*Sandbox) Close

func (s *Sandbox) Close() error

Close releases all os.Root file descriptors. It is safe to call multiple times.

func (*Sandbox) HostPrefix added in v0.0.10

func (s *Sandbox) HostPrefix() string

HostPrefix returns the current host mount prefix.

func (*Sandbox) IsDirEmpty

func (s *Sandbox) IsDirEmpty(path string, cwd string) (bool, error)

IsDirEmpty checks whether a directory is empty by reading at most one entry. More efficient than reading all entries when only emptiness needs to be determined.

func (*Sandbox) Lstat

func (s *Sandbox) Lstat(path string, cwd string) (fs.FileInfo, error)

Lstat implements the restricted lstat policy. Like stat, it uses a metadata-only call, but does not follow symbolic links — the returned FileInfo describes the link itself rather than its target.

func (*Sandbox) Open

func (s *Sandbox) Open(path string, cwd string, flag int, perm os.FileMode) (io.ReadWriteCloser, error)

Open implements the restricted file-open policy. The file is opened through os.Root for atomic path validation, which uses openat under the hood and is immune to symlink and ".." traversal between path validation and open.

In the default read-only mode only O_RDONLY opens are accepted; any write flag returns ErrPermission. Call SetWritable to enable write opens.

In write-permitted mode, flag bits must be within allowedOpenFlags; anything else (unknown platform flags, O_RDWR, O_EXCL, etc.) returns ErrPermission.

The cross-root symlink fallback (resolveFollowingSymlinks) is read-only regardless of sandbox mode. Following a symlink that escapes its os.Root and then performing a write (O_CREATE/O_TRUNC/O_APPEND/O_WRONLY) is the classic TOCTOU footgun: a malicious symlink could redirect a create or truncate to a target that has changed between resolution and open, defeating the sandbox. Writes must stay within a single os.Root.

func (*Sandbox) OpenDir

func (s *Sandbox) OpenDir(path string, cwd string) (fs.ReadDirFile, error)

OpenDir opens a directory within the sandbox for incremental reading via ReadDir(n). The caller must close the returned handle when done. Returns fs.ReadDirFile to expose only read-only directory methods.

func (*Sandbox) Paths added in v0.0.10

func (s *Sandbox) Paths() []string

Paths returns the resolved absolute paths of all allowed directories.

func (*Sandbox) ReadDir

func (s *Sandbox) ReadDir(path string, cwd string) ([]fs.DirEntry, error)

ReadDir implements the restricted directory-read policy.

func (*Sandbox) ReadDirForGlob

func (s *Sandbox) ReadDirForGlob(path string, cwd string) ([]fs.DirEntry, error)

ReadDirForGlob reads directory entries for glob expansion, capped at MaxGlobEntries. The underlying ReadDir call is limited to MaxGlobEntries+1 so the kernel never materialises more entries than needed. If the directory exceeds the limit an error is returned before any pattern matching or sorting can occur, making the failure explicit rather than silently returning a partial listing that could miss valid matches.

func (*Sandbox) ReadDirLimited

func (s *Sandbox) ReadDirLimited(path string, cwd string, offset, maxRead int) ([]fs.DirEntry, bool, error)

ReadDirLimited reads directory entries, skipping the first offset entries and returning up to maxRead entries sorted by name within the read window. Returns (entries, truncated, error). When truncated is true, the directory contained more entries beyond the returned set.

The offset skips raw directory entries during reading (before sorting). This means offset does NOT correspond to positions in a sorted listing — pages may overlap or miss entries. This is an acceptable tradeoff to achieve O(n) memory regardless of offset value, where n = min(maxRead, entries).

func (s *Sandbox) Readlink(path string, cwd string) (string, error)

Readlink returns the destination of a symbolic link within the sandbox.

func (*Sandbox) SetHostPrefix added in v0.0.10

func (s *Sandbox) SetHostPrefix(prefix string)

SetHostPrefix overrides the mount prefix used to translate host-absolute symlink targets inside containers.

func (*Sandbox) SetWritable added in v0.0.21

func (s *Sandbox) SetWritable()

SetWritable switches the sandbox from its default read-only mode into write-permitted mode. In write-permitted mode, Open accepts write flags (O_WRONLY, O_APPEND, O_CREATE, O_TRUNC) in addition to O_RDONLY, as long as the target path is within the configured allowlist.

This is called by the interpreter when RemediationMode is active. The default (read-only) enforces that even if a code path mistakenly passes write flags to Open, the sandbox rejects them — defense-in-depth on top of the interpreter-level redirection guard.

func (*Sandbox) Stat

func (s *Sandbox) Stat(path string, cwd string) (fs.FileInfo, error)

Stat implements the restricted stat policy. It uses os.Root.Stat for metadata-only access — no file descriptor is opened, so it works on unreadable files and does not block on special files (e.g. FIFOs).

func (*Sandbox) Truncate added in v0.0.21

func (s *Sandbox) Truncate(path string, cwd string, size int64, create bool) error

Truncate sets the size of the file at path to size bytes. When create is true, a missing file is created with the open(2) permissive default (0666 & ~umask), matching GNU truncate and bash redirect semantics; the process umask is what actually decides the mode. When create is false, a missing file returns os.ErrNotExist (the caller, e.g. truncate -c, decides whether to treat that as an error or a silent skip).

Like Open, the operation goes through os.Root for atomic openat-based path validation. The cross-root symlink fallback is intentionally NOT used: resolving a symlink that escapes one root and then writing through the resolved path is the classic TOCTOU footgun. Writes must stay within a single allowed root.

Non-regular targets (FIFO, socket, char/block device) are rejected by an atomic open-and-fstat sequence:

  1. The open includes O_NONBLOCK so that an O_WRONLY open of a FIFO with no reader returns ENXIO immediately instead of blocking the shell waiting for a connection. (O_NONBLOCK is benign on regular files — it sets the fd's status flag but does not change open semantics — and is a no-op on platforms where the constant is zero, e.g. Windows.)
  2. After a successful open, fstat on the returned fd verifies the file is regular before any ftruncate runs. This closes the TOCTOU window that a pre-open Stat would have left open: even if a regular file is swapped for a FIFO between path resolution and the open syscall, the resulting fd is rejected before the size change reaches the kernel.

Negative sizes are rejected with EINVAL. Sizes within int64 range are passed through to the kernel; the kernel/filesystem rejects values it cannot represent (e.g. exceeding the filesystem's maximum file size).

Jump to

Keyboard shortcuts

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