CNA-BUG-065: Curve::Evaluate divides by a zero key span in the cyclic loop modes and converts the resulting infinity to int

CNA snapshot 3b43bd86  ·  Known Issues › Current bugs  ·  source links pinned to 3b43bd86

✓

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 3b43bd86; unless a sentence says otherwise, nothing here was built or executed. Nothing on this page was executed unless the Evidence section says so.

When every key shares one position, GetNumberOfCycle divides by the zero key span without a guard and casts the infinity to int (undefined behaviour) in the Cycle, CycleOffset and Oscillate loop modes, where XNA's cycle count is 0.

Identifier
CNA-BUG-065
Category
Bug
Subsystem
Math & geometry
Status
Narrowed (partially fixed; describes only what survives)
Verified against
CNA 3b43bd86 (3b43bd8687ed46523d873f9bec4b32bd8481dcb5)
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 all share one position, with a Cycle, CycleOffset or Oscillate pre- or post-loop; C ABI cna_curve_evaluate

Expected behaviour

XNA 4.0 guards the division (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 of an all-coincident curve is 0 and XNA returns finite values from its keys.

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.
  • All keys at one position p with a Cycle, CycleOffset or Oscillate pre- or post-loop: GetNumberOfCycle in Curve.cpp computes (position - p) / 0, an infinity, and static_cast<int> of it is undefined behaviour in C++ (typically INT_MIN on x86-64). A CycleOffset loop then adds cycle * (last.Value - first.Value), so a curve whose key values differ returns a huge number; XNA returns a finite value from the keys.
  • The path is ported unchanged from FNA's C# implementation (read at FNA commit 1358793, 2026-08-01).

Source locations

Evidence

Checked by reading at 3b43bd86; nothing was built or executed for this entry. The division and the cast are in the function body; XNA's guard was 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.

Narrowed at c6dec91f: d753d0997 (CNA-DEMOS-ISSUE-138) fixed the segment half of this entry: Curve::GetCurvePosition now normalises in double and uses t = 0 for a span of 1e-10 or less, so coincident keys select the first key instead of returning NaN (CurveTest.DuplicateAndTinySegmentsMatchXnaSelection). The cycle count is unchanged.

Focused reproduction

Illustrative; not compiled or run for this entry.

Curve c;\nc.setPostLoopProperty(CurveLoopType::CycleOffset);\nc.getKeysProperty().Add(CurveKey(1.0f, 3.0f));\nc.getKeysProperty().Add(CurveKey(1.0f, 4.0f));\nfloat v = c.Evaluate(2.0f);   // GetNumberOfCycle: (2 - 1) / 0 cast to int -- undefined behaviour

Current tests

CurveTests.cpp now evaluates duplicate and tiny segments, but no test evaluates an all-coincident curve with a cyclic loop type; the loop-type tests use keys at distinct positions.

Regression test

An all-coincident curve with each cyclic loop type evaluated outside the range, expecting XNA's finite value, run under UBSan, whose float-cast-overflow check flags the current code.

Blast radius

Degenerate curves only: 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

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