On 2/15/23 17:53, Richard W.M. Jones wrote:
> 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.

Ah, thank you for pointing that out! I'll capture this in the commit
message.

Laszlo

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

_______________________________________________
Libguestfs mailing list
Libguestfs@redhat.com
https://listman.redhat.com/mailman/listinfo/libguestfs

Reply via email to