diff --git a/internal/plugin/installer/oci_installer.go b/internal/plugin/installer/oci_installer.go index 383ddb914..ea9e16262 100644 --- a/internal/plugin/installer/oci_installer.go +++ b/internal/plugin/installer/oci_installer.go @@ -209,6 +209,28 @@ func extractTarGz(r io.Reader, targetDir string) error { return extractTar(gzr, targetDir) } +// extractFile creates a single file from the tar archive. +func extractFile(path string, mode int64, src io.Reader) error { + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0o755); err != nil { + return err + } + + outFile, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(mode)) + if err != nil { + return err + } + + if _, err := io.Copy(outFile, src); err != nil { + if cErr := outFile.Close(); cErr != nil { + return fmt.Errorf("%w (also failed to close: %v)", err, cErr) + } + return err + } + + return outFile.Close() +} + // extractTar extracts a tar archive to a directory func extractTar(r io.Reader, targetDir string) error { tarReader := tar.NewReader(r) @@ -233,17 +255,7 @@ func extractTar(r io.Reader, targetDir string) error { return err } case tar.TypeReg: - dir := filepath.Dir(path) - if err := os.MkdirAll(dir, 0o755); err != nil { - return err - } - - outFile, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) - if err != nil { - return err - } - defer outFile.Close() - if _, err := io.Copy(outFile, tarReader); err != nil { + if err := extractFile(path, header.Mode, tarReader); err != nil { return err } case tar.TypeXGlobalHeader, tar.TypeXHeader: diff --git a/internal/plugin/installer/oci_installer_unix_test.go b/internal/plugin/installer/oci_installer_unix_test.go new file mode 100644 index 000000000..90cba1631 --- /dev/null +++ b/internal/plugin/installer/oci_installer_unix_test.go @@ -0,0 +1,79 @@ +//go:build unix + +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package installer + +import ( + "archive/tar" + "bytes" + "fmt" + "syscall" + "testing" +) + +func TestExtractTarFileDescriptorLeak(t *testing.T) { + var rLimit syscall.Rlimit + err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit) + if err != nil { + t.Skipf("Skipping test because Getrlimit failed: %v", err) + } + + // Lower the limit to 50 + oldLimit := rLimit + if rLimit.Max < 50 { + t.Skipf("Skipping test because Max limit (%d) is too low", rLimit.Max) + } + rLimit.Cur = 50 + + err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit) + if err != nil { + t.Skipf("Skipping test because Setrlimit failed: %v", err) + } + defer func() { + // Restore the original limit + if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &oldLimit); err != nil { + t.Logf("Failed to restore RLIMIT_NOFILE: %v", err) + } + }() + + // Create a dummy tar archive with 100 files (more than the limit of 50) + var buf bytes.Buffer + tw := tar.NewWriter(&buf) + for i := 0; i < 100; i++ { + hdr := &tar.Header{ + Name: fmt.Sprintf("file_%d.txt", i), + Mode: 0o600, + Size: 0, + Typeflag: tar.TypeReg, + } + if err := tw.WriteHeader(hdr); err != nil { + t.Fatalf("Failed to write header: %v", err) + } + if _, err := tw.Write([]byte{}); err != nil { + t.Fatalf("Failed to write content: %v", err) + } + } + if err := tw.Close(); err != nil { + t.Fatalf("Failed to close tar writer: %v", err) + } + + // Extract the tar archive + tempDir := t.TempDir() + if err := extractTar(&buf, tempDir); err != nil { + t.Fatalf("extractTar failed, likely due to file descriptor exhaustion: %v", err) + } +}