Title: Maglev OSR: object literals returned by a function share their double fields, so earlier results change after later calls (v24.11.0, v26.9.0) · Issue #66457 · nodejs/node · GitHub
Open Graph Title: Maglev OSR: object literals returned by a function share their double fields, so earlier results change after later calls (v24.11.0, v26.9.0) · Issue #66457 · nodejs/node
X Title: Maglev OSR: object literals returned by a function share their double fields, so earlier results change after later calls (v24.11.0, v26.9.0) · Issue #66457 · nodejs/node
Description: Version v24.11.0 (V8 13.6.233.10-node.28) and v26.9.0 (V8 14.6.202.34-node.32). Not reproduced on v22.23.1 or v20.20.1. Later 24.x releases were not tested. Platform Darwin 27.0.0 arm64 (macOS 27.0, Apple M4 Pro, 14 cores) Subsystem V8 (...
Open Graph Description: Version v24.11.0 (V8 13.6.233.10-node.28) and v26.9.0 (V8 14.6.202.34-node.32). Not reproduced on v22.23.1 or v20.20.1. Later 24.x releases were not tested. Platform Darwin 27.0.0 arm64 (macOS 27.0...
X Description: Version v24.11.0 (V8 13.6.233.10-node.28) and v26.9.0 (V8 14.6.202.34-node.32). Not reproduced on v22.23.1 or v20.20.1. Later 24.x releases were not tested. Platform Darwin 27.0.0 arm64 (macOS 27.0...
Opengraph URL: https://github.com/nodejs/node/issues/66457
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"Maglev OSR: object literals returned by a function share their double fields, so earlier results change after later calls (v24.11.0, v26.9.0)","articleBody":"### Version\n\nv24.11.0 (V8 13.6.233.10-node.28) and v26.9.0 (V8 14.6.202.34-node.32). Not reproduced on v22.23.1 or v20.20.1. Later 24.x releases were not tested.\n\n### Platform\n\n```text\nDarwin 27.0.0 arm64 (macOS 27.0, Apple M4 Pro, 14 cores)\n```\n\n### Subsystem\n\nV8 (Maglev OSR)\n\n### What steps will reproduce the bug?\n\nRun the script below many times, one process per run. It exits 1 and prints `BUG` when it catches the problem.\n\n```bash\nfor i in $(seq 1 2000); do node maglev-osr-alias.mjs > /dev/null || echo FAIL; done | sort | uniq -c\n```\n\nThe script is plain JavaScript with no dependencies. `faceStats(s, face, off)` loops over a triangle soup held in typed arrays, accumulates two doubles (`area`, `maxOff`), fills a `Set` and a `Map`, and returns `{ area, maxOff, verts, rim }`. The script calls it eight times on different generated inputs, doing some allocation between calls, and keeps each returned object next to the numbers read from it immediately after the call. At the end it compares each object with what it returned.\n\n\u003cdetails>\n\u003csummary>\u003ccode>maglev-osr-alias.mjs\u003c/code> (105 lines)\u003c/summary>\n\n```js\n// Node 24 (V8 13.6): objects returned by a function come out sharing the storage of\n// their double-valued fields, so results of earlier calls change after later calls.\n//\n// Intermittent: about 1 run in 50 on Node 24.11.0 and 1 in 175 on Node 26.9.0 (macOS arm64).\n// Run it in a loop:\n//\n// for i in $(seq 1 2000); do node maglev-osr-alias.mjs > /dev/null || echo FAIL; done | sort | uniq -c\n//\n// It does not fail with --no-maglev-osr.\n//\n// `faceStats` sums the area of the triangles of one face of a triangle soup and returns\n// { area, maxOff, verts, rim }. The script calls it eight times on different inputs, keeps\n// each returned object together with the numbers read from it right after the call, and\n// at the end compares each object with what it returned.\n\nclass V3 {\n constructor(x = 0, y = 0, z = 0) { this.x = x; this.y = y; this.z = z; }\n clone() { return new V3(this.x, this.y, this.z); }\n sub(v) { this.x -= v.x; this.y -= v.y; this.z -= v.z; return this; }\n add(v) { this.x += v.x; this.y += v.y; this.z += v.z; return this; }\n cross(v) { const ax = this.x, ay = this.y, az = this.z; this.x = ay * v.z - az * v.y; this.y = az * v.x - ax * v.z; this.z = ax * v.y - ay * v.x; return this; }\n length() { return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); }\n divideScalar(s) { this.x /= s; this.y /= s; this.z /= s; return this; }\n}\nfunction faceStats(s, face, off) {\n const id = s.ids.get(face.id);\n const P = (i) => new V3(s.pos[3 * i], s.pos[3 * i + 1], s.pos[3 * i + 2]);\n let area2 = 0, maxOff = 0;\n const verts = new Set();\n const use = new Map();\n for (let t = 0; t \u003c s.tris.length / 3; t++) {\n if (s.faceOf[t] !== id) continue;\n const idx = [s.tris[3 * t], s.tris[3 * t + 1], s.tris[3 * t + 2]];\n const [a, b, c] = idx.map(P);\n area2 += b.clone().sub(a).cross(c.clone().sub(a)).length() / 2;\n maxOff = Math.max(maxOff, off(a.clone().add(b).add(c).divideScalar(3)));\n for (let e = 0; e \u003c 3; e++) {\n verts.add(idx[e]);\n const k = [idx[e], idx[(e + 1) % 3]].sort((x, y) => x - y).join(\"|\");\n use.set(k, (use.get(k) ?? 0) + 1);\n }\n }\n const rim = new Set();\n for (const [k, c] of use) if (c === 1) for (const i of k.split(\"|\")) rim.add(Number(i));\n return { area: area2, maxOff, verts, rim };\n}\n\n\n// ---- generated input: triangle soups on a cylinder of radius 5, each triangle tagged with a face label ----\nlet seed = 12345;\nconst rnd = () => { seed = (seed * 1664525 + 1013904223) >>> 0; return seed / 4294967296; };\nfunction soup(nVerts, nTris, nFaces, label, labelTris) {\n const pos = new Float32Array(3 * nVerts);\n for (let i = 0; i \u003c nVerts; i++) { const th = rnd() * 2 * Math.PI; pos[3 * i] = 5 * Math.cos(th); pos[3 * i + 1] = 5 * Math.sin(th); pos[3 * i + 2] = rnd() * 8; }\n const tris = new Uint32Array(3 * nTris), faceOf = new Uint32Array(nTris);\n for (let t = 0; t \u003c nTris; t++) {\n const a = Math.floor(rnd() * (nVerts - 2));\n tris[3 * t] = a; tris[3 * t + 1] = a + 1; tris[3 * t + 2] = a + 2;\n let other = 1 + Math.floor(rnd() * nFaces); if (other === label) other = (other % nFaces) + 1;\n faceOf[t] = t \u003c labelTris ? label : other;\n }\n const faces = Array.from({ length: nFaces }, (_, k) => ({ id: 'face-' + k + '-' + Math.floor(rnd() * 1e9) }));\n return { s: { pos, tris, faceOf, ids: new Map(faces.map((f, k) => [f.id, k + 1])) }, faceId: faces[label - 1].id };\n}\n// [vertices, triangles, faces, label asked for, triangles carrying that label, ms of other work before the call]\nconst CALLS = [\n [5112, 10220, 3, 1, 10082, 40],\n [5206, 528, 4, 1, 338, 20],\n [5206, 528, 4, 4, 52, 0],\n [5206, 528, 4, 4, 52, 40],\n [5206, 528, 4, 4, 52, 10],\n [2664, 5324, 4, 2, 2592, 60],\n [2766, 2994, 5, 2, 204, 20],\n [2766, 2994, 5, 5, 58, 0],\n];\n// Other work between calls: allocation, some of it retained, so garbage collections run.\nconst ring = []; let sink = 0, w = 0;\nfunction churn(ms) {\n if (ms \u003c= 0) return;\n const end = performance.now() + ms;\n while (performance.now() \u003c end) {\n for (let i = 0; i \u003c 2000; i++) {\n const o = { a: w * 0.5, b: [w, w + 1, w + 2], c: 'k' + (w % 977), d: { e: w } };\n if (ring.length \u003c 200000) ring.push(o); else ring[w % 200000] = o;\n sink += o.b.length; w++;\n }\n }\n}\nconst mkOff = (R) => (p) => Math.abs(Math.hypot(p.x, p.y) - R);\nconst out = [];\nCALLS.forEach(([nv, nt, nf, label, labelTris, gap], ci) => {\n churn(gap);\n const { s, faceId } = soup(nv, nt, nf, label, labelTris);\n const face = ci % 3 ? { id: faceId, kind: 'x' + ci, n: ci } : { id: faceId }; // a few object shapes, as real callers have\n const r = faceStats(s, face, mkOff(5)); // a fresh closure per call\n out.push({ r, area: r.area, maxOff: r.maxOff }); // remember what the call returned\n});\nlet bad = 0;\nout.forEach((o, i) => {\n if (!Object.is(o.r.area, o.area) || !Object.is(o.r.maxOff, o.maxOff)) {\n bad++;\n console.log(`call ${i + 1}: returned area=${o.area} maxOff=${o.maxOff}; the same object now reads area=${o.r.area} maxOff=${o.r.maxOff}`);\n }\n});\nconsole.log(bad ? 'BUG: results of earlier calls changed after later calls' : 'ok');\nprocess.exit(bad ? 1 : 0);\n```\n\n\u003c/details>\n\n### How often does it reproduce? Is there a required condition?\n\nIntermittent. Eight runs in parallel on the 14-core machine above, which was also busy with other work:\n\n| runtime | failed / runs |\n|---|---|\n| v24.11.0 | 58 / 3000 |\n| v24.11.0 `--no-maglev-osr` | 0 / 3000 |\n| v24.11.0 `--no-concurrent-osr` | 0 / 1500 |\n| v24.11.0 `--single-threaded-gc` | 2 / 1500 |\n| v26.9.0 | 17 / 3000 |\n| v26.9.0 `--no-maglev-osr` | 0 / 3000 |\n| v22.23.1 | 0 / 3000 |\n| v20.20.1 | 0 / 1500 |\n\nThe timing of the work between the calls matters: with every gap set to 0 the script did not fail in 600 runs, and some other gap patterns did not fail either. The gaps in the script (40, 20, 0, 40, 10, 60, 20, 0 ms) are the ones that failed most often here; a different machine may need different ones.\n\n### What is the expected behavior? Why is that the expected behavior?\n\nEvery call returns a fresh object literal, so an object's `area` and `maxOff` must keep the values they had when the call returned. The script should always print `ok`.\n\n### What do you see instead?\n\nTwo or more of the returned objects share their double-valued fields. They are distinct objects (`!==`) with different `verts` and `rim` sets, but `area` and `maxOff` of the earlier ones now read as the values of the latest one:\n\n```text\ncall 4: returned area=906.4244995830119 maxOff=4.603054157215277; the same object now reads area=1227.004391521744 maxOff=4.335027103651656\ncall 5: returned area=993.2168554423932 maxOff=4.695433341606722; the same object now reads area=1227.004391521744 maxOff=4.335027103651656\ncall 6: returned area=46422.496206723445 maxOff=4.9146857121757845; the same object now reads area=1227.004391521744 maxOff=4.335027103651656\ncall 7: returned area=3754.819562707991 maxOff=4.836269298746963; the same object now reads area=1227.004391521744 maxOff=4.335027103651656\nBUG: results of earlier calls changed after later calls\n```\n\nRecomputing with the same inputs gives the originally returned values, so the computation is right and the stored fields are what changes.\n\n### Additional information\n\n**Trace signature.** 600 runs on v24.11.0 with `--trace-opt --trace-deopt`: 13 failed. Every failing run has two or three eager deopts of the *same* Maglev code object for `faceStats`, compiled for OSR, at the bytecode right after `CreateObjectLiteral` (the first `DefineNamedOwn` of the returned literal):\n\n```text\n[bailout (kind: deopt-eager, reason: Insufficient type feedback for generic named access): begin. deoptimizing \u003cJSFunction faceStats>, \u003cCode MAGLEV>, opt id 32, bytecode offset 1390, ...]\n[bailout (kind: deopt-eager, reason: Insufficient type feedback for generic named access): begin. deoptimizing \u003cJSFunction faceStats>, \u003cCode MAGLEV>, opt id 32, bytecode offset 1390, ...]\n```\n\nOf the 587 passing runs, 575 have no such deopt and 12 have exactly one. The number of objects sharing fields equals the number of these deopts: 9 runs had two deopts and one earlier object changed, 4 runs had three deopts and two earlier objects changed. So the objects that end up aliased are the ones allocated by that cached Maglev OSR code just before it deopts at the literal's first store; the interpreter then stores the doubles.\n\nMy reading, not verified in V8's source: those objects are created sharing one mutable `HeapNumber` box per double field (the boilerplate's, or one another's), and the interpreter's in-place double stores then write through to all of them.\n\n**Flags.** `--no-maglev-osr` removes it on both versions. In the application where this was found, `--no-turbofan`, `--no-osr-from-maglev`, `--no-concurrent-osr` and `--single-threaded-gc` each also gave 0 failures in 400 runs against 6 in 400 by default; with the reduced script `--single-threaded-gc` still fails, so that one only shifts timing.\n\n**How it was found.** A vitest suite (one forked process per test file) had two geometry tests fail intermittently with values wrong by a large factor. With the application's own code, one cold process per test file: v24.11.0 failed 27 of 2820 runs, 0 of 2680 with `--no-maglev-osr`, and v22.23.1 0 of 2552. The script above is a reduction of the test helper with generated data; it is not minimal.\n\nPossibly related: #66126 (concurrent-compiler races on v24.x), #64841.\n","author":{"url":"https://github.com/jbacus","@type":"Person","name":"jbacus"},"datePublished":"2026-10-02T05:11:55Z","url":"https://github.com/nodejs/node/issues/66457"}
| 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:f1fc59c8-ce74-a706-2517-1cabad964c00 |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | C46E:1085A1:77DAB0:9B0AE3:6AED317E |
| html-safe-nonce | 95f715bc154bc574e823288b187dc5b3770f1f81110088e43ec3d19b148daf95 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJDNDZFOjEwODVBMTo3N0RBQjA6OUIwQUUzOjZBRUQzMTdFIiwidmlzaXRvcl9pZCI6IjQyNzA5NzM5NTI2MjEwOTczNDIiLCJyZWdpb25fZWRnZSI6ImlhZCIsInJlZ2lvbl9yZW5kZXIiOiJpYWQifQ== |
| visitor-hmac | 3a951f0c88b6e89a77de81ebb7347532898c22c9e22397b587699ae38b7a8ad8 |
| hovercard-subject-tag | issue:5673181314 |
| 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/66457/issue_layout |
| twitter:image | https://opengraph.githubassets.com/f39caebf7486ca2f003b1c3a665b0818225a2a8bba664d42d8c9e4c2360a5562/nodejs/node/issues/66457 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/f39caebf7486ca2f003b1c3a665b0818225a2a8bba664d42d8c9e4c2360a5562/nodejs/node/issues/66457 |
| og:image:alt | Version v24.11.0 (V8 13.6.233.10-node.28) and v26.9.0 (V8 14.6.202.34-node.32). Not reproduced on v22.23.1 or v20.20.1. Later 24.x releases were not tested. Platform Darwin 27.0.0 arm64 (macOS 27.0... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | jbacus |
| hostname | github.com |
| expected-hostname | github.com |
| None | e51d0b7b5ab34b1688c05b54c3ed17b8658b2fa9bca973a9c740683ccb12cf4a |
| 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 | 59ebb7b901cc154ecc179a44f266fcf69f343b30 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
| react-app-name | issues |
Links:
Viewport: width=device-width