diff --git a/libio/bits/types/struct_FILE.h b/libio/bits/types/struct_FILE.h
index 0e8ac64e36..2012d70681 100644
--- a/libio/bits/types/struct_FILE.h
+++ b/libio/bits/types/struct_FILE.h
@@ -97,8 +97,15 @@ struct _IO_FILE_complete
void *_freeres_buf;
struct _IO_FILE **_prevchain;
int _mode;
+#ifdef __LP64__
+ int _unused3;
+#endif
+ __uint64_t _total_written;
+#ifndef __LP64__
+ int _unused3;
+#endif
/* 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 775999deb3..12b440b09d 100644
--- a/libio/fileops.c
+++ b/libio/fileops.c
@@ -113,6 +113,7 @@ _IO_new_file_init_internal (struct _IO_FILE_plus *fp)
_IO_link_in (fp);
fp->file._fileno = -1;
+ fp->file._total_written = 0;
}
/* External version of _IO_new_file_init_internal which switches off
@@ -1185,6 +1186,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 f94493c6ba..7897c4afaf 100644
--- a/libio/iofwrite.c
+++ b/libio/iofwrite.c
@@ -36,13 +36,42 @@ _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)
+ {
+ /* An error happened and we need to find the appropriate return
+ value. There 3 possible scenarios:
+ 1. If the number of bytes written is between 0..[buffer content],
+ we need to return 0 because none of the bytes from this
+ request have been written;
+ 2. If the number of bytes written is between
+ [buffer content]+1..request-1, that means we managed to write
+ data requested in this fwrite call;
+ 3. We might have written all the requested data and got an error
+ anyway. We can't return success, which means we still have to
+ return less than request. */
+ 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;
diff --git a/stdio-common/Makefile b/stdio-common/Makefile
index 8a29980375..0b9f85e129 100644
--- a/stdio-common/Makefile
+++ b/stdio-common/Makefile
@@ -263,6 +263,7 @@ tests := \
tst-fwrite \
tst-fwrite-memstrm \
tst-fwrite-overflow \
+ tst-fwrite-pipe \
tst-fwrite-ro \
tst-getline \
tst-getline-enomem \
@@ -358,6 +359,7 @@ tests-internal = \
test-srcs = \
$(xprintf-srcs) \
+ tst-fwrite-bz29459 \
tst-printf \
tst-printfsz-islongdouble \
tst-unbputc \
@@ -366,6 +368,7 @@ test-srcs = \
ifeq ($(run-built-tests),yes)
tests-special += \
$(foreach f,$(xprintf-stems),$(objpfx)$(f).out) \
+ $(objpfx)tst-fwrite-bz29459.out \
$(objpfx)tst-printf.out \
$(objpfx)tst-printfsz-islongdouble.out \
$(objpfx)tst-setvbuf1-cmp.out \
@@ -563,6 +566,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 0000000000..0640faac0c
--- /dev/null
+++ b/stdio-common/tst-fwrite-bz29459.c
@@ -0,0 +1,89 @@
+/* Test fwrite against bug 29459.
+ Copyright (C) 2025 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
+ . */
+
+/* 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
+#include
+#include
+#include
+#include
+#include
+#include
+
+/* 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. The test succeeds if 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 0000000000..164313532b
--- /dev/null
+++ b/stdio-common/tst-fwrite-bz29459.sh
@@ -0,0 +1,34 @@
+#!/bin/sh
+# Test fwrite for bug 29459.
+# Copyright (C) 2025 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
+# .
+
+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 0000000000..a6119125b2
--- /dev/null
+++ b/stdio-common/tst-fwrite-pipe.c
@@ -0,0 +1,130 @@
+/* Test if fwrite returns EPIPE.
+ Copyright (C) 2025 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
+ . */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+/* 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 < 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(bytes > 0 && 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. */
+ 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