LibWeb: Fix use-after-free in live-collection filter captures
Problem: Holding form.elements while the form is detached + dropped hit a use-after-free: the form is GC’ed while the collection’s still live. Cause: HTMLCollection (and LiveNodeList too) was storing its filter as an AK::Function — which the garbage collector doesn’t visit. When a filter lambda captures a GC object (e.g. the form in form.elements) that object has no GC edge keeping it alive. So it can be collected while the collection using it’s still reachable — leaving a dangling pointer. Fix: HTMLCollection and LiveNodeList are GC cells with their own visit_edges. So, visit the filter’s (and sort’s) capture range there: conservatively mark any GC object a captured lambda holds — to ensure it’s kept alive as long as the collection’s reachable. Fixes https://github.com/LadybirdBrowser/ladybird/issues/9948
This commit is contained in:
parent
9a99b54036
commit
efa9388adc
4 changed files with 35 additions and 0 deletions
|
|
@ -50,6 +50,8 @@ void HTMLCollection::visit_edges(Cell::Visitor& visitor)
|
|||
{
|
||||
Base::visit_edges(visitor);
|
||||
visitor.visit(m_root);
|
||||
visitor.visit_possible_values(m_filter.raw_capture_range());
|
||||
visitor.visit_possible_values(m_sort.raw_capture_range());
|
||||
}
|
||||
|
||||
GC::Cell const& HTMLCollection::owner_cell(Badge<GC::Heap>) const
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ void LiveNodeList::visit_edges(Cell::Visitor& visitor)
|
|||
{
|
||||
Base::visit_edges(visitor);
|
||||
visitor.visit(m_root);
|
||||
visitor.visit_possible_values(m_filter.raw_capture_range());
|
||||
}
|
||||
|
||||
GC::RootVector<Node*> LiveNodeList::collection() const
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
form alive while collection is held: true
|
||||
collection.length: 0
|
||||
30
Tests/LibWeb/Text/input/form-elements-keeps-form-alive.html
Normal file
30
Tests/LibWeb/Text/input/form-elements-keeps-form-alive.html
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<!DOCTYPE html>
|
||||
<script src="include.js"></script>
|
||||
<body></body>
|
||||
<script>
|
||||
// Regression test: a live HTMLFormControlsCollection captures its form in the filter, so the form must stay
|
||||
// alive while the collection is reachable. Previously the form was garbage-collected out from under the live
|
||||
// collection — leaving a dangling pointer in the filter (a use-after-free). See issue #9948.
|
||||
asyncTest(done => {
|
||||
let form = document.createElement("form");
|
||||
form.appendChild(document.createElement("input"));
|
||||
document.body.appendChild(form);
|
||||
|
||||
let collection = form.elements;
|
||||
let weakForm = new WeakRef(form);
|
||||
form.remove();
|
||||
form = null;
|
||||
|
||||
setTimeout(() => {
|
||||
internals.gc();
|
||||
// Churn the heap to dislodge any stale references, then collect again.
|
||||
for (let i = 0; i < 10000; ++i)
|
||||
document.body.appendChild(document.createElement("div"));
|
||||
internals.gc();
|
||||
|
||||
println(`form alive while collection is held: ${weakForm.deref() !== undefined}`);
|
||||
println(`collection.length: ${collection.length}`);
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
</script>
|
||||
Loading…
Reference in a new issue