Skip to content

File descriptor leak in ObjectMapper#writeValue with File #3508

@fxha

Description

@fxha

Describe the bug
Jackson will leak a file descriptor when using writeValue(File resultFile, Object value) in ObjectMapper and a failure occurs during closing the internal stream that's owned by Jackson (e.g. when flushing buffers). In this case, a file stream is created but the access to the file content is prohibited. This leads to the exceptional case that the stream is never closed and leaked. This bug may affect jackson.core but I think the core issues is located in databind's _writeValueAndClose due to poor resource handling.

Stack Trace:

writeBytes:-1, FileOutputStream (java.io)
write:354, FileOutputStream (java.io)
_flushBuffer:2171, UTF8JsonGenerator (com.fasterxml.jackson.core.json)
close:1214, UTF8JsonGenerator (com.fasterxml.jackson.core.json)
_writeValueAndClose:4573, ObjectMapper (com.fasterxml.jackson.databind)
writeValue:3763, ObjectMapper (com.fasterxml.jackson.databind)
lambda$testJacksonBug2$1:54, MainTest
execute:-1, 1778629809 (MainTest$$Lambda$338)
assertThrows:55, AssertThrows (org.junit.jupiter.api)
assertThrows:37, AssertThrows (org.junit.jupiter.api)
assertThrows:3082, Assertions (org.junit.jupiter.api)
testJacksonBug2:54, MainTest

Version information
v2.13.3

To Reproduce
This bug is a little bit tricky to reproduce but you can lock a file on Windows, where you can successfully create a file stream (with a valid file descriptor) but without access to any file content. On Unix, you need a lot of "luck" to get this case happening.

  1. Create a demo Java project with both jackson core and databind on Windows!
  2. Create a JUnit 5 test
  3. Copy example code
class MainTest {
    @TempDir
    File tempDir;

    @Test
    void testJacksonBug2() throws IOException {
        Path filePath = Paths.get(tempDir.getAbsolutePath(), "example.json");
        try (FileOutputStream outputStream = new FileOutputStream(filePath.toString());
             FileChannel channel = outputStream.getChannel();
             FileLock lock = channel.lock()) {
            assertThrows(IOException.class, () -> new ObjectMapper().writeValue(filePath.toFile(), new JsonEntry()));
        }

        assertTrue(filePath.toFile().delete());
    }

    private static class JsonEntry {
        public int value = 1;
    }
}

You'll notice that the the deletion of the example file (last line) will fail but also that JUnit is unable to clearup the temporary directory because a file descriptor is still open. Everything is fine if you comment out the "writeValue"-line or write through the locked channel.

Expected behavior
The file stream should be correctly closed, regardless of whether an exception is thrown or not.

Additional context
This issue is related to #3455 but during close of UTF8JsonGenerator.

Workaround: use an owned stream with try-with-resources and passing the stream into Jackson.

Metadata

Metadata

Assignees

No one assigned

    Labels

    to-evaluateIssue that has been received but not yet evaluated

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions