Re: [RFC v1] man/man2/close.2: CAVEATS: Document divergence from POSIX.1-2024
From: Rich Felker <dalias@libc.org>
Date: 2025-05-24 02:24:23
Also in:
linux-fsdevel
On Fri, May 23, 2025 at 02:10:57PM -0400, Zack Weinberg wrote:
Taking everything said in this thread into account, I have attempted to
wordsmith new language for the close(2) manpage. Please let me know
what you think, and please help me with the bits marked in square
brackets. I can make this into a proper patch for the manpages
when everyone is happy with it.
zw
---
DESCRIPTION
... existing text ...
close() always succeeds. That is, after it returns, _fd_ has
always been disconnected from the open file it formerly referred
to, and its number can be recycled to refer to some other file.
Furthermore, if _fd_ was the last reference to the underlying
open file description, the resources associated with the open file
description will always have been scheduled to be released.
However, close may report _delayed errors_ from a previous I/O
operation. Therefore, its return value should not be ignored.
RETURN VALUE
close() returns zero if there are no delayed errors to report,
or -1 if there _might_ be delayed errors.
When close() returns -1, check _errno_ to see what the situation
actually is. Most, but not all, _errno_ codes indicate a delayed
I/O error that should be reported to the user. See ERRORS and
NOTES for more detail.
[QUERY: Is it ever possible to get delayed errors on close() from
a file that was opened with O_RDONLY? What about a file that was
opened with O_RDWR but never actually written to? If people only
have to worry about delayed errors if the file was actually
written to, we should say so at this point.
It would also be good to mention whether it is possible to get a
delayed error on close() even if a previous call to fsync() or
fdatasync() succeeded and there haven’t been any more writes to
that file *description* (not necessarily via the fd being closed)
since.]
ERRORS
EBADF _fd_ wasn’t open in the first place, or is outside the
valid numeric range for file descriptors.
EINPROGRESS
EINTR
There are no delayed errors to report, but the kernel is
still doing some clean-up work in the background. This
situation should be treated the same as if close() had
returned zero. Do not retry the close(), and do not report
an error to the user.Since this behavior for EINTR is non-conforming (and even prior to the POSIX 2024 update, it was contrary to the general semantics for EINTR, that no non-ignoreable side-effects have taken place), it should be noted that it's Linux/glibc-specific.
EDQUOT
EFBIG
EIO
ENOSPC
These are the most common errno codes associated with
delayed I/O errors. They should be treated as a hard
failure to write to the file that was formerly associated
with _fd_, the same as if an earlier write(2) had failed
with one of these codes. The file has still been closed!
Do not retry the close(). But do report an error to the user.
Depending on the underlying file, close() may return other errno
codes; these should generally also be treated as delayed I/O errors.
NOTES
Dealing with error returns from close()
As discussed above, close() always closes the file. Except when
errno is set to EBADF, EINPROGRESS, or EINTR, an error return from
close() reports a _delayed I/O error_ from a previous write()
operation.
It is vital to report delayed I/O errors to the user; failing to
check the return value of close() can cause _silent_ loss of data.
The most common situations where this actually happens involve
networked filesystems, where, in the name of throughput, write()
often returns success before the server has actually confirmed a
successful write.
However, it is also vital to understand that _no matter what_
close() returns, and _no matter what_ it sets errno to, when it
returns, _the file descriptor passed to close() has been closed_,
and its number is _immediately_ available for reuse by open(2),
dup(2), etc. Therefore, one should never retry a close(), not
even if it set errno to a value that normally indicates the
operation needs to be retried (e.g. EINTR). Retrying a close()
is a serious bug, particularly in a multithreaded program; if
the file descriptor number has already been reused, _that file_
will get closed out from under whatever other thread opened it.
[Possibly something about fsync/fdatasync here?]While I agree with all of this, I think the tone is way too proscriptive. The man pages are to document the behaviors, not tell people how to program. And again, it should be noted that the standard behavior is that you *do* have to retry on EINTR, or arrange to ensure it never happens (e.g. by not installing interrupting signal handlers, or blocking signals across calls to close), and that treating EINTR as "fd has been closed" is something you should only do on known-nonconforming systems. Aside: the reason EINTR *has to* be specified this way is that pthread cancellation is aligned with EINTR. If EINTR were defined to have closed the fd, then acting on cancellation during close would also have closed the fd, but the cancellation handler would have no way to distinguish this, leading to a situation where you're forced to either leak fds or introduce a double-close vuln.
BUGS
Prior to POSIX.1-2024, there was no official guarantee that
close() would always close the file descriptor, even on error.
Linux has always closed the file descriptor, even on error,
but other implementations might not have.
The only such implementation we have heard of is HP-UX; at least
some versions of HP-UX’s man page for close() said it should be
retried if it returned -1 with errno set to EINTR. (If you know
exactly which versions of HP-UX are affected, or of any other
Unix where close() doesn’t always close the file descriptor,
please contact us about it.)
Portable code should nonetheless never retry a failed close(); the
consequences of a file descriptor leak are far less dangerous than
the consequences of closing a file out from under another thread.This is explicitly the opposite of what's specified for portable code. It sounds like you are intentionally omitting that POSIX says the opposite of what you want it to, and treating the standard behavior as a historical HP-UX quirk/bug. This is polemic, not the sort of documentation that belongs in a man page. An outline of what I'd like to see instead: - Clear explanation of why double-close is a serious bug that must always be avoided. (I think we all agree on this.) - Statement that the historical Linux/glibc behavior and current POSIX requirement differ, without language that tries to paint the POSIX behavior as a HP-UX bug/quirk. Possibly citing real sources/history of the issue (Austin Group tracker items 529, 614; maybe others). - Consequence of just assuming the Linux behavior (fd leaks on conforming systems). - Consequences of assuming the POSIX behavior (double-close vulns on GNU/Linux, maybe others). - Survey of methods for avoiding the problem (ways to preclude EINTR, possibly ways to infer behavior, etc). Rich