diff --git a/libio/fileops.c b/libio/fileops.c
index 42e695265d..e868d520b6 100644
--- a/libio/fileops.c
+++ b/libio/fileops.c
@@ -128,15 +128,48 @@ _IO_new_file_init (struct _IO_FILE_plus *fp)
int
_IO_new_file_close_it (FILE *fp)
{
- int write_status;
+ int flush_status = 0;
if (!_IO_file_is_open (fp))
return EOF;
if ((fp->_flags & _IO_NO_WRITES) == 0
&& (fp->_flags & _IO_CURRENTLY_PUTTING) != 0)
- write_status = _IO_do_flush (fp);
- else
- write_status = 0;
+ flush_status = _IO_do_flush (fp);
+ else if (fp->_fileno >= 0
+ /* If this is the active handle, we must seek the
+ underlying open file description (possibly shared with
+ other file descriptors that remain open) to the correct
+ offset. But if this stream is in a state such that some
+ other handle might have become the active handle, then
+ (a) at the time it entered that state, the underlying
+ open file description had the correct offset, and (b)
+ seeking the underlying open file description, even to
+ its newly determined current offset, is not safe because
+ it can race with operations on a different active
+ handle. So check here for cases where it is necessary
+ to seek, while avoiding seeking in cases where it is
+ unsafe to do so. */
+ && (_IO_in_backup (fp)
+ || (fp->_mode <= 0 && fp->_IO_read_ptr < fp->_IO_read_end)
+ || (_IO_vtable_offset (fp) == 0
+ && fp->_mode > 0 && (fp->_wide_data->_IO_read_ptr
+ < fp->_wide_data->_IO_read_end))))
+ {
+ off64_t o = _IO_SEEKOFF (fp, 0, _IO_seek_cur, 0);
+ if (o == EOF)
+ {
+ if (errno != ESPIPE)
+ flush_status = EOF;
+ }
+ else
+ {
+ if (_IO_in_backup (fp))
+ o -= fp->_IO_save_end - fp->_IO_save_base;
+ flush_status = (_IO_SYSSEEK (fp, o, SEEK_SET) < 0 && errno != ESPIPE
+ ? EOF
+ : 0);
+ }
+ }
_IO_unsave_markers (fp);
@@ -161,7 +194,7 @@ _IO_new_file_close_it (FILE *fp)
fp->_fileno = -1;
fp->_offset = _IO_pos_BAD;
- return close_status ? close_status : write_status;
+ return close_status ? close_status : flush_status;
}
libc_hidden_ver (_IO_new_file_close_it, _IO_file_close_it)
diff --git a/stdio-common/Makefile b/stdio-common/Makefile
index 4a3810cdf2..f6bdc32489 100644
--- a/stdio-common/Makefile
+++ b/stdio-common/Makefile
@@ -234,6 +234,7 @@ tests := \
tst-bz11319-fortify2 \
tst-cookie \
tst-dprintf-length \
+ tst-fclose-offset \
tst-fdopen \
tst-fdopen2 \
tst-ferror \
diff --git a/stdio-common/tst-fclose-offset.c b/stdio-common/tst-fclose-offset.c
new file mode 100644
index 0000000000..a31de1117c
--- /dev/null
+++ b/stdio-common/tst-fclose-offset.c
@@ -0,0 +1,225 @@
+/* Test offset of input file descriptor after close (bug 12724).
+ 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
+
+int
+do_test (void)
+{
+ char *filename = NULL;
+ int fd = create_temp_file ("tst-fclose-offset", &filename);
+ TEST_VERIFY_EXIT (fd != -1);
+
+ /* Test offset of open file description for output and input streams
+ after fclose, case from bug 12724. */
+
+ const char buf[] = "hello world";
+ xwrite (fd, buf, sizeof buf);
+ TEST_COMPARE (lseek (fd, 1, SEEK_SET), 1);
+ int fd2 = xdup (fd);
+ FILE *f = fdopen (fd2, "w");
+ TEST_VERIFY_EXIT (f != NULL);
+ TEST_COMPARE (fputc (buf[1], f), buf[1]);
+ xfclose (f);
+ errno = 0;
+ TEST_COMPARE (lseek (fd2, 0, SEEK_CUR), -1);
+ TEST_COMPARE (errno, EBADF);
+ TEST_COMPARE (lseek (fd, 0, SEEK_CUR), 2);
+
+ /* Likewise for an input stream. */
+ fd2 = xdup (fd);
+ f = fdopen (fd2, "r");
+ TEST_VERIFY_EXIT (f != NULL);
+ TEST_COMPARE (fgetc (f), buf[2]);
+ xfclose (f);
+ errno = 0;
+ TEST_COMPARE (lseek (fd2, 0, SEEK_CUR), -1);
+ TEST_COMPARE (errno, EBADF);
+ TEST_COMPARE (lseek (fd, 0, SEEK_CUR), 3);
+
+ /* Test offset of open file description for output and input streams
+ after fclose, case from comment on bug 12724 (failed after first
+ attempt at fixing that bug). This verifies that the offset is
+ not reset when there has been no input or output on the FILE* (in
+ that case, the FILE* might not be the active handle). */
+
+ TEST_COMPARE (lseek (fd, 0, SEEK_SET), 0);
+ xwrite (fd, buf, sizeof buf);
+ TEST_COMPARE (lseek (fd, 1, SEEK_SET), 1);
+ fd2 = xdup (fd);
+ f = fdopen (fd2, "w");
+ TEST_VERIFY_EXIT (f != NULL);
+ TEST_COMPARE (lseek (fd, 4, SEEK_SET), 4);
+ xfclose (f);
+ errno = 0;
+ TEST_COMPARE (lseek (fd2, 0, SEEK_CUR), -1);
+ TEST_COMPARE (errno, EBADF);
+ TEST_COMPARE (lseek (fd, 0, SEEK_CUR), 4);
+
+ /* Likewise for an input stream. */
+ fd2 = xdup (fd);
+ f = fdopen (fd2, "r");
+ TEST_VERIFY_EXIT (f != NULL);
+ TEST_COMPARE (lseek (fd, 4, SEEK_SET), 4);
+ xfclose (f);
+ errno = 0;
+ TEST_COMPARE (lseek (fd2, 0, SEEK_CUR), -1);
+ TEST_COMPARE (errno, EBADF);
+ TEST_COMPARE (lseek (fd, 0, SEEK_CUR), 4);
+
+ /* Further cases without specific tests in bug 12724, to verify
+ proper operation of the rules about the offset only being set
+ when the stream is the active handle. */
+
+ /* Test offset set by fclose after fseek and fgetc. */
+ TEST_COMPARE (lseek (fd, 0, SEEK_SET), 0);
+ fd2 = xdup (fd);
+ f = fdopen (fd2, "r");
+ TEST_VERIFY_EXIT (f != NULL);
+ TEST_COMPARE (fseek (f, 1, SEEK_SET), 0);
+ TEST_COMPARE (fgetc (f), buf[1]);
+ xfclose (f);
+ errno = 0;
+ TEST_COMPARE (lseek (fd2, 0, SEEK_CUR), -1);
+ TEST_COMPARE (errno, EBADF);
+ TEST_COMPARE (lseek (fd, 0, SEEK_CUR), 2);
+
+ /* Test offset not set by fclose after fseek and fgetc, if that
+ fgetc is at EOF (in which case the active handle might have
+ changed). */
+ TEST_COMPARE (lseek (fd, 0, SEEK_SET), 0);
+ fd2 = xdup (fd);
+ f = fdopen (fd2, "r");
+ TEST_VERIFY_EXIT (f != NULL);
+ TEST_COMPARE (fseek (f, sizeof buf, SEEK_SET), 0);
+ TEST_COMPARE (fgetc (f), EOF);
+ TEST_COMPARE (lseek (fd, 4, SEEK_SET), 4);
+ xfclose (f);
+ errno = 0;
+ TEST_COMPARE (lseek (fd2, 0, SEEK_CUR), -1);
+ TEST_COMPARE (errno, EBADF);
+ TEST_COMPARE (lseek (fd, 0, SEEK_CUR), 4);
+
+ /* Test offset not set by fclose after fseek and fgetc and fflush
+ (active handle might have changed after fflush). */
+ TEST_COMPARE (lseek (fd, 0, SEEK_SET), 0);
+ fd2 = xdup (fd);
+ f = fdopen (fd2, "r");
+ TEST_VERIFY_EXIT (f != NULL);
+ TEST_COMPARE (fseek (f, 1, SEEK_SET), 0);
+ TEST_COMPARE (fgetc (f), buf[1]);
+ TEST_COMPARE (fflush (f), 0);
+ TEST_COMPARE (lseek (fd, 4, SEEK_SET), 4);
+ xfclose (f);
+ errno = 0;
+ TEST_COMPARE (lseek (fd2, 0, SEEK_CUR), -1);
+ TEST_COMPARE (errno, EBADF);
+ TEST_COMPARE (lseek (fd, 0, SEEK_CUR), 4);
+
+ /* Test offset not set by fclose after fseek and fgetc, if the
+ stream is unbuffered (active handle might change at any
+ time). */
+ TEST_COMPARE (lseek (fd, 0, SEEK_SET), 0);
+ fd2 = xdup (fd);
+ f = fdopen (fd2, "r");
+ TEST_VERIFY_EXIT (f != NULL);
+ setbuf (f, NULL);
+ TEST_COMPARE (fseek (f, 1, SEEK_SET), 0);
+ TEST_COMPARE (fgetc (f), buf[1]);
+ TEST_COMPARE (lseek (fd, 4, SEEK_SET), 4);
+ xfclose (f);
+ errno = 0;
+ TEST_COMPARE (lseek (fd2, 0, SEEK_CUR), -1);
+ TEST_COMPARE (errno, EBADF);
+ TEST_COMPARE (lseek (fd, 0, SEEK_CUR), 4);
+
+ /* Also test such cases with the stream in wide mode. */
+
+ /* Test offset set by fclose after fseek and fgetwc. */
+ TEST_COMPARE (lseek (fd, 0, SEEK_SET), 0);
+ fd2 = xdup (fd);
+ f = fdopen (fd2, "r");
+ TEST_VERIFY_EXIT (f != NULL);
+ TEST_COMPARE (fseek (f, 1, SEEK_SET), 0);
+ TEST_COMPARE (fgetwc (f), (wint_t) buf[1]);
+ xfclose (f);
+ errno = 0;
+ TEST_COMPARE (lseek (fd2, 0, SEEK_CUR), -1);
+ TEST_COMPARE (errno, EBADF);
+ TEST_COMPARE (lseek (fd, 0, SEEK_CUR), 2);
+
+ /* Test offset not set by fclose after fseek and fgetwc, if that
+ fgetwc is at EOF (in which case the active handle might have
+ changed). */
+ TEST_COMPARE (lseek (fd, 0, SEEK_SET), 0);
+ fd2 = xdup (fd);
+ f = fdopen (fd2, "r");
+ TEST_VERIFY_EXIT (f != NULL);
+ TEST_COMPARE (fseek (f, sizeof buf, SEEK_SET), 0);
+ TEST_COMPARE (fgetwc (f), WEOF);
+ TEST_COMPARE (lseek (fd, 4, SEEK_SET), 4);
+ xfclose (f);
+ errno = 0;
+ TEST_COMPARE (lseek (fd2, 0, SEEK_CUR), -1);
+ TEST_COMPARE (errno, EBADF);
+ TEST_COMPARE (lseek (fd, 0, SEEK_CUR), 4);
+
+ /* Test offset not set by fclose after fseek and fgetwc and fflush
+ (active handle might have changed after fflush). */
+ TEST_COMPARE (lseek (fd, 0, SEEK_SET), 0);
+ fd2 = xdup (fd);
+ f = fdopen (fd2, "r");
+ TEST_VERIFY_EXIT (f != NULL);
+ TEST_COMPARE (fseek (f, 1, SEEK_SET), 0);
+ TEST_COMPARE (fgetwc (f), (wint_t) buf[1]);
+ TEST_COMPARE (fflush (f), 0);
+ TEST_COMPARE (lseek (fd, 4, SEEK_SET), 4);
+ xfclose (f);
+ errno = 0;
+ TEST_COMPARE (lseek (fd2, 0, SEEK_CUR), -1);
+ TEST_COMPARE (errno, EBADF);
+ TEST_COMPARE (lseek (fd, 0, SEEK_CUR), 4);
+
+ /* Test offset not set by fclose after fseek and fgetwc, if the
+ stream is unbuffered (active handle might change at any
+ time). */
+ TEST_COMPARE (lseek (fd, 0, SEEK_SET), 0);
+ fd2 = xdup (fd);
+ f = fdopen (fd2, "r");
+ TEST_VERIFY_EXIT (f != NULL);
+ setbuf (f, NULL);
+ TEST_COMPARE (fseek (f, 1, SEEK_SET), 0);
+ TEST_COMPARE (fgetwc (f), (wint_t) buf[1]);
+ TEST_COMPARE (lseek (fd, 4, SEEK_SET), 4);
+ xfclose (f);
+ errno = 0;
+ TEST_COMPARE (lseek (fd2, 0, SEEK_CUR), -1);
+ TEST_COMPARE (errno, EBADF);
+ TEST_COMPARE (lseek (fd, 0, SEEK_CUR), 4);
+
+ return 0;
+}
+
+#include