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
32 changes: 17 additions & 15 deletions .github/workflows/vulncheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,21 @@ jobs:
vulncheck:
name: Analysis
runs-on: ubuntu-latest
strategy:
matrix:
go-version: [ 1.19 ]
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go-version }}
check-latest: true
- name: Get govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@latest
shell: bash
- name: Run govulncheck
run: govulncheck ./...
shell: bash
- name: Check out code into the Go module directory
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.24.0 # Same as 'go x.y.z' in go.mod
cache: false
- name: Get official govulncheck
shell: bash
env:
TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
run: |
git config --global url."https://minio-bot:${TOKEN}@github.com".insteadOf "https://github.com"
go install golang.org/x/vuln/cmd/govulncheck@latest
- name: Run govulncheck
run: govulncheck -show verbose -scan package ./...
shell: bash
54 changes: 41 additions & 13 deletions apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ func Apply(update io.Reader, opts Options) error {
}

// PrepareAndCheckBinary reads the new binary content from io.Reader and performs the following actions:
// 1. If configured, applies the contents of the update io.Reader as a binary patch.
// 2. If configured, computes the checksum of the executable and verifies it matches.
// 3. If configured, verifies the signature with a public key.
// 4. Creates a new file, /path/to/.target.new with the TargetMode with the contents of the updated file
// 1. If configured, applies the contents of the update io.Reader as a binary patch.
// 2. If configured, computes the checksum of the executable and verifies it matches.
// 3. If configured, verifies the signature with a public key.
// 4. Creates a new file, /path/to/.target.new with the TargetMode with the contents of the updated file
func PrepareAndCheckBinary(update io.Reader, opts Options) error {
// get target path
targetPath, err := opts.getPath()
Expand Down Expand Up @@ -88,12 +88,12 @@ func PrepareAndCheckBinary(update io.Reader, opts Options) error {

// CommitBinary moves the new executable to the location of the current executable or opts.TargetPath
// if specified. It performs the following operations:
// 1. Renames /path/to/target to /path/to/.target.old
// 2. Renames /path/to/.target.new to /path/to/target
// 3. If the final rename is successful, deletes /path/to/.target.old, returns no error. On Windows,
// the removal of /path/to/target.old always fails, so instead Apply hides the old file instead.
// 4. If the final rename fails, attempts to roll back by renaming /path/to/.target.old
// back to /path/to/target.
// 1. Renames /path/to/target to /path/to/.target.old
// 2. Renames /path/to/.target.new to /path/to/target
// 3. If the final rename is successful, deletes /path/to/.target.old, returns no error. On Windows,
// the removal of /path/to/target.old always fails, so instead Apply hides the old file instead.
// 4. If the final rename fails, attempts to roll back by renaming /path/to/.target.old
// back to /path/to/target.
//
// If the roll back operation fails, the file system is left in an inconsistent state where there is
// no new executable file and the old executable file could not be be moved to its original location.
Expand Down Expand Up @@ -233,12 +233,40 @@ func (o *Options) CheckPermissions() error {
return nil
}

// normalizeExecutablePath handles the case where os.Executable() returns a path
// ending with .old (which can happen after a previous update on some systems).
// It strips the .old suffix and any leading dot from the base filename.
//
// Examples:
// - /path/to/.minio.old -> /path/to/minio
// - /path/to/program.old -> /path/to/program
// - /path/to/program -> /path/to/program (unchanged)
func normalizeExecutablePath(path string) string {
path = filepath.Clean(path)
baseFilename := filepath.Base(path)

// Handle case where the executable path ends with .old
if filepath.Ext(baseFilename) == ".old" {
baseFilename = baseFilename[:len(baseFilename)-4] // Remove .old suffix
// Remove leading dot if present (e.g., ".minio.old" -> "minio")
if len(baseFilename) > 1 && baseFilename[0] == '.' {
baseFilename = baseFilename[1:]
}
path = filepath.Join(filepath.Dir(path), baseFilename)
}

return path
}

func (o *Options) getPath() (string, error) {
if o.TargetPath == "" {
return osext.Executable()
} else {
return o.TargetPath, nil
path, err := osext.Executable()
if err != nil {
return "", err
}
return normalizeExecutablePath(path), nil
}
return o.TargetPath, nil
}

func (o *Options) getMode() os.FileMode {
Expand Down
58 changes: 58 additions & 0 deletions apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,3 +247,61 @@ func sign(parsePrivKey func([]byte) (crypto.Signer, error), privatePEM string, s

return sig
}

func TestNormalizeExecutablePath(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "normal path unchanged",
input: "/path/to/program",
expected: "/path/to/program",
},
{
name: "path with .old suffix",
input: "/path/to/program.old",
expected: "/path/to/program",
},
{
name: "path with leading dot and .old suffix",
input: "/path/to/.minio.old",
expected: "/path/to/minio",
},
{
name: "path with leading dot and .old suffix (complex)",
input: "/usr/local/bin/.server.old",
expected: "/usr/local/bin/server",
},
{
name: "simple filename with .old",
input: "program.old",
expected: "program",
},
{
name: "simple filename with leading dot and .old",
input: ".program.old",
expected: "program",
},
{
name: "path with multiple dots but not .old extension",
input: "/path/to/my.program.exe",
expected: "/path/to/my.program.exe",
},
{
name: "windows style path with .old",
input: "C:\\Program Files\\app.exe.old",
expected: "C:\\Program Files\\app.exe",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := normalizeExecutablePath(tt.input)
if result != tt.expected {
t.Errorf("normalizeExecutablePath(%q) = %q, expected %q", tt.input, result, tt.expected)
}
})
}
}
10 changes: 7 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
module github.com/minio/selfupdate

go 1.14
go 1.24.0

toolchain go1.24.8

require aead.dev/minisign v0.2.0

require (
aead.dev/minisign v0.2.0
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/sys v0.37.0 // indirect
)
10 changes: 4 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,15 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b h1:QAqMVf3pSa6eeTsuklijukjXBlj7Es2QQplab+/RbQ4=
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=