Title: UTF-8 character corruption in fast-utf8-stream.js via releaseWritingBuf() leads to silent data loss on partial writes · Issue #61744 · nodejs/node · GitHub
Open Graph Title: UTF-8 character corruption in fast-utf8-stream.js via releaseWritingBuf() leads to silent data loss on partial writes · Issue #61744 · nodejs/node
X Title: UTF-8 character corruption in fast-utf8-stream.js via releaseWritingBuf() leads to silent data loss on partial writes · Issue #61744 · nodejs/node
Description: Version No response Platform **Summary:** releaseWritingBuf() in lib/internal/streams/fast-utf8-stream.js incorrectly calculates string slice positions when fs.write returns a byte count that splits a multi-byte UTF-8 character, causing ...
Open Graph Description: Version No response Platform **Summary:** releaseWritingBuf() in lib/internal/streams/fast-utf8-stream.js incorrectly calculates string slice positions when fs.write returns a byte count that split...
X Description: Version No response Platform **Summary:** releaseWritingBuf() in lib/internal/streams/fast-utf8-stream.js incorrectly calculates string slice positions when fs.write returns a byte count that split...
Opengraph URL: https://github.com/nodejs/node/issues/61744
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"UTF-8 character corruption in fast-utf8-stream.js via releaseWritingBuf() leads to silent data loss on partial writes","articleBody":"### Version\n\n_No response_\n\n### Platform\n\n```text\n**Summary:** releaseWritingBuf() in lib/internal/streams/fast-utf8-stream.js incorrectly calculates string slice positions when fs.write returns a byte count that splits a multi-byte UTF-8 character, causing silent data corruption (lost characters, lone surrogates in output).\n\n**Description:** \n\nThe releaseWritingBuf function (line 896) converts bytes-written to character count using:\n\n\nn = Buffer.from(writingBuf).subarray(0, n).toString().length;\n\n\nWhen n bytes cuts through a multi-byte character, the incomplete UTF-8 sequence becomes U+FFFD (replacement character) via .toString(). This replacement character has a different .length than the original character in JS UTF-16, causing .slice(n) to cut at the wrong position:\n\n- 3-byte characters (CJK, most non-Latin): character silently dropped from output\n- 4-byte characters (emoji, supplementary CJK): lone low surrogate left in remaining buffer, producing invalid UTF-8 on next write\n\nThe file was recently added (January 2026), ported from SonicBoom. It is used as the fast path for streaming UTF-8 output.\n\n## Steps To Reproduce:\n\n1. Save this as poc.js and run with node poc.js:\n\n\n// Reproduces the releaseWritingBuf logic from lib/internal/streams/fast-utf8-stream.js lines 896-906\nfunction releaseWritingBuf(writingBuf, len, n) {\n if (typeof writingBuf === 'string' \u0026\u0026 Buffer.byteLength(writingBuf) !== n) {\n n = Buffer.from(writingBuf).subarray(0, n).toString().length;\n }\n len = Math.max(len - n, 0);\n writingBuf = writingBuf.slice(n);\n return { writingBuf, len };\n}\n\n// Case 1: 4-byte emoji split at byte 7 — lone surrogate\nconst r1 = releaseWritingBuf(\"hello🌍world\", 14, 7);\nconsole.log(\"Case 1 - Emoji split:\");\nconsole.log(\" Result:\", JSON.stringify(r1.writingBuf));\nconsole.log(\" Expected:\", JSON.stringify(\"🌍world\"));\nconsole.log(\" First char code: 0x\" + r1.writingBuf.charCodeAt(0).toString(16));\nconsole.log(\" Is lone surrogate:\", r1.writingBuf.charCodeAt(0) \u003e= 0xDC00 \u0026\u0026\nr1.writingBuf.charCodeAt(0) \u003c= 0xDFFF);\n\n// Case 2: 3-byte CJK char split at byte 4 — character lost\nconst r2 = releaseWritingBuf(\"abc中def\", 9, 4);\nconsole.log(\"\\nCase 2 - CJK split:\");\nconsole.log(\" Result:\", JSON.stringify(r2.writingBuf));\nconsole.log(\" Expected:\", JSON.stringify(\"中def\"));\nconsole.log(\" Character 中 lost:\", !r2.writingBuf.includes(\"中\"));\n\n\n2. Output shows:\n\nCase 1 - Emoji split:\n Result: \"\\udf0dworld\" ← CORRUPTED (lone surrogate)\n Expected: \"🌍world\"\n First char code: 0xdf0d\n Is lone surrogate: true\n\nCase 2 - CJK split:\n Result: \"def\" ← CHARACTER LOST\n Expected: \"中def\"\n Character 中 lost: true\n\n\n3. The vulnerable code is at:\nhttps://github.com/nodejs/node/blob/main/lib/internal/streams/fast-utf8-stream.js#L896-L906\n\nPartial fs.write returns are possible when writing to pipes near capacity, under disk I/O pressure, or to Docker log pipes (the exact use case mentioned in the file's comments on line 69-70).\n\nAdditional finding: Line 240 has a typo from the SonicBoom port — this._asyncDrainScheduled should be this.#asyncDrainScheduled. All other 5 references use the private field correctly. The newListener handler is effectively dead code.\n\n## Impact:\n\nSilent data corruption in output files. Applications using Utf8Stream for logging with international characters (CJK, emoji, Cyrillic) can produce corrupted output when partial writes occur. 3-byte characters are silently lost (no error emitted). 4-byte characters produce invalid UTF-8 (lone surrogates). This is especially relevant for the Docker container logging use case the file was designed for.\n\n## Supporting Material/References:\n\n- Vulnerable function: releaseWritingBuf() at https://github.com/nodejs/node/blob/main/lib/internal/streams/fast-utf8-stream.js#L896-L906\n- Typo (secondary): line 240, _asyncDrainScheduled vs #asyncDrainScheduled\n- File derived from SonicBoom (https://github.com/pinojs/sonic-boom) — the original has a similar issue but uses _ prefix consistently\n- The PoC script above is standalone and runs on any Node.js version\n```\n\n### Subsystem\n\n_No response_\n\n### What steps will reproduce the bug?\n\n1. Save the following script as `poc.js` and run it with `node poc.js`.\n\n2. The script reproduces the exact logic from\n `lib/internal/streams/fast-utf8-stream.js` (lines 896–906),\n specifically the `releaseWritingBuf()` function.\n\n3. The script simulates partial `fs.write()` behavior where the number\n of bytes written splits a multi-byte UTF-8 character.\n\n4. Observe the output:\n - When a 4-byte UTF-8 character (emoji) is split, a lone surrogate\n remains in the output.\n - When a 3-byte UTF-8 character (CJK) is split, the character is\n silently dropped.\n\n5. This demonstrates incorrect string slicing caused by converting\n byte counts to character counts via `.toString().length`.\n\n### How often does it reproduce? Is there a required condition?\n\nIt reproduces deterministically whenever `fs.write()` (or an equivalent\ninternal write) returns a byte count that splits a multi-byte UTF-8\ncharacter.\n\nThe issue is not timing-dependent or race-based. The required condition\nis a partial write that ends in the middle of a UTF-8 sequence.\n\nThis can occur when writing to pipes, sockets, or log streams under\nbackpressure (e.g. near-capacity pipes, Docker container logs, or heavy\nI/O), which is a documented and expected behavior of `fs.write()`.\n\n### What is the expected behavior? Why is that the expected behavior?\n\nThe output must always preserve valid UTF-8 and must not silently\ncorrupt data.\n\nWhen a partial write ends in the middle of a multi-byte UTF-8 character,\nthe remaining bytes for that character should be preserved and written\nin a subsequent write, rather than being dropped or converted into\nreplacement characters.\n\nThis is the expected behavior because:\n- `fs.write()` is documented to return partial byte counts.\n- UTF-8 stream handling must be byte-safe across writes.\n- Producing lone surrogates or dropping characters violates UTF-8\n correctness and results in silent data corruption.\n\nThe current behavior breaks UTF-8 invariants and can corrupt log output\nin real-world streaming scenarios, such as container logging and pipe-\nbased streams, which this module explicitly targets.\n\n### What do you see instead?\n\nInstead of preserving valid UTF-8 output, the stream produces corrupted\nresults when a partial write splits a multi-byte character.\n\nSpecifically:\n- For 3-byte UTF-8 characters (e.g. CJK), the character is silently\n dropped from the output with no error.\n- For 4-byte UTF-8 characters (e.g. emoji), the remaining buffer starts\n with a lone UTF-16 surrogate, producing invalid UTF-8 on subsequent\n writes.\n\nNo error or warning is emitted, resulting in silent data corruption in\nthe output stream.\n\n### Additional information\n\n_No response_","author":{"url":"https://github.com/pitiflautico","@type":"Person","name":"pitiflautico"},"datePublished":"2026-02-08T18:50:26.000Z","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":0},"url":"https://github.com/61744/node/issues/61744"}
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:83040702-b431-82c1-ec9e-1e23f9c9dc89 |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | EE02:20AD09:27426AB:37872A5:6A4CEBAB |
| html-safe-nonce | 30b326e76ea297247f7b9874307e17ba21b247de29c3e4f7012c24fff3a7ea84 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJFRTAyOjIwQUQwOToyNzQyNkFCOjM3ODcyQTU6NkE0Q0VCQUIiLCJ2aXNpdG9yX2lkIjoiMTM0ODkxNjYyMjM0MTc2MTk2MyIsInJlZ2lvbl9lZGdlIjoiaWFkIiwicmVnaW9uX3JlbmRlciI6ImlhZCJ9 |
| visitor-hmac | 61486b3b3e4fa505711d835bbcc53c003f858b91448549593f1dc125e1cd942a |
| hovercard-subject-tag | issue:3913313662 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/nodejs/node/61744/issue_layout |
| twitter:image | https://opengraph.githubassets.com/15dc821bbff6f069e03f8081dd68cb681a83e5fea2dd6c90b1226dc30324a833/nodejs/node/issues/61744 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/15dc821bbff6f069e03f8081dd68cb681a83e5fea2dd6c90b1226dc30324a833/nodejs/node/issues/61744 |
| og:image:alt | Version No response Platform **Summary:** releaseWritingBuf() in lib/internal/streams/fast-utf8-stream.js incorrectly calculates string slice positions when fs.write returns a byte count that split... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | pitiflautico |
| hostname | github.com |
| expected-hostname | github.com |
| None | 299b43bca6e02ad35197ffeba30d2466846d5fb02ab96fbced5b5e6cec589fb8 |
| turbo-cache-control | no-preview |
| go-import | github.com/nodejs/node git https://github.com/nodejs/node.git |
| octolytics-dimension-user_id | 9950313 |
| octolytics-dimension-user_login | nodejs |
| octolytics-dimension-repository_id | 27193779 |
| octolytics-dimension-repository_nwo | nodejs/node |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 27193779 |
| octolytics-dimension-repository_network_root_nwo | nodejs/node |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | c5a57f04eeb310f57c73fd6d751d957e2ca27ed2 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width