← Back to Feed CACHED · 2026-10-15 16:35:43 · CACHE_KEY CVE-2026-85091
CVE-2026-85091 · CWE-787 · Disclosed 2026-09-03

zlib gz_vacate() heap buffer overflow via non-blocking gzprintf after write stall

ASSESSED — NOISGATE
Vendor
—
—
—
Reassessed
—
—
—
Verdict: —
Do you agree?
01 · The Real Story

A fire alarm wired only to a room nobody uses

CVE-2026-85091 is a heap buffer overflow in zlib's gz_vacate() function, affecting versions 1.3.1.2 through 1.3.2 only. The flaw lives in the *non-blocking gzwrite* path — a feature added just months ago via the "N" mode flag in gzopen(). When gzwrite() stalls on a non-blocking descriptor, subsequent gzprintf() or gzvprintf() calls trigger gz_vacate() with a pending-input buffer larger than state->size. An inverted state->again check lets execution fall through into two sequential out-of-bounds heap writes: a null-terminator past the buffer end, then up to ~8 KB of attacker-influenced data via vsnprintf(). Under the right conditions, this is a real heap corruption primitive.

The vendor CVSS of 7.4 HIGH (AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:H) and VulnCheck's CVSS v4 of 8.3 rate the *theoretical* ceiling of a network-reachable heap overflow in a ubiquitous library — and that ceiling is real. But the score ignores the brutal narrowing: the vulnerable code path requires an application that (a) links against the exact 1.3.1.2–1.3.2 window, (b) explicitly opts into the brand-new non-blocking gz* mode, and (c) calls gzprintf()/gzvprintf() after a write stall. nginx — the single largest zlib consumer — confirmed it **never calls the gz* file-handle API. Ubuntu's security team independently triaged this as Medium** priority. Multiple engineering teams (Prowler, govie-ds, Docker Hardened Images) are actively suppressing this CVE in container scans as non-exploitable in their stacks. The vendor score is technically defensible on paper but dramatically overstates the risk most defenders actually face.

"Heap overflow in zlib's niche non-blocking gz API — most apps never touch the vulnerable code path"
02 · The Attack Path

5 steps from start to impact.

STEP 01

Target runs zlib 1.3.1.2–1.3.2

The attacker must find a host running the narrow affected version window. Versions prior to 1.3.1.2 lack the non-blocking gz* code entirely — the vulnerable gz_vacate() compaction logic does not exist. Most production Linux distributions still ship zlib 1.2.x or 1.3.1. Alpine 3.22+ and some bleeding-edge container images ship 1.3.2.
Conditions required:
  • Target host links against zlib >= 1.3.1.2 and <= 1.3.2
Where this breaks in practice:
  • The majority of enterprise fleets run older zlib (1.2.11–1.3.1). Debian bookworm ships 1.2.13, RHEL 9 ships 1.2.11.
  • Container images using Alpine 3.21 or earlier are unaffected.
Detection/coverage: Version-based detection via dpkg -l zlib1g, rpm -q zlib, apk info zlib, or SCA scanners (Trivy, Grype, Snyk). All major scanners flag this CVE.
STEP 02

Application uses non-blocking gz* file-handle API

The application must open a gzFile with the "N" (non-blocking) mode flag, or call gzdopen() on an already-O_NONBLOCK file descriptor. This is a brand-new feature introduced in zlib 1.3.1.2. The overwhelming majority of zlib consumers use the lower-level deflate()/inflate() streaming API or blocking gzopen() calls. Logging proxies and streaming compressors writing to pipes or sockets are the plausible (but still rare) consumers.
Conditions required:
  • Application explicitly opts into non-blocking gz* mode
  • Application calls gzwrite() on this handle
Where this breaks in practice:
  • nginx, curl, Python's zlib module, Go's compress/zlib, Java's java.util.zip — none use the gz* file-handle layer for non-blocking I/O.
  • The 'N' mode is undocumented in most language bindings; adoption is near-zero outside bespoke C code.
Detection/coverage: Static analysis or strace/ltrace to confirm gzopen() calls with 'N' flag. No standard scanner checks for this API usage pattern.
STEP 03

Write stall occurs on non-blocking descriptor

The non-blocking gzwrite() must encounter an EAGAIN / EWOULDBLOCK condition, causing it to return a partial count and leave more than state->size bytes of pending input with strm->next_in still pointing to caller-owned memory. This requires the output fd to have backpressure — a full pipe buffer, congested socket, or slow consumer.
Conditions required:
  • Output fd returns EAGAIN during gzwrite()
  • Pending input exceeds state->size after the stall
Where this breaks in practice:
  • Applications writing to local files (the common case) never see EAGAIN.
  • Even pipe/socket writers may never stall under normal load.
STEP 04

Subsequent gzprintf()/gzvprintf() triggers overflow

After the stall, the application calls gzprintf() or gzvprintf(). The inverted state->again check in gz_vacate() lets execution proceed instead of returning early. The function computes a next pointer past the allocated buffer, writes a null terminator out of bounds, then calls vsnprintf() writing up to ~8 KB of data past the heap buffer boundary. This corrupts adjacent heap metadata/objects.
Conditions required:
  • Application calls gzprintf() or gzvprintf() after the stall — not gzwrite()
  • Attacker has some influence over the format string arguments to shape the overflow
Where this breaks in practice:
  • Mixing gzwrite() with gzprintf() on the same handle is unusual — most applications use one or the other consistently.
  • Controlling the overflow content enough for reliable RCE requires deep knowledge of the target's heap layout.
Detection/coverage: ASan/heap-canary builds detect the overflow immediately. Production binaries without hardening are blind.
STEP 05

Heap corruption → crash or code execution

The two sequential OOB writes corrupt heap metadata. In the best case for the attacker, this leads to arbitrary write or code execution via corrupted function pointers or heap management structures. In the common case, it's a process crash (DoS). Reliable exploitation requires heap grooming and architecture-specific techniques.
Conditions required:
  • Target binary lacks heap hardening (glibc tcache, jemalloc randomization, stack canaries)
  • Attacker can retry or predict heap layout
Where this breaks in practice:
  • Modern allocators (glibc 2.34+ tcache hardening, musl's simple allocator) and ASLR make heap exploitation unreliable.
  • Most affected Alpine containers use musl libc, which has a different heap layout than the PoC environment.
Detection/coverage: Crash monitoring, core dump analysis, and heap integrity checks (libasan in dev, Valgrind).
03 · Compensating Control

1
MEDIUM 5.3→IGNORE 0.0
SEVERITY REDUCED
Audit your fleet for non-blocking gz* API usage — Before patching, determine if any application in your environment actually calls gzopen() with mode "N" or gzdopen() on a non-blocking fd. Run grep -r 'gzopen\|gzdopen' /path/to/source on your codebases, or use ltrace -e 'gzopen*' <binary> at runtime. If zero applications use this API, the CVE is a scanner false positive for your environment and you can document the exception. This is a no-SLA item per noisgate — go straight to the 365-day remediation window.
2
MEDIUM 5.3→IGNORE 0.0
SEVERITY REDUCED
Update Alpine containers to zlib 1.3.2-r1 — For container workloads on Alpine 3.22+, update zlib to 1.3.2-r1 which includes the upstream fix. Add RUN apk upgrade --no-cache zlib to Dockerfiles or rebuild base images. This eliminates the vulnerability at the library level. Target within the noisgate remediation SLA of 365 days for MEDIUM findings — though container rebuild cycles typically accomplish this much sooner.
3
MEDIUM 5.3→IGNORE 0.0
SEVERITY REDUCED
Pin zlib to 1.3.1 in new builds if 1.3.2-r1 is unavailable — If your distro hasn't shipped the fix (Debian, Ubuntu as of 2026-10-16), pin zlib to version 1.3.1 which predates the non-blocking gz* code entirely. The omnistrate-ctl project took this approach via PR #726. This removes the vulnerable code path completely. Deploy within the 365-day remediation window.
4
MEDIUM 5.3→MEDIUM 4.0
Compile with AddressSanitizer in staging for affected binaries — If you have custom C code that *does* use non-blocking gz* mode, compile with -fsanitize=address in staging/CI environments. ASan detects the heap overflow immediately and converts a silent corruption into a loud, deterministic crash with a stack trace. This won't help in production but catches the issue before deployment.
What doesn't work
  • WAF / network-layer filtering — This is a library-level memory corruption bug, not a network protocol flaw. There is no HTTP request pattern or network signature to block.
  • Disabling gzip compression in nginx/Apache — These servers use the deflate API, not the gz* file-handle API. They were never vulnerable regardless of compression settings.
  • ASLR alone — ASLR raises the bar for reliable RCE but does not prevent the heap overflow or crash. The ~8 KB write primitive can still corrupt adjacent objects.
04 · Intelligence Metadata

The supporting signals.

In-the-wild exploitationNone observed. Not listed on CISA KEV. No campaigns, APT advisories, or incident reports reference this CVE as of 2026-10-16.
Proof-of-conceptA PoC gist by thesmartshadow (Ali Firas) exists at gist.github.com/thesmartshadow/e0b9481792afb7c31e86fee1ff084490. The upstream issue #1256 (filed by mat-mo) provides detailed reproduction steps and ASan traces. No weaponized exploit or Metasploit/Nuclei module exists. Not indexed on pocindex.io.
EPSS0.00356 (0.356% probability of exploitation in next 30 days). This places it roughly in the ~66th percentile — well below the threshold where EPSS signals real-world activity.
CISA KEVNot listed. No binding operational directive applies.
CVSS vectorsv3.1: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:H (7.4 HIGH) — note AC:H (high complexity) and C:N (no confidentiality impact). v4.0: 8.3 HIGH per VulnCheck.
Affected versionszlib 1.3.1.2 through 1.3.2. The non-blocking gz* code was introduced in commit 81cc0beb. Versions ≤ 1.3.1 and the entire 1.2.x series are not affected.
Fixed versionsUpstream fix: commit df84af25dc (2026-09-17). Alpine: 1.3.2-r1. Debian/Ubuntu: no fix shipped yet (deferred). No formal zlib point release (e.g. 1.3.3) has been tagged.
Scanning / exposureNo GreyNoise or Shodan signatures exist for this CVE — it is a *library* vulnerability, not a network-service flaw. SCA scanners (Trivy, Grype, Snyk, Mend) all flag zlib 1.3.2 containers. Docker Hardened Images catalog rates nginx-alpine images at zero vulns (nginx doesn't call gz* API).
Disclosure timeline2026-09-03: CVE published (CNA: VulnCheck). 2026-09-11: NVD last modified. 2026-09-17: upstream fix merged by Mark Adler. 2026-10-07: Alpine ships 1.3.2-r1.
ReporterAli Firas (GitHub: thesmartshadow) — coordinated via VulnCheck CNA. Separately, mat-mo filed issue #1256 with detailed technical analysis of the inverted state->again check.

Sources.

  1. NVD / CVE Record
  2. Upstream GitHub Issue #1310
  3. Upstream GitHub Issue #1256 (root cause analysis)
  4. Fix PR #1317 by thesmartshadow
  5. Debian Security Tracker
  6. Ubuntu Security (Medium priority)
  7. VulnCheck Advisory
  8. Alpine Docker Issue (fix tracking)
05 · The Call

Final Verdict
↓ DOWNGRADED to MEDIUM (5.3/10)

Why this verdict

  • Version-window friction: Only zlib 1.3.1.2–1.3.2 is affected. The 1.2.x series (still dominant on RHEL 8/9, Debian bookworm, Ubuntu 22.04/24.04) and 1.3.1 are completely immune. In a typical 10,000-host fleet, fewer than 15–20% of hosts are likely to ship the affected version — primarily bleeding-edge Alpine-based containers.
  • API-path friction: The vulnerability is reachable *only* through the non-blocking gz* file-handle API using the "N" mode flag introduced months ago. The vast majority of zlib consumers — nginx, curl, Python, Go, Java, OpenSSL — use the deflate/inflate streaming API or blocking gz* calls. Realistic estimate: <1% of applications linking zlib exercise the vulnerable code path.
  • Trigger-sequence friction: Even on applications using non-blocking gz*, the attacker must induce a write stall (EAGAIN) on the output fd *and* have the application subsequently call gzprintf()/gzvprintf() — not gzwrite(). Mixing these calls on the same handle is architecturally unusual.
  • No exploitation evidence: EPSS 0.356%, no KEV listing, no in-the-wild campaigns, no weaponized exploits. The PoC is a local reproducer requiring a purpose-built test harness, not a network-deliverable exploit.
  • Role multiplier: zlib is linked into nearly every Linux binary, giving it surface-area ubiquity. However, the *vulnerable code path* (non-blocking gz*) is NOT exercised by any canonical high-value-role software: domain controllers don't use it, hypervisors don't use it, CI/CD runners don't use it, backup agents don't use it, network edge appliances don't use it, database engines don't use it. The blast radius of this specific flaw is confined to the tiny population of bespoke C applications that opted into a brand-new, niche I/O mode. No high-value-role outcome (domain takeover, fleet compromise, supply-chain pivot) is plausible through this chain. The CRITICAL/HIGH floor does not apply.
  • Independent triage confirmation: Ubuntu Security rated this Medium priority. Multiple engineering teams (Prowler, Docker Hardened Images, govie-ds) are explicitly suppressing it as non-exploitable in standard deployments.

Why not higher?

The non-blocking gz* API is exercised by essentially zero mainstream applications. Without a reachable code path, the heap overflow is theoretical in virtually all deployments. The AC:H rating in the vendor's own CVSS vector acknowledges the complexity. There are no network-service exposure vectors — this is a library bug that requires a cooperating application to trigger. No evidence of in-the-wild exploitation exists to override the friction analysis.

Why not lower?

It is still a genuine heap buffer overflow (CWE-787) with a demonstrated ~8 KB out-of-bounds write primitive. The PoC is public and reproducible under ASan. If a bespoke application *does* use non-blocking gz* mode (e.g., a custom logging proxy writing compressed streams to non-blocking pipes), the bug is real and exploitable for at least DoS, with theoretical RCE. The container ecosystem's rapid adoption of Alpine 3.22+ means the affected zlib version is spreading, even if the vulnerable API path is dormant. LOW would understate the risk to the small but nonzero population of affected applications.

06 · Verification

Crowdsourced verification payload.

Run this on each target host (or in a container) as any user. It checks the installed zlib version against the affected range. Example: bash check_cve_2026_85091.sh. No special privileges required.

noisgate-verify.sh
BASHREAD-ONLYSAFE
#!/usr/bin/env bash
# check_cve_2026_85091.sh — Detect zlib versions affected by CVE-2026-85091
# Affected: 1.3.1.2 through 1.3.2
# Run on target host or inside container. No root required.

set -euo pipefail

VULN_MIN="1.3.1.2"
VULN_MAX="1.3.2"

# Compare dotted version strings: returns 0 if $1 <= $2
version_lte() {
  printf '%s\n%s' "$1" "$2" | sort -V | head -n1 | grep -qFx "$1"
}

# Try multiple methods to find zlib version
get_zlib_version() {
  # Method 1: dpkg (Debian/Ubuntu)
  if command -v dpkg >/dev/null 2>&1; then
    ver=$(dpkg -l 'zlib1g' 2>/dev/null | awk '/^ii/{print $3}' | sed 's/.*://;s/[-+].*//')
    if [ -n "$ver" ]; then echo "$ver"; return; fi
  fi
  # Method 2: rpm (RHEL/Fedora)
  if command -v rpm >/dev/null 2>&1; then
    ver=$(rpm -q --qf '%{VERSION}' zlib 2>/dev/null || true)
    if [ -n "$ver" ] && [ "$ver" != "package zlib is not installed" ]; then echo "$ver"; return; fi
  fi
  # Method 3: apk (Alpine)
  if command -v apk >/dev/null 2>&1; then
    ver=$(apk info -v zlib 2>/dev/null | sed 's/^zlib-//;s/-r.*//')
    if [ -n "$ver" ]; then echo "$ver"; return; fi
  fi
  # Method 4: parse libz.so directly
  for lib in /usr/lib/libz.so /usr/lib/x86_64-linux-gnu/libz.so /lib/libz.so.1; do
    if [ -f "$lib" ]; then
      ver=$(strings "$lib" 2>/dev/null | grep -oP '^1\.[0-9]+\.[0-9]+(\.[0-9]+)?' | head -1)
      if [ -n "$ver" ]; then echo "$ver"; return; fi
    fi
  done
  echo ""
}

ZLIB_VER=$(get_zlib_version)

if [ -z "$ZLIB_VER" ]; then
  echo "UNKNOWN — could not determine zlib version"
  exit 2
fi

echo "Detected zlib version: $ZLIB_VER"

if version_lte "$VULN_MIN" "$ZLIB_VER" && version_lte "$ZLIB_VER" "$VULN_MAX"; then
  echo "VULNERABLE — zlib $ZLIB_VER is in the affected range ($VULN_MIN – $VULN_MAX)"
  exit 1
else
  echo "PATCHED — zlib $ZLIB_VER is outside the affected range"
  exit 0
fi