Skip to content

Getting file descriptor count on macOS throws #65254

Closed

Description

@Therzok

Description

A known way to get the file descriptor count on macOS is by using /dev/fd. Here is an example implementation in Rust.

The problem is that some file descriptors listed under /dev/fd are not stat-able, so they will fail when queried. One example is kqueues.

Reproduction Steps

Directory.EnumerateFileSystemEntries("/dev/fd").Count(); on macOS.

Expected behavior

The number of open file descriptors.

Actual behavior

C# > Directory.EnumerateFileSystemEntries("/dev/fd").Count();
╭─❌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ System.UnauthorizedAccessException: Access to the path '/dev/fd/5' is denied.                                                          │
│  ---> System.IO.IOException: Bad file descriptor                                                                                       │
│    --- End of inner exception stack trace ---                                                                                          │
│    at System.IO.FileStatus.ThrowOnCacheInitializationError(ReadOnlySpan`1 path)                                                        │
│    at System.IO.Enumeration.FileSystemEntry.get_IsSymbolicLink()                                                                       │
│    at System.IO.Enumeration.FileSystemEntry.Initialize(FileSystemEntry& entry, DirectoryEntry directoryEntry, ReadOnlySpan`1           │
│ directory, ReadOnlySpan`1 rootDirectory, ReadOnlySpan`1 originalRootDirectory, Span`1 pathBuffer)                                      │
│    at System.IO.Enumeration.FileSystemEnumerator`1.MoveNext()                                                                          │
│    at System.Linq.Enumerable.Count[TSource](IEnumerable`1 source)                                                                      │
│    at Submission#3.<<Initialize>>d__0.MoveNext()                                                                                       │
│ --- End of stack trace from previous location ---                                                                                      │
│    at Microsoft.CodeAnalysis.Scripting.ScriptExecutionState.RunSubmissionsAsync[TResult](ImmutableArray`1 precedingExecutors, Func`2   │
│ currentExecutor, StrongBox`1 exceptionHolderOpt, Func`2 catchExceptionOpt, CancellationToken cancellationToken)                        │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

Regression?

Not sure if it's a regression.

Known Workarounds

Manually PInvoke opendir and then iterate with readdir.

Configuration

dotnet 6.0.101, macOS 10.14+, x64 and arm64

Other information

No response

Activity

  1. ghost added
    untriagedNew issue has not been triaged by the area owner
    on Feb 12, 2022
  2. Therzok commented on Feb 12, 2022

    @Therzok
    ContributorAuthor

    area-System.IO, I guess :D

  3. danmoseley commented on Feb 13, 2022

    @danmoseley
    Contributor

    @Therzok what do you propose here? Presumably in the general case, we do want to throw this exception.

  4. Therzok commented on Feb 13, 2022

    @Therzok
    ContributorAuthor

    The API implicitly does the evaluation of dir entries and their metadata. Something like directory entry count shouldn't populate the stat information of files, unless it's explicitly requested.

    The equivalent C code is something like:

    DIR *dir = opendir("/dev/fd");
    struct dirent *entries;
    
    int count = 0;
    while ((entries = readdir(dir)) {
        count++;
    }

    I'd understand if something like GetFileSystemEntries would do implicit evaluation here to have a snapshot of the filesystem data, but EnumerateFileSystemEntries is advertised as lazy. Shouldn't the entry properties be populated lazily?

  5. adamsitnik commented on Feb 14, 2022

    @adamsitnik
    Member

    This code has been recently refactored by @tmds:

    What we have in 6.0:

    if (isUnknown)
    {
    isSymlink = entry.IsSymbolicLink;
    // Need to fail silently in case we are enumerating
    isDirectory = entry._status.IsDirectory(entry.FullPath, continueOnError: true);
    }
    // Same idea as the directory check, just repeated for (and tweaked due to the
    // nature of) symlinks.
    // Whether we had the dirent structure or not, we treat a symlink to a directory as a directory,
    // so we need to reflect that in our isDirectory variable.
    else if (isSymlink)
    {
    // Need to fail silently in case we are enumerating
    isDirectory = entry._status.IsDirectory(entry.FullPath, continueOnError: true);
    }
    entry._status.InitiallyDirectory = isDirectory;

    What we have in 7.0:

    if (isDirectory)
    {
    entry._isDirectory = true;
    }
    else if (isSymlink)
    {
    entry._isDirectory = entry._status.IsDirectory(entry.FullPath, continueOnError: true);
    }
    else if (isUnknown)
    {
    entry._isDirectory = entry._status.IsDirectory(entry.FullPath, continueOnError: true);
    if (entry._status.IsSymbolicLink(entry.FullPath, continueOnError: true))
    {
    entry._directoryEntry.InodeType = Interop.Sys.NodeType.DT_LNK;
    }
    }

    in theory it should not throw, let me start my Linux machine and check it

  6. tmds commented on Feb 14, 2022

    @tmds
    Member

    This code has been recently refactored by @tmds:

    Yes, this was refactored (and fixed) in #60214.
    One thing that was missing is continueOnError: true for IsSymbolicLink which is the cause for this exception.

  7. adamsitnik commented on Feb 14, 2022

    @adamsitnik
    Member

    I've added a test that ensures that the bug is not coming back: #65301

  8. removed
    untriagedNew issue has not been triaged by the area owner
    on Feb 14, 2022
  9. kasperk81 commented on Feb 14, 2022

    @kasperk81
    Contributor

    One thing that was missing is continueOnError: true for IsSymbolicLink which is the cause for this exception.

    is this fix planned for backporting to net6.0? if not, any workaround for users?

  10. tmds commented on Feb 14, 2022

    @tmds
    Member

    if not, any workaround for users?

    Not using .NET APIs.

    Maybe you can try PInvoking the proc_pidinfo API mentioned in this article: https://zameermanji.com/blog/2021/8/1/counting-open-file-descriptors-on-macos/.

  11. adamsitnik commented on Feb 14, 2022

    @adamsitnik
    Member

    I was not able to reproduce it using net5.0, it's a regression that has been introduced in net6.0 and it's specific to macOS.

    @tmds how difficult would it be to backport part of #60214 (the bug fix without refactor) to 6.0?

  12. tmds commented on Feb 15, 2022

    @tmds
    Member

    it's a regression that has been introduced in net6.0

    It was introduced in #52235

    and it's specific to macOS.

    Yes, and also specific to the special directory of /dev/fd which represents the open file descriptors of the process.
    One of these file descriptors doesn't support stat probably because its something special that is not ordinarily found in a real file system.

    how difficult would it be to backport part of #60214 (the bug fix without refactor) to 6.0?

    #60214 is a bugfix PR, it doesn't do much refactoring. There are little things to be left out. I think you should take it as a whole.

    There was a separate PR to refactor some things: #62721.

  13. adamsitnik commented on Feb 15, 2022

    @adamsitnik
    Member

    @tmds please excuse me, for some reason I mixed these two PRs

  14. adamsitnik commented on Feb 15, 2022

    @adamsitnik
    Member

    is this fix planned for backporting to net6.0? if not, any workaround for users?

    @kasperk81 in what circumstances have you hit this bug? Is it exactly the same use case?

  15. adamsitnik commented on Mar 14, 2022

    @adamsitnik
    Member

    I have not received any answers for a month, so I am going to close the issue (it's fixed in 7.0, we currently don't plan to backport it to 6.0)

  16. Therzok commented on Mar 14, 2022

    @Therzok
    ContributorAuthor

    None of the questions were directed towards me, so I'm pretty sure the issue should still be open, especially given it's a regression in net6.0

  17. reopened this on Apr 1, 2022
  18. jozkee commented on Apr 1, 2022

    @jozkee
    Member

    it's a regression that has been introduced in net6.0 and it's specific to macOS.

    @danmoseley @jeffhandley does this meets the bar for backporting to 6.0?
    I will check later if I can repro this on macOS.

  19. jeffhandley commented on Apr 6, 2022

    @jeffhandley
    Member

    @Therzok Could you describe the type of application this is impacting for you and whether you'd be able to adopt a preview release of .NET 7.0 to gain the fix? Additionally, it would be helpful if you could validate that the preview .NET 7 releases do in fact address the regression in your scenario. These data help us assess the risk/reward of backporting a fix.

    Thanks!

  20. Therzok commented on Apr 7, 2022

    @Therzok
    ContributorAuthor

    Hey @jeffhandley , thanks for the response!

    Could you describe the type of application this is impacting for you and whether you'd be able to adopt a preview release of .NET 7.0 to gain the fix?

    I work on Visual Studio for Mac. We use this API to check whether we are close to hitting the ridiculously low limit of open file handles (256). There currently is a workaround in place where we count filescriptors in C - did not have one at the time.

    I'm not sure if publishing a release on top of .NET 7.0 is an option right now.

    Testing on net7

    Console.WriteLine(Directory.EnumerateFileSystemEntries("/dev/fd").Count());

    Yields 38 which is correct and matches lsof -p.

    lsof -p output
    ...
    net7    27990 therzok    0u     CHR               16,6  0t38803                 757 /dev/ttys006
    net7    27990 therzok    1u     CHR               16,6  0t38803                 757 /dev/ttys006
    net7    27990 therzok    2u     CHR               16,6  0t38803                 757 /dev/ttys006
    net7    27990 therzok    3     PIPE 0xcc8e9a57a037605b    16384                     ->0xefd942e58ae5b326
    net7    27990 therzok    4     PIPE 0xefd942e58ae5b326    16384                     ->0xcc8e9a57a037605b
    net7    27990 therzok    5u  KQUEUE                                                 count=0, state=0xa
    net7    27990 therzok    6u     CHR               16,6  0t38803                 757 /dev/ttys006
    net7    27990 therzok    7u     CHR               16,6  0t38803                 757 /dev/ttys006
    net7    27990 therzok    8u     CHR               16,6  0t38803                 757 /dev/ttys006
    net7    27990 therzok    9u    unix 0xb90d2201eb329317      0t0                     /var/folders/vl/jslq2mpn1s940dlk4wdx93zc0000gn/T/dotnet-diagnostic-27990-1649337425-socket
    net7    27990 therzok   11r     REG                1,5 10703872            97260277 /usr/local/share/dotnet/shared/Microsoft.NETCore.App/7.0.0-preview.2.22152.2/System.Private.CoreLib.dll
    net7    27990 therzok   12r     REG                1,5 10703872            97260277 /usr/local/share/dotnet/shared/Microsoft.NETCore.App/7.0.0-preview.2.22152.2/System.Private.CoreLib.dll
    net7    27990 therzok   13     PIPE 0x727c006809e6d393    16384                     ->0x159a16bc09365536
    net7    27990 therzok   14     PIPE 0x159a16bc09365536    16384                     ->0x727c006809e6d393
    net7    27990 therzok   15u  KQUEUE                                                 count=0, state=0
    net7    27990 therzok   16r     REG                1,5     5120            97264134 /Users/therzok/Projects/net7/bin/Debug/net7.0/net7.dll
    net7    27990 therzok   17r     REG                1,5     5120            97264134 /Users/therzok/Projects/net7/bin/Debug/net7.0/net7.dll
    net7    27990 therzok   18r     REG                1,5    32768            97260327 /usr/local/share/dotnet/shared/Microsoft.NETCore.App/7.0.0-preview.2.22152.2/System.Runtime.dll
    net7    27990 therzok   19r     REG                1,5    32768            97260327 /usr/local/share/dotnet/shared/Microsoft.NETCore.App/7.0.0-preview.2.22152.2/System.Runtime.dll
    net7    27990 therzok   20r     REG                1,5   196096            97260230 /usr/local/share/dotnet/shared/Microsoft.NETCore.App/7.0.0-preview.2.22152.2/System.Console.dll
    net7    27990 therzok   21r     REG                1,5   196096            97260230 /usr/local/share/dotnet/shared/Microsoft.NETCore.App/7.0.0-preview.2.22152.2/System.Console.dll
    net7    27990 therzok   22r     REG                1,5   530944            97260351 /usr/local/share/dotnet/shared/Microsoft.NETCore.App/7.0.0-preview.2.22152.2/System.Linq.dll
    net7    27990 therzok   23r     REG                1,5   530944            97260351 /usr/local/share/dotnet/shared/Microsoft.NETCore.App/7.0.0-preview.2.22152.2/System.Linq.dll
    net7    27990 therzok   24r     REG                1,5     5632            97260347 /usr/local/share/dotnet/shared/Microsoft.NETCore.App/7.0.0-preview.2.22152.2/System.Threading.Thread.dll
    net7    27990 therzok   25r     REG                1,5     5632            97260347 /usr/local/share/dotnet/shared/Microsoft.NETCore.App/7.0.0-preview.2.22152.2/System.Threading.Thread.dll
    net7    27990 therzok   26r     REG                1,5    70144            97260221 /usr/local/share/dotnet/shared/Microsoft.NETCore.App/7.0.0-preview.2.22152.2/System.Threading.dll
    net7    27990 therzok   27r     REG                1,5    70144            97260221 /usr/local/share/dotnet/shared/Microsoft.NETCore.App/7.0.0-preview.2.22152.2/System.Threading.dll
    net7    27990 therzok   28r     REG                1,5    41472            97260255 /usr/local/share/dotnet/shared/Microsoft.NETCore.App/7.0.0-preview.2.22152.2/System.Runtime.InteropServices.dll
    net7    27990 therzok   29r     REG                1,5    41472            97260255 /usr/local/share/dotnet/shared/Microsoft.NETCore.App/7.0.0-preview.2.22152.2/System.Runtime.InteropServices.dll
    net7    27990 therzok   30u     CHR               16,6  0t38803                 757 /dev/ttys006
    net7    27990 therzok   31r     REG                1,5     5632            97260360 /usr/local/share/dotnet/shared/Microsoft.NETCore.App/7.0.0-preview.2.22152.2/Microsoft.Win32.Primitives.dll
    net7    27990 therzok   32r     REG                1,5     5632            97260360 /usr/local/share/dotnet/shared/Microsoft.NETCore.App/7.0.0-preview.2.22152.2/Microsoft.Win32.Primitives.dll
    net7    27990 therzok   33     PIPE 0xec3abe33a5671d10    16384                     ->0x6ba264456d0ba0ba
    net7    27990 therzok   34     PIPE 0x6ba264456d0ba0ba    16384                     ->0xec3abe33a5671d10
    net7    27990 therzok   35r     REG                1,5   165888            97260376 /usr/local/share/dotnet/shared/Microsoft.NETCore.App/7.0.0-preview.2.22152.2/System.Memory.dll
    net7    27990 therzok   36r     REG                1,5   255488            97260297 /usr/local/share/dotnet/shared/Microsoft.NETCore.App/7.0.0-preview.2.22152.2/System.Collections.dll
    net7    27990 therzok   37r     REG                1,5   255488            97260297 /usr/local/share/dotnet/shared/Microsoft.NETCore.App/7.0.0-preview.2.22152.2/System.Collections.dll
    net7    27990 therzok   38r     REG                1,5   165888            97260376 /usr/local/share/dotnet/shared/Microsoft.NETCore.App/7.0.0-preview.2.22152.2/System.Memory.dll
    ...
    

    Closing as we added a workaround on our end, but having this API work in net6 would be nice.

  21. jeffhandley commented on Apr 7, 2022

    @jeffhandley
    Member

    Thank you, @Therzok. Since you have the (albeit unfortunate) workaround in place are are unblocked, and the backport isn't 100% straightforward/contained, I'm not inclined to backport the fix. If others report the same issue in a way that they are blocked, that would encourage the backport though.

  22. ghost locked as resolved and limited conversation to collaborators on May 7, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions