Skip to content

Commit faa8a05

Browse files
authored
fix: uneval loses a repeated typed array/DataView referenced inside a cycle (#163)
* fix: emit uneval reconstructions before the statements that reference them A typed array, DataView or ArrayBuffer that is referenced more than once is hoisted into the IIFE preamble as a placeholder (`{}`) and rebuilt with a reassignment (`b = new Uint8Array(...)`). That reassignment was pushed onto the same ordered `statements` list as the property fills, so when a referencing container came first, it captured the `{}` placeholder: const view = new Uint8Array([1, 2, 3]); const obj = { a: view, b: view }; obj.self = obj; // make `obj` a named, cyclic value uneval(obj); // (function(a,b){a.self=a;a.a=b;a.b=b;b=new Uint8Array([1,2,3]);return a}({},{})) // ^^^^^^^ captured the {} placeholder, not the view Emit these reassignments first. They only depend on IIFE arguments (a typed array's buffer is always passed as an argument, never reassigned), never on each other or on the containers, so running them before the property fills is safe and makes the references resolve to the real value. * chore: add changeset
1 parent 3770846 commit faa8a05

3 files changed

Lines changed: 29 additions & 3 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'devalue': patch
3+
---
4+
5+
fix: emit uneval reconstructions before the statements that reference them

‎src/uneval.js‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,13 @@ export function uneval(value, replacer) {
361361
/** @type {string[]} */
362362
const values = [];
363363

364+
// Reconstructions (e.g. `b = new Uint8Array(...)`) reassign a placeholder
365+
// parameter. They must run before the `statements` that reference them,
366+
// otherwise those statements capture the placeholder. They only depend on
367+
// IIFE arguments (never on each other), so emitting them first is safe.
368+
/** @type {string[]} */
369+
const reconstructions = [];
370+
364371
names.forEach((name, thing) => {
365372
params.push(name);
366373

@@ -457,7 +464,7 @@ export function uneval(value, replacer) {
457464
}
458465

459466
values.push(`{}`);
460-
statements.push(`${name}=${str}`);
467+
reconstructions.push(`${name}=${str}`);
461468
break;
462469
}
463470

@@ -478,7 +485,7 @@ export function uneval(value, replacer) {
478485
str += ')';
479486

480487
values.push(`{}`);
481-
statements.push(`${name}=${str}`);
488+
reconstructions.push(`${name}=${str}`);
482489
break;
483490
}
484491

@@ -507,7 +514,8 @@ export function uneval(value, replacer) {
507514

508515
statements.push(`return ${str}`);
509516

510-
return `(function(${params.join(',')}){${statements.join(';')}}(${values.join(',')}))`;
517+
const body = [...reconstructions, ...statements].join(';');
518+
return `(function(${params.join(',')}){${body}}(${values.join(',')}))`;
511519
} else {
512520
return str;
513521
}

‎test/index.test.js‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1493,6 +1493,19 @@ uvu.test('does not create duplicate parameter names', () => {
14931493
eval(serialized);
14941494
});
14951495

1496+
uvu.test('reconstructs a referenced typed array before it is used in a cycle', () => {
1497+
const view = new Uint8Array([1, 2, 3]);
1498+
const obj = { a: view, b: view };
1499+
obj.self = obj;
1500+
1501+
const result = (0, eval)(uneval(obj));
1502+
1503+
assert.is(result.self, result);
1504+
assert.ok(result.a instanceof Uint8Array);
1505+
assert.is(result.a, result.b);
1506+
assert.equal(Array.from(result.a), [1, 2, 3]);
1507+
});
1508+
14961509
uvu.test('rejects sparse array __proto__ pollution via parse', () => {
14971510
// Attempt to set __proto__ on an array via the sparse array encoding
14981511
const payload = JSON.stringify([[consts.SPARSE, 1, '__proto__', { polluted: true }]]);

0 commit comments

Comments
 (0)