Skip to content

IPGlobalProperties.GetActiveTcpListeners() returns zero listeners on macOS 27 #133960

Description

@benjamindehli

Description

IPGlobalProperties.GetActiveTcpListeners() returns zero listeners on macOS 27.

This looks like the macOS sysctl PCB-list path returning nothing, plausibly a struct or ABI change in macOS 27. That attribution is inference from how the API is implemented on macOS, not something observed directly. Reproduces on .NET 8.0.421 and on the .NET 10.0.12 runtime.

Real-world impact: tooling that discovers local services this way silently finds nothing rather than failing loudly.

Reproduction Steps

  1. Start any process that listens on a TCP port. In my case an ASP.NET Core app on 127.0.0.1:5005, but python3 -m http.server 5005 behaves the same.

  2. Run:

    var l = System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners();
    Console.WriteLine($"listeners: {l.Length}");

    Output:

    listeners: 0
  3. Confirm that the operating system does list the socket at the same moment:

    $ lsof -nP -iTCP:5005
    Altinn.Ap 45270 ... TCP 127.0.0.1:5005 (LISTEN)
    
    $ netstat -an -p tcp | grep 5005
    tcp4  0  0  127.0.0.1.5005  *.*  LISTEN
  4. Run the same binary against the .NET 10 runtime:

    $ DOTNET_ROLL_FORWARD=LatestMajor dotnet bin/Debug/net8.0/tcpprobe.dll
    listeners: 0

Expected behavior

The machine's listening TCP endpoints, including 127.0.0.1:5005

Actual behavior

listeners: 0

Regression?

I don't know. I never ran GetActiveTcpListeners() on macOS 26, so all I can say is that the same SDK version returns zero on macOS 27 while lsof and netstat both see the listener.

Known Workarounds

Read the listener list from the operating system instead:

  • Parse netstat -anv -p tcp and filter on LISTEN. Note that on macOS 27 the verbose output merges the former pid and epid columns into a single process:pid field whose value can contain spaces, for example Code Helper:7260, so splitting on whitespace by field index no longer works for the process part.
  • P/Invoke libproc (proc_listpids plus proc_pidfdinfo), which returns ports and owning PIDs directly.
  • Shell out to lsof -nP -iTCP -sTCP:LISTEN.

Configuration

  • .NET SDK 8.0.421, and the .NET 10.0.12 runtime via DOTNET_ROLL_FORWARD=LatestMajor
  • macOS 27.0 (Golden Gate), arm64, Apple Silicon
  • Not specific to any one application. The listening socket in the repro was an ASP.NET Core app, but any listener shows the same result.

Other information

This surfaced while debugging a local development tool that could no longer find a running app after a macOS 26 to 27 upgrade. That tool uses its own netstat parsing rather than this API, so the two failures are independent, but both point at port enumeration on macOS 27 as the common factor.

Activity

  1. dotnet-policy-service commented on Sep 15, 2026

    @dotnet-policy-service
    Contributor

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

  2. MihuBot commented on Sep 15, 2026

    @MihuBot

    I'm a bot. Here is a possible related and/or duplicate issue (I may be wrong):

  3. added and removed
    untriagedNew issue has not been triaged by the area owner
    on Sep 15, 2026
  4. added this to the 12.0.0 milestone on Sep 15, 2026
  5. self-assigned this
    on Sep 17, 2026
  6. wfurt commented on Sep 17, 2026

    @wfurt
    Member

    I reproduced this on macOS 27.0 build 26A428 with .NET 8.0.421, the .NET
    10.0.12 runtime, and current runtime development bits.

    This is not an xtcpcb layout change. On macOS 27,
    net.inet.tcp.pcblist now filters the returned records for ordinary
    third-party processes. If the querying process does not own a TCP socket,
    the sysctl call returns only the 24-byte header and trailer and no PCB
    records. Sockets owned by the querying process remain visible.

    That explains why the existing runtime test still passes: it creates and
    queries its listener in the same process. A listener owned by a separate
    Python process is visible to lsof and netstat, but not to
    GetActiveTcpListeners().

    Apple's netstat binary has the private
    com.apple.private.network.statistics entitlement, which is unavailable
    to third-party applications.

    I also confirmed that GetActiveUdpListeners() is affected in the same
    way. A libproc-based fallback can recover many same-user sockets,
    including the reported local-development scenario, but cannot inspect
    every protected or system process, so it would not fully restore the
    previous machine-wide behavior.

    The only alternative would be spawn the netstat or wait for Apple to have the entertainment available for 3rd party pass.

  7. modified the milestones: 12.0.0, Future on Sep 17, 2026
  8. benjamindehli commented on Sep 17, 2026

    @benjamindehli
    Author

    Thanks for tracking that down, it explains everything I saw. The listener in my repro is a separate process started by the same user, so it falls inside what you describe a libproc fallback recovering.

    Possibly relevant to that:
    The tool that led me here (Altinn/altinn-studio#20453) spawns netstat and parses its output, and that still returns other processes' PIDs on macOS 27 because Apple's binary has the entitlement. So for same-user local development the information is reachable, just not through the sysctl the runtime uses.
    Happy to test a libproc-based build on 27.0 (26A428) if that would help.

  9. wfurt commented on Sep 17, 2026

    @wfurt
    Member

    we will need to figure out if we want to call external binary or document it its platform limitation. The changed behavior comes from Apple's change. I can somewhat see the motivation but 1) the netstat is still there so one can get equal information and 2) they should add public entitlement so one can choose.

  10. benjamindehli commented on Sep 18, 2026

    @benjamindehli
    Author

    Thanks a lot, Tomas! Either direction works for my case, since the tool that led me here spawns netstat itself.
    Today the failure is silent so an empty array looks just like a machine with nothing listening. So the first assumption (at least in my case) is a local misconfiguration rather than the API. It cost me a fair amount of time before I suspected GetActiveTcpListeners() at all. If this ends up documented as a platform limitation, stating plainly that on macOS 27 and later the result contains only sockets owned by the current process would help.
    However if there's a cheap/easy way to surface the truncation at runtime, that would help more.

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions