Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

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

未关闭
#508 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

@r-Larch 已经在做这个了。

开始于 2026年10月21日。

  • #510 来自 @r-Larch —— 未关闭

评估

难度
2/5
预计耗时
1-3 小时
新手友好度
30/100
Issue 类型
缺陷
描述清晰度
描述清楚
活跃度
停滞
技术栈
csharp, node.js
领域
api, backend

调研方向

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.

由索引模型根据 Issue 内容生成。

描述

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.

主要语言
C#
星标
789
派生
81
平均合并
4 天 22 小时
30 天内合并 PR
4

环境准备

  • 没有 Dockerfile 或 Docker Compose 文件
  • 没有 Pull Request 模板
  • 阅读贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

microsoft/node-api-dotnet 的其他 Issue

查看 microsoft/node-api-dotnet 的全部 Issue

相似的 Issue

更多 C# Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。