`PooledBuffer.FromStringUtf8`/`FromSpanUtf8` never write the NUL terminator → garbage in JS error messages and linked module names on .NET
評価
- 難易度
- 2/5
- 見積もり時間
- 1〜3時間
- 初心者へのやさしさ
- 30/100
調査の方向性
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.Modulesasgaiawas linked under a corrupted name, andprocess._linkedBinding('gaia')failed withNo 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.messageis"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. SoJSError.ThrowError("")passesmsg = NULL,napi_throw_errorreturnsnapi_invalid_arg, and no error is thrown at all. An empty module name would likewise passmodule_name = NULL. NodejsRuntime.EmbeddingRuntimeConfigAddModulenever disposes itsPooledBuffer, 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時間
- マージ済み PR(30日)
- 4
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートなし
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
microsoft/node-api-dotnet のほかの issue
-
難易度 1/5 1〜3時間 初心者へのやさしさ 88/100
microsoft/node-api-dotnet#484 · コメント 1 件 ·
-
`JSError.ThrowTypeError` / `ThrowRangeError` / `ThrowSyntaxError` throw a plain `Error`対応中かも @r-Larch が 7 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 58/100
microsoft/node-api-dotnet#509 ·
-
JSError.ThrowError(Exception) aborts the process when a Worker terminates during a .NET call対応中かも @GalaxiasKyklos が 15 日前に担当しました。 オープン
難易度 4/5 3〜5日 初心者へのやさしさ 18/100
microsoft/node-api-dotnet#505 ·
-
難易度 3/5 1〜2日 初心者へのやさしさ 70/100
microsoft/node-api-dotnet#504 ·
-
難易度 3/5 1〜2日 初心者へのやさしさ 82/100
microsoft/node-api-dotnet#502 ·
microsoft/node-api-dotnet の issue をすべて見る
似ている issue
-
bug 優先: 低
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
メンテナーはふだん 1 日以内に返信
-
難易度 1/5 1時間未満 初心者へのやさしさ 92/100
-
area-ai bug untriaged
難易度 2/5 1〜3時間 初心者へのやさしさ 67/100
dotnet/extensions#7893 ·
メンテナーはふだん 1 日以内に返信
-
難易度 1/5 1時間未満 初心者へのやさしさ 72/100
-
documentation good first issue
難易度 2/5 1〜3時間 初心者へのやさしさ 85/100