fsref

package
v0.0.1 Latest Latest
Warning

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

Go to latest
Published: Aug 24, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package fsref resolves a path into the single metadata record every rule and collector reads from.

The whole design rests on one invariant: a path is resolved exactly once per walk step, no matter how many rules examine it. Collectors never call stat themselves; they ask the Cache, which the walker has already primed.

Index

Constants

View Source
const (

	// stx_attributes bits we care about; see statx(2).
	AttrCompressed = 0x00000004
	AttrImmutable  = 0x00000010
	AttrAppend     = 0x00000020
	AttrNodump     = 0x00000040
	AttrEncrypted  = 0x00000800
)
View Source
const MaxMetadataFile = 4 << 20

MaxMetadataFile caps how much of an image file the metadata helpers will read. Hostnames and passwd files are small; anything larger is not one.

Variables

View Source
var ErrEscapesRoot = fmt.Errorf("path escapes the collection root")

ErrEscapesRoot reports a path that would leave the tree it is supposed to stay inside.

Functions

func Canonical

func Canonical(p string) string

Canonical resolves a path to an absolute, symlink-free form, for comparing two paths that may be written differently.

It is how the walk recognises its own output directory: that path is chosen by the operator, so it may be relative, and it may be reached through a symlink. A path that cannot be resolved -- it does not exist yet, or a component is unreadable -- is returned cleaned, which is the best available answer and never worse than the input.

func CleanImagePath

func CleanImagePath(p string) (string, error)

CleanImagePath normalises a path taken from inside the image.

Paths that come from a file's *contents* -- a HISTFILE assignment in an rc file, say -- are attacker-controlled on a hostile image. Interpreting them requires remembering that "/" means the image root, not the examiner's root, so "/../../etc/shadow" resolves back to "/etc/shadow" inside the image rather than climbing out of it. path.Clean does exactly that: leading ".." at the root are absorbed.

A path that is not absolute is rejected: the shell would have resolved it against a working directory this walk does not have.

func CreateNoFollow

func CreateNoFollow(path string, perm os.FileMode) (*os.File, error)

CreateNoFollow creates a file for writing, refusing to follow a symlink that is already sitting at the destination.

Without O_NOFOLLOW, a symlink left in the output tree -- by a previous run, or by anything else with write access to it -- would redirect a collected file somewhere else entirely.

func DestinationUnder

func DestinationUnder(dir, rel string) (string, error)

DestinationUnder joins a relative path onto a destination directory and verifies the result stays inside it.

The copy destination is built from the same image-controlled path as the source, so it needs the same containment check. filepath.Join cleans, which means a path that climbed out would do so silently.

func DirExistsBeneath

func DirExistsBeneath(root, rel string) bool

DirExistsBeneath reports whether rel names an existing directory inside root.

func ExistsBeneath

func ExistsBeneath(root, rel string) bool

ExistsBeneath reports whether rel names an existing regular file inside root. Used for the operating-system markers, where a symlink pointing out of the image must not count as evidence of anything.

func Join

func Join(root, rel string) string

Join prefixes an image-relative path with the mount point.

func OpenBeneath

func OpenBeneath(root, rel string) (*os.File, error)

OpenBeneath opens a regular file inside root for reading, without following symlinks and without blocking.

The metadata helpers -- hostname, account database, OS markers -- read files chosen by the image, and a hostile image can make any of them a symlink to the examiner's filesystem or a FIFO that never opens. os.Open does both: follows the link, and blocks. So the path is contained first, the open is O_NOFOLLOW|O_NONBLOCK, and the result must be a regular file.

func ReadBeneath

func ReadBeneath(root, rel string) ([]byte, error)

ReadBeneath reads a metadata file from inside root, subject to the same containment and file-type rules as OpenBeneath and capped in size.

func Rel

func Rel(real, root string) string

Rel strips the mount point, so recorded paths read /etc/passwd no matter where the image happened to be mounted.

Types

type Cache

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

Cache is the single-entry memo that keeps InspectFile(path) from turning into one stat per rule. The walker primes it before dispatching a path, so every collector asking for the same path gets the already-resolved record. A miss is not an error -- it just means somebody called a collector outside a walk, so we resolve on demand and stay correct.

func NewCache

func NewCache(root string) *Cache

func (*Cache) Current

func (c *Cache) Current() *FileRef

Current returns the primed entry, if any.

func (*Cache) Get

func (c *Cache) Get(path string) (*FileRef, error)

Get returns the record for path, resolving it only on a miss.

func (*Cache) Hit

func (c *Cache) Hit(path string) bool

Hit reports whether Get(path) would avoid a syscall. Tests use it to assert the no-restat invariant.

func (*Cache) Set

func (c *Cache) Set(f *FileRef)

Set primes the cache. Called by the walker, once per path.

type FileRef

type FileRef struct {
	Path  string // image-relative, e.g. /etc/passwd -- this is what gets recorded
	Real  string // syscall path, e.g. /mnt/img/etc/passwd
	Name  string // basename
	Depth int    // 0 for the walk root

	RawMode uint16 // st_mode, type bits included
	Nlink   uint64
	UID     uint32
	GID     uint32
	Ino     uint64
	Dev     uint64
	Size    int64

	Atime time.Time
	Mtime time.Time
	Ctime time.Time
	Btime time.Time

	HasBtime  bool
	Attrs     uint64 // stx_attributes
	AttrsMask uint64 // which bits of Attrs the filesystem actually reports
	// contains filtered or unexported fields
}

FileRef is the resolved metadata for one path. It holds no file descriptor: an open descriptor would cost two extra syscalls per file, would pin the inode, and on a mounted image would risk opening the *host's* device nodes. Bytes are handled separately, by the content broker, and only for files some rule actually asked to read.

func Resolve

func Resolve(real, rel string, depth int) (*FileRef, error)

Resolve stats one path. real is the path to hand the kernel; rel is the image-relative path to record.

func ResolveBeneath

func ResolveBeneath(root, rel string) (*FileRef, error)

ResolveBeneath stats an image-relative path while guaranteeing the result lies inside root.

Cleaning the path is not enough on its own. The kernel follows symlinks in the intermediate components, so an image containing "/logs -> /" turns "<mount>/logs/etc/shadow" into the *examiner's* /etc/shadow -- and on a scan run as root that is a read of the host, or, once the same path reaches the copy destination, a write outside the output directory.

On Linux the kernel is asked to resolve the path under the root with openat2's RESOLVE_BENEATH, which fails outright if any component would leave it -- through a symlink, a magic link, or "..". That is one atomic resolution rather than a sequence of separate checks.

Where openat2 is unavailable -- an older kernel, or any other platform -- every directory leading to the target is checked with lstat instead, and a symlinked one is refused.

Either way this is a check followed by an open by path, so a window remains against something mutating the image mid-scan. That does not apply to the case it defends against: a forensic image is static, and mounted read-only if the examiner is doing it properly.

func (*FileRef) Immutable

func (f *FileRef) Immutable() (set, known bool)

Immutable reports whether the immutable flag is set, and whether the filesystem actually answered. statx gives us this without the FS_IOC_GETFLAGS ioctl, which would have required an open descriptor.

func (*FileRef) IsBlockDev

func (f *FileRef) IsBlockDev() bool

func (*FileRef) IsCharDev

func (f *FileRef) IsCharDev() bool

func (*FileRef) IsDir

func (f *FileRef) IsDir() bool

func (*FileRef) IsFIFO

func (f *FileRef) IsFIFO() bool

func (*FileRef) IsRegular

func (f *FileRef) IsRegular() bool

func (*FileRef) IsSocket

func (f *FileRef) IsSocket() bool
func (f *FileRef) IsSymlink() bool
func (f *FileRef) Link() (string, error)

Link returns the symlink target, resolved at most once.

func (*FileRef) ModeString

func (f *FileRef) ModeString() string

ModeString renders permissions the way GNU stat's %A does, which is the format the mactime bodyfile expects.

func (*FileRef) Perm

func (f *FileRef) Perm() uint16

Perm returns the permission bits including setuid/setgid/sticky.

func (*FileRef) String

func (f *FileRef) String() string

func (*FileRef) TypeChar

func (f *FileRef) TypeChar() byte

TypeChar returns the find(1) -type letter for this file.

Jump to

Keyboard shortcuts

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