Skip to content

DriveInfo.Linux: use procfs mountinfo for formats and mount point paths. #116102

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

Merged
merged 13 commits into from
Jun 16, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 27 additions & 56 deletions src/libraries/Common/src/Interop/Linux/cgroups/Interop.cgroups.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ internal enum CGroupVersion

/// <summary>Path to cgroup filesystem that tells us which version of cgroup is in use.</summary>
private const string SysFsCgroupFileSystemPath = "/sys/fs/cgroup";
/// <summary>Path to mountinfo file in procfs for the current process.</summary>
private const string ProcMountInfoFilePath = "/proc/self/mountinfo";
/// <summary>Path to cgroup directory in procfs for the current process.</summary>
private const string ProcCGroupFilePath = "/proc/self/cgroup";

Expand Down Expand Up @@ -209,13 +207,10 @@ internal static bool TryReadMemoryValueFromFile(string path, out ulong result)
private static unsafe CGroupVersion FindCGroupVersion()
{
CGroupVersion cgroupVersion = CGroupVersion.None;
const int MountPointFormatBufferSizeInBytes = 32;
byte* formatBuffer = stackalloc byte[MountPointFormatBufferSizeInBytes]; // format names should be small
long numericFormat;
int result = Interop.Sys.GetFormatInfoForMountPoint(SysFsCgroupFileSystemPath, formatBuffer, MountPointFormatBufferSizeInBytes, &numericFormat);
if (result == 0)
Interop.Error error = Interop.procfs.GetFileSystemTypeForRealPath(SysFsCgroupFileSystemPath, out string format);
if (error == Interop.Error.SUCCESS)
{
if (numericFormat == (int)Interop.Sys.UnixFileSystemTypes.cgroup2fs)
if (format == "cgroup2")
{
cgroupVersion = CGroupVersion.CGroup2;
}
Expand Down Expand Up @@ -304,7 +299,7 @@ internal static string FindCGroupPath(string hierarchyRoot, string hierarchyMoun
/// <returns>true if the mount was found; otherwise, null.</returns>
private static bool TryFindHierarchyMount(CGroupVersion cgroupVersion, string subsystem, [NotNullWhen(true)] out string? root, [NotNullWhen(true)] out string? path)
{
return TryFindHierarchyMount(cgroupVersion, ProcMountInfoFilePath, subsystem, out root, out path);
return TryFindHierarchyMount(cgroupVersion, Interop.procfs.ProcMountInfoFilePath, subsystem, out root, out path);
}

/// <summary>Find the cgroup mount information for the specified subsystem.</summary>
Expand All @@ -325,65 +320,41 @@ internal static bool TryFindHierarchyMount(CGroupVersion cgroupVersion, string m
string? line;
while ((line = reader.ReadLine()) != null)
{
// Look for an entry that has cgroup as the "filesystem type"
// and, for cgroup1, that has options containing the specified subsystem
// See man page for /proc/[pid]/mountinfo for details, e.g.:
// (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11)
// 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
// but (7) is optional and could exist as multiple fields; the (8) separator marks
// the end of the optional values.

const string Separator = " - ";
int endOfOptionalFields = line.IndexOf(Separator, StringComparison.Ordinal);
if (endOfOptionalFields == -1)
if (Interop.procfs.TryParseMountInfoLine(line, out Interop.procfs.ParsedMount mount))
{
// Malformed line.
continue;
}

string postSeparatorLine = line.Substring(endOfOptionalFields + Separator.Length);
string[] postSeparatorlineParts = postSeparatorLine.Split(' ');
if (postSeparatorlineParts.Length < 3)
{
// Malformed line.
continue;
}

if (cgroupVersion == CGroupVersion.CGroup1)
{
bool validCGroup1Entry = ((postSeparatorlineParts[0] == "cgroup") &&
(Array.IndexOf(postSeparatorlineParts[2].Split(','), subsystem) >= 0));
if (!validCGroup1Entry)
if (cgroupVersion == CGroupVersion.CGroup1)
{
continue;
bool validCGroup1Entry = mount.FileSystemType.SequenceEqual("cgroup") && mount.SuperOptions.IndexOf(subsystem) >= 0;
if (!validCGroup1Entry)
{
continue;
}
}
}
else if (cgroupVersion == CGroupVersion.CGroup2)
{
bool validCGroup2Entry = postSeparatorlineParts[0] == "cgroup2";
if (!validCGroup2Entry)
else if (cgroupVersion == CGroupVersion.CGroup2)
{
continue;
}

}
else
{
Debug.Fail($"Unexpected cgroup version \"{cgroupVersion}\"");
}
bool validCGroup2Entry = mount.FileSystemType.SequenceEqual("cgroup2");
if (!validCGroup2Entry)
{
continue;
}

}
else
{
Debug.Fail($"Unexpected cgroup version \"{cgroupVersion}\"");
}

string[] lineParts = line.Substring(0, endOfOptionalFields).Split(' ');
root = lineParts[3];
path = lineParts[4];
root = mount.Root.ToString();
path = mount.MountPoint.ToString();

return true;
return true;
}
}
}
}
catch (Exception e)
{
Debug.Fail($"Failed to read or parse \"{ProcMountInfoFilePath}\": {e}");
Debug.Fail($"Failed to read or parse \"{mountInfoFilePath}\": {e}");
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;

internal static partial class Interop
{
internal static partial class @procfs
{
internal ref struct ParsedMount
{
public required ReadOnlySpan<char> Root { get; init; }
public required ReadOnlySpan<char> MountPoint { get; init; }
public required ReadOnlySpan<char> FileSystemType { get; init; }
public required ReadOnlySpan<char> SuperOptions { get; init; }
}

internal static bool TryParseMountInfoLine(ReadOnlySpan<char> line, out ParsedMount result)
{
result = default;

// See man page for /proc/[pid]/mountinfo for details, e.g.:
// (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11)
// 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
// but (7) is optional and could exist as multiple fields; the (8) separator marks
// the end of the optional values.

MemoryExtensions.SpanSplitEnumerator<char> fields = line.Split(' ');

// (1) mount ID
// (2) parent ID
// (3) major:minor
if (!fields.MoveNext() || !fields.MoveNext() || !fields.MoveNext())
{
return false;
}

// (4) root
if (!fields.MoveNext())
{
return false;
}
ReadOnlySpan<char> root = line[fields.Current];

// (5) mount point
if (!fields.MoveNext())
{
return false;
}
ReadOnlySpan<char> mountPoint = line[fields.Current];

// (8) separator
const string Separator = " - ";
int endOfOptionalFields = line.IndexOf(Separator, StringComparison.Ordinal);
if (endOfOptionalFields == -1)
{
return false;
}
line = line.Slice(endOfOptionalFields + Separator.Length);
fields = line.Split(' ');

// (9) filesystem type
if (!fields.MoveNext())
Comment on lines +58 to +63
Copy link
Member Author

@tmds tmds Jun 4, 2025

Choose a reason for hiding this comment

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

Each block handles the field that is in the comment at the first line.
// (8) separator updates fields to be past the separator.

{
return false;
}
ReadOnlySpan<char> fileSystemType = line[fields.Current];

// (10) mount source
if (!fields.MoveNext())
{
return false;
}

// (11) super options
if (!fields.MoveNext())
{
return false;
}
ReadOnlySpan<char> superOptions = line[fields.Current];

result = new ParsedMount()
{
Root = root,
MountPoint = mountPoint,
FileSystemType = fileSystemType,
SuperOptions = superOptions
};
return true;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics;
using System.IO;

internal static partial class Interop
{
internal static partial class @procfs
{
internal const string ProcMountInfoFilePath = "/proc/self/mountinfo";

internal static Error GetFileSystemTypeForRealPath(string path, out string format)
{
format = "";

if (File.Exists(ProcMountInfoFilePath))
{
try
{
Comment on lines +18 to +21
Copy link
Member

Choose a reason for hiding this comment

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

Reduce nesting.

Suggested change
if (File.Exists(ProcMountInfoFilePath))
{
try
{
if (!File.Exists(ProcMountInfoFilePath))
return Error.ENOTSUP
try
{

ReadOnlySpan<char> currentFormat = default;
int currentBestLength = 0;

using StreamReader reader = new(ProcMountInfoFilePath);

string? line;
while ((line = reader.ReadLine()) is not null)
{
if (TryParseMountInfoLine(line, out ParsedMount mount))
{
if (mount.MountPoint.Length < currentBestLength)
{
continue;
}

if (!path.StartsWith(mount.MountPoint))
{
continue;
}

if (mount.MountPoint.Length == path.Length)
{
currentFormat = mount.FileSystemType;
break;
}

if (path[mount.MountPoint.Length] != '/')
{
continue;
}

currentBestLength = mount.MountPoint.Length;
currentFormat = mount.FileSystemType;
}
}

if (currentFormat.Length > 0)
{
format = currentFormat.ToString();
return Error.SUCCESS;
}
return Error.ENOENT;
}
catch (Exception e)
{
Debug.Fail($"Failed to read \"{ProcMountInfoFilePath}\": {e}");
}
}

return Error.ENOTSUP;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,43 +33,53 @@ internal struct MountPointInformation
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetSpaceInfoForMountPoint", SetLastError = true)]
internal static partial int GetSpaceInfoForMountPoint([MarshalAs(UnmanagedType.LPUTF8Str)] string name, out MountPointInformation mpi);

[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetFormatInfoForMountPoint", SetLastError = true)]
internal static unsafe partial int GetFormatInfoForMountPoint(
[MarshalAs(UnmanagedType.LPUTF8Str)] string name,
byte* formatNameBuffer,
int bufferLength,
long* formatType);

internal static int GetFormatInfoForMountPoint(string name, out string format)
internal static Error GetFileSystemTypeNameForMountPoint(string name, out string format)
{
return GetFormatInfoForMountPoint(name, out format, out _);
if (OperatingSystem.IsLinux())
{
// Canonicalize and resolve symbolic links.
string? path = Sys.RealPath(name);
Copy link
Member Author

Choose a reason for hiding this comment

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

This function is now going to make several syscalls where there used to be one.

If this turns out to be an performance issue, we can reduce the nr of calls:

  • If we use statx then we can get the STATX_MNT_ID which corresponds to the first field in mountinfo. This replaces multiple syscalls made by realpath into a single syscall.

  • For Linux 6.8+, we can call statx with STATX_MNT_ID_UNIQUE and then use that with the statmount syscall with STATMOUNT_FS_TYPE which avoids the syscalls for opening and reading mountinfo to get the filesystem type name.

I don't intend to include these changes as part of this PR (unless someone finds it important).

if (path is null)
{
format = "";
Copy link
Member

Choose a reason for hiding this comment

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

string.Empty is used on ln 68.

Suggested change
format = "";
format = string.Empty;

return GetLastError();
}

return procfs.GetFileSystemTypeForRealPath(path, out format);
}
else
{
return GetFileSystemTypeNameForMountPoint(name, out format);
}
}

internal static int GetFormatInfoForMountPoint(string name, out DriveType type)
internal static Error GetDriveTypeForMountPoint(string name, out DriveType type)
{
return GetFormatInfoForMountPoint(name, out _, out type);
Error error = GetFileSystemTypeNameForMountPoint(name, out string format);
type = error == Error.SUCCESS ? GetDriveType(format) : DriveType.Unknown;
return error;
}

private static unsafe int GetFormatInfoForMountPoint(string name, out string format, out DriveType type)
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetFileSystemTypeNameForMountPoint", SetLastError = true)]
private static unsafe partial int GetFileSystemTypeNameForMountPoint(
[MarshalAs(UnmanagedType.LPUTF8Str)] string name,
byte* formatNameBuffer,
int bufferLength);

private static unsafe Error GetFileSystemTypeNameForMountPoint(string name, out ReadOnlySpan<char> format)
{
byte* formatBuffer = stackalloc byte[MountPointFormatBufferSizeInBytes]; // format names should be small
long numericFormat;
int result = GetFormatInfoForMountPoint(name, formatBuffer, MountPointFormatBufferSizeInBytes, &numericFormat);
int result = GetFileSystemTypeNameForMountPoint(name, formatBuffer, MountPointFormatBufferSizeInBytes);
if (result == 0)
{
// Check if we have a numeric answer or string
format = numericFormat != -1 ?
Enum.GetName(typeof(UnixFileSystemTypes), numericFormat) ?? string.Empty :
Marshal.PtrToStringUTF8((IntPtr)formatBuffer)!;
type = GetDriveType(format);
format = Marshal.PtrToStringUTF8((IntPtr)formatBuffer)!;
return Error.SUCCESS;
}
else
{
format = string.Empty;
type = DriveType.Unknown;
return GetLastError();
}

return result;
}

/// <summary>Categorizes a file system name into a drive type.</summary>
Expand Down Expand Up @@ -261,8 +271,10 @@ private static DriveType GetDriveType(string fileSystemName)
case "aptfs":
case "avfs":
case "bdev":
case "bpf":
case "binfmt_misc":
case "cgroup":
case "cgroup2":
case "cgroupfs":
case "cgroup2fs":
case "configfs":
Expand All @@ -280,6 +292,7 @@ private static DriveType GetDriveType(string fileSystemName)
case "fd":
case "fdesc":
case "fuse.gvfsd-fuse":
case "fuse.portal":
case "fusectl":
case "futexfs":
case "hugetlbfs":
Expand Down
Loading
Loading