Skip to content
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

CA1838 Avoid 'StringBuilder' parameters for P/Invokes #7186

Merged
merged 25 commits into from
Feb 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
994b696
CA1838 Avoid 'StringBuilder' parameters for P/Invokes
elachlan Dec 30, 2021
4b519a9
revert ruleset change
elachlan Jan 7, 2022
3a88ed9
rebase
elachlan Jan 7, 2022
638775f
Enable warning on CA1838
elachlan Jan 7, 2022
751fea3
Merge branch 'main' into CA1838
elachlan Jan 11, 2022
661c2c8
CA1838 Avoid 'StringBuilder' parameters for P/Invokes. Consider using…
elachlan Jan 11, 2022
4f0424c
trying again
elachlan Jan 12, 2022
e042e66
Changes from review
elachlan Jan 27, 2022
4ddf661
GetGacPath changes
elachlan Jan 27, 2022
8f0c31c
Making sure we are form strings properly
elachlan Jan 28, 2022
04e4a1b
changes from review
elachlan Jan 28, 2022
903f35d
Remove unused field
elachlan Jan 28, 2022
69cf05f
Use ArrayPool in GetRuntimeVersion and fix usage in GetModuleFileName
elachlan Jan 28, 2022
300179b
Refactor GetRuntimeVersion to remove loop and use stackalloc
elachlan Jan 28, 2022
c392277
changes from review
elachlan Jan 31, 2022
de3618c
Fix conflict and xml doc
elachlan Jan 31, 2022
a0178d7
Merge branch 'main' into CA1838
elachlan Jan 31, 2022
54878ac
Fix possible StackOverflow in GetShortPathName/GetLongPathName by mov…
elachlan Feb 1, 2022
567cc08
Merge branch 'CA1838' of github.com:elachlan/msbuild into CA1838
elachlan Feb 1, 2022
8af24e5
remove unneeded unsafe from pinvoke definitions
elachlan Feb 1, 2022
031af51
Added cached GAC Path and changes from review
elachlan Feb 2, 2022
b4f6bf7
Add documentation to Pinvoke
elachlan Feb 3, 2022
b5f60db
Changes from review
elachlan Feb 4, 2022
53d3da4
Change from review
elachlan Feb 8, 2022
0a97bfd
Add VerifyThrow instead of loop condition
elachlan Feb 8, 2022
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
2 changes: 1 addition & 1 deletion eng/Common.globalconfig
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ dotnet_diagnostic.CA1836.severity = warning
dotnet_diagnostic.CA1837.severity = suggestion

# Avoid 'StringBuilder' parameters for P/Invokes
dotnet_diagnostic.CA1838.severity = suggestion
dotnet_diagnostic.CA1838.severity = warning

# Dispose objects before losing scope
dotnet_diagnostic.CA2000.severity = none
Expand Down
50 changes: 13 additions & 37 deletions src/Framework/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ internal static class NativeMethods
internal const int MAX_PATH = 260;

private const string kernel32Dll = "kernel32.dll";
private const string mscoreeDLL = "mscoree.dll";

private const string WINDOWS_FILE_SYSTEM_REGISTRY_KEY = @"SYSTEM\CurrentControlSet\Control\FileSystem";
private const string WINDOWS_LONG_PATHS_ENABLED_VALUE_NAME = "LongPathsEnabled";
Expand Down Expand Up @@ -950,13 +949,13 @@ internal static string GetShortFilePath(string path)

if (length > 0)
{
StringBuilder fullPathBuffer = new StringBuilder(length);
char[] fullPathBuffer = new char[length];
length = GetShortPathName(path, fullPathBuffer, length);
errorCode = Marshal.GetLastWin32Error();

if (length > 0)
{
string fullPath = fullPathBuffer.ToString();
string fullPath = new(fullPathBuffer, 0, length);
path = fullPath;
}
}
Expand Down Expand Up @@ -989,13 +988,13 @@ internal static string GetLongFilePath(string path)

if (length > 0)
{
StringBuilder fullPathBuffer = new StringBuilder(length);
char[] fullPathBuffer = new char[length];
length = GetLongPathName(path, fullPathBuffer, length);
errorCode = Marshal.GetLastWin32Error();

if (length > 0)
{
string fullPath = fullPathBuffer.ToString();
string fullPath = new(fullPathBuffer, 0, length);
path = fullPath;
}
}
Expand Down Expand Up @@ -1084,7 +1083,7 @@ DateTime LastWriteFileUtcTime(string path)

if (success && (data.fileAttributes & NativeMethods.FILE_ATTRIBUTE_DIRECTORY) == 0)
{
long dt = ((long) (data.ftLastWriteTimeHigh) << 32) | ((long) data.ftLastWriteTimeLow);
long dt = ((long)(data.ftLastWriteTimeHigh) << 32) | ((long)data.ftLastWriteTimeLow);
fileModifiedTime = DateTime.FromFileTimeUtc(dt);

// If file is a symlink _and_ we're not instructed to do the wrong thing, get a more accurate timestamp.
Expand Down Expand Up @@ -1468,17 +1467,6 @@ internal static void VerifyThrowWin32Result(int result)
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetFileAttributesEx(String name, int fileInfoLevel, ref WIN32_FILE_ATTRIBUTE_DATA lpFileInformation);

[DllImport(kernel32Dll, SetLastError = true, CharSet = CharSet.Unicode)]
private static extern uint SearchPath
(
string path,
string fileName,
string extension,
int numBufferChars,
[Out] StringBuilder buffer,
int[] filePart
);

[DllImport("kernel32.dll", PreserveSig = true, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool FreeLibrary([In] IntPtr module);
Expand All @@ -1489,26 +1477,14 @@ int[] filePart
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true)]
internal static extern IntPtr LoadLibrary(string fileName);

[DllImport(mscoreeDLL, SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern uint GetRequestedRuntimeInfo(String pExe,
String pwszVersion,
String pConfigurationFile,
uint startupFlags,
uint runtimeInfoFlags,
[Out] StringBuilder pDirectory,
int dwDirectory,
out uint dwDirectoryLength,
[Out] StringBuilder pVersion,
int cchBuffer,
out uint dwlength);

/// <summary>
/// Gets the fully qualified filename of the currently executing .exe
/// Gets the fully qualified filename of the currently executing .exe.
/// </summary>
/// <param name="hModule"><see cref="HandleRef"/> of the module for which we are finding the file name.</param>
/// <param name="buffer">The character buffer used to return the file name.</param>
/// <param name="length">The length of the buffer.</param>
[DllImport(kernel32Dll, SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern int GetModuleFileName(
HandleRef hModule,
[Out] StringBuilder buffer, int length);
internal static extern int GetModuleFileName(HandleRef hModule, [Out] char[] buffer, int length);

[DllImport("kernel32.dll")]
internal static extern IntPtr GetStdHandle(int nStdHandle);
Expand Down Expand Up @@ -1557,10 +1533,10 @@ internal static bool SetCurrentDirectory(string path)
private static extern bool GlobalMemoryStatusEx([In, Out] MemoryStatus lpBuffer);

[DllImport("kernel32.dll", CharSet = CharSet.Unicode, BestFitMapping = false)]
internal static extern int GetShortPathName(string path, [Out] StringBuilder fullpath, [In] int length);
internal static extern int GetShortPathName(string path, [Out] char[] fullpath, [In] int length);

[DllImport("kernel32.dll", CharSet = CharSet.Unicode, BestFitMapping = false)]
internal static extern int GetLongPathName([In] string path, [Out] StringBuilder fullpath, [In] int length);
internal static extern int GetLongPathName([In] string path, [Out] char[] fullpath, [In] int length);

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
internal static extern bool CreatePipe(out SafeFileHandle hReadPipe, out SafeFileHandle hWritePipe, SecurityAttributes lpPipeAttributes, int nSize);
Expand Down Expand Up @@ -1648,7 +1624,7 @@ internal static bool MsgWaitOne(this WaitHandle handle, int timeout)

if (!(returnValue == 0 || ((uint)returnValue == RPC_S_CALLPENDING && timeout != Timeout.Infinite)))
{
throw new InternalErrorException($"Received {returnValue} from CoWaitForMultipleHandles, but expected 0 (S_OK)");
throw new InternalErrorException($"Received {returnValue} from CoWaitForMultipleHandles, but expected 0 (S_OK)");
}

return returnValue == 0;
Expand Down
45 changes: 25 additions & 20 deletions src/Tasks/AssemblyDependency/AssemblyInformation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -569,30 +569,36 @@ internal static string GetRuntimeVersion(string path)
#if FEATURE_MSCOREE
if (NativeMethodsShared.IsWindows)
{
StringBuilder runtimeVersion;
uint hresult;
#if DEBUG
// Just to make sure and exercise the code that doubles the size
// every time GetRequestedRuntimeInfo fails due to insufficient buffer size.
// Just to make sure and exercise the code that uses dwLength to allocate the buffer
// when GetRequestedRuntimeInfo fails due to insufficient buffer size.
int bufferLength = 1;
#else
int bufferLength = 11; // 11 is the length of a runtime version and null terminator v2.0.50727/0
#endif
do
{
runtimeVersion = new StringBuilder(bufferLength);
hresult = NativeMethods.GetFileVersion(path, runtimeVersion, bufferLength, out _);
Copy link
Member

Choose a reason for hiding this comment

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

That said, I'm not a fan of this code, so I'm glad it's gone 😁

bufferLength *= 2;
} while (hresult == NativeMethodsShared.ERROR_INSUFFICIENT_BUFFER);

if (hresult == NativeMethodsShared.S_OK)
unsafe
{
return runtimeVersion.ToString();
}
else
{
return String.Empty;
}
// Allocate an initial buffer
char* runtimeVersion = stackalloc char[bufferLength];

// Run GetFileVersion, this should succeed using the initial buffer.
// It also returns the dwLength which is used if there is insufficient buffer.
uint hresult = NativeMethods.GetFileVersion(path, runtimeVersion, bufferLength, out int dwLength);

if (hresult == NativeMethodsShared.ERROR_INSUFFICIENT_BUFFER)
{
// Allocate new buffer based on the returned length.
char* runtimeVersion2 = stackalloc char[dwLength];
Copy link
Member

Choose a reason for hiding this comment

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

Is there an unstackalloc? And maybe check what dwLength is?

If dwLength is big, it would be good not to overrun the stack. I imagine we'd have a little more space if we can un-allocate the first stack before allocating the second.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No Its scoped the the current method.

stack allocated memory block created during the method execution is automatically discarded when that method returns. You cannot explicitly free the memory allocated with stackalloc.

https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/stackalloc

runtimeVersion = runtimeVersion2;

// Get the RuntimeVersion in this second call.
bufferLength = dwLength;
hresult = NativeMethods.GetFileVersion(path, runtimeVersion, bufferLength, out dwLength);
}

return hresult == NativeMethodsShared.S_OK ? new string(runtimeVersion, 0, dwLength - 1) : string.Empty;
}
}
else
{
Expand All @@ -603,7 +609,6 @@ internal static string GetRuntimeVersion(string path)
#endif
}


/// <summary>
/// Import assembly dependencies.
/// </summary>
Expand Down Expand Up @@ -790,7 +795,7 @@ private static AssemblyNameExtension ConstructAssemblyName(IntPtr asmMetaPtr, ch
// Construct the assembly name. (Note asmNameLength should/must be > 0.)
var assemblyName = new AssemblyName
{
Name = new string(asmNameBuf, 0, (int) asmNameLength - 1),
Name = new string(asmNameBuf, 0, (int)asmNameLength - 1),
Version = new Version(
asmMeta.usMajorVersion,
asmMeta.usMinorVersion,
Expand Down Expand Up @@ -911,7 +916,7 @@ public static string GetRuntimeVersion(string path)
// Read the PE header signature

sr.BaseStream.Position = peHeaderOffset;
if (!ReadBytes(sr, (byte) 'P', (byte) 'E', 0, 0))
if (!ReadBytes(sr, (byte)'P', (byte)'E', 0, 0))
{
return string.Empty;
}
Expand Down
24 changes: 18 additions & 6 deletions src/Tasks/AssemblyDependency/GlobalAssemblyCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ internal static class GlobalAssemblyCache
/// </summary>
internal static readonly GetGacEnumerator gacEnumerator = GetGacNativeEnumerator;

/// <summary>
/// Lazy loaded cached root path of the GAC.
/// </summary>
private static readonly Lazy<string> _gacPath = new(() => GetGacPath());
Copy link
Member

Choose a reason for hiding this comment

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

Is it worth making this lazy vs. just leaving it as a static call? It looks like it's only used once per ResolveComReference call, which seems like not very much to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My thought process was to hold a cached static value for it so we only have to call once per global run. I am unsure if that is how it works in practice.


/// <summary>
/// Gets the root path of the GAC.
/// </summary>
internal static string GacPath => _gacPath.Value;

/// <summary>
/// Given a strong name, find its path in the GAC.
/// </summary>
Expand Down Expand Up @@ -367,16 +377,18 @@ bool specificVersion
}

/// <summary>
/// Return the root path of the GAC
/// Return the root path of the GAC.
/// </summary>
internal static string GetGacPath()
{
int gacPathLength = 0;
NativeMethods.GetCachePath(AssemblyCacheFlags.GAC, null, ref gacPathLength);
StringBuilder gacPath = new StringBuilder(gacPathLength);
NativeMethods.GetCachePath(AssemblyCacheFlags.GAC, gacPath, ref gacPathLength);

return gacPath.ToString();
unsafe
{
NativeMethods.GetCachePath(AssemblyCacheFlags.GAC, null, ref gacPathLength);
char* gacPath = stackalloc char[gacPathLength];
NativeMethods.GetCachePath(AssemblyCacheFlags.GAC, gacPath, ref gacPathLength);
return new string(gacPath, 0, gacPathLength - 1);
}
}
}
}
29 changes: 20 additions & 9 deletions src/Tasks/ComReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -406,22 +406,33 @@ internal static string StripTypeLibNumberFromPath(string typeLibPath, FileExists

private static string GetModuleFileName(IntPtr handle)
{
bool success = false;
var buffer = new StringBuilder();
char[] buffer = null;

// Try increased buffer sizes if on longpath-enabled Windows
for (int bufferSize = NativeMethodsShared.MAX_PATH; !success && bufferSize <= NativeMethodsShared.MaxPath; bufferSize *= 2)
for (int bufferSize = NativeMethodsShared.MAX_PATH; bufferSize <= NativeMethodsShared.MaxPath; bufferSize *= 2)
Copy link
Member

Choose a reason for hiding this comment

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

I don't think it's relevant for this PR, but MaxPath can be as large as int.MaxValue; since that isn't exactly a power of 2, doesn't that mean it could theoretically (if we keep getting ERROR_INSUFFICIENT_BUFFER or pathLength is 0) reach the top, overflow, and throw an exception?

Copy link
Contributor Author

@elachlan elachlan Feb 4, 2022

Choose a reason for hiding this comment

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

There are 23 doublings going from MAX_PATH (260) to int.MaxValue. So its not a trivial risk for long path enabled windows. Interestingly NTFS has a 65,535 character limit and The Windows API has many functions that also have Unicode versions to permit an extended-length path for a maximum total path length of 32,767 characters.

So I think we would run into file system/WinAPI limitations before hitting overflows.

I think this would work.
for (int bufferSize = NativeMethodsShared.MAX_PATH; bufferSize <= NativeMethodsShared.MaxPath && bufferSize <= int.MaxValue/2; bufferSize *= 2)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Forgind let me know if you think the additional check is helpful or not.

Copy link
Member

Choose a reason for hiding this comment

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

I think that would work. I'd be mildly in favor of adding it, but I don't care too much. It hasn't been an important case up to this point, so I doubt it'll be an important case in the future.

It isn't really the point of this PR, but a VerifyThrow at the end of the loop might be a nicer solution? It's almost certainly a bug if we get close to int.MaxValue, and it would be good to make the bug as visible as possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added it.

{
buffer.EnsureCapacity(bufferSize);
buffer = System.Buffers.ArrayPool<char>.Shared.Rent(bufferSize);
try
{
var handleRef = new System.Runtime.InteropServices.HandleRef(buffer, handle);
int pathLength = NativeMethodsShared.GetModuleFileName(handleRef, buffer, bufferSize);

var handleRef = new System.Runtime.InteropServices.HandleRef(buffer, handle);
int pathLength = NativeMethodsShared.GetModuleFileName(handleRef, buffer, buffer.Capacity);
bool isBufferTooSmall = (uint)Marshal.GetLastWin32Error() == NativeMethodsShared.ERROR_INSUFFICIENT_BUFFER;
if (pathLength != 0 && !isBufferTooSmall)
{
return new string(buffer, 0, pathLength);
}
}
finally
{
System.Buffers.ArrayPool<char>.Shared.Return(buffer);
}

bool isBufferTooSmall = ((uint)Marshal.GetLastWin32Error() == NativeMethodsShared.ERROR_INSUFFICIENT_BUFFER);
success = pathLength != 0 && !isBufferTooSmall;
// Double check that the buffer is not insanely big
ErrorUtilities.VerifyThrow(bufferSize <= int.MaxValue / 2, "Buffer size approaching int.MaxValue");
}

return success ? buffer.ToString() : string.Empty;
return string.Empty;
}

/// <summary>
Expand Down
64 changes: 58 additions & 6 deletions src/Tasks/LockCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,57 @@ private static extern int RmRegisterResources(uint pSessionHandle,
uint nServices,
string[] rgsServiceNames);

/// <summary>
/// Starts a new Restart Manager session.
/// A maximum of 64 Restart Manager sessions per user session
/// can be open on the system at the same time. When this
/// function starts a session, it returns a session handle
/// and session key that can be used in subsequent calls to
/// the Restart Manager API.
/// </summary>
/// <param name="pSessionHandle">
/// A pointer to the handle of a Restart Manager session.
/// The session handle can be passed in subsequent calls
/// to the Restart Manager API.
/// </param>
/// <param name="dwSessionFlags">
/// Reserved. This parameter should be 0.
/// </param>
/// <param name="strSessionKey">
/// A null-terminated string that contains the session key
/// to the new session. The string must be allocated before
/// calling the RmStartSession function.
/// </param>
/// <returns>System error codes that are defined in Winerror.h.</returns>
/// <remarks>
/// The Rm­­StartSession function doesn’t properly null-terminate
/// the session key, even though the function is documented as
/// returning a null-terminated string. To work around this bug,
/// we pre-fill the buffer with null characters so that whatever
/// ends gets written will have a null terminator (namely, one of
/// the null characters we placed ahead of time).
/// <para>
/// see <see href="http://blogs.msdn.com/b/oldnewthing/archive/2012/02/17/10268840.aspx"/>.
/// </para>
/// </remarks>
[DllImport(RestartManagerDll, CharSet = CharSet.Unicode)]
private static extern int RmStartSession(out uint pSessionHandle,
int dwSessionFlags, StringBuilder strSessionKey);

private static extern unsafe int RmStartSession(
out uint pSessionHandle,
int dwSessionFlags,
char* strSessionKey);

/// <summary>
/// Ends the Restart Manager session.
/// This function should be called by the primary installer that
/// has previously started the session by calling the <see cref="RmStartSession"/>
/// function. The RmEndSession function can be called by a secondary installer
/// that is joined to the session once no more resources need to be registered
/// by the secondary installer.
/// </summary>
/// <param name="pSessionHandle">A handle to an existing Restart Manager session.</param>
/// <returns>
/// The function can return one of the system error codes that are defined in Winerror.h.
/// </returns>
[DllImport(RestartManagerDll)]
private static extern int RmEndSession(uint pSessionHandle);

Expand Down Expand Up @@ -207,11 +254,16 @@ internal static IEnumerable<ProcessInfo> GetLockingProcessInfos(params string[]
}

const int maxRetries = 6;
uint handle;
int res;

// See http://blogs.msdn.com/b/oldnewthing/archive/2012/02/17/10268840.aspx.
var key = new StringBuilder(new string('\0', CCH_RM_SESSION_KEY + 1));
unsafe
{
// See http://blogs.msdn.com/b/oldnewthing/archive/2012/02/17/10268840.aspx.
char* key = stackalloc char[CCH_RM_SESSION_KEY + 1];
res = RmStartSession(out handle, 0, key);
}

int res = RmStartSession(out uint handle, 0, key);
if (res != 0)
{
throw GetException(res, "RmStartSession", "Failed to begin restart manager session.");
Expand Down
Loading