// |jit-test| allow-unhandlable-oom // weakmap marking tests that use the testing mark queue to force an ordering // of marking. // We are carefully controlling the sequence of GC events. gczeal(0); // If a command-line parameter is given, use it as a substring restriction on // the tests to run. var testRestriction = scriptArgs[0]; printErr(`testRestriction is ${testRestriction || '(run all tests)'}`); function runtest(func) { if (testRestriction && ! func.name.includes(testRestriction)) { print("\Skipping " + func.name); } else { print("\nRunning " + func.name); func(); } } function reportMarks(prefix = "") { const marks = getMarks(); const current = currentgc(); const markstr = marks.join("/"); print(`${prefix}[${current.incrementalState}/${current.sweepGroup}@${current.queuePos}] ${markstr}`); return markstr; } function startGCMarking() { startgc(100000); while (gcstate() === "Prepare") { gcslice(100000); } } function purgeKey() { const m = new WeakMap(); const vals = {}; vals.key = Object.create(null); vals.val = Object.create(null); m.set(vals.key, vals.val); minorgc(); addMarkObservers([m, vals.key, vals.val]); enqueueMark(m); enqueueMark("yield"); enqueueMark(vals.key); enqueueMark("yield"); vals.key = vals.val = null; startGCMarking(); // getMarks() returns map/key/value assertEq(getMarks().join("/"), "black/unmarked/unmarked", "marked the map black"); gcslice(100000); assertEq(getMarks().join("/"), "black/black/unmarked", "key is now marked"); // Trigger purgeWeakKey: the key is in weakkeys (because it was unmarked when // the map was marked), and now we're removing it. m.delete(nondeterministicGetWeakMapKeys(m)[0]); finishgc(); // Finish the GC assertEq(getMarks().join("/"), "black/black/black", "at end, value is marked too"); clearMarkQueue(); clearMarkObservers(); } if (this.enqueueMark) runtest(purgeKey); function removeKey() { reportMarks("removeKey start: "); const m = new WeakMap(); const vals = {}; vals.key = Object.create(null); vals.val = Object.create(null); m.set(vals.key, vals.val); minorgc(); addMarkObservers([m, vals.key, vals.val]); enqueueMark(m); enqueueMark("yield"); startGCMarking(); reportMarks("first: "); var marks = getMarks(); assertEq(marks[0], "black", "map is black"); assertEq(marks[1], "unmarked", "key not marked yet"); assertEq(marks[2], "unmarked", "value not marked yet"); m.delete(vals.key); finishgc(); // Finish the GC reportMarks("done: "); marks = getMarks(); assertEq(marks[0], "black", "map is black"); assertEq(marks[1], "black", "key is black"); assertEq(marks[2], "black", "value is black"); // Do it again, but this time, remove all other roots. m.set(vals.key, vals.val); vals.key = vals.val = null; startgc(10000); while (gcstate() !== "Mark") { gcslice(100000); } marks = getMarks(); assertEq(marks[0], "black", "map is black"); assertEq(marks[1], "unmarked", "key not marked yet"); assertEq(marks[2], "unmarked", "value not marked yet"); // This was meant to test the weakmap deletion barrier, which would remove // the key from weakkeys. Unfortunately, JS-exposed WeakMaps now have a read // barrier on lookup that marks the key, and deletion requires a lookup. m.delete(nondeterministicGetWeakMapKeys(m)[0]); finishgc(); marks = getMarks(); assertEq(marks[0], "black", "map is black"); assertEq(marks[1], "black", "key was blackened by lookup read barrier during deletion"); assertEq(marks[2], "black", "value is black because map and key are black"); clearMarkQueue(); clearMarkObservers(); } if (this.enqueueMark) runtest(removeKey); // Test: // 1. mark the map // - that inserts the delegate into weakKeys // 2. nuke the CCW key // - removes the delegate from weakKeys // 3. mark the key // 4. enter weak marking mode // // The problem it's attempting to recreate is that entering weak marking mode // will no longer mark the value, because there's no delegate to trigger it, // and the key was not added to weakKeys (because at the time the map was // scanned, the key had a delegate, so it was added to weakKeys instead.) function nukeMarking() { const g1 = newGlobal({newCompartment: true}); const vals = {}; vals.map = new WeakMap(); vals.key = g1.eval("Object.create(null)"); vals.val = Object.create(null); vals.map.set(vals.key, vals.val); vals.val = null; gc(); // Set up the sequence of marking events. enqueueMark(vals.map); enqueueMark("yield"); // We will nuke the key's delegate here. enqueueMark(vals.key); enqueueMark("enter-weak-marking-mode"); // Okay, run through the GC now. startgc(1000000); while (gcstate() !== "Mark") { gcslice(100000); } assertEq(gcstate(), "Mark", "expected to yield after marking map"); // We should have marked the map and then yielded back here. nukeCCW(vals.key); // Finish up the GC. gcslice(); clearMarkQueue(); } if (this.enqueueMark) runtest(nukeMarking); // Similar to the above, but trying to get a different failure: // - start marking // - find a map, add its key to ephemeronEdges // - nuke the key (and all other CCWs between the key -> delegate zones) // - when sweeping, we will no longer have any edges between the key // and delegate zones. So they will be placed in separate sweep groups. // - for this test, the delegate zone must be swept after the key zone // - make sure we don't try to mark back in the key zone (due to an // ephemeron edge) while sweeping the delegate zone. In a DEBUG build, // this would assert. function nukeMarkingSweepGroups() { // Create g1 before host, because that will result in the right zone // ordering to trigger the bug. const g1 = newGlobal({newCompartment: true}); const host = newGlobal({newCompartment: true}); host.g1 = g1; host.eval(` const vals = {}; vals.map = new WeakMap(); vals.key = g1.eval("Object.create(null)"); vals.val = Object.create(null); vals.map.set(vals.key, vals.val); vals.val = null; gc(); // Set up the sequence of marking events. enqueueMark(vals.map); enqueueMark("yield"); // We will nuke the key's delegate here. enqueueMark(vals.key); enqueueMark("enter-weak-marking-mode"); // Okay, run through the GC now. startgc(1); while (gcstate() === "Prepare") { gcslice(100000); } assertEq(gcstate(), "Mark", "expected to yield after marking map"); // We should have marked the map and then yielded back here. nukeAllCCWs(); // Finish up the GC. while (gcstate() === "Mark") { gcslice(1000); } gcslice(); clearMarkQueue(); `); } if (this.enqueueMark) runtest(nukeMarkingSweepGroups); function transplantMarking() { const g1 = newGlobal({newCompartment: true}); const vals = {}; vals.map = new WeakMap(); let {object, transplant} = transplantableObject(); vals.key = object; object = null; vals.val = Object.create(null); vals.map.set(vals.key, vals.val); vals.val = null; gc(); // Set up the sequence of marking events. enqueueMark(vals.map); enqueueMark("yield"); // We will transplant the key here. enqueueMark(vals.key); enqueueMark("enter-weak-marking-mode"); // Okay, run through the GC now. startgc(1000000); while (gcstate() !== "Mark") { gcslice(100000); } assertEq(gcstate(), "Mark", "expected to yield after marking map"); // We should have marked the map and then yielded back here. transplant(g1); // Finish up the GC. gcslice(); clearMarkQueue(); } if (this.enqueueMark) runtest(transplantMarking); // 1. Mark the map // => add delegate to weakKeys // 2. Mark the delegate black // => do nothing because we are not in weak marking mode // 3. Mark the key gray // => mark value gray, not that we really care // 4. Enter weak marking mode // => black delegate darkens the key from gray to black function grayMarkingMapFirst() { const g = newGlobal({newCompartment: true}); const vals = {}; vals.map = new WeakMap(); vals.key = g.eval("Object.create(null)"); vals.val = Object.create(null); vals.map.set(vals.key, vals.val); g.delegate = vals.key; g.eval("dummy = Object.create(null)"); g.eval("grayRoot().push(delegate, dummy)"); addMarkObservers([vals.map, vals.key]); g.addMarkObservers([vals.key, g.dummy]); addMarkObservers([vals.val]); gc(); enqueueMark(vals.map); enqueueMark("yield"); // checkpoint 1 g.enqueueMark(vals.key); enqueueMark("yield"); // checkpoint 2 vals.val = null; vals.key = null; g.delegate = null; g.dummy = null; const showmarks = () => { print("[map,key,delegate,graydummy,value] marked " + JSON.stringify(getMarks())); }; print("Starting incremental GC"); startGCMarking(); // Checkpoint 1, after marking map showmarks(); var marks = getMarks(); assertEq(marks[0], "black", "map is black"); assertEq(marks[1], "unmarked", "key is not marked yet"); assertEq(marks[2], "unmarked", "delegate is not marked yet"); gcslice(100000); // Checkpoint 2, after marking delegate showmarks(); marks = getMarks(); assertEq(marks[0], "black", "map is black"); assertEq(marks[1], "unmarked", "key is not marked yet"); assertEq(marks[2], "black", "delegate is black"); gcslice(); // GC complete. Key was marked black (b/c of delegate), then gray marking saw // it was already black and skipped it. showmarks(); marks = getMarks(); assertEq(marks[0], "black", "map is black"); assertEq(marks[1], "black", "delegate marked key black because of weakmap"); assertEq(marks[2], "black", "delegate is still black"); assertEq(marks[3], "gray", "basic gray marking is working"); assertEq(marks[4], "black", "black map + black delegate => black value"); clearMarkQueue(); clearMarkObservers(); grayRoot().length = 0; g.eval("grayRoot().length = 0"); } if (this.enqueueMark) runtest(grayMarkingMapFirst); function grayMarkingMapLast() { const g = newGlobal({newCompartment: true}); const vals = {}; vals.map = new WeakMap(); vals.key = g.eval("Object.create(null)"); vals.val = Object.create(null); vals.map.set(vals.key, vals.val); vals.map2 = new WeakMap(); vals.key2 = g.eval("Object.create(null)"); vals.val2 = Object.create(null); vals.map2.set(vals.key2, vals.val2); g.delegate = vals.key; g.eval("grayRoot().push(delegate)"); addMarkObservers([vals.map, vals.key]); g.addMarkObservers([vals.key]); addMarkObservers([vals.val]); grayRoot().push(vals.key2); addMarkObservers([vals.map2, vals.key2]); g.addMarkObservers([vals.key2]); addMarkObservers([vals.val2]); const labels = ["map", "key", "delegate", "value", "map2", "key2", "delegate2", "value2"]; gc(); g.enqueueMark(vals.key); g.enqueueMark(vals.key2); enqueueMark("yield"); // checkpoint 1 vals.val = null; vals.key = null; g.delegate = null; vals.map2 = null; // Important! Second map is never marked, keeps nothing alive. vals.key2 = null; vals.val2 = null; g.delegate2 = null; const labeledMarks = () => { const info = {}; const marks = getMarks(); for (let i = 0; i < labels.length; i++) info[labels[i]] = marks[i]; return info; }; const showmarks = () => { print("Marks:"); for (const [label, mark] of Object.entries(labeledMarks())) print(` ${label}: ${mark}`); }; print("Starting incremental GC"); startGCMarking(); // Checkpoint 1, after marking key showmarks(); var marks = labeledMarks(); assertEq(marks.map, "unmarked", "map is unmarked"); assertEq(marks.key, "unmarked", "key is not marked yet"); assertEq(marks.delegate, "black", "delegate is black"); assertEq(marks.map2, "unmarked", "map2 is unmarked"); assertEq(marks.key2, "unmarked", "key2 is not marked yet"); assertEq(marks.delegate2, "black", "delegate2 is black"); gcslice(); // GC complete. When entering weak marking mode, black delegate propagated to // key. showmarks(); marks = labeledMarks(); assertEq(marks.map, "black", "map is black"); assertEq(marks.key, "black", "delegate marked key black because of weakmap"); assertEq(marks.delegate, "black", "delegate is still black"); assertEq(marks.value, "black", "black map + black delegate => black value"); assertEq(marks.map2, "dead", "map2 is dead"); assertEq(marks.key2, "gray", "key2 marked gray, map2 had no effect"); assertEq(marks.delegate2, "black", "delegate artificially marked black via mark queue"); assertEq(marks.value2, "dead", "dead map + black delegate => dead value"); clearMarkQueue(); clearMarkObservers(); grayRoot().length = 0; g.eval("grayRoot().length = 0"); return vals; // To prevent the JIT from optimizing out vals. } if (this.enqueueMark) runtest(grayMarkingMapLast); function grayMapKey() { const vals = {}; vals.m = new WeakMap(); vals.key = Object.create(null); vals.val = Object.create(null); vals.m.set(vals.key, vals.val); // Maps are allocated black, so we won't be able to mark it gray during the // first GC. gc(); addMarkObservers([vals.m, vals.key, vals.val]); // Wait until we can mark gray (ie, sweeping). Mark the map gray and yield. // This should happen all in one slice. enqueueMark("set-color-gray"); enqueueMark(vals.m); enqueueMark("unset-color"); enqueueMark("yield"); // Make the weakmap no longer reachable from the roots, so we can mark it // gray. vals.m = null; enqueueMark(vals.key); enqueueMark("yield"); vals.key = vals.val = null; startGCMarking(); assertEq(getMarks().join("/"), "gray/unmarked/unmarked", "marked the map gray"); gcslice(100000); assertEq(getMarks().join("/"), "gray/black/unmarked", "key is now marked black"); finishgc(); // Finish the GC assertEq(getMarks().join("/"), "gray/black/gray", "at end: black/gray => gray"); clearMarkQueue(); clearMarkObservers(); } if (this.enqueueMark) runtest(grayMapKey); function grayKeyMap() { const vals = {}; vals.m = new WeakMap(); vals.key = Object.create(null); vals.val = Object.create(null); vals.m.set(vals.key, vals.val); addMarkObservers([vals.m, vals.key, vals.val]); enqueueMark(vals.key); enqueueMark("yield"); // Wait until we are gray marking. enqueueMark("set-color-gray"); enqueueMark(vals.m); enqueueMark("unset-color"); enqueueMark("yield"); enqueueMark("set-color-black"); enqueueMark(vals.m); enqueueMark("unset-color"); // Make the weakmap no longer reachable from the roots, so we can mark it // gray. vals.m = null; vals.key = vals.val = null; // Only mark this zone, to avoid interference from other tests that may have // created additional zones. schedulezone(vals); startGCMarking(); // getMarks() returns map/key/value reportMarks("1: "); assertEq(getMarks().join("/"), "unmarked/black/unmarked", "marked key black"); // We always yield before sweeping (in the absence of zeal), so we will see // the unmarked state another time. gcslice(100000); reportMarks("2: "); assertEq(getMarks().join("/"), "unmarked/black/unmarked", "marked key black, yield before sweeping"); gcslice(100000); reportMarks("3: "); assertEq(getMarks().join("/"), "gray/black/gray", "marked the map gray, which marked the value when map scanned"); finishgc(); // Finish the GC reportMarks("4: "); assertEq(getMarks().join("/"), "black/black/black", "further marked the map black, so value should also be blackened"); clearMarkQueue(); clearMarkObservers(); } if (this.enqueueMark) runtest(grayKeyMap); // Cause a key to be marked black *during gray marking*, by first marking a // delegate black, then marking the map and key gray. When the key is scanned, // it should be seen to be a CCW of a black delegate and so should itself be // marked black. // // The bad behavior being prevented is: // // 1. You wrap an object in a CCW and use it as a weakmap key to some // information. // 2. You keep a strong reference to the object (in its compartment). // 3. The only references to the CCW are gray, and are in fact part of a cycle. // 4. The CC runs and discards the CCW. // 5. You look up the object in the weakmap again. This creates a new wrapper // to use as a key. It is not in the weakmap, so the information you stored // before is not found. (It may have even been collected, if you had no // other references to it.) // function blackDuringGray() { const g = newGlobal({newCompartment: true}); const vals = {}; vals.map = new WeakMap(); vals.key = g.eval("Object.create(null)"); vals.val = Object.create(null); vals.map.set(vals.key, vals.val); g.delegate = vals.key; addMarkObservers([vals.map, vals.key]); g.addMarkObservers([vals.key]); addMarkObservers([vals.val]); // Mark observers: map, key, delegate, value gc(); g.enqueueMark(vals.key); // Mark the delegate black enqueueMark("yield"); // checkpoint 1 // Mark the map gray. This will scan through all entries, find our key, and // mark it black because its delegate is black. enqueueMark("set-color-gray"); enqueueMark(vals.map); // Mark the map gray vals.map = null; vals.val = null; vals.key = null; g.delegate = null; const showmarks = () => { print("[map,key,delegate,value] marked " + JSON.stringify(getMarks())); }; print("Starting incremental GC"); startGCMarking(); // Checkpoint 1, after marking delegate black showmarks(); var marks = getMarks(); assertEq(marks[0], "unmarked", "map is not marked yet"); assertEq(marks[1], "unmarked", "key is not marked yet"); assertEq(marks[2], "black", "delegate is black"); assertEq(marks[3], "unmarked", "values is not marked yet"); finishgc(); showmarks(); marks = getMarks(); assertEq(marks[0], "gray", "map is gray"); assertEq(marks[1], "gray", "gray map + black delegate should mark key gray"); assertEq(marks[2], "black", "delegate is still black"); assertEq(marks[3], "gray", "gray map + gray key => gray value"); clearMarkQueue(); clearMarkObservers(); grayRoot().length = 0; g.eval("grayRoot().length = 0"); } if (this.enqueueMark) runtest(blackDuringGray); // Same as above, except relying on the implicit edge from delegate -> key. function blackDuringGrayImplicit() { const g = newGlobal({newCompartment: true}); const vals = {}; vals.map = new WeakMap(); vals.key = g.eval("Object.create(null)"); vals.val = Object.create(null); vals.map.set(vals.key, vals.val); g.delegate = vals.key; addMarkObservers([vals.map, vals.key]); g.addMarkObservers([vals.key]); addMarkObservers([vals.val]); // Mark observers: map, key, delegate, value gc(); // Mark the map gray. This will scan through all entries, find our key, and // add implicit edges from delegate -> key and delegate -> value. enqueueMark("set-color-gray"); enqueueMark(vals.map); // Mark the map gray enqueueMark("yield"); // checkpoint 1 enqueueMark("set-color-black"); g.enqueueMark(vals.key); // Mark the delegate black, propagating to key. vals.map = null; vals.val = null; vals.key = null; g.delegate = null; const showmarks = () => { print("[map,key,delegate,value] marked " + JSON.stringify(getMarks())); }; print("Starting incremental GC"); startGCMarking(); // Checkpoint 1, after marking map gray showmarks(); var marks = getMarks(); assertEq(marks[0], "gray", "map is gray"); assertEq(marks[1], "unmarked", "key is not marked yet"); assertEq(marks[2], "unmarked", "delegate is not marked yet"); assertEq(marks[3], "unmarked", "value is not marked yet"); finishgc(); showmarks(); marks = getMarks(); assertEq(marks[0], "gray", "map is gray"); assertEq(marks[1], "gray", "gray map + black delegate should mark key gray"); assertEq(marks[2], "black", "delegate is black"); assertEq(marks[3], "gray", "gray map + gray key => gray value via delegate -> value"); clearMarkQueue(); clearMarkObservers(); grayRoot().length = 0; g.eval("grayRoot().length = 0"); } if (this.enqueueMark) runtest(blackDuringGrayImplicit);