Subtle bug in fflush and fix (long)

peterson at milano.UUCP peterson at milano.UUCP
Wed Jul 9 01:47:32 AEST 1986


The stdio package does not seem to properly account for some effects
of signals.  In particular, I have had instances of the following:

1. Output by one process into a pipe or socket, using the stdio
package (specifically putc), buffering output and eventually calling
_flsbuf to actually write the buffer to the pipe or socket.

2. Over time, the writing process gets ahead of the reading process,
causing the internal system block buffers for this pipe/socket to
fill.  Eventually the "write" system call in _flsbuf or fflush
blocks, waiting for the buffered data to be read so that more
can be written.

3.  The reading process was busy because of some interesting other
business and sends a signal to the writing process to let it know
of the interesting events it has had, and then goes back to reading.

4. The signal breaks the write system call (in _flsbuf or fflush)
and returns only a partial write ( 0 < write(...,n) < n) Presumably,
errno is EINTR.  The stdio routines interpret this condition as an
error and return EOF.  The buffer is set empty however, so that the
application program has no way of knowing how much was (and was not)
written.

Correcting this problem requires checking each write to see if some
bytes have been written, and as long as bytes are being written,
continuing to write more.  Eventually, either all bytes will be
written or an error will occur (write(...) <= 0).

The affected code is in flsbuf.c.  A revised version follows:

/* @(#)flsbuf.c	4.6 (Berkeley) 6/30/83 */

/* ************************************************************** */

/*
	There are four routines in this file:

	_flsbuf(c, FILE *)	an attempt was made to put the
				char c into an apparently full buffer

	_cleanup()		flush all buffers on exit

	fclose(FILE *)		flush and close the file

	fflush(FILE *)		flush the buffer
*/

/*
	In general, each FILE * has an I/O buffer allocated for it.
	Output charcters are put into the buffer until it is full.
	The buffer is flushed when 

	it is full and another character is to be added to 
		the buffer (_flsbuf -- called by putc),
	the program terminates (_cleanup), 
	the file is closed (fclose), or 
	it is explicitly emptied (fflush).

	The FILE * structure uses the following variables to keep
	track of the state of the buffer:

	_base		the base address of the buffer; if it is
			NULL, then no space has yet been allocated.

	_bufsiz		the amount of space in the buffer

	_ptr		pointer to the next empty space in the buffer

	_cnt		the number of empty spaces still in the buffer
			normally we would expect _cnt to be 
				_bufsiz - (_ptr - _base)
			but if the file is unbuffered or line buffered,
			_cnt is always set to zero.

	_flag		various flags: see the list in stdio.h
*/

/* ************************************************************** */

#include	<stdio.h>
#include	<sys/types.h>
#include	<sys/stat.h>

char   *malloc ();

_flsbuf(c, iop)
register    FILE * iop;
{

    register int    returncode = c;

    /* I/O to stdout should go through the stdout buffer */
    extern char _sobuf[];


 /* 
  check if the file can be either read or write (IORW) and,
  if so, change it to be in a write state (IOWRT). Also clear
  the read state (IOREAD) and any EOF flag (IOEOF)
  */
    if (iop->_flag & _IORW)
	{
	    iop->_flag |= _IOWRT;
	    iop->_flag &= ~(_IOEOF | _IOREAD);
	}

    /* if the file is not in a write state (IOWRT), exit */
    if ((iop->_flag & _IOWRT) == 0)
	return(EOF);

tryagain: 

    /*  check for line buffering -- flush on buffer full or newline */
    if (iop->_flag & _IOLBF)
	{
	    *iop->_ptr++ = c;
	    if (iop->_ptr >= iop->_base + iop->_bufsiz || c == '\n')
		{
		    if (WriteBuffer(iop) == EOF)
			return(EOF);
		}

	/* setting cnt to zero falsely indicates that there is no room
	   left in the buffer -- forces putc to call flsbuf on every
	   character */
	    iop->_cnt = 0;

	    return(c);
	}

    /*  check for no buffering -- flush immediately */
    if (iop->_flag & _IONBF)
	{
	/* if no buffering is wanted, output the character immediately 
	*/
	    char    c1;
	    int     n;

	    c1 = c;
	    n = write(fileno(iop), &c1, 1);
	    iop->_cnt = 0;

	/* since only one character is being written, it is either
	   written, or we have an error */
	    if (n == 0)
		return(EOF);
	    return(c);
	}

 /* 
  normal case: block buffering.  
  In general, we want to empty the buffer.
  The other possibility is that this is the very first
  call, and we need to allocate space for this file
  */

    if (iop->_base != NULL)
	{
	/* normal case -- empty the buffer */
	/* compute the number of bytes to be written */
	    if (iop->_ptr > iop->_base)
		{
		    returncode = WriteBuffer(iop);
		}
	}
    else
	{
	    /* no space has been allocated for the buffer for
		this file */

	    int     size;

	/* 
	 The _base field points at the buffer space; if it
	 is NULL, we need to allocate space.
	 
	 We want to have our buffer size match the size of
	 the blocks used by the operating system for this
	 file, if possible.  Check if the file exists and has
	 a block size. If not, use a default block size
	 */
	    {
		struct stat stbuf;
		if (fstat(fileno(iop), &stbuf) < 0 ||
			stbuf.st_blksize <= NULL)
		    size = BUFSIZ;
		else
		    size = stbuf.st_blksize;
	    }

	/* for standard output (stdout) to a tty, we should use line
	   buffering. Set the line buffering flag (LBF) and,
	   set the buffer pointers to _sobuf, and start over.
	*/
	    if (iop == stdout)
		{
		    if (isatty(fileno(stdout)))
			{
			    iop->_flag |= _IOLBF;
			    iop->_base = _sobuf;
			    iop->_ptr = _sobuf;
			    iop->_bufsiz = size;
			    goto tryagain;
			}
		}

	/* now malloc space for the i/o buffer -- if no space
	   available, try as unbuffered */

	    if ((iop->_base = malloc(size)) == NULL)
		{
		    iop->_flag |= _IONBF;
		    goto tryagain;
		}

	/* space is allocated; set MYBUF to indicate this, and set the
	   buffer size (bufsiz).  No write is needed yet -- just store
	   the character in the buffer this time and return */
	    iop->_flag |= _IOMYBUF;
	    iop->_bufsiz = size;
	    iop->_ptr = iop->_base;
	    iop->_cnt = iop->_bufsiz;
	    returncode = 0;
	}

 /* 
  now add the extra character that caused the buffer 
  to be flushed to the previously empty buffer.  
  
  -- _cnt is the number of available spaces 
  -- _ptr points to the next available buffer byte
  */

    *iop->_ptr++ = c;
    iop->_cnt -= 1;

    if (returncode == EOF)
	return(EOF);

 /* if no error, return the input character */
    return(c);
}


/* ************************************************************** */

fflush(iop)
register struct _iobuf *iop;
{

    if ((iop->_flag & (_IONBF | _IOWRT)) == _IOWRT
	    && iop->_base != NULL && iop->_ptr > iop->_base)
	return(WriteBuffer(iop));
    return(0);
}

/* ************************************************************** */

/* write a buffer; return EOF if there was any error */

/*
	This code handles the problem that if the
	write system call is interrupted, for example by a signal,
	the write would return without writing the entire buffer.
	In this case, we retry the write until it either fails
	or completes.
*/

static  WriteBuffer (iop)
register struct _iobuf *iop;
{
    register char  *base;
    register int    nBufferBytes;
    register int    nBytesWritten;

    base = iop->_base;
    nBufferBytes = iop->_ptr - base;


    /* write bytes as long as there are bytes to write, and bytes are
    being written */
    do
	{
	    nBytesWritten = write(fileno(iop), base, nBufferBytes);
	    base += nBytesWritten;
	    nBufferBytes -= nBytesWritten;
    } while (nBufferBytes > 0 && nBytesWritten > 0);

    /* reset buffer variables for an empty buffer */
    iop->_ptr = iop->_base;
    if (iop->_flag & (_IOLBF | _IONBF))
	iop->_cnt = 0;
    else
	iop->_cnt = iop->_bufsiz;

    /* check for possible error in the write */
    if (nBytesWritten == -1)
	{
	    iop->_flag |= _IOERR;
	    return(EOF);
	}
    return(0);
}

/* ************************************************************** */
/*
 * Flush buffers on exit
 */

_cleanup()
{
    register struct _iobuf *iop;
    extern struct _iobuf   *_lastbuf;

    for (iop = _iob; iop < _lastbuf; iop++)
	fclose(iop);
}

/* ************************************************************** */

fclose(iop)
register struct _iobuf *iop;
{
    register int    r;

    r = EOF;
    if (iop->_flag & (_IOREAD | _IOWRT | _IORW)
	    && (iop->_flag & _IOSTRG) == 0)
	{
	    /* flush the buffers and close the file */
	    r = fflush(iop);
	    if (close(fileno(iop)) < 0)
		r = EOF;
	    /* if memory was malloc'ed, free it */
	    if (iop->_flag & _IOMYBUF)
		free(iop->_base);
	}

 /* reinitialize the i/o buffer to its initial state */
    iop->_cnt = 0;
    iop->_base = (char *) NULL;
    iop->_ptr = (char *) NULL;
    iop->_bufsiz = 0;
    iop->_flag = 0;
    iop->_file = 0;

    return(r);
}
-- 
James Peterson
peterson at mcc.arpa  or  ...sally!im4u!milano!peterson



More information about the Comp.unix.wizards mailing list