Repository navigation
fix: a deny lifts where a condition keys a map by NaN, converts NaN to a string, or compares a host's map or optional holding it - #84
Merged
Conversation
…, as one keyed by NaN equals no map
…ite decides, and looking one up by an infinite key a condition reads finds it
…cides, as "NaN" compares as any string does
…nite compares as unequal, and comparing one is charged as a single value however much it holds
…te would be read unchecked
…e that isn't finite is false for a key it doesn't hold
…t is, as `(dyn([true]) + [""])[0]`, decides where CEL's conversion fails
…that isn't finite fails against one of as many entries, as one of another size compares unequal
…is a double that isn't finite
…gh few convert a value to a string
Michael McQuade (giraffesyo)
force-pushed
the
fix/nan-map-keys
branch
from
October 22, 2026 05:30
93c0869 to
4e3f311
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A condition fails when it compares a double that isn't finite (NaN or an infinity), which dividing by zero or
double("NaN")makes. Before that rule, a deny'sfailed / total >= 0.5lifted where both were 0.0. The check reached the operands and the values a list or map holds, but these paths still let such a double decide, so a deny written that way lifted and a grant held.{failed / total: 1} == {0.5: 1}was false where it should fail,!=was true,0.5 in {double(score): 1}was false, and a loop's{r: 1} == {0.5: 1}over[failed / total]was false too. A map the host passes in an undeclared kind can be keyed by NaN too:request.attributes.m == {0.5: 1}was false. Indexing a host map by an infinite key a condition reads,m[request.attributes.y], found it.finiteKey). The decorators that already charge every computed key, as a call (keyedCall) or an attribute (keyedAttribute), refuse one. That covers map literals, lookups and loop variables.qualifier) refuses one. CEL indexes by a double only forfloat32,float64andtypes.Double, so only those are checked, which keeps a JSON attribute from being parsed a second time.nonFiniteKey). The walk runs after the size check, within what the comparison is charged. Maps of different sizes still compare unequal, as CEL says.ina map the host passes keyed by one stays false for a key it doesn't hold. Finding whether it holds one would read every key, whichinisn't charged for. Condition's and Attributes' docs say so.string(failed / total)is"NaN", so a deny'sstring(failed / total) == "0.5"lifted.int()anduint()of one already fail.stringingdecorator runsstring()through CEL's own bindings for the overload the checker resolved, or by name where it resolved several, the same lookup CEL's planner does, after refusing a double that isn't finite. Every other value converts exactly as CEL converts it, including a value CEL checks as another type than it holds. The bindings are read only when a condition callsstring(), and plan refuses a conversion it finds no binding for. That can't happen with this version of CEL, and a stored condition refused so fails closed.valueSizecharged any optional as 1, so comparing one holding a long list walked it all for that charge.equalcompares what two optionals hold, andvalueSizecounts it, so the charge only grows.0.0 / 0.0into a constant key thatfiniteKeynever sees.Testing
TestADoubleAConditionMakesThatIsntFiniteFailsWhatComparesIthas five more grant and deny pairs: map literals keyed byfailed / totalcompared with==, with!=, inside a list, from the other side, and searched within; a map keyed bydouble(score)searched within; the same comparisons insideallandexistsloops; andstring()of the ratio. Each runs with NaN, +Inf and −Inf. Without the fixes the new cases fail: grants hold and denies lift. With the call path fixed but not the attribute path, the loop cases still fail.TestAMapTheHostPassesKeyedByADoubleThatIsntFiniteFailsWhatComparesOrIndexesIt: host maps keyed by NaN and ±Inf, compared from either side and indexed by an infinite key. Each case fails without its check.TestAnOptionalTheHostPassesComparesAsWhatItHolds: optionals holding NaN, nested, and holding a long list. These fail without the equality change, and without the size change.TestFiniteValuesDecideAsCELDoes: a differential test against plain CEL. It coversstring()of every type, plain, throughdyn()and checked as each other type, optionals, and maps with mixed int, uint and double keys. It fails with a conversion chosen by Go type.TestAKeyFoldedIntoADoubleThatIsntFiniteIsNoConditionandTestAConversionToAStringBoundOtherwiseIsNoCondition: each refusal fails its test without it.string()each cost no more than their bound. The limit and actual cost of 110string()conditions are identical to canary's.make check: runs before it lands.Checklist
make checkpasses