Actual behavior
For an input PCSTR parameter, CsWin32 generates a friendly overload that accepts a managed string and converts it using code equivalent to:
fixed (byte* valueLocal =
value is object ? Encoding.Default.GetBytes(value) : null)
{
NativeMethod(new PCSTR(valueLocal));
}
However, Encoding.GetBytes(string) allocates a byte[] containing exactly the encoded bytes. It does not allocate or append a trailing NUL byte.
For example:
byte[] bytes = Encoding.Default.GetBytes("GetTickCount");
Debug.Assert(bytes.Length == 12);
Debug.Assert(bytes[^1] != 0);
The generated code pins this byte[], not the original managed string. Unlike a pinned string, a pinned array has no guarantee that pointer[array.Length] points to a null byte.
Consequently, the generated friendly overload can pass a buffer that does not satisfy the PCSTR contract. A native function that scans for the terminator may read beyond the bounds of the managed array until it happens to encounter a zero byte.
The existing GetProcAddress_String runtime test may still pass when memory immediately following the array happens to contain zero, but this is not guaranteed.
The current implementation is here:
|
else if (isIn && !isOut && isConst && externParam.Type is QualifiedNameSyntax { Right: { Identifier: { ValueText: "PCSTR" } } }) |
|
{ |
|
IdentifierNameSyntax localName = IdentifierName(origName + "Local"); |
|
signatureChanged = true; |
|
parameters[paramIndex] = externParam |
|
.WithType(PredefinedType(TokenWithSpace(SyntaxKind.StringKeyword))); |
|
|
|
// fixed (byte* someLocal = some is object ? System.Text.Encoding.Default.GetBytes(some) : null) |
|
fixedBlocks.Add(VariableDeclaration( |
|
PointerType(PredefinedType(Token(SyntaxKind.ByteKeyword))), |
|
[ |
|
VariableDeclarator( |
|
localName.Identifier, |
|
EqualsValueClause( |
|
ConditionalExpression( |
|
BinaryExpression(SyntaxKind.IsExpression, origName, PredefinedType(Token(SyntaxKind.ObjectKeyword))), |
|
InvocationExpression( |
|
MemberAccessExpression( |
|
SyntaxKind.SimpleMemberAccessExpression, |
|
ParseTypeName("global::System.Text.Encoding.Default"), |
|
IdentifierName(nameof(Encoding.GetBytes))), |
|
[Argument(origName)]), |
|
LiteralExpression(SyntaxKind.NullLiteralExpression)))) |
|
])); |
|
|
|
// new PCSTR(someLocal) |
|
arguments[paramIndex] = Argument(ObjectCreationExpression(externParam.Type, [Argument(localName)])); |
|
} |
Expected behavior
The generated friendly overload should ensure that the encoded native buffer contains an explicit trailing NUL byte.
For example, it could allocate one additional byte:
int byteCount = Encoding.Default.GetByteCount(value);
byte[] bytes = new byte[checked(byteCount + 1)];
Encoding.Default.GetBytes(
value,
0,
value.Length,
bytes,
0);
bytes[byteCount] = 0;
Alternatively, the generator could use an appropriate generated string marshaller that guarantees null termination.
Repro steps
NativeMethods.txt content:
-
No NativeMethods.json is required.
-
Inspect the generated string-friendly overload for GetProcAddress. It uses:
Encoding.Default.GetBytes(lpProcName)
and passes the resulting array directly as a PCSTR without appending a null terminator.
The issue is also visible directly in the friendly-overload generator source linked above.
Context
- CsWin32 version:
0.3.298
- Also present on current
main: cc9d25e833fb41aeb4cc931a61366d6328a8b51d
- Win32Metadata version: default package dependency
- Target Framework: reproduced with
net10.0 and net11.0
LangVersion: default
Additional context
PCSTR is documented and generated as a pointer to a null-terminated constant character string. The raw PCSTR overload correctly leaves this contract to the caller, but the generated string-friendly overload should satisfy that contract on behalf of its caller.
The corresponding PCWSTR friendly-overload path does not have this problem because fixing a managed string directly guarantees a null character at pointer[string.Length].
.NET's UTF-8 LibraryImport marshaller also explicitly allocates space for and writes a trailing null byte.
This issue was created with assistance from OpenAI Codex.
Actual behavior
For an input
PCSTRparameter, CsWin32 generates a friendly overload that accepts a managedstringand converts it using code equivalent to:However,
Encoding.GetBytes(string)allocates abyte[]containing exactly the encoded bytes. It does not allocate or append a trailing NUL byte.For example:
The generated code pins this
byte[], not the original managedstring. Unlike a pinnedstring, a pinned array has no guarantee thatpointer[array.Length]points to a null byte.Consequently, the generated friendly overload can pass a buffer that does not satisfy the
PCSTRcontract. A native function that scans for the terminator may read beyond the bounds of the managed array until it happens to encounter a zero byte.The existing
GetProcAddress_Stringruntime test may still pass when memory immediately following the array happens to contain zero, but this is not guaranteed.The current implementation is here:
CsWin32/src/Microsoft.Windows.CsWin32/Generator.FriendlyOverloads.cs
Lines 985 to 1012 in cc9d25e
Expected behavior
The generated friendly overload should ensure that the encoded native buffer contains an explicit trailing NUL byte.
For example, it could allocate one additional byte:
Alternatively, the generator could use an appropriate generated string marshaller that guarantees null termination.
Repro steps
NativeMethods.txtcontent:No
NativeMethods.jsonis required.Inspect the generated string-friendly overload for
GetProcAddress. It uses:and passes the resulting array directly as a
PCSTRwithout appending a null terminator.The issue is also visible directly in the friendly-overload generator source linked above.
Context
0.3.298main:cc9d25e833fb41aeb4cc931a61366d6328a8b51dnet10.0andnet11.0LangVersion: defaultAdditional context
PCSTRis documented and generated as a pointer to a null-terminated constant character string. The rawPCSTRoverload correctly leaves this contract to the caller, but the generatedstring-friendly overload should satisfy that contract on behalf of its caller.The corresponding
PCWSTRfriendly-overload path does not have this problem because fixing a managedstringdirectly guarantees a null character atpointer[string.Length]..NET's UTF-8
LibraryImportmarshaller also explicitly allocates space for and writes a trailing null byte.This issue was created with assistance from OpenAI Codex.