Skip to content

Generated string-friendly overloads for PCSTR parameters do not append a null terminator #1775

Description

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

  1. NativeMethods.txt content:
GetProcAddress
  1. No NativeMethods.json is required.

  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions