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 <devjunsday@gmail.com>

See gh-51519
This commit is contained in:
Junhwan Choi
2026-09-04 11:06:03 +01:00
committed by Andy Wilkinson
parent a833fe58f9
commit 5d6e417134
2 changed files with 48 additions and 15 deletions
@@ -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);
@@ -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<String> 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<String> 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"));