Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
31 changes: 30 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,35 @@ jobs:
${{ matrix.dotnet.sdk_version }}
- name: Run tests
run: dotnet test --blame-hang --blame-hang-dump-type none --blame-hang-timeout 60s --framework ${{ matrix.dotnet.framework_version }} -- tests/StatsdClient.Tests/
unit-tests-macos:
name: Tests (macOS)
runs-on: macos-latest
permissions:
actions: read
contents: read
strategy:
fail-fast: false
matrix:
dotnet:
- framework_version: net6.0
sdk_version: 6.0.x
- framework_version: net7.0
sdk_version: 7.0.x
- framework_version: net8.0
sdk_version: 8.0.x
- framework_version: net9.0
sdk_version: 9.0.x
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup .NET Core
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
9.0.x
${{ matrix.dotnet.sdk_version }}
- name: Run tests
run: dotnet test --blame-hang --blame-hang-dump-type none --blame-hang-timeout 60s --framework ${{ matrix.dotnet.framework_version }} -- tests/StatsdClient.Tests/
unit-tests-windows:
name: Tests (Windows)
runs-on: windows-latest
Expand All @@ -71,7 +100,7 @@ jobs:
- framework_version: net5.0
sdk_version: 5.0.x
- framework_version: net6.0
sdk_version: skip-install
sdk_version: 6.0.x
- framework_version: net7.0
sdk_version: 7.0.x
- framework_version: net8.0
Expand Down
16 changes: 11 additions & 5 deletions src/StatsdClient/IFileSystem.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System;
using System.IO;
using Mono.Unix.Native;
using System.Runtime.InteropServices;

namespace StatsdClient
{
Expand Down Expand Up @@ -81,15 +81,21 @@ public TextReader OpenText(string path)
/// <returns>True if the file stat was successful, false otherwise</returns>
public bool TryStat(string path, out ulong inode)
{
if (Environment.OSVersion.Platform == PlatformID.Unix &&
Syscall.stat(path, out var stat) > 0)
#if NET461
// Unix Domain Sockets not supported on .NET Framework (always runs on Windows).
inode = 0;
return false;
#else
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
inode = stat.st_ino;
return true;
// P/Invoke to libc
return NativeMethods.TryStat(path, out inode);
}

// Unsupported OS
inode = 0;
return false;
#endif
}
}
}
160 changes: 160 additions & 0 deletions src/StatsdClient/NativeMethods.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
#if !NET461

using System.Runtime.InteropServices;

namespace StatsdClient;

/// <summary>
/// P/Invoke wrapper for Unix system calls
/// </summary>
internal static class NativeMethods
{
/// <summary>
/// Attempts to get the inode of the file at the given path
/// </summary>
/// <param name="path">File path</param>
/// <param name="inode">The inode number if successful, 0 otherwise</param>
/// <returns>True if successful, false otherwise</returns>
public static bool TryStat(string path, out ulong inode)
{
try
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
int result = StatMacOS(path, out var statBuf);
if (result == 0)
{
inode = statBuf.st_ino;
return true;
}
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
// Linux (and other Unix-like systems)
int result = StatLinux(path, out var statBuf);
if (result == 0)
{
inode = statBuf.st_ino;
return true;
}
}
}
catch
{
// P/Invoke failed
}

// P/Invoke failed or unsupported OS
inode = 0;
return false;
}

// Linux stat syscall (x86_64 and ARM64/aarch64)
[DllImport("libc", SetLastError = true, EntryPoint = "stat", CharSet = CharSet.Ansi)]
private static extern int StatLinux(string pathname, out StatStructLinux buf);

// macOS stat syscall (x86_64 and ARM64/Apple Silicon)
[DllImport("libc", SetLastError = true, EntryPoint = "stat", CharSet = CharSet.Ansi)]
private static extern int StatMacOS(string pathname, out StatStructMacOS buf);

// Linux struct stat (asm-generic/stat.h - used by ARM64/aarch64 and x86_64)
// Reference: https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/stat.h
[StructLayout(LayoutKind.Sequential)]
private struct StatStructLinux
{
public ulong st_dev; // Device
public ulong st_ino; // File serial number (inode)
public uint st_mode; // File mode
public uint st_nlink; // Link count
public uint st_uid; // User ID of the file's owner
Comment on lines +65 to +69

Choose a reason for hiding this comment

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

P1 Badge Fix Linux stat struct size for P/Invoke

On 64‑bit Linux (e.g., ubuntu-latest runners) the native struct stat layout uses 64‑bit st_nlink/st_blksize with a total size of 144 bytes (/usr/include/x86_64-linux-gnu/bits/struct_stat.h), but StatStructLinux declares those fields as 32‑bit and omits the trailing padding, making the managed struct only 128 bytes. The P/Invoke call to stat() therefore writes past the allocated buffer, which can corrupt the stack or return garbage whenever TryStat runs (OriginDetection calls it during startup). The struct definition needs to match the platform layout (e.g., 64‑bit fields or use stat64) to avoid undefined behavior.

Useful? React with 👍 / 👎.

public uint st_gid; // Group ID of the file's group
public ulong st_rdev; // Device number, if device
public ulong __pad1;
public long st_size; // Size of file, in bytes
public int st_blksize; // Optimal block size for I/O
public int __pad2;
public long st_blocks; // Number 512-byte blocks allocated
public long st_atime; // Time of last access
public ulong st_atime_nsec;
public long st_mtime; // Time of last modification
public ulong st_mtime_nsec;
public long st_ctime; // Time of last status change
public ulong st_ctime_nsec;
public uint __unused4;
public uint __unused5;
}

// macOS struct stat (sys/stat.h - x86_64 and ARM64)
// Reference: https://stackoverflow.com/questions/39671660
[StructLayout(LayoutKind.Explicit, Size = 144)]
private struct StatStructMacOS
{
[FieldOffset(0)]
public int st_dev; // Device (4 bytes)

[FieldOffset(4)]
public ushort st_mode; // File mode (2 bytes)

[FieldOffset(6)]
public ushort st_nlink; // Link count (2 bytes)

[FieldOffset(8)]
public ulong st_ino; // File serial number (inode) (8 bytes)

[FieldOffset(16)]
public uint st_uid; // User ID (4 bytes)

[FieldOffset(20)]
public uint st_gid; // Group ID (4 bytes)

[FieldOffset(24)]
public int st_rdev; // Device number (4 bytes)

// Padding to align timespec at offset 32
// [FieldOffset(28)] - 4 bytes padding

// Four timespec structures (each 16 bytes: 8 bytes tv_sec + 8 bytes tv_nsec)
[FieldOffset(32)]
public long st_atimespec_sec; // Access time seconds (8 bytes)

[FieldOffset(40)]
public long st_atimespec_nsec; // Access time nanoseconds (8 bytes)

[FieldOffset(48)]
public long st_mtimespec_sec; // Modification time seconds (8 bytes)

[FieldOffset(56)]
public long st_mtimespec_nsec; // Modification time nanoseconds (8 bytes)

[FieldOffset(64)]
public long st_ctimespec_sec; // Status change time seconds (8 bytes)

[FieldOffset(72)]
public long st_ctimespec_nsec; // Status change time nanoseconds (8 bytes)

[FieldOffset(80)]
public long st_birthtimespec_sec; // Birth time seconds (8 bytes) - macOS specific

[FieldOffset(88)]
public long st_birthtimespec_nsec; // Birth time nanoseconds (8 bytes) - macOS specific

[FieldOffset(96)]
public long st_size; // File size in bytes (8 bytes)

[FieldOffset(104)]
public long st_blocks; // Blocks allocated (8 bytes)

[FieldOffset(112)]
public int st_blksize; // Optimal block size (4 bytes)

[FieldOffset(116)]
public uint st_flags; // User defined flags - macOS specific (4 bytes)

[FieldOffset(120)]
public uint st_gen; // File generation number - macOS specific (4 bytes)

// Remaining fields up to 144 bytes
}
}

#endif
2 changes: 1 addition & 1 deletion src/StatsdClient/StatsdClient.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<Description>A DogStatsD client for C#. DogStatsD is an extension of the StatsD metric server for use with Datadog. For more information visit http://datadoghq.com.</Description>
<Authors>Datadog</Authors>
<TargetFrameworks>net461;netstandard2.0;netcoreapp3.1;net6.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<PackageVersion>9.0.0</PackageVersion>
<Version>9.0.0</Version>
<PackageLicenseUrl>https://github.com/DataDog/dogstatsd-csharp-client/blob/master/MIT-LICENCE.md</PackageLicenseUrl>
Expand All @@ -21,7 +22,6 @@
<DefineConstants>HAS_SPAN</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Mono.Unix" Version="7.1.0-final.1.21458.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.0-beta004" PrivateAssets="All" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
Loading
Loading