bug-gnulib
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: Solaris ftell bug


From: Bruno Haible
Subject: Re: Solaris ftell bug
Date: Sat, 1 May 2010 20:37:33 +0200
User-agent: KMail/1.9.9

Eric Blake wrote:
> As proof that the bug exists independently of gnulib:
> 
> $ cat foo.c
> #include <stdio.h>
> #include <stdio_ext.h>
> int main() {
>   FILE *fp = fopen ("file", "r+");
>   fseek (fp, -1, SEEK_END);
>   if (getc (fp) != 'h') return 1;
>   if (getc (fp) != EOF) return 2;
>   __fpurge (fp);
>   if (putc ('!', fp) != '!') return 3;
>   printf ("%d\n", (int) ftell (fp));
>   if (fclose (fp) != 0) return 4;
>   return 0;
> }
> $ printf foogarsh > file
> $ ./foo
> -8183
> 
> I guess we need to beef up the unit tests to expose the Solaris bug,
> then work on replacing broken __fpurge with a version that works
> correctly on read-write streams.

The bug in Solaris is not in fpurge, bug actually in getc + putc + ftell.

Here's a sample code without fpurge:

=============================== foo.c ================================
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define ASSERT(expr) \
  do                                                                         \
    {                                                                        \
      if (!(expr))                                                           \
        {                                                                    \
          fprintf (stderr, "%s:%d: assertion failed\n",                      \
                   __FILE__, __LINE__);                                      \
          fflush (stderr);                                                   \
          abort ();                                                          \
        }                                                                    \
    }                                                                        \
  while (0)

#define TESTFILE "t-fpurge2.tmp"

int
main (void)
{
  FILE *fp;

  /* Create a file with some contents.  */
  fp = fopen (TESTFILE, "w");
  if (fp == NULL)
    goto skip;
  if (fwrite ("foogarsh", 1, 8, fp) < 8)
    goto skip;
  if (fclose (fp))
    goto skip;

  /* The file's contents is now "foogarsh".  */

  /* Ensure that purging a read does not corrupt subsequent writes.  */
  fp = fopen (TESTFILE, "r+");
  if (fp == NULL)
    goto skip;
  if (fseek (fp, -1, SEEK_END))
    goto skip;
  ASSERT (getc (fp) == 'h');
  ASSERT (getc (fp) == EOF);
  ASSERT (ftell (fp) == 8);
  ASSERT (putc ('!', fp) == '!');
  ASSERT (ftell (fp) == 9);
  ASSERT (fclose (fp) == 0);
  fp = fopen (TESTFILE, "r");
  if (fp == NULL)
    goto skip;
  {
    char buf[10];
    ASSERT (fread (buf, 1, 10, fp) == 9);
    ASSERT (memcmp (buf, "foogarsh!", 9) == 0);
  }
  ASSERT (fclose (fp) == 0);

  /* The file's contents is now "foogarsh!".  */

  remove (TESTFILE);
  return 0;

 skip:
  fprintf (stderr, "Skipping test: prerequisite file operations failed.\n");
  remove (TESTFILE);
  return 77;
}
==========================================================================
$ ./foo
foo.c:46: assertion failed
Abort (core dumped)

POSIX <http://www.opengroup.org/onlinepubs/9699919799/functions/fopen.html>
imposes the restriction that
  "When a file is opened with update mode ('+' as the second or third
   character in the mode argument), both input and output may be performed
   on the associated stream. However, the application shall ensure that
   ... input is not directly followed by output without an intervening
   call to a file positioning function (fseek(), fsetpos(), or rewind()),
   unless the input operation encounters end-of-file."

Here the input operation did encounter end-of-file, and nevertheless the
putc call disturb's ftell's notion of current file position.

Changing the test program by inserting an fflush operation
  ASSERT (getc (fp) == EOF);
  ASSERT (ftell (fp) == 8);
  fflush (fp);
  ASSERT (ftell (fp) == 8);
  ASSERT (putc ('!', fp) == '!');
makes it succeed. Before the fflush, the stream looks like this:
  (gdb) print *fp
  $1 = {_cnt = 0, _ptr = 0x8061654 "hoogarsh", _base = 0x8061654 "hoogarsh", 
    _flag = 153 '\231', _file = 6 '\006', __orientation = 0, __ionolock = 0, 
    __seekable = 1, __extendedfd = 0, __xf_nocheck = 0, __filler = 0}
After the fflush, the stream looks like this:
  (gdb) print *fp
  $3 = {_cnt = 0, _ptr = 0x8061654 "hoogarsh", _base = 0x8061654 "hoogarsh", 
    _flag = 152 '\230', _file = 6 '\006', __orientation = 0, __ionolock = 0, 
    __seekable = 1, __extendedfd = 0, __xf_nocheck = 0, __filler = 0}
As you can see, the _flag field has changed from
  _IORW | _IOEOF | _IOMYBUF | _IOREAD
to
  _IORW | _IOEOF | _IOMYBUF

Changing the test program by inserting an fseek operation
  ASSERT (getc (fp) == EOF);
  ASSERT (ftell (fp) == 8);
  fseek (fp, 0, SEEK_CUR);
  ASSERT (ftell (fp) == 8);
  ASSERT (putc ('!', fp) == '!');
makes it succeed as well. Before the fseek, the stream looks like this:
  (gdb) print *fp
  $2 = {_cnt = 0, _ptr = 0x8061664 "hoogarsh", _base = 0x8061664 "hoogarsh", 
    _flag = 153 '\231', _file = 6 '\006', __orientation = 0, __ionolock = 0, 
    __seekable = 1, __extendedfd = 0, __xf_nocheck = 0, __filler = 0}
After the fseek, it looks like this
  $3 = {_cnt = 0, _ptr = 0x8061664 "hoogarsh", _base = 0x8061664 "hoogarsh", 
    _flag = 136 '\210', _file = 6 '\006', __orientation = 0, __ionolock = 0, 
    __seekable = 1, __extendedfd = 0, __xf_nocheck = 0, __filler = 0}
As you can see, the _flag field has changed from
  _IORW | _IOEOF | _IOMYBUF | _IOREAD
to
  _IORW | _IOMYBUF

The crucial difference is the _IOREAD flag. Indeed, the OpenSolaris
ftell.c code looks at this flag.

For comparison, here's the same program, with a dump of the FILE's flags:

=================================== foo.c ===============================
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define ASSERT(expr) \
  do                                                                         \
    {                                                                        \
      if (!(expr))                                                           \
        {                                                                    \
          fprintf (stderr, "%s:%d: assertion failed\n",                      \
                   __FILE__, __LINE__);                                      \
          fflush (stderr);                                                   \
          abort ();                                                          \
        }                                                                    \
    }                                                                        \
  while (0)

static void dumpFILE (FILE *fp)
{
  fprintf (stderr, "_ptr = %p, _base = %p, _cnt = %d, _flag = 0x%x 
=%s%s%s%s%s\n",
           fp->_ptr, fp->_base, fp->_cnt, fp->_flag,
           fp->_flag & _IORW ? " _IORW" : "",
           fp->_flag & _IOEOF ? " _IOEOF" : "",
           fp->_flag & _IOMYBUF ? " _IOMYBUF" : "",
           fp->_flag & _IOWRT ? " _IOWRT" : "",
           fp->_flag & _IOREAD ? " _IOREAD" : "");
  fflush (stderr);
}

#define TESTFILE "t-fpurge2.tmp"

int
main (void)
{
  FILE *fp;

  /* Create a file with some contents.  */
  fp = fopen (TESTFILE, "w");
  if (fp == NULL)
    goto skip;
  if (fwrite ("foogarsh", 1, 8, fp) < 8)
    goto skip;
  if (fclose (fp))
    goto skip;

  /* The file's contents is now "foogarsh".  */

  /* Ensure that purging a read does not corrupt subsequent writes.  */
  fp = fopen (TESTFILE, "r+");
  if (fp == NULL)
    goto skip;
  if (fseek (fp, -1, SEEK_END))
    goto skip;
  ASSERT (getc (fp) == 'h');
  ASSERT (getc (fp) == EOF);
  dumpFILE(fp);
  ASSERT (ftell (fp) == 8);
  dumpFILE(fp);
#if 0
  fseek (fp, 0, SEEK_CUR);
  dumpFILE(fp);
#endif
  ASSERT (ftell (fp) == 8);
  dumpFILE(fp);
  ASSERT (putc ('!', fp) == '!');
  dumpFILE(fp);
  ASSERT (ftell (fp) == 9);
  ASSERT (fclose (fp) == 0);
  fp = fopen (TESTFILE, "r");
  if (fp == NULL)
    goto skip;
  {
    char buf[10];
    ASSERT (fread (buf, 1, 10, fp) == 9);
    ASSERT (memcmp (buf, "foogarsh!", 9) == 0);
  }
  ASSERT (fclose (fp) == 0);

  /* The file's contents is now "foogarsh!".  */

  remove (TESTFILE);
  return 0;

 skip:
  fprintf (stderr, "Skipping test: prerequisite file operations failed.\n");
  remove (TESTFILE);
  return 77;
}
=========================================================================

Result on various systems:

AIX
_ptr = 20000e30, _base = 20000e30, _cnt = 0, _flag = 0x98 = _IORW _IOEOF 
_IOMYBUF
_ptr = 20000e30, _base = 20000e30, _cnt = 0, _flag = 0x98 = _IORW _IOEOF 
_IOMYBUF
_ptr = 20000e30, _base = 20000e30, _cnt = 0, _flag = 0x98 = _IORW _IOEOF 
_IOMYBUF
_ptr = 20000e31, _base = 20000e30, _cnt = 4095, _flag = 0x8a = _IORW _IOMYBUF 
_IOWRT

HP-UX
_ptr = 40003228, _base = 40003228, _cnt = 0, _flag = 0x118 = _IORW _IOEOF 
_IOMYBUF
_ptr = 40003228, _base = 40003228, _cnt = 0, _flag = 0x118 = _IORW _IOEOF 
_IOMYBUF
_ptr = 40003228, _base = 40003228, _cnt = 0, _flag = 0x118 = _IORW _IOEOF 
_IOMYBUF
_ptr = 40003229, _base = 40003228, _cnt = 8191, _flag = 0x10a = _IORW _IOMYBUF 
_IOWRT

OSF/1
_ptr = 140006008, _base = 140006008, _cnt = 0, _flag = 0x118 = _IORW _IOEOF 
_IOMYBUF
_ptr = 140006008, _base = 140006008, _cnt = 0, _flag = 0x118 = _IORW _IOEOF 
_IOMYBUF
_ptr = 140006008, _base = 140006008, _cnt = 0, _flag = 0x118 = _IORW _IOEOF 
_IOMYBUF
_ptr = 140006009, _base = 140006008, _cnt = 65535, _flag = 0x10a = _IORW 
_IOMYBUF _IOWRT

Solaris
_ptr = 2162c, _base = 2162c, _cnt = 0, _flag = 0x99 = _IORW _IOEOF _IOMYBUF 
_IOREAD
_ptr = 2162c, _base = 2162c, _cnt = 0, _flag = 0x99 = _IORW _IOEOF _IOMYBUF 
_IOREAD
_ptr = 2162c, _base = 2162c, _cnt = 0, _flag = 0x99 = _IORW _IOEOF _IOMYBUF 
_IOREAD
_ptr = 2162d, _base = 2162c, _cnt = 8191, _flag = 0x8b = _IORW _IOMYBUF _IOWRT 
_IOREAD
foo.c:67: assertion failed
Abort

mingw
_ptr = 003E3B28, _base = 003E3B28, _cnt = 0, _flag = 0x99 = _IORW _IOEOF 
_IOMYBUF _IOREAD
_ptr = 003E3B28, _base = 003E3B28, _cnt = 0, _flag = 0x99 = _IORW _IOEOF 
_IOMYBUF _IOREAD
_ptr = 003E3B28, _base = 003E3B28, _cnt = 0, _flag = 0x99 = _IORW _IOEOF 
_IOMYBUF _IOREAD
_ptr = 003E3B29, _base = 003E3B28, _cnt = 4095, _flag = 0x8a = _IORW _IOMYBUF 
_IOWRT

As you can see, the _IOREAD flag was cleared during the putc call (when it set
the _IOWRT flag) on all platforms, but not on Solaris.

I don't think gnulib should hook into getc (to clear the _IOREAD flag when EOF
is reached) or into putc (to clear the _IOREAD flag when switching to write
mode), because that would cause major inefficiencies. Instead, the ftell 
function
should be taught to ignore the _IOREAD flag when _IOWRT is set.

I'm applying these two fixes.


2010-05-01  Bruno Haible  <address@hidden>

        freading: Adapt to special meaning of _IOREAD flag on Solaris.
        * lib/freading.c (freading): On Solaris, ignore the _IOREAD flag if
        the _IOWRT flag is also set.

--- lib/freading.c.orig Sat May  1 20:21:32 2010
+++ lib/freading.c      Sat May  1 19:20:54 2010
@@ -40,7 +40,11 @@
 #elif defined __EMX__               /* emx+gcc */
   return (fp->_flags & _IOREAD) != 0;
 #elif defined _IOERR                /* AIX, HP-UX, IRIX, OSF/1, Solaris, 
OpenServer, mingw */
+# if defined __sun                  /* Solaris */
+  return (fp->_flag & _IOREAD) != 0 && (fp->_flag & _IOWRT) == 0;
+# else
   return (fp->_flag & _IOREAD) != 0;
+# endif
 #elif defined __UCLIBC__            /* uClibc */
   return (fp->__modeflags & (__FLAG_READONLY | __FLAG_READING)) != 0;
 #elif defined __QNX__               /* QNX */


2010-05-01  Bruno Haible  <address@hidden>

        ftell, ftello: Work around Solaris bug.
        * m4/ftello.m4 (gl_FUNC_FTELLO): Detect Solaris bug.
        * lib/ftello.c: Include stdio-impl.h.
        (ftello): On Solaris, when _IOWRT is set, compute the result without
        looking at _IOREAD.
        * modules/ftello (Files): Add lib/stdio-impl.h.
        * doc/posix-functions/ftell.texi: Mention Solaris bug.
        * doc/posix-functions/ftello.texi: Likewise.
        Reported by Eric Blake.

--- doc/posix-functions/ftell.texi.orig Sat May  1 20:31:42 2010
+++ doc/posix-functions/ftell.texi      Sat May  1 19:21:26 2010
@@ -10,6 +10,10 @@
 @itemize
 @item
 This function mistakenly succeeds on pipes on some platforms: mingw.
address@hidden
+This function produces incorrect results after @code{putc} that followed a
address@hidden call that reached EOF on some platforms:
+Solaris 10.
 @end itemize
 
 Portability problems not fixed by Gnulib:
--- doc/posix-functions/ftello.texi.orig        Sat May  1 20:31:42 2010
+++ doc/posix-functions/ftello.texi     Sat May  1 19:21:19 2010
@@ -15,6 +15,10 @@
 The declaration of @code{ftello} in @code{<stdio.h>} is not enabled by default
 on some platforms: glibc 2.3.6.
 @item
+This function produces incorrect results after @code{putc} that followed a
address@hidden call that reached EOF on some platforms:
+Solaris 10.
address@hidden
 This function fails on seekable stdin, stdout, and stderr: cygwin <= 1.5.24.
 @end itemize
 
--- lib/ftello.c.orig   Sat May  1 20:31:42 2010
+++ lib/ftello.c        Sat May  1 20:17:25 2010
@@ -22,6 +22,8 @@
 /* Get lseek.  */
 #include <unistd.h>
 
+#include "stdio-impl.h"
+
 off_t
 ftello (FILE *fp)
 #undef ftello
@@ -36,6 +38,28 @@
     return -1;
 #endif
 
+#if FTELLO_BROKEN_AFTER_SWITCHING_FROM_READ_TO_WRITE /* Solaris */
+  /* The Solaris stdio leaves the _IOREAD flag set after reading from a file
+     reaches EOF and the program then starts writing to the file.  ftello
+     gets confused by this.  */
+  if (fp_->_flag & _IOWRT)
+    {
+      off_t pos;
+
+      /* Call ftello nevertheless, for the side effects that it does on fp.  */
+      ftello (fp);
+
+      /* Compute the file position ourselves.  */
+      pos = llseek (fileno (fp), (off_t) 0, SEEK_CUR);
+      if (pos >= 0)
+        {
+          if ((fp_->_flag & _IONBF) == 0 && fp_->_base != NULL)
+            pos += fp_->_ptr - fp_->_base;
+        }
+      return pos;
+    }
+#endif
+
 #if defined __SL64 && defined __SCLE /* Cygwin */
   if ((fp->_flags & __SL64) == 0)
     {
--- m4/ftello.m4.orig   Sat May  1 20:31:42 2010
+++ m4/ftello.m4        Sat May  1 19:49:09 2010
@@ -1,5 +1,5 @@
-# ftello.m4 serial 6
-dnl Copyright (C) 2007, 2008, 2009, 2010 Free Software Foundation, Inc.
+# ftello.m4 serial 7
+dnl Copyright (C) 2007-2010 Free Software Foundation, Inc.
 dnl This file is free software; the Free Software Foundation
 dnl gives unlimited permission to copy and/or distribute it,
 dnl with or without modifications, as long as this notice is preserved.
@@ -10,7 +10,7 @@
   AC_REQUIRE([AC_PROG_CC])
   AC_REQUIRE([gl_STDIN_LARGE_OFFSET])
 
-  dnl Persuade glibc <stdio.h> to declare fseeko().
+  dnl Persuade glibc <stdio.h> to declare ftello().
   AC_REQUIRE([AC_USE_SYSTEM_EXTENSIONS])
 
   AC_CACHE_CHECK([for ftello], [gl_cv_func_ftello],
@@ -23,6 +23,93 @@
   else
     if test $gl_cv_var_stdin_large_offset = no; then
       REPLACE_FTELLO=1
+    else
+      dnl Detect bug on Solaris.
+      dnl ftell and ftello produce incorrect results after putc that followed a
+      dnl getc call that reached EOF on Solaris. This is because the _IOREAD
+      dnl flag does not get cleared in this case, even though _IOWRT gets set,
+      dnl and ftell and ftello look whether the _IOREAD flag is set.
+      AC_REQUIRE([AC_CANONICAL_HOST])
+      AC_CACHE_CHECK([whether ftello works],
+        [gl_cv_func_ftello_works],
+        [
+          dnl Initial guess, used when cross-compiling or when /dev/tty cannot
+          dnl be opened.
+changequote(,)dnl
+          case "$host_os" in
+                      # Guess no on Solaris.
+            solaris*) gl_cv_func_ftello_works="guessing no" ;;
+                      # Guess yes otherwise.
+            *)        gl_cv_func_ftello_works="guessing yes" ;;
+          esac
+changequote([,])dnl
+          AC_TRY_RUN([
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#define TESTFILE "conftest.tmp"
+int
+main (void)
+{
+  FILE *fp;
+
+  /* Create a file with some contents.  */
+  fp = fopen (TESTFILE, "w");
+  if (fp == NULL)
+    return 70;
+  if (fwrite ("foogarsh", 1, 8, fp) < 8)
+    return 71;
+  if (fclose (fp))
+    return 72;
+
+  /* The file's contents is now "foogarsh".  */
+
+  /* Try writing after reading to EOF.  */
+  fp = fopen (TESTFILE, "r+");
+  if (fp == NULL)
+    return 73;
+  if (fseek (fp, -1, SEEK_END))
+    return 74;
+  if (!(getc (fp) == 'h'))
+    return 1;
+  if (!(getc (fp) == EOF))
+    return 2;
+  if (!(ftell (fp) == 8))
+    return 3;
+  if (!(ftell (fp) == 8))
+    return 4;
+  if (!(putc ('!', fp) == '!'))
+    return 5;
+  if (!(ftell (fp) == 9))
+    return 6;
+  if (!(fclose (fp) == 0))
+    return 7;
+  fp = fopen (TESTFILE, "r");
+  if (fp == NULL)
+    return 75;
+  {
+    char buf[10];
+    if (!(fread (buf, 1, 10, fp) == 9))
+      return 10;
+    if (!(memcmp (buf, "foogarsh!", 9) == 0))
+      return 11;
+  }
+  if (!(fclose (fp) == 0))
+    return 12;
+
+  /* The file's contents is now "foogarsh!".  */
+
+  return 0;
+}], [gl_cv_func_ftello_works=yes], [gl_cv_func_ftello_works=no], [:])
+        ])
+      case "$gl_cv_func_ftello_works" in
+        *yes) ;;
+        *)
+          REPLACE_FTELLO=1
+          AC_DEFINE([FTELLO_BROKEN_AFTER_SWITCHING_FROM_READ_TO_WRITE], [1],
+            [Define to 1 if the system's ftello function has the Solaris bug.])
+          ;;
+      esac
     fi
   fi
   if test $HAVE_FTELLO = 0 || test $REPLACE_FTELLO = 1; then
--- modules/ftello.orig Sat May  1 20:31:42 2010
+++ modules/ftello      Sat May  1 19:50:45 2010
@@ -3,6 +3,7 @@
 
 Files:
 lib/ftello.c
+lib/stdio-impl.h
 m4/ftello.m4
 
 Depends-on:




reply via email to

[Prev in Thread] Current Thread [Next in Thread]