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 <lersek(a)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 <rjones(a)redhat.com>
Rich.