1

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.

6
  • Maybe you could use Object.create? Commented Nov 29, 2022 at 7:40
  • It is impossible to clone them, period. It is their purpose to hide direct access to keys and values. Commented Nov 30, 2022 at 14:24
  • @vitaly-t While it's currently true, the inability of accessing keys cannot be reasons for the inability of cloning (i.e. logically unrelated). Commented Nov 30, 2022 at 14:31
  • 1
    Why would one want to clone a data structure which the garbage collector takes care of and where the latter maybe is one big argument for the existence of WeakMap and WeakSet? Every clone duplicates the references which might make it more complicated for a clean-up mechanism to figure out which references are really "dead". Commented Nov 30, 2022 at 14:45
  • 1
    I said accessing keys and cloning are “logically unrelated” because cloning can be done without letting the user access the keys and values and exposing when the GC collected what. The two concepts are simply different, not related. Commented Nov 30, 2022 at 19:34

2 Answers 2

5

Why are WeakMap/WeakSet not "cloneable"?

WeakMaps and WeakSets 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()]);

Sign up to request clarification or add additional context in comments.

2 Comments

"Cloning it is not timing-attack-proof and thus violates the ECMAScript specification".... This, I haven't thought of, and it's a pretty fair point! Thank you for your insights.
I'm not sure a timing attack vector is the real reason why it's not possible. I think it's simply because WeakMap/WeakSet were added before WeakRef. And they didn't want to expose the inner workings of the garbage collector too much. After all, an attacker with access to your WeakMap can already override the set method and observe any parameters passed to it.
0

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")

2 Comments

to whoever minused 1 on my post, while it is not ideal question this is a working answer
If you store the keys in a separate Map then there will always be a strong reference to that key (i.e. it will never be garbage-collected), so this kind of defeats the purpose of using a WeakMap in the first place.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.