From 30d052db99645d7fbf593e6d8dab3ca37e3e4939 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sat, 17 Jan 2026 15:57:43 +0800 Subject: [PATCH] Fix potential use-after-free with PerformanceObserver. TL;DR - use page.arena instead of page.call_arena This probably comes from copying the implementation of MutationObserver and/or IntersectionObserver. But those dispatches are different in that they directly dispatch a slice (e.g. of MutationRecords) which gets mapped to a v8::Array when doing the callback. The MutationRecords exist on the heap, not in _pending_records, so the call_arena is fine. PerformanceObserver returns an Zig object, not a slice. Therefore it gets mapped to a v8::Object which references the Zig object. The state of that object, the _entries list, has to exist for the lifetime of that object, not the call_arena. --- .../mutation_observer/mutation_observer.html | 25 +++++++++++++++++ .../performance_observer.html | 28 +++++++++++++++++++ src/browser/webapi/PerformanceObserver.zig | 4 ++- 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/browser/tests/mutation_observer/mutation_observer.html b/src/browser/tests/mutation_observer/mutation_observer.html index 76b95bb7..b80c37c2 100644 --- a/src/browser/tests/mutation_observer/mutation_observer.html +++ b/src/browser/tests/mutation_observer/mutation_observer.html @@ -112,3 +112,28 @@ }); }); + + diff --git a/src/browser/tests/performance_observer/performance_observer.html b/src/browser/tests/performance_observer/performance_observer.html index 11c82f23..f0d48ea9 100644 --- a/src/browser/tests/performance_observer/performance_observer.html +++ b/src/browser/tests/performance_observer/performance_observer.html @@ -36,3 +36,31 @@ performance.mark("operationEnd", { startTime: 34.0 }); } + + diff --git a/src/browser/webapi/PerformanceObserver.zig b/src/browser/webapi/PerformanceObserver.zig index c23a559d..2da5ab3e 100644 --- a/src/browser/webapi/PerformanceObserver.zig +++ b/src/browser/webapi/PerformanceObserver.zig @@ -126,7 +126,9 @@ pub fn disconnect(self: *PerformanceObserver, page: *Page) void { /// Returns the current list of PerformanceEntry objects /// stored in the performance observer, emptying it out. pub fn takeRecords(self: *PerformanceObserver, page: *Page) ![]*Performance.Entry { - const records = try page.call_arena.dupe(*Performance.Entry, self._entries.items); + // Use page.arena instead of call_arena because this slice is wrapped in EntryList + // and may be accessed later. + const records = try page.arena.dupe(*Performance.Entry, self._entries.items); self._entries.clearRetainingCapacity(); return records; }