Memory safety / slice aliasing of pooled buffer in Proxy Protocol v2 parser

MEDIUM
victoriametrics/victoriametrics
Commit: 05112e54e2f3
Affected: <= 1.139.0
2026-04-20 10:41 UTC

Description

The commit fixes a memory-safety issue in the Proxy Protocol v2 parser used by the lib/netutil code. Previously, the IPv6 address was taken directly as a subslice of a pooled buffer (bb.B[0:16]). Since bb.B comes from a sync.Pool and is reused after the function returns, the IP slice could alias the underlying pool buffer. Subsequent pool reuse could overwrite the memory backing the IP address, leading to data corruption or leakage when the parsed address is kept by the caller. The fix copies the IPv6 address into a new slice (ipv6Addr) and uses that as the IP value, preventing aliasing with the pooled memory. A regression test TestParseProxyProtocolIPv6DoesNotAliasPool was added to ensure the IP does not alias the pool across consecutive parses.

Proof of Concept

Go PoC demonstrating the memory aliasing vulnerability pre-fix: package main import ( "bytes" "encoding/binary" "fmt" "io" "net" "sync" ) type pooledBuffer struct { B []byte } var bbPool = &sync.Pool{New: func() interface{} { return &pooledBuffer{B: make([]byte, 64)} }} // readProxyProto mirrors the vulnerable behavior: it uses a slice of the pooled buffer for the IP func readProxyProto(r io.Reader) (net.Addr, error) { bb := bbPool.Get().(*pooledBuffer) defer bbPool.Put(bb) // Fill 36 bytes to simulate the proxy protocol block if _, err := io.ReadFull(r, bb.B[:36]); err != nil { return nil, err } // Vulnerable aliasing: IP shares backing array with pooled buffer ip := bb.B[:16] port := int(binary.BigEndian.Uint16(bb.B[32:34])) return &net.TCPAddr{IP: ip, Port: port}, nil } func main() { // First 36-byte block with IPv6-like address and port 80 ip1 := []byte{0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01} h1 := make([]byte, 36) copy(h1[0:16], ip1) binary.BigEndian.PutUint16(h1[32:34], 80) // Second 36-byte block with a different IPv6-like address ip2 := []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02} h2 := make([]byte, 36) copy(h2[0:16], ip2) binary.BigEndian.PutUint16(h2[32:34], 80) r := bytes.NewBuffer(append(h1, h2...)) addr1, err := readProxyProto(r) if err != nil { fmt.Println("err1:", err) return } // Read again to reuse the pool, which could mutate addr1 if it aliases pool memory addr2, err := readProxyProto(r) if err != nil { fmt.Println("err2:", err) return } fmt.Printf("addr1: %v\n", addr1) fmt.Printf("addr2: %v\n", addr2) fmt.Printf("addr1.IP before mutation: %v\n", addr1.(*net.TCPAddr).IP) // Demonstrate mutation risk: addr1's backing array may have been overwritten by pool reuse }

Commit Details

Author: andriibeee

Date: 2026-04-20 10:11 UTC

Message:

lib/netutil: fix IPv6 address corruption in proxy protocol v2 parser Proxy protocol parser kept sub-slice reference for pooled bytesBuffer at readProxyProto ``` bb := bbPool.Get() defer bbPool.Put(bb) // ← buffer returned to pool AFTER function returns ... IP: bb.B[0:16], // ← BUG: sub-slice of pooled buffer! ... ``` This commit properly allocates new slice for ipv6 address and copies buffer content to it. Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10839

Triage Assessment

Vulnerability Type: Memory safety

Confidence: MEDIUM

Reasoning:

The commit fixes a memory safety/data integrity issue where a sub-slice of a pooled buffer (ipv6 address) could alias the underlying pool buffer, leading to potential corruption or unintended data reuse. By copying the address into a new slice, it prevents aliasing with the pooled buffer. This mitigates a potential vulnerability caused by unsafe reuse of memory in the proxy protocol v2 parser.

Verification Assessment

Vulnerability Type: Memory safety / slice aliasing of pooled buffer in Proxy Protocol v2 parser

Confidence: MEDIUM

Affected Versions: <= 1.139.0

Code Diff

diff --git a/lib/netutil/proxyprotocol.go b/lib/netutil/proxyprotocol.go index ba13075be82ac..539a7deafac70 100644 --- a/lib/netutil/proxyprotocol.go +++ b/lib/netutil/proxyprotocol.go @@ -128,8 +128,10 @@ func readProxyProto(r io.Reader) (net.Addr, error) { if len(bb.B) < 36 { return nil, fmt.Errorf("cannot read ipv6 address from proxy protocol block with the length %d bytes; expected at least 36 bytes", len(bb.B)) } + var ipv6Addr net.IP + ipv6Addr = append(ipv6Addr, bb.B[:16]...) remoteAddr := &net.TCPAddr{ - IP: bb.B[0:16], + IP: ipv6Addr, Port: int(binary.BigEndian.Uint16(bb.B[32:34])), } return remoteAddr, nil diff --git a/lib/netutil/proxyprotocol_test.go b/lib/netutil/proxyprotocol_test.go index 64cccb5dbc5e6..e000cc9c66b50 100644 --- a/lib/netutil/proxyprotocol_test.go +++ b/lib/netutil/proxyprotocol_test.go @@ -107,6 +107,28 @@ func TestParseProxyProtocolFail(t *testing.T) { 0, 80, 0, 0}) } +func TestParseProxyProtocolIPv6DoesNotAliasPool(t *testing.T) { + header := func(last byte) *bytes.Buffer { + return bytes.NewBuffer([]byte{ + 0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A, 0x21, 0x21, 0x00, 0x24, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, last, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 0, 80, 0, 0, + }) + } + got, err := readProxyProto(header(1)) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + if _, err := readProxyProto(header(2)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + want := &net.TCPAddr{IP: net.ParseIP("::1"), Port: 80} + if !reflect.DeepEqual(got, want) { + t.Fatalf("first addr mutated by pool reuse; got %v, want %v", got, want) + } +} + func TestProxyProtocolConnReadWriteSuccessful(t *testing.T) { server, client := net.Pipe() defer server.Close()
← Back to Alerts View on GitHub →