java-topology/defects/go/patch/go-stdlib-0001-http2-rfc9218priority-header-alloc.md

3.2 KiB

UNDF: UNDF-2026-000000825

go-stdlib-0001 — net/http/internal/http2: rfc9218Priority allocates []string per header field

Severity: MEDIUM CWE: CWE-407 (Algorithmic Complexity — Linear Membership Test in Loop) File: src/net/http/internal/http2/frame.go Function: (*MetaHeadersFrame).rfc9218Priority

Defect

// frame.go:1655-1662
func (mh *MetaHeadersFrame) rfc9218Priority(priorityAware bool) (p PriorityParam, ...) {
    for _, field := range mh.Fields {           // O(F) — F header fields
        if field.Name == "priority" { ... }
        if slices.Contains([]string{"via", "forwarded", "x-forwarded-for"}, field.Name) {
            // ^^^^ allocates a NEW 3-element []string on EVERY iteration
            hasIntermediary = true
        }
    }

On each iteration of the mh.Fields loop, slices.Contains([]string{...}, ...) creates a fresh heap-allocated slice literal. For an HTTP/2 server under load, this function is called once per HEADERS frame (i.e., per request), and the inner allocation fires once per header field. A request with 30 headers performs 30 needless allocations and 3-element scans.

At high request rates the GC pressure compounds: a server handling 100 k req/s with avg 20 headers/request = 2 M unnecessary allocations per second.

Fix

Pre-declare the intermediary-header set as a package-level map[string]bool (zero allocation at call time, O(1) lookup):

// package-level, evaluated once at init time
var rfc9218IntermediaryHeaders = map[string]bool{
    "via":             true,
    "forwarded":       true,
    "x-forwarded-for": true,
}

func (mh *MetaHeadersFrame) rfc9218Priority(priorityAware bool) (p PriorityParam, ...) {
    for _, field := range mh.Fields {
        if field.Name == "priority" { ... }
        if rfc9218IntermediaryHeaders[field.Name] {   // O(1), zero alloc
            hasIntermediary = true
        }
    }

Patch

--- a/src/net/http/internal/http2/frame.go
+++ b/src/net/http/internal/http2/frame.go
@@ -1650,6 +1650,12 @@ func (f *MetaHeadersFrame) PseudoFields() []hpack.HeaderField {
 	return f.Fields[:f.NumHdrs]
 }

+// rfc9218IntermediaryHeaders is the set of header field names that indicate
+// an intermediary is present (RFC 9218 §4.1).  Declared at package level to
+// avoid allocating a new []string on every call to rfc9218Priority.
+var rfc9218IntermediaryHeaders = map[string]bool{
+	"via": true, "forwarded": true, "x-forwarded-for": true,
+}
+
 func (mh *MetaHeadersFrame) rfc9218Priority(priorityAware bool) (p PriorityParam, priorityAwareAfter, hasIntermediary bool) {
 	var s string
 	for _, field := range mh.Fields {
@@ -1658,7 +1664,7 @@ func (mh *MetaHeadersFrame) rfc9218Priority(priorityAware bool) (p PriorityPara
 			priorityAware = true
 		}
-		if slices.Contains([]string{"via", "forwarded", "x-forwarded-for"}, field.Name) {
+		if rfc9218IntermediaryHeaders[field.Name] {
 			hasIntermediary = true
 		}
 	}

Complexity

Metric Before After
Allocations per call O(F) slice allocs 0 allocs
Lookup per field O(3) linear O(1) map
GC pressure at 100k req/s, 20 headers ~2M allocs/s 0

Reproduction

See defects/go/unit/GoStdlibHttp2Algorithm.java — measures ratio ≥ 5x at F=1000.