cli/file_store: Go 1.26 compatibility#7026
Conversation
The implementation seems to have diverged already. Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
Add a fallback for unbracketed IPv6 literals to preserve behavior. Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟡 NEEDS ATTENTION
The fallback parser correctly handles the primary target case (https://::1:6555/v2/ → [::1]:6555), but the same LastIndex-based port-splitting heuristic misfires for unbracketed IPv6 addresses that have no explicit port (e.g. https://2001:db8::1/path), silently producing a malformed credential key. Two additional lower-severity edge cases are noted inline.
|
|
||
| if strings.Count(hostName, ":") > 1 && !strings.HasPrefix(hostName, "[") { | ||
| portStart := strings.LastIndex(hostName, ":") | ||
| addr, port := hostName[:portStart], hostName[portStart+1:] |
There was a problem hiding this comment.
[MEDIUM] hostFromURLFallback misidentifies last IPv6 segment as port, producing a malformed address
For any unbracketed IPv6 address without an explicit port — e.g. a legacy credential stored as "https://2001:db8::1/path" — the fallback parser splits on the last : and misidentifies the final group of the IPv6 address as a port number.
Trace for "https://2001:db8::1/path":
url.Parsefails (Go 1.26 rejects unbracketed IPv6), so the fallback is invoked.- After stripping the scheme:
hostName = "2001:db8::1". strings.Count("2001:db8::1", ":") = 3 > 1→ enters the branch.portStart = strings.LastIndex("2001:db8::1", ":") = 9→addr = "2001:db8:",port = "1".isPort("1") = true→ returnsnet.JoinHostPort("2001:db8:", "1")="[2001:db8:]:1".
The addr value "2001:db8:" has a trailing colon and is not a valid IPv6 address. Any credential previously stored under "https://2001:db8::1" would fail to match after normalization because the key becomes "[2001:db8:]:1" instead of "2001:db8::1".
Fix suggestion: Before splitting on the last :, check whether the suffix after the last : is also a valid hex IPv6 group — or, more robustly, attempt net.ParseIP(hostName) first. If it parses as a valid IP (no port), return net.JoinHostPort(hostName, "") (i.e. bracket but no port) or just hostName unchanged.
| if r < '0' || r > '9' { | ||
| return false | ||
| } | ||
| } |
There was a problem hiding this comment.
[LOW] isPort accepts values > 65535 — no upper-bound validation
isPort validates only that all characters are ASCII digits; it does not check that the parsed number is ≤ 65535. An input like "https://::1:99999/" passes the check and returns "[::1]:99999" as the normalised credential key.
In practice this mirrors whatever was stored under old Go (which also had no port-range enforcement in url.Parse), so the risk of a new mismatch is low. Nonetheless, adding a simple range check (strconv.Atoi + n >= 0 && n <= 65535) would make the validation more correct and align with the semantics of a real TCP port.
| addr, port := hostName[:portStart], hostName[portStart+1:] | ||
| if addr != "" && isPort(port) { | ||
| return net.JoinHostPort(addr, port) | ||
| } |
There was a problem hiding this comment.
[LOW] Non-numeric-port branch falls through and returns unbracketed IPv6 string
When strings.Count(hostName, ":") > 1 (IPv6-like) but the last segment is not a decimal port (e.g. "https://::1:bad/"), the if addr != "" && isPort(port) guard fails and the function falls through to return hostName, returning "::1:bad" — an unbracketed IPv6 string — rather than a bracketed form.
All other code paths that handle IPv6 addresses produce bracketed output (net.JoinHostPort always adds brackets). This inconsistency could surprise callers that assume IPv6 addresses are always returned in [addr] or [addr]:port form.
In practice, credentials with non-numeric ports are not realistic, so the real-world impact is near zero. But the fall-through behaviour is worth documenting (or guarding) for correctness.
Go 1.26 rejects unbracketed IPv6 literals in URL hosts, so this preserves the existing behavior by adding a fallback parser for those legacy inputs.