Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,20 @@ jobs:
if: steps.src-tarball.outputs.cache-hit != 'true'
run: wget -q -O "${{ matrix.dir }}.tar.gz" "${{ matrix.url }}"

- name: Verify nginx tarball PGP signature
if: matrix.flavor == 'nginx'
run: |
set -euo pipefail
wget -q -O "${{ matrix.dir }}.tar.gz.asc" "${{ matrix.url }}.asc"
gnupghome="$(mktemp -d)"
export GNUPGHOME="$gnupghome"
chmod 700 "$gnupghome"
for keyfile in tools/keys/*.key; do
gpg --quiet --import "$keyfile" 2>/dev/null
done
gpg --quiet --verify "${{ matrix.dir }}.tar.gz.asc" "${{ matrix.dir }}.tar.gz"
rm -rf "$gnupghome"

- name: Configure (full debug build)
run: |
tar -xzf "${{ matrix.dir }}.tar.gz"
Expand Down Expand Up @@ -425,6 +439,20 @@ jobs:
if: steps.nginx-tarball-old.outputs.cache-hit != 'true'
run: wget -q "https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz"

- name: Verify nginx tarball PGP signature
run: |
set -euo pipefail
wget -q "https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz.asc" \
-O "nginx-${NGINX_VERSION}.tar.gz.asc"
gnupghome="$(mktemp -d)"
export GNUPGHOME="$gnupghome"
chmod 700 "$gnupghome"
for keyfile in tools/keys/*.key; do
gpg --quiet --import "$keyfile" 2>/dev/null
done
gpg --quiet --verify "nginx-${NGINX_VERSION}.tar.gz.asc" "nginx-${NGINX_VERSION}.tar.gz"
rm -rf "$gnupghome"

- name: Configure & build nginx with module against libzstd 1.4.x
run: |
tar -xzf "nginx-${NGINX_VERSION}.tar.gz"
Expand Down Expand Up @@ -501,6 +529,20 @@ jobs:
if: steps.nginx-tarball.outputs.cache-hit != 'true'
run: wget -q "https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz"

- name: Verify nginx tarball PGP signature
run: |
set -euo pipefail
wget -q "https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz.asc" \
-O "nginx-${NGINX_VERSION}.tar.gz.asc"
gnupghome="$(mktemp -d)"
export GNUPGHOME="$gnupghome"
chmod 700 "$gnupghome"
for keyfile in tools/keys/*.key; do
gpg --quiet --import "$keyfile" 2>/dev/null
done
gpg --quiet --verify "nginx-${NGINX_VERSION}.tar.gz.asc" "nginx-${NGINX_VERSION}.tar.gz"
rm -rf "$gnupghome"

- name: Configure & build nginx with ASAN + UBSAN
run: |
tar -xzf "nginx-${NGINX_VERSION}.tar.gz"
Expand Down
44 changes: 26 additions & 18 deletions .github/workflows/bump.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
name: Bump versions

# Weekly: check nginx.org/angie.software for newer nginx-stable/angie
# releases than what's pinned in ci-deep.yml's build-flavors matrix, and pull
# the vendored nginx-tests submodule to its upstream HEAD. nginx mainline is
# NOT touched here -- it's resolved dynamically at CI run time (see
# build-test.yml's `resolve` job / ci-deep.yml's per-job resolve steps), so
# there's no static mainline pin to bump.
# releases than what's pinned in ci-deep.yml's build-flavors matrix. nginx
# mainline is NOT touched here -- it's resolved dynamically at CI run time
# (see build-test.yml's `resolve` job / ci-deep.yml's per-job resolve steps),
# so there's no static mainline pin to bump.
#
# tools/bump-versions.sh does the actual work (also runnable locally with
# --dry-run to preview). Commits and pushes straight to master -- this repo
# has no branch protection gate for this path, and a version/digest/submodule
# bump is exactly the kind of low-risk, easily-revertable change that doesn't
# need a human in the loop before landing. The next build-test.yml run on
# master is what actually proves the bump didn't break anything.
# --dry-run to preview). Opens a PR rather than pushing straight to master --
# master carries a required-pull-request ruleset with no bypass actor, so a
# direct push is always rejected (audit sha e289021 F4); the bot instead
# pushes a branch and opens a PR via BUMP_PR_TOKEN (a classic PAT -- the
# default GITHUB_TOKEN cannot create PRs on myguard-labs repos). Normal
# review/required-checks still gate the merge.
#
# Action pins (keep in sync with build-test.yml):
# actions/checkout@v5 -> 93cb6efe18208431cddfb8368fd83d5badbf9bfd
Expand All @@ -28,36 +28,44 @@ concurrency:
cancel-in-progress: true

permissions:
contents: write
contents: read

jobs:
bump:
name: Bump nginx-stable/angie/nginx-tests pins
name: Bump nginx-stable/angie pins
runs-on: [self-hosted, builder02, lxc]
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
persist-credentials: true
submodules: true
persist-credentials: false
fetch-depth: 0
Comment on lines 39 to 43

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

sed -n '1,220p' .github/workflows/bump.yml

Repository: myguard-labs/nginx-zstd-module

Length of output: 2744


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path
p = Path('.github/workflows/bump.yml')
text = p.read_text()
for i, line in enumerate(text.splitlines(), 1):
    if 1 <= i <= 220:
        print(f"{i:4}: {line}")
PY

Repository: myguard-labs/nginx-zstd-module

Length of output: 3170


🏁 Script executed:

sed -n '1,220p' tools/bump-versions.sh

Repository: myguard-labs/nginx-zstd-module

Length of output: 7599


Authenticate git before the push. persist-credentials: false removes checkout’s credential helper, so git push origin "$branch" needs a git auth step after GH_TOKEN is set (gh auth setup-git or equivalent). Ensure BUMP_PR_TOKEN can write to the repo.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/bump.yml around lines 39 - 43, Update the bump workflow’s
authentication setup after GH_TOKEN is initialized to configure Git credentials
before the git push, using gh auth setup-git or an equivalent mechanism. Ensure
the BUMP_PR_TOKEN has repository write permission, while preserving the checkout
step’s disabled credential persistence.


- name: Run bump script
id: bump
run: bash tools/bump-versions.sh

- name: Commit and push if anything changed
- name: Open PR if anything changed
env:
GH_TOKEN: ${{ secrets.BUMP_PR_TOKEN }}
run: |
set -euo pipefail
if [ -z "$(git status --porcelain)" ]; then
echo "nothing changed, skipping commit"
echo "nothing changed, skipping PR"
exit 0
fi
branch="bump/versions-$(date -u +%Y%m%d)"
git config user.name "myguard-bump-bot"
git config user.email "actions@users.noreply.github.com"
git checkout -b "$branch"
git add -A
git commit -m "chore: bump nginx-stable/angie/nginx-tests pins
git commit -m "chore: bump nginx-stable/angie pins

Automated weekly version check (tools/bump-versions.sh)."
git push origin HEAD:master
git push origin "$branch"
gh pr create \
--title "chore: bump nginx-stable/angie pins ($(date -u +%Y-%m-%d))" \
--body "Automated weekly version check (tools/bump-versions.sh). Review the diff and let required CI checks gate the merge." \
--base master \
--head "$branch"
Comment on lines +58 to +71

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Make the bump branch and PR flow idempotent.

The date-only branch name collides whenever the workflow is rerun on the same UTC day. git checkout -b or the subsequent push will then fail if that branch or its PR already exists. Use a run-unique branch suffix or detect and update an existing branch/PR before creating a new one.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/bump.yml around lines 58 - 71, Update the branch naming
and PR creation flow around the branch variable, git checkout, git push, and gh
pr create so rerunning the workflow on the same UTC day cannot collide with an
existing branch or pull request. Use a run-unique suffix for the bump branch, or
explicitly detect and update the existing branch and PR before creation, while
preserving the current automated commit and PR metadata.

27 changes: 19 additions & 8 deletions .github/workflows/ci-deep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ name: CI Deep

# Exhaustive dynamic analysis — monthly cron + on-demand only, NEVER on PR.
# The fast PR-time gates live in standalone workflows: fuzzing.yml (120s fuzz
# regression), valgrind.yml (60s memcheck-lite soak) and security-scanners.yml
# (scanners) run on every PR/push; THIS file stays the exhaustive monthly +
# workflow_dispatch run. codeql.yml is dropped entirely:
# CodeQL / the GitHub Security tab is GitHub-Advanced-Security only and does
# nothing on a private repo, so the SARIF-upload path is removed too — the
# scanners still run and GATE on findings, they just don't upload.
# regression), valgrind.yml (60s memcheck-lite soak), security-scanners.yml
# (scanners), and codeql.yml (semantic analysis) all run on every PR/push;
# THIS file stays the exhaustive monthly + workflow_dispatch run: the
# multi-flavor build matrix (mainline/stable/angie) plus hours-long fuzzing
# and full Memcheck/Helgrind soaks that would blow the PR-gate time budget.
#
# Jobs: build-flavors (nginx mainline/stable + angie) · fuzz (14400s/target) ·
# memcheck soak · helgrind soak · scanners.
Expand Down Expand Up @@ -130,11 +129,23 @@ jobs:
sudo apt-get install -y clang

- name: Select fuzz duration
env:
RAW_FUZZ_SECONDS: ${{ github.event.inputs.fuzz_seconds }}
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "FUZZ_SECS=${{ github.event.inputs.fuzz_seconds }}" >> "$GITHUB_ENV"
case "$RAW_FUZZ_SECONDS" in
''|*[!0-9]*)
echo "::error::fuzz_seconds must be a non-negative integer, got: $RAW_FUZZ_SECONDS"
exit 1
;;
esac
if [ "$RAW_FUZZ_SECONDS" -gt 14400 ]; then
echo "::error::fuzz_seconds exceeds the 14400s (4h) budget: $RAW_FUZZ_SECONDS"
exit 1
fi
printf 'FUZZ_SECS=%s\n' "$RAW_FUZZ_SECONDS" >> "$GITHUB_ENV"
Comment on lines +136 to +146

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reject oversized numeric inputs before the -gt comparison.

The digit-only check still accepts arbitrarily long values; once they overflow the shell’s integer comparison, the -gt 14400 guard can fail open and the invalid value gets written to GITHUB_ENV. Use a bounded parse or a string-length check after trimming leading zeroes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci-deep.yml around lines 136 - 146, Update the
RAW_FUZZ_SECONDS validation before the numeric -gt comparison to reject values
exceeding the allowed range even when they overflow shell integer parsing. After
validating digits, trim leading zeroes and enforce a bounded length or
equivalent safe parse, while preserving valid zero-padded values and the
existing 14400-second maximum before writing FUZZ_SECS to GITHUB_ENV.

else
echo "FUZZ_SECS=3600" >> "$GITHUB_ENV"
printf 'FUZZ_SECS=3600\n' >> "$GITHUB_ENV"
fi

- name: Build fuzz target
Expand Down
14 changes: 14 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,20 @@ jobs:
if: steps.nginx-tarball.outputs.cache-hit != 'true'
run: wget -q "https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz"

- name: Verify nginx tarball PGP signature
run: |
set -euo pipefail
wget -q "https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz.asc" \
-O "nginx-${NGINX_VERSION}.tar.gz.asc"
gnupghome="$(mktemp -d)"
export GNUPGHOME="$gnupghome"
chmod 700 "$gnupghome"
for keyfile in tools/keys/*.key; do
gpg --quiet --import "$keyfile" 2>/dev/null
done
gpg --quiet --verify "nginx-${NGINX_VERSION}.tar.gz.asc" "nginx-${NGINX_VERSION}.tar.gz"
rm -rf "$gnupghome"

- name: Initialize CodeQL
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/security-scanners.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ jobs:
ver=$(curl -fsSL https://nginx.org/en/download.html \
| grep -oP 'nginx-\K[0-9]+\.[0-9]+\.[0-9]+(?=\.tar\.gz)' | sort -V | tail -1)
wget -q "https://nginx.org/download/nginx-${ver}.tar.gz"
wget -q "https://nginx.org/download/nginx-${ver}.tar.gz.asc" -O "nginx-${ver}.tar.gz.asc"
gnupghome="$(mktemp -d)"
export GNUPGHOME="$gnupghome"
chmod 700 "$gnupghome"
for keyfile in "$GITHUB_WORKSPACE"/tools/keys/*.key; do
gpg --quiet --import "$keyfile" 2>/dev/null
Comment on lines +56 to +61

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- security-scanners.yml ---'
nl -ba .github/workflows/security-scanners.yml | sed -n '1,180p'

echo
echo '--- valgrind.yml ---'
nl -ba .github/workflows/valgrind.yml | sed -n '1,180p'

Repository: myguard-labs/nginx-zstd-module

Length of output: 238


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

for path in [Path('.github/workflows/security-scanners.yml'), Path('.github/workflows/valgrind.yml')]:
    print(f'--- {path} ---')
    lines = path.read_text().splitlines()
    for i, line in enumerate(lines, 1):
        if 1 <= i <= 220:
            print(f'{i:4}: {line}')
    print()
PY

Repository: myguard-labs/nginx-zstd-module

Length of output: 8976


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

for path in [Path('.github/workflows/security-scanners.yml'), Path('.github/workflows/valgrind.yml')]:
    print(f'=== {path} ===')
    text = path.read_text().splitlines()
    for start in [1, 41, 81, 121, 161]:
        if start > len(text):
            continue
        end = min(start + 59, len(text))
        print(f'--- lines {start}-{end} ---')
        for i in range(start, end + 1):
            print(f'{i:4}: {text[i-1]}')
    print()
PY

Repository: myguard-labs/nginx-zstd-module

Length of output: 12578


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

for path in [Path('.github/workflows/security-scanners.yml'), Path('.github/workflows/valgrind.yml')]:
    print(f'=== {path} ===')
    lines = path.read_text().splitlines()
    for i, line in enumerate(lines, 1):
        if 'gpg' in line or 'apt' in line or 'apk' in line or 'yum' in line or 'dnf' in line or 'brew' in line or 'pacman' in line or 'zypper' in line or 'apt-get' in line:
            lo = max(1, i - 8)
            hi = min(len(lines), i + 8)
            print(f'--- context around line {i} ---')
            for j in range(lo, hi + 1):
                print(f'{j:4}: {lines[j-1]}')
    print()
PY

Repository: myguard-labs/nginx-zstd-module

Length of output: 7933


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

for path in [Path('.github/workflows/security-scanners.yml'), Path('.github/workflows/valgrind.yml')]:
    print(f'=== {path} ===')
    lines = path.read_text().splitlines()
    for i, line in enumerate(lines, 1):
        if any(tok in line for tok in ['gpg', 'apt', 'apk', 'yum', 'dnf', 'brew', 'pacman', 'zypper', 'apt-get']):
            lo = max(1, i - 8)
            hi = min(len(lines), i + 8)
            print(f'--- context around line {i} ---')
            for j in range(lo, hi + 1):
                print(f'{j:4}: {lines[j-1]}')
    print()
PY

Repository: myguard-labs/nginx-zstd-module

Length of output: 7933


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

for path in Path('.github/workflows').glob('*.yml'):
    text = path.read_text()
    if 'gpg' in text or 'gnupg' in text:
        print(f'=== {path} ===')
        for i, line in enumerate(text.splitlines(), 1):
            if 'gpg' in line or 'gnupg' in line:
                print(f'{i:4}: {line}')
        print()
PY

Repository: myguard-labs/nginx-zstd-module

Length of output: 2281


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

for path in Path('.').rglob('*'):
    if path.is_file() and path.suffix in {'.yml', '.yaml', '.md', '.txt'}:
        try:
            text = path.read_text()
        except Exception:
            continue
        if 'gpg' in text or 'gnupg' in text:
            print(f'=== {path} ===')
            for i, line in enumerate(text.splitlines(), 1):
                if 'gpg' in line or 'gnupg' in line:
                    print(f'{i:4}: {line}')
            print()
PY

Repository: myguard-labs/nginx-zstd-module

Length of output: 2281


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

patterns = ['gnupg', 'gpg', 'builder02', 'lxc', 'self-hosted', 'apt-get install -y', 'apt install -y']
for path in Path('.').rglob('*'):
    if not path.is_file():
        continue
    if path.suffix.lower() not in {'.yml', '.yaml', '.md', '.txt', '.adoc'}:
        continue
    try:
        lines = path.read_text(errors='ignore').splitlines()
    except Exception:
        continue
    hits = []
    for i, line in enumerate(lines, 1):
        if any(p in line for p in patterns):
            hits.append((i, line))
    if hits:
        print(f'=== {path} ===')
        for i, line in hits[:80]:
            print(f'{i:4}: {line}')
        print()
PY

Repository: myguard-labs/nginx-zstd-module

Length of output: 4408


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

targets = [Path('README.md'), Path('docs'), Path('.github')]
for root in targets:
    if root.is_file():
        paths = [root]
    elif root.is_dir():
        paths = [p for p in root.rglob('*') if p.is_file()]
    else:
        continue

    for path in paths:
        try:
            text = path.read_text(errors='ignore')
        except Exception:
            continue
        if any(k in text.lower() for k in ['gpg', 'gnupg', 'builder02', 'lxc', 'self-hosted runner', 'runner image', 'preinstalled']):
            print(f'=== {path} ===')
            for i, line in enumerate(text.splitlines(), 1):
                if any(k in line.lower() for k in ['gpg', 'gnupg', 'builder02', 'lxc', 'self-hosted runner', 'runner image', 'preinstalled']):
                    print(f'{i:4}: {line}')
            print()
PY

Repository: myguard-labs/nginx-zstd-module

Length of output: 3490


Install gpg before the verification steps. Both workflows call gpg --import/--verify, but neither job’s dependency list includes it.

📍 Affects 2 files
  • .github/workflows/security-scanners.yml#L56-L61 (this comment)
  • .github/workflows/valgrind.yml#L59-L65
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/security-scanners.yml around lines 56 - 61, Install the
gpg package in the dependency setup for both verification workflows before their
gpg usage: update .github/workflows/security-scanners.yml at lines 56-61 and
.github/workflows/valgrind.yml at lines 59-65. Ensure the existing gpg --import
and gpg --verify steps can run without changing their verification behavior.

done
gpg --quiet --verify "nginx-${ver}.tar.gz.asc" "nginx-${ver}.tar.gz"
rm -rf "$gnupghome"
Comment on lines +57 to +64

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== security-scanners.yml ==\n'
sed -n '1,180p' .github/workflows/security-scanners.yml

printf '\n== valgrind.yml ==\n'
sed -n '1,180p' .github/workflows/valgrind.yml

Repository: myguard-labs/nginx-zstd-module

Length of output: 7796


Install an EXIT trap for GNUPGHOME.

A failed key import or signature check skips the final rm -rf, leaving temp GnuPG directories behind on self-hosted runners.

  • .github/workflows/security-scanners.yml#L57-L64: add trap 'rm -rf "$gnupghome"' EXIT right after creating gnupghome.
  • .github/workflows/valgrind.yml#L61-L68: add the same trap and drop the success-only cleanup.
📍 Affects 2 files
  • .github/workflows/security-scanners.yml#L57-L64 (this comment)
  • .github/workflows/valgrind.yml#L61-L68
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/security-scanners.yml around lines 57 - 64, In the GnuPG
setup around gnupghome in .github/workflows/security-scanners.yml lines 57-64,
add an EXIT trap immediately after creating the temporary directory and retain
the existing cleanup as appropriate. Apply the same trap in
.github/workflows/valgrind.yml lines 61-68, and remove that workflow’s
success-only rm -rf cleanup so both workflows clean up on success and failure.

tar -xzf "nginx-${ver}.tar.gz"; cd "nginx-${ver}"
CC=gcc ./configure --with-compat --add-dynamic-module="$GITHUB_WORKSPACE" >/dev/null
echo "NGINX_SRC_TREE=$PWD" >> "$GITHUB_ENV"
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/valgrind.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ jobs:
test -f "nginx-${NGINX_VERSION}.tar.gz" || \
wget -q -O "nginx-${NGINX_VERSION}.tar.gz" \
"https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz"
wget -q "https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz.asc" \
-O "nginx-${NGINX_VERSION}.tar.gz.asc"
gnupghome="$(mktemp -d)"
export GNUPGHOME="$gnupghome"
chmod 700 "$gnupghome"
for keyfile in tools/keys/*.key; do
gpg --quiet --import "$keyfile" 2>/dev/null
done
gpg --quiet --verify "nginx-${NGINX_VERSION}.tar.gz.asc" "nginx-${NGINX_VERSION}.tar.gz"
rm -rf "$gnupghome"
tar -xzf "nginx-${NGINX_VERSION}.tar.gz"
cd "nginx-${NGINX_VERSION}"
./configure \
Expand Down
8 changes: 6 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,12 @@ Your PR merges when **all** checks are green. If a gate fails and you
believe the gate is wrong, say so in the PR — with evidence, not vibes.

Before pushing a parser or fuzz-harness change, fuzz locally first:
`fuzz/build.sh` compiles every `fuzz/fuzz_*.c` with `-Werror`, so a stale
harness signature fails right there instead of in CI.
`fuzz/build.sh` compiles the target under ASAN+UBSAN
(`-fsanitize=address,undefined`), so a stale harness signature or a
sanitizer-caught bug surfaces right there instead of in CI. (The
module's own C sources are compiled `-Werror` — see
[`build-test.yml`](.github/workflows/build-test.yml) — the fuzz
harness itself is not.)

## Coding conventions

Expand Down
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
[![CI Deep](https://github.com/myguard-labs/nginx-zstd-module/actions/workflows/ci-deep.yml/badge.svg)](https://github.com/myguard-labs/nginx-zstd-module/actions/workflows/ci-deep.yml)
[![CodeQL](https://github.com/myguard-labs/nginx-zstd-module/actions/workflows/codeql.yml/badge.svg)](https://github.com/myguard-labs/nginx-zstd-module/actions/workflows/codeql.yml)

📖 **Background reading:**
📖 **Background reading:**
- [zstd nginx module: what it does, bugs fixed](https://deb.myguard.nl/2026/05/zstd-nginx-module-what-it-does-bugs-fixed/)
- [nginx zstd vs brotli vs zlib-ng — a compression comparison](https://deb.myguard.nl/2026/05/nginx-zstd-vs-brotli-vs-zlib-ng-compression/)

# zstd-nginx-module

An nginx module for [Zstandard (zstd)](https://facebook.github.io/zstd/) compression. Zstandard typically achieves better compression ratios than gzip at comparable or faster speeds, making it a good choice for reducing transmitted response sizes.

This is a hardened fork: every build is exercised against **nginx mainline and [Angie](https://angie.software/)**, the full test suite runs under **ASAN/UBSAN**, the `Accept-Encoding` parser is **continuously fuzzed**, and flawfinder/semgrep/clang-tidy run on every change (see the badges above and [Testing & CI](#testing--ci)).
This is a hardened fork: every push/PR is exercised against **nginx mainline**, the full test suite runs under **ASAN/UBSAN**, the `Accept-Encoding` parser is **continuously fuzzed**, flawfinder/semgrep/clang-tidy run on every change, and a monthly deep pass additionally covers **nginx stable** and **[Angie](https://angie.software/)** (see the badges above and [Testing & CI](#testing--ci)).

# Table of Contents

Expand Down Expand Up @@ -173,7 +173,7 @@ load_module modules/ngx_http_zstd_static_module.so;

| Component | Minimum | Recommended | CI-verified |
|---|---|---|---|
| **nginx** | 1.9.11 (first `--add-dynamic-module` release) | latest mainline / stable | **1.31.0 mainline** |
| **nginx** | 1.9.11 (first `--add-dynamic-module` release) | latest mainline / stable | **latest mainline** (resolved dynamically every CI run, not pinned) + **stable** and **Angie**, both pinned in CI Deep's monthly matrix |
| **Angie** | 1.x | latest | **1.11.5** |
| **libzstd** | **1.4.0** | **≥ 1.5.6** | 1.5.x (full suite) + **1.4.x** fallback-paths build |
| **OS** | Linux/BSD/RHEL-family | — | Ubuntu (GitHub runners) |
Expand Down Expand Up @@ -843,16 +843,19 @@ log_format zstd '$request in=$zstd_bytes_in out=$zstd_bytes_out '

# Testing & CI

Five workflows guard the module (badges at the top): four gate every
push & PR, and CI Deep runs the exhaustive monthly pass:
Six workflows guard the module (badges at the top): five gate every
push & PR, and CI Deep runs the exhaustive monthly pass. A seventh,
Bump, runs weekly but only opens a version-bump PR — it does not gate
anything itself.

| Workflow | Cadence | What it does |
|---|---|---|
| **Build & Test** | every push & PR | Compiles the module against **nginx 1.31.0 mainline** and **Angie 1.11.5** with strict `-Werror` flags, then runs the full test suite: 46 `Test::Nginx::Socket` filter tests, 21 static-module tests, and end-to-end Python smoke tests (truncation, `Vary`, boundary sizes, repeated/concurrent requests, terminal-frame, the proxy-unbuffered and compression-matrix regressions, per-request CCtx isolation, reload-under-load, `zstd_long`/LDM, `$zstd_ratio`). A separate matrix entry rebuilds against **libzstd 1.4.x** (from source) to exercise the `< 1.5.6` and `≥ 1.4.0` fallback paths, and a parallel job rebuilds with **ASAN+UBSAN** and re-runs the smoke tests plus a `zstd_dict_file` config-reload leak check. A 10-minute mixed-load soak under ASAN+UBSAN runs on the weekly schedule. |
| **Build & Test** | every push & PR | Compiles the module against **nginx mainline** (resolved at run time — see [Compatibility](#compatibility)) with strict `-Werror` flags, then runs the full test suite: 79 `Test::Nginx::Socket` filter tests, 28 static-module tests, 4 config-warning tests, and end-to-end Python smoke tests (truncation, `Vary`, boundary sizes, repeated/concurrent requests, terminal-frame, the proxy-unbuffered and compression-matrix regressions, per-request CCtx isolation, reload-under-load, `zstd_long`/LDM, `$zstd_ratio`). A separate matrix entry rebuilds against **libzstd 1.4.x** (from source) to exercise the `< 1.5.6` and `≥ 1.4.0` fallback paths, and a parallel job rebuilds with **ASAN+UBSAN** and re-runs the smoke tests plus a `zstd_dict_file` config-reload leak check. **Angie is not built here** — see CI Deep below, the only workflow that exercises it. |
| **Security scanners** | every push & PR | flawfinder, clang-tidy (`cert-*`, `clang-analyzer-security.*`), and semgrep, with the reports uploaded as build artifacts. |
| **Fuzzing** | every push & PR | A 120-second libFuzzer regression run for the `ngx_http_zstd_accept_encoding()` / `ngx_http_zstd_eval_qvalue()` RFC 9110 `Accept-Encoding`/q-value parser. The fuzz target is sliced from the shipped header at build time, so there is no copy drift. See [`fuzz/README.md`](fuzz/README.md). |
| **Valgrind** | every push & PR | A 60-second Memcheck-lite soak against a debug nginx build, catching uninitialised-value reads and leaks that ASAN cannot. |
| **CI Deep** | monthly + manual dispatch | The exhaustive run: hours-long fuzzing on the same target, full Memcheck and Helgrind soaks (a valgrind soak is ~20–50× slower than native), and the same security scanners. |
| **CodeQL** | every push & PR + monthly | GitHub's semantic C/C++ analysis (`security-extended` query pack) over the filter/static module sources. |
| **CI Deep** | monthly + manual dispatch | The exhaustive run: a `build-flavors` matrix that compiles and runs the full `Test::Nginx::Socket` suite against **nginx mainline, nginx stable, and Angie** (the only workflow that builds Angie at all), hours-long fuzzing on the same target, full Memcheck and Helgrind soaks (a valgrind soak is ~20–50× slower than native), and the same security scanners. |

The test suite includes a dedicated regression test for every known
historical bug class:
Expand Down
3 changes: 2 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,5 @@ bash fuzz/build.sh && ./fuzz/fuzz_accept_encoding -max_total_time=60 fuzz/corpus
```

See [`.github/workflows/`](.github/workflows/) (build-test, codeql,
security-scanners, fuzzing) and [`AGENTS.md`](AGENTS.md).
security-scanners, fuzzing, valgrind, ci-deep, bump) and the
[Testing & CI](README.md#testing--ci) section of the README.
14 changes: 7 additions & 7 deletions fuzz/extract_parser.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,18 @@ awk '
print
if ($0 == "}") { capture = 0 }
}
' "$HEADER" > "$OUT"
' "$HEADER" >"$OUT"

if ! grep -q 'ngx_http_zstd_skip_quoted' "$OUT" \
|| ! grep -q 'ngx_http_zstd_eval_qvalue' "$OUT" \
|| ! grep -q 'ngx_http_zstd_accept_encoding' "$OUT" \
|| [ "$(tail -n1 "$OUT")" != "}" ]; then
if ! grep -q 'ngx_http_zstd_skip_quoted' "$OUT" ||
! grep -q 'ngx_http_zstd_eval_qvalue' "$OUT" ||
! grep -q 'ngx_http_zstd_accept_encoding' "$OUT" ||
[ "$(tail -n1 "$OUT")" != "}" ]; then
echo "✗ failed to extract the Accept-Encoding parser from $HEADER" >&2
echo " (header layout changed? update extract_parser.sh)" >&2
rm -f "$OUT"
exit 1
fi

LINES=$(wc -l < "$OUT")
LINES=$(wc -l <"$OUT")
echo "✓ extracted ngx_http_zstd_skip_quoted() + ngx_http_zstd_eval_qvalue()" \
"+ ngx_http_zstd_accept_encoding() — $LINES lines -> $OUT"
"+ ngx_http_zstd_accept_encoding() — $LINES lines -> $OUT"
13 changes: 13 additions & 0 deletions ngx_http_zstd_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,19 @@ ngx_http_zstd_eval_qvalue(ngx_str_t *ae, u_char *p)
while (p < end && (*p == ' ' || *p == '\t')) {
p++;
}

/*
* After the OWS that may trail any parameter (its q-specific
* check above only catches junk BEFORE this OWS skip, e.g.
* "q=1x"), only ';' (another parameter), ',' (next element), or
* end may follow -- anything else is trailing junk the loop's own
* "while (*p == ';')" condition would otherwise silently accept
* as "no more parameters" instead of rejecting (e.g.
* "zstd;q=1 garbage" previously parsed as zstd;q=1).
*/
if (p < end && *p != ';' && *p != ',') {
return -1;
}
}

return q;
Expand Down
Loading
Loading