Hacktoberfest 2026 : les issues que les mainteneurs ont marquées pour octobre, ouvertes et accessibles aux débutants. Parcourir les issues Hacktoberfest

`PooledBuffer.FromStringUtf8`/`FromSpanUtf8` never write the NUL terminator → garbage in JS error messages and linked module names on .NET

Fermée
#508 0 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

@r-Larch y travaille déjà.

Depuis le 21/10/2026.

  • #510 par @r-Larch — ouverte

Évaluation

Difficulté
2/5
Temps estimé
1-3 heures
Accessibilité débutants
30/100
Type d'issue
Bug
Clarté
Clairement spécifiée
Activité
À l'abandon
Stack technique
csharp, node.js
Domaine
api, backend

Piste de recherche

Start with src/NodeApi/Runtime/PooledBuffer.cs and inspect FromStringUtf8 and FromSpanUtf8; the issue includes a deterministic reproduction that dirties the shared array pool. Check the linked pull request #510 for work already underway. Done means UTF-8 buffers terminate correctly, empty strings are handled, and the reported error message and module name cases no longer include garbage.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Description

PooledBuffer.FromStringUtf8 and PooledBuffer.FromSpanUtf8 (src/NodeApi/Runtime/PooledBuffer.cs) reserve byteLength + 1 bytes but never write the terminating \0:

PooledBuffer buffer = new(byteLength, byteLength + 1);              // reserves the terminator byte...
Encoding.UTF8.GetBytes(value, 0, value!.Length, buffer.Buffer, 0); // ...but never writes it

On .NET the array comes from ArrayPool<byte>.Shared.Rent, which is not zeroed. (On .NET Framework / netstandard it is a fresh new byte[], so the bug only shows on .NET.) Several call sites pass the pointer to Node-API functions that take a NUL-terminated const char* without a length:

Managed API Native function Argument
JSError.ThrowError / ThrowTypeError / ThrowRangeError / ThrowSyntaxError (string overloads) napi_throw_error, napi_throw_type_error, napi_throw_range_error, node_api_throw_syntax_error code and msg
NodeEmbeddingRuntimeSettings.Modules node_embedding_runtime_config_add_module module_name
exceptions in the embedding callback adapters (TaskRunCallbackAdapter, …) node_embedding_last_error_message_set message

Once the pool is warm, native code reads stale bytes after the string, or past the end of the array if it contains no zero byte (out-of-bounds read).

Call sites that pass an explicit length (CreateFunction, DefineClass, GetSymbolFor, FatalError) are not affected.

What we saw
  • In production (0.9.21, net10.0): a module registered via NodeEmbeddingRuntimeSettings.Modules as gaia was linked under a corrupted name, and process._linkedBinding('gaia') failed with No such binding was linked: gaia. It happened intermittently, only after the app had been running for a while.
  • JSError.ThrowError("x") from a .NET callback → in JS, e.message is "x" followed by 15 garbage bytes (x���������������).
Repro

Make it deterministic by dirtying the shared pool on the JS thread first:

static void FillArrayPoolWithGarbage()
{
    List<byte[]> rented = new();
    for (int size = 1; size <= 1024; size *= 2)
        for (int i = 0; i < 8; i++)
        {
            byte[] array = ArrayPool<byte>.Shared.Rent(size);
            array.AsSpan().Fill(0xAA);
            rented.Add(array);
        }
    foreach (byte[] array in rented) ArrayPool<byte>.Shared.Return(array);
}

nodejs.Run(() =>
{
    JSValue.Global.SetProperty("throwError", JSValue.CreateFunction("throwError", (args) =>
    {
        FillArrayPoolWithGarbage();
        JSError.ThrowError((string)args[0]);
        return JSValue.Undefined;
    }));

    string message = (string)JSValue.RunScript(
        "(() => { try { throwError('x'); } catch (e) { return e.message; } })()");
    // Expected "x"; actual "x" + garbage.
});
Related
  • An empty string returns PooledBuffer.Empty, which pins to a NULL pointer. So JSError.ThrowError("") passes msg = NULL, napi_throw_error returns napi_invalid_arg, and no error is thrown at all. An empty module name would likewise pass module_name = NULL.
  • NodejsRuntime.EmbeddingRuntimeConfigAddModule never disposes its PooledBuffer, so the rented array is not returned to the pool.
  • Same class of bug as #481 (Utf8StringArray, fix in PR #482).
Suggested fix

Write the terminator explicitly in both helpers (buffer.Buffer[byteLength] = 0). Encode "" as a 1-byte buffer holding only the terminator, and keep null → null pointer (used for the optional code). Add using in EmbeddingRuntimeConfigAddModule; Node.js copies the name into a std::string, so disposing right after the call is safe. I have a PR ready.

Versions: reproduced on main (54d2b74) and in 0.9.21; the code is unchanged in 0.9.28. Windows x64, net10.0, Microsoft.JavaScript.LibNode 20.1800.203.

Langage dominant
C#
Étoiles
789
Forks
81
Merge moyen
4 j 20 h
PR mergées (30 j)
6

Préparer son environnement

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Autres issues de microsoft/node-api-dotnet

Toutes les issues de microsoft/node-api-dotnet

Issues similaires

Plus d'issues C#

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.