Documentation
¶
Overview ¶
Package safeio provides small, dependency-free allocation, bounds, and loop guards for parsing UNTRUSTED on-disk filesystem images.
The filesystem drivers in this org parse images supplied by an attacker. A malicious image must never panic the host, read out of bounds, integer-overflow into a bad allocation or slice, loop forever, or OOM. This package makes the four near-universal defenses easy to apply:
- class (A) unbounded make([]byte, N) → OOM: MakeBytes, ReadAtFull
- class (B) unbounded chain/tree traversal → loop: LoopGuard, VisitSet
- class (C) fixed-offset read without length check: CheckBounds, Slice
- class (D) unvalidated geometry → divide-by-zero: (callers compare to 0)
All helpers return errors instead of panicking. The sentinel errors all wrap ErrSafeIO, so callers can match either the specific cause (errors.Is(err, ErrTooLarge)) or the family (errors.Is(err, ErrSafeIO)).
The package has no dependencies outside the standard library and is compatible with go 1.25 and CGO_ENABLED=0.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrTooLarge is returned when a requested size is negative or exceeds // the supplied ceiling (class A: unbounded allocation). ErrTooLarge = fmt.Errorf("%w: size too large", ErrSafeIO) // ErrOutOfBounds is returned when an offset/length pair would read or // slice outside the available buffer (class C: out-of-bounds access). ErrOutOfBounds = fmt.Errorf("%w: out of bounds", ErrSafeIO) // ErrLoopLimit is returned by a LoopGuard once its iteration budget is // exhausted (class B: unbounded traversal). ErrLoopLimit = fmt.Errorf("%w: loop iteration limit exceeded", ErrSafeIO) // ErrCycle is returned (or signalled) when a VisitSet observes an // already-visited node id (class B: cyclic traversal). ErrCycle = fmt.Errorf("%w: cycle detected", ErrSafeIO) )
Sentinel errors. Each wraps ErrSafeIO.
var ErrSafeIO = errors.New("safeio")
ErrSafeIO is the base error that every sentinel in this package wraps, so callers can match the whole family with errors.Is(err, ErrSafeIO).
Functions ¶
func CheckBounds ¶
CheckBounds verifies that the half-open range [off, off+n) lies entirely within a buffer of the given length, i.e. off >= 0 && n >= 0 && off+n <= length. The sum is computed in int64 so it cannot wrap on a 64-bit platform, defeating class (C) overflow tricks such as off = maxint, n = 1.
It returns nil when the range is valid, otherwise ErrOutOfBounds.
func MakeBytes ¶
MakeBytes returns make([]byte, n) after validating n against max, the universal fix for class (A) unbounded allocations. Callers pass the device/image size (or a sane ceiling) as max.
It returns ErrTooLarge if n < 0, max < 0, or n > max. n == 0 yields a non-nil empty slice. Because n and max are int64, callers can pass raw on-disk fields without a lossy conversion to int first; the result length still fits in int on every supported (64-bit) platform once n <= max.
func ReadAtFull ¶
ReadAtFull allocates a bounded buffer of n bytes (rejecting n > max or n < 0 via MakeBytes) and fills it from r at off using io.ReadFull semantics: it reads exactly n bytes or returns an error. A short image therefore yields io.ErrUnexpectedEOF (or io.EOF when n > 0 and nothing could be read) instead of a partially-populated buffer.
off, n, and max are int64 so callers can pass raw on-disk fields without a lossy narrowing. This is the combined fix for classes (A) and (C) on the common "seek to an attacker-controlled offset and read an attacker-controlled length" pattern.
func Slice ¶
Slice returns buf[off:off+n] after a CheckBounds validation, so a malformed offset/length yields an error rather than a slice-bounds panic (class C). The returned slice aliases buf; callers that need an independent copy must copy it themselves.
Types ¶
type LoopGuard ¶
type LoopGuard struct {
// contains filtered or unexported fields
}
LoopGuard bounds the number of iterations of a chain or tree walk where a full visited-set is overkill (e.g. a FAT cluster chain or an extent chain). Construct it with NewLoopGuard and call LoopGuard.Next once per iteration; after max successful calls the next call returns ErrLoopLimit. The zero value is not usable; use NewLoopGuard.
func NewLoopGuard ¶
NewLoopGuard returns a LoopGuard that permits up to max iterations. A non-positive max means "no iterations are allowed": the first LoopGuard.Next returns ErrLoopLimit, which is the safe default for an attacker-supplied or nonsensical bound.
func (*LoopGuard) Count ¶
Count reports how many times LoopGuard.Next has returned nil so far.
func (*LoopGuard) Next ¶
Next records one iteration. It returns nil for the first max calls and ErrLoopLimit thereafter, so a malformed image that forms an unbounded or cyclic chain terminates the walk with an error instead of spinning forever.
type VisitSet ¶
type VisitSet struct {
// contains filtered or unexported fields
}
VisitSet detects revisited node ids during a traversal that must not follow a cycle (e.g. a B-tree whose block pointers form a loop). The zero value is ready to use.
func (*VisitSet) Add ¶
Add records id as visited and reports whether this is the first time id has been seen. A false return means id was already present, i.e. the traversal has looped back; callers should treat that as ErrCycle via VisitSet.Check or by bailing out directly.
func (*VisitSet) Check ¶
Check is a convenience wrapper around VisitSet.Add that returns ErrCycle (annotated with id) when id has already been visited, and nil otherwise.