CNA-BUG-065: Curve::Evaluate divides by zero for coincident key positions, returning NaN and converting infinity to int in the cyclic loop modes

CNA snapshot 009d40f5  ·  Known Issues › Current bugs  ·  source links pinned to 009d40f5

✓

Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Nothing on this page was executed unless the Evidence section says so.

GetNumberOfCycle and GetCurvePosition divide by the key span and the segment width without a guard, so curves with coincident keys yield NaN or undefined behaviour where XNA returns the first key's value.

Identifier
CNA-BUG-065
Category
Bug
Subsystem
Math & geometry
Status
Open
Verified against
CNA 009d40f5 (009d40f5dd085c4e674d3479675fac84b12b3e0a)
Severity
Low (a triage suggestion, not a project priority)
Evidence basis
Source-verified: read at TARGET, not executed
Tests touching this area
Yes: see Current tests
Affected contract
Curve::Evaluate(float) for curves whose keys share positions (accepted by CurveKeyCollection::Add); C ABI cna_curve_evaluate

Expected behaviour

XNA 4.0 guards both divisions (read from the IL of the genuine XNA 4.0 Microsoft.Xna.Framework assembly; not executed): CurveKeyCollection.ComputeCacheValues leaves the inverse time range at 0 unless the range is above float.Epsilon (1.401298E-45), so the cycle count is 0, and FindSegment returns t = 0 for a segment no wider than 1e-10. XNA therefore returns finite values for coincident keys: the first key's value for keys (1, a), (1, b), (2, c) at position 1, and for an all-coincident curve evaluated before its range; after the range XNA's segment search runs past the last key, so it evaluates the last key against itself, which is the last key's value when the tangents are 0.

Actual behaviour at TARGET

  • CurveKeyCollection::Add in CurveKeyCollection.cpp accepts a key at an existing position (it is inserted after the equal ones), as XNA's collection does.
  • Keys (1, a), (1, b), (2, c): Evaluate(1.0f) reaches GetCurvePosition with a previous and next key both at 1, computes t = 0 / 0 and returns NaN; XNA returns a.
  • All keys at one position p with a Cycle, CycleOffset or Oscillate pre- or post-loop: GetNumberOfCycle computes (position - p) / 0, an infinity, and static_cast<int> of it is undefined behaviour in C++ (typically INT_MIN on x86-64). For a position before the range GetCurvePosition then divides by zero again and returns NaN; for a position after the range no key satisfies next.Position >= position, so it falls through to return 0.0f (a CycleOffset loop then adds cycle * (last.Value - first.Value) with the cycle at INT_MIN, so a curve whose values differ returns a huge number). XNA returns a finite value from the keys in both cases.
  • Both paths are ported unchanged from FNA's C# implementation (read at FNA commit 1358793, 2026-08-01).

Source locations

Evidence

Checked by reading at 009d40f5; nothing was built or executed for this entry. The divisions and the cast are in the function bodies; XNA's guards were read from its IL. The C++ rule that converting an out-of-range floating value to int is undefined comes from the standard's floating-integral conversions.

Independent re-verification: Checked by reading at 009d40f5; nothing was built or executed for this entry. The divisions and the cast are in the function bodies; XNA's guards and its post-loop path were read from its IL. The C++ rule that converting an out-of-range floating value to int is undefined comes from the standard's floating-integral conversions. The regression case for an all-coincident curve evaluated after its range should expect the last key's value with zero tangents, not the first's.

Focused reproduction

Illustrative; not compiled or run for this entry.

Curve c;
c.getKeysProperty().Add(CurveKey(1.0f, 3.0f));
c.getKeysProperty().Add(CurveKey(1.0f, 4.0f));
c.getKeysProperty().Add(CurveKey(2.0f, 5.0f));
float v = c.Evaluate(1.0f);             // XNA: 3     TARGET code: NaN

Current tests

Neither CurveTests.cpp nor CurveKeyCollectionTests.cpp evaluates a curve with coincident key positions; the loop-type tests all use keys at 0 and 1.

Regression test

The duplicate-position case above (expect the first key's value) and an all-coincident curve with each cyclic loop type evaluated outside the range (expect the first key's value); run under UBSan, whose float-cast-overflow check flags the current code.

Blast radius

Degenerate curves only: duplicate key positions, or all keys at one position with a cyclic loop mode, whether built in code, through the C ABI or loaded from XNB. Well-formed curves are unaffected.

Workaround

Keep key positions distinct and give cyclic curves a positive time range.

The same subject is explained at several altitudes. These are the neighbouring pages at each one.

Known issues
Bug index