Skip to content

test(compose): fuzz envResolverWithCase resolution semantics - #14388

Closed
TDnorthgarden wants to merge 1 commit into
docker:mainfrom
TDnorthgarden:test/envresolver-fuzz
Closed

TDnorthgarden wants to merge 1 commit into
docker:mainfrom
TDnorthgarden:test/envresolver-fuzz

Conversation

@TDnorthgarden

Copy link
Copy Markdown

Add a Go native fuzz target locking the envResolverWithCase resolution
semantics:

  • case-sensitive resolution is exactly a map lookup;
  • case-insensitive resolution returns the exact match first, then the
    lower-cased fallback;
  • a nil environment never resolves.

128k executions over the corpus found no violation.

Lock the resolution invariants under Go native fuzzing: the resolver is
exactly the map when case-sensitive, an exact match wins over the
lower-cased fallback when case-insensitive, and a nil environment never
resolves. 128k executions over the corpus found no violation.

Signed-off-by: Aucoto <tdnorthgarden@gmail.com>
@TDnorthgarden
TDnorthgarden requested review from a team as code owners October 26, 2026 02:20
Comment on lines +31 to +36
// keep the two keys distinct after case folding so the lower-cased
// fallback has a single unambiguous winner
if strings.EqualFold(k1, k2) {
k2 += "#"
}
environment := map[string]string{k1: v1, k2: v2}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks to be explicitly avoiding an ... actually problematic case in the implementation; the implementation handles uppercase and lowercase correctly, but mixed case will be randomised; given, e.g.;

env := map[string]string{
    "FOO": "upper",
    "foo": "lower",
    "Foo": "mixed",
}

A direct match (on either FOO or foo, or Foo) will return a deterministic value, but a case-insensitive match will be randomised (return either upper, lower, or mixed), depending on how the Go runtime randomises the map.

The code also unconditionally allocates a new map, regardless if a direct match was found;

// variable names must be treated case-insensitively.
// Resolves in this way:
// * Return the value if its name matches with the passed name case-sensitively.
// * Otherwise, return the value if its lower-cased name matches lower-cased passed name.
// * The value is indefinite if multiple variable matches.
loweredEnvironment := make(map[string]string, len(environment))
for k, v := range environment {
loweredEnvironment[strings.ToLower(k)] = v
}
return func(s string) (string, bool) {
v, ok := environment[s]
if ok {
return v, ok
}
v, ok = loweredEnvironment[strings.ToLower(s)]
return v, ok
}

(So given that it already loops over values, it may as well just loop over values, and do a case-insensitive (try match exact UPPER or lower first, then fallback to Equalfold if desired) only if a direct match wasn't found.

@thaJeztah thaJeztah left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not sure if the fuzz-test adds much here; at least, at a glance, I don't see much that's added that cannot be added through additional cases in the existing table-test 🤔

@TDnorthgarden

Copy link
Copy Markdown
Author

Fair point — the EqualFold normalization in the corpus was dodging the very behavior worth exercising, and the invariants it pinned are just table-test cases. Closing this one.
I'll take the implementation suggestion in a follow-up instead: drop the unconditional lowered-environment allocation and fall back to a lazy case-insensitive scan of the original map. Thanks!

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.

2 participants