Skip to content

Conversation

xtqqczze
Copy link
Contributor

@xtqqczze xtqqczze commented Aug 13, 2025

CollectionsMarshal.AsSpan relies on the exact internal layout of List<T>. Passing a subclass is unsafe because a derived list may reimplement IEnumerable<T> with altered enumeration semantics.

Related: #96574, #85288

@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Aug 13, 2025
Copy link
Contributor

Tagging subscribers to this area: @dotnet/area-system-linq
See info in area-owners.md if you want to be subscribed.

`CollectionsMarshal.AsSpan` relies on the exact internal layout of `List<T>`. Passing a subclass is unsafe because a derived list may reimplement `IEnumerable<T>` with altered enumeration semantics.
@xtqqczze
Copy link
Contributor Author

@MihuBot

@stephentoub
Copy link
Member

Where are we currently using AsSpan on a List<T> that's possibly a derived type?

if (source.GetType() == typeof(List<TSource>)) // avoid accidentally bypassing a derived type's reimplementation of IEnumerable<T>
{
return new ListSelectIterator<TSource, TResult>(list, selector);
return new ListSelectIterator<TSource, TResult>(Unsafe.As<List<TSource>>(source), selector);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this something for JIT to fix instead?

cc @EgorBo

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see no difference locally from this change

@stephentoub
Copy link
Member

a derived list may reimplement IEnumerable with altered enumeration semantics.

This has long been the case, even on .NET Framework, e.g. on .NET Framework, this doesn't throw:

using System;
using System.Collections.Generic;
using System.Linq;

internal class Program
{
    static void Main()
    {
        foreach (var item in new MyFailingList().Where(i => i % 2 == 0))
        {
            Console.WriteLine(item);
        }
    }
}

class MyFailingList : List<int>, IEnumerable<int>
{
    public MyFailingList()
    {
        Add(1);
        Add(2);
        Add(3);
    }

    IEnumerator<int> IEnumerable<int>.GetEnumerator()
    {
        throw new InvalidOperationException("This is a failing enumerator.");
    }
}

That has nothing to do with AsSpan, which isn't used there.

@stephentoub stephentoub added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Aug 13, 2025
@xtqqczze
Copy link
Contributor Author

This has long been the case, even on .NET Framework

Is it a breaking change to change Linq to respect the IEnumerable<T> implementation? Take the following:

using System;
using System.Collections.Generic;
using System.Linq;

internal class Program
{
    private static IEnumerable<int> _en = new MyFailingList();

    static void Main()
    {
        foreach (var item in _en)
        {
            Console.WriteLine(item); // throws
        }
        foreach (var item in _en.Where(i => i % 2 == 0))
        {
            Console.WriteLine(item); // does not throw
        }
    }
}

class MyFailingList : List<int>, IEnumerable<int>
{
    public MyFailingList()
    {
        Add(1);
        Add(2);
        Add(3);
    }

    IEnumerator<int> IEnumerable<int>.GetEnumerator()
    {
        throw new InvalidOperationException("This is a failing enumerator.");
    }
}

@stephentoub
Copy link
Member

Is it a breaking change to change Linq to respect the IEnumerable implementation?

Yes. That doesn't mean we couldn't, but it's been this way for 20 years and I've not heard any concerns raised, so I don't think we should. Note, as well, this isn't just LINQ. If you just do:

foreach (var item in new MyFailingList()) { }

in the cited example, it will not throw, because the C# compiler will bind to the public GetEnumerator method on List rather than to its IEnumerable implementation. I don't see a good argument for changing behavior here at this point.

@teo-tsirpanis
Copy link
Contributor

There is an implicit contract in collection types supporting both IEnumerable<T> and IReadOnlyList<T>, that they must behave equivalently, or in other words, that foreach (var x in xs) { } and for (int i = 0; i < xs.Length; i++) { var x = xs[i]; } have no functional differences. People can go out of their way and write a hostile and non-conformant implementation of an interface, but the rest of the ecosystem does not have to be defensive and account for them.

@xtqqczze
Copy link
Contributor Author

There is an implicit contract in collection types supporting both IEnumerable<T> and IReadOnlyList<T>, that they must behave equivalently, or in other words, that foreach (var x in xs) { } and for (int i = 0; i < xs.Length; i++) { var x = xs[i]; } have no functional differences. People can go out of their way and write a hostile and non-conformant implementation of an interface, but the rest of the ecosystem does not have to be defensive and account for them.

A derived type of List<T> that also reimplements both IEnumerable<T> and IReadOnlyList<T> can have those two interfaces behave equivalently when enumerated. However, this equivalence does not extend if someone bypasses the interface and directly enumerates over the backing array, this may yield different results if the logical view and physical storage diverge.

@stephentoub
Copy link
Member

A derived type of List that also reimplements both IEnumerable and IReadOnlyList can have those two interfaces behave equivalently when enumerated.

But a developer has no ability to control the List's public APIs, e.g. its indexer, its public GetEnumerator, etc. It's also expected that those are in sync behaviorally with any such interface implementations, which then effectively means that no one should ever be changing the behavior of those interfaces on a derived type.

@teo-tsirpanis
Copy link
Contributor

A derived type of List<T> that also reimplements both IEnumerable<T> and IReadOnlyList<T> can have those two interfaces behave equivalently when enumerated.

This violates another implicit contract; that implict and explicit implementations of an interface have to behave equivalently (if not being identical), and the Liskov substitution principle for that matter. Since List<T>'s implicit interface implementations are not virtual, it is undefined behavior for a subclass to alter the explicit ones.

@xtqqczze
Copy link
Contributor Author

A derived type of List that also reimplements both IEnumerable and IReadOnlyList can have those two interfaces behave equivalently when enumerated.

But a developer has no ability to control the List's public APIs, e.g. its indexer, its public GetEnumerator, etc. It's also expected that those are in sync behaviorally with any such interface implementations, which then effectively means that no one should ever be changing the behavior of those interfaces on a derived type.

The derived type could reimplement these interfaces explicitly, hiding members inherited from List, for example:

https://github.com/microsoft/CollectServiceFabricData/blob/a7e559e95191fa2366251aea68191b8f14300098/src/CollectSFDataDll/Common/SynchronizedList.cs#L238-L250

@stephentoub
Copy link
Member

The derived type could reimplement these interfaces explicitly, hiding members inherited from List, for example:

I'm not talking about the interface methods; I'm talking about List's public APIs, which are not virtual and cannot be overridden.

@xtqqczze
Copy link
Contributor Author

xtqqczze commented Aug 14, 2025

The derived type could reimplement these interfaces explicitly, hiding members inherited from List, for example:

I'm not talking about the interface methods; I'm talking about List's public APIs, which are not virtual and cannot be overridden.

I meant an explicit interface implementation that hides inherited non-virtual members, using new, as in the following example:

See sharplab for how this works.

@stephentoub
Copy link
Member

I meant an explicit interface implementation that hides inherited non-virtual members

I understand what you're saying. And I'm saying code that queries for List<T> and then uses its public APIs will not be impacted by any interface implementations on the derived type. Such a derived type therefore can't expect its deviating behaviors in those interface implementations to be used.

@jeffhandley
Copy link
Member

@xtqqczze With the discussion above, I'm closing this PR without merge. If you think it should be further considered, please file an issue that captures what challenge is present with the current implementation. Thank you for the effort and PR regardless.

@jeffhandley jeffhandley closed this Sep 2, 2025
@github-actions github-actions bot locked and limited conversation to collaborators Oct 2, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Linq community-contribution Indicates that the PR has been added by a community member NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants