[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