Skip to content

A NaN with the sign bit clear sorts above +Infinity in an index on a double #257

Description

@CaffeinatedCoder

IndexKey(double) gives a NaN with the sign bit clear a key above +Infinity, and a NaN with the sign bit set (the constant double.NaN) a key below -Infinity. On Arm64 every NaN the program computes has the sign bit clear, so index-served orderings put those documents last and index-served counts with a lower bound include them. On x64, where computed NaNs carry the sign bit, the same documents sort first: the order of a query depends on the CPU that wrote the value.

Found by the B-tree model test (tests/BLite.Tests/Oracles/BTreeModelTests.cs, #259), which carries the affected rows as a known candidate until this is fixed.

Sibling of #249 (-0.0 and +0.0 get different keys): the same encoding, whose text names NaN as a separate question; this is that question. #252 leaves NaN out of scope. If #254 is fixed by keying dynamic doubles with IndexKey(double), DynamicCollection inherits this too.

Repro

double zero = double.Parse("0");
double computed = zero / zero;             // 0x7FF8000000000000 on Arm64; double.NaN is 0xFFF8000000000000
IndexKey.Create(computed) > IndexKey.Create(double.PositiveInfinity);   // true
IndexKey.Create(computed) == IndexKey.Create(double.NaN);               // false

// DiffDoc has double Ratio with .HasIndex(x => x.Ratio).
await ctx.Docs.InsertBulkAsync([
    new DiffDoc { Name = "d1", Ratio = computed },
    new DiffDoc { Name = "d2", Ratio = 1.0 },
    new DiffDoc { Name = "d3", Ratio = double.NaN }]);
ctx.Docs.AsQueryable().OrderBy(x => x.Ratio).Take(3);              // expected d1, d3, d2; actual d3, d2, d1
ctx.Docs.AsQueryable().OrderByDescending(x => x.Ratio).Take(3);    // expected d2, d1, d3; actual d1, d2, d3
ctx.Docs.AsQueryable().Count(x => x.Ratio > 0.5);                  // expected 1;          actual 2
ctx.Docs.AsQueryable().Where(x => x.Ratio >= double.MaxValue);     // expected [];         actual [d1]

Expected: double.CompareTo, which LINQ-to-Objects and the unindexed collection use, orders every NaN below every number and equal to every other NaN; every comparison operator with a NaN operand is false in C#.

Actual: on Arm64, every computed NaN (0.0 / 0.0, Math.Sqrt(-1), ∞ - ∞ all give 0x7FF8000000000000) sorts above +Infinity, while double.NaN sorts below -Infinity.

Mechanism (verified on 0450b97)

  • IndexKey(double) (src/BLite.Core/Indexing/IndexKey.cs:94-105) flips all bits of a value with the sign bit set and only the sign bit otherwise. A NaN's sign bit is arbitrary, so the two NaN halves land below -Infinity (sign set) and above +Infinity (sign clear), and every distinct payload is a distinct key.
  • Where(x => x.Ratio > 0.5) itself returns the right answer because it is post-filtered with the BSON predicate; Count is answered from the index (CountRange) and is not.

Affected versions: since the sort-preserving double encoding in f83e0d5 (2026-03-17, first in v3.7.0). Reproduced on v5.2.3 / 0450b97 on Arm64; x64 not run.

Fix considerations

  • Canonicalise in IndexKey(double) together with the -0.0 fix of Indexed double equality misses -0.0: IndexKey(double) gives the two zeros different keys #249: if (double.IsNaN(value)) value = double.NaN; before taking the bits gives every NaN the one key below -Infinity, which is where double.CompareTo puts it.
  • Range bounds then exclude NaN from > x and >= x. Whether < x should include it (C#: false, CompareTo: NaN is smallest) is the same question the index already answers for -Infinity; the BSON post-filter decides for Where, not for Count.
  • Existing indexes with a positive-NaN key need a rebuild.

Activity

  1. moved this from Todo to In Progress in BLiteon Oct 17, 2026
  2. CaffeinatedCoder commented on Oct 17, 2026

    @CaffeinatedCoder
    CollaboratorAuthor

    Fix in #282: every NaN takes the key of 0xFFF8000000000000 (double.NaN), below -Infinity, where double.CompareTo orders it. Existing indexes holding a sign-clear NaN need a rebuild. Conflicts textually with #252 (same lines), trivially.

  3. added
    severity/mediumSpace leak, inconsistency or failure in a narrow scenario
    and removed on Oct 18, 2026
  4. mrdevrobot commented on Oct 18, 2026

    @mrdevrobot
    Contributor

    Reproduced on 0450b97 (Arm64): a computed NaN sorts above +Infinity. Edge value: medium. x64 not run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/indexingB-Tree and secondary indexes, index lifecyclebugSomething isn't workingpackage/coreAffects BLite.Coreseverity/mediumSpace leak, inconsistency or failure in a narrow scenario

    Type

    No type

    Projects

    • Status
      In Progress

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions