From ad3c1ab54438f924e55d6727caad0c24d70f5d3d Mon Sep 17 00:00:00 2001 From: Ahmad Saleem Date: Tue, 30 Jun 2026 01:45:47 -0700 Subject: [PATCH 01/84] Remove redundant m_renderRange.start() null checks in RenderHighlight::highlightStateForRenderer https://bugs.webkit.org/show_bug.cgi?id=318175 rdar://180977788 Reviewed by Alan Baradlay. The branch is entered only when `&renderer == m_renderRange.start()`. Since renderer is a reference, &renderer is never null, so m_renderRange.start() is guaranteed non-null inside the block. The two further m_renderRange.start() checks were therefore always true: the leading term of the Both test and the guard on the Start return. Drop both dead checks. The Start return becomes the unconditional fall-through after the Both case, and the end() null check is retained since end() can legitimately be null. No change in behavior. * Source/WebCore/rendering/RenderHighlight.cpp: (WebCore::RenderHighlight::highlightStateForRenderer): Canonical link: https://commits.webkit.org/316115@main --- Source/WebCore/rendering/RenderHighlight.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Source/WebCore/rendering/RenderHighlight.cpp b/Source/WebCore/rendering/RenderHighlight.cpp index 6e0f15e254b0..b35199bcaef8 100644 --- a/Source/WebCore/rendering/RenderHighlight.cpp +++ b/Source/WebCore/rendering/RenderHighlight.cpp @@ -126,10 +126,9 @@ RenderObject::HighlightState RenderHighlight::highlightStateForRenderer(const Re return renderer.selectionState(); if (&renderer == m_renderRange.start()) { - if (m_renderRange.start() && m_renderRange.end() && m_renderRange.start() == m_renderRange.end()) + if (m_renderRange.end() && m_renderRange.start() == m_renderRange.end()) return RenderObject::HighlightState::Both; - if (m_renderRange.start()) - return RenderObject::HighlightState::Start; + return RenderObject::HighlightState::Start; } if (&renderer == m_renderRange.end()) return RenderObject::HighlightState::End; From 144aa0db1a51ac0b8a993f17bd35a136fb4a6b4a Mon Sep 17 00:00:00 2001 From: Mike Wyrzykowski Date: Tue, 30 Jun 2026 01:47:46 -0700 Subject: [PATCH 02/84] [GPUP] WebGPU BindGroup Validation-Cache Key Collision Leads to GPU OOB Write https://bugs.webkit.org/show_bug.cgi?id=313360 rdar://174662781 Reviewed by Dan Glastonbury. Share identifiers for render and compute pipelines to avoid validation succeeding for a given bind group + render pipeline which was validated for the hash key identical to the same bind group but with a compute pipeline which has the same key. * Source/WebGPU/WebGPU/ComputePipeline.mm: * Source/WebGPU/WebGPU/Device.h: * Source/WebGPU/WebGPU/RenderPipeline.mm: Originally-landed-as: 305413.739@safari-7624-branch (427fc7a6307d). rdar://180429272 Canonical link: https://commits.webkit.org/316116@main --- ...n-bindgroup-validation-bypass-expected.txt | 1 + ...collision-bindgroup-validation-bypass.html | 120 ++++++++++++++++++ Source/WebGPU/WebGPU/ComputePipeline.mm | 6 +- Source/WebGPU/WebGPU/Device.h | 3 +- Source/WebGPU/WebGPU/RenderPipeline.mm | 6 +- 5 files changed, 128 insertions(+), 8 deletions(-) create mode 100644 LayoutTests/fast/webgpu/pipeline-id-collision-bindgroup-validation-bypass-expected.txt create mode 100644 LayoutTests/fast/webgpu/pipeline-id-collision-bindgroup-validation-bypass.html diff --git a/LayoutTests/fast/webgpu/pipeline-id-collision-bindgroup-validation-bypass-expected.txt b/LayoutTests/fast/webgpu/pipeline-id-collision-bindgroup-validation-bypass-expected.txt new file mode 100644 index 000000000000..9490d44ca416 --- /dev/null +++ b/LayoutTests/fast/webgpu/pipeline-id-collision-bindgroup-validation-bypass-expected.txt @@ -0,0 +1 @@ +PASS: validation error raised for undersized buffer in render pass after compute cache priming CONTROL: PASS: validation error raised for fresh BindGroup with undersized buffer diff --git a/LayoutTests/fast/webgpu/pipeline-id-collision-bindgroup-validation-bypass.html b/LayoutTests/fast/webgpu/pipeline-id-collision-bindgroup-validation-bypass.html new file mode 100644 index 000000000000..0fba47cb94d6 --- /dev/null +++ b/LayoutTests/fast/webgpu/pipeline-id-collision-bindgroup-validation-bypass.html @@ -0,0 +1,120 @@ + + + diff --git a/Source/WebGPU/WebGPU/ComputePipeline.mm b/Source/WebGPU/WebGPU/ComputePipeline.mm index 0d5378fb51c5..d3923a0a687e 100644 --- a/Source/WebGPU/WebGPU/ComputePipeline.mm +++ b/Source/WebGPU/WebGPU/ComputePipeline.mm @@ -128,7 +128,7 @@ if (!size.width || size.width > deviceLimits.maxComputeWorkgroupSizeX || !size.height || size.height > deviceLimits.maxComputeWorkgroupSizeY || !size.depth || size.depth > deviceLimits.maxComputeWorkgroupSizeZ || size.width * size.height * size.depth > deviceLimits.maxComputeInvocationsPerWorkgroup) return returnInvalidComputePipeline(*this, isAsync); - if (m_computePipelineId == Device::maxPipelines) { + if (m_pipelineId == Device::maxPipelines) { loseTheDevice(WGPUDeviceLostReason_Undefined); return returnInvalidComputePipeline(*this, isAsync, @"too many compute pipelines"); } @@ -149,14 +149,14 @@ if (!computePipelineState) return returnFailedPSOCreation(); - return std::make_pair(ComputePipeline::create(computePipelineState, WTF::move(generatedPipelineLayout), size, WTF::move(minimumBufferSizes), ++m_computePipelineId, *this), nil); + return std::make_pair(ComputePipeline::create(computePipelineState, WTF::move(generatedPipelineLayout), size, WTF::move(minimumBufferSizes), ++m_pipelineId, *this), nil); } auto computePipelineState = createComputePipelineState(m_device, function, pipelineLayout, size, label.get(), shaderValidationState(), WTF::move(shaderSource)); if (!computePipelineState) return returnFailedPSOCreation(); - return std::make_pair(ComputePipeline::create(computePipelineState, WTF::move(pipelineLayout), size, WTF::move(minimumBufferSizes), ++m_computePipelineId, *this), nil); + return std::make_pair(ComputePipeline::create(computePipelineState, WTF::move(pipelineLayout), size, WTF::move(minimumBufferSizes), ++m_pipelineId, *this), nil); } void Device::createComputePipelineAsync(const WGPUComputePipelineDescriptor& descriptor, CompletionHandler&&, String&& message)>&& callback) diff --git a/Source/WebGPU/WebGPU/Device.h b/Source/WebGPU/WebGPU/Device.h index 8f87a4639a56..66aa24e815d5 100644 --- a/Source/WebGPU/WebGPU/Device.h +++ b/Source/WebGPU/WebGPU/Device.h @@ -335,8 +335,7 @@ class Device : public WGPUDeviceImpl, public ThreadSafeRefCountedAndCanMakeThrea mutable HashSet, WTF::UnsignedWithZeroKeyHashTraits> m_bindGroupCompatibilityCache; uint64_t m_commandEncoderId { 0 }; uint64_t m_pipelineLayoutId { 0 }; - uint64_t m_renderPipelineId { 0 }; - uint64_t m_computePipelineId { 0 }; + uint64_t m_pipelineId { 0 }; uint32_t m_bindGroupLayoutId { 0 }; uint32_t m_bindGroupId { 0 }; uint32_t m_appleGPUFamily { 0 }; diff --git a/Source/WebGPU/WebGPU/RenderPipeline.mm b/Source/WebGPU/WebGPU/RenderPipeline.mm index 599d0f0efbc9..8e3e83bcae6c 100644 --- a/Source/WebGPU/WebGPU/RenderPipeline.mm +++ b/Source/WebGPU/WebGPU/RenderPipeline.mm @@ -1753,7 +1753,7 @@ static uint32_t NODELETE componentsForDataType(MTLDataType dataType) if (error) return returnInvalidRenderPipeline(*this, isAsync, error.localizedDescription); - if (m_renderPipelineId == Device::maxPipelines) { + if (m_pipelineId == Device::maxPipelines) { loseTheDevice(WGPUDeviceLostReason_Undefined); return returnInvalidRenderPipeline(*this, isAsync, @"too many render pipelines"); } @@ -1762,10 +1762,10 @@ static uint32_t NODELETE componentsForDataType(MTLDataType dataType) if (!generatedPipelineLayout->isValid()) return returnInvalidRenderPipeline(*this, isAsync, "Generated pipeline layout is not valid"_s); - return std::make_pair(RenderPipeline::create(mtlPrimitiveType, mtlIndexType, mtlFrontFace, mtlCullMode, mtlDepthClipMode, depthStencilDescriptor, WTF::move(generatedPipelineLayout), depthBias, depthBiasSlopeScale, depthBiasClamp, sampleMask, mtlRenderPipelineDescriptor, colorAttachmentCount, descriptor, WTF::move(requiredBufferIndices), WTF::move(minimumBufferSizes), ++m_renderPipelineId, vertexShaderBindingCount, *this), nil); + return std::make_pair(RenderPipeline::create(mtlPrimitiveType, mtlIndexType, mtlFrontFace, mtlCullMode, mtlDepthClipMode, depthStencilDescriptor, WTF::move(generatedPipelineLayout), depthBias, depthBiasSlopeScale, depthBiasClamp, sampleMask, mtlRenderPipelineDescriptor, colorAttachmentCount, descriptor, WTF::move(requiredBufferIndices), WTF::move(minimumBufferSizes), ++m_pipelineId, vertexShaderBindingCount, *this), nil); } - return std::make_pair(RenderPipeline::create(mtlPrimitiveType, mtlIndexType, mtlFrontFace, mtlCullMode, mtlDepthClipMode, depthStencilDescriptor, const_cast(*pipelineLayout), depthBias, depthBiasSlopeScale, depthBiasClamp, sampleMask, mtlRenderPipelineDescriptor, colorAttachmentCount, descriptor, WTF::move(requiredBufferIndices), WTF::move(minimumBufferSizes), ++m_renderPipelineId, vertexShaderBindingCount, *this), nil); + return std::make_pair(RenderPipeline::create(mtlPrimitiveType, mtlIndexType, mtlFrontFace, mtlCullMode, mtlDepthClipMode, depthStencilDescriptor, const_cast(*pipelineLayout), depthBias, depthBiasSlopeScale, depthBiasClamp, sampleMask, mtlRenderPipelineDescriptor, colorAttachmentCount, descriptor, WTF::move(requiredBufferIndices), WTF::move(minimumBufferSizes), ++m_pipelineId, vertexShaderBindingCount, *this), nil); } void Device::createRenderPipelineAsync(const WGPURenderPipelineDescriptor& descriptor, CompletionHandler&&, String&& message)>&& callback) From b6908339696324c13c05000979821df430e2cefa Mon Sep 17 00:00:00 2001 From: Zak Ridouh Date: Tue, 30 Jun 2026 02:01:03 -0700 Subject: [PATCH 03/84] Uninitialized-heap disclosure in convertImagePixelsFromFloat16ToFloat16 via color-space early-return Reviewed by Gerald Squelart. convertImagePixelsFromFloat16ToFloat16() early-returned on a color-space mismatch without writing the destination. The destination buffer is allocated via JSC::Float16Array::tryCreateUninitialized() and ImageBufferBackend:: getPixelBuffer() only zero-fills on a size mismatch, so an RGBA16F getPixelBuffer() readback whose output color space differed from the backing IOSurface's left the destination populated with uninitialized Gigacage::Primitive heap. RemoteImageBuffer::getPixelBufferWithNewMemory() then memcpy'd those bytes into WebContent-mapped shared memory causing a WebContent<->GPUProcess heap disclosure. Float16<->Float16 color-space conversion is unimplemented (the 8-bit sibling path also doesn't perform it). Remove the early return so the destination is always populated with the source pixels (alpha-format conversion applied), matching the sibling's behavior and closing the disclosure. Test: ipc/rgba16f-getpixelbuffer-colorspace-mismatch-heap-disclosure.html * LayoutTests/ipc/rgba16f-getpixelbuffer-colorspace-mismatch-heap-disclosure-expected.txt: Added. * LayoutTests/ipc/rgba16f-getpixelbuffer-colorspace-mismatch-heap-disclosure.html: Added. * Source/WebCore/platform/graphics/PixelBufferConversion.cpp: (WebCore::convertImagePixelsFromFloat16ToFloat16): Originally-landed-as: 305413.933@safari-7624-branch (e7a0eda7b7ff). rdar://180428968 Canonical link: https://commits.webkit.org/316117@main --- ...pace-mismatch-heap-disclosure-expected.txt | 2 + ...r-colorspace-mismatch-heap-disclosure.html | 120 ++++++++++++++++++ .../graphics/PixelBufferConversion.cpp | 5 +- 3 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 LayoutTests/ipc/rgba16f-getpixelbuffer-colorspace-mismatch-heap-disclosure-expected.txt create mode 100644 LayoutTests/ipc/rgba16f-getpixelbuffer-colorspace-mismatch-heap-disclosure.html diff --git a/LayoutTests/ipc/rgba16f-getpixelbuffer-colorspace-mismatch-heap-disclosure-expected.txt b/LayoutTests/ipc/rgba16f-getpixelbuffer-colorspace-mismatch-heap-disclosure-expected.txt new file mode 100644 index 000000000000..69cfc5a98db7 --- /dev/null +++ b/LayoutTests/ipc/rgba16f-getpixelbuffer-colorspace-mismatch-heap-disclosure-expected.txt @@ -0,0 +1,2 @@ +PASS + diff --git a/LayoutTests/ipc/rgba16f-getpixelbuffer-colorspace-mismatch-heap-disclosure.html b/LayoutTests/ipc/rgba16f-getpixelbuffer-colorspace-mismatch-heap-disclosure.html new file mode 100644 index 000000000000..960a47394d31 --- /dev/null +++ b/LayoutTests/ipc/rgba16f-getpixelbuffer-colorspace-mismatch-heap-disclosure.html @@ -0,0 +1,120 @@ + +

+
diff --git a/Source/WebCore/platform/graphics/PixelBufferConversion.cpp b/Source/WebCore/platform/graphics/PixelBufferConversion.cpp
index c9974199cf08..6f8b43243b32 100644
--- a/Source/WebCore/platform/graphics/PixelBufferConversion.cpp
+++ b/Source/WebCore/platform/graphics/PixelBufferConversion.cpp
@@ -343,8 +343,9 @@ static void writeFloat16(Float16 f16, const std::span& spanFloat16, siz
 
 static void convertImagePixelsFromFloat16ToFloat16(const ConstPixelBufferConversionView& source, const PixelBufferConversionView& destination, const IntSize& destinationSize)
 {
-    if (source.format.colorSpace != destination.format.colorSpace)
-        return;
+    // FIXME: Float16-to-Float16 color-space conversion is unimplemented; fall through and copy
+    // verbatim. Do not early-return on a color-space mismatch: the destination is allocated
+    // uninitialized, so skipping the write would leak heap bytes through getPixelBuffer().
 
     auto sourceBytes = source.rows.size_bytes();
     auto sourcePixelComponents = sourceBytes / 2;

From c3ca56b3af803be584aad6498beab4fec68b6a1e Mon Sep 17 00:00:00 2001
From: Vassili Bykov 
Date: Tue, 30 Jun 2026 02:31:01 -0700
Subject: [PATCH 04/84] [JSC] DFGArgumentsEliminationPhase removeViaKill should
 reset node scan index between InlineCallFrames
 https://bugs.webkit.org/show_bug.cgi?id=314850 rdar://176966728

Reviewed by Yusuke Suzuki.

In DFGArgumentsEliminationPhase::eliminateCandidatesThatInterfere(), the removeViaKill
lambda contains two nested loops. The outer loop iterates over inline call frames. For
each frame, the inner loop is expected to iterate the nodes of the given basic block in
reverse order in the range [0, nodeIndex). However, because the inner loop mutates the
lambda parameter nodeIndex directly, this only works as intended for the first inline call
frame. Each subsequent call frame begins where the previous one left off.

The change introduces a separate iteration variable for the inner loop, which starts off
at nodeIndex for each inline call frame. nodeIndex parameter is marked as 'const' to make
explicit the expectation that it should not change.

Test: JSTests/stress/arguments-elimination-multiple-inline-call-frames.js

Originally-landed-as: 305413.912@safari-7624-branch (3270ebdb7366). rdar://180435783
Canonical link: https://commits.webkit.org/316118@main
---
 ...elimination-multiple-inline-call-frames.js | 38 +++++++++++++++++++
 .../dfg/DFGArgumentsEliminationPhase.cpp      | 28 ++++++--------
 2 files changed, 50 insertions(+), 16 deletions(-)
 create mode 100644 JSTests/stress/arguments-elimination-multiple-inline-call-frames.js

diff --git a/JSTests/stress/arguments-elimination-multiple-inline-call-frames.js b/JSTests/stress/arguments-elimination-multiple-inline-call-frames.js
new file mode 100644
index 000000000000..8a87b89cc5cd
--- /dev/null
+++ b/JSTests/stress/arguments-elimination-multiple-inline-call-frames.js
@@ -0,0 +1,38 @@
+//@ skip if $buildType == "debug"
+//@ runDefault("--useConcurrentJIT=false", "--jitPolicyScale=0", "--maximumFunctionForCallInlineCandidateBytecodeCostForFTL=500")
+
+let g = 0;
+function restY(c, ...r) { g = c ? 1 : 2; return r; }
+function h(c, ...r) { return r; }
+function h2(...r) { return r; }
+function sink() {
+    let out = [];
+    for (let i = 0; i < arguments.length; i++) out.push(arguments[i]);
+    return out;
+}
+noInline(sink);
+
+function restX(c, ...rx) {
+    let arr = [...rx, ...restY(c, ...rx)];
+    let dummy = [0, 0, 0, 0, 0, 0, 0, h(50, 9.9, 8.8)];
+    return [sink.apply(null, arr), dummy];
+}
+for (let i = 0; i < 1000000; i++) restX(i & 1, 0.1, 0.2);
+
+function makeSrc(k) {
+return `
+(function() {
+function victim${k}(c1) {
+    let q = restX(c1, 0.1, 0.2);
+    let z = h2(7.7, 6.6);
+    return [q, z];
+}
+noInline(victim${k});
+for (let i = 0; i < 1000000; i++) {
+    victim${k}(i & 1);
+}
+})()
+`;
+}
+
+for (let k = 0; k < 30; k++) eval(makeSrc(k));
diff --git a/Source/JavaScriptCore/dfg/DFGArgumentsEliminationPhase.cpp b/Source/JavaScriptCore/dfg/DFGArgumentsEliminationPhase.cpp
index 509c0ccf40d1..b90a0b9649cf 100644
--- a/Source/JavaScriptCore/dfg/DFGArgumentsEliminationPhase.cpp
+++ b/Source/JavaScriptCore/dfg/DFGArgumentsEliminationPhase.cpp
@@ -604,7 +604,7 @@ class ArgumentsEliminationPhase : public Phase {
             return interfere;
         };
 
-        auto removeViaKill = [&](BasicBlock* block, unsigned nodeIndex, Node* candidate) {
+        auto removeViaKill = [&](BasicBlock* block, const unsigned nodeIndex, Node* candidate) {
             if (!m_candidates.contains(candidate))
                 return;
 
@@ -660,21 +660,17 @@ class ArgumentsEliminationPhase : public Phase {
                 }
 
                 // This loop considers all nodes up to the nodeIndex, excluding the nodeIndex.
+                // scanIndex is a working copy so each inline call frame independently
+                // scans the full [0, nodeIndex) range.
                 //
-                // Note: nodeIndex here has a double meaning. Before entering this
-                // while loop, it refers to the remaining number of nodes that have
-                // yet to be processed. Inside the loop, it refers to the index
-                // of the current node to process (after we decrement it).
-                //
-                // If the remaining number of nodes is 0, we should not decrement nodeIndex.
-                // Hence, we must only decrement nodeIndex inside the while loop instead of
-                // in its condition statement. Note that this while loop is embedded in an
-                // outer for loop. If we decrement nodeIndex in the condition statement, a
-                // nodeIndex of 0 will become UINT_MAX, and the outer loop will wrongly
-                // treat this as there being UINT_MAX remaining nodes to process.
-                while (nodeIndex) {
-                    --nodeIndex;
-                    Node* node = block->at(nodeIndex);
+                // If the remaining number of nodes is 0, we should not decrement
+                // scanIndex. Hence, we must only decrement it inside the while loop
+                // instead of in its condition statement; otherwise a scanIndex of 0 would
+                // become UINT_MAX.
+                unsigned scanIndex = nodeIndex;
+                while (scanIndex) {
+                    --scanIndex;
+                    Node* node = block->at(scanIndex);
                     if (node == candidate)
                         break;
 
@@ -693,7 +689,7 @@ class ArgumentsEliminationPhase : public Phase {
                         NoOpClobberize());
 
                     if (found) {
-                        dataLogLnIf(DFGArgumentsEliminationPhaseInternal::verbose, "eliminating candidate: ", candidate, " because it is clobbered by ", block->at(nodeIndex));
+                        dataLogLnIf(DFGArgumentsEliminationPhaseInternal::verbose, "eliminating candidate: ", candidate, " because it is clobbered by ", block->at(scanIndex));
                         transitivelyRemoveCandidate(candidate);
                         return;
                     }

From 3ccfb3e02a8d5ab9539efb2a4c214ef48f247a71 Mon Sep 17 00:00:00 2001
From: Carlos Alberto Lopez Perez 
Date: Tue, 30 Jun 2026 03:20:24 -0700
Subject: [PATCH 05/84] [Tools][WPE][browserperfdash-benchmark]
 browser-binary-size plan plugin should report the sizes of the stripped
 binaries and libs. https://bugs.webkit.org/show_bug.cgi?id=318205

Reviewed by Nikolas Zimmermann.

Ensure to strip the binary and libraries before reporting the size.
That is done in a temporal file to not affect the original ones because
the user may want to keep those with debug symbols to use gdb or similar.

* Tools/Scripts/webkitpy/browserperfdash/plans/browser_binary_size.py:
(get_stripped_object_size):
(get_basenames_and_sizes):
(get_browser_relevant_objects_glib):

Canonical link: https://commits.webkit.org/316119@main
---
 .../plans/browser_binary_size.py              | 21 +++++++++++++++++--
 1 file changed, 19 insertions(+), 2 deletions(-)

diff --git a/Tools/Scripts/webkitpy/browserperfdash/plans/browser_binary_size.py b/Tools/Scripts/webkitpy/browserperfdash/plans/browser_binary_size.py
index 10a493019211..0e68ac17ed73 100644
--- a/Tools/Scripts/webkitpy/browserperfdash/plans/browser_binary_size.py
+++ b/Tools/Scripts/webkitpy/browserperfdash/plans/browser_binary_size.py
@@ -22,6 +22,8 @@
 
 import json
 import os
+import subprocess
+import tempfile
 from webkitpy.common.host import Host
 from webkitpy.port import configuration_options, platform_options, factory
 from webkitpy.binary_bundling.ldd import SharedObjectResolver
@@ -38,20 +40,35 @@ def generate_json_for_benchmark(basename_sizes):
     return benchmark_json
 
 
+def get_stripped_object_size(object_path):
+    fd, tmp_path = tempfile.mkstemp(prefix=f'{PLUGIN_NAME}_', suffix='.stripped')
+    os.close(fd)
+    try:
+        result = subprocess.run(['strip', '--strip-all', '-o', tmp_path, object_path], capture_output=True, text=True)
+        if result.returncode != 0:
+            raise RuntimeError(f"'strip --strip-all' failed for '{object_path}' (exit {result.returncode}): {result.stderr.strip()}")
+        return os.path.getsize(tmp_path)
+    finally:
+        try:
+            os.remove(tmp_path)
+        except OSError:
+            pass
+
+
 def get_basenames_and_sizes(path_list):
     basename_sizes = {}
     for object_path in path_list:
         object_base = os.path.basename(object_path)
         if object_base in basename_sizes:
             raise RuntimeError(f'There are repeated objects on the list. Object {object_path} is at least twice')
-        basename_sizes[object_base] = os.path.getsize(object_path)
+        basename_sizes[object_base] = get_stripped_object_size(object_path)
     return basename_sizes
 
 
 def get_browser_relevant_objects_glib(browser_driver):
     required_binary = 'Tools/Scripts/run-minibrowser'
     if not browser_driver.process_name.endswith(required_binary):
-        raise NotImplementedError(f'Getting the browser size data for browser "{browser_name}" is only supported when running the browser via "{required_binary}" and the driver was going to run "{browser_driver.process_name}"')
+        raise NotImplementedError(f'Getting the browser size data for browser "{browser_driver.browser_name}" is only supported when running the browser via "{required_binary}" and the driver was going to run "{browser_driver.process_name}"')
     browser_relevant_objects = []
     port_name = 'gtk' if browser_driver.browser_name == 'minibrowser-gtk' else 'wpe'
     port_driver = factory.PortFactory(Host()).get(port_name)

From d8576e6cceeb0595321ad30fb85d0b861377ce9d Mon Sep 17 00:00:00 2001
From: Charlie Wolfe 
Date: Tue, 30 Jun 2026 03:39:33 -0700
Subject: [PATCH 06/84] Reject requestStorageAccess() without gesture should
 not synthesize user activation https://bugs.webkit.org/show_bug.cgi?id=313478
 rdar://174964081

Reviewed by Matthew Finkel.

The completion handler for requestStorageAccess() preserved the user gesture whenever the prompt was
not shown, including on the no-gesture fast-reject path. This synthesized a UserGestureIndicator
from nothing, allowing the .catch handler to call gesture-gated APIs from a cross-site iframe with
zero user interaction.

Gate gesture preservation on a real gesture having existed at call time.

Test: http/tests/storageAccess/request-storage-access-rejected-without-gesture-should-not-activate.html

* LayoutTests/http/tests/storageAccess/request-storage-access-rejected-without-gesture-should-not-activate-expected.txt: Added.
* LayoutTests/http/tests/storageAccess/request-storage-access-rejected-without-gesture-should-not-activate.html: Added.
* LayoutTests/http/tests/storageAccess/resources/request-storage-access-without-gesture-check-activation-iframe.html: Added.
* Source/WebCore/dom/DocumentStorageAccess.cpp:
(WebCore::DocumentStorageAccess::requestStorageAccess):
(WebCore::DocumentStorageAccess::requestStorageAccessQuirk):
* Source/WebCore/dom/DocumentStorageAccess.h:
* Source/WebCore/page/ChromeClient.h:
(WebCore::ChromeClient::requestStorageAccess):
* Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.cpp:
(WebKit::WebResourceLoadStatisticsStore::requestStorageAccess):
* Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.h:
* Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp:
(WebKit::NetworkConnectionToWebProcess::requestStorageAccess):
* Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.h:
* Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in:
* Source/WebKit/Scripts/webkit/messages.py:
(headers_for_type):
* Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in:
* Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.cpp:
(WebKit::WebChromeClient::requestStorageAccess):
* Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.h:
* Source/WebKit/WebProcess/WebPage/WebPage.cpp:
(WebKit::WebPage::requestStorageAccess):
* Source/WebKit/WebProcess/WebPage/WebPage.h:

Originally-landed-as: 305413.854@safari-7624-branch (6fae18f756d2). rdar://180436613
Canonical link: https://commits.webkit.org/316120@main
---
 ...t-gesture-should-not-activate-expected.txt | 10 ++++
 ...d-without-gesture-should-not-activate.html | 49 +++++++++++++++++++
 ...thout-gesture-check-activation-iframe.html | 24 +++++++++
 Source/WebCore/dom/DocumentStorageAccess.cpp  |  9 ++--
 Source/WebCore/dom/DocumentStorageAccess.h    |  2 +-
 Source/WebCore/page/ChromeClient.h            |  2 +-
 .../WebResourceLoadStatisticsStore.cpp        |  4 +-
 .../WebResourceLoadStatisticsStore.h          |  2 +-
 .../NetworkConnectionToWebProcess.cpp         |  4 +-
 .../NetworkConnectionToWebProcess.h           |  2 +-
 .../NetworkConnectionToWebProcess.messages.in |  2 +-
 Source/WebKit/Scripts/webkit/messages.py      |  2 +-
 .../WebCoreArgumentCoders.serialization.in    |  2 +-
 .../WebCoreSupport/WebChromeClient.cpp        |  4 +-
 .../WebCoreSupport/WebChromeClient.h          |  2 +-
 Source/WebKit/WebProcess/WebPage/WebPage.cpp  |  4 +-
 Source/WebKit/WebProcess/WebPage/WebPage.h    |  4 +-
 17 files changed, 106 insertions(+), 22 deletions(-)
 create mode 100644 LayoutTests/http/tests/storageAccess/request-storage-access-rejected-without-gesture-should-not-activate-expected.txt
 create mode 100644 LayoutTests/http/tests/storageAccess/request-storage-access-rejected-without-gesture-should-not-activate.html
 create mode 100644 LayoutTests/http/tests/storageAccess/resources/request-storage-access-without-gesture-check-activation-iframe.html

diff --git a/LayoutTests/http/tests/storageAccess/request-storage-access-rejected-without-gesture-should-not-activate-expected.txt b/LayoutTests/http/tests/storageAccess/request-storage-access-rejected-without-gesture-should-not-activate-expected.txt
new file mode 100644
index 000000000000..38eed2435aec
--- /dev/null
+++ b/LayoutTests/http/tests/storageAccess/request-storage-access-rejected-without-gesture-should-not-activate-expected.txt
@@ -0,0 +1,10 @@
+Tests that rejecting requestStorageAccess() without a user gesture does not synthesize user activation.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS Rejection handler correctly had no user activation.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/storageAccess/request-storage-access-rejected-without-gesture-should-not-activate.html b/LayoutTests/http/tests/storageAccess/request-storage-access-rejected-without-gesture-should-not-activate.html
new file mode 100644
index 000000000000..5b9aea0c27da
--- /dev/null
+++ b/LayoutTests/http/tests/storageAccess/request-storage-access-rejected-without-gesture-should-not-activate.html
@@ -0,0 +1,49 @@
+
+
+
+    
+    
+    
+    
+
+
+
+
diff --git a/LayoutTests/http/tests/storageAccess/resources/request-storage-access-without-gesture-check-activation-iframe.html b/LayoutTests/http/tests/storageAccess/resources/request-storage-access-without-gesture-check-activation-iframe.html
new file mode 100644
index 000000000000..5610afddfdf8
--- /dev/null
+++ b/LayoutTests/http/tests/storageAccess/resources/request-storage-access-without-gesture-check-activation-iframe.html
@@ -0,0 +1,24 @@
+
+
+
+    
+
+
+
+
diff --git a/Source/WebCore/dom/DocumentStorageAccess.cpp b/Source/WebCore/dom/DocumentStorageAccess.cpp
index 3ba8a7e499a5..54adf96d04c6 100644
--- a/Source/WebCore/dom/DocumentStorageAccess.cpp
+++ b/Source/WebCore/dom/DocumentStorageAccess.cpp
@@ -238,8 +238,8 @@ void DocumentStorageAccess::requestStorageAccess(Ref&& promise)
     if (!page->settings().storageAccessAPIPerPageScopeEnabled())
         m_storageAccessScope = StorageAccessScope::PerFrame;
 
-    auto hasOrShouldIgnoreUserGesture = frame->requestSkipUserActivationCheckForStorageAccess(RegistrableDomain { document->url() }) || UserGestureIndicator::processingUserGesture() ? HasOrShouldIgnoreUserGesture::Yes : HasOrShouldIgnoreUserGesture::No;
-    page->chrome().client().requestStorageAccess(RegistrableDomain::uncheckedCreateFromHost(protect(document->securityOrigin())->host()), RegistrableDomain::uncheckedCreateFromHost(protect(document->topOrigin())->host()), *frame, m_storageAccessScope, hasOrShouldIgnoreUserGesture, [weakThis = WeakPtr { *this }, promise = WTF::move(promise)] (RequestStorageAccessResult result) mutable {
+    auto hasUserGestureOrNoUserGestureRequired = frame->requestSkipUserActivationCheckForStorageAccess(RegistrableDomain { document->url() }) || UserGestureIndicator::processingUserGesture() ? HasUserGestureOrNoUserGestureRequired::Yes : HasUserGestureOrNoUserGestureRequired::No;
+    page->chrome().client().requestStorageAccess(RegistrableDomain::uncheckedCreateFromHost(protect(document->securityOrigin())->host()), RegistrableDomain::uncheckedCreateFromHost(protect(document->topOrigin())->host()), *frame, m_storageAccessScope, hasUserGestureOrNoUserGestureRequired, [weakThis = WeakPtr { *this }, promise = WTF::move(promise), hasUserGestureOrNoUserGestureRequired] (RequestStorageAccessResult result) mutable {
         RefPtr protectedThis = weakThis.get();
         if (!protectedThis)
             return;
@@ -252,7 +252,8 @@ void DocumentStorageAccess::requestStorageAccess(Ref&& promise)
             shouldPreserveUserGesture = true;
             break;
         case StorageAccessWasGranted::No:
-            shouldPreserveUserGesture = result.promptWasShown == StorageAccessPromptWasShown::No;
+            const bool promptNotShownButMayHaveUserGesture = result.promptWasShown == StorageAccessPromptWasShown::No && hasUserGestureOrNoUserGestureRequired == HasUserGestureOrNoUserGestureRequired::Yes;
+            shouldPreserveUserGesture = promptNotShownButMayHaveUserGesture;
         }
 
         Ref document = protectedThis->m_document.get();
@@ -349,7 +350,7 @@ void DocumentStorageAccess::requestStorageAccessQuirk(RegistrableDomain&& reques
     auto topFrameDomain = RegistrableDomain(page->mainFrameURL());
 
     RefPtr frame = document->frame();
-    page->chrome().client().requestStorageAccess(WTF::move(requestingDomain), WTF::move(topFrameDomain), *frame, m_storageAccessScope, HasOrShouldIgnoreUserGesture::Yes, [weakThis = WeakPtr { *this }, completionHandler = WTF::move(completionHandler)] (RequestStorageAccessResult result) mutable {
+    page->chrome().client().requestStorageAccess(WTF::move(requestingDomain), WTF::move(topFrameDomain), *frame, m_storageAccessScope, HasUserGestureOrNoUserGestureRequired::Yes, [weakThis = WeakPtr { *this }, completionHandler = WTF::move(completionHandler)] (RequestStorageAccessResult result) mutable {
         RefPtr protectedThis = weakThis.get();
         if (!protectedThis)
             return;
diff --git a/Source/WebCore/dom/DocumentStorageAccess.h b/Source/WebCore/dom/DocumentStorageAccess.h
index 65417557af76..6b46f8105710 100644
--- a/Source/WebCore/dom/DocumentStorageAccess.h
+++ b/Source/WebCore/dom/DocumentStorageAccess.h
@@ -41,7 +41,7 @@ enum class StorageAccessWasGranted : uint8_t { No, Yes, YesWithException };
 
 enum class StorageAccessPromptWasShown : bool { No, Yes };
 
-enum class HasOrShouldIgnoreUserGesture : bool { No, Yes };
+enum class HasUserGestureOrNoUserGestureRequired : bool { No, Yes };
 
 enum class StorageAccessScope : bool {
     PerFrame,
diff --git a/Source/WebCore/page/ChromeClient.h b/Source/WebCore/page/ChromeClient.h
index ed67709d049b..8bd12ba1ab6a 100644
--- a/Source/WebCore/page/ChromeClient.h
+++ b/Source/WebCore/page/ChromeClient.h
@@ -683,7 +683,7 @@ class ChromeClient {
     virtual RefPtr createIconForFiles(const Vector& /* filenames */) = 0;
 
     virtual void hasStorageAccess(RegistrableDomain&& /*subFrameDomain*/, RegistrableDomain&& /*topFrameDomain*/, LocalFrame&, CompletionHandler&& completionHandler) { completionHandler(false); }
-    virtual void requestStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, LocalFrame&, StorageAccessScope scope, HasOrShouldIgnoreUserGesture, CompletionHandler&& completionHandler) { completionHandler({ StorageAccessWasGranted::No, StorageAccessPromptWasShown::No, scope, WTF::move(topFrameDomain), WTF::move(subFrameDomain) }); }
+    virtual void requestStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, LocalFrame&, StorageAccessScope scope, HasUserGestureOrNoUserGestureRequired, CompletionHandler&& completionHandler) { completionHandler({ StorageAccessWasGranted::No, StorageAccessPromptWasShown::No, scope, WTF::move(topFrameDomain), WTF::move(subFrameDomain) }); }
     virtual bool hasPageLevelStorageAccess(const RegistrableDomain& /*topLevelDomain*/, const RegistrableDomain& /*resourceDomain*/) const { return false; }
 
     virtual void setLoginStatus(RegistrableDomain&&, IsLoggedIn, CompletionHandler&&) { }
diff --git a/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.cpp b/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.cpp
index df275bb0c5ac..96157f4ae9f9 100644
--- a/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.cpp
+++ b/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.cpp
@@ -416,7 +416,7 @@ void WebResourceLoadStatisticsStore::callHasStorageAccessForFrameHandler(const R
     callback(false);
 }
 
-void WebResourceLoadStatisticsStore::requestStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, FrameIdentifier frameID, PageIdentifier webPageID, WebPageProxyIdentifier webPageProxyID, StorageAccessScope scope, HasOrShouldIgnoreUserGesture hasOrShouldIgnoreUserGesture, CompletionHandler&& completionHandler)
+void WebResourceLoadStatisticsStore::requestStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, FrameIdentifier frameID, PageIdentifier webPageID, WebPageProxyIdentifier webPageProxyID, StorageAccessScope scope, HasUserGestureOrNoUserGestureRequired hasUserGestureOrNoUserGestureRequired, CompletionHandler&& completionHandler)
 {
     ASSERT(RunLoop::isMain());
 
@@ -425,7 +425,7 @@ void WebResourceLoadStatisticsStore::requestStorageAccess(RegistrableDomain&& su
         return;
     }
 
-    if (hasOrShouldIgnoreUserGesture == HasOrShouldIgnoreUserGesture::No) {
+    if (hasUserGestureOrNoUserGestureRequired == HasUserGestureOrNoUserGestureRequired::No) {
         auto it = m_domainsGrantedStorageAccessPermissionInPage.find(webPageProxyID);
         if (it == m_domainsGrantedStorageAccessPermissionInPage.end() || !it->value.contains({ topFrameDomain, subFrameDomain }))
             return completionHandler({ StorageAccessWasGranted::No, StorageAccessPromptWasShown::No, scope, topFrameDomain, subFrameDomain });
diff --git a/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.h b/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.h
index 27144ad291df..f8622ac1885f 100644
--- a/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.h
+++ b/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.h
@@ -149,7 +149,7 @@ class WebResourceLoadStatisticsStore final : public ThreadSafeRefCountedAndCanMa
     void hasHadUserInteraction(RegistrableDomain&&, CompletionHandler&&);
     void hasStorageAccess(SubFrameDomain&&, TopFrameDomain&&, std::optional, WebCore::PageIdentifier, CompletionHandler&&);
     bool hasStorageAccessForFrame(const SubFrameDomain&, const TopFrameDomain&, WebCore::FrameIdentifier, WebCore::PageIdentifier);
-    void requestStorageAccess(SubFrameDomain&&, TopFrameDomain&&, WebCore::FrameIdentifier, WebCore::PageIdentifier, WebPageProxyIdentifier, StorageAccessScope, WebCore::HasOrShouldIgnoreUserGesture, CompletionHandler&&);
+    void requestStorageAccess(SubFrameDomain&&, TopFrameDomain&&, WebCore::FrameIdentifier, WebCore::PageIdentifier, WebPageProxyIdentifier, StorageAccessScope, WebCore::HasUserGestureOrNoUserGestureRequired, CompletionHandler&&);
     void queryStorageAccessPermission(SubFrameDomain&&, TopFrameDomain&&, std::optional, CompletionHandler&&);
     void setLoginStatus(RegistrableDomain&&, IsLoggedIn, std::optional&&, CompletionHandler&&);
     void isLoggedIn(RegistrableDomain&&, CompletionHandler&&);
diff --git a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp
index 17eb67e55342..981445834cec 100644
--- a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp
+++ b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp
@@ -1376,11 +1376,11 @@ void NetworkConnectionToWebProcess::hasStorageAccess(RegistrableDomain&& subFram
     completionHandler(false);
 }
 
-void NetworkConnectionToWebProcess::requestStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, FrameIdentifier frameID, PageIdentifier webPageID, WebPageProxyIdentifier webPageProxyID, StorageAccessScope scope, HasOrShouldIgnoreUserGesture hasOrShouldIgnoreUserGesture, CompletionHandler&& completionHandler)
+void NetworkConnectionToWebProcess::requestStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, FrameIdentifier frameID, PageIdentifier webPageID, WebPageProxyIdentifier webPageProxyID, StorageAccessScope scope, HasUserGestureOrNoUserGestureRequired hasUserGestureOrNoUserGestureRequired, CompletionHandler&& completionHandler)
 {
     if (CheckedPtr networkSession = this->networkSession()) {
         if (RefPtr resourceLoadStatistics = networkSession->resourceLoadStatistics()) {
-            resourceLoadStatistics->requestStorageAccess(WTF::move(subFrameDomain), WTF::move(topFrameDomain), frameID, webPageID, webPageProxyID, scope, hasOrShouldIgnoreUserGesture, WTF::move(completionHandler));
+            resourceLoadStatistics->requestStorageAccess(WTF::move(subFrameDomain), WTF::move(topFrameDomain), frameID, webPageID, webPageProxyID, scope, hasUserGestureOrNoUserGestureRequired, WTF::move(completionHandler));
             return;
         }
     }
diff --git a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.h b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.h
index 20ab2d499c49..fda15c3bcac9 100644
--- a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.h
+++ b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.h
@@ -407,7 +407,7 @@ class NetworkConnectionToWebProcess final
     void logUserInteraction(RegistrableDomain&&);
     void resourceLoadStatisticsUpdated(Vector&&, CompletionHandler&&);
     void hasStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, WebCore::FrameIdentifier, WebCore::PageIdentifier, CompletionHandler&&);
-    void requestStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, WebCore::FrameIdentifier, WebCore::PageIdentifier, WebPageProxyIdentifier, WebCore::StorageAccessScope, WebCore::HasOrShouldIgnoreUserGesture, CompletionHandler&&);
+    void requestStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, WebCore::FrameIdentifier, WebCore::PageIdentifier, WebPageProxyIdentifier, WebCore::StorageAccessScope, WebCore::HasUserGestureOrNoUserGestureRequired, CompletionHandler&&);
     void queryStorageAccessPermission(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, std::optional, CompletionHandler&&);
     void storageAccessQuirkForTopFrameDomain(URL&& topFrameURL, CompletionHandler)>&&);
     void requestStorageAccessUnderOpener(WebCore::RegistrableDomain&& domainInNeedOfStorageAccess, WebCore::PageIdentifier openerPageID, WebCore::RegistrableDomain&& openerDomain);
diff --git a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in
index 8228094b0fab..cc292e637bc5 100644
--- a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in
+++ b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in
@@ -89,7 +89,7 @@ messages -> NetworkConnectionToWebProcess WantsDispatchMessage {
     LogUserInteraction(WebCore::RegistrableDomain domain)
     ResourceLoadStatisticsUpdated(Vector statistics) -> ()
     [EnabledBy=StorageAccessAPIEnabled] HasStorageAccess(WebCore::RegistrableDomain subFrameDomain, WebCore::RegistrableDomain topFrameDomain, WebCore::FrameIdentifier frameID, WebCore::PageIdentifier pageID) -> (bool hasStorageAccess)
-    [EnabledBy=StorageAccessAPIEnabled] RequestStorageAccess(WebCore::RegistrableDomain subFrameDomain, WebCore::RegistrableDomain topFrameDomain, WebCore::FrameIdentifier frameID, WebCore::PageIdentifier webPageID, WebKit::WebPageProxyIdentifier webPageProxyID, enum:bool WebCore::StorageAccessScope scope, enum:bool WebCore::HasOrShouldIgnoreUserGesture hasOrShouldIgnoreUserGesture) -> (struct WebCore::RequestStorageAccessResult result)
+    [EnabledBy=StorageAccessAPIEnabled] RequestStorageAccess(WebCore::RegistrableDomain subFrameDomain, WebCore::RegistrableDomain topFrameDomain, WebCore::FrameIdentifier frameID, WebCore::PageIdentifier webPageID, WebKit::WebPageProxyIdentifier webPageProxyID, enum:bool WebCore::StorageAccessScope scope, enum:bool WebCore::HasUserGestureOrNoUserGestureRequired hasUserGestureOrNoUserGestureRequired) -> (struct WebCore::RequestStorageAccessResult result)
     [EnabledBy=StorageAccessAPIEnabled] QueryStorageAccessPermission(WebCore::RegistrableDomain subFrameDomain, WebCore::RegistrableDomain topFrameDomain, std::optional webPageProxyID) -> (enum:uint8_t WebCore::PermissionState permissionState)
     [EnabledBy=StorageAccessAPIEnabled] StorageAccessQuirkForTopFrameDomain(URL topFrameURL) -> (Vector domains)
     [EnabledBy=StorageAccessAPIEnabled] RequestStorageAccessUnderOpener(WebCore::RegistrableDomain domainInNeedOfStorageAccess, WebCore::PageIdentifier openerPageID, WebCore::RegistrableDomain openerDomain)
diff --git a/Source/WebKit/Scripts/webkit/messages.py b/Source/WebKit/Scripts/webkit/messages.py
index bc1b7244a963..3d997b234e25 100644
--- a/Source/WebKit/Scripts/webkit/messages.py
+++ b/Source/WebKit/Scripts/webkit/messages.py
@@ -1278,7 +1278,7 @@ def headers_for_type(type, for_implementation_file=False):
         'WebCore::GraphicsLayerKeyframeValueList': [''],
         'WebCore::HasAvailableTargets': [''],
         'WebCore::HasInsecureContent': [''],
-        'WebCore::HasOrShouldIgnoreUserGesture': [''],
+        'WebCore::HasUserGestureOrNoUserGestureRequired': [''],
         'WebCore::Headroom': [''],
         'WebCore::HighlightRequestOriginatedInApp': [''],
         'WebCore::HighlightVisibility': [''],
diff --git a/Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in b/Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in
index 50c3e74ecfbe..b58ffa50d2b2 100644
--- a/Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in
+++ b/Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in
@@ -2267,7 +2267,7 @@ enum class WebCore::StorageAccessWasGranted : uint8_t {
 
 enum class WebCore::StorageAccessPromptWasShown : bool
 
-enum class WebCore::HasOrShouldIgnoreUserGesture : bool
+enum class WebCore::HasUserGestureOrNoUserGestureRequired : bool
 
 enum class WebCore::StorageAccessScope : bool
 
diff --git a/Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.cpp b/Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.cpp
index 1d1af9ff7dd6..a6f1b13095b9 100644
--- a/Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.cpp
+++ b/Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.cpp
@@ -2074,12 +2074,12 @@ void WebChromeClient::hasStorageAccess(RegistrableDomain&& subFrameDomain, Regis
         completionHandler(false);
 }
 
-void WebChromeClient::requestStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, LocalFrame& frame, StorageAccessScope scope, HasOrShouldIgnoreUserGesture hasOrShouldIgnoreUserGesture, CompletionHandler&& completionHandler)
+void WebChromeClient::requestStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, LocalFrame& frame, StorageAccessScope scope, HasUserGestureOrNoUserGestureRequired hasUserGestureOrNoUserGestureRequired, CompletionHandler&& completionHandler)
 {
     RefPtr webFrame = WebFrame::fromCoreFrame(frame);
     ASSERT(webFrame);
     if (RefPtr page = m_page.get())
-        page->requestStorageAccess(WTF::move(subFrameDomain), WTF::move(topFrameDomain), *webFrame, scope, hasOrShouldIgnoreUserGesture, WTF::move(completionHandler));
+        page->requestStorageAccess(WTF::move(subFrameDomain), WTF::move(topFrameDomain), *webFrame, scope, hasUserGestureOrNoUserGestureRequired, WTF::move(completionHandler));
     else
         completionHandler({ });
 }
diff --git a/Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.h b/Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.h
index 19896b0cd0d5..e47d57813d3b 100644
--- a/Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.h
+++ b/Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.h
@@ -458,7 +458,7 @@ class WebChromeClient final : public WebCore::ChromeClient {
     void didInvalidateDocumentMarkerRects() final;
 
     void hasStorageAccess(WebCore::RegistrableDomain&& subFrameDomain, WebCore::RegistrableDomain&& topFrameDomain, WebCore::LocalFrame&, WTF::CompletionHandler&&) final;
-    void requestStorageAccess(WebCore::RegistrableDomain&& subFrameDomain, WebCore::RegistrableDomain&& topFrameDomain, WebCore::LocalFrame&, WebCore::StorageAccessScope, WebCore::HasOrShouldIgnoreUserGesture, WTF::CompletionHandler&&) final;
+    void requestStorageAccess(WebCore::RegistrableDomain&& subFrameDomain, WebCore::RegistrableDomain&& topFrameDomain, WebCore::LocalFrame&, WebCore::StorageAccessScope, WebCore::HasUserGestureOrNoUserGestureRequired, WTF::CompletionHandler&&) final;
     bool hasPageLevelStorageAccess(const WebCore::RegistrableDomain& topLevelDomain, const WebCore::RegistrableDomain& resourceDomain) const final;
 
     void setLoginStatus(WebCore::RegistrableDomain&&, WebCore::IsLoggedIn, WTF::CompletionHandler&&) final;
diff --git a/Source/WebKit/WebProcess/WebPage/WebPage.cpp b/Source/WebKit/WebProcess/WebPage/WebPage.cpp
index f1043a817fad..14b7a11d8032 100644
--- a/Source/WebKit/WebProcess/WebPage/WebPage.cpp
+++ b/Source/WebKit/WebProcess/WebPage/WebPage.cpp
@@ -8962,9 +8962,9 @@ void WebPage::hasStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDo
     WebProcess::singleton().ensureNetworkProcessConnection().connection().sendWithAsyncReply(Messages::NetworkConnectionToWebProcess::HasStorageAccess(WTF::move(subFrameDomain), WTF::move(topFrameDomain), frame.frameID(), m_identifier), WTF::move(completionHandler));
 }
 
-void WebPage::requestStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, WebFrame& frame, StorageAccessScope scope, HasOrShouldIgnoreUserGesture hasOrShouldIgnoreUserGesture, CompletionHandler&& completionHandler)
+void WebPage::requestStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, WebFrame& frame, StorageAccessScope scope, HasUserGestureOrNoUserGestureRequired hasUserGestureOrNoUserGestureRequired, CompletionHandler&& completionHandler)
 {
-    WebProcess::singleton().ensureNetworkProcessConnection().connection().sendWithAsyncReply(Messages::NetworkConnectionToWebProcess::RequestStorageAccess(WTF::move(subFrameDomain), WTF::move(topFrameDomain), frame.frameID(), m_identifier, m_webPageProxyIdentifier, scope, hasOrShouldIgnoreUserGesture), [this, protectedThis = Ref { *this }, completionHandler = WTF::move(completionHandler), frame = Ref { frame }, pageID = m_identifier, frameID = frame.frameID()](RequestStorageAccessResult result) mutable {
+    WebProcess::singleton().ensureNetworkProcessConnection().connection().sendWithAsyncReply(Messages::NetworkConnectionToWebProcess::RequestStorageAccess(WTF::move(subFrameDomain), WTF::move(topFrameDomain), frame.frameID(), m_identifier, m_webPageProxyIdentifier, scope, hasUserGestureOrNoUserGestureRequired), [this, protectedThis = Ref { *this }, completionHandler = WTF::move(completionHandler), frame = Ref { frame }, pageID = m_identifier, frameID = frame.frameID()](RequestStorageAccessResult result) mutable {
         if (result.wasGranted == StorageAccessWasGranted::Yes) {
             switch (result.scope) {
             case StorageAccessScope::PerFrame:
diff --git a/Source/WebKit/WebProcess/WebPage/WebPage.h b/Source/WebKit/WebProcess/WebPage/WebPage.h
index 6a01b62af0c9..4d6f03a245e1 100644
--- a/Source/WebKit/WebProcess/WebPage/WebPage.h
+++ b/Source/WebKit/WebProcess/WebPage/WebPage.h
@@ -254,7 +254,7 @@ enum class EventHandling : uint8_t;
 enum class EventMakesGamepadsVisible : bool;
 enum class ExceptionCode : uint8_t;
 enum class FinalizeRenderingUpdateFlags : uint8_t;
-enum class HasOrShouldIgnoreUserGesture : bool;
+enum class HasUserGestureOrNoUserGestureRequired : bool;
 enum class HighlightRequestOriginatedInApp : bool;
 enum class IFrameUnloadReason : bool;
 enum class ImageDecodingError : uint8_t;
@@ -1787,7 +1787,7 @@ class WebPage final : public API::ObjectImpl, pub
 #endif
 
     void hasStorageAccess(WebCore::RegistrableDomain&& subFrameDomain, WebCore::RegistrableDomain&& topFrameDomain, WebFrame&, CompletionHandler&&);
-    void requestStorageAccess(WebCore::RegistrableDomain&& subFrameDomain, WebCore::RegistrableDomain&& topFrameDomain, WebFrame&, WebCore::StorageAccessScope, WebCore::HasOrShouldIgnoreUserGesture, CompletionHandler&&);
+    void requestStorageAccess(WebCore::RegistrableDomain&& subFrameDomain, WebCore::RegistrableDomain&& topFrameDomain, WebFrame&, WebCore::StorageAccessScope, WebCore::HasUserGestureOrNoUserGestureRequired, CompletionHandler&&);
     void setLoginStatus(WebCore::RegistrableDomain&&, WebCore::IsLoggedIn, CompletionHandler&&);
     void isLoggedIn(WebCore::RegistrableDomain&&, CompletionHandler&&);
     bool hasPageLevelStorageAccess(const WebCore::RegistrableDomain& topLevelDomain, const WebCore::RegistrableDomain& resourceDomain) const;

From 82a21e0b3ea89e71d4d34894e98f6c5ebe254a87 Mon Sep 17 00:00:00 2001
From: Jer Noble 
Date: Tue, 30 Jun 2026 05:05:28 -0700
Subject: [PATCH 07/84] MockSampleBox should not have negative timeScales and
 duration rdar://174740124 https://bugs.webkit.org/show_bug.cgi?id=313917

Reviewed by Simon Fraser.

* Source/WebCore/platform/mock/mediasource/MockBox.cpp:
(WebCore::MockInitializationBox::MockInitializationBox):
(WebCore::MockSampleBox::MockSampleBox):

Originally-landed-as: 305413.813@safari-7624-branch (4f7a3cfefe44). rdar://180437504
Canonical link: https://commits.webkit.org/316121@main
---
 Source/WebCore/platform/mock/mediasource/MockBox.cpp | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/Source/WebCore/platform/mock/mediasource/MockBox.cpp b/Source/WebCore/platform/mock/mediasource/MockBox.cpp
index cf0f1847dbd9..e40fc4a6208c 100644
--- a/Source/WebCore/platform/mock/mediasource/MockBox.cpp
+++ b/Source/WebCore/platform/mock/mediasource/MockBox.cpp
@@ -92,7 +92,7 @@ MockInitializationBox::MockInitializationBox(ArrayBuffer* data)
 
     auto view = JSC::DataView::create(data, 0, data->byteLength());
     int32_t timeValue = view->get(8, true);
-    int32_t timeScale = view->get(12, true);
+    uint32_t timeScale = view->get(12, true);
     m_duration = MediaTime(timeValue, timeScale);
     
     size_t offset = 16;
@@ -120,7 +120,7 @@ MockSampleBox::MockSampleBox(ArrayBuffer* data)
     ASSERT(m_length == 30);
 
     auto view = JSC::DataView::create(data, 0, data->byteLength());
-    int32_t timeScale = view->get(8, true);
+    uint32_t timeScale = view->get(8, true);
 
     int32_t timeValue = view->get(12, true);
     m_presentationTimestamp = MediaTime(timeValue, timeScale);
@@ -128,8 +128,8 @@ MockSampleBox::MockSampleBox(ArrayBuffer* data)
     timeValue = view->get(16, true);
     m_decodeTimestamp = MediaTime(timeValue, timeScale);
 
-    timeValue = view->get(20, true);
-    m_duration = MediaTime(timeValue, timeScale);
+    uint32_t durationValue = view->get(20, true);
+    m_duration = MediaTime(durationValue, timeScale);
 
     m_trackID = view->get(24, true);
     m_flags = view->get(28, true);

From 3c46119f53450bbe2474543a21f78b7b24b28fec Mon Sep 17 00:00:00 2001
From: Alan Baradlay 
Date: Tue, 30 Jun 2026 05:08:13 -0700
Subject: [PATCH 08/84] [cleanup] Rename RenderBox::setWidth()/setHeight() to
 setBorderBoxWidth()/setBorderBoxHeight()
 https://bugs.webkit.org/show_bug.cgi?id=318039 

Reviewed by Antti Koivisto.

Follow-up to the width()/height() -> borderBoxWidth()/borderBoxHeight() rename.
setWidth()/setHeight() set the border box width/height, but the bare names
didn't say so -- the unqualified odd ones out next to setBorderBoxSize() and the
borderBox* getters. Name them for what they are.

Mechanical rename across WebCore; no behavior change.

* Source/WebCore/layout/integration/flex/LayoutIntegrationFlexLayout.cpp:
(FlexLayout::layout):
(FlexLayout::updateRenderers):
* Source/WebCore/layout/integration/grid/LayoutIntegrationGridLayout.cpp:
(GridLayout::updateGridItemRenderers):
(GridLayout::updateFormattingContextRootRenderer):
* Source/WebCore/rendering/RenderBox.h:
* Source/WebCore/rendering/RenderBoxInlines.h:
(RenderBox::setLogicalHeight):
(RenderBox::setLogicalWidth):
* Source/WebCore/rendering/RenderDeprecatedFlexibleBox.cpp:
(RenderDeprecatedFlexibleBox::layoutBlock):
(RenderDeprecatedFlexibleBox::layoutHorizontalBox):
(RenderDeprecatedFlexibleBox::layoutSingleClampedFlexItem):
(RenderDeprecatedFlexibleBox::layoutVerticalBox):
* Source/WebCore/rendering/RenderFrameSet.cpp:
(RenderFrameSet::layout):
(resetFrameRendererAndDescendants):
(RenderFrameSet::positionFrames):
* Source/WebCore/rendering/RenderListMarker.cpp:
(RenderListMarker::layout):
* Source/WebCore/rendering/RenderReplaced.cpp:
(RenderReplaced::layout):
* Source/WebCore/rendering/RenderScrollbarPart.cpp:
(RenderScrollbarPart::layoutHorizontalPart):
(RenderScrollbarPart::layoutVerticalPart):
(RenderScrollbarPart::computeScrollbarWidth):
(RenderScrollbarPart::computeScrollbarHeight):
(RenderScrollbarPart::paintIntoRect):
* Source/WebCore/rendering/svg/RenderSVGForeignObject.cpp:
(RenderSVGForeignObject::updateLogicalWidth):

Canonical link: https://commits.webkit.org/316122@main
---
 .../flex/LayoutIntegrationFlexLayout.cpp      | 12 +++----
 .../grid/LayoutIntegrationGridLayout.cpp      |  8 ++---
 Source/WebCore/rendering/RenderBox.h          |  4 +--
 Source/WebCore/rendering/RenderBoxInlines.h   |  8 ++---
 .../rendering/RenderDeprecatedFlexibleBox.cpp | 34 +++++++++----------
 Source/WebCore/rendering/RenderFrameSet.cpp   | 12 +++----
 Source/WebCore/rendering/RenderListMarker.cpp |  4 +--
 Source/WebCore/rendering/RenderReplaced.cpp   |  2 +-
 .../WebCore/rendering/RenderScrollbarPart.cpp | 16 ++++-----
 .../rendering/svg/RenderSVGForeignObject.cpp  |  2 +-
 10 files changed, 51 insertions(+), 51 deletions(-)

diff --git a/Source/WebCore/layout/integration/flex/LayoutIntegrationFlexLayout.cpp b/Source/WebCore/layout/integration/flex/LayoutIntegrationFlexLayout.cpp
index 669200ca1b30..143145db0f6d 100644
--- a/Source/WebCore/layout/integration/flex/LayoutIntegrationFlexLayout.cpp
+++ b/Source/WebCore/layout/integration/flex/LayoutIntegrationFlexLayout.cpp
@@ -153,8 +153,8 @@ void FlexLayout::layout()
             auto isOrthogonal = flexContainerIsHorizontal != renderer->writingMode().isHorizontal();
             auto borderBox = Layout::BoxGeometry::borderBoxRect(layoutState().geometryForBox(layoutBox));
 
-            renderer->setWidth(LayoutUnit { });
-            renderer->setHeight(LayoutUnit { });
+            renderer->setBorderBoxWidth(LayoutUnit { });
+            renderer->setBorderBoxHeight(LayoutUnit { });
             // logical here means width and height constraints for the _content_ of the flex items not the flex items' own dimension inside the flex container.
             renderer->setOverridingBorderBoxLogicalWidth(isOrthogonal ? borderBox.height() : borderBox.width());
             renderer->setOverridingBorderBoxLogicalHeight(isOrthogonal ? borderBox.width() : borderBox.height());
@@ -163,8 +163,8 @@ void FlexLayout::layout()
             renderer->layoutIfNeeded();
             renderer->clearOverridingSize();
 
-            renderer->setWidth(flexContainerIsHorizontal ? borderBox.width() : borderBox.height());
-            renderer->setHeight(flexContainerIsHorizontal ? borderBox.height() : borderBox.width());
+            renderer->setBorderBoxWidth(flexContainerIsHorizontal ? borderBox.width() : borderBox.height());
+            renderer->setBorderBoxHeight(flexContainerIsHorizontal ? borderBox.height() : borderBox.width());
         }
     };
     relayoutFlexItems();
@@ -178,8 +178,8 @@ void FlexLayout::updateRenderers()
         auto& flexItemGeometry = layoutState().geometryForBox(layoutBox);
         auto borderBox = Layout::BoxGeometry::borderBoxRect(flexItemGeometry);
         renderer->setLocation(flexContainerIsHorizontal ? borderBox.topLeft() : borderBox.topLeft().transposedPoint());
-        renderer->setWidth(flexContainerIsHorizontal ? borderBox.width() : borderBox.height());
-        renderer->setHeight(flexContainerIsHorizontal ? borderBox.height() : borderBox.width());
+        renderer->setBorderBoxWidth(flexContainerIsHorizontal ? borderBox.width() : borderBox.height());
+        renderer->setBorderBoxHeight(flexContainerIsHorizontal ? borderBox.height() : borderBox.width());
 
         renderer->setMarginStart(flexItemGeometry.marginStart());
         renderer->setMarginEnd(flexItemGeometry.marginEnd());
diff --git a/Source/WebCore/layout/integration/grid/LayoutIntegrationGridLayout.cpp b/Source/WebCore/layout/integration/grid/LayoutIntegrationGridLayout.cpp
index 4e084be3cb48..721feae82185 100644
--- a/Source/WebCore/layout/integration/grid/LayoutIntegrationGridLayout.cpp
+++ b/Source/WebCore/layout/integration/grid/LayoutIntegrationGridLayout.cpp
@@ -161,8 +161,8 @@ void GridLayout::updateGridItemRenderers()
         auto borderBoxRect = Layout::BoxGeometry::borderBoxRect(gridItemGeometry);
 
         renderer->setLocation(contentBoxOffset + borderBoxRect.topLeft());
-        renderer->setWidth(borderBoxRect.width());
-        renderer->setHeight(borderBoxRect.height());
+        renderer->setBorderBoxWidth(borderBoxRect.width());
+        renderer->setBorderBoxHeight(borderBoxRect.height());
 
         renderer->setMarginBefore(gridItemGeometry.marginBefore());
         renderer->setMarginAfter(gridItemGeometry.marginAfter());
@@ -182,9 +182,9 @@ void GridLayout::updateFormattingContextRootRenderer(const Layout::GridLayoutCon
         auto& rowSizes = usedTrackSizes.rowSizes;
         auto usedRowGutter = Layout::GridFormattingContext::usedGapValue(renderGrid->style().rowGap());
         auto blockContentSize = std::reduce(rowSizes.begin(), rowSizes.end()) + Layout::GridLayoutUtils::totalGuttersSize(rowSizes.size(), usedRowGutter);
-        renderGrid->setHeight(blockContentSize + renderGrid->borderAndPaddingLogicalHeight());
+        renderGrid->setBorderBoxHeight(blockContentSize + renderGrid->borderAndPaddingLogicalHeight());
     } else
-        renderGrid->setHeight(layoutConstraints.blockAxis.availableSpace() + renderGrid->borderAndPaddingLogicalHeight());
+        renderGrid->setBorderBoxHeight(layoutConstraints.blockAxis.availableSpace() + renderGrid->borderAndPaddingLogicalHeight());
 
     for (CheckedRef layoutBox : formattingContextBoxes(gridBox()))
         orderIteratorPopulator.collectChild(CheckedRef { downcast(*layoutBox->rendererForIntegration()) });
diff --git a/Source/WebCore/rendering/RenderBox.h b/Source/WebCore/rendering/RenderBox.h
index 94a9bec2c5fb..d75c9eef40b9 100644
--- a/Source/WebCore/rendering/RenderBox.h
+++ b/Source/WebCore/rendering/RenderBox.h
@@ -74,8 +74,8 @@ class RenderBox : public RenderBoxModelObject {
 
     template void setX(T x) { m_frameRect.setX(x); }
     template void setY(T y) { m_frameRect.setY(y); }
-    template void setWidth(T width) { m_frameRect.setWidth(width); }
-    template void setHeight(T height) { m_frameRect.setHeight(height); }
+    template void setBorderBoxWidth(T width) { m_frameRect.setWidth(width); }
+    template void setBorderBoxHeight(T height) { m_frameRect.setHeight(height); }
 
     inline LayoutUnit logicalLeft() const;
     inline LayoutUnit logicalRight() const;
diff --git a/Source/WebCore/rendering/RenderBoxInlines.h b/Source/WebCore/rendering/RenderBoxInlines.h
index a01fdff806c7..198f53d163ce 100644
--- a/Source/WebCore/rendering/RenderBoxInlines.h
+++ b/Source/WebCore/rendering/RenderBoxInlines.h
@@ -210,9 +210,9 @@ inline const LayoutRect RenderBox::scrollablePaddingAreaOverflowRect() const
 inline void RenderBox::setLogicalHeight(LayoutUnit size)
 {
     if (writingMode().isHorizontal())
-        setHeight(size);
+        setBorderBoxHeight(size);
     else
-        setWidth(size);
+        setBorderBoxWidth(size);
 }
 
 inline void RenderBox::setLogicalLeft(LayoutUnit left)
@@ -234,9 +234,9 @@ inline void RenderBox::setLogicalTop(LayoutUnit top)
 inline void RenderBox::setLogicalWidth(LayoutUnit size)
 {
     if (writingMode().isHorizontal())
-        setWidth(size);
+        setBorderBoxWidth(size);
     else
-        setHeight(size);
+        setBorderBoxHeight(size);
 }
 
 inline bool RenderBox::hasStretchedLogicalHeight(StretchingMode mode) const
diff --git a/Source/WebCore/rendering/RenderDeprecatedFlexibleBox.cpp b/Source/WebCore/rendering/RenderDeprecatedFlexibleBox.cpp
index 51d16e3e2b09..f5e38eec1fb6 100644
--- a/Source/WebCore/rendering/RenderDeprecatedFlexibleBox.cpp
+++ b/Source/WebCore/rendering/RenderDeprecatedFlexibleBox.cpp
@@ -370,7 +370,7 @@ void RenderDeprecatedFlexibleBox::layoutBlock(RelayoutChildren relayoutChildren,
                 && parent()->style().boxAlign() == BoxAlignment::Stretch))
             relayoutChildren = RelayoutChildren::Yes;
 
-        setHeight(0);
+        setBorderBoxHeight(0);
 
         m_stretchingChildren = false;
 
@@ -487,7 +487,7 @@ void RenderDeprecatedFlexibleBox::layoutHorizontalBox(RelayoutChildren relayoutC
     // The first pass skips flexible objects completely.
     do {
         // Reset our height.
-        setHeight(yPos);
+        setBorderBoxHeight(yPos);
 
         xPos = borderLeft() + paddingLeft();
 
@@ -527,17 +527,17 @@ void RenderDeprecatedFlexibleBox::layoutHorizontalBox(RelayoutChildren relayoutC
                 maxDescent = std::max(maxDescent, descent);
 
                 // Now update our height.
-                setHeight(std::max(yPos + maxAscent + maxDescent, borderBoxHeight()));
+                setBorderBoxHeight(std::max(yPos + maxAscent + maxDescent, borderBoxHeight()));
             }
             else
-                setHeight(std::max(borderBoxHeight(), yPos + child->borderBoxHeight() + child->verticalMarginExtent()));
+                setBorderBoxHeight(std::max(borderBoxHeight(), yPos + child->borderBoxHeight() + child->verticalMarginExtent()));
         }
         ASSERT(childIndex == childLayoutDeltas.size());
 
         if (!iterator.first() && hasLineIfEmpty())
-            setHeight(borderBoxHeight() + lineHeight());
+            setBorderBoxHeight(borderBoxHeight() + lineHeight());
 
-        setHeight(borderBoxHeight() + toAdd);
+        setBorderBoxHeight(borderBoxHeight() + toAdd);
 
         oldHeight = borderBoxHeight();
         updateLogicalHeight();
@@ -746,7 +746,7 @@ void RenderDeprecatedFlexibleBox::layoutHorizontalBox(RelayoutChildren relayoutC
     // So that the computeLogicalHeight in layoutBlock() knows to relayout positioned objects because of
     // a height change, we revert our height back to the intrinsic height before returning.
     if (heightSpecified)
-        setHeight(oldHeight);
+        setBorderBoxHeight(oldHeight);
 }
 
 void RenderDeprecatedFlexibleBox::layoutSingleClampedFlexItem()
@@ -774,7 +774,7 @@ void RenderDeprecatedFlexibleBox::layoutSingleClampedFlexItem()
     } else
         childBoxBottom += clampedRendererCandidate.contentBoxRect().height() + clampedRendererCandidate.marginBottom();
 
-    setHeight(childBoxBottom + paddingBottom() + borderBottom());
+    setBorderBoxHeight(childBoxBottom + paddingBottom() + borderBottom());
     updateLogicalHeight();
 
     computeInFlowOverflow(flippedContentBoxRect());
@@ -815,7 +815,7 @@ void RenderDeprecatedFlexibleBox::layoutVerticalBox(RelayoutChildren relayoutChi
     // Our first pass is done without flexing.  We simply lay the children
     // out within the box.
     do {
-        setHeight(borderTop() + paddingTop());
+        setBorderBoxHeight(borderTop() + paddingTop());
         LayoutUnit minHeight = borderBoxHeight() + toAdd;
 
         for (RenderBox* child = iterator.first(); child; child = iterator.next()) {
@@ -839,7 +839,7 @@ void RenderDeprecatedFlexibleBox::layoutVerticalBox(RelayoutChildren relayoutChi
             child->computeAndSetBlockDirectionMargins(*this);
 
             // Add in the child's marginTop to our height.
-            setHeight(borderBoxHeight() + child->marginTop());
+            setBorderBoxHeight(borderBoxHeight() + child->marginTop());
 
             if (!haveLineClamp)
                 child->markForPaginationRelayoutIfNeeded();
@@ -872,20 +872,20 @@ void RenderDeprecatedFlexibleBox::layoutVerticalBox(RelayoutChildren relayoutChi
 
             // Place the child.
             placeChild(child, LayoutPoint(childX, borderBoxHeight()));
-            setHeight(borderBoxHeight() + child->borderBoxHeight() + child->marginBottom());
+            setBorderBoxHeight(borderBoxHeight() + child->borderBoxHeight() + child->marginBottom());
         }
 
         yPos = borderBoxHeight();
 
         if (!iterator.first() && hasLineIfEmpty())
-            setHeight(borderBoxHeight() + lineHeight());
+            setBorderBoxHeight(borderBoxHeight() + lineHeight());
 
-        setHeight(borderBoxHeight() + toAdd);
+        setBorderBoxHeight(borderBoxHeight() + toAdd);
 
         // Negative margins can cause our height to shrink below our minimal height (border/padding).
         // If this happens, ensure that the computed height is increased to the minimal height.
         if (borderBoxHeight() < minHeight)
-            setHeight(minHeight);
+            setBorderBoxHeight(minHeight);
 
         // Now we have to calc our height, so we know how much space we have remaining.
         oldHeight = borderBoxHeight();
@@ -1041,12 +1041,12 @@ void RenderDeprecatedFlexibleBox::layoutVerticalBox(RelayoutChildren relayoutChi
         };
         auto usedHeight = borderBoxHeight();
         auto clampedHeight = contentOffset() + clampedContent.contentHeight + borderBottom() + paddingBottom();
-        setHeight(clampedHeight);
+        setBorderBoxHeight(clampedHeight);
         updateLogicalHeight();
         if (clampedHeight != borderBoxHeight())
-            setHeight(heightSpecified ? oldHeight : usedHeight);
+            setBorderBoxHeight(heightSpecified ? oldHeight : usedHeight);
     } else if (heightSpecified)
-        setHeight(oldHeight);
+        setBorderBoxHeight(oldHeight);
 }
 
 static size_t lineCountFor(const RenderBlockFlow& blockFlow)
diff --git a/Source/WebCore/rendering/RenderFrameSet.cpp b/Source/WebCore/rendering/RenderFrameSet.cpp
index 0b81ad43b249..e541c3263b82 100644
--- a/Source/WebCore/rendering/RenderFrameSet.cpp
+++ b/Source/WebCore/rendering/RenderFrameSet.cpp
@@ -440,8 +440,8 @@ void RenderFrameSet::layout()
     }
 
     if (!parent()->isRenderFrameSet() && !protect(document())->printing()) {
-        setWidth(view().viewWidth());
-        setHeight(view().viewHeight());
+        setBorderBoxWidth(view().viewWidth());
+        setBorderBoxHeight(view().viewHeight());
     }
 
     unsigned cols = frameSetElement().totalCols();
@@ -480,8 +480,8 @@ static void resetFrameRendererAndDescendants(RenderBox* frameSetChild, RenderFra
         return;
 
     for (auto* descendant = frameSetChild; descendant; descendant = downcast(RenderObjectTraversal::next(*descendant, &parentFrameSet))) {
-        descendant->setWidth(0);
-        descendant->setHeight(0);
+        descendant->setBorderBoxWidth(0);
+        descendant->setBorderBoxHeight(0);
         descendant->clearNeedsLayout();
     }
 }
@@ -505,8 +505,8 @@ void RenderFrameSet::positionFrames()
             int width = m_cols.m_sizes[c];
 
             // has to be resized and itself resize its contents
-            child->setWidth(width);
-            child->setHeight(height);
+            child->setBorderBoxWidth(width);
+            child->setBorderBoxHeight(height);
 #if PLATFORM(IOS_FAMILY)
             // FIXME: Is this iOS-specific?
             child->setNeedsLayout(MarkingBehavior::MarkOnlyThis);
diff --git a/Source/WebCore/rendering/RenderListMarker.cpp b/Source/WebCore/rendering/RenderListMarker.cpp
index d79f01842240..cdc0f074e1ab 100644
--- a/Source/WebCore/rendering/RenderListMarker.cpp
+++ b/Source/WebCore/rendering/RenderListMarker.cpp
@@ -339,8 +339,8 @@ void RenderListMarker::layout()
     if (isImage()) {
         updateInlineMarginsAndContent();
         RefPtr image = m_image;
-        setWidth(image->imageSize(this, style().usedZoom()).width());
-        setHeight(image->imageSize(this, style().usedZoom()).height());
+        setBorderBoxWidth(image->imageSize(this, style().usedZoom()).width());
+        setBorderBoxHeight(image->imageSize(this, style().usedZoom()).height());
         m_layoutBounds = { borderBoxHeight(), 0 };
     } else {
         setLogicalWidth(minContentLogicalWidthContribution());
diff --git a/Source/WebCore/rendering/RenderReplaced.cpp b/Source/WebCore/rendering/RenderReplaced.cpp
index 3e6d59f00492..a2825434d72f 100644
--- a/Source/WebCore/rendering/RenderReplaced.cpp
+++ b/Source/WebCore/rendering/RenderReplaced.cpp
@@ -158,7 +158,7 @@ void RenderReplaced::layout()
 
     LayoutRect oldContentRect = replacedContentRect();
     
-    setHeight(minimumReplacedHeight());
+    setBorderBoxHeight(minimumReplacedHeight());
 
     updateLogicalWidth();
     updateLogicalHeight();
diff --git a/Source/WebCore/rendering/RenderScrollbarPart.cpp b/Source/WebCore/rendering/RenderScrollbarPart.cpp
index 71d7c434a9d4..0d2bb4989c41 100644
--- a/Source/WebCore/rendering/RenderScrollbarPart.cpp
+++ b/Source/WebCore/rendering/RenderScrollbarPart.cpp
@@ -67,11 +67,11 @@ void RenderScrollbarPart::layout()
 void RenderScrollbarPart::layoutHorizontalPart()
 {
     if (m_part == ScrollbarBGPart) {
-        setWidth(protect(m_scrollbar.get())->width());
+        setBorderBoxWidth(protect(m_scrollbar.get())->width());
         computeScrollbarHeight();
     } else {
         computeScrollbarWidth();
-        setHeight(protect(m_scrollbar.get())->height());
+        setBorderBoxHeight(protect(m_scrollbar.get())->height());
     }
 }
 
@@ -79,9 +79,9 @@ void RenderScrollbarPart::layoutVerticalPart()
 {
     if (m_part == ScrollbarBGPart) {
         computeScrollbarWidth();
-        setHeight(protect(m_scrollbar.get())->height());
+        setBorderBoxHeight(protect(m_scrollbar.get())->height());
     } else {
-        setWidth(protect(m_scrollbar.get())->width());
+        setBorderBoxWidth(protect(m_scrollbar.get())->width());
         computeScrollbarHeight();
     }
 }
@@ -115,7 +115,7 @@ void RenderScrollbarPart::computeScrollbarWidth()
     auto width = calcScrollbarThicknessUsing(style().width(), zoomFactor);
     auto minWidth = calcScrollbarThicknessUsing(style().minWidth(), zoomFactor);
     auto maxWidth = style().maxWidth().isNone() ? width : calcScrollbarThicknessUsing(style().maxWidth(), zoomFactor);
-    setWidth(std::max(minWidth, std::min(maxWidth, width)));
+    setBorderBoxWidth(std::max(minWidth, std::min(maxWidth, width)));
     
     // Buttons and track pieces can all have margins along the axis of the scrollbar. 
     m_marginBox.setLeft(Style::evaluateMinimum(style().marginLeft(), 0_lu, style().usedZoomForLength()));
@@ -130,7 +130,7 @@ void RenderScrollbarPart::computeScrollbarHeight()
     auto height = calcScrollbarThicknessUsing(style().height(), zoomFactor);
     auto minHeight = calcScrollbarThicknessUsing(style().minHeight(), zoomFactor);
     auto maxHeight = style().maxHeight().isNone() ? height : calcScrollbarThicknessUsing(style().maxHeight(), zoomFactor);
-    setHeight(std::max(minHeight, std::min(maxHeight, height)));
+    setBorderBoxHeight(std::max(minHeight, std::min(maxHeight, height)));
 
     // Buttons and track pieces can all have margins along the axis of the scrollbar. 
     m_marginBox.setTop(Style::evaluateMinimum(style().marginTop(), 0_lu, style().usedZoomForLength()));
@@ -167,8 +167,8 @@ void RenderScrollbarPart::paintIntoRect(GraphicsContext& graphicsContext, const
 {
     // Make sure our dimensions match the rect.
     setLocation(rect.location() - toLayoutSize(paintOffset));
-    setWidth(rect.width());
-    setHeight(rect.height());
+    setBorderBoxWidth(rect.width());
+    setBorderBoxHeight(rect.height());
 
     if (graphicsContext.paintingDisabled() || style().opacity().isTransparent())
         return;
diff --git a/Source/WebCore/rendering/svg/RenderSVGForeignObject.cpp b/Source/WebCore/rendering/svg/RenderSVGForeignObject.cpp
index 51804a367b56..8a105200698d 100644
--- a/Source/WebCore/rendering/svg/RenderSVGForeignObject.cpp
+++ b/Source/WebCore/rendering/svg/RenderSVGForeignObject.cpp
@@ -80,7 +80,7 @@ void RenderSVGForeignObject::paint(PaintInfo& paintInfo, const LayoutPoint& pain
 
 void RenderSVGForeignObject::updateLogicalWidth()
 {
-    setWidth(enclosingLayoutRect(m_viewport).width());
+    setBorderBoxWidth(enclosingLayoutRect(m_viewport).width());
 }
 
 RenderBox::LogicalExtentComputedValues RenderSVGForeignObject::computeLogicalHeight(LayoutUnit, LayoutUnit logicalTop) const

From bdf08cf8d7889d8990a601ff576fad7c06b89e3f Mon Sep 17 00:00:00 2001
From: Alan Baradlay 
Date: Tue, 30 Jun 2026 05:11:02 -0700
Subject: [PATCH 09/84] [cleanup] Use RenderBox::borderBoxSize() in size-only
 callers of borderBoxRect() https://bugs.webkit.org/show_bug.cgi?id=318020

Reviewed by Antti Koivisto.

These call sites only read the size of borderBoxRect(); none of them use its
location. borderBoxRect() is { 0, 0, borderBoxSize() }, so constructing the
positioned rect just to call .size()/.width()/.height()/.isEmpty() is wasteful
and hides the intent. Use the direct accessor (borderBoxSize(), borderBoxWidth(),
borderBoxHeight()) instead. No behavior change.

* Source/WebCore/rendering/RenderBox.cpp:
(RenderBox::reflectionOffset):
* Source/WebCore/rendering/svg/RenderSVGRoot.cpp:
(RenderSVGRoot::paint):
(RenderSVGRoot::boundingRects):
* Source/WebCore/style/values/transforms/StyleTransformList.cpp:
(Blending::blend):

Canonical link: https://commits.webkit.org/316123@main
---
 Source/WebCore/rendering/RenderBox.cpp                        | 4 ++--
 Source/WebCore/rendering/svg/RenderSVGRoot.cpp                | 4 ++--
 Source/WebCore/style/values/transforms/StyleTransformList.cpp | 2 +-
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/Source/WebCore/rendering/RenderBox.cpp b/Source/WebCore/rendering/RenderBox.cpp
index 3deb92520f3f..9ed73427f6fc 100644
--- a/Source/WebCore/rendering/RenderBox.cpp
+++ b/Source/WebCore/rendering/RenderBox.cpp
@@ -1020,8 +1020,8 @@ int RenderBox::reflectionOffset() const
     if (!reflection)
         return 0;
     if (reflection->direction == ReflectionDirection::Left || reflection->direction == ReflectionDirection::Right)
-        return Style::evaluate(reflection->offset, borderBoxRect().width(), Style::ZoomNeeded { });
-    return Style::evaluate(reflection->offset, borderBoxRect().height(), Style::ZoomNeeded { });
+        return Style::evaluate(reflection->offset, borderBoxWidth(), Style::ZoomNeeded { });
+    return Style::evaluate(reflection->offset, borderBoxHeight(), Style::ZoomNeeded { });
 }
 
 LayoutRect RenderBox::reflectedRect(const LayoutRect& r) const
diff --git a/Source/WebCore/rendering/svg/RenderSVGRoot.cpp b/Source/WebCore/rendering/svg/RenderSVGRoot.cpp
index e23d67968271..4f43cebc1f1f 100644
--- a/Source/WebCore/rendering/svg/RenderSVGRoot.cpp
+++ b/Source/WebCore/rendering/svg/RenderSVGRoot.cpp
@@ -329,7 +329,7 @@ void RenderSVGRoot::paint(PaintInfo& paintInfo, const LayoutPoint& paintOffset)
         return;
 
     // An empty viewport disables rendering.
-    if (borderBoxRect().isEmpty())
+    if (borderBoxSize().isEmpty())
         return;
 
     auto adjustedPaintOffset = paintOffset + location();
@@ -653,7 +653,7 @@ LayoutRect RenderSVGRoot::overflowClipRect(const LayoutPoint& location, OverlayS
 
 void RenderSVGRoot::boundingRects(Vector& rects, const LayoutPoint& accumulatedOffset) const
 {
-    rects.append({ accumulatedOffset, borderBoxRect().size() });
+    rects.append({ accumulatedOffset, borderBoxSize() });
 }
 
 void RenderSVGRoot::absoluteQuads(Vector& quads, bool* wasFixed) const
diff --git a/Source/WebCore/style/values/transforms/StyleTransformList.cpp b/Source/WebCore/style/values/transforms/StyleTransformList.cpp
index 0022f83a5dcc..7307f1bc7370 100644
--- a/Source/WebCore/style/values/transforms/StyleTransformList.cpp
+++ b/Source/WebCore/style/values/transforms/StyleTransformList.cpp
@@ -112,7 +112,7 @@ auto Blending::blend(const TransformList& from, const TransformLi
     }
 
     CheckedPtr renderBox = dynamicDowncast(context.client.renderer());
-    auto boxSize = renderBox ? renderBox->borderBoxRect().size() : LayoutSize();
+    auto boxSize = renderBox ? renderBox->borderBoxSize() : LayoutSize();
 
     bool shouldFallBackToDiscrete = shouldFallBackToDiscreteInterpolation(from, to, boxSize);
 

From e1084c3d52e835ffeaa42129b87e311e0cf5acf9 Mon Sep 17 00:00:00 2001
From: Chris Dumez 
Date: Tue, 30 Jun 2026 05:30:53 -0700
Subject: [PATCH 10/84] Use-after-free under
 WebsiteDataStore::beginAppBoundDomainCheck()
 https://bugs.webkit.org/show_bug.cgi?id=314295 rdar://176438133

Reviewed by Ryosuke Niwa.

Stop capturing `host` and `protocol` by reference in the lambda since
the lambda can get called asynchronously.

* Source/WebKit/UIProcess/WebsiteData/Cocoa/WebsiteDataStoreCocoa.mm:
(WebKit::WebsiteDataStore::beginAppBoundDomainCheck):

Originally-landed-as: 305413.851@safari-7624-branch (3602ee8745a7). rdar://180438476
Canonical link: https://commits.webkit.org/316124@main
---
 .../WebKit/UIProcess/WebsiteData/Cocoa/WebsiteDataStoreCocoa.mm | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Source/WebKit/UIProcess/WebsiteData/Cocoa/WebsiteDataStoreCocoa.mm b/Source/WebKit/UIProcess/WebsiteData/Cocoa/WebsiteDataStoreCocoa.mm
index fc7a4caef8be..d2cf28cfb7a2 100644
--- a/Source/WebKit/UIProcess/WebsiteData/Cocoa/WebsiteDataStoreCocoa.mm
+++ b/Source/WebKit/UIProcess/WebsiteData/Cocoa/WebsiteDataStoreCocoa.mm
@@ -718,7 +718,7 @@ static NavigatingToAppBoundDomain schemeOrDomainIsAppBound(const String& host, c
 {
     ASSERT(RunLoop::isMain());
 
-    ensureAppBoundDomains([&host, &protocol, listener = Ref { listener }] (auto& domains, auto& schemes) mutable {
+    ensureAppBoundDomains([host, protocol, listener = Ref { listener }] (auto& domains, auto& schemes) mutable {
         // Must check for both an empty app bound domains list and an empty key before returning nullopt
         // because test cases may have app bound domains but no key.
         bool hasAppBoundDomains = keyExists || !domains.isEmpty();

From 8c0e20bc65f3887acb03bda989f747dd047ab015 Mon Sep 17 00:00:00 2001
From: Kimmo Kinnunen 
Date: Tue, 30 Jun 2026 05:32:26 -0700
Subject: [PATCH 11/84] WebGL: Uninitialized FastMalloc heap disclosure in
 RemoteGraphicsContextGL::readPixelsInline
 https://bugs.webkit.org/show_bug.cgi?id=312564 rdar://174640403

Reviewed by Dan Glastonbury.

Allocate the read pixels area with zero init.

* Source/WebKit/GPUProcess/graphics/RemoteGraphicsContextGL.cpp:
(WebKit::RemoteGraphicsContextGL::readPixelsInline):

Originally-landed-as: 305413.708@safari-7624-branch (0c204da15932). rdar://180438472
Canonical link: https://commits.webkit.org/316125@main
---
 .../graphics/RemoteGraphicsContextGL.cpp      | 33 +++++++------------
 1 file changed, 12 insertions(+), 21 deletions(-)

diff --git a/Source/WebKit/GPUProcess/graphics/RemoteGraphicsContextGL.cpp b/Source/WebKit/GPUProcess/graphics/RemoteGraphicsContextGL.cpp
index 6ace2f95a57e..3aeee1a0ea91 100644
--- a/Source/WebKit/GPUProcess/graphics/RemoteGraphicsContextGL.cpp
+++ b/Source/WebKit/GPUProcess/graphics/RemoteGraphicsContextGL.cpp
@@ -293,17 +293,14 @@ void RemoteGraphicsContextGL::getBufferSubDataInline(uint32_t target, uint64_t o
         return;
     }
 
-    MallocSpan bufferStore;
-    std::span bufferData;
-    bufferStore = MallocSpan::tryMalloc(dataSize);
-    if (bufferStore) {
-        bufferData = bufferStore.mutableSpan();
-        if (!context->getBufferSubDataWithStatus(target, offset, bufferData))
-            bufferData = { };
+    MallocSpan buffer = MallocSpan::tryMalloc(dataSize);
+    if (buffer) {
+        if (!context->getBufferSubDataWithStatus(target, offset, buffer.mutableSpan()))
+            buffer = { };
     } else
         context->addError(GCGLErrorCode::OutOfMemory);
 
-    completionHandler(bufferData);
+    completionHandler(buffer.span());
 }
 
 void RemoteGraphicsContextGL::getBufferSubDataSharedMemory(uint32_t target, uint64_t offset, uint64_t dataSize, WebCore::SharedMemory::Handle handle, CompletionHandler&& completionHandler)
@@ -340,25 +337,19 @@ void RemoteGraphicsContextGL::readPixelsInline(WebCore::IntRect rect, uint32_t f
         completionHandler(std::nullopt, { });
         return;
     }
-    MallocSpan pixelsStore;
-    std::span pixels;
-    if (replyImageBytes && replyImageBytes <= readPixelsInlineSizeLimit) {
-        pixelsStore = MallocSpan::tryMalloc(replyImageBytes);
-        if (pixelsStore)
-            pixels = pixelsStore.mutableSpan();
-    }
+    MallocSpan pixels;
+    if (replyImageBytes && replyImageBytes <= readPixelsInlineSizeLimit)
+        pixels = MallocSpan::tryZeroedMalloc(replyImageBytes);
 
     RefPtr context = m_context;
     std::optional readArea;
-    if (pixels.size() == replyImageBytes)
-        readArea = context->readPixelsWithStatus(rect, format, type, packReverseRowOrder, pixels);
+    if (pixels.sizeInBytes() == replyImageBytes)
+        readArea = context->readPixelsWithStatus(rect, format, type, packReverseRowOrder, pixels.mutableSpan());
     else
         context->addError(GCGLErrorCode::OutOfMemory);
-    if (!readArea) {
+    if (!readArea)
         pixels = { };
-        pixelsStore = { };
-    }
-    completionHandler(readArea, pixels);
+    completionHandler(readArea, pixels.span());
 }
 
 

From bb98ffd5c7336141c471e359b03b57d0f6de7142 Mon Sep 17 00:00:00 2001
From: Said Abou-Hallawa 
Date: Tue, 30 Jun 2026 05:57:23 -0700
Subject: [PATCH 12/84] FilterImage may return uninitialized PixelBuffer
 https://bugs.webkit.org/show_bug.cgi?id=314906 rdar://176813834

Reviewed by Darin Adler.

FilterImage provides three different representations of the image: ImageBuffer,
Unpremultiplied PixelBuffer and Premultiplied PixelBuffer. These three buffers
are lazily created. Each of them is created only when it is needed. But keep in
mind they are copies of each other. In other words, when one is created it has
to copy the pixels from the existing buffers. Otherwise it has to be zero-filled.

A problem may happen when copying the pixels from one buffer to a newly created
buffer fails. In this case we send uninitialized PixelBuffer which may expose user
private data. Not able to copy existing pixels to the PixelBuffer should be treated
as an error. So a null PixelBuffer should be returned in this case.

* Source/WebCore/platform/graphics/cpu/arm/filters/FEBlendNeonApplier.cpp:
(WebCore::FEBlendNeonApplier::apply const):
* Source/WebCore/platform/graphics/filters/FilterImage.cpp:
(WebCore::copyImageBytes):
(WebCore::FilterImage::pixelBuffer):
(WebCore::FilterImage::getPixelBuffer):
(WebCore::FilterImage::copyPixelBuffer):
* Source/WebCore/platform/graphics/filters/FilterImage.h:

Originally-landed-as: 305413.919@safari-7624-branch (dbf10417aa9e). rdar://180436188
Canonical link: https://commits.webkit.org/316126@main
---
 .../cpu/arm/filters/FEBlendNeonApplier.cpp    |  3 +
 .../platform/graphics/filters/FilterImage.cpp | 55 +++++++++++--------
 .../platform/graphics/filters/FilterImage.h   |  2 +-
 3 files changed, 36 insertions(+), 24 deletions(-)

diff --git a/Source/WebCore/platform/graphics/cpu/arm/filters/FEBlendNeonApplier.cpp b/Source/WebCore/platform/graphics/cpu/arm/filters/FEBlendNeonApplier.cpp
index 94c2f76eeb73..b504e89c3d20 100644
--- a/Source/WebCore/platform/graphics/cpu/arm/filters/FEBlendNeonApplier.cpp
+++ b/Source/WebCore/platform/graphics/cpu/arm/filters/FEBlendNeonApplier.cpp
@@ -187,6 +187,9 @@ bool FEBlendNeonApplier::apply(const Filter&, std::span>
     auto effectBDrawingRect = result.absoluteImageRectRelativeTo(input2);
     auto sourcePixelArrayB = input2.getPixelBuffer(AlphaPremultiplication::Premultiplied, effectBDrawingRect);
 
+    if (!sourcePixelArrayA || !sourcePixelArrayB)
+        return false;
+
     unsigned sourcePixelArrayLength = sourcePixelArrayA->bytes().size();
     ASSERT(sourcePixelArrayLength == sourcePixelArrayB->bytes().size());
 
diff --git a/Source/WebCore/platform/graphics/filters/FilterImage.cpp b/Source/WebCore/platform/graphics/filters/FilterImage.cpp
index cfdafbeef8ab..e78634c2b7df 100644
--- a/Source/WebCore/platform/graphics/filters/FilterImage.cpp
+++ b/Source/WebCore/platform/graphics/filters/FilterImage.cpp
@@ -149,25 +149,26 @@ ImageBuffer* FilterImage::imageBufferFromPixelBuffer()
     return m_imageBuffer.get();
 }
 
-static void copyImageBytes(const PixelBuffer& sourcePixelBuffer, PixelBuffer& destinationPixelBuffer)
+static bool copyImageBytes(const PixelBuffer& sourcePixelBuffer, PixelBuffer& destinationPixelBuffer)
 {
     ASSERT(sourcePixelBuffer.size() == destinationPixelBuffer.size());
 
     auto destinationSize = destinationPixelBuffer.size();
     auto rowBytes = CheckedUint32(destinationSize.width()) * 4;
     if (rowBytes.hasOverflowed()) [[unlikely]]
-        return;
+        return false;
 
     ConstPixelBufferConversionView source { sourcePixelBuffer.format(), rowBytes, sourcePixelBuffer.bytes() };
     PixelBufferConversionView destination { destinationPixelBuffer.format(), rowBytes, destinationPixelBuffer.bytes() };
 
     convertImagePixels(source, destination, destinationSize);
+    return true;
 }
 
-static void copyImageBytes(const PixelBuffer& sourcePixelBuffer, PixelBuffer& destinationPixelBuffer, const IntRect& sourceRect)
+static bool copyImageBytes(const PixelBuffer& sourcePixelBuffer, PixelBuffer& destinationPixelBuffer, const IntRect& sourceRect)
 {
-    auto sourcePixelBufferRect = IntRect { { }, sourcePixelBuffer.size() };
-    auto destinationPixelBufferRect = IntRect { { }, destinationPixelBuffer.size() };
+    const auto sourcePixelBufferRect = IntRect { { }, sourcePixelBuffer.size() };
+    const auto destinationPixelBufferRect = IntRect { { }, destinationPixelBuffer.size() };
 
     auto sourceRectClipped = intersection(sourcePixelBufferRect, sourceRect);
     auto destinationRect = IntRect { { }, sourceRectClipped.size() };
@@ -181,13 +182,9 @@ static void copyImageBytes(const PixelBuffer& sourcePixelBuffer, PixelBuffer& de
     destinationRect.intersect(destinationPixelBufferRect);
     sourceRectClipped.setSize(destinationRect.size());
 
-    // Initialize the destination to transparent black, if not entirely covered by the source.
-    if (destinationRect.size() != destinationPixelBufferRect.size())
-        destinationPixelBuffer.zeroFill();
-
     // Early return if the rect does not intersect with the source.
     if (destinationRect.isEmpty())
-        return;
+        return false;
 
     auto size = CheckedUint32(sourceRectClipped.width()) * 4;
     auto destinationBytesPerRow = CheckedUint32(destinationPixelBufferRect.width()) * 4;
@@ -196,7 +193,11 @@ static void copyImageBytes(const PixelBuffer& sourcePixelBuffer, PixelBuffer& de
     auto sourceOffset = sourceRectClipped.y() * sourceBytesPerRow + CheckedUint32(sourceRectClipped.x()) * 4;
 
     if (size.hasOverflowed() || destinationBytesPerRow.hasOverflowed() || sourceBytesPerRow.hasOverflowed() || destinationOffset.hasOverflowed() || sourceOffset.hasOverflowed()) [[unlikely]]
-        return;
+        return false;
+
+    // Initialize the destination to transparent black, if not entirely covered by the source.
+    if (destinationRect.size() != destinationPixelBufferRect.size())
+        destinationPixelBuffer.zeroFill();
 
     auto destinationPixel = destinationPixelBuffer.bytes().subspan(destinationOffset.value());
     auto sourcePixel = sourcePixelBuffer.bytes().subspan(sourceOffset.value());
@@ -208,6 +209,8 @@ static void copyImageBytes(const PixelBuffer& sourcePixelBuffer, PixelBuffer& de
         }
         memcpySpan(destinationPixel, sourcePixel.first(size));
     }
+
+    return true;
 }
 
 static RefPtr getConvertedPixelBuffer(ImageBuffer& imageBuffer, AlphaPremultiplication alphaFormat, const IntRect& sourceRect, DestinationColorSpace colorSpace, ImageBufferAllocator& allocator)
@@ -280,11 +283,15 @@ PixelBuffer* FilterImage::pixelBuffer(AlphaPremultiplication alphaFormat)
         return nullptr;
 
     if (alphaFormat == AlphaPremultiplication::Unpremultiplied) {
-        if (auto& sourcePixelBuffer = pixelBufferSlot(AlphaPremultiplication::Premultiplied))
-            copyImageBytes(*sourcePixelBuffer, *pixelBuffer);
+        if (auto& sourcePixelBuffer = pixelBufferSlot(AlphaPremultiplication::Premultiplied)) {
+            if (!copyImageBytes(*sourcePixelBuffer, *pixelBuffer))
+                return nullptr;
+        }
     } else {
-        if (auto& sourcePixelBuffer = pixelBufferSlot(AlphaPremultiplication::Unpremultiplied))
-            copyImageBytes(*sourcePixelBuffer, *pixelBuffer);
+        if (auto& sourcePixelBuffer = pixelBufferSlot(AlphaPremultiplication::Unpremultiplied)) {
+            if (!copyImageBytes(*sourcePixelBuffer, *pixelBuffer))
+                return nullptr;
+        }
     }
 
     return pixelBuffer.get();
@@ -300,11 +307,13 @@ RefPtr FilterImage::getPixelBuffer(AlphaPremultiplication alphaForm
     if (!pixelBuffer)
         return nullptr;
 
-    copyPixelBuffer(*pixelBuffer, sourceRect);
+    if (!copyPixelBuffer(*pixelBuffer, sourceRect))
+        return nullptr;
+
     return pixelBuffer;
 }
 
-void FilterImage::copyPixelBuffer(PixelBuffer& destinationPixelBuffer, const IntRect& sourceRect)
+bool FilterImage::copyPixelBuffer(PixelBuffer& destinationPixelBuffer, const IntRect& sourceRect)
 {
     auto alphaFormat = destinationPixelBuffer.format().alphaFormat;
     auto& colorSpace = destinationPixelBuffer.format().colorSpace;
@@ -317,8 +326,8 @@ void FilterImage::copyPixelBuffer(PixelBuffer& destinationPixelBuffer, const Int
             if (m_imageBuffer) {
                 IntRect rect { { }, m_absoluteImageRect.size() };
                 if (auto convertedPixelBuffer = getConvertedPixelBuffer(Ref { *m_imageBuffer }, alphaFormat, rect, colorSpace, m_allocator))
-                    copyImageBytes(*convertedPixelBuffer, destinationPixelBuffer, sourceRect);
-                return;
+                    return copyImageBytes(*convertedPixelBuffer, destinationPixelBuffer, sourceRect);
+                return false;
             }
         }
 
@@ -326,15 +335,15 @@ void FilterImage::copyPixelBuffer(PixelBuffer& destinationPixelBuffer, const Int
     }
 
     if (!sourcePixelBuffer)
-        return;
+        return false;
 
     if (requiresPixelBufferColorSpaceConversion(colorSpace)) {
         if (auto convertedPixelBuffer = getConvertedPixelBuffer(*sourcePixelBuffer, alphaFormat, colorSpace, m_allocator))
-            copyImageBytes(*convertedPixelBuffer, destinationPixelBuffer, sourceRect);
-        return;
+            return copyImageBytes(*convertedPixelBuffer, destinationPixelBuffer, sourceRect);
+        return false;
     }
 
-    copyImageBytes(*sourcePixelBuffer, destinationPixelBuffer, sourceRect);
+    return copyImageBytes(*sourcePixelBuffer, destinationPixelBuffer, sourceRect);
 }
 
 void FilterImage::correctPremultipliedPixelBuffer()
diff --git a/Source/WebCore/platform/graphics/filters/FilterImage.h b/Source/WebCore/platform/graphics/filters/FilterImage.h
index d8793955a65d..af8a531ddcf2 100644
--- a/Source/WebCore/platform/graphics/filters/FilterImage.h
+++ b/Source/WebCore/platform/graphics/filters/FilterImage.h
@@ -75,7 +75,7 @@ class FilterImage : public RefCounted {
     PixelBuffer* pixelBuffer(AlphaPremultiplication);
 
     RefPtr getPixelBuffer(AlphaPremultiplication, const IntRect& sourceRect, std::optional = std::nullopt);
-    void copyPixelBuffer(PixelBuffer& destinationPixelBuffer, const IntRect& sourceRect);
+    bool copyPixelBuffer(PixelBuffer& destinationPixelBuffer, const IntRect& sourceRect);
 
     void NODELETE correctPremultipliedPixelBuffer();
     void NODELETE transformToColorSpace(const DestinationColorSpace&);

From bd5f1e5b17d9fbc4bfc6e5d78e1f7d6153f7416a Mon Sep 17 00:00:00 2001
From: Anand Srinivasan 
Date: Tue, 30 Jun 2026 06:03:12 -0700
Subject: [PATCH 13/84] YarrJIT negativeOffsetIndexedAddress discards adjusted
 base register https://bugs.webkit.org/show_bug.cgi?id=312415 rdar://174714198

Reviewed by Yijia Huang.

In YarrJIT negativeOffsetIndexedAddress computes a negative offset from
a base address but the function mistakenly uses the original value
instead of the adjusted value. This patch fixes it to use the adjusted
value.

Test: JSTests/stress/yarr-negative-offset.js

* JSTests/stress/yarr-negative-offset.js: Added.
(catch):
* Source/JavaScriptCore/yarr/YarrJIT.cpp:

Originally-landed-as: 305413.685@safari-7624-branch (30c8460241f3). rdar://180436598
Canonical link: https://commits.webkit.org/316127@main
---
 JSTests/stress/yarr-negative-offset.js | 33 ++++++++++++++++++++++++++
 Source/JavaScriptCore/yarr/YarrJIT.cpp |  4 ++--
 2 files changed, 35 insertions(+), 2 deletions(-)
 create mode 100644 JSTests/stress/yarr-negative-offset.js

diff --git a/JSTests/stress/yarr-negative-offset.js b/JSTests/stress/yarr-negative-offset.js
new file mode 100644
index 000000000000..c9a51ec18814
--- /dev/null
+++ b/JSTests/stress/yarr-negative-offset.js
@@ -0,0 +1,33 @@
+//@ skip if $memoryLimited
+
+// YarrJIT OOB crash — page-aligned variant
+//
+// sizeof(StringImpl) = 0x14, page_size = 0x4000 (Apple Silicon 16K pages)
+// Solve: 0x14 + (K + 0x40000000)*2 ≡ 0 (mod 0x4000)  →  K = 0x1FF6
+//
+// Pattern: \u0100{K} [\u0100] \u0100{0x3FFFFFFF}    flags: y
+// Subject: "\u0100".repeat(K + 0x40000000)
+//
+// The OOB ldrh lands at alloc_base + page_size*N exactly → unmapped → SIGSEGV
+
+"use strict";
+
+const K     = 0x1FF6;
+const LEN   = K + 0x40000000;   // 0x40001FF6
+const QUANT = 0x3FFFFFFF;
+
+// allocate
+let s;
+try {
+    s = "\u0100".repeat(LEN);
+} catch (e) {
+    print("[!] OOM: " + e);
+    quit();
+}
+if(s.length !== 0x40001ff6) throw new Error("unexpected s.length "+s.length);
+
+let pattern = "\\u0100{" + K + "}[\\u0100]\\u0100{" + QUANT + "}";
+let re = new RegExp(pattern, "y");
+re.lastIndex = 0;
+
+re.test(s); // should SIGSEGV if the bug is present
diff --git a/Source/JavaScriptCore/yarr/YarrJIT.cpp b/Source/JavaScriptCore/yarr/YarrJIT.cpp
index 224ee1fb527d..749899cf2786 100644
--- a/Source/JavaScriptCore/yarr/YarrJIT.cpp
+++ b/Source/JavaScriptCore/yarr/YarrJIT.cpp
@@ -1326,9 +1326,9 @@ class YarrGenerator final : public YarrJITInfo {
         Checked characterOffset(-static_cast(negativeCharacterOffset));
 
         if (m_charSize == CharSize::Char8)
-            return MacroAssembler::BaseIndex(m_regs.input, indexReg, MacroAssembler::TimesOne, characterOffset * static_cast(sizeof(char)));
+            return MacroAssembler::BaseIndex(base, indexReg, MacroAssembler::TimesOne, characterOffset * static_cast(sizeof(char)));
 
-        return MacroAssembler::BaseIndex(m_regs.input, indexReg, MacroAssembler::TimesTwo, characterOffset * static_cast(sizeof(char16_t)));
+        return MacroAssembler::BaseIndex(base, indexReg, MacroAssembler::TimesTwo, characterOffset * static_cast(sizeof(char16_t)));
     }
 
 #if ENABLE(YARR_JIT_UNICODE_EXPRESSIONS)

From 0d6fe1662d2de50ca7f91db10a75541c19bf178d Mon Sep 17 00:00:00 2001
From: Kai Tamkun 
Date: Tue, 30 Jun 2026 06:04:14 -0700
Subject: [PATCH 14/84] [JSC] Spread operator doesn't account for
 cellButterflyOnlyAtomStringsStructure in DFG
 https://bugs.webkit.org/show_bug.cgi?id=313252 rdar://175498631

Reviewed by Yijia Huang.

This patch fixes the abstract interpreter's structure prediction for the spread operator.
Instead of only cellButterflyStructure(CopyOnWriteArrayWithContiguous), it now also takes
cellButterflyOnlyAtomStringsStructure into account.

Test: JSTests/stress/spread-with-OnlyAtomStringsStructure.js

* JSTests/stress/spread-with-OnlyAtomStringsStructure.js: Added.
(index):
* Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h:
(JSC::DFG::AbstractInterpreter::executeEffects):

Originally-landed-as: 305413.768@safari-7624-branch (62a1052dd1fe). rdar://180435430
Canonical link: https://commits.webkit.org/316128@main
---
 JSTests/stress/spread-with-OnlyAtomStringsStructure.js    | 6 ++++++
 Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h | 7 ++++++-
 2 files changed, 12 insertions(+), 1 deletion(-)
 create mode 100644 JSTests/stress/spread-with-OnlyAtomStringsStructure.js

diff --git a/JSTests/stress/spread-with-OnlyAtomStringsStructure.js b/JSTests/stress/spread-with-OnlyAtomStringsStructure.js
new file mode 100644
index 000000000000..da8a25a94426
--- /dev/null
+++ b/JSTests/stress/spread-with-OnlyAtomStringsStructure.js
@@ -0,0 +1,6 @@
+//@ runDefault("--forceEagerCompilation=1", "--validateAbstractInterpreterState=1")
+
+const array = [""];
+
+for (let index = 0; index < testLoopCount; index++)
+    (() => {})(...array);
diff --git a/Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h b/Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h
index 6cdbcebf28b0..38498a569112 100644
--- a/Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h
+++ b/Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h
@@ -3860,7 +3860,12 @@ bool AbstractInterpreter::executeEffects(unsigned clobberLimi
             break;
         }
 
-        setForNode(node, m_vm.cellButterflyStructure(CopyOnWriteArrayWithContiguous));
+        {
+            RegisteredStructureSet structureSet;
+            structureSet.add(m_graph.registerStructure(m_vm.cellButterflyStructure(CopyOnWriteArrayWithContiguous)));
+            structureSet.add(m_graph.registerStructure(m_vm.cellButterflyOnlyAtomStringsStructure.get()));
+            setForNode(node, structureSet);
+        }
         break;
         
     case NewArrayBuffer:

From 16b1bde3c1918236d0eefee504bcf4180566087f Mon Sep 17 00:00:00 2001
From: Zak Ridouh 
Date: Tue, 30 Jun 2026 06:05:48 -0700
Subject: [PATCH 15/84] [CoreIPC] [NP] Heap UAF in
 WebCore::IDBServer::MemoryIndexCursor reverse iterator when a Prev/Prevunique
 cursor's next-higher index entry is deleted
  

Reviewed by Sihui Liu.

MemoryIndexCursor caches an IndexValueStore::Iterator across IPC messages.
For Prev/Prevunique cursors this wraps std::set::reverse_iterator
objects (one for the outer IndexValueStore::m_orderedKeys set, one nested in
IndexValueEntry::Iterator for the inner per-index-key set). A
std::reverse_iterator stores a forward base() iterator pointing one element
past the logical position; for libc++ std::set that is a raw __tree_node*.

MemoryIndexCursor::indexValueChanged() only invalidates m_currentIterator
when the changed (key, primaryKey) equals the cursor's current logical
position. Deleting the next-higher index key (Variant A, outer set) or the
next-higher primary key under a shared index key (Variant B, inner set)
therefore frees exactly the __tree_node base() references while the guard
early-returns. The next IterateCursor IPC executes ++m_reverseIterator =>
--base() => __tree_prev_iter(freed_node), a heap-use-after-free in
com.apple.WebKit.Networking reachable from a compromised WebContent process
via NetworkStorageManager IPC with WCP-controlled
IDBDatabaseIdentifier.m_isTransient = true forcing MemoryIDBBackingStore.

Fix by gating the equality early-return on info().isDirectionForward(). For
reverse cursors we now invalidate m_currentIterator on any index mutation;
iterate() already re-seeks via reverseFind(m_currentKey, m_currentPrimaryKey)
when the iterator is invalid. Both removeEntriesWithValueKey() erase paths
(outer m_orderedKeys.erase() and inner IndexValueEntry::removeKey()) reach
indexValueChanged() through MemoryIndex::notifyCursorsOfValueChange(), so
both variants are closed.

* LayoutTests/storage/indexeddb/index-cursor-reverse-delete-next-higher-key-private-expected.txt: Added.
* LayoutTests/storage/indexeddb/index-cursor-reverse-delete-next-higher-key-private.html: Added.
* Source/WebCore/Modules/indexeddb/server/MemoryIndexCursor.cpp:
(WebCore::IDBServer::MemoryIndexCursor::indexValueChanged):

Originally-landed-as: 305413.842@safari-7624-branch (d14d12e915f3). rdar://180436983
Canonical link: https://commits.webkit.org/316129@main
---
 ...elete-next-higher-key-private-expected.txt | 16 ++++
 ...everse-delete-next-higher-key-private.html | 86 +++++++++++++++++++
 .../indexeddb/server/MemoryIndexCursor.cpp    |  7 +-
 3 files changed, 108 insertions(+), 1 deletion(-)
 create mode 100644 LayoutTests/storage/indexeddb/index-cursor-reverse-delete-next-higher-key-private-expected.txt
 create mode 100644 LayoutTests/storage/indexeddb/index-cursor-reverse-delete-next-higher-key-private.html

diff --git a/LayoutTests/storage/indexeddb/index-cursor-reverse-delete-next-higher-key-private-expected.txt b/LayoutTests/storage/indexeddb/index-cursor-reverse-delete-next-higher-key-private-expected.txt
new file mode 100644
index 000000000000..df3122fe29ba
--- /dev/null
+++ b/LayoutTests/storage/indexeddb/index-cursor-reverse-delete-next-higher-key-private-expected.txt
@@ -0,0 +1,16 @@
+Tests that a reverse index cursor doesn't use-after-free when a record is deleted whose index key is the next-higher value from the cursor's current position.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+indexedDB = self.indexedDB || self.webkitIndexedDB || self.mozIndexedDB || self.msIndexedDB || self.OIndexedDB;
+
+PASS cursorKeys.length is 3
+PASS cursorKeys[0] is 30
+PASS cursorKeys[1] is 20
+PASS cursorKeys[2] is 10
+Transaction completed successfully.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/storage/indexeddb/index-cursor-reverse-delete-next-higher-key-private.html b/LayoutTests/storage/indexeddb/index-cursor-reverse-delete-next-higher-key-private.html
new file mode 100644
index 000000000000..6ea56d78bdeb
--- /dev/null
+++ b/LayoutTests/storage/indexeddb/index-cursor-reverse-delete-next-higher-key-private.html
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/WebCore/Modules/indexeddb/server/MemoryIndexCursor.cpp b/Source/WebCore/Modules/indexeddb/server/MemoryIndexCursor.cpp
index 99773da3464b..f054bb5150fd 100644
--- a/Source/WebCore/Modules/indexeddb/server/MemoryIndexCursor.cpp
+++ b/Source/WebCore/Modules/indexeddb/server/MemoryIndexCursor.cpp
@@ -221,7 +221,12 @@ void MemoryIndexCursor::indexRecordsAllChanged()
 
 void MemoryIndexCursor::indexValueChanged(const IDBKeyData& key, const IDBKeyData& primaryKey)
 {
-    if (m_currentKey != key || m_currentPrimaryKey != primaryKey)
+    // For Prev/Prevunique cursors, m_currentIterator wraps std::set reverse_iterators
+    // whose stored base() points one element past the logical position. Erasing that
+    // adjacent element leaves base() dangling even though m_currentKey/m_currentPrimaryKey
+    // are unchanged, so for reverse cursors we must invalidate on any index mutation
+    // and let iterate() re-seek via reverseFind().
+    if (info().isDirectionForward() && (m_currentKey != key || m_currentPrimaryKey != primaryKey))
         return;
 
     m_currentIterator.invalidate();

From 8d4c7092266659c6cf81d92ba23176feb8852178 Mon Sep 17 00:00:00 2001
From: Kimmo Kinnunen 
Date: Tue, 30 Jun 2026 06:07:07 -0700
Subject: [PATCH 16/84] ANGLE: Metal: Inconsistent implementation of various
 DrawElements variations https://bugs.webkit.org/show_bug.cgi?id=312470
 rdar://174919762
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Reviewed by Dan Glastonbury.

The implementation of DrawElements would be inconsistent:

The implementation has multiple stages of possible intermediate buffers.
At various stages, offsets and counts were mixed between incorrect
stages.

Historically the confusions were partly due to the logic loading
the primitive ranges sometimes from generate intermediary buffers,
sometimes from the original client-provided buffer. This was partly
fixed to load only from client-provided buffers and client arrays
in "WebGL: consecutive UNSIGNED_BYTE drawElements() draw in wrong
location", but this caused a regression by other part of the code
using counts from incorrect stages.

The implementation strategy would not working for non-strip topologies,
such as GL_TRIANGLES, in primitive restart cases when provoking vertex
rewrite was needed. The provoking vertex rewrite operates per primitive.
For strip topologies, this works as each vertex index also defines a
primitive. For non-strip topologies, this is not possible as the
provoking vertex generation shader cannot know where the primitive
starts. Example: GL_TRIANGLES of {0, ff, 1, 2, 3, 4} would be dispatched
as primitives starting at 0 and 2, but the proper dispatch list
would contain just primitive at 1.

Furthermore, the logic to determine drawn index ranges was based on
scanning for primitive restart ranges, and then converting those
to draw index ranges. This algorithm was brittle and contained
quite many confusions.

Fix by:
- Resolve the draw index ranges by resolving the draw index ranges
  instead of restart ranges. These index ranges are then intersected
  with the client provided offset and count, which define the first
  index, last index range pair.
- Consistently resolve the draw index ranges ƒrom the client buffers.
- Keep all intermediate buffers consistent with the first index, count:
  the intermediate buffers always have the similar unused prefix
  than the original client-provided index buffer.
- Keep all the intermediate buffers offsets separated from the
  client-provided offset (first index). The intermediate buffers are
  allocated from the buffer pool, so they're not separate buffers but
  buffer, offset pairs. Keep these buffer, offset pairs separate from
  the client first index
- For non-strip topology draws, provide the draw ranges already to the
  provoking vertex shader helper. The provoking vertex shader is
  only run for the real draw indices instead of the full draw.
  This allows the shader to function correctly.

* Source/ThirdParty/ANGLE/ANGLE.xcodeproj/project.pbxproj:
* Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/BufferMtl.h:
(rx::DrawIndexRange::DrawIndexRange):
(rx::BufferMtl::DrawIndexRangeCache::DrawIndexRangeCache):
(rx::IndexRange::IndexRange): Deleted.
(rx::BufferMtl::RestartRangeCache::RestartRangeCache): Deleted.
* Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/BufferMtl.mm:
(rx::BufferMtl::markConversionBuffersDirty):
(rx::BufferMtl::clearConversionBuffers):
(rx::CalculateDrawIndexRanges):
(rx::BufferMtl::getDrawIndexRanges):
(rx::BufferMtl::GetDrawIndexRangesFromClientData):
(rx::IndexConversionBufferMtl::getRangeForConvertedBuffer): Deleted.
(rx::CalculateRestartRanges): Deleted.
(rx::BufferMtl::getRestartIndices): Deleted.
(rx::BufferMtl::GetRestartIndicesFromClientData): Deleted.
* Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ContextMtl.h:
(rx::ContextMtl::getProvokingVertexHelper):
* Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ContextMtl.mm:
(rx::ContextMtl::drawElementsImpl):
* Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ProvokingVertexHelper.h:
* Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ProvokingVertexHelper.mm:
(rx::ProvokingVertexHelper::preconditionIndexBuffer):
(rx::ProvokingVertexHelper::generateIndexBuffer):
* Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/VertexArrayMtl.h:
* Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/VertexArrayMtl.mm:
(rx::AppendDrawCommands):
(rx::AppendDrawCommandRanges):
(rx::VertexArrayMtl::resolveDrawElementsDraw):
(rx::VertexArrayMtl::getIndexBuffer): Deleted.
(rx::VertexArrayMtl::getDrawIndices): Deleted.
* Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/VertexArrayMtl_unittest.mm: Added.
* Source/ThirdParty/ANGLE/src/tests/gl_tests/DrawElementsTest.cpp:
(angle::convertIndexBufferContents):

Originally-landed-as: 305413.713@safari-7624-branch (23eba5e02916). rdar://180437073
Canonical link: https://commits.webkit.org/316130@main
---
 Source/ThirdParty/ANGLE/ANGLE.xcodeproj/project.pbxproj | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/Source/ThirdParty/ANGLE/ANGLE.xcodeproj/project.pbxproj b/Source/ThirdParty/ANGLE/ANGLE.xcodeproj/project.pbxproj
index 643352fd41ab..9f220a9cfb6c 100644
--- a/Source/ThirdParty/ANGLE/ANGLE.xcodeproj/project.pbxproj
+++ b/Source/ThirdParty/ANGLE/ANGLE.xcodeproj/project.pbxproj
@@ -479,6 +479,7 @@
 		7B8EC8472DF2E5FC00105EB6 /* EnsureLoopForwardProgress.h in Headers */ = {isa = PBXBuildFile; fileRef = 7B8EC8442DF2E5FC00105EB6 /* EnsureLoopForwardProgress.h */; };
 		7B8EC8482DF2E5FC00105EB6 /* EnsureLoopForwardProgress.h in Headers */ = {isa = PBXBuildFile; fileRef = 7B8EC8442DF2E5FC00105EB6 /* EnsureLoopForwardProgress.h */; };
 		7B8EC8492DF2E5FC00105EB6 /* EnsureLoopForwardProgress.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 7B8EC8452DF2E5FC00105EB6 /* EnsureLoopForwardProgress.cpp */; };
+		7B91C9A82F8CE63A00420427 /* VertexArrayMtl_unittest.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7B91C9A62F8CDB5400420427 /* VertexArrayMtl_unittest.mm */; };
 		7B9A99BA2DDC685100160B6E /* ReduceInterfaceBlocks.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 7B9A99B92DDC685100160B6E /* ReduceInterfaceBlocks.cpp */; };
 		7B9A99BB2DDC685100160B6E /* ReduceInterfaceBlocks.h in Headers */ = {isa = PBXBuildFile; fileRef = 7B9A99B82DDC685100160B6E /* ReduceInterfaceBlocks.h */; };
 		7B9A99BC2DDC685100160B6E /* ReduceInterfaceBlocks.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 7B9A99B92DDC685100160B6E /* ReduceInterfaceBlocks.cpp */; };
@@ -2402,6 +2403,7 @@
 		7B8EC83F2DF1BC9100105EB6 /* ImageTestMetal.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ImageTestMetal.mm; sourceTree = ""; };
 		7B8EC8442DF2E5FC00105EB6 /* EnsureLoopForwardProgress.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EnsureLoopForwardProgress.h; sourceTree = ""; };
 		7B8EC8452DF2E5FC00105EB6 /* EnsureLoopForwardProgress.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = EnsureLoopForwardProgress.cpp; sourceTree = ""; };
+		7B91C9A62F8CDB5400420427 /* VertexArrayMtl_unittest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = VertexArrayMtl_unittest.mm; sourceTree = ""; };
 		7B9829372E0EB05200F1E9FB /* ANGLETranslator.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ANGLETranslator.xcconfig; sourceTree = ""; };
 		7B9A99B82DDC685100160B6E /* ReduceInterfaceBlocks.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ReduceInterfaceBlocks.h; sourceTree = ""; };
 		7B9A99B92DDC685100160B6E /* ReduceInterfaceBlocks.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ReduceInterfaceBlocks.cpp; sourceTree = ""; };
@@ -4711,6 +4713,7 @@
 				FF81FEA425818D6800894E24 /* TransformFeedbackMtl.mm */,
 				FF81FE9725818D6800894E24 /* VertexArrayMtl.h */,
 				FF81FE9A25818D6800894E24 /* VertexArrayMtl.mm */,
+				7B91C9A62F8CDB5400420427 /* VertexArrayMtl_unittest.mm */,
 			);
 			path = metal;
 			sourceTree = "";
@@ -6547,6 +6550,7 @@
 				7BFAF5C12DE59E0900327F7F /* UniformTest.cpp in Sources */,
 				7BFAF6062DE59E0900327F7F /* UnpackAlignmentTest.cpp in Sources */,
 				7BFAF5F22DE59E0900327F7F /* UnpackRowLength.cpp in Sources */,
+				7B91C9A82F8CE63A00420427 /* VertexArrayMtl_unittest.mm in Sources */,
 				7BFAF5FE2DE59E0900327F7F /* VertexAttributeTest.cpp in Sources */,
 				7BFAF5FC2DE59E0900327F7F /* ViewportTest.cpp in Sources */,
 				7BFAF6152DE59E2300327F7F /* WebGLCompatibilityTest.cpp in Sources */,

From 543f99d9d38bc37514d8cb3a4e03f66e41e5089d Mon Sep 17 00:00:00 2001
From: Tadeu Zagallo 
Date: Tue, 30 Jun 2026 06:08:55 -0700
Subject: [PATCH 17/84] [WebGPU] signed-negation overflow on
 baseVertex==INT32_MIN in RenderPassEncoder::clampIndexBufferToValidValues
 https://bugs.webkit.org/show_bug.cgi?id=315196 rdar://176882934

Reviewed by Mike Wyrzykowski.

RenderPassEncoder::clampIndexBufferToValidValues guards baseVertex magnitude with
-baseVertex > static_cast(minVertexCount), but the unary negation runs
in 32-bit signed arithmetic before the comparison widens, so baseVertex == INT32_MIN
wraps to itself and passes the guard. The fix widens baseVertex to int64_t before
negation and sign extension preserves all valid negative baseVertex values, so the
guard's behaviour is unchanged for every input except INT32_MIN, whose magnitude (2^31)
is now representable and correctly rejected.

Test: fast/webgpu/nocrash/fuzz-176882934.html

* LayoutTests/fast/webgpu/nocrash/fuzz-176882934-expected.txt: Added.
* LayoutTests/fast/webgpu/nocrash/fuzz-176882934.html: Added.
* Source/WebGPU/WebGPU/RenderPassEncoder.mm:
(WebGPU::RenderPassEncoder::clampIndexBufferToValidValues):

Originally-landed-as: 305413.952@safari-7624-branch (04553772a362). rdar://180436823
Canonical link: https://commits.webkit.org/316131@main
---
 .../nocrash/fuzz-176882934-expected.txt       |  2 +
 .../fast/webgpu/nocrash/fuzz-176882934.html   | 60 +++++++++++++++++++
 Source/WebGPU/WebGPU/RenderPassEncoder.mm     |  3 +-
 3 files changed, 64 insertions(+), 1 deletion(-)
 create mode 100644 LayoutTests/fast/webgpu/nocrash/fuzz-176882934-expected.txt
 create mode 100644 LayoutTests/fast/webgpu/nocrash/fuzz-176882934.html

diff --git a/LayoutTests/fast/webgpu/nocrash/fuzz-176882934-expected.txt b/LayoutTests/fast/webgpu/nocrash/fuzz-176882934-expected.txt
new file mode 100644
index 000000000000..0533f45490c1
--- /dev/null
+++ b/LayoutTests/fast/webgpu/nocrash/fuzz-176882934-expected.txt
@@ -0,0 +1,2 @@
+Pass
+
diff --git a/LayoutTests/fast/webgpu/nocrash/fuzz-176882934.html b/LayoutTests/fast/webgpu/nocrash/fuzz-176882934.html
new file mode 100644
index 000000000000..a1c824b04dfe
--- /dev/null
+++ b/LayoutTests/fast/webgpu/nocrash/fuzz-176882934.html
@@ -0,0 +1,60 @@
+
+
+
diff --git a/Source/WebGPU/WebGPU/RenderPassEncoder.mm b/Source/WebGPU/WebGPU/RenderPassEncoder.mm
index 12eef5366bc3..620b6d36813b 100644
--- a/Source/WebGPU/WebGPU/RenderPassEncoder.mm
+++ b/Source/WebGPU/WebGPU/RenderPassEncoder.mm
@@ -771,7 +771,8 @@ static void setViewportMinMaxDepthIntoBuffer(auto& fragmentDynamicOffsets, float
 
     uint32_t indexSizeInBytes = indexType == MTLIndexTypeUInt16 ? sizeof(uint16_t) : sizeof(uint32_t);
     uint32_t firstIndex = indexBufferOffsetInBytes / indexSizeInBytes;
-    if (!minVertexCount || !minInstanceCount || indexBufferOffsetInBytes >= indexBuffer.length || -baseVertex > static_cast(minVertexCount) || baseVertex > static_cast(minVertexCount))
+    int64_t baseVertex64 = baseVertex;
+    if (!minVertexCount || !minInstanceCount || indexBufferOffsetInBytes >= indexBuffer.length || -baseVertex64 > static_cast(minVertexCount) || baseVertex64 > static_cast(minVertexCount))
         return DrawIndexResult { IndexCall::Skip, nil, 0 };
 
     auto primitiveOffset = primitiveType == MTLPrimitiveTypeLineStrip || primitiveType == MTLPrimitiveTypeTriangleStrip ? 1u : 0u;

From 20d1bae11e90cb09967c16480965584fc14b89c6 Mon Sep 17 00:00:00 2001
From: Vignesh Rao 
Date: Tue, 30 Jun 2026 06:10:07 -0700
Subject: [PATCH 18/84] [JSC] Unconditionally keep OMGOSREntryCallee alive
 while updating its callsites https://bugs.webkit.org/show_bug.cgi?id=313063
 rdar://174492346

Reviewed by Keith Miller.

After a re-tier, when a fresh BBQCallee replaces a retired one,
m_osrEntryCallees may hold a stale weak ref to an OMGOSREntryCallee not owned
by the current BBQCallee. Currently, updateCallsitesToCallUs will not track
this since it assumes that this OMGOSREntryCallee will be owned by the BBQCallee

This patch fixes it by unconditionally keeping the OMGOSREntryCallee alive
while we update all the callsites within it. The assert in ~BBQCallee is void
now since an OMGOSREntryCallee can now have multiple owners.

* Source/JavaScriptCore/wasm/WasmCallee.cpp:
(JSC::Wasm::BBQCallee::~BBQCallee):
* Source/JavaScriptCore/wasm/WasmCalleeGroup.cpp:
(JSC::Wasm::CalleeGroup::updateCallsitesToCallUs):

Originally-landed-as: 305413.784@safari-7624-branch (84a5f91f00a5). rdar://180437899
Canonical link: https://commits.webkit.org/316132@main
---
 Source/JavaScriptCore/wasm/WasmCallee.cpp     |  1 -
 .../JavaScriptCore/wasm/WasmCalleeGroup.cpp   | 27 ++++---------------
 2 files changed, 5 insertions(+), 23 deletions(-)

diff --git a/Source/JavaScriptCore/wasm/WasmCallee.cpp b/Source/JavaScriptCore/wasm/WasmCallee.cpp
index 778b440e9a1e..7a6a8ade547e 100644
--- a/Source/JavaScriptCore/wasm/WasmCallee.cpp
+++ b/Source/JavaScriptCore/wasm/WasmCallee.cpp
@@ -585,7 +585,6 @@ unsigned OptimizingJITCallee::computeCodeHashImpl() const
 BBQCallee::~BBQCallee()
 {
     if (Options::freeRetiredWasmCode() && m_osrEntryCallee) {
-        ASSERT(m_osrEntryCallee->hasOneRef());
         m_osrEntryCallee->reportToVMsForDestruction();
     }
 }
diff --git a/Source/JavaScriptCore/wasm/WasmCalleeGroup.cpp b/Source/JavaScriptCore/wasm/WasmCalleeGroup.cpp
index 4cf13d7638e8..14cc14c6d209 100644
--- a/Source/JavaScriptCore/wasm/WasmCalleeGroup.cpp
+++ b/Source/JavaScriptCore/wasm/WasmCalleeGroup.cpp
@@ -346,14 +346,9 @@ void CalleeGroup::updateCallsitesToCallUs(const AbstractLocker& locker, CodeLoca
 
     // This is necessary since Callees are released under `Heap::stopThePeriphery()`, but that only stops JS compiler
     // threads and not wasm ones. So a weakly held BBQCallee and its OMGOSREntryCallee could die between the time we
-    // collect the callsites and when we actually repatch its callsites. Since BBQCallee owns OMGOSREntryCallee,
-    // keeping BBQCallee alive is enough to ensure that both are alive for the required duration.
-    //
-    // There is however an edge case here - it can happen that a BBQCallee has been freed but its OMGOSREntryCallee
-    // has been added to the pending-destruction set and not yet free'd. This means that m_osrEntryCallees will still
-    // hold a weak ref to it. In this scenario, BBQCallee won't be kept alive since it does not exist so we manually
-    // have to keep the OMGOSREntryCallee alive separately. This should only be done in this scenario else we will
-    // end up with multiple owners for OMGOSREntryCallee.
+    // collect the callsites and when we actually repatch its callsites. Additionally, after a re-tier (where a fresh
+    // BBQCallee replaces a retired one), m_osrEntryCallees may hold a stale weak ref to an OMGOSREntryCallee not
+    // owned by the current BBQCallee, so we always keep it alive unconditionally.
 
     // FIXME: These inline capacities were picked semi-randomly. We should figure out if there's a better number.
     Vector, 4> keepAliveBBQCallees;
@@ -382,8 +377,6 @@ void CalleeGroup::updateCallsitesToCallUs(const AbstractLocker& locker, CodeLoca
         if (!tuple)
             return;
 
-        bool bbqCalleeKeptAlive = false;
-        UNUSED_VARIABLE(bbqCalleeKeptAlive);
 #if ENABLE(WEBASSEMBLY_BBQJIT)
         // This callee could be weak but we still need to update it since it could call our BBQ callee
         // that we're going to want to destroy.
@@ -396,25 +389,15 @@ void CalleeGroup::updateCallsitesToCallUs(const AbstractLocker& locker, CodeLoca
             collectCallsites(bbqCallee.get());
             ASSERT(!bbqCallee->osrEntryCallee() || m_osrEntryCallees.find(callerIndex) != m_osrEntryCallees.end());
             keepAliveBBQCallees.append(bbqCallee.releaseNonNull());
-            bbqCalleeKeptAlive = true;
         }
 #endif
 #if ENABLE(WEBASSEMBLY_OMGJIT)
         collectCallsites(tuple->m_omgCallee.get());
         if (auto iter = m_osrEntryCallees.find(callerIndex); iter != m_osrEntryCallees.end()) {
             if (RefPtr callee = iter->value.get()) {
+                // Since there is a OMGOSREntryCallee, we need to collect all the callsites there and also keep it alive until we patch it.
                 collectCallsites(callee.get());
-                // If we track the OMGOSREntryCallee as a callsite there are 2 possibilities -
-                // 1. The BBQCallee is already being tracked - in this case we don't have to
-                //    track the OMGOSREntryCallee since the BBQCallee owns it and keeping the
-                //    BBQCallee alive is good enough to keep the OMGOSREntryCallee alive. Also,
-                //    OMGOSREntryCallee is only supposed to be owned by BBQCallee
-                // 2. The BBQCallee is not tracked - This happens if the BBQCallee is already
-                //    released but the OMGOSREntryCallee is still alive. In this case there is
-                //    no other strong reference to OMGOSREntryCallee so we have to keep it
-                //    alive here.
-                if (!bbqCalleeKeptAlive)
-                    keepAliveOSREntryCallees.append(callee.releaseNonNull());
+                keepAliveOSREntryCallees.append(callee.releaseNonNull());
             } else
                 m_osrEntryCallees.remove(iter);
         }

From e2b88c2f80488fed156450007f317f5ff0e564f4 Mon Sep 17 00:00:00 2001
From: Anthony Tarbinian 
Date: Tue, 30 Jun 2026 06:11:22 -0700
Subject: [PATCH 19/84] Reject IndexedDB transactions during version change
 operation https://bugs.webkit.org/show_bug.cgi?id=312391 rdar://173799670

Reviewed by Sihui Liu.

When an IndexedDB connection is closed while a version
change transaction is active, UniqueIDBDatabase::connectionClosedFromClient
successfully closes the version change transaction however it does
not clear any other pending transactions due to an early return
when clearing version change transactions.

Instead of being defensive and clearing the pending transaciton
in the case of the early return, we should reject any incoming
transactions which come on a version change connection.

Only whenever a version change operation completes, can
a new transaction be established. So, this patch rejects any
incoming transactions which come while a version change operation
is active.

The IndexedDB spec describes the steps for starting a new transaction:

	The transaction(storeNames, mode, options) method steps are:
	1. If a live upgrade transaction is associated with the connection, throw an "InvalidStateError" DOMException.

https://w3c.github.io/IndexedDB/#dom-idbdatabase-transaction

This check already happens in the web process during
IDBDatabase::transaction where it returns an "InvalidStateError"
in this state.

However, this doesn't account for a compromised web process
who avoids this client side check and makes it across IPC
to invoke transaction creation in the NetworkProcess.

This patch adds a check for this scenario of a comprimised web
process who manages to start a new transaction during a
version change, if this is detected, we return early and
don't move forward with creating the transaction

This patch checks that the current connection isn't the
stored UniqueIDBDatabase::m_versionChangeDatabaseConnection.
That member variable will point to the active version change connection,
if any, and it is cleared when the version change completes.
It also checks if m_versionChangeTransaction is non-null
since it will only be null once the version change transaction
is completed.

* LayoutTests/ipc/networksindexeddb-close-connection-during-version-change-expected.txt: Added.
* LayoutTests/ipc/networksindexeddb-close-connection-during-version-change.html: Added.
* Source/WebCore/Modules/indexeddb/server/UniqueIDBDatabase.cpp:
(WebCore::IDBServer::UniqueIDBDatabase::isVersionChangeTransactionActive const):
* Source/WebCore/Modules/indexeddb/server/UniqueIDBDatabase.h:
* Source/WebKit/NetworkProcess/storage/NetworkStorageManager.cpp:
(WebKit::NetworkStorageManager::establishTransaction):
(WebKit::NetworkStorageManager::databaseConnectionPendingClose):

Originally-landed-as: 305413.734@safari-7624-branch (6b2393e40648). rdar://180437837
Canonical link: https://commits.webkit.org/316133@main
---
 ...nection-during-version-change-expected.txt |   1 +
 ...lose-connection-during-version-change.html | 117 ++++++++++++++++++
 .../indexeddb/server/UniqueIDBDatabase.cpp    |   5 +
 .../indexeddb/server/UniqueIDBDatabase.h      |   1 +
 .../storage/NetworkStorageManager.cpp         |  12 +-
 5 files changed, 134 insertions(+), 2 deletions(-)
 create mode 100644 LayoutTests/ipc/networksindexeddb-close-connection-during-version-change-expected.txt
 create mode 100644 LayoutTests/ipc/networksindexeddb-close-connection-during-version-change.html

diff --git a/LayoutTests/ipc/networksindexeddb-close-connection-during-version-change-expected.txt b/LayoutTests/ipc/networksindexeddb-close-connection-during-version-change-expected.txt
new file mode 100644
index 000000000000..654ddf7f17ef
--- /dev/null
+++ b/LayoutTests/ipc/networksindexeddb-close-connection-during-version-change-expected.txt
@@ -0,0 +1 @@
+This test passes if it does not crash.
diff --git a/LayoutTests/ipc/networksindexeddb-close-connection-during-version-change.html b/LayoutTests/ipc/networksindexeddb-close-connection-during-version-change.html
new file mode 100644
index 000000000000..c692c667b7f7
--- /dev/null
+++ b/LayoutTests/ipc/networksindexeddb-close-connection-during-version-change.html
@@ -0,0 +1,117 @@
+
+This test passes if it does not crash.
+
diff --git a/Source/WebCore/Modules/indexeddb/server/UniqueIDBDatabase.cpp b/Source/WebCore/Modules/indexeddb/server/UniqueIDBDatabase.cpp
index 7fea86ececa1..5e2024f19b39 100644
--- a/Source/WebCore/Modules/indexeddb/server/UniqueIDBDatabase.cpp
+++ b/Source/WebCore/Modules/indexeddb/server/UniqueIDBDatabase.cpp
@@ -1379,6 +1379,11 @@ void UniqueIDBDatabase::connectionClosedFromClient(UniqueIDBDatabaseConnection&
     handleTransactions();
 }
 
+bool UniqueIDBDatabase::isVersionChangeTransactionActive(const UniqueIDBDatabaseConnection& connection) const
+{
+    return m_versionChangeDatabaseConnection == &connection && m_versionChangeTransaction;
+}
+
 void UniqueIDBDatabase::connectionClosedFromServer(UniqueIDBDatabaseConnection& connection)
 {
     ASSERT(!isMainThread());
diff --git a/Source/WebCore/Modules/indexeddb/server/UniqueIDBDatabase.h b/Source/WebCore/Modules/indexeddb/server/UniqueIDBDatabase.h
index b934e0720987..5a49d2aaa7b5 100644
--- a/Source/WebCore/Modules/indexeddb/server/UniqueIDBDatabase.h
+++ b/Source/WebCore/Modules/indexeddb/server/UniqueIDBDatabase.h
@@ -110,6 +110,7 @@ class UniqueIDBDatabase final : public CanMakeWeakPtr, public
 
     void didFinishHandlingVersionChange(UniqueIDBDatabaseConnection&, const IDBResourceIdentifier& transactionIdentifier);
     void connectionClosedFromClient(UniqueIDBDatabaseConnection&);
+    WEBCORE_EXPORT bool isVersionChangeTransactionActive(const UniqueIDBDatabaseConnection&) const;
     void didFireVersionChangeEvent(UniqueIDBDatabaseConnection&, const IDBResourceIdentifier& requestIdentifier, IndexedDB::ConnectionClosedOnBehalfOfServer);
     WEBCORE_EXPORT void openDBRequestCancelled(const IDBResourceIdentifier& requestIdentifier);
 
diff --git a/Source/WebKit/NetworkProcess/storage/NetworkStorageManager.cpp b/Source/WebKit/NetworkProcess/storage/NetworkStorageManager.cpp
index 635c30272863..25ee7e11ce95 100644
--- a/Source/WebKit/NetworkProcess/storage/NetworkStorageManager.cpp
+++ b/Source/WebKit/NetworkProcess/storage/NetworkStorageManager.cpp
@@ -2045,8 +2045,16 @@ void NetworkStorageManager::deleteDatabase(IPC::Connection& connection, const We
 
 void NetworkStorageManager::establishTransaction(IPC::Connection& ipcConnection, WebCore::IDBDatabaseConnectionIdentifier databaseConnectionIdentifier, const WebCore::IDBTransactionInfo& transactionInfo)
 {
-    if (RefPtr connection = m_idbStorageRegistry->connection(databaseConnectionIdentifier, ipcConnection))
-        connection->establishTransaction(transactionInfo);
+    RefPtr databaseConnection = m_idbStorageRegistry->connection(databaseConnectionIdentifier, ipcConnection);
+    if (!databaseConnection)
+        return;
+
+    // FIXME: consider converting this early return to MESSAGE_CHECK.
+    CheckedPtr database = databaseConnection->database();
+    if (database && database->isVersionChangeTransactionActive(*databaseConnection))
+        return;
+
+    databaseConnection->establishTransaction(transactionInfo);
 }
 
 void NetworkStorageManager::databaseConnectionPendingClose(IPC::Connection& ipcConnection, WebCore::IDBDatabaseConnectionIdentifier databaseConnectionIdentifier)

From aad3188c8a8d1f828860100602581ee568aa24d5 Mon Sep 17 00:00:00 2001
From: Tadeu Zagallo 
Date: Tue, 30 Jun 2026 06:12:46 -0700
Subject: [PATCH 20/84] [WebGPU] Stale WeakPtr comparison in
 BindGroupLayout::errorValidatingBindGroupCompatibility allows incompatible
 auto/explicit layout pairing https://bugs.webkit.org/show_bug.cgi?id=315496
 rdar://176812014

Reviewed by Mike Wyrzykowski.

We were using weak pointers to compare the autogenerated pipeline layouts when
checking for compatibility of two bind groups, however the weak pointer can be
null in two scenarios: when the layout is explicit or when the pipeline layout
has been destroyed. That could result in incorrectly considering two bind groups
compatible when one is pipeline layout is explicit and the other has been destroyed.
In order to avoid that we explicitly check whether both bind groups use auto
generated layouts.

Test: fast/webgpu/regression/repro_176812014.html

* LayoutTests/fast/webgpu/regression/repro_176812014-expected.txt: Added.
* LayoutTests/fast/webgpu/regression/repro_176812014.html: Added.
* Source/WebGPU/WebGPU/BindGroupLayout.mm:
(WebGPU::BindGroupLayout::errorValidatingBindGroupCompatibility const):

Originally-landed-as: 305413.964@safari-7624-branch (48e38ee5acbc). rdar://180428905
Canonical link: https://commits.webkit.org/316134@main
---
 .../regression/repro_176812014-expected.txt   |  2 +
 .../webgpu/regression/repro_176812014.html    | 73 +++++++++++++++++++
 Source/WebGPU/WebGPU/BindGroupLayout.mm       |  2 +-
 3 files changed, 76 insertions(+), 1 deletion(-)
 create mode 100644 LayoutTests/fast/webgpu/regression/repro_176812014-expected.txt
 create mode 100644 LayoutTests/fast/webgpu/regression/repro_176812014.html

diff --git a/LayoutTests/fast/webgpu/regression/repro_176812014-expected.txt b/LayoutTests/fast/webgpu/regression/repro_176812014-expected.txt
new file mode 100644
index 000000000000..ee4a6c18face
--- /dev/null
+++ b/LayoutTests/fast/webgpu/regression/repro_176812014-expected.txt
@@ -0,0 +1,2 @@
+PASS dispatch was rejected: Auto-generated layouts mismatch
+
diff --git a/LayoutTests/fast/webgpu/regression/repro_176812014.html b/LayoutTests/fast/webgpu/regression/repro_176812014.html
new file mode 100644
index 000000000000..238837725c97
--- /dev/null
+++ b/LayoutTests/fast/webgpu/regression/repro_176812014.html
@@ -0,0 +1,73 @@
+
+
+
diff --git a/Source/WebGPU/WebGPU/BindGroupLayout.mm b/Source/WebGPU/WebGPU/BindGroupLayout.mm
index 1b805841d217..c555be162c34 100644
--- a/Source/WebGPU/WebGPU/BindGroupLayout.mm
+++ b/Source/WebGPU/WebGPU/BindGroupLayout.mm
@@ -572,7 +572,7 @@ static uint64_t NODELETE makeBindGroupLayoutPairIdentifier(uint32_t bindGroupIde
     if (device->isCachedCompatibile(*this, otherLayout))
         return nil;
 
-    if (autogeneratedPipelineLayout() != otherLayout.autogeneratedPipelineLayout())
+    if (isAutoGenerated() != otherLayout.isAutoGenerated() || autogeneratedPipelineLayout() != otherLayout.autogeneratedPipelineLayout())
         return @"Auto-generated layouts mismatch";
 
     auto& entries = m_sortedEntries;

From e147963ac4e9a6cf2bb7c40c23d26b85637db828 Mon Sep 17 00:00:00 2001
From: Simon Lewis 
Date: Tue, 30 Jun 2026 06:13:45 -0700
Subject: [PATCH 21/84] use-after-free in WebCodecsAudioData::memoryCost() via
 concurrent GC marker / close() rdar://175520011

Reviewed by Jean-Yves Avenard

WebCodecsAudioData is annotated with ReportExtraMemoryCost, so the generated
JS wrapper's visitChildren calls WebCodecsAudioData::memoryCost() from a
concurrent GC marker thread. memoryCost() dereferenced m_data.audioData (a
RefPtr) without synchronization while close() on the
main thread assigns m_data.audioData = nullptr and frees the
PlatformRawAudioData, leading to a heap-use-after-free in
PlatformRawAudioDataCocoa::memoryCost().

Cache the memory cost in a std::atomic on WebCodecsAudioData,
computed once at construction time on the main thread and zeroed in close(),
so the GC-thread memoryCost() never touches the RefPtr. This matches the
existing pattern in ImageBitmap.

* LayoutTests/fast/webcodecs/audio-data-close-during-gc-crash-expected.txt: Added.
* LayoutTests/fast/webcodecs/audio-data-close-during-gc-crash.html: Added.
* Source/WebCore/Modules/webcodecs/WebCodecsAudioData.cpp:
(WebCore::WebCodecsAudioData::WebCodecsAudioData):
(WebCore::WebCodecsAudioData::close):
* Source/WebCore/Modules/webcodecs/WebCodecsAudioData.h:
(WebCore::WebCodecsAudioData::memoryCost const):

Originally-landed-as: 305413.845@safari-7624-branch (2102c1dad1d3). rdar://180438342
Canonical link: https://commits.webkit.org/316135@main
---
 ...io-data-close-during-gc-crash-expected.txt |  1 +
 .../audio-data-close-during-gc-crash.html     | 64 +++++++++++++++++++
 .../Modules/webcodecs/WebCodecsAudioData.cpp  |  2 +
 .../Modules/webcodecs/WebCodecsAudioData.h    |  5 +-
 4 files changed, 71 insertions(+), 1 deletion(-)
 create mode 100644 LayoutTests/fast/webcodecs/audio-data-close-during-gc-crash-expected.txt
 create mode 100644 LayoutTests/fast/webcodecs/audio-data-close-during-gc-crash.html

diff --git a/LayoutTests/fast/webcodecs/audio-data-close-during-gc-crash-expected.txt b/LayoutTests/fast/webcodecs/audio-data-close-during-gc-crash-expected.txt
new file mode 100644
index 000000000000..03831620f648
--- /dev/null
+++ b/LayoutTests/fast/webcodecs/audio-data-close-during-gc-crash-expected.txt
@@ -0,0 +1 @@
+Test passes if it does not crash.
diff --git a/LayoutTests/fast/webcodecs/audio-data-close-during-gc-crash.html b/LayoutTests/fast/webcodecs/audio-data-close-during-gc-crash.html
new file mode 100644
index 000000000000..26498a31beeb
--- /dev/null
+++ b/LayoutTests/fast/webcodecs/audio-data-close-during-gc-crash.html
@@ -0,0 +1,64 @@
+
+
+
+
diff --git a/Source/WebCore/Modules/webcodecs/WebCodecsAudioData.cpp b/Source/WebCore/Modules/webcodecs/WebCodecsAudioData.cpp
index 8f6e315aeed6..d9d428f08a7e 100644
--- a/Source/WebCore/Modules/webcodecs/WebCodecsAudioData.cpp
+++ b/Source/WebCore/Modules/webcodecs/WebCodecsAudioData.cpp
@@ -65,6 +65,7 @@ WebCodecsAudioData::WebCodecsAudioData(ScriptExecutionContext& context)
 WebCodecsAudioData::WebCodecsAudioData(ScriptExecutionContext& context, WebCodecsAudioInternalData&& data)
     : ContextDestructionObserver(&context)
     , m_data(WTF::move(data))
+    , m_memoryCost(m_data.memoryCost())
 {
 }
 
@@ -151,6 +152,7 @@ ExceptionOr> WebCodecsAudioData::clone(ScriptExecutionCo
 // https://www.w3.org/TR/webcodecs/#dom-audiodata-close
 void WebCodecsAudioData::close()
 {
+    m_memoryCost.store(0, std::memory_order_relaxed);
     m_data.audioData = nullptr;
 
     m_isDetached = true;
diff --git a/Source/WebCore/Modules/webcodecs/WebCodecsAudioData.h b/Source/WebCore/Modules/webcodecs/WebCodecsAudioData.h
index 97b6e9b3244c..46cd29c6295e 100644
--- a/Source/WebCore/Modules/webcodecs/WebCodecsAudioData.h
+++ b/Source/WebCore/Modules/webcodecs/WebCodecsAudioData.h
@@ -84,13 +84,16 @@ class WebCodecsAudioData : public RefCounted, public Context
 
     const WebCodecsAudioInternalData& data() const LIFETIME_BOUND { return m_data; }
 
-    size_t memoryCost() const { return m_data.memoryCost(); }
+    // memoryCost() may be called from a GC thread by the JS wrapper's visitChildren, so it must
+    // not touch m_data.audioData (which close() may concurrently null on the main thread).
+    size_t memoryCost() const { return m_memoryCost.load(std::memory_order_relaxed); }
 
 private:
     explicit WebCodecsAudioData(ScriptExecutionContext&);
     WebCodecsAudioData(ScriptExecutionContext&, WebCodecsAudioInternalData&&);
 
     WebCodecsAudioInternalData m_data;
+    std::atomic m_memoryCost { 0 };
     bool m_isDetached { false };
 };
 

From d9783c3c13e7b9da3ff3cdb8947022f377b20c62 Mon Sep 17 00:00:00 2001
From: Chris Dumez 
Date: Tue, 30 Jun 2026 06:14:41 -0700
Subject: [PATCH 22/84] [WebKit Process Model] Use-after-free in
 WebExtensionContext::storageSet from HashMap mutation during keys() iteration
 https://bugs.webkit.org/show_bug.cgi?id=315538 rdar://177375129
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Reviewed by Timothy Hatcher.

When WebExtensionStorageSQLiteStore::setKeyedData hits an INSERT error
mid-batch (e.g. SQLITE_FULL), it returns a keysSuccessfullySet vector
that is non-empty but smaller than the input map. The completion lambda
in storageSet then iterated `data.keys()` while calling `data.remove()`
inside the loop. `keys()` returns a live iterator range backed by raw
pointers into the HashTable buffer, and remove() can shrink/rehash and
free that buffer, leaving the iterator dangling — a heap UAF in the UI
Process driven by IPC from the WebContent process.

Replace the iterate-and-mutate loop with HashMap::removeIf, which walks
the table safely.

* Source/WebKit/UIProcess/Extensions/API/WebExtensionContextAPIStorage.cpp:
(WebKit::WebExtensionContext::storageSet):

Originally-landed-as: 305413.961@safari-7624-branch (de9fb008197a). rdar://180427449
Canonical link: https://commits.webkit.org/316136@main
---
 .../Extensions/API/WebExtensionContextAPIStorage.cpp      | 8 ++------
 1 file changed, 2 insertions(+), 6 deletions(-)

diff --git a/Source/WebKit/UIProcess/Extensions/API/WebExtensionContextAPIStorage.cpp b/Source/WebKit/UIProcess/Extensions/API/WebExtensionContextAPIStorage.cpp
index ca3d362301a5..601870a9a2c4 100644
--- a/Source/WebKit/UIProcess/Extensions/API/WebExtensionContextAPIStorage.cpp
+++ b/Source/WebKit/UIProcess/Extensions/API/WebExtensionContextAPIStorage.cpp
@@ -149,12 +149,8 @@ void WebExtensionContext::storageSet(WebPageProxyIdentifier webPageProxyIdentifi
             if (!keysSuccessfullySet.size())
                 return;
 
-            if (keysSuccessfullySet.size() != data.size()) {
-                for (const auto& key : data.keys()) {
-                    if (!keysSuccessfullySet.contains(key))
-                        data.remove(key);
-                }
-            }
+            if (keysSuccessfullySet.size() != data.size())
+                data.removeIf([&](auto& entry) { return !keysSuccessfullySet.contains(entry.key); });
 
             fireStorageChangedEventIfNeeded(existingKeysAndValues, data, dataType);
         });

From 7a9d14998def6f77f60708cb89c5e68db69982e5 Mon Sep 17 00:00:00 2001
From: Kai Tamkun 
Date: Tue, 30 Jun 2026 06:17:17 -0700
Subject: [PATCH 23/84] [JSC] TypedArray.from() Out-of-Bounds Read via
 Resizable ArrayBuffer resize/transfer in mapFn callback.
 https://bugs.webkit.org/show_bug.cgi?id=312513 rdar://174428778

Reviewed by Yusuke Suzuki.

Adjusts TypedArray.from to properly handle cases where the map function changes the bounds of the arraylike input.

Test: JSTests/stress/typedarray-from-oob.js

* JSTests/stress/typedarray-from-oob.js: Added.
(testResize):
(testDetach):
* Source/JavaScriptCore/builtins/TypedArrayConstructor.js:
(from):

Originally-landed-as: 305413.702@safari-7624-branch (e99a325bb9b8). rdar://180429231
Canonical link: https://commits.webkit.org/316137@main
---
 JSTests/stress/typedarray-from-oob.js         | 47 +++++++++++++++++++
 .../builtins/TypedArrayConstructor.js         | 10 +++-
 2 files changed, 55 insertions(+), 2 deletions(-)
 create mode 100644 JSTests/stress/typedarray-from-oob.js

diff --git a/JSTests/stress/typedarray-from-oob.js b/JSTests/stress/typedarray-from-oob.js
new file mode 100644
index 000000000000..eba79c459c51
--- /dev/null
+++ b/JSTests/stress/typedarray-from-oob.js
@@ -0,0 +1,47 @@
+function testResize(ctor, bytesPerElement, initialBytes, shrinkBytes, resizeAt) {
+    const newCount = shrinkBytes / bytesPerElement;
+    const resizableArrayBuffer = new ArrayBuffer(initialBytes, { maxByteLength: initialBytes * 4 });
+    const source = new ctor(resizableArrayBuffer);
+    source[Symbol.iterator] = null;
+
+    let callbacks = 0;
+    ctor.from(source, (val, index) => {
+        if (index === resizeAt)
+            resizableArrayBuffer.resize(shrinkBytes);
+        callbacks++;
+        return val;
+    });
+
+    const expected = Math.max(resizeAt + 1, newCount);
+    if (callbacks > expected)
+        throw new Error(ctor.name + ": " + callbacks + " callbacks (expected <= " + expected + ")");
+}
+
+function testDetach(detachAt) {
+    const arrayBuffer = new ArrayBuffer(256);
+    const source = new Int32Array(arrayBuffer);
+    source[Symbol.iterator] = null;
+
+    let callbacks = 0;
+    try {
+        Int32Array.from(source, (val, index) => {
+            if (index === detachAt)
+                arrayBuffer.transfer();
+            callbacks++;
+            return val;
+        });
+    } catch (e) {
+        return;
+    }
+
+    if (callbacks > detachAt + 1)
+        throw new Error("detach: " + callbacks + " callbacks (expected <= " + (detachAt + 1) + ")");
+}
+
+testResize(Int32Array,   4, 4096, 16, 4);
+testResize(Int32Array,   4, 4096,  8, 8);
+testResize(Float64Array, 8, 8192, 32, 8);
+testResize(Uint8Array,   1, 1024,  4, 8);
+testResize(Int32Array,   4, 4096,  8, 0);
+testDetach(4);
+testDetach(0);
diff --git a/Source/JavaScriptCore/builtins/TypedArrayConstructor.js b/Source/JavaScriptCore/builtins/TypedArrayConstructor.js
index 785388293703..8e757caae907 100644
--- a/Source/JavaScriptCore/builtins/TypedArrayConstructor.js
+++ b/Source/JavaScriptCore/builtins/TypedArrayConstructor.js
@@ -109,11 +109,17 @@ function from(items /* [ , mapfn [ , thisArg ] ] */)
         @throwTypeError("TypedArray.from constructed typed array of insufficient length");
 
     for (var k = 0; k < arrayLikeLength; k++) {
+        if (@isTypedArrayView(arrayLike) && (@isDetached(arrayLike) || k >= @typedArrayLength(arrayLike)))
+            break;
         var value = arrayLike[k];
         if (mapFn === @undefined)
             result[k] = value;
-        else
-            result[k] = thisArg === @undefined ? mapFn(value, k) : mapFn.@call(thisArg, value, k);
+        else {
+            var mapped = thisArg === @undefined ? mapFn(value, k) : mapFn.@call(thisArg, value, k);
+            if (@isTypedArrayView(result) && (k >= @typedArrayLength(result) || @isDetached(result)))
+                break;
+            result[k] = mapped;
+        }
     }
 
     return result;

From 4691d48077d6de7bb72d10ba3f171dc41fcc87ad Mon Sep 17 00:00:00 2001
From: Rob Buis 
Date: Tue, 30 Jun 2026 06:17:58 -0700
Subject: [PATCH 24/84] Fix animateMotion-spline-invalid-keyTimes.html
 https://bugs.webkit.org/show_bug.cgi?id=318132

Reviewed by Nikolas Zimmermann.

Fix animateMotion-spline-invalid-keyTimes.html by keeping the animation invalid when keyTimes list does not end with 1
for the spline calcMode [1].

[1] https://svgwg.org/specs/animations/#KeyTimesAttribute

* LayoutTests/imported/w3c/web-platform-tests/svg/animations/animateMotion-spline-invalid-keyTimes-expected.txt:
* Source/WebCore/svg/SVGAnimationElement.cpp:
(WebCore::SVGAnimationElement::startedActiveInterval):

Canonical link: https://commits.webkit.org/316138@main
---
 .../animateMotion-spline-invalid-keyTimes-expected.txt         | 2 +-
 Source/WebCore/svg/SVGAnimationElement.cpp                     | 3 ++-
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/LayoutTests/imported/w3c/web-platform-tests/svg/animations/animateMotion-spline-invalid-keyTimes-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/svg/animations/animateMotion-spline-invalid-keyTimes-expected.txt
index 1c94220c7f29..6cc630b55bd8 100644
--- a/LayoutTests/imported/w3c/web-platform-tests/svg/animations/animateMotion-spline-invalid-keyTimes-expected.txt
+++ b/LayoutTests/imported/w3c/web-platform-tests/svg/animations/animateMotion-spline-invalid-keyTimes-expected.txt
@@ -1,3 +1,3 @@
 
-FAIL keyTimes does not end with one, calcMode=spline assert_equals: motion path not applied expected 0 but got -10
+PASS keyTimes does not end with one, calcMode=spline
 
diff --git a/Source/WebCore/svg/SVGAnimationElement.cpp b/Source/WebCore/svg/SVGAnimationElement.cpp
index b2c4992328ea..442b628f8664 100644
--- a/Source/WebCore/svg/SVGAnimationElement.cpp
+++ b/Source/WebCore/svg/SVGAnimationElement.cpp
@@ -552,7 +552,8 @@ void SVGAnimationElement::startedActiveInterval()
         if (!splinesCount
             || (hasAttributeWithoutSynchronization(SVGNames::keyPointsAttr) && m_keyPoints.size() - 1 != splinesCount)
             || (animationMode == AnimationMode::Values && m_values.size() - 1 != splinesCount)
-            || (hasAttributeWithoutSynchronization(SVGNames::keyTimesAttr) && keyTimes.size() - 1 != splinesCount))
+            || (hasAttributeWithoutSynchronization(SVGNames::keyTimesAttr) && keyTimes.size() - 1 != splinesCount)
+            || (!keyTimes.isEmpty() && keyTimes.last() != 1))
             return;
     }
 

From 2620d0dbd518fe150e430667996aeb2d94ecd75d Mon Sep 17 00:00:00 2001
From: Shu-yu Guo 
Date: Tue, 30 Jun 2026 06:19:12 -0700
Subject: [PATCH 25/84] [JSC] Insert barrier for MultiPutByOffset when it can
 reallocate storage https://bugs.webkit.org/show_bug.cgi?id=314747
 rdar://176792407

Reviewed by Keith Miller.

The DFG barrier insertion phase works by tracking an "epoch" serial number,
which is bumped every time it encounters a node that can GC. The assumption is
that each DFG node either does a store, which would need to be considered for
barrier insertion, or performs a GC.

MultiPutByOffset is a "fat" node that can GC and perform a store after that GC,
in sequence. The current analysis therefore incorrectly elides the barrier for
it. This PR fixes by special casing.

Test: JSTests/stress/multi-put-by-offset-reallocating-storage-needs-write-barrier.js

* JSTests/stress/multi-put-by-offset-reallocating-storage-needs-write-barrier.js: Added.
(foo):
* Source/JavaScriptCore/dfg/DFGStoreBarrierInsertionPhase.cpp:

Originally-landed-as: 305413.909@safari-7624-branch (2b4933eb1658). rdar://180427769
Canonical link: https://commits.webkit.org/316139@main
---
 ...eallocating-storage-needs-write-barrier.js | 32 +++++++++++++++++++
 .../dfg/DFGStoreBarrierInsertionPhase.cpp     | 17 ++++++++--
 2 files changed, 47 insertions(+), 2 deletions(-)
 create mode 100644 JSTests/stress/multi-put-by-offset-reallocating-storage-needs-write-barrier.js

diff --git a/JSTests/stress/multi-put-by-offset-reallocating-storage-needs-write-barrier.js b/JSTests/stress/multi-put-by-offset-reallocating-storage-needs-write-barrier.js
new file mode 100644
index 000000000000..3539c166af87
--- /dev/null
+++ b/JSTests/stress/multi-put-by-offset-reallocating-storage-needs-write-barrier.js
@@ -0,0 +1,32 @@
+//@ runDefault("--useConcurrentJIT=false", "--jitPolicyScale=0.1", "--useConcurrentGC=false", "--verifyGC=true", "--gcMaxHeapSize=500000")
+
+var g_value = {};
+
+function makeObj() { return {}; }
+
+function foo(cond) {
+    let a = makeObj();
+    a.p1 = 1;
+    a.p2 = 1;
+    a.p3 = 1;
+    a.p4 = 1;
+    a.p5 = 1;
+    if (cond)
+        a.y = 1;
+    else
+        a.z = 1;
+    a.x = g_value;
+    return a;
+}
+noInline(foo);
+
+for (let i = 0; i < testLoopCount; ++i) {
+    g_value = {};
+    foo(i & 1);
+}
+
+var holder = new Array(100000).fill(null);
+fullGC();
+
+for (let i = 0; i < testLoopCount * 10; ++i)
+    holder[i % 100000] = foo(i & 1);
diff --git a/Source/JavaScriptCore/dfg/DFGStoreBarrierInsertionPhase.cpp b/Source/JavaScriptCore/dfg/DFGStoreBarrierInsertionPhase.cpp
index d8ffe1fc95e8..c899eb57615d 100644
--- a/Source/JavaScriptCore/dfg/DFGStoreBarrierInsertionPhase.cpp
+++ b/Source/JavaScriptCore/dfg/DFGStoreBarrierInsertionPhase.cpp
@@ -68,6 +68,8 @@ enum class PhaseMode {
     Global
 };
 
+// FIXME(rdar://177100632): Document barrier placement invariants expected of nodes for this phase
+// and improve barrier-related phases overall.
 template
 class StoreBarrierInsertionPhase : public Phase {
 public:
@@ -369,9 +371,20 @@ class StoreBarrierInsertionPhase : public Phase {
                 break;
             }
                 
-            case MultiPutByOffset:
+            case MultiPutByOffset: {
+                // This node may cause a transition before performing a store if it reallocates
+                // storage. This is different from the usual assumption in this phase a node's fast
+                // path either does GC or performs a store, but not both within the same node (a
+                // slow path call to an operation always performs its own barrier). In this special
+                // case, bump the epoch before considering the barrier.
+                if (m_node->multiPutByOffsetData().reallocatesStorage())
+                    m_currentEpoch.bump();
+                considerBarrier(m_node->child1());
+                break;
+            }
+
             case MultiDeleteByOffset: {
-                // These nodes may cause transition too.
+                // This node may cause a transition but does not GC.
                 considerBarrier(m_node->child1());
                 break;
             }

From ff4c71709968a3deef34f315736b65abef410b0b Mon Sep 17 00:00:00 2001
From: Jonathan Bedard 
Date: Tue, 30 Jun 2026 06:29:29 -0700
Subject: [PATCH 26/84] REGRESSION(316095@main): error: initializer 'init(_:)'
 is not available due to missing import of defining module 'WebCore_Private'
 [#MemberImportVisibility] https://bugs.webkit.org/show_bug.cgi?id=318219
 rdar://181020438

Unreviewed build fix.

* Source/WebKit/UIProcess/mac/WKAppKitGestureController.swift:

Canonical link: https://commits.webkit.org/316140@main
---
 Source/WebKit/UIProcess/mac/WKAppKitGestureController.swift | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Source/WebKit/UIProcess/mac/WKAppKitGestureController.swift b/Source/WebKit/UIProcess/mac/WKAppKitGestureController.swift
index 9a2633b878cd..1f6654c2a493 100644
--- a/Source/WebKit/UIProcess/mac/WKAppKitGestureController.swift
+++ b/Source/WebKit/UIProcess/mac/WKAppKitGestureController.swift
@@ -28,6 +28,7 @@ import WebKit_Internal
 import AppKit
 import AppKit_Private.NSPanGestureRecognizer_Private
 private import CxxStdlib
+private import WebCore_Private
 
 final class WKPanGestureRecognizer: NSPanGestureRecognizer {
     private weak var webView: WKWebView?

From ebcf15019b705c4f4b52eed43c85192089f709d8 Mon Sep 17 00:00:00 2001
From: Nikolas Zimmermann 
Date: Tue, 30 Jun 2026 06:54:21 -0700
Subject: [PATCH 27/84] [LBSE] Rebaseline iOS specific results after
 315674@main https://bugs.webkit.org/show_bug.cgi?id=318220

Unreviewed gardening.

Forgot to reset LBSE specific results after 315674@main, fix that.

* LayoutTests/platform/ios/svg/compositing/anonymous-RenderSVGViewportContainer-no-repaints-expected.txt:
* LayoutTests/platform/ios/svg/compositing/segment-removed-after-anchor-decomposited-layer-tree-expected.txt: Added.
* LayoutTests/platform/ios/svg/compositing/transform-change-repainting-viewBox-repaintRects-expected.txt: Added.
* LayoutTests/platform/ios/svg/transforms/nested-svg-transform-attribute-creates-layer-expected.txt: Added.

Canonical link: https://commits.webkit.org/316141@main
---
 ...ViewportContainer-no-repaints-expected.txt | 22 +++--
 ...nchor-decomposited-layer-tree-expected.txt | 91 +++++++++++++++++++
 ...painting-viewBox-repaintRects-expected.txt | 49 ++++++++++
 ...sform-attribute-creates-layer-expected.txt | 13 +++
 4 files changed, 167 insertions(+), 8 deletions(-)
 create mode 100644 LayoutTests/platform/ios/svg/compositing/segment-removed-after-anchor-decomposited-layer-tree-expected.txt
 create mode 100644 LayoutTests/platform/ios/svg/compositing/transform-change-repainting-viewBox-repaintRects-expected.txt
 create mode 100644 LayoutTests/platform/ios/svg/transforms/nested-svg-transform-attribute-creates-layer-expected.txt

diff --git a/LayoutTests/platform/ios/svg/compositing/anonymous-RenderSVGViewportContainer-no-repaints-expected.txt b/LayoutTests/platform/ios/svg/compositing/anonymous-RenderSVGViewportContainer-no-repaints-expected.txt
index 568ce4182ec1..64274d2d4bfe 100644
--- a/LayoutTests/platform/ios/svg/compositing/anonymous-RenderSVGViewportContainer-no-repaints-expected.txt
+++ b/LayoutTests/platform/ios/svg/compositing/anonymous-RenderSVGViewportContainer-no-repaints-expected.txt
@@ -13,16 +13,22 @@
           (children 1
             (GraphicsLayer
               (bounds 784.00 784.00)
-              (children 2
+              (children 1
                 (GraphicsLayer
-                  (position 100.00 100.00)
-                  (bounds 100.00 100.00)
-                  (drawsContent 1)
-                  (transform [0.71 0.71 0.00 0.00] [-0.71 0.71 0.00 0.00] [0.00 0.00 1.00 0.00] [0.00 0.00 0.00 1.00])
-                )
-                (GraphicsLayer
-                  (bounds 784.00 784.00)
+                  (offsetFromRenderer width=79 height=79)
+                  (position 79.00 79.00)
+                  (anchor -0.56 -0.56)
+                  (bounds 142.00 142.00)
                   (drawsContent 1)
+                  (transform [2.61 0.00 0.00 0.00] [0.00 2.61 0.00 0.00] [0.00 0.00 1.00 0.00] [0.00 0.00 0.00 1.00])
+                  (children 1
+                    (GraphicsLayer
+                      (position 21.00 21.00)
+                      (bounds 100.00 100.00)
+                      (drawsContent 1)
+                      (transform [0.71 0.71 0.00 0.00] [-0.71 0.71 0.00 0.00] [0.00 0.00 1.00 0.00] [0.00 0.00 0.00 1.00])
+                    )
+                  )
                 )
               )
             )
diff --git a/LayoutTests/platform/ios/svg/compositing/segment-removed-after-anchor-decomposited-layer-tree-expected.txt b/LayoutTests/platform/ios/svg/compositing/segment-removed-after-anchor-decomposited-layer-tree-expected.txt
new file mode 100644
index 000000000000..0f5a7082fe05
--- /dev/null
+++ b/LayoutTests/platform/ios/svg/compositing/segment-removed-after-anchor-decomposited-layer-tree-expected.txt
@@ -0,0 +1,91 @@
+ === Initial: middle  painted into trailing segment ===
+(GraphicsLayer
+  (anchor 0.00 0.00)
+  (bounds 800.00 600.00)
+  (children 1
+    (GraphicsLayer
+      (bounds 800.00 600.00)
+      (contentsOpaque 1)
+      (children 1
+        (GraphicsLayer
+          (bounds 200.00 200.00)
+          (drawsContent 1)
+          (children 1
+            (GraphicsLayer
+              (bounds 200.00 200.00)
+              (children 1
+                (GraphicsLayer
+                  (offsetFromRenderer width=10 height=10)
+                  (position 10.00 10.00)
+                  (anchor -0.06 -0.06)
+                  (bounds 180.00 180.00)
+                  (drawsContent 1)
+                  (children 3
+                    (GraphicsLayer
+                      (anchor -0.08 -0.08)
+                      (bounds 120.00 120.00)
+                      (drawsContent 1)
+                    )
+                    (GraphicsLayer
+                      (offsetFromRenderer width=10 height=10)
+                      (bounds 180.00 180.00)
+                      (drawsContent 1)
+                    )
+                    (GraphicsLayer
+                      (position 60.00 60.00)
+                      (anchor -0.58 -0.58)
+                      (bounds 120.00 120.00)
+                      (drawsContent 1)
+                    )
+                  )
+                )
+              )
+            )
+          )
+        )
+      )
+    )
+  )
+)
+
+=== After mutation: middle  segment layer dropped ===
+(GraphicsLayer
+  (anchor 0.00 0.00)
+  (bounds 800.00 931.00)
+  (children 1
+    (GraphicsLayer
+      (bounds 800.00 931.00)
+      (contentsOpaque 1)
+      (children 1
+        (GraphicsLayer
+          (bounds 200.00 200.00)
+          (drawsContent 1)
+          (children 1
+            (GraphicsLayer
+              (bounds 200.00 200.00)
+              (children 1
+                (GraphicsLayer
+                  (offsetFromRenderer width=10 height=10)
+                  (position 10.00 10.00)
+                  (anchor -0.06 -0.06)
+                  (bounds 180.00 180.00)
+                  (drawsContent 1)
+                  (children 1
+                    (GraphicsLayer
+                      (position 60.00 60.00)
+                      (anchor -0.58 -0.58)
+                      (bounds 120.00 120.00)
+                      (drawsContent 1)
+                    )
+                  )
+                )
+              )
+            )
+          )
+        )
+      )
+    )
+  )
+)
+
+
diff --git a/LayoutTests/platform/ios/svg/compositing/transform-change-repainting-viewBox-repaintRects-expected.txt b/LayoutTests/platform/ios/svg/compositing/transform-change-repainting-viewBox-repaintRects-expected.txt
new file mode 100644
index 000000000000..596a7c7736a5
--- /dev/null
+++ b/LayoutTests/platform/ios/svg/compositing/transform-change-repainting-viewBox-repaintRects-expected.txt
@@ -0,0 +1,49 @@
+ (repaint rects
+  (rect 124 24 270 270)
+  (rect 124 24 270 270)
+  (rect 68 8 382 302)
+  (rect 8 8 502 302)
+)
+(GraphicsLayer
+  (anchor 0.00 0.00)
+  (bounds 800.00 600.00)
+  (children 1
+    (GraphicsLayer
+      (bounds 800.00 600.00)
+      (contentsOpaque 1)
+      (children 1
+        (GraphicsLayer
+          (position 8.00 8.00)
+          (bounds 502.00 302.00)
+          (drawsContent 1)
+          (children 1
+            (GraphicsLayer
+              (offsetFromRenderer width=1 height=1)
+              (position 1.00 1.00)
+              (bounds 500.00 300.00)
+              (children 1
+                (GraphicsLayer
+                  (offsetFromRenderer width=-27.50 height=-27.50)
+                  (position -27.50 -27.50)
+                  (anchor 0.11 0.11)
+                  (bounds 255.00 255.00)
+                  (drawsContent 1)
+                  (transform [1.50 0.00 0.00 0.00] [0.00 1.50 0.00 0.00] [0.00 0.00 1.00 0.00] [100.00 0.00 0.00 1.00])
+                  (children 1
+                    (GraphicsLayer
+                      (position 37.50 37.50)
+                      (bounds 180.00 180.00)
+                      (drawsContent 1)
+                      (transform [0.71 0.71 0.00 0.00] [-0.71 0.71 0.00 0.00] [0.00 0.00 1.00 0.00] [0.00 0.00 0.00 1.00])
+                    )
+                  )
+                )
+              )
+            )
+          )
+        )
+      )
+    )
+  )
+)
+
diff --git a/LayoutTests/platform/ios/svg/transforms/nested-svg-transform-attribute-creates-layer-expected.txt b/LayoutTests/platform/ios/svg/transforms/nested-svg-transform-attribute-creates-layer-expected.txt
new file mode 100644
index 000000000000..dba642154834
--- /dev/null
+++ b/LayoutTests/platform/ios/svg/transforms/nested-svg-transform-attribute-creates-layer-expected.txt
@@ -0,0 +1,13 @@
+layer at (0,0) size 800x600
+  RenderView at (0,0) size 800x600
+layer at (0,0) size 800x205
+  RenderBlock {HTML} at (0,0) size 800x204.50
+    RenderBody {BODY} at (0,0) size 800x204.50
+      RenderText {#text} at (0,0) size 0x0
+layer at (0,0) size 200x200
+  RenderSVGRoot {svg} at (0,0) size 200x200
+layer at (0,0) size 200x200
+  RenderSVGViewportContainer at (0,0) size 200x200
+layer at (0,0) size 200x200
+  RenderSVGViewportContainer {svg} at (0,0) size 200x200
+    RenderSVGRect {rect} at (60,90) size 80x20 [fill={[type=SOLID] [color=#008000]}] [x=60.00] [y=90.00] [width=80.00] [height=20.00]

From 5dd6865434312c3def91b287b6faba33ba795833 Mon Sep 17 00:00:00 2001
From: Yusuke Suzuki 
Date: Tue, 30 Jun 2026 07:12:58 -0700
Subject: [PATCH 28/84] [JSC] Add ArrayStorage + GetByVal specific operation
 https://bugs.webkit.org/show_bug.cgi?id=318180 rdar://180990441

Reviewed by Tadeu Zagallo.

This patch adds operationGetByValArrayStorageInt, which is tailored for
ArrayStorage slow path operation. DFG / FTL can use this when we already
speculate ArrayStorage, but it is a slow path.

                                                 ToT                     Patched

get-by-val-array-storage-sparse-hole      122.9244+-1.1533     ^     62.5947+-9.6089        ^ definitely 1.9638x faster
get-by-val-array-storage-sparse           117.8623+-0.4612     ^     84.7042+-0.5769        ^ definitely 1.3915x faster

Tests: JSTests/microbenchmarks/get-by-val-array-storage-sparse-hole.js
       JSTests/microbenchmarks/get-by-val-array-storage-sparse.js

* JSTests/microbenchmarks/get-by-val-array-storage-sparse-hole.js: Added.
(get array):
* JSTests/microbenchmarks/get-by-val-array-storage-sparse.js: Added.
(get array):
* Source/JavaScriptCore/dfg/DFGOperations.cpp:
(JSC::DFG::getByValArrayStorageInt):
(JSC::DFG::JSC_DEFINE_JIT_OPERATION):
* Source/JavaScriptCore/dfg/DFGOperations.h:
* Source/JavaScriptCore/dfg/DFGSpeculativeJIT32_64.cpp:
(JSC::DFG::SpeculativeJIT::compileGetByVal):
* Source/JavaScriptCore/dfg/DFGSpeculativeJIT64.cpp:
(JSC::DFG::SpeculativeJIT::compileGetByVal):
* Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp:
(JSC::FTL::DFG::LowerDFGToB3::compileGetByValImpl):

Canonical link: https://commits.webkit.org/316142@main
---
 .../get-by-val-array-storage-sparse-hole.js   | 32 ++++++++++++++
 .../get-by-val-array-storage-sparse.js        | 31 +++++++++++++
 Source/JavaScriptCore/dfg/DFGOperations.cpp   | 44 +++++++++++++++++++
 Source/JavaScriptCore/dfg/DFGOperations.h     |  1 +
 .../dfg/DFGSpeculativeJIT32_64.cpp            |  2 +-
 .../dfg/DFGSpeculativeJIT64.cpp               |  2 +-
 Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp |  2 +-
 7 files changed, 111 insertions(+), 3 deletions(-)
 create mode 100644 JSTests/microbenchmarks/get-by-val-array-storage-sparse-hole.js
 create mode 100644 JSTests/microbenchmarks/get-by-val-array-storage-sparse.js

diff --git a/JSTests/microbenchmarks/get-by-val-array-storage-sparse-hole.js b/JSTests/microbenchmarks/get-by-val-array-storage-sparse-hole.js
new file mode 100644
index 000000000000..ef68d4097357
--- /dev/null
+++ b/JSTests/microbenchmarks/get-by-val-array-storage-sparse-hole.js
@@ -0,0 +1,32 @@
+//@ skip if $model == "Apple Watch Series 3" # added by mark-jsc-stress-test.py
+// Hole/miss-dominated reads of a large sparse array (ArrayStorage mode with a sparse map). Every read
+// hits an in-bounds hole that is absent from the sparse map, so the int-indexed GetByVal slow path
+// (operationGetByValArrayStorageInt) must resolve it to undefined via the (sane) prototype chain.
+function get(array, i)
+{
+    return array[i];
+}
+noInline(get);
+
+var maxIndex = 200000;
+var step = 16;
+
+var array = [];
+for (var i = maxIndex - step; i >= 0; i -= step)
+    array[i] = i + 1;
+
+var expectedPass = 0;
+for (var i = 1; i < maxIndex; i += step) // i = 1, 17, 33, ... are all holes (never written).
+    ++expectedPass;
+
+var iterations = 400;
+var holes = 0;
+for (var iter = 0; iter < iterations; ++iter) {
+    for (var i = 1; i < maxIndex; i += step) {
+        if (get(array, i) === void 0)
+            ++holes;
+    }
+}
+
+if (holes !== expectedPass * iterations)
+    throw "Error: bad hole count: " + holes;
diff --git a/JSTests/microbenchmarks/get-by-val-array-storage-sparse.js b/JSTests/microbenchmarks/get-by-val-array-storage-sparse.js
new file mode 100644
index 000000000000..2cd433b339d0
--- /dev/null
+++ b/JSTests/microbenchmarks/get-by-val-array-storage-sparse.js
@@ -0,0 +1,31 @@
+//@ skip if $model == "Apple Watch Series 3" # added by mark-jsc-stress-test.py
+// Hit-dominated reads of a large sparse array (ArrayStorage mode with a sparse map). Every read
+// resolves through the int-indexed GetByVal slow path (operationGetByValArrayStorageInt) and finds
+// its value in the sparse map.
+function get(array, i)
+{
+    return array[i];
+}
+noInline(get);
+
+var maxIndex = 200000;
+var step = 16; // 1/16 density (< 1/8) keeps the array in ArrayStorage with a sparse map.
+
+var array = [];
+// Descending writes: the first one is far beyond length, forcing ArrayStorage + sparse map.
+for (var i = maxIndex - step; i >= 0; i -= step)
+    array[i] = i + 1;
+
+var expectedPass = 0;
+for (var i = 0; i < maxIndex; i += step)
+    expectedPass += i + 1;
+
+var iterations = 800;
+var sum = 0;
+for (var iter = 0; iter < iterations; ++iter) {
+    for (var i = 0; i < maxIndex; i += step)
+        sum += get(array, i);
+}
+
+if (sum !== expectedPass * iterations)
+    throw "Error: bad sum: " + sum;
diff --git a/Source/JavaScriptCore/dfg/DFGOperations.cpp b/Source/JavaScriptCore/dfg/DFGOperations.cpp
index 9ce89c8214a9..52c9f7b701cf 100644
--- a/Source/JavaScriptCore/dfg/DFGOperations.cpp
+++ b/Source/JavaScriptCore/dfg/DFGOperations.cpp
@@ -847,6 +847,40 @@ ALWAYS_INLINE EncodedJSValue getByValCellInt(JSGlobalObject* globalObject, VM& v
     return JSValue::encode(JSValue(base).get(globalObject, static_cast(index)));
 }
 
+ALWAYS_INLINE EncodedJSValue getByValArrayStorageInt(JSGlobalObject* globalObject, VM& vm, JSObject* base, int32_t index)
+{
+    ASSERT(hasAnyArrayStorage(base->indexingType()));
+    if (index >= 0) [[likely]] {
+        unsigned i = static_cast(index);
+        ArrayStorage* storage = base->butterfly()->arrayStorage();
+        SparseArrayValueMap* map = storage->m_sparseMap.get();
+        // Only the standard ArrayStorage layout is handled here: indices < vectorLength live in the
+        // vector, overflow indices live in a non-sparse-mode map with no per-entry attributes. When the
+        // map is in sparse mode (frozen/sealed/read-only/accessor arrays), values for in-vector indices
+        // may have migrated into the map and entries carry attributes, so defer to the generic path.
+        if (!map || !map->sparseMode()) [[likely]] {
+            if (i < storage->vectorLength()) {
+                JSValue value = storage->m_vector[i].get();
+                if (value)
+                    return JSValue::encode(value);
+            } else if (map) {
+                SparseArrayValueMap::iterator it = map->find(i);
+                if (it != map->notFound()) {
+                    if (it->value.attributes()) [[unlikely]] // accessor / special: needs the full slot path.
+                        return JSValue::encode(JSValue(base).get(globalObject, i));
+                    return JSValue::encode(it->value.getNonSparseMode());
+                }
+            }
+
+            // Missing own indexed property: undefined unless the prototype chain can intercept it.
+            if (!base->structure()->holesMustForwardToPrototype(base))
+                return JSValue::encode(jsUndefined());
+        }
+    }
+    // Negative index, sparse/dictionary mode, accessor entry, or intercepting prototype chain.
+    return getByValCellInt(globalObject, vm, base, index);
+}
+
 JSC_DEFINE_JIT_OPERATION(operationGetByValObjectInt, EncodedJSValue, (JSGlobalObject* globalObject, JSObject* base, int32_t index))
 {
     VM& vm = globalObject->vm();
@@ -857,6 +891,16 @@ JSC_DEFINE_JIT_OPERATION(operationGetByValObjectInt, EncodedJSValue, (JSGlobalOb
     OPERATION_RETURN(scope, getByValCellInt(globalObject, vm, base, index));
 }
 
+JSC_DEFINE_JIT_OPERATION(operationGetByValArrayStorageInt, EncodedJSValue, (JSGlobalObject* globalObject, JSObject* base, int32_t index))
+{
+    VM& vm = globalObject->vm();
+    CallFrame* callFrame = DECLARE_CALL_FRAME(vm);
+    JITOperationPrologueCallFrameTracer tracer(vm, callFrame);
+    auto scope = DECLARE_THROW_SCOPE(vm);
+
+    OPERATION_RETURN(scope, getByValArrayStorageInt(globalObject, vm, base, index));
+}
+
 JSC_DEFINE_JIT_OPERATION(operationGetByValStringInt, EncodedJSValue, (JSGlobalObject* globalObject, JSString* base, int32_t index))
 {
     VM& vm = globalObject->vm();
diff --git a/Source/JavaScriptCore/dfg/DFGOperations.h b/Source/JavaScriptCore/dfg/DFGOperations.h
index 55e98e335a7a..ed1c416c66a5 100644
--- a/Source/JavaScriptCore/dfg/DFGOperations.h
+++ b/Source/JavaScriptCore/dfg/DFGOperations.h
@@ -117,6 +117,7 @@ JSC_DECLARE_JIT_OPERATION(operationArithTrunc, EncodedJSValue, (JSGlobalObject*,
 JSC_DECLARE_NOEXCEPT_JIT_OPERATION(operationArithMinMultipleDouble, double, (const double* buffer, unsigned elementCount));
 JSC_DECLARE_NOEXCEPT_JIT_OPERATION(operationArithMaxMultipleDouble, double, (const double* buffer, unsigned elementCount));
 JSC_DECLARE_JIT_OPERATION(operationGetByValObjectInt, EncodedJSValue, (JSGlobalObject*, JSObject*, int32_t));
+JSC_DECLARE_JIT_OPERATION(operationGetByValArrayStorageInt, EncodedJSValue, (JSGlobalObject*, JSObject*, int32_t));
 JSC_DECLARE_JIT_OPERATION(operationGetByValStringInt, EncodedJSValue, (JSGlobalObject*, JSString*, int32_t));
 JSC_DECLARE_JIT_OPERATION(operationGetByValObjectString, EncodedJSValue, (JSGlobalObject*, JSCell*, JSCell* string));
 JSC_DECLARE_JIT_OPERATION(operationGetByValObjectSymbol, EncodedJSValue, (JSGlobalObject*, JSCell*, JSCell* symbol));
diff --git a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT32_64.cpp b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT32_64.cpp
index abb1f2a38960..06c06e28c75b 100644
--- a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT32_64.cpp
+++ b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT32_64.cpp
@@ -2112,7 +2112,7 @@ void SpeculativeJIT::compileGetByVal(Node* node, const ScopedLambda
Date: Tue, 30 Jun 2026 07:13:55 -0700
Subject: [PATCH 29/84] Need a process-specific
 `WebBackForwardListItem::allItems()` instead of the process-global map for
 better message checking rdar://174702519

Reviewed by Ben Nham.

When considering whether a given web process should have access to a given back/forward entry,
the global map is the wrong tool.

Check on a per-process basis instead.

Test: Tools/TestWebKitAPI/Tests/WebKit/WKBackForwardListTests.mm

* Source/WebKit/UIProcess/WebBackForwardList.cpp:
(WebKit::messageCheckItemURLs):
(WebKit::WebBackForwardList::backForwardAddItemShared):
(WebKit::WebBackForwardList::backForwardSetChildItem):
(WebKit::WebBackForwardList::backForwardUpdateItem):
* Source/WebKit/UIProcess/WebProcessProxy.cpp:
(WebKit::WebProcessProxy::checkURLReceivedFromWebProcess):
* Tools/TestWebKitAPI/Tests/WebKit/WKBackForwardListTests.mm:
(" baseURL:simple2];
+    TestWebKitAPI::Util::run(&done);
+    EXPECT_STREQ(webView.get().backForwardList.currentItem.URL.absoluteString.UTF8String, simple.absoluteString.UTF8String);
+}
+
+TEST(WKBackForwardList, InteractionStateRestoration)
+{
+    auto webView = adoptNS([[WKWebView alloc] init]);
+
+    RetainPtr url1 = [NSBundle.test_resourcesBundle URLForResource:@"simple" withExtension:@"html"];
+    RetainPtr url2 = [NSBundle.test_resourcesBundle URLForResource:@"simple2" withExtension:@"html"];
+    RetainPtr url3 = [NSBundle.test_resourcesBundle URLForResource:@"simple3" withExtension:@"html"];
+
+    [webView loadRequest:[NSURLRequest requestWithURL:url1.get()]];
+    [webView _test_waitForDidFinishNavigation];
+
+    [webView loadRequest:[NSURLRequest requestWithURL:url2.get()]];
+    [webView _test_waitForDidFinishNavigation];
+
+    [webView loadRequest:[NSURLRequest requestWithURL:url3.get()]];
+    [webView _test_waitForDidFinishNavigation];
+
+    WKBackForwardList *list = [webView backForwardList];
+    EXPECT_EQ((NSUInteger)2, list.backList.count);
+    EXPECT_EQ((NSUInteger)0, list.forwardList.count);
+    EXPECT_STREQ([[list.currentItem URL] absoluteString].UTF8String, [url3 absoluteString].UTF8String);
+
+    id interactionState = [webView interactionState];
+    RetainPtr temporaryFile = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:[NSUUID UUID].UUIDString] isDirectory:NO];
+    NSError *error = nil;
+    RetainPtr archivedInteractionState = [NSKeyedArchiver archivedDataWithRootObject:interactionState requiringSecureCoding:YES error:&error];
+    EXPECT_TRUE(!error);
+    interactionState = nil;
+    [archivedInteractionState writeToURL:temporaryFile.get() options:NSDataWritingAtomic error:&error];
+    archivedInteractionState = nil;
+    EXPECT_TRUE(!error);
+
+    webView = adoptNS([[WKWebView alloc] init]);
+
+    archivedInteractionState = [NSData dataWithContentsOfURL:temporaryFile.get()];
+    interactionState = [NSKeyedUnarchiver unarchivedObjectOfClass:[(id)[webView interactionState] class] fromData:archivedInteractionState.get() error:&error];
+    EXPECT_TRUE(!error);
+
+    [webView setInteractionState:interactionState];
+    [webView _test_waitForDidFinishNavigation];
+
+    WKBackForwardList *newList = [webView backForwardList];
+
+    EXPECT_EQ((NSUInteger)2, newList.backList.count);
+    EXPECT_EQ((NSUInteger)0, newList.forwardList.count);
+    EXPECT_STREQ([[newList.currentItem URL] absoluteString].UTF8String, [url3 absoluteString].UTF8String);
+
+    done = false;
+    [webView evaluateJavaScript:@"document.body.innerText" completionHandler:^(id result, NSError *error) {
+        EXPECT_TRUE(!error);
+        NSString* bodyText = result;
+        EXPECT_WK_STREQ(@"Third simple HTML file.", bodyText);
+        done = true;
+    }];
+    TestWebKitAPI::Util::run(&done);
+
+    [webView goBack];
+    [webView _test_waitForDidFinishNavigation];
+
+    done = false;
+    [webView evaluateJavaScript:@"document.body.innerText" completionHandler:^(id result, NSError *error) {
+        EXPECT_TRUE(!error);
+        NSString* bodyText = result;
+        EXPECT_WK_STREQ(@"Second simple HTML file.", bodyText);
+        done = true;
+    }];
+    TestWebKitAPI::Util::run(&done);
+
+    [webView goBack];
+    [webView _test_waitForDidFinishNavigation];
+
+    done = false;
+    [webView evaluateJavaScript:@"document.body.innerText" completionHandler:^(id result, NSError *error) {
+        EXPECT_TRUE(!error);
+        NSString* bodyText = result;
+        EXPECT_WK_STREQ(@"Simple HTML file.", bodyText);
+        done = true;
+    }];
+    TestWebKitAPI::Util::run(&done);
+}
+
+TEST(WKBackForwardList, InteractionStateRestorationNil)
+{
+    auto webView = adoptNS([[WKWebView alloc] init]);
+
+    RetainPtr url1 = [NSBundle.test_resourcesBundle URLForResource:@"simple" withExtension:@"html"];
+    RetainPtr url2 = [NSBundle.test_resourcesBundle URLForResource:@"simple2" withExtension:@"html"];
+    RetainPtr url3 = [NSBundle.test_resourcesBundle URLForResource:@"simple3" withExtension:@"html"];
+
+    [webView loadRequest:[NSURLRequest requestWithURL:url1.get()]];
+    [webView _test_waitForDidFinishNavigation];
+
+    [webView loadRequest:[NSURLRequest requestWithURL:url2.get()]];
+    [webView _test_waitForDidFinishNavigation];
+
+    [webView loadRequest:[NSURLRequest requestWithURL:url3.get()]];
+    [webView _test_waitForDidFinishNavigation];
+
+    WKBackForwardList *list = [webView backForwardList];
+    EXPECT_EQ((NSUInteger)2, list.backList.count);
+    EXPECT_EQ((NSUInteger)0, list.forwardList.count);
+    EXPECT_STREQ([[list.currentItem URL] absoluteString].UTF8String, [url3 absoluteString].UTF8String);
+
+    [webView setInteractionState:nil];
+
+    list = [webView backForwardList];
+    EXPECT_EQ((NSUInteger)2, list.backList.count);
+    EXPECT_EQ((NSUInteger)0, list.forwardList.count);
+    EXPECT_STREQ([[list.currentItem URL] absoluteString].UTF8String, [url3 absoluteString].UTF8String);
+}
+
+TEST(WKBackForwardList, InteractionStateRestorationInvalid)
+{
+    auto webView = adoptNS([[WKWebView alloc] init]);
+
+    RetainPtr url1 = [NSBundle.test_resourcesBundle URLForResource:@"simple" withExtension:@"html"];
+    RetainPtr url2 = [NSBundle.test_resourcesBundle URLForResource:@"simple2" withExtension:@"html"];
+    RetainPtr url3 = [NSBundle.test_resourcesBundle URLForResource:@"simple3" withExtension:@"html"];
+
+    [webView loadRequest:[NSURLRequest requestWithURL:url1.get()]];
+    [webView _test_waitForDidFinishNavigation];
+
+    [webView loadRequest:[NSURLRequest requestWithURL:url2.get()]];
+    [webView _test_waitForDidFinishNavigation];
+
+    [webView loadRequest:[NSURLRequest requestWithURL:url3.get()]];
+    [webView _test_waitForDidFinishNavigation];
+
+    WKBackForwardList *list = [webView backForwardList];
+    EXPECT_EQ((NSUInteger)2, list.backList.count);
+    EXPECT_EQ((NSUInteger)0, list.forwardList.count);
+    EXPECT_STREQ([[list.currentItem URL] absoluteString].UTF8String, [url3 absoluteString].UTF8String);
+
+    NSString *invalidState = @"foo";
+    [webView setInteractionState:invalidState];
+
+    list = [webView backForwardList];
+    EXPECT_EQ((NSUInteger)2, list.backList.count);
+    EXPECT_EQ((NSUInteger)0, list.forwardList.count);
+    EXPECT_STREQ([[list.currentItem URL] absoluteString].UTF8String, [url3 absoluteString].UTF8String);
+}
+
+@interface WKBackForwardNavigationDelegate : NSObject 
+- (void)waitForDidFinishNavigationOrDidSameDocumentNavigation;
+@end
+
+static RetainPtr lastNavigation;
+
+@implementation WKBackForwardNavigationDelegate {
+    bool _navigated;
+    bool _didFinishNavigation;
+}
+
+- (instancetype) init
+{
+    self = [super init];
+    return self;
+}
+
+- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
+{
+    EXPECT_WK_STREQ(challenge.protectionSpace.authenticationMethod, NSURLAuthenticationMethodServerTrust);
+    completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
+}
+
+- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
+{
+    _navigated = true;
+    _didFinishNavigation = true;
+    lastNavigation = navigation;
+}
+
+- (void)_webView:(WKWebView *)webView navigation:(WKNavigation *)navigation didSameDocumentNavigation:(_WKSameDocumentNavigationType)navigationType
+{
+    if (navigationType == _WKSameDocumentNavigationTypeSessionStatePush || navigationType == _WKSameDocumentNavigationTypeSessionStatePop) {
+        _navigated = true;
+        lastNavigation = navigation;
+    }
+}
+
+- (void)waitForDidFinishNavigationOrDidSameDocumentNavigation
+{
+    _navigated = false;
+    TestWebKitAPI::Util::run(&_navigated);
+}
+
+- (void)waitForDidFinishNavigation
+{
+    _didFinishNavigation = false;
+    TestWebKitAPI::Util::run(&_didFinishNavigation);
+}
+
+@end
+
+// _beginBackSwipeForTesting / _completeBackSwipeForTesting are not implemented on macOS.
+#if !PLATFORM(MAC)
+
+TEST(WKBackForwardList, BackSwipeNavigationSkipsItemsWithoutUserGesture)
+{
+    auto webView = adoptNS([[WKWebView alloc] initWithFrame:CGRectMake(0, 0, 320, 500)]);
+    [webView setAllowsBackForwardNavigationGestures:YES];
+    [webView becomeFirstResponder];
+
+    auto navigationDelegate = adoptNS([WKBackForwardNavigationDelegate new]);
+    webView.get().navigationDelegate = navigationDelegate.get();
+
+    RetainPtr url1 = [NSBundle.test_resourcesBundle URLForResource:@"simple" withExtension:@"html"];
+    RetainPtr url2 = [NSBundle.test_resourcesBundle URLForResource:@"simple2" withExtension:@"html"];
+
+    [webView loadRequest:[NSURLRequest requestWithURL:url1.get()]];
+    [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation];
+
+    [webView loadRequest:[NSURLRequest requestWithURL:url2.get()]];
+    [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation];
+
+    // Add back/forward list items without user gestures.
+    [webView _evaluateJavaScriptWithoutUserGesture:@"history.pushState(null, document.title, location.pathname + '#a');" completionHandler:nil];
+    [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation];
+
+    [webView _evaluateJavaScriptWithoutUserGesture:@"history.pushState(null, document.title, location.pathname + '#b');" completionHandler:nil];
+    [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation];
+
+    [webView _evaluateJavaScriptWithoutUserGesture:@"history.pushState(null, document.title, location.pathname + '#c');" completionHandler:nil];
+    [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation];
+
+    EXPECT_EQ([webView backForwardList].backList.count, 4U);
+    EXPECT_EQ([webView backForwardList].forwardList.count, 0U);
+
+    // Navigating back via a swipe gesture should skip those back/forward list items without a user gesture.
+    [webView _beginBackSwipeForTesting];
+    [webView _completeBackSwipeForTesting];
+    [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation];
+
+    EXPECT_STREQ([webView URL].absoluteString.UTF8String, [url1 absoluteString].UTF8String);
+
+    EXPECT_EQ([webView backForwardList].backList.count, 0U);
+    EXPECT_EQ([webView backForwardList].forwardList.count, 4U);
+}
+
+TEST(WKBackForwardList, BackSwipeNavigationDoesNotSkipItemsWithUserGesture)
+{
+    auto webView = adoptNS([[WKWebView alloc] initWithFrame:CGRectMake(0, 0, 320, 500)]);
+    [webView setAllowsBackForwardNavigationGestures:YES];
+    [webView becomeFirstResponder];
+
+    auto navigationDelegate = adoptNS([WKBackForwardNavigationDelegate new]);
+    webView.get().navigationDelegate = navigationDelegate.get();
+
+    RetainPtr url1 = [NSBundle.test_resourcesBundle URLForResource:@"simple" withExtension:@"html"];
+    RetainPtr url2 = [NSBundle.test_resourcesBundle URLForResource:@"simple2" withExtension:@"html"];
+
+    [webView loadRequest:[NSURLRequest requestWithURL:url1.get()]];
+    [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation];
+
+    [webView loadRequest:[NSURLRequest requestWithURL:url2.get()]];
+    [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation];
+
+    // Add back/forward list item with a user gesture.
+    [webView evaluateJavaScript:@"history.pushState(null, document.title, location.pathname + '#a');" completionHandler:nil];
+    [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation];
+
+    EXPECT_EQ([webView backForwardList].backList.count, 2U);
+    EXPECT_EQ([webView backForwardList].forwardList.count, 0U);
+
+    // Navigating back via a swipe gesture should skip those back/forward list items without a user gesture.
+    [webView _beginBackSwipeForTesting];
+    [webView _completeBackSwipeForTesting];
+    [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation];
+
+    EXPECT_STREQ([webView URL].absoluteString.UTF8String, [url2 absoluteString].UTF8String);
+
+    EXPECT_EQ([webView backForwardList].backList.count, 1U);
+    EXPECT_EQ([webView backForwardList].forwardList.count, 1U);
+}
+
+#endif
+
+static void runBackForwardNavigationSkipsItemsWithoutUserGestureTest(Function&& navigate)
+{
+    auto webView = adoptNS([[WKWebView alloc] init]);
+
+    auto navigationDelegate = adoptNS([WKBackForwardNavigationDelegate new]);
+    webView.get().navigationDelegate = navigationDelegate.get();
+
+    RetainPtr url1 = [NSBundle.test_resourcesBundle URLForResource:@"simple" withExtension:@"html"];
+    RetainPtr url2 = [NSBundle.test_resourcesBundle URLForResource:@"simple2" withExtension:@"html"];
+    RetainPtr url3 = [NSBundle.test_resourcesBundle URLForResource:@"simple3" withExtension:@"html"];
+
+    [webView loadRequest:[NSURLRequest requestWithURL:url1.get()]];
+    [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation];
+
+    [webView loadRequest:[NSURLRequest requestWithURL:url2.get()]];
+    [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation];
+
+    // Test case:
+    // url1 -> url2 -> url2#a (no user gesture) -> url2#b (no user gesture) -> url2#c (no user gesture) -> url3.
+
+    // Add back/forward list items without user gestures.
+    navigate(webView.get(), "location.pathname + '#a'"_s);
+    [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation];
+    EXPECT_FALSE([lastNavigation _isUserInitiated]);
+    EXPECT_TRUE(webView.get().backForwardList.currentItem._wasCreatedByJSWithoutUserInteraction);
+    RetainPtr expectedURLString = makeString(String([url2 absoluteString]), "#a"_s).createNSString();
+    EXPECT_WK_STREQ([lastNavigation _request].URL.absoluteString.UTF8String, expectedURLString.get().UTF8String);
+
+    navigate(webView.get(), "location.pathname + '#b'"_s);
+    [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation];
+    EXPECT_FALSE([lastNavigation _isUserInitiated]);
+    EXPECT_TRUE(webView.get().backForwardList.currentItem._wasCreatedByJSWithoutUserInteraction);
+    expectedURLString = makeString(String([url2 absoluteString]), "#b"_s).createNSString();
+    EXPECT_WK_STREQ([lastNavigation _request].URL.absoluteString.UTF8String, expectedURLString.get().UTF8String);
+
+    navigate(webView.get(), "location.pathname + '#c'"_s);
+    [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation];
+    EXPECT_FALSE([lastNavigation _isUserInitiated]);
+    EXPECT_TRUE(webView.get().backForwardList.currentItem._wasCreatedByJSWithoutUserInteraction);
+    expectedURLString = makeString(String([url2 absoluteString]), "#c"_s).createNSString();
+    EXPECT_WK_STREQ([lastNavigation _request].URL.absoluteString.UTF8String, expectedURLString.get().UTF8String);
+
+    [webView loadRequest:[NSURLRequest requestWithURL:url3.get()]];
+    [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation];
+    EXPECT_FALSE(webView.get().backForwardList.currentItem._wasCreatedByJSWithoutUserInteraction);
+
+    EXPECT_EQ([webView backForwardList].backList.count, 5U);
+    EXPECT_EQ([webView backForwardList].forwardList.count, 0U);
+
+    // We are now on url3. Let's go back.
+    [webView goBack];
+    [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation];
+
+    // We should go back to url2#c.
+    expectedURLString = makeString(String([url2 absoluteString]), "#c"_s).createNSString();
+    EXPECT_STREQ([webView URL].absoluteString.UTF8String, expectedURLString.get().UTF8String);
+    EXPECT_EQ([webView backForwardList].backList.count, 4U);
+    EXPECT_EQ([webView backForwardList].forwardList.count, 1U);
+
+    // Let's go back again.
+    [webView goBack];
+    [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation];
+
+    // We should have skipped over url2#b, url2#a and url2, to end up on url1.
+    EXPECT_STREQ([webView URL].absoluteString.UTF8String, [url1 absoluteString].UTF8String);
+    EXPECT_EQ([webView backForwardList].backList.count, 0U);
+    EXPECT_EQ([webView backForwardList].forwardList.count, 5U);
+
+    // Now let's go forward.
+    [webView goForward];
+    [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation];
+
+    // We should get to the latest url2 URL, that is url2#c.
+    expectedURLString = makeString(String([url2 absoluteString]), "#c"_s).createNSString();
+    EXPECT_STREQ([webView URL].absoluteString.UTF8String, expectedURLString.get().UTF8String);
+    EXPECT_EQ([webView backForwardList].backList.count, 4U);
+    EXPECT_EQ([webView backForwardList].forwardList.count, 1U);
+
+    // Let's go forward again.
+    [webView goForward];
+    [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation];
+
+    // We should now be on url3.
+    EXPECT_STREQ([webView URL].absoluteString.UTF8String, [url3 absoluteString].UTF8String);
+    EXPECT_EQ([webView backForwardList].backList.count, 5U);
+    EXPECT_EQ([webView backForwardList].forwardList.count, 0U);
+
+    // Navigating via the JS API shouldn't skip those back/forward list items.
+    [webView _evaluateJavaScriptWithoutUserGesture:@"history.back();" completionHandler:^(id, NSError *) { }];
+    [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation];
+
+    expectedURLString = makeString(String([url2 absoluteString]), "#c"_s).createNSString();
+    EXPECT_STREQ([webView URL].absoluteString.UTF8String, expectedURLString.get().UTF8String);
+    EXPECT_EQ([webView backForwardList].backList.count, 4U);
+    EXPECT_EQ([webView backForwardList].forwardList.count, 1U);
+
+    [webView _evaluateJavaScriptWithoutUserGesture:@"history.back();" completionHandler:^(id, NSError *) { }];
+    [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation];
+    expectedURLString = makeString(String([url2 absoluteString]), "#b"_s).createNSString();
+    EXPECT_STREQ([webView URL].absoluteString.UTF8String, expectedURLString.get().UTF8String);
+    EXPECT_EQ([webView backForwardList].backList.count, 3U);
+    EXPECT_EQ([webView backForwardList].forwardList.count, 2U);
+}
+
+TEST(WKBackForwardList, BackForwardNavigationSkipsItemsWithoutUserGesturePushState)
+{
+    runBackForwardNavigationSkipsItemsWithoutUserGestureTest([](WKWebView* webView, ASCIILiteral destination) {
+        [webView _evaluateJavaScriptWithoutUserGesture:makeString("history.pushState(null, document.title, "_s, destination, ");"_s).createNSString().get() completionHandler:nil];
+    });
+}
+
+TEST(WKBackForwardList, BackForwardNavigationSkipsItemsWithoutUserGestureFragment)
+{
+    runBackForwardNavigationSkipsItemsWithoutUserGestureTest([](WKWebView* webView, ASCIILiteral destination) {
+        [webView _evaluateJavaScriptWithoutUserGesture:makeString("location.href = "_s, destination, ";"_s).createNSString().get() completionHandler:nil];
+    });
+}
+
+TEST(WKBackForwardList, BackForwardNavigationSkipsItemsWithoutUserGesturePushStateAfterEvaluateJS)
+{
+    runBackForwardNavigationSkipsItemsWithoutUserGestureTest([](WKWebView* webView, ASCIILiteral destination) {
+        // Do a call to evaluateJavaScript (with user gesture) *BEFORE* the pushState and make sure it doesn't count
+        // as a user gesture for the pushState().
+        __block bool didRunScript = false;
+        [webView evaluateJavaScript:@"window.foo = 1;" completionHandler:^(id, NSError *) {
+            didRunScript = true;
+        }];
+        TestWebKitAPI::Util::run(&didRunScript);
+        [webView _evaluateJavaScriptWithoutUserGesture:makeString("history.pushState(null, document.title, "_s, destination, ");"_s).createNSString().get() completionHandler:nil];
+    });
+}
+
+TEST(WKBackForwardList, BackForwardNavigationSkipsItemsWithoutUserGestureSubframe)
+{
+    TestWebKitAPI::HTTPServer server({
+        { "/source.html"_s, { "foo"_s } },
+        { "/destination.html"_s, { ""_s } },
+        { "/iframe.html"_s, { ""_s } },
+    }, TestWebKitAPI::HTTPServer::Protocol::Http);
+
+    auto webView = adoptNS([[WKWebView alloc] init]);
+
+    auto navigationDelegate = adoptNS([WKBackForwardNavigationDelegate new]);
+    webView.get().navigationDelegate = navigationDelegate.get();
+
+    [webView loadRequest:server.request("/source.html"_s)];
+    [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation];
+
+    [webView loadRequest:server.request("/destination.html"_s)];
+    [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation];
+
+    // Wait for the subframe to call pushState().
+    while ([webView backForwardList].backList.count != 2)
+        TestWebKitAPI::Util::spinRunLoop();
+
+    [webView goBack];
+    [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation];
+
+    // We should be back to source.html since we would have ignored the history item
+    // added by the subframe without user interaction.
+    EXPECT_EQ([webView backForwardList].backList.count, 0U);
+    EXPECT_STREQ([webView URL].absoluteString.UTF8String, server.request("/source.html"_s).URL.absoluteString.UTF8String);
+
+    [webView goForward];
+    [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation];
+
+    EXPECT_EQ([webView backForwardList].backList.count, 2U);
+    EXPECT_EQ([webView backForwardList].forwardList.count, 0U);
+    EXPECT_STREQ([webView URL].absoluteString.UTF8String, server.request("/destination.html"_s).URL.absoluteString.UTF8String);
+}
+
+TEST(WKBackForwardList, BackForwardNavigationSkipsClientSideRedirectWithCOOP)
+{
+    TestWebKitAPI::HTTPServer server({
+        { "/source.html"_s, { "click me"_s } },
+        { "/form.html"_s, { "
"_s } }, + { "/redirect.html"_s, { { { "Content-Type"_s, "text/html"_s }, { "cross-origin-opener-policy"_s, "same-origin"_s } }, ""_s } }, + { "/destination.html"_s, { "foo"_s } }, + }, TestWebKitAPI::HTTPServer::Protocol::Https); + + auto webView = adoptNS([[WKWebView alloc] init]); + + auto navigationDelegate = adoptNS([WKBackForwardNavigationDelegate new]); + webView.get().navigationDelegate = navigationDelegate.get(); + + [webView loadRequest:server.request("/source.html"_s)]; + [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; + + [webView evaluateJavaScript:@"document.getElementById('testLink').click()" completionHandler:nil]; + [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; + + EXPECT_EQ([webView backForwardList].backList.count, 1U); + EXPECT_EQ([webView backForwardList].forwardList.count, 0U); + EXPECT_STREQ([webView URL].absoluteString.UTF8String, server.request("/form.html"_s).URL.absoluteString.UTF8String); + + // Wait for form submission to happen. + [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; + EXPECT_EQ([webView backForwardList].backList.count, 1U); + EXPECT_EQ([webView backForwardList].forwardList.count, 0U); + EXPECT_STREQ([webView URL].absoluteString.UTF8String, server.request("/redirect.html"_s).URL.absoluteString.UTF8String); + + // Wait for redirect to finish. + [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; + EXPECT_EQ([webView backForwardList].backList.count, 1U); + EXPECT_EQ([webView backForwardList].forwardList.count, 0U); + EXPECT_STREQ([webView URL].absoluteString.UTF8String, server.request("/destination.html"_s).URL.absoluteString.UTF8String); + + [webView goBack]; + [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; + EXPECT_EQ([webView backForwardList].backList.count, 0U); + EXPECT_EQ([webView backForwardList].forwardList.count, 1U); + EXPECT_STREQ([webView URL].absoluteString.UTF8String, server.request("/source.html"_s).URL.absoluteString.UTF8String); +} + +static void runBackForwardNavigationDoesNotSkipItemsWithUserGestureTest(Function&& navigate) +{ + auto webView = adoptNS([[WKWebView alloc] init]); + + auto navigationDelegate = adoptNS([WKBackForwardNavigationDelegate new]); + webView.get().navigationDelegate = navigationDelegate.get(); + + // Test case: url1 -> url2 -> url2#a (with user gesture) + // No item should be skipped when navigating backwards or forwards. + + RetainPtr url1 = [NSBundle.test_resourcesBundle URLForResource:@"simple" withExtension:@"html"]; + RetainPtr url2 = [NSBundle.test_resourcesBundle URLForResource:@"simple2" withExtension:@"html"]; + + [webView loadRequest:[NSURLRequest requestWithURL:url1.get()]]; + [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; + + [webView loadRequest:[NSURLRequest requestWithURL:url2.get()]]; + [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; + + // Add back/forward list items without user gestures. + navigate(webView.get(), "#a"_s); + [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; + RetainPtr expectedURLString = makeString(String([url2 absoluteString]), "#a"_s).createNSString(); + EXPECT_WK_STREQ([lastNavigation _request].URL.absoluteString.UTF8String, expectedURLString.get().UTF8String); + + RetainPtr lastURL = [webView URL]; + EXPECT_FALSE([lastURL isEqual:url2.get()]); + + EXPECT_FALSE(webView.get().backForwardList.backItem._wasCreatedByJSWithoutUserInteraction); + [webView goBack]; + [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; + EXPECT_WK_STREQ([lastNavigation _request].URL.absoluteString.UTF8String, [url2 absoluteString].UTF8String); + + EXPECT_STREQ([webView URL].absoluteString.UTF8String, [url2 absoluteString].UTF8String); + + EXPECT_FALSE(webView.get().backForwardList.backItem._wasCreatedByJSWithoutUserInteraction); + [webView goBack]; + [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; + + EXPECT_STREQ([webView URL].absoluteString.UTF8String, [url1 absoluteString].UTF8String); + + [webView goForward]; + [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; + + EXPECT_STREQ([webView URL].absoluteString.UTF8String, [url2 absoluteString].UTF8String); + + [webView goForward]; + [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; + expectedURLString = makeString(String([url2 absoluteString]), "#a"_s).createNSString(); + EXPECT_WK_STREQ([lastNavigation _request].URL.absoluteString.UTF8String, expectedURLString.get().UTF8String); + + EXPECT_STREQ([webView URL].absoluteString.UTF8String, [lastURL absoluteString].UTF8String); +} + +TEST(WKBackForwardList, BackForwardNavigationDoesNotSkipItemsWithUserGesturePushState) +{ + runBackForwardNavigationDoesNotSkipItemsWithUserGestureTest([](WKWebView *webView, ASCIILiteral fragment) { + [webView evaluateJavaScript:makeString("history.pushState(null, document.title, location.pathname + '"_s, fragment, "');"_s).createNSString().get() completionHandler:nil]; + }); +} + +TEST(WKBackForwardList, BackForwardNavigationDoesNotSkipItemsWithUserGestureFragment) +{ + runBackForwardNavigationDoesNotSkipItemsWithUserGestureTest([](WKWebView *webView, ASCIILiteral fragment) { + [webView evaluateJavaScript:makeString("location.href = location.pathname + '"_s, fragment, "';"_s).createNSString().get() completionHandler:nil]; + }); +} + +TEST(WKBackForwardList, BackForwardNavigationDoesNotSkipItemsFromLoadRequest) +{ + runBackForwardNavigationDoesNotSkipItemsWithUserGestureTest([](WKWebView *webView, ASCIILiteral fragment) { + auto newURLString = makeString(String([webView URL].absoluteString), fragment); + [webView loadRequest:adoptNS([[NSURLRequest alloc] initWithURL:adoptNS([[NSURL alloc] initWithString:newURLString.createNSString().get()]).get()]).get()]; + }); +} + +TEST(WKBackForwardList, BackForwardNavigationDoesNotSkipItemsWithRecentUserGesturePushState) +{ + runBackForwardNavigationDoesNotSkipItemsWithUserGestureTest([](WKWebView *webView, ASCIILiteral fragment) { + // Call pushState() in a setTimeout() so that it has a recent user gesture but not a current one. + [webView evaluateJavaScript:makeString("setTimeout(() => { history.pushState(null, document.title, location.pathname + '"_s, fragment, "'); }, 0);"_s).createNSString().get() completionHandler:nil]; + }); +} + +TEST(WKBackForwardList, BackForwardNavigationDoesNotSkipItemsWithRecentUserGestureFragment) +{ + runBackForwardNavigationDoesNotSkipItemsWithUserGestureTest([](WKWebView *webView, ASCIILiteral fragment) { + // Do fragment navigation in a setTimeout() so that it has a recent user gesture but not a current one. + [webView evaluateJavaScript:makeString("setTimeout(() => { location.href = location.pathname + '"_s, fragment, "'; }, 0);"_s).createNSString().get() completionHandler:nil]; + }); +} + +TEST(WKBackForwardList, BackForwardNavigationDoesNotSkipUpdatedItemWithRecentUserGesture) +{ + auto webView = adoptNS([[WKWebView alloc] init]); + + auto navigationDelegate = adoptNS([WKBackForwardNavigationDelegate new]); + webView.get().navigationDelegate = navigationDelegate.get(); + + RetainPtr url1 = [NSBundle.test_resourcesBundle URLForResource:@"simple" withExtension:@"html"]; + RetainPtr url2 = [NSBundle.test_resourcesBundle URLForResource:@"fragment-navigation-before-load-event" withExtension:@"html"]; + + [webView loadRequest:[NSURLRequest requestWithURL:url1.get()]]; + [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; + + [webView loadRequest:[NSURLRequest requestWithURL:url2.get()]]; + [navigationDelegate waitForDidFinishNavigation]; + + // Page navigated to #fragment before the load event. + RetainPtr expectedURLString = makeString(String([url2 absoluteString]), "#fragment"_s).createNSString(); + EXPECT_STREQ([webView URL].absoluteString.UTF8String, expectedURLString.get().UTF8String); + + // Navigate with a user gesture. + [webView evaluateJavaScript:@"location.href = location.pathname + '#otherFragment';" completionHandler:nil]; + [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; + + // Should go back to #fragment. + [webView goBack]; + [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; + + EXPECT_STREQ([webView URL].absoluteString.UTF8String, expectedURLString.get().UTF8String); +} + +TEST(WKBackForwardList, BackNavigationHijacking) +{ + auto webView = adoptNS([[WKWebView alloc] init]); + + auto navigationDelegate = adoptNS([WKBackForwardNavigationDelegate new]); + webView.get().navigationDelegate = navigationDelegate.get(); + + RetainPtr url1 = [NSBundle.test_resourcesBundle URLForResource:@"simple" withExtension:@"html"]; + RetainPtr url2 = [NSBundle.test_resourcesBundle URLForResource:@"simple2" withExtension:@"html"]; + + [webView loadRequest:[NSURLRequest requestWithURL:url1.get()]]; + [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; + + [webView _evaluateJavaScriptWithoutUserGesture:@"history.pushState(null, null, '');" completionHandler:nil]; + __block bool ranJS = false; + [webView _evaluateJavaScriptWithoutUserGesture:@"onpopstate = (e) => { history.forward(); };false" completionHandler:^(id, NSError *) { + ranJS = true; + }]; + [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; + TestWebKitAPI::Util::run(&ranJS); + + [webView loadRequest:[NSURLRequest requestWithURL:url2.get()]]; + [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; + EXPECT_STREQ([webView URL].absoluteString.UTF8String, [url2 absoluteString].UTF8String); + + EXPECT_TRUE(webView.get().backForwardList.backItem._wasCreatedByJSWithoutUserInteraction); + [webView goBack]; + [navigationDelegate waitForDidFinishNavigationOrDidSameDocumentNavigation]; + EXPECT_STREQ([webView URL].absoluteString.UTF8String, [url1 absoluteString].UTF8String); + + TestWebKitAPI::Util::spinRunLoop(10); + usleep(100000); + TestWebKitAPI::Util::spinRunLoop(10); + + EXPECT_STREQ([webView URL].absoluteString.UTF8String, [url1 absoluteString].UTF8String); +} + +TEST(WKBackForwardList, BackForwardListRemoveAndAddSubframes) +{ + auto indexHTML = "" + ""_s; + TestWebKitAPI::HTTPServer server({ + { "/index"_s, { indexHTML } }, + { "/frame1"_s, { ""_s } }, + { "/frame2"_s, { ""_s } }, + { "/frame3"_s, { ""_s } }, + }, TestWebKitAPI::HTTPServer::Protocol::Https); + auto webView = adoptNS([[WKWebView alloc] init]); + auto navigationDelegate = adoptNS([WKBackForwardNavigationDelegate new]); + webView.get().navigationDelegate = navigationDelegate.get(); + auto uiDelegate = adoptNS([TestUIDelegate new]); + webView.get().UIDelegate = uiDelegate.get(); + + [webView loadRequest:server.request("/index"_s)]; + EXPECT_WK_STREQ([uiDelegate waitForAlert], "frame2"); + + [webView _frames:^(_WKFrameTreeNode *mainFrame) { + [webView evaluateJavaScript:@"location.href = '/frame3'" inFrame:mainFrame.childFrames[1].info inContentWorld:WKContentWorld.pageWorld completionHandler:nil]; + }]; + EXPECT_WK_STREQ([uiDelegate waitForAlert], "frame3"); + + auto removeAndAddFrame = @"let frame = document.getElementById('1');" + "frame.parentNode.removeChild(frame);" + "let newFrame = document.createElement('iframe');" + "newFrame.src = '/frame1';" + "document.body.appendChild(newFrame);"; + __block bool done = false; + [webView evaluateJavaScript:removeAndAddFrame completionHandler:^(id, NSError *) { + done = true; + }]; + TestWebKitAPI::Util::run(&done); + done = false; + + [webView goBack]; + EXPECT_WK_STREQ([uiDelegate waitForAlert], "frame2"); + + __block auto expectedFrameURL = server.request("/frame2"_s).URL.absoluteString.UTF8String; + [webView evaluateJavaScript:@"document.getElementById('2').contentWindow.location.href" completionHandler:^(id result, NSError *) { + EXPECT_WK_STREQ(expectedFrameURL, [result UTF8String]); + done = true; + }]; + TestWebKitAPI::Util::run(&done); +} + +TEST(WKBackForwardList, SessionStateTitleTruncation) +{ + TestWebKitAPI::HTTPServer server({ + { "/"_s, { ""_s } } + }); + + auto webView = adoptNS([WKWebView new]); + [webView loadRequest:server.request()]; + while (!webView.get().canGoBack) + TestWebKitAPI::Util::spinRunLoop(); + while (webView.get()._sessionState.data.length < 1000u) + TestWebKitAPI::Util::spinRunLoop(); + _WKSessionState *sessionState = webView.get()._sessionState; + NSData *stateData = sessionState.data; + EXPECT_LT(stateData.length, 2000u); +} + +TEST(WKBackForwardList, RestoreSessionStateResetProvisionalItem) +{ + RetainPtr webView = adoptNS([[TestWKWebView alloc] init]); + [webView synchronouslyLoadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:loadableURL1]]]; + [webView synchronouslyLoadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:loadableURL2]]]; + [webView synchronouslyGoBack]; + [webView synchronouslyGoForward]; + + RetainPtr sessionState = [webView _sessionStateWithFilter:^BOOL(WKBackForwardListItem *item) { + return [item.URL isEqual:[NSURL URLWithString:loadableURL1]]; + }]; + [webView _restoreSessionState:sessionState.get() andNavigate:NO]; + [[webView backForwardList] currentItem]; +} + +TEST(WKBackForwardList, GoBackToPageAfterNavigatingIframeAndRestoringSession) +{ + TestWebKitAPI::HTTPServer server({ + { "/example"_s, { ""_s } }, + { "/a"_s, { ""_s } }, + { "/b"_s, { ""_s } }, + }); + RetainPtr webView = adoptNS([[WKWebView alloc] init]); + [webView loadRequest:server.request("/example"_s)]; + EXPECT_WK_STREQ([webView _test_waitForAlert], "a"); + + [webView evaluateJavaScript:@"document.querySelector('iframe').src = '/b';" completionHandler:nil]; + EXPECT_WK_STREQ([webView _test_waitForAlert], "b"); + + [webView loadRequest:server.requestWithLocalhost("/example"_s)]; + EXPECT_WK_STREQ([webView _test_waitForAlert], "a"); + + [webView _restoreSessionState:[webView _sessionState] andNavigate:NO]; + [webView goBack]; + EXPECT_WK_STREQ([webView _test_waitForAlert], "b"); + EXPECT_WK_STREQ([webView URL].absoluteString, server.request("/example"_s).URL.absoluteString.UTF8String); +} + +#if ENABLE(IPC_TESTING_API) + +static void enableIPCTestingAPI(WKWebViewConfiguration *configuration) +{ + for (_WKFeature *feature in [WKPreferences _features]) { + if ([feature.key isEqualToString:@"IPCTestingAPIEnabled"]) { + [[configuration preferences] _setEnabled:YES forFeature:feature]; + break; + } + } +} + +TEST(WKBackForwardList, BackForwardUpdateItemRejectsFileURL) +{ + TestWebKitAPI::HTTPServer server({ + { "/page"_s, { "page"_s } }, + }, TestWebKitAPI::HTTPServer::Protocol::Http); + + auto poolConfig = adoptNS([[_WKProcessPoolConfiguration alloc] init]); + [poolConfig setProcessSwapsOnNavigation:YES]; + auto pool = adoptNS([[WKProcessPool alloc] _initWithConfiguration:poolConfig.get()]); + + auto configA = adoptNS([[WKWebViewConfiguration alloc] init]); + [configA setProcessPool:pool.get()]; + enableIPCTestingAPI(configA.get()); + + auto webViewA = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configA.get()]); + [webViewA synchronouslyLoadRequest:server.request("/page"_s)]; + + // B shares A's process via _relatedWebView. + auto configB = adoptNS([[WKWebViewConfiguration alloc] init]); + [configB setProcessPool:pool.get()]; + configB.get()._relatedWebView = webViewA.get(); + enableIPCTestingAPI(configB.get()); + + auto webViewB = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configB.get()]); + [webViewB synchronouslyLoadRequest:server.request("/page"_s)]; + + EXPECT_EQ([webViewA _webProcessIdentifier], [webViewB _webProcessIdentifier]); + + long long bPageID = [[webViewB stringByEvaluatingJavaScript:@"String(IPC.pageID)"] longLongValue]; + EXPECT_GT(bPageID, 0LL); + + // Using pushState on A, capture an `AddItem` IPC message for later use. + [webViewA stringByEvaluatingJavaScript: + @"(function() {" + " var addName = null, updateName = null;" + " var keys = Object.keys(IPC.messages);" + " for (var i = 0; i < keys.length; i++) {" + " if (keys[i].indexOf('BackForwardAddItem') !== -1 && keys[i].indexOf('InProcess') === -1)" + " addName = IPC.messages[keys[i]].name;" + " if (keys[i].indexOf('BackForwardUpdateItem') !== -1)" + " updateName = IPC.messages[keys[i]].name;" + " }" + " window._updateItemMsgName = updateName;" + " window._addBuf = null;" + " IPC.addOutgoingMessageListener('UI', function(msg) {" + " if (msg.name === addName && msg.buffer)" + " window._addBuf = new Uint8Array(msg.buffer.slice(0));" + " });" + " history.pushState({}, '', '?pad=' + 'X'.repeat(40));" + "})()" + ]; + int attempts = 0; + while (![[webViewA stringByEvaluatingJavaScript:@"window._addBuf ? 'ok' : ''"] isEqualToString:@"ok"] && ++attempts < 50) + TestWebKitAPI::Util::spinRunLoop(5); + EXPECT_LT(attempts, 50); + + // Modify the captured buffer to carry a file:// URL, then send it as + // BackForwardUpdateItem to page B's destination ID. B's handler + // resolves A's item via the global itemForID map — the ownership + // check (item->pageID() vs B's identifier) must reject it. + NSString *attackJS = [NSString stringWithFormat: + @"(function() {" + "var HDR = 0x10;" + "var args = new Uint8Array(window._addBuf.buffer.slice(HDR));" + "var origURL = location.href;" + "var targetBase = 'file:///etc/passwd';" + "var padTarget = targetBase + '/'.repeat(Math.max(0, origURL.length - targetBase.length));" + "if (padTarget.length !== origURL.length) return 'FAIL:len_mismatch';" + "var oldBytes = new TextEncoder().encode(origURL);" + "var newBytes = new TextEncoder().encode(padTarget);" + "var count = 0;" + "for (var i = 0; i <= args.length - oldBytes.length; i++) {" + " var match = true;" + " for (var j = 0; j < oldBytes.length; j++) {" + " if (args[i+j] !== oldBytes[j]) { match = false; break; }" + " }" + " if (match) {" + " args.set(newBytes, i);" + " count++;" + " i += oldBytes.length - 1;" + " }" + "}" + "if (!count) return 'FAIL:no_url_found';" + "IPC.sendMessage('UI', %lld, window._updateItemMsgName, args);" + "return 'sent:' + count;" + "})()", bPageID + ]; + NSString *attackResult = [webViewA stringByEvaluatingJavaScript:attackJS]; + EXPECT_TRUE([attackResult hasPrefix:@"sent:"]); + + for (int i = 0; i < 100; i++) + TestWebKitAPI::Util::spinRunLoop(); + + WKBackForwardList *list = [webViewA backForwardList]; + EXPECT_FALSE([list.currentItem.URL.absoluteString hasPrefix:@"file://"]); + for (WKBackForwardListItem *item in list.backList) + EXPECT_FALSE([item.URL.absoluteString hasPrefix:@"file://"]); +} + +static constexpr auto forgedFileURLTestMainBytes = R"TESTRESOURCE( + + + +)TESTRESOURCE"_s; + +TEST(WKBackForwardList, ForgedFileURLItemIsRejected) +{ + RetainPtr coreIPCURL = [NSBundle.test_resourcesBundle URLForResource:@"coreipc" withExtension:@"js"]; + RetainPtr coreIPCData = [NSData dataWithContentsOfURL:coreIPCURL.get()]; + ++ TestWebKitAPI::HTTPServer server({ ++ { "/"_s, { forgedFileURLTestMainBytes } }, ++ { "/coreipc.js"_s, { { { "Content-Type"_s, "text/javascript"_s } }, coreIPCData.get() } }, ++ }); ++ ++ RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]); ++ for (_WKFeature *feature in [WKPreferences _features]) { ++ if ([feature.key isEqualToString:@"IPCTestingAPIEnabled"]) { ++ [[configuration preferences] _setEnabled:YES forFeature:feature]; ++ break; ++ } ++ } ++ ++ RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration.get()]); ++ [webView loadRequest:server.request("/"_s)]; ++ ++ EXPECT_WK_STREQ([webView _test_waitForAlert], "PASS: forged file:// back-forward item was rejected by MESSAGE_CHECK"); ++} + +#endif // ENABLE(IPC_TESTING_API) From ef2bac73ff9346238a2821c222980d38f9aaad97 Mon Sep 17 00:00:00 2001 From: Karl Dubost Date: Tue, 30 Jun 2026 07:20:31 -0700 Subject: [PATCH 30/84] Combining marks on SVG render as dotted circles https://bugs.webkit.org/show_bug.cgi?id=266832 rdar://120284006 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed by Elika Etemad. SVG text on a path (and any per-character-positioned text) was laid out one Unicode code point at a time: SVGTextLayoutEngine opened a fresh single-code-point fragment for every code point whenever a per-character condition fired (text on a path, an explicit rotate, vertical text, …), and SVGTextBoxPainter then shaped each fragment's substring in isolation. A lone combining mark shaped on its own (e.g. U+0301, or the Devanagari virama U+094D) makes the font emit a dotted circle. SVG 2 section 11 addresses the text-positioning attributes (x/y/dx/dy/rotate) per Unicode code point, but it also defines a "typographic character" a UAX #29 grapheme cluster such as a base letter plus its combining marks, or a Devanagari syllable as an indivisible unit whose internal glyph arrangement is "not user controllable". So code points are addressed, but typographic characters are positioned and shaped. WebKit placed code points. Compute grapheme-cluster boundaries over the renderer text with an ICU character-break iterator, keyed by the same code-unit offset the layout loop walks, and only let a cluster-start code point open a new fragment. A continuation code point (a combining mark, a Devanagari continuation, the second regional indicator of a flag) now joins the current fragment, so the painter hands base + marks to the shaper together. This is font-independent, so it groups base + mark even when the font has no composed glyph -- precisely the dotted-circle case. Per-code-point metrics, character-data lookups, and the getNumberOfChars / query contract are unchanged; the fragment simply spans multiple metrics entries, which recordTextFragment already sums. Matches Gecko and Blink, which both treat the typographic character as the positioning unit. For pure ASCII / precomposed Latin-1 text every code point is a grapheme boundary, so behavior is unchanged there. Tests: imported/w3c/web-platform-tests/svg/text/reftests/textpath-combining-marks-ref.html imported/w3c/web-platform-tests/svg/text/reftests/textpath-combining-marks.html * LayoutTests/imported/w3c/web-platform-tests/svg/text/reftests/textpath-combining-marks-expected.html: Added. * LayoutTests/imported/w3c/web-platform-tests/svg/text/reftests/textpath-combining-marks-ref.html: Added. * LayoutTests/imported/w3c/web-platform-tests/svg/text/reftests/textpath-combining-marks.html: Added. * Source/WebCore/rendering/svg/SVGTextLayoutEngine.cpp: (WebCore::SVGTextLayoutEngine::layoutTextOnLineOrPath): Canonical link: https://commits.webkit.org/316144@main --- .../textpath-combining-marks-expected.html | 17 ++++++++++++++ .../textpath-combining-marks-ref.html | 17 ++++++++++++++ .../reftests/textpath-combining-marks.html | 23 +++++++++++++++++++ .../rendering/svg/SVGTextLayoutEngine.cpp | 12 ++++++++-- 4 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 LayoutTests/imported/w3c/web-platform-tests/svg/text/reftests/textpath-combining-marks-expected.html create mode 100644 LayoutTests/imported/w3c/web-platform-tests/svg/text/reftests/textpath-combining-marks-ref.html create mode 100644 LayoutTests/imported/w3c/web-platform-tests/svg/text/reftests/textpath-combining-marks.html diff --git a/LayoutTests/imported/w3c/web-platform-tests/svg/text/reftests/textpath-combining-marks-expected.html b/LayoutTests/imported/w3c/web-platform-tests/svg/text/reftests/textpath-combining-marks-expected.html new file mode 100644 index 000000000000..0798211e668a --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/svg/text/reftests/textpath-combining-marks-expected.html @@ -0,0 +1,17 @@ + + + +SVG text: combining marks on a textPath shape as one typographic character (reference) + + + + + + + + café exposé + + + + + diff --git a/LayoutTests/imported/w3c/web-platform-tests/svg/text/reftests/textpath-combining-marks-ref.html b/LayoutTests/imported/w3c/web-platform-tests/svg/text/reftests/textpath-combining-marks-ref.html new file mode 100644 index 000000000000..0798211e668a --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/svg/text/reftests/textpath-combining-marks-ref.html @@ -0,0 +1,17 @@ + + + +SVG text: combining marks on a textPath shape as one typographic character (reference) + + + + + + + + café exposé + + + + + diff --git a/LayoutTests/imported/w3c/web-platform-tests/svg/text/reftests/textpath-combining-marks.html b/LayoutTests/imported/w3c/web-platform-tests/svg/text/reftests/textpath-combining-marks.html new file mode 100644 index 000000000000..7ab7b5361916 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/svg/text/reftests/textpath-combining-marks.html @@ -0,0 +1,23 @@ + + + + +SVG text: combining marks on a textPath shape as one typographic character + + + + + + + + + + + café exposé + + + + + diff --git a/Source/WebCore/rendering/svg/SVGTextLayoutEngine.cpp b/Source/WebCore/rendering/svg/SVGTextLayoutEngine.cpp index 74c2799243af..69b8ec1f3b55 100644 --- a/Source/WebCore/rendering/svg/SVGTextLayoutEngine.cpp +++ b/Source/WebCore/rendering/svg/SVGTextLayoutEngine.cpp @@ -33,6 +33,8 @@ #include "SVGTextLayoutEngineBaseline.h" #include "SVGTextLayoutEngineSpacing.h" #include "StyleComputedStyle+GettersInlines.h" +#include +#include // Set to a value > 0 to dump the text fragments #define DUMP_SVG_TEXT_LAYOUT_FRAGMENTS 0 @@ -434,6 +436,9 @@ void SVGTextLayoutEngine::layoutTextOnLineOrPath(InlineIterator::SVGTextBoxItera float baselineShift = baselineLayout.calculateBaselineShift(style); baselineShift -= baselineLayout.calculateAlignmentBaselineShift(m_isVerticalText, text); + // Grapheme-cluster boundaries, keyed by the same code-unit offset the loop walks (m_visualCharacterOffset). + NonSharedCharacterBreakIterator clusterIterator(StringView(text.text())); + // Main layout algorithm. while (true) { // Find the start of the current text box in this list, respecting ligatures. @@ -629,9 +634,12 @@ void SVGTextLayoutEngine::layoutTextOnLineOrPath(InlineIterator::SVGTextBoxItera m_lastChunkHasTextLength = definesTextLength; } + // Only a cluster start may open a new fragment, so a combining mark stays with its base rather than being shaped alone (dotted circle). Bug 266832. + bool isClusterStart = ubrk_isBoundary(clusterIterator, m_visualCharacterOffset); + // Determine whether we have to start a new fragment. - bool shouldStartNewFragment = hasXOrY || m_dx || m_dy || m_isVerticalText || m_inPathLayout || angle || angle != lastAngle - || orientationAngle || applySpacingToNextCharacter || definesTextLength; + bool shouldStartNewFragment = isClusterStart && (hasXOrY || m_dx || m_dy || m_isVerticalText || m_inPathLayout || angle || angle != lastAngle + || orientationAngle || applySpacingToNextCharacter || definesTextLength); // If we already started a fragment, close it now. if (didStartTextFragment && shouldStartNewFragment) { From 5c93ade672f8cebd902fc2e07d4656f7a4db0d00 Mon Sep 17 00:00:00 2001 From: Lily Spiniolas Date: Tue, 30 Jun 2026 07:23:29 -0700 Subject: [PATCH 31/84] [macOS] Scroll pocket color may be lost in fullscreen after refresh https://bugs.webkit.org/show_bug.cgi?id=317824 rdar://179516708 Reviewed by Wenson Hsieh and Abrar Rahman Protyasha. In Safari, if the option to automatically hide the toolbar in fullscreen is enabled, the top obscured content inset becomes zero while the toolbar is hidden. In WebPage::sidesRequiringFixedContainerEdges(), we skip the top edge if the top obscured content inset is zero. This means that, if a user is in fullscreen with the toolbar hidden, navigates to a website with a fixed top header, scrolls down, and then reveals the toolbar, the scroll pocket color will not match the header. To fix this, we add an aditional case where we add the top edge: when the window is in fullscreen, and the toolbar overlay height is greater than zero. Tested by new `ObscuredContentInsets` API tests `ScrollPocketCoversFullScreenTitlebarAfterReload` and `ScrollPocketCoversFullScreenTitlebarAfterMovingIntoFullScreenWindow`. * Source/WebKit/Shared/WebPageCreationParameters.h: * Source/WebKit/Shared/WebPageCreationParameters.serialization.in: * Source/WebKit/UIProcess/WebPageProxy.cpp: (WebKit::WebPageProxy::creationParameters): * Source/WebKit/UIProcess/WebPageProxy.h: * Source/WebKit/UIProcess/mac/WebPageProxyMac.mm: (WebKit::WebPageProxy::setWindowIsInNativeFullScreen): (WebKit::WebPageProxy::setFullScreenTitlebarOverlayIsRevealed): * Source/WebKit/UIProcess/mac/WebViewImpl.h: * Source/WebKit/UIProcess/mac/WebViewImpl.mm: (-[WKWindowVisibilityObserver startObserving:]): (-[WKWindowVisibilityObserver _windowDidEnterFullScreen:]): (-[WKWindowVisibilityObserver _windowDidExitFullScreen:]): (WebKit::WebViewImpl::windowDidEnterOrExitFullScreen): (WebKit::WebViewImpl::viewDidMoveToWindow): (WebKit::WebViewImpl::setFullScreenTitlebarOverlayHeight): (-[WKWindowVisibilityObserver _windowDidEnterOrExitFullScreen:]): Deleted. * Source/WebKit/WebProcess/WebPage/Cocoa/WebPageCocoa.mm: (WebKit::WebPage::sidesRequiringFixedContainerEdges const): * Source/WebKit/WebProcess/WebPage/WebPage.cpp: * Source/WebKit/WebProcess/WebPage/WebPage.h: * Source/WebKit/WebProcess/WebPage/WebPage.messages.in: * Tools/TestWebKitAPI/Tests/WebKit/WKWebView/ObscuredContentInsets.mm: (TestWebKitAPI::TEST(ObscuredContentInsets, ScrollPocketCoversFullScreenTitlebar)): (TestWebKitAPI::createFullScreenCapableWindow): (TestWebKitAPI::TEST(ObscuredContentInsets, ScrollPocketCoversFullScreenTitlebarAfterReload)): (TestWebKitAPI::TEST(ObscuredContentInsets, ScrollPocketCoversFullScreenTitlebarAfterMovingIntoFullScreenWindow)): Canonical link: https://commits.webkit.org/316145@main --- .../WebKit/Shared/WebPageCreationParameters.h | 3 + ...WebPageCreationParameters.serialization.in | 3 + Source/WebKit/UIProcess/WebPageProxy.cpp | 3 + Source/WebKit/UIProcess/WebPageProxy.h | 13 ++ .../WebKit/UIProcess/mac/WebPageProxyMac.mm | 31 ++++ Source/WebKit/UIProcess/mac/WebViewImpl.h | 2 +- Source/WebKit/UIProcess/mac/WebViewImpl.mm | 29 +++- .../WebProcess/WebPage/Cocoa/WebPageCocoa.mm | 5 + Source/WebKit/WebProcess/WebPage/WebPage.cpp | 3 + Source/WebKit/WebProcess/WebPage/WebPage.h | 7 +- .../WebProcess/WebPage/WebPage.messages.in | 3 + .../WebKit/WKWebView/ObscuredContentInsets.mm | 155 ++++++++++++++++++ 12 files changed, 250 insertions(+), 7 deletions(-) diff --git a/Source/WebKit/Shared/WebPageCreationParameters.h b/Source/WebKit/Shared/WebPageCreationParameters.h index 9558194e444c..f912f9204579 100644 --- a/Source/WebKit/Shared/WebPageCreationParameters.h +++ b/Source/WebKit/Shared/WebPageCreationParameters.h @@ -332,6 +332,9 @@ struct WebPageCreationParameters { #if PLATFORM(MAC) double overflowHeightForTopScrollEdgeEffect { 0 }; +#if ENABLE(SCROLL_POCKET_IN_FULLSCREEN) + bool fullScreenTitlebarOverlayIsDisplayed { false }; +#endif #if HAVE(NSVIEW_CORNER_CONFIGURATION) WebCore::CornerRadii scrollbarAvoidanceCornerRadii; #endif diff --git a/Source/WebKit/Shared/WebPageCreationParameters.serialization.in b/Source/WebKit/Shared/WebPageCreationParameters.serialization.in index 57d7197ca6c9..8db5473446e6 100644 --- a/Source/WebKit/Shared/WebPageCreationParameters.serialization.in +++ b/Source/WebKit/Shared/WebPageCreationParameters.serialization.in @@ -246,6 +246,9 @@ enum class WebCore::UserInterfaceLayoutDirection : bool; #if PLATFORM(MAC) double overflowHeightForTopScrollEdgeEffect; +#if ENABLE(SCROLL_POCKET_IN_FULLSCREEN) + bool fullScreenTitlebarOverlayIsDisplayed; +#endif #if HAVE(NSVIEW_CORNER_CONFIGURATION) WebCore::CornerRadii scrollbarAvoidanceCornerRadii; #endif diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp index 9ff190ec986d..9bfa2641939c 100644 --- a/Source/WebKit/UIProcess/WebPageProxy.cpp +++ b/Source/WebKit/UIProcess/WebPageProxy.cpp @@ -13841,6 +13841,9 @@ WebPageCreationParameters WebPageProxy::creationParameters(WebProcessProxy& proc if (m_viewWindowCoordinates) parameters.viewWindowCoordinates = *m_viewWindowCoordinates; parameters.overflowHeightForTopScrollEdgeEffect = m_overflowHeightForTopScrollEdgeEffect; +#if ENABLE(SCROLL_POCKET_IN_FULLSCREEN) + parameters.fullScreenTitlebarOverlayIsDisplayed = fullScreenTitlebarOverlayIsDisplayed(); +#endif #if HAVE(NSVIEW_CORNER_CONFIGURATION) parameters.scrollbarAvoidanceCornerRadii = internals().scrollbarAvoidanceCornerRadii; #endif diff --git a/Source/WebKit/UIProcess/WebPageProxy.h b/Source/WebKit/UIProcess/WebPageProxy.h index 4dc6cd5f94cd..fd9ea7fb4b33 100644 --- a/Source/WebKit/UIProcess/WebPageProxy.h +++ b/Source/WebKit/UIProcess/WebPageProxy.h @@ -1040,6 +1040,15 @@ class WebPageProxy final : public API::ObjectImpl, publ double overflowHeightForTopScrollEdgeEffect() const { return m_overflowHeightForTopScrollEdgeEffect; } void setOverflowHeightForTopScrollEdgeEffect(double); + +#if ENABLE(SCROLL_POCKET_IN_FULLSCREEN) + enum class WindowIsInNativeFullScreen : bool { No, Yes }; + void setWindowIsInNativeFullScreen(WindowIsInNativeFullScreen); + void setFullScreenTitlebarOverlayIsRevealed(bool); + + bool fullScreenTitlebarOverlayIsDisplayed() const { return m_windowIsInNativeFullScreen && m_fullScreenTitlebarOverlayIsRevealed; } +#endif + #if HAVE(NSVIEW_CORNER_CONFIGURATION) void setScrollbarAvoidanceCornerRadii(WebCore::CornerRadii&&); #endif @@ -3832,6 +3841,10 @@ class WebPageProxy final : public API::ObjectImpl, publ #if PLATFORM(MAC) bool m_acceptsFirstMouse { false }; double m_overflowHeightForTopScrollEdgeEffect { 0 }; +#if ENABLE(SCROLL_POCKET_IN_FULLSCREEN) + bool m_windowIsInNativeFullScreen { false }; + bool m_fullScreenTitlebarOverlayIsRevealed { false }; +#endif #endif #if USE(SYSTEM_PREVIEW) diff --git a/Source/WebKit/UIProcess/mac/WebPageProxyMac.mm b/Source/WebKit/UIProcess/mac/WebPageProxyMac.mm index 456ef335e3f0..571be8819700 100644 --- a/Source/WebKit/UIProcess/mac/WebPageProxyMac.mm +++ b/Source/WebKit/UIProcess/mac/WebPageProxyMac.mm @@ -479,6 +479,37 @@ static inline bool expectsLegacyImplicitRubberBandControl() protect(legacyMainFrameProcess())->send(Messages::WebPage::SetOverflowHeightForTopScrollEdgeEffect(value), webPageIDInMainFrameProcess()); } +#if ENABLE(SCROLL_POCKET_IN_FULLSCREEN) + +void WebPageProxy::setWindowIsInNativeFullScreen(WindowIsInNativeFullScreen windowIsInNativeFullScreen) +{ + auto isInFullScreen = windowIsInNativeFullScreen == WindowIsInNativeFullScreen::Yes; + if (m_windowIsInNativeFullScreen == isInFullScreen) + return; + + m_windowIsInNativeFullScreen = isInFullScreen; + + if (!hasRunningProcess()) + return; + + protect(legacyMainFrameProcess())->send(Messages::WebPage::SetFullScreenTitlebarOverlayIsDisplayed(fullScreenTitlebarOverlayIsDisplayed()), webPageIDInMainFrameProcess()); +} + +void WebPageProxy::setFullScreenTitlebarOverlayIsRevealed(bool fullScreenTitlebarOverlayIsRevealed) +{ + if (m_fullScreenTitlebarOverlayIsRevealed == fullScreenTitlebarOverlayIsRevealed) + return; + + m_fullScreenTitlebarOverlayIsRevealed = fullScreenTitlebarOverlayIsRevealed; + + if (!hasRunningProcess()) + return; + + protect(legacyMainFrameProcess())->send(Messages::WebPage::SetFullScreenTitlebarOverlayIsDisplayed(fullScreenTitlebarOverlayIsDisplayed()), webPageIDInMainFrameProcess()); +} + +#endif + void WebPageProxy::setObscuredContentInsetsAsync(const FloatBoxExtent& obscuredContentInsets) { m_internals->pendingObscuredContentInsets = obscuredContentInsets; diff --git a/Source/WebKit/UIProcess/mac/WebViewImpl.h b/Source/WebKit/UIProcess/mac/WebViewImpl.h index 4e6601bd2421..f0f5fd52ab90 100644 --- a/Source/WebKit/UIProcess/mac/WebViewImpl.h +++ b/Source/WebKit/UIProcess/mac/WebViewImpl.h @@ -362,7 +362,7 @@ class WebViewImpl final : public CanMakeWeakPtr, public CanMakeChec void windowDidChangeOcclusionState(); void windowWillClose(); void NODELETE windowWillEnterOrExitFullScreen(); - void windowDidEnterOrExitFullScreen(); + void windowDidEnterOrExitFullScreen(bool windowIsInFullScreen); void screenDidChangeColorSpace(); bool shouldDelayWindowOrderingForEvent(NSEvent *); bool windowResizeMouseLocationIsInVisibleScrollerThumb(CGPoint); diff --git a/Source/WebKit/UIProcess/mac/WebViewImpl.mm b/Source/WebKit/UIProcess/mac/WebViewImpl.mm index 619c97bee425..f8317488d10b 100644 --- a/Source/WebKit/UIProcess/mac/WebViewImpl.mm +++ b/Source/WebKit/UIProcess/mac/WebViewImpl.mm @@ -398,9 +398,9 @@ - (void)startObserving:(NSWindow *)window [defaultNotificationCenter addObserver:self selector:@selector(_windowDidChangeOcclusionState:) name:NSWindowDidChangeOcclusionStateNotification object:window]; [defaultNotificationCenter addObserver:self selector:@selector(_windowWillClose:) name:NSWindowWillCloseNotification object:window]; [defaultNotificationCenter addObserver:self selector:@selector(_windowWillEnterOrExitFullScreen:) name:NSWindowWillEnterFullScreenNotification object:window]; - [defaultNotificationCenter addObserver:self selector:@selector(_windowDidEnterOrExitFullScreen:) name:NSWindowDidEnterFullScreenNotification object:window]; + [defaultNotificationCenter addObserver:self selector:@selector(_windowDidEnterFullScreen:) name:NSWindowDidEnterFullScreenNotification object:window]; [defaultNotificationCenter addObserver:self selector:@selector(_windowWillEnterOrExitFullScreen:) name:NSWindowWillExitFullScreenNotification object:window]; - [defaultNotificationCenter addObserver:self selector:@selector(_windowDidEnterOrExitFullScreen:) name:NSWindowDidExitFullScreenNotification object:window]; + [defaultNotificationCenter addObserver:self selector:@selector(_windowDidExitFullScreen:) name:NSWindowDidExitFullScreenNotification object:window]; [defaultNotificationCenter addObserver:self selector:@selector(_screenDidChangeColorSpace:) name:NSScreenColorSpaceDidChangeNotification object:nil]; #if HAVE(SUPPORT_HDR_DISPLAY_APIS) @@ -621,10 +621,16 @@ - (void)_dictionaryLookupPopoverWillClose:(NSNotification *)notification } #endif -- (void)_windowDidEnterOrExitFullScreen:(NSNotification *)notification +- (void)_windowDidEnterFullScreen:(NSNotification *)notification { if (CheckedPtr impl = _impl.get()) - impl->windowDidEnterOrExitFullScreen(); + impl->windowDidEnterOrExitFullScreen(true); +} + +- (void)_windowDidExitFullScreen:(NSNotification *)notification +{ + if (CheckedPtr impl = _impl.get()) + impl->windowDidEnterOrExitFullScreen(false); } - (void)_windowWillEnterOrExitFullScreen:(NSNotification *)notification @@ -2221,10 +2227,14 @@ static NSTrackingAreaOptions NODELETE flagsChangedEventMonitorTrackingAreaOption m_windowIsEnteringOrExitingFullScreen = true; } -void WebViewImpl::windowDidEnterOrExitFullScreen() +void WebViewImpl::windowDidEnterOrExitFullScreen(bool windowIsInFullScreen) { m_windowIsEnteringOrExitingFullScreen = false; +#if ENABLE(SCROLL_POCKET_IN_FULLSCREEN) + m_page->setWindowIsInNativeFullScreen(windowIsInFullScreen ? WebPageProxy::WindowIsInNativeFullScreen::Yes : WebPageProxy::WindowIsInNativeFullScreen::No); +#endif + #if ENABLE(CONTENT_INSET_BACKGROUND_FILL) updateScrollPocket(); #endif @@ -2478,6 +2488,11 @@ static NSTrackingAreaOptions NODELETE flagsChangedEventMonitorTrackingAreaOption m_page->setIntrinsicDeviceScaleFactor(intrinsicDeviceScaleFactor()); m_page->webViewDidMoveToWindow(); + +#if ENABLE(SCROLL_POCKET_IN_FULLSCREEN) + auto isInNativeFullScreen = window && (window.get().styleMask & NSWindowStyleMaskFullScreen); + m_page->setWindowIsInNativeFullScreen(isInNativeFullScreen ? WebPageProxy::WindowIsInNativeFullScreen::Yes : WebPageProxy::WindowIsInNativeFullScreen::No); +#endif } void WebViewImpl::viewDidChangeBackingProperties() @@ -2609,7 +2624,11 @@ static NSTrackingAreaOptions NODELETE flagsChangedEventMonitorTrackingAreaOption if (m_fullScreenTitlebarOverlayHeight == fullScreenTitlebarOverlayHeight) return; + auto wasRevealed = m_fullScreenTitlebarOverlayHeight > 0; m_fullScreenTitlebarOverlayHeight = fullScreenTitlebarOverlayHeight; + auto isRevealed = m_fullScreenTitlebarOverlayHeight > 0; + if (wasRevealed != isRevealed) + m_page->setFullScreenTitlebarOverlayIsRevealed(isRevealed); updateScrollPocket(); } diff --git a/Source/WebKit/WebProcess/WebPage/Cocoa/WebPageCocoa.mm b/Source/WebKit/WebProcess/WebPage/Cocoa/WebPageCocoa.mm index 109a39d7e936..b1a5044d7a5b 100644 --- a/Source/WebKit/WebProcess/WebPage/Cocoa/WebPageCocoa.mm +++ b/Source/WebKit/WebProcess/WebPage/Cocoa/WebPageCocoa.mm @@ -1696,6 +1696,11 @@ static void drawPDFPage(PDFDocument *pdfDocument, CFIndex pageIndex, CGContextRe auto sides = m_page->fixedContainerEdges().fixedEdges(); +#if ENABLE(SCROLL_POCKET_IN_FULLSCREEN) + if (m_fullScreenTitlebarOverlayIsDisplayed) + sides.add(BoxSide::Top); +#endif + if ((additionalHeight + obscuredInsets.top()) > 0) sides.add(BoxSide::Top); diff --git a/Source/WebKit/WebProcess/WebPage/WebPage.cpp b/Source/WebKit/WebProcess/WebPage/WebPage.cpp index 14b7a11d8032..0a4e49cfb18a 100644 --- a/Source/WebKit/WebProcess/WebPage/WebPage.cpp +++ b/Source/WebKit/WebProcess/WebPage/WebPage.cpp @@ -693,6 +693,9 @@ WebPage::WebPage(PageIdentifier pageID, WebPageCreationParameters&& parameters) #endif #if PLATFORM(MAC) , m_overflowHeightForTopScrollEdgeEffect(parameters.overflowHeightForTopScrollEdgeEffect) +#if ENABLE(SCROLL_POCKET_IN_FULLSCREEN) + , m_fullScreenTitlebarOverlayIsDisplayed(parameters.fullScreenTitlebarOverlayIsDisplayed) +#endif #endif #if ENABLE(META_VIEWPORT) , m_forceAlwaysUserScalable(parameters.ignoresViewportScaleLimits) diff --git a/Source/WebKit/WebProcess/WebPage/WebPage.h b/Source/WebKit/WebProcess/WebPage/WebPage.h index 4d6f03a245e1..d019f9230bab 100644 --- a/Source/WebKit/WebProcess/WebPage/WebPage.h +++ b/Source/WebKit/WebProcess/WebPage/WebPage.h @@ -2196,6 +2196,9 @@ class WebPage final : public API::ObjectImpl, pub #if PLATFORM(MAC) void setOverflowHeightForTopScrollEdgeEffect(double value) { m_overflowHeightForTopScrollEdgeEffect = value; } +#if ENABLE(SCROLL_POCKET_IN_FULLSCREEN) + void setFullScreenTitlebarOverlayIsDisplayed(bool fullScreenTitlebarOverlayIsDisplayed) { m_fullScreenTitlebarOverlayIsDisplayed = fullScreenTitlebarOverlayIsDisplayed; } +#endif #endif RefPtr shareableBitmapSnapshotForNode(WebCore::Node&); @@ -3090,12 +3093,14 @@ class WebPage final : public API::ObjectImpl, pub #if PLATFORM(MAC) double m_overflowHeightForTopScrollEdgeEffect { 0 }; - // Root-view origin of the in-flight selection-extend drag: captured on the first extent update and // cleared by `cancelAutoscroll` (which the UI process calls at gesture begin/end). Lets the edge check // require a minimum drag toward an edge before selection autoscroll engages, so a selection that merely // originates near an edge doesn't scroll. Persists across hot-zone enter/exit within a single drag. std::optional m_selectionAutoscrollDragOrigin; +#if ENABLE(SCROLL_POCKET_IN_FULLSCREEN) + bool m_fullScreenTitlebarOverlayIsDisplayed { false }; +#endif #endif bool m_needsScrollGeometryUpdates { false }; diff --git a/Source/WebKit/WebProcess/WebPage/WebPage.messages.in b/Source/WebKit/WebProcess/WebPage/WebPage.messages.in index 25e1d4943c97..2b3711f32f3c 100644 --- a/Source/WebKit/WebProcess/WebPage/WebPage.messages.in +++ b/Source/WebKit/WebProcess/WebPage/WebPage.messages.in @@ -677,6 +677,9 @@ messages -> WebPage WantsAsyncDispatchMessage { DidEndMagnificationGesture() SetOverflowHeightForTopScrollEdgeEffect(double value) +#if ENABLE(SCROLL_POCKET_IN_FULLSCREEN) + SetFullScreenTitlebarOverlayIsDisplayed(bool fullScreenTitlebarOverlayIsDisplayed) +#endif #endif StartDeferringResizeEvents(); diff --git a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/ObscuredContentInsets.mm b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/ObscuredContentInsets.mm index 2602e2e662b6..40bf842cfe16 100644 --- a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/ObscuredContentInsets.mm +++ b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/ObscuredContentInsets.mm @@ -1032,6 +1032,161 @@ static void runSubcases(TestWKWebView *webView, bool leftFixedEdge, bool rightFi [NSNotificationCenter.defaultCenter removeObserver:exitObserver.get()]; } +static RetainPtr createFullScreenCapableWindow() +{ + auto styleMask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable | NSWindowStyleMaskFullSizeContentView; + RetainPtr toolbar = adoptNS([[NSToolbar alloc] initWithIdentifier:@"ScrollPocketTestToolbar"]); + RetainPtr window = adoptNS([[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600) styleMask:styleMask backing:NSBackingStoreBuffered defer:NO]); + + [window setCollectionBehavior:[window collectionBehavior] | NSWindowCollectionBehaviorFullScreenPrimary]; + [window setToolbar:toolbar.get()]; + + // Attach a titlebar accessory so AppKit does not autohide the fullscreen toolbar mid-test. + RetainPtr accessoryViewController = adoptNS([[NSTitlebarAccessoryViewController alloc] init]); + [accessoryViewController setView:adoptNS([[NSView alloc] initWithFrame:NSMakeRect(0, 0, 1, 1)]).get()]; + [accessoryViewController setLayoutAttribute:NSLayoutAttributeRight]; + [window addTitlebarAccessoryViewController:accessoryViewController.get()]; + + return window; +} + +TEST(ObscuredContentInsets, ScrollPocketCoversFullScreenTitlebarAfterReload) +{ + constexpr CGFloat topInset = 20; + + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + [NSApp activateIgnoringOtherApps:YES]; + + RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600)]); + [webView _setAutomaticallyAdjustsContentInsets:NO]; + [webView setObscuredContentInsets:NSEdgeInsetsMake(topInset, 0, 0, 0)]; + [webView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + + RetainPtr window = createFullScreenCapableWindow(); + [[window contentView] addSubview:webView.get()]; + [webView setFrame:[[window contentView] bounds]]; + [window makeKeyAndOrderFront:nil]; + + [webView synchronouslyLoadTestPageNamed:@"top-fixed-element"]; + [webView waitForNextPresentationUpdate]; + + EXPECT_NOT_NULL([webView _topScrollPocket]); + + __block bool didEnter = false; + __block bool didExit = false; + RetainPtr enterObserver = [NSNotificationCenter.defaultCenter addObserverForName:NSWindowDidEnterFullScreenNotification object:window.get() queue:nil usingBlock:^(NSNotification *) { + didEnter = true; + }]; + RetainPtr exitObserver = [NSNotificationCenter.defaultCenter addObserverForName:NSWindowDidExitFullScreenNotification object:window.get() queue:nil usingBlock:^(NSNotification *) { + didExit = true; + }]; + + [window toggleFullScreen:nil]; + EXPECT_TRUE(Util::runFor(&didEnter, 10_s)); + [webView waitForNextPresentationUpdate]; + + auto overlayHeight = [webView _fullScreenTitlebarOverlayHeightForTesting]; + EXPECT_GT(overlayHeight, topInset); + + auto screenTopSafeArea = [[[webView window] screen] safeAreaInsets].top; + EXPECT_NEAR(overlayHeight - screenTopSafeArea, NSHeight([[webView _topScrollPocket] frame]), 1); + + RetainPtr expectedColor = [webView _sampledTopFixedPositionContentColor]; + EXPECT_NOT_NULL(expectedColor.get()); + EXPECT_TRUE(Util::compareColors([[webView _topScrollPocket] captureColor], expectedColor.get())); + + __block bool reloadCommitted = false; + RetainPtr navigationDelegate = adoptNS([[TestNavigationDelegate alloc] init]); + [navigationDelegate setDidCommitNavigation:^(WKWebView *view, WKNavigation *) { + [view _doAfterNextPresentationUpdate:^{ + reloadCommitted = true; + }]; + }]; + [webView setNavigationDelegate:navigationDelegate.get()]; + + [webView reload]; + Util::run(&reloadCommitted); + [webView waitForNextPresentationUpdate]; + + EXPECT_NOT_NULL([webView _topScrollPocket]); + EXPECT_NEAR(overlayHeight - screenTopSafeArea, NSHeight([[webView _topScrollPocket] frame]), 1); + + RetainPtr expectedColorAfterReload = [webView _sampledTopFixedPositionContentColor]; + EXPECT_NOT_NULL(expectedColorAfterReload.get()); + EXPECT_TRUE(Util::compareColors([[webView _topScrollPocket] captureColor], expectedColorAfterReload.get())); + + [window toggleFullScreen:nil]; + EXPECT_TRUE(Util::runFor(&didExit, 10_s)); + [webView waitForNextPresentationUpdate]; + + [NSNotificationCenter.defaultCenter removeObserver:enterObserver.get()]; + [NSNotificationCenter.defaultCenter removeObserver:exitObserver.get()]; +} + +TEST(ObscuredContentInsets, ScrollPocketCoversFullScreenTitlebarAfterMovingIntoFullScreenWindow) +{ + constexpr CGFloat topInset = 20; + + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + [NSApp activateIgnoringOtherApps:YES]; + + RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600)]); + [webView _setAutomaticallyAdjustsContentInsets:NO]; + [webView setObscuredContentInsets:NSEdgeInsetsMake(topInset, 0, 0, 0)]; + [webView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + + RetainPtr originalWindow = createFullScreenCapableWindow(); + [[originalWindow contentView] addSubview:webView.get()]; + [webView setFrame:[[originalWindow contentView] bounds]]; + [originalWindow makeKeyAndOrderFront:nil]; + + [webView synchronouslyLoadTestPageNamed:@"top-fixed-element"]; + [webView waitForNextPresentationUpdate]; + + EXPECT_NOT_NULL([webView _topScrollPocket]); + + // Set up a second window and put it into fullscreen *before* the WKWebView is moved into it. + RetainPtr fullScreenWindow = createFullScreenCapableWindow(); + [fullScreenWindow makeKeyAndOrderFront:nil]; + + __block bool didEnter = false; + __block bool didExit = false; + RetainPtr enterObserver = [NSNotificationCenter.defaultCenter addObserverForName:NSWindowDidEnterFullScreenNotification object:fullScreenWindow.get() queue:nil usingBlock:^(NSNotification *) { + didEnter = true; + }]; + RetainPtr exitObserver = [NSNotificationCenter.defaultCenter addObserverForName:NSWindowDidExitFullScreenNotification object:fullScreenWindow.get() queue:nil usingBlock:^(NSNotification *) { + didExit = true; + }]; + + [fullScreenWindow toggleFullScreen:nil]; + EXPECT_TRUE(Util::runFor(&didEnter, 10_s)); + + EXPECT_TRUE([fullScreenWindow styleMask] & NSWindowStyleMaskFullScreen); + + // Move the WKWebView into the already-fullscreen window. + [webView removeFromSuperview]; + [[fullScreenWindow contentView] addSubview:webView.get()]; + [webView setFrame:[[fullScreenWindow contentView] bounds]]; + [webView waitForNextPresentationUpdate]; + + auto overlayHeight = [webView _fullScreenTitlebarOverlayHeightForTesting]; + EXPECT_GT(overlayHeight, topInset); + + auto screenTopSafeArea = [[[webView window] screen] safeAreaInsets].top; + EXPECT_NEAR(overlayHeight - screenTopSafeArea, NSHeight([[webView _topScrollPocket] frame]), 1); + + RetainPtr expectedColor = [webView _sampledTopFixedPositionContentColor]; + EXPECT_NOT_NULL(expectedColor.get()); + EXPECT_TRUE(Util::compareColors([[webView _topScrollPocket] captureColor], expectedColor.get())); + + [fullScreenWindow toggleFullScreen:nil]; + EXPECT_TRUE(Util::runFor(&didExit, 10_s)); + [webView waitForNextPresentationUpdate]; + + [NSNotificationCenter.defaultCenter removeObserver:enterObserver.get()]; + [NSNotificationCenter.defaultCenter removeObserver:exitObserver.get()]; +} + #endif // ENABLE(SCROLL_POCKET_IN_FULLSCREEN) #endif // ENABLE(CONTENT_INSET_BACKGROUND_FILL) From a828526141568b22502568378e54a53f77fe85ef Mon Sep 17 00:00:00 2001 From: Jean-Yves Avenard Date: Tue, 30 Jun 2026 07:27:25 -0700 Subject: [PATCH 32/84] [MSE] Loosen gap tolerance during playback and seek https://bugs.webkit.org/show_bug.cgi?id=317681 rdar://180439090 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed by Jer Noble. MediaSourcePrivate now owns a single gap policy consulted at every buffered-range boundary. Three tolerances replace the prior fixed timeFudgeFactor: - Start-of-stream allowance: 1s. Per MSE 2 §presentation-start-time: "implementations MAY choose to allow a current playback position at or after presentation start time and before the first TimeRanges to play the first TimeRanges if that TimeRanges starts within a reasonably short time, like 1 second, after presentation start time". Applied only when time is before ranges.start(0). HTMLMediaElement. buffered reported to JS is unchanged. - Mid-stream gap tolerance: 125 ms. Default tolerance for gaps between buffered sub-ranges. - Audio-covered gap tolerance: 250 ms. Widened tolerance when an active audio track covers the gap position (typical video-only gap with audio continuous through it). setTimeFudgeFactor IPC and MediaSource::currentTimeFudgeFactor removed. * LayoutTests/media/media-source/media-source-fudge-factor-expected.txt: Removed. * LayoutTests/media/media-source/media-source-fudge-factor.html: Removed. * LayoutTests/media/media-source/media-source-gap-policy-expected.txt: Added. * LayoutTests/media/media-source/media-source-gap-policy.html: Added. * LayoutTests/media/media-source/media-source-video-renders.html: * Source/WebCore/Modules/mediasource/MediaSource.cpp: (WebCore::MediaSource::setPrivateAndOpen): (WebCore::MediaSource::currentTimeFudgeFactor): Deleted. * Source/WebCore/Modules/mediasource/MediaSource.h: * Source/WebCore/platform/graphics/MediaSourcePrivate.cpp: (WebCore::MediaSourcePrivate::hasFutureTime const): (WebCore::MediaSourcePrivate::hasBufferedTime const): (WebCore::MediaSourcePrivate::hasCurrentTime const): (WebCore::MediaSourcePrivate::isBuffered const): (WebCore::MediaSourcePrivate::cancelPendingWaitForTarget): (WebCore::MediaSourcePrivate::updateBufferedRanges): (WebCore::MediaSourcePrivate::nextStallTime const): (WebCore::MediaSourcePrivate::gapToleranceAtTime const): returns the applicable tolerance for time, consulted by hasFutureTime, hasBufferedTime, isBuffered, nextStallTime, canCompleteWaitForTarget, and TrackBuffer's enqueue gap check. (WebCore::MediaSourcePrivate::isWithinStartGapAllowance const): * Source/WebCore/platform/graphics/MediaSourcePrivate.h: * Source/WebCore/platform/graphics/PlatformTimeRanges.cpp: (WebCore::PlatformTimeRanges::containWithEpsilon const): overload so that isBuffered can apply a per-position tolerance at each gap. * Source/WebCore/platform/graphics/PlatformTimeRanges.h: * Source/WebCore/platform/graphics/SourceBufferPrivate.cpp: (WebCore::SourceBufferPrivate::reenqueueMediaForTime): (WebCore::SourceBufferPrivate::addTrackBuffer): (WebCore::SourceBufferPrivate::isAudioBufferedAt const): (WebCore::SourceBufferPrivate::audioBufferedRanges const): * Source/WebCore/platform/graphics/SourceBufferPrivate.h: (WebCore::SourceBufferPrivate::timeFudgeFactor const): Deleted. * Source/WebCore/platform/graphics/TrackBuffer.cpp: (WebCore::TrackBuffer::create): (WebCore::TrackBuffer::TrackBuffer): (WebCore::TrackBuffer::isAcceptableEnqueueGap const): (WebCore::TrackBuffer::nextSample): (WebCore::TrackBuffer::reenqueueMediaForTime): * Source/WebCore/platform/graphics/TrackBuffer.h: * Source/WebCore/platform/graphics/avfoundation/objc/MediaPlayerPrivateMediaSourceAVFObjC.mm: (WebCore::MediaPlayerPrivateMediaSourceAVFObjC::resetStallForTime): Move nextStallTime(time) logic to MediaSourcePrivate. * Source/WebCore/platform/graphics/avfoundation/objc/SourceBufferPrivateAVFObjC.h: * Source/WebCore/platform/graphics/avfoundation/objc/SourceBufferPrivateAVFObjC.mm: (WebCore::SourceBufferPrivateAVFObjC::timeFudgeFactor const): Deleted. * Source/WebCore/platform/graphics/cocoa/MediaPlayerPrivateWebM.mm: (WebCore::MediaPlayerPrivateWebM::reenqueueMediaForTime): (WebCore::MediaPlayerPrivateWebM::addTrackBuffer): (WebCore::MediaPlayerPrivateWebM::monitorReadyState): * Source/WebKit/GPUProcess/media/RemoteMediaSourceProxy.cpp: (WebKit::RemoteMediaSourceProxy::setTimeFudgeFactor): Deleted. * Source/WebKit/GPUProcess/media/RemoteMediaSourceProxy.h: * Source/WebKit/GPUProcess/media/RemoteMediaSourceProxy.messages.in: * Source/WebKit/WebProcess/GPU/media/MediaPlayerPrivateRemote.cpp: (WebKit::MediaPlayerPrivateRemote::currentTimeWithLockHeld const): * Source/WebKit/WebProcess/GPU/media/MediaSourcePrivateRemote.cpp: (WebKit::MediaSourcePrivateRemote::setTimeFudgeFactor): Deleted. * Source/WebKit/WebProcess/GPU/media/MediaSourcePrivateRemote.h: Canonical link: https://commits.webkit.org/316146@main --- .../media-source-fudge-factor-expected.txt | 19 --- .../media-source-fudge-factor.html | 58 --------- .../media-source-gap-policy-expected.txt | 30 +++++ .../media-source/media-source-gap-policy.html | 88 ++++++++++++++ .../Modules/mediasource/MediaSource.cpp | 8 -- .../WebCore/Modules/mediasource/MediaSource.h | 1 - .../platform/graphics/MediaSourcePrivate.cpp | 110 +++++++++++++++++- .../platform/graphics/MediaSourcePrivate.h | 32 ++++- .../platform/graphics/PlatformTimeRanges.cpp | 18 ++- .../platform/graphics/PlatformTimeRanges.h | 8 ++ .../platform/graphics/SourceBufferPrivate.cpp | 38 +++++- .../platform/graphics/SourceBufferPrivate.h | 13 ++- .../WebCore/platform/graphics/TrackBuffer.cpp | 34 +++--- .../WebCore/platform/graphics/TrackBuffer.h | 30 ++++- .../MediaPlayerPrivateMediaSourceAVFObjC.mm | 15 +-- .../objc/SourceBufferPrivateAVFObjC.h | 1 - .../objc/SourceBufferPrivateAVFObjC.mm | 8 -- .../graphics/cocoa/MediaPlayerPrivateWebM.mm | 9 +- 18 files changed, 374 insertions(+), 146 deletions(-) delete mode 100644 LayoutTests/media/media-source/media-source-fudge-factor-expected.txt delete mode 100644 LayoutTests/media/media-source/media-source-fudge-factor.html create mode 100644 LayoutTests/media/media-source/media-source-gap-policy-expected.txt create mode 100644 LayoutTests/media/media-source/media-source-gap-policy.html diff --git a/LayoutTests/media/media-source/media-source-fudge-factor-expected.txt b/LayoutTests/media/media-source/media-source-fudge-factor-expected.txt deleted file mode 100644 index cb7f1408b773..000000000000 --- a/LayoutTests/media/media-source/media-source-fudge-factor-expected.txt +++ /dev/null @@ -1,19 +0,0 @@ - -RUN(video.src = URL.createObjectURL(source)) -EVENT(loadedmetadata) -EVENT(update) -Samples with presentation times after currentTime should not cause loadedData. -EVENT(update) -EXPECTED (video.readyState == '1') OK -Samples with presentation times very close to currentTime should cause loadedData. -EVENT(loadeddata) -EVENT(update) -EXPECTED (video.readyState == '2') OK -Samples with presentation end times very close to currentTime should not cause canPlay. -EVENT(update) -EXPECTED (video.readyState == '2') OK -Continuous samples with presentation end times after currentTime should cause canPlay. -EVENT(canplay) -EXPECTED (video.readyState >= '3') OK -END OF TEST - diff --git a/LayoutTests/media/media-source/media-source-fudge-factor.html b/LayoutTests/media/media-source/media-source-fudge-factor.html deleted file mode 100644 index e0c0a6697da4..000000000000 --- a/LayoutTests/media/media-source/media-source-fudge-factor.html +++ /dev/null @@ -1,58 +0,0 @@ - - - - media-source-fudge-factor - - - - - - - - diff --git a/LayoutTests/media/media-source/media-source-gap-policy-expected.txt b/LayoutTests/media/media-source/media-source-gap-policy-expected.txt new file mode 100644 index 000000000000..631529d4e6fb --- /dev/null +++ b/LayoutTests/media/media-source/media-source-gap-policy-expected.txt @@ -0,0 +1,30 @@ + +RUN(video.src = URL.createObjectURL(source)) +EVENT(update) +EVENT(loadedmetadata) +EVENT(update) +EVENT(update) +=== Start-of-stream allowance (1 s) === +Video [1500, 4000]: 1.5 s start gap > 1 s, readyState stays HAVE_METADATA. +EVENT(update) +EXPECTED (video.readyState == '1') OK +Video [500, 1000]: 500 ms start gap <= 1 s, canplay fires. +EVENT(canplay) +EVENT(update) +EXPECTED (video.readyState >= '3') OK +=== Mid-stream gap tolerance (125 ms) === +Remove audio [1000, 1500) so it no longer bridges the 500 ms video gap. +EVENT(update) +500 ms gap > 125 ms tolerance: readyState below HAVE_ENOUGH_DATA. +EXPECTED (video.readyState < '4') OK +=== Audio-covered gap tolerance (250 ms) === +Re-append audio [1000, 1500): audio bridges the video gap. +EVENT(update) +500 ms gap > 250 ms audio-covered tolerance: readyState still below HAVE_ENOUGH_DATA. +EXPECTED (video.readyState < '4') OK +Append video [1200, 1500] to shrink the gap to 200 ms: canplaythrough fires. +EVENT(canplaythrough) +EVENT(update) +EXPECTED (video.readyState == '4') OK +END OF TEST + diff --git a/LayoutTests/media/media-source/media-source-gap-policy.html b/LayoutTests/media/media-source/media-source-gap-policy.html new file mode 100644 index 000000000000..a1a7a4042153 --- /dev/null +++ b/LayoutTests/media/media-source/media-source-gap-policy.html @@ -0,0 +1,88 @@ + + + + media-source-gap-policy + + + + + + + + diff --git a/Source/WebCore/Modules/mediasource/MediaSource.cpp b/Source/WebCore/Modules/mediasource/MediaSource.cpp index 10291bce97b3..82d59f918bc5 100644 --- a/Source/WebCore/Modules/mediasource/MediaSource.cpp +++ b/Source/WebCore/Modules/mediasource/MediaSource.cpp @@ -264,7 +264,6 @@ void MediaSource::setPrivateAndOpen(Ref&& mediaSourcePrivate ASSERT(!m_private); setPrivate(WTF::move(mediaSourcePrivate)); - protect(m_private)->setTimeFudgeFactor(currentTimeFudgeFactor()); open(); } @@ -405,13 +404,6 @@ ExceptionOr MediaSource::clearLiveSeekableRange() return { }; } -const MediaTime& MediaSource::currentTimeFudgeFactor() -{ - // Allow hasCurrentTime() to be off by as much as the length of two 24fps video frames - static NeverDestroyed fudgeFactor(2002, 24000); - return fudgeFactor; -} - bool MediaSource::contentTypeShouldGenerateTimestamps(const ContentType& contentType) { return contentType.containerType() == "audio/aac"_s || contentType.containerType() == "audio/mpeg"_s; diff --git a/Source/WebCore/Modules/mediasource/MediaSource.h b/Source/WebCore/Modules/mediasource/MediaSource.h index 238194945d61..90a305ba6c4e 100644 --- a/Source/WebCore/Modules/mediasource/MediaSource.h +++ b/Source/WebCore/Modules/mediasource/MediaSource.h @@ -136,7 +136,6 @@ class MediaSource ScriptExecutionContext* NODELETE scriptExecutionContext() const final; - static const MediaTime& NODELETE currentTimeFudgeFactor(); static bool contentTypeShouldGenerateTimestamps(const ContentType&); #if !RELEASE_LOG_DISABLED diff --git a/Source/WebCore/platform/graphics/MediaSourcePrivate.cpp b/Source/WebCore/platform/graphics/MediaSourcePrivate.cpp index 44e79f12968e..e13cf3229600 100644 --- a/Source/WebCore/platform/graphics/MediaSourcePrivate.cpp +++ b/Source/WebCore/platform/graphics/MediaSourcePrivate.cpp @@ -58,7 +58,7 @@ bool MediaSourcePrivate::hasFutureTime(const MediaTime& currentTime, const Media auto ranges = buffered(); MediaTime nearest = ranges.nearest(currentTime); - if (abs(nearest - currentTime) > timeFudgeFactor()) + if (abs(nearest - currentTime) > gapToleranceAtTime(currentTime)) return false; size_t found = ranges.find(nearest); @@ -94,12 +94,20 @@ bool MediaSourcePrivate::hasBufferedTime(const MediaTime& time) const if (!ranges.length()) return false; - return abs(ranges.nearest(time) - time) <= timeFudgeFactor(); + return abs(ranges.nearest(time) - time) <= gapToleranceAtTime(time); } bool MediaSourcePrivate::hasCurrentTime() const { - return hasBufferedTime(currentTime()); + auto time = currentTime(); + if (hasBufferedTime(time)) + return true; + // Per MSE 2 §presentation-start-time: the user agent may treat a current + // playback position at or after time 0 and before the first buffered + // range as covered, provided the first range starts within a small + // window after presentation start. Affects readyState only; + // HTMLMediaElement.buffered reported to JS is unchanged. + return m_readyState.load() != MediaSourceReadyState::Closed && isWithinStartGapAllowance(time); } bool MediaSourcePrivate::isBuffered(const PlatformTimeRanges& ranges) const @@ -107,7 +115,9 @@ bool MediaSourcePrivate::isBuffered(const PlatformTimeRanges& ranges) const if (m_readyState.load() == MediaSourceReadyState::Closed) return false; - return buffered().containWithEpsilon(ranges, timeFudgeFactor()); + return buffered().containWithEpsilon(ranges, [&](const MediaTime& time) { + return gapToleranceAtTime(time); + }); } MediaSourcePrivate::MediaSourcePrivate(MediaSourcePrivateClient& client) @@ -173,7 +183,6 @@ void MediaSourcePrivate::cancelPendingWaitForTarget() protectedThis->m_pendingSeekTarget.reset(); } protectedThis->m_waitForTargetPromise.reset(); - protectedThis->m_reenqueuePending = false; }); } @@ -376,6 +385,20 @@ void MediaSourcePrivate::updateBufferedRanges() }); auto newBuffered = MediaSourcePrivate::computeBufferedRanges(activeRanges, ended); + + // Aggregate audio-only buffered ranges across active SourceBuffers so + // the canplaythrough gap policy can ask "does any audio bridge this + // gap?" — see MediaSourcePrivate::gapToleranceAtTime. + PlatformTimeRanges newAudioBuffered; + for (RefPtr sourceBuffer : m_activeSourceBuffers) { + if (sourceBuffer) + newAudioBuffered.unionWith(sourceBuffer->audioBufferedRanges()); + } + { + Locker locker { m_lock }; + m_audioBuffered = WTF::move(newAudioBuffered); + } + if (isBufferedEqual(newBuffered)) return; bufferedChanged(WTF::move(newBuffered)); @@ -601,6 +624,83 @@ void MediaSourcePrivate::shutdown() { } +MediaTime MediaSourcePrivate::nextStallTime(const MediaTime& currentTime) const +{ + auto stallAtTime = duration(); + auto ranges = buffered(); + size_t index = ranges.find(currentTime); + if (index == notFound) + return stallAtTime; + + // Find the next gap (or end of media). + auto remaining = ranges.span().subspan(index); + for (size_t i = 0; i < remaining.size(); i++) { + auto rangeEnd = remaining[i].end; + if (i + 1 < remaining.size()) { + if (remaining[i + 1].start - rangeEnd > gapToleranceAtTime(rangeEnd)) { + stallAtTime = rangeEnd; + break; + } + continue; + } + // Final range. + if (rangeEnd > currentTime) + stallAtTime = rangeEnd; + break; + } + return stallAtTime; +} + +MediaTime MediaSourcePrivate::gapToleranceAtTime(const MediaTime& time, std::optional excluded) const +{ + // Start-of-stream allowance: gap is before the first buffered range and + // close to presentation start time. + if (isWithinStartGapAllowance(time)) + return startGapAllowance(); + + // Mid-stream: widen to audioCoveredGapTolerance() when an audio track + // bridges the gap, otherwise midStreamGapTolerance(). + // + // When called with an `excluded` track the caller is on the dispatcher + // (TrackBuffer's IsCoveredByOtherTracks lambda runs from + // SourceBufferPrivate methods that assert dispatcher); walk + // m_activeSourceBuffers directly. + if (excluded) { + assertIsCurrent(m_dispatcher.get()); + for (RefPtr sourceBuffer : m_activeSourceBuffers) { + if (sourceBuffer && sourceBuffer->isAudioBufferedAt(time, *excluded)) + return audioCoveredGapTolerance(); + } + return midStreamGapTolerance(); + } + + // Other callers read the cached audio union. + Locker locker { m_lock }; + return m_audioBuffered.contain(time) ? audioCoveredGapTolerance() : midStreamGapTolerance(); +} + + + +bool MediaSourcePrivate::isWithinStartGapAllowance(const MediaTime& time) const +{ + // MSE 2 §presentation-start-time: + // "For the purposes of determining if HTMLMediaElement's buffered contains + // a TimeRanges that includes the current playback position, implementations + // MAY choose to allow a current playback position at or after presentation + // start time and before the first TimeRanges to play the first TimeRanges + // if that TimeRanges starts within a reasonably short time, like 1 second, + // after presentation start time. This allowance accommodates the reality + // that muxed streams commonly do not begin all tracks precisely at + // presentation start time. Implementations MUST report the actual buffered + // range, regardless of this allowance." + if (time < MediaTime::zeroTime() || time >= startGapAllowance()) + return false; + auto ranges = buffered(); + if (!ranges.length()) + return false; + return ranges.start(0) <= startGapAllowance() && time < ranges.start(0); +} + } // namespace WebCore #endif diff --git a/Source/WebCore/platform/graphics/MediaSourcePrivate.h b/Source/WebCore/platform/graphics/MediaSourcePrivate.h index 5211a885cc9f..46ea2b792c79 100644 --- a/Source/WebCore/platform/graphics/MediaSourcePrivate.h +++ b/Source/WebCore/platform/graphics/MediaSourcePrivate.h @@ -127,8 +127,33 @@ class WEBCORE_EXPORT MediaSourcePrivate void clearReenqueuePending() { m_reenqueuePending = false; } void cancelPendingWaitForTarget(); - void setTimeFudgeFactor(const MediaTime& fudgeFactor) { m_timeFudgeFactor = fudgeFactor; } - MediaTime timeFudgeFactor() const { return m_timeFudgeFactor; } + // Single source of truth for canplaythrough / gap-handling policy across + // the MSE pipeline. All values are durations. + // - startGapAllowance: per MSE 2 §presentation-start-time, allow up to + // 1s gap from time 0 to the first buffered range to count as + // "buffered" for HAVE_FUTURE_DATA / canplay; HTMLMediaElement.buffered + // reported to JS is unaffected. + // - midStreamGapTolerance: default tolerance for gaps mid-stream; + // gaps below this are skipped. + // - audioCoveredGapTolerance: widened tolerance applied when an audio + // track bridges a gap (typically a video-only gap with audio + // continuous through it). + static constexpr MediaTime startGapAllowance() { return { 1, 1 }; } + static constexpr MediaTime midStreamGapTolerance() { return { 125, 1000 }; } + static constexpr MediaTime audioCoveredGapTolerance() { return { 250, 1000 }; } + + // Returns the gap tolerance that should apply at `time`: + // - startGapAllowance if `time` is in the start-of-stream window; + // - audioCoveredGapTolerance if some active audio track other than + // `excluded` has `time` inside its buffered range; + // - midStreamGapTolerance otherwise. + // + // When `excluded` is unset the implementation reads a lock-protected + // audio-buffered cache and may be called from any thread. When + // `excluded` is set the caller MUST be on the dispatcher (used by + // TrackBuffer's IsCoveredByOtherTracks callback). + MediaTime gapToleranceAtTime(const MediaTime&, std::optional excluded = std::nullopt) const; + bool isWithinStartGapAllowance(const MediaTime&) const; MediaTime duration() const; PlatformTimeRanges buffered() const; @@ -139,6 +164,7 @@ class WEBCORE_EXPORT MediaSourcePrivate bool isBuffered(const PlatformTimeRanges&) const; PlatformTimeRanges seekable() const; + MediaTime nextStallTime(const MediaTime& currentTime) const; bool hasBufferedData() const; bool hasCurrentTime() const; bool hasFutureTime() const; @@ -181,13 +207,13 @@ class WEBCORE_EXPORT MediaSourcePrivate MediaTime m_duration WTF_GUARDED_BY_LOCK(m_lock) { MediaTime::invalidTime() }; PlatformTimeRanges m_buffered WTF_GUARDED_BY_LOCK(m_lock); + PlatformTimeRanges m_audioBuffered WTF_GUARDED_BY_LOCK(m_lock); std::optional m_pendingSeekTarget WTF_GUARDED_BY_LOCK(m_lock); std::optional m_waitForTargetPromise WTF_GUARDED_BY_CAPABILITY(m_dispatcher.get()); HashMap> m_bufferedRanges; PlatformTimeRanges m_liveSeekable WTF_GUARDED_BY_LOCK(m_lock); std::atomic m_streaming { false }; std::atomic m_streamingAllowed { false }; - MediaTime m_timeFudgeFactor; HashMap m_tracksTypes WTF_GUARDED_BY_CAPABILITY(m_dispatcher.get()); std::atomic m_tracksCombinedTypes; const ThreadSafeWeakPtr m_client; diff --git a/Source/WebCore/platform/graphics/PlatformTimeRanges.cpp b/Source/WebCore/platform/graphics/PlatformTimeRanges.cpp index 6ae78ecceab4..611c72131b26 100644 --- a/Source/WebCore/platform/graphics/PlatformTimeRanges.cpp +++ b/Source/WebCore/platform/graphics/PlatformTimeRanges.cpp @@ -270,7 +270,19 @@ bool PlatformTimeRanges::containWithEpsilon(const MediaTime& time, const MediaTi return findWithEpsilon(time, epsilon) != notFound; } +bool PlatformTimeRanges::containWithEpsilon(const MediaTime& time, NOESCAPE const Function& epsilonAtTime) const +{ + return findWithEpsilon(time, epsilonAtTime(time)) != notFound; +} + bool PlatformTimeRanges::containWithEpsilon(const PlatformTimeRanges& ranges, const MediaTime& epsilon) const +{ + return containWithEpsilon(ranges, [&](const MediaTime&) { + return epsilon; + }); +} + +bool PlatformTimeRanges::containWithEpsilon(const PlatformTimeRanges& ranges, NOESCAPE const Function& epsilonAtTime) const { if (ranges.length() < 1) return true; @@ -285,7 +297,7 @@ bool PlatformTimeRanges::containWithEpsilon(const PlatformTimeRanges& ranges, co return false; auto hasBufferedTime = [&] (const MediaTime& time) { - return abs(bufferedRanges.nearest(time) - time) <= epsilon; + return abs(bufferedRanges.nearest(time) - time) <= epsilonAtTime(time); }; if (!hasBufferedTime(ranges.minimumBufferedTime()) || !hasBufferedTime(ranges.maximumBufferedTime())) @@ -294,9 +306,9 @@ bool PlatformTimeRanges::containWithEpsilon(const PlatformTimeRanges& ranges, co if (bufferedRanges.length() == 1) return true; - // Ensure that if we have a gap in the buffered range, it is smaller than the fudge factor; + // Ensure that if we have a gap in the buffered range, it is smaller than the epsilon tolerance at the gap's start; for (unsigned i = 1; i < bufferedRanges.length(); i++) { - if (bufferedRanges.start(i) - bufferedRanges.end(i - 1) > epsilon) + if (bufferedRanges.start(i) - bufferedRanges.end(i - 1) > epsilonAtTime(bufferedRanges.end(i - 1))) return false; } diff --git a/Source/WebCore/platform/graphics/PlatformTimeRanges.h b/Source/WebCore/platform/graphics/PlatformTimeRanges.h index a1fbb4d0acb7..2bb8c4528f9b 100644 --- a/Source/WebCore/platform/graphics/PlatformTimeRanges.h +++ b/Source/WebCore/platform/graphics/PlatformTimeRanges.h @@ -78,6 +78,12 @@ class WEBCORE_EXPORT PlatformTimeRanges final { bool contain(const MediaTime&) const; bool containWithEpsilon(const MediaTime&, const MediaTime& epsilon) const; bool containWithEpsilon(const PlatformTimeRanges&, const MediaTime& epsilon) const; + // Variable-epsilon overloads: `epsilonAtTime(t)` is consulted at every + // boundary / gap location (rangeMin, rangeMax, and between adjacent + // sub-ranges). Used by the MSE gap-skipping policy where the + // tolerance varies by stream position. + bool containWithEpsilon(const MediaTime&, NOESCAPE const Function& epsilonAtTime) const; + bool containWithEpsilon(const PlatformTimeRanges&, NOESCAPE const Function& epsilonAtTime) const; size_t find(const MediaTime&) const; size_t findWithEpsilon(const MediaTime&, const MediaTime& epsilon) const; @@ -132,6 +138,8 @@ class WEBCORE_EXPORT PlatformTimeRanges final { friend bool operator==(const Range&, const Range&) = default; }; + std::span span() const LIFETIME_BOUND { return m_ranges.span(); } + friend bool operator==(const PlatformTimeRanges&, const PlatformTimeRanges&) = default; private: diff --git a/Source/WebCore/platform/graphics/SourceBufferPrivate.cpp b/Source/WebCore/platform/graphics/SourceBufferPrivate.cpp index cc5438a6b44d..9ca485e9bf19 100644 --- a/Source/WebCore/platform/graphics/SourceBufferPrivate.cpp +++ b/Source/WebCore/platform/graphics/SourceBufferPrivate.cpp @@ -488,7 +488,7 @@ void SourceBufferPrivate::reenqueueMediaForTime(TrackBuffer& trackBuffer, TrackI bool isEnded = false; if (RefPtr mediaSource = m_mediaSource.get()) isEnded = mediaSource->isEnded(); - if (trackBuffer.reenqueueMediaForTime(time, timeFudgeFactor(), isEnded)) + if (trackBuffer.reenqueueMediaForTime(time, isEnded)) provideMediaData(trackBuffer, trackID); } @@ -821,7 +821,13 @@ void SourceBufferPrivate::addTrackBuffer(TrackID trackId, RefPtrtimeFudgeFactor() : PlatformTimeRanges::timeFudgeFactor()); + UniqueRef trackBuffer = mediaSource + ? TrackBuffer::create(WTF::move(description), + [weakMediaSource = ThreadSafeWeakPtr { *mediaSource }, trackId](const MediaTime& fromTime, const MediaTime& toTime) -> bool { + RefPtr ms = weakMediaSource.get(); + return ms && (toTime - fromTime) <= ms->gapToleranceAtTime(fromTime, trackId); + }) + : TrackBuffer::create(WTF::move(description)); #if !RELEASE_LOG_DISABLED // False positive see webkit.org/b/302520 SUPPRESS_UNCOUNTED_ARG trackBuffer->setLogger(protect(buffer.logger()), buffer.logIdentifier()); @@ -1767,6 +1773,34 @@ void SourceBufferPrivate::iterateTrackBuffers(NOESCAPE const Function excluded) const +{ + assertIsCurrent(m_dispatcher.get()); + for (auto& [trackID, trackBuffer] : m_trackBufferMap) { + if (excluded && *excluded == trackID) + continue; + const auto& description = trackBuffer->description(); + if (!description || !description->isAudio()) + continue; + if (trackBuffer->buffered().contain(time)) + return true; + } + return false; +} + +PlatformTimeRanges SourceBufferPrivate::audioBufferedRanges() const +{ + assertIsCurrent(m_dispatcher.get()); + PlatformTimeRanges ranges; + for (auto& [_, trackBuffer] : m_trackBufferMap) { + const auto& description = trackBuffer->description(); + if (!description || !description->isAudio()) + continue; + ranges.unionWith(trackBuffer->buffered()); + } + return ranges; +} + // Issue flushTrack IPCs now (still on m_dispatcher) for any track whose // renderer holds samples about to be re-enqueued. Done at the end of an // append (or removal) operation so the flush IPC reaches the renderer diff --git a/Source/WebCore/platform/graphics/SourceBufferPrivate.h b/Source/WebCore/platform/graphics/SourceBufferPrivate.h index 38d54957dc89..216950c42cc4 100644 --- a/Source/WebCore/platform/graphics/SourceBufferPrivate.h +++ b/Source/WebCore/platform/graphics/SourceBufferPrivate.h @@ -143,6 +143,18 @@ class SourceBufferPrivate // Methods used by MediaSourcePrivate bool NODELETE hasReceivedFirstInitializationSegment() const; + // Returns true if this SourceBuffer has an audio track whose buffered + // ranges include `time`. If `excluded` is set, the track with that ID is + // skipped (used so an audio TrackBuffer doesn't claim self-coverage when + // querying the unified gap policy). + bool isAudioBufferedAt(const MediaTime&, std::optional excluded) const; + + // Union of all audio TrackBuffers' buffered ranges in this + // SourceBuffer. Caller must be on the dispatcher; the result is a + // copy that's safe to merge into MediaSourcePrivate's lock-protected + // audio-buffered cache. + PlatformTimeRanges audioBufferedRanges() const; + virtual size_t platformMaximumBufferSize() const { return 0; } Ref setMaximumBufferSize(size_t); @@ -184,7 +196,6 @@ class SourceBufferPrivate virtual Ref appendInternal(Ref&&) = 0; virtual void resetParserStateInternal() = 0; - virtual MediaTime timeFudgeFactor() const { return PlatformTimeRanges::timeFudgeFactor(); } virtual void flush(TrackID) { } virtual void enqueueSample(Ref&&, TrackID) { } virtual void allSamplesInTrackEnqueued(TrackID) { } diff --git a/Source/WebCore/platform/graphics/TrackBuffer.cpp b/Source/WebCore/platform/graphics/TrackBuffer.cpp index 09b89ed56a42..95184b305ad0 100644 --- a/Source/WebCore/platform/graphics/TrackBuffer.cpp +++ b/Source/WebCore/platform/graphics/TrackBuffer.cpp @@ -55,21 +55,23 @@ static inline MediaTime roundTowardsTimeScaleWithRoundingMargin(const MediaTime& } }; -UniqueRef TrackBuffer::create(RefPtr&& description) +UniqueRef TrackBuffer::create(RefPtr&& description, IsAcceptableEnqueueGapFn&& isAcceptableEnqueueGap) { - return create(WTF::move(description), MediaTime::zeroTime()); + return makeUniqueRef(WTF::move(description), WTF::move(isAcceptableEnqueueGap)); } -UniqueRef TrackBuffer::create(RefPtr&& description, const MediaTime& discontinuityTolerance) +TrackBuffer::TrackBuffer(RefPtr&& description, IsAcceptableEnqueueGapFn&& isAcceptableEnqueueGap) + : m_description(WTF::move(description)) + , m_enqueueDiscontinuityBoundary(PlatformTimeRanges::timeFudgeFactor()) + , m_isAcceptableEnqueueGap(WTF::move(isAcceptableEnqueueGap)) { - return makeUniqueRef(WTF::move(description), discontinuityTolerance); } -TrackBuffer::TrackBuffer(RefPtr&& description, const MediaTime& discontinuityTolerance) - : m_description(WTF::move(description)) - , m_enqueueDiscontinuityBoundary(discontinuityTolerance) - , m_discontinuityTolerance(discontinuityTolerance) +bool TrackBuffer::isAcceptableEnqueueGap(const MediaTime& fromTime, const MediaTime& toTime) const { + if (toTime - fromTime <= PlatformTimeRanges::timeFudgeFactor()) + return true; + return m_isAcceptableEnqueueGap && m_isAcceptableEnqueueGap(fromTime, toTime); } MediaTime TrackBuffer::maximumBufferedTime() const @@ -202,7 +204,8 @@ RefPtr TrackBuffer::nextSample() Ref sample = decodeQueue().begin()->second; - if (sample->decodeTime() > enqueueDiscontinuityBoundary()) { + if (sample->decodeTime() > enqueueDiscontinuityBoundary() + && (!m_isAcceptableEnqueueGap || !m_isAcceptableEnqueueGap(m_lastEnqueueDecodeEnd, sample->decodeTime()))) { WARNING_LOG(LOGIDENTIFIER, "bailing early because of unbuffered gap, new sample DTS: ", sample->decodeTime(), " >= the current discontinuity boundary: ", enqueueDiscontinuityBoundary()); return { }; } @@ -216,7 +219,8 @@ RefPtr TrackBuffer::nextSample() setLastEnqueuedDecodeKey({ sample->decodeTime(), sample->presentationTime() }); auto decodeEnd = std::max(sample->decodeTime() + sample->duration(), samplePresentationEnd); - setEnqueueDiscontinuityBoundary(decodeEnd + m_discontinuityTolerance); + m_lastEnqueueDecodeEnd = decodeEnd; + setEnqueueDiscontinuityBoundary(decodeEnd + PlatformTimeRanges::timeFudgeFactor()); m_minimumEnqueuedPresentationTime = MediaTime::invalidTime(); if (m_hasOutOfOrderFrames) @@ -263,10 +267,11 @@ void TrackBuffer::updateMinimumUpcomingPresentationTime() m_minimumEnqueuedPresentationTime = MediaTime::invalidTime(); } -bool TrackBuffer::reenqueueMediaForTime(const MediaTime& time, const MediaTime& timeFudgeFactor, bool isEnded) +bool TrackBuffer::reenqueueMediaForTime(const MediaTime& time, bool isEnded) { clearDecodeQueue(); - m_enqueueDiscontinuityBoundary = time + m_discontinuityTolerance; + m_lastEnqueueDecodeEnd = time; + m_enqueueDiscontinuityBoundary = time + PlatformTimeRanges::timeFudgeFactor(); m_needsReenqueueing = false; @@ -276,10 +281,11 @@ bool TrackBuffer::reenqueueMediaForTime(const MediaTime& time, const MediaTime& // Find the sample which contains the current presentation time. auto currentSamplePTSIterator = m_samples.presentationOrder().findSampleContainingPresentationTime(time); - // Find the next sample, so long as its presentation start time is within the timeFudgeFactor. + // Find the next sample, so long as its presentation start time is within + // the gap-skipping policy from the seek target. if (currentSamplePTSIterator == m_samples.presentationOrder().end()) { auto nextSampleIterator = m_samples.presentationOrder().findSampleStartingOnOrAfterPresentationTime(time); - if ((nextSampleIterator->first - time) <= timeFudgeFactor) + if (nextSampleIterator != m_samples.presentationOrder().end() && isAcceptableEnqueueGap(time, nextSampleIterator->first)) currentSamplePTSIterator = nextSampleIterator; } diff --git a/Source/WebCore/platform/graphics/TrackBuffer.h b/Source/WebCore/platform/graphics/TrackBuffer.h index b0f47595b02f..7f15a16a7055 100644 --- a/Source/WebCore/platform/graphics/TrackBuffer.h +++ b/Source/WebCore/platform/graphics/TrackBuffer.h @@ -46,8 +46,17 @@ class TrackBuffer final { WTF_MAKE_TZONE_ALLOCATED(TrackBuffer); public: - static UniqueRef NODELETE create(RefPtr&&); - static UniqueRef create(RefPtr&&, const MediaTime&); + // Returns true if the timestamp gap between `fromTime` and `toTime` + // is acceptable per the gap-skipping policy. Used both with DTS gaps + // (between adjacent samples in nextSample) and with PTS gaps + // (between a seek target and the next available sample in + // reenqueueMediaForTime). Only consulted when the gap exceeds + // PlatformTimeRanges::timeFudgeFactor() — gaps below that threshold + // are treated as contiguous (matching how the buffered range itself + // is reported to clients) and never reach the lambda. + using IsAcceptableEnqueueGapFn = Function; + + static UniqueRef create(RefPtr&&, IsAcceptableEnqueueGapFn&& = nullptr); MediaTime NODELETE maximumBufferedTime() const; void addBufferedRange(const MediaTime& start, const MediaTime& end, AddTimeRangeOption = AddTimeRangeOption::None); @@ -58,7 +67,7 @@ class TrackBuffer final // SampleMap and m_decodeQueue, and adjusts m_buffered to match. void adjustSampleStartTime(MediaSample& original, const MediaTime& offset); - bool reenqueueMediaForTime(const MediaTime&, const MediaTime& timeFudgeFactor, bool isEnded = false); + bool reenqueueMediaForTime(const MediaTime&, bool isEnded = false); MediaTime findSeekTimeForTargetTime(const MediaTime& targetTime, const MediaTime& negativeThreshold, const MediaTime& positiveThreshold); int64_t removeCodedFrames(const MediaTime& start, const MediaTime& end, const MediaTime& currentTime); PlatformTimeRanges removeSamples(const DecodeOrderSampleMap::MapType&, ASCIILiteral); @@ -125,8 +134,16 @@ class TrackBuffer final #endif private: - friend UniqueRef WTF::makeUniqueRefWithoutFastMallocCheck(RefPtr&&, const WTF::MediaTime&); - TrackBuffer(RefPtr&&, const MediaTime&); + friend UniqueRef WTF::makeUniqueRefWithoutFastMallocCheck(RefPtr&&, IsAcceptableEnqueueGapFn&&); + TrackBuffer(RefPtr&&, IsAcceptableEnqueueGapFn&&); + + // Returns true if the DTS gap from `fromTime` to `toTime` is small + // enough to enqueue across. Gaps within + // PlatformTimeRanges::timeFudgeFactor() are always accepted (the + // buffered range itself would be reported as contiguous); larger + // gaps consult the constructor-supplied callback (when set — + // currently MSE only). + bool isAcceptableEnqueueGap(const MediaTime& fromTime, const MediaTime& toTime) const; const DecodeOrderSampleMap::MapType& decodeQueue() const LIFETIME_BOUND { return m_decodeQueue; } DecodeOrderSampleMap::MapType& decodeQueue() LIFETIME_BOUND { return m_decodeQueue; } @@ -173,7 +190,8 @@ class TrackBuffer final DecodeOrderSampleMap::KeyType m_lastEnqueuedDecodeKey { MediaTime::invalidTime(), MediaTime::invalidTime() }; MediaTime m_enqueueDiscontinuityBoundary; - MediaTime m_discontinuityTolerance; + MediaTime m_lastEnqueueDecodeEnd; + IsAcceptableEnqueueGapFn m_isAcceptableEnqueueGap; MediaTime m_roundedTimestampOffset { MediaTime::invalidTime() }; diff --git a/Source/WebCore/platform/graphics/avfoundation/objc/MediaPlayerPrivateMediaSourceAVFObjC.mm b/Source/WebCore/platform/graphics/avfoundation/objc/MediaPlayerPrivateMediaSourceAVFObjC.mm index 3e2cee5e6408..7b42a13ea95e 100644 --- a/Source/WebCore/platform/graphics/avfoundation/objc/MediaPlayerPrivateMediaSourceAVFObjC.mm +++ b/Source/WebCore/platform/graphics/avfoundation/objc/MediaPlayerPrivateMediaSourceAVFObjC.mm @@ -808,25 +808,12 @@ MediaPlayerScope supportedScope() const final return; } - auto ranges = protect(m_mediaSourcePrivate)->buffered(); if (!protect(m_mediaSourcePrivate)->hasFutureTime(time) && shouldBePlaying()) { ALWAYS_LOG(LOGIDENTIFIER, "Not having data to play at time: ", time, " stalling"); stall(); } - auto stallAtTime = duration(); - size_t index = ranges.find(time); - if (index != notFound) { - // Find the next gap (or end of media) - for (; index < ranges.length(); index++) { - if ((index < ranges.length() - 1 && ranges.start(index + 1) - ranges.end(index) > m_mediaSourcePrivate->timeFudgeFactor()) - || (index == ranges.length() - 1 && ranges.end(index) > time)) { - stallAtTime = ranges.end(index); - break; - } - } - } - + auto stallAtTime = protect(m_mediaSourcePrivate)->nextStallTime(time); ALWAYS_LOG(LOGIDENTIFIER, "will stall playback at time: ", stallAtTime); m_renderer->notifyTimeReachedAndStall(stallAtTime)->whenSettled(RunLoop::mainSingleton(), WTF::move(onStallReached))->track(m_stallRequest); } diff --git a/Source/WebCore/platform/graphics/avfoundation/objc/SourceBufferPrivateAVFObjC.h b/Source/WebCore/platform/graphics/avfoundation/objc/SourceBufferPrivateAVFObjC.h index 9105b89fd38a..8542ae93fceb 100644 --- a/Source/WebCore/platform/graphics/avfoundation/objc/SourceBufferPrivateAVFObjC.h +++ b/Source/WebCore/platform/graphics/avfoundation/objc/SourceBufferPrivateAVFObjC.h @@ -139,7 +139,6 @@ class SourceBufferPrivateAVFObjC final void flush(TrackID) final; void enqueueSample(Ref&&, TrackID) final; bool isReadyForMoreSamples(TrackID) final; - MediaTime timeFudgeFactor() const final; void notifyClientWhenReadyForMoreSamples(TrackID) final; bool canSetMinimumUpcomingPresentationTime(TrackID) const override; void setMinimumUpcomingPresentationTime(TrackID, const MediaTime&) override; diff --git a/Source/WebCore/platform/graphics/avfoundation/objc/SourceBufferPrivateAVFObjC.mm b/Source/WebCore/platform/graphics/avfoundation/objc/SourceBufferPrivateAVFObjC.mm index 6500ebdaff46..ded8eaaac10a 100644 --- a/Source/WebCore/platform/graphics/avfoundation/objc/SourceBufferPrivateAVFObjC.mm +++ b/Source/WebCore/platform/graphics/avfoundation/objc/SourceBufferPrivateAVFObjC.mm @@ -695,14 +695,6 @@ return false; } -MediaTime SourceBufferPrivateAVFObjC::timeFudgeFactor() const -{ - if (RefPtr mediaSource = m_mediaSource.get()) - return mediaSource->timeFudgeFactor(); - - return SourceBufferPrivate::timeFudgeFactor(); -} - FloatSize SourceBufferPrivateAVFObjC::naturalSize() { assertIsCurrent(m_dispatcher.get()); diff --git a/Source/WebCore/platform/graphics/cocoa/MediaPlayerPrivateWebM.mm b/Source/WebCore/platform/graphics/cocoa/MediaPlayerPrivateWebM.mm index 218713b87c34..0b5d42d0f86f 100644 --- a/Source/WebCore/platform/graphics/cocoa/MediaPlayerPrivateWebM.mm +++ b/Source/WebCore/platform/graphics/cocoa/MediaPlayerPrivateWebM.mm @@ -1276,7 +1276,7 @@ if (needsFlush == NeedsFlush::Yes) m_renderer->flushTrack(*trackIdentifier); - if (trackBuffer.reenqueueMediaForTime(time, timeFudgeFactor(), m_loadFinished)) + if (trackBuffer.reenqueueMediaForTime(time, m_loadFinished)) provideMediaData(trackBuffer, trackId); } @@ -1643,7 +1643,10 @@ setHasAudio(m_hasAudio || description->isAudio()); setHasVideo(m_hasVideo || description->isVideo()); - auto trackBuffer = TrackBuffer::create(WTF::move(description), discontinuityTolerance); + auto trackBuffer = TrackBuffer::create(WTF::move(description), + [](const MediaTime& fromTime, const MediaTime& toTime) { + return (toTime - fromTime) <= discontinuityTolerance; + }); trackBuffer->setLogger(protect(logger()), logIdentifier()); m_trackBufferMap.try_emplace(trackId, WTF::move(trackBuffer)); m_requestReadyForMoreSamplesSetMap[trackId] = false; @@ -1963,7 +1966,7 @@ MediaPlayerScope supportedScope() const final auto currentTime = this->currentTime(); MediaTime aheadTime = std::min(durationOnRunningQueue(), currentTime + MediaTime::createWithDouble(kHaveEnoughDataThreshold)); PlatformTimeRanges neededBufferedRange { currentTime, std::max(currentTime, aheadTime) }; - auto newState = m_buffered.containWithEpsilon(neededBufferedRange, MediaTime(2002, 24000)) ? MediaPlayer::ReadyState::HaveEnoughData : MediaPlayer::ReadyState::HaveFutureData; + auto newState = m_buffered.containWithEpsilon(neededBufferedRange, timeFudgeFactor()) ? MediaPlayer::ReadyState::HaveEnoughData : MediaPlayer::ReadyState::HaveFutureData; ensureOnMainThread([weakThis = ThreadSafeWeakPtr { *this }, newState] { if (RefPtr protectedThis = weakThis.get()) protectedThis->setReadyState(newState); From b590f9134b0ac936876ee26bcc7204a25f5bb19f Mon Sep 17 00:00:00 2001 From: Alan Baradlay Date: Tue, 30 Jun 2026 07:33:25 -0700 Subject: [PATCH 33/84] [cleanup] Use RenderBox::borderBoxSize() in size-only callers of frameRect() https://bugs.webkit.org/show_bug.cgi?id=318045 Reviewed by Antti Koivisto. Follow-up to 318020, which swept the size-only callers of borderBoxRect() but not the frameRect().size() spelling of the same thing. These RenderTableCell paint helpers only want the border box size, and frameRect().size() is exactly borderBoxSize() (both are m_frameRect.size()), so use the direct accessor. No behavior change. * Source/WebCore/rendering/RenderTableCell.cpp: (RenderTableCell::paintCollapsedBorders): (RenderTableCell::paintBoxDecorations): (RenderTableCell::paintMask): Canonical link: https://commits.webkit.org/316147@main --- Source/WebCore/rendering/RenderTableCell.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/WebCore/rendering/RenderTableCell.cpp b/Source/WebCore/rendering/RenderTableCell.cpp index 7f6ca50a8fc4..b0e7ff624d06 100644 --- a/Source/WebCore/rendering/RenderTableCell.cpp +++ b/Source/WebCore/rendering/RenderTableCell.cpp @@ -1423,7 +1423,7 @@ void RenderTableCell::paintCollapsedBorders(PaintInfo& paintInfo, const LayoutPo return; LayoutRect localRepaintRect = paintInfo.rect; - LayoutRect paintRect = LayoutRect(paintOffset + location(), frameRect().size()); + LayoutRect paintRect = LayoutRect(paintOffset + location(), borderBoxSize()); if (paintRect.y() - table()->outerBorderTop() >= localRepaintRect.maxY()) return; @@ -1643,7 +1643,7 @@ void RenderTableCell::paintBoxDecorations(PaintInfo& paintInfo, const LayoutPoin if (!table->collapseBorders() && style().emptyCells() == EmptyCell::Hide && !firstChild()) return; - LayoutRect paintRect = LayoutRect(paintOffset, frameRect().size()); + LayoutRect paintRect = LayoutRect(paintOffset, borderBoxSize()); adjustBorderBoxRectForPainting(paintRect); BackgroundPainter backgroundPainter { *this, paintInfo }; @@ -1675,7 +1675,7 @@ void RenderTableCell::paintMask(PaintInfo& paintInfo, const LayoutPoint& paintOf if (!tableElt->collapseBorders() && style().emptyCells() == EmptyCell::Hide && !firstChild()) return; - LayoutRect paintRect = LayoutRect(paintOffset, frameRect().size()); + LayoutRect paintRect = LayoutRect(paintOffset, borderBoxSize()); adjustBorderBoxRectForPainting(paintRect); paintMaskImages(paintInfo, paintRect); From 9b4ffe1f77999658bdda22d8b76dee99fa6e5c7c Mon Sep 17 00:00:00 2001 From: Justin Michaud Date: Tue, 30 Jun 2026 07:46:07 -0700 Subject: [PATCH 34/84] Add findIfCached / insert to TinyLRUCache https://bugs.webkit.org/show_bug.cgi?id=314417 Reviewed by Keith Miller. This is useful for cases when you need to cache something that can't created statically. We also add a generated test to gently exercise this new code. We also add a new safe iterator mechanism to ensure that this new API can't be used unsafely. * Source/WTF/wtf/TinyLRUCache.h: (WTF::TinyLRUCache::~TinyLRUCache): (WTF::TinyLRUCache::TinyLRUCache): (WTF::TinyLRUCache::operator=): (WTF::TinyLRUCache::FindResult::~FindResult): (WTF::TinyLRUCache::FindResult::operator bool const): (WTF::TinyLRUCache::FindResult::operator* const): (WTF::TinyLRUCache::FindResult::operator-> const): (WTF::TinyLRUCache::FindResult::FindResult): (WTF::TinyLRUCache::FindResult::assertValid const): (WTF::TinyLRUCache::get): (WTF::TinyLRUCache::findIfCached): (WTF::TinyLRUCache::insert): (WTF::TinyLRUCache::clear): (WTF::TinyLRUCache::size const): (WTF::TinyLRUCache::trackFindResult): (WTF::TinyLRUCache::invalidateIterators): (WTF::TinyLRUCache::findInternal): (WTF::TinyLRUCache::insertInternal): * Tools/TestWebKitAPI/CMakeLists.txt: * Tools/TestWebKitAPI/Tests/WTF/TinyLRUCache.cpp: Added. (TestWebKitAPI::CountingPolicy::createValueForKey): (TestWebKitAPI::TEST(WTF_TinyLRUCache, GetCachesOnHit)): (TestWebKitAPI::TEST(WTF_TinyLRUCache, GetEvictsLRU)): (TestWebKitAPI::NullKeyPolicy::isKeyNull): (TestWebKitAPI::NullKeyPolicy::createValueForNullKey): (TestWebKitAPI::NullKeyPolicy::createValueForKey): (TestWebKitAPI::TEST(WTF_TinyLRUCache, GetWithNullKeyReturnsNullValueAndDoesNotStore)): (TestWebKitAPI::TEST(WTF_TinyLRUCache, FindIfCachedReturnsNullOnMiss)): (TestWebKitAPI::TEST(WTF_TinyLRUCache, FindIfCachedHitsAfterInsert)): (TestWebKitAPI::TEST(WTF_TinyLRUCache, InsertEvictsLRU)): (TestWebKitAPI::TEST(WTF_TinyLRUCache, FindIfCachedPromotesToMRU)): (TestWebKitAPI::TEST(WTF_TinyLRUCache, ClearReleasesEntriesAndResetsSize)): (TestWebKitAPI::TrackedRefCounted::create): (TestWebKitAPI::TrackedRefCounted::value const): (TestWebKitAPI::TrackedRefCounted::~TrackedRefCounted): (TestWebKitAPI::TrackedRefCounted::TrackedRefCounted): (TestWebKitAPI::TEST(WTF_TinyLRUCache, ClearReleasesRefsImmediately)): (TestWebKitAPI::TEST(WTF_TinyLRUCache, EvictionViaInsertReleasesRef)): (TestWebKitAPI::TEST(WTF_TinyLRUCache, FindIfCachedDoesNotConsumeEntry)): (TestWebKitAPI::TEST(WTF_TinyLRUCache, FindIfCachedAfterClearMisses)): (TestWebKitAPI::TEST(WTF_TinyLRUCache, FindOrComputeWorkflow)): (TestWebKitAPI::TEST(WTF_TinyLRUCache, FindResultSafeAfterCacheDestroyed)): (TestWebKitAPI::TEST(WTF_TinyLRUCache, InsertInvalidatesFindResult)): (TestWebKitAPI::TEST(WTF_TinyLRUCache, ClearInvalidatesFindResult)): (TestWebKitAPI::TEST(WTF_TinyLRUCache, SubsequentFindInvalidatesPriorFindResult)): (TestWebKitAPI::TEST(WTF_TinyLRUCacheDeathTest, UseAfterInsertDeathTest)): (TestWebKitAPI::TEST(WTF_TinyLRUCacheDeathTest, UseAfterClearDeathTest)): (TestWebKitAPI::TEST(WTF_TinyLRUCacheDeathTest, UseAfterSubsequentFindDeathTest)): Canonical link: https://commits.webkit.org/316148@main --- Source/WTF/wtf/TinyLRUCache.h | 165 +++++++- Tools/TestWebKitAPI/CMakeLists.txt | 1 + .../TestWebKitAPI/Tests/WTF/TinyLRUCache.cpp | 355 ++++++++++++++++++ 3 files changed, 514 insertions(+), 7 deletions(-) create mode 100644 Tools/TestWebKitAPI/Tests/WTF/TinyLRUCache.cpp diff --git a/Source/WTF/wtf/TinyLRUCache.h b/Source/WTF/wtf/TinyLRUCache.h index eb801103a79b..0c078d06ac74 100644 --- a/Source/WTF/wtf/TinyLRUCache.h +++ b/Source/WTF/wtf/TinyLRUCache.h @@ -28,6 +28,7 @@ #include #include #include +#include #include namespace WTF { @@ -44,45 +45,195 @@ templatem_findResult == this); + m_cache->invalidateIterators(); + } + } + + explicit operator bool() const + { + return m_ptr; + } + + const ValueType& operator*() const + { + RELEASE_ASSERT(m_ptr); + return *m_ptr; + } + + const ValueType* operator->() const + { + RELEASE_ASSERT(m_ptr); + return m_ptr; + } + + private: + friend class TinyLRUCache; + + using Cache = TinyLRUCache; + + FindResult(const ValueType* ptr, Cache& cache) + : m_ptr(ptr) + , m_cache(ptr ? &cache : nullptr) + { + if (m_cache) + m_cache->trackFindResult(this); + } + + void assertValid() const + { + RELEASE_ASSERT_IMPLIES(m_ptr, m_cache); + } + + const ValueType* m_ptr { nullptr }; + Cache* m_cache { nullptr }; + }; + const ValueType& get(const KeyType& key) { + invalidateIterators(); + if (Policy::isKeyNull(key)) { static NeverDestroyed valueForNull = Policy::createValueForNullKey(); return valueForNull; } + if (auto* found = findInternal(key)) + return *found; + + insertInternal(key, Policy::createValueForKey(key)); + return cacheBuffer()[m_size - 1].second; + } + + FindResult findIfCached(const KeyType& key) + { + invalidateIterators(); + + if (Policy::isKeyNull(key)) + return { nullptr, *this }; + + return { findInternal(key), *this }; + } + + void insert(const KeyType& key, ValueType&& value) + { + ASSERT(!Policy::isKeyNull(key)); +#if ASSERT_ENABLED + { + auto cacheBuffer = this->cacheBuffer(); + for (size_t i = 0; i < m_size; ++i) + ASSERT(!(cacheBuffer[i].first == key)); + } +#endif + invalidateIterators(); + insertInternal(key, WTF::move(value)); + } + + void clear() + { + invalidateIterators(); + auto cacheBuffer = this->cacheBuffer(); + for (size_t i = 0; i < m_size; ++i) + cacheBuffer[i] = Entry { }; + m_size = 0; + } + + size_t size() const { return m_size; } + +private: + void trackFindResult(FindResult* p) + { + ASSERT(p); + ASSERT(m_findResult != p); + invalidateIterators(); + m_findResult = p; + } + + void invalidateIterators() + { + if (m_findResult) { + ASSERT(m_findResult->m_cache == this); + m_findResult->m_cache = nullptr; + m_findResult->m_ptr = nullptr; + m_findResult = nullptr; + } + } + + ALWAYS_INLINE const ValueType* findInternal(const KeyType& key) + { auto cacheBuffer = this->cacheBuffer(); for (size_t i = m_size; i-- > 0;) { if (cacheBuffer[i].first == key) { if (i < m_size - 1) { - // Move entry to the end of the cache if necessary. auto entry = WTF::move(cacheBuffer[i]); do { cacheBuffer[i] = WTF::move(cacheBuffer[i + 1]); } while (++i < m_size - 1); cacheBuffer[m_size - 1] = WTF::move(entry); } - return cacheBuffer[m_size - 1].second; + return &cacheBuffer[m_size - 1].second; } } + return nullptr; + } - // cacheBuffer[0] is the LRU entry, so remove it. + ALWAYS_INLINE void insertInternal(const KeyType& key, ValueType&& value) + { + auto cacheBuffer = this->cacheBuffer(); if (m_size == capacity) { for (size_t i = 0; i < m_size - 1; ++i) cacheBuffer[i] = WTF::move(cacheBuffer[i + 1]); } else ++m_size; - - cacheBuffer[m_size - 1] = std::pair { Policy::createKeyForStorage(key), Policy::createValueForKey(key) }; - return cacheBuffer[m_size - 1].second; + cacheBuffer[m_size - 1] = std::pair { Policy::createKeyForStorage(key), WTF::move(value) }; } -private: using Entry = std::pair; std::span cacheBuffer() { return m_cacheBuffer; } alignas(Entry) std::array m_cacheBuffer; size_t m_size { 0 }; + FindResult* m_findResult { nullptr }; }; } // namespace WTF diff --git a/Tools/TestWebKitAPI/CMakeLists.txt b/Tools/TestWebKitAPI/CMakeLists.txt index 1ea34713d9dc..24512eb7e1aa 100644 --- a/Tools/TestWebKitAPI/CMakeLists.txt +++ b/Tools/TestWebKitAPI/CMakeLists.txt @@ -136,6 +136,7 @@ set(TestWTF_SOURCES Tests/WTF/StringView.cpp Tests/WTF/SynchronizedFixedQueue.cpp Tests/WTF/TextBreakIterator.cpp + Tests/WTF/TinyLRUCache.cpp Tests/WTF/ThreadGroup.cpp Tests/WTF/ThreadMessages.cpp Tests/WTF/Threading.cpp diff --git a/Tools/TestWebKitAPI/Tests/WTF/TinyLRUCache.cpp b/Tools/TestWebKitAPI/Tests/WTF/TinyLRUCache.cpp new file mode 100644 index 000000000000..fdfbbf0c7e6e --- /dev/null +++ b/Tools/TestWebKitAPI/Tests/WTF/TinyLRUCache.cpp @@ -0,0 +1,355 @@ +/* + * Copyright (C) 2026 Igalia S.L. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include + +#include "Helpers/Test.h" +#include +#include + +namespace TestWebKitAPI { + +struct CountingPolicy : public WTF::TinyLRUCachePolicy { + static unsigned createCount; + static int createValueForKey(const int& key) + { + ++createCount; + return key * 10; + } +}; +unsigned CountingPolicy::createCount = 0; + +TEST(WTF_TinyLRUCache, GetCachesOnHit) +{ + CountingPolicy::createCount = 0; + WTF::TinyLRUCache cache; + EXPECT_EQ(0u, cache.size()); + + EXPECT_EQ(10, cache.get(1)); + EXPECT_EQ(1u, CountingPolicy::createCount); + EXPECT_EQ(1u, cache.size()); + + EXPECT_EQ(10, cache.get(1)); + EXPECT_EQ(1u, CountingPolicy::createCount); + EXPECT_EQ(1u, cache.size()); + + EXPECT_EQ(20, cache.get(2)); + EXPECT_EQ(2u, CountingPolicy::createCount); + EXPECT_EQ(2u, cache.size()); +} + +TEST(WTF_TinyLRUCache, GetEvictsLRU) +{ + CountingPolicy::createCount = 0; + WTF::TinyLRUCache cache; + + cache.get(1); + cache.get(2); + EXPECT_EQ(2u, cache.size()); + + cache.get(3); // evicts 1 + EXPECT_EQ(2u, cache.size()); + + cache.get(2); + EXPECT_EQ(3u, CountingPolicy::createCount); + + cache.get(1); // re-computed because evicted + EXPECT_EQ(4u, CountingPolicy::createCount); +} + +struct NullKeyPolicy : public WTF::TinyLRUCachePolicy { + static unsigned createCount; + static unsigned createNullCount; + static bool isKeyNull(const int& key) { return !key; } + static int createValueForNullKey() + { + ++createNullCount; + return -1; + } + static int createValueForKey(const int& key) + { + ++createCount; + return key * 10; + } +}; +unsigned NullKeyPolicy::createCount = 0; +unsigned NullKeyPolicy::createNullCount = 0; + +TEST(WTF_TinyLRUCache, GetWithNullKeyReturnsNullValueAndDoesNotStore) +{ + NullKeyPolicy::createCount = 0; + NullKeyPolicy::createNullCount = 0; + WTF::TinyLRUCache cache; + + EXPECT_EQ(-1, cache.get(0)); + EXPECT_EQ(0u, cache.size()); + EXPECT_EQ(0u, NullKeyPolicy::createCount); + + // Repeated null lookups must not grow the cache or re-create the null value. + EXPECT_EQ(-1, cache.get(0)); + EXPECT_EQ(0u, cache.size()); + EXPECT_EQ(1u, NullKeyPolicy::createNullCount); + + // Non-null keys still work and are unaffected by null-key lookups. + EXPECT_EQ(50, cache.get(5)); + EXPECT_EQ(1u, cache.size()); + EXPECT_EQ(1u, NullKeyPolicy::createCount); + + // findIfCached must miss for null keys regardless of prior get() calls. + EXPECT_FALSE(cache.findIfCached(0)); +} + +TEST(WTF_TinyLRUCache, FindIfCachedReturnsNullOnMiss) +{ + WTF::TinyLRUCache cache; + EXPECT_FALSE(cache.findIfCached(42)); +} + +TEST(WTF_TinyLRUCache, FindIfCachedHitsAfterInsert) +{ + WTF::TinyLRUCache cache; + cache.insert(1, 100); + auto hit = cache.findIfCached(1); + ASSERT_TRUE(hit); + EXPECT_EQ(100, *hit); + EXPECT_EQ(1u, cache.size()); +} + +TEST(WTF_TinyLRUCache, InsertEvictsLRU) +{ + WTF::TinyLRUCache cache; + cache.insert(1, 100); + cache.insert(2, 200); + EXPECT_EQ(2u, cache.size()); + + cache.insert(3, 300); // evicts 1 + EXPECT_EQ(2u, cache.size()); + EXPECT_FALSE(cache.findIfCached(1)); + auto two = cache.findIfCached(2); + ASSERT_TRUE(two); + EXPECT_EQ(200, *two); + auto three = cache.findIfCached(3); + ASSERT_TRUE(three); + EXPECT_EQ(300, *three); +} + +TEST(WTF_TinyLRUCache, FindIfCachedPromotesToMRU) +{ + WTF::TinyLRUCache cache; + cache.insert(1, 100); + cache.insert(2, 200); + cache.insert(3, 300); + + // Promote 1 to MRU; subsequent insert should evict 2 (now LRU), not 1. + EXPECT_TRUE(cache.findIfCached(1)); + + cache.insert(4, 400); + EXPECT_TRUE(cache.findIfCached(1)); + EXPECT_FALSE(cache.findIfCached(2)); + EXPECT_TRUE(cache.findIfCached(3)); + EXPECT_TRUE(cache.findIfCached(4)); +} + +TEST(WTF_TinyLRUCache, ClearReleasesEntriesAndResetsSize) +{ + WTF::TinyLRUCache cache; + cache.insert(1, 100); + cache.insert(2, 200); + EXPECT_EQ(2u, cache.size()); + cache.clear(); + EXPECT_EQ(0u, cache.size()); + EXPECT_FALSE(cache.findIfCached(1)); + EXPECT_FALSE(cache.findIfCached(2)); +} + +class TrackedRefCounted : public RefCounted { +public: + static unsigned aliveCount; + static Ref create(int value) { return adoptRef(*new TrackedRefCounted(value)); } + int value() const { return m_value; } + ~TrackedRefCounted() { --aliveCount; } +private: + TrackedRefCounted(int value) : m_value(value) { ++aliveCount; } + int m_value; +}; +unsigned TrackedRefCounted::aliveCount = 0; + +TEST(WTF_TinyLRUCache, ClearReleasesRefsImmediately) +{ + TrackedRefCounted::aliveCount = 0; + { + WTF::TinyLRUCache, 4> cache; + cache.insert(1, TrackedRefCounted::create(100).ptr()); + cache.insert(2, TrackedRefCounted::create(200).ptr()); + EXPECT_EQ(2u, TrackedRefCounted::aliveCount); + + cache.clear(); + EXPECT_EQ(0u, TrackedRefCounted::aliveCount); + } +} + +TEST(WTF_TinyLRUCache, EvictionViaInsertReleasesRef) +{ + TrackedRefCounted::aliveCount = 0; + { + WTF::TinyLRUCache, 2> cache; + cache.insert(1, TrackedRefCounted::create(100).ptr()); + cache.insert(2, TrackedRefCounted::create(200).ptr()); + EXPECT_EQ(2u, TrackedRefCounted::aliveCount); + + // Inserting a third entry into a capacity-2 cache must drop the LRU's ref. + cache.insert(3, TrackedRefCounted::create(300).ptr()); + EXPECT_EQ(2u, TrackedRefCounted::aliveCount); + } + EXPECT_EQ(0u, TrackedRefCounted::aliveCount); +} + +TEST(WTF_TinyLRUCache, FindIfCachedDoesNotConsumeEntry) +{ + WTF::TinyLRUCache cache; + cache.insert(1, 100); + { + auto hit = cache.findIfCached(1); + ASSERT_TRUE(hit); + EXPECT_EQ(100, *hit); + } + { + auto hit = cache.findIfCached(1); + ASSERT_TRUE(hit); + EXPECT_EQ(100, *hit); + } + EXPECT_EQ(1u, cache.size()); +} + +TEST(WTF_TinyLRUCache, FindIfCachedAfterClearMisses) +{ + WTF::TinyLRUCache cache; + cache.insert(1, 100); + cache.clear(); + EXPECT_FALSE(cache.findIfCached(1)); + cache.insert(1, 999); // re-insert with different value + auto hit = cache.findIfCached(1); + ASSERT_TRUE(hit); + EXPECT_EQ(999, *hit); +} + +TEST(WTF_TinyLRUCache, FindOrComputeWorkflow) +{ + WTF::TinyLRUCache cache; + int externalContext = 7; + + auto findOrInsert = [&](int key) -> int { + if (auto hit = cache.findIfCached(key)) + return *hit; + int value = key * externalContext; + cache.insert(key, WTF::move(value)); + auto hit = cache.findIfCached(key); + return hit ? *hit : -1; + }; + + EXPECT_EQ(7, findOrInsert(1)); + EXPECT_EQ(14, findOrInsert(2)); + EXPECT_EQ(7, findOrInsert(1)); + EXPECT_EQ(14, findOrInsert(2)); + EXPECT_EQ(2u, cache.size()); +} + +TEST(WTF_TinyLRUCache, FindResultSafeAfterCacheDestroyed) +{ + WTF::TinyLRUCache* cache = new WTF::TinyLRUCache(); + cache->insert(1, 100); + auto result = cache->findIfCached(1); + ASSERT_TRUE(result); + delete cache; + EXPECT_FALSE(result); +} + +TEST(WTF_TinyLRUCache, InsertInvalidatesFindResult) +{ + WTF::TinyLRUCache cache; + cache.insert(1, 100); + auto result = cache.findIfCached(1); + ASSERT_TRUE(result); + cache.insert(2, 200); + EXPECT_FALSE(result); +} + +TEST(WTF_TinyLRUCache, ClearInvalidatesFindResult) +{ + WTF::TinyLRUCache cache; + cache.insert(1, 100); + auto result = cache.findIfCached(1); + ASSERT_TRUE(result); + cache.clear(); + EXPECT_FALSE(result); +} + +TEST(WTF_TinyLRUCache, SubsequentFindInvalidatesPriorFindResult) +{ + WTF::TinyLRUCache cache; + cache.insert(1, 100); + cache.insert(2, 200); + auto result = cache.findIfCached(2); + ASSERT_TRUE(result); + cache.findIfCached(1); + EXPECT_FALSE(result); +} + +TEST(WTF_TinyLRUCacheDeathTest, UseAfterInsertDeathTest) +{ + ::testing::FLAGS_gtest_death_test_style = "threadsafe"; + WTF::TinyLRUCache cache; + cache.insert(1, 100); + auto result = cache.findIfCached(1); + ASSERT_TRUE(result); + cache.insert(2, 200); + ASSERT_DEATH_IF_SUPPORTED(*result, ""); +} + +TEST(WTF_TinyLRUCacheDeathTest, UseAfterClearDeathTest) +{ + ::testing::FLAGS_gtest_death_test_style = "threadsafe"; + WTF::TinyLRUCache cache; + cache.insert(1, 100); + auto result = cache.findIfCached(1); + ASSERT_TRUE(result); + cache.clear(); + ASSERT_DEATH_IF_SUPPORTED(*result, ""); +} + +TEST(WTF_TinyLRUCacheDeathTest, UseAfterSubsequentFindDeathTest) +{ + ::testing::FLAGS_gtest_death_test_style = "threadsafe"; + WTF::TinyLRUCache cache; + cache.insert(1, 100); + cache.insert(2, 200); + auto result = cache.findIfCached(2); + ASSERT_TRUE(result); + cache.findIfCached(1); + ASSERT_DEATH_IF_SUPPORTED(*result, ""); +} + +} // namespace TestWebKitAPI From 5044a542fff2e6738265591c28b44eedf72c6bd8 Mon Sep 17 00:00:00 2001 From: Charlie Wolfe Date: Tue, 30 Jun 2026 07:47:17 -0700 Subject: [PATCH 35/84] Validate several ITP and storage access IPC messages https://bugs.webkit.org/show_bug.cgi?id=312798 rdar://174708437 Reviewed by Matthew Finkel. ResourceLoadStatisticsUpdated, LogUserInteraction, and RequestStorageAccessUnderOpener accept WebContent-supplied data with no validation. A WCP can forge storageAccessUnderTopFrameDomains and isPrevalentResource in the ITP database, then obtain cross-origin cookie access without a user prompt. Verify that ResourceLoadStatisticsUpdated only contains fields the WebContent process legitimately observes, and that LogUserInteraction and RequestStorageAccessUnderOpener are called with domains the process owns. Test: ipc/forged-resource-load-statistics-storage-access.html * LayoutTests/ipc/forged-resource-load-statistics-storage-access-expected.txt: Added. * LayoutTests/ipc/forged-resource-load-statistics-storage-access.html: Added. * Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp: (WebKit::NetworkConnectionToWebProcess::logUserInteraction): (WebKit::resourceLoadStatisticsContainsOnlyObservableFields): (WebKit::NetworkConnectionToWebProcess::resourceLoadStatisticsUpdated): (WebKit::NetworkConnectionToWebProcess::requestStorageAccessUnderOpener): Originally-landed-as: 305413.716@safari-7624-branch (9d8f969c538a). rdar://180428563 Canonical link: https://commits.webkit.org/316149@main --- ...oad-statistics-storage-access-expected.txt | 4 + ...source-load-statistics-storage-access.html | 110 ++++++++++++++++++ .../NetworkConnectionToWebProcess.cpp | 21 ++++ 3 files changed, 135 insertions(+) create mode 100644 LayoutTests/ipc/forged-resource-load-statistics-storage-access-expected.txt create mode 100644 LayoutTests/ipc/forged-resource-load-statistics-storage-access.html diff --git a/LayoutTests/ipc/forged-resource-load-statistics-storage-access-expected.txt b/LayoutTests/ipc/forged-resource-load-statistics-storage-access-expected.txt new file mode 100644 index 000000000000..dfa02d6ca0b0 --- /dev/null +++ b/LayoutTests/ipc/forged-resource-load-statistics-storage-access-expected.txt @@ -0,0 +1,4 @@ +Test that a WebContent process cannot forge a persistent storage-access grant by sending storageAccessUnderTopFrameDomains via NetworkConnectionToWebProcess::ResourceLoadStatisticsUpdated. + +PASS: no storage-access grant was created from forged statistics + diff --git a/LayoutTests/ipc/forged-resource-load-statistics-storage-access.html b/LayoutTests/ipc/forged-resource-load-statistics-storage-access.html new file mode 100644 index 000000000000..1f5fdad59173 --- /dev/null +++ b/LayoutTests/ipc/forged-resource-load-statistics-storage-access.html @@ -0,0 +1,110 @@ + + + +

Test that a WebContent process cannot forge a persistent storage-access grant by sending +storageAccessUnderTopFrameDomains via NetworkConnectionToWebProcess::ResourceLoadStatisticsUpdated.

+

+
+
+
diff --git a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp
index 981445834cec..7c8ab2ac6e19 100644
--- a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp
+++ b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp
@@ -1341,14 +1341,32 @@ void NetworkConnectionToWebProcess::removeStorageAccessForFrame(FrameIdentifier
 
 void NetworkConnectionToWebProcess::logUserInteraction(RegistrableDomain&& domain)
 {
+    MESSAGE_CHECK(m_networkProcess->allowsFirstPartyForCookies(m_webProcessIdentifier, domain) == NetworkProcess::AllowCookieAccess::Allow);
+
     if (CheckedPtr networkSession = this->networkSession()) {
         if (RefPtr resourceLoadStatistics = networkSession->resourceLoadStatistics())
             resourceLoadStatistics->logUserInteraction(WTF::move(domain), [] { });
     }
 }
 
+// Validate that the WebContent process is not setting fields it has no authority over. The WCP-side ResourceLoadObserver
+// never populates these; only the network grocess grant/classification paths should write them.
+static bool resourceLoadStatisticsContainsOnlyObservableFields(const ResourceLoadStatistics& statistics)
+{
+    return statistics.storageAccessUnderTopFrameDomains.isEmpty()
+        && !statistics.grandfathered
+        && !statistics.isPrevalentResource
+        && !statistics.isVeryPrevalentResource
+        && !statistics.dataRecordsRemoved
+        && !statistics.timesAccessedAsFirstPartyDueToUserInteraction
+        && !statistics.timesAccessedAsFirstPartyDueToStorageAccessAPI;
+}
+
 void NetworkConnectionToWebProcess::resourceLoadStatisticsUpdated(Vector&& statistics, CompletionHandler&& completionHandler)
 {
+    for (auto& statistic : statistics)
+        MESSAGE_CHECK_COMPLETION(resourceLoadStatisticsContainsOnlyObservableFields(statistic), completionHandler());
+
     if (CheckedPtr networkSession = this->networkSession()) {
         if (networkSession->sessionID().isEphemeral()) {
             completionHandler();
@@ -1436,6 +1454,9 @@ void NetworkConnectionToWebProcess::storageAccessQuirkForTopFrameDomain(URL&& to
 
 void NetworkConnectionToWebProcess::requestStorageAccessUnderOpener(WebCore::RegistrableDomain&& domainInNeedOfStorageAccess, PageIdentifier openerPageID, WebCore::RegistrableDomain&& openerDomain)
 {
+    MESSAGE_CHECK(m_networkProcess->allowsFirstPartyForCookies(m_webProcessIdentifier, domainInNeedOfStorageAccess) == NetworkProcess::AllowCookieAccess::Allow);
+    MESSAGE_CHECK(m_networkProcess->allowsFirstPartyForCookies(m_webProcessIdentifier, openerDomain) == NetworkProcess::AllowCookieAccess::Allow);
+
     if (CheckedPtr networkSession = this->networkSession()) {
         if (RefPtr resourceLoadStatistics = networkSession->resourceLoadStatistics())
             resourceLoadStatistics->requestStorageAccessUnderOpener(WTF::move(domainInNeedOfStorageAccess), openerPageID, WTF::move(openerDomain));

From 6ea1f0a2bf517e6e086f8b9e28a6f52e003c07d0 Mon Sep 17 00:00:00 2001
From: Chris Dumez 
Date: Tue, 30 Jun 2026 07:50:17 -0700
Subject: [PATCH 36/84] [WebCore][bindings] Empty JSValue returned to script
 from callPromisePairFunction when argument conversion throws
 https://bugs.webkit.org/show_bug.cgi?id=313618 rdar://175673155
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Reviewed by Ryosuke Niwa.

callPromisePairFunction stored the functor's EncodedJSValue and returned it
after passing the catch scope through rejectPromiseWithExceptionIfAny. When the
generated operation body threw during IDL argument conversion, it returned
encodedJSValue() (the empty JSValue sentinel) with a pending exception. The
first rejectPromiseWithExceptionIfAny call cleared that exception to reject the
first promise, so the second call was a no-op — leaving the second promise
unrejected — and RETURN_IF_EXCEPTION did not fire. The empty sentinel was then
returned to JavaScript as a script-visible value.

Fix by:
  - Introducing rejectPromisesWithExceptionIfAny, which saves the error value
    before clearing the exception and rejects both promises. Shared
    takeNonTerminationException helper factors out the check-save-clear logic
    from rejectPromiseWithExceptionIfAny.
  - Adding a DictionaryType template parameter to callPromisePairFunction. When
    the functor returns an empty JSValue (the error-path sentinel),
    callPromisePairFunction reconstructs a valid result dictionary from the
    rejected promises via convertDictionaryToJS.
  - Threading the concrete dictionary type (e.g. Navigation::Result) from the
    code generator through callReturningPromisePair to callPromisePairFunction.

Test: navigation-api/navigation-navigate-throwing-argument-conversion.html

* LayoutTests/navigation-api/navigation-navigate-throwing-argument-conversion-expected.txt: Added.
* LayoutTests/navigation-api/navigation-navigate-throwing-argument-conversion.html: Added.
* Source/WebCore/bindings/js/JSDOMOperationReturningPromise.h:
(WebCore::IDLOperationReturningPromise::callReturningPromisePair):
* Source/WebCore/bindings/js/JSDOMPromiseDeferred.cpp:
(WebCore::takeNonTerminationException):
(WebCore::rejectPromiseWithExceptionIfAny):
(WebCore::rejectPromisesWithExceptionIfAny):
* Source/WebCore/bindings/js/JSDOMPromiseDeferred.h:
(WebCore::callPromisePairFunction):
* Source/WebCore/bindings/scripts/CodeGeneratorJS.pm:
(GenerateOperationTrampolineDefinition):
* Source/WebCore/bindings/scripts/test/JS/JSTestObj.cpp:
(WebCore::JSC_DEFINE_HOST_FUNCTION):

Originally-landed-as: 305413.776@safari-7624-branch (332eff8c4870). rdar://180436895
Canonical link: https://commits.webkit.org/316150@main
---
 ...-throwing-argument-conversion-expected.txt | 16 ++++++++
 ...navigate-throwing-argument-conversion.html | 39 +++++++++++++++++++
 .../js/JSDOMOperationReturningPromise.h       |  4 +-
 .../bindings/js/JSDOMPromiseDeferred.cpp      | 22 ++++++++---
 .../bindings/js/JSDOMPromiseDeferred.h        | 22 ++++++++---
 .../bindings/scripts/CodeGeneratorJS.pm       |  5 +++
 .../bindings/scripts/test/JS/JSTestObj.cpp    |  3 +-
 7 files changed, 97 insertions(+), 14 deletions(-)
 create mode 100644 LayoutTests/navigation-api/navigation-navigate-throwing-argument-conversion-expected.txt
 create mode 100644 LayoutTests/navigation-api/navigation-navigate-throwing-argument-conversion.html

diff --git a/LayoutTests/navigation-api/navigation-navigate-throwing-argument-conversion-expected.txt b/LayoutTests/navigation-api/navigation-navigate-throwing-argument-conversion-expected.txt
new file mode 100644
index 000000000000..6705aa6c2453
--- /dev/null
+++ b/LayoutTests/navigation-api/navigation-navigate-throwing-argument-conversion-expected.txt
@@ -0,0 +1,16 @@
+Tests that navigation.navigate() returns a valid NavigationResult when argument conversion throws
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS typeof result is 'object'
+PASS 'committed' in result is true
+PASS 'finished' in result is true
+PASS result.committed instanceof Promise is true
+PASS result.finished instanceof Promise is true
+PASS committedError.message is "test error"
+PASS finishedError.message is "test error"
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/navigation-api/navigation-navigate-throwing-argument-conversion.html b/LayoutTests/navigation-api/navigation-navigate-throwing-argument-conversion.html
new file mode 100644
index 000000000000..5e60aa03bfce
--- /dev/null
+++ b/LayoutTests/navigation-api/navigation-navigate-throwing-argument-conversion.html
@@ -0,0 +1,39 @@
+
+
+
diff --git a/Source/WebCore/bindings/js/JSDOMOperationReturningPromise.h b/Source/WebCore/bindings/js/JSDOMOperationReturningPromise.h
index 78c4c04cfed0..ec257c3f3264 100644
--- a/Source/WebCore/bindings/js/JSDOMOperationReturningPromise.h
+++ b/Source/WebCore/bindings/js/JSDOMOperationReturningPromise.h
@@ -56,10 +56,10 @@ class IDLOperationReturningPromise {
     }
 
     using Operation2 = JSC::EncodedJSValue(JSC::JSGlobalObject*, JSC::CallFrame*, ClassParameter, Ref&&, Ref&&);
-    template
+    template
     static JSC::EncodedJSValue callReturningPromisePair(JSC::JSGlobalObject& lexicalGlobalObject, JSC::CallFrame& callFrame, const char* operationName)
     {
-        return callPromisePairFunction(lexicalGlobalObject, callFrame, [&operationName] (JSC::JSGlobalObject& lexicalGlobalObject, JSC::CallFrame& callFrame, Ref&& promise, Ref&& promise2) {
+        return callPromisePairFunction(lexicalGlobalObject, callFrame, [&operationName] (JSC::JSGlobalObject& lexicalGlobalObject, JSC::CallFrame& callFrame, Ref&& promise, Ref&& promise2) {
             auto* thisObject = IDLOperation::cast(lexicalGlobalObject, callFrame);
             if constexpr (shouldThrow != CastedThisErrorBehavior::Assert) {
                 if (!thisObject) [[unlikely]]
diff --git a/Source/WebCore/bindings/js/JSDOMPromiseDeferred.cpp b/Source/WebCore/bindings/js/JSDOMPromiseDeferred.cpp
index d35cecd44b73..04a0e024c3c4 100644
--- a/Source/WebCore/bindings/js/JSDOMPromiseDeferred.cpp
+++ b/Source/WebCore/bindings/js/JSDOMPromiseDeferred.cpp
@@ -250,18 +250,30 @@ void DeferredPromise::reject(ExceptionCode ec, const String& message, RejectAsHa
         handleUncaughtException(scope, lexicalGlobalObject);
 }
 
-void rejectPromiseWithExceptionIfAny(JSC::JSGlobalObject& lexicalGlobalObject, JSDOMGlobalObject& globalObject, JSPromise& promise, JSC::TopExceptionScope& catchScope)
+static JSValue takeNonTerminationException(JSC::TopExceptionScope& catchScope)
 {
-    UNUSED_PARAM(lexicalGlobalObject);
     if (!catchScope.exception()) [[likely]]
-        return;
+        return { };
     if (catchScope.vm().hasPendingTerminationException())
-        return;
+        return { };
 
     JSValue error = catchScope.exception()->value();
     catchScope.clearException();
+    return error;
+}
+
+void rejectPromiseWithExceptionIfAny(JSC::JSGlobalObject&, JSDOMGlobalObject& globalObject, JSPromise& promise, JSC::TopExceptionScope& catchScope)
+{
+    if (auto error = takeNonTerminationException(catchScope)) [[unlikely]]
+        DeferredPromise::create(globalObject, promise)->reject(error);
+}
 
-    DeferredPromise::create(globalObject, promise)->reject(error);
+void rejectPromisesWithExceptionIfAny(JSC::JSGlobalObject&, JSDOMGlobalObject& globalObject, JSPromise& promise1, JSPromise& promise2, JSC::TopExceptionScope& catchScope)
+{
+    if (auto error = takeNonTerminationException(catchScope)) [[unlikely]] {
+        DeferredPromise::create(globalObject, promise1)->reject(error);
+        DeferredPromise::create(globalObject, promise2)->reject(error);
+    }
 }
 
 JSC::EncodedJSValue createRejectedPromiseWithTypeError(JSC::JSGlobalObject& lexicalGlobalObject, const String& errorMessage, RejectedPromiseWithTypeErrorCause cause)
diff --git a/Source/WebCore/bindings/js/JSDOMPromiseDeferred.h b/Source/WebCore/bindings/js/JSDOMPromiseDeferred.h
index 69ac0663fad3..d4b41e5aa3c4 100644
--- a/Source/WebCore/bindings/js/JSDOMPromiseDeferred.h
+++ b/Source/WebCore/bindings/js/JSDOMPromiseDeferred.h
@@ -32,6 +32,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 
@@ -373,6 +374,7 @@ void fulfillPromiseWithArrayBufferFromSpan(Ref&&, std::span&&, Uint8Array*);
 void fulfillPromiseWithUint8ArrayFromSpan(Ref&&, std::span);
 WEBCORE_EXPORT void rejectPromiseWithExceptionIfAny(JSC::JSGlobalObject&, JSDOMGlobalObject&, JSC::JSPromise&, JSC::TopExceptionScope&);
+WEBCORE_EXPORT void rejectPromisesWithExceptionIfAny(JSC::JSGlobalObject&, JSDOMGlobalObject&, JSC::JSPromise&, JSC::JSPromise&, JSC::TopExceptionScope&);
 
 enum class RejectedPromiseWithTypeErrorCause { NativeGetter, InvalidThis };
 JSC::EncodedJSValue createRejectedPromiseWithTypeError(JSC::JSGlobalObject&, const String&, RejectedPromiseWithTypeErrorCause);
@@ -422,26 +424,34 @@ inline JSC::JSValue callPromiseFunction(JSC::JSGlobalObject& lexicalGlobalObject
 
 using PromisePairFunction = JSC::EncodedJSValue(JSC::JSGlobalObject&, JSC::CallFrame&, Ref&&, Ref&&);
 
-template
+template
 inline JSC::EncodedJSValue callPromisePairFunction(JSC::JSGlobalObject& lexicalGlobalObject, JSC::CallFrame& callFrame, PromisePairFunctor functor)
 {
     JSC::VM& vm = JSC::getVM(&lexicalGlobalObject);
     auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm);
 
     auto& globalObject = downcast(lexicalGlobalObject);
-    auto* promise = JSC::JSPromise::create(vm, globalObject.promiseStructure());
-    ASSERT(promise);
+    auto* promise1 = JSC::JSPromise::create(vm, globalObject.promiseStructure());
+    ASSERT(promise1);
     auto* promise2 = JSC::JSPromise::create(vm, globalObject.promiseStructure());
     ASSERT(promise2);
 
-    auto result = functor(globalObject, callFrame, DeferredPromise::create(globalObject, *promise, DeferredPromise::Mode::RetainPromiseOnResolve), DeferredPromise::create(globalObject, *promise2, DeferredPromise::Mode::RetainPromiseOnResolve));
+    auto result = functor(globalObject, callFrame, DeferredPromise::create(globalObject, *promise1, DeferredPromise::Mode::RetainPromiseOnResolve), DeferredPromise::create(globalObject, *promise2, DeferredPromise::Mode::RetainPromiseOnResolve));
+
+    rejectPromisesWithExceptionIfAny(lexicalGlobalObject, globalObject, *promise1, *promise2, catchScope);
 
-    rejectPromiseWithExceptionIfAny(lexicalGlobalObject, globalObject, *promise, catchScope);
-    rejectPromiseWithExceptionIfAny(lexicalGlobalObject, globalObject, *promise2, catchScope);
     // FIXME: We could have error since any JS call can throw stack-overflow errors.
     // https://bugs.webkit.org/show_bug.cgi?id=203402
     RETURN_IF_EXCEPTION(catchScope, JSC::encodedJSValue());
 
+    // When the functor threw (e.g. during IDL argument conversion), its return
+    // value is an empty JSValue sentinel. Rebuild a valid result dictionary from
+    // the (now rejected) promises via convertDictionaryToJS.
+    if (!JSC::JSValue::decode(result)) [[unlikely]] {
+        result = JSC::JSValue::encode(convertDictionaryToJS(lexicalGlobalObject, globalObject, DictionaryType { DOMPromise::create(globalObject, *promise1), DOMPromise::create(globalObject, *promise2) }));
+        RETURN_IF_EXCEPTION(catchScope, JSC::encodedJSValue());
+    }
+
     return result;
 }
 
diff --git a/Source/WebCore/bindings/scripts/CodeGeneratorJS.pm b/Source/WebCore/bindings/scripts/CodeGeneratorJS.pm
index cc5689700415..a8f0269ae14c 100644
--- a/Source/WebCore/bindings/scripts/CodeGeneratorJS.pm
+++ b/Source/WebCore/bindings/scripts/CodeGeneratorJS.pm
@@ -6369,6 +6369,11 @@ sub GenerateOperationTrampolineDefinition
 
     my @callFunctionTemplateArguments = ();
     push(@callFunctionTemplateArguments, $functionBodyName);
+    if ($operation->extendedAttributes->{ReturnsPromisePair}) {
+        my $dictClassName = GetDictionaryClassName($operation->type, $interface);
+        push(@callFunctionTemplateArguments, $dictClassName);
+        AddToImplIncludes("JSDOMPromise.h", $operation->extendedAttributes->{Conditional});
+    }
     push(@callFunctionTemplateArguments, "CastedThisErrorBehavior::Assert") if ($operation->extendedAttributes->{PrivateIdentifier} and not $operation->extendedAttributes->{PublicIdentifier});
 
     push(@$outputArray, "JSC_DEFINE_HOST_FUNCTION(${functionName}, (JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame))\n");
diff --git a/Source/WebCore/bindings/scripts/test/JS/JSTestObj.cpp b/Source/WebCore/bindings/scripts/test/JS/JSTestObj.cpp
index a7d0cf0d81cc..b4acba80feee 100644
--- a/Source/WebCore/bindings/scripts/test/JS/JSTestObj.cpp
+++ b/Source/WebCore/bindings/scripts/test/JS/JSTestObj.cpp
@@ -65,6 +65,7 @@
 #include "JSDOMIterator.h"
 #include "JSDOMOperation.h"
 #include "JSDOMOperationReturningPromise.h"
+#include "JSDOMPromise.h"
 #include "JSDOMStringList.h"
 #include "JSDOMWindowBase.h"
 #include "JSDOMWrapperCache.h"
@@ -7824,7 +7825,7 @@ static inline JSC::EncodedJSValue jsTestObjPrototypeFunction_returnsPromisePairB
 
 JSC_DEFINE_HOST_FUNCTION(jsTestObjPrototypeFunction_returnsPromisePair, (JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame))
 {
-    return IDLOperationReturningPromise::callReturningPromisePair(*lexicalGlobalObject, *callFrame, "returnsPromisePair");
+    return IDLOperationReturningPromise::callReturningPromisePair(*lexicalGlobalObject, *callFrame, "returnsPromisePair");
 }
 
 #if ENABLE(TEST_FEATURE)

From a93904b9771d104ee1230f7d2ce6e1024b030657 Mon Sep 17 00:00:00 2001
From: Said Abou-Hallawa 
Date: Tue, 30 Jun 2026 08:11:10 -0700
Subject: [PATCH 37/84] =?UTF-8?q?Use-after-free=20in=20SVGGeometryElement:?=
 =?UTF-8?q?:getPointAtLength=20=E2=80=94=20raw=20renderer=20pointer=20held?=
 =?UTF-8?q?=20across=20nested=20updateLayout=20https://bugs.webkit.org/sho?=
 =?UTF-8?q?w=5Fbug.cgi=3Fid=3D314326=20rdar://175670940?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Reviewed by Simon Fraser.

Between the call to `updateLayoutIgnorePendingStylesheets()` and the call to
`SVGGeometryElement::getTotalLength()`, `SVGGeometryElement::getPointAtLength()`
caches its renderer() in a raw pointer. The problem is `getTotalLength()` also
calls `updateLayoutIgnorePendingStylesheets()` which may delete the renderer.
This will make `getPointAtLength()` use-after-free the cached raw pointer renderer.

Rearrange the code such that `updateLayoutIgnorePendingStylesheets()` is called
only once per `getPointAtLength()` and read `this->renderer()` exactly before it
is referenced.

* Source/WebCore/svg/SVGGeometryElement.cpp:
(WebCore::SVGGeometryElement::calculateTotalLength const):
(WebCore::SVGGeometryElement::getTotalLength const):
(WebCore::SVGGeometryElement::calculatePointAtLength const):
(WebCore::SVGGeometryElement::getPointAtLength const):
* Source/WebCore/svg/SVGGeometryElement.h:

Originally-landed-as: 305413.857@safari-7624-branch (d1e59347508c). rdar://180437982
Canonical link: https://commits.webkit.org/316151@main
---
 Source/WebCore/svg/SVGGeometryElement.cpp | 35 ++++++++++++++---------
 Source/WebCore/svg/SVGGeometryElement.h   |  3 ++
 2 files changed, 25 insertions(+), 13 deletions(-)

diff --git a/Source/WebCore/svg/SVGGeometryElement.cpp b/Source/WebCore/svg/SVGGeometryElement.cpp
index 385f70c215a9..13c44665f002 100644
--- a/Source/WebCore/svg/SVGGeometryElement.cpp
+++ b/Source/WebCore/svg/SVGGeometryElement.cpp
@@ -49,47 +49,56 @@ SVGGeometryElement::SVGGeometryElement(const QualifiedName& tagName, Document& d
     }
 }
 
-float SVGGeometryElement::getTotalLength() const
+float SVGGeometryElement::calculateTotalLength() const
 {
-    protect(document())->updateLayoutIgnorePendingStylesheets({ LayoutOptions::TreatContentVisibilityHiddenAsVisible, LayoutOptions::TreatContentVisibilityAutoAsVisible }, this);
-
-    auto* renderer = this->renderer();
+    CheckedPtr renderer = this->renderer();
     if (!renderer)
         return 0;
 
-    if (CheckedPtr renderSVGShape = dynamicDowncast(renderer))
+    if (CheckedPtr renderSVGShape = dynamicDowncast(*renderer))
         return renderSVGShape->getTotalLength();
 
-    if (CheckedPtr renderSVGShape = dynamicDowncast(renderer))
+    if (CheckedPtr renderSVGShape = dynamicDowncast(*renderer))
         return renderSVGShape->getTotalLength();
 
     ASSERT_NOT_REACHED();
     return 0;
 }
 
-ExceptionOr> SVGGeometryElement::getPointAtLength(float distance) const
+float SVGGeometryElement::getTotalLength() const
 {
     protect(document())->updateLayoutIgnorePendingStylesheets({ LayoutOptions::TreatContentVisibilityHiddenAsVisible, LayoutOptions::TreatContentVisibilityAutoAsVisible }, this);
+    return calculateTotalLength();
+}
 
-    auto* renderer = this->renderer();
+ExceptionOr> SVGGeometryElement::calculatePointAtLength(float distance) const
+{
+    CheckedPtr renderer = this->renderer();
     // Spec: If current element is a non-rendered element, throw an InvalidStateError.
     if (!renderer)
         return Exception { ExceptionCode::InvalidStateError };
 
-    // Spec: Clamp distance to [0, length].
-    distance = clampTo(distance, 0, getTotalLength());
-
     // Spec: Return a newly created, detached SVGPoint object.
-    if (CheckedPtr renderSVGShape = dynamicDowncast(renderer))
+    if (CheckedPtr renderSVGShape = dynamicDowncast(*renderer))
         return SVGPoint::create(renderSVGShape->getPointAtLength(distance));
 
-    if (CheckedPtr renderSVGShape = dynamicDowncast(renderer))
+    if (CheckedPtr renderSVGShape = dynamicDowncast(*renderer))
         return SVGPoint::create(renderSVGShape->getPointAtLength(distance));
 
     ASSERT_NOT_REACHED();
     return Exception { ExceptionCode::InvalidStateError };
 }
 
+ExceptionOr> SVGGeometryElement::getPointAtLength(float distance) const
+{
+    protect(document())->updateLayoutIgnorePendingStylesheets({ LayoutOptions::TreatContentVisibilityHiddenAsVisible, LayoutOptions::TreatContentVisibilityAutoAsVisible }, this);
+
+    // Spec: Clamp distance to [0, length].
+    distance = clampTo(distance, 0, calculateTotalLength());
+
+    return calculatePointAtLength(distance);
+}
+
 bool SVGGeometryElement::isPointInFill(DOMPointInit&& pointInit)
 {
     protect(document())->updateLayoutIgnorePendingStylesheets({ LayoutOptions::TreatContentVisibilityHiddenAsVisible, LayoutOptions::TreatContentVisibilityAutoAsVisible }, this);
diff --git a/Source/WebCore/svg/SVGGeometryElement.h b/Source/WebCore/svg/SVGGeometryElement.h
index dd21c40964a3..41847b9318df 100644
--- a/Source/WebCore/svg/SVGGeometryElement.h
+++ b/Source/WebCore/svg/SVGGeometryElement.h
@@ -56,6 +56,9 @@ class SVGGeometryElement : public SVGGraphicsElement {
 private:
     bool isSVGGeometryElement() const override { return true; }
 
+    float calculateTotalLength() const;
+    ExceptionOr> calculatePointAtLength(float distance) const;
+
     const Ref m_pathLength { SVGAnimatedNumber::create(this) };
 };
 

From 736ce62a5254f322562cf1ee8f4e12c98a913029 Mon Sep 17 00:00:00 2001
From: Shu-yu Guo 
Date: Tue, 30 Jun 2026 08:20:18 -0700
Subject: [PATCH 38/84] [JSC] Disallow defining private names on WasmGC objects
 https://bugs.webkit.org/show_bug.cgi?id=314438 rdar://176445615

Reviewed by Yusuke Suzuki.

While the spec currently technically allows for defining private fields and
methods on WasmGC objects, this is against the spirit of those objects being
fixed-layout.

For compat, both V8 and SpiderMonkey also disallow addition of private names on
WasmGC objects.

Test: JSTests/wasm/gc/private-fields-and-methods.js

* JSTests/wasm/gc/private-fields-and-methods.js: Added.
(testPrivateMethodOnStruct.B):
(testPrivateMethodOnStruct.D.prototype.m):
(testPrivateMethodOnStruct.D):
(testPrivateMethodOnStruct):
(testPrivateFieldOnStruct.B):
(testPrivateFieldOnStruct.D):
(testPrivateFieldOnStruct):
(testPrivateMethodOnArray.B):
(testPrivateMethodOnArray.D.prototype.m):
(testPrivateMethodOnArray.D):
(testPrivateMethodOnArray):
(testPrivateFieldOnArray.B):
(testPrivateFieldOnArray.D):
(testPrivateFieldOnArray):
(testPrivateGetterOnStruct.B):
(testPrivateGetterOnStruct.D.prototype.get x):
(testPrivateGetterOnStruct.D):
(testPrivateGetterOnStruct):
(testGCSurvival.B):
(testGCSurvival.D.prototype.m):
(testGCSurvival.D):
(testGCSurvival):
* Source/JavaScriptCore/runtime/JSObjectInlines.h:
(JSC::JSObject::getPrivateField):
(JSC::JSObject::setPrivateField):
(JSC::JSObject::definePrivateField):
(JSC::JSObject::setPrivateBrand):

Originally-landed-as: 305413.881@safari-7624-branch (525600a227a6). rdar://180438063
Canonical link: https://commits.webkit.org/316152@main
---
 JSTests/wasm/gc/private-fields-and-methods.js | 170 ++++++++++++++++++
 .../JavaScriptCore/runtime/JSObjectInlines.h  |  19 +-
 2 files changed, 185 insertions(+), 4 deletions(-)
 create mode 100644 JSTests/wasm/gc/private-fields-and-methods.js

diff --git a/JSTests/wasm/gc/private-fields-and-methods.js b/JSTests/wasm/gc/private-fields-and-methods.js
new file mode 100644
index 000000000000..89c37c87e30f
--- /dev/null
+++ b/JSTests/wasm/gc/private-fields-and-methods.js
@@ -0,0 +1,170 @@
+import * as assert from "../assert.js";
+import { instantiate } from "./wast-wrapper.js";
+
+function testPrivateMethodOnStruct() {
+  let m = instantiate(`
+    (module
+      (type (struct (field i32)))
+      (func (export "make") (result anyref)
+        (struct.new 0 (i32.const 42)))
+      (func (export "use") (param (ref 0)) (result i32)
+        (struct.get 0 0 (local.get 0)))
+    )
+  `);
+  const s = m.exports.make();
+
+  class B { constructor() { return s; } }
+  class D extends B {
+    #m() {}
+    constructor() { super(); }
+  }
+
+  assert.throws(
+    () => new D(),
+    TypeError,
+    "Cannot add private method to a WebAssembly GC object"
+  );
+
+  // Struct must remain usable after the rejected attempt.
+  assert.eq(m.exports.use(s), 42);
+}
+
+function testPrivateFieldOnStruct() {
+  let m = instantiate(`
+    (module
+      (type (struct (field i32)))
+      (func (export "make") (result anyref)
+        (struct.new 0 (i32.const 42)))
+      (func (export "use") (param (ref 0)) (result i32)
+        (struct.get 0 0 (local.get 0)))
+    )
+  `);
+  const s = m.exports.make();
+
+  class B { constructor() { return s; } }
+  class D extends B {
+    #x = 1;
+    constructor() { super(); }
+  }
+
+  assert.throws(
+    () => new D(),
+    TypeError,
+    "Cannot define private field on a WebAssembly GC object"
+  );
+
+  assert.eq(m.exports.use(s), 42);
+}
+
+function testPrivateMethodOnArray() {
+  let m = instantiate(`
+    (module
+      (type (array i32))
+      (func (export "make") (result anyref)
+        (array.new 0 (i32.const 7) (i32.const 3)))
+      (func (export "use") (param (ref 0) i32) (result i32)
+        (array.get 0 (local.get 0) (local.get 1)))
+    )
+  `);
+  const a = m.exports.make();
+
+  class B { constructor() { return a; } }
+  class D extends B {
+    #m() {}
+    constructor() { super(); }
+  }
+
+  assert.throws(
+    () => new D(),
+    TypeError,
+    "Cannot add private method to a WebAssembly GC object"
+  );
+
+  assert.eq(m.exports.use(a, 0), 7);
+}
+
+function testPrivateFieldOnArray() {
+  let m = instantiate(`
+    (module
+      (type (array i32))
+      (func (export "make") (result anyref)
+        (array.new 0 (i32.const 7) (i32.const 3)))
+      (func (export "use") (param (ref 0) i32) (result i32)
+        (array.get 0 (local.get 0) (local.get 1)))
+    )
+  `);
+  const a = m.exports.make();
+
+  class B { constructor() { return a; } }
+  class D extends B {
+    #x = 1;
+    constructor() { super(); }
+  }
+
+  assert.throws(
+    () => new D(),
+    TypeError,
+    "Cannot define private field on a WebAssembly GC object"
+  );
+
+  assert.eq(m.exports.use(a, 0), 7);
+}
+
+function testPrivateGetterOnStruct() {
+  let m = instantiate(`
+    (module
+      (type (struct (field i32)))
+      (func (export "make") (result anyref)
+        (struct.new 0 (i32.const 42)))
+      (func (export "use") (param (ref 0)) (result i32)
+        (struct.get 0 0 (local.get 0)))
+    )
+  `);
+  const s = m.exports.make();
+
+  class B { constructor() { return s; } }
+  class D extends B {
+    get #x() { return 1; }
+    constructor() { super(); }
+  }
+
+  assert.throws(
+    () => new D(),
+    TypeError,
+    "Cannot add private method to a WebAssembly GC object"
+  );
+
+  assert.eq(m.exports.use(s), 42);
+}
+
+function testGCSurvival() {
+  let m = instantiate(`
+    (module
+      (type (struct (field i32)))
+      (func (export "make") (result anyref)
+        (struct.new 0 (i32.const 42)))
+      (func (export "use") (param (ref 0)) (result i32)
+        (struct.get 0 0 (local.get 0)))
+    )
+  `);
+  const s = m.exports.make();
+
+  class B { constructor() { return s; } }
+  class D extends B {
+    #m() {}
+    constructor() { super(); }
+  }
+
+  try { new D(); } catch {}
+
+  // The struct must survive GC with its structure intact.
+  gc();
+  assert.eq(m.exports.use(s), 42);
+}
+
+testPrivateMethodOnStruct();
+testPrivateFieldOnStruct();
+testPrivateMethodOnArray();
+testPrivateFieldOnArray();
+testPrivateGetterOnStruct();
+testGCSurvival();
diff --git a/Source/JavaScriptCore/runtime/JSObjectInlines.h b/Source/JavaScriptCore/runtime/JSObjectInlines.h
index 7c62e655f023..08e1003ab491 100644
--- a/Source/JavaScriptCore/runtime/JSObjectInlines.h
+++ b/Source/JavaScriptCore/runtime/JSObjectInlines.h
@@ -919,7 +919,7 @@ inline bool JSObject::getPrivateField(JSGlobalObject* globalObject, PropertyName
     ASSERT(!slot.isVMInquiry());
     if (!JSObject::getPrivateFieldSlot(this, globalObject, propertyName, slot)) {
         throwException(globalObject, scope, createInvalidPrivateNameError(globalObject));
-        RELEASE_AND_RETURN(scope, false);
+        return false;
     }
     EXCEPTION_ASSERT(!scope.exception());
     RELEASE_AND_RETURN(scope, true);
@@ -932,7 +932,7 @@ inline void JSObject::setPrivateField(JSGlobalObject* globalObject, PropertyName
     PropertySlot slot(this, PropertySlot::InternalMethodType::HasProperty);
     if (!JSObject::getPrivateFieldSlot(this, globalObject, propertyName, slot)) {
         throwException(globalObject, scope, createInvalidPrivateNameError(globalObject));
-        RELEASE_AND_RETURN(scope, void());
+        return;
     }
     EXCEPTION_ASSERT(!scope.exception());
 
@@ -944,10 +944,16 @@ inline void JSObject::definePrivateField(JSGlobalObject* globalObject, PropertyN
 {
     VM& vm = getVM(globalObject);
     auto scope = DECLARE_THROW_SCOPE(vm);
+
+    if (type() == WebAssemblyGCObjectType) {
+        throwTypeError(globalObject, scope, "Cannot define private field on a WebAssembly GC object"_s);
+        return;
+    }
+
     PropertySlot slot(this, PropertySlot::InternalMethodType::HasProperty);
     if (JSObject::getPrivateFieldSlot(this, globalObject, propertyName, slot)) {
         throwException(globalObject, scope, createRedefinedPrivateNameError(globalObject));
-        RELEASE_AND_RETURN(scope, void());
+        return;
     }
     EXCEPTION_ASSERT(!scope.exception());
 
@@ -1008,10 +1014,15 @@ inline void JSObject::setPrivateBrand(JSGlobalObject* globalObject, JSValue bran
     Structure* structure = this->structure();
     if (structure->isBrandedStructure() && uncheckedDowncast(structure)->checkBrand(asSymbol(brand))) {
         throwException(globalObject, scope, createReinstallPrivateMethodError(globalObject));
-        RELEASE_AND_RETURN(scope, void());
+        return;
     }
     EXCEPTION_ASSERT(!scope.exception());
 
+    if (type() == WebAssemblyGCObjectType) {
+        throwTypeError(globalObject, scope, "Cannot add private method to a WebAssembly GC object"_s);
+        return;
+    }
+
     scope.release();
 
     DeferredStructureTransitionWatchpointFire deferredWatchpointFire(vm, structure);

From d3cffddf149e0bcfbf135ddd233c66cf0caf4f97 Mon Sep 17 00:00:00 2001
From: Said Abou-Hallawa 
Date: Tue, 30 Jun 2026 08:44:15 -0700
Subject: [PATCH 39/84] Uninitialized result of FEGaussianBlur if the input
 isAlphaImage https://bugs.webkit.org/show_bug.cgi?id=314345 rdar://175674593

Reviewed by Simon Fraser.

`FEGaussianBlurSoftwareApplier::apply()` creates uninitialized `tempBuffer`
through `createScratchPixelBuffer()`.

-- If the stdDeviation of feGaussianBlur is asymmetric, e.g. "5 0", then applying
   this filter falls back to `boxBlurUnaccelerated()`.
-- If the input of feGaussianBlur isAlphaImage, e.g. feColorMatrix(luminanceToAlpha),
   then `boxBlur()` short-circuits to `boxBlurAlphaOnly()`, which writes only
   the alpha channel of each pixel.
-- If one of the stdDeviation is zero, e.g. "5 0", `boxBlurUnaccelerated()` will
   copy the `tempBuffer` to the `destinationPixelBuffer` with three uninitialized
   channels before it returns.

If isAlphaImage is true Make sure `tempBuffer` is zero-filled.

* Source/WebCore/platform/graphics/filters/software/FEGaussianBlurSoftwareApplier.cpp:
(WebCore::FEGaussianBlurSoftwareApplier::boxBlurGeneric):

Originally-landed-as: 305413.858@safari-7624-branch (96b0a368415e). rdar://180437556
Canonical link: https://commits.webkit.org/316153@main
---
 .../filters/software/FEGaussianBlurSoftwareApplier.cpp       | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/Source/WebCore/platform/graphics/filters/software/FEGaussianBlurSoftwareApplier.cpp b/Source/WebCore/platform/graphics/filters/software/FEGaussianBlurSoftwareApplier.cpp
index 10df4e6adcbb..dfca48c32320 100644
--- a/Source/WebCore/platform/graphics/filters/software/FEGaussianBlurSoftwareApplier.cpp
+++ b/Source/WebCore/platform/graphics/filters/software/FEGaussianBlurSoftwareApplier.cpp
@@ -345,6 +345,11 @@ inline void FEGaussianBlurSoftwareApplier::boxBlurGeneric(PixelBuffer& ioBuffer,
     }
 #endif
 
+    // boxBlurAlphaOnly() fills the alpha channel only. Make
+    // sure the other channels are initialized in this case.
+    if (isAlphaImage)
+        tempBuffer.zeroFill();
+
     boxBlurUnaccelerated(ioBuffer, tempBuffer, kernelSizeX, kernelSizeY, stride, paintSize, isAlphaImage, edgeMode);
 }
 

From 8e5b8549ead676b61521dafa9a2dc1d159d38dd5 Mon Sep 17 00:00:00 2001
From: Abrar Rahman Protyasha 
Date: Tue, 30 Jun 2026 09:00:18 -0700
Subject: [PATCH 40/84] Sort TestWebKitAPI pbxproj file after 316044@main
 https://bugs.webkit.org/show_bug.cgi?id=318227 rdar://181029069

Reviewed by Lily Spiniolas.

In this patch, we let Xcode sort the entries in this project file after
the addition of AttributedStringFontCache.mm in 316044@main.

* Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:

Canonical link: https://commits.webkit.org/316154@main
---
 Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj b/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
index 4b74c83f074d..f992cbbb57a8 100644
--- a/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
+++ b/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
@@ -669,10 +669,10 @@
 				WebCore/CBORReaderTest.cpp,
 				WebCore/CBORValueTest.cpp,
 				WebCore/CBORWriterTest.cpp,
+				WebCore/cocoa/AttributedStringFontCache.mm,
 				WebCore/cocoa/AudioStreamDescriptionCocoa.mm,
 				WebCore/cocoa/AudioVideoRendererAVFObjCTests.mm,
 				WebCore/cocoa/AVFoundationSoftLinkTest.mm,
-				WebCore/cocoa/AttributedStringFontCache.mm,
 				WebCore/cocoa/BifurcatedGraphicsContextTestsCG.cpp,
 				WebCore/cocoa/CaptionPreferencesTests.mm,
 				WebCore/cocoa/CoreMediaUtilities.mm,

From ce030f744b02e378e10393b0edc00ef7c54df5cf Mon Sep 17 00:00:00 2001
From: Kimmo Kinnunen 
Date: Tue, 30 Jun 2026 09:23:40 -0700
Subject: [PATCH 41/84] WebGL: GraphicsContextGLANGLE PACK_* state is
 modifiable through pixelStorei https://bugs.webkit.org/show_bug.cgi?id=312566
 rdar://174740214

Reviewed by Dan Glastonbury.

Filter out pixelStorei parameters.

* Source/WebCore/platform/graphics/angle/GraphicsContextGLANGLE.cpp:
(WebCore::GraphicsContextGLANGLE::pixelStorei):

Originally-landed-as: 305413.710@safari-7624-branch (dd484347691b). rdar://180435316
Canonical link: https://commits.webkit.org/316155@main
---
 .../graphics/angle/GraphicsContextGLANGLE.cpp      | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/Source/WebCore/platform/graphics/angle/GraphicsContextGLANGLE.cpp b/Source/WebCore/platform/graphics/angle/GraphicsContextGLANGLE.cpp
index 14a725d37127..25fefe171fa8 100644
--- a/Source/WebCore/platform/graphics/angle/GraphicsContextGLANGLE.cpp
+++ b/Source/WebCore/platform/graphics/angle/GraphicsContextGLANGLE.cpp
@@ -1678,7 +1678,19 @@ void GraphicsContextGLANGLE::pixelStorei(GCGLenum pname, GCGLint param)
 {
     if (!makeContextCurrent())
         return;
-
+    switch (pname) {
+    case UNPACK_ALIGNMENT:
+    case UNPACK_ROW_LENGTH:
+    case UNPACK_IMAGE_HEIGHT:
+    case UNPACK_SKIP_PIXELS:
+    case UNPACK_SKIP_ROWS:
+    case UNPACK_SKIP_IMAGES:
+        break;
+    default:
+        // Should be never set, rather passed to the commands that need these.
+        addError(GCGLErrorCode::InvalidOperation);
+        return;
+    }
     GL_PixelStorei(pname, param);
 }
 

From 8bdea3050d839cea58c0c51722b524572780fd01 Mon Sep 17 00:00:00 2001
From: Ahmad Saleem 
Date: Tue, 30 Jun 2026 09:28:50 -0700
Subject: [PATCH 42/84] Marked-text highlight pseudo-element fill color is
 taken from the stroke color https://bugs.webkit.org/show_bug.cgi?id=318073
 rdar://180871490
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Reviewed by Antti Koivisto.

This patch aligns WebKit with Gecko / Firefox and Blink / Chromium.

computeStyleForPseudoElementStyle() set the text fill color from
usedStrokeColor() — a copy/paste from the strokeColor line below it. As a
result, marked text styled via a highlight pseudo-element (::highlight(),
::target-text, ::spelling-error, ::grammar-error) rendered its glyph fill
using the stroke color rather than the text/fill color, and ignored any
applied color filter.

Set fillColor from visitedDependentTextFillColorApplyingColorFilter(),
matching how the fill color is resolved in TextPaintStyle.cpp.

Tests: fast/css/highlight-pseudo-fill-applies-color-filter.html
       imported/w3c/web-platform-tests/css/css-pseudo/highlight-fill-color-ignores-stroke-color-ref.html
       imported/w3c/web-platform-tests/css/css-pseudo/highlight-fill-color-ignores-stroke-color.html

* LayoutTests/fast/css/highlight-pseudo-fill-applies-color-filter-expected.html: Added.
* LayoutTests/fast/css/highlight-pseudo-fill-applies-color-filter.html: Added.
* LayoutTests/imported/w3c/web-platform-tests/css/css-pseudo/highlight-fill-color-ignores-stroke-color-expected.html: Added.
* LayoutTests/imported/w3c/web-platform-tests/css/css-pseudo/highlight-fill-color-ignores-stroke-color-ref.html: Added.
* LayoutTests/imported/w3c/web-platform-tests/css/css-pseudo/highlight-fill-color-ignores-stroke-color.html: Added.
* Source/WebCore/rendering/StyledMarkedText.cpp:
(WebCore::computeStyleForPseudoElementStyle):

Canonical link: https://commits.webkit.org/316156@main
---
 ...do-fill-applies-color-filter-expected.html | 17 +++++++++++++
 ...ight-pseudo-fill-applies-color-filter.html | 17 +++++++++++++
 ...l-color-ignores-stroke-color-expected.html | 18 ++++++++++++++
 ...t-fill-color-ignores-stroke-color-ref.html | 18 ++++++++++++++
 ...light-fill-color-ignores-stroke-color.html | 24 +++++++++++++++++++
 Source/WebCore/rendering/StyledMarkedText.cpp |  2 +-
 6 files changed, 95 insertions(+), 1 deletion(-)
 create mode 100644 LayoutTests/fast/css/highlight-pseudo-fill-applies-color-filter-expected.html
 create mode 100644 LayoutTests/fast/css/highlight-pseudo-fill-applies-color-filter.html
 create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/css-pseudo/highlight-fill-color-ignores-stroke-color-expected.html
 create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/css-pseudo/highlight-fill-color-ignores-stroke-color-ref.html
 create mode 100644 LayoutTests/imported/w3c/web-platform-tests/css/css-pseudo/highlight-fill-color-ignores-stroke-color.html

diff --git a/LayoutTests/fast/css/highlight-pseudo-fill-applies-color-filter-expected.html b/LayoutTests/fast/css/highlight-pseudo-fill-applies-color-filter-expected.html
new file mode 100644
index 000000000000..c6e7d04568b5
--- /dev/null
+++ b/LayoutTests/fast/css/highlight-pseudo-fill-applies-color-filter-expected.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+    

PASS

+ + + diff --git a/LayoutTests/fast/css/highlight-pseudo-fill-applies-color-filter.html b/LayoutTests/fast/css/highlight-pseudo-fill-applies-color-filter.html new file mode 100644 index 000000000000..6136535bf7c1 --- /dev/null +++ b/LayoutTests/fast/css/highlight-pseudo-fill-applies-color-filter.html @@ -0,0 +1,17 @@ + + + + + + +

PASS

+ + + diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-pseudo/highlight-fill-color-ignores-stroke-color-expected.html b/LayoutTests/imported/w3c/web-platform-tests/css/css-pseudo/highlight-fill-color-ignores-stroke-color-expected.html new file mode 100644 index 000000000000..f2d20af47c8b --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-pseudo/highlight-fill-color-ignores-stroke-color-expected.html @@ -0,0 +1,18 @@ + + +CSS Pseudo-Elements Reference: highlight text fill color uses 'color', not 'stroke-color' + + + +

Test passes if the word below is green. +

PASS
+ diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-pseudo/highlight-fill-color-ignores-stroke-color-ref.html b/LayoutTests/imported/w3c/web-platform-tests/css/css-pseudo/highlight-fill-color-ignores-stroke-color-ref.html new file mode 100644 index 000000000000..f2d20af47c8b --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-pseudo/highlight-fill-color-ignores-stroke-color-ref.html @@ -0,0 +1,18 @@ + + +CSS Pseudo-Elements Reference: highlight text fill color uses 'color', not 'stroke-color' + + + +

Test passes if the word below is green. +

PASS
+ diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-pseudo/highlight-fill-color-ignores-stroke-color.html b/LayoutTests/imported/w3c/web-platform-tests/css/css-pseudo/highlight-fill-color-ignores-stroke-color.html new file mode 100644 index 000000000000..59367b367307 --- /dev/null +++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-pseudo/highlight-fill-color-ignores-stroke-color.html @@ -0,0 +1,24 @@ + + +CSS Pseudo-Elements Test: highlight text fill color uses 'color', not 'stroke-color' + + + + + + + +

Test passes if the word below is green. +

PASS
+ diff --git a/Source/WebCore/rendering/StyledMarkedText.cpp b/Source/WebCore/rendering/StyledMarkedText.cpp index 63d986798170..8569bc014ed8 100644 --- a/Source/WebCore/rendering/StyledMarkedText.cpp +++ b/Source/WebCore/rendering/StyledMarkedText.cpp @@ -42,7 +42,7 @@ static void computeStyleForPseudoElementStyle(StyledMarkedText::Style& style, co return; style.backgroundColor = pseudoElementStyle->visitedDependentBackgroundColorApplyingColorFilter(paintInfo.paintBehavior); - style.textStyles.fillColor = pseudoElementStyle->usedStrokeColor(); + style.textStyles.fillColor = pseudoElementStyle->visitedDependentTextFillColorApplyingColorFilter(paintInfo.paintBehavior); style.textStyles.strokeColor = pseudoElementStyle->usedStrokeColor(); style.textStyles.hasExplicitlySetFillColor = pseudoElementStyle->hasExplicitlySetColor(); From 0012e6118d87c33aaae3c342375566b32762093c Mon Sep 17 00:00:00 2001 From: Anand Srinivasan Date: Tue, 30 Jun 2026 10:01:46 -0700 Subject: [PATCH 43/84] Add size limit to Yarr generated code https://bugs.webkit.org/show_bug.cgi?id=314589 rdar://176137052 Reviewed by Yusuke Suzuki. Patterns with many sequential non-greedy quantified parenthesized groups (e.g. (?:a){0,2}? repeated thousands of times) cause O(N^2) code emission in saveParenContext/restoreParenContext, as each group saves/restores all frame slots for the entire pattern. This patch adds a code size limit in VM options above which the code bails out to the interpreter. Test: JSTests/stress/regexp-many-non-greedy-paren-groups.js * JSTests/stress/regexp-many-non-greedy-paren-groups.js: Added. (testLargeNonGreedyParens): * Source/JavaScriptCore/runtime/OptionsList.h: * Source/JavaScriptCore/yarr/YarrJIT.cpp: (JSC::Yarr::dumpCompileFailure): * Source/JavaScriptCore/yarr/YarrJIT.h: Originally-landed-as: 305413.923@safari-7624-branch (e6d449d59b50). rdar://180427748 Canonical link: https://commits.webkit.org/316157@main --- .../regexp-many-non-greedy-paren-groups.js | 25 ++++++++++++++++++ Source/JavaScriptCore/runtime/OptionsList.h | 1 + Source/JavaScriptCore/yarr/YarrJIT.cpp | 26 +++++++++++++++++-- Source/JavaScriptCore/yarr/YarrJIT.h | 1 + 4 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 JSTests/stress/regexp-many-non-greedy-paren-groups.js diff --git a/JSTests/stress/regexp-many-non-greedy-paren-groups.js b/JSTests/stress/regexp-many-non-greedy-paren-groups.js new file mode 100644 index 000000000000..bedfd82896b5 --- /dev/null +++ b/JSTests/stress/regexp-many-non-greedy-paren-groups.js @@ -0,0 +1,25 @@ +// Test that regular expressions with many sequential non-greedy quantified +// parenthesized groups produce correct results at various sizes. + +function testLargeNonGreedyParens(n) { + let s = '(?:a){0,2}?'.repeat(n); + + let r = new RegExp(s); + + let result = 'aaa'.match(r); + if (result === null) + throw new Error("Expected match for n=" + n); + if (result.index !== 0) + throw new Error("Expected index 0 for n=" + n + ", got " + result.index); + + let replaced = 'a'.replace(r, 'x'); + if (typeof replaced !== 'string') + throw new Error("replace failed for n=" + n); +} + +testLargeNonGreedyParens(10); +testLargeNonGreedyParens(100); +testLargeNonGreedyParens(1000); +testLargeNonGreedyParens(2000); +testLargeNonGreedyParens(4000); +testLargeNonGreedyParens(8193); diff --git a/Source/JavaScriptCore/runtime/OptionsList.h b/Source/JavaScriptCore/runtime/OptionsList.h index 6047d3894909..7a9e9c6eb367 100644 --- a/Source/JavaScriptCore/runtime/OptionsList.h +++ b/Source/JavaScriptCore/runtime/OptionsList.h @@ -335,6 +335,7 @@ bool hasCapacityToUseLargeGigacage(); v(Unsigned, maximumBinaryStringSwitchCaseLength, 50, Normal, nullptr) \ v(Unsigned, maximumBinaryStringSwitchTotalLength, 2000, Normal, nullptr) \ v(Unsigned, maximumRegExpTestInlineCodesize, 500, Normal, "Maximum code size in bytes for inlined RegExp.test JIT code."_s) \ + v(Unsigned, maximumRegExpJITCodeSize, 16 * MB, Normal, "Maximum generated code size in bytes for RegExp JIT compilation before falling back to the interpreter."_s) \ \ v(Unsigned, wasmInliningMaximumDepth, 7, Normal, "Maximum inlining depth to consider inlining a wasm function."_s) \ v(Unsigned, wasmInliningMaximumWasmCalleeSize, 500, Normal, "Maximum wasm size in bytes to consider inlining a wasm function."_s) \ diff --git a/Source/JavaScriptCore/yarr/YarrJIT.cpp b/Source/JavaScriptCore/yarr/YarrJIT.cpp index 749899cf2786..dccdd0c365a0 100644 --- a/Source/JavaScriptCore/yarr/YarrJIT.cpp +++ b/Source/JavaScriptCore/yarr/YarrJIT.cpp @@ -4525,7 +4525,7 @@ class YarrGenerator final : public YarrJITInfo { } ++opIndex; - } while (opIndex < m_ops.size()); + } while (opIndex < m_ops.size() && !hasExceededCodeSizeLimit()); termMatchTargets.takeLast(); } @@ -5332,7 +5332,18 @@ class YarrGenerator final : public YarrJITInfo { case YarrOpCode::MatchFailed: break; } - } while (opIndex); + } while (opIndex && !hasExceededCodeSizeLimit()); + } + + bool hasExceededCodeSizeLimit() + { + if (m_failureReason) + return true; + if (m_jit.m_assembler.buffer().codeSize() > Options::maximumRegExpJITCodeSize()) [[unlikely]] { + m_failureReason = JITFailureReason::GeneratedCodeSizeTooLarge; + return true; + } + return false; } // Compilation methods: @@ -6951,9 +6962,17 @@ class YarrGenerator final : public YarrJITInfo { generate(); if (m_disassembler) m_disassembler->setEndOfGenerate(m_jit.label()); + if (m_failureReason) { + codeBlock.setFallBackWithFailureReason(*m_failureReason); + return; + } backtrack(); if (m_disassembler) m_disassembler->setEndOfBacktrack(m_jit.label()); + if (m_failureReason) { + codeBlock.setFallBackWithFailureReason(*m_failureReason); + return; + } ptrdiff_t codeSize = MacroAssembler::differenceBetween(startOfMainCode, m_jit.label()); bool canInline = ([&] -> bool { @@ -7585,6 +7604,9 @@ static void dumpCompileFailure(JITFailureReason failure) case JITFailureReason::OffsetTooLarge: dataLog("Can't JIT because pattern exceeds string length limits\n"); break; + case JITFailureReason::GeneratedCodeSizeTooLarge: + dataLog("Can't JIT because generated code size exceeds limit\n"); + break; } } diff --git a/Source/JavaScriptCore/yarr/YarrJIT.h b/Source/JavaScriptCore/yarr/YarrJIT.h index 383263cc8cf4..16214096a057 100644 --- a/Source/JavaScriptCore/yarr/YarrJIT.h +++ b/Source/JavaScriptCore/yarr/YarrJIT.h @@ -63,6 +63,7 @@ enum class JITFailureReason : uint8_t { ParenthesisNestedTooDeep, ExecutableMemoryAllocationFailure, OffsetTooLarge, + GeneratedCodeSizeTooLarge, }; class BoyerMooreFastCandidates { From 3ab3db0b0feca78b2895ab53af90b90e46e44f21 Mon Sep 17 00:00:00 2001 From: Ling Ho Date: Tue, 30 Jun 2026 10:13:03 -0700 Subject: [PATCH 44/84] DOM XSS in committers-autocomplete.js https://bugs.webkit.org/show_bug.cgi?id=318161 rdar://180319278 Reviewed by Alexey Proskuryakov. Construct the autocomplete suggestion menu with createElement and behavior. * Websites/bugs.webkit.org/committers-autocomplete.js: (updateMenu.appendSpan): (updateMenu): Canonical link: https://commits.webkit.org/316158@main --- .../committers-autocomplete.js | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/Websites/bugs.webkit.org/committers-autocomplete.js b/Websites/bugs.webkit.org/committers-autocomplete.js index 79c6809fe78e..c17f6fe6a24f 100644 --- a/Websites/bugs.webkit.org/committers-autocomplete.js +++ b/Websites/bugs.webkit.org/committers-autocomplete.js @@ -157,18 +157,31 @@ WebKitCommitters = (function() { return; } - var html = []; + function appendSpan(parent, className, text) { + var span = document.createElement('span'); + span.className = className; + span.textContent = text; + parent.append(span, ' '); + } + + var menu = getMenu(); + menu.textContent = ''; for (var i = 0; i < contacts.length; i++) { var contact = contacts[i]; - html.push('
' + contact.name + ' <' + contact.emails[0] + '> '); + + var suggestion = document.createElement('div'); + suggestion.className = 'committer-suggestion'; + suggestion.setAttribute('email', contact.emails[0]); + + appendSpan(suggestion, 'committer-name', contact.name); + appendSpan(suggestion, 'committer-email', '<' + contact.emails[0] + '>'); if (contact.nicks) - html.push(' @' + contact.nicks.join(', @') + ''); + appendSpan(suggestion, 'committer-nick', '@' + contact.nicks.join(', @')); if (contact.type) - html.push(' ' + contact.type + ''); - html.push('
'); + appendSpan(suggestion, 'committer-type', contact.type); + + menu.appendChild(suggestion); } - getMenu().innerHTML = html.join(''); selectItem(0); showMenu(true); } From 0f857107ffab578c2c25cb74afd5c68e22fc3a33 Mon Sep 17 00:00:00 2001 From: Said Abou-Hallawa Date: Tue, 30 Jun 2026 10:20:23 -0700 Subject: [PATCH 45/84] GetPixelBuffer should zeroFill the destination if any error happens https://bugs.webkit.org/show_bug.cgi?id=312945 rdar://174640273 Reviewed by Kimmo Kinnunen. RemoteImageBuffer fails to GetPixelBuffer if the destination colorSpace is non-RGB model (e.g. kCGColorSpaceGenericCMYK). In this case the PixelBuffer is allocated but never be initialized. So stale heap can be exposed to callers. To fix this an IPC validator is needed to ensure the colorSpace is indeed RGB model. And to handle other possible unknown failures, the destination PixelBuffer will be zero-filled if vImage fails to do the conversion the colorSpace conversion. * Source/WebCore/platform/graphics/DestinationColorSpace.cpp: (WebCore::DestinationColorSpace::usesRGBColorModel const): * Source/WebCore/platform/graphics/DestinationColorSpace.h: * Source/WebCore/platform/graphics/PixelBufferConversion.cpp: (WebCore::convertImagePixelsAccelerated): * Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in: * Tools/TestWebKitAPI/Tests/WebCore/ImageBufferTests.cpp: (TestWebKitAPI::TEST(ImageBufferTests, GetPixelBufferAllZeros)): Originally-landed-as: 305413.874@safari-7624-branch (8e2784fd0807). rdar://180429216 * Source/WebKit/Shared/WebCoreArgumentCodersPlatform.serialization.in: Canonical link: https://commits.webkit.org/316159@main --- .../graphics/DestinationColorSpace.cpp | 10 +++++ .../platform/graphics/DestinationColorSpace.h | 1 + .../graphics/PixelBufferConversion.cpp | 19 +++++++++- .../WebCoreArgumentCoders.serialization.in | 2 - ...oreArgumentCodersPlatform.serialization.in | 4 +- .../Tests/WebCore/ImageBufferTests.cpp | 37 +++++++++++++++++++ 6 files changed, 68 insertions(+), 5 deletions(-) diff --git a/Source/WebCore/platform/graphics/DestinationColorSpace.cpp b/Source/WebCore/platform/graphics/DestinationColorSpace.cpp index c74fb6fe6dc5..f99b66ec4130 100644 --- a/Source/WebCore/platform/graphics/DestinationColorSpace.cpp +++ b/Source/WebCore/platform/graphics/DestinationColorSpace.cpp @@ -208,6 +208,16 @@ bool DestinationColorSpace::supportsOutput() const #endif } +bool DestinationColorSpace::usesRGBColorModel() const +{ +#if USE(CG) + // Avoid refing color space here as this is performance-sensitive. + SUPPRESS_UNRETAINED_ARG return CGColorSpaceGetModel(platformColorSpace()) == kCGColorSpaceModelRGB; +#else + return true; +#endif +} + bool DestinationColorSpace::usesExtendedRange() const { #if USE(CG) diff --git a/Source/WebCore/platform/graphics/DestinationColorSpace.h b/Source/WebCore/platform/graphics/DestinationColorSpace.h index d567d73f15b5..eb08a70b37c2 100644 --- a/Source/WebCore/platform/graphics/DestinationColorSpace.h +++ b/Source/WebCore/platform/graphics/DestinationColorSpace.h @@ -73,6 +73,7 @@ class DestinationColorSpace { WEBCORE_EXPORT bool supportsOutput() const; + WEBCORE_EXPORT bool usesRGBColorModel() const; WEBCORE_EXPORT bool usesExtendedRange() const; bool usesITUR_2100TF() const; diff --git a/Source/WebCore/platform/graphics/PixelBufferConversion.cpp b/Source/WebCore/platform/graphics/PixelBufferConversion.cpp index 6f8b43243b32..0ff1a6eba25e 100644 --- a/Source/WebCore/platform/graphics/PixelBufferConversion.cpp +++ b/Source/WebCore/platform/graphics/PixelBufferConversion.cpp @@ -113,6 +113,12 @@ static void convertImagePixelsAccelerated(const ConstPixelBufferConversionView& auto sourceVImageBuffer = makeVImageBuffer(source, destinationSize); auto destinationVImageBuffer = makeVImageBuffer(destination, destinationSize); + auto zeroFillDestination = [&] { + size_t rowFillBytes = static_cast(destinationSize.width()) * 4; + for (int y = 0; y < destinationSize.height(); ++y) + zeroSpan(destination.rows.subspan(static_cast(y) * destination.bytesPerRow, rowFillBytes)); + }; + if (source.format.colorSpace != destination.format.colorSpace) { // FIXME: Consider using vImageConvert_AnyToAny for all conversions, not just ones that need a color space conversion, // after judiciously performance testing them against each other. @@ -122,11 +128,20 @@ static void convertImagePixelsAccelerated(const ConstPixelBufferConversionView& vImage_Error converterCreateError = kvImageNoError; auto converter = adoptCF(vImageConverter_CreateWithCGImageFormat(&sourceCGImageFormat, &destinationCGImageFormat, nullptr, kvImageNoFlags, &converterCreateError)); - if (converterCreateError != kvImageNoError) + if (converterCreateError != kvImageNoError) { + RELEASE_LOG_ERROR(Images, "%s: vImageConverter_CreateWithCGImageFormat() failed with error: %zd", __FUNCTION__, converterCreateError); + // The destination may be uninitialized; ensure no stale heap is exposed to callers. + zeroFillDestination(); return; + } vImage_Error converterConvertError = vImageConvert_AnyToAny(converter.get(), &sourceVImageBuffer, &destinationVImageBuffer, nullptr, kvImageNoFlags); - ASSERT_WITH_MESSAGE_UNUSED(converterConvertError, converterConvertError == kvImageNoError, "vImageConvert_AnyToAny failed conversion with error: %zd", converterConvertError); + if (converterConvertError != kvImageNoError) { + RELEASE_LOG_ERROR(Images, "%s: vImageConvert_AnyToAny() failed with error: %zd", __FUNCTION__, converterConvertError); + // The destination may be uninitialized; ensure no stale heap is exposed to callers. + zeroFillDestination(); + } + return; } diff --git a/Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in b/Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in index b58ffa50d2b2..1bbb46028367 100644 --- a/Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in +++ b/Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in @@ -1066,8 +1066,6 @@ struct WebCore::ClientOrigin { enum class WebCore::AdjustViewSize : bool; -enum class WebCore::UseLosslessCompression : bool; - [RefCounted] class WebCore::TextIndicator { WebCore::TextIndicatorData data(); }; diff --git a/Source/WebKit/Shared/WebCoreArgumentCodersPlatform.serialization.in b/Source/WebKit/Shared/WebCoreArgumentCodersPlatform.serialization.in index d5bfa414bc44..64bde797ba98 100644 --- a/Source/WebKit/Shared/WebCoreArgumentCodersPlatform.serialization.in +++ b/Source/WebKit/Shared/WebCoreArgumentCodersPlatform.serialization.in @@ -651,6 +651,8 @@ enum class WebCore::PixelFormat : uint8_t { #endif }; +enum class WebCore::UseLosslessCompression : bool; + [AdditionalEncoder=StreamConnectionEncoder] struct WebCore::ImageBufferFormat { WebCore::PixelFormat pixelFormat; WebCore::UseLosslessCompression useLosslessCompression; @@ -659,7 +661,7 @@ enum class WebCore::PixelFormat : uint8_t { [AdditionalEncoder=StreamConnectionEncoder] struct WebCore::PixelBufferFormat { WebCore::AlphaPremultiplication alphaFormat; WebCore::PixelFormat pixelFormat; - WebCore::DestinationColorSpace colorSpace; + [Validator='colorSpace->usesRGBColorModel()'] WebCore::DestinationColorSpace colorSpace; }; #if USE(SOUP) diff --git a/Tools/TestWebKitAPI/Tests/WebCore/ImageBufferTests.cpp b/Tools/TestWebKitAPI/Tests/WebCore/ImageBufferTests.cpp index 5217363bd3a6..e92116a3a69b 100644 --- a/Tools/TestWebKitAPI/Tests/WebCore/ImageBufferTests.cpp +++ b/Tools/TestWebKitAPI/Tests/WebCore/ImageBufferTests.cpp @@ -326,4 +326,41 @@ INSTANTIATE_TEST_SUITE_P(ImageBufferTests, testing::Values(RenderingMode::Unaccelerated, RenderingMode::Accelerated)), TestParametersToStringFormatter()); +#if USE(CG) + +TEST(ImageBufferTests, GetPixelBufferAllZeros) +{ + auto sourceColorSpace = DestinationColorSpace::SRGB(); + auto sourcePixelFormat = PixelFormat::BGRA8; + FloatSize size { 1000, 1000 }; + FloatRect fillRect = FloatRect { { }, size }; + float scale = 1.f; + + RefPtr imageBuffer = ImageBuffer::create(size, RenderingMode::Unaccelerated, RenderingPurpose::Unspecified, scale, sourceColorSpace, sourcePixelFormat); + EXPECT_NE(nullptr, imageBuffer); + + auto& context = imageBuffer->context(); + context.fillRect(fillRect, Color::green); + + auto getPixelBufferAllZeros = [&](const FloatRect& rect) { + RetainPtr platformColorSpace = adoptCF(CGColorSpaceCreateWithName(kCGColorSpaceGenericCMYK)); + auto destinationColorSpace = DestinationColorSpace(WTF::move(platformColorSpace)); + PixelBufferFormat destinationPixelFormat { AlphaPremultiplication::Unpremultiplied, PixelFormat::RGBA8, destinationColorSpace }; + + RefPtr pixelBuffer = imageBuffer->getPixelBuffer(destinationPixelFormat, enclosingIntRect(rect)); + EXPECT_NE(nullptr, pixelBuffer); + + auto bytes = pixelBuffer->bytes(); + return std::none_of(bytes.begin(), bytes.end(), [](auto byte) { + return byte; + }); + }; + + EXPECT_TRUE(getPixelBufferAllZeros({ { }, size })); + EXPECT_TRUE(getPixelBufferAllZeros({ { }, size / 10 })); + EXPECT_TRUE(getPixelBufferAllZeros({ FloatPoint { size - size / 10 }, size / 10 })); +} + +#endif + } From 00534948835e357313fb11e13bda0e58486df5fc Mon Sep 17 00:00:00 2001 From: Roberto Rodriguez Date: Tue, 30 Jun 2026 10:38:13 -0700 Subject: [PATCH 46/84] Lowercase CSP directive names eagerly https://bugs.webkit.org/show_bug.cgi?id=317730 rdar://180530658 Reviewed by Anne van Kesteren. Lowercase each CSP directive name as soon as it is read, aligning with the 'Parse a serialized CSP' section of the CSP Level 3 spec (https://w3c.github.io/webappsec-csp/#parse-serialized-policy). The case insensitive name comparisons become plain equality checks, and the extra lowercasing in the reporting path is removed. When an author writes a directive name in some casing other than the spec's lowercase form, it now behaves like the lowercase form. Test: http/tests/security/contentSecurityPolicy/directive-name-case-insensitive.html http/tests/security/contentSecurityPolicy/directive-name-case-insensitive-strict-dynamic.html * LayoutTests/http/tests/security/contentSecurityPolicy/directive-name-case-insensitive-expected.txt: Added. * LayoutTests/http/tests/security/contentSecurityPolicy/directive-name-case-insensitive-strict-dynamic-expected.txt: Added. * LayoutTests/http/tests/security/contentSecurityPolicy/directive-name-case-insensitive-strict-dynamic.html: Added. * LayoutTests/http/tests/security/contentSecurityPolicy/directive-name-case-insensitive.html: Added. * Source/WebCore/page/csp/ContentSecurityPolicy.cpp: (WebCore::ContentSecurityPolicy::reportViolation const): * Source/WebCore/page/csp/ContentSecurityPolicyDirectiveList.cpp: (WebCore::ContentSecurityPolicyDirectiveList::parse): (WebCore::ContentSecurityPolicyDirectiveList::parseDirective): (WebCore::ContentSecurityPolicyDirectiveList::addDirective): * Source/WebCore/page/csp/ContentSecurityPolicySourceList.cpp: (WebCore::ContentSecurityPolicySourceList::isProtocolAllowedByStar const): Canonical link: https://commits.webkit.org/316160@main --- ...rective-name-case-insensitive-expected.txt | 5 ++ ...se-insensitive-strict-dynamic-expected.txt | 4 + ...-name-case-insensitive-strict-dynamic.html | 18 +++++ .../directive-name-case-insensitive.html | 13 ++++ .../page/csp/ContentSecurityPolicy.cpp | 4 +- .../ContentSecurityPolicyDirectiveList.cpp | 75 ++++++++++--------- .../csp/ContentSecurityPolicySourceList.cpp | 5 +- 7 files changed, 83 insertions(+), 41 deletions(-) create mode 100644 LayoutTests/http/tests/security/contentSecurityPolicy/directive-name-case-insensitive-expected.txt create mode 100644 LayoutTests/http/tests/security/contentSecurityPolicy/directive-name-case-insensitive-strict-dynamic-expected.txt create mode 100644 LayoutTests/http/tests/security/contentSecurityPolicy/directive-name-case-insensitive-strict-dynamic.html create mode 100644 LayoutTests/http/tests/security/contentSecurityPolicy/directive-name-case-insensitive.html diff --git a/LayoutTests/http/tests/security/contentSecurityPolicy/directive-name-case-insensitive-expected.txt b/LayoutTests/http/tests/security/contentSecurityPolicy/directive-name-case-insensitive-expected.txt new file mode 100644 index 000000000000..b32cb0600d27 --- /dev/null +++ b/LayoutTests/http/tests/security/contentSecurityPolicy/directive-name-case-insensitive-expected.txt @@ -0,0 +1,5 @@ +CONSOLE MESSAGE: The Content Security Policy directive 'frame-ancestors' is ignored when delivered via an HTML meta element. +CONSOLE MESSAGE: The Content Security Policy directive 'report-uri' is ignored when delivered via an HTML meta element. +CONSOLE MESSAGE: Unrecognized Content-Security-Policy directive 'aaa'. + +This tests that mixed-case Content Security Policy directive names are lowercased, so the console warnings refer to them in lowercase. diff --git a/LayoutTests/http/tests/security/contentSecurityPolicy/directive-name-case-insensitive-strict-dynamic-expected.txt b/LayoutTests/http/tests/security/contentSecurityPolicy/directive-name-case-insensitive-strict-dynamic-expected.txt new file mode 100644 index 000000000000..d7523311b6a4 --- /dev/null +++ b/LayoutTests/http/tests/security/contentSecurityPolicy/directive-name-case-insensitive-strict-dynamic-expected.txt @@ -0,0 +1,4 @@ +CONSOLE MESSAGE: Refused to load http://127.0.0.1:8000/security/contentSecurityPolicy/resources/simpleSourcedScript.js because it does not appear in the script-src directive of the Content Security Policy. + +PASS An uppercase SCRIPT-SRC directive still applies strict-dynamic, so a parser-inserted script is blocked. + diff --git a/LayoutTests/http/tests/security/contentSecurityPolicy/directive-name-case-insensitive-strict-dynamic.html b/LayoutTests/http/tests/security/contentSecurityPolicy/directive-name-case-insensitive-strict-dynamic.html new file mode 100644 index 000000000000..4d2e72118528 --- /dev/null +++ b/LayoutTests/http/tests/security/contentSecurityPolicy/directive-name-case-insensitive-strict-dynamic.html @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/LayoutTests/http/tests/security/contentSecurityPolicy/directive-name-case-insensitive.html b/LayoutTests/http/tests/security/contentSecurityPolicy/directive-name-case-insensitive.html new file mode 100644 index 000000000000..c1cdbea3f05c --- /dev/null +++ b/LayoutTests/http/tests/security/contentSecurityPolicy/directive-name-case-insensitive.html @@ -0,0 +1,13 @@ + + + + + + + +

This tests that mixed-case Content Security Policy directive names are lowercased, so the console warnings refer to them in lowercase.

+ + diff --git a/Source/WebCore/page/csp/ContentSecurityPolicy.cpp b/Source/WebCore/page/csp/ContentSecurityPolicy.cpp index 9e7c72a7e6ff..5656e3e7cc4a 100644 --- a/Source/WebCore/page/csp/ContentSecurityPolicy.cpp +++ b/Source/WebCore/page/csp/ContentSecurityPolicy.cpp @@ -911,7 +911,7 @@ std::optional ContentSecurityPolicy::getCurrentCodePosition() void ContentSecurityPolicy::reportViolation(const ContentSecurityPolicyDirective& violatedDirective, const String& blockedURL, const String& consoleMessage, JSC::JSGlobalObject* state, StringView sourceContent) const { // FIXME: Extract source file, and position from JSC::ExecState. - return reportViolation(violatedDirective.nameForReporting().convertToASCIILowercase(), violatedDirective.directiveList(), blockedURL, consoleMessage, String(), sourceContent.left(40), { }, state); + return reportViolation(violatedDirective.nameForReporting(), violatedDirective.directiveList(), blockedURL, consoleMessage, String(), sourceContent.left(40), { }, state); } void ContentSecurityPolicy::reportViolation(const String& violatedDirective, const ContentSecurityPolicyDirectiveList& violatedDirectiveList, const String& blockedURL, const String& consoleMessage, JSC::JSGlobalObject* state) const @@ -922,7 +922,7 @@ void ContentSecurityPolicy::reportViolation(const String& violatedDirective, con void ContentSecurityPolicy::reportViolation(const ContentSecurityPolicyDirective& violatedDirective, const String& blockedURL, const String& consoleMessage, const String& sourceURL, StringView sourceContent, std::optional&& sourcePosition, const URL& preRedirectURL, JSC::JSGlobalObject* state, Element* element) const { - return reportViolation(violatedDirective.nameForReporting().convertToASCIILowercase(), violatedDirective.directiveList(), blockedURL, consoleMessage, sourceURL, sourceContent.left(40), WTF::move(sourcePosition), state, preRedirectURL, element); + return reportViolation(violatedDirective.nameForReporting(), violatedDirective.directiveList(), blockedURL, consoleMessage, sourceURL, sourceContent.left(40), WTF::move(sourcePosition), state, preRedirectURL, element); } void ContentSecurityPolicy::reportViolation(const String& effectiveViolatedDirective, const ContentSecurityPolicyDirectiveList& violatedDirectiveList, const String& blockedURLString, const String& consoleMessage, const String& sourceURL, StringView sourceContent, std::optional&& maybeSourcePosition, JSC::JSGlobalObject* state, const URL& preRedirectURL, Element* element) const diff --git a/Source/WebCore/page/csp/ContentSecurityPolicyDirectiveList.cpp b/Source/WebCore/page/csp/ContentSecurityPolicyDirectiveList.cpp index b2ead0efba50..806ff2b00542 100644 --- a/Source/WebCore/page/csp/ContentSecurityPolicyDirectiveList.cpp +++ b/Source/WebCore/page/csp/ContentSecurityPolicyDirectiveList.cpp @@ -487,20 +487,20 @@ void ContentSecurityPolicyDirectiveList::parse(const String& policy, ContentSecu if (auto directive = parseDirective(std::span { directiveBegin, buffer.position() })) { ASSERT(!directive->name.isEmpty()); if (policyFrom == ContentSecurityPolicy::PolicyFrom::Inherited) { - if (equalIgnoringASCIICase(directive->name, ContentSecurityPolicyDirectiveNames::upgradeInsecureRequests) - || equalIgnoringASCIICase(directive->name, ContentSecurityPolicyDirectiveNames::sandbox)) + if (directive->name == ContentSecurityPolicyDirectiveNames::upgradeInsecureRequests + || directive->name == ContentSecurityPolicyDirectiveNames::sandbox) continue; } else if (policyFrom == ContentSecurityPolicy::PolicyFrom::HTTPEquivMeta) { - if (equalIgnoringASCIICase(directive->name, ContentSecurityPolicyDirectiveNames::sandbox) - || equalIgnoringASCIICase(directive->name, ContentSecurityPolicyDirectiveNames::reportURI) - || equalIgnoringASCIICase(directive->name, ContentSecurityPolicyDirectiveNames::frameAncestors)) { + if (directive->name == ContentSecurityPolicyDirectiveNames::sandbox + || directive->name == ContentSecurityPolicyDirectiveNames::reportURI + || directive->name == ContentSecurityPolicyDirectiveNames::frameAncestors) { m_policy->reportInvalidDirectiveInHTTPEquivMeta(directive->name); continue; } } else if (policyFrom == ContentSecurityPolicy::PolicyFrom::InheritedForPluginDocument) { - if (!equalIgnoringASCIICase(directive->name, ContentSecurityPolicyDirectiveNames::pluginTypes) - && !equalIgnoringASCIICase(directive->name, ContentSecurityPolicyDirectiveNames::reportURI) - && !equalIgnoringASCIICase(directive->name, ContentSecurityPolicyDirectiveNames::reportTo)) + if (directive->name != ContentSecurityPolicyDirectiveNames::pluginTypes + && directive->name != ContentSecurityPolicyDirectiveNames::reportURI + && directive->name != ContentSecurityPolicyDirectiveNames::reportTo) continue; } addDirective(WTF::move(*directive)); @@ -535,7 +535,8 @@ template auto ContentSecurityPolicyDirectiveList::parseD return std::nullopt; } - String name { nameBegin.first(buffer.position() - nameBegin.data()) }; + // Lowercase the directive name eagerly so downstream code can use case-sensitive comparisons. + String name = StringView { nameBegin.first(buffer.position() - nameBegin.data()) }.convertToASCIILowercase(); if (buffer.atEnd()) return ParsedDirective { WTF::move(name), { } }; @@ -689,75 +690,75 @@ void ContentSecurityPolicyDirectiveList::addDirective(ParsedDirective&& directiv { ASSERT(!directive.name.isEmpty()); - if (equalIgnoringASCIICase(directive.name, ContentSecurityPolicyDirectiveNames::defaultSrc)) { + if (directive.name == ContentSecurityPolicyDirectiveNames::defaultSrc) { setCSPDirective(WTF::move(directive), m_defaultSrc); m_policy->addHashAlgorithmsForInlineScripts(m_defaultSrc->hashAlgorithmsUsed()); m_policy->addHashAlgorithmsForInlineStylesheets(m_defaultSrc->hashAlgorithmsUsed()); - } else if (equalIgnoringASCIICase(directive.name, ContentSecurityPolicyDirectiveNames::scriptSrc)) { + } else if (directive.name == ContentSecurityPolicyDirectiveNames::scriptSrc) { setCSPDirective(WTF::move(directive), m_scriptSrc); m_policy->addHashAlgorithmsForInlineScripts(m_scriptSrc->hashAlgorithmsUsed()); - } else if (equalIgnoringASCIICase(directive.name, ContentSecurityPolicyDirectiveNames::scriptSrcElem)) { + } else if (directive.name == ContentSecurityPolicyDirectiveNames::scriptSrcElem) { setCSPDirective(WTF::move(directive), m_scriptSrcElem); m_policy->addHashAlgorithmsForInlineScripts(m_scriptSrcElem->hashAlgorithmsUsed()); - } else if (equalIgnoringASCIICase(directive.name, ContentSecurityPolicyDirectiveNames::scriptSrcAttr)) { + } else if (directive.name == ContentSecurityPolicyDirectiveNames::scriptSrcAttr) { setCSPDirective(WTF::move(directive), m_scriptSrcAttr); m_policy->addHashAlgorithmsForInlineScripts(m_scriptSrcAttr->hashAlgorithmsUsed()); - } else if (equalIgnoringASCIICase(directive.name, ContentSecurityPolicyDirectiveNames::styleSrc)) { + } else if (directive.name == ContentSecurityPolicyDirectiveNames::styleSrc) { setCSPDirective(WTF::move(directive), m_styleSrc); m_policy->addHashAlgorithmsForInlineStylesheets(m_styleSrc->hashAlgorithmsUsed()); - } else if (equalIgnoringASCIICase(directive.name, ContentSecurityPolicyDirectiveNames::styleSrcElem)) { + } else if (directive.name == ContentSecurityPolicyDirectiveNames::styleSrcElem) { setCSPDirective(WTF::move(directive), m_styleSrcElem); m_policy->addHashAlgorithmsForInlineStylesheets(m_styleSrcElem->hashAlgorithmsUsed()); - } else if (equalIgnoringASCIICase(directive.name, ContentSecurityPolicyDirectiveNames::styleSrcAttr)) { + } else if (directive.name == ContentSecurityPolicyDirectiveNames::styleSrcAttr) { setCSPDirective(WTF::move(directive), m_styleSrcAttr); m_policy->addHashAlgorithmsForInlineStylesheets(m_styleSrcAttr->hashAlgorithmsUsed()); - } else if (equalIgnoringASCIICase(directive.name, ContentSecurityPolicyDirectiveNames::objectSrc)) + } else if (directive.name == ContentSecurityPolicyDirectiveNames::objectSrc) setCSPDirective(WTF::move(directive), m_objectSrc); - else if (equalIgnoringASCIICase(directive.name, ContentSecurityPolicyDirectiveNames::workerSrc)) + else if (directive.name == ContentSecurityPolicyDirectiveNames::workerSrc) setCSPDirective(WTF::move(directive), m_workerSrc); - else if (equalIgnoringASCIICase(directive.name, ContentSecurityPolicyDirectiveNames::frameSrc)) { + else if (directive.name == ContentSecurityPolicyDirectiveNames::frameSrc) { // FIXME: Log to console "The frame-src directive is deprecated. Use the child-src directive instead." // See . setCSPDirective(WTF::move(directive), m_frameSrc); - } else if (equalIgnoringASCIICase(directive.name, ContentSecurityPolicyDirectiveNames::imgSrc)) + } else if (directive.name == ContentSecurityPolicyDirectiveNames::imgSrc) setCSPDirective(WTF::move(directive), m_imgSrc); - else if (equalIgnoringASCIICase(directive.name, ContentSecurityPolicyDirectiveNames::fontSrc)) + else if (directive.name == ContentSecurityPolicyDirectiveNames::fontSrc) setCSPDirective(WTF::move(directive), m_fontSrc); #if ENABLE(APPLICATION_MANIFEST) - else if (equalIgnoringASCIICase(directive.name, ContentSecurityPolicyDirectiveNames::manifestSrc)) + else if (directive.name == ContentSecurityPolicyDirectiveNames::manifestSrc) setCSPDirective(WTF::move(directive), m_manifestSrc); #endif - else if (equalIgnoringASCIICase(directive.name, ContentSecurityPolicyDirectiveNames::mediaSrc)) + else if (directive.name == ContentSecurityPolicyDirectiveNames::mediaSrc) setCSPDirective(WTF::move(directive), m_mediaSrc); - else if (equalIgnoringASCIICase(directive.name, ContentSecurityPolicyDirectiveNames::connectSrc)) + else if (directive.name == ContentSecurityPolicyDirectiveNames::connectSrc) setCSPDirective(WTF::move(directive), m_connectSrc); - else if (equalIgnoringASCIICase(directive.name, ContentSecurityPolicyDirectiveNames::childSrc)) + else if (directive.name == ContentSecurityPolicyDirectiveNames::childSrc) setCSPDirective(WTF::move(directive), m_childSrc); - else if (equalIgnoringASCIICase(directive.name, ContentSecurityPolicyDirectiveNames::formAction)) + else if (directive.name == ContentSecurityPolicyDirectiveNames::formAction) setCSPDirective(WTF::move(directive), m_formAction); - else if (equalIgnoringASCIICase(directive.name, ContentSecurityPolicyDirectiveNames::baseURI)) + else if (directive.name == ContentSecurityPolicyDirectiveNames::baseURI) setCSPDirective(WTF::move(directive), m_baseURI); - else if (equalIgnoringASCIICase(directive.name, ContentSecurityPolicyDirectiveNames::frameAncestors)) + else if (directive.name == ContentSecurityPolicyDirectiveNames::frameAncestors) setCSPDirective(WTF::move(directive), m_frameAncestors); - else if (equalIgnoringASCIICase(directive.name, ContentSecurityPolicyDirectiveNames::pluginTypes)) { + else if (directive.name == ContentSecurityPolicyDirectiveNames::pluginTypes) { auto name = directive.name; setCSPDirective(WTF::move(directive), m_pluginTypes); m_policy->reportDeprecatedDirectiveToConsole(name); - } else if (equalIgnoringASCIICase(directive.name, ContentSecurityPolicyDirectiveNames::prefetchSrc)) + } else if (directive.name == ContentSecurityPolicyDirectiveNames::prefetchSrc) setCSPDirective(WTF::move(directive), m_prefetchSrc); - else if (equalIgnoringASCIICase(directive.name, ContentSecurityPolicyDirectiveNames::sandbox)) + else if (directive.name == ContentSecurityPolicyDirectiveNames::sandbox) applySandboxPolicy(WTF::move(directive)); - else if (equalIgnoringASCIICase(directive.name, ContentSecurityPolicyDirectiveNames::reportTo)) + else if (directive.name == ContentSecurityPolicyDirectiveNames::reportTo) parseReportTo(WTF::move(directive)); - else if (equalIgnoringASCIICase(directive.name, ContentSecurityPolicyDirectiveNames::reportURI)) + else if (directive.name == ContentSecurityPolicyDirectiveNames::reportURI) parseReportURI(WTF::move(directive)); - else if (equalIgnoringASCIICase(directive.name, ContentSecurityPolicyDirectiveNames::upgradeInsecureRequests)) + else if (directive.name == ContentSecurityPolicyDirectiveNames::upgradeInsecureRequests) setUpgradeInsecureRequests(WTF::move(directive)); - else if (equalIgnoringASCIICase(directive.name, ContentSecurityPolicyDirectiveNames::blockAllMixedContent)) + else if (directive.name == ContentSecurityPolicyDirectiveNames::blockAllMixedContent) setBlockAllMixedContentEnabled(WTF::move(directive)); - else if (equalIgnoringASCIICase(directive.name, ContentSecurityPolicyDirectiveNames::trustedTypes)) + else if (directive.name == ContentSecurityPolicyDirectiveNames::trustedTypes) setCSPDirective(WTF::move(directive), m_trustedTypes); - else if (equalIgnoringASCIICase(directive.name, ContentSecurityPolicyDirectiveNames::requireTrustedTypesFor)) + else if (directive.name == ContentSecurityPolicyDirectiveNames::requireTrustedTypesFor) parseRequireTrustedTypesFor(WTF::move(directive)); else m_policy->reportUnsupportedDirective(WTF::move(directive.name)); diff --git a/Source/WebCore/page/csp/ContentSecurityPolicySourceList.cpp b/Source/WebCore/page/csp/ContentSecurityPolicySourceList.cpp index 142083d1e8c1..2f84719ec286 100644 --- a/Source/WebCore/page/csp/ContentSecurityPolicySourceList.cpp +++ b/Source/WebCore/page/csp/ContentSecurityPolicySourceList.cpp @@ -43,6 +43,7 @@ namespace WebCore { static bool NODELETE isCSPDirectiveName(StringView name) { + // Called with a source-expression token, not a parsed directive name, so it is not lowercased. return equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::baseURI) || equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::connectSrc) || equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::defaultSrc) @@ -120,9 +121,9 @@ bool ContentSecurityPolicySourceList::isProtocolAllowedByStar(const URL& url) co bool isAllowed = url.protocolIsInHTTPFamily() || url.protocolIs("ws"_s) || url.protocolIs("wss"_s) || url.protocolIs(m_policy->selfProtocol()); // Also not allowed by the Content Security Policy Level 3 spec., we allow a data URL to match // "img-src *" and either a data URL or blob URL to match "media-src *" for web compatibility. - if (equalIgnoringASCIICase(m_directiveName, ContentSecurityPolicyDirectiveNames::imgSrc)) + if (m_directiveName == ContentSecurityPolicyDirectiveNames::imgSrc) isAllowed |= url.protocolIsData(); - else if (equalIgnoringASCIICase(m_directiveName, ContentSecurityPolicyDirectiveNames::mediaSrc)) + else if (m_directiveName == ContentSecurityPolicyDirectiveNames::mediaSrc) isAllowed |= url.protocolIsData() || url.protocolIsBlob(); return isAllowed; } From e23afe633acbe60a383dea12b39e33856b074239 Mon Sep 17 00:00:00 2001 From: Basuke Suzuki Date: Tue, 30 Jun 2026 10:43:58 -0700 Subject: [PATCH 47/84] [Site Isolation] Per-frame back/forward walk regresses iframe to stale initial about:blank https://bugs.webkit.org/show_bug.cgi?id=317458 rdar://180077264 Reviewed by Sihui Liu. Under UseUIProcessForBackForwardItemLoading, WebPageProxy::dispatchPerFrameTraversals walks the (current, target) BF frame trees and dispatches a per-frame GoToBackForwardItem whenever itemSequenceNumber differs. When the target entry holds the stale "initial about:blank" state for an iframe whose live document has since loaded a real URL, the walk dispatches a navigation back to about:blank and regresses the live iframe. The HTML spec (https://html.spec.whatwg.org/#initialise-the-document-object) requires the iframe's initial about:blank entry to be replaced by the iframe's first real navigation, but WebKit's BF list does not propagate that replacement to non-current entries. The legacy navigatedFrameID-heuristic routing path masked the issue by never dispatching a per-frame iframe traversal in the first place; the walk-as-replacement (https://bugs.webkit.org/show_bug.cgi?id=317090) exposes it because it compares each frame's state pair-wise. Reproduction is the imported WPT imported/w3c/web-platform-tests/html/browsers/browsing-the-web/history-traversal/history-traversal-navigate-parent-while-child-loading.html which performs a parent pushState() while an iframe is mid-initial-navigation, then asserts on history.back() that the iframe URL is preserved. Without the fix, the iframe regresses: FAIL pushState() in parent while child is doing initial navigation, then go back assert_equals: expected "http://web-platform.test:8800/common/blank.html" but got "about:blank" This regression surfaces only when UseUIProcessForBackForwardItemLoading is enabled under SiteIsolationEnabled, the configuration that PR #67459 (https://bugs.webkit.org/show_bug.cgi?id=316588) re-enables in the test harness. Fix: introduce a walk-side guard isStaleInitialAboutBlankIframeTarget that skips dispatching a per-frame traversal into a non-main child frame's stale initial about:blank entry. The stale entry is identified by an authoritative isInitialAboutBlank flag, not a URL-string match. The flag rides the existing HistoryItem -> FrameState pipeline (mirroring wasCreatedByJSWithoutUserInteraction): HistoryController::initializeItem sets it from DocumentLoader::isInitialAboutBlank(), which is true only for the frame's initial empty document; toFrameState / applyFrameState round-trip it through SessionState serialization. Because an intentional iframe.src="about:blank" navigation happens after committedFirstRealDocumentLoad() its DocumentLoader is not the initial empty document, so its flag is false and the walk dispatches the traversal as a real navigation. The earlier URL-string heuristic (toURL is empty or "about:blank") could not distinguish the initial empty document from an intentional about:blank load and so would have wrongly skipped the latter; the flag closes that gap. A spec-level fix would be to propagate the initial about:blank replacement to older BF entries when an iframe completes its first real navigation; that is a separate follow-up bug and is not attempted here because it touches BackForwardClient / HistoryController across processes and is out of scope for this regression. The imported WPT imported/w3c/web-platform-tests/html/browsers/browsing-the-web/history-traversal/history-traversal-navigate-parent-while-child-loading.html covers the behavior but shares the upstream baseline across configurations, so it cannot pin SiteIsolationEnabled / UseUIProcessForBackForwardItemLoading via a webkit-test-runner header and only exercises this fix once the test harness auto-couples the flags (bug 316588). Add a http/wpt/site-isolation copy that pins both flags in its header, so the regression is covered on this PR independently of 316588. Verified negatively: the copy fails (iframe regresses to about:blank) without this fix and passes with it. Add an API test that covers the gap the flag closes, which the WPT above does not: a child frame that navigates intentionally to about:blank after its first real load, then back. With the old URL-string heuristic the back traversal into that intentional about:blank entry was wrongly skipped and the child was stranded on the prior real URL; with the isInitialAboutBlank flag it is dispatched. Verified negatively: the test fails at the go-back assertion with the old heuristic and passes with the flag. * LayoutTests/http/wpt/site-isolation/history-traversal/history-traversal-navigate-parent-while-child-loading.html: Added. * LayoutTests/http/wpt/site-isolation/history-traversal/history-traversal-navigate-parent-while-child-loading-expected.txt: Added. * Source/WebCore/history/HistoryItem.h: (WebCore::HistoryItem::setIsInitialAboutBlank): (WebCore::HistoryItem::isInitialAboutBlank const): * Source/WebCore/loader/HistoryController.cpp: (WebCore::HistoryController::initializeItem): * Source/WebKit/Shared/SessionState.cpp: (WebKit::FrameState::FrameState): (WebKit::FrameState::copy): (WebKit::FrameState::replacePayloadFrom): * Source/WebKit/Shared/SessionState.h: * Source/WebKit/Shared/SessionState.serialization.in: * Source/WebKit/WebProcess/WebCoreSupport/SessionStateConversion.cpp: (WebKit::toFrameState): (WebKit::applyFrameState): * Source/WebKit/UIProcess/WebPageProxy.cpp: (WebKit::isStaleInitialAboutBlankIframeTarget): (WebKit::WebPageProxy::dispatchPerFrameTraversals): * Tools/TestWebKitAPI/Tests/WebKit/WKWebView/SiteIsolation.mm: (TestWebKitAPI::TEST): Canonical link: https://commits.webkit.org/316161@main --- ...te-parent-while-child-loading-expected.txt | 4 ++ ...l-navigate-parent-while-child-loading.html | 31 ++++++++++++++ Source/WebCore/history/HistoryItem.h | 4 ++ Source/WebCore/loader/HistoryController.cpp | 2 + Source/WebKit/Shared/SessionState.cpp | 8 +++- Source/WebKit/Shared/SessionState.h | 5 ++- .../Shared/SessionState.serialization.in | 1 + Source/WebKit/UIProcess/WebPageProxy.cpp | 20 ++++++++- .../WebCoreSupport/SessionStateConversion.cpp | 2 + .../Tests/WebKit/WKWebView/SiteIsolation.mm | 41 +++++++++++++++++++ 10 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 LayoutTests/http/wpt/site-isolation/history-traversal/history-traversal-navigate-parent-while-child-loading-expected.txt create mode 100644 LayoutTests/http/wpt/site-isolation/history-traversal/history-traversal-navigate-parent-while-child-loading.html diff --git a/LayoutTests/http/wpt/site-isolation/history-traversal/history-traversal-navigate-parent-while-child-loading-expected.txt b/LayoutTests/http/wpt/site-isolation/history-traversal/history-traversal-navigate-parent-while-child-loading-expected.txt new file mode 100644 index 000000000000..fc07a2561091 --- /dev/null +++ b/LayoutTests/http/wpt/site-isolation/history-traversal/history-traversal-navigate-parent-while-child-loading-expected.txt @@ -0,0 +1,4 @@ + + +PASS pushState() in parent while child is doing initial navigation, then go back + diff --git a/LayoutTests/http/wpt/site-isolation/history-traversal/history-traversal-navigate-parent-while-child-loading.html b/LayoutTests/http/wpt/site-isolation/history-traversal/history-traversal-navigate-parent-while-child-loading.html new file mode 100644 index 000000000000..769278f25057 --- /dev/null +++ b/LayoutTests/http/wpt/site-isolation/history-traversal/history-traversal-navigate-parent-while-child-loading.html @@ -0,0 +1,31 @@ + + + + + + + + diff --git a/Source/WebCore/history/HistoryItem.h b/Source/WebCore/history/HistoryItem.h index 835a3d4a199d..dde8deea1cee 100644 --- a/Source/WebCore/history/HistoryItem.h +++ b/Source/WebCore/history/HistoryItem.h @@ -227,6 +227,9 @@ class HistoryItem : public RefCountedAndCanMakeWeakPtr { WEBCORE_EXPORT void setWasCreatedByJSWithoutUserInteraction(bool); bool wasCreatedByJSWithoutUserInteraction() const { return m_wasCreatedByJSWithoutUserInteraction; } + void setIsInitialAboutBlank(bool isInitialAboutBlank) { m_isInitialAboutBlank = isInitialAboutBlank; } + bool isInitialAboutBlank() const { return m_isInitialAboutBlank; } + #if !LOG_DISABLED String logString() const; #endif @@ -260,6 +263,7 @@ class HistoryItem : public RefCountedAndCanMakeWeakPtr { bool m_lastVisitWasFailure { false }; bool m_wasRestoredFromSession { false }; bool m_wasCreatedByJSWithoutUserInteraction { false }; + bool m_isInitialAboutBlank { false }; bool m_shouldRestoreScrollPosition { true }; bool m_isTargetItem { false }; diff --git a/Source/WebCore/loader/HistoryController.cpp b/Source/WebCore/loader/HistoryController.cpp index 47a8284c3b77..cca0ca00923d 100644 --- a/Source/WebCore/loader/HistoryController.cpp +++ b/Source/WebCore/loader/HistoryController.cpp @@ -886,6 +886,8 @@ void HistoryController::initializeItem(HistoryItem& item, RefPtr item.setShouldOpenExternalURLsPolicy(documentLoader->shouldOpenExternalURLsPolicyToPropagate()); + item.setIsInitialAboutBlank(documentLoader->isInitialAboutBlank()); + // Save form state if this is a POST item.setFormInfoFromRequest(documentLoader->request()); } diff --git a/Source/WebKit/Shared/SessionState.cpp b/Source/WebKit/Shared/SessionState.cpp index 1bc50bc95a01..2d2320a6ce55 100644 --- a/Source/WebKit/Shared/SessionState.cpp +++ b/Source/WebKit/Shared/SessionState.cpp @@ -43,7 +43,7 @@ FrameState::FrameState() RELEASE_ASSERT(RunLoop::isMain()); } -FrameState::FrameState(String&& urlString, String&& originalURLString, String&& referrer, AtomString&& target, std::optional frameID, std::optional>&& stateObjectData, int64_t documentSequenceNumber, int64_t itemSequenceNumber, std::optional navigationAPIKey, WebCore::IntPoint scrollPosition, bool shouldRestoreScrollPosition, float pageScaleFactor, std::optional&& httpBody, std::optional itemID, std::optional frameItemID, String&& title, WebCore::ShouldOpenExternalURLsPolicy shouldOpenExternalURLsPolicy, RefPtr&& sessionStateObject, bool wasCreatedByJSWithoutUserInteraction, bool wasRestoredFromSession, std::optional&& policyContainer, +FrameState::FrameState(String&& urlString, String&& originalURLString, String&& referrer, AtomString&& target, std::optional frameID, std::optional>&& stateObjectData, int64_t documentSequenceNumber, int64_t itemSequenceNumber, std::optional navigationAPIKey, WebCore::IntPoint scrollPosition, bool shouldRestoreScrollPosition, float pageScaleFactor, std::optional&& httpBody, std::optional itemID, std::optional frameItemID, String&& title, WebCore::ShouldOpenExternalURLsPolicy shouldOpenExternalURLsPolicy, RefPtr&& sessionStateObject, bool wasCreatedByJSWithoutUserInteraction, bool wasRestoredFromSession, bool isInitialAboutBlank, std::optional&& policyContainer, #if PLATFORM(IOS_FAMILY) WebCore::FloatRect exposedContentRect, WebCore::IntRect unobscuredContentRect, WebCore::FloatSize minimumLayoutSizeInScrollViewCoordinates, WebCore::IntSize contentSize, bool scaleIsInitial, WebCore::FloatBoxExtent obscuredInsets, #endif @@ -69,6 +69,7 @@ FrameState::FrameState(String&& urlString, String&& originalURLString, String&& , sessionStateObject(WTF::move(sessionStateObject)) , wasCreatedByJSWithoutUserInteraction(wasCreatedByJSWithoutUserInteraction) , wasRestoredFromSession(wasRestoredFromSession) + , isInitialAboutBlank(isInitialAboutBlank) , policyContainer(WTF::move(policyContainer)) #if PLATFORM(IOS_FAMILY) , exposedContentRect(exposedContentRect) @@ -83,7 +84,7 @@ FrameState::FrameState(String&& urlString, String&& originalURLString, String&& { } -FrameState::FrameState(const String& urlString, const String& originalURLString, const String& referrer, const AtomString& target, std::optional frameID, std::optional> stateObjectData, int64_t documentSequenceNumber, int64_t itemSequenceNumber, std::optional navigationAPIKey, WebCore::IntPoint scrollPosition, bool shouldRestoreScrollPosition, float pageScaleFactor, const std::optional& httpBody, std::optional itemID, std::optional frameItemID, const String& title, WebCore::ShouldOpenExternalURLsPolicy shouldOpenExternalURLsPolicy, RefPtr&& sessionStateObject, bool wasCreatedByJSWithoutUserInteraction, bool wasRestoredFromSession, const std::optional& policyContainer, +FrameState::FrameState(const String& urlString, const String& originalURLString, const String& referrer, const AtomString& target, std::optional frameID, std::optional> stateObjectData, int64_t documentSequenceNumber, int64_t itemSequenceNumber, std::optional navigationAPIKey, WebCore::IntPoint scrollPosition, bool shouldRestoreScrollPosition, float pageScaleFactor, const std::optional& httpBody, std::optional itemID, std::optional frameItemID, const String& title, WebCore::ShouldOpenExternalURLsPolicy shouldOpenExternalURLsPolicy, RefPtr&& sessionStateObject, bool wasCreatedByJSWithoutUserInteraction, bool wasRestoredFromSession, bool isInitialAboutBlank, const std::optional& policyContainer, #if PLATFORM(IOS_FAMILY) WebCore::FloatRect exposedContentRect, WebCore::IntRect unobscuredContentRect, WebCore::FloatSize minimumLayoutSizeInScrollViewCoordinates, WebCore::IntSize contentSize, bool scaleIsInitial, WebCore::FloatBoxExtent obscuredInsets, #endif @@ -109,6 +110,7 @@ FrameState::FrameState(const String& urlString, const String& originalURLString, , sessionStateObject(WTF::move(sessionStateObject)) , wasCreatedByJSWithoutUserInteraction(wasCreatedByJSWithoutUserInteraction) , wasRestoredFromSession(wasRestoredFromSession) + , isInitialAboutBlank(isInitialAboutBlank) , policyContainer(policyContainer) #if PLATFORM(IOS_FAMILY) , exposedContentRect(exposedContentRect) @@ -146,6 +148,7 @@ Ref FrameState::copy() sessionStateObject.copyRef(), wasCreatedByJSWithoutUserInteraction, wasRestoredFromSession, + isInitialAboutBlank, policyContainer, #if PLATFORM(IOS_FAMILY) exposedContentRect, @@ -184,6 +187,7 @@ void FrameState::replacePayloadFrom(Ref&& other) sessionStateObject = WTF::move(other->sessionStateObject); wasCreatedByJSWithoutUserInteraction = other->wasCreatedByJSWithoutUserInteraction; wasRestoredFromSession = other->wasRestoredFromSession; + isInitialAboutBlank = other->isInitialAboutBlank; policyContainer = WTF::move(other->policyContainer); #if PLATFORM(IOS_FAMILY) exposedContentRect = other->exposedContentRect; diff --git a/Source/WebKit/Shared/SessionState.h b/Source/WebKit/Shared/SessionState.h index 351054a6e110..85baa0555cb2 100644 --- a/Source/WebKit/Shared/SessionState.h +++ b/Source/WebKit/Shared/SessionState.h @@ -117,6 +117,7 @@ class FrameState : public RefCounted { RefPtr sessionStateObject; bool wasCreatedByJSWithoutUserInteraction { false }; bool wasRestoredFromSession { false }; + bool isInitialAboutBlank { false }; std::optional policyContainer; // FIXME: These should not be per frame. @@ -137,14 +138,14 @@ class FrameState : public RefCounted { // This is used to help debug . FrameState(); - FrameState(String&& urlString, String&& originalURLString, String&& referrer, AtomString&& target, std::optional, std::optional>&& stateObjectData, int64_t documentSequenceNumber, int64_t itemSequenceNumber, std::optional navigationAPIKey, WebCore::IntPoint scrollPosition, bool shouldRestoreScrollPosition, float pageScaleFactor, std::optional&&, std::optional, std::optional, String&& title, WebCore::ShouldOpenExternalURLsPolicy, RefPtr&& sessionStateObject, bool wasCreatedByJSWithoutUserInteraction, bool wasRestoredFromSession, std::optional&&, + FrameState(String&& urlString, String&& originalURLString, String&& referrer, AtomString&& target, std::optional, std::optional>&& stateObjectData, int64_t documentSequenceNumber, int64_t itemSequenceNumber, std::optional navigationAPIKey, WebCore::IntPoint scrollPosition, bool shouldRestoreScrollPosition, float pageScaleFactor, std::optional&&, std::optional, std::optional, String&& title, WebCore::ShouldOpenExternalURLsPolicy, RefPtr&& sessionStateObject, bool wasCreatedByJSWithoutUserInteraction, bool wasRestoredFromSession, bool isInitialAboutBlank, std::optional&&, #if PLATFORM(IOS_FAMILY) WebCore::FloatRect exposedContentRect, WebCore::IntRect unobscuredContentRect, WebCore::FloatSize minimumLayoutSizeInScrollViewCoordinates, WebCore::IntSize contentSize, bool scaleIsInitial, WebCore::FloatBoxExtent obscuredInsets, #endif Vector>&& children, Vector&& documentState ); - FrameState(const String& urlString, const String& originalURLString, const String& referrer, const AtomString& target, std::optional, std::optional> stateObjectData, int64_t documentSequenceNumber, int64_t itemSequenceNumber, std::optional navigationAPIKey, WebCore::IntPoint scrollPosition, bool shouldRestoreScrollPosition, float pageScaleFactor, const std::optional&, std::optional, std::optional, const String& title, WebCore::ShouldOpenExternalURLsPolicy, RefPtr&& sessionStateObject, bool wasCreatedByJSWithoutUserInteraction, bool wasRestoredFromSession, const std::optional&, + FrameState(const String& urlString, const String& originalURLString, const String& referrer, const AtomString& target, std::optional, std::optional> stateObjectData, int64_t documentSequenceNumber, int64_t itemSequenceNumber, std::optional navigationAPIKey, WebCore::IntPoint scrollPosition, bool shouldRestoreScrollPosition, float pageScaleFactor, const std::optional&, std::optional, std::optional, const String& title, WebCore::ShouldOpenExternalURLsPolicy, RefPtr&& sessionStateObject, bool wasCreatedByJSWithoutUserInteraction, bool wasRestoredFromSession, bool isInitialAboutBlank, const std::optional&, #if PLATFORM(IOS_FAMILY) WebCore::FloatRect exposedContentRect, WebCore::IntRect unobscuredContentRect, WebCore::FloatSize minimumLayoutSizeInScrollViewCoordinates, WebCore::IntSize contentSize, bool scaleIsInitial, WebCore::FloatBoxExtent obscuredInsets, #endif diff --git a/Source/WebKit/Shared/SessionState.serialization.in b/Source/WebKit/Shared/SessionState.serialization.in index 3bd8fcf873ee..a3b6f3663760 100644 --- a/Source/WebKit/Shared/SessionState.serialization.in +++ b/Source/WebKit/Shared/SessionState.serialization.in @@ -64,6 +64,7 @@ header: "SessionState.h" RefPtr sessionStateObject; bool wasCreatedByJSWithoutUserInteraction; bool wasRestoredFromSession; + bool isInitialAboutBlank; std::optional policyContainer; #if PLATFORM(IOS_FAMILY) diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp index 9bfa2641939c..de365b14593d 100644 --- a/Source/WebKit/UIProcess/WebPageProxy.cpp +++ b/Source/WebKit/UIProcess/WebPageProxy.cpp @@ -2874,10 +2874,28 @@ RefPtr WebPageProxy::goToBackForwardItem(WebBackForwardListFram return RefPtr { WTF::move(navigation) }; } +// The HTML spec replaces an iframe's initial about:blank entry on the frame's first real +// navigation; WebKit's BF list keeps the stale state in older entries, so dispatching a +// traversal into one would regress the live iframe. The target entry is identified by the +// authoritative isInitialAboutBlank flag (set from DocumentLoader::isInitialAboutBlank when the +// history item is created), not by a URL-string match, so an intentional iframe.src="about:blank" +// navigation — which is not the initial empty document — is correctly dispatched. +static bool isStaleInitialAboutBlankIframeTarget(WebBackForwardListFrameItem& toFrame) +{ + if (!toFrame.frameState().isInitialAboutBlank) + return false; + auto toFrameID = toFrame.frameID(); + if (!toFrameID) + return false; + RefPtr toLiveFrame = WebFrameProxy::webFrame(*toFrameID); + return toLiveFrame && !toLiveFrame->isMainFrame(); +} + bool WebPageProxy::dispatchPerFrameTraversals(WebBackForwardListFrameItem& fromFrame, WebBackForwardListFrameItem& toFrame, NavigationIdentifier navigationID, FrameLoadType frameLoadType, ShouldRestoreFromBackForwardCache shouldRestore, const WebCore::PublicSuffix& publicSuffix) { bool anySent = false; - if (fromFrame.frameState().itemSequenceNumber != toFrame.frameState().itemSequenceNumber) + if (fromFrame.frameState().itemSequenceNumber != toFrame.frameState().itemSequenceNumber + && !isStaleInitialAboutBlankIframeTarget(toFrame)) anySent = sendGoToBackForwardItemForFrame(toFrame, navigationID, frameLoadType, shouldRestore, publicSuffix); bool sameDocument = fromFrame.frameState().documentSequenceNumber == toFrame.frameState().documentSequenceNumber; diff --git a/Source/WebKit/WebProcess/WebCoreSupport/SessionStateConversion.cpp b/Source/WebKit/WebProcess/WebCoreSupport/SessionStateConversion.cpp index 7f1e6a6645a8..77816e2b98e5 100644 --- a/Source/WebKit/WebProcess/WebCoreSupport/SessionStateConversion.cpp +++ b/Source/WebKit/WebProcess/WebCoreSupport/SessionStateConversion.cpp @@ -101,6 +101,7 @@ Ref toFrameState(const HistoryItem& historyItem) frameState->sessionStateObject = historyItem.stateObject(); frameState->wasCreatedByJSWithoutUserInteraction = historyItem.wasCreatedByJSWithoutUserInteraction(); frameState->wasRestoredFromSession = historyItem.wasRestoredFromSession(); + frameState->isInitialAboutBlank = historyItem.isInitialAboutBlank(); frameState->policyContainer = historyItem.policyContainer(); static constexpr auto maxTitleLength = 1000u; // Closest power of 10 above the W3C recommendation for Title length. @@ -174,6 +175,7 @@ static void applyFrameState(HistoryItemClient& client, HistoryItem& historyItem, historyItem.setStateObject(frameState.sessionStateObject.get()); historyItem.setWasCreatedByJSWithoutUserInteraction(frameState.wasCreatedByJSWithoutUserInteraction); historyItem.setWasRestoredFromSession(frameState.wasRestoredFromSession); + historyItem.setIsInitialAboutBlank(frameState.isInitialAboutBlank); if (auto policyContainer = frameState.policyContainer) historyItem.setPolicyContainer(*policyContainer); diff --git a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/SiteIsolation.mm b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/SiteIsolation.mm index 3209dafe2820..e6cac4f5feda 100644 --- a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/SiteIsolation.mm +++ b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/SiteIsolation.mm @@ -4961,6 +4961,47 @@ HTTPServer server({ EXPECT_WK_STREQ([webView _test_waitForAlert], "c"); } +TEST(SiteIsolation, IntentionalAboutBlankIframeBackForwardNotSkipped) +{ + // Regression test for the URL-heuristic gap in isStaleInitialAboutBlankIframeTarget + // (https://bugs.webkit.org/show_bug.cgi?id=317458). A child frame that navigates + // *intentionally* to about:blank after its first real load produces a legitimate, + // traversable back/forward entry — distinct from the frame's initial empty document. + // A URL-string guard would wrongly skip the traversal into that entry; the + // authoritative isInitialAboutBlank flag lets it through. + HTTPServer server({ + { "/example"_s, { ""_s } }, + { "/a"_s, { ""_s } }, + { "/c"_s, { ""_s } } + }, HTTPServer::Protocol::HttpsProxy); + auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server); + [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]]; + EXPECT_WK_STREQ([webView _test_waitForAlert], "a"); + + auto childURLIs = [webView = RetainPtr { webView }] (NSString *expected) { + for (int i = 0; i < 100; ++i) { + RetainPtr value = [webView objectByEvaluatingJavaScript:@"location.href" inFrame:[webView firstChildFrame]]; + if ([value isKindOfClass:[NSString class]] && [(NSString *)value.get() isEqualToString:expected]) + return true; + TestWebKitAPI::Util::runFor(0.05_s); + } + return false; + }; + + // Intentional about:blank navigation in the child frame, after its first real load. + [webView evaluateJavaScript:@"location.href = 'about:blank'" inFrame:[webView firstChildFrame] completionHandler:nil]; + EXPECT_TRUE(childURLIs(@"about:blank")); + + // Navigate the child to a real URL so the live frame is on a real document. + [webView evaluateJavaScript:@"location.href = 'https://webkit.org/c'" inFrame:[webView firstChildFrame] completionHandler:nil]; + EXPECT_WK_STREQ([webView _test_waitForAlert], "c"); + + // Going back must traverse the child back to the intentional about:blank entry, + // not leave it stranded on /c. With the old URL heuristic this was skipped. + [webView goBack]; + EXPECT_TRUE(childURLIs(@"about:blank")); +} + TEST(SiteIsolation, RedirectToCSP) { HTTPServer server({ From e081edf2cd1304ad8c7b521f4def4ee87508e9ad Mon Sep 17 00:00:00 2001 From: Issac Roy Date: Tue, 30 Jun 2026 10:49:16 -0700 Subject: [PATCH 48/84] [Scripts] --print-expectations fails when no tests match https://bugs.webkit.org/show_bug.cgi?id=294566 rdar://153547585 Reviewed by Sam Sneddon. When --print-expectations is passed a path that matches no tests, _collect_tests returns empty sets and aggregate_tests is empty. The call to max() over an empty generator then raises ValueError. Guard against this by skipping to the next driver iteration when no tests are found, matching the behavior of run() which also short-circuits on an empty test set. * Tools/Scripts/webkitpy/layout_tests/controllers/manager.py: (Manager.print_expectations): Guard against ValueError when no tests are found. * Tools/Scripts/webkitpy/layout_tests/controllers/manager_unittest.py: (test_print_expectations_no_tests_found): Added test for the case when no tests are found. Canonical link: https://commits.webkit.org/316162@main --- .../webkitpy/layout_tests/controllers/manager.py | 2 ++ .../layout_tests/controllers/manager_unittest.py | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/Tools/Scripts/webkitpy/layout_tests/controllers/manager.py b/Tools/Scripts/webkitpy/layout_tests/controllers/manager.py index cd10a8cf064d..98f86fcf6076 100644 --- a/Tools/Scripts/webkitpy/layout_tests/controllers/manager.py +++ b/Tools/Scripts/webkitpy/layout_tests/controllers/manager.py @@ -955,6 +955,8 @@ def print_expectations(self, args): aggregate_tests = aggregate_tests_to_run | aggregate_tests_to_skip self._printer.print_found(len(aggregate_tests), len(aggregate_tests_to_run), self._options.repeat_each, self._options.iterations) + if not aggregate_tests: + continue test_col_width = max(len(test.test_path) for test in aggregate_tests) + 1 self._print_expectations_for_subset(device_type_list[0], test_col_width, tests_to_run_by_device[device_type_list[0]], aggregate_tests_to_skip) diff --git a/Tools/Scripts/webkitpy/layout_tests/controllers/manager_unittest.py b/Tools/Scripts/webkitpy/layout_tests/controllers/manager_unittest.py index b799df2b5722..da1b3d475c84 100644 --- a/Tools/Scripts/webkitpy/layout_tests/controllers/manager_unittest.py +++ b/Tools/Scripts/webkitpy/layout_tests/controllers/manager_unittest.py @@ -141,3 +141,14 @@ def parse_exp(test_names, expectations): # so if the output is not *exactly* as expected including whitespaces, this # could lead to unwanted effects, like blocking builds for a long time. self.assertEqual(get_printed_expectations(), out) + + def test_print_expectations_no_tests_found(self): + # Passing a non-existent path should not raise ValueError from max() on an + # empty sequence; it should return 0 cleanly. + manager = self._get_manager() + manager._options.update(repeat_each=1, iterations=1) + device_type_list = manager._port.supported_device_types() + manager._create_port_for_driver = Mock(return_value=manager._port) + manager._collect_tests = Mock(return_value=({dt: [] for dt in device_type_list}, set())) + exit_code = manager.print_expectations(['this/file/does/not/exist.html']) + self.assertEqual(exit_code, 0) From 8d85f545cea5c8741fd893cbfb172a92e647c5a7 Mon Sep 17 00:00:00 2001 From: Marcus Plutowski Date: Tue, 30 Jun 2026 10:58:18 -0700 Subject: [PATCH 49/84] Re-enable MTE hard-mode https://bugs.webkit.org/show_bug.cgi?id=318170 rdar://165772439 Reviewed by Yusuke Suzuki and Dan Hecht. This was disabled (or rather, soft-mode was enabled) for the benefit of internal development stability, a need which has since passed. This patch removes the soft-mode entitlments and thus re-enables MTE hard-mode for all processes. * Source/WebKit/Scripts/process-entitlements.sh: remove soft-mode entitlments Canonical link: https://commits.webkit.org/316163@main --- Source/WebKit/Scripts/process-entitlements.sh | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/Source/WebKit/Scripts/process-entitlements.sh b/Source/WebKit/Scripts/process-entitlements.sh index 3c47c4d486e0..1dd610b479b5 100755 --- a/Source/WebKit/Scripts/process-entitlements.sh +++ b/Source/WebKit/Scripts/process-entitlements.sh @@ -90,7 +90,6 @@ function mac_process_gpu_entitlements() then plistbuddy Add :com.apple.security.hardened-process.checked-allocations.no-tagged-receive bool YES fi - plistbuddy Add :com.apple.security.hardened-process.checked-allocations.soft-mode bool YES # FIXME: Should be removed before release fi } @@ -140,7 +139,6 @@ function mac_process_network_entitlements() then plistbuddy Add :com.apple.security.hardened-process.checked-allocations.no-tagged-receive bool YES fi - plistbuddy Add :com.apple.security.hardened-process.checked-allocations.soft-mode bool YES # FIXME: Should be removed before release # FIXME: This should be removed after crash investigation as part of plistbuddy Add :com.apple.private.get-system-corpse bool YES @@ -232,7 +230,6 @@ function mac_process_webcontent_shared_entitlements() then plistbuddy Add :com.apple.security.hardened-process.checked-allocations.no-tagged-receive bool YES fi - plistbuddy Add :com.apple.security.hardened-process.checked-allocations.soft-mode bool YES # FIXME: Should be removed before release plistbuddy Add :com.apple.private.webkit.use-xpc-endpoint bool YES plistbuddy Add :com.apple.rootless.storage.WebKitWebContentSandbox bool YES @@ -292,11 +289,6 @@ function maccatalyst_process_webcontent_shared_entitlements() plistbuddy Add :com.apple.private.webkit.use-xpc-endpoint bool YES plistbuddy Add :com.apple.runningboard.assertions.webkit bool YES - if (( "${TARGET_MAC_OS_X_VERSION_MAJOR}" >= 260000 )) - then - plistbuddy Add :com.apple.security.hardened-process.checked-allocations.soft-mode bool YES # FIXME: Should be removed before release - fi - if [[ "${WK_USE_FATAL_EXCEPTIONS}" == YES ]] then plistbuddy Add :com.apple.security.fatal-exceptions array @@ -365,7 +357,6 @@ function maccatalyst_process_gpu_entitlements() plistbuddy Add :com.apple.QuartzCore.webkit-limited-types bool YES plistbuddy Add :com.apple.private.coremedia.allow-fps-attachment bool YES plistbuddy Add :com.apple.developer.hardened-process bool YES - plistbuddy Add :com.apple.security.hardened-process.checked-allocations.soft-mode bool YES # FIXME: Should be removed before release if [[ "${WK_USE_FATAL_EXCEPTIONS}" == YES ]] then @@ -391,7 +382,6 @@ function maccatalyst_process_network_entitlements() plistbuddy Add :com.apple.runningboard.assertions.webkit bool YES plistbuddy Add :com.apple.private.webkit.use-xpc-endpoint bool YES plistbuddy Add :com.apple.developer.hardened-process bool YES - plistbuddy Add :com.apple.security.hardened-process.checked-allocations.soft-mode bool YES # FIXME: Should be removed before release plistbuddy Add :com.apple.private.tcc.manager.check-by-audit-token array plistbuddy Add :com.apple.private.tcc.manager.check-by-audit-token:0 string kTCCServiceWebKitIntelligentTrackingPrevention @@ -451,7 +441,6 @@ function ios_family_process_webcontent_shared_entitlements() plistbuddy add :com.apple.coreaudio.allow-vorbis-decode bool YES plistbuddy Add :com.apple.developer.hardened-process bool YES plistbuddy Add :com.apple.security.hardened-process.checked-allocations.no-tagged-receive bool YES - plistbuddy Add :com.apple.security.hardened-process.checked-allocations.soft-mode bool YES # FIXME: Should be removed before release if [[ "${WK_USE_FATAL_EXCEPTIONS}" == YES ]] then @@ -569,7 +558,6 @@ function ios_family_process_gpu_entitlements() plistbuddy Add :com.apple.developer.hardened-process bool YES plistbuddy Add :com.apple.security.hardened-process.checked-allocations.no-tagged-receive bool YES - plistbuddy Add :com.apple.security.hardened-process.checked-allocations.soft-mode bool YES # FIXME: Should be removed before release plistbuddy Add :com.apple.developer.kernel.extended-virtual-addressing bool YES } @@ -590,7 +578,6 @@ function ios_family_process_model_entitlements() plistbuddy Add :com.apple.private.pac.exception bool YES fi plistbuddy Add :com.apple.security.hardened-process.checked-allocations.no-tagged-receive bool YES - plistbuddy Add :com.apple.security.hardened-process.checked-allocations.soft-mode bool YES # FIXME: Should be removed before release } function ios_family_process_adattributiond_entitlements() @@ -663,7 +650,6 @@ function ios_family_process_network_entitlements() plistbuddy Add :com.apple.private.assets.accessible-asset-types:0 string com.apple.MobileAsset.WebContentRestrictions plistbuddy Add :com.apple.developer.hardened-process bool YES plistbuddy Add :com.apple.security.hardened-process.checked-allocations.no-tagged-receive bool YES - plistbuddy Add :com.apple.security.hardened-process.checked-allocations.soft-mode bool YES # FIXME: Should be removed before release plistbuddy Add :com.apple.private.security.mutable-state-flags array plistbuddy Add :com.apple.private.security.mutable-state-flags:0 string BlockNetworkAccess From 140ce5b5c1ce2e8a74af55850e65a948f43a24be Mon Sep 17 00:00:00 2001 From: Kai Tamkun Date: Tue, 30 Jun 2026 11:01:26 -0700 Subject: [PATCH 50/84] [JSC] Missing codeBlock->m_lock in repatchGetBySlowPathCall https://bugs.webkit.org/show_bug.cgi?id=312405 rdar://174630697 Reviewed by Yusuke Suzuki. Adds usage of GCSafeConcurrentJSLocker in three repatch methods. This avoids a data race. Test: JSTests/stress/regress-174630697.js * JSTests/stress/regress-174630697.js: Added. * Source/JavaScriptCore/bytecode/Repatch.cpp: (JSC::repatchGetBySlowPathCall): (JSC::repatchPutBySlowPathCall): (JSC::repatchInBySlowPathCall): Originally-landed-as: 305413.703@safari-7624-branch (bb9e30e27a73). rdar://180436444 Canonical link: https://commits.webkit.org/316164@main --- JSTests/stress/regress-174630697.js | 25 ++++++++++++++++++++++ Source/JavaScriptCore/bytecode/Repatch.cpp | 3 +++ 2 files changed, 28 insertions(+) create mode 100644 JSTests/stress/regress-174630697.js diff --git a/JSTests/stress/regress-174630697.js b/JSTests/stress/regress-174630697.js new file mode 100644 index 000000000000..5b422c5fafdd --- /dev/null +++ b/JSTests/stress/regress-174630697.js @@ -0,0 +1,25 @@ +const icCount = 100; +const structCount = 16; + +let body = "var x = 0;\n"; +for (let i = 0; i < icCount; i++) + body += "x += o.p;\n"; +body += "return x;\n"; + +let objs = []; +for (let i = 0; i < structCount; i++) { + let o = {}; + o["k" + i] = i; + o.p = 1; + objs.push(o); +} + +let f = new Function("o", body); + +for (let j = 0; j < 130; j++) + f(objs[j % structCount]); + +for (let j = 0; j < 100000; j++) + f(objs[j % structCount]); + +f(42); diff --git a/Source/JavaScriptCore/bytecode/Repatch.cpp b/Source/JavaScriptCore/bytecode/Repatch.cpp index 081e1f33a76e..fd98ba46efc3 100644 --- a/Source/JavaScriptCore/bytecode/Repatch.cpp +++ b/Source/JavaScriptCore/bytecode/Repatch.cpp @@ -796,6 +796,7 @@ void repatchGetBy(JSGlobalObject* globalObject, CodeBlock* codeBlock, JSValue ba // Mainly used to transition from megamorphic case to generic case. void repatchGetBySlowPathCall(CodeBlock* codeBlock, PropertyInlineCache& propertyCache, GetByKind kind) { + ConcurrentJSLocker locker(codeBlock->m_lock); resetGetBy(codeBlock, propertyCache, kind); repatchSlowPathCall(codeBlock, propertyCache, appropriateGetByGaveUpFunction(kind)); } @@ -1000,6 +1001,7 @@ static CodePtr NODELETE appropriatePutByGaveUpFunction(PutByKin // Mainly used to transition from megamorphic case to generic case. void repatchPutBySlowPathCall(CodeBlock* codeBlock, PropertyInlineCache& propertyCache, PutByKind kind) { + ConcurrentJSLocker locker(codeBlock->m_lock); resetPutBy(codeBlock, propertyCache, kind); repatchSlowPathCall(codeBlock, propertyCache, appropriatePutByGaveUpFunction(kind)); } @@ -1631,6 +1633,7 @@ inline CodePtr NODELETE appropriateInByGaveUpFunction(InByKind // Mainly used to transition from megamorphic case to generic case. void repatchInBySlowPathCall(CodeBlock* codeBlock, PropertyInlineCache& propertyCache, InByKind kind) { + ConcurrentJSLocker locker(codeBlock->m_lock); resetInBy(codeBlock, propertyCache, kind); repatchSlowPathCall(codeBlock, propertyCache, appropriateInByGaveUpFunction(kind)); } From 7deaf9ddf361672fb0b5183b85c57fe0b16df3db Mon Sep 17 00:00:00 2001 From: Alex Christensen Date: Tue, 30 Jun 2026 11:26:12 -0700 Subject: [PATCH 51/84] Prepare to replace WKJSScriptingBuffer with NSData https://bugs.webkit.org/show_bug.cgi?id=318234 rdar://181037054 Reviewed by Richard Robinson. This is the first in a 2-step process to remove WKJSScriptingBuffer. I got some API review feedback that WKJSScriptingBuffer is an odd wrapper around NSData so why not just use NSData directly? This change will allow Safari to switch from WKJSScriptingBuffer to NSData without breaking binary or source compatibility. The next change will change the parameter from id to NSData. Test: Tools/TestWebKitAPI/Tests/WebKit/WKWebView/JSBuffer.mm * Source/WebKit/UIProcess/API/Cocoa/WKUserContentController.h: * Source/WebKit/UIProcess/API/Cocoa/WKUserContentController.mm: (-[WKUserContentController addBuffer:name:contentWorld:]): * Tools/TestWebKitAPI/Tests/WebKit/WKWebView/JSBuffer.mm: (TEST(JSBuffer, Data)): Canonical link: https://commits.webkit.org/316165@main --- .../UIProcess/API/Cocoa/WKUserContentController.h | 2 +- .../UIProcess/API/Cocoa/WKUserContentController.mm | 10 ++++++++-- Tools/TestWebKitAPI/Tests/WebKit/WKWebView/JSBuffer.mm | 4 ++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Source/WebKit/UIProcess/API/Cocoa/WKUserContentController.h b/Source/WebKit/UIProcess/API/Cocoa/WKUserContentController.h index f72ac538ef0c..3ff9231dd841 100644 --- a/Source/WebKit/UIProcess/API/Cocoa/WKUserContentController.h +++ b/Source/WebKit/UIProcess/API/Cocoa/WKUserContentController.h @@ -150,7 +150,7 @@ WK_CLASS_AVAILABLE(macos(10.10), ios(8.0)) @param contentWorld The WKContentWorld to add the buffer to. The buffer will only be visible to JavaScript executing in that content world. */ -- (void)addBuffer:(WKJSScriptingBuffer *)buffer name:(NSString *)name contentWorld:(WKContentWorld *)world WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA), visionos(WK_XROS_TBA)); +- (void)addBuffer:(id)buffer name:(NSString *)name contentWorld:(WKContentWorld *)world WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA), visionos(WK_XROS_TBA)); /*! @abstract Removes a previously added data buffer from the given `WKContentWorld @param name The name of the buffer to remove. diff --git a/Source/WebKit/UIProcess/API/Cocoa/WKUserContentController.mm b/Source/WebKit/UIProcess/API/Cocoa/WKUserContentController.mm index 9d28b56cf0de..bb7abd8be445 100644 --- a/Source/WebKit/UIProcess/API/Cocoa/WKUserContentController.mm +++ b/Source/WebKit/UIProcess/API/Cocoa/WKUserContentController.mm @@ -231,9 +231,15 @@ - (void)removeAllScriptMessageHandlers protect(*_userContentControllerProxy)->removeAllUserMessageHandlers(); } -- (void)addBuffer:(WKJSScriptingBuffer *)buffer name:(NSString *)name contentWorld:(WKContentWorld *)world +- (void)addBuffer:(id)buffer name:(NSString *)name contentWorld:(WKContentWorld *)world { - protect(*_userContentControllerProxy)->addJSBuffer(Ref { *buffer->_buffer }, Ref { *world->_contentWorld }, name); + RetainPtr bufferToAdd; + if (RetainPtr data = dynamic_objc_cast(buffer)) + bufferToAdd = adoptNS([[WKJSScriptingBuffer alloc] initWithData:data.get()]); + else + bufferToAdd = dynamic_objc_cast(buffer); + + protect(*_userContentControllerProxy)->addJSBuffer(Ref { *bufferToAdd->_buffer }, Ref { *world->_contentWorld }, name); } - (void)removeBufferWithName:(NSString *)name contentWorld:(WKContentWorld *)world diff --git a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/JSBuffer.mm b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/JSBuffer.mm index d970733312f1..c60b93cb79f2 100644 --- a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/JSBuffer.mm +++ b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/JSBuffer.mm @@ -38,12 +38,12 @@ static const char constantString[] = "Hello world!"; RetainPtr oddLength = adoptNS([[WKJSScriptingBuffer alloc] initWithData:[NSData dataWithBytes:"abc" length:3]]); - RetainPtr evenLength = adoptNS([[_WKJSBuffer alloc] initWithData:[NSData dataWithBytes:"abcd" length:4]]); + RetainPtr evenLength = adoptNS([NSData dataWithBytes:"abcd" length:4]); RetainPtr invalidSurrogatePair = adoptNS([[_WKJSBuffer alloc] initWithData:[NSData dataWithBytes:"\x3d\xd8\x27\x00\xff\xff\x00\x00" length:8]]); RetainPtr readOnlyBuffer = adoptNS([[_WKJSBuffer alloc] initWithData:[NSData dataWithBytesNoCopy:(void *)constantString length:sizeof(constantString)-1 freeWhenDone:NO]]); RetainPtr configuration = adoptNS([WKWebViewConfiguration new]); [configuration.get().userContentController addBuffer:oddLength.get() name:@"oddLength" contentWorld:WKContentWorld.pageWorld]; - [configuration.get().userContentController _addBuffer:evenLength.get() contentWorld:WKContentWorld.pageWorld name:@"evenLength"]; + [configuration.get().userContentController addBuffer:evenLength.get() name:@"evenLength" contentWorld:WKContentWorld.pageWorld]; [configuration.get().userContentController _addBuffer:invalidSurrogatePair.get() contentWorld:WKContentWorld.pageWorld name:@"invalidSurrogatePair"]; [configuration.get().userContentController _addBuffer:readOnlyBuffer.get() contentWorld:WKContentWorld.pageWorld name:@"readOnlyBuffer"]; From a18c0a8d15801b652cd1db8724d3d07e129f14bd Mon Sep 17 00:00:00 2001 From: Rupin Mittal Date: Tue, 30 Jun 2026 11:32:05 -0700 Subject: [PATCH 52/84] Unvalidated replacementPath in NetworkConnectionToWebProcess::registerInternalFileBlobURL() could lead to wrongful file deletion https://bugs.webkit.org/show_bug.cgi?id=314149 rdar://175677292 Reviewed by Brady Eidson and Per Arne Vollan. NetworkConnectionToWebProcess::registerInternalFileBlobURL() takes in a a file path called replacementPath and a SandboxExtensionHandle. If the web process sends an empty handle, then the only validation done on replacementPath is isFilePathAllowed(), which may be too permissive. Later on, the web process could send the IPC NetworkConnectionToWebProcess::unregisterBlobURL() with a null topOrigin, which kicks off this chain: BlobData destroyed, BlobDataFileReference destroyed, FileSystem::deleteFile(m_replacementPath) which deleted the file at the replacementPath that was passed in. This may allow the web process to delete a file that it shouldn't be able to. To prevent this, we add a message check which confirms that if the web process would like to use the file at the replacementPath it specifies, it must have a sandbox extension allowing it to do so. Otherwise, the web process will be terminated. We also add a fixme comment to note that in the future, we should look into ensuring that the web process does not send file paths to the network process at all. * Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp: (WebKit::NetworkConnectionToWebProcess::registerInternalFileBlobURL): Originally-landed-as: 305413.843@safari-7624-branch (32b29db38205). rdar://180437008 Canonical link: https://commits.webkit.org/316166@main --- .../NetworkConnectionToWebProcess.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp index 7c8ab2ac6e19..f3ddc8929e2b 100644 --- a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp +++ b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp @@ -1151,6 +1151,7 @@ static bool shouldCheckBlobFileAccess() #endif } +// FIXME: (rdar://176402219) The web process should not send file paths to the network process. File paths should come from the UI process. void NetworkConnectionToWebProcess::registerInternalFileBlobURL(const URL& url, const String& path, const String& replacementPath, SandboxExtension::Handle&& extensionHandle, const String& contentType) { MESSAGE_CHECK(!url.isEmpty()); @@ -1168,14 +1169,12 @@ void NetworkConnectionToWebProcess::registerInternalFileBlobURL(const URL& url, // For transcoded files, check if the WebProcess has actual sandbox access // via the extension granted for the original file, rather than checking // our internal allowed paths list (which won't include temporary transcoded files). - if (sandboxExtension) { - // sandbox_check returns 0 on success (has access), non-zero on failure - if (sandbox_check(m_connection->remoteProcessID(), "file-read-data", static_cast(SANDBOX_FILTER_PATH | SANDBOX_CHECK_NO_REPORT), FileSystem::fileSystemRepresentation(replacementPath).data())) { - CONNECTION_RELEASE_LOG_ERROR(Sandbox, "registerInternalFileBlobURL: WebProcess does not have sandbox access to replacementPath"); - MESSAGE_CHECK(false); - } - } else // No sandbox extension provided, fall back to path allowlist check - MESSAGE_CHECK(isFilePathAllowed(*session, replacementPath)); + MESSAGE_CHECK(sandboxExtension); + // sandbox_check returns 0 on success (has access), non-zero on failure + if (sandbox_check(m_connection->remoteProcessID(), "file-read-data", static_cast(SANDBOX_FILTER_PATH | SANDBOX_CHECK_NO_REPORT), FileSystem::fileSystemRepresentation(replacementPath).data())) { + CONNECTION_RELEASE_LOG_ERROR(Sandbox, "registerInternalFileBlobURL: WebProcess does not have sandbox access to replacementPath"); + MESSAGE_CHECK(false); + } #else MESSAGE_CHECK(isFilePathAllowed(*session, replacementPath)); #endif From 6aa96bce5d11350a8e088781eede139a860d8bb6 Mon Sep 17 00:00:00 2001 From: Rupin Mittal Date: Tue, 30 Jun 2026 11:33:15 -0700 Subject: [PATCH 53/84] Missing allowsFirstPartyForCookies check in loadPing() IPC may lead to cross-origin cookie access https://bugs.webkit.org/show_bug.cgi?id=313496 rdar://174708224 Reviewed by Charlie Wolfe and Sihui Liu. A web process could send an IPC message containing a cross-site origin and loadPing() would then send the request to the cross-site origin which would respond with its cookies. To prevent this, add a MESSAGE_CHECK in loadPing() which will check if the web process has first party cookie access to the origin in the request and terminate the web process if not. New layout test confirms that we hit the message check: loadping-firstpartyforcookies-message-check.html * LayoutTests/ipc/coreipc.js: (export.ArgumentSerializer): * LayoutTests/ipc/loadping-firstpartyforcookies-message-check-expected.txt: Added. * LayoutTests/ipc/loadping-firstpartyforcookies-message-check.html: Added. * Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp: (WebKit::NetworkConnectionToWebProcess::loadPing): Originally-landed-as: 305413.799@safari-7624-branch (fd5906672902). rdar://180435156 Canonical link: https://commits.webkit.org/316167@main --- LayoutTests/ipc/coreipc.js | 8 +- ...partyforcookies-message-check-expected.txt | 3 + ...ng-firstpartyforcookies-message-check.html | 149 ++++++++++++++++++ .../NetworkConnectionToWebProcess.cpp | 1 + 4 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 LayoutTests/ipc/loadping-firstpartyforcookies-message-check-expected.txt create mode 100644 LayoutTests/ipc/loadping-firstpartyforcookies-message-check.html diff --git a/LayoutTests/ipc/coreipc.js b/LayoutTests/ipc/coreipc.js index 03ba39ec36c5..a15c63ed87c4 100644 --- a/LayoutTests/ipc/coreipc.js +++ b/LayoutTests/ipc/coreipc.js @@ -759,6 +759,10 @@ export class ArgumentSerializer { if (argument === null) { return []; } else throw new SerializationError(`std::nullptr_t is not null`); + case 'std::monostate': + if (argument === null) { + return []; + } else throw new SerializationError(`std::monostate is not null`); case 'WebCore::SharedMemory::Handle': case 'WebCore::SharedMemoryHandle': case 'MachSendRight': @@ -1199,7 +1203,9 @@ export class ArgumentParser { return [position, {parsedValue: result, parsedType: 'String'}]; } case 'std::nullptr_t': - return [position, {parsedValue: 'null', parsedType: 'std::nullptr_t'}]; + return [position, {parsedValue: null, parsedType: 'std::nullptr_t'}]; + case 'std::monostate': + return [position, {parsedValue: null, parsedType: 'std::monostate'}]; } return undefined; } diff --git a/LayoutTests/ipc/loadping-firstpartyforcookies-message-check-expected.txt b/LayoutTests/ipc/loadping-firstpartyforcookies-message-check-expected.txt new file mode 100644 index 000000000000..b7c9dd005623 --- /dev/null +++ b/LayoutTests/ipc/loadping-firstpartyforcookies-message-check-expected.txt @@ -0,0 +1,3 @@ + +PASS LoadPing rejects forged firstPartyForCookies via MESSAGE_CHECK + diff --git a/LayoutTests/ipc/loadping-firstpartyforcookies-message-check.html b/LayoutTests/ipc/loadping-firstpartyforcookies-message-check.html new file mode 100644 index 000000000000..8b5c383161f3 --- /dev/null +++ b/LayoutTests/ipc/loadping-firstpartyforcookies-message-check.html @@ -0,0 +1,149 @@ + +Test that LoadPing validates firstPartyForCookies via MESSAGE_CHECK + + + + + diff --git a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp index f3ddc8929e2b..dbbc7345e7b7 100644 --- a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp +++ b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp @@ -669,6 +669,7 @@ void NetworkConnectionToWebProcess::testProcessIncomingSyncMessagesWhenWaitingFo void NetworkConnectionToWebProcess::loadPing(NetworkResourceLoadParameters&& loadParameters) { + MESSAGE_CHECK(m_networkProcess->allowsFirstPartyForCookies(m_webProcessIdentifier, loadParameters.request.firstPartyForCookies()) == NetworkProcess::AllowCookieAccess::Allow); CONNECTION_RELEASE_LOG(Loading, "loadPing: (parentPID=%d, pageProxyID=%" PRIu64 ", webPageID=%" PRIu64 ", frameID=%" PRIu64 ", resourceID=%" PRIu64 ")", loadParameters.parentPID, loadParameters.webPageProxyID.toUInt64(), loadParameters.webPageID.toUInt64(), loadParameters.webFrameID.toUInt64(), loadParameters.identifier ? loadParameters.identifier->toUInt64() : 0); auto completionHandler = [connection = m_connection, identifier = *loadParameters.identifier] (const ResourceError& error, const ResourceResponse& response) { From 20202846a16f7e7b51ed01f4fad523d23ac72ae4 Mon Sep 17 00:00:00 2001 From: Andy Estes Date: Tue, 30 Jun 2026 11:36:03 -0700 Subject: [PATCH 54/84] [iOS] Remove support for AVMediaSource from MediaDeviceRoute https://bugs.webkit.org/show_bug.cgi?id=318166 rdar://178555755 Reviewed by Jer Noble. Removed support for AVMediaSource from MediaDeviceRoute and simplified the code now that an AVPlaybackControl-conforming object is the only observee. No new tests. Covered by existing tests. * Source/WebCore/platform/audio/ios/MediaDeviceRoute.h: (WebCore::MediaDeviceRouteClient::errorDidChange): (WebCore::MediaDeviceRouteClient::playbackPositionDidChange): (WebCore::MediaDeviceRouteClient::playbackErrorDidChange): Deleted. (WebCore::MediaDeviceRouteClient::currentPlaybackPositionDidChange): Deleted. * Source/WebCore/platform/audio/ios/MediaDeviceRoute.mm: (-[WebPlaybackControlObserver setPlaybackControl:]): (-[WebPlaybackControlObserver observeValueForKeyPath:ofObject:change:context:]): (-[WebPlaybackControlObserver dealloc]): (WebCore::convert): (WebCore::MediaDeviceRoute::setPlaybackPosition): (-[WebMediaSourceObserver initWithRoute:]): Deleted. (-[WebMediaSourceObserver mediaSource]): Deleted. (-[WebMediaSourceObserver setMediaSource:]): Deleted. (-[WebMediaSourceObserver playbackControl]): Deleted. (-[WebMediaSourceObserver setPlaybackControl:]): Deleted. (-[WebMediaSourceObserver observeValueForKeyPath:ofObject:change:context:]): Deleted. (-[WebMediaSourceObserver dealloc]): Deleted. (WebCore::MediaDeviceRoute::playbackError const): Deleted. (WebCore::MediaDeviceRoute::currentPlaybackPosition const): Deleted. (WebCore::MediaDeviceRoute::setCurrentPlaybackPosition): Deleted. * Source/WebCore/platform/graphics/MediaPlayerPrivateWirelessPlayback.cpp: (WebCore::MediaPlayerPrivateWirelessPlayback::seekToTarget): (WebCore::MediaPlayerPrivateWirelessPlayback::errorDidChange): (WebCore::MediaPlayerPrivateWirelessPlayback::playbackPositionDidChange): (WebCore::MediaPlayerPrivateWirelessPlayback::ensureTimebase): (WebCore::MediaPlayerPrivateWirelessPlayback::playbackErrorDidChange): Deleted. (WebCore::MediaPlayerPrivateWirelessPlayback::currentPlaybackPositionDidChange): Deleted. * Source/WebCore/platform/graphics/MediaPlayerPrivateWirelessPlayback.h: Canonical link: https://commits.webkit.org/316168@main --- .../platform/audio/ios/MediaDeviceRoute.h | 14 +- .../platform/audio/ios/MediaDeviceRoute.mm | 168 ++++-------------- .../MediaPlayerPrivateWirelessPlayback.cpp | 18 +- .../MediaPlayerPrivateWirelessPlayback.h | 4 +- 4 files changed, 52 insertions(+), 152 deletions(-) diff --git a/Source/WebCore/platform/audio/ios/MediaDeviceRoute.h b/Source/WebCore/platform/audio/ios/MediaDeviceRoute.h index e08f7c16faa1..639b033f5272 100644 --- a/Source/WebCore/platform/audio/ios/MediaDeviceRoute.h +++ b/Source/WebCore/platform/audio/ios/MediaDeviceRoute.h @@ -39,7 +39,7 @@ #include #include -OBJC_CLASS WebMediaSourceObserver; +OBJC_CLASS WebPlaybackControlObserver; namespace WebCore { @@ -91,9 +91,9 @@ class MediaDeviceRouteClient : public AbstractRefCountedAndCanMakeWeakPtr playbackError() const; + std::optional error() const; Vector audioOptions() const; - MediaTime currentPlaybackPosition() const; + MediaTime playbackPosition() const; bool playing() const; float playbackSpeed() const; float scanSpeed() const; bool muted() const; float volume() const; - void setCurrentPlaybackPosition(MediaTime); + void setPlaybackPosition(MediaTime); void setPlaying(bool); void setPlaybackSpeed(float); void setScanSpeed(float); @@ -141,7 +141,7 @@ class MediaDeviceRoute final : public RefCountedAndCanMakeWeakPtr m_platformRoute; - RetainPtr m_mediaSourceObserver; + RetainPtr m_playbackControlObserver; WeakPtr m_client; #if HAVE(AVROUTING_FRAMEWORK) RetainPtr m_routeSession; diff --git a/Source/WebCore/platform/audio/ios/MediaDeviceRoute.mm b/Source/WebCore/platform/audio/ios/MediaDeviceRoute.mm index 13230fa39b2d..16bab672f212 100644 --- a/Source/WebCore/platform/audio/ios/MediaDeviceRoute.mm +++ b/Source/WebCore/platform/audio/ios/MediaDeviceRoute.mm @@ -36,14 +36,16 @@ #import -#define FOR_EACH_COMMON_READONLY_KEY_PATH(Macro) \ +#define FOR_EACH_READONLY_KEY_PATH(Macro) \ Macro(timeRange, TimeRange, MediaTimeRange) \ Macro(ready, Ready, bool) \ Macro(buffering, Buffering, bool) \ Macro(audioOptions, AudioOptions, Vector) \ + Macro(error, Error, std::optional) \ + Macro(playbackPosition, PlaybackPosition, MediaTime) \ \ -#define FOR_EACH_COMMON_READWRITE_KEY_PATH(Macro) \ +#define FOR_EACH_READWRITE_KEY_PATH(Macro) \ Macro(playing, Playing, bool) \ Macro(playbackSpeed, PlaybackSpeed, float) \ Macro(scanSpeed, ScanSpeed, float) \ @@ -51,43 +53,16 @@ Macro(volume, Volume, float) \ \ -#define FOR_EACH_COMMON_KEY_PATH(Macro) \ - FOR_EACH_COMMON_READONLY_KEY_PATH(Macro) \ - FOR_EACH_COMMON_READWRITE_KEY_PATH(Macro) \ +#define FOR_EACH_KEY_PATH(Macro) \ + FOR_EACH_READONLY_KEY_PATH(Macro) \ + FOR_EACH_READWRITE_KEY_PATH(Macro) \ \ -#define FOR_EACH_MEDIA_SOURCE_READONLY_KEY_PATH(Macro) \ - FOR_EACH_COMMON_READONLY_KEY_PATH(Macro) \ - Macro(playbackError, PlaybackError, std::optional) \ -\ - -#define FOR_EACH_MEDIA_SOURCE_READWRITE_KEY_PATH(Macro) \ - FOR_EACH_COMMON_READWRITE_KEY_PATH(Macro) \ - Macro(currentPlaybackPosition, CurrentPlaybackPosition, MediaTime) \ -\ - -#define FOR_EACH_MEDIA_SOURCE_KEY_PATH(Macro) \ - FOR_EACH_MEDIA_SOURCE_READONLY_KEY_PATH(Macro) \ - FOR_EACH_MEDIA_SOURCE_READWRITE_KEY_PATH(Macro) \ -\ - -#define FOR_EACH_PLAYBACK_CONTROL_KEY_PATH(Macro) \ - FOR_EACH_COMMON_KEY_PATH(Macro) \ -\ - -#define ADD_MEDIA_SOURCE_OBSERVER(KeyPath, SetterSuffix, Type) \ - [_mediaSource addObserver:self forKeyPath:@#KeyPath options:NSKeyValueObservingOptionInitial context:WebMediaSourceObserverContext]; \ -\ - -#define REMOVE_MEDIA_SOURCE_OBSERVER(KeyPath, SetterSuffix, Type) \ - [_mediaSource removeObserver:self forKeyPath:@#KeyPath context:WebMediaSourceObserverContext]; \ -\ - -#define ADD_PLAYBACK_CONTROL_OBSERVER(KeyPath, SetterSuffix, Type) \ +#define ADD_OBSERVER(KeyPath, SetterSuffix, Type) \ [_playbackControl addObserver:self forKeyPath:@#KeyPath options:NSKeyValueObservingOptionInitial context:WebPlaybackControlObserverContext]; \ \ -#define REMOVE_PLAYBACK_CONTROL_OBSERVER(KeyPath, SetterSuffix, Type) \ +#define REMOVE_OBSERVER(KeyPath, SetterSuffix, Type) \ [_playbackControl removeObserver:self forKeyPath:@#KeyPath context:WebPlaybackControlObserverContext]; \ \ @@ -108,37 +83,30 @@ #define DEFINE_GETTER(KeyPath, SetterSuffix, Type) \ Type MediaDeviceRoute::KeyPath() const \ { \ - if (RetainPtr playbackControl = [m_mediaSourceObserver playbackControl]) \ - return convert(playbackControl.get().KeyPath); \ - return convert([m_mediaSourceObserver mediaSource].KeyPath); \ + return convert([m_playbackControlObserver playbackControl].KeyPath); \ } \ \ #define DEFINE_SETTER(KeyPath, SetterSuffix, Type) \ void MediaDeviceRoute::set##SetterSuffix(Type KeyPath) \ { \ - if (RetainPtr playbackControl = [m_mediaSourceObserver playbackControl]) \ - return [playbackControl set##SetterSuffix:convert(WTF::move(KeyPath))]; \ - [[m_mediaSourceObserver mediaSource] set##SetterSuffix:convert(WTF::move(KeyPath))]; \ + [[m_playbackControlObserver playbackControl] set##SetterSuffix:convert(WTF::move(KeyPath))]; \ } \ \ NS_ASSUME_NONNULL_BEGIN -static void* WebMediaSourceObserverContext = &WebMediaSourceObserverContext; static void* WebPlaybackControlObserverContext = &WebPlaybackControlObserverContext; -@interface WebMediaSourceObserver : NSObject +@interface WebPlaybackControlObserver : NSObject + (instancetype)new NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithRoute:(WebCore::MediaDeviceRoute&)route NS_DESIGNATED_INITIALIZER; -@property (nonatomic, nullable, strong) AVMediaSource *mediaSource; @property (nonatomic, nullable, strong) AVPlaybackControl *playbackControl; @end -@implementation WebMediaSourceObserver { +@implementation WebPlaybackControlObserver { WeakPtr _route; - RetainPtr _mediaSource; RetainPtr _playbackControl; } @@ -151,23 +119,6 @@ - (instancetype)initWithRoute:(WebCore::MediaDeviceRoute&)route return self; } -- (AVMediaSource * _Nullable)mediaSource -{ - return _mediaSource.get(); -} - -- (void)setMediaSource:(AVMediaSource * _Nullable)mediaSource -{ - if (mediaSource) - self.playbackControl = nil; - - FOR_EACH_MEDIA_SOURCE_KEY_PATH(REMOVE_MEDIA_SOURCE_OBSERVER) - - _mediaSource = mediaSource; - - FOR_EACH_MEDIA_SOURCE_KEY_PATH(ADD_MEDIA_SOURCE_OBSERVER) -} - - (AVPlaybackControl * _Nullable)playbackControl { return _playbackControl.get(); @@ -175,60 +126,29 @@ - (AVPlaybackControl * _Nullable)playbackControl - (void)setPlaybackControl:(AVPlaybackControl * _Nullable)playbackControl { - if (playbackControl) - self.mediaSource = nil; - - FOR_EACH_PLAYBACK_CONTROL_KEY_PATH(REMOVE_PLAYBACK_CONTROL_OBSERVER) - REMOVE_PLAYBACK_CONTROL_OBSERVER(error, Error, std::optional) - REMOVE_PLAYBACK_CONTROL_OBSERVER(playbackPosition, PlaybackPosition, AVPlaybackUserInterfacePlaybackPosition *) + FOR_EACH_KEY_PATH(REMOVE_OBSERVER) _playbackControl = playbackControl; - FOR_EACH_PLAYBACK_CONTROL_KEY_PATH(ADD_PLAYBACK_CONTROL_OBSERVER) - ADD_PLAYBACK_CONTROL_OBSERVER(error, Error, std::optional) - ADD_PLAYBACK_CONTROL_OBSERVER(playbackPosition, PlaybackPosition, AVPlaybackUserInterfacePlaybackPosition *) + FOR_EACH_KEY_PATH(ADD_OBSERVER) } - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary *)change context:(nullable void*)context { - if (context != WebMediaSourceObserverContext && context != WebPlaybackControlObserverContext) { + if (context != WebPlaybackControlObserverContext) { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; return; } dispatch_async(mainDispatchQueueSingleton(), ^{ - if (context == WebMediaSourceObserverContext) { - FOR_EACH_MEDIA_SOURCE_KEY_PATH(OBSERVE_VALUE) - ASSERT_NOT_REACHED(); - return; - } - - if ([keyPath isEqualToString:@"error"]) { - if (RefPtr route = _route.get()) { - if (RefPtr client = route->client()) - client->playbackErrorDidChange(*route); - } - return; - } - if ([keyPath isEqualToString:@"playbackPosition"]) { - if (RefPtr route = _route.get()) { - if (RefPtr client = route->client()) - client->currentPlaybackPositionDidChange(*route); - } - return; - } - - FOR_EACH_PLAYBACK_CONTROL_KEY_PATH(OBSERVE_VALUE) + FOR_EACH_KEY_PATH(OBSERVE_VALUE) ASSERT_NOT_REACHED(); }); } - (void)dealloc { - FOR_EACH_MEDIA_SOURCE_KEY_PATH(REMOVE_MEDIA_SOURCE_OBSERVER) - FOR_EACH_PLAYBACK_CONTROL_KEY_PATH(REMOVE_PLAYBACK_CONTROL_OBSERVER) - REMOVE_PLAYBACK_CONTROL_OBSERVER(error, Error, std::optional) - REMOVE_PLAYBACK_CONTROL_OBSERVER(playbackPosition, PlaybackPosition, AVPlaybackUserInterfacePlaybackPosition *) + FOR_EACH_KEY_PATH(REMOVE_OBSERVER) [super dealloc]; } @@ -260,6 +180,11 @@ static MediaTime convert(CMTime time) return PAL::toMediaTime(time); } +static MediaTime convert(AVPlaybackUserInterfacePlaybackPosition *playbackPosition) +{ + return convert(playbackPosition.position); +} + static MediaTimeRange convert(CMTimeRange timeRange) { MediaTime start = PAL::toMediaTime(timeRange.start); @@ -301,7 +226,7 @@ static MediaTimeRange convert(CMTimeRange timeRange) MediaDeviceRoute::MediaDeviceRoute(WebMediaDevicePlatformRoute *platformRoute) : m_identifier { WTF::UUID::createVersion4() } , m_platformRoute { platformRoute } - , m_mediaSourceObserver { adoptNS([[WebMediaSourceObserver alloc] initWithRoute:*this]) } + , m_playbackControlObserver { adoptNS([[WebPlaybackControlObserver alloc] initWithRoute:*this]) } { } @@ -315,29 +240,10 @@ static MediaTimeRange convert(CMTimeRange timeRange) return m_platformRoute.get(); } -std::optional MediaDeviceRoute::playbackError() const +void MediaDeviceRoute::setPlaybackPosition(MediaTime playbackPosition) { - if (RetainPtr playbackControl = [m_mediaSourceObserver playbackControl]) - return convert(playbackControl.get().error); - return convert([m_mediaSourceObserver mediaSource].playbackError); -} - -MediaTime MediaDeviceRoute::currentPlaybackPosition() const -{ - if (RetainPtr playbackControl = [m_mediaSourceObserver playbackControl]) - return convert(playbackControl.get().playbackPosition.position); - return convert([m_mediaSourceObserver mediaSource].currentPlaybackPosition); -} - -void MediaDeviceRoute::setCurrentPlaybackPosition(MediaTime currentPlaybackPosition) -{ - if (RetainPtr playbackControl = [m_mediaSourceObserver playbackControl]) { - // FIXME: We should introduce a proper seek-with-tolerance function on MediaDeviceRoute rather than assuming a zero tolerance here. - [playbackControl seekToPosition:convert(WTF::move(currentPlaybackPosition)) tolerance:PAL::kCMTimeZero]; - return; - } - - [m_mediaSourceObserver mediaSource].currentPlaybackPosition = convert(WTF::move(currentPlaybackPosition)); + // FIXME: We should introduce a proper seek-with-tolerance function on MediaDeviceRoute rather than assuming a zero tolerance here. + [[m_playbackControlObserver playbackControl] seekToPosition:convert(WTF::move(playbackPosition)) tolerance:PAL::kCMTimeZero]; } MediaDeviceRoute::~MediaDeviceRoute() @@ -347,22 +253,16 @@ static MediaTimeRange convert(CMTimeRange timeRange) #endif } -FOR_EACH_COMMON_KEY_PATH(DEFINE_GETTER) -FOR_EACH_COMMON_READWRITE_KEY_PATH(DEFINE_SETTER) +FOR_EACH_KEY_PATH(DEFINE_GETTER) +FOR_EACH_READWRITE_KEY_PATH(DEFINE_SETTER) } // namespace WebCore -#undef FOR_EACH_COMMON_READONLY_KEY_PATH -#undef FOR_EACH_COMMON_READWRITE_KEY_PATH -#undef FOR_EACH_COMMON_KEY_PATH -#undef FOR_EACH_MEDIA_SOURCE_READONLY_KEY_PATH -#undef FOR_EACH_MEDIA_SOURCE_READWRITE_KEY_PATH -#undef FOR_EACH_MEDIA_SOURCE_KEY_PATH -#undef FOR_EACH_PLAYBACK_CONTROL_KEY_PATH -#undef ADD_MEDIA_SOURCE_OBSERVER -#undef REMOVE_MEDIA_SOURCE_OBSERVER -#undef ADD_PLAYBACK_CONTROL_OBSERVER -#undef REMOVE_PLAYBACK_CONTROL_OBSERVER +#undef FOR_EACH_READONLY_KEY_PATH +#undef FOR_EACH_READWRITE_KEY_PATH +#undef FOR_EACH_KEY_PATH +#undef ADD_OBSERVER +#undef REMOVE_OBSERVER #undef OBSERVE_VALUE #undef DEFINE_GETTER #undef DEFINE_SETTER diff --git a/Source/WebCore/platform/graphics/MediaPlayerPrivateWirelessPlayback.cpp b/Source/WebCore/platform/graphics/MediaPlayerPrivateWirelessPlayback.cpp index 115d8254dc3f..b4b547813277 100644 --- a/Source/WebCore/platform/graphics/MediaPlayerPrivateWirelessPlayback.cpp +++ b/Source/WebCore/platform/graphics/MediaPlayerPrivateWirelessPlayback.cpp @@ -280,7 +280,7 @@ void MediaPlayerPrivateWirelessPlayback::seekToTarget(const SeekTarget& seekTarg return; ALWAYS_LOG(LOGIDENTIFIER, seekTarget); - route->setCurrentPlaybackPosition(seekTarget.time); + route->setPlaybackPosition(seekTarget.time); } bool MediaPlayerPrivateWirelessPlayback::paused() const @@ -430,12 +430,12 @@ void MediaPlayerPrivateWirelessPlayback::readyDidChange(MediaDeviceRoute& route) setReadyState(MediaPlayerReadyState::HaveEnoughData); } -void MediaPlayerPrivateWirelessPlayback::playbackErrorDidChange(MediaDeviceRoute& route) +void MediaPlayerPrivateWirelessPlayback::errorDidChange(MediaDeviceRoute& route) { ASSERT(&route == this->route()); - ALWAYS_LOG(LOGIDENTIFIER, !!route.playbackError()); + ALWAYS_LOG(LOGIDENTIFIER, !!route.error()); - if (route.playbackError()) + if (route.error()) setNetworkState(route.ready() ? MediaPlayer::NetworkState::DecodeError : MediaPlayer::NetworkState::FormatError); } @@ -448,14 +448,14 @@ void MediaPlayerPrivateWirelessPlayback::audioOptionsDidChange(MediaDeviceRoute& player->characteristicChanged(); } -void MediaPlayerPrivateWirelessPlayback::currentPlaybackPositionDidChange(MediaDeviceRoute& route) +void MediaPlayerPrivateWirelessPlayback::playbackPositionDidChange(MediaDeviceRoute& route) { ASSERT(&route == this->route()); - auto currentPlaybackPosition = route.currentPlaybackPosition(); - ALWAYS_LOG(LOGIDENTIFIER, currentPlaybackPosition); + auto playbackPosition = route.playbackPosition(); + ALWAYS_LOG(LOGIDENTIFIER, playbackPosition); - updateTimebaseTimeAndRate(currentPlaybackPosition, route.playing() ? route.playbackSpeed() : 0); + updateTimebaseTimeAndRate(playbackPosition, route.playing() ? route.playbackSpeed() : 0); auto currentTime = this->currentTime(); @@ -492,7 +492,7 @@ CMTimebaseRef MediaPlayerPrivateWirelessPlayback::ensureTimebase() dispatch_activate(m_timerSource.get()); if (RefPtr route = this->route()) - updateTimebaseTimeAndRate(route->currentPlaybackPosition() ?: MediaTime::zeroTime(), route->playing() ? route->playbackSpeed() : 0); + updateTimebaseTimeAndRate(route->playbackPosition() ?: MediaTime::zeroTime(), route->playing() ? route->playbackSpeed() : 0); return m_timebase.get(); } diff --git a/Source/WebCore/platform/graphics/MediaPlayerPrivateWirelessPlayback.h b/Source/WebCore/platform/graphics/MediaPlayerPrivateWirelessPlayback.h index 8f599b6d1d01..89d553fa6f03 100644 --- a/Source/WebCore/platform/graphics/MediaPlayerPrivateWirelessPlayback.h +++ b/Source/WebCore/platform/graphics/MediaPlayerPrivateWirelessPlayback.h @@ -129,9 +129,9 @@ class MediaPlayerPrivateWirelessPlayback final // MediaDeviceRouteClient void timeRangeDidChange(MediaDeviceRoute&) final; void readyDidChange(MediaDeviceRoute&) final; - void playbackErrorDidChange(MediaDeviceRoute&) final; + void errorDidChange(MediaDeviceRoute&) final; void audioOptionsDidChange(MediaDeviceRoute&) final; - void currentPlaybackPositionDidChange(MediaDeviceRoute&) final; + void playbackPositionDidChange(MediaDeviceRoute&) final; CMTimebaseRef ensureTimebase(); void destroyTimebase(); From 6c94926886b44e8b5d50d95ba23c2cce00a32c16 Mon Sep 17 00:00:00 2001 From: Antti Koivisto Date: Tue, 30 Jun 2026 12:00:09 -0700 Subject: [PATCH 55/84] [css-mixins-1] Implement argument parsing in terms of first-valid() https://bugs.webkit.org/show_bug.cgi?id=318212 rdar://181014309 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed by Alan Baradlay and Sam Weinig. https://drafts.csswg.org/css-mixins-1/#evaluate-a-custom-function 3. Add a custom property to argument rule with a name of the parameter’s name, and a value of first-valid(arg value, default value). This patch implements first-valid() internally for @function use but does not yet expose it externally. * LayoutTests/imported/w3c/web-platform-tests/css/css-mixins/dashed-function-cycles-expected.txt: * LayoutTests/imported/w3c/web-platform-tests/css/css-mixins/dashed-function-eval-expected.txt: * LayoutTests/imported/w3c/web-platform-tests/css/css-mixins/function-attr-expected.txt: * Source/WebCore/style/StyleBuilder.cpp: (WebCore::Style::Builder::applyCustomPropertyFromCallingContext): A parameter shadows inherited custom properties; other properties are inherited lazily from the calling context. (WebCore::Style::Builder::applyCustomPropertyImpl): In a function context an invalid value resolves to guaranteed-invalid, not unset. (WebCore::Style::Builder::resolveCustomPropertyValue): Pass the registration so first-valid() can validate candidates against the target syntax. * Source/WebCore/style/StyleBuilder.h: * Source/WebCore/style/StyleSubstitutionResolver.cpp: (WebCore::Style::SubstitutionResolver::substituteFirstValid): Substitute to the first candidate valid for the target syntax, else guaranteed-invalid. (WebCore::Style::SubstitutionResolver::resolveAndRegisterDashedFunctionArguments): Build each parameter value as first-valid(arg, default). A missing positional argument for a parameter without a default makes the invocation guaranteed-invalid. (WebCore::Style::SubstitutionResolver::substituteDashedFunction): Leave a failed argument substitution guaranteed-invalid rather than aborting the function. * Source/WebCore/style/StyleSubstitutionResolver.h: Canonical link: https://commits.webkit.org/316169@main --- .../dashed-function-cycles-expected.txt | 6 +- .../dashed-function-eval-expected.txt | 24 ++-- .../css/css-mixins/function-attr-expected.txt | 2 +- Source/WebCore/style/StyleBuilder.cpp | 39 ++++-- Source/WebCore/style/StyleBuilder.h | 1 + .../style/StyleSubstitutionResolver.cpp | 119 ++++++++++++++---- .../WebCore/style/StyleSubstitutionResolver.h | 7 +- 7 files changed, 146 insertions(+), 52 deletions(-) diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-mixins/dashed-function-cycles-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/css/css-mixins/dashed-function-cycles-expected.txt index 6add1050a037..46035d2c174e 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/css/css-mixins/dashed-function-cycles-expected.txt +++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-mixins/dashed-function-cycles-expected.txt @@ -20,8 +20,8 @@ PASS Cycle through global, self PASS Cycle through local, other function PASS Cycle through local, other function, fallback in function PASS Cycle through various variables and other functions -FAIL Function in a cycle with its own default assert_equals: expected "PASS" but got "10px" -FAIL Cyclic defaults assert_equals: expected "42px PASS-y PASS-z" but got "42px var(--z) var(--y)" -FAIL Cyclic outer --b shadows custom property assert_equals: expected "PASS" but got "var(--b)" +PASS Function in a cycle with its own default +PASS Cyclic defaults +PASS Cyclic outer --b shadows custom property PASS Locals are function specific diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-mixins/dashed-function-eval-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/css/css-mixins/dashed-function-eval-expected.txt index 2cefce64fa61..fed4beebcf02 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/css/css-mixins/dashed-function-eval-expected.txt +++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-mixins/dashed-function-eval-expected.txt @@ -19,19 +19,19 @@ PASS Parameter with complex type (px) PASS Passing argument to inner function PASS var() in argument resolved before call PASS var() in argument resolved before call, typed -FAIL Argument captures IACVT due to invalid var() assert_equals: expected "PASS" but got "" -FAIL Argument captures IACVT due to invalid var(), typed assert_equals: expected "PASS" but got "" +PASS Argument captures IACVT due to invalid var() +PASS Argument captures IACVT due to invalid var(), typed PASS Argument captures IACVT due to type mismatch PASS Single parameter with default value PASS Multiple parameters with defaults PASS Multiple parameters with defaults, typed -FAIL Default referencing another parameter assert_equals: expected "5px 5px" but got "5px var(--x)" -FAIL Default referencing another parameter, local interference assert_equals: expected "17px 5px" but got "17px var(--x)" -FAIL Default referencing another defaulted parameter assert_equals: expected "5px 5px" but got "5px var(--x)" +PASS Default referencing another parameter +PASS Default referencing another parameter, local interference +PASS Default referencing another defaulted parameter FAIL Typed default with reference assert_equals: expected "5px 6px" but got "" -FAIL IACVT arguments are defaulted assert_equals: expected "1 2 3" but got "" -FAIL IACVT arguments are defaulted, typed assert_equals: expected "1 2 3" but got "" -FAIL Arguments are defaulted on type mismatch assert_equals: expected "1 2 3" but got "" +PASS IACVT arguments are defaulted +PASS IACVT arguments are defaulted, typed +PASS Arguments are defaulted on type mismatch PASS Unused local PASS Local does not affect outer scope PASS Substituting local in result @@ -55,9 +55,9 @@ PASS Inner function call should see resolved outer locals PASS Inner function call should see resolved outer locals (reverse) PASS Parameter shadows custom property PASS Local shadows parameter -FAIL IACVT argument shadows outer scope assert_equals: expected "PASS" but got "" -FAIL IACVT argument shadows outer scope, typed assert_equals: expected "PASS" but got "" -FAIL IACVT argument shadows outer scope, type mismatch assert_equals: expected "PASS" but got "FAIL" +PASS IACVT argument shadows outer scope +PASS IACVT argument shadows outer scope, typed +PASS IACVT argument shadows outer scope, type mismatch PASS Missing only argument PASS Missing one argument of several FAIL Passing list as only argument assert_equals: expected "1px,2px" but got "{1px,2px}" @@ -68,7 +68,7 @@ FAIL Passing {} as argument assert_equals: expected "{}" but got "{{}}" FAIL Passing non-whole-value {} as argument assert_equals: expected "foo{}" but got "{foo{}}" PASS Local variable with initial keyword PASS Local variable with initial keyword, defaulted -FAIL Local variable with initial keyword, no value via IACVT-capture assert_equals: expected "PASS" but got "" +PASS Local variable with initial keyword, no value via IACVT-capture PASS Default with initial keyword FAIL initial appearing via fallback assert_equals: expected "PASS" but got "" PASS Local variable with inherit keyword diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-mixins/function-attr-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/css/css-mixins/function-attr-expected.txt index 5d1141889307..b03177520afe 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/css/css-mixins/function-attr-expected.txt +++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-mixins/function-attr-expected.txt @@ -7,7 +7,7 @@ PASS Return attr(type()) from untyped function PASS Return attr(type()) from typed function PASS Return attr(type(*)) from typed function PASS Return attr(type(*)) from untyped function -FAIL attr() in default parameter value assert_equals: expected "42px" but got "784px" +PASS attr() in default parameter value PASS attr() in local variable PASS Returned url() is attr-tainted PASS Returned url() is attr-tainted, typed attr() diff --git a/Source/WebCore/style/StyleBuilder.cpp b/Source/WebCore/style/StyleBuilder.cpp index 8e490f1f9c31..367b615cc6a3 100644 --- a/Source/WebCore/style/StyleBuilder.cpp +++ b/Source/WebCore/style/StyleBuilder.cpp @@ -226,23 +226,33 @@ void Builder::applyCustomProperty(const AtomString& name) auto iterator = m_cascade.customProperties().find(name); if (iterator == m_cascade.customProperties().end()) { - // The property is not in this cascade, but a custom function body inherits the calling - // context's custom properties, which are resolved lazily. Resolve it there and copy in the - // computed value so var() references can find it. - if (auto* callingContextBuilder = m_state->callingContextBuilder()) { - callingContextBuilder->applyCustomProperty(name); - if (RefPtr value = callingContextBuilder->state().style().customPropertyValue(name)) { - bool isInherited = isInheritedCustomProperty(m_state->registeredProperty(name)); - m_state->style().setCustomPropertyValue(value.releaseNonNull(), isInherited); - } - m_state->m_appliedCustomProperties.add(name); - } + // A property missing from this cascade is resolved against the function evaluation context, if any. + if (m_state->callingContextBuilder()) + applyCustomPropertyFromCallingContext(name); return; } applyCustomPropertyImpl(name, iterator->value); } +// A custom property absent from a function's cascade is either a parameter (locally registered: it +// shadows inheritance and resolves to its registered value) or one inherited from the calling context +// (resolved lazily). https://drafts.csswg.org/css-mixins/#evaluating-custom-functions +void Builder::applyCustomPropertyFromCallingContext(const AtomString& name) +{ + auto* callingContextBuilder = m_state->callingContextBuilder(); + ASSERT(callingContextBuilder); + + if (m_state->registeredProperty(name)) + applyCustomProperty(name, CSSWideKeyword::Initial); + else { + callingContextBuilder->applyCustomProperty(name); + if (RefPtr value = callingContextBuilder->state().style().customPropertyValue(name)) + m_state->style().setCustomPropertyValue(value.releaseNonNull(), isInheritedCustomProperty(m_state->registeredProperty(name))); + } + m_state->m_appliedCustomProperties.add(name); +} + void Builder::applyCustomPropertyImpl(const AtomString& name, const PropertyCascade::Property& property) { if (!property.cssValue[SelectorChecker::MatchDefault]) @@ -264,6 +274,11 @@ void Builder::applyCustomPropertyImpl(const AtomString& name, const PropertyCasc // The computed value is the guaranteed-invalid value. if (!registered || registered->syntax.isUniversal()) return CustomProperty::createForGuaranteedInvalid(name); + // For a custom function's hypothetical element, an invalid value resolves to the guaranteed-invalid + // value (the parameter overrides inheritance), not unset. + // https://drafts.csswg.org/css-mixins/#evaluating-custom-functions + if (m_state->callingContextBuilder()) + return CustomProperty::createForGuaranteedInvalid(name); // Otherwise: // ...as if the property’s value had been specified as the unset keyword. return CSSWideKeyword::Unset; @@ -692,7 +707,7 @@ std::optional Builder::resolveCustomPropertyVa auto resolvedData = switchOn(value.value(), [&](const Ref& substitutionValue) -> RefPtr { - SubstitutionResolver substitutionResolver(*this); + SubstitutionResolver substitutionResolver(*this, registered); return substitutionResolver.substitute(substitutionValue.get()); }, [&](const Ref& data) -> RefPtr { diff --git a/Source/WebCore/style/StyleBuilder.h b/Source/WebCore/style/StyleBuilder.h index 3e10aa20a684..3612247790e9 100644 --- a/Source/WebCore/style/StyleBuilder.h +++ b/Source/WebCore/style/StyleBuilder.h @@ -69,6 +69,7 @@ class Builder { void applyLogicalGroupProperties(); void applyCustomProperties(); void applyCustomPropertyImpl(const AtomString&, const PropertyCascade::Property&); + void applyCustomPropertyFromCallingContext(const AtomString&); enum CustomPropertyCycleTracking { Enabled = 0, Disabled }; template diff --git a/Source/WebCore/style/StyleSubstitutionResolver.cpp b/Source/WebCore/style/StyleSubstitutionResolver.cpp index 2adde8fdbf79..8ad876adb9db 100644 --- a/Source/WebCore/style/StyleSubstitutionResolver.cpp +++ b/Source/WebCore/style/StyleSubstitutionResolver.cpp @@ -60,6 +60,7 @@ #include "StyleLocalPropertyRegistry.h" #include "StyleResolver.h" #include "StyleScope.h" +#include namespace WebCore { namespace Style { @@ -75,6 +76,24 @@ static bool containsURLTokens(std::span tokens) return false; } +static Ref createFirstValidVariableData(std::span> candidates, const CSSParserContext& context) +{ + // Uses the internal -internal-first-valid name rather than the public first-valid(). The public + // function must validate against any property's grammar, which is not implemented yet. See the + // FIXME on substituteFirstValid(). + Vector tokens; + tokens.append(CSSParserToken(FunctionToken, "-internal-first-valid"_s, CSSParserToken::BlockStart)); + bool isFirst = true; + for (auto& candidate : candidates) { + if (!isFirst) + tokens.append(CSSParserToken(CommaToken)); + isFirst = false; + tokens.append(candidate); + } + tokens.append(CSSParserToken(RightParenthesisToken, CSSParserToken::BlockEnd)); + return CSSVariableData::create(CSSParserTokenRange { tokens }, context); +} + void SubstitutionResolver::propagateAttrTaint(IsAttrTainted isAttrTainted, std::span tokens) { if (isAttrTainted != IsAttrTainted::Yes) @@ -84,8 +103,9 @@ void SubstitutionResolver::propagateAttrTaint(IsAttrTainted isAttrTainted, std:: m_hasTaintedURL = true; } -SubstitutionResolver::SubstitutionResolver(Builder& builder) +SubstitutionResolver::SubstitutionResolver(Builder& builder, const CSSRegisteredCustomProperty* registration) : m_styleBuilder(builder) + , m_registration(registration) { } @@ -173,12 +193,44 @@ bool SubstitutionResolver::substituteVariableFunction(CSSParserTokenRange range, return true; } +// https://drafts.csswg.org/css-values-5/#first-valid +// FIXME: This only validates against a custom property's registered syntax. The real, author-exposed +// first-valid() is a usable on any property, so candidates must be validated against the +// target property's grammar (e.g. by feeding them to the property parser) rather than only a registration. +bool SubstitutionResolver::substituteFirstValid(CSSParserTokenRange range, Vector& tokens, const CSSParserContext& context) +{ + for (unsigned i = 0; !range.atEnd(); ++i) { + auto candidateRange = CSSPropertyParserHelpers::consumeArgument(range, i); + if (!candidateRange) + break; + + auto substituted = substituteTokenRange(*candidateRange, context); + if (!substituted || substituted->isEmpty()) + continue; + + if (m_registration && !m_registration->syntax.isUniversal() + && !CSSPropertyParser::isValidCustomPropertyValueForSyntax(m_registration->syntax, CSSParserTokenRange { *substituted }, context)) + continue; + + tokens.appendVector(*substituted); + return true; + } + return false; +} + // https://drafts.csswg.org/css-mixins/#evaluate-a-custom-function // Registers each parameter with its type, resolves argument styles, then updates registrations // to universal syntax with resolved values as initial values. // Returns resolved argument properties to prepend to the body rule, or nullptr on failure. RefPtr SubstitutionResolver::resolveAndRegisterDashedFunctionArguments(const Vector& parameters, const Vector>& arguments, LocalPropertyRegistry& registrations) { + // A parameter without a default requires a corresponding argument. A missing one makes the whole + // invocation guaranteed-invalid (unlike a supplied-but-invalid argument, which defaults below). + for (auto [i, parameter] : indexedRange(parameters)) { + if (!parameter.defaultValue && i >= arguments.size()) + return nullptr; + } + // "For each function parameter, create a custom property registration with the parameter's type." auto argumentRegistrations = LocalPropertyRegistry { }; for (auto& parameter : parameters) { @@ -191,26 +243,42 @@ RefPtr SubstitutionResolver::resolveAndRegisterDashedFun // "Let argument rule be an initially empty style rule" with first-valid(arg value, default value) for each parameter. auto argumentRule = MutableStyleProperties::create(); - for (unsigned i = 0; i < parameters.size(); ++i) { - auto& parameter = parameters[i]; - auto argumentData = [&] -> RefPtr { - if (i < arguments.size() && !arguments[i].isEmpty()) - return CSSVariableData::create(CSSParserTokenRange { arguments[i] }, m_substitutionValue->context()); - return parameter.defaultValue; + for (auto [i, parameter] : indexedRange(parameters)) { + bool hasArgument = i < arguments.size() && !arguments[i].isEmpty(); + + // first-valid(arg value, default value). A parameter with neither is omitted from the rule, + // resolving to the guaranteed-invalid value via its (valueless) registration. + auto candidates = [&] { + Vector, 2> candidates; + if (hasArgument) + candidates.append(arguments[i].span()); + if (parameter.defaultValue) + candidates.append(parameter.defaultValue->tokens().span()); + return candidates; }(); - if (!argumentData) - return nullptr; + if (candidates.isEmpty()) + continue; + + // A bare CSS-wide keyword (e.g. a parameter defaulting to `inherit`) keeps its keyword semantics + // rather than becoming a literal value. https://drafts.csswg.org/css-mixins/#evaluating-custom-functions + auto primaryTokens = CSSParserTokenRange { candidates.first() }; + primaryTokens.consumeWhitespace(); + if (auto keyword = CSSPropertyParserHelpers::consumeCSSWideKeyword(primaryTokens); keyword && primaryTokens.atEnd()) { + argumentRule->addParsedProperty({ CSSPropertyCustom, CSSCustomPropertyValue::createWithCSSWideKeyword(parameter.name, *keyword) }); + continue; + } - // A bare CSS-wide keyword argument/default (e.g. a parameter defaulting to `inherit`) must keep - // its keyword semantics so it resolves against the calling context, rather than being treated as - // a literal universal value. https://drafts.csswg.org/css-mixins/#evaluating-custom-functions - auto value = [&] -> Ref { - auto tokens = argumentData->tokenRange(); - tokens.consumeWhitespace(); - if (auto keyword = CSSPropertyParserHelpers::consumeCSSWideKeyword(tokens); keyword && tokens.atEnd()) - return CSSCustomPropertyValue::createWithCSSWideKeyword(parameter.name, *keyword); - return CSSCustomPropertyValue::createSyntaxAll(parameter.name, argumentData.releaseNonNull()); - }(); + // Fast path: an untyped parameter with an argument needs no first-valid(). The argument is + // already substituted and, being non-empty, is always valid for the universal syntax, so it + // wins outright and the default is irrelevant. + if (hasArgument && parameter.type.isUniversal()) { + auto value = CSSCustomPropertyValue::createSyntaxAll(parameter.name, CSSVariableData::create(arguments[i], m_substitutionValue->context())); + argumentRule->addParsedProperty({ CSSPropertyCustom, WTF::move(value) }); + continue; + } + + auto firstValidData = createFirstValidVariableData(candidates.span(), m_substitutionValue->context()); + auto value = CSSCustomPropertyValue::createUnresolved(parameter.name, CSSSubstitutionValue::create(WTF::move(firstValidData))); argumentRule->addParsedProperty({ CSSPropertyCustom, WTF::move(value) }); } @@ -247,7 +315,7 @@ RefPtr SubstitutionResolver::resolveAndRegisterDashedFun }); if (resolvedValue && !resolvedValue->isGuaranteedInvalid()) { - auto tokenData = CSSVariableData::create(CSSParserTokenRange { resolvedValue->tokens() }); + auto tokenData = CSSVariableData::create(CSSParserTokenRange { resolvedValue->tokens() }, resolvedValue->isAttrTainted(), m_substitutionValue->context()); auto value = CSSCustomPropertyValue::createSyntaxAll(parameter.name, WTF::move(tokenData)); resolvedArgumentProperties->addParsedProperty({ CSSPropertyCustom, WTF::move(value) }); } @@ -290,9 +358,9 @@ bool SubstitutionResolver::substituteDashedFunction(StringView functionName, CSS if (!argumentRange) break; auto substituted = substituteTokenRange(*argumentRange, m_substitutionValue->context()); - if (!substituted) - return { }; - result.append(WTF::move(*substituted)); + // A failed substitution leaves the argument guaranteed-invalid (empty) so it defaults via + // first-valid(), rather than aborting. https://drafts.csswg.org/css-mixins/#replace-a-dashed-function + result.append(substituted.value_or(Vector { })); } if (result.size() > parameters.size()) return { }; @@ -701,6 +769,11 @@ std::optional> SubstitutionResolver::substituteTokenRange success = false; continue; } + if (token.value() == "-internal-first-valid"_s) { + if (!substituteFirstValid(range.consumeBlock(), tokens, context)) + success = false; + continue; + } if (isCustomPropertyName(token.value())) { // if (!substituteDashedFunction(token.value(), range.consumeBlock(), tokens)) diff --git a/Source/WebCore/style/StyleSubstitutionResolver.h b/Source/WebCore/style/StyleSubstitutionResolver.h index 7d2622cafde0..618b845709fe 100644 --- a/Source/WebCore/style/StyleSubstitutionResolver.h +++ b/Source/WebCore/style/StyleSubstitutionResolver.h @@ -36,6 +36,7 @@ class CSSValue; class CSSVariableData; class CSSSubstitutionValue; struct CSSParserContext; +struct CSSRegisteredCustomProperty; enum CSSPropertyID : uint16_t; enum CSSValueID : uint16_t; @@ -51,7 +52,9 @@ class LocalPropertyRegistry; // https://drafts.csswg.org/css-values-5/#arbitrary-substitution class SubstitutionResolver { public: - explicit SubstitutionResolver(Builder&); + // The registration is that of the custom property whose value is being resolved, if any. It is + // used to validate first-valid() candidates against the target syntax. + explicit SubstitutionResolver(Builder&, const CSSRegisteredCustomProperty* = nullptr); RefPtr substituteAndParse(const CSSSubstitutionValue&, CSSPropertyID); RefPtr substituteAndParseShorthand(const CSSShorthandSubstitutionValue&, CSSPropertyID); @@ -61,6 +64,7 @@ class SubstitutionResolver { std::optional> substituteTokenRange(CSSParserTokenRange, const CSSParserContext&); bool substituteVariableFunction(CSSParserTokenRange, CSSValueID, Vector&, const CSSParserContext&); + bool substituteFirstValid(CSSParserTokenRange, Vector&, const CSSParserContext&); bool substituteDashedFunction(StringView functionName, CSSParserTokenRange, Vector&); RefPtr resolveAndRegisterDashedFunctionArguments(const Vector&, const Vector>&, LocalPropertyRegistry&); bool substituteAttrFunction(CSSParserTokenRange, Vector&, const CSSParserContext&); @@ -83,6 +87,7 @@ class SubstitutionResolver { void propagateAttrTaint(IsAttrTainted, std::span); Builder& m_styleBuilder; + const CSSRegisteredCustomProperty* m_registration { nullptr }; RefPtr m_substitutionValue; Vector m_intermediateTokenStrings; Vector> m_intermediateCustomProperties; From f9eddcaf98287f34656c442729d52b77fe28b714 Mon Sep 17 00:00:00 2001 From: Ling Ho Date: Tue, 30 Jun 2026 12:01:05 -0700 Subject: [PATCH 56/84] Stored XSS on bugs.webkit.org in Commits extension; fix guidance (escape captured groups; optionally tighten \S+) https://bugs.webkit.org/show_bug.cgi?id=317709 rdar://180318956 Reviewed by Stephanie Lewis. Escape matched text in Commits extension link generation _replace_reference interpolated matched comment text into an tag without HTML-encoding it. Because bug_format_comment regexes run before html_quote in Bugzilla::Template::quoteUrls and their return values are reinserted after escaping, that text reached the page unescaped (core avoids this by calling html_quote itself). Fix: run both captured groups through Bugzilla::Util::html_quote before interpolation (primary fix), and narrow the two \S+ matchers to the legal commit-identifier charset [A-Za-z0-9._/-]+ as defense in depth. * Websites/bugs.webkit.org/extensions/Commits/Extension.pm: (bug_format_comment): (_replace_reference): Canonical link: https://commits.webkit.org/316170@main --- Websites/bugs.webkit.org/extensions/Commits/Extension.pm | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Websites/bugs.webkit.org/extensions/Commits/Extension.pm b/Websites/bugs.webkit.org/extensions/Commits/Extension.pm index 8573d2bd9c27..b143c8a9317d 100644 --- a/Websites/bugs.webkit.org/extensions/Commits/Extension.pm +++ b/Websites/bugs.webkit.org/extensions/Commits/Extension.pm @@ -26,6 +26,7 @@ use strict; use warnings; use parent qw(Bugzilla::Extension); +use Bugzilla::Util qw(html_quote); our $VERSION = "1.0.0"; @@ -36,14 +37,14 @@ sub bug_format_comment { # Should match "r12345" and "trac.webkit.org/r12345" but not "https://trac.webkit.org/r12345" push(@$regexes, { match => qr/(? \&_replace_reference }); push(@$regexes, { match => qr/(? \&_replace_reference }); - push(@$regexes, { match => qr/\b((? \&_replace_reference }); - push(@$regexes, { match => qr/\b((? \&_replace_reference }); + push(@$regexes, { match => qr/\b((? \&_replace_reference }); + push(@$regexes, { match => qr/\b((? \&_replace_reference }); } sub _replace_reference { my $args = shift; - my $text = $args->{matches}->[0]; - my $reference = $args->{matches}->[1]; + my $text = html_quote($args->{matches}->[0]); + my $reference = html_quote($args->{matches}->[1]); return qq{$text}; }; From 4b7a49ae210b8b428eef19e382d8246568a13f33 Mon Sep 17 00:00:00 2001 From: Devin Rousso Date: Tue, 30 Jun 2026 12:04:00 -0700 Subject: [PATCH 57/84] Web Inspector: Support ES2022 Private Methods https://bugs.webkit.org/show_bug.cgi?id=235859 Reviewed by Yusuke Suzuki. Unlike private fields that live as own-properties, private methods/accessors instead live in the class lexical scope behind a private brand. In order for Web Inspector to display them, they have to be gathered from the class scope of the object's class (and ancestor superclasses). * Source/JavaScriptCore/inspector/InjectedScriptSource.js: (let.InjectedScript.prototype._forEachPropertyDescriptor): * Source/JavaScriptCore/inspector/JSInjectedScriptHost.h: * Source/JavaScriptCore/inspector/JSInjectedScriptHost.cpp: (Inspector::JSInjectedScriptHost::getOwnPrivatePropertyMethods): Added. * Source/JavaScriptCore/inspector/JSInjectedScriptHostPrototype.cpp: (Inspector::JSInjectedScriptHostPrototype::finishCreation): (Inspector::jsInjectedScriptHostPrototypeFunctionGetOwnPrivatePropertyMethods): Added. * LayoutTests/inspector/runtime/getProperties-expected.txt: * LayoutTests/inspector/runtime/getDisplayableProperties-expected.txt: Canonical link: https://commits.webkit.org/316171@main --- .../getDisplayableProperties-expected.txt | 224 +++++++++++++----- .../runtime/getProperties-expected.txt | 216 ++++++++++++----- .../inspector/InjectedScriptSource.js | 23 ++ .../inspector/JSInjectedScriptHost.cpp | 177 +++++++++++++- .../inspector/JSInjectedScriptHost.h | 1 + .../JSInjectedScriptHostPrototype.cpp | 15 ++ 6 files changed, 532 insertions(+), 124 deletions(-) diff --git a/LayoutTests/inspector/runtime/getDisplayableProperties-expected.txt b/LayoutTests/inspector/runtime/getDisplayableProperties-expected.txt index 9688c4fddf4b..cbfd72377605 100644 --- a/LayoutTests/inspector/runtime/getDisplayableProperties-expected.txt +++ b/LayoutTests/inspector/runtime/getDisplayableProperties-expected.txt @@ -184,89 +184,173 @@ Internal Properties: Evaluating expression... Getting displayable properties... Properties: - "#instancePrivateProperty" => "instancePrivatePropertyValue parent" (string) [writable | enumerable | configurable | isOwn | isPrivate] - "#parentInstancePrivateProperty" => "parentInstancePrivatePropertyValue" (string) [writable | enumerable | configurable | isOwn | isPrivate] - "instancePublicProperty" => "instancePublicPropertyValue parent" (string) [writable | enumerable | configurable | isOwn] - "parentInstancePublicProperty" => "parentInstancePublicPropertyValue" (string) [writable | enumerable | configurable | isOwn] - "__proto__" => "PrivateMembersTestClassParent" (object) [writable | configurable | isOwn] + "#instancePrivateProperty" => "instancePrivatePropertyValue parent" (string) [writable | enumerable | configurable | isOwn | isPrivate] + "#parentInstancePrivateProperty" => "parentInstancePrivatePropertyValue" (string) [writable | enumerable | configurable | isOwn | isPrivate] + "#instancePrivateGetter" => get "get #instancePrivateGetter() { parent }" (function) [isOwn | isPrivate] + "#instancePrivateGetter" => set undefined (undefined) [isOwn | isPrivate] + "#instancePrivateGetterSetter" => get "get #instancePrivateGetterSetter() { parent }" (function) [isOwn | isPrivate] + "#instancePrivateGetterSetter" => set "set #instancePrivateGetterSetter(x) { parent }" (function) [isOwn | isPrivate] + "#instancePrivateMethod" => "#instancePrivateMethod() { parent }" (function) [isOwn | isPrivate] + "#instancePrivateSetter" => get undefined (undefined) [isOwn | isPrivate] + "#instancePrivateSetter" => set "set #instancePrivateSetter(x) { parent }" (function) [isOwn | isPrivate] + "#parentInstancePrivateGetter" => get "get #parentInstancePrivateGetter() { }" (function) [isOwn | isPrivate] + "#parentInstancePrivateGetter" => set undefined (undefined) [isOwn | isPrivate] + "#parentInstancePrivateGetterSetter" => get "get #parentInstancePrivateGetterSetter() { }" (function) [isOwn | isPrivate] + "#parentInstancePrivateGetterSetter" => set "set #parentInstancePrivateGetterSetter(x) { }" (function) [isOwn | isPrivate] + "#parentInstancePrivateMethod" => "#parentInstancePrivateMethod() { }" (function) [isOwn | isPrivate] + "#parentInstancePrivateSetter" => get undefined (undefined) [isOwn | isPrivate] + "#parentInstancePrivateSetter" => set "set #parentInstancePrivateSetter(x) { }" (function) [isOwn | isPrivate] + "instancePublicProperty" => "instancePublicPropertyValue parent" (string) [writable | enumerable | configurable | isOwn] + "parentInstancePublicProperty" => "parentInstancePublicPropertyValue" (string) [writable | enumerable | configurable | isOwn] + "__proto__" => "PrivateMembersTestClassParent" (object) [writable | configurable | isOwn] -- Running test case: Runtime.getDisplayableProperties.Private.Instance.Child Evaluating expression... Getting displayable properties... Properties: - "#instancePrivateProperty" => "instancePrivatePropertyValue parent" (string) [writable | enumerable | configurable | isOwn | isPrivate] - "#parentInstancePrivateProperty" => "parentInstancePrivatePropertyValue" (string) [writable | enumerable | configurable | isOwn | isPrivate] - "#instancePrivateProperty" => "instancePrivatePropertyValue child" (string) [writable | enumerable | configurable | isOwn | isPrivate] - "#childInstancePrivateProperty" => "childInstancePrivatePropertyValue" (string) [writable | enumerable | configurable | isOwn | isPrivate] - "instancePublicProperty" => "instancePublicPropertyValue child" (string) [writable | enumerable | configurable | isOwn] - "parentInstancePublicProperty" => "parentInstancePublicPropertyValue" (string) [writable | enumerable | configurable | isOwn] - "childInstancePublicProperty" => "childInstancePublicPropertyValue" (string) [writable | enumerable | configurable | isOwn] - "__proto__" => "PrivateMembersTestClassChild" (object) [writable | configurable | isOwn] + "#instancePrivateProperty" => "instancePrivatePropertyValue parent" (string) [writable | enumerable | configurable | isOwn | isPrivate] + "#parentInstancePrivateProperty" => "parentInstancePrivatePropertyValue" (string) [writable | enumerable | configurable | isOwn | isPrivate] + "#instancePrivateProperty" => "instancePrivatePropertyValue child" (string) [writable | enumerable | configurable | isOwn | isPrivate] + "#childInstancePrivateProperty" => "childInstancePrivatePropertyValue" (string) [writable | enumerable | configurable | isOwn | isPrivate] + "#instancePrivateGetter" => get "get #instancePrivateGetter() { parent }" (function) [isOwn | isPrivate] + "#instancePrivateGetter" => set undefined (undefined) [isOwn | isPrivate] + "#instancePrivateGetterSetter" => get "get #instancePrivateGetterSetter() { parent }" (function) [isOwn | isPrivate] + "#instancePrivateGetterSetter" => set "set #instancePrivateGetterSetter(x) { parent }" (function) [isOwn | isPrivate] + "#instancePrivateMethod" => "#instancePrivateMethod() { parent }" (function) [isOwn | isPrivate] + "#instancePrivateSetter" => get undefined (undefined) [isOwn | isPrivate] + "#instancePrivateSetter" => set "set #instancePrivateSetter(x) { parent }" (function) [isOwn | isPrivate] + "#parentInstancePrivateGetter" => get "get #parentInstancePrivateGetter() { }" (function) [isOwn | isPrivate] + "#parentInstancePrivateGetter" => set undefined (undefined) [isOwn | isPrivate] + "#parentInstancePrivateGetterSetter" => get "get #parentInstancePrivateGetterSetter() { }" (function) [isOwn | isPrivate] + "#parentInstancePrivateGetterSetter" => set "set #parentInstancePrivateGetterSetter(x) { }" (function) [isOwn | isPrivate] + "#parentInstancePrivateMethod" => "#parentInstancePrivateMethod() { }" (function) [isOwn | isPrivate] + "#parentInstancePrivateSetter" => get undefined (undefined) [isOwn | isPrivate] + "#parentInstancePrivateSetter" => set "set #parentInstancePrivateSetter(x) { }" (function) [isOwn | isPrivate] + "#childInstancePrivateGetter" => get "get #childInstancePrivateGetter() { }" (function) [isOwn | isPrivate] + "#childInstancePrivateGetter" => set undefined (undefined) [isOwn | isPrivate] + "#childInstancePrivateGetterSetter" => get "get #childInstancePrivateGetterSetter() { }" (function) [isOwn | isPrivate] + "#childInstancePrivateGetterSetter" => set "set #childInstancePrivateGetterSetter(x) { }" (function) [isOwn | isPrivate] + "#childInstancePrivateMethod" => "#childInstancePrivateMethod() { }" (function) [isOwn | isPrivate] + "#childInstancePrivateSetter" => get undefined (undefined) [isOwn | isPrivate] + "#childInstancePrivateSetter" => set "set #childInstancePrivateSetter(x) { }" (function) [isOwn | isPrivate] + "#instancePrivateGetter" => get "get #instancePrivateGetter() { child }" (function) [isOwn | isPrivate] + "#instancePrivateGetter" => set undefined (undefined) [isOwn | isPrivate] + "#instancePrivateGetterSetter" => get "get #instancePrivateGetterSetter() { child }" (function) [isOwn | isPrivate] + "#instancePrivateGetterSetter" => set "set #instancePrivateGetterSetter(x) { child }" (function) [isOwn | isPrivate] + "#instancePrivateMethod" => "#instancePrivateMethod() { child }" (function) [isOwn | isPrivate] + "#instancePrivateSetter" => get undefined (undefined) [isOwn | isPrivate] + "#instancePrivateSetter" => set "set #instancePrivateSetter(x) { child }" (function) [isOwn | isPrivate] + "instancePublicProperty" => "instancePublicPropertyValue child" (string) [writable | enumerable | configurable | isOwn] + "parentInstancePublicProperty" => "parentInstancePublicPropertyValue" (string) [writable | enumerable | configurable | isOwn] + "childInstancePublicProperty" => "childInstancePublicPropertyValue" (string) [writable | enumerable | configurable | isOwn] + "__proto__" => "PrivateMembersTestClassChild" (object) [writable | configurable | isOwn] -- Running test case: Runtime.getDisplayableProperties.Private.Constructor.Parent Evaluating expression... Getting displayable properties... Properties: - "#classPrivateProperty" => "classPrivatePropertyValue parent" (string) [writable | enumerable | configurable | isOwn | isPrivate] - "#parentClassPrivateProperty" => "parentClassPrivatePropertyValue" (string) [writable | enumerable | configurable | isOwn | isPrivate] - "length" => 0 (number) [configurable | isOwn] - "name" => "PrivateMembersTestClassParent" (string) [configurable | isOwn] - "prototype" => "PrivateMembersTestClassParent" (object) [isOwn] - "classPublicMethod" => "classPublicMethod() { parent }" (function) [writable | configurable | isOwn] - "classPublicGetter" => get "get classPublicGetter() { parent }" (function) [configurable | isOwn] - "classPublicGetter" => set undefined (undefined) [configurable | isOwn] - "classPublicSetter" => get undefined (undefined) [configurable | isOwn] - "classPublicSetter" => set "set classPublicSetter(x) { parent }" (function) [configurable | isOwn] - "classPublicGetterSetter" => get "get classPublicGetterSetter() { parent }" (function) [configurable | isOwn] - "classPublicGetterSetter" => set "set classPublicGetterSetter(x) { parent }" (function) [configurable | isOwn] - "parentClassPublicMethod" => "parentClassPublicMethod() { }" (function) [writable | configurable | isOwn] - "parentClassPublicGetter" => get "get parentClassPublicGetter() { }" (function) [configurable | isOwn] - "parentClassPublicGetter" => set undefined (undefined) [configurable | isOwn] - "parentClassPublicSetter" => get undefined (undefined) [configurable | isOwn] - "parentClassPublicSetter" => set "set parentClassPublicSetter(x) { }" (function) [configurable | isOwn] - "parentClassPublicGetterSetter" => get "get parentClassPublicGetterSetter() { }" (function) [configurable | isOwn] - "parentClassPublicGetterSetter" => set "set parentClassPublicGetterSetter(x) { }" (function) [configurable | isOwn] - "toString" => "toString() { return \"\"; }" (function) [writable | configurable | isOwn] - "classPublicProperty" => "classPublicPropertyValue parent" (string) [writable | enumerable | configurable | isOwn] - "parentClassPublicProperty" => "parentClassPublicPropertyValue" (string) [writable | enumerable | configurable | isOwn] - "arguments" => "TypeError: 'arguments', 'callee', and 'caller' cannot be accessed in this context." (object error) [wasThrown] - "caller" => "TypeError: 'arguments', 'callee', and 'caller' cannot be accessed in this context." (object error) [wasThrown] - "__proto__" => "function () {\n [native code]\n}" (function) [writable | configurable | isOwn] + "#classPrivateProperty" => "classPrivatePropertyValue parent" (string) [writable | enumerable | configurable | isOwn | isPrivate] + "#parentClassPrivateProperty" => "parentClassPrivatePropertyValue" (string) [writable | enumerable | configurable | isOwn | isPrivate] + "#classPrivateGetter" => get "get #classPrivateGetter() { parent }" (function) [isOwn | isPrivate] + "#classPrivateGetter" => set undefined (undefined) [isOwn | isPrivate] + "#classPrivateGetterSetter" => get "get #classPrivateGetterSetter() { parent }" (function) [isOwn | isPrivate] + "#classPrivateGetterSetter" => set "set #classPrivateGetterSetter(x) { parent }" (function) [isOwn | isPrivate] + "#classPrivateMethod" => "#classPrivateMethod() { parent }" (function) [isOwn | isPrivate] + "#classPrivateSetter" => get undefined (undefined) [isOwn | isPrivate] + "#classPrivateSetter" => set "set #classPrivateSetter(x) { parent }" (function) [isOwn | isPrivate] + "#parentClassPrivateGetter" => get "get #parentClassPrivateGetter() { }" (function) [isOwn | isPrivate] + "#parentClassPrivateGetter" => set undefined (undefined) [isOwn | isPrivate] + "#parentClassPrivateGetterSetter" => get "get #parentClassPrivateGetterSetter() { }" (function) [isOwn | isPrivate] + "#parentClassPrivateGetterSetter" => set "set #parentClassPrivateGetterSetter(x) { }" (function) [isOwn | isPrivate] + "#parentClassPrivateMethod" => "#parentClassPrivateMethod() { }" (function) [isOwn | isPrivate] + "#parentClassPrivateSetter" => get undefined (undefined) [isOwn | isPrivate] + "#parentClassPrivateSetter" => set "set #parentClassPrivateSetter(x) { }" (function) [isOwn | isPrivate] + "length" => 0 (number) [configurable | isOwn] + "name" => "PrivateMembersTestClassParent" (string) [configurable | isOwn] + "prototype" => "PrivateMembersTestClassParent" (object) [isOwn] + "classPublicMethod" => "classPublicMethod() { parent }" (function) [writable | configurable | isOwn] + "classPublicGetter" => get "get classPublicGetter() { parent }" (function) [configurable | isOwn] + "classPublicGetter" => set undefined (undefined) [configurable | isOwn] + "classPublicSetter" => get undefined (undefined) [configurable | isOwn] + "classPublicSetter" => set "set classPublicSetter(x) { parent }" (function) [configurable | isOwn] + "classPublicGetterSetter" => get "get classPublicGetterSetter() { parent }" (function) [configurable | isOwn] + "classPublicGetterSetter" => set "set classPublicGetterSetter(x) { parent }" (function) [configurable | isOwn] + "parentClassPublicMethod" => "parentClassPublicMethod() { }" (function) [writable | configurable | isOwn] + "parentClassPublicGetter" => get "get parentClassPublicGetter() { }" (function) [configurable | isOwn] + "parentClassPublicGetter" => set undefined (undefined) [configurable | isOwn] + "parentClassPublicSetter" => get undefined (undefined) [configurable | isOwn] + "parentClassPublicSetter" => set "set parentClassPublicSetter(x) { }" (function) [configurable | isOwn] + "parentClassPublicGetterSetter" => get "get parentClassPublicGetterSetter() { }" (function) [configurable | isOwn] + "parentClassPublicGetterSetter" => set "set parentClassPublicGetterSetter(x) { }" (function) [configurable | isOwn] + "toString" => "toString() { return \"\"; }" (function) [writable | configurable | isOwn] + "classPublicProperty" => "classPublicPropertyValue parent" (string) [writable | enumerable | configurable | isOwn] + "parentClassPublicProperty" => "parentClassPublicPropertyValue" (string) [writable | enumerable | configurable | isOwn] + "arguments" => "TypeError: 'arguments', 'callee', and 'caller' cannot be accessed in this context." (object error) [wasThrown] + "caller" => "TypeError: 'arguments', 'callee', and 'caller' cannot be accessed in this context." (object error) [wasThrown] + "__proto__" => "function () {\n [native code]\n}" (function) [writable | configurable | isOwn] -- Running test case: Runtime.getDisplayableProperties.Private.Constructor.Child Evaluating expression... Getting displayable properties... Properties: - "#classPrivateProperty" => "classPrivatePropertyValue child" (string) [writable | enumerable | configurable | isOwn | isPrivate] - "#childClassPrivateProperty" => "childClassPrivatePropertyValue" (string) [writable | enumerable | configurable | isOwn | isPrivate] - "length" => 0 (number) [configurable | isOwn] - "name" => "PrivateMembersTestClassChild" (string) [configurable | isOwn] - "prototype" => "PrivateMembersTestClassChild" (object) [isOwn] - "classPublicMethod" => "classPublicMethod() { child }" (function) [writable | configurable | isOwn] - "classPublicGetter" => get "get classPublicGetter() { child }" (function) [configurable | isOwn] - "classPublicGetter" => set undefined (undefined) [configurable | isOwn] - "classPublicSetter" => get undefined (undefined) [configurable | isOwn] - "classPublicSetter" => set "set classPublicSetter(x) { child }" (function) [configurable | isOwn] - "classPublicGetterSetter" => get "get classPublicGetterSetter() { child }" (function) [configurable | isOwn] - "classPublicGetterSetter" => set "set classPublicGetterSetter(x) { child }" (function) [configurable | isOwn] - "childClassPublicMethod" => "childClassPublicMethod() { }" (function) [writable | configurable | isOwn] - "childClassPublicGetter" => get "get childClassPublicGetter() { }" (function) [configurable | isOwn] - "childClassPublicGetter" => set undefined (undefined) [configurable | isOwn] - "childClassPublicSetter" => get undefined (undefined) [configurable | isOwn] - "childClassPublicSetter" => set "set childClassPublicSetter(x) { }" (function) [configurable | isOwn] - "childClassPublicGetterSetter" => get "get childClassPublicGetterSetter() { }" (function) [configurable | isOwn] - "childClassPublicGetterSetter" => set "set childClassPublicGetterSetter(x) { }" (function) [configurable | isOwn] - "toString" => "toString() { return \"\"; }" (function) [writable | configurable | isOwn] - "classPublicProperty" => "classPublicPropertyValue child" (string) [writable | enumerable | configurable | isOwn] - "childClassPublicProperty" => "childClassPublicPropertyValue" (string) [writable | enumerable | configurable | isOwn] - "arguments" => "TypeError: 'arguments', 'callee', and 'caller' cannot be accessed in this context." (object error) [wasThrown] - "caller" => "TypeError: 'arguments', 'callee', and 'caller' cannot be accessed in this context." (object error) [wasThrown] - "__proto__" => "" (function class) [writable | configurable | isOwn] + "#classPrivateProperty" => "classPrivatePropertyValue child" (string) [writable | enumerable | configurable | isOwn | isPrivate] + "#childClassPrivateProperty" => "childClassPrivatePropertyValue" (string) [writable | enumerable | configurable | isOwn | isPrivate] + "#childClassPrivateGetter" => get "get #childClassPrivateGetter() { }" (function) [isOwn | isPrivate] + "#childClassPrivateGetter" => set undefined (undefined) [isOwn | isPrivate] + "#childClassPrivateGetterSetter" => get "get #childClassPrivateGetterSetter() { }" (function) [isOwn | isPrivate] + "#childClassPrivateGetterSetter" => set "set #childClassPrivateGetterSetter(x) { }" (function) [isOwn | isPrivate] + "#childClassPrivateMethod" => "#childClassPrivateMethod() { }" (function) [isOwn | isPrivate] + "#childClassPrivateSetter" => get undefined (undefined) [isOwn | isPrivate] + "#childClassPrivateSetter" => set "set #childClassPrivateSetter(x) { }" (function) [isOwn | isPrivate] + "#classPrivateGetter" => get "get #classPrivateGetter() { child }" (function) [isOwn | isPrivate] + "#classPrivateGetter" => set undefined (undefined) [isOwn | isPrivate] + "#classPrivateGetterSetter" => get "get #classPrivateGetterSetter() { child }" (function) [isOwn | isPrivate] + "#classPrivateGetterSetter" => set "set #classPrivateGetterSetter(x) { child }" (function) [isOwn | isPrivate] + "#classPrivateMethod" => "#classPrivateMethod() { child }" (function) [isOwn | isPrivate] + "#classPrivateSetter" => get undefined (undefined) [isOwn | isPrivate] + "#classPrivateSetter" => set "set #classPrivateSetter(x) { child }" (function) [isOwn | isPrivate] + "length" => 0 (number) [configurable | isOwn] + "name" => "PrivateMembersTestClassChild" (string) [configurable | isOwn] + "prototype" => "PrivateMembersTestClassChild" (object) [isOwn] + "classPublicMethod" => "classPublicMethod() { child }" (function) [writable | configurable | isOwn] + "classPublicGetter" => get "get classPublicGetter() { child }" (function) [configurable | isOwn] + "classPublicGetter" => set undefined (undefined) [configurable | isOwn] + "classPublicSetter" => get undefined (undefined) [configurable | isOwn] + "classPublicSetter" => set "set classPublicSetter(x) { child }" (function) [configurable | isOwn] + "classPublicGetterSetter" => get "get classPublicGetterSetter() { child }" (function) [configurable | isOwn] + "classPublicGetterSetter" => set "set classPublicGetterSetter(x) { child }" (function) [configurable | isOwn] + "childClassPublicMethod" => "childClassPublicMethod() { }" (function) [writable | configurable | isOwn] + "childClassPublicGetter" => get "get childClassPublicGetter() { }" (function) [configurable | isOwn] + "childClassPublicGetter" => set undefined (undefined) [configurable | isOwn] + "childClassPublicSetter" => get undefined (undefined) [configurable | isOwn] + "childClassPublicSetter" => set "set childClassPublicSetter(x) { }" (function) [configurable | isOwn] + "childClassPublicGetterSetter" => get "get childClassPublicGetterSetter() { }" (function) [configurable | isOwn] + "childClassPublicGetterSetter" => set "set childClassPublicGetterSetter(x) { }" (function) [configurable | isOwn] + "toString" => "toString() { return \"\"; }" (function) [writable | configurable | isOwn] + "classPublicProperty" => "classPublicPropertyValue child" (string) [writable | enumerable | configurable | isOwn] + "childClassPublicProperty" => "childClassPublicPropertyValue" (string) [writable | enumerable | configurable | isOwn] + "arguments" => "TypeError: 'arguments', 'callee', and 'caller' cannot be accessed in this context." (object error) [wasThrown] + "caller" => "TypeError: 'arguments', 'callee', and 'caller' cannot be accessed in this context." (object error) [wasThrown] + "__proto__" => "" (function class) [writable | configurable | isOwn] -- Running test case: Runtime.getDisplayableProperties.Private.Prototype.Parent Evaluating expression... Getting displayable properties... Properties: + "#classPrivateGetter" => get "get #classPrivateGetter() { parent }" (function) [isOwn | isPrivate] + "#classPrivateGetter" => set undefined (undefined) [isOwn | isPrivate] + "#classPrivateGetterSetter" => get "get #classPrivateGetterSetter() { parent }" (function) [isOwn | isPrivate] + "#classPrivateGetterSetter" => set "set #classPrivateGetterSetter(x) { parent }" (function) [isOwn | isPrivate] + "#classPrivateMethod" => "#classPrivateMethod() { parent }" (function) [isOwn | isPrivate] + "#classPrivateSetter" => get undefined (undefined) [isOwn | isPrivate] + "#classPrivateSetter" => set "set #classPrivateSetter(x) { parent }" (function) [isOwn | isPrivate] + "#parentClassPrivateGetter" => get "get #parentClassPrivateGetter() { }" (function) [isOwn | isPrivate] + "#parentClassPrivateGetter" => set undefined (undefined) [isOwn | isPrivate] + "#parentClassPrivateGetterSetter" => get "get #parentClassPrivateGetterSetter() { }" (function) [isOwn | isPrivate] + "#parentClassPrivateGetterSetter" => set "set #parentClassPrivateGetterSetter(x) { }" (function) [isOwn | isPrivate] + "#parentClassPrivateMethod" => "#parentClassPrivateMethod() { }" (function) [isOwn | isPrivate] + "#parentClassPrivateSetter" => get undefined (undefined) [isOwn | isPrivate] + "#parentClassPrivateSetter" => set "set #parentClassPrivateSetter(x) { }" (function) [isOwn | isPrivate] "constructor" => "" (function class) [writable | configurable | isOwn] "instancePublicMethod" => "instancePublicMethod() { parent }" (function) [writable | configurable | isOwn] "instancePublicGetter" => get "get instancePublicGetter() { parent }" (function) [configurable | isOwn] @@ -288,6 +372,20 @@ Properties: Evaluating expression... Getting displayable properties... Properties: + "#childClassPrivateGetter" => get "get #childClassPrivateGetter() { }" (function) [isOwn | isPrivate] + "#childClassPrivateGetter" => set undefined (undefined) [isOwn | isPrivate] + "#childClassPrivateGetterSetter" => get "get #childClassPrivateGetterSetter() { }" (function) [isOwn | isPrivate] + "#childClassPrivateGetterSetter" => set "set #childClassPrivateGetterSetter(x) { }" (function) [isOwn | isPrivate] + "#childClassPrivateMethod" => "#childClassPrivateMethod() { }" (function) [isOwn | isPrivate] + "#childClassPrivateSetter" => get undefined (undefined) [isOwn | isPrivate] + "#childClassPrivateSetter" => set "set #childClassPrivateSetter(x) { }" (function) [isOwn | isPrivate] + "#classPrivateGetter" => get "get #classPrivateGetter() { child }" (function) [isOwn | isPrivate] + "#classPrivateGetter" => set undefined (undefined) [isOwn | isPrivate] + "#classPrivateGetterSetter" => get "get #classPrivateGetterSetter() { child }" (function) [isOwn | isPrivate] + "#classPrivateGetterSetter" => set "set #classPrivateGetterSetter(x) { child }" (function) [isOwn | isPrivate] + "#classPrivateMethod" => "#classPrivateMethod() { child }" (function) [isOwn | isPrivate] + "#classPrivateSetter" => get undefined (undefined) [isOwn | isPrivate] + "#classPrivateSetter" => set "set #classPrivateSetter(x) { child }" (function) [isOwn | isPrivate] "constructor" => "" (function class) [writable | configurable | isOwn] "instancePublicMethod" => "instancePublicMethod() { child }" (function) [writable | configurable | isOwn] "instancePublicGetter" => get "get instancePublicGetter() { child }" (function) [configurable | isOwn] diff --git a/LayoutTests/inspector/runtime/getProperties-expected.txt b/LayoutTests/inspector/runtime/getProperties-expected.txt index f5f04a2239da..eb2d8ac3fe14 100644 --- a/LayoutTests/inspector/runtime/getProperties-expected.txt +++ b/LayoutTests/inspector/runtime/getProperties-expected.txt @@ -158,85 +158,169 @@ Internal Properties: Evaluating expression... Getting own properties... Properties: - "#instancePrivateProperty" => "instancePrivatePropertyValue parent" (string) [writable | enumerable | configurable | isOwn | isPrivate] - "#parentInstancePrivateProperty" => "parentInstancePrivatePropertyValue" (string) [writable | enumerable | configurable | isOwn | isPrivate] - "instancePublicProperty" => "instancePublicPropertyValue parent" (string) [writable | enumerable | configurable | isOwn] - "parentInstancePublicProperty" => "parentInstancePublicPropertyValue" (string) [writable | enumerable | configurable | isOwn] - "__proto__" => "PrivateMembersTestClassParent" (object) [writable | configurable | isOwn] + "#instancePrivateProperty" => "instancePrivatePropertyValue parent" (string) [writable | enumerable | configurable | isOwn | isPrivate] + "#parentInstancePrivateProperty" => "parentInstancePrivatePropertyValue" (string) [writable | enumerable | configurable | isOwn | isPrivate] + "#instancePrivateGetter" => get "get #instancePrivateGetter() { parent }" (function) [isOwn | isPrivate] + "#instancePrivateGetter" => set undefined (undefined) [isOwn | isPrivate] + "#instancePrivateGetterSetter" => get "get #instancePrivateGetterSetter() { parent }" (function) [isOwn | isPrivate] + "#instancePrivateGetterSetter" => set "set #instancePrivateGetterSetter(x) { parent }" (function) [isOwn | isPrivate] + "#instancePrivateMethod" => "#instancePrivateMethod() { parent }" (function) [isOwn | isPrivate] + "#instancePrivateSetter" => get undefined (undefined) [isOwn | isPrivate] + "#instancePrivateSetter" => set "set #instancePrivateSetter(x) { parent }" (function) [isOwn | isPrivate] + "#parentInstancePrivateGetter" => get "get #parentInstancePrivateGetter() { }" (function) [isOwn | isPrivate] + "#parentInstancePrivateGetter" => set undefined (undefined) [isOwn | isPrivate] + "#parentInstancePrivateGetterSetter" => get "get #parentInstancePrivateGetterSetter() { }" (function) [isOwn | isPrivate] + "#parentInstancePrivateGetterSetter" => set "set #parentInstancePrivateGetterSetter(x) { }" (function) [isOwn | isPrivate] + "#parentInstancePrivateMethod" => "#parentInstancePrivateMethod() { }" (function) [isOwn | isPrivate] + "#parentInstancePrivateSetter" => get undefined (undefined) [isOwn | isPrivate] + "#parentInstancePrivateSetter" => set "set #parentInstancePrivateSetter(x) { }" (function) [isOwn | isPrivate] + "instancePublicProperty" => "instancePublicPropertyValue parent" (string) [writable | enumerable | configurable | isOwn] + "parentInstancePublicProperty" => "parentInstancePublicPropertyValue" (string) [writable | enumerable | configurable | isOwn] + "__proto__" => "PrivateMembersTestClassParent" (object) [writable | configurable | isOwn] -- Running test case: Runtime.getProperties.Private.Instance.Child Evaluating expression... Getting own properties... Properties: - "#instancePrivateProperty" => "instancePrivatePropertyValue parent" (string) [writable | enumerable | configurable | isOwn | isPrivate] - "#parentInstancePrivateProperty" => "parentInstancePrivatePropertyValue" (string) [writable | enumerable | configurable | isOwn | isPrivate] - "#instancePrivateProperty" => "instancePrivatePropertyValue child" (string) [writable | enumerable | configurable | isOwn | isPrivate] - "#childInstancePrivateProperty" => "childInstancePrivatePropertyValue" (string) [writable | enumerable | configurable | isOwn | isPrivate] - "instancePublicProperty" => "instancePublicPropertyValue child" (string) [writable | enumerable | configurable | isOwn] - "parentInstancePublicProperty" => "parentInstancePublicPropertyValue" (string) [writable | enumerable | configurable | isOwn] - "childInstancePublicProperty" => "childInstancePublicPropertyValue" (string) [writable | enumerable | configurable | isOwn] - "__proto__" => "PrivateMembersTestClassChild" (object) [writable | configurable | isOwn] + "#instancePrivateProperty" => "instancePrivatePropertyValue parent" (string) [writable | enumerable | configurable | isOwn | isPrivate] + "#parentInstancePrivateProperty" => "parentInstancePrivatePropertyValue" (string) [writable | enumerable | configurable | isOwn | isPrivate] + "#instancePrivateProperty" => "instancePrivatePropertyValue child" (string) [writable | enumerable | configurable | isOwn | isPrivate] + "#childInstancePrivateProperty" => "childInstancePrivatePropertyValue" (string) [writable | enumerable | configurable | isOwn | isPrivate] + "#instancePrivateGetter" => get "get #instancePrivateGetter() { parent }" (function) [isOwn | isPrivate] + "#instancePrivateGetter" => set undefined (undefined) [isOwn | isPrivate] + "#instancePrivateGetterSetter" => get "get #instancePrivateGetterSetter() { parent }" (function) [isOwn | isPrivate] + "#instancePrivateGetterSetter" => set "set #instancePrivateGetterSetter(x) { parent }" (function) [isOwn | isPrivate] + "#instancePrivateMethod" => "#instancePrivateMethod() { parent }" (function) [isOwn | isPrivate] + "#instancePrivateSetter" => get undefined (undefined) [isOwn | isPrivate] + "#instancePrivateSetter" => set "set #instancePrivateSetter(x) { parent }" (function) [isOwn | isPrivate] + "#parentInstancePrivateGetter" => get "get #parentInstancePrivateGetter() { }" (function) [isOwn | isPrivate] + "#parentInstancePrivateGetter" => set undefined (undefined) [isOwn | isPrivate] + "#parentInstancePrivateGetterSetter" => get "get #parentInstancePrivateGetterSetter() { }" (function) [isOwn | isPrivate] + "#parentInstancePrivateGetterSetter" => set "set #parentInstancePrivateGetterSetter(x) { }" (function) [isOwn | isPrivate] + "#parentInstancePrivateMethod" => "#parentInstancePrivateMethod() { }" (function) [isOwn | isPrivate] + "#parentInstancePrivateSetter" => get undefined (undefined) [isOwn | isPrivate] + "#parentInstancePrivateSetter" => set "set #parentInstancePrivateSetter(x) { }" (function) [isOwn | isPrivate] + "#childInstancePrivateGetter" => get "get #childInstancePrivateGetter() { }" (function) [isOwn | isPrivate] + "#childInstancePrivateGetter" => set undefined (undefined) [isOwn | isPrivate] + "#childInstancePrivateGetterSetter" => get "get #childInstancePrivateGetterSetter() { }" (function) [isOwn | isPrivate] + "#childInstancePrivateGetterSetter" => set "set #childInstancePrivateGetterSetter(x) { }" (function) [isOwn | isPrivate] + "#childInstancePrivateMethod" => "#childInstancePrivateMethod() { }" (function) [isOwn | isPrivate] + "#childInstancePrivateSetter" => get undefined (undefined) [isOwn | isPrivate] + "#childInstancePrivateSetter" => set "set #childInstancePrivateSetter(x) { }" (function) [isOwn | isPrivate] + "#instancePrivateGetter" => get "get #instancePrivateGetter() { child }" (function) [isOwn | isPrivate] + "#instancePrivateGetter" => set undefined (undefined) [isOwn | isPrivate] + "#instancePrivateGetterSetter" => get "get #instancePrivateGetterSetter() { child }" (function) [isOwn | isPrivate] + "#instancePrivateGetterSetter" => set "set #instancePrivateGetterSetter(x) { child }" (function) [isOwn | isPrivate] + "#instancePrivateMethod" => "#instancePrivateMethod() { child }" (function) [isOwn | isPrivate] + "#instancePrivateSetter" => get undefined (undefined) [isOwn | isPrivate] + "#instancePrivateSetter" => set "set #instancePrivateSetter(x) { child }" (function) [isOwn | isPrivate] + "instancePublicProperty" => "instancePublicPropertyValue child" (string) [writable | enumerable | configurable | isOwn] + "parentInstancePublicProperty" => "parentInstancePublicPropertyValue" (string) [writable | enumerable | configurable | isOwn] + "childInstancePublicProperty" => "childInstancePublicPropertyValue" (string) [writable | enumerable | configurable | isOwn] + "__proto__" => "PrivateMembersTestClassChild" (object) [writable | configurable | isOwn] -- Running test case: Runtime.getProperties.Private.Constructor.Parent Evaluating expression... Getting own properties... Properties: - "#classPrivateProperty" => "classPrivatePropertyValue parent" (string) [writable | enumerable | configurable | isOwn | isPrivate] - "#parentClassPrivateProperty" => "parentClassPrivatePropertyValue" (string) [writable | enumerable | configurable | isOwn | isPrivate] - "length" => 0 (number) [configurable | isOwn] - "name" => "PrivateMembersTestClassParent" (string) [configurable | isOwn] - "prototype" => "PrivateMembersTestClassParent" (object) [isOwn] - "classPublicMethod" => "classPublicMethod() { parent }" (function) [writable | configurable | isOwn] - "classPublicGetter" => get "get classPublicGetter() { parent }" (function) [configurable | isOwn] - "classPublicGetter" => set undefined (undefined) [configurable | isOwn] - "classPublicSetter" => get undefined (undefined) [configurable | isOwn] - "classPublicSetter" => set "set classPublicSetter(x) { parent }" (function) [configurable | isOwn] - "classPublicGetterSetter" => get "get classPublicGetterSetter() { parent }" (function) [configurable | isOwn] - "classPublicGetterSetter" => set "set classPublicGetterSetter(x) { parent }" (function) [configurable | isOwn] - "parentClassPublicMethod" => "parentClassPublicMethod() { }" (function) [writable | configurable | isOwn] - "parentClassPublicGetter" => get "get parentClassPublicGetter() { }" (function) [configurable | isOwn] - "parentClassPublicGetter" => set undefined (undefined) [configurable | isOwn] - "parentClassPublicSetter" => get undefined (undefined) [configurable | isOwn] - "parentClassPublicSetter" => set "set parentClassPublicSetter(x) { }" (function) [configurable | isOwn] - "parentClassPublicGetterSetter" => get "get parentClassPublicGetterSetter() { }" (function) [configurable | isOwn] - "parentClassPublicGetterSetter" => set "set parentClassPublicGetterSetter(x) { }" (function) [configurable | isOwn] - "toString" => "toString() { return \"\"; }" (function) [writable | configurable | isOwn] - "classPublicProperty" => "classPublicPropertyValue parent" (string) [writable | enumerable | configurable | isOwn] - "parentClassPublicProperty" => "parentClassPublicPropertyValue" (string) [writable | enumerable | configurable | isOwn] - "__proto__" => "function () {\n [native code]\n}" (function) [writable | configurable | isOwn] + "#classPrivateProperty" => "classPrivatePropertyValue parent" (string) [writable | enumerable | configurable | isOwn | isPrivate] + "#parentClassPrivateProperty" => "parentClassPrivatePropertyValue" (string) [writable | enumerable | configurable | isOwn | isPrivate] + "#classPrivateGetter" => get "get #classPrivateGetter() { parent }" (function) [isOwn | isPrivate] + "#classPrivateGetter" => set undefined (undefined) [isOwn | isPrivate] + "#classPrivateGetterSetter" => get "get #classPrivateGetterSetter() { parent }" (function) [isOwn | isPrivate] + "#classPrivateGetterSetter" => set "set #classPrivateGetterSetter(x) { parent }" (function) [isOwn | isPrivate] + "#classPrivateMethod" => "#classPrivateMethod() { parent }" (function) [isOwn | isPrivate] + "#classPrivateSetter" => get undefined (undefined) [isOwn | isPrivate] + "#classPrivateSetter" => set "set #classPrivateSetter(x) { parent }" (function) [isOwn | isPrivate] + "#parentClassPrivateGetter" => get "get #parentClassPrivateGetter() { }" (function) [isOwn | isPrivate] + "#parentClassPrivateGetter" => set undefined (undefined) [isOwn | isPrivate] + "#parentClassPrivateGetterSetter" => get "get #parentClassPrivateGetterSetter() { }" (function) [isOwn | isPrivate] + "#parentClassPrivateGetterSetter" => set "set #parentClassPrivateGetterSetter(x) { }" (function) [isOwn | isPrivate] + "#parentClassPrivateMethod" => "#parentClassPrivateMethod() { }" (function) [isOwn | isPrivate] + "#parentClassPrivateSetter" => get undefined (undefined) [isOwn | isPrivate] + "#parentClassPrivateSetter" => set "set #parentClassPrivateSetter(x) { }" (function) [isOwn | isPrivate] + "length" => 0 (number) [configurable | isOwn] + "name" => "PrivateMembersTestClassParent" (string) [configurable | isOwn] + "prototype" => "PrivateMembersTestClassParent" (object) [isOwn] + "classPublicMethod" => "classPublicMethod() { parent }" (function) [writable | configurable | isOwn] + "classPublicGetter" => get "get classPublicGetter() { parent }" (function) [configurable | isOwn] + "classPublicGetter" => set undefined (undefined) [configurable | isOwn] + "classPublicSetter" => get undefined (undefined) [configurable | isOwn] + "classPublicSetter" => set "set classPublicSetter(x) { parent }" (function) [configurable | isOwn] + "classPublicGetterSetter" => get "get classPublicGetterSetter() { parent }" (function) [configurable | isOwn] + "classPublicGetterSetter" => set "set classPublicGetterSetter(x) { parent }" (function) [configurable | isOwn] + "parentClassPublicMethod" => "parentClassPublicMethod() { }" (function) [writable | configurable | isOwn] + "parentClassPublicGetter" => get "get parentClassPublicGetter() { }" (function) [configurable | isOwn] + "parentClassPublicGetter" => set undefined (undefined) [configurable | isOwn] + "parentClassPublicSetter" => get undefined (undefined) [configurable | isOwn] + "parentClassPublicSetter" => set "set parentClassPublicSetter(x) { }" (function) [configurable | isOwn] + "parentClassPublicGetterSetter" => get "get parentClassPublicGetterSetter() { }" (function) [configurable | isOwn] + "parentClassPublicGetterSetter" => set "set parentClassPublicGetterSetter(x) { }" (function) [configurable | isOwn] + "toString" => "toString() { return \"\"; }" (function) [writable | configurable | isOwn] + "classPublicProperty" => "classPublicPropertyValue parent" (string) [writable | enumerable | configurable | isOwn] + "parentClassPublicProperty" => "parentClassPublicPropertyValue" (string) [writable | enumerable | configurable | isOwn] + "__proto__" => "function () {\n [native code]\n}" (function) [writable | configurable | isOwn] -- Running test case: Runtime.getProperties.Private.Constructor.Child Evaluating expression... Getting own properties... Properties: - "#classPrivateProperty" => "classPrivatePropertyValue child" (string) [writable | enumerable | configurable | isOwn | isPrivate] - "#childClassPrivateProperty" => "childClassPrivatePropertyValue" (string) [writable | enumerable | configurable | isOwn | isPrivate] - "length" => 0 (number) [configurable | isOwn] - "name" => "PrivateMembersTestClassChild" (string) [configurable | isOwn] - "prototype" => "PrivateMembersTestClassChild" (object) [isOwn] - "classPublicMethod" => "classPublicMethod() { child }" (function) [writable | configurable | isOwn] - "classPublicGetter" => get "get classPublicGetter() { child }" (function) [configurable | isOwn] - "classPublicGetter" => set undefined (undefined) [configurable | isOwn] - "classPublicSetter" => get undefined (undefined) [configurable | isOwn] - "classPublicSetter" => set "set classPublicSetter(x) { child }" (function) [configurable | isOwn] - "classPublicGetterSetter" => get "get classPublicGetterSetter() { child }" (function) [configurable | isOwn] - "classPublicGetterSetter" => set "set classPublicGetterSetter(x) { child }" (function) [configurable | isOwn] - "childClassPublicMethod" => "childClassPublicMethod() { }" (function) [writable | configurable | isOwn] - "childClassPublicGetter" => get "get childClassPublicGetter() { }" (function) [configurable | isOwn] - "childClassPublicGetter" => set undefined (undefined) [configurable | isOwn] - "childClassPublicSetter" => get undefined (undefined) [configurable | isOwn] - "childClassPublicSetter" => set "set childClassPublicSetter(x) { }" (function) [configurable | isOwn] - "childClassPublicGetterSetter" => get "get childClassPublicGetterSetter() { }" (function) [configurable | isOwn] - "childClassPublicGetterSetter" => set "set childClassPublicGetterSetter(x) { }" (function) [configurable | isOwn] - "toString" => "toString() { return \"\"; }" (function) [writable | configurable | isOwn] - "classPublicProperty" => "classPublicPropertyValue child" (string) [writable | enumerable | configurable | isOwn] - "childClassPublicProperty" => "childClassPublicPropertyValue" (string) [writable | enumerable | configurable | isOwn] - "__proto__" => "" (function class) [writable | configurable | isOwn] + "#classPrivateProperty" => "classPrivatePropertyValue child" (string) [writable | enumerable | configurable | isOwn | isPrivate] + "#childClassPrivateProperty" => "childClassPrivatePropertyValue" (string) [writable | enumerable | configurable | isOwn | isPrivate] + "#childClassPrivateGetter" => get "get #childClassPrivateGetter() { }" (function) [isOwn | isPrivate] + "#childClassPrivateGetter" => set undefined (undefined) [isOwn | isPrivate] + "#childClassPrivateGetterSetter" => get "get #childClassPrivateGetterSetter() { }" (function) [isOwn | isPrivate] + "#childClassPrivateGetterSetter" => set "set #childClassPrivateGetterSetter(x) { }" (function) [isOwn | isPrivate] + "#childClassPrivateMethod" => "#childClassPrivateMethod() { }" (function) [isOwn | isPrivate] + "#childClassPrivateSetter" => get undefined (undefined) [isOwn | isPrivate] + "#childClassPrivateSetter" => set "set #childClassPrivateSetter(x) { }" (function) [isOwn | isPrivate] + "#classPrivateGetter" => get "get #classPrivateGetter() { child }" (function) [isOwn | isPrivate] + "#classPrivateGetter" => set undefined (undefined) [isOwn | isPrivate] + "#classPrivateGetterSetter" => get "get #classPrivateGetterSetter() { child }" (function) [isOwn | isPrivate] + "#classPrivateGetterSetter" => set "set #classPrivateGetterSetter(x) { child }" (function) [isOwn | isPrivate] + "#classPrivateMethod" => "#classPrivateMethod() { child }" (function) [isOwn | isPrivate] + "#classPrivateSetter" => get undefined (undefined) [isOwn | isPrivate] + "#classPrivateSetter" => set "set #classPrivateSetter(x) { child }" (function) [isOwn | isPrivate] + "length" => 0 (number) [configurable | isOwn] + "name" => "PrivateMembersTestClassChild" (string) [configurable | isOwn] + "prototype" => "PrivateMembersTestClassChild" (object) [isOwn] + "classPublicMethod" => "classPublicMethod() { child }" (function) [writable | configurable | isOwn] + "classPublicGetter" => get "get classPublicGetter() { child }" (function) [configurable | isOwn] + "classPublicGetter" => set undefined (undefined) [configurable | isOwn] + "classPublicSetter" => get undefined (undefined) [configurable | isOwn] + "classPublicSetter" => set "set classPublicSetter(x) { child }" (function) [configurable | isOwn] + "classPublicGetterSetter" => get "get classPublicGetterSetter() { child }" (function) [configurable | isOwn] + "classPublicGetterSetter" => set "set classPublicGetterSetter(x) { child }" (function) [configurable | isOwn] + "childClassPublicMethod" => "childClassPublicMethod() { }" (function) [writable | configurable | isOwn] + "childClassPublicGetter" => get "get childClassPublicGetter() { }" (function) [configurable | isOwn] + "childClassPublicGetter" => set undefined (undefined) [configurable | isOwn] + "childClassPublicSetter" => get undefined (undefined) [configurable | isOwn] + "childClassPublicSetter" => set "set childClassPublicSetter(x) { }" (function) [configurable | isOwn] + "childClassPublicGetterSetter" => get "get childClassPublicGetterSetter() { }" (function) [configurable | isOwn] + "childClassPublicGetterSetter" => set "set childClassPublicGetterSetter(x) { }" (function) [configurable | isOwn] + "toString" => "toString() { return \"\"; }" (function) [writable | configurable | isOwn] + "classPublicProperty" => "classPublicPropertyValue child" (string) [writable | enumerable | configurable | isOwn] + "childClassPublicProperty" => "childClassPublicPropertyValue" (string) [writable | enumerable | configurable | isOwn] + "__proto__" => "" (function class) [writable | configurable | isOwn] -- Running test case: Runtime.getProperties.Private.Prototype.Parent Evaluating expression... Getting own properties... Properties: + "#classPrivateGetter" => get "get #classPrivateGetter() { parent }" (function) [isOwn | isPrivate] + "#classPrivateGetter" => set undefined (undefined) [isOwn | isPrivate] + "#classPrivateGetterSetter" => get "get #classPrivateGetterSetter() { parent }" (function) [isOwn | isPrivate] + "#classPrivateGetterSetter" => set "set #classPrivateGetterSetter(x) { parent }" (function) [isOwn | isPrivate] + "#classPrivateMethod" => "#classPrivateMethod() { parent }" (function) [isOwn | isPrivate] + "#classPrivateSetter" => get undefined (undefined) [isOwn | isPrivate] + "#classPrivateSetter" => set "set #classPrivateSetter(x) { parent }" (function) [isOwn | isPrivate] + "#parentClassPrivateGetter" => get "get #parentClassPrivateGetter() { }" (function) [isOwn | isPrivate] + "#parentClassPrivateGetter" => set undefined (undefined) [isOwn | isPrivate] + "#parentClassPrivateGetterSetter" => get "get #parentClassPrivateGetterSetter() { }" (function) [isOwn | isPrivate] + "#parentClassPrivateGetterSetter" => set "set #parentClassPrivateGetterSetter(x) { }" (function) [isOwn | isPrivate] + "#parentClassPrivateMethod" => "#parentClassPrivateMethod() { }" (function) [isOwn | isPrivate] + "#parentClassPrivateSetter" => get undefined (undefined) [isOwn | isPrivate] + "#parentClassPrivateSetter" => set "set #parentClassPrivateSetter(x) { }" (function) [isOwn | isPrivate] "constructor" => "" (function class) [writable | configurable | isOwn] "instancePublicMethod" => "instancePublicMethod() { parent }" (function) [writable | configurable | isOwn] "instancePublicGetter" => get "get instancePublicGetter() { parent }" (function) [configurable | isOwn] @@ -258,6 +342,20 @@ Properties: Evaluating expression... Getting own properties... Properties: + "#childClassPrivateGetter" => get "get #childClassPrivateGetter() { }" (function) [isOwn | isPrivate] + "#childClassPrivateGetter" => set undefined (undefined) [isOwn | isPrivate] + "#childClassPrivateGetterSetter" => get "get #childClassPrivateGetterSetter() { }" (function) [isOwn | isPrivate] + "#childClassPrivateGetterSetter" => set "set #childClassPrivateGetterSetter(x) { }" (function) [isOwn | isPrivate] + "#childClassPrivateMethod" => "#childClassPrivateMethod() { }" (function) [isOwn | isPrivate] + "#childClassPrivateSetter" => get undefined (undefined) [isOwn | isPrivate] + "#childClassPrivateSetter" => set "set #childClassPrivateSetter(x) { }" (function) [isOwn | isPrivate] + "#classPrivateGetter" => get "get #classPrivateGetter() { child }" (function) [isOwn | isPrivate] + "#classPrivateGetter" => set undefined (undefined) [isOwn | isPrivate] + "#classPrivateGetterSetter" => get "get #classPrivateGetterSetter() { child }" (function) [isOwn | isPrivate] + "#classPrivateGetterSetter" => set "set #classPrivateGetterSetter(x) { child }" (function) [isOwn | isPrivate] + "#classPrivateMethod" => "#classPrivateMethod() { child }" (function) [isOwn | isPrivate] + "#classPrivateSetter" => get undefined (undefined) [isOwn | isPrivate] + "#classPrivateSetter" => set "set #classPrivateSetter(x) { child }" (function) [isOwn | isPrivate] "constructor" => "" (function class) [writable | configurable | isOwn] "instancePublicMethod" => "instancePublicMethod() { child }" (function) [writable | configurable | isOwn] "instancePublicGetter" => get "get instancePublicGetter() { child }" (function) [configurable | isOwn] diff --git a/Source/JavaScriptCore/inspector/InjectedScriptSource.js b/Source/JavaScriptCore/inspector/InjectedScriptSource.js index 87e20d1d2d75..36aae68bf3fd 100644 --- a/Source/JavaScriptCore/inspector/InjectedScriptSource.js +++ b/Source/JavaScriptCore/inspector/InjectedScriptSource.js @@ -842,6 +842,29 @@ let InjectedScript = class InjectedScript extends PrototypelessObjectBase break; } + if (shouldBreak) + break; + + let privateMethods = InjectedScriptHost.getOwnPrivatePropertyMethods(o, isOwnProperty); + for (let i = 0; i < privateMethods.length; ++i) { + let privateMethod = privateMethods[i]; + let descriptor = @createObjectWithoutPrototype(); + descriptor.name = privateMethod.name; + if (@Object.@hasOwn(privateMethod, "value")) + descriptor.value = privateMethod.value; + if (@Object.@hasOwn(privateMethod, "get")) + descriptor.get = privateMethod.get; + if (@Object.@hasOwn(privateMethod, "set")) + descriptor.set = privateMethod.set; + if (isOwnProperty) + descriptor.isOwn = true; + descriptor.isPrivate = true; + let result = processDescriptor(descriptor, isOwnProperty); + shouldBreak = result === InjectedScript.PropertyFetchAction.Stop; + if (shouldBreak) + break; + } + if (shouldBreak) break; diff --git a/Source/JavaScriptCore/inspector/JSInjectedScriptHost.cpp b/Source/JavaScriptCore/inspector/JSInjectedScriptHost.cpp index ba1287ef8556..ee1c95accc67 100644 --- a/Source/JavaScriptCore/inspector/JSInjectedScriptHost.cpp +++ b/Source/JavaScriptCore/inspector/JSInjectedScriptHost.cpp @@ -46,10 +46,12 @@ #include "JSCInlines.h" #include "JSFinalizationRegistry.h" #include "JSInjectedScriptHostPrototype.h" +#include "JSLexicalEnvironment.h" #include "JSMap.h" #include "JSMapIterator.h" #include "JSPromise.h" #include "JSPromisePrototype.h" +#include "JSScope.h" #include "JSSet.h" #include "JSSetIterator.h" #include "JSStringIterator.h" @@ -66,6 +68,7 @@ #include "ScopedArguments.h" #include "SourceCode.h" #include "StructureCreateInlines.h" +#include "SymbolTable.h" #include #include #include @@ -305,7 +308,7 @@ static JSObject* constructInternalProperty(JSGlobalObject* globalObject, const S JSValue JSInjectedScriptHost::getOwnPrivatePropertySymbols(JSGlobalObject* globalObject, CallFrame* callFrame) { - if (callFrame->argumentCount() < 1) + if (callFrame->argumentCount() < 1) [[unlikely]] return jsUndefined(); VM& vm = globalObject->vm(); @@ -316,7 +319,7 @@ JSValue JSInjectedScriptHost::getOwnPrivatePropertySymbols(JSGlobalObject* globa RETURN_IF_EXCEPTION(scope, JSValue()); JSObject* object = dynamicDowncast(value); - if (!object) + if (!object) [[unlikely]] return result; unsigned index = 0; @@ -336,6 +339,176 @@ JSValue JSInjectedScriptHost::getOwnPrivatePropertySymbols(JSGlobalObject* globa return result; } +JSValue JSInjectedScriptHost::getOwnPrivatePropertyMethods(JSGlobalObject* globalObject, CallFrame* callFrame) +{ + if (callFrame->argumentCount() < 1) [[unlikely]] + return jsUndefined(); + + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue value = callFrame->uncheckedArgument(0); + + // Static private methods/accessors are not accessible through the prototype chain, so only + // surface them when the class constructor or its `prototype` is the object being inspected. + bool isDirectlyInspectingObject = callFrame->argument(1).toBoolean(globalObject); + + JSArray* result = constructEmptyArray(globalObject, nullptr); + RETURN_IF_EXCEPTION(scope, JSValue()); + + JSObject* object = dynamicDowncast(value); + if (!object) [[unlikely]] + return result; + + Identifier nameIdentifier = Identifier::fromString(vm, "name"_s); + Identifier valueIdentifier = Identifier::fromString(vm, "value"_s); + Identifier getIdentifier = Identifier::fromString(vm, "get"_s); + Identifier setIdentifier = Identifier::fromString(vm, "set"_s); + + enum class IncludeStatic : bool { No, Yes }; + + unsigned index = 0; + auto appendMembers = [&](JSScope* classScope, IncludeStatic includeStatic) { + SymbolTable* symbolTable = classScope->symbolTable(); + if (!symbolTable) + return; + + Vector, PrivateNameEntry>> members; + { + ConcurrentJSLocker locker(symbolTable->m_lock); + if (!symbolTable->hasPrivateNames()) + return; + auto privateNames = symbolTable->privateNames(); + for (auto end = privateNames.end(), iter = privateNames.begin(); iter != end; ++iter) { + const PrivateNameEntry& entry = iter->value; + if (!entry.isPrivateMethodOrAccessor() || entry.isStatic() != (includeStatic == IncludeStatic::Yes)) + continue; + members.append({ iter->key.get(), entry }); + } + } + + std::sort(members.begin(), members.end(), [](const auto& a, const auto& b) { + return codePointCompareLessThan(StringView(a.first.get()), StringView(b.first.get())); + }); + + for (const auto& [name, entry] : members) { + Identifier memberIdentifier = Identifier::fromUid(vm, name.get()); + JSValue memberValue = classScope->get(globalObject, memberIdentifier); + RETURN_IF_EXCEPTION(scope, void()); + + JSObject* descriptor = constructEmptyObject(globalObject); + descriptor->putDirect(vm, nameIdentifier, jsString(vm, memberIdentifier.string())); + if (entry.isMethod()) + descriptor->putDirect(vm, valueIdentifier, memberValue); + else { + JSValue getter = jsUndefined(); + JSValue setter = jsUndefined(); + if (JSObject* holder = dynamicDowncast(memberValue)) { + getter = holder->get(globalObject, vm.propertyNames->builtinNames().getPrivateName()); + RETURN_IF_EXCEPTION(scope, void()); + setter = holder->get(globalObject, vm.propertyNames->builtinNames().setPrivateName()); + RETURN_IF_EXCEPTION(scope, void()); + } + descriptor->putDirect(vm, getIdentifier, getter); + descriptor->putDirect(vm, setIdentifier, setter); + } + + result->putDirectIndex(globalObject, index++, descriptor); + RETURN_IF_EXCEPTION(scope, void()); + } + }; + + auto scopeHasPrivateMethod = [&](SymbolTable* symbolTable, IncludeStatic includeStatic) -> bool { + ConcurrentJSLocker locker(symbolTable->m_lock); + if (!symbolTable->hasPrivateNames()) + return false; + auto privateNames = symbolTable->privateNames(); + for (auto end = privateNames.end(), iter = privateNames.begin(); iter != end; ++iter) { + if (iter->value.isPrivateMethodOrAccessor() && iter->value.isStatic() == (includeStatic == IncludeStatic::Yes)) + return true; + } + return false; + }; + + // Static private methods/accessors live in the class scope behind a "brand" that is the class itself. + auto appendStaticPrivateMethods = [&](JSFunction* classConstructor) { + for (JSScope* classScope = classConstructor->scope(); classScope; classScope = classScope->next()) { + SymbolTable* symbolTable = classScope->symbolTable(); + if (!symbolTable || !scopeHasPrivateMethod(symbolTable, IncludeStatic::Yes)) + continue; + + JSValue classBrand = classScope->get(globalObject, vm.propertyNames->builtinNames().privateClassBrandPrivateName()); + RETURN_IF_EXCEPTION(scope, void()); + if (classBrand != classConstructor) + continue; + + appendMembers(classScope, IncludeStatic::Yes); + RETURN_IF_EXCEPTION(scope, void()); + break; + } + }; + + if (isDirectlyInspectingObject) { + // When inspecting the class constructor directly. + if (JSFunction* function = dynamicDowncast(object)) { + appendStaticPrivateMethods(function); + RETURN_IF_EXCEPTION(scope, { }); + return result; + } + + // When inspecting the class `prototype`, either directly or via the instance. + JSValue classConstructorValue = object->getDirect(vm, vm.propertyNames->constructor); + if (JSFunction* classConstructor = classConstructorValue ? dynamicDowncast(classConstructorValue) : nullptr) { + JSValue classConstructorPrototype = classConstructor->get(globalObject, vm.propertyNames->prototype); + RETURN_IF_EXCEPTION(scope, { }); + if (classConstructorPrototype == object) { + appendStaticPrivateMethods(classConstructor); + RETURN_IF_EXCEPTION(scope, { }); + } + } + } + + // Instance private methods/accessors live in the class scope behind a structural brand carried + // by every instance (including instances of superclasses, via `super()`). Walk the prototype + // chain to reach each class scope, keeping only those whose brand this object actually carries. + MarkedVector seenSymbolTables; + MarkedVector instanceScopes; + for (JSValue prototype = object->getPrototypeDirect(); prototype.isObject(); prototype = asObject(prototype)->getPrototypeDirect()) { + JSValue constructorValue = asObject(prototype)->getDirect(vm, vm.propertyNames->constructor); + if (!constructorValue) + continue; + JSFunction* constructorFunction = dynamicDowncast(constructorValue); + if (!constructorFunction) + continue; + + for (JSScope* classScope = constructorFunction->scope(); classScope; classScope = classScope->next()) { + SymbolTable* symbolTable = classScope->symbolTable(); + if (!symbolTable || std::find(seenSymbolTables.begin(), seenSymbolTables.end(), symbolTable) != seenSymbolTables.end()) + continue; + + seenSymbolTables.append(symbolTable); + + if (!scopeHasPrivateMethod(symbolTable, IncludeStatic::No)) + continue; + + JSValue instanceBrand = classScope->get(globalObject, vm.propertyNames->builtinNames().privateBrandPrivateName()); + RETURN_IF_EXCEPTION(scope, { }); + if (!instanceBrand.isSymbol()) + continue; + + if (object->hasPrivateBrand(globalObject, instanceBrand)) + instanceScopes.append(classScope); + } + } + + // Emit superclass members before subclass members, matching how private fields are ordered. + for (size_t i = instanceScopes.size(); i--;) { + appendMembers(instanceScopes[i], IncludeStatic::No); + RETURN_IF_EXCEPTION(scope, { }); + } + + return result; +} + JSValue JSInjectedScriptHost::getInternalProperties(JSGlobalObject* globalObject, CallFrame* callFrame) { if (callFrame->argumentCount() < 1) diff --git a/Source/JavaScriptCore/inspector/JSInjectedScriptHost.h b/Source/JavaScriptCore/inspector/JSInjectedScriptHost.h index f47b40dbb850..a163a627c889 100644 --- a/Source/JavaScriptCore/inspector/JSInjectedScriptHost.h +++ b/Source/JavaScriptCore/inspector/JSInjectedScriptHost.h @@ -71,6 +71,7 @@ class JSInjectedScriptHost final : public JSC::JSNonFinalObject { JSC::JSValue subtype(JSC::JSGlobalObject*, JSC::CallFrame*); JSC::JSValue functionDetails(JSC::JSGlobalObject*, JSC::CallFrame*); JSC::JSValue getOwnPrivatePropertySymbols(JSC::JSGlobalObject*, JSC::CallFrame*); + JSC::JSValue getOwnPrivatePropertyMethods(JSC::JSGlobalObject*, JSC::CallFrame*); JSC::JSValue getInternalProperties(JSC::JSGlobalObject*, JSC::CallFrame*); JSC::JSValue NODELETE proxyTargetValue(JSC::CallFrame*); JSC::JSValue weakRefTargetValue(JSC::JSGlobalObject*, JSC::CallFrame*); diff --git a/Source/JavaScriptCore/inspector/JSInjectedScriptHostPrototype.cpp b/Source/JavaScriptCore/inspector/JSInjectedScriptHostPrototype.cpp index bf4f64d0a6da..5b934f9a3fb1 100644 --- a/Source/JavaScriptCore/inspector/JSInjectedScriptHostPrototype.cpp +++ b/Source/JavaScriptCore/inspector/JSInjectedScriptHostPrototype.cpp @@ -37,6 +37,7 @@ using namespace JSC; static JSC_DECLARE_HOST_FUNCTION(jsInjectedScriptHostPrototypeFunctionSubtype); static JSC_DECLARE_HOST_FUNCTION(jsInjectedScriptHostPrototypeFunctionFunctionDetails); static JSC_DECLARE_HOST_FUNCTION(jsInjectedScriptHostPrototypeFunctionGetOwnPrivatePropertySymbols); +static JSC_DECLARE_HOST_FUNCTION(jsInjectedScriptHostPrototypeFunctionGetOwnPrivatePropertyMethods); static JSC_DECLARE_HOST_FUNCTION(jsInjectedScriptHostPrototypeFunctionGetInternalProperties); static JSC_DECLARE_HOST_FUNCTION(jsInjectedScriptHostPrototypeFunctionInternalConstructorName); static JSC_DECLARE_HOST_FUNCTION(jsInjectedScriptHostPrototypeFunctionIsHTMLAllCollection); @@ -65,6 +66,7 @@ void JSInjectedScriptHostPrototype::finishCreation(VM& vm, JSGlobalObject* globa JSC_NATIVE_FUNCTION_WITHOUT_TRANSITION("subtype"_s, jsInjectedScriptHostPrototypeFunctionSubtype, static_cast(PropertyAttribute::DontEnum), 1, ImplementationVisibility::Private); JSC_NATIVE_FUNCTION_WITHOUT_TRANSITION("functionDetails"_s, jsInjectedScriptHostPrototypeFunctionFunctionDetails, static_cast(PropertyAttribute::DontEnum), 1, ImplementationVisibility::Private); JSC_NATIVE_FUNCTION_WITHOUT_TRANSITION("getOwnPrivatePropertySymbols"_s, jsInjectedScriptHostPrototypeFunctionGetOwnPrivatePropertySymbols, static_cast(PropertyAttribute::DontEnum), 1, ImplementationVisibility::Private); + JSC_NATIVE_FUNCTION_WITHOUT_TRANSITION("getOwnPrivatePropertyMethods"_s, jsInjectedScriptHostPrototypeFunctionGetOwnPrivatePropertyMethods, static_cast(PropertyAttribute::DontEnum), 2, ImplementationVisibility::Private); JSC_NATIVE_FUNCTION_WITHOUT_TRANSITION("getInternalProperties"_s, jsInjectedScriptHostPrototypeFunctionGetInternalProperties, static_cast(PropertyAttribute::DontEnum), 1, ImplementationVisibility::Private); JSC_NATIVE_FUNCTION_WITHOUT_TRANSITION("internalConstructorName"_s, jsInjectedScriptHostPrototypeFunctionInternalConstructorName, static_cast(PropertyAttribute::DontEnum), 1, ImplementationVisibility::Private); JSC_NATIVE_FUNCTION_WITHOUT_TRANSITION("isHTMLAllCollection"_s, jsInjectedScriptHostPrototypeFunctionIsHTMLAllCollection, static_cast(PropertyAttribute::DontEnum), 1, ImplementationVisibility::Private); @@ -319,6 +321,19 @@ JSC_DEFINE_HOST_FUNCTION(jsInjectedScriptHostPrototypeFunctionGetOwnPrivatePrope return JSValue::encode(castedThis->getOwnPrivatePropertySymbols(globalObject, callFrame)); } +JSC_DEFINE_HOST_FUNCTION(jsInjectedScriptHostPrototypeFunctionGetOwnPrivatePropertyMethods, (JSGlobalObject* globalObject, CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue thisValue = callFrame->thisValue(); + JSInjectedScriptHost* castedThis = dynamicDowncast(thisValue); + if (!castedThis) + return throwVMTypeError(globalObject, scope); + + return JSValue::encode(castedThis->getOwnPrivatePropertyMethods(globalObject, callFrame)); +} + JSC_DEFINE_HOST_FUNCTION(jsInjectedScriptHostPrototypeFunctionGetInternalProperties, (JSGlobalObject* globalObject, CallFrame* callFrame)) { VM& vm = globalObject->vm(); From 508bfb240c52c8817dac7a8bd717c8cfa0a65eec Mon Sep 17 00:00:00 2001 From: Simon Lewis Date: Tue, 30 Jun 2026 12:19:35 -0700 Subject: [PATCH 58/84] Migrate CoreIPC defines from WebKitAdditions https://bugs.webkit.org/show_bug.cgi?id=317767 rdar://180539641 Reviewed by Richard Robinson. This change also addresses SaferCPP issues for CoreIPCSecTrust when moving the defines. Covered by existing tests. * Source/WTF/wtf/PlatformHave.h: * Source/WebKit/Shared/cf/CoreIPCSecTrust.mm: (WebKit::updatePolicyVector): (WebKit::CoreIPCSecTrust::CoreIPCSecTrust): Canonical link: https://commits.webkit.org/316172@main --- Source/WTF/wtf/PlatformHave.h | 45 +++++++++++++ Source/WebKit/Shared/cf/CoreIPCSecTrust.mm | 78 +++++++++++----------- 2 files changed, 84 insertions(+), 39 deletions(-) diff --git a/Source/WTF/wtf/PlatformHave.h b/Source/WTF/wtf/PlatformHave.h index 863bfaa16646..54b4e0458811 100644 --- a/Source/WTF/wtf/PlatformHave.h +++ b/Source/WTF/wtf/PlatformHave.h @@ -1235,6 +1235,51 @@ #define HAVE_WK_SECURE_CODING_PKDATECOMPONENTSRANGE 1 #endif +#if ((PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 150400) \ + || ((PLATFORM(IOS) || PLATFORM(MACCATALYST)) && __IPHONE_OS_VERSION_MIN_REQUIRED >= 180400) \ + || (PLATFORM(VISION) && __VISION_OS_VERSION_MIN_REQUIRED >= 20400)) \ + || (PLATFORM(WATCHOS) && __WATCH_OS_VERSION_MIN_REQUIRED >= 110400) \ + || (PLATFORM(APPLETV) && __TV_OS_VERSION_MIN_REQUIRED >= 180400) +#define HAVE_WK_SECURE_CODING_NSURLPROTECTIONSPACE 1 +#endif + +#if ((PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 150400) \ + || ((PLATFORM(IOS) || PLATFORM(MACCATALYST)) && __IPHONE_OS_VERSION_MIN_REQUIRED >= 180400) \ + || (PLATFORM(VISION) && __VISION_OS_VERSION_MIN_REQUIRED >= 20400)) \ + || (PLATFORM(WATCHOS) && __WATCH_OS_VERSION_MIN_REQUIRED >= 110400) \ + || (PLATFORM(APPLETV) && __TV_OS_VERSION_MIN_REQUIRED >= 180400) +#define HAVE_WK_SECURE_CODING_NSURLCREDENTIAL 1 +#endif + +#if ((PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 160000) \ + || ((PLATFORM(IOS) || PLATFORM(MACCATALYST)) && __IPHONE_OS_VERSION_MIN_REQUIRED >= 190000) \ + || (PLATFORM(VISION) && __VISION_OS_VERSION_MIN_REQUIRED >= 30000)) \ + || (PLATFORM(WATCHOS) && __WATCH_OS_VERSION_MIN_REQUIRED >= 120000) \ + || (PLATFORM(APPLETV) && __TV_OS_VERSION_MIN_REQUIRED >= 190000) +#define HAVE_WK_SECURE_CODING_SECTRUST 1 +#endif + +#if ((PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 160000) \ + || ((PLATFORM(IOS) || PLATFORM(MACCATALYST)) && __IPHONE_OS_VERSION_MIN_REQUIRED >= 190000) \ + || (PLATFORM(VISION) && __VISION_OS_VERSION_MIN_REQUIRED >= 30000)) \ + || (PLATFORM(WATCHOS) && __WATCH_OS_VERSION_MIN_REQUIRED >= 120000) \ + || (PLATFORM(APPLETV) && __TV_OS_VERSION_MIN_REQUIRED >= 190000) +#define HAVE_WK_SECURE_CODING_AVOUTPUTCONTEXT 1 +#endif + +#if ((PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 270000) \ + || ((PLATFORM(IOS) || PLATFORM(MACCATALYST)) && __IPHONE_OS_VERSION_MIN_REQUIRED >= 270000) \ + || (PLATFORM(VISION) && __VISION_OS_VERSION_MIN_REQUIRED >= 270000)) \ + || (PLATFORM(WATCHOS) && __WATCH_OS_VERSION_MIN_REQUIRED >= 270000) \ + || (PLATFORM(APPLETV) && __TV_OS_VERSION_MIN_REQUIRED >= 270000) +#define HAVE_WK_SECURE_CODING_PKPAYMENTMETHOD 1 +#define HAVE_WK_SECURE_CODING_PKPAYMENTTOKEN 1 +#define HAVE_WK_SECURE_CODING_PKSHIPPINGMETHOD 1 +#define HAVE_WK_SECURE_CODING_PKPAYMENTMERCHANTSESSION 1 +#define HAVE_WK_SECURE_CODING_PKPAYMENT 1 +#define HAVE_WK_SECURE_CODING_PKPAYMENTSETUPFEATURE 1 +#endif + #if PLATFORM(COCOA) #define HAVE_CFNETWORK_SEPARATE_CREDENTIAL_STORAGE 1 #define HAVE_STRICT_DECODABLE_CNCONTACT 1 diff --git a/Source/WebKit/Shared/cf/CoreIPCSecTrust.mm b/Source/WebKit/Shared/cf/CoreIPCSecTrust.mm index 25685b72a064..aa36ce6422d5 100644 --- a/Source/WebKit/Shared/cf/CoreIPCSecTrust.mm +++ b/Source/WebKit/Shared/cf/CoreIPCSecTrust.mm @@ -96,7 +96,7 @@ static String updatePolicyVector(NSDictionary *policyOption, CoreIPCSecTrustData case CoreIPCSecTrust::PolicyOptionValueShape::Bool: { if (![optionValue isKindOfClass:NSNumber.class]) return makeString("CoreIPCSecTrust::PolicyOptionValueShape::Bool unexpected type for key "_s, (String)optionKey); - NSNumber *value = optionValue; + RetainPtr value = optionValue; CoreIPCSecTrustData::PolicyVariant v = static_cast([value boolValue]); policyVector.append(std::make_pair(WTF::move(k), WTF::move(v))); break; @@ -104,22 +104,22 @@ static String updatePolicyVector(NSDictionary *policyOption, CoreIPCSecTrustData case CoreIPCSecTrust::PolicyOptionValueShape::String: { if (![optionValue isKindOfClass:NSString.class]) return makeString("CoreIPCSecTrust::PolicyOptionValueShape::String unexpected type for key "_s, (String)optionKey); - NSString *value = optionValue; - CoreIPCSecTrustData::PolicyVariant v = CoreIPCString(value); + RetainPtr value = optionValue; + CoreIPCSecTrustData::PolicyVariant v = CoreIPCString(value.get()); policyVector.append(std::make_pair(WTF::move(k), WTF::move(v))); break; } case CoreIPCSecTrust::PolicyOptionValueShape::ArrayOfNumbers: { if (![optionValue isKindOfClass:NSArray.class]) return makeString("CoreIPCSecTrust::PolicyOptionValueShape::ArrayOfNumbers unexpected type for key "_s, (String)optionKey, " (expecting NSArray)"_s); - NSArray* value = optionValue; - if (!value.count) + RetainPtr value = optionValue; + if (![value count]) return makeString("CoreIPCSecTrust::PolicyOptionValueShape::ArrayOfNumbers array length 0 for key "_s, (String)optionKey); - if (!arrayElementsTheSameType(value, NSNumber.class)) + if (!arrayElementsTheSameType(value.get(), NSNumber.class)) return makeString("CoreIPCSecTrust::PolicyOptionValueShape::ArrayOfNumbers unexpected type for key "_s, (String)optionKey, " (expecting NSNumber)"_s); Vector vector; - vector.reserveCapacity(value.count); - for (NSNumber *element in value) { + vector.reserveCapacity([value count]); + for (NSNumber *element in value.get()) { CoreIPCNumber n { element }; vector.append(WTF::move(n)); } @@ -130,14 +130,14 @@ static String updatePolicyVector(NSDictionary *policyOption, CoreIPCSecTrustData case CoreIPCSecTrust::PolicyOptionValueShape::ArrayOfStrings: { if (![optionValue isKindOfClass:NSArray.class]) return makeString("CoreIPCSecTrust::PolicyOptionValueShape::ArrayOfStrings unexpected type for key "_s, (String)optionKey, " (expecting NSArray)"_s); - NSArray* value = optionValue; - if (!value.count) + RetainPtr value = optionValue; + if (![value count]) return makeString("CoreIPCSecTrust::PolicyOptionValueShape::ArrayOfStrings array length 0 for key "_s, (String)optionKey); - if (!arrayElementsTheSameType(value, NSString.class)) + if (!arrayElementsTheSameType(value.get(), NSString.class)) return makeString("CoreIPCSecTrust::PolicyOptionValueShape::ArrayOfStrings unexpected type for key "_s, (String)optionKey, " (expecting NSString)"_s); Vector vector; - vector.reserveCapacity(value.count); - for (NSString *element in value) { + vector.reserveCapacity([value count]); + for (NSString *element in value.get()) { CoreIPCString s { element }; vector.append(WTF::move(s)); } @@ -148,14 +148,14 @@ static String updatePolicyVector(NSDictionary *policyOption, CoreIPCSecTrustData case CoreIPCSecTrust::PolicyOptionValueShape::ArrayOfData: { if (![optionValue isKindOfClass:NSArray.class]) return makeString("CoreIPCSecTrust::PolicyOptionValueShape::ArrayOfData unexpected type for key %@ "_s, (String)optionKey, " (expecting NSArray)"_s); - NSArray* value = optionValue; - if (!value.count) + RetainPtr value = optionValue; + if (![value count]) return makeString("CoreIPCSecTrust::PolicyOptionValueShape::ArrayOfData array length 0 for key "_s, (String)optionKey); - if (!arrayElementsTheSameType(value, NSData.class)) + if (!arrayElementsTheSameType(value.get(), NSData.class)) return makeString("CoreIPCSecTrust::PolicyOptionValueShape::ArrayOfData unexpected type for key "_s, (String)optionKey, " (expecting NSData)"_s); Vector vector; - vector.reserveCapacity(value.count); - for (NSData *element in value) { + vector.reserveCapacity([value count]); + for (NSData *element in value.get()) { CoreIPCData d { element }; vector.append(WTF::move(d)); } @@ -166,16 +166,16 @@ static String updatePolicyVector(NSDictionary *policyOption, CoreIPCSecTrustData case CoreIPCSecTrust::PolicyOptionValueShape::ArrayOfArrayContainingDateOrNumber: { if (![optionValue isKindOfClass:NSArray.class]) return makeString("CoreIPCSecTrust::PolicyOptionValueShape::ArrayOfArrayContainingDateOrNumber unexpected type for key "_s, (String)optionKey, " (expecting NSArray)"_s); - NSArray *value = optionValue; - if (!value.count) + RetainPtr value = optionValue; + if (![value count]) return makeString("CoreIPCSecTrust::PolicyOptionValueShape::ArrayOfArrayContainingDateOrNumber array length 0 for key "_s, (String)optionKey); - if (!arrayElementsTheSameType(value, NSArray.class)) + if (!arrayElementsTheSameType(value.get(), NSArray.class)) return makeString("CoreIPCSecTrust::PolicyOptionValueShape::ArrayOfArrayContainingDateOrNumber unexpected type for key "_s, (String)optionKey, " (expecting NSArray)"_s); CoreIPCSecTrustData::PolicyArrayOfArrayContainingDateOrNumbers outerVector; - outerVector.reserveCapacity(value.count); + outerVector.reserveCapacity([value count]); - for (NSArray *secondLevelArray in value) { + for (NSArray *secondLevelArray in value.get()) { if (![secondLevelArray isKindOfClass:NSArray.class]) return makeString("CoreIPCSecTrust::PolicyOptionValueShape::ArrayOfArrayContainingDateOrNumber second level array unexpected type for key "_s, (String)optionKey); @@ -184,12 +184,12 @@ static String updatePolicyVector(NSDictionary *policyOption, CoreIPCSecTrustData for (id element in secondLevelArray) { if ([element isKindOfClass:NSNumber.class]) { - NSNumber *e = element; - Variant v = CoreIPCNumber(e); + RetainPtr e = element; + Variant v = CoreIPCNumber(e.get()); innerVector.append(WTF::move(v)); } else if ([element isKindOfClass:NSDate.class]) { - NSDate *d = element; - Variant v = CoreIPCDate(d); + RetainPtr d = element; + Variant v = CoreIPCDate(d.get()); innerVector.append(WTF::move(v)); } else return makeString("CoreIPCSecTrust::PolicyOptionValueShape::ArrayOfArrayContainingDateOrNumber second level array contents unexpected type for key "_s, (String)optionKey); @@ -203,10 +203,10 @@ static String updatePolicyVector(NSDictionary *policyOption, CoreIPCSecTrustData case CoreIPCSecTrust::PolicyOptionValueShape::DictionaryValueIsNumber: { if (![optionValue isKindOfClass:NSDictionary.class]) return makeString("CoreIPCSecTrust::PolicyOptionValueShape::DictionaryValueIsNumber unexpected type for key "_s, (String)optionKey, " (expecting NSDictionary)"_s); - NSDictionary *d = optionValue; + RetainPtr d = optionValue; CoreIPCSecTrustData::PolicyDictionaryValueIsNumber vector; - vector.reserveCapacity(d.count); - for (NSString* key in d) { + vector.reserveCapacity([d count]); + for (NSString* key in d.get()) { if (![key isKindOfClass:NSString.class]) return makeString("CoreIPCSecTrust::PolicyOptionValueShape::DictionaryValueIsNumber unexpected dictionary key type for key "_s, (String)optionKey, " (expecting NSString)"_s); NSNumber *value = [d objectForKey:key]; @@ -408,7 +408,7 @@ static String optionalArrayOfDataHelper(std::optional>& toSe CoreIPCSecTrustData::InfoOption v = WTF::move(s); vector.append(std::make_pair(WTF::move(k), WTF::move(v))); } else if ([value isKindOfClass:NSNumber.class]) { - NSNumber *candidateBool = value; + RetainPtr candidateBool = value; if ([candidateBool isEqualToNumber:@YES] || [candidateBool isEqualToNumber:@NO]) { bool v = [candidateBool boolValue]; vector.append(std::make_pair(WTF::move(k), v)); @@ -418,11 +418,11 @@ static String optionalArrayOfDataHelper(std::optional>& toSe return; } } else if ([value isKindOfClass:NSArray.class]) { - NSArray *revocationInfoArray = value; + RetainPtr revocationInfoArray = value; CoreIPCSecTrustData::RevocationInfoArray revocationInfo; revocationInfo.reserveCapacity([revocationInfoArray count]); - for (NSDictionary *entry in revocationInfoArray) { + for (NSDictionary *entry in revocationInfoArray.get()) { if (![entry isKindOfClass:NSDictionary.class]) { RELEASE_LOG_ERROR(IPC, "CoreIPCSecTrust 'RevocationInfo' array contains non-dictionary element"); ASSERT_NOT_REACHED(); @@ -460,12 +460,12 @@ static String optionalArrayOfDataHelper(std::optional>& toSe id subValue = [subDict objectForKey:subKey]; if ([subValue isKindOfClass:NSNumber.class]) { - NSNumber *number = subValue; + RetainPtr number = subValue; if ([number isEqualToNumber:@YES] || [number isEqualToNumber:@NO]) { CoreIPCSecTrustData::RevocationInfoSubDictValue v = [number boolValue]; revocationSubDict.append(std::make_pair(WTF::move(subKeyString), WTF::move(v))); } else { - CoreIPCNumber n { number }; + CoreIPCNumber n { number.get() }; CoreIPCSecTrustData::RevocationInfoSubDictValue v = WTF::move(n); revocationSubDict.append(std::make_pair(WTF::move(subKeyString), WTF::move(v))); } @@ -494,10 +494,10 @@ static String optionalArrayOfDataHelper(std::optional>& toSe CoreIPCSecTrustData::InfoOption v = WTF::move(revocationInfo); vector.append(std::make_pair(WTF::move(k), WTF::move(v))); } else if ([value isKindOfClass:NSDictionary.class]) { - NSDictionary *subDict = value; + RetainPtr subDict = value; CoreIPCSecTrustData::InfoSubDict infoSubDict; infoSubDict.reserveCapacity([subDict count]); - for (NSString *subKey in subDict) { + for (NSString *subKey in subDict.get()) { if (![subKey isKindOfClass:NSString.class]) { RELEASE_LOG_ERROR(IPC, "CoreIPCSecTrust 'info' sub-dictionary key is not a string"); ASSERT_NOT_REACHED(); @@ -616,13 +616,13 @@ static String optionalArrayOfDataHelper(std::optional>& toSe auto p = std::make_pair(WTF::move(k), WTF::move(v)); innerVector.append(WTF::move(p)); } else if ([value isKindOfClass:NSNumber.class]) { - NSNumber *number = value; + RetainPtr number = value; if ([number isEqualToNumber:@YES] || [number isEqualToNumber:@NO]) { bool v = [number boolValue]; auto p = std::make_pair(WTF::move(k), v); innerVector.append(WTF::move(p)); } else { - CoreIPCNumber n { number }; + CoreIPCNumber n { number.get() }; auto p = std::make_pair(WTF::move(k), n); innerVector.append(WTF::move(p)); } From 88e4fef2d2777c3f1313007b6d1c169fdb8198b1 Mon Sep 17 00:00:00 2001 From: Ruthvik Konda Date: Tue, 30 Jun 2026 12:23:52 -0700 Subject: [PATCH 59/84] Reduce use of `.get()` for smart pointers in WebModelPlayer https://bugs.webkit.org/show_bug.cgi?id=318135 rdar://180954475 Reviewed by Mike Wyrzykowski. `RefPtr` has constructors that take `WeakPtr` and `ThreadSafeWeakPtr` directly (RefPtr.h:104, 106), calling `.get()` internally. Removing redundant `.get()` calls at thirteen sites in `WebModelPlayer` simplifies the code without changing behavior, since the constructor invokes `.get()` itself. No new tests needed (no behavioral change). * Source/WebKit/WebProcess/Model/WebModelPlayer.mm: (WebKit::ModelDisplayBufferDisplayDelegate::display): (WebKit::WebModelPlayer::WebModelPlayer): (WebKit::WebModelPlayer::load): (WebKit::WebModelPlayer::notifyEntityTransformUpdated): (WebKit::WebModelPlayer::sizeDidChange): (WebKit::WebModelPlayer::snapshotCurrentFrame): (WebKit::WebModelPlayer::scheduleUpdateIfNeeded): (WebKit::WebModelPlayer::scheduleDisplayUpdate): (WebKit::WebModelPlayer::setEnvironmentMap): (WebKit::WebModelPlayer::visibilityStateDidChange): (WebKit::WebModelPlayer::updateScreenHeadroomFromPage): Canonical link: https://commits.webkit.org/316173@main --- .../WebKit/WebProcess/Model/WebModelPlayer.mm | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Source/WebKit/WebProcess/Model/WebModelPlayer.mm b/Source/WebKit/WebProcess/Model/WebModelPlayer.mm index ee242bcee36b..44302f05d99e 100644 --- a/Source/WebKit/WebProcess/Model/WebModelPlayer.mm +++ b/Source/WebKit/WebProcess/Model/WebModelPlayer.mm @@ -88,7 +88,7 @@ void display(WebCore::PlatformCALayer& layer) final } else layer.clearContents(); - if (RefPtr player = m_modelPlayer.get()) + if (RefPtr player = m_modelPlayer) player->scheduleUpdateIfNeeded(); } WebCore::GraphicsLayer::CompositingCoordinatesOrientation orientation() const final @@ -142,7 +142,7 @@ void setContentsFormat(WebCore::ContentsFormat contentsFormat) if (RefPtr document = page.localTopDocument()) { m_screenPropertiesChangedObserver = ScreenPropertiesChangedObserver::create([weakThis = ThreadSafeWeakPtr { *this }](WebCore::PlatformDisplayID displayID) { - RefPtr protectedThis = weakThis.get(); + RefPtr protectedThis { weakThis }; if (!protectedThis) return; auto platformScreen = WebCore::PlatformScreen::singleton(); @@ -192,7 +192,7 @@ void setContentsFormat(WebCore::ContentsFormat contentsFormat) void WebModelPlayer::load(WebCore::Model& modelSource, WebCore::LayoutSize size, bool) { - RefPtr corePage = m_page.get(); + RefPtr corePage { m_page }; if (!corePage) return; m_modelLoader = nil; @@ -262,7 +262,7 @@ void setContentsFormat(WebCore::ContentsFormat contentsFormat) if (!model) return; - if (RefPtr client = protectedThis->m_client.get(); client && !protectedThis->m_didFinishLoading) { + if (RefPtr client = protectedThis->m_client; client && !protectedThis->m_didFinishLoading) { protectedThis->m_didFinishLoading = true; [protectedThis->m_modelLoader setLoop:protectedThis->m_isLooping]; protectedThis->m_cachedAnimationState = protectedThis->currentAnimationState(); @@ -312,14 +312,14 @@ void setContentsFormat(WebCore::ContentsFormat contentsFormat) m_retainedData = modelSource.data()->createNSData(); if ([m_modelLoader loadModel:m_retainedData.get() mimeType:modelSource.mimeType().createNSString().get()]) startUpdateLoopIfNeeded(); - else if (RefPtr client = m_client.get()) + else if (RefPtr client = m_client) client->didFailLoading(protectedThis.get(), { }); } void WebModelPlayer::notifyEntityTransformUpdated() { RefPtr model = m_currentModel; - RefPtr client = m_client.get(); + RefPtr client { m_client }; if (!model || !client || !model->entityTransform()) return; @@ -333,7 +333,7 @@ void setContentsFormat(WebCore::ContentsFormat contentsFormat) if (!currentModel) return; - RefPtr corePage = m_page.get(); + RefPtr corePage { m_page }; if (!corePage) return; RefPtr document = corePage->localTopDocument(); @@ -506,7 +506,7 @@ void setContentsFormat(WebCore::ContentsFormat contentsFormat) if (!currentModel || !m_hasRenderedFrame || m_displayTextureIndex >= m_displayBuffers.size()) return nullptr; - RefPtr corePage { m_page.get() }; + RefPtr corePage { m_page }; if (!corePage) return nullptr; @@ -580,7 +580,7 @@ void setContentsFormat(WebCore::ContentsFormat contentsFormat) if (!m_isUpdateLoopRunning || m_isUpdateScheduled) return; - RefPtr corePage = m_page.get(); + RefPtr corePage { m_page }; if (!corePage) return; @@ -676,7 +676,7 @@ void setContentsFormat(WebCore::ContentsFormat contentsFormat) void WebModelPlayer::scheduleDisplayUpdate() { - if (RefPtr graphicsLayer = m_graphicsLayer.get()) + if (RefPtr graphicsLayer = m_graphicsLayer) graphicsLayer->setContentsNeedsDisplay(); } @@ -817,7 +817,7 @@ void setContentsFormat(WebCore::ContentsFormat contentsFormat) } startUpdateLoopIfNeeded(); - if (RefPtr client = m_client.get()) + if (RefPtr client = m_client) client->didFinishEnvironmentMapLoading(*this, success); } @@ -831,7 +831,7 @@ static bool disableReloading() { // When the model becomes invisible, release memory-intensive resources. // When it becomes visible again, HTMLModelElement will trigger a reload through startLoadModelTimer(). - RefPtr client = m_client.get(); + RefPtr client { m_client }; if (!client || disableReloading()) return; @@ -966,7 +966,7 @@ static float interpolateHeadroom(float headroomForLow, float headroomForHigh, fl void WebModelPlayer::updateScreenHeadroomFromPage() { - RefPtr page = m_page.get(); + RefPtr page { m_page }; if (!page) return; From ae0d90b965a8ae1d40a6c2ad9d1bfdad66dffed8 Mon Sep 17 00:00:00 2001 From: Rob Buis Date: Tue, 30 Jun 2026 12:27:02 -0700 Subject: [PATCH 60/84] Fix some failures in imported/w3c/web-platform-tests/svg/styling https://bugs.webkit.org/show_bug.cgi?id=318224 Reviewed by Nikolas Zimmermann. Fix some failures in imported/w3c/web-platform-tests/svg/styling by copying the HTMLStyleElement code that handles dynamic changes of type and media attributes. * LayoutTests/imported/w3c/web-platform-tests/svg/styling/attr-style-media-dynamic-expected.txt: * LayoutTests/imported/w3c/web-platform-tests/svg/styling/attr-style-type-dynamic-expected.txt: * Source/WebCore/svg/SVGStyleElement.cpp: (WebCore::SVGStyleElement::attributeChanged): Canonical link: https://commits.webkit.org/316174@main --- .../svg/styling/attr-style-media-dynamic-expected.txt | 4 ++-- .../svg/styling/attr-style-type-dynamic-expected.txt | 4 ++-- Source/WebCore/svg/SVGStyleElement.cpp | 10 ++++++++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/LayoutTests/imported/w3c/web-platform-tests/svg/styling/attr-style-media-dynamic-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/svg/styling/attr-style-media-dynamic-expected.txt index 83cccce10a03..59051c3044a0 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/svg/styling/attr-style-media-dynamic-expected.txt +++ b/LayoutTests/imported/w3c/web-platform-tests/svg/styling/attr-style-media-dynamic-expected.txt @@ -1,5 +1,5 @@ -FAIL Changing the media attribute updates the associated stylesheet assert_equals: fill should switch once the second sheet's media matches expected "rgb(0, 128, 0)" but got "rgb(255, 0, 0)" -FAIL Removing the media attribute updates the associated stylesheet assert_equals: removing the media attribute should make the rule apply expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)" +PASS Changing the media attribute updates the associated stylesheet +PASS Removing the media attribute updates the associated stylesheet diff --git a/LayoutTests/imported/w3c/web-platform-tests/svg/styling/attr-style-type-dynamic-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/svg/styling/attr-style-type-dynamic-expected.txt index d2211c6e4944..313644a41c58 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/svg/styling/attr-style-type-dynamic-expected.txt +++ b/LayoutTests/imported/w3c/web-platform-tests/svg/styling/attr-style-type-dynamic-expected.txt @@ -1,6 +1,6 @@ PASS Initial state: invalid type leaves the sheet unprocessed -FAIL Changing the type from invalid to valid creates the stylesheet assert_equals: sheet should be created once the type becomes CSS expected 1 but got 0 +PASS Changing the type from invalid to valid creates the stylesheet PASS Changing the type from valid to invalid removes the stylesheet -FAIL Removing the type attribute restores the default CSS handling assert_equals: sheet should be created once the type attribute is removed expected 1 but got 0 +PASS Removing the type attribute restores the default CSS handling diff --git a/Source/WebCore/svg/SVGStyleElement.cpp b/Source/WebCore/svg/SVGStyleElement.cpp index cc6ef216ab25..88de20dabf45 100644 --- a/Source/WebCore/svg/SVGStyleElement.cpp +++ b/Source/WebCore/svg/SVGStyleElement.cpp @@ -26,6 +26,7 @@ #include "CSSStyleSheet.h" #include "CommonAtomStrings.h" #include "Document.h" +#include "MediaQueryParser.h" #include "NodeName.h" #include "SVGElementInlines.h" #include "SVGNames.h" @@ -79,9 +80,18 @@ void SVGStyleElement::attributeChanged(const QualifiedName& name, const AtomStri break; case AttributeNames::typeAttr: m_styleSheetOwner.setContentType(newValue); + m_styleSheetOwner.childrenChanged(*this); + if (CheckedPtr scope = m_styleSheetOwner.styleScope()) + scope->didChangeStyleSheetContents(); break; case AttributeNames::mediaAttr: m_styleSheetOwner.setMedia(newValue); + if (RefPtr sheet = this->sheet()) { + sheet->setMediaQueries(MQ::MediaQueryParser::parse(newValue, protect(document())->cssParserContext())); + if (CheckedPtr scope = m_styleSheetOwner.styleScope()) + scope->didChangeStyleSheetContents(); + } else + m_styleSheetOwner.childrenChanged(*this); break; default: break; From 2f91fd7d2ec69d1ca85edef694626ee9cebac8f1 Mon Sep 17 00:00:00 2001 From: Anthony Tarbinian Date: Tue, 30 Jun 2026 12:30:39 -0700 Subject: [PATCH 61/84] [WebCore] Take graphLock() in BiquadFilterNode::setType() to avoid race with audio-thread kernel reallocation rdar://174652790 https://bugs.webkit.org/show_bug.cgi?id=312796 Reviewed by Chris Dumez. BiquadFilterNode::setType() runs on the main thread from JS bindings and calls BiquadProcessor::setType(), which in turn calls AudioDSPKernelProcessor::reset() to iterate m_kernels and virtual-call reset() on each kernel. This was done without holding the context's graphLock(). Concurrently, every render quantum the audio thread runs AudioBasicProcessorNode::checkNumberOfChannelsForInput() under graphLock() and, when the input channel count has changed, calls uninitialize() / initialize() on the processor, which clear()s and move-assigns m_kernels. With the lock held only on one side, the main thread could load a kernel pointer from a Vector slot that is being/already freed, leading to a use-after-free virtual dispatch into a destroyed BiquadDSPKernel and writes through stale Biquad buffer spans, or a null deref if the slot was already zeroed. Fix by taking context().graphLock() in BiquadFilterNode::setType(), matching the existing convention in WaveShaperNode::setOversampleForBindings() and AudioNode::setChannelCount(). Test: webaudio/biquadfilternode-set-type-channel-count-race.html * LayoutTests/webaudio/biquadfilternode-set-type-channel-count-race-expected.txt: Added. * LayoutTests/webaudio/biquadfilternode-set-type-channel-count-race.html: Added. * Source/WebCore/Modules/webaudio/BiquadFilterNode.cpp: (WebCore::BiquadFilterNode::setType): Originally-landed-as: 305413.717@safari-7624-branch (41efaeddddf2). rdar://180436998 Canonical link: https://commits.webkit.org/316175@main --- ...e-set-type-channel-count-race-expected.txt | 3 + ...ilternode-set-type-channel-count-race.html | 57 +++++++++++++++++++ .../Modules/webaudio/BiquadFilterNode.cpp | 7 +++ 3 files changed, 67 insertions(+) create mode 100644 LayoutTests/webaudio/biquadfilternode-set-type-channel-count-race-expected.txt create mode 100644 LayoutTests/webaudio/biquadfilternode-set-type-channel-count-race.html diff --git a/LayoutTests/webaudio/biquadfilternode-set-type-channel-count-race-expected.txt b/LayoutTests/webaudio/biquadfilternode-set-type-channel-count-race-expected.txt new file mode 100644 index 000000000000..13e6354090c4 --- /dev/null +++ b/LayoutTests/webaudio/biquadfilternode-set-type-channel-count-race-expected.txt @@ -0,0 +1,3 @@ +This test passes if it does not crash when toggling BiquadFilterNode.type while the channel count changes on the audio thread. + +PASS: did not crash. diff --git a/LayoutTests/webaudio/biquadfilternode-set-type-channel-count-race.html b/LayoutTests/webaudio/biquadfilternode-set-type-channel-count-race.html new file mode 100644 index 000000000000..d1af2217d775 --- /dev/null +++ b/LayoutTests/webaudio/biquadfilternode-set-type-channel-count-race.html @@ -0,0 +1,57 @@ + + + + + + +

This test passes if it does not crash when toggling BiquadFilterNode.type while the channel count changes on the audio thread.

+

+ + diff --git a/Source/WebCore/Modules/webaudio/BiquadFilterNode.cpp b/Source/WebCore/Modules/webaudio/BiquadFilterNode.cpp index 501879620549..795387fc6b52 100644 --- a/Source/WebCore/Modules/webaudio/BiquadFilterNode.cpp +++ b/Source/WebCore/Modules/webaudio/BiquadFilterNode.cpp @@ -28,6 +28,8 @@ #if ENABLE(WEB_AUDIO) #include "BiquadFilterNode.h" + +#include "BaseAudioContext.h" #include "ExceptionOr.h" #include #include @@ -70,6 +72,11 @@ BiquadFilterType BiquadFilterNode::type() const void BiquadFilterNode::setType(BiquadFilterType type) { + ASSERT(isMainThread()); + + // Synchronize with any graph changes or changes to channel configuration since + // BiquadProcessor::setType() may iterate the processor's kernels via reset(). + Locker contextLocker { context().graphLock() }; protect(biquadProcessor())->setType(type); } From 44540933ff90a0494bb2130b5cfcdb04e1d91ef8 Mon Sep 17 00:00:00 2001 From: Timothy Hatcher Date: Tue, 30 Jun 2026 12:40:04 -0700 Subject: [PATCH 62/84] Cherry-pick 343f135c4791. rdar://175672573 Web Extensions: stack-use-after-return of &handledCount in WebExtensionContext async-reply lambdas. https://webkit.org/b/313465 rdar://175672573 Reviewed by Brian Weinstein. Fix use-after-return of handledCount in runtimeConnect, runtimeWebPageConnect, and tabsConnect. The stack-local counter was captured by reference into async-reply lambdas that execute after the enclosing frame returns. Use Box to heap-allocate the shared counter instead. * Source/WebKit/UIProcess/Extensions/Cocoa/API/WebExtensionContextAPIRuntimeCocoa.mm: (WebKit::WebExtensionContext::runtimeConnect): (WebKit::WebExtensionContext::runtimeWebPageConnect): * Source/WebKit/UIProcess/Extensions/Cocoa/API/WebExtensionContextAPITabsCocoa.mm: (WebKit::WebExtensionContext::tabsConnect): Identifier: 305413.749@safari-7624-branch Originally-landed-as: 305413.718@safari-7624.4-branch (634df48e0742). rdar://180435539 Canonical link: https://commits.webkit.org/316176@main --- .../Cocoa/API/WebExtensionContextAPIRuntimeCocoa.mm | 13 +++++++------ .../Cocoa/API/WebExtensionContextAPITabsCocoa.mm | 7 ++++--- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Source/WebKit/UIProcess/Extensions/Cocoa/API/WebExtensionContextAPIRuntimeCocoa.mm b/Source/WebKit/UIProcess/Extensions/Cocoa/API/WebExtensionContextAPIRuntimeCocoa.mm index f92c726da35a..04a8c1379eb9 100644 --- a/Source/WebKit/UIProcess/Extensions/Cocoa/API/WebExtensionContextAPIRuntimeCocoa.mm +++ b/Source/WebKit/UIProcess/Extensions/Cocoa/API/WebExtensionContextAPIRuntimeCocoa.mm @@ -44,6 +44,7 @@ #import "WebExtensionMessageTargetParameters.h" #import "WebExtensionUtilities.h" #import +#import #import #import @@ -210,11 +211,11 @@ return; } - size_t handledCount = 0; + auto handledCount = Box::create(0); size_t totalExpected = mainWorldProcesses.size(); for (auto& process : mainWorldProcesses) { - process->sendWithAsyncReply(Messages::WebExtensionContextProxy::DispatchRuntimeConnectEvent(targetContentWorldType, channelIdentifier, name, std::nullopt, completeSenderParameters, resolvedUserGesture), [=, this, protectedThis = Ref { *this }, &handledCount](HashCountedSet&& addedPortCounts) mutable { + process->sendWithAsyncReply(Messages::WebExtensionContextProxy::DispatchRuntimeConnectEvent(targetContentWorldType, channelIdentifier, name, std::nullopt, completeSenderParameters, resolvedUserGesture), [=, this, protectedThis = Ref { *this }](HashCountedSet&& addedPortCounts) mutable { // Flip target and source worlds since we're adding the opposite side of the port connection, sending from target back to source. addPorts(targetContentWorldType, sourceContentWorldType, channelIdentifier, WTF::move(addedPortCounts)); @@ -223,7 +224,7 @@ firePortDisconnectEventIfNeeded(sourceContentWorldType, targetContentWorldType, channelIdentifier); - if (++handledCount < totalExpected) + if (++*handledCount < totalExpected) return; clearQueuedPortMessages(targetContentWorldType, channelIdentifier); @@ -549,11 +550,11 @@ return; } - size_t handledCount = 0; + auto handledCount = Box::create(0); size_t totalExpected = mainWorldProcesses.size(); for (auto& process : mainWorldProcesses) { - process->sendWithAsyncReply(Messages::WebExtensionContextProxy::DispatchRuntimeConnectEvent(targetContentWorldType, channelIdentifier, name, std::nullopt, completeSenderParameters, resolvedUserGesture), [=, this, protectedThis = Ref { *this }, &handledCount](HashCountedSet&& addedPortCounts) mutable { + process->sendWithAsyncReply(Messages::WebExtensionContextProxy::DispatchRuntimeConnectEvent(targetContentWorldType, channelIdentifier, name, std::nullopt, completeSenderParameters, resolvedUserGesture), [=, this, protectedThis = Ref { *this }](HashCountedSet&& addedPortCounts) mutable { // Flip target and source worlds since we're adding the opposite side of the port connection, sending from target back to source. addPorts(targetContentWorldType, sourceContentWorldType, channelIdentifier, WTF::move(addedPortCounts)); @@ -562,7 +563,7 @@ firePortDisconnectEventIfNeeded(sourceContentWorldType, targetContentWorldType, channelIdentifier); - if (++handledCount < totalExpected) + if (++*handledCount < totalExpected) return; clearQueuedPortMessages(targetContentWorldType, channelIdentifier); diff --git a/Source/WebKit/UIProcess/Extensions/Cocoa/API/WebExtensionContextAPITabsCocoa.mm b/Source/WebKit/UIProcess/Extensions/Cocoa/API/WebExtensionContextAPITabsCocoa.mm index 41b2a71a2197..36191a9f20f1 100644 --- a/Source/WebKit/UIProcess/Extensions/Cocoa/API/WebExtensionContextAPITabsCocoa.mm +++ b/Source/WebKit/UIProcess/Extensions/Cocoa/API/WebExtensionContextAPITabsCocoa.mm @@ -47,6 +47,7 @@ #import "WebExtensionWindowIdentifier.h" #import "WebPageProxy.h" #import +#import #import #import #import @@ -528,11 +529,11 @@ static inline String toMIMEType(WebExtensionTab::ImageFormat format) return; } - size_t handledCount = 0; + auto handledCount = Box::create(0); size_t totalExpected = processes.size(); for (Ref process : processes) { - process->sendWithAsyncReply(Messages::WebExtensionContextProxy::DispatchRuntimeConnectEvent(targetContentWorldType, channelIdentifier, name, targetParameters, senderParameters, userGesture), [=, this, protectedThis = Ref { *this }, &handledCount](HashCountedSet&& addedPortCounts) mutable { + process->sendWithAsyncReply(Messages::WebExtensionContextProxy::DispatchRuntimeConnectEvent(targetContentWorldType, channelIdentifier, name, targetParameters, senderParameters, userGesture), [=, this, protectedThis = Ref { *this }](HashCountedSet&& addedPortCounts) mutable { // Flip target and source worlds since we're adding the opposite side of the port connection, sending from target back to source. addPorts(targetContentWorldType, sourceContentWorldType, channelIdentifier, WTF::move(addedPortCounts)); @@ -541,7 +542,7 @@ static inline String toMIMEType(WebExtensionTab::ImageFormat format) firePortDisconnectEventIfNeeded(sourceContentWorldType, targetContentWorldType, channelIdentifier); - if (++handledCount < totalExpected) + if (++*handledCount < totalExpected) return; clearQueuedPortMessages(targetContentWorldType, channelIdentifier); From 9bb8fdfc34571f35695a3510d84fac8bebde1ecf Mon Sep 17 00:00:00 2001 From: Ling Ho Date: Tue, 30 Jun 2026 12:57:18 -0700 Subject: [PATCH 63/84] PrettyPatch should HTML-escape image URLs when rendering binary image diffs https://bugs.webkit.org/show_bug.cgi?id=317806 rdar://180321002 Reviewed by Stephanie Lewis. When rendering binary image diffs in the legacy SVN format, PrettyPatch interpolated the image URL into the generated markup without escaping it, unlike the equivalent handling elsewhere. Escape the value before it is emitted so untrusted content cannot affect the surrounding HTML. * Websites/bugs.webkit.org/PrettyPatch/PrettyPatch.rb: Canonical link: https://commits.webkit.org/316177@main --- Websites/bugs.webkit.org/PrettyPatch/PrettyPatch.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Websites/bugs.webkit.org/PrettyPatch/PrettyPatch.rb b/Websites/bugs.webkit.org/PrettyPatch/PrettyPatch.rb index eb4be3f5372f..f30ffa977945 100644 --- a/Websites/bugs.webkit.org/PrettyPatch/PrettyPatch.rb +++ b/Websites/bugs.webkit.org/PrettyPatch/PrettyPatch.rb @@ -740,7 +740,8 @@ def image_to_html image_checksum = IMAGE_CHECKSUM_ERROR end - return "

" + image_checksum + "

" + return "

" + image_checksum + "

" + end def to_html From d7eb859e77ec20de2fb76043835de0b924ed22f0 Mon Sep 17 00:00:00 2001 From: Samuel Engida Date: Tue, 30 Jun 2026 12:58:58 -0700 Subject: [PATCH 64/84] Add system version prefix mapping for macOS 26 https://bugs.webkit.org/show_bug.cgi?id=317956 rdar://180738811 Reviewed by Elliott Williams. This adds the missing macOS 26 case so the lookup resolves correctly: - SYSTEM_VERSION_MAJOR_SHORT_MACOS_260000 = 26 - macOS 26 maps to system version prefix 21 * Configurations/Version.xcconfig: Canonical link: https://commits.webkit.org/316178@main --- Configurations/Version.xcconfig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Configurations/Version.xcconfig b/Configurations/Version.xcconfig index 21cb2ace1206..cfcca3a32eb6 100644 --- a/Configurations/Version.xcconfig +++ b/Configurations/Version.xcconfig @@ -35,6 +35,7 @@ SHORT_VERSION_STRING = $(SHORT_VERSION_STRING_$(CONFIGURATION)) SYSTEM_VERSION_MAJOR_SHORT_MACOS = $(SYSTEM_VERSION_MAJOR_SHORT_MACOS_$(TARGET_MAC_OS_X_VERSION_MAJOR)) SYSTEM_VERSION_MAJOR_SHORT_MACOS_150000 = 15 SYSTEM_VERSION_MAJOR_SHORT_MACOS_160000 = 16 +SYSTEM_VERSION_MAJOR_SHORT_MACOS_260000 = 26 SYSTEM_VERSION_MAJOR_SHORT_MACOS_270000 = 27 SYSTEM_VERSION_MAJOR_SHORT_MACOS_280000 = 28 SYSTEM_VERSION_MAJOR_SHORT_MACOS_290000 = 29 @@ -44,6 +45,7 @@ SYSTEM_VERSION_MAJOR_SHORT_MACOS_300000 = 30 SYSTEM_VERSION_PREFIX = $(SYSTEM_VERSION_PREFIX_$(PLATFORM_NAME)_$(SYSTEM_VERSION_MAJOR_SHORT_MACOS)) SYSTEM_VERSION_PREFIX_macosx_15 = 20 SYSTEM_VERSION_PREFIX_macosx_16 = 21 +SYSTEM_VERSION_PREFIX_macosx_26 = 21 SYSTEM_VERSION_PREFIX_macosx_27 = 22 SYSTEM_VERSION_PREFIX_macosx_28 = 23 SYSTEM_VERSION_PREFIX_macosx_29 = 24 From d6fb60c4fa7912ac11adc9b79f6de7aac952d967 Mon Sep 17 00:00:00 2001 From: Anthony Tarbinian Date: Tue, 30 Jun 2026 13:01:40 -0700 Subject: [PATCH 65/84] [WebCore] Capture WeakPtr to this (MediaMetadata) in ArtworkImageLoader callback https://bugs.webkit.org/show_bug.cgi?id=312480 rdar://174651594 Reviewed by Geoffrey Garen. In media artwork image loading, it is possible for a callback lambda to outlive the lifetime of the MediaMetadata which it captures with a raw "this". This callback is stored in ArtworkImageLoader::m_callback and ArtworkImageLoader is owned by MediaMetadata. The lambda captures a raw this to MediaMetadata during MediaMetadata::tryNextArtworkImage. However, it's possible for the lambda to outlive ArtworkImageLoader and, in turn, MediaMetadata after beeing std::exchanged outside of the artwork loader in ArtworkImageLoader::notifyFinished. Then, it's possible for MediaMetadata to be destroyed while the lambda has a dangling "this" pointer to the destroyed object. This patch changes the lambda to capture a WeakPtr to "this" (MediaMetadata) and returns early if it has been destroyed. If weakThis is still alive, we keep it alive for the duration of the lambda body with a RefPtr. * LayoutTests/fast/mediasession/metadata/artwork-image-loader-callback-crash-expected.txt: Added. * LayoutTests/fast/mediasession/metadata/artwork-image-loader-callback-crash.html: Added. * Source/WebCore/Modules/mediasession/MediaMetadata.cpp: (WebCore::MediaMetadata::tryNextArtworkImage): Changed the artwork loader lambda to capture a WeakPtr to "this" and early return if weakThis is null. The rest of the lambda explicitly uses weakThis where needed. * Source/WebCore/Modules/mediasession/MediaMetadata.h: Changed MediaMetadata to inherit from CanMakeWeakPtr. Originally-landed-as: 305413.695@safari-7624-branch (9da5185f2406). rdar://180437956 Canonical link: https://commits.webkit.org/316179@main --- ...k-image-loader-callback-crash-expected.txt | 1 + .../artwork-image-loader-callback-crash.html | 48 +++++++++++++++++++ .../Modules/mediasession/MediaMetadata.cpp | 15 +++--- .../Modules/mediasession/MediaMetadata.h | 2 +- 4 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 LayoutTests/fast/mediasession/metadata/artwork-image-loader-callback-crash-expected.txt create mode 100644 LayoutTests/fast/mediasession/metadata/artwork-image-loader-callback-crash.html diff --git a/LayoutTests/fast/mediasession/metadata/artwork-image-loader-callback-crash-expected.txt b/LayoutTests/fast/mediasession/metadata/artwork-image-loader-callback-crash-expected.txt new file mode 100644 index 000000000000..730ebf66a0da --- /dev/null +++ b/LayoutTests/fast/mediasession/metadata/artwork-image-loader-callback-crash-expected.txt @@ -0,0 +1 @@ +This test passes if it doesn't crash. diff --git a/LayoutTests/fast/mediasession/metadata/artwork-image-loader-callback-crash.html b/LayoutTests/fast/mediasession/metadata/artwork-image-loader-callback-crash.html new file mode 100644 index 000000000000..a800754e8aa4 --- /dev/null +++ b/LayoutTests/fast/mediasession/metadata/artwork-image-loader-callback-crash.html @@ -0,0 +1,48 @@ + + + + + + +This test passes if it doesn't crash. + + \ No newline at end of file diff --git a/Source/WebCore/Modules/mediasession/MediaMetadata.cpp b/Source/WebCore/Modules/mediasession/MediaMetadata.cpp index f3d7164e63a8..2b7a6408cccb 100644 --- a/Source/WebCore/Modules/mediasession/MediaMetadata.cpp +++ b/Source/WebCore/Modules/mediasession/MediaMetadata.cpp @@ -270,14 +270,17 @@ void MediaMetadata::tryNextArtworkImage(uint32_t index, Vector&& artworks) String artworkImageSrc = artworks[index].src; - m_artworkLoader = ArtworkImageLoader::create(*document, artworkImageSrc, [this, index, artworkImageSrc, artworks = WTF::move(artworks)](Image* image) mutable { + m_artworkLoader = ArtworkImageLoader::create(*document, artworkImageSrc, [weakThis = WeakPtr { *this }, index, artworkImageSrc, artworks = WTF::move(artworks)](Image* image) mutable { + RefPtr strongThis = weakThis; + if (!strongThis) + return; if (image && image->data() && image->width() && image->height()) { IntSize size { int(image->width()), int(image->height()) }; float imageScore = imageDimensionsScore(size.width(), size.height(), s_minimumSize, s_idealSize); - if (!index || (m_artworkImage && (imageDimensionsScore(protect(m_artworkImage)->width(), protect(m_artworkImage)->height(), s_minimumSize, s_idealSize) < imageScore))) { - m_artworkImageSrc = artworkImageSrc; - setArtworkImage(image); - metadataUpdated(); + if (!index || (strongThis->m_artworkImage && (imageDimensionsScore(protect(strongThis->m_artworkImage)->width(), protect(strongThis->m_artworkImage)->height(), s_minimumSize, s_idealSize) < imageScore))) { + strongThis->m_artworkImageSrc = artworkImageSrc; + strongThis->setArtworkImage(image); + strongThis->metadataUpdated(); } // If selection from `sizes` attribute yielded a valid image, or we have downloaded an image bigger than the ideal size we stop. if (artworks[index].score >= 0 || size.maxDimension() >= s_idealSize) @@ -285,7 +288,7 @@ void MediaMetadata::tryNextArtworkImage(uint32_t index, Vector&& artworks) } if (++index < artworks.size()) - tryNextArtworkImage(index, WTF::move(artworks)); + strongThis->tryNextArtworkImage(index, WTF::move(artworks)); }); protect(m_artworkLoader)->requestImageResource(); } diff --git a/Source/WebCore/Modules/mediasession/MediaMetadata.h b/Source/WebCore/Modules/mediasession/MediaMetadata.h index a4d20458a387..932c83f49997 100644 --- a/Source/WebCore/Modules/mediasession/MediaMetadata.h +++ b/Source/WebCore/Modules/mediasession/MediaMetadata.h @@ -75,7 +75,7 @@ class ArtworkImageLoader final : public CachedImageClient, public RefCounted m_cachedImage; }; -class MediaMetadata final : public RefCounted { +class MediaMetadata final : public RefCountedAndCanMakeWeakPtr { public: static ExceptionOr> create(ScriptExecutionContext&, std::optional&&); static Ref create(MediaSession&, Vector&&); From bf0c7c6154c617cd29731d5f3a05d3918ec8efa2 Mon Sep 17 00:00:00 2001 From: Said Abou-Hallawa Date: Tue, 30 Jun 2026 13:03:36 -0700 Subject: [PATCH 66/84] REGRESSION(312893@main): ASSERT(m_decodedSize >= decodedSize) in BitmapImageSource::decodedSizeReset() https://bugs.webkit.org/show_bug.cgi?id=318238 rdar://181042189 Reviewed by Richard Robinson. In 312893@main and 296430@main, ImageFrame was changed to include more than one NativeImage. Managing the decodedSize has to be adjusted to take this into account. We should consider removing one or more NativeImages from ImageFrame. In BitmapImageSource::cacheNativeImageAtIndex() one or more NativeImage are removed. But only one NativeImage is added. Removing the NativeImages is done first by `destroyNativeImageAtIndex()`. The size of the removed NativeImages is decremented correctly from decodedSize. Adding the new NativeImage is done second. The whole size of the `ImageFrame` is added to the decodedSize which this is wrong. And this is the cause of this assertion. The size of the added NativeImage should be added only to decodedSize. * Source/WebCore/platform/graphics/BitmapImageSource.cpp: (WebCore::BitmapImageSource::cacheNativeImageAtIndex): Canonical link: https://commits.webkit.org/316180@main --- Source/WebCore/platform/graphics/BitmapImageSource.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/WebCore/platform/graphics/BitmapImageSource.cpp b/Source/WebCore/platform/graphics/BitmapImageSource.cpp index 276ac65b83eb..e4fd04575808 100644 --- a/Source/WebCore/platform/graphics/BitmapImageSource.cpp +++ b/Source/WebCore/platform/graphics/BitmapImageSource.cpp @@ -513,7 +513,7 @@ void BitmapImageSource::cacheNativeImageAtIndex(unsigned index, SubsamplingLevel destination.headroom = nativeImage->headroom(); cacheMetadataAtIndex(index, subsamplingLevel, options); - decodedSizeIncreased(frame.sizeInBytes()); + decodedSizeIncreased(destination.sizeInBytes()); } const ImageFrame& BitmapImageSource::frameAtIndex(unsigned index) const From ba3be26a064abdfdcc87698f202ff0be33452663 Mon Sep 17 00:00:00 2001 From: Antti Koivisto Date: Tue, 30 Jun 2026 13:05:36 -0700 Subject: [PATCH 67/84] =?UTF-8?q?[WebCore]=20use-after-free=20in=20Style::?= =?UTF-8?q?TreeResolver=20=E2=80=94=20XMLDocumentParser::startElementNs=20?= =?UTF-8?q?missing=20parentNode=20re-check=20after=20custom-element=20upgr?= =?UTF-8?q?ade=20rdar://177479772?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed by Ryosuke Niwa. Draining the custom element reaction stack in startElementNs runs the constructor synchronously, which can re-parent newElement, adopt it into another document, or detach the current parser node. parserAppendChild requires its argument to have no parent and to share our document, so falling through into it leaves the tree linked into two child lists. Re-check those invariants after the reaction stack drains and stop parsing if any of them no longer holds. A spec-aligned follow-up should route XML element creation through constructElementWithFallback like HTMLDocumentParser does, which post-validates and falls back to HTMLUnknownElement; a FIXME records that. Test: fast/custom-elements/xml-parser-reparent-during-construction-crash.xhtml * LayoutTests/fast/custom-elements/xml-parser-reparent-during-construction-crash-expected.txt: Added. * LayoutTests/fast/custom-elements/xml-parser-reparent-during-construction-crash.xhtml: Added. * Source/WebCore/xml/parser/XMLDocumentParserLibxml2.cpp: (WebCore::XMLDocumentParser::startElementNs): Originally-landed-as: 305413.947@safari-7624-branch (19beaaec03d0). rdar://180436996 Canonical link: https://commits.webkit.org/316181@main --- ...ent-during-construction-crash-expected.txt | 3 +++ ...r-reparent-during-construction-crash.xhtml | 19 +++++++++++++++++++ .../xml/parser/XMLDocumentParserLibxml2.cpp | 17 +++++++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 LayoutTests/fast/custom-elements/xml-parser-reparent-during-construction-crash-expected.txt create mode 100644 LayoutTests/fast/custom-elements/xml-parser-reparent-during-construction-crash.xhtml diff --git a/LayoutTests/fast/custom-elements/xml-parser-reparent-during-construction-crash-expected.txt b/LayoutTests/fast/custom-elements/xml-parser-reparent-during-construction-crash-expected.txt new file mode 100644 index 000000000000..49004868ff5d --- /dev/null +++ b/LayoutTests/fast/custom-elements/xml-parser-reparent-during-construction-crash-expected.txt @@ -0,0 +1,3 @@ +This test passes if it does not crash. + + diff --git a/LayoutTests/fast/custom-elements/xml-parser-reparent-during-construction-crash.xhtml b/LayoutTests/fast/custom-elements/xml-parser-reparent-during-construction-crash.xhtml new file mode 100644 index 000000000000..fd96782b0ae3 --- /dev/null +++ b/LayoutTests/fast/custom-elements/xml-parser-reparent-during-construction-crash.xhtml @@ -0,0 +1,19 @@ + + + + + + +

This test passes if it does not crash.

+
+ + diff --git a/Source/WebCore/xml/parser/XMLDocumentParserLibxml2.cpp b/Source/WebCore/xml/parser/XMLDocumentParserLibxml2.cpp index dfb9640ea3c1..ff8f3b47f528 100644 --- a/Source/WebCore/xml/parser/XMLDocumentParserLibxml2.cpp +++ b/Source/WebCore/xml/parser/XMLDocumentParserLibxml2.cpp @@ -910,6 +910,23 @@ void XMLDocumentParser::startElementNs(const xmlChar* xmlLocalName, const xmlCha if (willConstructCustomElement) [[unlikely]] { customElementReactionStack.reset(); markupInsertionCountIncrementer.reset(); + // Draining the reaction stack runs the custom element constructor, which may + // have re-parented newElement, adopted it into another document, or detached + // the current parser node. parserAppendChild requires its argument to have no + // parent and to share our document. + // FIXME: We are misusing the upgrade-an-element machinery here to emulate + // synchronous custom element construction. Per HTML's "create an element for + // a token" with willExecuteScript=true we should run the constructor inside + // Document::createElement (via constructElementWithFallback), which post- + // validates parent/children/attributes/document and falls back to + // HTMLUnknownElement on violation, like HTMLDocumentParser does in + // runScriptsForPausedTreeBuilder. Switching to that would also fix the + // spec-incorrect attribute-ordering above (we currently set attributes before + // the constructor runs). + if (!m_currentNode || newElement->parentNode() || &newElement->document() != &m_currentNode->document()) { + stopParsing(); + return; + } } newElement->beginParsingChildren(); From b12f122d84409745fa49aa45e2776aa59fdda5aa Mon Sep 17 00:00:00 2001 From: Abrar Rahman Protyasha Date: Tue, 30 Jun 2026 13:06:53 -0700 Subject: [PATCH 68/84] Compromised web content processes can use percent-encoded path separators in PDF suggested filenames to write outside temporary directory rdar://174079512 Reviewed by Wenson Hsieh. The pdfOpenWithPreview IPC response sanitizes the WCP supplied filename while it is still percent encoded. Then, pathToPDFOnDisk percent decodes the assembled path. As such, a %2F (`/`) sequence can pass through sanitization but still decodes to `/` after assembly. This means that a compromised web content process could write to a PDF file outside of the temporary WebKit PDF directory with a properly crafted IPC message. In this patch, we address this asymmetry by percent decoding the filename before sanitization. We also remove the percent decoding that happens post joining. Instead, we enforce an invariant that rejects any sanitized filename that is not its own last path component. Tests: TestWebKitAPI.WebKit.OpenPDFWithPreviewIPCTraversalEncodedFilename TestWebKitAPI.WebKit.OpenPDFWithPreviewIPCTraversalUnencodedFilename * Source/WebKit/UIProcess/mac/WebPageProxyMac.mm: (WebKit::pathToPDFOnDisk): (WebKit::WebPageProxy::savePDFToTemporaryFolderAndOpenWithNativeApplication): * Tools/TestWebKitAPI/Tests/WebKitCocoa/UIDelegate.mm: (-[OpenPDFWithPreviewDelegate _webView:shouldAllowPDFAtURL:toOpenFromFrame:completionHandler:]): ((WebKit, OpenPDFWithPreviewIPCTraversalEncodedFilename)): ((WebKit, OpenPDFWithPreviewIPCTraversalUnencodedFilename)): Originally-landed-as: 305413.948@safari-7624-branch (06ccfa7d7a2d). rdar://180438093 Canonical link: https://commits.webkit.org/316182@main --- .../WebKit/UIProcess/mac/WebPageProxyMac.mm | 23 +++- .../Tests/WebKit/WKWebView/UIDelegate.mm | 109 ++++++++++++++++++ 2 files changed, 128 insertions(+), 4 deletions(-) diff --git a/Source/WebKit/UIProcess/mac/WebPageProxyMac.mm b/Source/WebKit/UIProcess/mac/WebPageProxyMac.mm index 571be8819700..0a85fdcd4b66 100644 --- a/Source/WebKit/UIProcess/mac/WebPageProxyMac.mm +++ b/Source/WebKit/UIProcess/mac/WebPageProxyMac.mm @@ -35,6 +35,7 @@ #import "FrameInfoData.h" #import "ImageAnalysisUtilities.h" #import "InsertTextOptions.h" +#import "Logging.h" #import "MenuUtilities.h" #import "MessageSenderInlines.h" #import "NativeWebKeyboardEvent.h" @@ -619,9 +620,7 @@ static inline bool expectsLegacyImplicitRubberBandControl() return nil; } - // The NSFileManager expects a path string, while NSWorkspace uses file URLs, and will decode any percent encoding - // in its passed URLs before loading from disk. Create the files using decoded file paths so they match up. - RetainPtr path = [[pdfDirectoryPath stringByAppendingPathComponent:suggestedFilename.createNSString().get()] stringByRemovingPercentEncoding]; + RetainPtr path = [pdfDirectoryPath stringByAppendingPathComponent:suggestedFilename.createNSString().get()]; RetainPtr fileManager = [NSFileManager defaultManager]; if ([fileManager fileExistsAtPath:path.get()]) { @@ -635,6 +634,10 @@ static inline bool expectsLegacyImplicitRubberBandControl() path = [fileManager stringWithFileSystemRepresentation:pathTemplateRepresentation.data() length:pathTemplateRepresentation.length()]; } + // Reject any path that resolves outside the temporary PDF directory. + if (![[path stringByStandardizingPath] hasPrefix:[pdfDirectoryPath stringByStandardizingPath]]) + return nil; + return path; } @@ -645,11 +648,23 @@ static inline bool expectsLegacyImplicitRubberBandControl() return; } - auto sanitizedFilename = ResourceResponseBase::sanitizeSuggestedFilename(suggestedFilename); + // Encoded path separator should get stripped rather than decoded into the assembled path after sanitisation. + // Otherwise, any encoded slash produces a path traversal on post-join decodes. (rdar://174079512) + RetainPtr nsSuggestedFilename = suggestedFilename.createNSString(); + if (RetainPtr decoded = [nsSuggestedFilename stringByRemovingPercentEncoding]) + nsSuggestedFilename = WTF::move(decoded); + + auto sanitizedFilename = ResourceResponseBase::sanitizeSuggestedFilename(nsSuggestedFilename.get()); if (!sanitizedFilename.endsWithIgnoringASCIICase(".pdf"_s)) { WTFLogAlways("Cannot save file without .pdf extension to the temporary directory."); return; } + + if (sanitizedFilename != FileSystem::lastComponentOfPathIgnoringTrailingSlash(sanitizedFilename)) { + RELEASE_LOG(PDF, "Cannot save PDF whose sanitized filename is not a single path component."); + return; + } + RetainPtr nsPath = pathToPDFOnDisk(sanitizedFilename); if (!nsPath) diff --git a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/UIDelegate.mm b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/UIDelegate.mm index df990f03fbe9..01d71d8ec01e 100644 --- a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/UIDelegate.mm +++ b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/UIDelegate.mm @@ -1478,6 +1478,115 @@ - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigat TestWebKitAPI::Util::run(&done); } +@interface OpenPDFWithPreviewDelegate : NSObject +@property (nonatomic, readonly) RetainPtr capturedFileURL; +@property (nonatomic, readonly) BOOL receivedCallback; +@end + +@implementation OpenPDFWithPreviewDelegate + +- (void)_webView:(WKWebView *)webView shouldAllowPDFAtURL:(NSURL *)fileURL toOpenFromFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler +{ + _capturedFileURL = fileURL; + _receivedCallback = YES; + completionHandler(NO); +} + +@end + +#if ENABLE(IPC_TESTING_API) + +static void runOpenPDFWithPreviewTraversalTest(NSString *injectedFilename, NSString *traversalTargetPath) +{ + [[NSFileManager defaultManager] removeItemAtPath:traversalTargetPath error:nil]; + + RetainPtr pdfURL = [NSBundle.test_resourcesBundle URLForResource:@"test" withExtension:@"pdf"]; + + RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]); + for (_WKFeature *feature in [WKPreferences _features]) { + if ([feature.key isEqualToString:@"PDFPluginHUDEnabled"] || [feature.key isEqualToString:@"IPCTestingAPIEnabled"]) + [[configuration preferences] _setEnabled:YES forFeature:feature]; + } + + RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 800, 600) configuration:configuration.get()]); + [webView _setWindowOcclusionDetectionEnabled:NO]; + + RetainPtr delegate = adoptNS([OpenPDFWithPreviewDelegate new]); + [webView setUIDelegate:delegate.get()]; + + [webView loadRequest:[NSURLRequest requestWithURL:pdfURL.get()]]; + [webView _test_waitForDidFinishNavigation]; + + EXPECT_TRUE(TestWebKitAPI::Util::waitFor([webView] { + return !![webView _pdfHUDs].count; + })); + + RetainPtr listenerScript = [NSString stringWithFormat:@R"JS( + IPC.addIncomingMessageListener('UI', (message) => { + const replyMsgInfo = IPC.messages.WebPage_OpenPDFWithPreviewReply; + if (!replyMsgInfo) + return; + const requestMsgInfo = IPC.messages.WebPage_OpenPDFWithPreview; + if (!requestMsgInfo || message.name !== requestMsgInfo.name) + return; + if (typeof message.listenerID === 'undefined') + return; + + IPC.sendMessage('UI', message.listenerID, replyMsgInfo.name, [ + {type: 'String', value: '%@'}, + {type: 'bool', value: 1}, + {type: 'FrameInfoData', value: IPC}, + {type: 'uint64_t', value: 4}, + new Uint8Array([0x25, 0x50, 0x44, 0x46]) + ]); + }); + 'listener installed'; + )JS", injectedFilename]; + + bool listenerInstalled = false; + [webView evaluateJavaScript:listenerScript.get() completionHandler:[&listenerInstalled](id, NSError *error) { + EXPECT_NULL(error); + listenerInstalled = true; + }]; + TestWebKitAPI::Util::run(&listenerInstalled); + + [[webView _pdfHUDs].anyObject performSelector:NSSelectorFromString(@"_performActionForControl:") withObject:@"preview"]; + + EXPECT_TRUE(TestWebKitAPI::Util::waitFor([delegate] { + return [delegate receivedCallback]; + })); + + EXPECT_FALSE([[NSFileManager defaultManager] fileExistsAtPath:traversalTargetPath]); + + RetainPtr writtenPath = [[[delegate capturedFileURL] path] stringByStandardizingPath]; + RetainPtr temporaryDirectory = [NSTemporaryDirectory() stringByStandardizingPath]; + + EXPECT_TRUE([writtenPath hasPrefix:temporaryDirectory.get()]); + EXPECT_TRUE([writtenPath containsString:@"/WebKitPDFs-"]); + EXPECT_FALSE([writtenPath containsString:@"/../"]); + + [[NSFileManager defaultManager] removeItemAtURL:[delegate capturedFileURL].get() error:nil]; + [[NSFileManager defaultManager] removeItemAtPath:traversalTargetPath error:nil]; +} + +TEST(WebKit, OpenPDFWithPreviewIPCTraversalEncodedFilename) +{ + int pid = [[NSProcessInfo processInfo] processIdentifier]; + RetainPtr traversalTarget = [NSString stringWithFormat:@"/tmp/DEEP-TRAVERSAL-%d-encoded.pdf", pid]; + RetainPtr injectedFilename = [NSString stringWithFormat:@"..%%2F..%%2F..%%2F..%%2F..%%2F..%%2F..%%2Ftmp%%2FDEEP-TRAVERSAL-%d-encoded.pdf", pid]; + runOpenPDFWithPreviewTraversalTest(injectedFilename.get(), traversalTarget.get()); +} + +TEST(WebKit, OpenPDFWithPreviewIPCTraversalUnencodedFilename) +{ + int pid = [[NSProcessInfo processInfo] processIdentifier]; + RetainPtr traversalTarget = [NSString stringWithFormat:@"/tmp/DEEP-TRAVERSAL-%d-unencoded.pdf", pid]; + RetainPtr injectedFilename = [NSString stringWithFormat:@"../../../../../../../tmp/DEEP-TRAVERSAL-%d-unencoded.pdf", pid]; + runOpenPDFWithPreviewTraversalTest(injectedFilename.get(), traversalTarget.get()); +} + +#endif // ENABLE(IPC_TESTING_API) + #endif // ENABLE(PDF_HUD) #define RELIABLE_DID_NOT_HANDLE_WHEEL_EVENT 0 From a2de66fe98291779d77a15d831083e51ab664b53 Mon Sep 17 00:00:00 2001 From: Kai Tamkun Date: Tue, 30 Jun 2026 13:08:14 -0700 Subject: [PATCH 69/84] [JSC][FTL] compileArrayIndexOfOrArrayIncludes (UntypedUse + Array::Contiguous): ensureStillAliveHere(base) placed before GC-capable vmCall https://bugs.webkit.org/show_bug.cgi?id=313490 rdar://175674067 Reviewed by Yusuke Suzuki. Delays an ensureStillAliveHere call until after a GC-capable function invocation. Test: JSTests/stress/array-indexof-ensure-still-alive.js * JSTests/stress/array-indexof-ensure-still-alive.js: Added. (opt): * Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp: (JSC::FTL::DFG::LowerDFGToB3::compileArrayIndexOfOrArrayIncludes): Originally-landed-as: 305413.767@safari-7624-branch (db99db96e504). rdar://180437023 Canonical link: https://commits.webkit.org/316183@main --- .../stress/array-indexof-ensure-still-alive.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 JSTests/stress/array-indexof-ensure-still-alive.js diff --git a/JSTests/stress/array-indexof-ensure-still-alive.js b/JSTests/stress/array-indexof-ensure-still-alive.js new file mode 100644 index 000000000000..3ede4c9daf16 --- /dev/null +++ b/JSTests/stress/array-indexof-ensure-still-alive.js @@ -0,0 +1,15 @@ +//@ runDefault("--useFTLJIT=1", "--jitPolicyScale=0.1", "--useConcurrentJIT=0", "--useConcurrentGC=0", "--sweepSynchronously=1", "--collectContinuously=1") + +function opt(s, needle) { + return [s + "A", s + "B", s + "C", s + "D", s + "E", s + "F", s + "G", s + "H"].indexOf(needle); +} +noInline(opt); + +let big = "Q".repeat(1024 * 1024); +let needleStr = "Z".repeat(big.length + 1); + +for (let i = 0; i < 2000; i++) + opt(big, (i & 1) ? needleStr : 1234); + +for (let i = 0; i < 10000; i++) + opt(big, needleStr); From 54b8e49ebf3396dbcea92fcbbcec2a0233264291 Mon Sep 17 00:00:00 2001 From: Youenn Fablet Date: Tue, 30 Jun 2026 13:09:49 -0700 Subject: [PATCH 70/84] Heap UaF in GPU Process via MediaStreamTrack.clone() + ImageCapture.takePhoto() + applyConstraints() rdar://176025005 Reviewed by Eric Carlson. We store constraints in the callback by value instead of reference to make sure that the constraints are valid if the callback is called asynchronously. Test: fast/mediastream/applyConstraints-with-takePhoto.html * LayoutTests/fast/mediastream/applyConstraints-with-takePhoto-expected.txt: Added. * LayoutTests/fast/mediastream/applyConstraints-with-takePhoto.html: Added. * Source/WebKit/GPUProcess/webrtc/UserMediaCaptureManagerProxy.cpp: Originally-landed-as: 305413.816@safari-7624-branch (207238632262). rdar://180436666 Canonical link: https://commits.webkit.org/316184@main --- ...plyConstraints-with-takePhoto-expected.txt | 3 ++ .../applyConstraints-with-takePhoto.html | 32 +++++++++++++++++++ .../webrtc/UserMediaCaptureManagerProxy.cpp | 2 +- 3 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 LayoutTests/fast/mediastream/applyConstraints-with-takePhoto-expected.txt create mode 100644 LayoutTests/fast/mediastream/applyConstraints-with-takePhoto.html diff --git a/LayoutTests/fast/mediastream/applyConstraints-with-takePhoto-expected.txt b/LayoutTests/fast/mediastream/applyConstraints-with-takePhoto-expected.txt new file mode 100644 index 000000000000..ce0583968657 --- /dev/null +++ b/LayoutTests/fast/mediastream/applyConstraints-with-takePhoto-expected.txt @@ -0,0 +1,3 @@ + +PASS applyConstraints while takePhoto is happening on a cloned track + diff --git a/LayoutTests/fast/mediastream/applyConstraints-with-takePhoto.html b/LayoutTests/fast/mediastream/applyConstraints-with-takePhoto.html new file mode 100644 index 000000000000..15bb77cc7738 --- /dev/null +++ b/LayoutTests/fast/mediastream/applyConstraints-with-takePhoto.html @@ -0,0 +1,32 @@ + + + + + + + + + + + diff --git a/Source/WebKit/GPUProcess/webrtc/UserMediaCaptureManagerProxy.cpp b/Source/WebKit/GPUProcess/webrtc/UserMediaCaptureManagerProxy.cpp index 074baf3e09aa..9d2a2f433b78 100644 --- a/Source/WebKit/GPUProcess/webrtc/UserMediaCaptureManagerProxy.cpp +++ b/Source/WebKit/GPUProcess/webrtc/UserMediaCaptureManagerProxy.cpp @@ -234,7 +234,7 @@ class UserMediaCaptureManagerProxySourceProxy final bool isObservingMedia = protectedThis->isObservingMedia(); protectedThis->unobserveMedia(); - source->applyConstraints(WTF::move(constraints), [weakThis = WTF::move(weakThis), &constraints, isObservingMedia, callback = WTF::move(callback)](auto&& error) mutable { + source->applyConstraints(constraints, [weakThis = WTF::move(weakThis), constraints, isObservingMedia, callback = WTF::move(callback)](auto&& error) mutable { RefPtr protectedThis = weakThis.get(); if (!protectedThis) { callback(RealtimeMediaSource::ApplyConstraintsError { { }, { } }); From 336c35e2c17c5f8c619d9a136dc8465cdba55e95 Mon Sep 17 00:00:00 2001 From: Kai Tamkun Date: Tue, 30 Jun 2026 13:12:48 -0700 Subject: [PATCH 71/84] [JSC] Stale structure bit in SlowPutArrayStorage https://bugs.webkit.org/show_bug.cgi?id=312487 rdar://172210517 Reviewed by Keith Miller. Correctly sets HasNonConfigurableProperties and related bits during Structure construction if the IndexingType has an array storage shape. Test: JSTests/stress/slowputarraystorage-stale-structure-bit.js * JSTests/stress/slowputarraystorage-stale-structure-bit.js: Added. (get k): (get configurable): (set t): (catch): * Source/JavaScriptCore/runtime/Structure.cpp: (JSC::Structure::Structure): Originally-landed-as: 305413.704@safari-7624-branch (dacb07e7c6bd). rdar://180427686 Canonical link: https://commits.webkit.org/316185@main --- ...slowputarraystorage-stale-structure-bit.js | 34 +++++++++++++++++++ Source/JavaScriptCore/runtime/Structure.cpp | 7 ++-- 2 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 JSTests/stress/slowputarraystorage-stale-structure-bit.js diff --git a/JSTests/stress/slowputarraystorage-stale-structure-bit.js b/JSTests/stress/slowputarraystorage-stale-structure-bit.js new file mode 100644 index 000000000000..0fe63895e357 --- /dev/null +++ b/JSTests/stress/slowputarraystorage-stale-structure-bit.js @@ -0,0 +1,34 @@ +Object.defineProperty(Object.prototype, 0, { get() {}, configurable: true }); +delete Object.prototype[0]; +let target = [1, 2, 3]; +Object.defineProperty(target, "length", { writable: false }); + +let proxyGet = new Proxy(target, { + get: (t, k) => k === "length" ? 999 : t[k] +}); + +try { + let lengthLie = proxyGet.length; + if (lengthLie === 999) { + throw "\"get\" trap successfully returned a lying value (999) for a non-configurable, non-writable property!"; + } +} catch (e) { + if (!(e instanceof TypeError)) { + throw "Expected TypeError for \"get\" trap invariant violation, got: " + e; + } +} + +let proxySet = new Proxy(target, { + set: (t, k, v) => true +}); + +try { + let setSuccess = Reflect.set(proxySet, "length", 999); + if (setSuccess === true && target.length !== 999) { + throw "Reflect.set returned true claiming success on a non-configurable, non-writable property!"; + } +} catch (e) { + if (!(e instanceof TypeError)) { + throw "Expected TypeError for \"set\" trap invariant violation, got: " + e; + } +} diff --git a/Source/JavaScriptCore/runtime/Structure.cpp b/Source/JavaScriptCore/runtime/Structure.cpp index 9fcfeb3702a1..bd9e01f437e4 100644 --- a/Source/JavaScriptCore/runtime/Structure.cpp +++ b/Source/JavaScriptCore/runtime/Structure.cpp @@ -245,15 +245,16 @@ Structure::Structure(VM& vm, JSGlobalObject* globalObject, JSValue prototype, co { bool hasStaticNonEnumerableProperty = m_classInfo->hasStaticPropertyWithAnyOfAttributes(static_cast(PropertyAttribute::DontEnum)); bool hasStaticNonConfigurableProperty = m_classInfo->hasStaticPropertyWithAnyOfAttributes(static_cast(PropertyAttribute::DontDelete)); + bool isArrayStorage = hasAnyArrayStorage(indexingType); setDictionaryKind(NoneDictionaryKind); setIsPinnedPropertyTable(false); setHasAnyKindOfGetterSetterProperties(m_classInfo->hasStaticPropertyWithAnyOfAttributes(static_cast(PropertyAttribute::AccessorOrCustomAccessorOrValue))); setHasReadOnlyOrGetterSetterPropertiesExcludingProto(hasAnyKindOfGetterSetterProperties() || m_classInfo->hasStaticPropertyWithAnyOfAttributes(static_cast(PropertyAttribute::ReadOnly))); - setHasNonEnumerableProperties(hasStaticNonEnumerableProperty || typeInfo.overridesGetOwnPropertySlot()); + setHasNonEnumerableProperties(hasStaticNonEnumerableProperty || typeInfo.overridesGetOwnPropertySlot() || isArrayStorage); setHasSpecialProperties(false); - setHasNonConfigurableProperties(hasStaticNonConfigurableProperty || typeInfo.overridesGetOwnPropertySlot()); - setHasNonConfigurableReadOnlyOrGetterSetterProperties(hasStaticNonConfigurableProperty || (typeInfo.overridesGetOwnPropertySlot() && typeInfo.type() != ArrayType)); + setHasNonConfigurableProperties(hasStaticNonConfigurableProperty || typeInfo.overridesGetOwnPropertySlot() || isArrayStorage); + setHasNonConfigurableReadOnlyOrGetterSetterProperties(hasStaticNonConfigurableProperty || (typeInfo.overridesGetOwnPropertySlot() && typeInfo.type() != ArrayType) || isArrayStorage); setHasUnderscoreProtoPropertyExcludingOriginalProto(false); setIsQuickPropertyAccessAllowedForEnumeration(true); setTransitionPropertyAttributes(0); From e540a45a4fa5f397f287874ffaeea0ee9aaefa6f Mon Sep 17 00:00:00 2001 From: Dawn Morningstar Date: Tue, 30 Jun 2026 13:15:18 -0700 Subject: [PATCH 72/84] [ Gardening ] Mark expectations for layout-tests that have been filed https://bugs.webkit.org/show_bug.cgi?id=318259 rdar://181053851 Unreviewed test gardening. * LayoutTests/platform/ios/TestExpectations: Canonical link: https://commits.webkit.org/316186@main --- LayoutTests/platform/ios/TestExpectations | 115 ++++++++++++++++++++-- 1 file changed, 107 insertions(+), 8 deletions(-) diff --git a/LayoutTests/platform/ios/TestExpectations b/LayoutTests/platform/ios/TestExpectations index a889ed73c5e9..3683fe80edfd 100644 --- a/LayoutTests/platform/ios/TestExpectations +++ b/LayoutTests/platform/ios/TestExpectations @@ -2377,7 +2377,7 @@ webkit.org/b/148806 imported/w3c/web-platform-tests/css/css-multicol/multicol-sp fast/dom/linkify-phone-numbers.html [ Pass ] accessibility/table-exposure-updates-dynamically.html [ Pass ] -accessibility/accessibility-node-reparent.html [ Pass ] +accessibility/accessibility-node-reparent.html [ Pass Timeout ] # rdar://178168616 accessibility/area-element-bounding-box.html [ Pass ] accessibility/aria-actions.html [ Pass ] accessibility/aria-owns-text-stitching.html [ Pass ] @@ -2408,7 +2408,7 @@ accessibility/canvas-drawFocusIfNeeded-bounds-with-object-fit.html [ Pass ] accessibility/canvas-drawFocusIfNeeded-bounds-with-transform.html [ Pass ] accessibility/changing-aria-hidden-with-display-none-parent.html [ Pass ] accessibility/checkbox-mixed-value.html [ Pass ] -accessibility/clip-path-bounding-box.html [ Pass ] +accessibility/clip-path-bounding-box.html [ Pass Failure Timeout ] # rdar://177742349, rdar://179387348 accessibility/css-content-alt-text.html [ Pass ] accessibility/dialog-slotted-content.html [ Pass ] accessibility/dirty-relations-and-modal-tree-update-crash.html [ Pass ] @@ -2470,7 +2470,7 @@ accessibility/nested-custom-element-accname.html [ Pass ] accessibility/node-only-inert-object.html [ Pass ] accessibility/node-only-object-element-rect.html [ Pass ] accessibility/checkbox-radio-element-rect.html [ Pass ] -accessibility/opacity-0-bounding-box.html [ Pass ] +accessibility/opacity-0-bounding-box.html [ Pass Failure ] # rdar://177981724 accessibility/out-of-bounds-rowspan.html [ Pass ] accessibility/out-of-bounds-rowspan-display-none.html [ Pass ] accessibility/out-of-bounds-rowspan-aria-hidden.html [ Pass ] @@ -3274,7 +3274,7 @@ fast/loader/plain-text-document-dark-mode.html [ Pass ] fast/forms/auto-fill-button/caps-lock-indicator-should-be-visible-after-hiding-auto-fill-strong-password-button.html [ Pass ] fast/forms/auto-fill-button/caps-lock-indicator-should-not-be-visible-when-auto-fill-strong-password-button-is-visible.html [ Pass ] -fast/forms/password-scrolled-after-caps-lock-toggled.html [ Pass ] +fast/forms/password-scrolled-after-caps-lock-toggled.html [ Pass Timeout Failure ] # REGRESSION (iOS 13): Three cookie layout tests failing http/wpt/beacon/cors/cors-preflight-cookie.html [ Failure ] @@ -3555,7 +3555,7 @@ http/wpt/mediarecorder/mute-tracks.html [ Pass Failure ] # rdar://80396502 ([ iOS15 ] http/wpt/mediarecorder/pause-recording.html is a flaky crash) http/wpt/mediarecorder/pause-recording.html [ Pass Crash ] -accessibility/misspelling-range.html [ Pass ] +accessibility/misspelling-range.html [ Pass Failure ] # rdar://131398291 # Behavior of navigator-language-ru changed in iOS 15. fast/text/international/system-language/navigator-language/navigator-language-ru.html [ Failure ] @@ -7751,7 +7751,7 @@ webkit.org/b/290670 imported/w3c/web-platform-tests/css/css-viewport/zoom/relati webkit.org/b/290767 [ x86_64 ] imported/w3c/web-platform-tests/webaudio/the-audio-api/the-audioworklet-interface/audioworklet-denormals.https.window.html [ Failure ] -webkit.org/b/290790 editing/selection/ios/show-selection-in-transformed-container.html [ Failure ] +webkit.org/b/290790 editing/selection/ios/show-selection-in-transformed-container.html [ Pass Failure Timeout ] # rdar://177980999 webkit.org/b/290794 fast/viewport/ios/content-visibility-layout-viewport-during-unstable-scroll.html [ Failure ] @@ -8417,7 +8417,7 @@ fast/dom/HTMLLinkElement/prefetch-too-many-clients.html [ Skip ] fast/dynamic/crash-paint-no-documentElement-renderer.html [ Skip ] fast/editing/ruby-with-edited-text-crash.html [ Skip ] fast/events/key-events-in-frame.html [ Skip ] -fast/forms/textarea/textarea-state-restore.html [ Pass Timeout ] +fast/forms/textarea/textarea-state-restore.html [ Pass Timeout Failure ] # rdar://179387373 [ Debug ] fast/layers/top-layer-ancestor-opacity-and-transform-crash.html [ Skip ] [ Release ] fast/mediastream/MediaStream-page-muted.html [ Pass Timeout ] fast/table/double-height-table-no-tbody.html [ Skip ] @@ -8464,7 +8464,7 @@ webkit.org/b/316014 [ Debug ] fast/dom/move-embedded-during-update.html [ Pass F webkit.org/b/316218 fast/scrolling/ios/scroll-into-view-smooth-after-snap.html [ Pass Failure ] -webkit.org/b/316228 fast/dom/Orientation/no-orientation-change-event-when-unparenting-view.html [ Failure ] +webkit.org/b/316228 fast/dom/Orientation/no-orientation-change-event-when-unparenting-view.html [ Pass Timeout Failure ] webkit.org/b/316118 imported/w3c/web-platform-tests/url/IdnaTestV2.any.html [ Failure ] @@ -8495,6 +8495,105 @@ imported/w3c/web-platform-tests/webrtc/RTCPeerConnection-setLocalDescription-off imported/w3c/web-platform-tests/css/css-scroll-snap/input/keyboard-snap-interruption.html [ Skip ] # timeout +compositing/backing/backing-store-attachment-animating-outside-viewport.html [ Pass Failure ] # rdar://178675997 +compositing/color-matching/image-color-matching.html [ Pass Crash ] # rdar://178675338 +editing/editable-region/hit-test-fixed.html [ Pass Failure ] # rdar://178675880 +editing/execCommand/typing-should-not-trigger-scrolling-when-selection-is-visible.html [ Pass Failure ] # rdar://178172917 +editing/input/cocoa/autocorrect-on.html [ Pass Timeout ] +editing/input/cocoa/extended-proofreading.html [ Pass Timeout ] # rdar://177981315 +editing/input/ios/compose-accent-with-hardware-keyboard-when-preventing-keydown.html [ Pass Failure Timeout ] # rdar://176615540 +editing/selection/ios/caret-rect-after-inserting-newlines-in-textarea.html [ Pass Timeout Crash ] # rdar://177980999 +editing/selection/ios/select-text-by-long-press-with-focused-element.html [ Pass Timeout ] # rdar://180007873 +editing/selection/ios/select-text-by-long-press-with-hardware-keyboard.html [ Pass Timeout ] # rdar://180007873 +editing/selection/ios/select-word-for-replacement-extends-grammar-marker.html [ Pass Timeout ] # rdar://180007873 +editing/selection/ios/selection-clip-in-position-relative-text-field.html [ Pass Timeout ] # rdar://177980999 +editing/selection/ios/selection-moves-between-composited-layers.html [ Pass Timeout Crash ] # rdar://177980999, rdar://179387140 +editing/selection/ios/show-edit-menu-with-transparent-caret.html [ Pass Timeout ] # rdar://177980999 +editing/selection/ios/show-grammar-replacements-on-tap.html [ Pass Timeout ] # rdar://177980999 +editing/selection/ios/tap-focused-input-clears-outside-selection.html [ Timeout ] # rdar://180007798 +editing/text-placeholder/caret-before-zero-width-placeholder-in-content-editable-start-of-word.html [ Pass Timeout ] # rdar://178158441 +fast/dynamic/anchor-lock.html [ Pass Failure ] # rdar://178675751 +fast/events/autoscroll-when-input-is-offscreen.html [ Pass Failure ] +fast/events/ios/pdf-modifer-key-down-crash.html [ Pass Crash ] +fast/forms/datalist/data-list-search-input-with-appearance-none.html [ Timeout ] # rdar://178168549 +fast/forms/datalist/datalist-textinput-dynamically-add-options-on-keydown.html [ Pass Timeout ] +fast/forms/ios/select-option-update-1000.html [ Failure Timeout ] # rdar://178168662 +fast/images/imageDocument-title.html [ Pass Crash ] # rdar://178675308 +fast/mediastream/audio-session-category-capture-audio-context.html [ Pass Timeout ] +fast/mediastream/media-stream-video-track-interrupted.html [ Pass Failure ] # rdar://178673961 +fast/mediastream/mediastreamtrack-clone-muted.html [ Pass Timeout ] # rdar://178215298 +fast/screen-orientation/orientation-in-resize-event.html [ Pass Failure Timeout ] # rdar://177741597 +fast/scrolling/scroll-anchoring/heuristic-enabled-below-threshold.html [ Pass Failure ] # rdar://178168995 +fast/text-extraction/text-extraction-scroll-fallback-to-large-container.html [ Pass Failure ] # rdar://178169238 +fast/viewport/ios/shrink-to-fit-for-page-without-viewport-meta.html [ Failure ] # rdar://178168569 +fast/viewport/ios/width-is-device-width-overflowing-body-overflow-hidden-tall.html [ Failure ] # rdar://178168569 +fast/visual-viewport/ios/visual-viewport-dimensions-during-scroll-with-keyboard.html [ Timeout ] # rdar://178016084 +http/tests/cache/cancel-multiple-post-xhrs.html [ Pass Failure ] # rdar://178676120 +http/tests/download/anchor-download-redirect-cross-origin.html [ Pass Crash ] # rdar://178214780 +http/tests/privateClickMeasurement/database-disabled-in-ephemeral-session.html [ Pass Timeout Crash ] # rdar://176608751, rdar://179387095 +http/tests/security/referrer-policy-header-invalid.html [ Pass Timeout ] # rdar://178168636 +http/tests/site-isolation/datalist-cross-origin-iframe.html [ Timeout ] # rdar://179387257 +http/tests/site-isolation/selection-focus.html [ ImageOnlyFailure ] # rdar://163216880 +http/tests/site-isolation/type-in-cross-origin-iframe.html [ Failure Timeout ] # rdar://178168718 +http/wpt/service-workers/persistent-modules.html [ Pass Failure ] # rdar://178676392 +imported/w3c/web-platform-tests/IndexedDB/interleaved-cursors-small.any.html [ Pass Failure ] # rdar://178158917 +imported/w3c/web-platform-tests/IndexedDB/nested-cloning-large-multiple.any.html [ Pass Failure ] # rdar://178158917 +imported/w3c/web-platform-tests/IndexedDB/nested-cloning-large-multiple.any.worker.html [ Pass Failure ] # rdar://178158917 +imported/w3c/web-platform-tests/css/css-anchor-position/scroll-to-anchored-fixed-000.html [ Pass ImageOnlyFailure ] # rdar://178676781 +imported/w3c/web-platform-tests/css/css-overflow/line-clamp/line-clamp-033.html [ Pass Crash ImageOnlyFailure ] +imported/w3c/web-platform-tests/css/css-shapes/spec-examples/shape-outside-011.html [ Pass Failure ] # rdar://178676728 +imported/w3c/web-platform-tests/css/css-text-decor/text-emphasis-position-auto-002.html [ Pass ImageOnlyFailure ] # rdar://178676895 +imported/w3c/web-platform-tests/css/css-view-transitions/view-transition-waituntil-finished-promise.html [ Pass Failure ] # rdar://178676670 +imported/w3c/web-platform-tests/css/css-writing-modes/text-orientation-upright-srl-018.xht [ Pass ImageOnlyFailure ] # rdar://178676919 +imported/w3c/web-platform-tests/css/cssom-view/scrollIntoView-smooth.html [ Pass Failure ] # rdar://178676605 +imported/w3c/web-platform-tests/custom-elements/reactions/Document.html [ Pass Crash ] # rdar://178158852 +imported/w3c/web-platform-tests/html/browsers/browsing-the-web/navigating-across-documents/cross-origin-top-navigation-with-user-activation-in-parent.window.html [ Pass Failure ] # rdar://178214939 +imported/w3c/web-platform-tests/html/canvas/element/manual/imagebitmap/createImageBitmap-origin.sub.html [ Failure ] # rdar://178012093 +imported/w3c/web-platform-tests/html/cross-origin-embedder-policy/anonymous-iframe/cookie.tentative.https.window.html [ Pass Crash Timeout ] # rdar://178158581 +imported/w3c/web-platform-tests/html/rendering/replaced-elements/attributes-for-embedded-content-and-images/video-default-object-height-constrained-by-max-width.html [ Crash ] # rdar://178169483 +imported/w3c/web-platform-tests/html/rendering/widgets/shadow-dom.html [ Pass Failure ] # rdar://178546346 +imported/w3c/web-platform-tests/html/semantics/embedded-content/the-canvas-element/security.pattern.fillStyle.sub.html [ Failure ] # rdar://178018070 +imported/w3c/web-platform-tests/html/semantics/forms/form-submission-target/form-target-blank-useractivation.html [ Pass Failure ] # rdar://180007963 +imported/w3c/web-platform-tests/navigation-api/navigate-event/defer/tentative/defer-same-document.html [ Pass Crash ] +imported/w3c/web-platform-tests/scroll-to-text-fragment/redirects.html [ Pass Failure ] # rdar://178158764 +imported/w3c/web-platform-tests/shadow-dom/event-on-pseudo-element-crash.html [ Pass Timeout ] # rdar://179387553 +imported/w3c/web-platform-tests/webaudio/the-audio-api/the-audiocontext-interface/audiocontext-getoutputtimestamp-cross-realm.html [ Pass Failure ] # rdar://179388032 +imported/w3c/web-platform-tests/webaudio/the-audio-api/the-audioworklet-interface/audioworkletnode-automatic-pull.https.html [ Pass Failure ] # rdar://178172495 +media/audio-session-category-play-unmute-pause.html [ Failure ] # rdar://178158431 +media/media-visible-in-viewport-in-fullscreen.html [ Timeout ] # rdar://178016189 +media/media-vp8-webm-with-poster.html [ Timeout Crash ] # rdar://178016189 +media/video-pause-immediately.html [ Pass Failure ] # rdar://178675666 +media/video-webm-seek-multi-audio-tracks.html [ Timeout ] # rdar://180007922 +quicklook/word-legacy.html [ Failure ] # rdar://174850780 +quicklook/word.html [ Failure ] # rdar://174850780 +svg/filters/feMorphology-negative-radius.html [ Pass Crash ] # rdar://178675229 +webgl/1.0.x/conformance/textures/misc/texture-srgb-upload.html [ Timeout ] # rdar://176399952 +workers/worker-set-delete-terminate-crash.html [ Timeout ] # rdar://178590046 +svg/transforms/nested-svg-transform-attribute-creates-layer.html [ Failure ] # rdar://180457293 +svg/compositing/segment-removed-after-anchor-decomposited-layer-tree.html [ Failure ] # rdar://180457293 +svg/compositing/transform-change-repainting-viewBox-repaintRects.html [ Failure ] # rdar://180457293 +svg/compositing/anonymous-RenderSVGViewportContainer-no-repaints.html [ Failure ] # rdar://180457293 +fast/events/touch/ios/touch-event-regions-layer-tree/svg-image-with-layer-based-svg-engine.html [ Failure ] # rdar://180457293 +fast/events/touch/ios/touch-event-regions-layer-tree/svg-path-with-layer-based-svg-engine.html [ Failure ] # rdar://180457293 +fast/events/touch/ios/touch-event-regions-layer-tree/svg-text-with-layer-based-svg-engine.html [ Failure ] # rdar://180457293 +fast/events/touch/ios/touch-event-regions-layer-tree/mousemove-mouseup.html [ Failure ] # rdar://180457293 +fast/forms/ios/click-should-not-suppress-misspelling.html [ Failure ] # webkit.org/b/318249 +fast/forms/ios/force-gregorian-calendar-for-credit-card-expiry.html [ Failure ] # webkit.org/b/318249 +fast/forms/ios/insert-autofill-suggestion.html [ Failure ] # webkit.org/b/318249 +fast/forms/ios/suppress-software-keyboard-while-focusing-input.html [ Failure ] # webkit.org/b/318249 +editing/selection/ios/do-not-hide-selection-in-visible-field.html [ Timeout ] # webkit.org/b/318250 +editing/selection/ios/update-selection-after-iframe-scroll.html [ Timeout ] # webkit.org/b/318250 +fast/events/ios/autocorrect-with-apostrophe.html [ Timeout ] # webkit.org/b/318251 +fast/events/ios/do-not-show-keyboard-when-focusing-after-blur.html [ Timeout ] # webkit.org/b/318251 +fast/forms/ios/hide-keyboard-on-node-removal.html [ Timeout ] # webkit.org/b/318252 +fast/forms/ios/zoom-to-reveal-focused-element-after-delay.html [ Timeout ] # webkit.org/b/318252 +editing/caret/ios/place-caret-after-autocorrected-word.html [ Failure ] # webkit.org/b/318253 +editing/input/ios/typing-with-inline-predictions.html [ Pass Timeout ] # webkit.org/b/318254 +imported/w3c/web-platform-tests/content-security-policy/inheritance/history.sub.html [ Failure ] # webkit.org/b/318255 +imported/w3c/web-platform-tests/digital-credentials/mdoc/mixed-requests.https.html [ Failure ] # webkit.org/b/318256 +imported/w3c/web-platform-tests/html/semantics/forms/the-input-element/number-constraint-validation.html [ Pass Failure ] # webkit.org/b/318257 +ipc/fecolormatrix-type-values-mismatch-crash.html [ Failure ] # webkit.org/b/318258 + webkit.org/b/317884 imported/w3c/web-platform-tests/digital-credentials/get-non-fully-active.https.html [ Skip ] webkit.org/b/318157 [ Debug ] imported/w3c/web-platform-tests/IndexedDB/interleaved-cursors-small.any.sharedworker.html [ Failure ] From c5fabb4129dc9dcba8ebba81189a118d2997a0ab Mon Sep 17 00:00:00 2001 From: Basuke Suzuki Date: Tue, 30 Jun 2026 13:17:03 -0700 Subject: [PATCH 73/84] Remove shouldRestrictHTTPResponseAccess from NetworkResourceLoadParameters rdar://174708348 Reviewed by Ryosuke Niwa. The shouldRestrictHTTPResponseAccess field in NetworkResourceLoadParameters was sent from WebContent process to NetworkProcess via IPC with no validation. A compromised WebContent process could set it to false to bypass response header sanitization (Set-Cookie stripping, cross-origin header filtering). The field was originally introduced in 2018 as a runtime-configurable flag (RestrictedHTTPResponseAccess preference) to allow WK1 to opt out of sanitization. After WK1 was removed, the preference was hardcoded to true (288687@main), but the IPC parameter was never cleaned up. Since shouldPerformSecurityChecks() unconditionally returns true, this field always carried the value true from a legitimate WebContent process. Remove it entirely and make sanitization unconditional on the NetworkProcess side. No new tests (hardening, no behavior change for legitimate WebContent process). * Source/WebKit/NetworkProcess/NetworkResourceLoadParameters.h: * Source/WebKit/NetworkProcess/NetworkResourceLoadParameters.serialization.in: * Source/WebKit/NetworkProcess/NetworkResourceLoader.cpp: (WebKit::NetworkResourceLoader::sanitizeResponseIfPossible): * Source/WebKit/WebProcess/Network/WebLoaderStrategy.cpp: (WebKit::WebLoaderStrategy::scheduleLoadFromNetworkProcess): (WebKit::WebLoaderStrategy::loadResourceSynchronously): (WebKit::WebLoaderStrategy::startPingLoad): (WebKit::WebLoaderStrategy::preconnectTo): Originally-landed-as: 305413.689@safari-7624-branch (1db91ab9400c). rdar://180428250 Canonical link: https://commits.webkit.org/316187@main --- .../NetworkResourceLoadParameters.h | 1 - ...orkResourceLoadParameters.serialization.in | 2 -- .../NetworkProcess/NetworkResourceLoader.cpp | 31 ++++++++----------- .../WebProcess/Network/WebLoaderStrategy.cpp | 7 ++--- 4 files changed, 15 insertions(+), 26 deletions(-) diff --git a/Source/WebKit/NetworkProcess/NetworkResourceLoadParameters.h b/Source/WebKit/NetworkProcess/NetworkResourceLoadParameters.h index accdf25d87f0..a03e3d77f978 100644 --- a/Source/WebKit/NetworkProcess/NetworkResourceLoadParameters.h +++ b/Source/WebKit/NetworkProcess/NetworkResourceLoadParameters.h @@ -90,7 +90,6 @@ struct NetworkResourceLoadParameters { WebCore::CrossOriginEmbedderPolicy parentCrossOriginEmbedderPolicy { }; WebCore::CrossOriginEmbedderPolicy crossOriginEmbedderPolicy { }; WebCore::HTTPHeaderMap originalRequestHeaders { }; - bool shouldRestrictHTTPResponseAccess { false }; WebCore::PreflightPolicy preflightPolicy { WebCore::PreflightPolicy::Consider }; bool shouldEnableCrossOriginResourcePolicy { false }; Vector> frameAncestorOrigins { }; diff --git a/Source/WebKit/NetworkProcess/NetworkResourceLoadParameters.serialization.in b/Source/WebKit/NetworkProcess/NetworkResourceLoadParameters.serialization.in index 25cfa9505188..a06e4fd47de2 100644 --- a/Source/WebKit/NetworkProcess/NetworkResourceLoadParameters.serialization.in +++ b/Source/WebKit/NetworkProcess/NetworkResourceLoadParameters.serialization.in @@ -66,8 +66,6 @@ enum class WebKit::NavigatingToAppBoundDomain : bool; WebCore::CrossOriginEmbedderPolicy crossOriginEmbedderPolicy; WebCore::HTTPHeaderMap originalRequestHeaders; - bool shouldRestrictHTTPResponseAccess; - WebCore::PreflightPolicy preflightPolicy; bool shouldEnableCrossOriginResourcePolicy; diff --git a/Source/WebKit/NetworkProcess/NetworkResourceLoader.cpp b/Source/WebKit/NetworkProcess/NetworkResourceLoader.cpp index e4055d4dadb0..9f8fca319ff4 100644 --- a/Source/WebKit/NetworkProcess/NetworkResourceLoader.cpp +++ b/Source/WebKit/NetworkProcess/NetworkResourceLoader.cpp @@ -156,23 +156,21 @@ NetworkResourceLoader::NetworkResourceLoader(NetworkResourceLoadParameters&& par if (CheckedPtr session = connection.networkProcess().networkSession(sessionID())) m_cache = session->cache(); - if (synchronousReply || m_parameters.shouldRestrictHTTPResponseAccess || m_parameters.options.keepAlive) { - NetworkLoadChecker::LoadType requestLoadType = isMainFrameLoad() ? NetworkLoadChecker::LoadType::MainFrame : NetworkLoadChecker::LoadType::Other; - m_networkLoadChecker = NetworkLoadChecker::create(Ref { connection.networkProcess() }.get(), this, &connection.schemeRegistry(), FetchOptions { m_parameters.options }, - sessionID(), webPageProxyID(), HTTPHeaderMap { m_parameters.originalRequestHeaders }, URL { m_parameters.request.url() }, - URL { m_parameters.documentURL }, m_parameters.sourceOrigin.copyRef(), m_parameters.topOrigin.copyRef(), m_parameters.parentOrigin(), - m_parameters.preflightPolicy, originalRequest().httpReferrer(), m_parameters.allowPrivacyProxy, m_parameters.advancedPrivacyProtections, - shouldCaptureExtraNetworkLoadMetrics(), requestLoadType); - - RefPtr networkLoadChecker = m_networkLoadChecker; - if (m_parameters.cspResponseHeaders) - networkLoadChecker->setCSPResponseHeaders(ContentSecurityPolicyResponseHeaders { m_parameters.cspResponseHeaders.value() }); - networkLoadChecker->setParentCrossOriginEmbedderPolicy(m_parameters.parentCrossOriginEmbedderPolicy); - networkLoadChecker->setCrossOriginEmbedderPolicy(m_parameters.crossOriginEmbedderPolicy); + NetworkLoadChecker::LoadType requestLoadType = isMainFrameLoad() ? NetworkLoadChecker::LoadType::MainFrame : NetworkLoadChecker::LoadType::Other; + m_networkLoadChecker = NetworkLoadChecker::create(Ref { connection.networkProcess() }.get(), this, &connection.schemeRegistry(), FetchOptions { m_parameters.options }, + sessionID(), webPageProxyID(), HTTPHeaderMap { m_parameters.originalRequestHeaders }, URL { m_parameters.request.url() }, + URL { m_parameters.documentURL }, m_parameters.sourceOrigin.copyRef(), m_parameters.topOrigin.copyRef(), m_parameters.parentOrigin(), + m_parameters.preflightPolicy, originalRequest().httpReferrer(), m_parameters.allowPrivacyProxy, m_parameters.advancedPrivacyProtections, + shouldCaptureExtraNetworkLoadMetrics(), requestLoadType); + + RefPtr networkLoadChecker = m_networkLoadChecker; + if (m_parameters.cspResponseHeaders) + networkLoadChecker->setCSPResponseHeaders(ContentSecurityPolicyResponseHeaders { m_parameters.cspResponseHeaders.value() }); + networkLoadChecker->setParentCrossOriginEmbedderPolicy(m_parameters.parentCrossOriginEmbedderPolicy); + networkLoadChecker->setCrossOriginEmbedderPolicy(m_parameters.crossOriginEmbedderPolicy); #if ENABLE(CONTENT_EXTENSIONS) - networkLoadChecker->setContentExtensionController(URL { m_parameters.mainDocumentURL }, URL { m_parameters.frameURL }, m_parameters.userContentControllerIdentifier); + networkLoadChecker->setContentExtensionController(URL { m_parameters.mainDocumentURL }, URL { m_parameters.frameURL }, m_parameters.userContentControllerIdentifier); #endif - } if (synchronousReply) m_synchronousLoadData = makeUnique(WTF::move(synchronousReply)); } @@ -1507,9 +1505,6 @@ static bool shouldSanitizeResponse(const NetworkProcess& process, std::optional< ResourceResponse NetworkResourceLoader::sanitizeResponseIfPossible(ResourceResponse&& response, ResourceResponse::SanitizationType type) { - if (!m_parameters.shouldRestrictHTTPResponseAccess) - return WTF::move(response); - if (shouldSanitizeResponse(Ref { m_connection->networkProcess() }.get(), pageID(), parameters().options, originalRequest().url())) response.sanitizeHTTPHeaderFields(type); diff --git a/Source/WebKit/WebProcess/Network/WebLoaderStrategy.cpp b/Source/WebKit/WebProcess/Network/WebLoaderStrategy.cpp index 71d29d8cf97f..41126289c29f 100644 --- a/Source/WebKit/WebProcess/Network/WebLoaderStrategy.cpp +++ b/Source/WebKit/WebProcess/Network/WebLoaderStrategy.cpp @@ -573,8 +573,6 @@ void WebLoaderStrategy::scheduleLoadFromNetworkProcess(ResourceLoader& resourceL } } - loadParameters.shouldRestrictHTTPResponseAccess = shouldPerformSecurityChecks(); - loadParameters.isMainFrameNavigation = isMainFrameNavigation; if (loadParameters.isMainFrameNavigation && document) { // Fall back to use opener's cross-origin opener policy like in Document::initSecurityContext. @@ -891,7 +889,6 @@ void WebLoaderStrategy::loadResourceSynchronously(FrameLoader& frameLoader, WebC loadParameters.storedCredentialsPolicy = options.credentials == FetchOptions::Credentials::Omit ? StoredCredentialsPolicy::DoNotUse : StoredCredentialsPolicy::Use; loadParameters.clientCredentialPolicy = clientCredentialPolicy; loadParameters.shouldClearReferrerOnHTTPSToHTTPRedirect = shouldClearReferrerOnHTTPSToHTTPRedirect(webFrame ? protect(webFrame->coreLocalFrame()).get() : nullptr); - loadParameters.shouldRestrictHTTPResponseAccess = shouldPerformSecurityChecks(); loadParameters.options = options; loadParameters.sourceOrigin = document->securityOrigin(); @@ -978,7 +975,7 @@ void WebLoaderStrategy::startPingLoad(LocalFrame& frame, ResourceRequest& reques loadParameters.options = options; loadParameters.originalRequestHeaders = originalRequestHeaders; loadParameters.shouldClearReferrerOnHTTPSToHTTPRedirect = shouldClearReferrerOnHTTPSToHTTPRedirect(&frame); - loadParameters.shouldRestrictHTTPResponseAccess = shouldPerformSecurityChecks(); + if (policyCheck == ContentSecurityPolicyImposition::DoPolicyCheck && !document->shouldBypassMainWorldContentSecurityPolicy()) { if (CheckedPtr contentSecurityPolicy = document->contentSecurityPolicy()) loadParameters.cspResponseHeaders = contentSecurityPolicy->responseHeaders(); @@ -1060,7 +1057,7 @@ void WebLoaderStrategy::preconnectTo(WebCore::ResourceRequest&& request, WebPage parameters.parentPID = legacyPresentingApplicationPID(); parameters.storedCredentialsPolicy = storedCredentialsPolicy; parameters.shouldPreconnectOnly = PreconnectOnly::Yes; - parameters.shouldRestrictHTTPResponseAccess = shouldPerformSecurityChecks(); + // FIXME: Use the proper destination once all fetch options are passed. parameters.options.destination = FetchOptions::Destination::EmptyString; #if ENABLE(APP_BOUND_DOMAINS) From b48b4d4d4034d4a207ba2e0d61570ecc051f30a4 Mon Sep 17 00:00:00 2001 From: Eric Carlson Date: Tue, 30 Jun 2026 13:18:29 -0700 Subject: [PATCH 74/84] [WebCore] Use-after-free in InternalAudioEncoderCocoa on ASBD change because converter->finish() promise is discarded https://bugs.webkit.org/show_bug.cgi?id=314107 rdar://175402146 Reviewed by Youenn Fablet and David Kilzer. When WebCodecs AudioEncoder receives consecutive AudioData frames whose PCM format differs (e.g. f32-planar then s16) but whose sampleRate and numberOfChannels are unchanged, encode() replaces the AudioSampleBufferConverter and calls finish() on the old one. The returned Ref was dropped, so nothing kept the encoder alive while the old converter's async drain ran on its own serial queue. If the client then called close(), which keeps the encoder alive for the new converter only, the old converter could fire its CMBufferQueueTrigger into a freed InternalAudioEncoderCocoa, producing a heap use-after-free in compressedAudioOutputBufferCallback. Apply the same whenSettled keep-alive that close() already uses, so the encoder stays alive on queueSingleton() until the old converter has finished draining. Test: http/wpt/webcodecs/audio-encoder-pcm-format-change.html * LayoutTests/http/wpt/webcodecs/audio-encoder-pcm-format-change-expected.txt: Added. * LayoutTests/http/wpt/webcodecs/audio-encoder-pcm-format-change.html: Added. * Source/WebCore/platform/audio/cocoa/AudioEncoderCocoa.cpp: (WebCore::InternalAudioEncoderCocoa::encode): Originally-landed-as: 305413.864@safari-7624-branch (8230e88bd918). rdar://180436545 Canonical link: https://commits.webkit.org/316188@main --- ...dio-encoder-pcm-format-change-expected.txt | 3 ++ .../audio-encoder-pcm-format-change.html | 51 +++++++++++++++++++ .../audio/cocoa/AudioEncoderCocoa.cpp | 4 +- 3 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 LayoutTests/http/wpt/webcodecs/audio-encoder-pcm-format-change-expected.txt create mode 100644 LayoutTests/http/wpt/webcodecs/audio-encoder-pcm-format-change.html diff --git a/LayoutTests/http/wpt/webcodecs/audio-encoder-pcm-format-change-expected.txt b/LayoutTests/http/wpt/webcodecs/audio-encoder-pcm-format-change-expected.txt new file mode 100644 index 000000000000..3d61793dea0e --- /dev/null +++ b/LayoutTests/http/wpt/webcodecs/audio-encoder-pcm-format-change-expected.txt @@ -0,0 +1,3 @@ + +PASS AudioEncoder handles switching PCM sample format from f32-planar to s16 mid-stream + diff --git a/LayoutTests/http/wpt/webcodecs/audio-encoder-pcm-format-change.html b/LayoutTests/http/wpt/webcodecs/audio-encoder-pcm-format-change.html new file mode 100644 index 000000000000..5544ce2eccf0 --- /dev/null +++ b/LayoutTests/http/wpt/webcodecs/audio-encoder-pcm-format-change.html @@ -0,0 +1,51 @@ + + + + + + + + + + diff --git a/Source/WebCore/platform/audio/cocoa/AudioEncoderCocoa.cpp b/Source/WebCore/platform/audio/cocoa/AudioEncoderCocoa.cpp index f8c04443535b..fa49a0e92d4e 100644 --- a/Source/WebCore/platform/audio/cocoa/AudioEncoderCocoa.cpp +++ b/Source/WebCore/platform/audio/cocoa/AudioEncoderCocoa.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Apple Inc. All rights reserved. + * Copyright (C) 2024-2026 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -314,7 +314,7 @@ Ref InternalAudioEncoderCocoa::encode(AudioEncoder: if (*asbd != m_inputDescription) { if (RefPtr converter = std::exchange(m_converter, { })) - converter->finish(); + converter->finish()->whenSettled(queueSingleton(), [protectedThis = Ref { *this }] { }); m_inputDescription = *asbd; AudioSampleBufferConverter::Options options = { From d75a9fda98e0e3033e8e7060fd341f0bf1c01d21 Mon Sep 17 00:00:00 2001 From: Shu-yu Guo Date: Tue, 30 Jun 2026 13:22:18 -0700 Subject: [PATCH 75/84] [JSC] Do not cache property absence on dictionaries https://bugs.webkit.org/show_bug.cgi?id=313265 rdar://175520268 Reviewed by Yusuke Suzuki. Structures with CachedDictionaryKind can add new properties without transitioning structures. Therefore, property absences are not cacheable. This PR fixes a bug in DFG which incorrectly caches property absences on structures with CachedDictionaryKind. Test: JSTests/stress/dfg-ensure-absence-cached-dictionary-then.js * JSTests/stress/dfg-ensure-absence-cached-dictionary-then.js: Added. (createObject1): (createObject2): (opt): (main): * Source/JavaScriptCore/dfg/DFGGraph.cpp: (JSC::DFG::Graph::tryEnsureAbsence): * Source/JavaScriptCore/runtime/Structure.h: (JSC::Structure::propertyAccessesAreCacheableForAbsence): Originally-landed-as: 305413.731@safari-7624-branch (343b8326d944). rdar://180437776 Canonical link: https://commits.webkit.org/316189@main --- ...g-ensure-absence-cached-dictionary-then.js | 87 +++++++++++++++++++ Source/JavaScriptCore/dfg/DFGGraph.cpp | 4 + Source/JavaScriptCore/runtime/Structure.h | 2 + 3 files changed, 93 insertions(+) create mode 100644 JSTests/stress/dfg-ensure-absence-cached-dictionary-then.js diff --git a/JSTests/stress/dfg-ensure-absence-cached-dictionary-then.js b/JSTests/stress/dfg-ensure-absence-cached-dictionary-then.js new file mode 100644 index 000000000000..5edeb5ee04f8 --- /dev/null +++ b/JSTests/stress/dfg-ensure-absence-cached-dictionary-then.js @@ -0,0 +1,87 @@ +function createObject1() { + const tmp = { + toJSON: 1, + a: 1, + }; + + Object.create(tmp); + + return tmp; +} + +function createObject2() { + const tmp = { + b: 1, + toJSON: {} + }; + + Object.create(tmp); + + return tmp; +} + +function opt(container1, object2, array, thenable, flags) { + const promise = new Promise(() => {}); + + container1.x; + thenable.x; + + const object1 = Object.getPrototypeOf(container1); + + let tmp = object1; + object1.a; + + if (flags & 1) { + tmp = object2; + tmp.b; + + 0[0]; + } + + +tmp.toJSON; + +tmp.toJSON; + + array[0]; + Promise.resolve(+tmp.toJSON === 1 ? thenable : promise); + + array[0] = 2.3023e-320; +} + +function main() { + noDFG(main); + + const object1 = createObject1(); + const object2 = createObject2(); + + createObject1().z = 1; + + const container1 = Object.create(object1); + + const thenable = { + x: 1 + }; + + for (let i = 0; i < 100; i++) { + thenable['a' + i] = 1; + } + + const array = { + 0: 1.1 + }; + + JSON.stringify(container1); + + for (let i = 0; i < 200; i++) { + opt(container1, object2, array, thenable, i); + } + + thenable.__defineGetter__('then', () => { + array[0] = {}; + }); + + opt(container1, object2, array, thenable, 0); + + array[0].x; +} + +main(); diff --git a/Source/JavaScriptCore/dfg/DFGGraph.cpp b/Source/JavaScriptCore/dfg/DFGGraph.cpp index 9c169d6f3ef4..9acd38e1d10e 100644 --- a/Source/JavaScriptCore/dfg/DFGGraph.cpp +++ b/Source/JavaScriptCore/dfg/DFGGraph.cpp @@ -1540,6 +1540,10 @@ ObjectPropertyConditionSet Graph::tryEnsureAbsence(JSGlobalObject* globalObject, return ObjectPropertyConditionSet::invalid(); auto isAbsenceCacheable = [&](Structure* structure) { + // Absences cannot be cached on any dictionaries, including CachedDictionaryKind, because + // new properties can be added without structure transitions. + if (structure->isDictionary()) + return false; if (structure->typeInfo().overridesGetOwnPropertySlot()) return false; if (!structure->propertyAccessesAreCacheable()) diff --git a/Source/JavaScriptCore/runtime/Structure.h b/Source/JavaScriptCore/runtime/Structure.h index 3471cc94a807..09d5a996c912 100644 --- a/Source/JavaScriptCore/runtime/Structure.h +++ b/Source/JavaScriptCore/runtime/Structure.h @@ -338,6 +338,8 @@ class Structure : public JSCell { bool propertyAccessesAreCacheableForAbsence() { + // FIXME: dictionaries cannot be cached for absence, so check for dictionaries here instead + // of at all call sites. return !typeInfo().getOwnPropertySlotIsImpureForPropertyAbsence(); } From a0291f16f65d6af683f9dc070d837206e8703bc5 Mon Sep 17 00:00:00 2001 From: Antti Koivisto Date: Tue, 30 Jun 2026 13:23:26 -0700 Subject: [PATCH 76/84] [css-mixins-1] Serialize types https://bugs.webkit.org/show_bug.cgi?id=318225 rdar://181027296 Reviewed by Tim Nguyen. Add CSSCustomPropertySyntax serialization support and use it for @function serialization. * LayoutTests/imported/w3c/web-platform-tests/css/css-mixins/at-function-cssom-expected.txt: * Source/WebCore/css/CSSFunctionRule.cpp: (WebCore::CSSFunctionRule::getParameters const): (WebCore::CSSFunctionRule::returnType const): (WebCore::CSSFunctionRule::cssText const): * Source/WebCore/css/parser/CSSCustomPropertySyntax.cpp: (WebCore::typeNameForType): (WebCore::serializeCustomPropertySyntax): (WebCore::serializeCustomPropertySyntaxAsCSSType): * Source/WebCore/css/parser/CSSCustomPropertySyntax.h: Canonical link: https://commits.webkit.org/316190@main --- .../css-mixins/at-function-cssom-expected.txt | 16 ++--- Source/WebCore/css/CSSFunctionRule.cpp | 23 ++++-- .../css/parser/CSSCustomPropertySyntax.cpp | 72 +++++++++++++++++++ .../css/parser/CSSCustomPropertySyntax.h | 12 +++- 4 files changed, 110 insertions(+), 13 deletions(-) diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-mixins/at-function-cssom-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/css/css-mixins/at-function-cssom-expected.txt index 111ae2e696d4..e393fc1e8d64 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/css/css-mixins/at-function-cssom-expected.txt +++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-mixins/at-function-cssom-expected.txt @@ -13,21 +13,21 @@ PASS item() PASS Indexed property getter PASS @supports in body PASS CSSFunctionRule.name -FAIL CSSFunctionRule.getParameters() assert_object_equals: property "type" expected "" got "*" -FAIL CSSFunctionRule.returnType assert_equals: expected "" but got "*" +PASS CSSFunctionRule.getParameters() +PASS CSSFunctionRule.returnType PASS CSSFunctionRule escapes PASS CSSFunctionRule.cssText (--empty) -FAIL CSSFunctionRule.cssText (--ret-length) assert_equals: expected "@function --ret-length() returns { }" but got "@function --ret-length() { }" -FAIL CSSFunctionRule.cssText (--ret-length-auto) assert_equals: expected "@function --ret-length-auto() returns type( | auto) { }" but got "@function --ret-length-auto() { }" +PASS CSSFunctionRule.cssText (--ret-length) +PASS CSSFunctionRule.cssText (--ret-length-auto) PASS CSSFunctionRule.cssText (--param-single) -FAIL CSSFunctionRule.cssText (--param-typed) assert_equals: expected "@function --param-typed(--x ) { }" but got "@function --param-typed(--x) { }" -FAIL CSSFunctionRule.cssText (--param-typed-default) assert_equals: expected "@function --param-typed-default(--x : 10px) { }" but got "@function --param-typed-default(--x: 10px) { }" +PASS CSSFunctionRule.cssText (--param-typed) +PASS CSSFunctionRule.cssText (--param-typed-default) PASS CSSFunctionRule.cssText (--param-default) PASS CSSFunctionRule.cssText (--param-multi) -FAIL CSSFunctionRule.cssText (--param-multi-mixed) assert_equals: expected "@function --param-multi-mixed(--x: 10px, --y, --z ) { }" but got "@function --param-multi-mixed(--x: 10px, --y, --z) { }" +PASS CSSFunctionRule.cssText (--param-multi-mixed) PASS CSSFunctionRule.cssText (--body-result) PASS CSSFunctionRule.cssText (--body-locals) -FAIL CSSFunctionRule.cssText (--param-type-fn) assert_equals: expected "@function --param-type-fn(--x ) { }" but got "@function --param-type-fn(--x) { }" +PASS CSSFunctionRule.cssText (--param-type-fn) PASS CSSFunctionRule.cssText (--param-type-fn-uni) PASS CSSFunctionRule.cssText (--ret-type-fn) PASS CSSFunctionRule.cssText (--ret-type-fn-uni) diff --git a/Source/WebCore/css/CSSFunctionRule.cpp b/Source/WebCore/css/CSSFunctionRule.cpp index 72f8f6eaec7f..85cc8f439fbe 100644 --- a/Source/WebCore/css/CSSFunctionRule.cpp +++ b/Source/WebCore/css/CSSFunctionRule.cpp @@ -50,9 +50,11 @@ auto CSSFunctionRule::getParameters() const -> Vector { return WTF::map(styleRuleFunction().parameters(), [](const auto& parameter) { RefPtr defaultValue = parameter.defaultValue; + StringBuilder type; + serializeCustomPropertySyntax(type, parameter.type); return FunctionParameter { .name = parameter.name, - .type = "*"_s, // FIXME: Implement. + .type = type.toString(), // FIXME: The spec says: // "The default value of the function parameter, or `null` if the argument does not have a default". // But WPT tests currently expect the value to missing/undefined, not `null`, so we are using @@ -64,7 +66,9 @@ auto CSSFunctionRule::getParameters() const -> Vector String CSSFunctionRule::returnType() const { - return "*"_s; + StringBuilder builder; + serializeCustomPropertySyntax(builder, styleRuleFunction().returnType()); + return builder.toString(); } String CSSFunctionRule::cssText() const @@ -78,14 +82,25 @@ String CSSFunctionRule::cssText() const for (auto& parameter : styleRuleFunction().parameters()) { builder.append(separator); serializeIdentifier(builder, parameter.name); - // FIXME: Serialize the type. + + if (!parameter.type.isUniversal()) { + builder.append(' '); + serializeCustomPropertySyntaxAsCSSType(builder, parameter.type); + } if (RefPtr defaultValue = parameter.defaultValue) builder.append(": "_s, defaultValue->serialize()); separator = ", "_s; } - builder.append(") { "_s); + builder.append(')'); + + if (auto& returnType = styleRuleFunction().returnType(); !returnType.isUniversal()) { + builder.append(" returns "_s); + serializeCustomPropertySyntaxAsCSSType(builder, returnType); + } + + builder.append(" { "_s); for (unsigned index = 0; index < length(); ++index) { Ref rule = *item(index); diff --git a/Source/WebCore/css/parser/CSSCustomPropertySyntax.cpp b/Source/WebCore/css/parser/CSSCustomPropertySyntax.cpp index 0cd7f30ce86c..f8088f756f1e 100644 --- a/Source/WebCore/css/parser/CSSCustomPropertySyntax.cpp +++ b/Source/WebCore/css/parser/CSSCustomPropertySyntax.cpp @@ -25,12 +25,14 @@ #include "config.h" #include "CSSCustomPropertySyntax.h" +#include "CSSMarkup.h" #include "CSSParserIdioms.h" #include "CSSParserTokenRange.h" #include "CSSPropertyParserConsumer+Primitives.h" #include "CSSTokenizer.h" #include #include +#include namespace WebCore { @@ -205,4 +207,74 @@ auto CSSCustomPropertySyntax::typeForTypeName(StringView dataTypeName) -> Type return typeMap.get(dataTypeName, Type::Unknown); } +static ASCIILiteral typeNameForType(CSSCustomPropertySyntax::Type type) +{ + using Type = CSSCustomPropertySyntax::Type; + switch (type) { + case Type::Length: return "length"_s; + case Type::LengthPercentage: return "length-percentage"_s; + case Type::Percentage: return "percentage"_s; + case Type::Integer: return "integer"_s; + case Type::Number: return "number"_s; + case Type::Angle: return "angle"_s; + case Type::Time: return "time"_s; + case Type::Resolution: return "resolution"_s; + case Type::Color: return "color"_s; + case Type::Image: return "image"_s; + case Type::URL: return "url"_s; + case Type::CustomIdent: return "custom-ident"_s; + case Type::String: return "string"_s; + case Type::TransformFunction: return "transform-function"_s; + case Type::TransformList: return "transform-list"_s; + case Type::Ident: + case Type::Unknown: + break; + } + return { }; +} + +void serializeCustomPropertySyntax(StringBuilder& builder, const CSSCustomPropertySyntax& syntax) +{ + if (syntax.isUniversal()) { + builder.append('*'); + return; + } + + auto separator = ""_s; + for (auto& component : syntax.definition) { + builder.append(separator); + separator = " | "_s; + + // The Ident type is a literal custom identifier rather than a named data type. + if (component.type == CSSCustomPropertySyntax::Type::Ident) + serializeIdentifier(builder, component.ident); + else + builder.append('<', typeNameForType(component.type), '>'); + + switch (component.multiplier) { + case CSSCustomPropertySyntax::Multiplier::Single: + break; + case CSSCustomPropertySyntax::Multiplier::SpaceList: + builder.append('+'); + break; + case CSSCustomPropertySyntax::Multiplier::CommaList: + builder.append('#'); + break; + } + } +} + +void serializeCustomPropertySyntaxAsCSSType(StringBuilder& builder, const CSSCustomPropertySyntax& syntax) +{ + ASSERT(!syntax.isUniversal()); + // A single syntax component is written bare. Anything else must be wrapped in type(). + if (syntax.definition.size() == 1) { + serializeCustomPropertySyntax(builder, syntax); + return; + } + builder.append("type("_s); + serializeCustomPropertySyntax(builder, syntax); + builder.append(')'); +} + } diff --git a/Source/WebCore/css/parser/CSSCustomPropertySyntax.h b/Source/WebCore/css/parser/CSSCustomPropertySyntax.h index 2c3e43f69a9f..f4af5605b3d5 100644 --- a/Source/WebCore/css/parser/CSSCustomPropertySyntax.h +++ b/Source/WebCore/css/parser/CSSCustomPropertySyntax.h @@ -27,6 +27,10 @@ #include #include +namespace WTF { +class StringBuilder; +} + namespace WebCore { class CSSParserTokenRange; @@ -73,7 +77,6 @@ struct CSSCustomPropertySyntax { static std::optional parse(StringView); static std::optional consumeType(CSSParserTokenRange&); - static CSSCustomPropertySyntax universal() { return { }; } bool NODELETE containsUnknownType() const; @@ -83,4 +86,11 @@ struct CSSCustomPropertySyntax { static Type typeForTypeName(StringView); }; +// Serializes to the string form, e.g. "*", "", "+", " | ". +void serializeCustomPropertySyntax(StringBuilder&, const CSSCustomPropertySyntax&); + +// Serializes to the form used in @function preludes: a single component is bare, while a +// multi-component syntax is wrapped in type(). Must not be called on the universal syntax. +void serializeCustomPropertySyntaxAsCSSType(StringBuilder&, const CSSCustomPropertySyntax&); + } From 019c43fd5de39b7d45fc2c9bfa821802535f9009 Mon Sep 17 00:00:00 2001 From: Basuke Suzuki Date: Tue, 30 Jun 2026 13:41:01 -0700 Subject: [PATCH 77/84] [SharedWorker] Add MESSAGE_CHECK to establishSharedWorkerContextConnection rdar://174708287 Reviewed by Per Arne Vollan (OOPS\!). establishSharedWorkerContextConnection accepts a WebContent-supplied Site with no MESSAGE_CHECK, allowing a compromised web process to hijack SharedWorker context connections for arbitrary domains. This mirrors the fix applied to the ServiceWorker equivalent (establishSWContextConnection) in rdar://107063897. Two changes: 1. Add allowsFirstPartyForCookies validation with MESSAGE_CHECK_COMPLETION before creating the context connection, matching the ServiceWorker pattern. 2. In WebSharedWorkerServer::addContextConnection, skip contextConnectionCreated when the domain already has a registered connection (replace debug-only ASSERT with runtime guard). No new tests. Covered by existing SharedWorker tests. * Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp: (WebKit::NetworkConnectionToWebProcess::establishSharedWorkerContextConnection): * Source/WebKit/NetworkProcess/SharedWorker/WebSharedWorkerServer.cpp: (WebKit::WebSharedWorkerServer::addContextConnection): Originally-landed-as: 305413.712@safari-7624-branch (701d9d99f21d). rdar://180428538 Canonical link: https://commits.webkit.org/316191@main --- .../NetworkProcess/NetworkConnectionToWebProcess.cpp | 9 ++++++--- .../SharedWorker/WebSharedWorkerServer.cpp | 6 +++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp index dbbc7345e7b7..e94410329af0 100644 --- a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp +++ b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp @@ -1599,9 +1599,12 @@ size_t NetworkConnectionToWebProcess::findNetworkActivityTracker(WebCore::Resour void NetworkConnectionToWebProcess::establishSharedWorkerContextConnection(WebPageProxyIdentifier, WebCore::Site&& site, WebCore::CrossOriginEmbedderPolicyValue crossOriginEmbedderPolicy, CompletionHandler&& completionHandler) { CONNECTION_RELEASE_LOG(SharedWorker, "establishSharedWorkerContextConnection:"); - CheckedPtr session = networkSession(); - if (CheckedPtr swServer = session ? session->sharedWorkerServer() : nullptr) - m_sharedWorkerContextConnection = WebSharedWorkerServerToContextConnection::create(*this, WTF::move(site), *swServer, crossOriginEmbedderPolicy); + if (CheckedPtr session = networkSession()) { + auto allowCookieAccess = session->networkProcess().allowsFirstPartyForCookies(webProcessIdentifier(), site.domain()); + MESSAGE_CHECK_COMPLETION(allowCookieAccess != NetworkProcess::AllowCookieAccess::Terminate, completionHandler()); + if (CheckedPtr swServer = session->sharedWorkerServer()) + m_sharedWorkerContextConnection = WebSharedWorkerServerToContextConnection::create(*this, WTF::move(site), *swServer, crossOriginEmbedderPolicy); + } completionHandler(); } diff --git a/Source/WebKit/NetworkProcess/SharedWorker/WebSharedWorkerServer.cpp b/Source/WebKit/NetworkProcess/SharedWorker/WebSharedWorkerServer.cpp index c3bbbdc0c738..cf085c237ffd 100644 --- a/Source/WebKit/NetworkProcess/SharedWorker/WebSharedWorkerServer.cpp +++ b/Source/WebKit/NetworkProcess/SharedWorker/WebSharedWorkerServer.cpp @@ -183,9 +183,9 @@ void WebSharedWorkerServer::addContextConnection(WebSharedWorkerServerToContextC RELEASE_LOG(SharedWorker, "WebSharedWorkerServer::addContextConnection(%p) webProcessIdentifier=%" PRIu64, &contextConnection, contextConnection.webProcessIdentifier() ? contextConnection.webProcessIdentifier()->toUInt64() : 0); ContextConnectionKey key { contextConnection.registrableDomain(), contextConnection.crossOriginEmbedderPolicyValue() }; - ASSERT(!m_contextConnections.contains(key)); - - m_contextConnections.add(key, contextConnection); + auto result = m_contextConnections.add(key, contextConnection); + if (!result.isNewEntry) + return; contextConnectionCreated(contextConnection); } From 95a70d5d80d3ce2c37ba7202d673b7a2574af819 Mon Sep 17 00:00:00 2001 From: Antti Koivisto Date: Tue, 30 Jun 2026 13:46:30 -0700 Subject: [PATCH 78/84] Custom property names in declaration blocks should be serialized escaped https://bugs.webkit.org/show_bug.cgi?id=318260 rdar://181055085 Reviewed by Tim Nguyen. * LayoutTests/imported/w3c/web-platform-tests/css/css-mixins/at-function-cssom-expected.txt: * LayoutTests/imported/w3c/web-platform-tests/css/cssom/variable-names-expected.txt: * Source/WebCore/css/StyleProperties.cpp: (WebCore::StyleProperties::asTextInternal const): Use serializeIdentifier. Canonical link: https://commits.webkit.org/316192@main --- .../css/css-mixins/at-function-cssom-expected.txt | 2 +- .../web-platform-tests/css/cssom/variable-names-expected.txt | 4 ++-- Source/WebCore/css/StyleProperties.cpp | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-mixins/at-function-cssom-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/css/css-mixins/at-function-cssom-expected.txt index e393fc1e8d64..0861559d1851 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/css/css-mixins/at-function-cssom-expected.txt +++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-mixins/at-function-cssom-expected.txt @@ -32,5 +32,5 @@ PASS CSSFunctionRule.cssText (--param-type-fn-uni) PASS CSSFunctionRule.cssText (--ret-type-fn) PASS CSSFunctionRule.cssText (--ret-type-fn-uni) PASS CSSFunctionRule.cssText (--body-result-multi) -FAIL CSSFunctionRule.cssText (--escaped-) assert_equals: expected "@function --escaped-\\9 -tab(--param-\\9 -tab) { --local-\\9 -tab: 1px; }" but got "@function --escaped-\\9 -tab(--param-\\9 -tab) { --local-\t-tab: 1px; }" +PASS CSSFunctionRule.cssText (--escaped-) diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/cssom/variable-names-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/css/cssom/variable-names-expected.txt index 205fff67ec8f..05ae5a07ca87 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/css/cssom/variable-names-expected.txt +++ b/LayoutTests/imported/w3c/web-platform-tests/css/cssom/variable-names-expected.txt @@ -1,8 +1,8 @@ PASS custom property '--a' -FAIL custom property '--a;b' assert_equals: appears on specified style (after serialization/re-parsing) expected 1 but got 0 +PASS custom property '--a;b' PASS custom property '---' -FAIL custom property '--\' assert_equals: appears on specified style (after serialization/re-parsing) expected 1 but got 0 +PASS custom property '--\' PASS custom property '--ab' PASS custom property '--0' diff --git a/Source/WebCore/css/StyleProperties.cpp b/Source/WebCore/css/StyleProperties.cpp index 2d1877a3e852..0ade9334464c 100644 --- a/Source/WebCore/css/StyleProperties.cpp +++ b/Source/WebCore/css/StyleProperties.cpp @@ -25,6 +25,7 @@ #include "CSSColorValue.h" #include "CSSCustomPropertyValue.h" +#include "CSSMarkup.h" #include "CSSPrimitiveValue.h" #include "CSSPropertyInitialValues.h" #include "CSSPropertyNames.h" @@ -305,7 +306,7 @@ StringBuilder StyleProperties::asTextInternal(const CSS::SerializationContext& c result.append(' '); if (propertyID == CSSPropertyCustom) - result.append(downcast(*property.value()).name()); + serializeIdentifier(result, downcast(*property.value()).name()); else result.append(nameLiteral(propertyID)); From c312747ab66d89c767c51172ca1eab0b9de5d288 Mon Sep 17 00:00:00 2001 From: Chris Dumez Date: Tue, 30 Jun 2026 13:50:07 -0700 Subject: [PATCH 79/84] [WebKit Networking] continueWillSendRequest m_redirectionForCurrentNavigation early-return reaches Cache::storeRedirect with unrestored cachePartition https://bugs.webkit.org/show_bug.cgi?id=314862 rdar://176914483 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed by Alex Christensen. A compromised WebContent process can poison the Network Process persistent disk cache for an arbitrary cross-origin partition. NetworkConnectionToWebProcess::useRedirectionForCurrentNavigation accepts a WebContent-supplied ResourceResponse with no validation and stashes it on the loader as m_redirectionForCurrentNavigation. When the loader's WillSendRequest async reply later delivers a WebContent-supplied ResourceRequest, NetworkResourceLoader::continueWillSendRequest takes the m_redirectionForCurrentNavigation early-return into willSendRedirectedRequest *before* the setCachePartition(originalRequest().cachePartition()) restore, so NetworkCache::Cache::storeRedirect keys the on-disk record by the unvalidated WebContent-chosen {cachePartition, url} and persists the WebContent-supplied 301 with WebContent-chosen Location:. Fix this by: 1. Hoisting the setCachePolicy/setCachePartition restore to the top of continueWillSendRequest so every path through the function — including the m_redirectionForCurrentNavigation, service-worker and m_shouldRestartLoad early returns — uses the original (Network-Process-side) cache partition, never the value round-tripped through the WebContent process. 2. Promoting the debug ASSERT(isMainFrameLoad()) and ASSERT(response.isRedirection()) at the NetworkConnectionToWebProcess::useRedirectionForCurrentNavigation IPC entry point to MESSAGE_CHECKs. The legitimate sender (WebPage::useRedirectionForCurrentNavigation, driven by RedirectSOAuthorizationSession in the UI process) only ever targets the main-frame main-resource loader with a redirection response; anything else is a compromised WebContent process. Test: ipc/use-redirection-for-current-navigation-message-check.html * LayoutTests/ipc/use-redirection-for-current-navigation-message-check-expected.txt: Added. * LayoutTests/ipc/use-redirection-for-current-navigation-message-check.html: Added. * Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp: (WebKit::NetworkConnectionToWebProcess::useRedirectionForCurrentNavigation): * Source/WebKit/NetworkProcess/NetworkResourceLoader.cpp: (WebKit::NetworkResourceLoader::continueWillSendRequest): Originally-landed-as: 305413.915@safari-7624-branch (a547e707161f). rdar://180438355 Canonical link: https://commits.webkit.org/316193@main --- ...rent-navigation-message-check-expected.txt | 2 + ...-for-current-navigation-message-check.html | 71 +++++++++++++++++++ .../NetworkConnectionToWebProcess.cpp | 8 ++- .../NetworkProcess/NetworkResourceLoader.cpp | 10 +-- 4 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 LayoutTests/ipc/use-redirection-for-current-navigation-message-check-expected.txt create mode 100644 LayoutTests/ipc/use-redirection-for-current-navigation-message-check.html diff --git a/LayoutTests/ipc/use-redirection-for-current-navigation-message-check-expected.txt b/LayoutTests/ipc/use-redirection-for-current-navigation-message-check-expected.txt new file mode 100644 index 000000000000..8155bcd41270 --- /dev/null +++ b/LayoutTests/ipc/use-redirection-for-current-navigation-message-check-expected.txt @@ -0,0 +1,2 @@ +PASS: UseRedirectionForCurrentNavigation with non-redirect response was rejected + diff --git a/LayoutTests/ipc/use-redirection-for-current-navigation-message-check.html b/LayoutTests/ipc/use-redirection-for-current-navigation-message-check.html new file mode 100644 index 000000000000..f22480e8d0fc --- /dev/null +++ b/LayoutTests/ipc/use-redirection-for-current-navigation-message-check.html @@ -0,0 +1,71 @@ + + +

+
+
diff --git a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp
index e94410329af0..df064b1aadf4 100644
--- a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp
+++ b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp
@@ -1853,8 +1853,12 @@ void NetworkConnectionToWebProcess::installMockContentFilter(WebCore::MockConten
 
 void NetworkConnectionToWebProcess::useRedirectionForCurrentNavigation(WebCore::ResourceLoaderIdentifier identifier, WebCore::ResourceResponse&& response)
 {
-    if (RefPtr loader = m_networkResourceLoaders.get(identifier))
-        loader->useRedirectionForCurrentNavigation(WTF::move(response));
+    MESSAGE_CHECK(response.isRedirection());
+    RefPtr loader = m_networkResourceLoaders.get(identifier);
+    if (!loader)
+        return;
+    MESSAGE_CHECK(loader->isMainFrameLoad());
+    loader->useRedirectionForCurrentNavigation(WTF::move(response));
 }
 
 #if ENABLE(DECLARATIVE_WEB_PUSH)
diff --git a/Source/WebKit/NetworkProcess/NetworkResourceLoader.cpp b/Source/WebKit/NetworkProcess/NetworkResourceLoader.cpp
index 9f8fca319ff4..fcfbb195411b 100644
--- a/Source/WebKit/NetworkProcess/NetworkResourceLoader.cpp
+++ b/Source/WebKit/NetworkProcess/NetworkResourceLoader.cpp
@@ -1539,6 +1539,12 @@ void NetworkResourceLoader::continueWillSendRequest(ResourceRequest&& newRequest
 {
     LOADER_RELEASE_LOG("continueWillSendRequest: (isAllowedToAskUserForCredentials=%d)", isAllowedToAskUserForCredentials);
 
+    // If there is a match in the network cache, we need to reuse the original cache policy and partition.
+    // This must happen before any branch below that may store a redirect in the disk cache, otherwise a
+    // compromised WebContent process could poison the cache for an arbitrary partition.
+    newRequest.setCachePolicy(originalRequest().cachePolicy());
+    newRequest.setShouldBlockThirdPartyStorage(originalRequest().shouldBlockThirdPartyStorage());
+
     if (m_redirectionForCurrentNavigation) {
         LOADER_RELEASE_LOG("continueWillSendRequest: using stored redirect response");
         auto redirection = std::exchange(m_redirectionForCurrentNavigation, { });
@@ -1600,10 +1606,6 @@ void NetworkResourceLoader::continueWillSendRequest(ResourceRequest&& newRequest
 
     m_isAllowedToAskUserForCredentials = isAllowedToAskUserForCredentials;
 
-    // If there is a match in the network cache, we need to reuse the original cache policy and partition.
-    newRequest.setCachePolicy(originalRequest().cachePolicy());
-    newRequest.setShouldBlockThirdPartyStorage(originalRequest().shouldBlockThirdPartyStorage());
-
     if (m_isWaitingContinueWillSendRequestForCachedRedirect) {
         m_isWaitingContinueWillSendRequestForCachedRedirect = false;
 

From cfced00e156e52c60a90495f76959ed2752a4167 Mon Sep 17 00:00:00 2001
From: Elliott Williams 
Date: Tue, 30 Jun 2026 13:53:51 -0700
Subject: [PATCH 80/84] [CI] Update measure-build-time swift patch
 https://bugs.webkit.org/show_bug.cgi?id=318239 rdar://181044227

Unreviewed CI fix. The webkit-swift-interface patch no longer applies
cleanly, blocking build perf testing.

* Tools/Scripts/measure-build-time:

Canonical link: https://commits.webkit.org/316194@main
---
 Tools/Scripts/measure-build-time | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/Tools/Scripts/measure-build-time b/Tools/Scripts/measure-build-time
index f6dcff645797..e26250e6eb1e 100755
--- a/Tools/Scripts/measure-build-time
+++ b/Tools/Scripts/measure-build-time
@@ -224,23 +224,23 @@ class IncrementalBuild6(PatchIncrementalBuild):
     description = 'Apply a small patch that changes a Swift-C++ class\'s public initializer and rebuild.'
     patch = '''\
 diff --git a/Source/WebKit/UIProcess/WebBackForwardList.cpp b/Source/WebKit/UIProcess/WebBackForwardList.cpp
-index 5fbcb767a0f5..b251202181fd 100644
+index 73419fbf4994..12e9c6ebda40 100644
 --- a/Source/WebKit/UIProcess/WebBackForwardList.cpp
 +++ b/Source/WebKit/UIProcess/WebBackForwardList.cpp
-@@ -968,7 +968,7 @@ void WebBackForwardList::didReceiveProvisionalMessage(IPC::Connection& connectio
+@@ -990,7 +990,7 @@ void WebBackForwardList::didReceiveProvisionalMessage(IPC::Connection& connectio
  #else // ENABLE(BACK_FORWARD_LIST_SWIFT)
  
  WebBackForwardListWrapper::WebBackForwardListWrapper(WebPageProxy& webPageProxy)
 -    : m_impl(WTF::makeUniqueWithoutFastMallocCheck(WebBackForwardList::init(webPageProxy)))
 +    : m_impl(WTF::makeUniqueWithoutFastMallocCheck(WebBackForwardList::init(webPageProxy, true)))
+     , m_messageForwarder(m_impl->getMessageReceiver())
  {
  }
- 
 diff --git a/Source/WebKit/UIProcess/WebBackForwardList.swift b/Source/WebKit/UIProcess/WebBackForwardList.swift
-index 3e1247fdf3d3..8b243cd79c1e 100644
+index 7556b716bef9..24e1cf8098e1 100644
 --- a/Source/WebKit/UIProcess/WebBackForwardList.swift
 +++ b/Source/WebKit/UIProcess/WebBackForwardList.swift
-@@ -137,7 +137,8 @@ final class WebBackForwardList {
+@@ -141,7 +141,8 @@ final class WebBackForwardList {
  
      // @used ensures these are retained even under -O -wmo: rdar://179098545
      @used

From 7a5ee54cd51a6c3aa25ca7f40f440d8ec7dc43cb Mon Sep 17 00:00:00 2001
From: Chris Dumez 
Date: Tue, 30 Jun 2026 14:00:25 -0700
Subject: [PATCH 81/84] UAF due to cross-thread destruction of worker
 DeferredPromise in WebLockManager::query()
 https://bugs.webkit.org/show_bug.cgi?id=312456 rdar://174652399

Reviewed by Ryosuke Niwa.

WebLockManager::MainThreadBridge::query() was taking in a CompletionHandler
but it may sometimes fail to call its completion handler. This happened
when the worker thread is exiting, causing `ScriptExecutionContext::ensureOnContextThread()`
to fail. Not calling the completion handler is bad but what's worse is that
the completion handler would end up getting destroyed on the main thread.
The completion handler was capturing a promise from the worker thread,
which led to security bugs.

To address the issue:
1. Have WebLockManager::MainThreadBridge::query() take in a Function
   instead of a CompletionHandler given that it cannot always call
   its callback.
2. Have WebLockManager store the promise in a HashMap and only capture
   a promise identifier in the MainThreadBridge::query() lambda instead
   of the promise itself. This pattern was already used for other
   promises in this class.

Test: workers/weblock-manager-query-crash.html

* LayoutTests/workers/weblock-manager-query-crash-expected.txt: Added.
* LayoutTests/workers/weblock-manager-query-crash.html: Added.
* Source/WebCore/Modules/web-locks/WebLockManager.cpp:
(WebCore::WebLockManager::MainThreadBridge::abortLockRequest):
(WebCore::WebLockManager::MainThreadBridge::query):
(WebCore::WebLockManager::query):
(WebCore::WebLockManager::clientIsGoingAway):
* Source/WebCore/Modules/web-locks/WebLockManager.h:

Originally-landed-as: 305413.688@safari-7624-branch (0b964ced2532). rdar://180436136
Canonical link: https://commits.webkit.org/316195@main
---
 .../weblock-manager-query-crash-expected.txt  |  1 +
 .../workers/weblock-manager-query-crash.html  | 42 ++++++++++++++++
 .../Modules/web-locks/WebLockManager.cpp      | 48 +++++++++++--------
 .../Modules/web-locks/WebLockManager.h        |  2 +
 4 files changed, 72 insertions(+), 21 deletions(-)
 create mode 100644 LayoutTests/workers/weblock-manager-query-crash-expected.txt
 create mode 100644 LayoutTests/workers/weblock-manager-query-crash.html

diff --git a/LayoutTests/workers/weblock-manager-query-crash-expected.txt b/LayoutTests/workers/weblock-manager-query-crash-expected.txt
new file mode 100644
index 000000000000..730ebf66a0da
--- /dev/null
+++ b/LayoutTests/workers/weblock-manager-query-crash-expected.txt
@@ -0,0 +1 @@
+This test passes if it doesn't crash.
diff --git a/LayoutTests/workers/weblock-manager-query-crash.html b/LayoutTests/workers/weblock-manager-query-crash.html
new file mode 100644
index 000000000000..de3aa909f638
--- /dev/null
+++ b/LayoutTests/workers/weblock-manager-query-crash.html
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+This test passes if it doesn't crash.
+
+
diff --git a/Source/WebCore/Modules/web-locks/WebLockManager.cpp b/Source/WebCore/Modules/web-locks/WebLockManager.cpp
index 32bbd739d03f..69691abf3fa7 100644
--- a/Source/WebCore/Modules/web-locks/WebLockManager.cpp
+++ b/Source/WebCore/Modules/web-locks/WebLockManager.cpp
@@ -96,8 +96,8 @@ class WebLockManager::MainThreadBridge : public ThreadSafeRefCounted&&, Function&& lockStolenHandler);
     void releaseLock(WebLockIdentifier, const String& name);
-    void abortLockRequest(WebLockIdentifier, const String& name, CompletionHandler&&);
-    void query(CompletionHandler&&);
+    void abortLockRequest(WebLockIdentifier, const String& name, Function&&);
+    void query(Function&&);
     void clientIsGoingAway();
 
 private:
@@ -137,23 +137,23 @@ void WebLockManager::MainThreadBridge::releaseLock(WebLockIdentifier lockIdentif
     });
 }
 
-void WebLockManager::MainThreadBridge::abortLockRequest(WebLockIdentifier lockIdentifier, const String& name, CompletionHandler&& completionHandler)
+void WebLockManager::MainThreadBridge::abortLockRequest(WebLockIdentifier lockIdentifier, const String& name, Function&& callback)
 {
-    callOnMainThread([this, protectedThis = Ref { *this }, lockIdentifier, name = crossThreadCopy(name), completionHandler = WTF::move(completionHandler)]() mutable {
-        WebLockRegistry::singleton().abortLockRequest(m_sessionID, m_clientOrigin, lockIdentifier, m_clientID, name, [clientID = m_clientID, completionHandler = WTF::move(completionHandler)](bool wasAborted) mutable {
-            ScriptExecutionContext::ensureOnContextThread(clientID, [completionHandler = WTF::move(completionHandler), wasAborted](auto&) mutable {
-                completionHandler(wasAborted);
+    callOnMainThread([this, protectedThis = Ref { *this }, lockIdentifier, name = crossThreadCopy(name), callback = WTF::move(callback)]() mutable {
+        WebLockRegistry::singleton().abortLockRequest(m_sessionID, m_clientOrigin, lockIdentifier, m_clientID, name, [clientID = m_clientID, callback = WTF::move(callback)](bool wasAborted) mutable {
+            ScriptExecutionContext::ensureOnContextThread(clientID, [callback = WTF::move(callback), wasAborted](auto&) mutable {
+                callback(wasAborted);
             });
         });
     });
 }
 
-void WebLockManager::MainThreadBridge::query(CompletionHandler&& completionHandler)
+void WebLockManager::MainThreadBridge::query(Function&& callback)
 {
-    callOnMainThread([this, protectedThis = Ref { *this }, completionHandler = WTF::move(completionHandler)]() mutable {
-        WebLockRegistry::singleton().snapshot(m_sessionID, m_clientOrigin, [clientID = m_clientID, completionHandler = WTF::move(completionHandler)](Snapshot&& snapshot) mutable {
-            ScriptExecutionContext::ensureOnContextThread(clientID, [completionHandler = WTF::move(completionHandler), snapshot = crossThreadCopy(snapshot)](auto&) mutable {
-                completionHandler(WTF::move(snapshot));
+    callOnMainThread([this, protectedThis = Ref { *this }, callback = WTF::move(callback)]() mutable {
+        WebLockRegistry::singleton().snapshot(m_sessionID, m_clientOrigin, [clientID = m_clientID, callback = WTF::move(callback)](Snapshot&& snapshot) mutable {
+            ScriptExecutionContext::ensureOnContextThread(clientID, [callback = WTF::move(callback), snapshot = crossThreadCopy(snapshot)](auto&) mutable {
+                callback(WTF::move(snapshot));
             });
         });
     });
@@ -322,11 +322,17 @@ void WebLockManager::query(Ref&& promise)
         return;
     }
 
-    m_mainThreadBridge->query([weakThis = WeakPtr { *this }, promise = WTF::move(promise)](Snapshot&& snapshot) mutable {
+    auto promiseIdentifier = WebLockIdentifier::generate();
+    m_queryPromises.add(promiseIdentifier, WTF::move(promise));
+    m_mainThreadBridge->query([weakThis = WeakPtr { *this }, promiseIdentifier](Snapshot&& snapshot) mutable {
         RefPtr protectedThis = weakThis.get();
         if (!protectedThis)
             return;
 
+        auto promise = protectedThis->m_queryPromises.take(promiseIdentifier);
+        if (!promise)
+            return;
+
         queueTaskKeepingObjectAlive(*protectedThis, TaskSource::DOMManipulation, [promise = WTF::move(promise), snapshot = WTF::move(snapshot)](auto&) mutable {
             promise->resolve>(WTF::move(snapshot));
         });
@@ -373,16 +379,16 @@ void WebLockManager::stop()
 
 void WebLockManager::clientIsGoingAway()
 {
-    if (m_pendingRequests.isEmpty() && m_releasePromises.isEmpty())
-        return;
-
     for (auto& request : m_pendingRequests.values())
         request.removeSignalAlgorithm();
 
-    // Reject all pending promises before clearing
-    for (Ref promise : m_releasePromises.values())
-        promise->reject(ExceptionCode::AbortError, "Promise was rejected because the browsing context is going away"_s);
-
+    // Reject all pending promises before clearing.
+    auto releasePromises = std::exchange(m_releasePromises, { });
+    for (auto& promise : releasePromises.values())
+        protect(promise)->reject(ExceptionCode::AbortError, "Promise was rejected because the browsing context is going away"_s);
+    auto queryPromises = std::exchange(m_queryPromises, { });
+    for (auto& promise : queryPromises.values())
+        protect(promise)->reject(ExceptionCode::AbortError, "Promise was rejected because the browsing context is going away"_s);
     m_pendingRequests.clear();
     m_releasePromises.clear();
 
@@ -392,7 +398,7 @@ void WebLockManager::clientIsGoingAway()
 
 bool WebLockManager::virtualHasPendingActivity() const
 {
-    return !m_pendingRequests.isEmpty() || !m_releasePromises.isEmpty();
+    return !m_pendingRequests.isEmpty() || !m_releasePromises.isEmpty() || !m_queryPromises.isEmpty();
 }
 
 void WebLockManager::suspend(ReasonForSuspension reason)
diff --git a/Source/WebCore/Modules/web-locks/WebLockManager.h b/Source/WebCore/Modules/web-locks/WebLockManager.h
index 22a8294a448b..14d5fd99bb1a 100644
--- a/Source/WebCore/Modules/web-locks/WebLockManager.h
+++ b/Source/WebCore/Modules/web-locks/WebLockManager.h
@@ -83,6 +83,8 @@ class WebLockManager : public RefCounted, public ActiveDOMObject
 
     struct LockRequest;
     HashMap m_pendingRequests;
+
+    HashMap> m_queryPromises;
 };
 
 } // namespace WebCore

From 428365d538396bd73d446fa27beb62b3768b83ca Mon Sep 17 00:00:00 2001
From: Ahmad Saleem 
Date: Tue, 30 Jun 2026 14:09:42 -0700
Subject: [PATCH 82/84] RenderFlexibleBox::LineState constructor copies
 FlexLayoutItems instead of moving it
 https://bugs.webkit.org/show_bug.cgi?id=318189 rdar://180998972

Reviewed by Alan Baradlay.

LineState's constructor takes `FlexLayoutItems&& flexLayoutItems` (an
rvalue reference), signaling intent to move. However, inside the member
initializer list a named rvalue-reference parameter is an lvalue, so
`flexLayoutItems(flexLayoutItems)` selected the Vector copy constructor
and deep-copied the whole Vector
on every flex line.

The caller already passes WTF::move(lineItems), so the intended behavior
was always a move. Wrap the parameter in WTF::move() to elide the
per-line copy.

* Source/WebCore/rendering/RenderFlexibleBox.cpp:
(WebCore::RenderFlexibleBox::LineState::LineState):

Canonical link: https://commits.webkit.org/316196@main
---
 Source/WebCore/rendering/RenderFlexibleBox.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Source/WebCore/rendering/RenderFlexibleBox.cpp b/Source/WebCore/rendering/RenderFlexibleBox.cpp
index 219ae8a20407..6e603f3d7913 100644
--- a/Source/WebCore/rendering/RenderFlexibleBox.cpp
+++ b/Source/WebCore/rendering/RenderFlexibleBox.cpp
@@ -115,7 +115,7 @@ struct RenderFlexibleBox::LineState {
         : crossAxisOffset(crossAxisOffset)
         , crossAxisExtent(crossAxisExtent)
         , baselineAlignmentState(baselineAlignmentState)
-        , flexLayoutItems(flexLayoutItems)
+        , flexLayoutItems(WTF::move(flexLayoutItems))
     {
     }
     

From 600eab67eab029ce922b74f6cd1769fe655d89e7 Mon Sep 17 00:00:00 2001
From: Nikolas Zimmermann 
Date: Tue, 30 Jun 2026 14:18:05 -0700
Subject: [PATCH 83/84] [LBSE] Add LBSE specific result for
 svg/repaint/svg-outline-repaint-on-hover.html after 315468@main
 https://bugs.webkit.org/show_bug.cgi?id=318268

Unreviewed.

315468@main added a new test, which needs a LBSE specific baseline (different repaint rect order / batching).

* LayoutTests/platform/mac-tahoe-wk2-lbse-text/svg/repaint/svg-outline-repaint-on-hover-expected.txt: Added.

Canonical link: https://commits.webkit.org/316197@main
---
 .../svg-outline-repaint-on-hover-expected.txt   | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)
 create mode 100644 LayoutTests/platform/mac-tahoe-wk2-lbse-text/svg/repaint/svg-outline-repaint-on-hover-expected.txt

diff --git a/LayoutTests/platform/mac-tahoe-wk2-lbse-text/svg/repaint/svg-outline-repaint-on-hover-expected.txt b/LayoutTests/platform/mac-tahoe-wk2-lbse-text/svg/repaint/svg-outline-repaint-on-hover-expected.txt
new file mode 100644
index 000000000000..9826a460f4b0
--- /dev/null
+++ b/LayoutTests/platform/mac-tahoe-wk2-lbse-text/svg/repaint/svg-outline-repaint-on-hover-expected.txt
@@ -0,0 +1,17 @@
+ SVG border box: 100x100 at (50,50)
+
+Repaint rects on hover (outline added):
+(repaint rects
+  (rect 50 50 100 100)
+  (rect 0 0 800 204)
+  (rect 35 35 130 130)
+)
+
+Repaint rects on un-hover (outline removed):
+(repaint rects
+  (rect 35 35 130 130)
+  (rect 50 50 100 100)
+  (rect 0 0 800 204)
+  (rect 35 35 130 130)
+)
+

From d81bcc3d833cfce4e4b892e4c3758454c01498b6 Mon Sep 17 00:00:00 2001
From: Alex Christensen 
Date: Tue, 30 Jun 2026 14:23:47 -0700
Subject: [PATCH 84/84] Prepare for removal of _WKJSHandle
 https://bugs.webkit.org/show_bug.cgi?id=318264 rdar://181058988

Reviewed by Wenson Hsieh.

In order to help some clients move off _WKJSHandle on to WKJSHandle,
we need to provide equivalent and similarly named interfaces.

* Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm:
(-[WKWebView _getSelectorPathData:completionHandler:]):
* Source/WebKit/UIProcess/API/Cocoa/WKWebViewPrivate.h:
* Source/WebKit/UIProcess/API/Cocoa/_WKTextExtraction.h:
* Source/WebKit/UIProcess/API/Cocoa/_WKTextExtraction.mm:
(-[_WKTextExtractionConfiguration targetNodeHandle]):
(-[_WKTextExtractionConfiguration setTargetNodeHandle:]):
(-[_WKTextExtractionResult requestHandleForNodeIdentifier:searchText:completionHandler:]):
(-[_WKTextExtractionResult requestContainerHandleForNodeIdentifier:searchText:completionHandler:]):
(-[_WKTextExtractionResult requestContainerHandleForSearchTexts:nodeIdentifier:completionHandler:]):

Canonical link: https://commits.webkit.org/316198@main
---
 .../WebKit/UIProcess/API/Cocoa/WKWebView.mm   |  5 +++
 .../UIProcess/API/Cocoa/WKWebViewPrivate.h    |  1 +
 .../UIProcess/API/Cocoa/_WKTextExtraction.h   |  7 ++++-
 .../UIProcess/API/Cocoa/_WKTextExtraction.mm  | 31 +++++++++++++++++++
 4 files changed, 43 insertions(+), 1 deletion(-)

diff --git a/Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm b/Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm
index a8e0213b3ed2..93b8a32fed18 100644
--- a/Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm
+++ b/Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm
@@ -7709,6 +7709,11 @@ - (void)_getSelectorPathDataForNode:(_WKJSHandle *)node completionHandler:(void
     });
 }
 
+- (void)_getSelectorPathData:(WKJSHandle *)node completionHandler:(void (^)(NSData *))completionHandler
+{
+    [self _getSelectorPathDataForNode:(_WKJSHandle *)node completionHandler:completionHandler];
+}
+
 - (void)_getNodeForSelectorPathData:(NSData *)data completionHandler:(void (^)(_WKJSHandle *))completion
 {
     RefPtr frame = _page->mainFrame();
diff --git a/Source/WebKit/UIProcess/API/Cocoa/WKWebViewPrivate.h b/Source/WebKit/UIProcess/API/Cocoa/WKWebViewPrivate.h
index d11a871c89c7..c39525a76b3a 100644
--- a/Source/WebKit/UIProcess/API/Cocoa/WKWebViewPrivate.h
+++ b/Source/WebKit/UIProcess/API/Cocoa/WKWebViewPrivate.h
@@ -660,6 +660,7 @@ typedef NS_OPTIONS(NSUInteger, _WKWebViewDataType) {
 #endif
 
 - (void)_getSelectorPathDataForNode:(_WKJSHandle *)node completionHandler:(WK_SWIFT_UI_ACTOR void (^)(NSData *))completionHandler WK_SWIFT_ASYNC_NAME(_getSelectorPathDataForNode(_:)) WK_API_AVAILABLE(macos(26.4), ios(26.4), visionos(26.4));
+- (void)_getSelectorPathData:(WKJSHandle *)node completionHandler:(WK_SWIFT_UI_ACTOR void (^)(NSData *))completionHandler WK_SWIFT_ASYNC_NAME(_getSelectorPathData(_:)) WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA), visionos(WK_XROS_TBA));
 - (void)_getNodeForSelectorPathData:(NSData *)data completionHandler:(WK_SWIFT_UI_ACTOR void (^)(_WKJSHandle *))completionHandler WK_SWIFT_ASYNC_NAME(_getNodeForSelectorPathData(_:)) WK_API_AVAILABLE(macos(26.4), ios(26.4), visionos(26.4));
 
 - (void)_debugTextWithConfiguration:(_WKTextExtractionConfiguration *)configuration completionHandler:(WK_SWIFT_UI_ACTOR void(^)(NSString *))completionHandler WK_API_AVAILABLE(macos(26.4), ios(26.4), visionos(26.4)) NS_SWIFT_NAME(_debugText(with:completionHandler:));
diff --git a/Source/WebKit/UIProcess/API/Cocoa/_WKTextExtraction.h b/Source/WebKit/UIProcess/API/Cocoa/_WKTextExtraction.h
index af5b807d0680..1ad868d35d36 100644
--- a/Source/WebKit/UIProcess/API/Cocoa/_WKTextExtraction.h
+++ b/Source/WebKit/UIProcess/API/Cocoa/_WKTextExtraction.h
@@ -30,6 +30,7 @@
 NS_HEADER_AUDIT_BEGIN(nullability, sendability)
 
 @class WKFrameInfo;
+@class WKJSHandle;
 @class WKSecurityOrigin;
 @class WKWebView;
 @class _WKJSHandle;
@@ -93,7 +94,7 @@ WK_CLASS_AVAILABLE(macos(26.4), ios(26.4), visionos(26.4))
 /*!
  Disables all optional metadata in the extraction output: URLs, bounding rects,
  node identifiers, event listeners, and accessibility attributes.
- The output format and other structural configuration (e.g. `targetRect`, `targetNode`)
+ The output format and other structural configuration (e.g. `targetRect`, `targetNodeHandle`)
  are left unchanged. Individual flags can still be re-enabled after calling this method.
  */
 - (void)configureForMinimalOutput;
@@ -190,6 +191,7 @@ WK_CLASS_AVAILABLE(macos(26.4), ios(26.4), visionos(26.4))
  The default value is `nil`.
  */
 @property (nonatomic, copy, nullable) _WKJSHandle *targetNode;
+@property (nonatomic, copy, nullable) WKJSHandle *targetNodeHandle WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA), visionos(WK_XROS_TBA));
 
 /*!
  If specified, these DOM nodes and their subtrees will be skipped during extraction.
@@ -270,6 +272,7 @@ WK_CLASS_AVAILABLE(macos(26.4), ios(26.4), visionos(26.4))
  At least one of `nodeIdentifier` or `searchText` must be specified.
  */
 - (void)requestJSHandleForNodeIdentifier:(nullable NSString *)nodeIdentifier searchText:(nullable NSString *)searchText completionHandler:(void (^)(_WKJSHandle * _Nullable))completionHandler;
+- (void)requestHandleForNodeIdentifier:(nullable NSString *)nodeIdentifier searchText:(nullable NSString *)searchText completionHandler:(void (^)(WKJSHandle * _Nullable))completionHandler WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA), visionos(WK_XROS_TBA));
 
 /*!
  Asynchronously map a node identifier string (corresponding to a `uid` in
@@ -282,6 +285,7 @@ WK_CLASS_AVAILABLE(macos(26.4), ios(26.4), visionos(26.4))
  At least one of `nodeIdentifier` or `searchText` must be specified.
  */
 - (void)requestContainerJSHandleForNodeIdentifier:(nullable NSString *)nodeIdentifier searchText:(nullable NSString *)searchText completionHandler:(void (^)(_WKJSHandle * _Nullable))completionHandler;
+- (void)requestContainerHandleForNodeIdentifier:(nullable NSString *)nodeIdentifier searchText:(nullable NSString *)searchText completionHandler:(void (^)(WKJSHandle * _Nullable))completionHandler WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA), visionos(WK_XROS_TBA));
 
 /*!
  Asynchronously find the smallest appropriately-sized container element that
@@ -294,6 +298,7 @@ WK_CLASS_AVAILABLE(macos(26.4), ios(26.4), visionos(26.4))
  At least one search text or a non-null node identifier must be specified.
  */
 - (void)requestContainerJSHandleForSearchTexts:(NSArray *)searchTexts nodeIdentifier:(nullable NSString *)nodeIdentifier completionHandler:(void (^)(_WKJSHandle * _Nullable))completionHandler;
+- (void)requestContainerHandleForSearchTexts:(NSArray *)searchTexts nodeIdentifier:(nullable NSString *)nodeIdentifier completionHandler:(void (^)(WKJSHandle * _Nullable))completionHandler WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA), visionos(WK_XROS_TBA));
 
 @end
 
diff --git a/Source/WebKit/UIProcess/API/Cocoa/_WKTextExtraction.mm b/Source/WebKit/UIProcess/API/Cocoa/_WKTextExtraction.mm
index 594105662089..577febe000ff 100644
--- a/Source/WebKit/UIProcess/API/Cocoa/_WKTextExtraction.mm
+++ b/Source/WebKit/UIProcess/API/Cocoa/_WKTextExtraction.mm
@@ -94,6 +94,16 @@ - (void)setTargetNode:(_WKJSHandle *)targetNode
     _targetNode = adoptNS([targetNode copy]);
 }
 
+- (WKJSHandle *)targetNodeHandle
+{
+    return _targetNode.get();
+}
+
+- (void)setTargetNodeHandle:(WKJSHandle *)targetNode
+{
+    _targetNode = adoptNS([targetNode copy]);
+}
+
 - (NSArray<_WKJSHandle *> *)nodesToSkip
 {
     return _nodesToSkip.get();
@@ -229,6 +239,13 @@ - (void)requestJSHandleForNodeIdentifier:(NSString *)nodeIdentifier searchText:(
     [webView _requestJSHandleForNodeIdentifier:nodeIdentifier searchText:searchText completionHandler:completionHandler];
 }
 
+- (void)requestHandleForNodeIdentifier:(nullable NSString *)nodeIdentifier searchText:(nullable NSString *)searchText completionHandler:(void (^)(WKJSHandle * _Nullable))completionHandler
+{
+    [self requestJSHandleForNodeIdentifier:nodeIdentifier searchText:searchText completionHandler:[completionHandler = makeBlockPtr(completionHandler)] (_WKJSHandle *handle) {
+        completionHandler(handle);
+    }];
+}
+
 - (void)requestContainerJSHandleForNodeIdentifier:(NSString *)nodeIdentifier searchText:(NSString *)searchText completionHandler:(void (^)(_WKJSHandle *))completionHandler
 {
     RetainPtr webView = _webView;
@@ -238,6 +255,13 @@ - (void)requestContainerJSHandleForNodeIdentifier:(NSString *)nodeIdentifier sea
     [webView _requestContainerJSHandleForNodeIdentifier:nodeIdentifier searchText:searchText completionHandler:completionHandler];
 }
 
+- (void)requestContainerHandleForNodeIdentifier:(NSString *)nodeIdentifier searchText:(NSString *)searchText completionHandler:(void (^)(WKJSHandle *))completionHandler
+{
+    [self requestContainerJSHandleForNodeIdentifier:nodeIdentifier searchText:searchText completionHandler:[completionHandler = makeBlockPtr(completionHandler)] (_WKJSHandle *handle) {
+        completionHandler(handle);
+    }];
+}
+
 - (void)requestContainerJSHandleForSearchTexts:(NSArray *)searchTexts nodeIdentifier:(NSString *)nodeIdentifier completionHandler:(void (^)(_WKJSHandle *))completionHandler
 {
     RetainPtr webView = _webView;
@@ -247,6 +271,13 @@ - (void)requestContainerJSHandleForSearchTexts:(NSArray *)searchText
     [webView _requestContainerJSHandleForSearchTexts:searchTexts nodeIdentifier:nodeIdentifier completionHandler:completionHandler];
 }
 
+- (void)requestContainerHandleForSearchTexts:(NSArray *)searchTexts nodeIdentifier:(nullable NSString *)nodeIdentifier completionHandler:(void (^)(WKJSHandle * _Nullable))completionHandler
+{
+    [self requestContainerJSHandleForSearchTexts:searchTexts nodeIdentifier:nodeIdentifier completionHandler:[completionHandler = makeBlockPtr(completionHandler)] (_WKJSHandle *handle) {
+        completionHandler(handle);
+    }];
+}
+
 @end
 
 @implementation _WKTextExtractionInteraction {