JDK-4952229 : FileOutputStream.close() doesn't propagate native close() errors via IOException
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.io
  • Affected Version: 1.4.2
  • Priority: P3
  • Status: Resolved
  • Resolution: Fixed
  • OS: solaris_8
  • CPU: sparc
  • Submitted: 2003-11-11
  • Updated: 2017-05-16
  • Resolved: 2004-02-06
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.
Other
5.0 b38Fixed
Description

Name: jl125535			Date: 11/11/2003


FULL PRODUCT VERSION :
Observed on:
java version "1.3.1_01"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.3.1_01)
Java HotSpot(TM) Client VM (build 1.3.1_01, mixed mode)

And

java version "1.4.2_01"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.4.2_01-b06)
Java HotSpot(TM) Client VM (build 1.4.2_01-b06, mixed mode)


FULL OS VERSION :
SunOS ourhostname 5.8 Generic_108528-01 sun4u sparc SUNW,Ultra-30
Haven't tested on Linux or Windows, but it may also be a problem there.

A DESCRIPTION OF THE PROBLEM :
The C close(fd) call can fail, returning -1. When closing a FileOutputStream, this error is not propagated via a thrown IOException.

This can be easily reproduced by creating a file on an NFS server, writing data, then pausing for console input. Unplug the connection to the NFS server, then press return. Close the FileOutputStream, catching any IOException from the close and displaying it. You won't see one; its a silent failure.

The same code in C will return -1 from the close with an appropriate error in errno.

The "severity" field below doesn't have an option for "Data Loss", but that's what this means for us.

Note that our actual case doesn't involve NFS, but a custom filesystem of ours which can return -1 on close. However, the problem can be easily reproduced as explained above.


Looking through the 1.3.1 (and 1.4.1) source, I see:
j2sdk1.3.1/src/share/classes/java/io/FileOutputStream.java -

public native void close() throws IOException;

This calls....

j2sdk1.3.1/src/share/native/java/io/FileOutputStream.c -

JNIEXPORT void JNICALL
Java_java_io_FileOutputStream_close(JNIEnv *env, jobject this) {
    int fd = GET_FD(fos_fd);
    SET_FD(-1, fos_fd);
    JVM_Close(fd);
}


Should that last line read:
    if (JVM_Close(fd) == -1)
        JNU_ThrowIOExceptionWithLastError(env, "close failed");

?


(Also scanned through the source for other calls to JVM_Close(fd), and didn't see the return being checked on any of them....)


STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
On your workstation, run the attached java or C test, passing as an argument the name of a file on a remote NFS (soft-mounted) server. When prompted "continue:", disconnect your workstation from the network, then press return.

After the NFS timeout...

With the C program, you should see "close() failed".

With the Java program you will see "close() succeeded".

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
I'd expect an appropriate IOException being thrown, as the API for FileOutputStream.close() suggests.

ACTUAL -
# this is the java version of the test program...
[cjones@rhino] java iotest abc.j
open(abc.j) succeeded
continue: y
NFS commit failed for server thelma: error 5 (RPC: Timed out)
close() succeeded

# this is the C version...
[cjones@rhino] ./raw abc.r
raw: open(abc.r) succeeded
continue: y
NFS commit failed for server thelma: error 5 (RPC: Timed out)
raw: close() failed: Connection timed out


REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
---------- Java version ------------

import java.io.FileOutputStream;
import java.io.IOException;

public class iotest
{
    public static void main(String[] args)
    {
        if (args.length != 1)
        {
            System.err.println("usage: test file");
            System.exit(1);
        }

        FileOutputStream outputStream = null;

        try
        {
            outputStream = new FileOutputStream(args[0]);
        }
        catch (IOException e)
        {
            System.err.println("open() failed: " + e.getMessage());
            System.exit(2);
        }

        System.out.println("open(" + args[0] + ") succeeded");

        byte[] buf = new String("01234567890\n01234567890\n").getBytes();

        for (int i = 0; i < 10; i++)
        {
            try
            {
                outputStream.write(buf);
            }
            catch (IOException e)
            {
                System.err.println("write() failed: " + e.getMessage());
                break;
            }
        }

        byte[] response = new byte[2];

        System.out.print("continue: ");

        try
        {
            System.in.read(response);
        }
        catch (IOException e)
        {
            System.err.println("read() failed: " + e.getMessage());
        }

        try
        {
            outputStream.close();
            System.out.println("close() succeeded");
        }
        catch (IOException e)
        {
            System.err.println("close() failed: " + e.getMessage());
        }
    }
}








-------- C version -----------
#include <libgen.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
    static char buf[] = "0123456789\n0123456789\n";
    char *progname = basename(argv[0]);
    int output;
    ssize_t n;
    char c;
    int i;

    if (argc != 2)
    {
        (void) fprintf(stderr, "usage: %s file\n", progname);
        return 1;
    }

    if ((output = open(argv[1], (O_WRONLY|O_TRUNC|O_CREAT), 0666)) < 0)
    {
        (void) fprintf(stderr, "%s: open(%s) failed: %s\n", progname, argv[1], strerror(errno));
        return 2;
    }

    (void) fprintf(stderr, "%s: open(%s) succeeded\n", progname, argv[1]);

    for (i = 0; i < 10; i++)
    {
        if ((n = write(output, buf, 22)) < 0)
        {
            (void) fprintf(stderr, "%s: write() failed: %s\n", progname, strerror(errno));
            break;
        }

        if (n < 22)
        {
            (void) fprintf(stderr, "%s: short write(): %d bytes written\n", progname, n);
            break;
        }
    }

    (void) fputs("continue: ", stdout);

    c = getc(stdin);

    if (close(output) < 0)
    {
        (void) fprintf(stderr, "%s: close() failed: %s\n", progname, strerror(errno));
        return 2;
    }

    (void) fprintf(stderr, "%s: close() succeeded\n", progname);
    return 0;
}


---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
I'd like to have been able to call FileOutputStream.getFD(), get the int fd from that, then go native myself, and throw the IOException if close(fd) fails. Pity the fd is private.
(Incident Review ID: 207606) 
======================================================================

Comments
CONVERTED DATA BugTraq+ Release Management Values COMMIT TO FIX: tiger-beta2 FIXED IN: tiger-beta2 INTEGRATED IN: tiger-b38 tiger-beta2
14-06-2004

EVALUATION The intent is to fix this in Tiger. ###@###.### 2003-11-18 Fix targeted for b32. ###@###.### 2003-12-04
18-11-2003