On Wed, Feb 15, 2023 at 03:11:44PM +0100, Laszlo Ersek wrote:
> The CONNECT_SA.START handler constructs several resources; some of those
> are needed permanently if the entire handler succeeds, while others are
> needed only temporarily, for constructing the permanent objects.
> Currently, the various error branches in the handler deal with resource
> release individually, leading to code duplication and dispersed
> responsibilities; if we want to construct a new resource, then, under the
> existent pattern, we'll have to modify multiple error branches to release
> it as well.
> 
> In my opinion, the best pattern for dealing with this scenario is the
> following structure:
> 
> - Place cascading error handling labels at the end of the function -- an
>   error handling slide in effect. The order of destruction on this slide
>   is the inverse of construction.
> 
> - Whenever an operation fails mid-construction, enter the slide such that
>   precisely the thus far constructed objects be freed. Note that the goto
>   statements and the destination labels nest properly.
> 
> - The very last construction step needs no rollback, because the last
>   successful step completes the entire function / handler. When this
>   happens, *commit* by (a) outputting the permanent resources, (b) setting
>   a top-level "final result" variable to "success" (aka "ownership of
>   permanent resources transferred to the caller"), and (c) falling through
>   to the slide. On the slide, use the "final result" variable to skip the
>   release of those resources that have been output as permanent ones.
> 
> This way, each resource is freed in exactly one location, and whenever an
> error occurs, no resource is even *checked* for rollback if its
> construction could not be tried, or completed, in the first place. The
> introduction of a new resource needs one properly placed construction step
> and one matchingly placed error handling label.
> 
> The restructuring highlights the following leak: whenever any construction
> step after bind() fails, the UNIX domain socket address (i.e., the
> pathname in the filesystem) is leaked. (The filesystem object may well be
> removed once the application decides to call nbd_close(), but until then,
> after the CONNECT_SA.START handler returns with failure, unusable
> (half-constructed) resources linger around indefinitely. A library API
> should either fully succeed or fully fail.) We'll plug the leak later in
> this series.
> 
> Signed-off-by: Laszlo Ersek <ler...@redhat.com>
> ---
> 
> Notes:
>     context:-W
> 
>  generator/states-connect-socket-activation.c | 82 ++++++++++++--------
>  1 file changed, 48 insertions(+), 34 deletions(-)
> 
> diff --git a/generator/states-connect-socket-activation.c 
> b/generator/states-connect-socket-activation.c
> index 766f46177bf3..164b8e6828f2 100644
> --- a/generator/states-connect-socket-activation.c
> +++ b/generator/states-connect-socket-activation.c
> @@ -96,132 +96,146 @@ prepare_socket_activation_environment (string_vector 
> *env)
>  
>  STATE_MACHINE {
>   CONNECT_SA.START:
> +  enum state next;
> +  char *tmpdir;
> +  char *sockpath;
>    int s;
>    struct sockaddr_un addr;
>    string_vector env;
>    pid_t pid;
>  
>    assert (!h->sock);
>    assert (h->argv.ptr);
>    assert (h->argv.ptr[0]);
>  
> +  next = %.DEAD;
> +
>    /* Use /tmp instead of TMPDIR because we must ensure the path is
>     * short enough to store in the sockaddr_un.  On some platforms this
>     * may cause problems so we may need to revisit it.  XXX
>     */
> -  h->sact_tmpdir = strdup ("/tmp/libnbdXXXXXX");
> -  if (h->sact_tmpdir == NULL) {
> -    SET_NEXT_STATE (%.DEAD);
> +  tmpdir = strdup ("/tmp/libnbdXXXXXX");
> +  if (tmpdir == NULL) {
>      set_error (errno, "strdup");
> -    return 0;
> +    goto done;
>    }
> -  if (mkdtemp (h->sact_tmpdir) == NULL) {
> -    SET_NEXT_STATE (%.DEAD);
> +
> +  if (mkdtemp (tmpdir) == NULL) {
>      set_error (errno, "mkdtemp");
> -    /* Avoid cleanup in nbd_close. */
> -    free (h->sact_tmpdir);
> -    h->sact_tmpdir = NULL;
> -    return 0;
> +    goto free_tmpdir;
>    }
>  
> -  if (asprintf (&h->sact_sockpath, "%s/sock", h->sact_tmpdir) == -1) {
> -    SET_NEXT_STATE (%.DEAD);
> +  if (asprintf (&sockpath, "%s/sock", tmpdir) == -1) {
>      set_error (errno, "asprintf");
> -    return 0;
> +    goto rmdir_tmpdir;
>    }
>  
>    s = nbd_internal_socket (AF_UNIX, SOCK_STREAM, 0, false);
>    if (s == -1) {
> -    SET_NEXT_STATE (%.DEAD);
>      set_error (errno, "socket");
> -    return 0;
> +    goto free_sockpath;
>    }
>  
>    addr.sun_family = AF_UNIX;
> -  memcpy (addr.sun_path, h->sact_sockpath, strlen (h->sact_sockpath) + 1);
> +  memcpy (addr.sun_path, sockpath, strlen (sockpath) + 1);
>    if (bind (s, (struct sockaddr *) &addr, sizeof addr) == -1) {
> -    SET_NEXT_STATE (%.DEAD);
> -    set_error (errno, "bind: %s", h->sact_sockpath);
> -    close (s);
> -    return 0;
> +    set_error (errno, "bind: %s", sockpath);
> +    goto close_socket;
>    }
>  
>    if (listen (s, SOMAXCONN) == -1) {
> -    SET_NEXT_STATE (%.DEAD);
>      set_error (errno, "listen");
> -    close (s);
> -    return 0;
> +    goto close_socket;
>    }
>  
>    if (prepare_socket_activation_environment (&env) == -1) {
> -    SET_NEXT_STATE (%.DEAD);
>      set_error (errno, "prepare_socket_activation_environment");
> -    close (s);
> -    return 0;
> +    goto close_socket;
>    }
>  
>    pid = fork ();
>    if (pid == -1) {
> -    SET_NEXT_STATE (%.DEAD);
>      set_error (errno, "fork");
> -    close (s);
> -    string_vector_empty (&env);
> -    return 0;
> +    goto empty_env;
>    }
> +
>    if (pid == 0) {         /* child - run command */
>      if (s != FIRST_SOCKET_ACTIVATION_FD) {
>        if (dup2 (s, FIRST_SOCKET_ACTIVATION_FD) == -1) {
>          nbd_internal_fork_safe_perror ("dup2");
>          _exit (126);
>        }
>        if (close (s) == -1) {
>          nbd_internal_fork_safe_perror ("close");
>          _exit (126);
>        }
>      }
>      else {
>        /* We must unset CLOEXEC on the fd.  (dup2 above does this
>         * implicitly because CLOEXEC is set on the fd, not on the
>         * socket).
>         */
>        int flags = fcntl (s, F_GETFD, 0);
>        if (flags == -1) {
>          nbd_internal_fork_safe_perror ("fcntl: F_GETFD");
>          _exit (126);
>        }
>        if (fcntl (s, F_SETFD, (int)(flags & ~(unsigned)FD_CLOEXEC)) == -1) {
>          nbd_internal_fork_safe_perror ("fcntl: F_SETFD");
>          _exit (126);
>        }
>      }
>  
>      char buf[32];
>      const char *v =
>        nbd_internal_fork_safe_itoa ((long) getpid (), buf, sizeof buf);
>      strcpy (&env.ptr[0][PREFIX_LENGTH], v);
>  
>      /* Restore SIGPIPE back to SIG_DFL. */
>      if (signal (SIGPIPE, SIG_DFL) == SIG_ERR) {
>        nbd_internal_fork_safe_perror ("signal");
>        _exit (126);
>      }
>  
>      environ = env.ptr;
>      execvp (h->argv.ptr[0], h->argv.ptr);
>      nbd_internal_fork_safe_perror (h->argv.ptr[0]);
>      if (errno == ENOENT)
>        _exit (127);
>      else
>        _exit (126);
>    }
>  
> -  /* Parent. */
> -  close (s);
> -  string_vector_empty (&env);
> +  /* Parent -- we're done; commit. */
> +  h->sact_tmpdir = tmpdir;
> +  h->sact_sockpath = sockpath;
>    h->pid = pid;
>  
>    h->connaddrlen = sizeof addr;
>    memcpy (&h->connaddr, &addr, h->connaddrlen);
> -  SET_NEXT_STATE (%^CONNECT.START);
> +  next = %^CONNECT.START;
> +
> +  /* fall through, for releasing the temporaries */
> +
> +empty_env:
> +  string_vector_empty (&env);
> +
> +close_socket:
> +  close (s);
> +
> +free_sockpath:
> +  if (next == %.DEAD)
> +    free (sockpath);
> +
> +rmdir_tmpdir:
> +  if (next == %.DEAD)
> +    rmdir (tmpdir);
> +
> +free_tmpdir:
> +  if (next == %.DEAD)
> +    free (tmpdir);
> +
> +done:
> +  SET_NEXT_STATE (next);
>    return 0;
>  } /* END STATE MACHINE */

A note that state machine labels match the basic regexp pattern:

^ \\([A-Z0-9][A-Z0-9_\\.]*\\):$

(see generator/state_machine_generator.ml), and because of the
all-capitalization of state names, they won't match the lower case
ordinary C labels we're using here.

Reviewed-by: Richard W.M. Jones <rjo...@redhat.com>

Rich.

-- 
Richard Jones, Virtualization Group, Red Hat http://people.redhat.com/~rjones
Read my programming and virtualization blog: http://rwmj.wordpress.com
nbdkit - Flexible, fast NBD server with plugins
https://gitlab.com/nbdkit/nbdkit
_______________________________________________
Libguestfs mailing list
Libguestfs@redhat.com
https://listman.redhat.com/mailman/listinfo/libguestfs

Reply via email to