Documentation
¶
Overview ¶
Package util provides small, hardened safety primitives shared across the starpkg modules: bounded I/O, constant-time secret comparison, float-safe duration parsing, and panic recovery. Each centralizes a guard that domain modules previously re-implemented (often subtly wrong), so a fix lives in one place and new modules are safe by construction.
Index ¶
- func BuildChildEnv(hostEnv []string, allow []string, extra map[string]string) []string
- func CheckInputSize(label string, n, max int) error
- func DurationFromSeconds(sec float64) time.Duration
- func OpContext(thread *starlark.Thread, timeout time.Duration) (context.Context, context.CancelFunc)
- func ReadAllLimited(r io.Reader, max int64) ([]byte, error)
- func Recover(errp *error, label string)
- func ResolveUnder(root, userPath string) (string, error)
- func Retry(ctx context.Context, attempts int, retryable func(error) bool, ...) error
- func SafeCall[T any](label string, fn func() (T, error)) (result T, err error)
- func SecretEqual(a, b []byte) bool
- func SecretEqualString(a, b string) bool
- func ToStarlark(v interface{}, lim DecodeLimits) (starlark.Value, error)
- type CappedWriter
- type DecodeLimits
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func BuildChildEnv ¶
BuildChildEnv builds the environment ("KEY=VALUE" slice) for a child process launched on behalf of a script. It inherits from the host ONLY the variables whose names are in allow — an explicit allowlist, NOT the host's full os.Environ(), which would leak every host secret (API keys, tokens) and LD_PRELOAD-style injection vectors into a script-spawned process — then applies extra (script-supplied additions/overrides). A nil/empty allow inherits nothing from the host. The result is sorted for determinism.
On Windows, environment variable names are case-insensitive, so an extra override reliably beats a case-variant host var ("PATH" vs "Path").
func CheckInputSize ¶
CheckInputSize returns an error if n exceeds max bytes; it is the in-memory counterpart of ReadAllLimited for input already held as a string or []byte. A max <= 0 means unlimited.
func DurationFromSeconds ¶
DurationFromSeconds converts a fractional number of seconds to a time.Duration without the truncation bug of time.Duration(sec) * time.Second, which converts the float to an integer BEFORE multiplying and so silently drops any sub-second part (0.5 -> 0, an immediately-firing deadline). It multiplies in float space first, then clamps to the representable range instead of overflowing/wrapping; a NaN yields 0.
func OpContext ¶
func OpContext(thread *starlark.Thread, timeout time.Duration) (context.Context, context.CancelFunc)
OpContext returns a context (and its cancel func) for a single host operation triggered from a script. It starts from the thread's run context — so the Machine's own cancellation/deadline (e.g. RunWithTimeout) propagates to the operation — and layers an additional host timeout when timeout > 0. The returned context is always cancellable and derived; a timeout <= 0 adds no extra deadline. The caller MUST call the returned cancel func (typically via defer) to release resources.
This closes the recurring gap where a blocking remote call (a DB query, an API request, a queue send) took no context and could hang the host until the vendor SDK's or the OS's TCP timeout.
func ReadAllLimited ¶
ReadAllLimited reads all of r but fails if it would exceed max bytes, instead of buffering an unbounded amount — the OOM vector of a bare io.ReadAll on untrusted input (a request body, a fetched object, an attachment). A max <= 0 means unlimited.
func Recover ¶
Recover, used in a deferred call, converts a panic into an error stored in *errp (only when *errp is still nil), prefixed with label. It lets a wrapper around a panicky third-party call return an error instead of crashing the host — the recurring "defer/recover around a 3rd-party parser/renderer" pattern. Use as: defer util.Recover(&err, "yaml decode").
func ResolveUnder ¶
ResolveUnder resolves userPath against root and returns the confined path ONLY if it stays within root — a path jail. It confines "..", absolute paths, and symlinks (including dangling ones) in ANY path segment whose target escapes root, so a script-supplied path cannot reach an arbitrary host file (the traversal / arbitrary read-write class of bug). userPath is treated as relative to root: an absolute userPath is re-anchored under root.
The check is time-of-check: it validates and returns a path the caller opens later. It is safe when path components under root cannot be concurrently replaced by a hostile party; if the tree is attacker-writable between the call and the open, a TOCTOU race remains (open the returned path with O_NOFOLLOW, or use an OS-level jail, for that threat model).
func Retry ¶
func Retry(ctx context.Context, attempts int, retryable func(error) bool, backoff func(attempt int) time.Duration, fn func() error) error
Retry runs fn up to attempts times, stopping early on success (nil error), on a non-retryable error (retryable returns false), or when ctx ends. It returns fn's last error, or ctx.Err() if the context ends first.
attempts < 1 is treated as 1: fn is ALWAYS invoked at least once. This is the point of the primitive — a retry count of 0 must not silently skip the call and report a fake success (the bug found where retry=0 returned an empty result with no error and no request made).
retryable may be nil (every error is retried). backoff may be nil (no delay); otherwise backoff(attempt) is waited between attempts, interruptible by ctx.
func SafeCall ¶
SafeCall runs fn and converts any panic into an error prefixed with label, so a panicky third-party call cannot crash the host. It returns fn's own result and error unchanged when fn does not panic.
func SecretEqual ¶
SecretEqual reports whether a and b are equal, in constant time relative to their contents — use it to compare a caller-supplied token, password, or signature against a host secret so the comparison does not leak the secret via timing. A plain a == b (or bytes.Equal) short-circuits on the first differing byte and is timing-attackable. (Length inequality is not hidden, which matches crypto/subtle and is the standard trade-off.)
func SecretEqualString ¶
SecretEqualString is SecretEqual for strings.
func ToStarlark ¶
func ToStarlark(v interface{}, lim DecodeLimits) (starlark.Value, error)
ToStarlark converts a Go value — typically the output of a decoder such as yaml/toml/json — into a Starlark value, bounded by lim. It centralizes the hardened "capwalk" every starpkg codec re-implemented (each subtly different):
- caps nesting depth and total node count from lim;
- materializes maps in deterministic (sorted-key) order;
- tames a time.Time (RFC 3339, so sub-second precision is discarded) and any other fmt.Stringer (e.g. a TOML local date) to a string, so an opaque Go type never leaks into a script;
- REJECTS a map whose distinct keys collide after stringification (e.g. int 1 and string "1") instead of silently dropping one — a real bug the hand-rolled decoders shared.
Types ¶
type CappedWriter ¶
type CappedWriter struct {
// contains filtered or unexported fields
}
CappedWriter wraps an io.Writer and fails a Write that would push the total bytes written past its limit, so a bounded amount of output is produced from an otherwise-unbounded render or copy. A limit <= 0 means unlimited.
func NewCappedWriter ¶
func NewCappedWriter(w io.Writer, limit int) *CappedWriter
NewCappedWriter returns a CappedWriter over w with the given byte limit (<= 0 means unlimited).
func (*CappedWriter) Write ¶
func (c *CappedWriter) Write(p []byte) (int, error)
Write writes p to the underlying writer, or returns an error (writing nothing) if it would exceed the limit.
func (*CappedWriter) Written ¶
func (c *CappedWriter) Written() int
Written reports how many bytes have been written through the CappedWriter.
type DecodeLimits ¶
type DecodeLimits struct {
MaxDepth int // maximum nesting depth
MaxNodes int // maximum total number of values materialized
}
DecodeLimits bounds the work a ToStarlark conversion may do, fencing a huge or malicious decoded document. A zero field means that dimension is unbounded.