JDK-8232003 : (fs) Files.write can leak file descriptor in the exception case
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.nio
  • Affected Version: 8,11,13,14
  • Priority: P4
  • Status: Closed
  • Resolution: Fixed
  • OS: linux
  • CPU: generic
  • Submitted: 2019-10-08
  • Updated: 2022-06-27
  • Resolved: 2019-10-09
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 11 JDK 13 JDK 14 JDK 7 JDK 8 Other
11.0.7-oracleFixed 13.0.4Fixed 14 b18Fixed 7u261Fixed 8u251Fixed openjdk7uFixed
Description
The code for Files.write() looks like this:

public static Path write(Path path, Iterable<? extends CharSequence> lines,
                             Charset cs, OpenOption... options)
        throws IOException
    {
        // ensure lines is not null before opening file
        Objects.requireNonNull(lines);
        CharsetEncoder encoder = cs.newEncoder();
        OutputStream out = newOutputStream(path, options);
        try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, encoder))) {
            for (CharSequence line: lines) {
                writer.append(line);
                writer.newLine();
            }
        }
        return path;
    }

Even though the Javadoc says that the file descriptors are properly closed, the OutputStream fd is being leaked in case of Exceptions. 

In order to reproduce on Linux do the following:

As root:
$ mkdir /mnt/tmpfs
$ mount -t tmpfs none -o size=4096 /mnt/tmpfs/

Then as regular user do:
$ cd /mnt/tmpfs
$ java Reproducer

In a separate terminal observe:
$ jps | grep Reproducer
31423 Reproducer
$ lsof -p 31423 | grep /mnt/tmpfs/foo | wc -l
92671

It's expected that there is only one file descriptor pointing to /mnt/tmpfs/foo, not many.

Depending on 'ulimit -n' setting the reproducer might behave slightly different.
Comments
Fix request (13u): I'd like to port it to 13u as well. Patch applies cleanly.
22-05-2020

Fix Request (OpenJDK 8u): Please approve backporting this to OpenJDK 8u. The fix for JDK 14 applies as is, modulo path unshuffelling and passes the manual reproducer. Risk is rather low as the patch only fixes a resource leak in implementation details. It's also a JDK 8u252 parity patch as of today.
07-01-2020

Fix Request (OpenJDK 11u): Please approve backporting this to OpenJDK 11u. The fix for JDK 14 applies as is and passes the manual reproducer. Risk is rather low as the patch only fixes a resource leak in an implementation.
02-12-2019

URL: https://hg.openjdk.java.net/jdk/jdk/rev/8f849d3ec1e5 User: sgehwolf Date: 2019-10-09 11:48:42 +0000
09-10-2019

RFR: http://mail.openjdk.java.net/pipermail/nio-dev/2019-October/006689.html
08-10-2019

NFS may be another scenario where this could arise. A reliable test may be a challenge (A test that fills the file system will cause problems for other tests running concurrently). Marking this noreg-hard is okay.
08-10-2019

[~alanb] I'll include it here for clarity. I was only able to get the FileSystemException with an ulimit of 200 (default 1024). $ ulimit -n 200 $ cd /mnt/tmpfs/ $ java Reproducer java.io.IOException: No space left on device at java.base/sun.nio.ch.FileDispatcherImpl.write0(Native Method) at java.base/sun.nio.ch.FileDispatcherImpl.write(FileDispatcherImpl.java:62) at java.base/sun.nio.ch.IOUtil.writeFromNativeBuffer(IOUtil.java:113) at java.base/sun.nio.ch.IOUtil.write(IOUtil.java:79) at java.base/sun.nio.ch.FileChannelImpl.write(FileChannelImpl.java:282) at java.base/java.nio.channels.Channels.writeFullyImpl(Channels.java:74) at java.base/java.nio.channels.Channels.writeFully(Channels.java:97) at java.base/java.nio.channels.Channels$1.write(Channels.java:172) at java.base/sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:242) at java.base/sun.nio.cs.StreamEncoder.implClose(StreamEncoder.java:346) at java.base/sun.nio.cs.StreamEncoder.close(StreamEncoder.java:168) at java.base/java.io.OutputStreamWriter.close(OutputStreamWriter.java:258) at java.base/java.io.BufferedWriter.close(BufferedWriter.java:269) at java.base/java.nio.file.Files.write(Files.java:3559) at java.base/java.nio.file.Files.write(Files.java:3604) at Reproducer.main(Reproducer.java:14) ---------------------------------------------- java.nio.file.FileSystemException: foo: Too many open files at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:100) at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:111) at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:116) at java.base/sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:219) at java.base/java.nio.file.spi.FileSystemProvider.newOutputStream(FileSystemProvider.java:478) at java.base/java.nio.file.Files.newOutputStream(Files.java:223) at java.base/java.nio.file.Files.write(Files.java:3553) at java.base/java.nio.file.Files.write(Files.java:3604) at Reproducer.main(Reproducer.java:14)
08-10-2019

Please bring this to nio-dev to discuss and include the exception is possible as it's not clear how newOutputStream can throw an exception with an open file descriptor (it something other than open failing?)
08-10-2019

webrev: http://cr.openjdk.java.net/~sgehwolf/webrevs/JDK-8232003/01/webrev/
08-10-2019

Suggested fix: diff --git a/src/java.base/share/classes/java/nio/file/Files.java b/src/java.base/share/classes/java/nio/file/Files.java --- a/src/java.base/share/classes/java/nio/file/Files.java +++ b/src/java.base/share/classes/java/nio/file/Files.java @@ -3550,8 +3550,8 @@ // ensure lines is not null before opening file Objects.requireNonNull(lines); CharsetEncoder encoder = cs.newEncoder(); - OutputStream out = newOutputStream(path, options); - try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, encoder))) { + try (OutputStream out = newOutputStream(path, options); + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, encoder))) { for (CharSequence line: lines) { writer.append(line); writer.newLine();
08-10-2019