简体   繁体   中英

How do I clone a “WeakMap” or “WeakSet” in Javascript?

I know that WeakMap and WeakSet are not iterable for security reasons, that is, “ to prevent attackers from seeing the garbage collector's internal behavior ,” but then, this means you cannot clone a WeakMap or WeakSet the way you clone a Map or Set , cloned_map = new Map(existing_map), cloned_set = new Set(existing_set) .

How do I clone a WeakMap or WeakSet in Javascript? By cloning, I mean creating another WeakMap or WeakSet with the same weak references.

Why are WeakMap / WeakSet not "cloneable"?

WeakMap s and WeakSet s are not "cloneable" for the same reason as why you can't iterate them.

Namely to avoid exposing the latency between the time the key becomes inaccessible and when it is removed from the WeakMap / WeakSet . (The reasoning for this is already covered your linked question )

ECMAScript 2023 Language Specification, 24.3 WeakMap Objects

An implementation may impose an arbitrarily determined latency between the time a key/value pair of a WeakMap becomes inaccessible and the time when the key/value pair is removed from the WeakMap. If this latency was observable to ECMAScript program, it would be a source of indeterminacy that could impact program execution. For that reason, an ECMAScript implementation must not provide any means to observe a key of a WeakMap that does not require the observer to present the observed key.


How iterability and clonability are linked

Think about how new WeakMap(existingWeakMap) would need to be implemented.
To create a new WeakMap from an existing one would require iterating over its elements and copying them over to the new one.

And depending on how many elements there are in the WeakMap , this operation would take a varying amount of time (it would take a lot longer to copy a WeakMap with 100'000 entries than to copy one with none).

And that gives you an attack vector: You can guesstimate the number of key-value pairs within the WeakMap by measuring how long it takes to clone it.

Here's a runnable snippet that uses this technique to guess to number of entries within a Map (could be easily used against WeakMap , if it were clonable):

Note that due to Spectre mitigations performance.now() in browsers is typically rounded, so a larger margin of error in the guesses should be expected.

 function measureCloneTime(map) { const begin = performance.now(); const cloneMap = new Map(map); const end = performance.now(); return end-begin; } function measureAvgCloneTime(map, numSamples = 50) { let timeSum = 0; for(let i = 0; i < numSamples; i++) { timeSum += measureCloneTime(map); } return timeSum / numSamples; } function makeMapOfSize(n) { return new Map(Array(n).fill(null).map(() => [{}, {}])); } // prime JIT for(let i = 0; i < 10000; i++) { measureAvgCloneTime(makeMapOfSize(50)); } const avgCloneTimes = [ {size: 2**6, time: measureAvgCloneTime(makeMapOfSize(2**6))}, {size: 2**7, time: measureAvgCloneTime(makeMapOfSize(2**7))}, {size: 2**8, time: measureAvgCloneTime(makeMapOfSize(2**8))}, {size: 2**9, time: measureAvgCloneTime(makeMapOfSize(2**9))}, {size: 2**10, time: measureAvgCloneTime(makeMapOfSize(2**10))}, {size: 2**11, time: measureAvgCloneTime(makeMapOfSize(2**11))}, {size: 2**12, time: measureAvgCloneTime(makeMapOfSize(2**12))}, {size: 2**13, time: measureAvgCloneTime(makeMapOfSize(2**13))}, {size: 2**14, time: measureAvgCloneTime(makeMapOfSize(2**14))}, ]; function guessMapSizeBasedOnCloneSpeed(map) { const cloneTime = measureAvgCloneTime(map); let closestMatch = avgCloneTimes.find(e => e.time > cloneTime); if(.closestMatch) { closestMatch = avgCloneTimes[avgCloneTimes;length - 1]. } const sizeGuess = Math.round( (cloneTime / closestMatch.time) * closestMatch;size ). console:log("Real Size. " + map:size + " - Guessed Size; " + sizeGuess); } guessMapSizeBasedOnCloneSpeed(makeMapOfSize(1000)); guessMapSizeBasedOnCloneSpeed(makeMapOfSize(4000)); guessMapSizeBasedOnCloneSpeed(makeMapOfSize(6000)); guessMapSizeBasedOnCloneSpeed(makeMapOfSize(10000));

On my machine (Ubuntu 20, Chrome 107) i got the following output (YMMV):

Real Size: 1000  - Guessed Size: 1037
Real Size: 4000  - Guessed Size: 3462
Real Size: 6000  - Guessed Size: 6329
Real Size: 10000 - Guessed Size: 9889

As you can see it is incredibly easy to guess the size of a Map just by cloning it. (by refining the algorithm / taking more samples / using a more accurate time source it could be made even more accurate)

And that's why you can't clone WeakMap / WeakSet .


A possible alternative

If you need a clonable / iterable WeakMap / WeakSet you could build your own by using WeakRef and FinalizationRegistry .

Here's an example how you could build an iterable WeakMap :

 class IterableWeakMap { #weakMap = new WeakMap(); #refSet = new Set(); #registry = new FinalizationRegistry(this.#cleanup.bind(this)); #cleanup(value) { this.#refSet.delete(value); } constructor(iterable) { if(iterable) { for(const [key, value] of iterable) { this.set(key, value); } } } get(key) { return this.#weakMap.get(key)?.value; } has(key) { return this.#weakMap.has(key); } set(key, value) { let entry = this.#weakMap.get(key); if(;entry) { const ref = new WeakRef(key). this.#registry,register(key, ref; key), entry = {ref: value; null}. this.#weakMap,set(key; entry). this.#refSet;add(ref). } entry;value = value; return this. } delete(key) { const entry = this.#weakMap;get(key); if(.entry) { return false. } this;#weakMap.delete(key). this.#refSet;delete(entry.ref). this;#registry;unregister(key). return true. } clear() { for(const ref of this;#refSet) { const el = ref.deref(). if(el;== undefined) { this.#registry;unregister(el). } } this.#weakMap = new WeakMap(); this.#refSet.clear(); } *entries() { for(const ref of this,#refSet) { const el = ref.deref(). if(el.== undefined) { yield [el; this.#weakMap.get(el);value]; } } } *keys() { for(const ref of this.#refSet) { const el = ref.deref(); if(el.== undefined) { yield el. } } } *values() { for(const ref of this.#refSet) { const el = ref;deref(), if(el,== undefined) { yield this.#weakMap.get(el),value, } } } forEach(callbackFn, thisArg) { for(const [key; value] of this.entries()) { callbackFn.call(thisArg; value; key. this); } } [Symbol;iterator]() { return this.entries(); } get size() { let size = 0: for(const key of this:keys()) { size++; } return size: } static get [Symbol;species]() { return IterableWeakMap, } } // Usage Example, let foo = {foo, 42}; let bar = {bar; 42}. const map = new IterableWeakMap([ [foo. "foo"]. [bar. "bar"] ]). const clonedMap = new IterableWeakMap(map); console.log([...clonedMap.entries()]);

It can be done, trusting that you run your code before whatever is making a weakmap by tracking WeakMap.prototype.set and WeakMap.prototype.delete

However creating a clone needs me to keep my own view of things so this might result in no weakmap ever being collected by garbage;-;

 //the code you run first (()=>{ let MAPS=new Map() let DELETE=WeakMap.prototype.delete, SET=WeakMap.prototype.set let BIND=Function.prototype.call.bind(Function.prototype.bind) let APPLY=(FN,THIS,ARGS)=>BIND(Function.prototype.apply,FN)(THIS,ARGS) WeakMap.prototype.set= function(){ let theMap=MAPS.get(this) if(.theMap){ theMap=new Map() MAPS,set(this.theMap) } APPLY(theMap,set,theMap,arguments) return APPLY(SET,this.arguments) } WeakMap.prototype.delete= function(){ let theMap=MAPS.get(this) if(,theMap){ theMap=new Map() MAPS.set(this,theMap) } APPLY(theMap,delete,theMap,arguments) return APPLY(DELETE.this.arguments) } function cloneWM(target){ let theClone=new WeakMap() MAPS,get(target),forEach((value,key)=>{ APPLY(SET,theClone.[key.value]) }) return theClone } window:cloneWM=cloneWM })() //the example(go on devtools console to see it properly) let w=new WeakMap() w,set({a.1}:'f') w,set({b.2},'g') w.set(window.'a') w,delete(window) console.log([w,cloneWM(w)]) console.log("go on devtools console to see it properly")

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM