diff --git a/src/libraries/Common/src/Interop/Windows/Advapi32/Interop.RegQueryValueEx.cs b/src/libraries/Common/src/Interop/Windows/Advapi32/Interop.RegQueryValueEx.cs index 355f792be734ab..aee1aac4dcb6b1 100644 --- a/src/libraries/Common/src/Interop/Windows/Advapi32/Interop.RegQueryValueEx.cs +++ b/src/libraries/Common/src/Interop/Windows/Advapi32/Interop.RegQueryValueEx.cs @@ -22,30 +22,12 @@ internal static partial int RegQueryValueEx( ref int lpcbData); [LibraryImport(Libraries.Advapi32, EntryPoint = "RegQueryValueExW", StringMarshalling = StringMarshalling.Utf16)] - internal static partial int RegQueryValueEx( - SafeRegistryHandle hKey, - string? lpValueName, - int[]? lpReserved, - ref int lpType, - ref int lpData, - ref int lpcbData); - - [LibraryImport(Libraries.Advapi32, EntryPoint = "RegQueryValueExW", StringMarshalling = StringMarshalling.Utf16)] - internal static partial int RegQueryValueEx( - SafeRegistryHandle hKey, - string? lpValueName, - int[]? lpReserved, - ref int lpType, - ref long lpData, - ref int lpcbData); - - [LibraryImport(Libraries.Advapi32, EntryPoint = "RegQueryValueExW", StringMarshalling = StringMarshalling.Utf16)] - internal static partial int RegQueryValueEx( + internal static unsafe partial int RegQueryValueEx( SafeRegistryHandle hKey, string? lpValueName, - int[]? lpReserved, - ref int lpType, - [Out] char[]? lpData, - ref int lpcbData); + int* lpReserved, + int* lpType, + byte* lpData, + uint* lpcbData); } } diff --git a/src/libraries/Microsoft.Win32.Registry/src/Microsoft/Win32/RegistryKey.Windows.cs b/src/libraries/Microsoft.Win32.Registry/src/Microsoft/Win32/RegistryKey.Windows.cs index f3de3537de407e..abe640ea9ca73b 100644 --- a/src/libraries/Microsoft.Win32.Registry/src/Microsoft/Win32/RegistryKey.Windows.cs +++ b/src/libraries/Microsoft.Win32.Registry/src/Microsoft/Win32/RegistryKey.Windows.cs @@ -8,7 +8,9 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Runtime.InteropServices; using System.Security; +using System.Text; /* Note on transaction support: @@ -530,263 +532,220 @@ private unsafe string[] GetValueNamesCore(int values) } [return: NotNullIfNotNull(nameof(defaultValue))] - private object? InternalGetValueCore(string? name, object? defaultValue, bool doNotExpand) + private unsafe object? InternalGetValueCore(string? name, object? defaultValue, bool doNotExpand) { - object? data = defaultValue; - int type = 0; - int datasize = 0; - - int ret = Interop.Advapi32.RegQueryValueEx(_hkey, name, null, ref type, (byte[]?)null, ref datasize); + // Create an initial stack buffer large enough to satisfy many reg keys. We need to call RegQueryValueEx + // in order to determine the type of the value, and we can avoid further retries if all of the data can be + // retrieved in that single call with this buffer. If we do need to grow, we grow into a pooled array. The + // caller is always handed back a copy of this data, either in an array, a string, or a boxed integer. + Span span = stackalloc byte[512]; + byte[]? pooledArray = null; + if (IsPerfDataKey()) + { + // If this is a performance key (rare usage), we're not actually retrieving data stored in the registry: + // calling RegQueryValueEx causes the system to collect the data from the appropriate provider. The value + // is expected to be much larger than for other keys, such that our stack-allocated space is less likely + // to be sufficient, so we immediately grow into the ArrayPool, using the same buffer size chosen + // in .NET Framework (until such time as a better estimate is selected). Additionally, the returned + // length when dealing with perf data keys can't be trusted, so the buffer needs to be zero'd. + span = pooledArray = ArrayPool.Shared.Rent(65_000); + span.Clear(); + } - if (ret != 0) + try { - if (IsPerfDataKey()) + // Loop in case we need to try again with a larger buffer size. + while (true) { - int size = 65000; - int sizeInput = size; + int type = 0; + int result; + int dataLength = span.Length; - int r; - byte[] blob = new byte[size]; - while (Interop.Errors.ERROR_MORE_DATA == (r = Interop.Advapi32.RegQueryValueEx(_hkey, name, null, ref type, blob, ref sizeInput))) + fixed (byte* lpData = &MemoryMarshal.GetReference(span)) { - if (size == int.MaxValue) + result = Interop.Advapi32.RegQueryValueEx(_hkey, name, null, &type, lpData, (uint*)&dataLength); + if (dataLength < 0) { - // ERROR_MORE_DATA was returned however we cannot increase the buffer size beyond Int32.MaxValue - Win32Error(r, name); + // Greater than 2GB values aren't supported. + throw new IOException(SR.Arg_RegValueTooLarge); } - else if (size > (int.MaxValue / 2)) - { - // at this point in the loop "size * 2" would cause an overflow - size = int.MaxValue; - } - else - { - size *= 2; - } - sizeInput = size; - blob = new byte[size]; - } - if (r != 0) - { - Win32Error(r, name); - } - return blob; - } - else - { - // For stuff like ERROR_FILE_NOT_FOUND, we want to return null (data). - // Some OS's returned ERROR_MORE_DATA even in success cases, so we - // want to continue on through the function. - if (ret != Interop.Errors.ERROR_MORE_DATA) - { - return data; } - } - } - if (datasize < 0) - { - // unexpected code path - Debug.Fail("[InternalGetValue] RegQueryValue returned ERROR_SUCCESS but gave a negative datasize"); - datasize = 0; - } - - switch (type) - { - case Interop.Advapi32.RegistryValues.REG_NONE: - case Interop.Advapi32.RegistryValues.REG_DWORD_BIG_ENDIAN: - case Interop.Advapi32.RegistryValues.REG_BINARY: + // If RegQueryValueEx told us we need a larger buffer, get one and then loop around to try again. + if (result == Interop.Errors.ERROR_MORE_DATA) { - byte[] blob = new byte[datasize]; - Interop.Advapi32.RegQueryValueEx(_hkey, name, null, ref type, blob, ref datasize); - data = blob; - } - break; - case Interop.Advapi32.RegistryValues.REG_QWORD: - { // also REG_QWORD_LITTLE_ENDIAN - if (datasize > 8) + if (IsPerfDataKey()) { - // prevent an AV in the edge case that datasize is larger than sizeof(long) - goto case Interop.Advapi32.RegistryValues.REG_BINARY; + // In the case of a performance key, dataLength is not reliable, but we know the existing + // size was insufficient, so double the buffer size. + dataLength = span.Length * 2; } - long blob = 0; - Debug.Assert(datasize == 8, "datasize==8"); - // Here, datasize must be 8 when calling this - Interop.Advapi32.RegQueryValueEx(_hkey, name, null, ref type, ref blob, ref datasize); - data = blob; - } - break; - case Interop.Advapi32.RegistryValues.REG_DWORD: - { // also REG_DWORD_LITTLE_ENDIAN - if (datasize > 4) + if (pooledArray is not null) { - // prevent an AV in the edge case that datasize is larger than sizeof(int) - goto case Interop.Advapi32.RegistryValues.REG_QWORD; + // This should only happen if the registry key was changed concurrently, such that + // we called RegQueryValueEx with our initial buffer size that was too small, we then + // rented a buffer of the reportedly right size and called RegQueryValueEx again, but + // it still came back with ERROR_MORE_DATA again. + byte[] toReturn = pooledArray; + pooledArray = null; + ArrayPool.Shared.Return(toReturn); } - int blob = 0; - Debug.Assert(datasize == 4, "datasize==4"); - // Here, datasize must be four when calling this - Interop.Advapi32.RegQueryValueEx(_hkey, name, null, ref type, ref blob, ref datasize); - - data = blob; - } - break; - case Interop.Advapi32.RegistryValues.REG_SZ: - { - if (datasize % 2 == 1) + // Greater than 2GB values aren't supported. + if (dataLength < 0) { - // handle the case where the registry contains an odd-byte length (corrupt data?) - try - { - datasize = checked(datasize + 1); - } - catch (OverflowException e) - { - throw new IOException(SR.Arg_RegGetOverflowBug, e); - } + throw new IOException(SR.Arg_RegValueTooLarge); } - char[] blob = new char[datasize / 2]; - Interop.Advapi32.RegQueryValueEx(_hkey, name, null, ref type, blob, ref datasize); - if (blob.Length > 0 && blob[blob.Length - 1] == (char)0) - { - data = new string(blob, 0, blob.Length - 1); - } - else + span = pooledArray = ArrayPool.Shared.Rent(dataLength); + if (IsPerfDataKey()) { - // in the very unlikely case the data is missing null termination, - // pass in the whole char[] to prevent truncating a character - data = new string(blob); + span.Clear(); } + + continue; } - break; - case Interop.Advapi32.RegistryValues.REG_EXPAND_SZ: + // For any other error, return the default value. This might be ERROR_FILE_NOT_FOUND if the reg key + // wasn't found, or any other system error value for unspecified reasons. For compat, an exception + // is thrown for perf keys rather than returning the default value. + if (result != Interop.Errors.ERROR_SUCCESS) { - if (datasize % 2 == 1) - { - // handle the case where the registry contains an odd-byte length (corrupt data?) - try - { - datasize = checked(datasize + 1); - } - catch (OverflowException e) - { - throw new IOException(SR.Arg_RegGetOverflowBug, e); - } - } - char[] blob = new char[datasize / 2]; - - Interop.Advapi32.RegQueryValueEx(_hkey, name, null, ref type, blob, ref datasize); - - if (blob.Length > 0 && blob[blob.Length - 1] == (char)0) - { - data = new string(blob, 0, blob.Length - 1); - } - else - { - // in the very unlikely case the data is missing null termination, - // pass in the whole char[] to prevent truncating a character - data = new string(blob); - } - - if (!doNotExpand) + if (IsPerfDataKey()) { - data = Environment.ExpandEnvironmentVariables((string)data); + Win32Error(result, name); } + return defaultValue; } - break; - case Interop.Advapi32.RegistryValues.REG_MULTI_SZ: + + // We only get here for a successful query of the data. Process and return the results. + Debug.Assert((uint)dataLength <= span.Length, $"Expected {dataLength} <= {span.Length}"); + switch (type) { - if (datasize % 2 == 1) - { - // handle the case where the registry contains an odd-byte length (corrupt data?) - try + case Interop.Advapi32.RegistryValues.REG_NONE: + case Interop.Advapi32.RegistryValues.REG_BINARY: + case Interop.Advapi32.RegistryValues.REG_DWORD_BIG_ENDIAN: + return span.Slice(0, dataLength).ToArray(); + + case Interop.Advapi32.RegistryValues.REG_DWORD: + case Interop.Advapi32.RegistryValues.REG_QWORD: + return dataLength switch { - datasize = checked(datasize + 1); - } - catch (OverflowException e) + 4 => MemoryMarshal.Read(span), + 8 => MemoryMarshal.Read(span), + _ => span.Slice(0, dataLength).ToArray(), // This shouldn't happen, but the previous implementation included it defensively. + }; + + case Interop.Advapi32.RegistryValues.REG_SZ: + case Interop.Advapi32.RegistryValues.REG_EXPAND_SZ: + case Interop.Advapi32.RegistryValues.REG_MULTI_SZ: { - throw new IOException(SR.Arg_RegGetOverflowBug, e); - } - } - char[] blob = new char[datasize / 2]; + // Handle the case where the registry contains an odd-byte length (corrupt data?) + // by increasing the data by a single zero byte. + if (dataLength % 2 == 1) + { + if (dataLength == int.MaxValue) + { + throw new IOException(SR.Arg_RegValueTooLarge); + } - ret = Interop.Advapi32.RegQueryValueEx(_hkey, name, null, ref type, blob, ref datasize); + if (dataLength >= span.Length) + { + byte[] newPooled = ArrayPool.Shared.Rent(dataLength + 1); + span.CopyTo(newPooled); + if (pooledArray is not null) + { + byte[] toReturn = pooledArray; + pooledArray = null; + ArrayPool.Shared.Return(toReturn); + } + span = pooledArray = newPooled; + } - // make sure the string is null terminated before processing the data - if (blob.Length > 0 && blob[blob.Length - 1] != (char)0) - { - Array.Resize(ref blob, blob.Length + 1); - } + span[dataLength++] = 0; + } - string[] strings = Array.Empty(); - int stringsCount = 0; + // From here on, we interpret the read bytes as chars; span and dataLength should no longer be used. + ReadOnlySpan chars = MemoryMarshal.Cast(span.Slice(0, dataLength)); - int cur = 0; - int len = blob.Length; + if (type == Interop.Advapi32.RegistryValues.REG_MULTI_SZ) + { + string[] strings = Array.Empty(); + int count = 0; - while (ret == 0 && cur < len) - { - int nextNull = cur; - while (nextNull < len && blob[nextNull] != (char)0) - { - nextNull++; - } + while (chars.Length > 1 || (chars.Length == 1 && chars[0] != '\0')) + { + int nullPos = chars.IndexOf('\0'); + string toAdd; + if (nullPos < 0) + { + toAdd = chars.ToString(); + chars = default; + } + else + { + toAdd = chars.Slice(0, nullPos).ToString(); + chars = chars.Slice(nullPos + 1); + } + + if (count == strings.Length) + { + Array.Resize(ref strings, count == 0 ? 4 : count * 2); + } + strings[count++] = toAdd; + } - string? toAdd = null; - if (nextNull < len) - { - Debug.Assert(blob[nextNull] == (char)0, "blob[nextNull] should be 0"); - if (nextNull - cur > 0) - { - toAdd = new string(blob, cur, nextNull - cur); + if (count != 0) + { + Array.Resize(ref strings, count); + } + + return strings; } else { - // we found an empty string. But if we're at the end of the data, - // it's just the extra null terminator. - if (nextNull != len - 1) + if (chars.Length == 0) { - toAdd = string.Empty; + return string.Empty; } - } - } - else - { - toAdd = new string(blob, cur, len - cur); - } - cur = nextNull + 1; - if (toAdd != null) - { - if (strings.Length == stringsCount) - { - Array.Resize(ref strings, stringsCount > 0 ? stringsCount * 2 : 4); + // Remove null termination if it exists. + if (chars[^1] == 0) + { + chars = chars[0..^1]; + } + + // Get the resulting string from the bytes. + string str = chars.ToString(); + if (type == Interop.Advapi32.RegistryValues.REG_EXPAND_SZ && !doNotExpand) + { + str = Environment.ExpandEnvironmentVariables(str); + } + + return str; } - strings[stringsCount++] = toAdd; } - } - Array.Resize(ref strings, stringsCount); - data = strings; + default: + return defaultValue; } - break; - case Interop.Advapi32.RegistryValues.REG_LINK: - default: - break; + } + } + finally + { + if (pooledArray is not null) + { + ArrayPool.Shared.Return(pooledArray); + } } - - return data; } - private RegistryValueKind GetValueKindCore(string? name) + private unsafe RegistryValueKind GetValueKindCore(string? name) { int type = 0; int datasize = 0; - int ret = Interop.Advapi32.RegQueryValueEx(_hkey, name, null, ref type, (byte[]?)null, ref datasize); + int ret = Interop.Advapi32.RegQueryValueEx(_hkey, name, null, &type, (byte*)null, (uint*)&datasize); if (ret != 0) { Win32Error(ret, null); diff --git a/src/libraries/Microsoft.Win32.Registry/src/Resources/Strings.resx b/src/libraries/Microsoft.Win32.Registry/src/Resources/Strings.resx index 1b789a4677232a..ac827241842f1b 100644 --- a/src/libraries/Microsoft.Win32.Registry/src/Resources/Strings.resx +++ b/src/libraries/Microsoft.Win32.Registry/src/Resources/Strings.resx @@ -1,4 +1,5 @@ - + + @@ -84,8 +85,8 @@ The specified RegistryValueKind is an invalid value. - - RegistryKey.GetValue does not allow a String that has a length greater than Int32.MaxValue. + + RegistryKey.GetValue does not support values with more than Int32.MaxValue bytes. The type of the value object did not match the specified RegistryValueKind or the object could not be properly converted. @@ -135,4 +136,4 @@ Cannot write to the registry key. - \ No newline at end of file + diff --git a/src/libraries/Microsoft.Win32.Registry/tests/RegistryKey/RegistryKey_GetValue_CorruptData.cs b/src/libraries/Microsoft.Win32.Registry/tests/RegistryKey/RegistryKey_GetValue_CorruptData.cs index 07d7816f41f077..9d6d6e7b6cbd32 100644 --- a/src/libraries/Microsoft.Win32.Registry/tests/RegistryKey/RegistryKey_GetValue_CorruptData.cs +++ b/src/libraries/Microsoft.Win32.Registry/tests/RegistryKey/RegistryKey_GetValue_CorruptData.cs @@ -44,10 +44,13 @@ public void ReadRegMultiSzLackingFinalNullTerminatorCorrectly() } [Theory] - [InlineData(RegistryValueKind.String, new byte[] { 6, 5, 6 })] - [InlineData(RegistryValueKind.ExpandString, new byte[] { 6, 5, 6 })] - [InlineData(RegistryValueKind.MultiString, new byte[] { 6, 5, 6, 0, 0 })] - public void RegSzOddByteLength(RegistryValueKind kind, byte[] contents) + [InlineData(RegistryValueKind.String, new byte[] { 6, 5, 6 }, "\u0506")] + [InlineData(RegistryValueKind.ExpandString, new byte[] { 6, 5, 6 }, "\u0506")] + [InlineData(RegistryValueKind.MultiString, new byte[] { 6, 5, 6 }, "\u0506")] + [InlineData(RegistryValueKind.String, new byte[] { 6, 5, 6, 0, 0 }, "\u0506\u0006")] + [InlineData(RegistryValueKind.ExpandString, new byte[] { 6, 5, 6, 0, 0 }, "\u0506\u0006")] + [InlineData(RegistryValueKind.MultiString, new byte[] { 6, 5, 6, 0, 0 }, "\u0506\u0006")] + public void RegSzOddByteLength(RegistryValueKind kind, byte[] contents, string expected) { const string TestValueName = "CorruptData2"; @@ -73,9 +76,7 @@ public void RegSzOddByteLength(RegistryValueKind kind, byte[] contents) s = (string)o; } - Assert.Equal(2, s.Length); - Assert.Equal(0x506, s[0]); - Assert.Equal(0x6, s[1]); + Assert.Equal(expected, s); } finally { diff --git a/src/libraries/Microsoft.Win32.Registry/tests/RegistryKey/RegistryKey_GetValue_str.cs b/src/libraries/Microsoft.Win32.Registry/tests/RegistryKey/RegistryKey_GetValue_str.cs index 2ca50590958156..341b89507a839b 100644 --- a/src/libraries/Microsoft.Win32.Registry/tests/RegistryKey/RegistryKey_GetValue_str.cs +++ b/src/libraries/Microsoft.Win32.Registry/tests/RegistryKey/RegistryKey_GetValue_str.cs @@ -86,5 +86,12 @@ public void GetStringArrayTest() Assert.Equal(expected, (string[])TestRegistryKey.GetValue(valueName)); TestRegistryKey.DeleteValue(valueName); } + + [Fact] + public void GetPerformanceKeyValue() + { + string[] counterNames = (string[])Registry.PerformanceData.GetValue("Counter"); + Assert.NotEmpty(counterNames); + } } } diff --git a/src/libraries/System.Private.CoreLib/src/Internal/Win32/RegistryKey.cs b/src/libraries/System.Private.CoreLib/src/Internal/Win32/RegistryKey.cs index da0c55c64eebf6..0f92891918fec0 100644 --- a/src/libraries/System.Private.CoreLib/src/Internal/Win32/RegistryKey.cs +++ b/src/libraries/System.Private.CoreLib/src/Internal/Win32/RegistryKey.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Runtime.InteropServices; using System.Security; using Internal.Win32.SafeHandles; @@ -219,217 +220,187 @@ public unsafe string[] GetValueNames() } [return: NotNullIfNotNull(nameof(defaultValue))] - public object? GetValue(string name, object? defaultValue) + public unsafe object? GetValue(string name, object? defaultValue) { - object? data = defaultValue; - int type = 0; - int datasize = 0; + // Create an initial stack buffer large enough to satisfy many reg keys. We need to call RegQueryValueEx + // in order to determine the type of the value, and we can avoid further retries if all of the data can be + // retrieved in that single call with this buffer. If we do need to grow, we grow into a pooled array. The + // caller is always handed back a copy of this data, either in an array, a string, or a boxed integer. + Span span = stackalloc byte[512]; + byte[]? pooledArray = null; - int ret = Interop.Advapi32.RegQueryValueEx(_hkey, name, null, ref type, (byte[]?)null, ref datasize); - - if (ret != 0) - { - // For stuff like ERROR_FILE_NOT_FOUND, we want to return null (data). - // Some OS's returned ERROR_MORE_DATA even in success cases, so we - // want to continue on through the function. - if (ret != Interop.Errors.ERROR_MORE_DATA) - return data; - } - - if (datasize < 0) + try { - // unexpected code path - Debug.Fail("[InternalGetValue] RegQueryValue returned ERROR_SUCCESS but gave a negative datasize"); - datasize = 0; - } - + // Loop in case we need to try again with a larger buffer size. + while (true) + { + int type = 0; + int result; + int dataLength = span.Length; - switch (type) - { - case Interop.Advapi32.RegistryValues.REG_NONE: - case Interop.Advapi32.RegistryValues.REG_DWORD_BIG_ENDIAN: - case Interop.Advapi32.RegistryValues.REG_BINARY: + fixed (byte* lpData = &MemoryMarshal.GetReference(span)) { - byte[] blob = new byte[datasize]; - Interop.Advapi32.RegQueryValueEx(_hkey, name, null, ref type, blob, ref datasize); - data = blob; - } - break; - case Interop.Advapi32.RegistryValues.REG_QWORD: - { // also REG_QWORD_LITTLE_ENDIAN - if (datasize > 8) + result = Interop.Advapi32.RegQueryValueEx(_hkey, name, null, &type, lpData, (uint*)&dataLength); + if (dataLength < 0) { - // prevent an AV in the edge case that datasize is larger than sizeof(long) - goto case Interop.Advapi32.RegistryValues.REG_BINARY; + // Greater than 2GB values aren't supported. + throw new IOException(SR.Arg_RegValueTooLarge); } - long blob = 0; - Debug.Assert(datasize == 8, "datasize==8"); - // Here, datasize must be 8 when calling this - Interop.Advapi32.RegQueryValueEx(_hkey, name, null, ref type, ref blob, ref datasize); - - data = blob; - } - break; - case Interop.Advapi32.RegistryValues.REG_DWORD: - { // also REG_DWORD_LITTLE_ENDIAN - if (datasize > 4) - { - // prevent an AV in the edge case that datasize is larger than sizeof(int) - goto case Interop.Advapi32.RegistryValues.REG_QWORD; - } - int blob = 0; - Debug.Assert(datasize == 4, "datasize==4"); - // Here, datasize must be four when calling this - Interop.Advapi32.RegQueryValueEx(_hkey, name, null, ref type, ref blob, ref datasize); - - data = blob; } - break; - case Interop.Advapi32.RegistryValues.REG_SZ: + // If RegQueryValueEx told us we need a larger buffer, get one and then loop around to try again. + if (result == Interop.Errors.ERROR_MORE_DATA) { - if (datasize % 2 == 1) - { - // handle the case where the registry contains an odd-byte length (corrupt data?) - try - { - datasize = checked(datasize + 1); - } - catch (OverflowException e) - { - throw new IOException(SR.Arg_RegGetOverflowBug, e); - } - } - char[] blob = new char[datasize / 2]; - - Interop.Advapi32.RegQueryValueEx(_hkey, name, null, ref type, blob, ref datasize); - if (blob.Length > 0 && blob[^1] == (char)0) - { - data = new string(blob, 0, blob.Length - 1); - } - else + if (pooledArray is not null) { - // in the very unlikely case the data is missing null termination, - // pass in the whole char[] to prevent truncating a character - data = new string(blob); + // This should only happen if the registry key was changed concurrently, such that + // we called RegQueryValueEx with our initial buffer size that was too small, we then + // rented a buffer of the reportedly right size and called RegQueryValueEx again, but + // it still came back with ERROR_MORE_DATA again. + byte[] toReturn = pooledArray; + pooledArray = null; + ArrayPool.Shared.Return(toReturn); } - } - break; - case Interop.Advapi32.RegistryValues.REG_EXPAND_SZ: - { - if (datasize % 2 == 1) + // Greater than 2GB values aren't supported. + if (dataLength < 0) { - // handle the case where the registry contains an odd-byte length (corrupt data?) - try - { - datasize = checked(datasize + 1); - } - catch (OverflowException e) - { - throw new IOException(SR.Arg_RegGetOverflowBug, e); - } + throw new IOException(SR.Arg_RegValueTooLarge); } - char[] blob = new char[datasize / 2]; - Interop.Advapi32.RegQueryValueEx(_hkey, name, null, ref type, blob, ref datasize); + span = pooledArray = ArrayPool.Shared.Rent(dataLength); - if (blob.Length > 0 && blob[^1] == (char)0) - { - data = new string(blob, 0, blob.Length - 1); - } - else - { - // in the very unlikely case the data is missing null termination, - // pass in the whole char[] to prevent truncating a character - data = new string(blob); - } + continue; + } - data = Environment.ExpandEnvironmentVariables((string)data); + // For any other error, return the default value. This might be ERROR_FILE_NOT_FOUND if the reg key + // wasn't found, or any other system error value for unspecified reasons. For compat, an exception + // is thrown for perf keys rather than returning the default value. + if (result != Interop.Errors.ERROR_SUCCESS) + { + return defaultValue; } - break; - case Interop.Advapi32.RegistryValues.REG_MULTI_SZ: + + // We only get here for a successful query of the data. Process and return the results. + Debug.Assert((uint)dataLength <= span.Length, $"Expected {dataLength} <= {span.Length}"); + switch (type) { - if (datasize % 2 == 1) - { - // handle the case where the registry contains an odd-byte length (corrupt data?) - try + case Interop.Advapi32.RegistryValues.REG_NONE: + case Interop.Advapi32.RegistryValues.REG_BINARY: + case Interop.Advapi32.RegistryValues.REG_DWORD_BIG_ENDIAN: + return span.Slice(0, dataLength).ToArray(); + + case Interop.Advapi32.RegistryValues.REG_DWORD: + case Interop.Advapi32.RegistryValues.REG_QWORD: + return dataLength switch { - datasize = checked(datasize + 1); - } - catch (OverflowException e) + 4 => MemoryMarshal.Read(span), + 8 => MemoryMarshal.Read(span), + _ => span.Slice(0, dataLength).ToArray(), // This shouldn't happen, but the previous implementation included it defensively. + }; + + case Interop.Advapi32.RegistryValues.REG_SZ: + case Interop.Advapi32.RegistryValues.REG_EXPAND_SZ: + case Interop.Advapi32.RegistryValues.REG_MULTI_SZ: { - throw new IOException(SR.Arg_RegGetOverflowBug, e); - } - } - char[] blob = new char[datasize / 2]; + // Handle the case where the registry contains an odd-byte length (corrupt data?) + // by increasing the data by a single zero byte. + if (dataLength % 2 == 1) + { + if (dataLength == int.MaxValue) + { + throw new IOException(SR.Arg_RegValueTooLarge); + } - ret = Interop.Advapi32.RegQueryValueEx(_hkey, name, null, ref type, blob, ref datasize); + if (dataLength >= span.Length) + { + byte[] newPooled = ArrayPool.Shared.Rent(dataLength + 1); + span.CopyTo(newPooled); + if (pooledArray is not null) + { + byte[] toReturn = pooledArray; + pooledArray = null; + ArrayPool.Shared.Return(toReturn); + } + span = pooledArray = newPooled; + } - // make sure the string is null terminated before processing the data - if (blob.Length > 0 && blob[^1] != (char)0) - { - Array.Resize(ref blob, blob.Length + 1); - } + span[dataLength++] = 0; + } - string[] strings = Array.Empty(); - int stringsCount = 0; + // From here on, we interpret the read bytes as chars; span and dataLength should no longer be used. + ReadOnlySpan chars = MemoryMarshal.Cast(span.Slice(0, dataLength)); - int cur = 0; - int len = blob.Length; + if (type == Interop.Advapi32.RegistryValues.REG_MULTI_SZ) + { + string[] strings = Array.Empty(); + int count = 0; - while (ret == 0 && cur < len) - { - int nextNull = cur; - while (nextNull < len && blob[nextNull] != (char)0) - { - nextNull++; - } + while (chars.Length > 1 || (chars.Length == 1 && chars[0] != '\0')) + { + int nullPos = chars.IndexOf('\0'); + string toAdd; + if (nullPos < 0) + { + toAdd = chars.ToString(); + chars = default; + } + else + { + toAdd = chars.Slice(0, nullPos).ToString(); + chars = chars.Slice(nullPos + 1); + } + + if (count == strings.Length) + { + Array.Resize(ref strings, count == 0 ? 4 : count * 2); + } + strings[count++] = toAdd; + } - string? toAdd = null; - if (nextNull < len) - { - Debug.Assert(blob[nextNull] == (char)0, "blob[nextNull] should be 0"); - if (nextNull - cur > 0) - { - toAdd = new string(blob, cur, nextNull - cur); + if (count != 0) + { + Array.Resize(ref strings, count); + } + + return strings; } else { - // we found an empty string. But if we're at the end of the data, - // it's just the extra null terminator. - if (nextNull != len - 1) + if (chars.Length == 0) { - toAdd = string.Empty; + return string.Empty; } - } - } - else - { - toAdd = new string(blob, cur, len - cur); - } - cur = nextNull + 1; - if (toAdd != null) - { - if (strings.Length == stringsCount) - { - Array.Resize(ref strings, stringsCount > 0 ? stringsCount * 2 : 4); + // Remove null termination if it exists. + if (chars[^1] == 0) + { + chars = chars[0..^1]; + } + + // Get the resulting string from the bytes. + string str = chars.ToString(); + if (type == Interop.Advapi32.RegistryValues.REG_EXPAND_SZ) + { + str = Environment.ExpandEnvironmentVariables(str); + } + + return str; } - strings[stringsCount++] = toAdd; } - } - Array.Resize(ref strings, stringsCount); - data = strings; + default: + return defaultValue; } - break; - case Interop.Advapi32.RegistryValues.REG_LINK: - default: - break; + } + } + finally + { + if (pooledArray is not null) + { + ArrayPool.Shared.Return(pooledArray); + } } - - return data; } // The actual api is SetValue(string name, object value) but we only need to set Strings diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index 3fc2d2014809e2..31d84adc96c557 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -699,8 +699,8 @@ Number of lengths and lowerBounds must match. - - RegistryKey.GetValue does not allow a String that has a length greater than Int32.MaxValue. + + RegistryKey.GetValue does not support values with more than Int32.MaxValue bytes. The specified registry key does not exist.