From 7bdb1320bf5c10e1759e53e072d63a06ae912f43 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Sat, 4 Jul 2026 11:03:56 -0700 Subject: [PATCH] Close streams and delete intermediate temps in ImageBuildpack ExportedLayers left the intermediate create-builder-scratch-source temp file behind after rebased layer files were written, and opened layer InputStreams without closing them when StreamUtils.copy does not close either stream. Delete the source temp in a finally block and use try-with-resources for the apply path so layer files can be deleted reliably. See gh-50919 Signed-off-by: Sebastien Tardif --- .../platform/build/ImageBuildpack.java | 48 +++++++++++-------- .../platform/build/ImageBuildpackTests.java | 39 +++++++++++++++ 2 files changed, 66 insertions(+), 21 deletions(-) diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageBuildpack.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageBuildpack.java index 93e22c19768a..4e943015525c 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageBuildpack.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageBuildpack.java @@ -132,35 +132,41 @@ private static class ExportedLayers { private Path createLayerFile(TarArchive tarArchive) throws IOException { Path sourceTarFile = Files.createTempFile("create-builder-scratch-source-", null); - try (OutputStream out = Files.newOutputStream(sourceTarFile)) { - tarArchive.writeTo(out); - } - Path layerFile = Files.createTempFile("create-builder-scratch-", null); - try (TarArchiveOutputStream out = new TarArchiveOutputStream(Files.newOutputStream(layerFile))) { - try (TarArchiveInputStream in = new TarArchiveInputStream(Files.newInputStream(sourceTarFile))) { - out.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); - TarArchiveEntry entry = in.getNextEntry(); - while (entry != null) { - String entryName = entry.getName(); - Path entryPath = Path.of(entryName); - Assert.state(entryPath.toAbsolutePath().equals(entryPath.toAbsolutePath().normalize()), - () -> "Malformed zip entry name '%s'".formatted(entryName)); - out.putArchiveEntry(entry); - StreamUtils.copy(in, out); - out.closeArchiveEntry(); - entry = in.getNextEntry(); + try { + try (OutputStream out = Files.newOutputStream(sourceTarFile)) { + tarArchive.writeTo(out); + } + Path layerFile = Files.createTempFile("create-builder-scratch-", null); + try (TarArchiveOutputStream out = new TarArchiveOutputStream(Files.newOutputStream(layerFile))) { + try (TarArchiveInputStream in = new TarArchiveInputStream(Files.newInputStream(sourceTarFile))) { + out.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); + TarArchiveEntry entry = in.getNextEntry(); + while (entry != null) { + String entryName = entry.getName(); + Path entryPath = Path.of(entryName); + Assert.state(entryPath.toAbsolutePath().equals(entryPath.toAbsolutePath().normalize()), + () -> "Malformed zip entry name '%s'".formatted(entryName)); + out.putArchiveEntry(entry); + StreamUtils.copy(in, out); + out.closeArchiveEntry(); + entry = in.getNextEntry(); + } + out.finish(); } - out.finish(); } + return layerFile; + } + finally { + Files.deleteIfExists(sourceTarFile); } - return layerFile; } void apply(IOConsumer layers) throws IOException { for (Path path : this.layerFiles) { layers.accept(Layer.fromTarArchive((out) -> { - InputStream in = Files.newInputStream(path); - StreamUtils.copy(in, out); + try (InputStream in = Files.newInputStream(path)) { + StreamUtils.copy(in, out); + } })); Files.delete(path); } diff --git a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ImageBuildpackTests.java b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ImageBuildpackTests.java index cac072547323..4fcd192a3f37 100644 --- a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ImageBuildpackTests.java +++ b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ImageBuildpackTests.java @@ -24,8 +24,11 @@ import java.io.IOException; import java.nio.file.Files; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Random; +import java.util.Set; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; @@ -197,6 +200,42 @@ void resolveWhenEntryWouldWriteOutsideOfDestinationThrowsException() throws Exce .withMessage("Malformed zip entry name '../cnb/'"); } + @Test + void resolveDeletesIntermediateSourceTarTempFiles() throws Exception { + File tempDir = new File(System.getProperty("java.io.tmpdir")); + Set tempsBefore = listTempFileNames(tempDir, "create-builder-scratch-"); + Image image = Image.of(getContent("buildpack-image.json")); + ImageReference imageReference = ImageReference.of("example/buildpack1:1.0.0"); + BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class); + given(resolverContext.getBuildpackLayersMetadata()).willReturn(BuildpackLayersMetadata.fromJson("{}")); + given(resolverContext.fetchImage(eq(imageReference), eq(ImageType.BUILDPACK))).willReturn(image); + willAnswer(this::withMockLayers).given(resolverContext).exportImageLayers(eq(imageReference), any()); + BuildpackReference reference = BuildpackReference.of("docker://example/buildpack1:1.0.0"); + Buildpack buildpack = ImageBuildpack.resolve(resolverContext, reference); + assertThat(buildpack).isNotNull(); + Set createdOnResolve = new HashSet<>(listTempFileNames(tempDir, "create-builder-scratch-")); + createdOnResolve.removeAll(tempsBefore); + assertThat(createdOnResolve).as("intermediate source tar temp files must be deleted after createLayerFile") + .noneMatch((name) -> name.startsWith("create-builder-scratch-source-")); + assertThat(createdOnResolve).isNotEmpty(); + assertAppliesExpectedLayers(buildpack); + Set remainingCreated = new HashSet<>(listTempFileNames(tempDir, "create-builder-scratch-")); + remainingCreated.retainAll(createdOnResolve); + assertThat(remainingCreated).as("layer temp files created on resolve must be deleted after apply").isEmpty(); + } + + private Set listTempFileNames(File tempDir, String prefix) { + File[] files = tempDir.listFiles((dir, name) -> name.startsWith(prefix)); + if (files == null) { + return Collections.emptySet(); + } + Set names = new HashSet<>(); + for (File file : files) { + names.add(file.getName()); + } + return names; + } + private @Nullable Object withMockLayers(InvocationOnMock invocation) { return withMockLayers(invocation, ""); }