[PATCH] Core: fixed environment on exit better

Maxim Dounin mdounin at mdounin.ru
Tue Jul 18 00:19:04 UTC 2023


Hello!

On Thu, Jul 13, 2023 at 08:58:44PM +0400, Sergey Kandaurov wrote:

> > On 24 Jun 2023, at 06:06, Maxim Dounin <mdounin at mdounin.ru> wrote:
> > 
> > Hello!
> > 
> > On Fri, Jun 23, 2023 at 07:55:47PM +0400, Sergey Kandaurov wrote:
> > 
> >> # HG changeset patch
> >> # User Sergey Kandaurov <pluknet at nginx.com>
> >> # Date 1687535739 -14400
> >> #      Fri Jun 23 19:55:39 2023 +0400
> >> # Node ID 34a8e1a4161542896c11c4a5c60d6a6fe1931e3d
> >> # Parent  c681f4906d838ac45b517fada4c4274ade03fd3c
> >> Core: fixed environment on exit better.
> >> 
> >> The fix in 6822:c045b4926b2c to replace environment allocations from a pool
> >> was incomplete, such that it left pool owned allocations for variables set
> >> with values using the "env" directive.  Another consumer of this interface
> >> is the QUIC BPF helper that uses the environment variable NGINX_BPF_MAPS.
> >> The fix is to re-allocate variables with malloc, to avoid use after free.
> >> If memory allocation failed, the only option is to expel from environment.
> >> 
> >> The observed symptoms are similar to described in 6822:c045b4926b2c, that
> >> is a segmentation fault on worker process exit from atexit() handler seen
> >> with 3rd party modules or if nginx was built with lcov profiling.
> >> 
> >> diff --git a/src/core/nginx.c b/src/core/nginx.c
> >> --- a/src/core/nginx.c
> >> +++ b/src/core/nginx.c
> >> @@ -631,17 +631,48 @@ ngx_cleanup_environment(void *data)
> >> {
> >>     char  **env = data;
> >> 
> >> -    if (environ == env) {
> >> +    char        *p, **penv;
> >> +    size_t       size;
> >> +    ngx_uint_t   i, n;
> >> 
> >> -        /*
> >> -         * if the environment is still used, as it happens on exit,
> >> -         * the only option is to leak it
> >> -         */
> >> -
> >> +    if (environ != env) {
> >> +        ngx_free(env);
> >>         return;
> >>     }
> > 
> > Note: if we assume that the environment can be arbitrary modified, 
> > I don't think it's enough.  For example, a setenv() call can 
> > replace environ with something different, so this code will free 
> > env and exit, but pool-allocated variables will be still 
> > referenced by the new environ.
> > 
> > This is probably out of the scope of this patch though.
> 
> It could be solved by always checking for pool-allocated variables,
> irrespective of whether environ was changed after setenv(),
> using that as an additional condition to free environment.
> 
> > 
> >> 
> >> -    ngx_free(env);
> >> +    /*
> >> +     * if the environment is still used, as it happens on exit,
> >> +     * the only option is to leak it
> >> +     */
> >> +
> >> +    for (n = 0; env[n]; n++) { }
> >> +
> >> +    for (i = 0; env[i]; i++) {
> >> +
> >> +        for (penv = ngx_os_environ; *penv; penv++) {
> >> +            if (ngx_strcmp(*penv, env[i]) == 0) {
> > 
> > I don't think it's going to work.  If I'm reading this correctly, 
> > with the initial environment "FOO=bar" and "env FOO=bar;" in the 
> > configuration this will assume the string is from ngx_os_environ, 
> > but it will be from ccf->env instead.
> > 
> 
> Thanks for catching that.
> So, we have to compare against ccf->env, instead.
> 
> >> +                break;
> >> +            }
> >> +        }
> >> +
> >> +        if (*penv) {
> >> +            continue;
> >> +        }
> >> +
> >> +        /* substitute pool allocated variables */
> >> +
> >> +        size = ngx_strlen(env[i]) + 1;
> >> +        p = malloc(size);
> > 
> > There should be no direct malloc() calls except in low-level code 
> > where logging is not available.  I don't think this particular 
> > place counts, so ngx_alloc() should be used instead.
> 
> I used to avoid using cycle in cleanup, thus direct malloc() calls.
> Anyway, it will be needed in order to access configuration.
> 
> Below is an updated patch (commit log unchanged).
> 
> # HG changeset patch
> # User Sergey Kandaurov <pluknet at nginx.com>
> # Date 1689267366 -14400
> #      Thu Jul 13 20:56:06 2023 +0400
> # Node ID 80a60d9707ed4b80402335d3d31a978f418f8056
> # Parent  f91dc350be9f4a6bf1379a32a210afece7b0d75e
> Core: fixed environment on exit better.
> 
> The fix in 6822:c045b4926b2c to replace environment allocations from a pool
> was incomplete, such that it left pool owned allocations for variables set
> with values using the "env" directive.  Another consumer of this interface
> is the QUIC BPF helper that uses the environment variable NGINX_BPF_MAPS.
> The fix is to re-allocate variables with malloc, to avoid use after free.
> If memory allocation failed, the only option is to expel from environment.
> 
> The observed symptoms are similar to described in 6822:c045b4926b2c, that
> is a segmentation fault on worker process exit from atexit() handler seen
> with 3rd party modules or if nginx was built with lcov profiling.
> 
> diff --git a/src/core/nginx.c b/src/core/nginx.c
> --- a/src/core/nginx.c
> +++ b/src/core/nginx.c
> @@ -592,7 +592,7 @@ tz_found:
>          }
>  
>          cln->handler = ngx_cleanup_environment;
> -        cln->data = env;
> +        cln->data = cycle;
>      }
>  
>      n = 0;
> @@ -629,19 +629,57 @@ tz_found:
>  static void
>  ngx_cleanup_environment(void *data)
>  {
> -    char  **env = data;
> -
> -    if (environ == env) {
> +    ngx_cycle_t  *cycle = data;
>  
> -        /*
> -         * if the environment is still used, as it happens on exit,
> -         * the only option is to leak it
> -         */
> +    char             *p, **env;
> +    size_t            size;
> +    ngx_str_t        *var;
> +    ngx_uint_t        i, n;
> +    ngx_core_conf_t  *ccf;
>  
> +    ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);
> +    env = ccf->environment;
> +
> +    if (environ != env) {
> +        ngx_free(env);
>          return;
>      }
>  
> -    ngx_free(env);
> +    /*
> +     * if the environment is still used, as it happens on exit,
> +     * the only option is to leak it
> +     */
> +
> +    for (n = 0; env[n]; n++) { /* void */ }
> +
> +    while (*env) {
> +        var = ccf->env.elts;
> +
> +        for (i = 0; i < ccf->env.nelts; i++) {
> +            if (var[i].data == (u_char *) *env) {
> +                break;
> +            }
> +        }
> +
> +        if (i == ccf->env.nelts) {
> +            env++;
> +            continue;
> +        }
> +
> +        /* substitute pool allocated variables */
> +
> +        size = ngx_strlen(*env) + 1;
> +        p = ngx_alloc(size, cycle->log);
> +
> +        if (p) {
> +            ngx_memcpy(p, *env, size);
> +            *env++ = p;
> +
> +        } else {
> +            ngx_memmove(env, env + 1, sizeof(char *) * (n - (env - environ)));
> +            n--;
> +        }
> +    }
>  }
>  
>  
> 
> > 
> >> +
> >> +        if (p) {
> >> +            ngx_memcpy(p, env[i], size);
> >> +            env[i] = p;
> >> +
> >> +        } else {
> >> +            ngx_memmove(&env[i], &env[i + 1], sizeof(char *) * (n - i));
> >> +            i--; n--;
> >> +        }
> >> +    }
> >> }
> > 
> > Overall, it looks like a better idea might be to simply add all 
> > the variables added to the environment to the environ allocation 
> > in ngx_set_environment().
> > 
> > In ngx_set_environment() we explicitly know if the particular 
> > variable is from OS environment or from ccf->environ, so there 
> > will be no need to guess.  Also, the appropriate log is readily 
> > available (and already used in the existing ngx_alloc() call), and 
> > the code to cleanup things is already there and will work without 
> > additional modifications.
> 
> I tried to avoid allocations in ngx_set_environment(),
> as it can be called from the master process and harm
> hypothetically by accumulating leaked allocations.

The ngx_set_environment() function installs a cleanup handler, 
which is to be called on cycle pool destruction, and therefore 
there can't be a leak unless the cleanup handler will decide it 
needs to leak the allocation.

And, if the cleanup handler will decide to leak it, there should 
be no difference with re-allocating things in the cleanup handler 
(except the fact that allocations in cleanup handler are risky, 
see below).

Rather, I would prefer to avoid allocations in 
ngx_cleanup_environment(), since on failure this will corrupt the 
environment with unpredictable results, and there are no options 
to avoid this.  Allocating everything in ngx_set_environment() 
seems to be much safer approach.

I've tried a few variants, and the most simple one seems to 
allocate variables one-by-one with appropriate cleanup handlers in 
ngx_set_environment().  (Doing the same in ngx_set_env() is 
slightly easier, but doesn't play well with QUIC.)  This approach 
addresses all possible environment modifications and also looks 
perfectly in line with the existing code.

Here is the patch:

# HG changeset patch
# User Maxim Dounin <mdounin at mdounin.ru>
# Date 1689637083 -10800
#      Tue Jul 18 02:38:03 2023 +0300
# Node ID dedb235210f816afc028f60a525009fc5dabd8bf
# Parent  77c1418916f7817a0d020f28d8a08f278a12fe43
Core: fixed environment variables on exit.

Similarly to 6822:c045b4926b2c, environment variables introduced with
the "env" directive (and "NGINX_BPF_MAPS" added by QUIC) are now allocated
via ngx_alloc(), and explicitly freed by a cleanup handler if no longer used.

In collaboration with Sergey Kandaurov.

diff --git a/src/core/nginx.c b/src/core/nginx.c
--- a/src/core/nginx.c
+++ b/src/core/nginx.c
@@ -13,6 +13,7 @@
 static void ngx_show_version_info(void);
 static ngx_int_t ngx_add_inherited_sockets(ngx_cycle_t *cycle);
 static void ngx_cleanup_environment(void *data);
+static void ngx_cleanup_environment_variable(void *data);
 static ngx_int_t ngx_get_options(int argc, char *const *argv);
 static ngx_int_t ngx_process_options(ngx_cycle_t *cycle);
 static ngx_int_t ngx_save_argv(ngx_cycle_t *cycle, int argc, char *const *argv);
@@ -518,7 +519,8 @@ ngx_add_inherited_sockets(ngx_cycle_t *c
 char **
 ngx_set_environment(ngx_cycle_t *cycle, ngx_uint_t *last)
 {
-    char                **p, **env;
+    char                **p, **env, *str;
+    size_t                len;
     ngx_str_t            *var;
     ngx_uint_t            i, n;
     ngx_core_conf_t      *ccf;
@@ -600,7 +602,31 @@ tz_found:
     for (i = 0; i < ccf->env.nelts; i++) {
 
         if (var[i].data[var[i].len] == '=') {
-            env[n++] = (char *) var[i].data;
+
+            if (last) {
+                env[n++] = (char *) var[i].data;
+                continue;
+            }
+
+            cln = ngx_pool_cleanup_add(cycle->pool, 0);
+            if (cln == NULL) {
+                return NULL;
+            }
+
+            len = ngx_strlen(var[i].data) + 1;
+
+            str = ngx_alloc(len, cycle->log);
+            if (str == NULL) {
+                return NULL;
+            }
+
+            ngx_memcpy(str, var[i].data, len);
+
+            cln->handler = ngx_cleanup_environment_variable;
+            cln->data = str;
+
+            env[n++] = str;
+
             continue;
         }
 
@@ -645,6 +671,29 @@ ngx_cleanup_environment(void *data)
 }
 
 
+static void
+ngx_cleanup_environment_variable(void *data)
+{
+    char  *var = data;
+
+    char  **p;
+
+    for (p = environ; *p; p++) {
+
+        /*
+         * if an environment variable is still used, as it happens on exit,
+         * the only option is to leak it
+         */
+
+        if (*p == var) {
+            return;
+        }
+    }
+
+    ngx_free(var);
+}
+
+
 ngx_pid_t
 ngx_exec_new_binary(ngx_cycle_t *cycle, char *const *argv)
 {


-- 
Maxim Dounin
http://mdounin.ru/


More information about the nginx-devel mailing list