Documentation
¶
Overview ¶
Package fsutil holds the durable-write and path-safety primitives shared by the ~/.abcd.noindex and repo .abcd store writers. It is transport-agnostic: no stdout, no os.Exit, no CLI knowledge.
It is the single home for the atomic temp-file+fsync+rename write and the "is this a real directory, not a symlink" check: the ahoy, capture, and memory store writers all route through WriteFileAtomic / WriteFileAtomicPreserveMode / IsRealDir rather than keep divergent copies (the one-canonical-primitive invariant, guarded by TestNoNonCanonicalAtomicWritePrimitives).
Directory validation is the same story one level up. IsRealDir answers the question; EnsureRealDir and EnsureRealDirAll are the create-then-prove sequence built on it, and the lifeboat voyage log, the intent record store and the transcript store all call them rather than each carrying the sequence (iss-2609091128479544).
Every flush to stable storage goes through Flush, the one place a test binary that opted in with SkipFlushEnv skips it; a shipped binary always flushes.
An append-only log has its own primitive, AppendLineIn: one line, one O_APPEND write, so concurrent writers land whole lines (the lifeboat voyage ledger and the implement run log both write through it).
Each read/write primitive comes in two forms. The plain form takes a path and guards the LEAF only; the …InRoot form takes an *os.Root and resolves every component inside it, which is what contains a path whose ANCESTOR is a symlink planted by untrusted content. Any path that arrives as data — a committed configuration value, a packed manifest entry — belongs in the …InRoot form.
Index ¶
- Constants
- Variables
- func AppendLineIn(root *os.Root, rel string, line []byte, perm os.FileMode) error
- func CallersAlone(path string, fi os.FileInfo) error
- func CallersOrRootsAlone(path string, fi os.FileInfo) error
- func CaseFoldingFS() bool
- func CreateExclusiveIn(root *os.Root, rel string, data []byte, perm os.FileMode) error
- func CreateRunDir(base, rel, name string, perm os.FileMode) (string, error)
- func DirHasEntries(path string) (bool, error)
- func DisplayPath(p string) string
- func DisplayPathsIn(s string, paths ...string) string
- func EnsureHomeScope(home, dir string, perm os.FileMode) (*os.Root, error)
- func EnsureRealDir(dir string, perm os.FileMode) error
- func EnsureRealDirAll(base, rel string, perm os.FileMode) error
- func Exists(path string) (bool, error)
- func ExistsNoFollow(path string) (bool, error)
- func ExpandHome(p, home string) string
- func ExpandTilde(p, home string) string
- func Flush(f *os.File) error
- func FoldPath(p string, fold bool) string
- func HomeDeclarationNames(home, rel string, limit int64, target string, fold bool) (declared bool, ignored string)
- func HomeScopeLink(home, rel string) error
- func InsideGitDir(p string) bool
- func IsDir(path string) (bool, error)
- func IsPathSegmentByte(b byte) bool
- func IsRealDir(path string) bool
- func ModuleRoot(start string) (string, error)
- func NameContinues(s string, end int) bool
- func OpenHomeScope(home, dir string) (*os.Root, error)
- func OpenRealDir(dir string) (*os.Root, error)
- func OpenRegular(path string) (*os.File, os.FileInfo, error)
- func OwnerUID(path string) (uint32, error)
- func PathWithin(child, parent string, fold bool) bool
- func PathsOverlap(a, b string, fold bool) bool
- func ProbeRealDirAll(base, rel string) (ok bool, err error)
- func ReadGuarded(path string, limit int64) ([]byte, error)
- func ReadGuardedInRoot(root *os.Root, rel string, limit int64) ([]byte, error)
- func RealExistingPath(p string) string
- func RedactHome(s string) string
- func RedactRoot(s, root, repl string) string
- func RepoRel(base, target string) string
- func RepoRelativePath(repoRoot, p string) string
- func SwapHomeScopeVettedForTest(fn func(dir string)) (restore func())
- func SwapOwnerUIDForTest(fn func(string) (uint32, error)) (restore func())
- func ValidRelPath(p string) bool
- func WithDirLock(dir string, timeout time.Duration, fn func() error) error
- func WithFileLock(lockPath string, timeout time.Duration, fn func() error) error
- func WithFileLockIn(root *os.Root, rel string, timeout time.Duration, fn func() error) error
- func WritableByOthers(fi os.FileInfo) bool
- func WriteFileAtomic(path string, data []byte, perm os.FileMode) error
- func WriteFileAtomicInRoot(root *os.Root, rel string, data []byte, perm os.FileMode) error
- func WriteFileAtomicPreserveMode(path string, data []byte) error
- func WriteFileAtomicPreserveModeInRoot(root *os.Root, rel string, data []byte) error
- type DeclarationModeError
- type DeclarationRefusal
- func ReadDeclaration(path string, limit int64) ([]byte, DeclarationRefusal, error)
- func ReadHomeDeclaration(home, rel string, limit int64) ([]byte, DeclarationRefusal, error)
- func ReadHomeDeclarationDenying(home, rel string, limit int64, deny os.FileMode) ([]byte, DeclarationRefusal, error)
- func ReadHomeDeclarationDenyingIn(dir *os.Root, home, rel string, limit int64, deny os.FileMode) ([]byte, DeclarationRefusal, error)
- type HomeScopeExposedError
- type HomeScopeLinkError
Constants ¶
const LockPollCeiling = 100 * time.Millisecond
LockPollCeiling is the longest a waiter on a WithFileLock lock sleeps between two attempts: the poll starts at lockPollStart and doubles up to it, never past it. A caller that frees a lock for a window so that such a waiter is sure to find it free — intent's pair acquisition, resting between attempts — keeps that window longer than this, so it is derived from it rather than restated (iss-2609262257227538).
const SkipFlushEnv = "ABCD_TEST_SKIP_FLUSH"
SkipFlushEnv names the opt-in a test run sets to skip the flush to stable storage. The Makefile's test and preflight targets and the test steps of ci.yml's check job set it to "1".
It is honoured only inside a test binary. On macOS, File.Sync is F_FULLFSYNC, which costs about 8.8 ms per synced write on a developer disk, and the tests write thousands of files while asserting nothing a flush makes true: a flush is what survives a power cut, and no test cuts the power. A shipped binary flushes whatever its environment says, because testing.Testing reports false in every binary the go command builds other than a test binary, so neither half of the gate alone disables durability.
Variables ¶
var ( ErrLockContention = errors.New("fsutil: lock contention") ErrLockPathUnsafe = errors.New("fsutil: lock path unsafe") )
ErrLockContention and ErrLockPathUnsafe are the file-lock sentinels: the exclusive lock could not be acquired within the caller's timeout, and the lock path is a symlink or not a regular file.
var ( ErrNotRegular = errors.New("fsutil: not a regular file") ErrTooBig = errors.New("fsutil: file exceeds size cap") )
ErrNotRegular and ErrTooBig are the guarded-read sentinels: a non-regular leaf (symlink/FIFO/device/directory) and a file over the caller's byte cap.
var ( ErrDeclarationWritable = errors.New("fsutil: declaration is writable by group or other") ErrDeclarationForeignOwner = errors.New("fsutil: declaration is not owned by this session's uid") // ErrDeclarationSwapped: the file opened is not the one the guards judged // (replaced between the vetting lstat and the open). ErrDeclarationSwapped = errors.New("fsutil: declaration was replaced between its vetting and its read") )
ErrDeclarationWritable and ErrDeclarationForeignOwner are the two guards that are ReadDeclaration's own, rather than ReadGuarded's. They are returned as errors as well as refusals so that a caller which checks only `err` — the shape a drop-in replacement of ReadGuarded takes — still fails closed.
var ErrHomeScopeExposed = errors.New("fsutil: a directory of this home-scoped path can be changed by another account")
ErrHomeScopeExposed is the refusal for a home-scoped declaration whose DIRECTORY another account can change: one every account can write (sticky or not), or one owned by an account that is neither the caller nor root. The declaration file's own guards judge who wrote the file; anyone who can write the directory can rename or hard-link a file of the caller's own shape in under the declaration's name, so those guards would judge a file the caller never put there (iss-2609290656480443). ReadHomeDeclaration returns it wrapped in a *HomeScopeExposedError, so errors.Is finds it.
A directory its group can write is deliberately not refused here: under a user-private-group umask of 002, a ~/.abcd.noindex made by hand is 0775 and its group is the caller alone, and refusing it is an open question on that record rather than a decision this read takes.
var ErrHomeScopeSwapped = errors.New("fsutil: a directory of this home-scoped path was replaced while it was opened")
ErrHomeScopeSwapped is the refusal for a directory of a home-scoped path that was a real directory when judged and was something else by the time it was opened: the descriptor OpenHomeScope obtained is not the directory its Lstat vetted, and that directory is not a symlink now either (a symlink found there is refused as a *HomeScopeLinkError, which names it).
var ErrHomeScopeSymlinked = errors.New("fsutil: a directory of this home-scoped path is a symlink")
ErrHomeScopeSymlinked is the refusal for a home-scoped path whose DIRECTORY is a symbolic link: ~/.abcd.noindex itself, or a directory below it on the way to the file. It is the one rule every reader and writer of the caller's machine scope applies, stated for the rules loader in AGENTS.md: a dotfiles-symlinked ~/.abcd.noindex can never host a file abcd trusts. HomeScopeLink returns it wrapped in a *HomeScopeLinkError, so errors.Is finds it.
var ErrNotOneLine = errors.New("fsutil: payload is not exactly one line")
ErrNotOneLine is AppendLineIn's refusal: the payload is empty or carries a line break, so one call would not append exactly one record.
var ErrNotRealDir = errors.New("fsutil: not a real directory")
ErrNotRealDir is the directory-validation sentinel: the path exists, and it is a symlink or a non-directory rather than a real directory. Callers map it onto their own typed refusal with errors.Is.
var ErrOwnerUnknown = errors.New("fsutil: owning uid unavailable")
ErrOwnerUnknown is OwnerUID's sentinel for a path that exists but whose owning uid the platform did not report. abcd builds only darwin and linux, where os.FileInfo.Sys() always carries a *syscall.Stat_t, so it cannot arise there — it exists so a fail-closed caller has a named condition to refuse on rather than an "ok" it never earned.
Functions ¶
func AppendLineIn ¶ added in v0.10.0
AppendLineIn appends line, plus a terminating newline, to rel INSIDE root. It is the canonical append-only log write: the file is opened O_APPEND|O_CREATE and the record reaches it in ONE write call, so the kernel positions every write at the end of the file and two writers — two goroutines or two processes — each land whole lines rather than interleaving within one. That is the whole contract a JSON-lines log needs, and it is why this is neither WriteFileAtomic (which replaces) nor CreateExclusiveIn (which refuses an existing file).
line must be one line: empty, or carrying '\n' or '\r', is ErrNotOneLine and nothing is opened. The newline is added here, so a caller cannot forget it and run two records together. rel is resolved inside root, so a symlinked ancestor is refused rather than followed, and the leaf gets the refusal its read twin ReadGuardedInRoot applies: a symlink or anything but a regular file at rel is ErrNotRegular, even when the symlink stays inside the root, so a log leaf planted as a link onto another file of the store can never append into it. A new file is created at perm, and an existing one keeps its mode. A short write is reported as io.ErrShortWrite.
func CallersAlone ¶ added in v0.11.1
CallersAlone is the half of ReadDeclaration's judgement that makes a path the caller's word, for a path the caller has already stat'd (fi describes it): nil when fi carries no group or other write bit AND path is owned by this session's uid. Otherwise ErrDeclarationWritable (someone else could write it) or ErrDeclarationForeignOwner (another uid owns it, or its owner could not be read). It is exported so a check on something other than a declaration file — a directory whose contents are trusted, such as the harness data directory ahoy promotes a binary out of — applies this one test rather than a copy of it (iss-2609260057111315).
Ownership is looked up through path (OwnerUID follows symlinks), so a caller that must judge a link as itself Lstat's and refuses it before calling, as ReadDeclaration does.
func CallersOrRootsAlone ¶ added in v0.13.1
CallersOrRootsAlone is CallersAlone for a directory a trusted file is reached through, which root may own as well as the caller: nil when fi carries no group or other write bit AND path is owned by this session's uid or by root. Root can replace anything anywhere, so refusing a root-owned 0755 system directory would protect nothing, while a group-writable one (a root:admin 0775 /usr/local/bin) lets every member of that group choose its contents. The errors are CallersAlone's.
func CaseFoldingFS ¶ added in v0.6.7
func CaseFoldingFS() bool
CaseFoldingFS reports whether the platform's default filesystem folds case. macOS (APFS/HFS+ default) and Windows do; abcd assumes this default rather than probing each volume, and the only cost of a false assumption is a stricter comparison.
func CreateExclusiveIn ¶
CreateExclusiveIn writes data to rel INSIDE root, failing if rel already exists. It is the canonical primitive for a durable write that must (a) stay contained under a directory even against a symlinked ancestor, and (b) never replace an existing file.
It is deliberately not WriteFileAtomic. That primitive's contract is atomic REPLACEMENT — a temp file renamed over the target — which is the right shape when a file is rewritten in place, and the wrong one here: the rename would happily clobber, and the containment would depend on the caller having resolved the directory safely rather than on the Root. Here the exclusive create IS both guarantees at once. There is no partial-overwrite hazard to protect against, because the file cannot already exist; a crash mid-write leaves a short file, which the caller's own no-overwrite refusal surfaces on the next run rather than silently completing.
rel is slash-separated and relative to root. os.ErrExist is returned unwrapped enough for errors.Is, so a caller can map it to its own typed refusal. A write that fails after the create removes the partial file.
func CreateRunDir ¶ added in v0.11.1
CreateRunDir creates a fresh directory for one run's log under rel below base and returns its path. It is the local tier's run-log create path: every level from base down through rel is proved a real directory first (EnsureRealDirAll, creating what is missing), so a tier a checkout carries as a committed symlink — at any level — is refused rather than followed out of the checkout. The run directory itself is then created exclusively, never reused: name when it is free, else name-001, name-002 and so on, so two runs in one instant keep two logs.
The directory is returned as a path because callers report it; a caller that writes into it opens it with OpenRealDir, which re-proves the leaf and pins the writes to the directory handle, so a level swapped for a link after this proof cannot carry them elsewhere.
func DirHasEntries ¶
DirHasEntries reports whether path is a directory holding at least one entry, dotfiles included — a directory kept alive by a lone .gitkeep is not empty.
An absent path is false with no error: "missing" and "empty" are distinct conditions, and pairing this with Exists lets a presence rule and a non-empty rule report independently rather than one masking the other. A path that exists but is not a directory is likewise false with no error.
func DisplayPath ¶ added in v0.11.1
DisplayPath is the one statement of how a surface prints a directory it names — a checkout, a worktree, a repository root: under HOME it is the home-relative form RedactHome gives ("~/rel", or "~" for HOME itself), and outside HOME it is the directory's base name. RedactHome alone leaves a path outside HOME whole, and printed whole it is an absolute local path in output a person pastes elsewhere (iss-2609281329007423). A relative or empty p is returned unchanged.
It is for a path shown so a reader can recognise it, not one the reader must type to act: a store directory the person passed with a flag, a brief an agent is handed or a receipt it must write keep RedactHome, because a base name there would leave the reader unable to act on it.
func DisplayPathsIn ¶ added in v0.12.0
DisplayPathsIn is DisplayPath inside a message: s is home-redacted, and each absolute path in paths is replaced by its DisplayPath wherever it starts a path in s (RedactRoot's boundary rule), in both the spelling given and its symlink-resolved one (the one git and the kernel report back). A longer path under it keeps its tail ("/tmp/wt/.git" becomes "wt/.git"). It is for a reason or an error that carries a path the caller already knows, such as a peer's not-read reason naming the peer's directory.
func EnsureHomeScope ¶ added in v0.12.0
EnsureHomeScope is OpenHomeScope for a writer: each missing level is created at perm (a single mkdir relative to the level above, which never follows a link at the name it creates) before it is judged and opened, so the walk that creates ~/.abcd.noindex is the same walk that proves it. It is what a writer uses in place of os.MkdirAll, which follows a symlinked ~/.abcd.noindex and creates under its target. A level that already exists keeps its mode.
func EnsureRealDir ¶ added in v0.8.0
EnsureRealDir creates dir at perm if it is absent — a single, non-following Mkdir, never MkdirAll — and then PROVES the result is a real directory rather than a symlink or a non-directory. It is the canonical directory-validation primitive: exactly one place knows that "create it" and "prove it" are two steps and that the second one is not optional.
The proof is the part a bare os.Mkdir cannot give. Mkdir on an existing symlink returns EEXIST whether the link points at a directory the caller meant or at somewhere it never did, so the ErrExist branch on its own accepts a planted redirect; the Lstat afterwards is what refuses it. Creating one level at a time is the other half — MkdirAll on a leaf follows a symlinked ancestor and creates directories under its target — which is why EnsureRealDirAll walks rather than delegating to MkdirAll.
perm is the mode a NEW directory is created with, and it is a parameter because callers differ legitimately: 0o700 for a private store under the caller's home, 0o755 for a record directory in a shared worktree. An already-existing dir keeps whatever mode it has — Mkdir returns ErrExist without touching it — so this never widens or narrows a directory the caller made themselves, and consolidating callers onto it changes no directory's mode.
A create that fails for any other reason returns os.Mkdir's error unwrapped enough for errors.Is and os.IsPermission. A path occupied by the wrong kind of thing returns ErrNotRealDir inside an *os.PathError, so the offending path is in the message and the class is testable.
func EnsureRealDirAll ¶ added in v0.8.0
EnsureRealDirAll is EnsureRealDir over a chain: it creates every missing level of rel under base and proves EACH level real, so a symlinked ANCESTOR is refused rather than followed. That is the guarantee os.MkdirAll cannot make, and the one a caller's private copy carried as a documented hole before this primitive absorbed it.
base must already exist and is itself proved real; nothing above it is created or inspected. The bound is deliberate rather than incidental — a walk that ran to the filesystem root would refuse the platform's own symlinked prefixes (/tmp and /var are symlinks on macOS, and the test temp directory lives under one) — so the caller's trusted root is where the proof starts.
rel is a slash-separated relative path held to ValidRelPath: a path that arrives as data has no business creating directories, and a caller that joins an untrusted segment onto its root gets a refusal rather than an escape.
func Exists ¶
Exists reports whether path exists, following symlinks — so a link to a real file exists and a dangling link does not. A stat error other than not-exist is returned rather than swallowed, so a caller checking a convention fails closed on a permission error instead of silently reporting "absent".
func ExistsNoFollow ¶
ExistsNoFollow reports whether a filesystem entry occupies path, NEVER following a symlink at the leaf — so a dangling symlink still exists: the name is taken, and whatever occupies it (including a link whose target string is itself sensitive) is what a placement check must see. Ancestor components are resolved by the kernel as usual. Error handling mirrors Exists: not-present is false with no error, anything else fails closed.
func ExpandHome ¶ added in v0.13.4
ExpandHome is ExpandTilde, and also the parameter spellings of the home directory a command line may carry unexpanded (`$HOME/`, `${HOME}/`). It is for text no shell has expanded yet; a word a shell has already expanded takes ExpandTilde alone, because a `$HOME` left in it was quoted.
func ExpandTilde ¶ added in v0.13.4
ExpandTilde resolves a leading `~` the way a shell's tilde expansion does for the account's own home: `~` alone is home, and `~/rest` is rest under home. Any other p — `~user`, a `~` later in the word, no `~` at all — is returned as written, and so is every p when home is empty.
It is the one tilde expander: the shell guard resolving a script a command names (internal/core/guard) and the harness trust check resolving a binary's path (ExpandHome) both route through it rather than keeping copies.
func Flush ¶ added in v0.12.0
Flush makes what was written to f durable — its content for a file, its entries for a directory — and returns the flush's error. It is the one call every durable write in this module flushes through (TestEveryFlushGoesThroughTheGate), so the test-only skip lives in one place.
func FoldPath ¶ added in v0.6.7
FoldPath returns p in the spelling a path comparison uses: lower-cased AND Unicode-normalised (NFC) when fold is set, unchanged otherwise. It is the single place a case-folding comparison key is minted, so a containment gate and a duplicate-target map fold identically rather than drifting apart.
A case-folding filesystem (APFS/HFS+, Windows) is ALSO normalisation- insensitive: the NFC and NFD spellings of one name address the same file, so a key that folds only case leaves the NFC/NFD half of the equivalence class open — the payload-destination gate, the pack overlap gate and embark's claimed key all then read two spellings of one directory as distinct (iss-2608270926030978). NFC is applied AFTER the lower-casing so the key is always in composed form. When fold is false the host is case-sensitive and normalisation-sensitive (Linux), so the key stays byte-exact and two distinct spellings remain distinct.
fold is a parameter rather than a call to CaseFoldingFS because a fail-closed gate's case-folding branch cannot be provoked on a case-sensitive host: the caller passes its own predicate, and a test passes the branch it means to prove.
func HomeDeclarationNames ¶ added in v0.12.0
func HomeDeclarationNames(home, rel string, limit int64, target string, fold bool) (declared bool, ignored string)
HomeDeclarationNames reads the line-oriented path declaration at rel under home through ReadHomeDeclaration and reports whether one of its entries names target. It is the one reader behind every "declare this checkout" opt-in — ~/.abcd.noindex/trusted-roots for the rules resolver, ~/.abcd.noindex/local-transcript-roots for the transcript store — so a hardening of what a declaration must be, or of how an entry is matched, lands once and reaches every caller (iss-2609090951283654).
An entry is a trimmed line that is neither blank nor a "#" comment and is absolute: a relative entry names a different directory per caller and names nothing here. Each entry is compared in two spellings, as written and symlink-resolved, against target in the same two spellings, because a declared path need not be resolved and a caller's target may be either. The comparison key is FoldPath under fold; fold is a parameter, not a call to CaseFoldingFS, so each caller holds the predicate in a variable a test can force and the case-folding branch is provable on a case-sensitive host (iss-2609090951297149).
ignored is empty when there is no declaration (the ordinary case, never a diagnostic) or when it was read; otherwise it is the terminal-safe clause saying why a present declaration was not honoured, for the caller to render in its own voice. A declaration that was not honoured names nothing.
func HomeScopeLink ¶ added in v0.12.0
HomeScopeLink is the check behind that rule. rel is a slash path relative to home (".abcd/path-entry", ".abcd/credentials.json"); every DIRECTORY component of it — ~/.abcd.noindex first, never home itself and never the leaf, whose own guard is the reader's or the writer's — is Lstat'd, and the first that is a symlink is refused with a *HomeScopeLinkError naming it in tilde form (wrapping ErrHomeScopeSymlinked). A component that is absent, or that this uid cannot search, is not a link and ends the walk: nothing below it can be reached through a link that is not there.
It judges by path, so it answers for the layout and not for a race: a caller that must hold the answer through its use — every reader and writer of a file there — reaches the file through OpenHomeScope or EnsureHomeScope, which judge each level the same way and keep the descriptor of the directory judged. A caller may still call HomeScopeLink first to refuse early, before any other work, in the same words.
It exists because every home-scoped primitive guards the LEAF (O_NOFOLLOW, an Lstat of the file) and resolves the directories above it through the kernel, which follows a link without comment. A ~/.abcd.noindex symlinked into a dotfiles checkout therefore hosted a path-entry that decides which binary the hook shims execute, a cache attestation that decides which binary is promoted onto PATH, and a credentials.json written into a repository — while the rules loader beside them refused its rules.json there (iss-2609281017573862, iss-2609260958587561). One check, called by all of them, is what keeps the rule from drifting per reader.
home itself is deliberately not judged: a home directory reached through a link (/home -> /usr/home, a relocated account) is the machine's layout, not a declaration the caller could have made somewhere else, and refusing it would refuse every account laid out that way.
rel must be a clean relative path (ValidRelPath); anything else is refused with an *os.PathError wrapping os.ErrInvalid rather than judged. An escaping rel ("../x/f") leaves home before any directory below it is reached, and an absolute one would judge home itself, so passing either would vouch for a path the walk never covered.
func InsideGitDir ¶ added in v0.8.0
InsideGitDir reports whether the repo-relative path p names the git directory or anything under it.
ValidRelPath accepts ".git/config" — it is clean, relative, and inside the containment root — so a configuration value that arrives as data and is then READ AND PUBLISHED needs this second gate on top of it: .git/config carries a credential-bearing remote URL (on a CI runner, the checkout token in an `http.…extraheader` line), and nothing under .git is ever a legitimate source of rendered text. The first segment is compared case-insensitively, because a case-folding filesystem reaches the same .git through ".GIT" (iss-150).
Only the leading segment is examined, so ".github/workflows/ci.yml" and ".gitignore" — different directories that merely start with the same bytes — are not caught.
func IsDir ¶
IsDir reports whether path exists and is a directory. An absent path is false with no error; any other stat error is returned (fail closed).
It follows symlinks: use IsRealDir where a symlinked directory must read as false (the owned-store guard).
func IsPathSegmentByte ¶ added in v0.12.0
IsPathSegmentByte reports whether b can be part of a path segment — a letter, a digit, '.', '-', '_' or the '/' separator — so a root that starts straight after such a byte is the tail of a longer path rather than a path of its own. It is the one statement of the leading boundary: RedactRoot reads it here and the scanner's home detectors import it (iss-2608292037564347).
func IsRealDir ¶
IsRealDir reports whether path is a directory and NOT a symlink. It lstats (never following) so a symlink pointing at a directory reads as false — the owned-directory guard the store re-runs on every mutating call.
It checks the LEAF only: every ancestor component is resolved by the kernel, so a symlinked parent passes. Where a write must be contained against a symlinked ancestor too, route it through CreateExclusiveIn under an os.Root opened at the trust boundary, which refuses symlink traversal at every level.
func ModuleRoot ¶
ModuleRoot walks up from start until it finds the directory holding go.mod — the module root.
It exists for the repo's code generators, which must write to the same file whether they are invoked by `go generate` (the working directory is the package being generated) or run directly from the repo root. Anchoring on go.mod rather than on .git means a generator also works inside a worktree or an export where the git directory is not where the walk expects it.
A start outside any module is an error rather than a fallback to the working directory: a generator that silently wrote its artefact into an unrelated directory would be worse than one that refuses.
func NameContinues ¶ added in v0.12.0
NameContinues reports whether the name ending at byte offset end of s goes on: a letter or a digit at end continues it, and nothing else does. It is the one statement of the trailing boundary, shared by RedactRoot and the scanner's home sweep, detector and backstop (iss-2608292037564347). The only false positive a trailing anchor exists for is a longer alphanumeric name that starts with the root ("/rootfs" under "/root"); '.', '-' and '_' end the name, so "<root>." at a sentence end, "<root>-old" and "<root>_snapshot" carry the root with a suffix and are redacted. Over-redacting a suffixed sibling is the safe side for an identity-bearing root.
func OpenHomeScope ¶ added in v0.12.0
OpenHomeScope opens the directory dir below home (a slash path, ".abcd" or ".abcd/history") as an *os.Root, walking it one level at a time relative to the descriptor of the level above, so HomeScopeLink's rule holds against a race and not only against a layout. It is the descriptor form of that rule: HomeScopeLink judges each level by path and the caller then reaches the file by path again, so a process running as the same uid that swaps ~/.abcd.noindex for a symlink between the judgement and the use reads or writes through the link (iss-2609281310017733). Here every use goes through the returned root, and the root is the very directory each level's judgement was made about.
Each level is judged and then opened: Lstat relative to the level above (never following), a symlink refused with a *HomeScopeLinkError naming it, a non-directory refused with ErrNotRealDir, then the level opened relative to the same descriptor and confirmed with os.SameFile to be the directory the Lstat vetted. The confirmation is what os.Root alone cannot give: it follows a symlink that stays inside the root, and a dotfiles ~/.abcd.noindex usually points inside home. A level replaced between its Lstat and its open is refused — as a *HomeScopeLinkError when a symlink stands there now, and as ErrHomeScopeSwapped otherwise. The guarantee is the one an openat with O_NOFOLLOW gives, which the standard library does not expose on every platform abcd builds for: the directory held is the one that stood at that name, a real directory, when it was vetted; a rename of it afterwards moves it without changing what the root refers to.
home itself is opened by path and never judged, for HomeScopeLink's reason (decision 2): a home reached through a link is the machine's layout. dir is held to ValidRelPath, or is "." for home itself. An absent level returns the Lstat's error, which os.IsNotExist recognises. The caller closes the root.
func OpenRealDir ¶ added in v0.11.1
OpenRealDir opens dir as an os.Root only when it is a real directory rather than a symlink or a non-directory, so every write through the handle lands in the directory that was proved and nowhere its path might name later. The caller closes the root.
func OpenRegular ¶ added in v0.12.0
OpenRegular is the open half of ReadGuarded, for a caller that reads through the descriptor itself rather than taking the whole file into memory (a ReaderAt consumer such as debug/buildinfo over a binary). It opens path once, read-only, with O_NOFOLLOW and O_NONBLOCK, and judges the SAME descriptor with fstat: a symlinked leaf fails the open (ELOOP, returned raw), and anything but a regular file — a FIFO swapped in after the caller's lstat included — is closed and refused with ErrNotRegular instead of blocking. The FileInfo returned is the descriptor's own; the caller closes the file.
func OwnerUID ¶ added in v0.8.0
OwnerUID reports the uid that owns path, following symlinks (the caller has usually resolved the path already, and a link's own uid is not the uid that controls its content). It is the canonical ownership lookup: code asking "is this directory mine?" compares this against os.Getuid() instead of reaching for syscall.Stat_t itself, so exactly one place knows the platform's stat shape.
The error is returned rather than folded into a boolean because ownership is a fail-closed question: a caller that cannot learn the owner must be able to tell that from learning the owner is itself, and a (uid, bool) shape makes the two look alike at the call site.
func PathWithin ¶ added in v0.6.7
PathWithin reports whether child is equal to or nested inside parent. With fold set the comparison is case-insensitive: otherwise a destination like ".../REPO/dist" computes as an out-of-tree sibling of ".../repo" and slips a containment gate, even though the two name the SAME directory on a case-folding filesystem — and the write then lands inside the tree the gate exists to protect. Erring toward "within" is the safe direction for a destructive-write gate (iss-2608270500192615).
It is the canonical containment comparison: the launch payload-destination gate and the lifeboat pack overlap gate both route through it rather than keeping divergent prefix tests.
func PathsOverlap ¶ added in v0.6.7
PathsOverlap reports whether a and b are the same directory or one contains the other — either way a write under one touches the other.
func ProbeRealDirAll ¶ added in v0.11.1
ProbeRealDirAll is EnsureRealDirAll's read-only counterpart: it walks rel under base one level at a time, creating nothing, so a reader refuses exactly the levels the creating walk refuses. ok is true when base and every level of rel stand as real directories. A missing level ends the walk with ok false and a nil error — there is nothing under it to read. A level a symlink or a non-directory occupies, base included, is ErrNotRealDir inside an *os.PathError naming that level, the error EnsureRealDirAll returns for it, so a reading verb and a writing verb refuse the same path in the same terms. Any other lstat failure is returned, so the probe fails closed.
func ReadGuarded ¶
ReadGuarded opens path once, read-only, with O_NOFOLLOW (refuse a symlinked leaf) and O_NONBLOCK (a FIFO/device leaf returns immediately instead of blocking the open forever), then validates on the SAME descriptor that it is a regular file within limit bytes before reading through a LimitReader — so no symlink swap between stat and read, no non-regular leaf, and no size overrun can reach the caller. It is the shared trust-boundary read primitive for any file inside a repo working tree that untrusted content could have replaced with a symlink to /dev/zero or an endless device. The raw open error is returned so callers can test os.IsNotExist / syscall.ELOOP; a non-regular or oversize file returns ErrNotRegular / ErrTooBig.
func ReadGuardedInRoot ¶
ReadGuardedInRoot is ReadGuarded resolved inside an os.Root containment scope. rel is a slash-separated path relative to root; every component is resolved by the OS within root, so a symlinked ANCESTOR directory — the shape a hostile clone commits as git mode 120000, `etclink -> /etc` or `home -> ../../..` — cannot walk the read outside the root. ValidRelPath is lexical and cannot see a symlink, so containment must rest here, not on it.
It keeps every guarantee ReadGuarded gives at the leaf: a symlinked leaf is refused (lstat, never followed), the descriptor is confirmed to be the very file that was vetted (os.SameFile closes the lstat→open swap; a replacement is vetted again from scratch, up to declarationAttempts times, so the benign rewrite a concurrent WriteFileAtomicInRoot makes is read and anything that is not a regular file is still refused), a non-regular leaf returns ErrNotRegular, O_NONBLOCK stops a FIFO or device blocking the open, and the caller's byte cap is enforced against both the fstat size and the bytes actually read.
Errors are returned raw where the OS raised them, so a caller can still test os.IsNotExist; a path that escapes the root is NOT an os.IsNotExist error, so a caller skipping absent files fails closed on the escape rather than treating it as "the file is not there".
func RealExistingPath ¶ added in v0.6.9
RealExistingPath returns p made absolute with its longest EXISTING prefix symlink-resolved and the absent remainder rejoined lexically. A destination that does not exist yet still resolves through its real parent, so a containment gate compares real locations rather than lexical strings a symlinked ancestor could disguise — and on a platform whose temp root is itself a symlink (/var -> /private/var on macOS) an unresolved comparison would silently miss an overlap. An empty p is returned empty rather than resolved to the working directory.
It is the canonical existing-prefix resolver: the launch payload-destination gate, the lifeboat pack destination gate and the site output-directory gate all route through it rather than keeping divergent copies. It does not case-canonicalise (Go's darwin EvalSymlinks keeps the caller's spelling for every non-symlink component), so compare its results with PathWithin under CaseFoldingFS.
func RedactHome ¶
RedactHome replaces the user's home directory in s with "~" wherever it appears — the home-polarity shortcut over RedactRoot for a success or receipt envelope that carries an absolute path the CLI error scrub (which runs only on the error value) never sees. A home-resolution failure or a root-level home leaves s unchanged, so the primitive is never worse than the raw string.
The home is redacted in both spellings a path can carry it in: the one the environment names, and its symlink-resolved one, which is what git and the kernel report back wherever the home sits behind a link (macOS's /var → /private/var). Where one spelling is the tail of the other, RedactRoot's left boundary keeps the shorter from matching inside the longer, so the order of the two passes does not matter.
func RedactRoot ¶
RedactRoot replaces every occurrence of the absolute directory root in s with repl — both a path UNDER the root (root + separator + …) and the BARE root itself when it sits at a right boundary: the end of s, or any byte that is not a letter or a digit (NameContinues), so "<root>.", "<root>-old" and "<root>_snapshot" are redacted while "/rootfs" under "/root" is not. The bare-root case matters because a message that names exactly $HOME (e.g. "cannot access /Users/alex") would otherwise leak the developer abcd-audit:allow identity — its base segment IS the username. The filesystem root ("/") and empty or relative roots are skipped so a message is never mangled.
An occurrence is redacted only where it STARTS a path: at the start of s, or after a character that cannot continue a path segment (a space, a quote, a colon, an equals sign — and a separator, so a file:/// URL still redacts). The same bytes as the tail of a longer path — root /var/… inside the symlink-resolved /private/var/… — name a different directory, and redacting them would strand that path's head in front of repl ("/private~/wt/a"), neither redacted nor intact (iss-2609230641546141).
It is the one statement of what a developer-identity root looks like in rendered output: the CLI's error scrub redacts the working directory and the home directory out of every command error, and `ahoy install`'s receipt redacts the same two roots out of every write it reports (iss-177). Callers pass the root and the replacement because the polarity differs — "." for the repo, "~" for home — but the boundary rule must not.
On a case-folding filesystem the match is case-insensitive: a message that echoes $HOME in a case variant (as a shell or a syscall may on macOS/APFS) names the same directory, so the identity must still be scrubbed. The original casing of any UNREDACTED text is preserved, and on a case-sensitive host the match stays byte-exact so two distinct paths are never merged (iss-2608270908341622).
func RepoRel ¶
RepoRel renders target as a path relative to base — the repo root, or the working directory — so machine output never carries an absolute developer-identity path (iss-81). A target outside base yields a "../…" form, which is acceptable: the contract is only that the result is never an absolute /Users/<name>/… path. It falls back to the base name when a relative form cannot be computed (a different volume, a relative target, or an empty base) and to target unchanged when target is already relative. A non-path value that merely looks absolute-free (e.g. a URL) is returned untouched.
func RepoRelativePath ¶ added in v0.12.0
RepoRelativePath is a path as a report names it when the report is about the inside of a root the caller passes. A report travels into --json, and machine output never carries an absolute developer-identity path (iss-81): a path inside repoRoot is named relative to it, slash-separated; one outside it has the home directory redacted to "~" (RedactHome); a relative path is reported as it was given. It is for display only — a caller that acts on the path keeps the working value beside it.
Three primitives, three rules: RedactHome names the home directory "~" in any text; DisplayPath names a directory a surface points at (a checkout, a worktree); RepoRelativePath names a path reported inside a root.
Inside is judged lexically first and then over the real locations, so a path the kernel resolved (macOS's /private/var for /var) still reads as inside a repoRoot spelled the other way.
func SwapHomeScopeVettedForTest ¶ added in v0.12.0
func SwapHomeScopeVettedForTest(fn func(dir string)) (restore func())
SwapHomeScopeVettedForTest substitutes the hook that runs between a home scope level's vetting Lstat and its open, and returns the restore. It is exported because the writers whose refusal needs proving live in other packages (credential, oracle, ahoy). Tests only; never called in production code, and never safe to call from a parallel test.
func SwapOwnerUIDForTest ¶ added in v0.9.0
SwapOwnerUIDForTest substitutes the owner lookup ReadDeclaration uses and returns the restore. It is exported because the callers whose refusals need proving live in other packages (rules, history, ahoy) and a second uid is not something a test process can create. Tests only; never called in production code, and never safe to call from a parallel test.
func ValidRelPath ¶
ValidRelPath reports whether p is a safe repo-relative slash path: non-empty, not absolute, already clean, free of empty, ".", ".." or control-character segments, and containing no backslash. It is the canonical lexical guard for a path that arrives as data — a committed configuration value, a packed lifeboat's manifest entry — before it is joined onto a trusted root. Lexical only: it says nothing about symlinks, so a caller that then follows the path still needs the guarded read/write primitives.
A backslash is refused outright, on every host. path.Clean is slash-only, so it leaves a backslash untouched: `..\..\x` is "clean" and slash-traversal-free, yet on a Windows target (abcd cross-compiles Windows binaries) filepath.FromSlash turns those backslashes into separators and the join walks out of the intended root. A legitimate committed repo-relative path is always a forward-slash path and never contains a backslash, so refusing it everywhere closes the Windows escape while keeping the gate identical across platforms (iss-2608280807166510).
func WithDirLock ¶ added in v0.12.0
WithDirLock acquires an exclusive advisory (flock) lock on the directory dir itself, holds it across fn, and releases it when fn returns. It is WithFileLock's contract for a store that locks its own directory rather than a lock file beside its records, so no lock artefact is left in a committed tree: the same poll and the same timeout, ErrLockContention past it naming the caller's budget, and fn's own error returned unwrapped.
The directory is opened O_NOFOLLOW|O_DIRECTORY and never created. A symlink or anything that is not a directory at dir is ErrLockPathUnsafe; an absent dir is the open's own errno, unwrapped as WithFileLock returns its open's (errors.Is fs.ErrNotExist), so a caller can tell a store that is not there from one it may not lock, and wrap it in words of its own. The descriptor is close-on-exec, as openLockFd's is, so a child the holder starts cannot keep the lock past the holder.
There is no inode revalidation, unlike WithFileLock: a directory lock is not retired by unlinking it, so a holder must not remove dir inside fn, or a waiter would be granted the lock on the removed inode.
func WithFileLock ¶
WithFileLock acquires an exclusive advisory (flock) lock on lockPath, holds it across fn, and releases it when fn returns. The lock file is opened O_NOFOLLOW (a symlinked lock path is refused) and verified on the same descriptor to be a regular file, both reported as ErrLockPathUnsafe; the exclusive flock is polled for up to timeout, after which ErrLockContention is returned. fn's own error is returned unwrapped.
It is the single inter-process load-modify-write lock primitive for a lock file, as WithDirLock is for a lock on a directory: every flock in the tree goes through one of the two rather than a divergent loop of its own (the one-canonical-primitive invariant, iss-129). The lock is advisory — every writer of the guarded state must take it — and its scope is one lock file; callers that must not deadlock keep their acquisitions unnested.
A holder MAY unlink lockPath inside fn, which is how a caller retires a per-key lock file once the key has nothing left to guard. That is safe because acquisition is revalidated: after the flock is granted, the path must still name the inode that was locked. A waiter that opened the file before a holder unlinked it would otherwise be granted the flock on the orphaned inode while a newcomer holds the fresh file at the same path — two holders of one lock. On a mismatch the waiter releases, reopens the current file and waits again, all within the one timeout.
func WithFileLockIn ¶ added in v0.11.1
WithFileLockIn is WithFileLock with the lock file resolved INSIDE root: rel is a slash path relative to it, so an ancestor swapped for a symlink cannot carry the lock out of the containment scope between a caller's checks and the open. The leaf keeps WithFileLock's refusals: a symlink or a non-regular file at rel is ErrLockPathUnsafe, judged by Lstat and confirmed on the opened descriptor (os.SameFile), because os.Root follows a symlink that stays inside the root.
func WritableByOthers ¶ added in v0.12.0
WritableByOthers reports whether fi's mode carries a group or other write bit: the one test of "someone else could write this" that CallersAlone applies, exported for a check that judges the mode alone, such as a harness binary a runner is about to start, which root may own.
func WriteFileAtomic ¶
WriteFileAtomic writes data to path durably: a temp file in the target directory is written, chmod'd to perm on its open descriptor, flushed, fsync'd, then renamed over the target, and finally the parent directory is fsync'd best-effort so the rename survives a crash. Parent directories are created as needed.
The rename is the commit point: a reader sees either the old file or the complete new one, never a half-written file. os.Rename does not follow a symlink at the leaf — a pre-planted symlink at path is replaced by the real file, not written through. The mode is set with fchmod on the open descriptor (never chmod-by-name on the closed temp), so the enumerable temp name cannot be swapped for a symlink between close and chmod and have the mode applied to an attacker-chosen target.
func WriteFileAtomicInRoot ¶
WriteFileAtomicInRoot is WriteFileAtomic resolved inside an os.Root containment scope: rel is a slash-separated path relative to root, and the missing parents, the temp file, the rename, and the parent fsync all happen through root. A symlinked ancestor committed by a hostile repo therefore cannot land the write outside the root — the counterpart of ReadGuardedInRoot, and the reason a write path validated only lexically is not contained.
The commit point and the ordering are WriteFileAtomic's: write, fchmod on the open descriptor (never chmod-by-name), fsync, close, rename, parent fsync. The temp name is unpredictable so the enumerable-name swap WriteFileAtomic guards against has nothing to aim at.
func WriteFileAtomicPreserveMode ¶
WriteFileAtomicPreserveMode is WriteFileAtomic that keeps the target's existing permission bits when it already exists, defaulting to 0644 for a new file. It is the canonical form for the store writers that rewrite a file in place and must not silently reset its mode.
func WriteFileAtomicPreserveModeInRoot ¶
WriteFileAtomicPreserveModeInRoot is WriteFileAtomicInRoot that keeps the target's existing permission bits when it already exists, defaulting to 0644 for a new file — the contained counterpart of WriteFileAtomicPreserveMode.
Types ¶
type DeclarationModeError ¶ added in v0.12.0
DeclarationModeError is the error of a DeclarationExposed refusal: Perm is the permission bits of the file that was opened, judged on its own descriptor, so a caller can name the mode it refused and the chmod that repairs it.
func (*DeclarationModeError) Error ¶ added in v0.12.0
func (e *DeclarationModeError) Error() string
type DeclarationRefusal ¶ added in v0.9.0
type DeclarationRefusal int
DeclarationRefusal names which of ReadDeclaration's guards refused a home-scoped declaration file. It exists so each caller can render the refusal in its own voice — the rules loader says a root "re-admitted nothing", the transcript store says "transcripts stay in ~/.abcd.noindex/transcripts" — while the judgement itself is made in exactly one place.
DeclarationAbsent is separated from DeclarationUnreadable deliberately: no declaration is the ordinary case and not a diagnostic, whereas a declaration that is there and cannot be read is something the operator needs told.
const ( // DeclarationOK: the file passed every guard and its bytes were returned. DeclarationOK DeclarationRefusal = iota // DeclarationAbsent: there is no declaration (the ordinary case). DeclarationAbsent // DeclarationNotRegular: a symlink, directory, FIFO or device. DeclarationNotRegular // DeclarationWritableByOthers: mode carries group or other write. DeclarationWritableByOthers // DeclarationForeignOwner: owned by another uid, or the owner could not be read. DeclarationForeignOwner // DeclarationUnreadable: it passed the guards but the read itself failed. DeclarationUnreadable // DeclarationBehindSymlink: the file is there, but a directory between the // home and it (~/.abcd.noindex first) is a symlink (ReadHomeDeclaration only). DeclarationBehindSymlink // DeclarationExposed: the opened file's mode carries a permission bit its // reader denies — a secret group or other can read // (ReadHomeDeclarationDenying only). The error is a *DeclarationModeError. DeclarationExposed // DeclarationDirectoryExposed: the file is there, but a directory between // the home and it (~/.abcd.noindex first) can be written by every account or is // owned by another (ReadHomeDeclaration only). The error is a // *HomeScopeExposedError naming the directory. DeclarationDirectoryExposed )
func ReadDeclaration ¶ added in v0.9.0
func ReadDeclaration(path string, limit int64) ([]byte, DeclarationRefusal, error)
ReadDeclaration is the guarded read for a HOME-SCOPED DECLARATION FILE — a record in the caller's own home that re-admits something abcd would otherwise refuse (~/.abcd.noindex/trusted-roots re-admits a marker root, ~/.abcd.noindex/path-entry names the binary the hook shims execute, ~/.abcd.noindex/local-transcript-roots pulls a repo's transcripts into its own tree).
It is ReadGuarded plus the two facts that make the file the CALLER'S WORD:
- not writable by group or other (Perm()&0o022 == 0), so nobody else could have written what it says;
- owned by this session's uid, so it is not another account's declaration being honoured as though it were this one's.
A declaration this process does not own, or one anyone can write, is not the caller's word and re-admits nothing. That is the whole reason this is one primitive rather than a guard each caller writes: the three records differ in what they declare, not in what makes a declaration trustworthy, and the one that skipped the two checks was the one whose consequence is code execution (iss-2609091927085132).
The permission and owner checks run on an Lstat BEFORE the open, so a symlink is judged as itself rather than through its target; the read then re-opens with O_NOFOLLOW, re-validates on its own descriptor, and confirms with os.SameFile that the descriptor is the file the lstat vetted, so the lstat→open window can promote no file into a read that the guards did not judge. The bytes returned are always those of a descriptor that is the very file an Lstat passed through every guard.
A file renamed into place inside that window is not read and not refused on sight: the whole judgement runs again on whatever the path names now, up to declarationAttempts times. A same-owner, owner-only-writable regular file — the rewrite a concurrent abcd makes through WriteFileAtomic — passes and is read; a symlink, FIFO, device or directory, a file writable by group or other, and a file another uid owns are each refused by the guard that judges them, exactly as they would be had they been there before the first Lstat. A replacement still unsettled after the last attempt is DeclarationUnreadable with ErrDeclarationSwapped.
The returned error is ALWAYS non-nil when the refusal is not DeclarationOK, so a caller that inspects only the error still fails closed. Callers that need to say WHICH guard refused — and to keep "absent" silent while reporting "unreadable" — switch on the refusal instead.
func ReadHomeDeclaration ¶ added in v0.12.0
func ReadHomeDeclaration(home, rel string, limit int64) ([]byte, DeclarationRefusal, error)
ReadHomeDeclaration is ReadDeclaration for a declaration file named by its place in the caller's home: home joined with rel (a slash path, ".abcd/trusted-roots"). It is the read every home-scoped declaration goes through, and it adds the one fact ReadDeclaration cannot see from a path — that no directory between home and the file is a symlink — and holds it through the read: the directory is opened by OpenHomeScope, and the file is judged and read relative to that descriptor, never by its path again (iss-2609281310017733).
The order keeps AGENTS.md's rule exactly: a file that is not there is DeclarationAbsent whatever the directories are, so a symlinked ~/.abcd.noindex holding no such file reads as absent and costs its owner nothing; a file that IS there behind a symlinked directory is DeclarationBehindSymlink, refused before a byte of it is read. The Lstat that decides absence follows the directories, which is what lets a file behind a link be found in order to be refused; it decides nothing else.
The file's own guards are ReadDeclaration's, applied to the descriptor: a leaf that is not a regular file is DeclarationNotRegular, one writable by group or other or owned by another uid is DeclarationWritableByOthers or DeclarationForeignOwner, and a leaf replaced between its judgement and its open is judged again from scratch, as ReadDeclaration judges one: read when the replacement passes every guard, refused by the guard it fails, and DeclarationUnreadable with ErrDeclarationSwapped when it is still being replaced after declarationAttempts judgements. A directory level replaced while it was opened is DeclarationBehindSymlink when a symlink stands there now and DeclarationUnreadable otherwise.
Each directory level below home is judged too, on the descriptor opened for it: one every account can write, or one owned by an account that is neither this uid nor root, is DeclarationDirectoryExposed with a *HomeScopeExposedError naming it, because whoever can change the directory can put a file of the caller's own shape in it under the declaration's name (iss-2609290656480443). The check follows the absence check, so an exposed directory holding no such file still reads as absent; home itself is not judged, for HomeScopeLink's reason.
A rel that is not a clean relative path is DeclarationUnreadable before anything is looked at: it names no place in the home to read.
func ReadHomeDeclarationDenying ¶ added in v0.12.0
func ReadHomeDeclarationDenying(home, rel string, limit int64, deny os.FileMode) ([]byte, DeclarationRefusal, error)
ReadHomeDeclarationDenying is ReadHomeDeclaration for a file whose reader refuses more of its mode than a declaration's group- and other-write: deny holds the permission bits that refuse it (0o077 for a secret no one else may read). The bits are judged on the fstat of the file that was opened, never on a path, so a file swapped or re-moded after any check by path is judged as the file that is read (iss-2609281310017733). A refusal is DeclarationExposed with a *DeclarationModeError naming the mode. Every other guard, and its order, is ReadHomeDeclaration's.
func ReadHomeDeclarationDenyingIn ¶ added in v0.12.0
func ReadHomeDeclarationDenyingIn(dir *os.Root, home, rel string, limit int64, deny os.FileMode) ([]byte, DeclarationRefusal, error)
ReadHomeDeclarationDenyingIn is ReadHomeDeclarationDenying over a directory the caller already holds: dir is the directory of rel under home, opened by the caller's own walk (EnsureHomeScope or OpenHomeScope), and the file is read relative to that descriptor rather than by walking home again. A caller that writes through dir reads through it too, so a same-uid swap of the directory between its walk and the read cannot make it read one directory and write another (iss-2609290300313698). Every guard on the file, and its order, is ReadHomeDeclarationDenying's; an absent file is DeclarationAbsent with an error satisfying os.ErrNotExist.
type HomeScopeExposedError ¶ added in v0.12.0
type HomeScopeExposedError struct {
// Dir is the exposed directory in tilde form ("~/.abcd.noindex").
Dir string
// Perm is the directory's permission bits, judged on its descriptor.
Perm os.FileMode
// Foreign is true when the refusal is the owner, not the mode.
Foreign bool
}
HomeScopeExposedError names the exposed directory in tilde form. Its message is the whole operator-facing sentence, the remedy included.
func (*HomeScopeExposedError) Error ¶ added in v0.12.0
func (e *HomeScopeExposedError) Error() string
func (*HomeScopeExposedError) Unwrap ¶ added in v0.12.0
func (e *HomeScopeExposedError) Unwrap() error
type HomeScopeLinkError ¶ added in v0.12.0
type HomeScopeLinkError struct {
// Link is the symlinked directory in tilde form ("~/.abcd.noindex").
Link string
}
HomeScopeLinkError names the symlinked directory in tilde form. Its message is the whole operator-facing sentence, the remedy included, so every reader and writer that refuses says the same thing.
func (*HomeScopeLinkError) Error ¶ added in v0.12.0
func (e *HomeScopeLinkError) Error() string
func (*HomeScopeLinkError) Unwrap ¶ added in v0.12.0
func (e *HomeScopeLinkError) Unwrap() error