From 4c1f0362ca0f25ef216ce59cad5fa6c9703c1438 Mon Sep 17 00:00:00 2001 From: Felipe Zipitria Date: Tue, 28 Jul 2026 08:22:06 -0300 Subject: [PATCH] test: add valgrind memcheck/helgrind soak of the running module Ports tools/soak.sh from coraza-nginx (itself adapted from the same soak added to ModSecurity-apache in #93), rewritten for this connector's directive names and architecture: drives a real nginx (optionally under valgrind memcheck or helgrind) with concurrent benign and attack-shaped traffic across ten request shapes covering every attacker-reachable path in the connector -- request/response body inspection (including the file-backed and multi-buffer paths), header forwarding across multiple ngx_list parts, and a WAF-triggered redirect replacing an already-populated response. Adds Dockerfile + Dockerfile.fuzz (mirroring ModSecurity-apache #93's structure): a multi-stage build image (libmodsecurity + nginx + this connector, matching the existing test_new.yml CI build steps) and a thin layer adding valgrind + curl on top, so the soak can run identically in CI and locally without rebuilding the connector from source for every run. Not a replacement for test_new.yml's existing per-PR build+functional test, which doesn't use Docker and stays as is; this is scoped to what the soak specifically needs. Adds .github/workflows/soak.yml: manual (workflow_dispatch) + weekly scheduled only, not on every PR -- a valgrind soak runs 10-50x slower than native. Adds tools/valgrind.suppress: nginx-core startup/event-loop entries (module-agnostic, apply to any nginx build) plus a libp11-kit exit-time pthread_mutex_destroy entry, generated via --gen-suppressions=all against an actual run and confirmed to be runtime/third-party noise at process teardown, not this connector. Co-Authored-By: Claude Sonnet 5 --- .dockerignore | 5 + .github/workflows/soak.yml | 73 +++++++ Dockerfile | 72 +++++++ Dockerfile.fuzz | 29 +++ tools/soak.sh | 377 +++++++++++++++++++++++++++++++++++++ tools/valgrind.suppress | 139 ++++++++++++++ 6 files changed, 695 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/soak.yml create mode 100644 Dockerfile create mode 100644 Dockerfile.fuzz create mode 100755 tools/soak.sh create mode 100644 tools/valgrind.suppress diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2b380f4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.git +.github +.claude +win32 +*.md diff --git a/.github/workflows/soak.yml b/.github/workflows/soak.yml new file mode 100644 index 0000000..5a9e3b7 --- /dev/null +++ b/.github/workflows/soak.yml @@ -0,0 +1,73 @@ +name: Valgrind soak + +# Manual/scheduled only, not on every PR: a memcheck/helgrind soak runs +# 10-50x slower than native. See tools/soak.sh. +on: + workflow_dispatch: + inputs: + duration: + description: Seconds per soak run + default: "120" + concurrency: + description: Concurrent traffic workers + default: "8" + schedule: + - cron: "0 3 * * 1" # weekly, Monday 03:00 UTC + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: true + +jobs: + memcheck: + name: memcheck soak + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Build base image + run: docker build -t modsec-nginx-test . + + - name: Build soak image + run: docker build -f Dockerfile.fuzz -t modsec-nginx-soak . + + - name: Run memcheck soak + env: + DURATION: ${{ inputs.duration || '120' }} + CONCURRENCY: ${{ inputs.concurrency || '8' }} + run: | + docker run --rm --cap-add=SYS_PTRACE \ + -e USE_VALGRIND=1 \ + modsec-nginx-soak /usr/local/nginx/sbin/nginx \ + "$DURATION" "$CONCURRENCY" + + helgrind: + name: helgrind soak + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Build base image + run: docker build -t modsec-nginx-test . + + - name: Build soak image + run: docker build -f Dockerfile.fuzz -t modsec-nginx-soak . + + - name: Run helgrind soak + env: + DURATION: ${{ inputs.duration || '120' }} + CONCURRENCY: ${{ inputs.concurrency || '8' }} + run: | + docker run --rm --cap-add=SYS_PTRACE \ + -e USE_HELGRIND=1 \ + modsec-nginx-soak /usr/local/nginx/sbin/nginx \ + "$DURATION" "$CONCURRENCY" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..088a9a2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,72 @@ +# Build image for the ModSecurity-nginx connector: libmodsecurity3, nginx, +# and this connector, statically linked (matching the .github/workflows/ +# test_new.yml CI build). Used as the base for Dockerfile.fuzz's +# valgrind/helgrind soak; not intended as a production nginx image. +FROM debian:bookworm-slim AS builder + +ARG NGINX_VERSION=1.29.1 + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + git \ + wget \ + automake \ + autoconf \ + libtool \ + pkg-config \ + pcre2-utils \ + libpcre2-dev \ + libyajl-dev \ + libxml2-dev \ + libmaxminddb-dev \ + libcurl4-openssl-dev \ + zlib1g-dev && \ + rm -rf /var/lib/apt/lists/* + +# Stage 1: libmodsecurity v3, matching test_new.yml's build. +WORKDIR /build +RUN git clone --depth 1 --branch v3/master --recurse-submodules \ + https://github.com/owasp-modsecurity/ModSecurity.git libmodsecurity +WORKDIR /build/libmodsecurity +RUN ./build.sh && \ + ./configure --without-lmdb --prefix=/usr && \ + make -j"$(nproc)" && \ + make install + +# Stage 2: nginx, statically linked against this connector. +WORKDIR /build +RUN wget -q -O nginx.tar.gz "https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz" && \ + tar -xzf nginx.tar.gz +COPY . /build/ModSecurity-nginx +WORKDIR /build/nginx-${NGINX_VERSION} +RUN ./configure \ + --with-ld-opt="-Wl,-rpath,/usr/lib" \ + --with-http_v2_module \ + --with-http_auth_request_module \ + --add-module=../ModSecurity-nginx && \ + make -j"$(nproc)" && \ + make install + +# Runtime image: nginx binary + libmodsecurity's runtime deps only. +FROM debian:bookworm-slim + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + libpcre2-8-0 \ + libyajl2 \ + libxml2 \ + libmaxminddb0 \ + libcurl4 \ + libstdc++6 \ + zlib1g && \ + rm -rf /var/lib/apt/lists/* + +COPY --from=builder /usr/local/nginx /usr/local/nginx +COPY --from=builder /usr/lib/libmodsecurity* /usr/lib/ +RUN ldconfig + +EXPOSE 80 +ENTRYPOINT ["/usr/local/nginx/sbin/nginx"] +CMD ["-g", "daemon off;"] diff --git a/Dockerfile.fuzz b/Dockerfile.fuzz new file mode 100644 index 0000000..cbf97be --- /dev/null +++ b/Dockerfile.fuzz @@ -0,0 +1,29 @@ +# Valgrind memcheck/helgrind soak image for the ModSecurity-nginx connector. +# +# Kept separate from the main Dockerfile so the production-shaped build image +# stays untouched; this just layers valgrind + curl + tools/soak.sh on top. +# +# Build (base image first, then this one): +# docker build -t modsec-nginx-test . +# docker build -f Dockerfile.fuzz -t modsec-nginx-soak . +# +# Run: +# docker run --rm --cap-add=SYS_PTRACE modsec-nginx-soak /usr/local/nginx/sbin/nginx 60 4 +# docker run --rm -e USE_VALGRIND=1 --cap-add=SYS_PTRACE modsec-nginx-soak \ +# /usr/local/nginx/sbin/nginx 120 8 +# docker run --rm -e USE_HELGRIND=1 --cap-add=SYS_PTRACE modsec-nginx-soak \ +# /usr/local/nginx/sbin/nginx 120 8 +# +# See tools/soak.sh for what the soak actually does. + +ARG BASE_IMAGE=modsec-nginx-test +FROM ${BASE_IMAGE} + +RUN apt-get update && \ + apt-get install -y --no-install-recommends valgrind curl && \ + rm -rf /var/lib/apt/lists/* + +COPY tools/soak.sh tools/valgrind.suppress /opt/soak/ + +ENTRYPOINT ["/opt/soak/soak.sh"] +CMD ["/usr/local/nginx/sbin/nginx"] diff --git a/tools/soak.sh b/tools/soak.sh new file mode 100755 index 0000000..1c24637 --- /dev/null +++ b/tools/soak.sh @@ -0,0 +1,377 @@ +#!/usr/bin/env bash +# +# Sustained mixed-load soak for the ModSecurity-nginx connector. Drives a real +# nginx (optionally under valgrind memcheck or helgrind) with concurrent +# benign AND attack-shaped requests for a fixed duration, then asserts the +# worker survived cleanly: no valgrind/helgrind error, no crash, no leak, no +# error-log [alert]/[emerg]. +# +# The traffic mix deliberately exercises the WAF decision path in both +# directions -- benign requests that must pass (200) and attack requests the +# in-config SecRules must block (403) -- across ten request shapes that drive +# every attacker-reachable path in this connector: +# * URI-arg, request-body, and request-header attacks (phase 1/2 deny) +# * benign GET, POST, and large response body (pass) +# * large chunked request body -> file-backed request-body inspection +# (msc_request_body_from_file, access.c) +# * 40 large request headers -> the multi-ngx_list-part traversal loop in +# the request-header forwarding code (access.c) +# * RESPONSE_BODY inspection, both pass and phase-4 deny -- the deny +# aborts the connection mid-transfer rather than a clean 403, since +# headers are already committed by the time the full body is +# buffered for inspection (body_filter.c) +# * a phase:3 "redirect:" action replacing an already-populated response -- +# the Location header must survive and the discarded response's entity +# headers must not leak (header_filter.c / body_filter.c) +# so allocation/free of the ModSecurity transaction, header forwarding, and +# request/response body inspection all run every iteration. +# +# Usage: +# tools/soak.sh [duration_seconds] [concurrency] +# USE_VALGRIND=1 tools/soak.sh 120 8 +# USE_HELGRIND=1 tools/soak.sh 120 8 +# +# Env: +# MODSECURITY_MODULE_SO : path to ngx_http_modsecurity_module.so, for a +# --add-dynamic-module build (default: sibling of +# the nginx binary). Unused for a static build. +# +# Exit non-zero on ANY of: valgrind/helgrind error, nginx crash/non-clean +# exit, error-log alert/emerg, or a WAF verdict regression (benign blocked / +# attack allowed). + +set -euo pipefail + +NGINX="${1:?usage: soak.sh [duration] [concurrency]}" +DURATION="${2:-60}" +CONC="${3:-8}" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +WORK="$(mktemp -d)" +# Kill the (possibly valgrind-wrapped) server too: under `set -e` an early +# failure would otherwise orphan it, holding the port for later runs. +trap 'kill -9 "${NGINX_PID:-}" 2>/dev/null || true; rm -rf "$WORK"' EXIT +mkdir -p "$WORK/conf" "$WORK/logs" "$WORK/html" +# When nginx runs as root (e.g. in a container) it drops worker processes to +# its compiled-in default user, which can't traverse mktemp's 0700-root dir. +chmod 755 "$WORK" "$WORK/html" + +echo "hello modsecurity" >"$WORK/html/index.html" +head -c 200000 /dev/urandom | base64 >"$WORK/html/medium" +# Benign response body scanned by the phase-4 RESPONSE_BODY rule (must pass). +head -c 120000 /dev/urandom | base64 >"$WORK/html/respbody" +# Response body carrying the leak marker (must be blocked 403 at phase 4). +{ + head -c 40000 /dev/urandom | base64 + echo "leakmarker" +} >"$WORK/html/leak" +# Served with real Content-Length/Last-Modified/ETag, so the redirect case +# below actually has stale entity headers to discard. +head -c 8000 /dev/urandom | base64 >"$WORK/html/entityheaders" + +# Locate the built module (.so). --add-dynamic-module builds it into objs/; +# if the caller installed it, allow an override via $MODSECURITY_MODULE_SO. +MODULE_SO="${MODSECURITY_MODULE_SO:-}" +if [ -z "$MODULE_SO" ]; then + MODULE_SO="$(dirname "$NGINX")/ngx_http_modsecurity_module.so" +fi +LOAD_MODULE_DIRECTIVE="" +if [ -f "$MODULE_SO" ]; then + LOAD_MODULE_DIRECTIVE="load_module $MODULE_SO;" +fi + +# SecRules live in their own file, not inline in modsecurity_rules: the +# redirect: action needs single-quoted string arguments +# (redirect:'/other'), and nesting single quotes inside nginx's own +# single-quoted modsecurity_rules '...' value does not parse. +cat >"$WORK/conf/rules.conf" <<'EOF' +SecRuleEngine On +SecRequestBodyAccess On +SecResponseBodyAccess On +SecResponseBodyMimeType text/plain +SecRule ARGS "@rx attackmarker" "id:100,phase:2,deny,status:403" +SecRule REQUEST_BODY "@rx evilbody" "id:101,phase:2,deny,status:403" +SecRule REQUEST_HEADERS:X-Attack "@rx headermarker" "id:102,phase:1,deny,status:403" +SecRule RESPONSE_BODY "@rx leakmarker" "id:103,phase:4,deny,status:403" +SecRule REQUEST_HEADERS:X-Redirect-Me "@streq 1" "id:104,phase:3,redirect:'/medium',log" +EOF + +cat >"$WORK/conf/nginx.conf" <"$WORK/logs/stdout.txt" 2>"$WORK/logs/stderr.txt" & +NGINX_PID=$! + +# Wait for listen. valgrind starts slowly, so allow up to ~120s; bail early +# if the process already died (config error, missing module, etc.) rather +# than burning the full timeout. +up=0 +for _ in $(seq 1 1200); do + if ! kill -0 "$NGINX_PID" 2>/dev/null; then + break # process gone -- startup failed, report below + fi + curl -fsS -o /dev/null "http://127.0.0.1:18223/" 2>/dev/null && { + up=1 + break + } + sleep 0.1 +done +if [ "$up" -ne 1 ]; then + echo "FAIL: nginx never came up" + echo "--- stderr ---" + cat "$WORK/logs/stderr.txt" 2>/dev/null || true + echo "--- error.log ---" + cat "$WORK/logs/error.log" 2>/dev/null || echo "(none written)" + if ls "$WORK"/logs/valgrind.* "$WORK"/logs/helgrind.* >/dev/null 2>&1; then + echo "--- valgrind/helgrind log ---" + cat "$WORK"/logs/valgrind.* "$WORK"/logs/helgrind.* 2>/dev/null || true + fi + kill "$NGINX_PID" 2>/dev/null || true + exit 1 +fi + +echo "soak: ${DURATION}s, concurrency ${CONC}$( + [ "${USE_VALGRIND:-0}" = 1 ] && echo ' (valgrind)' + [ "${USE_HELGRIND:-0}" = 1 ] && echo ' (helgrind)' +)" +END=$(($(date +%s) + DURATION)) +fail=0 + +# A large body forces nginx to buffer the request into a temp file, driving +# the connector's file-backed request-body path (not just the in-memory +# single-buffer case a tiny -d body hits). +BIG_BODY="$WORK/html/bigreq" +head -c 300000 /dev/urandom | base64 >"$BIG_BODY" + +worker() { + while [ "$(date +%s)" -lt "$END" ]; do + case $((RANDOM % 10)) in + 0) # benign GET -> must pass + code=$(curl -s -o /dev/null -w '%{http_code}' \ + "http://127.0.0.1:18223/" 2>/dev/null || echo 000) + [ "$code" = "200" ] || { + echo "benign GET got $code" + return 1 + } + ;; + 1) # benign larger response body -> must pass + code=$(curl -s -o /dev/null -w '%{http_code}' \ + "http://127.0.0.1:18223/medium" 2>/dev/null || echo 000) + [ "$code" = "200" ] || { + echo "benign /medium got $code" + return 1 + } + ;; + 2) # URI-arg attack -> must be blocked 403 + code=$(curl -s -o /dev/null -w '%{http_code}' \ + "http://127.0.0.1:18223/?q=attackmarker" 2>/dev/null || echo 000) + [ "$code" = "403" ] || { + echo "URI attack got $code (want 403)" + return 1 + } + ;; + 3) # request-body attack -> must be blocked 403 + code=$(curl -s -o /dev/null -w '%{http_code}' \ + -d 'x=evilbody' \ + "http://127.0.0.1:18223/" 2>/dev/null || echo 000) + [ "$code" = "403" ] || { + echo "body attack got $code (want 403)" + return 1 + } + ;; + 4) # benign POST body -> must pass + code=$(curl -s -o /dev/null -w '%{http_code}' \ + -d 'x=harmless' \ + "http://127.0.0.1:18223/" 2>/dev/null || echo 000) + [ "$code" = "200" ] || { + echo "benign POST got $code" + return 1 + } + ;; + 5) # large chunked request body -> temp-file inspection path, must pass + code=$(curl -s -o /dev/null -w '%{http_code}' \ + -H 'Transfer-Encoding: chunked' \ + --data-binary "@$BIG_BODY" \ + "http://127.0.0.1:18223/" 2>/dev/null || echo 000) + [ "$code" = "200" ] || { + echo "large chunked body got $code" + return 1 + } + ;; + 6) # many + large request headers -> multi-ngx_list-part traversal + hdrs=() + for i in $(seq 1 40); do hdrs+=(-H "X-H$i: v$i-$(head -c 64 /dev/zero | tr '\0' a)"); done + code=$(curl -s -o /dev/null -w '%{http_code}' \ + "${hdrs[@]}" \ + "http://127.0.0.1:18223/" 2>/dev/null || echo 000) + [ "$code" = "200" ] || { + echo "many-headers got $code" + return 1 + } + ;; + 7) # request-header attack -> phase-1 header rule, must block 403 + code=$(curl -s -o /dev/null -w '%{http_code}' \ + -H 'X-Attack: headermarker' \ + "http://127.0.0.1:18223/" 2>/dev/null || echo 000) + [ "$code" = "403" ] || { + echo "header attack got $code (want 403)" + return 1 + } + ;; + 8) # response-body: benign scanned body passes; leak marker triggers + # a phase-4 deny. This connector doesn't delay response headers + # until body inspection finishes, so by the time the leak marker + # is found (msc_process_response_body(), after the full body is + # buffered), the 200 status and Content-Length are already + # committed to the client -- blocking manifests as the + # connection aborting mid-transfer, not a clean 403. Assert the + # transfer fails/truncates rather than completing normally. + if [ $((RANDOM % 2)) -eq 0 ]; then + code=$(curl -s -o /dev/null -w '%{http_code}' \ + "http://127.0.0.1:18223/respbody" 2>/dev/null || echo 000) + [ "$code" = "200" ] || { + echo "benign respbody got $code" + return 1 + } + else + rc=0 + curl -s -o /dev/null "http://127.0.0.1:18223/leak" 2>/dev/null || rc=$? + [ "$rc" -ne 0 ] || { + echo "resp leak: transfer completed cleanly (want aborted mid-transfer)" + return 1 + } + fi + ;; + 9) # WAF-triggered redirect on an already-populated response: Location + # must be present, and the discarded response's stale + # Content-Length must not leak into the redirect. + resp_headers=$(curl -s -D - -o /dev/null -H 'X-Redirect-Me: 1' \ + "http://127.0.0.1:18223/entityheaders" 2>/dev/null || echo "") + case "$resp_headers" in + *"HTTP/1.1 302"*) : ;; + *) + echo "redirect: wrong status: $resp_headers" + return 1 + ;; + esac + case "$resp_headers" in + *"Location:"*) : ;; + *) + echo "redirect: missing Location: $resp_headers" + return 1 + ;; + esac + case "$resp_headers" in + *"Content-Length: 0"*) : ;; + *) + echo "redirect: stale Content-Length leaked: $resp_headers" + return 1 + ;; + esac + ;; + esac + done +} + +pids=() +for _ in $(seq 1 "$CONC"); do + worker & + pids+=($!) +done +for pid in "${pids[@]}"; do wait "$pid" || fail=1; done + +# Clean shutdown so all pool cleanups (incl. the ModSecurity transaction) run. +kill -QUIT "$NGINX_PID" 2>/dev/null || true +rc=0 +wait "$NGINX_PID" 2>/dev/null || rc=$? + +problems=0 +if ls "$WORK"/logs/valgrind.* "$WORK"/logs/helgrind.* >/dev/null 2>&1; then + if grep -qE 'ERROR SUMMARY: [1-9]|definitely lost: [1-9]' \ + "$WORK"/logs/valgrind.* "$WORK"/logs/helgrind.* 2>/dev/null; then + echo "FAIL: valgrind/helgrind errors:" + grep -E 'ERROR SUMMARY|definitely lost' \ + "$WORK"/logs/valgrind.* "$WORK"/logs/helgrind.* 2>/dev/null + problems=1 + fi +fi +if grep -nE '\[alert\]|\[emerg\]' "$WORK/logs/error.log" 2>/dev/null; then + echo "FAIL: alert/emerg in error.log" + problems=1 +fi +if [ "$fail" -ne 0 ]; then + echo "FAIL: a worker reported a WAF verdict regression" + problems=1 +fi +# QUIT is a clean exit; valgrind/helgrind use 99 on error. +if [ "$rc" -ne 0 ] && [ "$rc" -ne 130 ]; then + echo "FAIL: nginx exited $rc" + tail -40 "$WORK/logs/error.log" || true + problems=1 +fi + +if [ "$problems" -ne 0 ]; then + echo "--- full valgrind/helgrind logs (for triage) ---" + cat "$WORK"/logs/valgrind.* "$WORK"/logs/helgrind.* 2>/dev/null || true + exit 1 +fi +echo "✓ soak clean: ${DURATION}s @ ${CONC} concurrent, no leak/race/crash, WAF verdicts held" diff --git a/tools/valgrind.suppress b/tools/valgrind.suppress new file mode 100644 index 0000000..5ce94c3 --- /dev/null +++ b/tools/valgrind.suppress @@ -0,0 +1,139 @@ +# Valgrind memcheck + helgrind suppressions for the ModSecurity-nginx soak +# (tools/soak.sh). +# +# nginx core does some startup/event-loop work (cycle init, epoll, sock_ntop, +# env, crc32 table) that memcheck flags as noise -- it's module-agnostic and +# matches every nginx build, not something this connector controls. +# +# Keep this file small: only add an entry once you've confirmed via +# --gen-suppressions=all that it is genuine third-party/nginx-core noise, not +# something the connector or libmodsecurity should actually be freeing. A +# real leak/race in ngx_http_modsecurity_* must still surface. +# +# Regenerate candidates with: +# USE_VALGRIND=1 tools/soak.sh 30 2 +# valgrind --gen-suppressions=all ... (rerun interactively to capture) + +{ + nginx-cycle-init-addr1 + Memcheck:Addr1 + fun:ngx_init_cycle + fun:ngx_master_process_cycle + fun:main +} +{ + nginx-cycle-init-addr4 + Memcheck:Addr4 + fun:ngx_init_cycle + fun:ngx_master_process_cycle + fun:main +} +{ + nginx-cycle-init-cond + Memcheck:Cond + fun:ngx_init_cycle + fun:ngx_master_process_cycle + fun:main +} +{ + nginx-sock-ntop-cond + Memcheck:Cond + fun:ngx_vslprintf + fun:ngx_snprintf + fun:ngx_sock_ntop + ... +} +{ + nginx-sock-ntop-addr1 + Memcheck:Addr1 + fun:ngx_vslprintf + fun:ngx_snprintf + fun:ngx_sock_ntop + ... +} +{ + nginx-conf-flush + Memcheck:Cond + fun:ngx_conf_flush_files + ... +} +{ + nginx-set-environment-leak + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + fun:ngx_alloc + fun:ngx_set_environment + ... +} +{ + nginx-create-pool-leak + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + fun:ngx_alloc + fun:ngx_create_pool + fun:main +} +{ + nginx-event-init-leak + Memcheck:Leak + fun:malloc + fun:ngx_alloc + fun:ngx_event_process_init +} +{ + nginx-crc32-table + Memcheck:Leak + match-leak-kinds: possible + fun:malloc + fun:ngx_alloc + fun:ngx_crc32_table_init + fun:main +} +{ + nginx-epoll-pwait-sigmask + Memcheck:Param + epoll_pwait(sigmask) + fun:epoll_pwait + ... +} +{ + nginx-epoll-ctl + Memcheck:Param + epoll_ctl(event) + fun:epoll_ctl + ... +} + +# --------------------------------------------------------------------------- +# libp11-kit (pulled in transitively via libcurl/openssl) does an invalid +# pthread_mutex_destroy() in its own atexit handler during process exit -- +# confirmed via --gen-suppressions=all against this exact soak. Runtime/ +# third-party noise at process teardown, not something the connector or +# nginx core controls. +# --------------------------------------------------------------------------- +{ + libp11-kit-exit-mutex-destroy-worker + Helgrind:Misc + obj:*/vgpreload_helgrind* + obj:*/libp11-kit.so* + fun:_dl_call_fini + fun:_dl_fini + fun:__run_exit_handlers + fun:exit + fun:ngx_worker_process_exit + ... +} +{ + libp11-kit-exit-mutex-destroy-master + Helgrind:Misc + obj:*/vgpreload_helgrind* + obj:*/libp11-kit.so* + fun:_dl_call_fini + fun:_dl_fini + fun:__run_exit_handlers + fun:exit + fun:ngx_master_process_exit + ... +}