Skip to content

[Medium] Canonical-form leaks at composite/categorical validate and normalize seams #31

Description

@gratus907

Severity: Medium

Canonical-form enforcement leaks at several composite/categorical seams: composite spaces validate more laxly than their own leaf spaces, CategoricalSpace.normalize doesn't canonicalize its output, and CategoricalSpace accepts non-finite choices at construction. All three let non-canonical or invalid candidates survive validation and diverge later under ==/hash-based dedup or crash mid-run.

1. Composite validate is laxer than the leaf contractsrc/variopt/spaces/composites/adapters.py:581-602, 840-846

require_real_candidate coerces int -> float before delegating, so TupleSpace/RecordSpace.validate accept an int at a RealSpace leaf that RealSpace.validate/ArraySpace.validate both reject directly:

TupleSpace(RealSpace(0.0, 10.0)).validate((3,))   # passes
RealSpace(0.0, 10.0).validate(3)                   # raises TypeError

2. CategoricalSpace.normalize returns the raw boundary value, not the matched choicesrc/variopt/spaces/scalar.py:590-604

With choices (1.0, 2.0), normalize(1) returns int 1 instead of the declared float choice 1.0; with choices (0, 1), normalize(True) returns True unchanged. Two "equal" candidates (per candidates_equal) then have different hash/== under plain Python equality, and JSON serialization preserves the non-canonical type. replace_leaf_values (scalar.py:740-742) does this correctly for comparison.

3. Non-finite float choices are accepted at CategoricalSpace.__init__src/variopt/spaces/scalar.py:579-585, 615-617

CategoricalSpace((1.0, float("nan"))) constructs successfully; sample() can return NaN, and validate(nan) then raises ValueError (because nan != nan breaks membership), crashing mid-optimization far from the misdeclared space.

Fix direction

Type-check without coercion in the composite validate seam (type(value) is float) so composite and leaf strictness agree; have CategoricalSpace.normalize return the matched choice object; reject non-finite float choices at CategoricalSpace.__init__, matching RealSpace's bound rigor.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions