From 5d6e417134000e75c69a5bc7dcb1b603185d2481 Mon Sep 17 00:00:00 2001 From: Junhwan Choi Date: Tue, 1 Sep 2026 21:13:43 +0900 Subject: [PATCH 1/2] Delete layer temp file when layer creation fails When ImageBuildpack.ExportedLayers creates a layer file, the copy from the source tar can fail, for example when a malformed entry name is rejected. The temporary layer file created before the copy was not deleted in that case and was left orphaned in the system temp directory. Delete the layer temp file when creating it fails, suppressing any secondary failure from the delete so the original exception is propagated. Signed-off-by: Junhwan Choi See gh-51519 --- .../platform/build/ImageBuildpack.java | 43 ++++++++++++------- .../platform/build/ImageBuildpackTests.java | 20 +++++++++ 2 files changed, 48 insertions(+), 15 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 4e943015525..eb8d74e46fb 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 @@ -49,6 +49,7 @@ import org.springframework.util.StreamUtils; * * @author Scott Frederick * @author Phillip Webb + * @author Junhwan Choi */ final class ImageBuildpack implements Buildpack { @@ -137,24 +138,36 @@ final class ImageBuildpack implements Buildpack { 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 (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; + } + catch (IOException | RuntimeException ex) { + try { + Files.deleteIfExists(layerFile); + } + catch (IOException suppressed) { + ex.addSuppressed(suppressed); + } + throw ex; } - return layerFile; } finally { Files.deleteIfExists(sourceTarFile); 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 4fcd192a3f3..fc61e9b33e5 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 @@ -61,6 +61,7 @@ import static org.mockito.Mockito.mock; * * @author Scott Frederick * @author Phillip Webb + * @author Junhwan Choi */ class ImageBuildpackTests extends AbstractJsonTests { @@ -200,6 +201,25 @@ class ImageBuildpackTests extends AbstractJsonTests { .withMessage("Malformed zip entry name '../cnb/'"); } + @Test + void resolveWhenLayerCreationFailsDeletesLayerTempFiles() 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:latest"); + BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class); + given(resolverContext.getBuildpackLayersMetadata()).willReturn(BuildpackLayersMetadata.fromJson("{}")); + given(resolverContext.fetchImage(eq(imageReference), eq(ImageType.BUILDPACK))).willReturn(image); + willAnswer((invocation) -> withMockLayers(invocation, "..")).given(resolverContext) + .exportImageLayers(eq(imageReference), any()); + BuildpackReference reference = BuildpackReference.of("example/buildpack1"); + assertThatIllegalStateException().isThrownBy(() -> ImageBuildpack.resolve(resolverContext, reference)) + .withMessage("Malformed zip entry name '../cnb/'"); + Set tempsAfter = new HashSet<>(listTempFileNames(tempDir, "create-builder-scratch-")); + tempsAfter.removeAll(tempsBefore); + assertThat(tempsAfter).as("layer temp files must be deleted when layer creation fails").isEmpty(); + } + @Test void resolveDeletesIntermediateSourceTarTempFiles() throws Exception { File tempDir = new File(System.getProperty("java.io.tmpdir")); From 71bfa94d1019c6bdae9979d0ae25d7c9536fe665 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 4 Sep 2026 11:12:11 +0100 Subject: [PATCH 2/2] Polish "Delete layer temp file when layer creation fails" See gh-51519 Signed-off-by: Andy Wilkinson --- .../platform/build/ImageBuildpack.java | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 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 eb8d74e46fb..c596ade2acd 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 @@ -139,25 +139,7 @@ final class ImageBuildpack implements Buildpack { } Path layerFile = Files.createTempFile("create-builder-scratch-", null); try { - 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(); - } - } - return layerFile; + writeLayerFile(layerFile, sourceTarFile); } catch (IOException | RuntimeException ex) { try { @@ -168,12 +150,33 @@ final class ImageBuildpack implements Buildpack { } throw ex; } + return layerFile; } finally { Files.deleteIfExists(sourceTarFile); } } + private void writeLayerFile(Path layerFile, Path sourceTarFile) throws IOException { + 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(); + } + } + } + void apply(IOConsumer layers) throws IOException { for (Path path : this.layerFiles) { layers.accept(Layer.fromTarArchive((out) -> {