Title: JSON.parse returns unexpected keys after decoding specified JSON key · Issue #63785 · nodejs/node · GitHub
Open Graph Title: JSON.parse returns unexpected keys after decoding specified JSON key · Issue #63785 · nodejs/node
X Title: JSON.parse returns unexpected keys after decoding specified JSON key · Issue #63785 · nodejs/node
Description: Version 24.16.0 Platform Microsoft Windows NT 10.0.20348.0 x64 Linux [REDACTED] 5.14.0-611.54.3.el9_7.x86_64 #1 SMP PREEMPT_DYNAMIC Thu May 7 16:31:24 EDT 2026 x86_64 x86_64 x86_64 GNU/Linux Subsystem No response What steps will reproduc...
Open Graph Description: Version 24.16.0 Platform Microsoft Windows NT 10.0.20348.0 x64 Linux [REDACTED] 5.14.0-611.54.3.el9_7.x86_64 #1 SMP PREEMPT_DYNAMIC Thu May 7 16:31:24 EDT 2026 x86_64 x86_64 x86_64 GNU/Linux Subsys...
X Description: Version 24.16.0 Platform Microsoft Windows NT 10.0.20348.0 x64 Linux [REDACTED] 5.14.0-611.54.3.el9_7.x86_64 #1 SMP PREEMPT_DYNAMIC Thu May 7 16:31:24 EDT 2026 x86_64 x86_64 x86_64 GNU/Linux Subsys...
Opengraph URL: https://github.com/nodejs/node/issues/63785
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"JSON.parse returns unexpected keys after decoding specified JSON key","articleBody":"### Version\n\n24.16.0\n\n### Platform\n\n```text\nMicrosoft Windows NT 10.0.20348.0 x64\nLinux [REDACTED] 5.14.0-611.54.3.el9_7.x86_64 #1 SMP PREEMPT_DYNAMIC Thu May 7 16:31:24 EDT 2026 x86_64 x86_64 x86_64 GNU/Linux\n```\n\n### Subsystem\n\n_No response_\n\n### What steps will reproduce the bug?\n\n### Background\n\nI was fuzzing a custom JSON parser by generating random stuff, serializing them with `JSON.stringify`, parsing them with my parser, and cross-checking the result against native `JSON.parse`.\n\nThe fuzz test reported divergences somehow, and during the troubleshoot I found this very weird bug. I'm able to reproduce it on two machines (Windows / Linux, Node 24.16.0 x64), but unable to reproduce it on my own machine with Node 20 / 16, nor can I reproduce it in ChatGPT's sandbox (Node v22.16.0 and Linux I guess).\n\n### Trigger sequence definition\n\nFor a given sequence pair:\n\n```js\nconst CANARY = '{\"\\\\r\":\"value are not important\",\"\\\\n\":\"just keys\"}', CHECK = '\\n'.charCodeAt(0);\nconst TRIGGER = '{\\\"\\\\r\\\":[],\\\"\\\\\\\\\\\":0}';\n```\n\nThe first key of two sequences should be the same (`\\r` vs `\\r`, or `\\f` vs `\\f`, etc.). And the second of canary should be another escaped control string (We use `\\n` in this case.\n\nThese are all valid trigger sequences:\n\n```js\nconst CANARY = '{\"\\\\r\":\"value are not important\",\"\\\\f\":\"just keys\"}', CHECK = '\\f'.charCodeAt(0);\nconst TRIGGER = '{\"\\\\r\":[],\"\\\\\\\\\":0}';\n\nconst CANARY = '{\"\\\\f\":\"value are not important\",\"\\\\n\":\"just keys\"}', CHECK = '\\n'.charCodeAt(0);\nconst TRIGGER = '{\"\\\\f\":[],\"\\\\\\\\\":0}';\n\nconst CANARY = '{\"\\\\n\":\"value are not important\",\"\\\\f\":\"just keys\"}', CHECK = '\\f'.charCodeAt(0);\nconst TRIGGER = '{\"\\\\n\":[],\"\\\\\\\\\":0}';\n\nconst CANARY = '{\"\\\\f\":\"value are not important\",\"\\\\r\":\"just keys\"}', CHECK = '\\r'.charCodeAt(0);\nconst TRIGGER = '{\"\\\\f\":[],\"\\\\\\\\\":0}';\n```\n\n### Minimal reproduce snippet\n\nThis is a hand-written version without AI slop.\n\n```js\nconsole.info('Node version: ' + process.versions.node);\n\nconst CANARY = '{\"\\\\r\":\"value are not important\",\"\\\\f\":\"just keys\"}', CHECK = '\\f'.charCodeAt(0);\nconst TRIGGER = '{\"\\\\r\":[],\"\\\\\\\\\":0}';\n\nconst isStateBroken = (pass) => {\n const keys = Object.keys(JSON.parse(CANARY));\n console.info(`[${pass.padEnd(10)}] Test keys: ${keys.map(k => Buffer.from(k).toString('hex')).join(', ')}`);\n return keys[1].charCodeAt(0) !== CHECK;\n};\n\n// Do a check first\nif (isStateBroken('Before')) {\n console.log('State already broken even before trigger sequence. Cannot reproduce!');\n process.exit(2);\n}\n\n// Parse some good jsons\nJSON.parse('{\"good\": \"json\"}');\nJSON.parse('{\"yay\": 114514}');\n\n// Still good\nif (isStateBroken('Good Parse')) {\n console.log('State already broken even before trigger sequence. Cannot reproduce!');\n process.exit(2);\n}\n\n// WTF\ntry {\n JSON.parse(TRIGGER);\n} catch {\n console.log('Trigger sequence parse failed!');\n process.exit(2);\n}\n\n// Is it broken now?\nconst bad = isStateBroken('Triggered');\nconsole.log(bad ? 'BROKEN' : 'Nope');\nprocess.exit(bad ? 0 : 1);\n```\n\n#### Output\n\n```\n[root@localhost bin]# ./node wtf.mjs\nNode version: 24.16.0\n[Before ] Test keys: 0d, 0c\n[Good Parse] Test keys: 0d, 0c\n[Triggered ] Test keys: 0d, 5c\nBROKEN\n\n[root@localhost bin]# node-20 wtf.mjs\nNode version: 20.14.0\n[Before ] Test keys: 0d, 0c\n[Good Parse] Test keys: 0d, 0c\n[Triggered ] Test keys: 0d, 5c\nNope\n```\n\n\u003cdetails>\n\n\u003csummary>Here's a GPT-5.5 generated version, that uses my original fuzzer\u003c/summary>\n\nIt basically does the same thing with the minimal version, just using my original fuzzer.\n\n```js\nconst POOL = ['a', 'b', ' ', '\"', '\\\\', '\\n', '\\t', '\\r', '\\b', '\\f', String.fromCharCode(0), String.fromCharCode(0x1f), '/', '中', 'é', '\\u{1f600}'].join('');\n\nfunction mulberry32(seed) {\n let a = seed >>> 0;\n return () => {\n a |= 0; a = (a + 0x6d2b79f5) | 0;\n let t = Math.imul(a ^ (a >>> 15), 1 | a);\n t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n };\n}\n\nfunction randNumber(rng) {\n const k = Math.floor(rng() * 8);\n if (k === 0) return 0;\n if (k === 1) return Math.floor((rng() - 0.5) * 2e6);\n if (k === 2) return (rng() - 0.5) * 2e6;\n if (k === 3) return rng() * 1e-7;\n if (k === 4) return rng() * 1e21;\n if (k === 5) return Number.MAX_SAFE_INTEGER;\n if (k === 6) return -0;\n return rng() \u003c 0.5 ? 0.1 + 0.2 : Math.PI;\n}\n\nfunction randString(rng) {\n const len = Math.floor(rng() * 8);\n let s = '';\n for (let i = 0; i \u003c len; i++) {\n s += rng() \u003c 0.6\n ? [...POOL][Math.floor(rng() * [...POOL].length)]\n : String.fromCharCode(Math.floor(rng() * 0x90));\n }\n return s;\n}\n\nfunction randValue(rng, depth) {\n const r = rng();\n if (depth \u003c= 0 || r \u003c 0.45) {\n const k = Math.floor(rng() * 5);\n if (k === 0) return null;\n if (k === 1) return rng() \u003c 0.5;\n if (k === 2) return randNumber(rng);\n return randString(rng);\n }\n\n const len = Math.floor(rng() * 5);\n if (r \u003c 0.72) return Array.from({ length: len }, () => randValue(rng, depth - 1));\n\n const o = {};\n for (let i = 0; i \u003c len; i++) {\n o[randString(rng)] = randValue(rng, depth - 1);\n }\n return o;\n}\n\nfunction reserialize(value, rng) {\n const styles = [undefined, 1, 2, 4, '\\t', ' \\t'];\n const indent = styles[Math.floor(rng() * styles.length)];\n const body = JSON.stringify(value, null, indent);\n const pad = () => ' \\t\\r\\n'.repeat(Math.floor(rng() * 2));\n return pad() + body + pad();\n}\n\n// Canary: a two-byte string value.\n// The second key is the JSON escape \"\\\\r\", which should decode to CR / U+000D.\nconst CANARY = '{\"\\\\f\":\"中\",\"\\\\r\":\"x\"}';\n\nconst canaryBad = () => {\n const keys = Object.keys(JSON.parse(CANARY));\n return keys[1].charCodeAt(0) !== 0x0d;\n};\n\nconst ITERS = Number(process.argv[2]) || 2000;\n\nconsole.log('node:', process.version);\nconsole.log('v8:', process.versions.v8);\nconsole.log('pool len:', [...POOL].length, '| cold canary OK:', !canaryBad());\n\nconst rng = mulberry32(305419896);\nlet firstBadAt = -1;\n\nfor (let n = 0; n \u003c ITERS; n++) {\n JSON.parse(reserialize(randValue(rng, 1 + Math.floor(rng() * 5)), rng));\n\n if (firstBadAt \u003c 0 && canaryBad()) {\n firstBadAt = n;\n }\n}\n\nconsole.log(`after ${ITERS} JSON.parse-only iters: canary bad = ${canaryBad()}, first bad at iter = ${firstBadAt}`);\n\nconst keys = Object.keys(JSON.parse(CANARY));\nconsole.log('final canary keys:');\nfor (const key of keys) {\n console.log(JSON.stringify(key), [...key].map(ch => ch.charCodeAt(0).toString(16).padStart(2, '0')));\n}\n```\n\n#### Output\n\n```\n>node wtf-gpt.mjs\nnode: v24.16.0\nv8: 13.6.233.17-node.49\npool len: 16 | cold canary OK: true\nafter 2000 JSON.parse-only iters: canary bad = true, first bad at iter = 104\nfinal canary keys:\n\"\\f\" [ '0c' ]\n\"\\\\\" [ '5c' ]\n```\n\u003c/details>\n\n\n### How often does it reproduce? Is there a required condition?\n\nAlways reproducible with node `v24.13.1` and `v24.16.0` (Windows and Linux x64).\n\nNOT reproducible with node `v16.17.0`, `v20.14.0` and `v22.16.0` (Linux x64).\n\n\n### What is the expected behavior? Why is that the expected behavior?\n\nThe 2nd key of parsed object is always `0x0A` or other expected value (e.g. '\\r') no matter how many times we call `JSON.parse` with whatever argument.\n\n`isStateBroken` always returns false.\n\n\n### What do you see instead?\n\nThe 2nd key become `0x5C` (or other values? all I see is `0x5C` for now) after passing the trigger sequence to `JSON.parse`.\n\nAnd `isStateBroken` returns true.\n\n### Additional information\n\n_No response_","author":{"url":"https://github.com/xWTF","@type":"Person","name":"xWTF"},"datePublished":"2026-06-07T14:19:03Z","url":"https://github.com/nodejs/node/issues/63785"}
| react-profiling | 0 |
| react-import-map | react,react-dom,react-dom/client,react-dom/profiling,react-is,react-reconciler,react/compiler-runtime,react/jsx-dev-runtime,react/jsx-runtime,scheduler@d5750093facf |
| 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:6e03f1a0-2d5f-5a5f-3858-d3c6b902124a |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | B6F8:6780E:5A7E6E:75F0AE:6AED2A34 |
| html-safe-nonce | fb4d944efd3cd0edde7d7ac0eaeee4c24a42afb8a5f264d4d852f50bdfa8a5b1 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJCNkY4OjY3ODBFOjVBN0U2RTo3NUYwQUU6NkFFRDJBMzQiLCJ2aXNpdG9yX2lkIjoiOTc2MTMxNTM0MjgxODQ1MzAwIiwicmVnaW9uX2VkZ2UiOiJpYWQiLCJyZWdpb25fcmVuZGVyIjoiaWFkIn0= |
| visitor-hmac | 64906e92eb57c6339e798621f5ccf4f692df12ff2882afc6d4c944e56fd36292 |
| hovercard-subject-tag | issue:4607610695 |
| 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/63785/issue_layout |
| twitter:image | https://opengraph.githubassets.com/006078e51ed3012ef8dc35f7b24f68d87684ec3ee4fe4d02e7ad1e99bb835dc2/nodejs/node/issues/63785 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/006078e51ed3012ef8dc35f7b24f68d87684ec3ee4fe4d02e7ad1e99bb835dc2/nodejs/node/issues/63785 |
| og:image:alt | Version 24.16.0 Platform Microsoft Windows NT 10.0.20348.0 x64 Linux [REDACTED] 5.14.0-611.54.3.el9_7.x86_64 #1 SMP PREEMPT_DYNAMIC Thu May 7 16:31:24 EDT 2026 x86_64 x86_64 x86_64 GNU/Linux Subsys... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | xWTF |
| hostname | github.com |
| expected-hostname | github.com |
| None | 9d95365dda0576efe9b71f45807595e396bd0ca9286fba5367a05048e6ddf8f2 |
| 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 | 98b41fa4b5635ceb45a4c54320fced73817ff290 |
| ui-target | canary-2 |
| theme-color | #1e2327 |
| color-scheme | light dark |
| react-app-name | issues |
Links:
Viewport: width=device-width