diff mbox series

libio: Start to return errors when flushing fwrite's buffer

Message ID 20241008190203.2856619-1-tuliom@ascii.art.br
State New
Headers show
Series libio: Start to return errors when flushing fwrite's buffer | expand

Commit Message

Tulio Magno Quites Machado Filho Oct. 8, 2024, 7:02 p.m. UTC
From: Tulio Magno Quites Machado Filho <tuliom@redhat.com>

When an error happens, fwrite is expected to return a value that is less
than nmemb.  If this error happens while flushing its internal buffer,
fwrite is in a complex scenario: all the data might have been written to
the buffer, indicating a successful copy, but the buffer is expected to
be flushed and it was not.

POSIX.1-2024 states the following about errors on fwrite:

    If an error occurs, the resulting value of the file-position indicator
    for the stream is unspecified.

    The fwrite() function shall return the number of elements successfully
    written, which may be less than nitems if a write error is encountered.

With that in mind, this commit modifies fwrite's behavior in case of an
error while flushing the buffer in order to return 0 in case an
irrecoverable error is found.  Contents copied to the buffer are kept
there despite the return value because the error is irrecoverable.

In case of recoverable errors, fwrite continues to return success (aka.
nmemb), keeping the old behavior and allowing the caller to try again.

Add 2 tests:

1. tst-fwrite-bz29459: This test is based on the reproducer attached to
   bug 29459.  In order to work, it requires to pipe stdout to another
   process making it hard to reuse test-driver.c.  This code is more
   specific to the issue reported.
2. tst-fwrite-pipe: Recreates the issue by creating a pipe that is shared
   with a child process.  Reuses test-driver.c.  Evaluates a more generic
   scenario.
---
 libio/iofwrite.c                   |  23 +++--
 stdio-common/Makefile              |   7 ++
 stdio-common/tst-fwrite-bz29459.c  |  89 ++++++++++++++++++++
 stdio-common/tst-fwrite-bz29459.sh |  34 ++++++++
 stdio-common/tst-fwrite-pipe.c     | 130 +++++++++++++++++++++++++++++
 5 files changed, 278 insertions(+), 5 deletions(-)
 create mode 100644 stdio-common/tst-fwrite-bz29459.c
 create mode 100755 stdio-common/tst-fwrite-bz29459.sh
 create mode 100644 stdio-common/tst-fwrite-pipe.c

Comments

Florian Weimer Oct. 31, 2024, 11:57 a.m. UTC | #1
* Tulio Magno Quites Machado Filho:

> diff --git a/libio/iofwrite.c b/libio/iofwrite.c
> index af2e2070aff..8c0323947ae 100644
> --- a/libio/iofwrite.c
> +++ b/libio/iofwrite.c
> @@ -38,12 +38,25 @@ _IO_fwrite (const void *buf, size_t size, size_t count, FILE *fp)
>    if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)
>      written = _IO_sputn (fp, (const char *) buf, request);
>    _IO_release_lock (fp);
> -  /* We have written all of the input in case the return value indicates
> -     this or EOF is returned.  The latter is a special case where we
> -     simply did not manage to flush the buffer.  But the data is in the
> -     buffer and therefore written as far as fwrite is concerned.  */
> -  if (written == request || written == EOF)
> +  if (written == request)
> +    /* We have written all of the input successfully.  */
>      return count;
> +  else if (written == EOF)
> +    {
> +      /* sputn() has the same semantics as fputs(), returning EOF on error.
> +	 It also means we did not manage to flush the buffer, but the data is
> +	 in the buffer and therefore written, which is a conflicting
> +	 scenario.
> +
> +	 Confirm that an irrecoverable error happened and return an error,
> +	 i.e. return less than count.
> +	 Otherwise, return success (aka. count) and let the caller try
> +	 again, which is the behavior that fwrite had for years.  */
> +      if ((fp->_flags & _IO_ERR_SEEN) && errno != 0 && errno != EAGAIN)
> +	return 0;
> +      else
> +	return count;

Is it really appropriate to handle EAGAIN differently here?  I don't see
anything about that in POSIX.  In fact, some EAGAIN errors may not
actually be recoverable as required by POSIX.

I think the proper error handling behavior for flushes would be like
this:

* If no bytes can be written successfully, return 0.

* If some bytes could be written, but nothing from the fwrite request,
  adjust the buffer pointers to record the fact that those bytes have
  been written, and return 0.

* If some bytes from the fwrite request have been written, and that
  amount happens to fall on am item boundary, return the number of
  items that could be written.

* If an item could be written partially, we have a few subcases:

  - The unwritten tail fits into the buffer, and is not the last item.
    Preserve the unwritten tail in the buffer and return the number
    of written items, including the partially written item.

  - The unwritten tail does not fit into the buffer.  This is a
    catastrophic failure that cannot be represented properly with the
    fwrite interface.  Return the number of completely written items as
    a fallback.  In this case, we should consider if we should translate
    EAGAIN and EWOULDBLOCK to EMSGSIZE because EAGAIN is specified as
    “This is a temporary condition and later calls to the same routine
    may complete normally.”, but this is not true because of the buffer
    management failure and the loss of record boundary synchronization
    because normal completion just isn't in the picture anymore.

    A carefully written application will be able to determine this
    scenario by examining the file-position indicator, which will will
    reflect the partially-written 

  - The unwritten tail fits into the buffer, but it is the last item.
    This is another hard-to-represent corner case.  I think we should
    set the error indicator, but report success because all requested
    items have been written, but not flushed.  This case is not to
    dissimilar to the scenario where fwrite does not trigger a flush,
    but system is in a state that all future writes to the underlying
    descriptor will fail.

The idea is that it's possible for an application to clear the error
indicator, try again, and preserve item boundaries (if the item size is
not greater than the buffer size).  This is all a bit theoretical at
this point because I don't think the way libio is layered, it can
actually handle things in this way.  But we should be able to something
more useful than just returning 0 here.


> diff --git a/stdio-common/tst-fwrite-bz29459.c b/stdio-common/tst-fwrite-bz29459.c
> new file mode 100644
> index 00000000000..7ced6d419ca
> --- /dev/null
> +++ b/stdio-common/tst-fwrite-bz29459.c
> @@ -0,0 +1,89 @@

> +/* The goal of this test is to use fwrite () on a redirected and closed
> +   stdout.  A script will guarantee that stdout is redirected to another
> +   process that closes it during the execution.  The process reading from
> +   the pipe must read at least the first line in order to guarantee that
> +   flag _IO_CURRENTLY_PUTTING is set in the write end of the pipe, triggering
> +   important parts of the code that flushes lines from fwrite's internal
> +   buffer.  The underlying write () returns EPIPE, which fwrite () must
> +   propagate.  */

I think we should have a FUSE-based test, so that we can also check that
the file-position indicator is updated as required by the specification.
This requires a seekable file descriptor, which rules out using a pipe.

> +  for (i = 1; i <= ITERATIONS; i++)
> +    {
> +      /* Keep writing to stdout.  Success means that fwrite () returns an
> +	 error.  */

“Success” is ambiguous here.  I think it's the test objective to trigger
an fwrite failure?

Thanks,
Florian
Andreas Schwab Oct. 31, 2024, 12:55 p.m. UTC | #2
On Okt 31 2024, Florian Weimer wrote:

> * If some bytes from the fwrite request have been written, and that
>   amount happens to fall on am item boundary, return the number of
>   items that could be written.
>
> * If an item could be written partially, we have a few subcases:

I don't think fwrite should do anything special wrt. item boundaries.
Applications that care about partially written items should not use
fwrite with size > 1.
Florian Weimer Oct. 31, 2024, 1:02 p.m. UTC | #3
* Andreas Schwab:

> On Okt 31 2024, Florian Weimer wrote:
>
>> * If some bytes from the fwrite request have been written, and that
>>   amount happens to fall on am item boundary, return the number of
>>   items that could be written.
>>
>> * If an item could be written partially, we have a few subcases:
>
> I don't think fwrite should do anything special wrt. item boundaries.
> Applications that care about partially written items should not use
> fwrite with size > 1.

Considering the interface, I would say that there is an expectation that
fwrite maintains item boundaries.  Otherwise the caller could do the
multiplication.

But it's probably not a good use of our resources.  We can document that
the return value is only used to indicate the error status, and that the
concrete item count should not matter.  I think that's fine if the
application can use ftello to figure out how much was written.

Thanks,
Florian
Andreas Schwab Oct. 31, 2024, 1:50 p.m. UTC | #4
On Okt 31 2024, Florian Weimer wrote:

> Considering the interface, I would say that there is an expectation that
> fwrite maintains item boundaries.

That's not how fwrite works.  It is defined in terms of fputc, like all
other output functions.

> Otherwise the caller could do the multiplication.

Applications that care, do.

> I think that's fine if the application can use ftello to figure out
> how much was written.

The file position unspecified in case of errors.
Tulio Magno Quites Machado Filho Oct. 31, 2024, 3:06 p.m. UTC | #5
I failed to make it explicit in the commit message that this patch is
fixing bug 29459.

Florian Weimer <fweimer@redhat.com> writes:

> * Tulio Magno Quites Machado Filho:
>
>> diff --git a/libio/iofwrite.c b/libio/iofwrite.c
>> index af2e2070aff..8c0323947ae 100644
>> --- a/libio/iofwrite.c
>> +++ b/libio/iofwrite.c
>> @@ -38,12 +38,25 @@ _IO_fwrite (const void *buf, size_t size, size_t count, FILE *fp)
>>    if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)
>>      written = _IO_sputn (fp, (const char *) buf, request);
>>    _IO_release_lock (fp);
>> -  /* We have written all of the input in case the return value indicates
>> -     this or EOF is returned.  The latter is a special case where we
>> -     simply did not manage to flush the buffer.  But the data is in the
>> -     buffer and therefore written as far as fwrite is concerned.  */
>> -  if (written == request || written == EOF)
>> +  if (written == request)
>> +    /* We have written all of the input successfully.  */
>>      return count;
>> +  else if (written == EOF)
>> +    {
>> +      /* sputn() has the same semantics as fputs(), returning EOF on error.
>> +	 It also means we did not manage to flush the buffer, but the data is
>> +	 in the buffer and therefore written, which is a conflicting
>> +	 scenario.
>> +
>> +	 Confirm that an irrecoverable error happened and return an error,
>> +	 i.e. return less than count.
>> +	 Otherwise, return success (aka. count) and let the caller try
>> +	 again, which is the behavior that fwrite had for years.  */
>> +      if ((fp->_flags & _IO_ERR_SEEN) && errno != 0 && errno != EAGAIN)
>> +	return 0;
>> +      else
>> +	return count;
>
> Is it really appropriate to handle EAGAIN differently here?  I don't see
> anything about that in POSIX.  In fact, some EAGAIN errors may not
> actually be recoverable as required by POSIX.

IMHO, this is the most important question that I'd like to answer with
this fix.
I'm happy to treat all cases where sputn() returns EOF as errors.
But I realized that we might be able to keep the old behavior
(i.e. returning success) if a recoverable error is found.

I do agree that treating EAGAIN as "recoverable error" is not optimal.

With that said, I return this question to you and the community:
should we treat recoverable errors differently?

> The idea is that it's possible for an application to clear the error
> indicator, try again, and preserve item boundaries (if the item size is
> not greater than the buffer size).  This is all a bit theoretical at
> this point because I don't think the way libio is layered, it can
> actually handle things in this way.  But we should be able to something
> more useful than just returning 0 here.

I don't think we can do this now, but I agree that we should be able to
do better.  While I do agree with some of your suggestions here, I
believe they're orthogonal to this patch, which is intended to fix bug
29459.  I'd appreciate if the fix could be merged without requiring to
rewrite libio.

> I think we should have a FUSE-based test, so that we can also check that
> the file-position indicator is updated as required by the specification.
> This requires a seekable file descriptor, which rules out using a pipe.

I didn't understand your suggestion. Are you suggesting to replace
tst-fwrite-bz29459.c with a FUSE-based one?
Keep in mind this is the original reproducer from bug 29459.

>> +  for (i = 1; i <= ITERATIONS; i++)
>> +    {
>> +      /* Keep writing to stdout.  Success means that fwrite () returns an
>> +	 error.  */
>
> “Success” is ambiguous here.  I think it's the test objective to trigger
> an fwrite failure?

I tried to clarify it with that source code comment.
Would it help if I replaced the current comment with the following?
"The test succeeds if fwrite () returns an error."
Florian Weimer Oct. 31, 2024, 3:27 p.m. UTC | #6
* Andreas Schwab:

> On Okt 31 2024, Florian Weimer wrote:
>
>> Considering the interface, I would say that there is an expectation that
>> fwrite maintains item boundaries.
>
> That's not how fwrite works.  It is defined in terms of fputc, like all
> other output functions.
>
>> Otherwise the caller could do the multiplication.
>
> Applications that care, do.
>
>> I think that's fine if the application can use ftello to figure out
>> how much was written.
>
> The file position unspecified in case of errors.

That's a good point.  But I think POSIX does not make a distinction for
the unspecified file-position indicator whether the item size is 1 or
not.  If I read it correctly, it's always undefined on error.

Thanks,
Florian
Florian Weimer Oct. 31, 2024, 6:31 p.m. UTC | #7
* Tulio Magno Quites Machado Filho:

>> Is it really appropriate to handle EAGAIN differently here?  I don't see
>> anything about that in POSIX.  In fact, some EAGAIN errors may not
>> actually be recoverable as required by POSIX.
>
> IMHO, this is the most important question that I'd like to answer with
> this fix.
> I'm happy to treat all cases where sputn() returns EOF as errors.
> But I realized that we might be able to keep the old behavior
> (i.e. returning success) if a recoverable error is found.
>
> I do agree that treating EAGAIN as "recoverable error" is not optimal.
>
> With that said, I return this question to you and the community:
> should we treat recoverable errors differently?

I think Andreas leans towards no.

I would like to stress again that straightforward recovery needs the
stream to be in a knowable state regarding item boundaries.  So you need
to know how many items have been written, and somehow deal with the
partially written item case.  That's not possible to provide in general
depending on buffer size.  And merely ignoring an error doesn't address
it, either.

It's also questionable how worthwhile all this effort is in practice.
I'm not sure what the Linux error handling story is, even with O_SYNC.
It doesn't really matter what we do at the stdio layer if the kernel
cannot provide synchronous write error reporting.  Or any type of write
error causes the file system to be remounted read-only anyway.

>> The idea is that it's possible for an application to clear the error
>> indicator, try again, and preserve item boundaries (if the item size is
>> not greater than the buffer size).  This is all a bit theoretical at
>> this point because I don't think the way libio is layered, it can
>> actually handle things in this way.  But we should be able to something
>> more useful than just returning 0 here.
>
> I don't think we can do this now, but I agree that we should be able to
> do better.  While I do agree with some of your suggestions here, I
> believe they're orthogonal to this patch, which is intended to fix bug
> 29459.  I'd appreciate if the fix could be merged without requiring to
> rewrite libio.

I came up with this (untested) way to report a correct item count.

diff --git a/libio/bits/types/struct_FILE.h b/libio/bits/types/struct_FILE.h
index d8d26639d1..290f308584 100644
--- a/libio/bits/types/struct_FILE.h
+++ b/libio/bits/types/struct_FILE.h
@@ -94,8 +94,10 @@ struct _IO_FILE_complete
   void *_freeres_buf;
   struct _IO_FILE **_prevchain;
   int _mode;
+  int _unused3;
+  __uint64_t _total_written;
   /* Make sure we don't get into trouble again.  */
-  char _unused2[15 * sizeof (int) - 5 * sizeof (void *)];
+  char _unused2[12 * sizeof (int) - 5 * sizeof (void *)];
 };
 
 /* These macros are used by bits/stdio.h and internal headers.  */
diff --git a/libio/fileops.c b/libio/fileops.c
index 4db4a76f75..8f5b56e3d2 100644
--- a/libio/fileops.c
+++ b/libio/fileops.c
@@ -1184,6 +1184,7 @@ _IO_new_file_write (FILE *f, const void *data, ssize_t n)
 	  f->_flags |= _IO_ERR_SEEN;
 	  break;
 	}
+      f->_total_written += count;
       to_do -= count;
       data = (void *) ((char *) data + count);
     }
diff --git a/libio/iofwrite.c b/libio/iofwrite.c
index af2e2070af..7aaa9e43c1 100644
--- a/libio/iofwrite.c
+++ b/libio/iofwrite.c
@@ -36,13 +36,31 @@ _IO_fwrite (const void *buf, size_t size, size_t count, FILE *fp)
     return 0;
   _IO_acquire_lock (fp);
   if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)
-    written = _IO_sputn (fp, (const char *) buf, request);
+    {
+      /* Compute actually written bytes plus pending buffer
+         contents.  */
+      uint64_t original_total_written
+        = fp->_total_written + (fp->_IO_write_ptr - fp->_IO_write_base);
+      written = _IO_sputn (fp, (const char *) buf, request);
+      if (written == EOF)
+        {
+          if (fp->_total_written > original_total_written)
+            {
+              written = fp->_total_written - original_total_written;
+              /* If everything was reported as written and somehow an
+                 error occurred afterwards, avoid reporting success.  */
+              if (written == request)
+                --written;
+            }
+          else
+            /* Only already-pending buffer contents was written.  */
+            written = 0;
+        }
+    }
   _IO_release_lock (fp);
   /* We have written all of the input in case the return value indicates
-     this or EOF is returned.  The latter is a special case where we
-     simply did not manage to flush the buffer.  But the data is in the
-     buffer and therefore written as far as fwrite is concerned.  */
-  if (written == request || written == EOF)
+     this.  */
+  if (written == request)
     return count;
   else
     return written / size;


Not sure if that's acceptable, it's kind of a hack.  It should play
relatively well with vtable interposition stuff.  It may report 0
instead of the actually written item count with a SYSWRITE override
that does not update _total_written.

Threading the byte count through all the existing interfaces is indeed
rather hard.

>> I think we should have a FUSE-based test, so that we can also check that
>> the file-position indicator is updated as required by the specification.
>> This requires a seekable file descriptor, which rules out using a pipe.
>
> I didn't understand your suggestion. Are you suggesting to replace
> tst-fwrite-bz29459.c with a FUSE-based one?
> Keep in mind this is the original reproducer from bug 29459.

We can keep this test, sure.  I think we need to verify that we actually
implement the POSIX rule that the number of successfully written items
is returned.

>>> +  for (i = 1; i <= ITERATIONS; i++)
>>> +    {
>>> +      /* Keep writing to stdout.  Success means that fwrite () returns an
>>> +	 error.  */
>>
>> “Success” is ambiguous here.  I think it's the test objective to trigger
>> an fwrite failure?
>
> I tried to clarify it with that source code comment.
> Would it help if I replaced the current comment with the following?
> "The test succeeds if fwrite () returns an error."

Yes, please.

Thanks,
Florian
diff mbox series

Patch

diff --git a/libio/iofwrite.c b/libio/iofwrite.c
index af2e2070aff..8c0323947ae 100644
--- a/libio/iofwrite.c
+++ b/libio/iofwrite.c
@@ -38,12 +38,25 @@  _IO_fwrite (const void *buf, size_t size, size_t count, FILE *fp)
   if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)
     written = _IO_sputn (fp, (const char *) buf, request);
   _IO_release_lock (fp);
-  /* We have written all of the input in case the return value indicates
-     this or EOF is returned.  The latter is a special case where we
-     simply did not manage to flush the buffer.  But the data is in the
-     buffer and therefore written as far as fwrite is concerned.  */
-  if (written == request || written == EOF)
+  if (written == request)
+    /* We have written all of the input successfully.  */
     return count;
+  else if (written == EOF)
+    {
+      /* sputn() has the same semantics as fputs(), returning EOF on error.
+	 It also means we did not manage to flush the buffer, but the data is
+	 in the buffer and therefore written, which is a conflicting
+	 scenario.
+
+	 Confirm that an irrecoverable error happened and return an error,
+	 i.e. return less than count.
+	 Otherwise, return success (aka. count) and let the caller try
+	 again, which is the behavior that fwrite had for years.  */
+      if ((fp->_flags & _IO_ERR_SEEN) && errno != 0 && errno != EAGAIN)
+	return 0;
+      else
+	return count;
+    }
   else
     return written / size;
 }
diff --git a/stdio-common/Makefile b/stdio-common/Makefile
index 88105b3c1b3..bb5a8b0462d 100644
--- a/stdio-common/Makefile
+++ b/stdio-common/Makefile
@@ -234,6 +234,7 @@  tests := \
   tst-fwrite \
   tst-fwrite-memstrm \
   tst-fwrite-overflow \
+  tst-fwrite-pipe \
   tst-fwrite-ro \
   tst-getline \
   tst-getline-enomem \
@@ -316,6 +317,7 @@  tests-internal = \
   # tests-internal
 
 test-srcs = \
+  tst-fwrite-bz29459 \
   tst-printf \
   tst-printfsz-islongdouble \
   tst-unbputc \
@@ -323,6 +325,7 @@  test-srcs = \
 
 ifeq ($(run-built-tests),yes)
 tests-special += \
+  $(objpfx)tst-fwrite-bz29459.out \
   $(objpfx)tst-printf.out \
   $(objpfx)tst-printfsz-islongdouble.out \
   $(objpfx)tst-setvbuf1-cmp.out \
@@ -517,6 +520,10 @@  tst-freopen64-6-ENV = \
   MALLOC_TRACE=$(objpfx)tst-freopen64-6.mtrace \
   LD_PRELOAD=$(common-objpfx)malloc/libc_malloc_debug.so
 
+$(objpfx)tst-fwrite-bz29459.out: tst-fwrite-bz29459.sh $(objpfx)tst-fwrite-bz29459
+	$(SHELL) $< $(common-objpfx) '$(test-program-prefix)'; \
+	$(evaluate-test)
+
 $(objpfx)tst-unbputc.out: tst-unbputc.sh $(objpfx)tst-unbputc
 	$(SHELL) $< $(common-objpfx) '$(test-program-prefix)'; \
 	$(evaluate-test)
diff --git a/stdio-common/tst-fwrite-bz29459.c b/stdio-common/tst-fwrite-bz29459.c
new file mode 100644
index 00000000000..7ced6d419ca
--- /dev/null
+++ b/stdio-common/tst-fwrite-bz29459.c
@@ -0,0 +1,89 @@ 
+/* Test fwrite against bug 29459.
+   Copyright (C) 2024 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+/* This test is based on the code attached to bug 29459.
+   It depends on stdout being redirected to a specific process via a script
+   with the same name.  Because of this, we cannot use the features from
+   test_driver.c.  */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <unistd.h>
+#include <support/check.h>
+#include <support/xsignal.h>
+
+/* Usually this test reproduces in a few iterations.  However, keep a high
+   number of iterations in order to avoid return false-positives due to an
+   overwhelmed/slow system.  */
+#define ITERATIONS 5000
+
+/* The goal of this test is to use fwrite () on a redirected and closed
+   stdout.  A script will guarantee that stdout is redirected to another
+   process that closes it during the execution.  The process reading from
+   the pipe must read at least the first line in order to guarantee that
+   flag _IO_CURRENTLY_PUTTING is set in the write end of the pipe, triggering
+   important parts of the code that flushes lines from fwrite's internal
+   buffer.  The underlying write () returns EPIPE, which fwrite () must
+   propagate.  */
+
+int
+main (void)
+{
+  int i;
+  size_t rc;
+  /* Ensure the string we send has a new line because we're dealing
+     with a lined-buffered stream.  */
+  const char *s = "hello\n";
+  const size_t len = strlen(s);
+
+  /* Ensure that fwrite buffers the output before writing to stdout.  */
+  setlinebuf(stdout);
+  /* Ignore SIGPIPE in order to catch the EPIPE returned by the
+     underlying call to write().  */
+  xsignal(SIGPIPE, SIG_IGN);
+
+  for (i = 1; i <= ITERATIONS; i++)
+    {
+      /* Keep writing to stdout.  Success means that fwrite () returns an
+	 error.  */
+      if ((rc = fwrite(s, 1, len, stdout)) < len)
+	{
+	  /* An error happened.  Check if ferror () does return an error
+	     and that it is indeed EPIPE.  */
+	  TEST_COMPARE (ferror (stdout), 1);
+	  TEST_COMPARE (errno, EPIPE);
+	  fprintf(stderr, "Success: i=%d. fwrite returned %zu < %zu \
+and errno=EPIPE\n",
+		  i, rc, len);
+	  /* The test succeeded!  */
+	  return 0;
+	}
+      else
+	{
+	  /* fwrite () was able to write all the contents.  Check if no errors
+	     have been reported and try again.  */
+	  TEST_COMPARE (ferror (stdout), 0);
+	  TEST_COMPARE (errno, 0);
+	}
+    }
+
+  fprintf(stderr, "Error: fwrite did not return an error\n");
+  return 1;
+}
diff --git a/stdio-common/tst-fwrite-bz29459.sh b/stdio-common/tst-fwrite-bz29459.sh
new file mode 100755
index 00000000000..c63e94bd6bf
--- /dev/null
+++ b/stdio-common/tst-fwrite-bz29459.sh
@@ -0,0 +1,34 @@ 
+#!/bin/sh
+# Test fwrite for bug 29459.
+# Copyright (C) 2024 Free Software Foundation, Inc.
+# This file is part of the GNU C Library.
+
+# The GNU C Library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+
+# The GNU C Library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+
+# You should have received a copy of the GNU Lesser General Public
+# License along with the GNU C Library; if not, see
+# <https://www.gnu.org/licenses/>.
+
+set -e
+
+common_objpfx=$1; shift
+test_program_prefix=$1; shift
+
+status=0
+
+${test_program_prefix} \
+  ${common_objpfx}stdio-common/tst-fwrite-bz29459 \
+    2> ${common_objpfx}stdio-common/tst-fwrite-bz29459.out \
+    | head -n1 > /dev/null
+
+grep -q Success ${common_objpfx}stdio-common/tst-fwrite-bz29459.out || status=1
+
+exit $status
diff --git a/stdio-common/tst-fwrite-pipe.c b/stdio-common/tst-fwrite-pipe.c
new file mode 100644
index 00000000000..7f88e962655
--- /dev/null
+++ b/stdio-common/tst-fwrite-pipe.c
@@ -0,0 +1,130 @@ 
+/* Test if fwrite returns EPIPE.
+   Copyright (C) 2024 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <support/check.h>
+#include <support/xstdio.h>
+#include <support/xsignal.h>
+#include <support/xunistd.h>
+
+/* Usually this test reproduces in a few iterations.  However, keep a high
+   number of iterations in order to avoid return false-positives due to an
+   overwhelmed/slow system.  */
+#define ITERATIONS 5000
+
+#define BUFFERSIZE 20
+
+/* When the underlying write () fails with EPIPE, fwrite () is expected to
+   return an error by returning < size*nmemb and keeping errno=EPIPE.  */
+
+static int
+do_test (void)
+{
+  int fd[2];
+  pid_t p;
+  FILE *f;
+  size_t written;
+  int ret = 1;  /* Return failure by default.  */
+
+  /* Try to create a pipe.  */
+  xpipe (fd);
+
+  p = xfork ();
+  if (p == 0)
+    {
+      char b[BUFFERSIZE];
+      size_t bytes;
+
+      /* Read at least the first line from the pipe before closing it.
+	 This is important because it guarantees the file stream will have
+	 flag _IO_CURRENTLY_PUTTING set, which triggers important parts of
+	 the code that flushes lines from fwrite's internal buffer.  */
+      do {
+	bytes = read (fd[0], b, BUFFERSIZE);
+      } while(memrchr (b, '\n', bytes) == NULL);
+
+      /* Child closes both ends of the pipe in order to trigger an EPIPE
+	 error on the parent.  */
+      xclose (fd[0]);
+      xclose (fd[1]);
+
+      return 0;
+    }
+  else
+    {
+      /* Ensure the string we send has a new line because we're dealing
+         with a lined-buffered stream.  */
+      const char *s = "hello\n";
+      size_t len = strlen (s);
+      int i;
+
+      /* Parent only writes to pipe.
+	 Close the unused read end of the pipe.  */
+      xclose (fd[0]);
+
+      /* Ignore SIGPIPE in order to catch the EPIPE returned by the
+	 underlying call to write().  */
+      xsignal(SIGPIPE, SIG_IGN);
+
+      /* Create a file stream associated with the write end of the pipe.  */
+      f = fdopen (fd[1], "w");
+      TEST_VERIFY_EXIT (f != NULL);
+      /* Ensure that fwrite buffers the output before writing to the pipe.  */
+      setlinebuf (f);
+
+      /* Ensure errno is not set before starting.  */
+      TEST_VERIFY_EXIT (errno == 0);
+      for (i = 1; i <= ITERATIONS; i++)
+	{
+	  /* Try to write to the pipe.  The first calls are expected to
+	     suceeded until the child process closes the read end.
+	     After that, fwrite () is expected to fail and errno should be
+	     set to EPIPE.  */
+	  written = fwrite (s, 1, len, f);
+
+	  if (written == len)
+	    {
+	      TEST_VERIFY_EXIT (ferror (f) == 0);
+	      TEST_VERIFY_EXIT (errno == 0);
+	    }
+	  else
+	    {
+	      /* An error happened.  Check if ferror () does return an error
+		 and that it is indeed EPIPE.  */
+	      TEST_COMPARE (ferror (f), 1);
+	      TEST_COMPARE (errno, EPIPE);
+	      /* The test succeeded!  Clear the error from the file stream and
+		 return success.  */
+	      clearerr (f);
+	      ret = 0;
+              break;
+	    }
+	}
+
+      xfclose (f);
+    }
+
+  if (ret)
+    FAIL_RET ("fwrite should have returned an error, but it didn't.\n");
+
+  return ret;
+}
+
+#include <support/test-driver.c>