Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Potential file leak in FileSystem.read() method #1364

Open
pablobaxter opened this issue Oct 10, 2023 · 9 comments
Open

Potential file leak in FileSystem.read() method #1364

pablobaxter opened this issue Oct 10, 2023 · 9 comments

Comments

@pablobaxter
Copy link
Contributor

Continued investigation in file descriptor leaks led to this stack trace:

#1 /Users/pablobaxter/.gradle/caches/modules-2/files-2.1/com.squareup.wire/wire-schema-jvm/4.7.0/4f9c99c8217fc7d53fd11fa3e885559c62280227/wire-schema-jvm-4.7.0.jar by thread:main on Tue Oct 10 12:47:57 PDT 2023
        at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:173)
        at java.base/java.util.jar.JarFile.<init>(JarFile.java:347)
        at java.base/sun.net.www.protocol.jar.URLJarFile.<init>(URLJarFile.java:103)
        at java.base/sun.net.www.protocol.jar.URLJarFile.getJarFile(URLJarFile.java:72)
        at java.base/sun.net.www.protocol.jar.JarFileFactory.getOrCreate(JarFileFactory.java:106)
        at java.base/sun.net.www.protocol.jar.JarURLConnection.connect(JarURLConnection.java:132)
        at java.base/sun.net.www.protocol.jar.JarURLConnection.getInputStream(JarURLConnection.java:175)
        at java.base/java.net.URL.openStream(URL.java:1161)
        at java.base/java.lang.ClassLoader.getResourceAsStream(ClassLoader.java:1735)
        at okio.internal.ResourceFileSystem.source(ResourceFileSystem.kt:129)
        at com.squareup.wire.schema.CoreLoader.load(CoreLoader.kt:54)
        at com.squareup.wire.schema.internal.CommonSchemaLoader.load(CommonSchemaLoader.kt:148)
        at com.squareup.wire.schema.Linker.getFileLinker$wire_schema(Linker.kt:74)
        at com.squareup.wire.schema.Linker.link(Linker.kt:95)
        at com.squareup.wire.schema.internal.CommonSchemaLoader.loadSchema(CommonSchemaLoader.kt:103)
        at com.squareup.wire.schema.SchemaLoader.loadSchema(SchemaLoader.kt:71)
        at com.squareup.wire.schema.WireRun.execute(WireRun.kt:219)
        at com.squareup.wire.schema.WireRun.execute(WireRun.kt:211)

From what I've been able to gather, this is the culprit code:

@Throws(IOException::class)
@JvmName("-read")
actual inline fun <T> read(file: Path, readerAction: BufferedSource.() -> T): T {
  return source(file).buffer().use {
    it.readerAction()
  }
}

From what I can tell, the use {} lambda is not closing the source property of the RealBufferedSource object, but it's unclear why.

@pablobaxter pablobaxter changed the title Potential leak in FileSystem.read() method Potential file leak in FileSystem.read() method Oct 10, 2023
@swankjesse
Copy link
Member

Any chance you could reproduce this with a FakeFileSystem and its ability to check for unexpected open files?

Looking at the stack trace here I don’t see the leak, though perhaps it’s in JarURLConnection or similar.

@pablobaxter
Copy link
Contributor Author

@swankjesse, I believe you are right. Using the FakeFileSystem, it shows the Source object generated under the covers of the read() function is closed properly. However, this wouldn't detect open streams within the standard Java library, which I believe is what we are seeing here. Looking at the trace, I'm seeing that calling getResourceAsStream() forces the stream to stay open even when close() is called due to a caching flag. Tracing the code:

https://github.com/openjdk/jdk17/blob/4afbcaf55383ec2f5da53282a1547bac3d099e9d/src/java.base/share/classes/java/lang/ClassLoader.java#L1731

public InputStream getResourceAsStream(String name) {
    Objects.requireNonNull(name);
    URL url = getResource(name);
    try {
        return url != null ? url.openStream() : null;
    } catch (IOException e) {
        return null;
    }
}

Here, the focus is on the openStream() function, which calls openConnection().getInputStream() to create a new JarURLConnection object and a new JarURLInputStream object:
https://github.com/openjdk/jdk17/blob/4afbcaf55383ec2f5da53282a1547bac3d099e9d/src/java.base/share/classes/sun/net/www/protocol/jar/Handler.java#L38C46-L38C46

protected java.net.URLConnection openConnection(URL u)
throws IOException {
    return new JarURLConnection(u, this);
}

https://github.com/openjdk/jdk17/blob/4afbcaf55383ec2f5da53282a1547bac3d099e9d/src/java.base/share/classes/sun/net/www/protocol/jar/JarURLConnection.java#L105

class JarURLInputStream extends java.io.FilterInputStream {
    JarURLInputStream (InputStream src) {
        super (src);
    }
    public void close () throws IOException {
        try {
            super.close();
        } finally {
            if (!getUseCaches()) {
                jarFile.close();
            }
        }
    }
}

What's interesting here is that the InputStream created here will not close so long as the the cache flag is enabled, which is true by default: https://github.com/openjdk/jdk17/blob/4afbcaf55383ec2f5da53282a1547bac3d099e9d/src/java.base/share/classes/java/net/URLConnection.java#L226

I believe there is a workaround for this (by calling setUseCaches(false) on the URLConnection), but I'd have to verify this on a test project. I still need to find a way to unit test this as well.

@swankjesse
Copy link
Member

swankjesse commented Oct 16, 2023

Great investigation!

@Lisi-Chen
Copy link

Hello @pablobaxter and @swankjesse

I've also encountered this issue and I'd like to help with the file leak in FileSystem.read(). I'm considering exploring the setUseCaches(false) workaround and its effects.

I'm interested in working on this issue for an assignment. If it's unassigned, could it be assigned to me?

Thanks!

@pablobaxter
Copy link
Contributor Author

I'm not actively working on a solution for this yet. My idea was to approach this similarly to how Gradle approached what I believe was a similar issue with the Java API.

If you intend to take this on, feel free to ping me on a PR for it.

@Lisi-Chen
Copy link

I tried a few attempts to resolve the file descriptor leak in FileSystem.read() method.

  1. Close InputStream in read() Method
    I tried to explicitly close the InputStream after converting it to a BufferedSource and before its subsequent use.

  2. Disable URL Connection Caching
    Disabled URL connection caching in an attempt to prevent resources from being retained.

  3. Use a Custom Resource Stream Wrapper with Caching Control
    Implemented a custom resource stream wrapper to provide direct control over the useCaches flag of JarURLConnection, and ensured that the InputStream is explicitly closed.

Preliminary tests indicate a successful resolution of the file descriptor leak issue for the third method, but I am still working on testing to validate the stability of this solution.

@Lisi-Chen
Copy link

Lisi-Chen commented Oct 29, 2023

I tried to devise a custom resource stream wrapper to gain better control over the useCaches flag of JarURLConnection. This is implemented to ensure that the InputStream is closed explicitly.

object CustomResourceLoader {

    fun getResourceAsStream(classLoader: ClassLoader, resourceName: String): InputStream? {
        val resourceURL = classLoader.getResource(resourceName) ?: return null
        val connection = resourceURL.openConnection()

        // Explicitly disable caching
        connection.useCaches = false

        return connection.getInputStream().let { CustomResourceInputStream(it) }
    }
}

private class CustomResourceInputStream(inputStream: InputStream) : FilterInputStream(inputStream) {

    override fun close() {
        try {
            super.close()
        } finally {
            // Ensure the underlying jar file is closed
            (this.`in` as? JarURLConnection)?.jarFile?.close()
        }
    }
}

// Usage:
val stream = CustomResourceLoader.getResourceAsStream(
    ClassLoader.getSystemClassLoader(), "path/to/resource"
)
stream?.use { /* process the stream */ }


// Usage:
val stream = CustomResourceLoader.getResourceAsStream(
    ClassLoader.getSystemClassLoader(), "path/to/resource"
)
stream?.use { /* process the stream */ }`

@pablobaxter
Copy link
Contributor Author

@Lisi-Chen I'm surprised that just setting connection.useCaches = false wasn't enough. From what I was seeing, this flag alone was preventing the underlying jar file from being closed.

pablobaxter added a commit to pablobaxter/okio that referenced this issue Feb 9, 2024
pablobaxter added a commit to pablobaxter/okio that referenced this issue Feb 10, 2024
pablobaxter added a commit to pablobaxter/okio that referenced this issue Feb 10, 2024
@pablobaxter
Copy link
Contributor Author

I decided to take a stab at this. Unit test took some effort, but it is able to confirm whether the file is cached by the URLConnection or not.

swankjesse pushed a commit that referenced this issue Feb 18, 2024
* Fix for #1364

* Fix issue for Java8 testing

* Ensure fileLeakInResourceFileSystemTest runs only if `/proc` is available
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants