Skip to content

[finding] the ADR-0032 §1c hydration retry re-interprets sub-comparisons that already evaluated cleanly — hydrateOverloadStrings's docblock claims it cannot #7098

Description

@os-zhuang

Out-of-scope finding measured while pinning #6679 / PR #7097. Recorded per Prime Directive #10, unassigned. That card was scoped to the retry's trigger; this is about what the retry does once armed, so it was deliberately not folded in.

The fact

packages/formula/src/cel-engine.ts, the docblock on hydrateOverloadStrings:

Used only on the isNumericOverloadError retry path, so it can never change a comparison that already evaluated cleanly — it only rescues one that already faulted.

That claim does not hold. The retry knows only that the whole expression faulted, not that each sub-comparison did. hydrateOverloadStrings then rewrites the entire scope and the entire expression is re-evaluated, so a sub-comparison that evaluated cleanly in evaluation 1 can be re-interpreted against the hydrated value.

Measured on origin/main @ 4e6ca32, through celEngine.evaluate, with a genuine cel-js no_such_overload arming the retry (i.e. this reproduces after PR #7097 lands):

record.n >= 4 && record.s == "5.0"
  record = { n: "7", s: "5.0" }        ->  { ok: true, value: false }

record.n >= 4 ? record.s == "5.0" : false
  record = { n: "7", s: "5.0" }        ->  { ok: true, value: false }

Walkthrough of the first: evaluation 1 faults on record.n >= 4 (dyn<string> >= int) — the fault ADR-0032 §1c exists for. The retry hydrates, and record.n correctly becomes 7, so 7 >= 4 is true. But record.s was also a numeric string and it became 5 too, so record.s == "5.0" — a string equality the author wrote deliberately, which was true in evaluation 1 — becomes 5 == "5.0", which CEL answers false across types. The expression returns false.

The hydration is unconditional and scope-wide; only the operand that faulted needed it.

Impact

A string-equality or in-style test against a numeric-looking string silently flips to false whenever some other field in the same expression triggers §1c. Field.rating"5.0", Field.currency"250000.00", and Field.percent are exactly the fields that both trip §1c and get compared as strings, so the two halves of the hazard are drawn from the same pool. Flow conditions and RLS predicates are where compound expressions of this shape live.

No production report is attached to this and none is claimed — the trigger is measured, the frequency is not.

Class

Observation-class with a demonstrated wrong value, filed for triage to grade. Two shapes are plausible if promoted and they are quite different in cost, so the grading should probably decide between them rather than inherit one:

  1. Narrow the hydration to the operands that faulted — correct, and much more than a three-line change: the fault carries the operand types in its message, not a path into the scope.
  2. Correct the docblock and accept the behaviour as the documented price of §1c — cheap, and honest, but leaves the wrong value.

Related

#6679 / PR #7097 (the trigger side of the same retry — narrows when it arms, not what it rewrites; this reproduces independently of it). #1530 / #1534 (why the retry exists). ADR-0032 §1c.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions