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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ All notable changes to this project are documented here. The format follows

## [Unreleased]

### Fixed

- `update` now keeps the replaced binary executable. It staged the new binary in a temp file
created with mode 0600 and relied on `OpenFile` to widen it, but `OpenFile` ignores the mode
argument for a file that already exists — so the installed binary was left non-executable and
the follow-up `setup` failed with "permission denied". The copy now forces the mode
explicitly. (regression in v0.2.4's `update`)

## [0.2.4] - 2026-08-02

### Added
Expand Down
7 changes: 7 additions & 0 deletions cmd/openshell-driver-applecontainer/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,13 @@ func copyFile(src, dst string, mode os.FileMode) error {
_ = out.Close()
return err
}
// OpenFile's mode is ignored when dst already exists — and it does here:
// replaceBinary copies into an os.CreateTemp file (created 0600). Force
// the mode so the replaced binary keeps its exec bit.
if err := out.Chmod(mode); err != nil {
_ = out.Close()
return err
}
return out.Close()
}

Expand Down
36 changes: 36 additions & 0 deletions cmd/openshell-driver-applecontainer/update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,42 @@ func TestExtractBinaryMissing(t *testing.T) {
}
}

// TestReplaceBinaryKeepsExecBit reproduces the regression where the replaced
// binary landed non-executable: replaceBinary copies into an os.CreateTemp
// file (0600) and OpenFile's mode is ignored for an existing file, so without
// an explicit chmod the renamed target lost its exec bit and `update`'s
// re-setup then failed with "permission denied".
func TestReplaceBinaryKeepsExecBit(t *testing.T) {
dir := t.TempDir()
target := filepath.Join(dir, "driver")
if err := os.WriteFile(target, []byte("old"), 0o755); err != nil {
t.Fatal(err)
}
newBin := filepath.Join(dir, "new")
if err := os.WriteFile(newBin, []byte("NEW-BINARY"), 0o755); err != nil {
t.Fatal(err)
}

if err := replaceBinary(target, newBin); err != nil {
t.Fatal(err)
}

got, err := os.ReadFile(target)
if err != nil {
t.Fatal(err)
}
if string(got) != "NEW-BINARY" {
t.Errorf("content = %q, want NEW-BINARY", got)
}
info, err := os.Stat(target)
if err != nil {
t.Fatal(err)
}
if info.Mode().Perm() != 0o755 {
t.Errorf("replaced binary mode = %o, want 755 (exec bit must survive)", info.Mode().Perm())
}
}

func TestVerifyChecksum(t *testing.T) {
dir := t.TempDir()
archive := filepath.Join(dir, "app.tar.gz")
Expand Down