diff --git a/cmd/update/linux_asset_suffix_linux.go b/cmd/update/linux_asset_suffix_linux.go index 9da6c62f..ea838751 100644 --- a/cmd/update/linux_asset_suffix_linux.go +++ b/cmd/update/linux_asset_suffix_linux.go @@ -13,7 +13,7 @@ import ( const ( linuxLdd235Suffix = "_ldd2-35" - linuxGlibcThreshold = "2.36" + linuxGlibcThreshold = "2.38" ) var glibcVersionPattern = regexp.MustCompile(`(\d+\.\d+)(?:\.\d+)?`) diff --git a/cmd/update/linux_asset_suffix_linux_test.go b/cmd/update/linux_asset_suffix_linux_test.go index e1c9edcd..946a5338 100644 --- a/cmd/update/linux_asset_suffix_linux_test.go +++ b/cmd/update/linux_asset_suffix_linux_test.go @@ -64,10 +64,22 @@ func TestLinuxAssetSuffixFromGlibcVersion(t *testing.T) { want string }{ { + // Ubuntu 22.04 (glibc 2.35) — cannot run the default glibc-2.38 binary. output: "ldd (Ubuntu GLIBC 2.35-0ubuntu3.8) 2.35\n", want: linuxLdd235Suffix, }, { + // Debian 12 (glibc 2.36) — cannot run the default glibc-2.38 binary. + output: "ldd (Debian GLIBC 2.36-9+deb12u14) 2.36\n", + want: linuxLdd235Suffix, + }, + { + // glibc 2.38 — the exact minimum required by the default binary. + output: "ldd (GNU libc) 2.38\n", + want: "", + }, + { + // Ubuntu 24.04 (glibc 2.39) — can run the default binary. output: "ldd (Ubuntu GLIBC 2.39-0ubuntu8.4) 2.39\n", want: "", }, diff --git a/cmd/update/replace.go b/cmd/update/replace.go new file mode 100644 index 00000000..0bbc2949 --- /dev/null +++ b/cmd/update/replace.go @@ -0,0 +1,69 @@ +package update + +import ( + "bytes" + "fmt" + "os" + "os/exec" + osruntime "runtime" + "strings" + + "github.com/smartcontractkit/cre-cli/internal/ui" +) + +const backupSuffix = ".bak" + +func verifyBinaryRuns(binPath string) error { + cmd := exec.Command(binPath, "version") // #nosec G204 -- binPath is a verified release artifact + var stderr bytes.Buffer + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + if msg := strings.TrimSpace(stderr.String()); msg != "" { + return fmt.Errorf("%s", msg) + } + return err + } + return nil +} + +func replaceSelf(newBin string) error { + self, err := os.Executable() + if err != nil { + return err + } + return replaceBinaryAt(self, newBin) +} + +func replaceBinaryAt(self, newBin string) error { + if osruntime.GOOS == "windows" { + ui.Warning("Automatic replacement not supported on Windows") + ui.Dim("Please close all running cre processes and manually replace the binary at:") + ui.Code(self) + ui.Dim("New binary downloaded at:") + ui.Code(newBin) + return fmt.Errorf("automatic replacement not supported on Windows") + } + + backupPath := self + backupSuffix + _ = os.Remove(backupPath) + + if err := os.Rename(self, backupPath); err != nil { + return fmt.Errorf("failed to backup current binary: %w", err) + } + + if err := os.Rename(newBin, self); err != nil { + _ = os.Rename(backupPath, self) + return fmt.Errorf("failed to replace binary: %w", err) + } + + if err := verifyBinaryRuns(self); err != nil { + _ = os.Remove(self) + if restoreErr := os.Rename(backupPath, self); restoreErr != nil { + return fmt.Errorf("post-install verification failed and could not restore backup: %v (verify error: %w)", restoreErr, err) + } + return fmt.Errorf("post-install verification failed; restored previous binary: %w", err) + } + + _ = os.Remove(backupPath) + return nil +} diff --git a/cmd/update/replace_test.go b/cmd/update/replace_test.go new file mode 100644 index 00000000..b4213a5d --- /dev/null +++ b/cmd/update/replace_test.go @@ -0,0 +1,95 @@ +package update + +import ( + "os" + "os/exec" + "path/filepath" + "runtime" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestVerifyBinaryRuns(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("shell scripts used for unix fake binaries") + } + t.Parallel() + + tmpDir := t.TempDir() + binPath := filepath.Join(tmpDir, "cre-test") + script := "#!/bin/sh\nexit 0\n" + require.NoError(t, os.WriteFile(binPath, []byte(script), 0755)) + + require.NoError(t, verifyBinaryRuns(binPath)) + + failPath := filepath.Join(tmpDir, "cre-fail") + failScript := "#!/bin/sh\necho glibc error >&2\nexit 1\n" + require.NoError(t, os.WriteFile(failPath, []byte(failScript), 0755)) + + err := verifyBinaryRuns(failPath) + require.Error(t, err) + require.Contains(t, err.Error(), "glibc error") +} + +func TestReplaceBinaryAt_restoresBackupOnPostInstallFailure(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("unix-only replace flow") + } + + tmpDir := t.TempDir() + current := filepath.Join(tmpDir, "cre") + newBin := filepath.Join(tmpDir, "cre-new") + + currentScript := "#!/bin/sh\necho old\n" + require.NoError(t, os.WriteFile(current, []byte(currentScript), 0755)) + + newScript := "#!/bin/sh\necho broken >&2\nexit 1\n" + require.NoError(t, os.WriteFile(newBin, []byte(newScript), 0755)) + + err := replaceBinaryAt(current, newBin) + require.Error(t, err) + require.Contains(t, err.Error(), "restored previous binary") + + restored, err := os.ReadFile(current) + require.NoError(t, err) + require.Equal(t, currentScript, string(restored)) + require.NoFileExists(t, current+backupSuffix) +} + +func TestReplaceBinaryAt_succeedsWithWorkingBinary(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("unix-only replace flow") + } + + tmpDir := t.TempDir() + current := filepath.Join(tmpDir, "cre") + newBin := filepath.Join(tmpDir, "cre-new") + + currentScript := "#!/bin/sh\necho old\n" + require.NoError(t, os.WriteFile(current, []byte(currentScript), 0755)) + + newScript := "#!/bin/sh\necho new\n" + require.NoError(t, os.WriteFile(newBin, []byte(newScript), 0755)) + + require.NoError(t, replaceBinaryAt(current, newBin)) + + updated, err := os.ReadFile(current) + require.NoError(t, err) + require.Equal(t, newScript, string(updated)) + require.NoFileExists(t, current+backupSuffix) + require.NoFileExists(t, newBin) +} + +func TestVerifyBinaryRuns_realGoBinary(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("shell scripts used for unix fake binaries") + } + + tmpDir := t.TempDir() + binPath := filepath.Join(tmpDir, "version-bin") + out, err := exec.Command("go", "build", "-o", binPath, "testdata/version_main.go").CombinedOutput() + require.NoError(t, err, string(out)) + + require.NoError(t, verifyBinaryRuns(binPath)) +} diff --git a/cmd/update/testdata/version_main.go b/cmd/update/testdata/version_main.go new file mode 100644 index 00000000..e7c7a029 --- /dev/null +++ b/cmd/update/testdata/version_main.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("test-version") +} \ No newline at end of file diff --git a/cmd/update/update.go b/cmd/update/update.go index fe1abaaf..a3bd55de 100644 --- a/cmd/update/update.go +++ b/cmd/update/update.go @@ -282,24 +282,6 @@ func unzip(assetPath string) (string, error) { return "", errors.New("binary not found in zip") } -func replaceSelf(newBin string) error { - self, err := os.Executable() - if err != nil { - return err - } - // On Windows, need to move after process exit - if osruntime.GOOS == "windows" { - ui.Warning("Automatic replacement not supported on Windows") - ui.Dim("Please close all running cre processes and manually replace the binary at:") - ui.Code(self) - ui.Dim("New binary downloaded at:") - ui.Code(newBin) - return fmt.Errorf("automatic replacement not supported on Windows") - } - // On Unix, can replace in-place - return os.Rename(newBin, self) -} - // Run accepts the currentVersion string and a force flag that overrides the // fail-closed behavior when versions cannot be compared. func Run(currentVersion string, force bool) error { @@ -395,11 +377,21 @@ func Run(currentVersion string, force bool) error { return fmt.Errorf("release signature verification failed: %w", err) } - spinner.Update("Installing...") if err := os.Chmod(binPath, 0755); err != nil { spinner.Stop() return fmt.Errorf("failed to set permissions: %w", err) } + + spinner.Update("Verifying binary...") + if err := verifyBinaryRuns(binPath); err != nil { + spinner.Stop() + if osruntime.GOOS == "linux" { + return fmt.Errorf("pre-install binary verification failed: %w; try downloading the compatible build manually: cre_linux_amd64_ldd2-35", err) + } + return fmt.Errorf("pre-install binary verification failed: %w", err) + } + + spinner.Update("Installing...") if err := replaceSelf(binPath); err != nil { spinner.Stop() return fmt.Errorf("failed to replace binary: %w", err) diff --git a/install/install.sh b/install/install.sh index 9862a8e9..dc31fd7e 100755 --- a/install/install.sh +++ b/install/install.sh @@ -196,7 +196,7 @@ verify_release_binary() { } LINUX_LDD235_SUFFIX="_ldd2-35" -LINUX_GLIBC_THRESHOLD="2.36" +LINUX_GLIBC_THRESHOLD="2.38" parse_glibc_version_from_ldd_output() { local output=$1