Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions internal/modules/selfupdate.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,11 +369,26 @@ func installSelfUpdate(tmpPath, currentBin string) error {
}

dir := filepath.Dir(currentBin)
if !spaceFor(dir, uint64(info.Size())) {

// The current binary is unlinked below before the new copy lands, so
// its space counts towards the budget; without it the pre-check
// demands the whole new binary from free space alone and every update
// on a tiny overlay fails even though the swap itself would fit (#547).
// Apparent size is used on purpose: on compressing filesystems (ubifs)
// it overestimates what the unlink actually frees, which is the safe
// direction for a pre-check (a false "not enough" only fails the
// update; a false "enough" defers ENOSPC to the copy, which is already
// handled gracefully).
var freed uint64
if cur, err := os.Stat(currentBin); err == nil && cur.Size() > 0 {
freed = uint64(cur.Size())
}

if !spaceFor(dir, uint64(info.Size()), freed) {
// Best effort: drop the package cache to make room on tiny
// overlays. No-op where the directory does not exist.
os.RemoveAll("/etc/apk/cache")
if !spaceFor(dir, uint64(info.Size())) {
if !spaceFor(dir, uint64(info.Size()), freed) {
return fmt.Errorf("not enough space on %s for the new binary (need ~%d bytes)", dir, info.Size())
}
}
Expand All @@ -390,14 +405,16 @@ func installSelfUpdate(tmpPath, currentBin string) error {
}

// spaceFor reports whether dir has at least needed bytes available plus a
// small headroom. Unknown filesystems (statfs failure) are treated as
// having room so the update is never blocked by an exotic mount.
func spaceFor(dir string, needed uint64) bool {
// small headroom, counting freed bytes that the install is about to
// release (the current binary being replaced). Unknown filesystems (statfs
// failure) are treated as having room so the update is never blocked by an
// exotic mount.
func spaceFor(dir string, needed, freed uint64) bool {
var st syscall.Statfs_t
if err := syscall.Statfs(dir, &st); err != nil || st.Bsize <= 0 {
return true
}
avail := uint64(st.Bavail) * uint64(st.Bsize)
avail := uint64(st.Bavail)*uint64(st.Bsize) + freed
return avail >= needed+(1<<20)
}

Expand Down
26 changes: 26 additions & 0 deletions internal/modules/selfupdate_install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package modules
import (
"os"
"path/filepath"
"syscall"
"testing"
)

Expand Down Expand Up @@ -55,6 +56,31 @@ func TestInstallSelfUpdateReplacesBinary(t *testing.T) {
}
}

// The space pre-check must count the current binary's size towards the
// budget, because the install unlinks it before copying the new one (#547):
// on a tiny overlay the free space alone is always smaller than the new
// binary, and demanding it up front makes every self-update fail.
func TestSpaceForCountsFreedBytes(t *testing.T) {
dir := t.TempDir()
var st syscall.Statfs_t
if err := syscall.Statfs(dir, &st); err != nil || st.Bsize <= 0 {
t.Skip("statfs not available")
}
avail := uint64(st.Bavail) * uint64(st.Bsize)

need := avail + 10<<20
if spaceFor(dir, need, 0) {
t.Fatalf("spaceFor(need=%d, freed=0) = true, want false", need)
}
if !spaceFor(dir, need, 20<<20) {
t.Fatalf("spaceFor(need=%d, freed=%d) = false, want true", need, 20<<20)
}
// The 1MiB headroom is still required on top of avail+freed.
if spaceFor(dir, avail+20<<20, 20<<20) {
t.Fatal("spaceFor without the 1MiB headroom = true, want false")
}
}

// A missing current binary (e.g. a previous partial install) must not
// block the install: the rescue copy fails, is logged, and the update
// continues.
Expand Down
Loading