Documentation
¶
Overview ¶
Package safefs contains DevProof's filesystem boundaries: private workspaces, snapshotting a source tree, and extracting a verified payload.
Everything here assumes the filesystem is adversarial. A source directory can change while it is being read, a path can be a symlink to somewhere else, an archive entry can claim to live outside its root, and a destination can appear between the moment it was checked and the moment it is published. The package is built so that none of those produce a wrong answer — only a failure.
Two rules run through all of it.
Confinement is by file descriptor, not by string comparison. Operations go through os.Root, which resolves every component relative to a held directory handle and refuses to traverse a symlink out of it. Checking that a cleaned path has the right prefix is not containment: the check and the open are separate moments, and a symlink can be introduced in between.
Nothing is published until it is complete and verified. A snapshot is hashed as it is copied and read back through the same digests; an expansion is written into a private staging directory and moved into place with an exclusive rename. A failed, canceled, or interrupted operation leaves nothing that could be mistaken for a successful one.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type ExtractOptions ¶
type ExtractOptions struct {
// Destination is the directory to publish. It must not exist.
Destination string
// Config is the verified inventory the layer must match exactly.
Config *bundle.Config
// Limits bounds the expansion.
Limits bundle.Limits
// LayerDigest is the compressed layer's digest from the OCI descriptor.
//
// Checking every decoded entry against the inventory is not the same
// claim as checking the bytes. A differently compressed archive holding
// exactly the declared files satisfies every entry check and is still not
// the layer the subject names, so identity has to be established over the
// compressed stream itself.
LayerDigest canonical.Digest
// LayerSize is the compressed layer's size from the OCI descriptor.
LayerSize int64
}
ExtractOptions configures expansion.
type ExtractResult ¶
ExtractResult reports what was published.
func Extract ¶
func Extract(ctx context.Context, layer io.Reader, opts ExtractOptions) (_ *ExtractResult, retErr error)
Extract expands a compressed layer into a new directory.
Extraction and verification are one operation. Every entry is checked against the config inventory before its destination is opened, and its bytes are hashed as they are written, so nothing is ever on disk that has not been verified. The result is assembled in a private staging directory and published with an exclusive rename, which is what makes a failed, canceled, or interrupted expansion leave no destination at all rather than a partial one that looks finished (DP-008, DP-022).
type Snapshot ¶
type Snapshot struct {
// contains filtered or unexported fields
}
Snapshot is a frozen, private copy of a source tree.
Packaging reads only from here, never from the original location. That is what closes the time-of-check window: content is hashed as it is copied in, and hashed again as it is read out, so a source edited mid-build fails the operation instead of producing a bundle whose layer disagrees with the inventory that describes it.
Paths are source-relative. A snapshot does not know where it will be mounted, which is what lets one source have a single tree digest no matter where composition places it.
func SnapshotDir ¶
SnapshotDir copies a local directory into a private workspace.
Every file is stat'd with Lstat and rejected unless it is a regular file. Following a symlink would let a source tree pull in content from anywhere the build account can read, and DP-005 excludes links from the portable profile precisely so that no consumer has to guess what one meant.
func SnapshotTree ¶
func SnapshotTree(ctx context.Context, tree TreeSource, opts TreeOptions) (_ *Snapshot, retErr error)
SnapshotTree copies a non-filesystem source into a private workspace.
It is the same contract as SnapshotDir — content hashed on the way in, stored privately, read back through the same digests — for sources that have no directory to walk.
func (*Snapshot) Open ¶
Open returns the content of a path in the snapshot.
Only a path that appears in the inventory can be opened. Anything else is refused before touching the filesystem, so there is no way to reach a file this snapshot did not record.
func (*Snapshot) Records ¶
func (s *Snapshot) Records() []canonical.FileRecord
Records returns the canonical inventory, sorted by path.
type SnapshotOptions ¶
type SnapshotOptions struct {
// Patterns filters source-relative paths. Nil selects everything.
Patterns *bundle.PatternSet
// Limits bounds what may be snapshotted.
Limits bundle.Limits
// TempRoot is the parent for the private workspace. Empty uses the
// system temporary directory.
TempRoot string
}
SnapshotOptions configures a source snapshot.
type TreeEntry ¶
type TreeEntry struct {
// Path is source-relative and slash-separated.
Path string
// Mode is already normalized to bundle.ModeFile or ModeExecutable. The
// source decides, because only it knows whether an execute bit is
// authoritative — for Git it is the tree entry, for a filesystem it is
// the stat.
Mode uint32
// Size is the entry's declared length, used for limit checks before any
// bytes are read.
Size int64
// Open returns the entry's content.
Open func() (io.ReadCloser, error)
}
TreeEntry is one file from a non-filesystem source.
type TreeOptions ¶
type TreeOptions struct {
Patterns *bundle.PatternSet
Limits bundle.Limits
TempRoot string
}
TreeOptions configures a tree snapshot.
type TreeSource ¶
TreeSource yields entries from a source that is not a directory tree.
It exists so that a Git resolver can hand over blobs from a commit object without ever materializing a working directory, where checkout filters would rewrite the bytes.
type VerifyLayerOptions ¶ added in v0.2.0
type VerifyLayerOptions struct {
// Config is the inventory the layer must match exactly.
Config *bundle.Config
// Limits bounds the work.
Limits bundle.Limits
// LayerDigest and LayerSize come from the OCI layer descriptor.
LayerDigest canonical.Digest
LayerSize int64
}
VerifyLayerOptions configures a validate-only pass over a layer.
type VerifyLayerResult ¶ added in v0.2.0
VerifyLayerResult reports what a validate-only pass observed.
func VerifyLayer ¶ added in v0.2.0
func VerifyLayer(ctx context.Context, layer io.Reader, opts VerifyLayerOptions) (*VerifyLayerResult, error)
VerifyLayer streams a layer and checks it without writing anything.
It runs the identical checks Extract runs — compressed identity, gzip framing, path canonicalization, inventory agreement, per-entry size and content digests, and every limit — and differs only in having nowhere to put the bytes. Sharing one implementation is the point: a verifier that checked less than the extractor would report a pass the extractor then refused, and a verifier that checked differently would eventually disagree with it about something subtler.
type Workspace ¶
type Workspace struct {
// contains filtered or unexported fields
}
Workspace is a private scratch directory owned by one operation.
It holds an os.Root rather than a path so that everything written inside it is resolved against a directory handle. That makes the confinement survive an attacker who introduces a symlink partway through the operation, which a path-prefix check would not.
func NewWorkspace ¶
NewWorkspace creates a private directory under parent.
parent must already exist. An empty parent uses the system temporary directory, which is the right default for snapshots; expansion staging passes the destination's own parent instead, because publication has to be a same-filesystem rename (DP-022).
func (*Workspace) Close ¶
Close removes the workspace and everything in it. It is idempotent.
Cleanup removes only the exact directory this workspace created. It never walks up, resolves a symlink, or accepts a path from anywhere but uniqueName, so there is no input through which it could be aimed at a broader tree.
func (*Workspace) Detach ¶
Detach gives up ownership, returning the path without removing it.
Expansion uses this after a successful publication: the staging directory has become the caller's destination, so removing it on Close would delete the result.
func (*Workspace) ReleaseHandle ¶
ReleaseHandle closes the confined directory handle without giving up ownership.
Publication has to happen with no handle open on the staging directory: Windows refuses to move a directory that anything still holds open, and the expansion would fail at the last step with a sharing violation. Ownership is kept, so a failure after this still removes the staging directory on Close — which works from the path and does not need the handle.
Safe to call more than once.