CNA-BUG-065: Curve::Evaluate divides by zero for coincident key positions, returning NaN and converting infinity to int in the cyclic loop modes
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::AddinCurveKeyCollection.cppaccepts 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)reachesGetCurvePositionwith a previous and next key both at 1, computes t = 0 / 0 and returnsNaN; XNA returns a. - All keys at one position p with a Cycle,
CycleOffsetor Oscillate pre- or post-loop:GetNumberOfCyclecomputes (position - p) / 0, an infinity, andstatic_cast<int>of it is undefined behaviour in C++ (typicallyINT_MINon x86-64). For a position before the rangeGetCurvePositionthen divides by zero again and returnsNaN; for a position after the range no key satisfies next.Position >= position, so it falls through to return 0.0f (aCycleOffsetloop then adds cycle * (last.Value - first.Value) with the cycle atINT_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
modules/math/src/Curve.cpp— Curve::GetNumberOfCycle, Curve::GetCurvePosition and the cyclic cases of Curve::Evaluatemodules/math/src/CurveKeyCollection.cpp— CurveKeyCollection::Add accepts duplicate positionsmodules/math/tests/Microsoft/Xna/Framework/CurveTests.cpp— no case with coincident positionsmodules/math/tests/Microsoft/Xna/Framework/CurveKeyCollectionTests.cpp— covers insertion and replacement, not evaluation
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.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- Math types: curve types
- Maintainer workflow
- Add a regression test: the math case
- Known issues
- Bug index