Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Getting file descriptor count on macOS throws #65254

Closed
Therzok opened this issue Feb 12, 2022 · 20 comments
Closed

Getting file descriptor count on macOS throws #65254

Therzok opened this issue Feb 12, 2022 · 20 comments

Comments

@Therzok
Copy link
Contributor

Therzok commented Feb 12, 2022

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

@dotnet-issue-labeler dotnet-issue-labeler bot added the untriaged New issue has not been triaged by the area owner label Feb 12, 2022
@dotnet-issue-labeler
Copy link

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

@Therzok
Copy link
Contributor Author

Therzok commented Feb 12, 2022

area-System.IO, I guess :D

@ghost
Copy link

ghost commented Feb 12, 2022

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

Issue Details

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

Author: Therzok
Assignees: -
Labels:

area-System.IO, untriaged

Milestone: -

@marek-safar marek-safar added the os-mac-os-x macOS aka OSX label Feb 12, 2022
@danmoseley
Copy link
Member

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

@Therzok
Copy link
Contributor Author

Therzok commented Feb 13, 2022

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?

@adamsitnik
Copy link
Member

adamsitnik commented Feb 14, 2022

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

@tmds
Copy link
Member

tmds commented Feb 14, 2022

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.

@adamsitnik
Copy link
Member

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

@adamsitnik adamsitnik removed the untriaged New issue has not been triaged by the area owner label Feb 14, 2022
@kasperk81
Copy link
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?

@tmds
Copy link
Member

tmds commented Feb 14, 2022

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/.

@adamsitnik
Copy link
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?

@tmds
Copy link
Member

tmds commented Feb 15, 2022

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.

@adamsitnik
Copy link
Member

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

@adamsitnik
Copy link
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?

@adamsitnik
Copy link
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)

@Therzok
Copy link
Contributor Author

Therzok commented Mar 14, 2022

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

@jozkee jozkee reopened this Apr 1, 2022
@jozkee
Copy link
Member

jozkee commented Apr 1, 2022

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.

@jeffhandley
Copy link
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!

@Therzok
Copy link
Contributor Author

Therzok commented Apr 7, 2022

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.

@Therzok Therzok closed this as completed Apr 7, 2022
@jeffhandley
Copy link
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.

@ghost ghost locked as resolved and limited conversation to collaborators May 7, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

8 participants