Repository navigation
v5.17.1 regression: fixPermissions sets pack files to 0444, breaking subsequent installs into the same directory #1942
Description
Summary
v5.17.1 introduced fixPermissions() which chmods .idx and .pack files to 0o444 (read-only) after writing them. Combined with the upgrade of go-billy to v5.8.0 (which made ChrootHelper.Chmod actually execute), this causes subsequent clones/installs into the same destination directory to fail with:
open .git/objects/pack/pack-<HASH>.idx: permission denied
Reverting to v5.16.5 fixes the issue.
Root cause
Two changes combined to create the bug
Change 1 — fixPermissions() added in storage/filesystem/dotgit/writers_unix.go:
func fixPermissions(fs billy.Filesystem, path string) {
if chmodFS, ok := fs.(billy.Chmod); ok {
if err := chmodFS.Chmod(path, 0o444); err != nil {
trace.General.Printf("failed to chmod %s: %v", path, err)
}
}
}Called after writing .idx and .pack files (lines 148, 154, 303 in writers.go).
Change 2 — go-billy upgraded from v5.6.2 → v5.8.0:
go-billyv5.7.0introducedbilly.Chmodas a dedicated interface (previouslyChmodwas only embedded insidebilly.Changewith no standalone interface type).go-billyv5.8.0addedChrootHelper.Chmodimplementing that interface:
func (fs *ChrootHelper) Chmod(path string, mode os.FileMode) error {
fullpath, err := fs.underlyingPath(path)
...
return c.Chmod(fullpath, mode)
}With v5.16.5 + go-billy v5.6.2: ChrootOS/ChrootHelper did not implement the standalone billy.Chmod interface (only the embedded-in-billy.Change version), so the type assertion fs.(billy.Chmod) returned ok=false. fixPermissions was a no-op. Pack files kept their default mode (0644).
With v5.17.1 + go-billy v5.8.0: The type assertion succeeds. Chmod(path, 0o444) is called on every .idx and .pack file written into the filesystem — including temporary shadow directories used by callers.
Failure scenario
Any code that:
- Uses go-git to clone/fetch into a temporary location, then
- Recursively copies that location to a persistent destination (using
os.Createor anyO_WRONLY/O_RDWRopen), then - Repeats the operation on the same destination
…will fail on the second iteration.
Step-by-step:
- Run 1: go-git clones into shadow dir →
fixPermissionssets shadow's.idxto0o444→ copy tool copies shadow → dest →os.Create(dest)succeeds (file is new) → copy tool preserves permissions → dest.idxnow has0o444 - Run 2: go-git clones into new shadow dir →
fixPermissionssets shadow's.idxto0o444→ copy tool triesos.Create(dest)→ dest exists with0o444→ kernel returnsEACCES→open .git/objects/pack/pack-XXX.idx: permission denied
The open in the error message is the open(2) syscall name emitted by Go's os.Create (which is open(O_RDWR|O_CREATE|O_TRUNC)). A 0o444 file has no write bits for anyone, including the owner, so the syscall fails.
Affected versions
| go-git | go-billy | Behavior |
|---|---|---|
| v5.16.5 | v5.6.2 | ✅ Works — fixPermissions is a no-op (no billy.Chmod on ChrootHelper) |
| v5.17.1 | v5.8.0 | ❌ Breaks — fixPermissions executes, sets pack files to 0o444 |
Suggested fix
The intent of fixPermissions — protecting pack files from accidental modification — is reasonable. However, 0o444 (no write for anyone) is more restrictive than necessary and breaks callers that legitimately need to overwrite pack files during subsequent operations on the same path.
Option A: Change the mode to 0o444 only when writing to the final (non-temporary) pack storage location, not in shadow/working directories.
Option B: Use 0o644 instead of 0o444. This preserves the "this file should not normally be written" signal while not breaking os.Create on existing files.
Option C: Document that callers using go-git to write into directories that are later overwritten via recursive copy must walk and chmod 0644 all pack files before the copy.
Option B is the most backward-compatible with the least blast radius.
Activity
pjbgf commentedon Mar 31, 2026
@pskrbasu thanks for creating this issue. Would you be able to share some steps to reproduce please?
pjbgf commentedon Mar 31, 2026
Got a fix above. For future reference those files are created as read-only for a reason - they are content addressable and shouldn't be overwritten. The key question I have is why the same packfile is being fetched. I could not reproduce this so any steps to reproduce would be valuable.
pjbgf commentedon Mar 31, 2026
A new v5.17.2 was cut fixing this issue. I will leave this open for a few days in the hope we get steps to reproduce on the same packfile being fetched/saved.
fix(deps): update module github.com/go-git/go-git/v5 to v5.17.2 (#385)
pjbgf commentedon Apr 2, 2026
Closing this as per message above - but will keep an eye out for the steps to reproduce.
Update github.com/go-git/go-git/v5 (indirect) to v5.18.0 [SECURITY] (…
Update github.com/go-git/go-git/v5 (indirect) to v5.18.0 [SECURITY] (…
Update module github.com/go-git/go-git/v5 to v5.18.0 [SECURITY] (v11.…
Update github.com/go-git/go-git/v5 (indirect) to v5.18.0 [SECURITY] (…
Update module github.com/go-git/go-git/v5 to v5.18.0 [SECURITY] (#1485)
fix(deps): bump go-git/v5 to v5.17.2 to resolve CVE-2026-34165, CVE-2…
Update github.com/go-git/go-git/v5 (indirect) to v5.18.0 [SECURITY] (…