From a24acef26c4dd0b80be36b8c8382c761d50f7d85 Mon Sep 17 00:00:00 2001 From: bircni Date: Mon, 12 Oct 2026 20:36:16 +0200 Subject: [PATCH] fix(run): keep the permission bits of extracted artifact files Signed-off-by: bircni --- internal/app/artifacts.go | 5 +++-- internal/app/artifacts_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/internal/app/artifacts.go b/internal/app/artifacts.go index fff6d7fa..82cd1a15 100644 --- a/internal/app/artifacts.go +++ b/internal/app/artifacts.go @@ -259,7 +259,8 @@ func extractZip(ctx context.Context, body io.Reader, dir string, clobber, skipEx return nil } -// extractFile writes one archive entry to path with the permissions the umask allows. +// extractFile writes one archive entry to path with its permission bits, such as an executable's, +// as far as the umask allows; special bits such as setuid are dropped. func extractFile(entry *zip.File, path string, clobber, skipExisting bool) error { if err := os.MkdirAll(filepath.Dir(path), 0o777); err != nil { return err @@ -268,7 +269,7 @@ func extractFile(entry *zip.File, path string, clobber, skipExisting bool) error if clobber { flags = os.O_WRONLY | os.O_CREATE | os.O_TRUNC } - file, err := os.OpenFile(path, flags, 0o666) + file, err := os.OpenFile(path, flags, cmp.Or(entry.Mode().Perm(), 0o666)) if errors.Is(err, os.ErrExist) && skipExisting { return nil } diff --git a/internal/app/artifacts_test.go b/internal/app/artifacts_test.go index 6111d630..e6795d48 100644 --- a/internal/app/artifacts_test.go +++ b/internal/app/artifacts_test.go @@ -10,6 +10,7 @@ import ( "net/http" "os" "path/filepath" + "runtime" "testing" "gitea.dev/tea/internal/clierr" @@ -127,6 +128,31 @@ func TestRunDownloadExtractsArtifactsByName(t *testing.T) { }) } +func TestExtractZipKeepsPermissionBits(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Windows has no permission bits") + } + var archive bytes.Buffer + writer := zip.NewWriter(&archive) + for name, mode := range map[string]os.FileMode{"bin/tool": 0o755, "setuid": 0o755 | os.ModeSetuid, "data": 0o644} { + header := &zip.FileHeader{Name: name} + header.SetMode(mode) + file, err := writer.CreateHeader(header) + require.NoError(t, err) + _, err = file.Write([]byte(name)) + require.NoError(t, err) + } + require.NoError(t, writer.Close()) + dir := t.TempDir() + require.NoError(t, extractZip(context.Background(), &archive, dir, false, false)) + for name, executable := range map[string]bool{"bin/tool": true, "setuid": true, "data": false} { + info, err := os.Stat(filepath.Join(dir, name)) + require.NoError(t, err) + require.Equal(t, executable, info.Mode().Perm()&0o100 != 0, name) + require.Zero(t, info.Mode()&os.ModeSetuid, name) + } +} + // filepathReplace puts dir in place of {dir} in text. func filepathReplace(text, dir string) string { return string(bytes.ReplaceAll([]byte(text), []byte("{dir}"), []byte(dir)))