Skip to content

ContiguousSet.AsSpan() / ContiguousMap.AsSpan() return a writable Span that silently desyncs the uniqueness/key index (duplicates in a set, wrong lookups in a map) #78

Description

@matt-edmondson

What's wrong

ContiguousSet<T>.AsSpan() (Containers/ContiguousSet.cs:543) and ContiguousMap<TKey,TValue>.AsSpan() (Containers/ContiguousMap.cs:565) return a mutable Span<T> / Span<Entry> over the backing items array:

public Span<T> AsSpan() => new(items, 0, Count);          // ContiguousSet
public Span<Entry> AsSpan() => new(items, 0, Count);      // ContiguousMap

Both types also keep a side index: uniquenessSet for the set and keyToIndex for the map. Writing through the span changes items but leaves that index untouched, so the container breaks its own invariants with no error. The doc comments give no warning. ContiguousCollection<T>.AsSpan() is fine because it has no side index.

Reproduction (verified against a local build of main)

Set

var s = new ContiguousSet<int> { 1, 2, 3 };
s.AsSpan()[0] = 2;
// items enumerate as 2,2,3  -> duplicate inside a set
s.Contains(1);   // True, although 1 is no longer stored
s.Add(1);        // False, so 1 cannot be re-added
s.Remove(2);     // items now 2,3
s.Remove(2);     // False, but 2 is still enumerated

Map

var m = new ContiguousMap<string,int> { ["a"] = 1, ["b"] = 2 };
m.AsSpan()[0] = new ContiguousMap<string,int>.Entry("b", 99);
// Keys enumerate as b,b
m["a"];              // 99
m.ContainsKey("a");  // True

Any caller who treats AsSpan() as the fast path for in-place updates, which is what a writable span invites, corrupts the container with no error.

Suggested fix

  • Make AsSpan() on ContiguousSet and ContiguousMap return ReadOnlySpan<…>, or remove it and keep only AsReadOnlySpan(). This is a breaking public API change, so it needs a major/minor version bump per the repo's versioning.
  • If in-place value mutation on the map is a use case worth keeping, add a narrow API instead, e.g. ref TValue GetValueRefOrNullRef(TKey key) in the style of CollectionsMarshal. It can update values without letting callers rewrite keys.

Acceptance criteria: no public API on ContiguousSet or ContiguousMap can change stored elements or keys without updating uniquenessSet / keyToIndex, and a test covers the scenario above.

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions