Skip to content

set_md5 and other set_* directives segfault nginx workers on nginx 1.30.4/1.31.3 (root cause: stale ndk_http_rewrite_value in ngx_devel_kit) #85

Description

@MemberIT

Summary

All NDK set_var-based directives (e.g. set_md5, set_sha1, set_encode_base64 from openresty/set-misc-nginx-module) crash the nginx worker with SIGSEGV on nginx 1.30.4 (stable) and 1.31.3 (mainline). The same configuration works fine on nginx 1.30.3 and earlier.

Root cause

The fix for CVE-2026-42533 (shipped in nginx 1.30.4 / 1.31.3) changed the script engine contract: the complex-value stack push was moved out of ngx_http_script_complex_value_code into a new instruction ngx_http_script_complex_value_end_code, which is emitted only by the stock rewrite module (ngx_http_rewrite_value in src/http/modules/ngx_http_rewrite_module.c).

NDK's ndk_http_rewrite_value (src/ndk_rewrite.c) is a stale copy of the pre-1.30.4 rewrite module code (the file header even says it was "taken from the rewrite module and http_script file because those functions are defined as being static"). It emits ngx_http_script_complex_value_code + ngx_http_script_compile() but never emits the new ngx_http_script_complex_value_end_code.

As a result, the evaluated value is never pushed onto e->sp. The consumer ndk_set_var_value_code (src/ndk_set_var.c) then reads e->sp - 1, which contains garbage, and passes it to the set-misc handler. ngx_http_set_misc_set_md5 hashes a bogus pointer/length and the worker segfaults.

Backtrace (gdb, nginx 1.30.4 built from source + NDK v0.3.3 + set-misc v0.34)

#0  ngx_md5_body (ctx=0x7fff3faaad30, data=0x79d23813d2b0 "HTTP", size=268435392) at src/core/ngx_md5.c:198
#1  ngx_md5_update (ctx=0x7fff3faaad30, data=0x79d23813d2b0, size=268435451) at src/core/ngx_md5.c:53
#2  ngx_http_set_misc_set_md5 (r=..., res=..., v=0x79d23813e030) at set-misc/src/ngx_http_set_hash.c:74
#3  ndk_set_var_value_code (e=0x79d23813dfe0) at ndk/src/ndk_set_var.c:144
#4  ngx_http_rewrite_handler (r=0x79d23813d2b0) at src/http/modules/ngx_http_rewrite_module.c:180
#5  ngx_http_core_rewrite_phase ...

Note data == the request pointer and size = 268435451 — garbage read from the stale stack slot.

Affected versions

nginx NDK set-misc result
1.30.3 (source build) simpl v0.3.3 v0.34 works
1.30.4 (source build) simpl v0.3.3 v0.34 SIGSEGV
1.30.4 (source build) openresty master (57270ba... per set-misc CI) v0.34 SIGSEGV

All set_var directives are affected, not only set_md5: verified crashes with set_sha1, set_encode_base64, set_quote_sql_str. Module load order does not matter. This is reproducible with pure upstream sources — it is not a distro packaging issue (verified against Alpine's nginx-mod-http-set-misc=1.30.4-r0 as well).

Minimal reproduction

Dockerfile (Alpine 3.24, pure source build):

FROM alpine:3.24.1
RUN apk add --no-cache build-base pcre-dev zlib-dev openssl-dev curl git
WORKDIR /build
RUN curl -fSL https://nginx.org/download/nginx-1.30.4.tar.gz | tar xz
RUN git clone --depth 1 --branch v0.3.3 https://github.com/simpl/ngx_devel_kit ndk
RUN git clone --depth 1 --branch v0.34 https://github.com/openresty/set-misc-nginx-module set-misc
WORKDIR /build/nginx-1.30.4
RUN ./configure --prefix=/opt/nginx --with-compat \
      --add-dynamic-module=/build/ndk \
      --add-dynamic-module=/build/set-misc \
    && make -j$(nproc) && make install

nginx.conf:

load_module /opt/nginx/modules/ndk_http_module.so;
load_module /opt/nginx/modules/ngx_http_set_misc_module.so;
worker_processes 1;
events { worker_connections 128; }
http {
  server {
    listen 8080;
    location /t { set $x "hello"; set_md5 $y $x; return 200 "$y\n"; }
  }
}
$ curl http://127.0.0.1:8080/t
curl: (52) Empty reply from server
# error log: [alert] worker process N exited on signal 11 (core dumped)

Changing only nginx-1.30.4.tar.gz to nginx-1.30.3.tar.gz makes the same build return 5d41402abc4b2a76b9719d911017c592.

Proposed fix

Mirror the upstream change in ndk_http_rewrite_value (src/ndk_rewrite.c): after ngx_http_script_compile(), append ngx_http_script_complex_value_end_code when building against nginx versions that have it (1.30.4+ stable, 1.31.3+ mainline):

#include <ngx_http_script.h>

/* after ngx_http_script_compile(&sc) succeeds */
#if defined(nginx_version) && \
    ((nginx_version >= 1030004 && nginx_version < 1031000) || nginx_version >= 1031003)
    {
        ngx_http_script_complex_value_end_code_t  *complex_end;

        complex_end = ngx_http_script_add_code(lcf->codes,
                          sizeof(ngx_http_script_complex_value_end_code_t),
                          &complex);
        if (complex_end == NULL) {
            return NGX_CONF_ERROR;
        }

        complex_end->code = ngx_http_script_complex_value_end_code;
    }
#endif

This matches what nginx itself does in ngx_http_rewrite_value (src/http/modules/ngx_http_rewrite_module.c, ~line 1021 in 1.30.4).

It may also be worth flagging to the nginx team that the 1.30.4/1.31.3 change silently broke the de-facto script-engine contract for every third-party module that builds its own instruction stream via ngx_http_script_compile() (NDK is the most widespread one — this affects at least all set-misc set_* users).

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions