diff --git a/src/libraries/Common/src/Interop/BSD/System.Native/Interop.TcpConnectionInfo.cs b/src/libraries/Common/src/Interop/BSD/System.Native/Interop.TcpConnectionInfo.cs index 946059dc870934..b0b73fc1d0c291 100644 --- a/src/libraries/Common/src/Interop/BSD/System.Native/Interop.TcpConnectionInfo.cs +++ b/src/libraries/Common/src/Interop/BSD/System.Native/Interop.TcpConnectionInfo.cs @@ -26,16 +26,16 @@ public struct NativeTcpConnectionInformation public TcpState State; } - [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetEstimatedTcpConnectionCount")] + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetEstimatedTcpConnectionCount", SetLastError = true)] public static partial int GetEstimatedTcpConnectionCount(); - [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetActiveTcpConnectionInfos")] + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetActiveTcpConnectionInfos", SetLastError = true)] public static unsafe partial int GetActiveTcpConnectionInfos(NativeTcpConnectionInformation* infos, int* infoCount); - [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetEstimatedUdpListenerCount")] + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetEstimatedUdpListenerCount", SetLastError = true)] public static partial int GetEstimatedUdpListenerCount(); - [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetActiveUdpListeners")] + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetActiveUdpListeners", SetLastError = true)] public static unsafe partial int GetActiveUdpListeners(IPEndPointInfo* infos, int* infoCount); } } diff --git a/src/libraries/System.Net.NetworkInformation/src/System/Net/NetworkInformation/BsdIPGlobalProperties.cs b/src/libraries/System.Net.NetworkInformation/src/System/Net/NetworkInformation/BsdIPGlobalProperties.cs index e2894a530f8f58..a51154cc7d76c4 100644 --- a/src/libraries/System.Net.NetworkInformation/src/System/Net/NetworkInformation/BsdIPGlobalProperties.cs +++ b/src/libraries/System.Net.NetworkInformation/src/System/Net/NetworkInformation/BsdIPGlobalProperties.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.Runtime.Versioning; namespace System.Net.NetworkInformation @@ -12,8 +13,7 @@ private unsafe TcpConnectionInformation[] GetTcpConnections(bool listeners) int realCount = Interop.Sys.GetEstimatedTcpConnectionCount(); if (realCount == -1) { - // The platform (e.g. OpenBSD) does not expose the TCP connection table. - throw new PlatformNotSupportedException(SR.net_InformationUnavailableOnPlatform); + ThrowExceptionForNativeError(); } int infoCount = realCount * 2; @@ -22,7 +22,7 @@ private unsafe TcpConnectionInformation[] GetTcpConnections(bool listeners) { if (Interop.Sys.GetActiveTcpConnectionInfos(infosPtr, &infoCount) == -1) { - throw new NetworkInformationException(SR.net_PInvokeError); + ThrowExceptionForNativeError(); } } @@ -78,8 +78,7 @@ public override unsafe IPEndPoint[] GetActiveUdpListeners() int realCount = Interop.Sys.GetEstimatedUdpListenerCount(); if (realCount == -1) { - // The platform (e.g. OpenBSD) does not expose the UDP listener table. - throw new PlatformNotSupportedException(SR.net_InformationUnavailableOnPlatform); + ThrowExceptionForNativeError(); } int infoCount = realCount * 2; @@ -88,7 +87,7 @@ public override unsafe IPEndPoint[] GetActiveUdpListeners() { if (Interop.Sys.GetActiveUdpListeners(infosPtr, &infoCount) == -1) { - throw new NetworkInformationException(SR.net_PInvokeError); + ThrowExceptionForNativeError(); } } @@ -107,6 +106,18 @@ public override unsafe IPEndPoint[] GetActiveUdpListeners() return endPoints; } + [DoesNotReturn] + private static void ThrowExceptionForNativeError() + { + Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo(); + if (error.Error == Interop.Error.ENOTSUP) + { + throw new PlatformNotSupportedException(SR.net_InformationUnavailableOnPlatform); + } + + throw new NetworkInformationException(error.RawErrno); + } + public override IcmpV4Statistics GetIcmpV4Statistics() { return new BsdIcmpV4Statistics(); diff --git a/src/libraries/System.Net.NetworkInformation/tests/FunctionalTests/IPGlobalPropertiesTest.cs b/src/libraries/System.Net.NetworkInformation/tests/FunctionalTests/IPGlobalPropertiesTest.cs index 4bbcc24f6ad566..2a3e2cae05a3cc 100644 --- a/src/libraries/System.Net.NetworkInformation/tests/FunctionalTests/IPGlobalPropertiesTest.cs +++ b/src/libraries/System.Net.NetworkInformation/tests/FunctionalTests/IPGlobalPropertiesTest.cs @@ -3,15 +3,19 @@ using System.Collections.Generic; using System.Net.Sockets; +using System.Runtime.InteropServices; using System.Threading.Tasks; +using Microsoft.DotNet.RemoteExecutor; using Microsoft.DotNet.XUnitExtensions; using Xunit; using Xunit.Abstractions; namespace System.Net.NetworkInformation.Tests { - public class IPGlobalPropertiesTest + public partial class IPGlobalPropertiesTest { + private const int OperationNotPermitted = 1; // EPERM on macOS. + private const int RemoteTimeoutMilliseconds = 10_000; private readonly ITestOutputHelper _log; public static IEnumerable Loopbacks() @@ -32,6 +36,56 @@ public IPGlobalPropertiesTest(ITestOutputHelper output) _log = output; } + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [PlatformSpecific(TestPlatforms.OSX)] + [InlineData(nameof(IPGlobalProperties.GetActiveTcpConnections), "pcbcount")] + [InlineData(nameof(IPGlobalProperties.GetActiveTcpConnections), "pcblist")] + [InlineData(nameof(IPGlobalProperties.GetActiveTcpListeners), "pcbcount")] + [InlineData(nameof(IPGlobalProperties.GetActiveTcpListeners), "pcblist")] + [InlineData(nameof(IPGlobalProperties.GetActiveUdpListeners), "pcbcount")] + [InlineData(nameof(IPGlobalProperties.GetActiveUdpListeners), "pcblist")] + public void IPGlobalProperties_SysctlDenied_ThrowsNetworkInformationException(string methodName, string queryName) + { + // This uses macOS's deprecated raw-profile sandbox API and requires an unsandboxed test host. + // A fresh child isolates the irreversible restriction but cannot escape an inherited sandbox. + RemoteExecutor.Invoke(static (methodName, queryName) => + { + string protocol = methodName == nameof(IPGlobalProperties.GetActiveUdpListeners) ? "udp" : "tcp"; + string profile = $"(version 1)(allow default)(deny sysctl-read (sysctl-name \"net.inet.{protocol}.{queryName}\"))"; + int result = SandboxInit(profile, 0, out nint errorBuffer); + try + { + Assert.True(result == 0, Marshal.PtrToStringUTF8(errorBuffer)); + } + finally + { + if (errorBuffer != 0) + { + SandboxFreeError(errorBuffer); + } + } + + IPGlobalProperties properties = IPGlobalProperties.GetIPGlobalProperties(); + Func query = methodName switch + { + nameof(IPGlobalProperties.GetActiveTcpConnections) => properties.GetActiveTcpConnections, + nameof(IPGlobalProperties.GetActiveTcpListeners) => properties.GetActiveTcpListeners, + nameof(IPGlobalProperties.GetActiveUdpListeners) => properties.GetActiveUdpListeners, + _ => throw new InvalidOperationException(methodName) + }; + + Marshal.SetLastPInvokeError(0); + NetworkInformationException exception = Assert.Throws(() => query()); + Assert.Equal(OperationNotPermitted, exception.ErrorCode); + }, methodName, queryName, new RemoteInvokeOptions { TimeOut = RemoteTimeoutMilliseconds }).Dispose(); + } + + [LibraryImport("/usr/lib/libsandbox.dylib", EntryPoint = "sandbox_init", StringMarshalling = StringMarshalling.Utf8)] + private static partial int SandboxInit(string profile, ulong flags, out nint errorBuffer); + + [LibraryImport("/usr/lib/libsandbox.dylib", EntryPoint = "sandbox_free_error")] + private static partial void SandboxFreeError(nint errorBuffer); + [Fact] [SkipOnPlatform(TestPlatforms.Android, "Expected behavior is different on Android")] [SkipOnPlatform(TestPlatforms.OpenBSD, "TCP/UDP connection enumeration is unsupported on OpenBSD")] diff --git a/src/libraries/System.Net.NetworkInformation/tests/FunctionalTests/System.Net.NetworkInformation.Functional.Tests.csproj b/src/libraries/System.Net.NetworkInformation/tests/FunctionalTests/System.Net.NetworkInformation.Functional.Tests.csproj index e3ea99ef506258..10c32695faee55 100644 --- a/src/libraries/System.Net.NetworkInformation/tests/FunctionalTests/System.Net.NetworkInformation.Functional.Tests.csproj +++ b/src/libraries/System.Net.NetworkInformation/tests/FunctionalTests/System.Net.NetworkInformation.Functional.Tests.csproj @@ -1,6 +1,7 @@ true + true ../../src/Resources/Strings.resx $(NetCoreAppCurrent) true diff --git a/src/native/libs/System.Native/pal_networkstatistics.c b/src/native/libs/System.Native/pal_networkstatistics.c index ec9c8bb0cb54e7..5659525d1ec3fa 100644 --- a/src/native/libs/System.Native/pal_networkstatistics.c +++ b/src/native/libs/System.Native/pal_networkstatistics.c @@ -84,13 +84,67 @@ static _Atomic(int) icmp6statSize = sizeof(struct icmp6stat); -static size_t GetEstimatedSize(const char* name) +static int32_t GetEstimatedSize(const char* name, size_t* size) { - void* oldp = NULL; - size_t oldlenp = 0; + *size = 0; + return sysctlbyname(name, NULL, size, NULL, 0); +} + +static int32_t GetSysctlBuffer(const char* name, uint8_t** buffer, size_t* size) +{ + *buffer = NULL; + *size = 0; + + size_t capacity; + if (GetEstimatedSize(name, &capacity) != 0) + { + return -1; + } + if (capacity == 0) + { + return 0; + } - sysctlbyname(name, oldp, &oldlenp, NULL, 0); - return oldlenp; + uint8_t* data = (uint8_t*)malloc(capacity); + if (data == NULL) + { + errno = ENOMEM; + return -1; + } + + for (;;) + { + size_t length = capacity; + if (sysctlbyname(name, data, &length, NULL, 0) == 0) + { + *buffer = data; + *size = length; + return 0; + } + + int error = errno; + free(data); + if (error != ENOMEM) + { + errno = error; + return -1; + } + + // A failed sysctl can change length; grow from the allocated capacity instead. + size_t nextCapacity; + if (!multiply_s(capacity, (size_t)2, &nextCapacity) || nextCapacity <= capacity) + { + errno = ENOMEM; + return -1; + } + capacity = nextCapacity; + data = (uint8_t*)malloc(capacity); + if (data == NULL) + { + errno = ENOMEM; + return -1; + } + } } int32_t SystemNative_GetTcpGlobalStatistics(TcpGlobalStatistics* retStats) @@ -282,7 +336,14 @@ int32_t SystemNative_GetIcmpv6GlobalStatistics(Icmpv6GlobalStatistics* retStats) { // We did not provide enough memory. // macOS 11.0 added new member to icmp6stat so as FreeBSD reported changes between versions. - oldlenp = GetEstimatedSize(sysctlName); + if (GetEstimatedSize(sysctlName, &oldlenp) != 0) + { + int error = errno; + free(buffer); + memset(retStats, 0, sizeof(Icmpv6GlobalStatistics)); + errno = error; + return -1; + } free(buffer); buffer = malloc(oldlenp); if (!buffer) @@ -350,49 +411,43 @@ int32_t SystemNative_GetIcmpv6GlobalStatistics(Icmpv6GlobalStatistics* retStats) int32_t SystemNative_GetEstimatedTcpConnectionCount(void) { - int32_t count; + int32_t count = 0; size_t oldlenp = sizeof(count); - sysctlbyname("net.inet.tcp.pcbcount", &count, &oldlenp, NULL, 0); + if (sysctlbyname("net.inet.tcp.pcbcount", &count, &oldlenp, NULL, 0) != 0) + { + return -1; + } return count; } int32_t SystemNative_GetActiveTcpConnectionInfos(NativeTcpConnectionInformation* infos, int32_t* infoCount) { - assert(infos != NULL); assert(infoCount != NULL); + assert(infos != NULL || *infoCount == 0); const char* sysctlName = "net.inet.tcp.pcblist"; + int32_t capacity = *infoCount; + *infoCount = 0; - size_t estimatedSize = GetEstimatedSize(sysctlName); - uint8_t* buffer = (uint8_t*)malloc(estimatedSize * sizeof(uint8_t)); - if (buffer == NULL) + size_t estimatedSize; + uint8_t* buffer; + if (GetSysctlBuffer(sysctlName, &buffer, &estimatedSize) != 0) { - errno = ENOMEM; return -1; } - - void* newp = NULL; - size_t newlen = 0; - - while (sysctlbyname(sysctlName, buffer, &estimatedSize, newp, newlen) != 0) + if (estimatedSize == 0) { free(buffer); - size_t tmpEstimatedSize; - if (!multiply_s(estimatedSize, (size_t)2, &tmpEstimatedSize) || - (buffer = (uint8_t*)malloc(tmpEstimatedSize * sizeof(uint8_t))) == NULL) - { - errno = ENOMEM; - return -1; - } - estimatedSize = tmpEstimatedSize; + return 0; } int32_t count = (int32_t)(estimatedSize / sizeof(struct xtcpcb)); - if (count > *infoCount) + if (count > capacity) { // Not enough space in caller-supplied buffer. free(buffer); *infoCount = count; + errno = ENOBUFS; return -1; } *infoCount = count; @@ -464,49 +519,59 @@ int32_t SystemNative_GetActiveTcpConnectionInfos(NativeTcpConnectionInformation* int32_t SystemNative_GetEstimatedUdpListenerCount(void) { - int32_t count; +#if defined(__FreeBSD__) + // FreeBSD exposes pcblist, but not pcbcount. + size_t size; + if (GetEstimatedSize("net.inet.udp.pcblist", &size) != 0) + { + return -1; + } + size_t count = size / sizeof(struct xinpcb); + if (count > INT32_MAX) + { + errno = EOVERFLOW; + return -1; + } + return (int32_t)count; +#else + int32_t count = 0; size_t oldlenp = sizeof(count); - sysctlbyname("net.inet.udp.pcbcount", &count, &oldlenp, NULL, 0); + if (sysctlbyname("net.inet.udp.pcbcount", &count, &oldlenp, NULL, 0) != 0) + { + return -1; + } return count; +#endif } int32_t SystemNative_GetActiveUdpListeners(IPEndPointInfo* infos, int32_t* infoCount) { - assert(infos != NULL); assert(infoCount != NULL); + assert(infos != NULL || *infoCount == 0); const char* sysctlName = "net.inet.udp.pcblist"; + int32_t capacity = *infoCount; + *infoCount = 0; - size_t estimatedSize = GetEstimatedSize(sysctlName); - uint8_t* buffer = (uint8_t*)malloc(estimatedSize * sizeof(uint8_t)); - if (buffer == NULL) + size_t estimatedSize; + uint8_t* buffer; + if (GetSysctlBuffer(sysctlName, &buffer, &estimatedSize) != 0) { - errno = ENOMEM; return -1; } - - void* newp = NULL; - size_t newlen = 0; - - while (sysctlbyname(sysctlName, buffer, &estimatedSize, newp, newlen) != 0) + if (estimatedSize == 0) { free(buffer); - size_t tmpEstimatedSize; - if (!multiply_s(estimatedSize, (size_t)2, &tmpEstimatedSize) || - (buffer = (uint8_t*)malloc(tmpEstimatedSize * sizeof(uint8_t))) == NULL) - { - errno = ENOMEM; - return -1; - } - estimatedSize = tmpEstimatedSize; + return 0; } int32_t count = (int32_t)(estimatedSize / sizeof(struct xinpcb)); - if (count > *infoCount) + if (count > capacity) { // Not enough space in caller-supplied buffer. free(buffer); *infoCount = count; + errno = ENOBUFS; return -1; } diff --git a/src/native/libs/System.Native/pal_networkstatistics.h b/src/native/libs/System.Native/pal_networkstatistics.h index af4bdf1082043d..8017e0c84cabdc 100644 --- a/src/native/libs/System.Native/pal_networkstatistics.h +++ b/src/native/libs/System.Native/pal_networkstatistics.h @@ -158,12 +158,18 @@ PALEXPORT int32_t SystemNative_GetIcmpv4GlobalStatistics(Icmpv4GlobalStatistics* PALEXPORT int32_t SystemNative_GetIcmpv6GlobalStatistics(Icmpv6GlobalStatistics* retStats); +// Returns the estimated count, or -1 with errno set on failure. PALEXPORT int32_t SystemNative_GetEstimatedTcpConnectionCount(void); +// Returns 0 on success, or -1 with errno set on failure. +// infoCount is the capacity on input and the actual count (or required count for ENOBUFS) on output. PALEXPORT int32_t SystemNative_GetActiveTcpConnectionInfos(NativeTcpConnectionInformation* infos, int32_t* infoCount); +// Returns the estimated count, or -1 with errno set on failure. PALEXPORT int32_t SystemNative_GetEstimatedUdpListenerCount(void); +// Returns 0 on success, or -1 with errno set on failure. +// infoCount is the capacity on input and the actual count (or required count for ENOBUFS) on output. PALEXPORT int32_t SystemNative_GetActiveUdpListeners(IPEndPointInfo* infos, int32_t* infoCount); PALEXPORT int32_t SystemNative_GetNativeIPInterfaceStatistics(char* interfaceName, NativeIPInterfaceStatistics* retStats);