Skip to content

cli/file_store: Go 1.26 compatibility#7026

Open
vvoland wants to merge 2 commits into
docker:masterfrom
vvoland:fix-hostname
Open

cli/file_store: Go 1.26 compatibility#7026
vvoland wants to merge 2 commits into
docker:masterfrom
vvoland:fix-hostname

Conversation

@vvoland
Copy link
Copy Markdown
Collaborator

@vvoland vvoland commented Jun 3, 2026

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.

vvoland added 2 commits June 3, 2026 17:40
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>
@vvoland vvoland self-assigned this Jun 3, 2026
@vvoland vvoland changed the title cli/file_store: Go 1.26 compatibility. cli/file_store: Go 1.26 compatibility Jun 3, 2026
@codecov-commenter
Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 22.72727% with 17 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
cli/config/credentials/file_store.go 22.72% 15 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@vvoland vvoland added this to the 29.6.0 milestone Jun 3, 2026
@vvoland vvoland requested a review from docker-agent June 5, 2026 13:11
Copy link
Copy Markdown

@docker-agent docker-agent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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":

  1. url.Parse fails (Go 1.26 rejects unbracketed IPv6), so the fallback is invoked.
  2. After stripping the scheme: hostName = "2001:db8::1".
  3. strings.Count("2001:db8::1", ":") = 3 > 1 → enters the branch.
  4. portStart = strings.LastIndex("2001:db8::1", ":") = 9addr = "2001:db8:", port = "1".
  5. isPort("1") = true → returns net.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
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants