Core: Avoid memcpy from NULL

Maxim Dounin mdounin at mdounin.ru
Wed Jan 24 08:57:16 UTC 2024


Hello!

On Wed, Jan 24, 2024 at 12:09:02AM +0000, Ben Kallus wrote:

> > As already pointed out previously, there are no known cases
> > when memcpy(p, NULL, 0) can result in miscompilation of nginx
> > code, ...  If you think there are cases when the code can be
> > miscompiled in practice, and not theoretically, please share.
> 
> There is no such thing as "miscompilation" of code that executes
> undefined behavior. The behavior is undefined; literally any
> instructions that the compiler emits is correct compilation. This is
> the definition of undefined behavior.

While it is certainly true, this is purely theoretical.  In 
practice, real things happen: the code is compiled even to 
something that is either correct or not.  In all known cases so 
far the compiled code is correct even if GCC with relevant 
(mis)optimizations enabled is used.

> You want me to cite a line in nginx that you would consider
> "miscompiled in practice." I'm not going to spend hours combing
> through assembly to convince you that undefined behavior is worth
> avoiding. Sorry!

There is no need to convince me that undefined behaviour worth 
avoiding.  The point is that patching it in the particular place 
you are trying to patch will make things worse by reducing 
pressure on developers, not better.  And there is no immediate 
need to patch this particular place.

Instead, we should ensure that a safe coding pattern is used 
across the code - either by patching ngx_memcpy() and other 
functions to check for length 0, or by reviewing and fixing all 
the affected calls we are able to find, or by explicitly asking 
GCC to avoid such misoptimizations (such as with 
-fno-delete-null-pointer-check like Linux kernel does), or by 
fixing the standard.

[...]

> > as nginx usually does not checks string data pointers
> > against NULL (and instead checks length, if needed).  In
> > particular, ngx_pstrdup() you are trying to patch doesn't.  That
> > is, this is exactly the "no direct impact" situation as assumed
> > above.
> 
> It is non-obvious that checks against NULL will be optimized away
> after calls to ngx_memcpy. Whether a function even calls ngx_memcpy on
> a given pointer may not always be obvious, especially if that call
> happens many layers down the stack.

In the particular place you are trying to patch, it is quite 
obvious, even assuming link-time optimizations, since 
ngx_pstrdup() is called in very few places.  And this can be 
easily verified at least for a particular compiler and compiler 
options.

For other places, that's indeed might not be obvious (but see 
below), and that's why I asking you to share cases of the nginx 
code miscompiled if you know any.  The point is, however, that the 
change you suggests with patching just ngx_pstrdup() won't fix 
these other places.  Instead, it will rather ensure these other 
places are never fixed.

OTOH, gcc13 with -O2 removes very few NULL pointer checks across 
nginx codebase.  In my tests (with "-S" added to compilation to 
obtain assembler output), -fno-delete-null-pointer-check affects 
only 31 files, and files I checked only have few pointer checks 
removed (restored with -fno-delete-null-pointer-check):

$ diff -urN objs.o2/src/ objs.o2fnodelete/src/ | diffstat
 core/ngx_inet.o                               |  703 +--
 core/ngx_log.o                                |  555 +--
 core/ngx_open_file_cache.o                    |  581 +--
 core/ngx_output_chain.o                       |  771 ++--
 core/ngx_palloc.o                             |  153 
 core/ngx_radix_tree.o                         |  327 -
 core/ngx_resolver.o                           | 2252 ++++++------
 event/ngx_event.o                             |  850 ++--
 event/ngx_event_openssl.o                     | 3576 ++++++++++---------
 event/ngx_event_openssl_stapling.o            |    6 
 event/ngx_event_timer.o                       |   18 
 event/ngx_event_udp.o                         |  127 
 http/modules/ngx_http_auth_basic_module.o     |   12 
 http/modules/ngx_http_charset_filter_module.o |    7 
 http/modules/ngx_http_fastcgi_module.o        | 2134 +++++------
 http/modules/ngx_http_geo_module.o            |  136 
 http/modules/ngx_http_image_filter_module.o   |  186 -
 http/modules/ngx_http_limit_conn_module.o     |  103 
 http/modules/ngx_http_log_module.o            |  948 ++---
 http/modules/ngx_http_secure_link_module.o    |   34 
 http/modules/ngx_http_slice_filter_module.o   |   77 
 http/modules/ngx_http_sub_filter_module.o     |  126 
 http/ngx_http_file_cache.o                    |  118 
 http/ngx_http_parse.o                         |  842 ++--
 http/ngx_http_script.o                        |   16 
 http/ngx_http_upstream.o                      | 4673 +++++++++++++-------------
 stream/ngx_stream_geo_module.o                |  134 
 stream/ngx_stream_limit_conn_module.o         |  105 
 stream/ngx_stream_log_module.o                |  872 ++--
 stream/ngx_stream_proxy_module.o              |  749 ++--
 stream/ngx_stream_script.o                    |   16 
 31 files changed, 10658 insertions(+), 10549 deletions(-)

In particular, in the only real change in ngx_palloc.o assembler 
is as follows:

@@ -452,16 +452,19 @@ ngx_reset_pool:
        testl   %ebx, %ebx
        jne     .L99 .L97:
+       testl   %esi, %esi
+       je      .L100
        movl    %esi, %eax
        .p2align 4,,10
        .p2align 3

Which corresponds to restored with -fno-delete-null-pointer-check 
initial "p" check on the first iteration of the second loop, due 
to "pool->large" being used in the first loop initialization:

    for (l = pool->large; l; l = l->next) {
        if (l->alloc) {
            ngx_free(l->alloc);
        }
    }

    for (p = pool; p; p = p->d.next) {
        p->d.last = (u_char *) p + sizeof(ngx_pool_t);
        p->d.failed = 0;
    }

Many other cases, such as multiple changes in ngx_inet.o, one of 
the two changes in ngx_open_file_cache.o, and one change 
ngx_http_parse.o, correspond to ngx_strlchr() calls, where 
subsequent result check is omitted because no-match is directly 
handled by the inlined ngx_strlchr() code:

    p = ngx_strlchr(uri->data, last, '?');

    if (p) { ... }

And there seems to be no real changes in ngx_http_script.o and 
ngx_stream_script.o, just some minor instructions reordering.

Overall, it might be feasible to review all the differences to 
ensure there are no real issues introduced by various compiler 
optimizations.

[...]

> There is a proposal for C2y to define memcpy(NULL,NULL,0):
> https://docs.google.com/document/d/1guH_HgibKrX7t9JfKGfWX2UCPyZOTLsnRfR6UleD1F8/edit
> If you feel strongly that memcpy from NULL should be defined, feel
> free to contribute to it :)

Interesting, thanks.  This would be the best solution and will 
eliminate the need for any changes by defining the behaviour, as 
it should.

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


More information about the nginx-devel mailing list