[Libguestfs] [libnbd PATCH v3 21/29] CONNECT_COMMAND.START: sanitize close() calls in the child process
Richard W.M. Jones
rjones at redhat.com
Wed Feb 15 16:58:17 UTC 2023
On Wed, Feb 15, 2023 at 03:11:50PM +0100, Laszlo Ersek wrote:
> In the child process:
>
> - we have no need for the parent-side end of the socket pair (sv[0]),
>
> - we duplicate the child-side end of the socket pair (sv[1]) to both of
> the child's fd#0 and fd#1, and then close sv[1].
>
> close (STDIN_FILENO);
> close (STDOUT_FILENO);
> close (sv[0]);
> dup2 (sv[1], STDIN_FILENO);
> dup2 (sv[1], STDOUT_FILENO);
> close (sv[1]);
>
> This code silently assumes that sv[1] falls outside of the the fd set
> {0,1} -- put differently, the code assumes that each dup2() call will
> duplicate sv[1] to a file descriptor that is *different* from sv[1].
>
> Let's investigate (a) if the logic is correct under said assumption, and
> (b) what happened if the assumption were wrong, (c) whether the assumption
> is valid to make.
>
> (a) Under the assumption, the initial two close() calls turn out to be
> superfluous. That's because dup2 (oldfd, newfd) closes "newfd" first,
> if "newfd" is open and *differs* from "oldfd".
>
> (b) If the assumption were wrong -- that is, if sv[1] equaled 0 or 1 --,
> then the logic would misbehave, at several levels.
>
> First, one of the initial close() calls would cause sv[1] to be
> closed, thereby breaking both dup2() calls.
>
> Second, even if we removed the first two close() calls, the final
> close() would still be wrong. In this case, one of the dup2() calls
> would be a no-op (oldfd == newfd), and the other dup2() would work
> correctly. However, the final close would retroactively invalidate the
> one dup2() call that had behaved as a no-op.
>
> (c) Now let's see whether we need to fix issue (b); that is, whether the
> assumption that sv[1] differs from both 0 and 1 is valid to make.
>
> This assumption is actually valid. ISO C guarantees that the stdin,
> stdout and stderr I/O streams are open at program startup, and POSIX
> translates the same requirement to file descriptors. In particular,
> the *informative* rationale in POSIX tallies its *normative*
> requirements as follows:
>
> https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xsh_chap02.html#tag_22_02_05
>
> | B.2.5 Standard I/O Streams
> |
> | Although the ISO C standard guarantees that, at program start-up,
> | /stdin/ is open for reading and /stdout/ and /stderr/ are open for
> | writing, this guarantee is contingent (as are all guarantees made by
> | the ISO C and POSIX standards) on the program being executed in a
> | conforming environment. Programs executed with file descriptor 0 not
> | open for reading or with file descriptor 1 or 2 not open for writing
> | are executed in a non-conforming environment. Application writers
> | are warned (in [exec], [posix_spawn], and [Redirection]) not to
> | execute a standard utility or a conforming application with file
> | descriptor 0 not open for reading or with file descriptor 1 or 2 not
> | open for writing.
>
> The proper way for "disabling" these file descriptors (as described by
> XRAT as well) is to redirect them from/to "/dev/null".
>
> Now, if the libnbd client application closed file descriptors 0 and/or
> 1 after its startup, it would effectively invalidate itself. Consider
> the (informative) APPLICATION USAGE section of the fclose() spec:
>
> https://pubs.opengroup.org/onlinepubs/9699919799/functions/fclose.html#tag_16_121_07
>
> | Since after the call to /fclose()/ any use of stream results in
> | undefined behavior, /fclose()/ should not be used on /stdin/,
> | /stdout/, or /stderr/ except immediately before process termination
> | (see XBD [Process Termination]), so as to avoid triggering undefined
> | behavior in other standard interfaces that rely on these streams. If
> | there are any [atexit()] handlers registered by the application,
> | such a call to /fclose()/ should not occur until the last handler is
> | finishing. Once /fclose()/ has been used to close /stdin/, /stdout/,
> | or /stderr/, there is no standard way to reopen any of these
> | streams.
> |
> | Use of [freopen()] to change /stdin/, /stdout/, or /stderr/ instead
> | of closing them avoids the danger of a file unexpectedly being
> | opened as one of the special file descriptors STDIN_FILENO,
> | STDOUT_FILENO, or STDERR_FILENO at a later time in the application.
>
> Thus, the assumption is deemed valid.
>
> Therefore:
>
> - While valid, the assumption is not trivial. So, assert it in the child
> process. Furthermore, because regular assert()'s in the parent process
> may be easier to read for the user, assert a slightly more comprehensive
> predicate about socketpair()'s output there, too.
>
> - Remove the first two close() calls, which are superfluous.
>
> Signed-off-by: Laszlo Ersek <lersek at redhat.com>
> ---
>
> Notes:
> context:-U6
>
> generator/states-connect.c | 13 +++++++++++--
> 1 file changed, 11 insertions(+), 2 deletions(-)
>
> diff --git a/generator/states-connect.c b/generator/states-connect.c
> index f0020d6f3b26..e08608336fd6 100644
> --- a/generator/states-connect.c
> +++ b/generator/states-connect.c
> @@ -248,26 +248,35 @@ CONNECT_COMMAND.START:
> if (nbd_internal_socketpair (AF_UNIX, SOCK_STREAM, 0, sv) == -1) {
> SET_NEXT_STATE (%.DEAD);
> set_error (errno, "socketpair");
> return 0;
> }
>
> + /* A process is effectively in an unusable state if any of STDIN_FILENO
> + * (fd#0), STDOUT_FILENO (fd#1) and STDERR_FILENO (fd#2) don't exist. If they
> + * exist however, then the socket pair created above will not intersect with
> + * the fd set { 0, 1, 2 }. This is relevant for the child-side dup2() logic
> + * below.
> + */
> + assert (sv[0] > STDERR_FILENO);
> + assert (sv[1] > STDERR_FILENO);
> +
> pid = fork ();
> if (pid == -1) {
> SET_NEXT_STATE (%.DEAD);
> set_error (errno, "fork");
> close (sv[0]);
> close (sv[1]);
> return 0;
> }
> if (pid == 0) { /* child - run command */
> - close (STDIN_FILENO);
> - close (STDOUT_FILENO);
> close (sv[0]);
> dup2 (sv[1], STDIN_FILENO);
> dup2 (sv[1], STDOUT_FILENO);
> + NBD_INTERNAL_FORK_SAFE_ASSERT (sv[1] != STDIN_FILENO);
> + NBD_INTERNAL_FORK_SAFE_ASSERT (sv[1] != STDOUT_FILENO);
> close (sv[1]);
>
> /* Restore SIGPIPE back to SIG_DFL. */
> signal (SIGPIPE, SIG_DFL);
>
> execvp (h->argv.ptr[0], h->argv.ptr);
Reviewed-by: Richard W.M. Jones <rjones at redhat.com>
--
Richard Jones, Virtualization Group, Red Hat http://people.redhat.com/~rjones
Read my programming and virtualization blog: http://rwmj.wordpress.com
virt-top is 'top' for virtual machines. Tiny program with many
powerful monitoring features, net stats, disk stats, logging, etc.
http://people.redhat.com/~rjones/virt-top
More information about the Libguestfs
mailing list