Skip to content

Commit 9b493d2

Browse files
tmdsjozkee
andauthored
DriveInfo.Linux: use procfs mountinfo for formats and mount point paths. (#116102)
* DriveInfo.Linux: use procfs mountinfo for formats and mount point paths. For the format, mountinfo includes a string 'filesystem type' field which is more granular than the statfs f_type numeric field. For example: the ext2, ext3 and ext4 are distinct mountinfo values which have the same same f_type value. For the mount paths, mountinfo provides the mount point paths relative to the process's root directory. When the process runs with changed root dir (chroot), the paths are adjusted for it. * Remove stackalloced buffer. * Resolve symbolic links. * Fix Common.Tests build on non-Linux. * Add test that gets the DriveFormat for /tmp. * Use test to debug CI failure. * Fix check for root mount path. * pal_mount: use buffer length based on API used. * Fix not calling native GetFileSystemTypeNameForMountPoint on non-Linux. * Add back using 'f_type' when 'f_fstypename' is not available. * UnixFileSystemTypes: prefer Linux file system type strings as names, and remove duplicate value members. * Fix wasi build breakage. --------- Co-authored-by: David Cantú <dacantu@microsoft.com>
1 parent abb26bc commit 9b493d2

File tree

18 files changed

+407
-163
lines changed

18 files changed

+407
-163
lines changed

src/libraries/Common/src/Interop/Linux/cgroups/Interop.cgroups.cs

Lines changed: 27 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ internal enum CGroupVersion
2626

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

@@ -209,13 +207,10 @@ internal static bool TryReadMemoryValueFromFile(string path, out ulong result)
209207
private static unsafe CGroupVersion FindCGroupVersion()
210208
{
211209
CGroupVersion cgroupVersion = CGroupVersion.None;
212-
const int MountPointFormatBufferSizeInBytes = 32;
213-
byte* formatBuffer = stackalloc byte[MountPointFormatBufferSizeInBytes]; // format names should be small
214-
long numericFormat;
215-
int result = Interop.Sys.GetFormatInfoForMountPoint(SysFsCgroupFileSystemPath, formatBuffer, MountPointFormatBufferSizeInBytes, &numericFormat);
216-
if (result == 0)
210+
Interop.Error error = Interop.procfs.GetFileSystemTypeForRealPath(SysFsCgroupFileSystemPath, out string format);
211+
if (error == Interop.Error.SUCCESS)
217212
{
218-
if (numericFormat == (int)Interop.Sys.UnixFileSystemTypes.cgroup2fs)
213+
if (format == "cgroup2")
219214
{
220215
cgroupVersion = CGroupVersion.CGroup2;
221216
}
@@ -304,7 +299,7 @@ internal static string FindCGroupPath(string hierarchyRoot, string hierarchyMoun
304299
/// <returns>true if the mount was found; otherwise, null.</returns>
305300
private static bool TryFindHierarchyMount(CGroupVersion cgroupVersion, string subsystem, [NotNullWhen(true)] out string? root, [NotNullWhen(true)] out string? path)
306301
{
307-
return TryFindHierarchyMount(cgroupVersion, ProcMountInfoFilePath, subsystem, out root, out path);
302+
return TryFindHierarchyMount(cgroupVersion, Interop.procfs.ProcMountInfoFilePath, subsystem, out root, out path);
308303
}
309304

310305
/// <summary>Find the cgroup mount information for the specified subsystem.</summary>
@@ -325,65 +320,41 @@ internal static bool TryFindHierarchyMount(CGroupVersion cgroupVersion, string m
325320
string? line;
326321
while ((line = reader.ReadLine()) != null)
327322
{
328-
// Look for an entry that has cgroup as the "filesystem type"
329-
// and, for cgroup1, that has options containing the specified subsystem
330-
// See man page for /proc/[pid]/mountinfo for details, e.g.:
331-
// (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11)
332-
// 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
333-
// but (7) is optional and could exist as multiple fields; the (8) separator marks
334-
// the end of the optional values.
335-
336-
const string Separator = " - ";
337-
int endOfOptionalFields = line.IndexOf(Separator, StringComparison.Ordinal);
338-
if (endOfOptionalFields == -1)
323+
if (Interop.procfs.TryParseMountInfoLine(line, out Interop.procfs.ParsedMount mount))
339324
{
340-
// Malformed line.
341-
continue;
342-
}
343-
344-
string postSeparatorLine = line.Substring(endOfOptionalFields + Separator.Length);
345-
string[] postSeparatorlineParts = postSeparatorLine.Split(' ');
346-
if (postSeparatorlineParts.Length < 3)
347-
{
348-
// Malformed line.
349-
continue;
350-
}
351-
352-
if (cgroupVersion == CGroupVersion.CGroup1)
353-
{
354-
bool validCGroup1Entry = ((postSeparatorlineParts[0] == "cgroup") &&
355-
(Array.IndexOf(postSeparatorlineParts[2].Split(','), subsystem) >= 0));
356-
if (!validCGroup1Entry)
325+
if (cgroupVersion == CGroupVersion.CGroup1)
357326
{
358-
continue;
327+
bool validCGroup1Entry = mount.FileSystemType.SequenceEqual("cgroup") && mount.SuperOptions.IndexOf(subsystem) >= 0;
328+
if (!validCGroup1Entry)
329+
{
330+
continue;
331+
}
359332
}
360-
}
361-
else if (cgroupVersion == CGroupVersion.CGroup2)
362-
{
363-
bool validCGroup2Entry = postSeparatorlineParts[0] == "cgroup2";
364-
if (!validCGroup2Entry)
333+
else if (cgroupVersion == CGroupVersion.CGroup2)
365334
{
366-
continue;
367-
}
368-
369-
}
370-
else
371-
{
372-
Debug.Fail($"Unexpected cgroup version \"{cgroupVersion}\"");
373-
}
335+
bool validCGroup2Entry = mount.FileSystemType.SequenceEqual("cgroup2");
336+
if (!validCGroup2Entry)
337+
{
338+
continue;
339+
}
374340

341+
}
342+
else
343+
{
344+
Debug.Fail($"Unexpected cgroup version \"{cgroupVersion}\"");
345+
}
375346

376-
string[] lineParts = line.Substring(0, endOfOptionalFields).Split(' ');
377-
root = lineParts[3];
378-
path = lineParts[4];
347+
root = mount.Root.ToString();
348+
path = mount.MountPoint.ToString();
379349

380-
return true;
350+
return true;
351+
}
381352
}
382353
}
383354
}
384355
catch (Exception e)
385356
{
386-
Debug.Fail($"Failed to read or parse \"{ProcMountInfoFilePath}\": {e}");
357+
Debug.Fail($"Failed to read or parse \"{mountInfoFilePath}\": {e}");
387358
}
388359
}
389360

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
6+
internal static partial class Interop
7+
{
8+
internal static partial class @procfs
9+
{
10+
internal ref struct ParsedMount
11+
{
12+
public required ReadOnlySpan<char> Root { get; init; }
13+
public required ReadOnlySpan<char> MountPoint { get; init; }
14+
public required ReadOnlySpan<char> FileSystemType { get; init; }
15+
public required ReadOnlySpan<char> SuperOptions { get; init; }
16+
}
17+
18+
internal static bool TryParseMountInfoLine(ReadOnlySpan<char> line, out ParsedMount result)
19+
{
20+
result = default;
21+
22+
// See man page for /proc/[pid]/mountinfo for details, e.g.:
23+
// (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11)
24+
// 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
25+
// but (7) is optional and could exist as multiple fields; the (8) separator marks
26+
// the end of the optional values.
27+
28+
MemoryExtensions.SpanSplitEnumerator<char> fields = line.Split(' ');
29+
30+
// (1) mount ID
31+
// (2) parent ID
32+
// (3) major:minor
33+
if (!fields.MoveNext() || !fields.MoveNext() || !fields.MoveNext())
34+
{
35+
return false;
36+
}
37+
38+
// (4) root
39+
if (!fields.MoveNext())
40+
{
41+
return false;
42+
}
43+
ReadOnlySpan<char> root = line[fields.Current];
44+
45+
// (5) mount point
46+
if (!fields.MoveNext())
47+
{
48+
return false;
49+
}
50+
ReadOnlySpan<char> mountPoint = line[fields.Current];
51+
52+
// (8) separator
53+
const string Separator = " - ";
54+
int endOfOptionalFields = line.IndexOf(Separator, StringComparison.Ordinal);
55+
if (endOfOptionalFields == -1)
56+
{
57+
return false;
58+
}
59+
line = line.Slice(endOfOptionalFields + Separator.Length);
60+
fields = line.Split(' ');
61+
62+
// (9) filesystem type
63+
if (!fields.MoveNext())
64+
{
65+
return false;
66+
}
67+
ReadOnlySpan<char> fileSystemType = line[fields.Current];
68+
69+
// (10) mount source
70+
if (!fields.MoveNext())
71+
{
72+
return false;
73+
}
74+
75+
// (11) super options
76+
if (!fields.MoveNext())
77+
{
78+
return false;
79+
}
80+
ReadOnlySpan<char> superOptions = line[fields.Current];
81+
82+
result = new ParsedMount()
83+
{
84+
Root = root,
85+
MountPoint = mountPoint,
86+
FileSystemType = fileSystemType,
87+
SuperOptions = superOptions
88+
};
89+
return true;
90+
}
91+
}
92+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Diagnostics;
6+
using System.IO;
7+
8+
internal static partial class Interop
9+
{
10+
internal static partial class @procfs
11+
{
12+
internal const string ProcMountInfoFilePath = "/proc/self/mountinfo";
13+
14+
internal static Error GetFileSystemTypeForRealPath(string path, out string format)
15+
{
16+
format = "";
17+
18+
if (File.Exists(ProcMountInfoFilePath))
19+
{
20+
try
21+
{
22+
ReadOnlySpan<char> currentFormat = default;
23+
int currentBestLength = 0;
24+
25+
using StreamReader reader = new(ProcMountInfoFilePath);
26+
27+
string? line;
28+
while ((line = reader.ReadLine()) is not null)
29+
{
30+
if (TryParseMountInfoLine(line, out ParsedMount mount))
31+
{
32+
if (mount.MountPoint.Length < currentBestLength)
33+
{
34+
continue;
35+
}
36+
37+
if (!path.StartsWith(mount.MountPoint))
38+
{
39+
continue;
40+
}
41+
42+
if (mount.MountPoint.Length == path.Length)
43+
{
44+
currentFormat = mount.FileSystemType;
45+
break;
46+
}
47+
48+
if (mount.MountPoint.Length > 1 && path[mount.MountPoint.Length] != '/')
49+
{
50+
continue;
51+
}
52+
53+
currentBestLength = mount.MountPoint.Length;
54+
currentFormat = mount.FileSystemType;
55+
}
56+
}
57+
58+
if (currentFormat.Length > 0)
59+
{
60+
format = currentFormat.ToString();
61+
return Error.SUCCESS;
62+
}
63+
return Error.ENOENT;
64+
}
65+
catch (Exception e)
66+
{
67+
Debug.Fail($"Failed to read \"{ProcMountInfoFilePath}\": {e}");
68+
}
69+
}
70+
71+
return Error.ENOTSUP;
72+
}
73+
}
74+
}

src/libraries/Common/src/Interop/Unix/System.Native/Interop.MountPoints.FormatInfo.cs

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -33,45 +33,57 @@ internal struct MountPointInformation
3333
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetSpaceInfoForMountPoint", SetLastError = true)]
3434
internal static partial int GetSpaceInfoForMountPoint([MarshalAs(UnmanagedType.LPUTF8Str)] string name, out MountPointInformation mpi);
3535

36-
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetFormatInfoForMountPoint", SetLastError = true)]
37-
internal static unsafe partial int GetFormatInfoForMountPoint(
38-
[MarshalAs(UnmanagedType.LPUTF8Str)] string name,
39-
byte* formatNameBuffer,
40-
int bufferLength,
41-
long* formatType);
42-
43-
internal static int GetFormatInfoForMountPoint(string name, out string format)
36+
internal static unsafe Error GetFileSystemTypeNameForMountPoint(string name, out string format)
4437
{
45-
return GetFormatInfoForMountPoint(name, out format, out _);
46-
}
38+
if (OperatingSystem.IsLinux())
39+
{
40+
// Canonicalize and resolve symbolic links.
41+
string? path = Sys.RealPath(name);
42+
if (path is null)
43+
{
44+
format = "";
45+
return GetLastError();
46+
}
4747

48-
internal static int GetFormatInfoForMountPoint(string name, out DriveType type)
49-
{
50-
return GetFormatInfoForMountPoint(name, out _, out type);
51-
}
48+
Error error = procfs.GetFileSystemTypeForRealPath(path, out format);
5249

53-
private static unsafe int GetFormatInfoForMountPoint(string name, out string format, out DriveType type)
54-
{
50+
// When there is no procfs mountinfo fall through.
51+
if (error != Error.ENOTSUP)
52+
{
53+
return error;
54+
}
55+
}
56+
57+
long formatType;
5558
byte* formatBuffer = stackalloc byte[MountPointFormatBufferSizeInBytes]; // format names should be small
56-
long numericFormat;
57-
int result = GetFormatInfoForMountPoint(name, formatBuffer, MountPointFormatBufferSizeInBytes, &numericFormat);
59+
int result = GetFileSystemTypeNameForMountPoint(name, formatBuffer, MountPointFormatBufferSizeInBytes, &formatType);
5860
if (result == 0)
5961
{
60-
// Check if we have a numeric answer or string
61-
format = numericFormat != -1 ?
62-
Enum.GetName(typeof(UnixFileSystemTypes), numericFormat) ?? string.Empty :
63-
Marshal.PtrToStringUTF8((IntPtr)formatBuffer)!;
64-
type = GetDriveType(format);
62+
format = formatType == -1 ? Marshal.PtrToStringUTF8((IntPtr)formatBuffer)!
63+
: (Enum.GetName(typeof(UnixFileSystemTypes), formatType) ?? "");
64+
return Error.SUCCESS;
6565
}
6666
else
6767
{
6868
format = string.Empty;
69-
type = DriveType.Unknown;
69+
return GetLastError();
7070
}
71+
}
7172

72-
return result;
73+
internal static Error GetDriveTypeForMountPoint(string name, out DriveType type)
74+
{
75+
Error error = GetFileSystemTypeNameForMountPoint(name, out string format);
76+
type = error == Error.SUCCESS ? GetDriveType(format) : DriveType.Unknown;
77+
return error;
7378
}
7479

80+
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetFileSystemTypeNameForMountPoint", SetLastError = true)]
81+
private static unsafe partial int GetFileSystemTypeNameForMountPoint(
82+
[MarshalAs(UnmanagedType.LPUTF8Str)] string name,
83+
byte* formatNameBuffer,
84+
int bufferLength,
85+
long* formatType);
86+
7587
/// <summary>Categorizes a file system name into a drive type.</summary>
7688
/// <param name="fileSystemName">The name to categorize.</param>
7789
/// <returns>The recognized drive type.</returns>
@@ -261,8 +273,10 @@ private static DriveType GetDriveType(string fileSystemName)
261273
case "aptfs":
262274
case "avfs":
263275
case "bdev":
276+
case "bpf":
264277
case "binfmt_misc":
265278
case "cgroup":
279+
case "cgroup2":
266280
case "cgroupfs":
267281
case "cgroup2fs":
268282
case "configfs":
@@ -280,6 +294,7 @@ private static DriveType GetDriveType(string fileSystemName)
280294
case "fd":
281295
case "fdesc":
282296
case "fuse.gvfsd-fuse":
297+
case "fuse.portal":
283298
case "fusectl":
284299
case "futexfs":
285300
case "hugetlbfs":

0 commit comments

Comments
 (0)