Repository navigation
feat(pkgdef): add package definition library - #3001
mikeland73 wants to merge 2 commits into
Conversation
Add internal/pkgdef, the library behind package definitions (see specs/package-definitions.md): parsing and validation of *.pkg.json files, pkg:<ref>@<version> reference parsing, loading local and remote definitions (remote ones cached and pinned by content hash), version resolution from static lists, JSON indexes (via gojq) and GitHub releases, per-system artifact locking with checksums, and idempotent installs into $XDG_CACHE_HOME/devbox/pkgs/<name>/<version>/<system>. The package is a leaf with no dependency on devpkg, lock or plugin so the wiring in a follow-up PR can build on it. The manager and script strategies defined by the spec return ErrNotImplemented for now. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
63a0202 to
e28622f
Compare
# Conflicts: # go.mod # go.sum # vendor-hash
| resolved := filepath.Join(filepath.Dir(abs), filepath.FromSlash(target)) | ||
| if !isInside(dest, resolved) { | ||
| return fmt.Errorf("archive entry %q: symlink target %q escapes the destination", name, target) |
There was a problem hiding this comment.
Symlink containment is checked lexically, so a chain of symlinks can still write outside dest:
x->.. The target resolves todest, so it is accepted.x/y->...absisdest/x/y, so the lexical targetdest/x/..isdestand is accepted. On disk,xisdest, soyactually points at the parent ofdest.- A regular file
x/y/evil.isInsidepasses lexically,MkdirAllandOpenFilefollow the planted symlinks, and the file is written to<parent of dest>/evil.
The doc comment on extractArchive says symlinks whose target escapes dest are rejected, but this sequence gets past that. The Lstat check in writeRegular only looks at the final path component.
A fix is to resolve the real parent directory with filepath.EvalSymlinks before every write or symlink creation, and re-check it with isInside. Alternatively, use os.Root (Go 1.24+) for all of the extraction I/O.
| if !isInside(dest, resolved) { | ||
| return fmt.Errorf("archive entry %q: symlink target %q escapes the destination", name, target) | ||
| } |
There was a problem hiding this comment.
Symlink containment check is lexical, so a crafted archive can still write outside dest.
filepath.Join cleans .. textually, but the OS resolves it through real symlinks. Example archive entries:
d/up->..(resolves todest, so it passes the check)d/up2->up/..(the join cleans it todest/d, so it passes the check, but the OS resolves it to the parent ofdest)- regular file
d/up2/evil
isInside and the Lstat check in writeRegular only look at the final component. The file is therefore written to <dest>/../evil, outside the install dir. This defeats the "symlinks whose target escapes dest are rejected" guarantee documented on extractArchive.
Suggested fix: resolve each parent directory with filepath.EvalSymlinks before writing files or symlinks and re-check containment. Alternatively, use os.Root (Go 1.24+) to scope all writes to dest.
Summary
Second PR of the package definitions stack (spec:
specs/package-definitions.md, added in the previous PR). Addsinternal/pkgdef, a leaf package with no dependency ondevpkg,lockorplugin:Parse/Validatefor*.pkg.json(hujson), with every error reported at once.ParseRefforpkg:<ref>@<version>(version split at the last@).Client.Loadfor path,https://andfile://refs. Remote definitions are cached by content hash and can be pinned to a hash from the lockfile (ErrDefinitionChangedwhen the server now serves something else).static,http_json(jq via gojq),github_releases(drafts and pre-releases skipped, token only ever sent to the GitHub API host).Client.Resolve/Lock: resolvelatest, a prefix or an exact version, then render the artifact URL and fetch its sha256 (http_jsonorsha256_url, which handles bare digests andSHASUMS-style lists) for every supported system.Client.Install: idempotent install into$XDG_CACHE_HOME/devbox/pkgs/<name>/<version>/<system>, serialized across processes with a file lock, verified against the lockfile digest, extracted with path and symlink sanitization (tar.{gz,xz,bz2,zst} and zip via mholt/archives, which the repo already uses),post_installhooks with their output kept for error messages.managerandscriptstrategies returnErrNotImplementedfor now, as the spec marks them planned.New direct dependency:
github.com/itchyny/gojq(vendor-hash refreshed).Test plan
go test ./internal/pkgdef/(unit tests against an httptest "vendor": resolve, lock, archive and binary installs, concurrent installs, checksum mismatch, trust on first use, GitHub releases, remote pinning, extraction safety)golangci-lintand gofumpt clean🤖 Generated with Claude Code